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 // just adds logging 378 @Override 379 public void addTab(String name, Component component) { 380 log.debug("addTab name = {} count = {}", name, getTabCount()); 381 super.addTab(name, component); 382 } 383 384 /** 385 * Overridden to remove the comopnent from the possible list of components 386 * this pane displays 387 */ 388 @Override 389 public void remove (int index) { 390 remove(panelToDetMap.get (getComponentAt (index))); 391 } 392 @Override 393 public void remove (Component comp) { 394 Detachable detachable = panelToDetMap.get (comp); 395 if (detachable != null) 396 remove (detachable); 397 else 398 super.remove(comp); 399 } 400 private void remove (Detachable d) { 401 if (d != null) { 402 panelToDetMap.remove (d.component); 403 shiftDetachables (false, d); 404 super.remove (d.component); // ok even if not 'attached' 405 d.dispose (); 406 } 407 } 408 /** 409 * This keeps the order of the detachables correct when adding or replacing 410 * in the tabbed. 411 */ 412 private void shiftDetachables (boolean insert, Detachable cause) { 413 Iterator<Detachable> iter = panelToDetMap.values ().iterator (); 414 while (iter.hasNext ()) { 415 Detachable d = iter.next(); 416 if (d.index >= cause.index) 417 d.index += (insert ? 1 : -1); 418 } 419 } 420 421 /** 422 * Bypass remove and add internal panel to the tabbedpane 423 */ 424 private void detach (Detachable d) { 425 if (detachable) { 426 super.remove (d.component); // ok, even if not 'attached' yet 427 validate (); 428 d.setDetached (true); 429 } 430 } 431 432 /** 433 * Bypass insertTab and add internal panel to the tabbedpane 434 */ 435 private void attach (Detachable d) { 436 int ti; 437 for (ti=0; ti<getTabCount(); ti++) { 438 Detachable tabD = panelToDetMap.get (getComponentAt(ti)); 439 if (tabD != null && tabD.index > d.index) 440 break; 441 } 442 d.setDetached (false); 443 super.insertTab (d.title, d.icon, d.component, d.tip, ti); 444 validate (); 445 } 446 447 /** 448 * Overridden to hide or show the detached tabs as well. State is retained, 449 * so that if you hide this TabbedPane, the detached panels will be hidden, 450 * but when you re-show this TabbedPane, the detached panels will be 451 * re-shown in their last positions. 452 */ 453 @Override 454 public void setVisible (boolean show) { 455 Iterator<Detachable> iter = panelToDetMap.values ().iterator (); 456 while (iter.hasNext ()) { 457 Detachable d = iter.next(); 458 if (d.isDetached ()) 459 d.getFrame().setVisible (show); 460 } 461 } 462 463 public void setDetachable (boolean detachable) { 464 if (detachable == this.detachable) 465 return; 466 467 this.detachable = detachable; 468 //TODO: finish! 469 // if (detachable) { 470 // /* 471 // for each tab, set icon d.icon; 472 // */ 473 // } else { // !detachable 474 // /* 475 // for each detachable, close if open: 476 // for each tab, set icon d.userIcon; 477 // */ 478 // } 479 } 480 481 /** 482 * Icon which remembers where it was drawn last so that it can be queried 483 * with 'contains' requests. Needed because JTabbedPane won't let us put a 484 * component (like a button) in the tab itself. This ability will be 485 * included in a future Java release, but for now, I've got to do it by hand 486 * with this. 487 */ 488 private static class LocatedIcon implements Icon { 489 Icon icon; 490 int x, y; 491 LocatedIcon (Icon icon) { 492 this.icon = icon; 493 } 494 boolean contains (int cx, int cy) { 495 return (x <= cx) && (cx <= x+icon.getIconWidth()) && 496 (y <= cy) && (cy <= y+icon.getIconHeight()); 497 } 498 @Override 499 public int getIconHeight () { 500 return icon.getIconHeight(); 501 } 502 @Override 503 public int getIconWidth () { 504 return icon.getIconWidth(); 505 } 506 @Override 507 public void paintIcon (Component c, Graphics g, int px, int py) { 508 x = px; 509 y = py; 510 icon.paintIcon (c, g, x, y); 511 } 512 } 513 514 /** 515 * Class to maintain info for panels as they are added and removed, detached 516 * and attached from the <code>DetachableTabPane</code>. 517 */ 518 public class Detachable { 519 520 protected String title = null; // remember to reattach to tabbed pane 521 protected Icon icon = null; // possibly composite icon 522 protected Icon userIcon = null; // user supplied icon 523 protected Component component = null; // component to display 524 protected String tip = null; 525 protected int index = 0; 526 527 //transient Icon cachedIcon = null; 528 529 //DetachButton button = null; // displayed in tab for detaching 530 protected LocatedIcon button = null; // displayed in tab for detaching 531 protected JmriJFrame frame = null; // detached container 532 protected boolean detached = false; // keeps detached state if not visible 533 534 public Detachable (String title, Icon icon, 535 Component comp, String tip, int index, String titleSuffix) { 536 537 this.title = title; 538 this.userIcon = icon; 539 this.component = comp; 540 this.tip = tip; 541 this.index = index; 542 543 /* frame to display component when detached. */ 544 frame = new JmriJFrame ( (title!=null?title:comp.getName())+titleSuffix); 545 frame.makePrivateWindow(); 546 frame.addHelpMenu(null,true); 547 frame.addWindowListener (new WindowAdapter () { 548 @Override 549 public void windowClosing (WindowEvent e) { 550 DetachableTabbedPane.this.attach (Detachable.this); 551 } 552 }); 553 554 // put it in the frame to set frames initial sizing 555 frame.getContentPane ().add (component); 556 frame.validate (); 557 frame.pack (); 558 frame.getContentPane ().remove (component); 559 // initially attached (added to tabbedPane at creation, so hide) 560 frame.setVisible (false); 561 562 button = new LocatedIcon (plainIcon); 563 // create composite if neccissary 564 if (userIcon != null) 565 this.icon = new CompositeIcon (button, userIcon); 566 else 567 this.icon = button; 568 } 569 570 public JFrame getFrame () { 571 return frame; 572 } 573 574 public Component getComponent () { 575 return component; 576 } 577 578 public String getTitle () { 579 return title; 580 } 581 582 public void setPressed (boolean pressed) { 583 if (pressed) 584 button.icon = pressedIcon; 585 else 586 button.icon = plainIcon; 587 repaint (); 588 } 589 590 public void repaint () { 591 DetachableTabbedPane.this. 592 repaint (0, button.x, button.y, 593 button.getIconWidth(),button.getIconHeight()); 594 } 595 596 public boolean isDetached () { 597 return detached; 598 } 599 600 public void setDetached (boolean detached) { 601 this.detached = detached; 602 603 if (detached && /*tabbedPane*/ isVisible () && ! frame.isVisible ()) { 604 // removeTabAt doesn't even set it visible again in java 1.3, so do it 605 // by hand 606 component.setVisible (true); 607 frame.getContentPane().add (component, BorderLayout.CENTER); 608 609 frame.makePublicWindow(); 610 611 // some window managers like to reposition windows. Don't let 'em 612 Rectangle bounds = frame.getBounds(); 613 614 // don't pack again, so it remains the size the user chose before 615 frame.setVisible (true); 616 frame.setBounds (bounds); 617 frame.validate (); 618 619 } else if (! detached && frame.isVisible()) { 620 frame.setVisible (false); 621 frame.getContentPane().removeAll (); 622 frame.makePrivateWindow(); 623 } 624 } 625 626 public void dispose () { 627 frame.dispose (); 628 } 629 630 public boolean contains (int x, int y) { 631 return (! detached && button.contains (x, y)); 632 } 633 } 634 635 /** Testing */ 636// public static void main(String s[]) { 637// JFrame frame = new JFrame("Annotation Editor Panel Demo"); 638// 639// frame.addWindowListener(new WindowAdapter() { 640// @Override 641// public void windowClosing(WindowEvent e) {System.exit(0);} 642// }); 643// 644// DetachableTabbedPane aep = new DetachableTabbedPane (); 645// 646// // add some tabs 647// aep.add ("One", new JLabel ("One")); 648// aep.add ("Two", new JLabel ("Two")); 649// 650// frame.getContentPane().add(aep); 651// frame.pack(); 652// frame.setVisible(true); 653// } 654 655 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DetachableTabbedPane.class); 656 657}