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