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