001/* 002 * This file is part of McIDAS-V 003 * 004 * Copyright 2007-2017 005 * Space Science and Engineering Center (SSEC) 006 * University of Wisconsin - Madison 007 * 1225 W. Dayton Street, Madison, WI 53706, USA 008 * https://www.ssec.wisc.edu/mcidas 009 * 010 * All Rights Reserved 011 * 012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and 013 * some McIDAS-V source code is based on IDV and VisAD source code. 014 * 015 * McIDAS-V is free software; you can redistribute it and/or modify 016 * it under the terms of the GNU Lesser Public License as published by 017 * the Free Software Foundation; either version 3 of the License, or 018 * (at your option) any later version. 019 * 020 * McIDAS-V is distributed in the hope that it will be useful, 021 * but WITHOUT ANY WARRANTY; without even the implied warranty of 022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 023 * GNU Lesser Public License for more details. 024 * 025 * You should have received a copy of the GNU Lesser Public License 026 * along with this program. If not, see http://www.gnu.org/licenses. 027 */ 028 029package edu.wisc.ssec.mcidasv.ui; 030 031import java.awt.Color; 032import java.awt.Component; 033import java.awt.Cursor; 034import java.awt.FontMetrics; 035import java.awt.Graphics; 036import java.awt.Image; 037import java.awt.Insets; 038import java.awt.Point; 039import java.awt.Rectangle; 040import java.awt.datatransfer.DataFlavor; 041import java.awt.datatransfer.Transferable; 042import java.awt.dnd.DnDConstants; 043import java.awt.dnd.DragGestureEvent; 044import java.awt.dnd.DragGestureListener; 045import java.awt.dnd.DragSource; 046import java.awt.dnd.DragSourceDragEvent; 047import java.awt.dnd.DragSourceDropEvent; 048import java.awt.dnd.DragSourceEvent; 049import java.awt.dnd.DragSourceListener; 050import java.awt.dnd.DropTarget; 051import java.awt.dnd.DropTargetDragEvent; 052import java.awt.dnd.DropTargetDropEvent; 053import java.awt.dnd.DropTargetEvent; 054import java.awt.dnd.DropTargetListener; 055import java.awt.event.InputEvent; 056import java.awt.event.MouseEvent; 057import java.awt.event.MouseListener; 058import java.awt.event.MouseMotionListener; 059 060import javax.swing.Icon; 061import javax.swing.ImageIcon; 062import javax.swing.JTabbedPane; 063import javax.swing.SwingConstants; 064import javax.swing.SwingUtilities; 065import javax.swing.plaf.basic.BasicTabbedPaneUI; 066import javax.swing.plaf.metal.MetalTabbedPaneUI; 067import javax.swing.text.View; 068 069import java.util.EnumMap; 070import java.util.List; 071 072import org.w3c.dom.Element; 073 074import org.slf4j.Logger; 075import org.slf4j.LoggerFactory; 076 077import ucar.unidata.idv.IntegratedDataViewer; 078import ucar.unidata.idv.ViewManager; 079import ucar.unidata.idv.ui.IdvWindow; 080import ucar.unidata.ui.ComponentGroup; 081import ucar.unidata.ui.ComponentHolder; 082import ucar.unidata.util.GuiUtils; 083import ucar.unidata.xml.XmlUtil; 084 085import edu.wisc.ssec.mcidasv.Constants; 086 087/** 088 * This is a rather simplistic drag and drop enabled JTabbedPane. It allows 089 * users to use drag and drop to move tabs between windows and reorder tabs. 090 */ 091public class DraggableTabbedPane extends JTabbedPane implements 092 DragGestureListener, DragSourceListener, DropTargetListener, MouseListener, 093 MouseMotionListener 094{ 095 private static final long serialVersionUID = -5710302260509445686L; 096 097 private static final Logger logger = 098 LoggerFactory.getLogger(DraggableTabbedPane.class); 099 100 /** Local shorthand for the actions we're accepting. */ 101 private static final int VALID_ACTION = DnDConstants.ACTION_COPY_OR_MOVE; 102 103 /** Path to the icon we'll use as an index indicator. */ 104 private static final String IDX_ICON = 105 "/edu/wisc/ssec/mcidasv/resources/icons/tabmenu/go-down.png"; 106 107 private static final Color unselected = new Color(165, 165, 165); 108 private static final Color selected = new Color(225, 225, 225); 109 110 private static final String INDEX_COLOR_METAL = "#AAAAAA"; 111 112 private static final String INDEX_COLOR_UGLY_TABS = "#708090"; 113 114 /** The actual image that we'll use to display the index indications. */ 115 private final Image INDICATOR = 116 new ImageIcon(getClass().getResource(IDX_ICON)).getImage(); 117 118 public enum ButtonState { DEFAULT, PRESSED, DISABLED, ROLLOVER }; 119 120 /** Path to icon that represents the default button state. */ 121 private static final String ICON_DEFAULT = 122 "/edu/wisc/ssec/mcidasv/resources/icons/closetab/metal_close_enabled.png"; 123 124 /** Path to icon that represents the pressed button state. */ 125 private static final String ICON_PRESSED = 126 "/edu/wisc/ssec/mcidasv/resources/icons/closetab/metal_close_pressed.png"; 127 128 /** Path to icon that represents the rollover button state. */ 129 private static final String ICON_ROLLOVER = 130 "/edu/wisc/ssec/mcidasv/resources/icons/closetab/metal_close_rollover.png"; 131 132 /** 133 * Used to signal across all DraggableTabbedPanes that the component 134 * currently being dragged originated in another window. This'll let McV 135 * determine if it has to do a quiet ComponentHolder transfer. 136 */ 137 protected static boolean outsideDrag = false; 138 139 /** The tab index where the drag started. */ 140 private int sourceIndex = -1; 141 142 /** The tab index that the user is currently over. */ 143 private int overIndex = -1; 144 145 private int draggedAtX; 146 147 private int draggedAtY; 148 149 /** Used for starting the dragging process. */ 150 private DragSource dragSource; 151 152 /** Used for signaling that we'll accept drops (registers listeners). */ 153 private DropTarget dropTarget; 154 155 /** The component group holding our components. */ 156 private McvComponentGroup group; 157 158 /** The IDV window that contains this tabbed pane. */ 159 private IdvWindow window; 160 161 /** Keep around this reference so that we can access the UI Manager. */ 162 private IntegratedDataViewer idv; 163 164 /** RGB string for the color of the current tab. */ 165 private String currentTabColor = INDEX_COLOR_METAL; 166 167 /** 168 * Mostly just registers that this component should listen for drag and 169 * drop operations. 170 * 171 * @param win The IDV window containing this tabbed pane. 172 * @param idv The main IDV instance. 173 * @param group The {@link McvComponentGroup} that holds this component's tabs. 174 */ 175 public DraggableTabbedPane(IdvWindow win, IntegratedDataViewer idv, 176 McvComponentGroup group) 177 { 178 dropTarget = new DropTarget(this, this); 179 dragSource = new DragSource(); 180 dragSource.createDefaultDragGestureRecognizer(this, VALID_ACTION, this); 181 182 this.group = group; 183 this.idv = idv; 184 window = win; 185 186 addMouseListener(this); 187 addMouseMotionListener(this); 188 189 if (getUI() instanceof MetalTabbedPaneUI) { 190 setUI(new CloseableMetalTabbedPaneUI(SwingConstants.LEFT)); 191 currentTabColor = INDEX_COLOR_METAL; 192 } else { 193 setUI(new CloseableTabbedPaneUI(SwingConstants.LEFT)); 194 currentTabColor = INDEX_COLOR_UGLY_TABS; 195 } 196 } 197 198 /** 199 * Triggered when the user does a (platform-dependent) drag initiating 200 * gesture. Used to populate the things that the user is attempting to 201 * drag. 202 */ 203 @Override public void dragGestureRecognized(DragGestureEvent e) { 204 // currently we want to disable drag and drop for "chrome-less" windows 205 // one alternative is to have drag and drop simply *reposition* 206 // chrome-less windows. 207 if (showTabArea(group, this)) { 208 sourceIndex = getSelectedIndex(); 209 210 // transferable allows us to store the current DraggableTabbedPane 211 // and the source index of the drag inside the various drag and 212 // drop event listeners. 213 Transferable transferable = new TransferableIndex(this, sourceIndex); 214 215 Cursor cursor = DragSource.DefaultMoveDrop; 216 if (e.getDragAction() != DnDConstants.ACTION_MOVE) { 217 cursor = DragSource.DefaultCopyDrop; 218 } 219 dragSource.startDrag(e, cursor, transferable, this); 220 } 221 } 222 223 /** 224 * Triggered when the user drags into {@code dropTarget}. 225 */ 226 @Override public void dragEnter(DropTargetDragEvent e) { 227 DataFlavor[] flave = e.getCurrentDataFlavors(); 228 if ((flave.length == 0) || !(flave[0] instanceof DraggableTabFlavor)) { 229 return; 230 } 231 232// logger.trace("entered window outsideDrag={} sourceIndex={}", outsideDrag, sourceIndex); 233 234 // if the DraggableTabbedPane associated with this drag isn't the 235 // "current" DraggableTabbedPane we're dealing with a drag from another 236 // window and we need to make this DraggableTabbedPane aware of that. 237 if (((DraggableTabFlavor)flave[0]).getDragTab() != this) { 238// logger.trace(" coming from outside"); 239 outsideDrag = true; 240 } else { 241// logger.trace(" re-entered parent window"); 242 outsideDrag = false; 243 } 244 } 245 246 /** 247 * Triggered when the user drags out of {@code dropTarget}. 248 */ 249 @Override public void dragExit(DropTargetEvent e) { 250 if (showTabArea(group, this)) { 251// logger.trace("drag left a window outsideDrag={} sourceIndex={}", outsideDrag, sourceIndex); 252 overIndex = -1; 253 //outsideDrag = true; 254 repaint(); 255 } 256 } 257 258 /** 259 * Triggered continually while the user is dragging over 260 * {@code dropTarget}. McIDAS-V uses this to draw the index indicator. 261 * 262 * @param e Information about the current state of the drag. 263 */ 264 @Override public void dragOver(DropTargetDragEvent e) { 265// logger.trace("dragOver outsideDrag={} sourceIndex={}", outsideDrag, sourceIndex); 266 if (showTabArea(group, this)) { 267 if (!outsideDrag && (sourceIndex == -1)) { 268 return; 269 } 270 271 Point dropPoint = e.getLocation(); 272 overIndex = indexAtLocation(dropPoint.x, dropPoint.y); 273 repaint(); 274 } 275 } 276 277 /** 278 * Triggered when a drop has happened over {@code dropTarget}. 279 * 280 * @param e State that we'll need in order to handle the drop. 281 */ 282 @Override public void drop(DropTargetDropEvent e) { 283 if (!showTabArea(group, this)) { 284 return; 285 } 286 // if the dragged ComponentHolder was dragged from another window we 287 // must do a behind-the-scenes transfer from its old ComponentGroup to 288 // the end of the new ComponentGroup. 289 if (outsideDrag) { 290 DataFlavor[] flave = e.getCurrentDataFlavors(); 291 DraggableTabbedPane other = 292 ((DraggableTabFlavor)flave[0]).getDragTab(); 293 294 ComponentHolder target = other.removeDragged(); 295 sourceIndex = group.quietAddComponent(target); 296 outsideDrag = false; 297 298 McvComponentHolder draggedHolder = (McvComponentHolder)target; 299 300 List<ViewManager> vms = draggedHolder.getViewManagers(); 301 for (ViewManager vm : vms) { 302 vm.setWindow(window); 303 } 304 } 305 306 // check to see if we've actually dropped something McV understands. 307 if (sourceIndex >= 0) { 308 e.acceptDrop(VALID_ACTION); 309 Point dropPoint = e.getLocation(); 310 int dropIndex = indexAtLocation(dropPoint.x, dropPoint.y); 311 312 // make sure the user chose to drop over a valid area/thing first 313 // then do the actual drop. 314 if ((dropIndex != -1) && (getComponentAt(dropIndex) != null)) { 315 doDrop(sourceIndex, dropIndex); 316 } 317 318 // clean up anything associated with the current drag and drop 319 e.getDropTargetContext().dropComplete(true); 320 sourceIndex = -1; 321 overIndex = -1; 322 323 repaint(); 324 } 325 } 326 327 /** 328 * {@literal "Quietly"} removes the dragged component from its group. If 329 * the last component in a group has been dragged out of the group, the 330 * associated window will be killed. 331 * 332 * @return The removed component. 333 */ 334 private ComponentHolder removeDragged() { 335 ComponentHolder removed = group.quietRemoveComponentAt(sourceIndex); 336 337 // no point in keeping an empty window around... but killing the 338 // window here doesn't properly terminate the drag and drop (as this 339 // method is typically called from *another* window). 340 return removed; 341 } 342 343 /** 344 * Moves a component to its new index within the component group. 345 * 346 * @param srcIdx The old index of the component. 347 * @param dstIdx The new index of the component. 348 */ 349 public void doDrop(int srcIdx, int dstIdx) { 350 List<ComponentHolder> comps = group.getDisplayComponents(); 351 ComponentHolder src = comps.get(srcIdx); 352 group.removeComponent(src); 353 group.addComponent(src, dstIdx); 354 } 355 356 /** 357 * Overridden so that McIDAS-V can draw an indicator of a dragged tab's 358 * possible new position. 359 */ 360 @Override public void paint(Graphics g) { 361 super.paint(g); 362 if (overIndex >= 0) { 363 Rectangle bounds = getBoundsAt(overIndex); 364 if (bounds != null) { 365 g.drawImage(INDICATOR, bounds.x-7, bounds.y, null); 366 } 367 } 368 } 369 370 /** 371 * Overriden so that McIDAS-V can change the window title upon changing 372 * tabs. 373 */ 374 @Override public void setSelectedIndex(int index) { 375 super.setSelectedIndex(index); 376 377 // there are only ever component holders in the display comps. 378 @SuppressWarnings("unchecked") 379 List<ComponentHolder> comps = group.getDisplayComponents(); 380 381 ComponentHolder h = comps.get(index); 382 String newTitle = 383 UIManager.makeTitle(idv.getStateManager().getTitle(), h.getName()); 384 if (window != null) { 385 window.setTitle(newTitle); 386 } 387 } 388 389 /** 390 * Used to simply provide a reference to the originating 391 * DraggableTabbedPane while we're dragging and dropping. 392 */ 393 private static class TransferableIndex implements Transferable { 394 private DraggableTabbedPane tabbedPane; 395 396 private int index; 397 398 public TransferableIndex(DraggableTabbedPane dt, int i) { 399 tabbedPane = dt; 400 index = i; 401 } 402 403 // whatever is returned here needs to be serializable. so we can't just 404 // return the tabbedPane. :( 405 @Override public Object getTransferData(DataFlavor flavor) { 406 return index; 407 } 408 409 @Override public DataFlavor[] getTransferDataFlavors() { 410 return new DataFlavor[] { new DraggableTabFlavor(tabbedPane) }; 411 } 412 413 @Override public boolean isDataFlavorSupported(DataFlavor flavor) { 414 return true; 415 } 416 } 417 418 /** 419 * To be perfectly honest I'm still a bit fuzzy about DataFlavors. As far 420 * as I can tell they're used like so: if a user dragged an image file on 421 * to a toolbar, the toolbar might be smart enough to add the image. If the 422 * user dragged the same image file into a text document, the text editor 423 * might be smart enough to insert the path to the image or something. 424 * 425 * I'm thinking that would require two data flavors: some sort of toolbar 426 * flavor and then some sort of text flavor? 427 */ 428 private static class DraggableTabFlavor extends DataFlavor { 429 private DraggableTabbedPane tabbedPane; 430 431 public DraggableTabFlavor(DraggableTabbedPane dt) { 432 super(DraggableTabbedPane.class, "DraggableTabbedPane"); 433 tabbedPane = dt; 434 } 435 436 public DraggableTabbedPane getDragTab() { 437 return tabbedPane; 438 } 439 } 440 441 /** 442 * Handle the user dropping a tab outside of a McV window. This will create 443 * a new window and add the dragged tab to the ComponentGroup within the 444 * newly created window. The new window is the same size as the origin 445 * window, with the top centered over the location where the user released 446 * the mouse. 447 * 448 * @param dragged The ComponentHolder that's being dragged around. 449 * @param drop The x- and y-coordinates where the user dropped the tab. 450 */ 451 private void newWindowDrag(ComponentHolder dragged, Point drop) { 452 if (dragged == null) { 453 return; 454 } 455 456 UIManager ui = (UIManager)idv.getIdvUIManager(); 457 458 try { 459 Element skinRoot = 460 XmlUtil.getRoot(Constants.BLANK_COMP_GROUP, getClass()); 461 462 // create the new window with visibility off, so we can position 463 // the window in a sensible way before the user has to see it. 464 IdvWindow w = ui.createNewWindow(null, false, "McIDAS-V", 465 Constants.BLANK_COMP_GROUP, skinRoot, false, null); 466 467 // make the new window the same size as the old and center the 468 // *top* of the window over the drop point. 469 int height = window.getBounds().height; 470 int width = window.getBounds().width; 471 int startX = drop.x - (width / 2); 472 473 w.setBounds(new Rectangle(startX, drop.y, width, height)); 474 475 // be sure to add the dragged component holder to the new window. 476 ComponentGroup newGroup = w.getComponentGroups().get(0); 477 478 newGroup.addComponent(dragged); 479 480 McvComponentHolder draggedHolder = (McvComponentHolder)dragged; 481 List<ViewManager> vms = draggedHolder.getViewManagers(); 482 for (ViewManager vm : vms) { 483 vm.setWindow(w); 484 } 485 486 // let there be a window 487 w.pack(); 488 w.setVisible(true); 489// GuiUtils.toFront(w.getWindow()); 490// logger.trace("active window: {} new window: {}", Integer 491// .toHexString 492// (IdvWindow.getActiveWindow().hashCode()), Integer 493// .toHexString(w.hashCode())); 494 } catch (Throwable e) { 495 logger.error("Error creating new window from dragged tab", e); 496 } 497 } 498 499 /** 500 * Handles what happens at the very end of a drag and drop. Since I could 501 * not find a better method for it, tabs that are dropped outside of a McV 502 * window are handled with this method. 503 */ 504 public void dragDropEnd(DragSourceDropEvent e) { 505 if (!e.getDropSuccess() && (e.getDropAction() == 0)) { 506 newWindowDrag(removeDragged(), e.getLocation()); 507 } 508 509 // this should probably be the last thing to happen in this method. 510 // checks to see if we've got a blank window after a drag and drop; 511 // if so, dispose! 512 List<ComponentHolder> comps = group.getDisplayComponents(); 513 if ((comps == null) || comps.isEmpty()) { 514 window.dispose(); 515 } 516 } 517 518 // required methods that we don't need to implement yet. 519 @Override public void dragEnter(DragSourceDragEvent e) { } 520 @Override public void dragExit(DragSourceEvent e) { } 521 @Override public void dragOver(DragSourceDragEvent e) { } 522 @Override public void dropActionChanged(DragSourceDragEvent e) { } 523 @Override public void dropActionChanged(DropTargetDragEvent e) { } 524 525 @Override public void mouseClicked(final MouseEvent e) { 526 if (showTabArea(group, this)) { 527 processMouseEvents(e); 528 } 529 } 530 531 @Override public void mouseExited(final MouseEvent e) { 532 if (showTabArea(group, this)) { 533 processMouseEvents(e); 534 } 535 } 536 537 @Override public void mousePressed(final MouseEvent e) { 538 if (showTabArea(group, this)) { 539 processMouseEvents(e); 540 } else { 541 draggedAtX = e.getX(); 542 draggedAtY = e.getY(); 543 } 544 } 545 546 @Override public void mouseEntered(final MouseEvent e) { 547 if (showTabArea(group, this)) { 548 processMouseEvents(e); 549 } 550 } 551 552 @Override public void mouseMoved(final MouseEvent e) { 553 if (showTabArea(group, this)) { 554 processMouseEvents(e); 555 } 556 } 557 558 @Override public void mouseDragged(final MouseEvent e) { 559 // note: this method is called continously throughout the dragging 560 // process 561 if (showTabArea(group, this)) { 562 processMouseEvents(e); 563 } else { 564 window.setLocation(e.getX() - draggedAtX + window.getLocation().x, 565 e.getY() - draggedAtY + window.getLocation().y); 566 } 567 } 568 569 @Override public void mouseReleased(final MouseEvent e) { 570 if (showTabArea(group, this)) { 571 processMouseEvents(e); 572 } 573 } 574 575 private void processMouseEvents(final MouseEvent e) { 576 int eventX = e.getX(); 577 int eventY = e.getY(); 578 579 int tabIndex = getUI().tabForCoordinate(this, eventX, eventY); 580 if (tabIndex < 0) { 581 return; 582 } 583 584 if (!showTabArea(group, this)) { 585 return; 586 } 587 588 TabButton icon = (TabButton)getIconAt(tabIndex); 589 if (icon == null) { 590 return; 591 } 592 593 int id = e.getID(); 594 Rectangle iconBounds = icon.getBounds(); 595 if (!iconBounds.contains(eventX, eventY) || (id == MouseEvent.MOUSE_EXITED)) { 596 ButtonState state = icon.getState(); 597 if ((state == ButtonState.ROLLOVER) || (state == ButtonState.PRESSED)) { 598 icon.setState(ButtonState.DEFAULT); 599 } 600 601 if ((e.getClickCount() >= 2) && !e.isPopupTrigger() && (id == MouseEvent.MOUSE_CLICKED)) { 602 group.renameDisplay(tabIndex); 603 } 604 605 repaint(iconBounds); 606 return; 607 } 608 609 if ((id == MouseEvent.MOUSE_PRESSED) && ((e.getModifiersEx() & InputEvent.BUTTON1_DOWN_MASK) != 0)) { 610 icon.setState(ButtonState.PRESSED); 611 } else if (id == MouseEvent.MOUSE_CLICKED) { 612 icon.setState(ButtonState.DEFAULT); 613 group.destroyDisplay(tabIndex); 614 } else { 615 icon.setState(ButtonState.ROLLOVER); 616 } 617 repaint(iconBounds); 618 } 619 620 @Override public void addTab(String title, Component component) { 621 addTab(title, component, null); 622 } 623 624 public void addTab(String title, Component component, Icon extraIcon) { 625 int tabCount = getTabCount(); 626 int displayNumber = 0; 627 if (tabCount < 9) { 628 displayNumber = tabCount + 1; 629 } else if (tabCount == 9) { 630 displayNumber = 0; 631 } 632 title = "<html><font color=\"" + currentTabColor+"\">"+displayNumber+"</font>"+title+"</html>"; 633 if (showTabArea(group, this)) { 634 super.addTab(title, new TabButton(), component); 635 } else { 636 super.addTab("", component); 637 } 638 } 639 640 public static boolean showTabArea(McvComponentGroup mcvCompGroup, 641 JTabbedPane tabbedPane) 642 { 643 return !mcvCompGroup.getHideTabArea() || (tabbedPane.getTabCount() > 1); 644 } 645 646 class CloseableTabbedPaneUI extends BasicTabbedPaneUI { 647 private final Insets borderInsets = new Insets(0, 0, 0, 0); 648 649 private int horizontalTextPosition = SwingConstants.LEFT; 650 651 public CloseableTabbedPaneUI() { } 652 653 public CloseableTabbedPaneUI(int horizontalTextPosition) { 654 this.horizontalTextPosition = horizontalTextPosition; 655 } 656 657 @Override protected void paintContentBorder(Graphics g, int tabPlacement, int selectedIndex) { 658 if (showTabArea(group, tabPane)) { 659 super.paintContentBorder(g, tabPlacement, selectedIndex); 660 } 661 } 662 663 @Override protected Insets getContentBorderInsets(int tabPlacement) { 664 Insets insets = null; 665 if (showTabArea(group, tabPane)) { 666 insets = super.getContentBorderInsets(tabPlacement); 667 } else { 668 insets = borderInsets; 669 } 670 return insets; 671 } 672 673 @Override protected void layoutLabel(int tabPlacement, 674 FontMetrics metrics, int tabIndex, String title, Icon icon, 675 Rectangle tabRect, Rectangle iconRect, Rectangle textRect, 676 boolean isSelected) 677 { 678 if (tabPane.getTabCount() == 0) { 679 return; 680 } 681 682 if (!showTabArea(group, tabPane)) { 683 return; 684 } 685 686 textRect.x = textRect.y = iconRect.x = iconRect.y = 0; 687 View v = getTextViewForTab(tabIndex); 688 if (v != null) { 689 tabPane.putClientProperty("html", v); 690 } 691 692 SwingUtilities.layoutCompoundLabel(tabPane, 693 metrics, 694 title, 695 icon, 696 SwingConstants.CENTER, 697 SwingConstants.CENTER, 698 SwingConstants.CENTER, 699 horizontalTextPosition, 700 tabRect, 701 iconRect, 702 textRect, 703 textIconGap + 2); 704 705 int xNudge = getTabLabelShiftX(tabPlacement, tabIndex, isSelected); 706 int yNudge = getTabLabelShiftY(tabPlacement, tabIndex, isSelected); 707 iconRect.x += xNudge; 708 iconRect.y += yNudge; 709 textRect.x += xNudge; 710 textRect.y += yNudge; 711 } 712 713 @Override protected int calculateTabAreaHeight(int placement, int count, int height) { 714 return showTabArea(group, tabPane) 715 ? super.calculateTabAreaHeight(placement, count, height) 716 : 0; 717 } 718 719 @Override protected void paintTabBorder(Graphics g, int placement, 720 int idx, 721 int x, int y, int w, int h, 722 boolean isSel) 723 { 724 if (showTabArea(group, tabPane)) { 725 super.paintTabBorder(g, placement, idx, x, y, w, h, isSel); 726 } 727 } 728 729 @Override protected void paintTabBackground(Graphics g, 730 int placement, int idx, int x, int y, int w, int h, 731 boolean isSelected) 732 { 733 if (showTabArea(group, tabPane)) { 734 if (isSelected) { 735 g.setColor(selected); 736 } else { 737 g.setColor(unselected); 738 } 739 g.fillRect(x, y, w, h); 740 g.setColor(selected); 741 g.drawLine(x, y, x, y + h); 742 } 743 } 744 } 745 746 class CloseableMetalTabbedPaneUI extends MetalTabbedPaneUI { 747 private final Insets borderInsets = new Insets(0, 0, 0, 0); 748 749 private int horizontalTextPosition = SwingUtilities.LEFT; 750 751 public CloseableMetalTabbedPaneUI() { } 752 753 public CloseableMetalTabbedPaneUI(int newHorizontalTextPosition) { 754 this.horizontalTextPosition = newHorizontalTextPosition; 755 } 756 757 @Override protected void paintContentBorder(Graphics g, int tabPlacement, int selectedIndex) { 758 if (showTabArea(group, tabPane)) { 759 super.paintContentBorder(g, tabPlacement, selectedIndex); 760 } 761 } 762 763 @Override protected Insets getContentBorderInsets(int tabPlacement) { 764 Insets insets = null; 765 if (showTabArea(group, tabPane)) { 766 insets = super.getContentBorderInsets(tabPlacement); 767 } else { 768 insets = borderInsets; 769 } 770 return insets; 771 } 772 773 @Override protected void paintTabBorder(Graphics g, int placement, 774 int idx, 775 int x, int y, int w, int h, 776 boolean isSel) 777 { 778 if (showTabArea(group, tabPane)) { 779 super.paintTabBorder(g, placement, idx, x, y, w, h, isSel); 780 } 781 } 782 783 @Override protected void paintTabBackground(Graphics g, int placement, 784 int idx, 785 int x, int y, int w, int h, 786 boolean isSel) 787 { 788 if (showTabArea(group, tabPane)) { 789 super.paintTabBackground(g, placement, idx, x, y, w, h, isSel); 790 } 791 } 792 793 @Override protected int calculateTabAreaHeight(int placement, int count, int height) { 794 return showTabArea(group, tabPane) 795 ? super.calculateTabAreaHeight(placement, count, height) 796 : 0; 797 } 798 799 @Override protected void layoutLabel(int placement, 800 FontMetrics metrics, int tabIndex, String title, Icon icon, 801 Rectangle tabRect, Rectangle iconRect, Rectangle textRect, 802 boolean isSelected) 803 { 804 if (tabPane.getTabCount() != 0) { 805 textRect.x = 0; 806 textRect.y = 0; 807 iconRect.x = 0; 808 iconRect.y = 0; 809 810 View v = getTextViewForTab(tabIndex); 811 if (v != null) { 812 tabPane.putClientProperty("html", v); 813 } 814 815 SwingUtilities.layoutCompoundLabel(tabPane, 816 metrics, 817 title, 818 icon, 819 SwingConstants.CENTER, 820 SwingConstants.CENTER, 821 SwingConstants.CENTER, 822 horizontalTextPosition, 823 tabRect, 824 iconRect, 825 textRect, 826 textIconGap + 2); 827 828 int xNudge = 829 getTabLabelShiftX(placement, tabIndex, isSelected); 830 int yNudge = 831 getTabLabelShiftY(placement, tabIndex, isSelected); 832 iconRect.x += xNudge; 833 iconRect.y += yNudge; 834 textRect.x += xNudge; 835 textRect.y += yNudge; 836 } 837 } 838 } 839 840 public static class TabButton implements Icon { 841 842 private static final EnumMap<ButtonState, String> iconPaths = 843 new EnumMap<>(ButtonState.class); 844 845 private ButtonState currentState = ButtonState.DEFAULT; 846 private int iconWidth = 0; 847 private int iconHeight = 0; 848 849 private int posX = 0; 850 private int posY = 0; 851 852 public TabButton() { 853 setStateIcon(ButtonState.DEFAULT, ICON_DEFAULT); 854 setStateIcon(ButtonState.PRESSED, ICON_PRESSED); 855 setStateIcon(ButtonState.ROLLOVER, ICON_ROLLOVER); 856 setState(ButtonState.DEFAULT); 857 } 858 859 public static Icon getStateIcon(final ButtonState state) { 860 String path = iconPaths.get(state); 861 if (path == null) { 862 path = iconPaths.get(ButtonState.DEFAULT); 863 } 864 return GuiUtils.getImageIcon(path); 865 } 866 867 public static void setStateIcon(final ButtonState state, 868 final String path) 869 { 870 iconPaths.put(state, path); 871 } 872 873 public static String getStateIconPath(final ButtonState state) { 874 String path = iconPaths.get(ButtonState.DEFAULT); 875 if (iconPaths.containsKey(state)) { 876 path = iconPaths.get(state); 877 } 878 return path; 879 } 880 881 public void setState(final ButtonState state) { 882 currentState = state; 883 Icon currentIcon = getStateIcon(state); 884 if (currentIcon != null) { 885 iconWidth = currentIcon.getIconWidth(); 886 iconHeight = currentIcon.getIconHeight(); 887 } 888 } 889 890 public ButtonState getState() { 891 return currentState; 892 } 893 894 public Icon getIcon() { 895 return getStateIcon(currentState); 896 } 897 898 @Override public void paintIcon(Component c, Graphics g, int x, int y) { 899 Icon current = getIcon(); 900 if (current != null) { 901 posX = x; 902 posY = y; 903 current.paintIcon(c, g, x, y); 904 } 905 } 906 907 @Override public int getIconWidth() { 908 return iconWidth; 909 } 910 911 @Override public int getIconHeight() { 912 return iconHeight; 913 } 914 915 public Rectangle getBounds() { 916 return new Rectangle(posX, posY, iconWidth, iconHeight); 917 } 918 } 919}