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