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