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