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.Collection;
20 import java.util.Collections;
21 import java.util.List;
22 import java.util.concurrent.CopyOnWriteArrayList;
23
24 import javax.swing.SwingUtilities;
25 import javax.swing.event.TreeModelEvent;
26 import javax.swing.event.TreeModelListener;
27 import javax.swing.tree.TreeModel;
28 import javax.swing.tree.TreePath;
29
30 import net.sf.jguiraffe.gui.builder.components.model.TreeConfigurationChangeHandler;
31 import net.sf.jguiraffe.gui.builder.components.model.TreeModelChangeListener;
32
33 import org.apache.commons.configuration.HierarchicalConfiguration;
34 import org.apache.commons.configuration.event.ConfigurationEvent;
35 import org.apache.commons.configuration.event.ConfigurationListener;
36 import org.apache.commons.configuration.tree.ConfigurationNode;
37 import org.apache.commons.lang.ObjectUtils;
38
39 /**
40 * <p>
41 * A specialized implementation of <code>TreeModel</code> that obtains its data
42 * from a <code>Configuration</code> object.
43 * </p>
44 * <p>
45 * This is a fully functional implementation of Swing's <code>TreeModel</code>
46 * interface. The content of the model is obtained from the nodes stored in a
47 * hierarchical configuration. The tree will display the keys of the
48 * configuration properties, i.e. the names of the nodes.
49 * </p>
50 * <p>
51 * The structure-related methods of the <code>TreeModel</code> interface (e.g.
52 * <code>getRoot()</code> or <code>getChild()</code>) are implemented by
53 * directly forwarding to methods provided by the <code>ConfigurationNode</code>
54 * interface. All of these methods expect that a passed in node (which is of
55 * type <code>Object</code> in the <code>TreeModel</code> interface) can be cast
56 * into a <code>ConfigurationNode</code>.
57 * </p>
58 * <p>
59 * The model also registers itself as event listener at the underlying
60 * configuration and tries to map configuration change events to corresponding
61 * model change events. This is not always possible because configuration change
62 * events often do not contain enough information for such a mapping. If the
63 * mapping is not possible, a very generic structure changed event is fired.
64 * </p>
65 * <p>
66 * As is true for most Swing objects, this class is not thread-safe. It is
67 * possible to manipulate the underlying configuration in a separate thread,
68 * which will cause change events received by this model. These events are then
69 * propagated to registered listeners in the event dispatch thread. However,
70 * this implementation relies on the fact that only a single configuration
71 * change event can be received at a time. (Because typical configuration
72 * implementations cannot be updated concurrently this should not be a
73 * limitation.)
74 * </p>
75 *
76 * @author Oliver Heger
77 * @version $Id: SwingConfigurationTreeModel.java 205 2012-01-29 18:29:57Z oheger $
78 */
79 public class SwingConfigurationTreeModel implements TreeModel,
80 ConfigurationListener, TreeModelChangeListener
81 {
82 /** Stores the underlying configuration object. */
83 private final HierarchicalConfiguration configuration;
84
85 /** A collection with the event listeners registered for this model. */
86 private final Collection<TreeModelListener> listeners;
87
88 /** The change handler. */
89 private final TreeConfigurationChangeHandler ccHandler;
90
91 /**
92 * Creates a new instance of {@code SwingConfigurationTreeModel} and
93 * initializes it with the given {@code HierarchicalConfiguration}
94 * object. Behind the scenes, a {@link TreeConfigurationChangeHandler} is
95 * created and registered at the configuration so that change events can
96 * be correctly processed.
97 *
98 * @param config the configuration (must not be <b>null</b>)
99 * @throws IllegalArgumentException if a required parameter is missing
100 */
101 public SwingConfigurationTreeModel(HierarchicalConfiguration config)
102 {
103 if (config == null)
104 {
105 throw new IllegalArgumentException(
106 "Configuration must not be null!");
107 }
108
109 configuration = config;
110 listeners = new CopyOnWriteArrayList<TreeModelListener>();
111 ccHandler = new TreeConfigurationChangeHandler(config, this);
112 configuration.addConfigurationListener(ccHandler);
113 }
114
115 /**
116 * Returns the configuration object that stores the data of this model.
117 *
118 * @return the underlying configuration object
119 */
120 public HierarchicalConfiguration getConfiguration()
121 {
122 return configuration;
123 }
124
125 /**
126 * Adds an event listener to this model. The listener must not be
127 * <b>null</b>.
128 *
129 * @param l the listener to add
130 * @throws IllegalArgumentException if the listener is <b>null</b>
131 */
132 public void addTreeModelListener(TreeModelListener l)
133 {
134 if (l == null)
135 {
136 throw new IllegalArgumentException("Listener must not be null!");
137 }
138
139 listeners.add(l);
140 }
141
142 /**
143 * Returns the child node of the specified node with the given index. This
144 * implementation expects that the node is of type
145 * <code>ConfigurationNode</code>.
146 *
147 * @param node the node
148 * @param index the index
149 * @return the child node with this index
150 */
151 public Object getChild(Object node, int index)
152 {
153 return ((ConfigurationNode) node).getChild(index);
154 }
155
156 /**
157 * Returns the number of child nodes of the specified node. This
158 * implementation expects that the node is of type
159 * <code>ConfigurationNode</code>.
160 *
161 * @param node the node
162 * @return the number of child nodes of this node
163 */
164 public int getChildCount(Object node)
165 {
166 return ((ConfigurationNode) node).getChildrenCount();
167 }
168
169 /**
170 * Returns the index of the specified child node relative to its parent
171 * node. This implementation expects that the node is of type
172 * <code>ConfigurationNode</code>. If the node is no child of the specified
173 * parent, -1 is returned. The parent and the child node can both be
174 * <b>null</b>; then -1 is returned, too.
175 *
176 * @param parent the parent node
177 * @param child the child node
178 * @return the index of this child node or -1
179 */
180 public int getIndexOfChild(Object parent, Object child)
181 {
182 if (parent == null || child == null)
183 {
184 return -1;
185 }
186
187 int index = 0;
188 for (Object o : ((ConfigurationNode) parent).getChildren())
189 {
190 if (o == child)
191 {
192 return index;
193 }
194 index++;
195 }
196
197 return -1;
198 }
199
200 /**
201 * Returns the root node of this tree model. This is the root node of the
202 * underlying configuration.
203 *
204 * @return the root node of this tree model
205 */
206 public Object getRoot()
207 {
208 return getRootNode();
209 }
210
211 /**
212 * Tests whether the passed in node is a leaf node. This implementation
213 * expects that the passed in object is a <code>ConfigurationNode</code>. It
214 * then checks whether it has children.
215 *
216 * @param node the node
217 * @return a flag whether this is a leaf node
218 */
219 public boolean isLeaf(Object node)
220 {
221 return ((ConfigurationNode) node).getChildrenCount() == 0;
222 }
223
224 /**
225 * Removes the specified event listener from this model.
226 *
227 * @param l the listener to be removed
228 */
229 public void removeTreeModelListener(TreeModelListener l)
230 {
231 listeners.remove(l);
232 }
233
234 /**
235 * The value of a node was changed. This method is called if the user edited
236 * a node in the tree control. This implementation will update the value of
237 * the corresponding configuration node. Then it will fire a change event.
238 *
239 * @param path the path to the changed node
240 * @param newValue the new value
241 */
242 public void valueForPathChanged(TreePath path, Object newValue)
243 {
244 ConfigurationNode node = (ConfigurationNode) path
245 .getLastPathComponent();
246 if (!ObjectUtils.equals(node.getValue(), newValue))
247 {
248 changeNodeName(node, String.valueOf(newValue));
249 if (path.getPathCount() > 1)
250 {
251 // It is not the root node
252 TreeModelEvent event =
253 new TreeModelEvent(this, path.getParentPath(),
254 new int[] {
255 getIndexOfChild(node.getParentNode(), node)
256 }, new Object[] {
257 node
258 });
259
260 for (TreeModelListener l : listeners)
261 {
262 l.treeNodesChanged(event);
263 }
264 }
265
266 else
267 {
268 // fire a generic structure changed event for the root node
269 fireStructureChangedEvent(getRootNode());
270 }
271 }
272 }
273
274 /**
275 * The underlying configuration has changed. This method tries to translate
276 * the configuration event into a tree model event. This involves finding
277 * the highest configuration node in the hierarchy affected by this event.
278 * In most cases this will not be possible because the configuration event
279 * typically won't contain enough information. Then a generic structure
280 * changed event for the root node is fired.
281 *
282 * @param event the event
283 * @deprecated This method is no longer used or called. Configuration change
284 * events are now processed by a {@link TreeConfigurationChangeHandler}
285 * and propagated to the {@link #treeModelChanged(ConfigurationNode)}
286 * method.
287 */
288 @Deprecated
289 public void configurationChanged(ConfigurationEvent event)
290 {
291 }
292
293 /**
294 * The configuration serving as tree model was changed in the sub tree
295 * referenced by the passed in node. This implementation fires a tree
296 * structure change event to all registered listeners.
297 *
298 * @param node the node in the configuration which has changed
299 * @since 1.3
300 */
301 public void treeModelChanged(ConfigurationNode node)
302 {
303 fireStructureChangedEvent(node);
304 }
305
306 /**
307 * Fires a structure changed event. All registered listeners are notified in
308 * the event dispatch thread.
309 *
310 * @param changedNode the configuration node affected by the change
311 */
312 private void fireStructureChangedEvent(ConfigurationNode changedNode)
313 {
314 final TreeModelEvent event = createEvent(changedNode);
315 SwingUtilities.invokeLater(new Runnable()
316 {
317 public void run()
318 {
319 for (TreeModelListener l : listeners)
320 {
321 l.treeStructureChanged(event);
322 }
323 }
324 });
325 }
326
327 /**
328 * Returns the root node of the associated configuration.
329 *
330 * @return the configuration's root node
331 */
332 private ConfigurationNode getRootNode()
333 {
334 return getConfiguration().getRootNode();
335 }
336
337 /**
338 * Creates a path for the specified configuration node. The path contains
339 * all nodes up to the root node.
340 *
341 * @param node the start node for the path
342 * @return the path
343 */
344 private Object[] createPath(ConfigurationNode node)
345 {
346 List<Object> pathElements = new ArrayList<Object>();
347 ConfigurationNode nd = node;
348
349 // iterate to the root node
350 while (nd.getParentNode() != null)
351 {
352 pathElements.add(nd);
353 nd = nd.getParentNode();
354 }
355
356 // Explicitly add the root node. This is a workaround for
357 // inconsistencies
358 // in the handling of parent nodes in hierarchical configurations
359 pathElements.add(getRoot());
360
361 // now reverse order and create an array
362 Collections.reverse(pathElements);
363 return pathElements.toArray();
364 }
365
366 /**
367 * Creates an event reporting a change of this tree model. The passed in
368 * node determines the part of the tree structure affected by this change.
369 *
370 * @param changedNode the node affected by the change
371 * @return the event
372 */
373 private TreeModelEvent createEvent(ConfigurationNode changedNode)
374 {
375 Object[] path = createPath(changedNode);
376 return new TreeModelEvent(this, path);
377 }
378
379 /**
380 * Changes the name of a configuration node. Nodes that have a parent
381 * usually must not be changed. So this method first removes the parent,
382 * then sets the new name, and finally restores the parent.
383 *
384 * @param node the node
385 * @param newName the new name
386 */
387 private void changeNodeName(ConfigurationNode node, String newName)
388 {
389 ccHandler.changeNodeName(node, newName);
390 }
391 }