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}