001    /*
002     * $Id: JXTaskPane.java 3344 2009-05-25 01:46:59Z kschaefe $
003     *
004     * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
005     * Santa Clara, California 95054, U.S.A. All rights reserved.
006     *
007     * This library is free software; you can redistribute it and/or
008     * modify it under the terms of the GNU Lesser General Public
009     * License as published by the Free Software Foundation; either
010     * version 2.1 of the License, or (at your option) any later version.
011     * 
012     * This library is distributed in the hope that it will be useful,
013     * but WITHOUT ANY WARRANTY; without even the implied warranty of
014     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015     * Lesser General Public License for more details.
016     * 
017     * You should have received a copy of the GNU Lesser General Public
018     * License along with this library; if not, write to the Free Software
019     * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
020     */
021    package org.jdesktop.swingx;
022    
023    import java.awt.BorderLayout;
024    import java.awt.Component;
025    import java.awt.Container;
026    import java.awt.LayoutManager;
027    import java.beans.PropertyChangeEvent;
028    import java.beans.PropertyChangeListener;
029    
030    import javax.swing.Action;
031    import javax.swing.Icon;
032    import javax.swing.JComponent;
033    import javax.swing.JPanel;
034    import javax.swing.UIManager;
035    
036    import org.jdesktop.swingx.plaf.LookAndFeelAddons;
037    import org.jdesktop.swingx.plaf.TaskPaneAddon;
038    import org.jdesktop.swingx.plaf.TaskPaneUI;
039    
040    /**
041     * <code>JXTaskPane</code> is a container for tasks and other
042     * arbitrary components.
043     * 
044     * <p>
045     * Several <code>JXTaskPane</code>s are usually grouped together within a
046     * {@link org.jdesktop.swingx.JXTaskPaneContainer}. However it is not mandatory
047     * to use a JXTaskPaneContainer as the parent for JXTaskPane. The JXTaskPane can
048     * be added to any other container. See
049     * {@link org.jdesktop.swingx.JXTaskPaneContainer} to understand the benefits of
050     * using it as the parent container.
051     * 
052     * <p>
053     * <code>JXTaskPane</code> provides control to expand and
054     * collapse the content area in order to show or hide the task list. It can have an
055     * <code>icon</code>, a <code>title</code> and can be marked as
056     * <code>special</code>. Marking a <code>JXTaskPane</code> as
057     * <code>special</code> ({@link #setSpecial(boolean)} is only a hint for
058     * the pluggable UI which will usually paint it differently (by example by
059     * using another color for the border of the pane).
060     * 
061     * <p> 
062     * When the JXTaskPane is expanded or collapsed, it will be
063     * animated with a fade effect. The animated can be disabled on a per
064     * component basis through {@link #setAnimated(boolean)}.
065     * 
066     * To disable the animation for all newly created <code>JXTaskPane</code>,
067     * use the UIManager property:
068     * <code>UIManager.put("TaskPane.animate", Boolean.FALSE);</code>.
069     * 
070     * <p>
071     * Example:
072     * <pre>
073     * <code>
074     * JXFrame frame = new JXFrame();
075     * 
076     * // a container to put all JXTaskPane together
077     * JXTaskPaneContainer taskPaneContainer = new JXTaskPaneContainer();
078     * 
079     * // create a first taskPane with common actions
080     * JXTaskPane actionPane = new JXTaskPane();
081     * actionPane.setTitle("Files and Folders");
082     * actionPane.setSpecial(true);
083     * 
084     * // actions can be added, a hyperlink will be created
085     * Action renameSelectedFile = createRenameFileAction();
086     * actionPane.add(renameSelectedFile);
087     * actionPane.add(createDeleteFileAction());
088     * 
089     * // add this taskPane to the taskPaneContainer
090     * taskPaneContainer.add(actionPane);
091     * 
092     * // create another taskPane, it will show details of the selected file
093     * JXTaskPane details = new JXTaskPane();
094     * details.setTitle("Details");
095     *  
096     * // add standard components to the details taskPane
097     * JLabel searchLabel = new JLabel("Search:");
098     * JTextField searchField = new JTextField("");
099     * details.add(searchLabel);
100     * details.add(searchField);
101     * 
102     * taskPaneContainer.add(details);
103     * 
104     * // put the action list on the left 
105     * frame.add(taskPaneContainer, BorderLayout.EAST);
106     * 
107     * // and a file browser in the middle
108     * frame.add(fileBrowser, BorderLayout.CENTER);
109     * 
110     * frame.pack();
111     * frame.setVisible(true);
112     * </code>
113     * </pre>
114     * 
115     * @see org.jdesktop.swingx.JXTaskPaneContainer
116     * @see org.jdesktop.swingx.JXCollapsiblePane
117     * @author <a href="mailto:fred@L2FProd.com">Frederic Lavigne</a>
118     * @author Karl George Schaefer
119     * 
120     * @javabean.attribute
121     *          name="isContainer"
122     *          value="Boolean.TRUE"
123     *          rtexpr="true"
124     *          
125     * @javabean.attribute
126     *          name="containerDelegate"
127     *          value="getContentPane"
128     *          
129     * @javabean.class
130     *          name="JXTaskPane"
131     *          shortDescription="JXTaskPane is a container for tasks and other arbitrary components."
132     *          stopClass="java.awt.Component"
133     * 
134     * @javabean.icons
135     *          mono16="JXTaskPane16-mono.gif"
136     *          color16="JXTaskPane16.gif"
137     *          mono32="JXTaskPane32-mono.gif"
138     *          color32="JXTaskPane32.gif"
139     */
140    public class JXTaskPane extends JPanel implements
141      JXCollapsiblePane.CollapsiblePaneContainer {
142    
143      /**
144       * JXTaskPane pluggable UI key <i>swingx/TaskPaneUI</i> 
145       */
146      public final static String uiClassID = "swingx/TaskPaneUI";
147      
148      // ensure at least the default ui is registered
149      static {
150        LookAndFeelAddons.contribute(new TaskPaneAddon());
151      }
152    
153      /**
154       * Used when generating PropertyChangeEvents for the "scrollOnExpand" property
155       */
156      public static final String SCROLL_ON_EXPAND_CHANGED_KEY = "scrollOnExpand";
157    
158      /**
159       * Used when generating PropertyChangeEvents for the "title" property
160       */
161      public static final String TITLE_CHANGED_KEY = "title";
162    
163      /**
164       * Used when generating PropertyChangeEvents for the "icon" property
165       */
166      public static final String ICON_CHANGED_KEY = "icon";
167    
168      /**
169       * Used when generating PropertyChangeEvents for the "special" property
170       */
171      public static final String SPECIAL_CHANGED_KEY = "special";
172    
173      /**
174       * Used when generating PropertyChangeEvents for the "animated" property
175       */
176      public static final String ANIMATED_CHANGED_KEY = "animated";
177    
178      private String title;
179      private Icon icon;
180      private boolean special;
181      private boolean collapsed;
182      private boolean scrollOnExpand;
183    
184      private int        mnemonic;
185      private int        mnemonicIndex           = -1;
186      
187      private JXCollapsiblePane collapsePane;
188      
189      /**
190       * Creates a new empty <code>JXTaskPane</code>.
191       */
192      public JXTaskPane() {
193        collapsePane = new JXCollapsiblePane();
194        super.setLayout(new BorderLayout(0, 0));
195        super.addImpl(collapsePane, BorderLayout.CENTER, -1);
196        
197        updateUI();
198        setFocusable(true);
199    
200        // disable animation if specified in UIManager
201        setAnimated(!Boolean.FALSE.equals(UIManager.get("TaskPane.animate")));
202        
203        // listen for animation events and forward them to registered listeners
204        collapsePane.addPropertyChangeListener(
205          JXCollapsiblePane.ANIMATION_STATE_KEY, new PropertyChangeListener() {
206            public void propertyChange(PropertyChangeEvent evt) {
207              JXTaskPane.this.firePropertyChange(evt.getPropertyName(), evt
208                .getOldValue(), evt.getNewValue());
209            }
210          });
211      }
212    
213      /**
214       * Returns the contentPane object for this JXTaskPane.
215       * @return the contentPane property
216       */
217      public Container getContentPane() {
218        return collapsePane.getContentPane();
219      }
220      
221      /**
222       * Notification from the <code>UIManager</code> that the L&F has changed.
223       * Replaces the current UI object with the latest version from the <code>UIManager</code>.
224       * 
225       * @see javax.swing.JComponent#updateUI
226       */
227      @Override
228      public void updateUI() {
229        // collapsePane is null when updateUI() is called by the "super()"
230        // constructor
231        if (collapsePane == null) {
232          return;
233        }
234        setUI((TaskPaneUI)LookAndFeelAddons.getUI(this, TaskPaneUI.class));
235      }
236      
237      /**
238       * Sets the L&F object that renders this component.
239       * 
240       * @param ui the <code>TaskPaneUI</code> L&F object
241       * @see javax.swing.UIDefaults#getUI
242       * 
243       * @beaninfo bound: true hidden: true description: The UI object that
244       * implements the taskpane group's LookAndFeel.
245       */
246      public void setUI(TaskPaneUI ui) {
247        super.setUI(ui);
248      }
249    
250      /**
251       * Returns the name of the L&F class that renders this component.
252       * 
253       * @return the string {@link #uiClassID}
254       * @see javax.swing.JComponent#getUIClassID
255       * @see javax.swing.UIDefaults#getUI
256       */
257      @Override
258      public String getUIClassID() {
259        return uiClassID;
260      }
261    
262      /**
263       * Returns the title currently displayed in the border of this pane.
264       * 
265       * @return the title currently displayed in the border of this pane
266       */
267      public String getTitle() {
268        return title;
269      }
270    
271      /**
272       * Sets the title to be displayed in the border of this pane.
273       * 
274       * @param title the title to be displayed in the border of this pane
275       * @javabean.property
276       *          bound="true"
277       *          preferred="true"
278       */
279      public void setTitle(String title) {
280        String old = this.title;
281        this.title = title;
282        firePropertyChange(TITLE_CHANGED_KEY, old, title);
283      }
284    
285      /**
286       * Returns the icon currently displayed in the border of this pane.
287       * 
288       * @return the icon currently displayed in the border of this pane
289       */
290      public Icon getIcon() {
291        return icon;
292      }
293    
294      /**
295       * Sets the icon to be displayed in the border of this pane. Some pluggable
296       * UIs may impose size constraints for the icon. A size of 16x16 pixels is
297       * the recommended icon size.
298       * 
299       * @param icon the icon to be displayed in the border of this pane
300       * @javabean.property
301       *          bound="true"
302       *          preferred="true"
303       */
304      public void setIcon(Icon icon) {
305        Icon old = this.icon;
306        this.icon = icon;
307        firePropertyChange(ICON_CHANGED_KEY, old, icon);
308      }
309    
310      /**
311       * Returns true if this pane is "special".
312       * 
313       * @return true if this pane is "special"
314       * @see #setSpecial(boolean)
315       */
316      public boolean isSpecial() {
317        return special;
318      }
319    
320      /**
321       * Sets this pane to be "special" or not. Marking a <code>JXTaskPane</code>
322       * as <code>special</code> is only a hint for the pluggable UI which will
323       * usually paint it differently (by example by using another color for the
324       * border of the pane).
325       * 
326       * <p>
327       * Usually the first JXTaskPane in a JXTaskPaneContainer is marked as special
328       * because it contains the default set of actions which can be executed given
329       * the current context.
330       * 
331       * @param special
332       *          true if this pane is "special", false otherwise
333       * @javabean.property bound="true" preferred="true"
334       */
335      public void setSpecial(boolean special) {
336          boolean oldValue = isSpecial();
337          this.special = special;
338          firePropertyChange(SPECIAL_CHANGED_KEY, oldValue, isSpecial());
339      }
340    
341      /**
342       * Should this group be scrolled to be visible on expand.
343       * 
344       * @param scrollOnExpand true to scroll this group to be
345       * visible if this group is expanded.
346       * 
347       * @see #setCollapsed(boolean)
348       * 
349       * @javabean.property
350       *          bound="true"
351       *          preferred="true"
352       */
353      public void setScrollOnExpand(boolean scrollOnExpand) {
354          boolean oldValue = isScrollOnExpand();
355          this.scrollOnExpand = scrollOnExpand;
356          firePropertyChange(SCROLL_ON_EXPAND_CHANGED_KEY,
357                  oldValue, isScrollOnExpand());
358      }
359      
360      /**
361       * Should this group scroll to be visible after
362       * this group was expanded.
363       * 
364       * @return true if we should scroll false if nothing
365       * should be done.
366       */
367      public boolean isScrollOnExpand() {
368        return scrollOnExpand;
369      }
370      
371        /**
372         * Expands or collapses this group.
373         * 
374         * @param collapsed
375         *                true to collapse the group, false to expand it
376         * @javabean.property
377         *          bound="true"
378         *          preferred="false"
379         */
380        public void setCollapsed(boolean collapsed) {
381            boolean oldValue = isCollapsed();
382            this.collapsed = collapsed;
383            collapsePane.setCollapsed(collapsed);
384            firePropertyChange("collapsed", oldValue, isCollapsed());
385        }
386        
387        /**
388         * Returns the collapsed state of this task pane.
389         * 
390         * @return {@code true} if the task pane is collapsed; {@code false}
391         *         otherwise
392         */
393        public boolean isCollapsed() {
394            return collapsed;
395        }
396    
397      /**
398       * Enables or disables animation during expand/collapse transition.
399       * 
400       * @param animated
401       * @javabean.property
402       *          bound="true"
403       *          preferred="true"
404       */
405      public void setAnimated(boolean animated) {
406          boolean oldValue = isAnimated();
407          collapsePane.setAnimated(animated);
408          firePropertyChange(ANIMATED_CHANGED_KEY, oldValue, isAnimated());
409      }
410      
411      /**
412       * Returns true if this task pane is animated during expand/collapse
413       * transition.
414       * 
415       * @return true if this task pane is animated during expand/collapse
416       *         transition.
417       */
418      public boolean isAnimated() {
419        return collapsePane.isAnimated();
420      }
421    
422        /**
423         * Returns the keyboard mnemonic for the task pane.
424         * 
425         * @return the keyboard mnemonic for the task pane
426         */
427        public int getMnemonic() {
428            return mnemonic;
429        }
430    
431        /**
432         * Sets the keyboard mnemonic on the task pane. The mnemonic is the key
433         * which when combined with the look and feel's mouseless modifier (usually
434         * Alt) will toggle this task pane.
435         * <p>
436         * A mnemonic must correspond to a single key on the keyboard and should be
437         * specified using one of the <code>VK_XXX</code> keycodes defined in
438         * <code>java.awt.event.KeyEvent</code>. Mnemonics are case-insensitive,
439         * therefore a key event with the corresponding keycode would cause the
440         * button to be activated whether or not the Shift modifier was pressed.
441         * <p>
442         * If the character defined by the mnemonic is found within the task pane's 
443         * text string, the first occurrence of it will be underlined to indicate
444         * the mnemonic to the user.
445         * 
446         * @param mnemonic
447         *            the key code which represents the mnemonic
448         * @see java.awt.event.KeyEvent
449         * @see #setDisplayedMnemonicIndex
450         * 
451         * @beaninfo bound: true attribute: visualUpdate true description: the
452         *           keyboard character mnemonic
453         */
454        public void setMnemonic(int mnemonic) {
455            int oldValue = getMnemonic();
456            this.mnemonic = mnemonic;
457            
458            firePropertyChange("mnemonic", oldValue, getMnemonic());
459            
460            updateDisplayedMnemonicIndex(getTitle(), mnemonic);
461            revalidate();
462            repaint();
463        }
464    
465        /**
466         * Update the displayedMnemonicIndex property. This method
467         * is called when either text or mnemonic changes. The new
468         * value of the displayedMnemonicIndex property is the index
469         * of the first occurrence of mnemonic in text.
470         */
471        private void updateDisplayedMnemonicIndex(String text, int mnemonic) {
472            if (text == null || mnemonic == '\0') {
473                mnemonicIndex = -1;
474                
475                return;
476            }
477    
478            char uc = Character.toUpperCase((char)mnemonic);
479            char lc = Character.toLowerCase((char)mnemonic);
480    
481            int uci = text.indexOf(uc);
482            int lci = text.indexOf(lc);
483    
484            if (uci == -1) {
485                mnemonicIndex = lci;
486            } else if(lci == -1) {
487                mnemonicIndex = uci;
488            } else {
489                mnemonicIndex = (lci < uci) ? lci : uci;
490            }
491        }
492    
493        /**
494         * Returns the character, as an index, that the look and feel should
495         * provide decoration for as representing the mnemonic character.
496         *
497         * @since 1.4
498         * @return index representing mnemonic character
499         * @see #setDisplayedMnemonicIndex
500         */
501        public int getDisplayedMnemonicIndex() {
502            return mnemonicIndex;
503        }
504    
505        /**
506         * Provides a hint to the look and feel as to which character in the
507         * text should be decorated to represent the mnemonic. Not all look and
508         * feels may support this. A value of -1 indicates either there is no
509         * mnemonic, the mnemonic character is not contained in the string, or
510         * the developer does not wish the mnemonic to be displayed.
511         * <p>
512         * The value of this is updated as the properties relating to the
513         * mnemonic change (such as the mnemonic itself, the text...).
514         * You should only ever have to call this if
515         * you do not wish the default character to be underlined. For example, if
516         * the text was 'Save As', with a mnemonic of 'a', and you wanted the 'A'
517         * to be decorated, as 'Save <u>A</u>s', you would have to invoke
518         * <code>setDisplayedMnemonicIndex(5)</code> after invoking
519         * <code>setMnemonic(KeyEvent.VK_A)</code>.
520         *
521         * @since 1.4
522         * @param index Index into the String to underline
523         * @exception IllegalArgumentException will be thrown if <code>index</code>
524         *            is &gt;= length of the text, or &lt; -1
525         * @see #getDisplayedMnemonicIndex
526         *
527         * @beaninfo
528         *        bound: true
529         *    attribute: visualUpdate true
530         *  description: the index into the String to draw the keyboard character
531         *               mnemonic at
532         */
533        public void setDisplayedMnemonicIndex(int index)
534                                              throws IllegalArgumentException {
535            int oldValue = mnemonicIndex;
536            if (index == -1) {
537                mnemonicIndex = -1;
538            } else {
539                String text = getTitle();
540                int textLength = (text == null) ? 0 : text.length();
541                if (index < -1 || index >= textLength) {  // index out of range
542                    throw new IllegalArgumentException("index == " + index);
543                }
544            }
545            mnemonicIndex = index;
546            firePropertyChange("displayedMnemonicIndex", oldValue, index);
547            if (index != oldValue) {
548                revalidate();
549                repaint();
550            }
551        }
552      
553      /**
554       * Adds an action to this <code>JXTaskPane</code>. Returns a
555       * component built from the action. The returned component has been
556       * added to the <code>JXTaskPane</code>.
557       * 
558       * @param action
559       * @return a component built from the action
560       */
561      public Component add(Action action) {
562        Component c = ((TaskPaneUI)ui).createAction(action);
563        add(c);
564        return c;
565      }
566    
567      /**
568       * @see JXCollapsiblePane.CollapsiblePaneContainer
569       */
570      public Container getValidatingContainer() {
571        return getParent();
572      }
573      
574      /**
575       * Overridden to redirect call to the content pane.
576       */
577      @Override
578      protected void addImpl(Component comp, Object constraints, int index) {
579        getContentPane().add(comp, constraints, index);
580        //Fixes SwingX #364; adding to internal component we need to revalidate ourself
581        revalidate();
582      }
583    
584      /**
585       * Overridden to redirect call to the content pane.
586       */
587      @Override
588      public void setLayout(LayoutManager mgr) {
589        if (collapsePane != null) {
590          getContentPane().setLayout(mgr);
591        }
592      }
593      
594      /**
595       * Overridden to redirect call to the content pane
596       */
597      @Override
598      public void remove(Component comp) {
599        getContentPane().remove(comp);
600      }
601    
602      /**
603       * Overridden to redirect call to the content pane.
604       */
605      @Override
606      public void remove(int index) {
607        getContentPane().remove(index);
608      }
609      
610      /**
611       * Overridden to redirect call to the content pane.
612       */
613      @Override
614      public void removeAll() {
615        getContentPane().removeAll();
616      }
617      
618      /**
619       * @see JComponent#paramString()
620       */
621      @Override
622      protected String paramString() {
623        return super.paramString()
624          + ",title="
625          + getTitle()
626          + ",icon="
627          + getIcon()
628          + ",collapsed="
629          + String.valueOf(isCollapsed())
630          + ",special="
631          + String.valueOf(isSpecial())
632          + ",scrollOnExpand=" 
633          + String.valueOf(isScrollOnExpand())
634          + ",ui=" + getUI();
635      }
636    
637    }