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}