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 (<= 0 for undefined)
98 * @param scrollHeight the preferred scroll height (<= 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 }