001
002/* ----------------------------------------------------------------------
003 * 
004 * Copyright (c) 2002-2009 The MITRE Corporation
005 * 
006 * Except as permitted below
007 * ALL RIGHTS RESERVED
008 * 
009 * The MITRE Corporation (MITRE) provides this software to you without
010 * charge to use for your internal purposes only. Any copy you make for
011 * such purposes is authorized provided you reproduce MITRE's copyright
012 * designation and this License in any such copy. You may not give or
013 * sell this software to any other party without the prior written
014 * permission of the MITRE Corporation.
015 * 
016 * The government of the United States of America may make unrestricted
017 * use of this software.
018 * 
019 * This software is the copyright work of MITRE. No ownership or other
020 * proprietary interest in this software is granted you other than what
021 * is granted in this license.
022 * 
023 * Any modification or enhancement of this software must inherit this
024 * license, including its warranty disclaimers. You hereby agree to
025 * provide to MITRE, at no charge, a copy of any such modification or
026 * enhancement without limitation.
027 * 
028 * MITRE IS PROVIDING THE PRODUCT "AS IS" AND MAKES NO WARRANTY, EXPRESS
029 * OR IMPLIED, AS TO THE ACCURACY, CAPABILITY, EFFICIENCY,
030 * MERCHANTABILITY, OR FUNCTIONING OF THIS SOFTWARE AND DOCUMENTATION. IN
031 * NO EVENT WILL MITRE BE LIABLE FOR ANY GENERAL, CONSEQUENTIAL,
032 * INDIRECT, INCIDENTAL, EXEMPLARY OR SPECIAL DAMAGES, EVEN IF MITRE HAS
033 * BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
034 * 
035 * You accept this software on the condition that you indemnify and hold
036 * harmless MITRE, its Board of Trustees, officers, agents, and
037 * employees, from any and all liability or damages to third parties,
038 * including attorneys' fees, court costs, and other related costs and
039 * expenses, arising out of your use of this software irrespective of the
040 * cause of said liability.
041 * 
042 * The export from the United States or the subsequent reexport of this
043 * software is subject to compliance with United States export control
044 * and munitions control restrictions. You agree that in the event you
045 * seek to export this software you assume full responsibility for
046 * obtaining all necessary export licenses and approvals and for assuring
047 * compliance with applicable reexport restrictions.
048 * 
049 * ----------------------------------------------------------------------
050 * 
051 * NOTICE
052 * 
053 * This software was produced for the U. S. Government
054 * under Contract No. W15P7T-09-C-F600, and is
055 * subject to the Rights in Noncommercial Computer Software
056 * and Noncommercial Computer Software Documentation
057 * Clause 252.227-7014 (JUN 1995).
058 * 
059 * (c) 2009 The MITRE Corporation. All Rights Reserved.
060 * 
061 * ----------------------------------------------------------------------
062 *
063 */
064/*
065 * Copyright (c) 2002-2006 The MITRE Corporation
066 * 
067 * Except as permitted below
068 * ALL RIGHTS RESERVED
069 * 
070 * The MITRE Corporation (MITRE) provides this software to you without
071 * charge to use for your internal purposes only. Any copy you make for
072 * such purposes is authorized provided you reproduce MITRE's copyright
073 * designation and this License in any such copy. You may not give or
074 * sell this software to any other party without the prior written
075 * permission of the MITRE Corporation.
076 * 
077 * The government of the United States of America may make unrestricted
078 * use of this software.
079 * 
080 * This software is the copyright work of MITRE. No ownership or other
081 * proprietary interest in this software is granted you other than what
082 * is granted in this license.
083 * 
084 * Any modification or enhancement of this software must inherit this
085 * license, including its warranty disclaimers. You hereby agree to
086 * provide to MITRE, at no charge, a copy of any such modification or
087 * enhancement without limitation.
088 * 
089 * MITRE IS PROVIDING THE PRODUCT "AS IS" AND MAKES NO WARRANTY, EXPRESS
090 * OR IMPLIED, AS TO THE ACCURACY, CAPABILITY, EFFICIENCY,
091 * MERCHANTABILITY, OR FUNCTIONING OF THIS SOFTWARE AND DOCUMENTATION. IN
092 * NO EVENT WILL MITRE BE LIABLE FOR ANY GENERAL, CONSEQUENTIAL,
093 * INDIRECT, INCIDENTAL, EXEMPLARY OR SPECIAL DAMAGES, EVEN IF MITRE HAS
094 * BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
095 * 
096 * You accept this software on the condition that you indemnify and hold
097 * harmless MITRE, its Board of Trustees, officers, agents, and
098 * employees, from any and all liability or damages to third parties,
099 * including attorneys' fees, court costs, and other related costs and
100 * expenses, arising out of your use of this software irrespective of the
101 * cause of said liability.
102 * 
103 * The export from the United States or the subsequent reexport of this
104 * software is subject to compliance with United States export control
105 * and munitions control restrictions. You agree that in the event you
106 * seek to export this software you assume full responsibility for
107 * obtaining all necessary export licenses and approvals and for assuring
108 * compliance with applicable reexport restrictions.
109 */
110
111package jmri.util.org.mitre.jawb.swing;
112
113import java.awt.*;
114import java.awt.event.*;
115import java.util.HashMap;
116import java.util.Iterator;
117
118import javax.swing.*;
119import javax.swing.event.MouseInputAdapter;
120
121// added as part of migration to JMRI
122import jmri.util.JmriJFrame;
123
124/**
125 * JTabbedPane implementation which allows tabbs to be 'torn off' as their own
126 * window. When the DetachableTabbedPane is set not visible using the
127 * 'setVisible' method, any detached tabs are also hidden. When set visible by
128 * the same means, previously detached, yet hidden tabs, are re-shown.
129 *
130 * @author <a href="mailto:red@mitre.org">Chadwick A. McHenry</a>
131 * @version 1.0
132 */
133public class DetachableTabbedPane extends JTabbedPane {
134
135  /* multiple use Icons */
136  private static Icon plainIcon = new DetachPanelIcon (false);
137  private static Icon pressedIcon = new DetachPanelIcon (true);
138  /* map panels to their Detachable objects
139   * @see #Detachable */
140  protected HashMap<Component, Detachable> panelToDetMap = new HashMap<Component, Detachable>();
141
142  /**
143   * Indicates whether the tabs in this TabbedPane are actually detachable,
144   * or just behave normally
145   */
146  protected boolean detachable = true;
147
148  /** Prettify the detached tabbs */
149  protected Image detachedIconImage = null;
150  
151  String titleSuffix = ": Foon";
152  
153  /**
154   * Creates an empty <code>DetachableTabbedPane</code> with a default tab
155   * placement of <code>JTabbedPane.TOP</code> and detachability on.
156   */
157  public DetachableTabbedPane () {
158    super ();
159    init ();
160  }
161
162  public DetachableTabbedPane (String titleSuffix) {
163    super ();
164    init ();
165    this.titleSuffix = titleSuffix;
166  }
167  
168  /**
169   * Creates an empty <code>DetachableTabbedPane</code> with the specified tab
170   * placement of either: <code>JTabbedPane.TOP</code>,
171   * <code>JTabbedPane.BOTTOM</code>, <code>JTabbedPane.LEFT</code>, or
172   * <code>JTabbedPane.RIGHT</code>, and specified detachability.
173 * @param tabPlacement tab placement
174 * @param detachable true if detachable
175   */
176  public DetachableTabbedPane (int tabPlacement, boolean detachable) {
177    super (tabPlacement);
178    this.detachable = detachable;
179    init ();
180  }
181  
182  /**
183   * Creates an empty <code>DetachableTabbedPane</code> with the specified tab
184   * placement and tab layout policy.
185 * @param tabPlacement tab placement
186 * @param tabLayoutPolicy tab layout policy
187 * @param detachable true if detachable
188   */
189  public DetachableTabbedPane (int tabPlacement, int tabLayoutPolicy,
190                               boolean detachable) {
191    super (tabPlacement, tabLayoutPolicy);
192    this.detachable = detachable;
193    init ();
194  }
195
196  /** Code common to all constructors. */
197  private void init () {
198    // retrieve the current mouse listeners (put in by L&F) and remove them
199    // from the standard dispatcher so we can filter some events
200    final MouseListener[] mListeners = getMouseListeners ();
201    for (int i=0; i<mListeners.length; i++)
202      removeMouseListener (mListeners[i]);
203
204    // this will forward mouse events to the little detach buttons and
205    // all the look and feel listeners (since we want to filter some)
206    MouseInputAdapter ma = new MouseInputAdapter () {
207        Detachable last = null;
208        // Returns a Detachable only if the mouse event is within the
209        // detachable's icon
210        private Detachable getDetachable (MouseEvent e) {
211          if (last != null && last.contains (e.getX(), e.getY()))
212            return last;
213
214          last = null;
215          Iterator<Detachable> iter = panelToDetMap.values ().iterator ();
216          while (iter.hasNext ()) {
217            Detachable d = iter.next();
218            if (d.contains (e.getX(), e.getY())) {
219              last = d;
220              break;
221            }
222          }
223          return last;
224        }
225        @Override
226        public void mouseMoved (MouseEvent e) {
227          Detachable old = last;
228          Detachable d = getDetachable (e);
229          if (old != d) {
230            if (old != null) {
231              old.setPressed (false);
232              old.repaint ();
233            }
234            if (d != null) {
235              d.setPressed (true);
236              d.repaint ();
237            }
238          }
239        }
240        @Override
241        public void mouseClicked (MouseEvent e) {
242          Detachable d = getDetachable (e);
243          last = null;
244          if (d != null) {
245            detach (d);
246            d.setPressed (false);
247            // filter the event from the other handlers
248            return;
249          }
250          // not 'contained' within a detachable? pass it on
251          for (int i=0; i<mListeners.length; i++)
252            mListeners[i].mouseClicked (e);
253        }
254        @Override
255        public void mouseExited (MouseEvent e) {
256          if (last != null) {
257            last.setPressed (false);
258            last.repaint ();
259          }
260          last = null;
261          // no filtering
262          for (int i=0; i<mListeners.length; i++)
263            mListeners[i].mouseExited (e);
264        }
265        @Override
266        public void mouseEntered (MouseEvent e) {
267          // no filtering
268          for (int i=0; i<mListeners.length; i++)
269            mListeners[i].mouseEntered (e);
270        }
271        @Override
272        public void mousePressed (MouseEvent e) {
273          // filter from the other handlers so it doesn't 'change tabs'
274          if (getDetachable (e) != null)
275            return;
276          // not 'contained' within a detachable? pass it on
277          for (int i=0; i<mListeners.length; i++)
278            mListeners[i].mousePressed (e);
279        }
280        @Override
281        public void mouseReleased (MouseEvent e) {
282          // no filtering
283          for (int i=0; i<mListeners.length; i++)
284            mListeners[i].mouseReleased (e);
285        }
286      };
287    addMouseListener (ma);
288    addMouseMotionListener (ma);
289  }
290
291  public void setDetachedIconImage (Image image) {
292    detachedIconImage = image;
293    Iterator<Detachable> iter = panelToDetMap.values ().iterator ();
294    while (iter.hasNext ()) {
295      Detachable d = iter.next();
296      d.getFrame ().setIconImage (detachedIconImage);
297    }
298  }
299
300  public Image getDetachedIconImage () {
301    return detachedIconImage;
302  }
303
304  /**
305   * Returns the default Detachable.
306 * @param title title
307 * @param icon icon
308 * @param comp component
309 * @param tip tool tip
310 * @param index index
311 * @param titleSuffix title suffix
312 * @return default Detachable
313   */
314  protected Detachable createDetachable (String title, Icon icon,
315                                         Component comp,
316                                         String tip, int index, String titleSuffix) {
317    return new Detachable (title, icon, comp, tip, index, titleSuffix);
318  }
319
320  /**
321   * Lookup the Detachable for the specified component, which must have been
322   * added as a tab. Returns null if not already added.
323 * @param comp component
324 * @return Returns null if not already added
325   */
326  protected Detachable getDetachable(Component comp) {
327    return panelToDetMap.get(comp);
328  }
329
330  /**
331   * Return Detachables which have been added as Tabs or Detached Frames. TODO:
332   * Currently, order is not accurate.
333 * @return Detachables
334   */
335  protected Detachable[] getDetachables() {
336    return panelToDetMap.values().toArray(new Detachable[0]);
337  }
338  
339  /**
340   * Overridden to add our 'detach' icon. All the <code>add</code> and
341   * <code>addTab</code> methods are cover methods for <code>insertTab</code>.
342   */
343  @Override
344  public void insertTab (String title, Icon icon, Component comp,
345                         String tip, int index) {
346    log.debug("insertTab {} at index {}", title, index);
347    // the index we get is based on the number of tabs show, not the number of
348    // components, so to remain consistent create the Detachable with an index
349    // based on the number of detachables we have
350    Detachable d = createDetachable (title, icon, comp, tip,
351                                     panelToDetMap.size(), titleSuffix);
352    d.getFrame ().setIconImage (detachedIconImage);
353    
354    shiftDetachables (true, d);
355    panelToDetMap.put (comp, d);
356
357    if (detachable && d.isDetached ())
358      detach (d);
359    else
360      attach (d);
361  }
362  
363  @Override
364  public void setComponentAt(int index, Component component) {
365    log.debug("setComponentAt name = {} index = {}", getTitleAt(index), index);
366    
367    var oldcomp = getComponentAt(index);
368    var detachable = panelToDetMap.get(oldcomp);
369    panelToDetMap.remove(oldcomp);
370    
371    panelToDetMap.put(component, detachable);
372    detachable.component = component;
373    
374    super.setComponentAt(index, component);
375  }
376
377  // just adds logging
378  @Override
379  public void addTab(String name, Component component) {
380    log.debug("addTab name = {} count = {}", name, getTabCount());
381    super.addTab(name, component);
382  }
383  
384  /**
385   * Overridden to remove the comopnent from the possible list of components
386   * this pane displays
387   */
388  @Override
389  public void remove (int index) {
390    remove(panelToDetMap.get (getComponentAt (index)));
391  }
392  @Override
393  public void remove (Component comp) {
394    Detachable detachable = panelToDetMap.get (comp);
395    if (detachable != null) {
396      log.trace("panelToDetMap of {} found {} with {}", comp, detachable, detachable.component);
397      remove (detachable);
398    } else {
399      super.remove(comp);
400    }
401  }
402  private void remove (Detachable d) {
403    if (d != null) {
404      panelToDetMap.remove (d.component);
405      shiftDetachables (false, d);
406      super.remove (d.component);  // ok even if not 'attached'
407      d.dispose ();
408    }
409  }
410  /**
411   * This keeps the order of the detachables correct when adding or replacing
412   * in the tabbed.
413   */
414  private void shiftDetachables (boolean insert, Detachable cause) {
415    Iterator<Detachable> iter = panelToDetMap.values ().iterator ();
416    while (iter.hasNext ()) {
417      Detachable d = iter.next();
418      if (d.index >= cause.index)
419        d.index += (insert ? 1 : -1);
420    }
421  }
422
423  /**
424   * Bypass remove and add internal panel to the tabbedpane
425   */
426  private void detach (Detachable d) {
427    if (detachable) {
428      super.remove (d.component); // ok, even if not 'attached' yet
429      validate ();
430      d.setDetached (true);
431    }
432  }
433    
434  /**
435   * Bypass insertTab and add internal panel to the tabbedpane
436   */
437  private void attach (Detachable d) {
438    int ti;
439    for (ti=0; ti<getTabCount(); ti++) {
440      Detachable tabD = panelToDetMap.get (getComponentAt(ti));
441      if (tabD != null && tabD.index > d.index)
442        break;
443    }
444    d.setDetached (false);
445    super.insertTab (d.title, d.icon, d.component, d.tip, ti);
446    validate ();
447  }
448
449  /**
450   * Overridden to hide or show the detached tabs as well. State is retained,
451   * so that if you hide this TabbedPane, the detached panels will be hidden,
452   * but when you re-show this TabbedPane, the detached panels will be
453   * re-shown in their last positions.
454   */
455  @Override
456  public void setVisible (boolean show) {
457    Iterator<Detachable> iter = panelToDetMap.values ().iterator ();
458    while (iter.hasNext ()) {
459      Detachable d = iter.next();
460      if (d.isDetached ())
461        d.getFrame().setVisible (show);
462    }
463  }
464  
465  public void setDetachable (boolean detachable) {
466    if (detachable == this.detachable)
467      return;
468
469    this.detachable = detachable;
470    //TODO: finish!
471    // if (detachable) {
472    //   /*
473    //     for each tab, set icon d.icon;
474    //   */
475    // } else { // !detachable
476    //   /*
477    //     for each detachable, close if open:
478    //     for each tab, set icon d.userIcon;
479    //    */      
480    // }
481  }
482
483  /**
484   * Icon which remembers where it was drawn last so that it can be queried
485   * with 'contains' requests. Needed because JTabbedPane won't let us put a
486   * component (like a button) in the tab itself. This ability will be
487   * included in a future Java release, but for now, I've got to do it by hand
488   * with this.
489   */
490  private static class LocatedIcon implements Icon {
491    Icon icon;
492    int x, y;
493    LocatedIcon (Icon icon) {
494      this.icon = icon;
495    }
496    boolean contains (int cx, int cy) {
497      return (x <= cx) && (cx <= x+icon.getIconWidth()) &&
498        (y <= cy) && (cy <= y+icon.getIconHeight());
499    }
500    @Override
501    public int getIconHeight () {
502      return icon.getIconHeight();
503    }
504    @Override
505    public int getIconWidth () {
506      return icon.getIconWidth();
507    }
508    @Override
509    public void paintIcon (Component c, Graphics g, int px, int py) {
510      x = px;
511      y = py;
512      icon.paintIcon (c, g, x, y);
513    }
514  }
515
516  /**
517   * Class to maintain info for panels as they are added and removed, detached
518   * and attached from the <code>DetachableTabPane</code>.
519   */
520  public class Detachable {
521
522    protected String title = null; // remember to reattach to tabbed pane
523    protected Icon icon = null; // possibly composite icon
524    protected Icon userIcon = null; // user supplied icon
525    protected Component component = null; // component to display
526    protected String tip = null;
527    protected int index = 0;
528
529    //transient Icon cachedIcon = null;
530    
531    //DetachButton button = null; // displayed in tab for detaching
532    protected LocatedIcon button = null; // displayed in tab for detaching
533    protected JmriJFrame frame = null; // detached container
534    protected boolean detached = false; // keeps detached state if not visible
535    
536    public Detachable (String title, Icon icon,
537                       Component comp, String tip, int index, String titleSuffix) {
538
539      this.title = title;
540      this.userIcon = icon;
541      this.component = comp;
542      this.tip = tip;
543      this.index = index;
544
545      /* frame to display component when detached. */
546      frame = new JmriJFrame ( (title!=null?title:comp.getName())+titleSuffix);
547      frame.makePrivateWindow();
548      frame.addHelpMenu(null,true);
549      frame.addWindowListener (new WindowAdapter () {
550          @Override
551          public void windowClosing (WindowEvent e) {
552            DetachableTabbedPane.this.attach (Detachable.this);
553          }
554        });
555
556      // put it in the frame to set frames initial sizing
557      frame.getContentPane ().add (component);
558      frame.validate ();
559      frame.pack ();
560      frame.getContentPane ().remove (component);  
561      // initially attached (added to tabbedPane at creation, so hide)
562      frame.setVisible (false);
563
564      button = new LocatedIcon (plainIcon);
565      // create composite if neccissary
566      if (userIcon != null)
567        this.icon = new CompositeIcon (button, userIcon);
568      else
569        this.icon = button;
570    }
571
572    public JFrame getFrame () {
573      return frame;
574    }
575
576    public Component getComponent () {
577      return component;
578    }
579
580    public String getTitle () {
581      return title;
582    }
583
584    public void setPressed (boolean pressed) {
585      if (pressed)
586        button.icon = pressedIcon;
587      else
588        button.icon = plainIcon;
589      repaint ();
590    }
591    
592    public void repaint () {
593      DetachableTabbedPane.this.
594        repaint (0, button.x, button.y,
595                 button.getIconWidth(),button.getIconHeight());
596    }
597
598    public boolean isDetached () {
599      return detached;
600    }
601    
602    public void setDetached (boolean detached) {
603      this.detached = detached;
604      
605      if (detached && /*tabbedPane*/ isVisible () && ! frame.isVisible ()) {
606        // removeTabAt doesn't even set it visible again in java 1.3, so do it
607        // by hand
608        component.setVisible (true);
609        frame.getContentPane().add (component, BorderLayout.CENTER);
610        
611        frame.makePublicWindow();
612        
613        // some window managers like to reposition windows. Don't let 'em
614        Rectangle bounds = frame.getBounds();
615        
616        // don't pack again, so it remains the size the user chose before
617        frame.setVisible (true);
618        frame.setBounds (bounds);
619        frame.validate ();
620        
621      } else if (! detached && frame.isVisible()) {
622        frame.setVisible (false);
623        frame.getContentPane().removeAll ();
624        frame.makePrivateWindow();
625      } 
626    }
627
628    public void dispose () {
629      frame.dispose ();
630    }
631
632    public boolean contains (int x, int y) {
633      return (! detached && button.contains (x, y));
634    }
635  }
636  
637  /** Testing */
638//   public static void main(String s[]) {
639//     JFrame frame = new JFrame("Annotation Editor Panel Demo");
640// 
641//     frame.addWindowListener(new WindowAdapter() {
642//         @Override
643//         public void windowClosing(WindowEvent e) {System.exit(0);}
644//       });
645// 
646//     DetachableTabbedPane aep = new DetachableTabbedPane ();
647//         
648//     // add some tabs
649//     aep.add ("One", new JLabel ("One"));
650//     aep.add ("Two", new JLabel ("Two"));
651// 
652//     frame.getContentPane().add(aep);
653//     frame.pack();
654//     frame.setVisible(true);
655//   }
656
657    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DetachableTabbedPane.class);
658
659}