001 002/* ---------------------------------------------------------------------- 003 * 004 * Copyright (c) 2002-2009 The MITRE Corporation 005 * 006 * Except as permitted below 007 * ALL RIGHTS RESERVED 008 * 009 * The MITRE Corporation (MITRE) provides this software to you without 010 * charge to use for your internal purposes only. Any copy you make for 011 * such purposes is authorized provided you reproduce MITRE's copyright 012 * designation and this License in any such copy. You may not give or 013 * sell this software to any other party without the prior written 014 * permission of the MITRE Corporation. 015 * 016 * The government of the United States of America may make unrestricted 017 * use of this software. 018 * 019 * This software is the copyright work of MITRE. No ownership or other 020 * proprietary interest in this software is granted you other than what 021 * is granted in this license. 022 * 023 * Any modification or enhancement of this software must inherit this 024 * license, including its warranty disclaimers. You hereby agree to 025 * provide to MITRE, at no charge, a copy of any such modification or 026 * enhancement without limitation. 027 * 028 * MITRE IS PROVIDING THE PRODUCT "AS IS" AND MAKES NO WARRANTY, EXPRESS 029 * OR IMPLIED, AS TO THE ACCURACY, CAPABILITY, EFFICIENCY, 030 * MERCHANTABILITY, OR FUNCTIONING OF THIS SOFTWARE AND DOCUMENTATION. IN 031 * NO EVENT WILL MITRE BE LIABLE FOR ANY GENERAL, CONSEQUENTIAL, 032 * INDIRECT, INCIDENTAL, EXEMPLARY OR SPECIAL DAMAGES, EVEN IF MITRE HAS 033 * BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 034 * 035 * You accept this software on the condition that you indemnify and hold 036 * harmless MITRE, its Board of Trustees, officers, agents, and 037 * employees, from any and all liability or damages to third parties, 038 * including attorneys' fees, court costs, and other related costs and 039 * expenses, arising out of your use of this software irrespective of the 040 * cause of said liability. 041 * 042 * The export from the United States or the subsequent reexport of this 043 * software is subject to compliance with United States export control 044 * and munitions control restrictions. You agree that in the event you 045 * seek to export this software you assume full responsibility for 046 * obtaining all necessary export licenses and approvals and for assuring 047 * compliance with applicable reexport restrictions. 048 * 049 * ---------------------------------------------------------------------- 050 * 051 * NOTICE 052 * 053 * This software was produced for the U. S. Government 054 * under Contract No. W15P7T-09-C-F600, and is 055 * subject to the Rights in Noncommercial Computer Software 056 * and Noncommercial Computer Software Documentation 057 * Clause 252.227-7014 (JUN 1995). 058 * 059 * (c) 2009 The MITRE Corporation. All Rights Reserved. 060 * 061 * ---------------------------------------------------------------------- 062 * 063 */ 064/* 065 * Copyright (c) 2002-2006 The MITRE Corporation 066 * 067 * Except as permitted below 068 * ALL RIGHTS RESERVED 069 * 070 * The MITRE Corporation (MITRE) provides this software to you without 071 * charge to use for your internal purposes only. Any copy you make for 072 * such purposes is authorized provided you reproduce MITRE's copyright 073 * designation and this License in any such copy. You may not give or 074 * sell this software to any other party without the prior written 075 * permission of the MITRE Corporation. 076 * 077 * The government of the United States of America may make unrestricted 078 * use of this software. 079 * 080 * This software is the copyright work of MITRE. No ownership or other 081 * proprietary interest in this software is granted you other than what 082 * is granted in this license. 083 * 084 * Any modification or enhancement of this software must inherit this 085 * license, including its warranty disclaimers. You hereby agree to 086 * provide to MITRE, at no charge, a copy of any such modification or 087 * enhancement without limitation. 088 * 089 * MITRE IS PROVIDING THE PRODUCT "AS IS" AND MAKES NO WARRANTY, EXPRESS 090 * OR IMPLIED, AS TO THE ACCURACY, CAPABILITY, EFFICIENCY, 091 * MERCHANTABILITY, OR FUNCTIONING OF THIS SOFTWARE AND DOCUMENTATION. IN 092 * NO EVENT WILL MITRE BE LIABLE FOR ANY GENERAL, CONSEQUENTIAL, 093 * INDIRECT, INCIDENTAL, EXEMPLARY OR SPECIAL DAMAGES, EVEN IF MITRE HAS 094 * BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 095 * 096 * You accept this software on the condition that you indemnify and hold 097 * harmless MITRE, its Board of Trustees, officers, agents, and 098 * employees, from any and all liability or damages to third parties, 099 * including attorneys' fees, court costs, and other related costs and 100 * expenses, arising out of your use of this software irrespective of the 101 * cause of said liability. 102 * 103 * The export from the United States or the subsequent reexport of this 104 * software is subject to compliance with United States export control 105 * and munitions control restrictions. You agree that in the event you 106 * seek to export this software you assume full responsibility for 107 * obtaining all necessary export licenses and approvals and for assuring 108 * compliance with applicable reexport restrictions. 109 */ 110 111package jmri.util.org.mitre.jawb.swing; 112 113import java.awt.*; 114import java.awt.event.*; 115import java.util.HashMap; 116import java.util.Iterator; 117 118import javax.swing.*; 119import javax.swing.event.MouseInputAdapter; 120 121// added as part of migration to JMRI 122import jmri.util.JmriJFrame; 123 124/** 125 * JTabbedPane implementation which allows tabbs to be 'torn off' as their own 126 * window. When the DetachableTabbedPane is set not visible using the 127 * 'setVisible' method, any detached tabs are also hidden. When set visible by 128 * the same means, previously detached, yet hidden tabs, are re-shown. 129 * 130 * @author <a href="mailto:red@mitre.org">Chadwick A. McHenry</a> 131 * @version 1.0 132 */ 133public class DetachableTabbedPane extends JTabbedPane { 134 135 /* multiple use Icons */ 136 private static Icon plainIcon = new DetachPanelIcon (false); 137 private static Icon pressedIcon = new DetachPanelIcon (true); 138 /* map panels to their Detachable objects 139 * @see #Detachable */ 140 protected HashMap<Component, Detachable> panelToDetMap = new HashMap<Component, Detachable>(); 141 142 /** 143 * Indicates whether the tabs in this TabbedPane are actually detachable, 144 * or just behave normally 145 */ 146 protected boolean detachable = true; 147 148 /** Prettify the detached tabbs */ 149 protected Image detachedIconImage = null; 150 151 String titleSuffix = ": Foon"; 152 153 /** 154 * Creates an empty <code>DetachableTabbedPane</code> with a default tab 155 * placement of <code>JTabbedPane.TOP</code> and detachability on. 156 */ 157 public DetachableTabbedPane () { 158 super (); 159 init (); 160 } 161 162 public DetachableTabbedPane (String titleSuffix) { 163 super (); 164 init (); 165 this.titleSuffix = titleSuffix; 166 } 167 168 /** 169 * Creates an empty <code>DetachableTabbedPane</code> with the specified tab 170 * placement of either: <code>JTabbedPane.TOP</code>, 171 * <code>JTabbedPane.BOTTOM</code>, <code>JTabbedPane.LEFT</code>, or 172 * <code>JTabbedPane.RIGHT</code>, and specified detachability. 173 * @param tabPlacement tab placement 174 * @param detachable true if detachable 175 */ 176 public DetachableTabbedPane (int tabPlacement, boolean detachable) { 177 super (tabPlacement); 178 this.detachable = detachable; 179 init (); 180 } 181 182 /** 183 * Creates an empty <code>DetachableTabbedPane</code> with the specified tab 184 * placement and tab layout policy. 185 * @param tabPlacement tab placement 186 * @param tabLayoutPolicy tab layout policy 187 * @param detachable true if detachable 188 */ 189 public DetachableTabbedPane (int tabPlacement, int tabLayoutPolicy, 190 boolean detachable) { 191 super (tabPlacement, tabLayoutPolicy); 192 this.detachable = detachable; 193 init (); 194 } 195 196 /** Code common to all constructors. */ 197 private void init () { 198 // retrieve the current mouse listeners (put in by L&F) and remove them 199 // from the standard dispatcher so we can filter some events 200 final MouseListener[] mListeners = getMouseListeners (); 201 for (int i=0; i<mListeners.length; i++) 202 removeMouseListener (mListeners[i]); 203 204 // this will forward mouse events to the little detach buttons and 205 // all the look and feel listeners (since we want to filter some) 206 MouseInputAdapter ma = new MouseInputAdapter () { 207 Detachable last = null; 208 // Returns a Detachable only if the mouse event is within the 209 // detachable's icon 210 private Detachable getDetachable (MouseEvent e) { 211 if (last != null && last.contains (e.getX(), e.getY())) 212 return last; 213 214 last = null; 215 Iterator<Detachable> iter = panelToDetMap.values ().iterator (); 216 while (iter.hasNext ()) { 217 Detachable d = iter.next(); 218 if (d.contains (e.getX(), e.getY())) { 219 last = d; 220 break; 221 } 222 } 223 return last; 224 } 225 @Override 226 public void mouseMoved (MouseEvent e) { 227 Detachable old = last; 228 Detachable d = getDetachable (e); 229 if (old != d) { 230 if (old != null) { 231 old.setPressed (false); 232 old.repaint (); 233 } 234 if (d != null) { 235 d.setPressed (true); 236 d.repaint (); 237 } 238 } 239 } 240 @Override 241 public void mouseClicked (MouseEvent e) { 242 Detachable d = getDetachable (e); 243 last = null; 244 if (d != null) { 245 detach (d); 246 d.setPressed (false); 247 // filter the event from the other handlers 248 return; 249 } 250 // not 'contained' within a detachable? pass it on 251 for (int i=0; i<mListeners.length; i++) 252 mListeners[i].mouseClicked (e); 253 } 254 @Override 255 public void mouseExited (MouseEvent e) { 256 if (last != null) { 257 last.setPressed (false); 258 last.repaint (); 259 } 260 last = null; 261 // no filtering 262 for (int i=0; i<mListeners.length; i++) 263 mListeners[i].mouseExited (e); 264 } 265 @Override 266 public void mouseEntered (MouseEvent e) { 267 // no filtering 268 for (int i=0; i<mListeners.length; i++) 269 mListeners[i].mouseEntered (e); 270 } 271 @Override 272 public void mousePressed (MouseEvent e) { 273 // filter from the other handlers so it doesn't 'change tabs' 274 if (getDetachable (e) != null) 275 return; 276 // not 'contained' within a detachable? pass it on 277 for (int i=0; i<mListeners.length; i++) 278 mListeners[i].mousePressed (e); 279 } 280 @Override 281 public void mouseReleased (MouseEvent e) { 282 // no filtering 283 for (int i=0; i<mListeners.length; i++) 284 mListeners[i].mouseReleased (e); 285 } 286 }; 287 addMouseListener (ma); 288 addMouseMotionListener (ma); 289 } 290 291 public void setDetachedIconImage (Image image) { 292 detachedIconImage = image; 293 Iterator<Detachable> iter = panelToDetMap.values ().iterator (); 294 while (iter.hasNext ()) { 295 Detachable d = iter.next(); 296 d.getFrame ().setIconImage (detachedIconImage); 297 } 298 } 299 300 public Image getDetachedIconImage () { 301 return detachedIconImage; 302 } 303 304 /** 305 * Returns the default Detachable. 306 * @param title title 307 * @param icon icon 308 * @param comp component 309 * @param tip tool tip 310 * @param index index 311 * @param titleSuffix title suffix 312 * @return default Detachable 313 */ 314 protected Detachable createDetachable (String title, Icon icon, 315 Component comp, 316 String tip, int index, String titleSuffix) { 317 return new Detachable (title, icon, comp, tip, index, titleSuffix); 318 } 319 320 /** 321 * Lookup the Detachable for the specified component, which must have been 322 * added as a tab. Returns null if not already added. 323 * @param comp component 324 * @return Returns null if not already added 325 */ 326 protected Detachable getDetachable(Component comp) { 327 return panelToDetMap.get(comp); 328 } 329 330 /** 331 * Return Detachables which have been added as Tabs or Detached Frames. TODO: 332 * Currently, order is not accurate. 333 * @return Detachables 334 */ 335 protected Detachable[] getDetachables() { 336 return panelToDetMap.values().toArray(new Detachable[0]); 337 } 338 339 /** 340 * Overridden to add our 'detach' icon. All the <code>add</code> and 341 * <code>addTab</code> methods are cover methods for <code>insertTab</code>. 342 */ 343 @Override 344 public void insertTab (String title, Icon icon, Component comp, 345 String tip, int index) { 346 log.debug("insertTab {} at index {}", title, index); 347 // the index we get is based on the number of tabs show, not the number of 348 // components, so to remain consistent create the Detachable with an index 349 // based on the number of detachables we have 350 Detachable d = createDetachable (title, icon, comp, tip, 351 panelToDetMap.size(), titleSuffix); 352 d.getFrame ().setIconImage (detachedIconImage); 353 354 shiftDetachables (true, d); 355 panelToDetMap.put (comp, d); 356 357 if (detachable && d.isDetached ()) 358 detach (d); 359 else 360 attach (d); 361 } 362 363 @Override 364 public void setComponentAt(int index, Component component) { 365 log.debug("setComponentAt name = {} index = {}", getTitleAt(index), index); 366 367 var oldcomp = getComponentAt(index); 368 var detachable = panelToDetMap.get(oldcomp); 369 panelToDetMap.remove(oldcomp); 370 371 panelToDetMap.put(component, detachable); 372 detachable.component = component; 373 374 super.setComponentAt(index, component); 375 } 376 377 /** 378 * Replace a component by identity, including when it is currently detached. 379 * 380 * @param oldComponent component currently displayed or detached 381 * @param newComponent replacement component 382 * @return true if the old component was found and replaced 383 */ 384 public boolean replaceComponent(Component oldComponent, Component newComponent) { 385 Detachable d = panelToDetMap.get(oldComponent); 386 if (d == null) { 387 return false; 388 } 389 390 if (d.isDetached()) { 391 d.frame.getContentPane().remove(oldComponent); 392 d.component = newComponent; 393 panelToDetMap.remove(oldComponent); 394 panelToDetMap.put(newComponent, d); 395 d.frame.getContentPane().add(newComponent, BorderLayout.CENTER); 396 newComponent.setVisible(true); 397 d.frame.validate(); 398 } else { 399 int index = indexOfComponent(oldComponent); 400 if (index < 0) { 401 return false; 402 } 403 panelToDetMap.remove(oldComponent); 404 panelToDetMap.put(newComponent, d); 405 d.component = newComponent; 406 super.setComponentAt(index, newComponent); 407 } 408 return true; 409 } 410 411 // just adds logging 412 @Override 413 public void addTab(String name, Component component) { 414 log.debug("addTab name = {} count = {}", name, getTabCount()); 415 super.addTab(name, component); 416 } 417 418 /** 419 * Overridden to remove the comopnent from the possible list of components 420 * this pane displays 421 */ 422 @Override 423 public void remove (int index) { 424 remove(panelToDetMap.get (getComponentAt (index))); 425 } 426 @Override 427 public void remove (Component comp) { 428 Detachable detachable = panelToDetMap.get (comp); 429 if (detachable != null) { 430 log.trace("panelToDetMap of {} found {} with {}", comp, detachable, detachable.component); 431 remove (detachable); 432 } else { 433 super.remove(comp); 434 } 435 } 436 private void remove (Detachable d) { 437 if (d != null) { 438 panelToDetMap.remove (d.component); 439 shiftDetachables (false, d); 440 super.remove (d.component); // ok even if not 'attached' 441 d.dispose (); 442 } 443 } 444 /** 445 * This keeps the order of the detachables correct when adding or replacing 446 * in the tabbed. 447 */ 448 private void shiftDetachables (boolean insert, Detachable cause) { 449 Iterator<Detachable> iter = panelToDetMap.values ().iterator (); 450 while (iter.hasNext ()) { 451 Detachable d = iter.next(); 452 if (d.index >= cause.index) 453 d.index += (insert ? 1 : -1); 454 } 455 } 456 457 /** 458 * Bypass remove and add internal panel to the tabbedpane 459 */ 460 private void detach (Detachable d) { 461 if (detachable) { 462 super.remove (d.component); // ok, even if not 'attached' yet 463 validate (); 464 d.setDetached (true); 465 } 466 } 467 468 /** 469 * Bypass insertTab and add internal panel to the tabbedpane 470 */ 471 private void attach (Detachable d) { 472 int ti; 473 for (ti=0; ti<getTabCount(); ti++) { 474 Detachable tabD = panelToDetMap.get (getComponentAt(ti)); 475 if (tabD != null && tabD.index > d.index) 476 break; 477 } 478 d.setDetached (false); 479 super.insertTab (d.title, d.icon, d.component, d.tip, ti); 480 validate (); 481 } 482 483 /** 484 * Overridden to hide or show the detached tabs as well. State is retained, 485 * so that if you hide this TabbedPane, the detached panels will be hidden, 486 * but when you re-show this TabbedPane, the detached panels will be 487 * re-shown in their last positions. 488 */ 489 @Override 490 public void setVisible (boolean show) { 491 Iterator<Detachable> iter = panelToDetMap.values ().iterator (); 492 while (iter.hasNext ()) { 493 Detachable d = iter.next(); 494 if (d.isDetached ()) 495 d.getFrame().setVisible (show); 496 } 497 } 498 499 public void setDetachable (boolean detachable) { 500 if (detachable == this.detachable) 501 return; 502 503 this.detachable = detachable; 504 //TODO: finish! 505 // if (detachable) { 506 // /* 507 // for each tab, set icon d.icon; 508 // */ 509 // } else { // !detachable 510 // /* 511 // for each detachable, close if open: 512 // for each tab, set icon d.userIcon; 513 // */ 514 // } 515 } 516 517 /** 518 * Icon which remembers where it was drawn last so that it can be queried 519 * with 'contains' requests. Needed because JTabbedPane won't let us put a 520 * component (like a button) in the tab itself. This ability will be 521 * included in a future Java release, but for now, I've got to do it by hand 522 * with this. 523 */ 524 private static class LocatedIcon implements Icon { 525 Icon icon; 526 int x, y; 527 LocatedIcon (Icon icon) { 528 this.icon = icon; 529 } 530 boolean contains (int cx, int cy) { 531 return (x <= cx) && (cx <= x+icon.getIconWidth()) && 532 (y <= cy) && (cy <= y+icon.getIconHeight()); 533 } 534 @Override 535 public int getIconHeight () { 536 return icon.getIconHeight(); 537 } 538 @Override 539 public int getIconWidth () { 540 return icon.getIconWidth(); 541 } 542 @Override 543 public void paintIcon (Component c, Graphics g, int px, int py) { 544 x = px; 545 y = py; 546 icon.paintIcon (c, g, x, y); 547 } 548 } 549 550 /** 551 * Class to maintain info for panels as they are added and removed, detached 552 * and attached from the <code>DetachableTabPane</code>. 553 */ 554 public class Detachable { 555 556 protected String title = null; // remember to reattach to tabbed pane 557 protected Icon icon = null; // possibly composite icon 558 protected Icon userIcon = null; // user supplied icon 559 protected Component component = null; // component to display 560 protected String tip = null; 561 protected int index = 0; 562 563 //transient Icon cachedIcon = null; 564 565 //DetachButton button = null; // displayed in tab for detaching 566 protected LocatedIcon button = null; // displayed in tab for detaching 567 protected JmriJFrame frame = null; // detached container 568 protected boolean detached = false; // keeps detached state if not visible 569 570 public Detachable (String title, Icon icon, 571 Component comp, String tip, int index, String titleSuffix) { 572 573 this.title = title; 574 this.userIcon = icon; 575 this.component = comp; 576 this.tip = tip; 577 this.index = index; 578 579 /* frame to display component when detached. */ 580 frame = new JmriJFrame ( (title!=null?title:comp.getName())+titleSuffix); 581 frame.makePrivateWindow(); 582 frame.addHelpMenu(null,true); 583 frame.addWindowListener (new WindowAdapter () { 584 @Override 585 public void windowClosing (WindowEvent e) { 586 DetachableTabbedPane.this.attach (Detachable.this); 587 } 588 }); 589 590 // put it in the frame to set frames initial sizing 591 frame.getContentPane ().add (component); 592 frame.validate (); 593 frame.pack (); 594 frame.getContentPane ().remove (component); 595 // initially attached (added to tabbedPane at creation, so hide) 596 frame.setVisible (false); 597 598 button = new LocatedIcon (plainIcon); 599 // create composite if neccissary 600 if (userIcon != null) 601 this.icon = new CompositeIcon (button, userIcon); 602 else 603 this.icon = button; 604 } 605 606 public JFrame getFrame () { 607 return frame; 608 } 609 610 public Component getComponent () { 611 return component; 612 } 613 614 public String getTitle () { 615 return title; 616 } 617 618 public void setPressed (boolean pressed) { 619 if (pressed) 620 button.icon = pressedIcon; 621 else 622 button.icon = plainIcon; 623 repaint (); 624 } 625 626 public void repaint () { 627 DetachableTabbedPane.this. 628 repaint (0, button.x, button.y, 629 button.getIconWidth(),button.getIconHeight()); 630 } 631 632 public boolean isDetached () { 633 return detached; 634 } 635 636 public void setDetached (boolean detached) { 637 this.detached = detached; 638 639 if (detached && /*tabbedPane*/ isVisible () && ! frame.isVisible ()) { 640 // removeTabAt doesn't even set it visible again in java 1.3, so do it 641 // by hand 642 component.setVisible (true); 643 frame.getContentPane().add (component, BorderLayout.CENTER); 644 645 frame.makePublicWindow(); 646 647 // some window managers like to reposition windows. Don't let 'em 648 Rectangle bounds = frame.getBounds(); 649 650 // don't pack again, so it remains the size the user chose before 651 frame.setVisible (true); 652 frame.setBounds (bounds); 653 frame.validate (); 654 655 } else if (! detached && frame.isVisible()) { 656 frame.setVisible (false); 657 frame.getContentPane().removeAll (); 658 frame.makePrivateWindow(); 659 } 660 } 661 662 public void dispose () { 663 frame.dispose (); 664 } 665 666 public boolean contains (int x, int y) { 667 return (! detached && button.contains (x, y)); 668 } 669 } 670 671 /** Testing */ 672// public static void main(String s[]) { 673// JFrame frame = new JFrame("Annotation Editor Panel Demo"); 674// 675// frame.addWindowListener(new WindowAdapter() { 676// @Override 677// public void windowClosing(WindowEvent e) {System.exit(0);} 678// }); 679// 680// DetachableTabbedPane aep = new DetachableTabbedPane (); 681// 682// // add some tabs 683// aep.add ("One", new JLabel ("One")); 684// aep.add ("Two", new JLabel ("Two")); 685// 686// frame.getContentPane().add(aep); 687// frame.pack(); 688// frame.setVisible(true); 689// } 690 691 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DetachableTabbedPane.class); 692 693}