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 javax.swing.*;
116import javax.swing.event.MouseInputAdapter;
117import java.util.HashMap;
118import java.util.Iterator;
119
120// added as part of migration to JMRI
121import jmri.util.JmriJFrame;
122
123/**
124 * JTabbedPane implementation which allows tabbs to be 'torn off' as their own
125 * window. When the DetachableTabbedPane is set not visible using the
126 * 'setVisible' method, any detached tabs are also hidden. When set visible by
127 * the same means, previously detached, yet hidden tabs, are re-shown.
128 *
129 * @author <a href="mailto:red@mitre.org">Chadwick A. McHenry</a>
130 * @version 1.0
131 */
132public class DetachableTabbedPane extends JTabbedPane {
133
134  /* multiple use Icons */
135  private static Icon plainIcon = new DetachPanelIcon (false);
136  private static Icon pressedIcon = new DetachPanelIcon (true);
137  /* map panels to their Detachable objects
138   * @see #Detachable */
139  protected HashMap<Component, Detachable> panelToDetMap = new HashMap<Component, Detachable>();
140
141  /**
142   * Indicates whether the tabs in this TabbedPane are actually detachable,
143   * or just behave normally
144   */
145  protected boolean detachable = true;
146
147  /** Prettify the detached tabbs */
148  protected Image detachedIconImage = null;
149  
150  String titleSuffix = ": Foon";
151  
152  /**
153   * Creates an empty <code>DetachableTabbedPane</code> with a default tab
154   * placement of <code>JTabbedPane.TOP</code> and detachability on.
155   */
156  public DetachableTabbedPane () {
157    super ();
158    init ();
159  }
160
161  public DetachableTabbedPane (String titleSuffix) {
162    super ();
163    init ();
164    this.titleSuffix = titleSuffix;
165  }
166  
167  /**
168   * Creates an empty <code>DetachableTabbedPane</code> with the specified tab
169   * placement of either: <code>JTabbedPane.TOP</code>,
170   * <code>JTabbedPane.BOTTOM</code>, <code>JTabbedPane.LEFT</code>, or
171   * <code>JTabbedPane.RIGHT</code>, and specified detachability.
172   */
173  public DetachableTabbedPane (int tabPlacement, boolean detachable) {
174    super (tabPlacement);
175    this.detachable = detachable;
176    init ();
177  }
178  
179  /**
180   * Creates an empty <code>DetachableTabbedPane</code> with the specified tab
181   * placement and tab layout policy.
182   */
183  public DetachableTabbedPane (int tabPlacement, int tabLayoutPolicy,
184                               boolean detachable) {
185    super (tabPlacement, tabLayoutPolicy);
186    this.detachable = detachable;
187    init ();
188  }
189
190  /** Code common to all constructors. */
191  private void init () {
192    // retrieve the current mouse listeners (put in by L&F) and remove them
193    // from the standard dispatcher so we can filter some events
194    final MouseListener[] mListeners = getMouseListeners ();
195    for (int i=0; i<mListeners.length; i++)
196      removeMouseListener (mListeners[i]);
197
198    // this will forward mouse events to the little detach buttons and
199    // all the look and feel listeners (since we want to filter some)
200    MouseInputAdapter ma = new MouseInputAdapter () {
201        Detachable last = null;
202        // Returns a Detachable only if the mouse event is within the
203        // detachable's icon
204        private Detachable getDetachable (MouseEvent e) {
205          if (last != null && last.contains (e.getX(), e.getY()))
206            return last;
207
208          last = null;
209          Iterator<Detachable> iter = panelToDetMap.values ().iterator ();
210          while (iter.hasNext ()) {
211            Detachable d = iter.next();
212            if (d.contains (e.getX(), e.getY())) {
213              last = d;
214              break;
215            }
216          }
217          return last;
218        }
219        @Override
220        public void mouseMoved (MouseEvent e) {
221          Detachable old = last;
222          Detachable d = getDetachable (e);
223          if (old != d) {
224            if (old != null) {
225              old.setPressed (false);
226              old.repaint ();
227            }
228            if (d != null) {
229              d.setPressed (true);
230              d.repaint ();
231            }
232          }
233        }
234        @Override
235        public void mouseClicked (MouseEvent e) {
236          Detachable d = getDetachable (e);
237          last = null;
238          if (d != null) {
239            detach (d);
240            d.setPressed (false);
241            // filter the event from the other handlers
242            return;
243          }
244          // not 'contained' within a detachable? pass it on
245          for (int i=0; i<mListeners.length; i++)
246            mListeners[i].mouseClicked (e);
247        }
248        @Override
249        public void mouseExited (MouseEvent e) {
250          if (last != null) {
251            last.setPressed (false);
252            last.repaint ();
253          }
254          last = null;
255          // no filtering
256          for (int i=0; i<mListeners.length; i++)
257            mListeners[i].mouseExited (e);
258        }
259        @Override
260        public void mouseEntered (MouseEvent e) {
261          // no filtering
262          for (int i=0; i<mListeners.length; i++)
263            mListeners[i].mouseEntered (e);
264        }
265        @Override
266        public void mousePressed (MouseEvent e) {
267          // filter from the other handlers so it doesn't 'change tabs'
268          if (getDetachable (e) != null)
269            return;
270          // not 'contained' within a detachable? pass it on
271          for (int i=0; i<mListeners.length; i++)
272            mListeners[i].mousePressed (e);
273        }
274        @Override
275        public void mouseReleased (MouseEvent e) {
276          // no filtering
277          for (int i=0; i<mListeners.length; i++)
278            mListeners[i].mouseReleased (e);
279        }
280      };
281    addMouseListener (ma);
282    addMouseMotionListener (ma);
283  }
284
285  public void setDetachedIconImage (Image image) {
286    detachedIconImage = image;
287    Iterator<Detachable> iter = panelToDetMap.values ().iterator ();
288    while (iter.hasNext ()) {
289      Detachable d = iter.next();
290      d.getFrame ().setIconImage (detachedIconImage);
291    }
292  }
293
294  public Image getDetachedIconImage () {
295    return detachedIconImage;
296  }
297
298  /**
299   * Returns the default Detachable.
300   */
301  protected Detachable createDetachable (String title, Icon icon,
302                                         Component comp,
303                                         String tip, int index, String titleSuffix) {
304    return new Detachable (title, icon, comp, tip, index, titleSuffix);
305  }
306
307  /**
308   * Lookup the Detachable for the specified component, which must have been
309   * added as a tab. Returns null if not already added.
310   */
311  protected Detachable getDetachable(Component comp) {
312    return panelToDetMap.get(comp);
313  }
314
315  /**
316   * Return Detachables which have been added as Tabs or Detached Frames. TODO:
317   * Currently, order is not accurate.
318   */
319  protected Detachable[] getDetachables() {
320    return panelToDetMap.values().toArray(new Detachable[0]);
321  }
322  
323  /**
324   * Overridden to add our 'detach' icon. All the <code>add</code> and
325   * <code>addTab</code> methods are cover methods for <code>insertTab</code>.
326   */
327  @Override
328  public void insertTab (String title, Icon icon, Component comp,
329                         String tip, int index) {
330    // the index we get is based on the number of tabs show, not the number of
331    // components, so to remain consistent create the Detachable with an index
332    // based on the number of detachables we have
333    Detachable d = createDetachable (title, icon, comp, tip,
334                                     panelToDetMap.size(), titleSuffix);
335    d.getFrame ().setIconImage (detachedIconImage);
336    
337    shiftDetachables (true, d);
338    panelToDetMap.put (comp, d);
339
340    if (detachable && d.isDetached ())
341      detach (d);
342    else
343      attach (d);
344  }
345  
346  /**
347   * Overridden to remove the comopnent from the possible list of components
348   * this pane displays
349   */
350  @Override
351  public void remove (int index) {
352    remove(panelToDetMap.get (getComponentAt (index)));
353  }
354  @Override
355  public void remove (Component comp) {
356    Detachable detachable = panelToDetMap.get (comp);
357    if (detachable != null)
358      remove (detachable);
359    else
360      super.remove(comp);
361  }
362  private void remove (Detachable d) {
363    if (d != null) {
364      panelToDetMap.remove (d.component);
365      shiftDetachables (false, d);
366      super.remove (d.component);  // ok even if not 'attached'
367      d.dispose ();
368    }
369  }
370  /**
371   * This keeps the order of the detachables correct when adding or replacing
372   * in the tabbed.
373   */
374  private void shiftDetachables (boolean insert, Detachable cause) {
375    Iterator<Detachable> iter = panelToDetMap.values ().iterator ();
376    while (iter.hasNext ()) {
377      Detachable d = iter.next();
378      if (d.index >= cause.index)
379        d.index += (insert ? 1 : -1);
380    }
381  }
382
383  /**
384   * Bypass remove and add internal panel to the tabbedpane
385   */
386  private void detach (Detachable d) {
387    if (detachable) {
388      super.remove (d.component); // ok, even if not 'attached' yet
389      validate ();
390      d.setDetached (true);
391    }
392  }
393  
394  /**
395   * Bypass insertTab and add internal panel to the tabbedpane
396   */
397  private void attach (Detachable d) {
398    int ti;
399    for (ti=0; ti<getTabCount(); ti++) {
400      Detachable tabD = panelToDetMap.get (getComponentAt(ti));
401      if (tabD.index > d.index)
402        break;
403    }
404    d.setDetached (false);
405    super.insertTab (d.title, d.icon, d.component, d.tip, ti);
406    validate ();
407  }
408
409  /**
410   * Overridden to hide or show the detached tabs as well. State is retained,
411   * so that if you hide this TabbedPane, the detached panels will be hidden,
412   * but when you re-show this TabbedPane, the detached panels will be
413   * re-shown in their last positions.
414   */
415  @Override
416  public void setVisible (boolean show) {
417    Iterator<Detachable> iter = panelToDetMap.values ().iterator ();
418    while (iter.hasNext ()) {
419      Detachable d = iter.next();
420      if (d.isDetached ())
421        d.getFrame().setVisible (show);
422    }
423  }
424  
425  public void setDetachable (boolean detachable) {
426    if (detachable == this.detachable)
427      return;
428
429    this.detachable = detachable;
430    //TODO: finish!
431    // if (detachable) {
432    //   /*
433    //     for each tab, set icon d.icon;
434    //   */
435    // } else { // !detachable
436    //   /*
437    //     for each detachable, close if open:
438    //     for each tab, set icon d.userIcon;
439    //    */      
440    // }
441  }
442
443  /**
444   * Icon which remembers where it was drawn last so that it can be queried
445   * with 'contains' requests. Needed because JTabbedPane won't let us put a
446   * component (like a button) in the tab itself. This ability will be
447   * included in a future Java release, but for now, I've got to do it by hand
448   * with this.
449   */
450  private static class LocatedIcon implements Icon {
451    Icon icon;
452    int x, y;
453    LocatedIcon (Icon icon) {
454      this.icon = icon;
455    }
456    boolean contains (int cx, int cy) {
457      return (x <= cx) && (cx <= x+icon.getIconWidth()) &&
458        (y <= cy) && (cy <= y+icon.getIconHeight());
459    }
460    public int getIconHeight () {
461      return icon.getIconHeight();
462    }
463    public int getIconWidth () {
464      return icon.getIconWidth();
465    }
466    public void paintIcon (Component c, Graphics g, int px, int py) {
467      x = px;
468      y = py;
469      icon.paintIcon (c, g, x, y);
470    }
471  }
472
473  /**
474   * Class to maintain info for panels as they are added and removed, detached
475   * and attached from the <code>DetachableTabPane</code>.
476   */
477  public class Detachable {
478
479    protected String title = null; // remember to reattach to tabbed pane
480    protected Icon icon = null; // possibly composite icon
481    protected Icon userIcon = null; // user supplied icon
482    protected Component component = null; // component to display
483    protected String tip = null;
484    protected int index = 0;
485
486    //transient Icon cachedIcon = null;
487    
488    //DetachButton button = null; // displayed in tab for detaching
489    protected LocatedIcon button = null; // displayed in tab for detaching
490    protected JmriJFrame frame = null; // detached container
491    protected boolean detached = false; // keeps detached state if not visible
492    
493    public Detachable (String title, Icon icon,
494                       Component comp, String tip, int index, String titleSuffix) {
495
496      this.title = title;
497      this.userIcon = icon;
498      this.component = comp;
499      this.tip = tip;
500      this.index = index;
501
502      /* frame to display component when detached. */
503      this.frame = new JmriJFrame ( (title!=null?title:comp.getName())+titleSuffix);
504      frame.addWindowListener (new WindowAdapter () {
505          @Override
506          public void windowClosing (WindowEvent e) {
507            DetachableTabbedPane.this.attach (Detachable.this);
508          }
509        });
510
511      // put it in the frame to set frames initial sizing
512      frame.getContentPane ().add (component);
513      frame.validate ();
514      frame.pack ();
515      frame.getContentPane ().remove (component);  
516      // initially attached (added to tabbedPane at creation, so hide)
517      frame.setVisible (false);
518
519      button = new LocatedIcon (plainIcon);
520      // create composite if neccissary
521      if (userIcon != null)
522        this.icon = new CompositeIcon (button, userIcon);
523      else
524        this.icon = button;
525    }
526
527    public JFrame getFrame () {
528      return frame;
529    }
530
531    public Component getComponent () {
532      return component;
533    }
534
535    public String getTitle () {
536      return title;
537    }
538
539    public void setPressed (boolean pressed) {
540      if (pressed)
541        button.icon = pressedIcon;
542      else
543        button.icon = plainIcon;
544      repaint ();
545    }
546    
547    public void repaint () {
548      DetachableTabbedPane.this.
549        repaint (0, button.x, button.y,
550                 button.getIconWidth(),button.getIconHeight());
551    }
552
553    public boolean isDetached () {
554      return detached;
555    }
556    
557    public void setDetached (boolean detached) {
558      this.detached = detached;
559      
560      if (detached && /*tabbedPane*/ isVisible () && ! frame.isVisible ()) {
561        // removeTabAt doesn't even set it visible again in java 1.3, so do it
562        // by hand
563        component.setVisible (true);
564        frame.getContentPane().add (component, BorderLayout.CENTER);
565        
566        // some window managers like to reposition windows. Don't let 'em
567        Rectangle bounds = frame.getBounds();
568        
569        // don't pack again, so it remains the size the user chose before
570        frame.setVisible (true);
571        frame.setBounds (bounds);
572        frame.validate ();
573        
574      } else if (! detached && frame.isVisible()) {
575        frame.setVisible (false);
576        frame.getContentPane().removeAll ();
577      } 
578    }
579
580    public void dispose () {
581      frame.dispose ();
582    }
583
584    public boolean contains (int x, int y) {
585      return (! detached && button.contains (x, y));
586    }
587  }
588  
589  /** Testing */
590//   public static void main(String s[]) {
591//     JFrame frame = new JFrame("Annotation Editor Panel Demo");
592// 
593//     frame.addWindowListener(new WindowAdapter() {
594//         @Override
595//         public void windowClosing(WindowEvent e) {System.exit(0);}
596//       });
597// 
598//     DetachableTabbedPane aep = new DetachableTabbedPane ();
599//         
600//     // add some tabs
601//     aep.add ("One", new JLabel ("One"));
602//     aep.add ("Two", new JLabel ("Two"));
603// 
604//     frame.getContentPane().add(aep);
605//     frame.pack();
606//     frame.setVisible(true);
607//   }
608
609}