View Javadoc

1   /*
2    * Copyright 2006-2016 The JGUIraffe Team.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License")
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package net.sf.jguiraffe.gui.platform.swing.builder.components;
17  
18  import java.util.ArrayList;
19  import java.util.List;
20  
21  import javax.swing.JScrollPane;
22  import javax.swing.JTree;
23  import javax.swing.event.EventListenerList;
24  import javax.swing.event.TreeExpansionEvent;
25  import javax.swing.event.TreeSelectionEvent;
26  import javax.swing.event.TreeSelectionListener;
27  import javax.swing.event.TreeWillExpandListener;
28  import javax.swing.tree.ExpandVetoException;
29  import javax.swing.tree.TreePath;
30  import javax.swing.tree.TreeSelectionModel;
31  
32  import net.sf.jguiraffe.gui.builder.components.model.TreeExpandVetoException;
33  import net.sf.jguiraffe.gui.builder.components.model.TreeExpansionListener;
34  import net.sf.jguiraffe.gui.builder.components.model.TreeHandler;
35  import net.sf.jguiraffe.gui.builder.components.model.TreeNodePath;
36  import net.sf.jguiraffe.gui.builder.components.model.TreePreExpansionListener;
37  
38  import org.apache.commons.configuration.HierarchicalConfiguration;
39  import org.apache.commons.configuration.tree.ConfigurationNode;
40  
41  /**
42   * <p>
43   * The Swing-specific implementation of a component handler for a tree.
44   * </p>
45   * <p>
46   * This class wraps a <code>javax.swing.JTree</code> component and implements
47   * the methods required for Swing component handlers in a suitable way. The
48   * following functionality is provided:
49   * <ul>
50   * <li>The data handling depends on the tree's selection model. If the tree
51   * supports single selection only, the handler's data type is
52   * {@link TreeNodePath} (i.e. a generic way of describing a single
53   * node in the tree). Otherwise the type is an array of
54   * {@link TreeNodePath} storing the paths to all selected nodes.
55   * The <code>getData()</code> and <code>setData()</code> methods work
56   * correspondingly.</li>
57   * <li>Methods are available for directly querying and manipulating the tree's
58   * selection. These methods are defined by the platform-neutral
59   * {@link TreeHandler} interface.</li>
60   * <li>The data model of the tree can be queried in form of a
61   * <code>HierarchicalConfiguration</code> object. Through this object the tree's
62   * data can be directly read or manipulated.</li>
63   * <li>Support for different kinds of event listeners. Platform-neutral change
64   * listeners can register at handlers of this type. They will then be notified
65   * whenever the tree's selection changes. Listeners can also be registered for
66   * node expansion or collapse events.</li>
67   * <li>A scroll pane for the table is automatically created and maintained.</li>
68   * </ul>
69   * </p>
70   *
71   * @author Oliver Heger
72   * @version $Id: SwingTreeComponentHandler.java 205 2012-01-29 18:29:57Z oheger $
73   */
74  class SwingTreeComponentHandler extends SwingComponentHandler<Object> implements
75          TreeHandler, TreeSelectionListener,
76          javax.swing.event.TreeExpansionListener, TreeWillExpandListener
77  {
78      /** Stores the model of the tree. */
79      private final SwingConfigurationTreeModel model;
80  
81      /** A list for managing event listeners. */
82      private final EventListenerList listenerList;
83  
84      /** Stores the scroll pane, which is the outer component. */
85      private final JScrollPane scrollPane;
86  
87      /** Stores the name of this component. */
88      private final String name;
89  
90      /**
91       * Creates a new instance of <code>SwingTreeComponentHandler</code> and
92       * initializes it.
93       *
94       * @param tree the tree component wrapped by this handler
95       * @param model the model for the tree
96       * @param name the name of this component
97       * @param scrollWidth the preferred scroll width (&lt;= 0 for undefined)
98       * @param scrollHeight the preferred scroll height (&lt;= 0 for undefined)
99       */
100     public SwingTreeComponentHandler(JTree tree,
101             SwingConfigurationTreeModel model, String name, int scrollWidth,
102             int scrollHeight)
103     {
104         super(tree);
105         this.model = model;
106         this.name = name;
107 
108         scrollPane = SwingComponentUtils.scrollPaneFor(tree, scrollWidth,
109                 scrollHeight);
110         listenerList = new EventListenerList();
111         tree.addTreeExpansionListener(this);
112         tree.addTreeWillExpandListener(this);
113     }
114 
115     /**
116      * Returns the tree wrapped by this handler.
117      *
118      * @return the underlying tree
119      */
120     public JTree getTree()
121     {
122         return (JTree) getComponent();
123     }
124 
125     /**
126      * Adds an expansion listener to this component. This listener will be
127      * notified whenever the expansion state of a node changes.
128      *
129      * @param l the listener to add (must not be <b>null</b>)
130      * @throws IllegalArgumentException if the listener is <b>null</b>
131      */
132     public void addExpansionListener(TreeExpansionListener l)
133     {
134         if (l == null)
135         {
136             throw new IllegalArgumentException(
137                     "Event listener must not be null!");
138         }
139         listenerList.add(TreeExpansionListener.class, l);
140     }
141 
142     /**
143      * Adds a pre-expansion listener to this component. This listener will be
144      * notified whenever a node's expansion state is about to change. It can
145      * even veto against this change.
146      *
147      * @param l the listener to add (must not be <b>null</b>)
148      * @throws IllegalArgumentException if the listener is <b>null</b>
149      */
150     public void addPreExpansionListener(TreePreExpansionListener l)
151     {
152         if (l == null)
153         {
154             throw new IllegalArgumentException(
155                     "Event listener must not be null!");
156         }
157         listenerList.add(TreePreExpansionListener.class, l);
158     }
159 
160     /**
161      * Adds the given path to the selection of this tree.
162      *
163      * @param path the path to add (must not be <b>null</b>)
164      * @throws IllegalArgumentException if the path is <b>null</b>
165      */
166     public void addSelectedPath(TreeNodePath path)
167     {
168         if (path == null)
169         {
170             throw new IllegalArgumentException(
171                     "Path to add to selection must not be null!");
172         }
173 
174         getTree().addSelectionPath(treePathFromNodePath(path));
175     }
176 
177     /**
178      * Clears the selection of the tree.
179      */
180     public void clearSelection()
181     {
182         getTree().clearSelection();
183     }
184 
185     /**
186      * Collapses the specified path.
187      *
188      * @param path the path to collapse (must not be <b>null</b>)
189      * @throws IllegalArgumentException if the path is <b>null</b>
190      */
191     public void collapse(TreeNodePath path)
192     {
193         if (path == null)
194         {
195             throw new IllegalArgumentException(
196                     "Path to collapse must not be null!");
197         }
198 
199         getTree().collapsePath(treePathFromNodePath(path));
200     }
201 
202     /**
203      * Expands the specified path.
204      *
205      * @param path the path to expand (must not be <b>null</b>)
206      * @throws IllegalArgumentException if the path is <b>null</b>
207      */
208     public void expand(TreeNodePath path)
209     {
210         if (path == null)
211         {
212             throw new IllegalArgumentException(
213                     "Path to expand must not be null!");
214         }
215 
216         getTree().expandPath(treePathFromNodePath(path));
217     }
218 
219     /**
220      * Returns the configuration that serves as the model for the tree. This
221      * object is obtained from the Swing-specific tree model implementation.
222      *
223      * @return the configuration serving as tree model
224      */
225     public HierarchicalConfiguration getModel()
226     {
227         return model.getConfiguration();
228     }
229 
230     /**
231      * Returns the selected path. If nothing is selected, <b>null</b> will be
232      * returned.
233      *
234      * @return the selected path
235      */
236     public TreeNodePath getSelectedPath()
237     {
238         TreePath path = getTree().getSelectionPath();
239         return (path == null) ? null : nodePathFromTreePath(path);
240     }
241 
242     /**
243      * Returns an array with all selected paths. If nothing is selected, an
244      * empty array is returned.
245      *
246      * @return an array with the selected paths
247      */
248     public TreeNodePath[] getSelectedPaths()
249     {
250         TreePath[] paths = getTree().getSelectionPaths();
251         if (paths == null)
252         {
253             return new TreeNodePath[0];
254         }
255 
256         TreeNodePath[] result = new TreeNodePath[paths.length];
257         for (int i = 0; i < result.length; i++)
258         {
259             result[i] = nodePathFromTreePath(paths[i]);
260         }
261 
262         return result;
263     }
264 
265     /**
266      * Removes the specified expansion listener from this component.
267      *
268      * @param l the listener to remove
269      */
270     public void removeExpansionListener(TreeExpansionListener l)
271     {
272         listenerList.remove(TreeExpansionListener.class, l);
273     }
274 
275     /**
276      * Removes the specified pre-expansion listener from this component.
277      *
278      * @param l the listener to remove
279      */
280     public void removePreExpansionListener(TreePreExpansionListener l)
281     {
282         listenerList.remove(TreePreExpansionListener.class, l);
283     }
284 
285     /**
286      * Selects a path.
287      *
288      * @param path the path to select (must not be <b>null</b>)
289      * @throws IllegalArgumentException if the path is <b>null</b>
290      */
291     public void setSelectedPath(TreeNodePath path)
292     {
293         if (path == null)
294         {
295             throw new IllegalArgumentException(
296                     "Path to select must not be null!");
297         }
298 
299         getTree().setSelectionPath(treePathFromNodePath(path));
300     }
301 
302     /**
303      * Returns the data of this handler. This corresponds to the tree's
304      * selection. The return value depends on the tree's selection model: if
305      * multiple selection is supported, it is an array of
306      * <code>{@link TreeNodePath}</code>. For single selection it is a single
307      * <code>{@link TreeNodePath}</code> object. In either case, if nothing is
308      * selected, result is <b>null</b>.
309      *
310      * @return the data of this handler
311      */
312     public Object getData()
313     {
314         if (getTree().getSelectionCount() == 0)
315         {
316             return null;
317         }
318 
319         if (isMultiSelection())
320         {
321             TreePath[] paths = getTree().getSelectionPaths();
322             TreeNodePath[] result = new TreeNodePath[paths.length];
323             for (int i = 0; i < paths.length; i++)
324             {
325                 result[i] = nodePathFromTreePath(paths[i]);
326             }
327             return result;
328         }
329 
330         else
331         {
332             return nodePathFromTreePath(getTree().getSelectionPath());
333         }
334     }
335 
336     /**
337      * Returns the outer component. This is the scroll pane that has the tree
338      * component as view component.
339      *
340      * @return the outer component
341      */
342     @Override
343     public Object getOuterComponent()
344     {
345         return scrollPane;
346     }
347 
348     /**
349      * Returns the type of this handler. Depending on the selection mode this is
350      * either <code>TreeNodePath</code> or an array of this class.
351      *
352      * @return the type of this handler
353      */
354     public Class<?> getType()
355     {
356         return isMultiSelection() ? TreeNodePath[].class : TreeNodePath.class;
357     }
358 
359     /**
360      * Sets the data of this handler. This implementation accepts the following
361      * data objects:
362      * <ul>
363      * <li><b>null</b> will clear the tree's selection.</li>
364      * <li>A single <code>{@link TreeNodePath}</code> object will become the
365      * selected path of the tree.</li>
366      * <li>An array of <code>{@link TreeNodePath}</code> objects can be passed
367      * in; then these paths will all be selected. (For this to work the tree
368      * must support multiple selection.)</li>
369      * </ul>
370      * All other objects will cause an exception.
371      *
372      * @param data the data for the tree component
373      * @throws IllegalArgumentException if the data is invalid
374      */
375     public void setData(Object data)
376     {
377         if (data == null)
378         {
379             clearSelection();
380         }
381         else if (data instanceof TreeNodePath)
382         {
383             setSelectedPath((TreeNodePath) data);
384         }
385 
386         else if (data instanceof TreeNodePath[])
387         {
388             TreeNodePath[] nodePaths = (TreeNodePath[]) data;
389             TreePath[] treePaths = new TreePath[nodePaths.length];
390             for (int i = 0; i < nodePaths.length; i++)
391             {
392                 treePaths[i] = treePathFromNodePath(nodePaths[i]);
393             }
394             getTree().setSelectionPaths(treePaths);
395         }
396 
397         else
398         {
399             throw new IllegalArgumentException(
400                     "Invalid data for tree handler: " + data);
401         }
402     }
403 
404     /**
405      * The selection of the wrapped tree component has changed. This event is
406      * propagated to the registered change listeners.
407      *
408      * @param e the event
409      */
410     public void valueChanged(TreeSelectionEvent e)
411     {
412         fireChangeEvent(e);
413     }
414 
415     /**
416      * A node in the wrapped tree was collapsed. Notify the registered expansion
417      * listeners.
418      *
419      * @param event the original expansion event
420      */
421     public void treeCollapsed(TreeExpansionEvent event)
422     {
423         fireExpansionEvent(
424                 event,
425                 net.sf.jguiraffe.gui.builder.components.model.TreeExpansionEvent
426                 .Type.NODE_COLLAPSE);
427     }
428 
429     /**
430      * A node in the wrapped tree was expanded. Notify the registered expansion
431      * listeners.
432      *
433      * @param event the original expansion event
434      */
435     public void treeExpanded(TreeExpansionEvent event)
436     {
437         fireExpansionEvent(
438                 event,
439                 net.sf.jguiraffe.gui.builder.components.model.TreeExpansionEvent
440                 .Type.NODE_EXPAND);
441     }
442 
443     /**
444      * A note in the wrapped tree will be collapsed. The pre-expansion listeners
445      * registered at this component will be notified.
446      *
447      * @param event the original event
448      * @throws ExpandVetoException if a listener forbids this operation
449      */
450     public void treeWillCollapse(TreeExpansionEvent event)
451             throws ExpandVetoException
452     {
453         firePreExpansionEvent(
454                 event,
455                 net.sf.jguiraffe.gui.builder.components.model.TreeExpansionEvent
456                 .Type.NODE_COLLAPSE);
457     }
458 
459     /**
460      * A note in the wrapped tree will be expanded. The pre-expansion listeners
461      * registered at this component will be notified.
462      *
463      * @param event the original event
464      * @throws ExpandVetoException if a listener forbids this operation
465      */
466     public void treeWillExpand(TreeExpansionEvent event)
467             throws ExpandVetoException
468     {
469         firePreExpansionEvent(
470                 event,
471                 net.sf.jguiraffe.gui.builder.components.model.TreeExpansionEvent
472                 .Type.NODE_EXPAND);
473     }
474 
475     /**
476      * Returns a flag whether multiple selection is supported. This information
477      * is obtained from the tree's selection model.
478      *
479      * @return the multiple selection flag
480      */
481     protected boolean isMultiSelection()
482     {
483         return getTree().getSelectionModel().getSelectionMode()
484             != TreeSelectionModel.SINGLE_TREE_SELECTION;
485     }
486 
487     /**
488      * Converts a tree path to a tree node path.
489      *
490      * @param treePath the tree path
491      * @return the node path
492      */
493     protected TreeNodePath nodePathFromTreePath(TreePath treePath)
494     {
495         Object[] nodes = treePath.getPath();
496         List<ConfigurationNode> ndLst = new ArrayList<ConfigurationNode>(
497                 nodes.length);
498         for (Object nd : nodes)
499         {
500             ndLst.add((ConfigurationNode) nd);
501         }
502 
503         return new TreeNodePath(ndLst);
504     }
505 
506     /**
507      * Converts a node path to a tree path.
508      *
509      * @param nodePath the node path
510      * @return the corresponding tree path
511      */
512     protected TreePath treePathFromNodePath(TreeNodePath nodePath)
513     {
514         return new TreePath(nodePath.getNodes().toArray());
515     }
516 
517     /**
518      * Converts a Swing <code>TreeExpansionEvent</code> to a platform-neutral
519      * expansion event.
520      *
521      * @param event the Swing-specific event
522      * @param type the type of the event (expand or collapse)
523      * @return the platform-neutral event
524      */
525     protected net.sf.jguiraffe.gui.builder.components.model.TreeExpansionEvent convertEvent(
526             TreeExpansionEvent event,
527             net.sf.jguiraffe.gui.builder.components.model.TreeExpansionEvent.Type type)
528     {
529         return new net.sf.jguiraffe.gui.builder.components.model.TreeExpansionEvent(
530                 event, this, name, type, nodePathFromTreePath(event.getPath()));
531     }
532 
533     /**
534      * Notifies all registered event listeners about a change in the expansion
535      * state of a node.
536      *
537      * @param event the original expansion event
538      * @param type the type of the event (expand or collapse)
539      */
540     protected void fireExpansionEvent(
541             TreeExpansionEvent event,
542             net.sf.jguiraffe.gui.builder.components.model.TreeExpansionEvent.Type type)
543     {
544         net.sf.jguiraffe.gui.builder.components.model.TreeExpansionEvent e = null;
545         Object[] listeners = listenerList.getListenerList();
546 
547         for (int i = listeners.length - 2; i >= 0; i -= 2)
548         {
549             if (listeners[i] == TreeExpansionListener.class)
550             {
551                 if (e == null)
552                 {
553                     e = convertEvent(event, type);
554                 }
555                 ((TreeExpansionListener) listeners[i + 1])
556                         .expansionStateChanged(e);
557             }
558         }
559     }
560 
561     /**
562      * Notifies all registered event listeners that a node of the tree is about
563      * to change its expansion state. The listeners can veto against this
564      * action.
565      *
566      * @param event the original expansion event
567      * @param type the type of the event (expand or collapse)
568      * @throws ExpandVetoException if at least one listeners vetos against this
569      *         operation
570      */
571     protected void firePreExpansionEvent(
572             TreeExpansionEvent event,
573             net.sf.jguiraffe.gui.builder.components.model.TreeExpansionEvent.Type type)
574             throws ExpandVetoException
575     {
576         net.sf.jguiraffe.gui.builder.components.model.TreeExpansionEvent e = null;
577         Object[] listeners = listenerList.getListenerList();
578 
579         for (int i = listeners.length - 2; i >= 0; i -= 2)
580         {
581             if (listeners[i] == TreePreExpansionListener.class)
582             {
583                 if (e == null)
584                 {
585                     e = convertEvent(event, type);
586                 }
587                 try
588                 {
589                     ((TreePreExpansionListener) listeners[i + 1])
590                             .beforeExpansionStateChange(e);
591                 }
592                 catch (TreeExpandVetoException tevex)
593                 {
594                     throw new ExpandVetoException(event, tevex.getMessage());
595                 }
596             }
597         }
598     }
599 
600     /**
601      * Registers this component handler as change listener at the wrapped
602      * component. This implementation will add a listener for selection changes
603      * of the wrapped tree component.
604      */
605     @Override
606     protected void registerChangeListener()
607     {
608         getTree().addTreeSelectionListener(this);
609     }
610 
611     /**
612      * Stops listening for change events. This implementation will unregister
613      * itself as tree selection listener.
614      */
615     @Override
616     protected void unregisterChangeListener()
617     {
618         getTree().removeTreeSelectionListener(this);
619     }
620 }