001 /*
002 * $Id: DefaultTreeTableModel.java 3187 2009-01-20 16:19:08Z 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.treetable;
022
023 import java.util.ArrayList;
024 import java.util.List;
025
026 import javax.swing.tree.TreePath;
027
028 /**
029 * {@code DefaultTreeTableModel} is a concrete implementation of
030 * {@code AbstractTreeTableModel} and is provided purely as a convenience.
031 * Applications that use {@code JXTreeTable} are expected to provide their own
032 * implementation of a {@code TreeTableModel}.
033 * <p>
034 * The {@code DefaultTreeTableModel} is designed to be used with
035 * {@code TreeTableNode}s. Specifically, users should extend
036 * {@code AbstractMutableTreeTableNode} to provide custom implementations for
037 * data display.
038 * <p>
039 * Users who do not provide a list of column identifiers must provide a root
040 * that contains at least one column. Without specified identifiers the model
041 * will attempt to calculate the columns required for display by querying the
042 * root node. Normally, the root node can be little more than a shell (in
043 * displays that hide it), but without identifiers, the model relies on the root
044 * node metadata for display.
045 *
046 * @author Ramesh Gupta
047 * @author Karl Schaefer
048 */
049 public class DefaultTreeTableModel extends AbstractTreeTableModel {
050 /** The <code>List</code> of column identifiers. */
051 protected List<?> columnIdentifiers;
052
053 private boolean useAutoCalculatedIdentifiers;
054
055 /**
056 * Creates a new {@code DefaultTreeTableModel} with a {@code null} root.
057 */
058 public DefaultTreeTableModel() {
059 this(null);
060 }
061
062 /**
063 * Creates a new {@code DefaultTreeTableModel} with the specified
064 * {@code root}.
065 *
066 * @param root
067 * the root node of the tree
068 */
069 public DefaultTreeTableModel(TreeTableNode root) {
070 this(root, null);
071 }
072
073 /**
074 * Creates a new {@code DefaultTreeTableModel} with the specified {@code
075 * root} and column names.
076 *
077 * @param root
078 * the root node of the tree
079 * @param columnNames
080 * the names of the columns used by this model
081 * @see #setColumnIdentifiers(List)
082 */
083 public DefaultTreeTableModel(TreeTableNode root, List<?> columnNames) {
084 super(root);
085
086 setColumnIdentifiers(columnNames);
087 }
088
089 private boolean isValidTreeTableNode(Object node) {
090 boolean result = false;
091
092 if (node instanceof TreeTableNode) {
093 TreeTableNode ttn = (TreeTableNode) node;
094
095 while (!result && ttn != null) {
096 result = ttn == root;
097
098 ttn = ttn.getParent();
099 }
100 }
101
102 return result;
103 }
104
105 /**
106 * Replaces the column identifiers in the model. If the number of
107 * <code>newIdentifier</code>s is greater than the current number of
108 * columns, new columns are added to the end of each row in the model. If
109 * the number of <code>newIdentifier</code>s is less than the current
110 * number of columns, all the extra columns at the end of a row are
111 * discarded.
112 * <p>
113 *
114 * @param columnIdentifiers
115 * vector of column identifiers. If <code>null</code>, set the
116 * model to zero columns
117 */
118 // from DefaultTableModel
119 public void setColumnIdentifiers(List<?> columnIdentifiers) {
120 useAutoCalculatedIdentifiers = columnIdentifiers == null;
121
122 this.columnIdentifiers = useAutoCalculatedIdentifiers
123 ? getAutoCalculatedIdentifiers(getRoot())
124 : columnIdentifiers;
125
126 modelSupport.fireNewRoot();
127 }
128
129 private static List<String> getAutoCalculatedIdentifiers(
130 TreeTableNode exemplar) {
131 List<String> autoCalculatedIndentifiers = new ArrayList<String>();
132
133 if (exemplar != null) {
134 for (int i = 0, len = exemplar.getColumnCount(); i < len; i++) {
135 // forces getColumnName to use super.getColumnName
136 autoCalculatedIndentifiers.add(null);
137 }
138 }
139
140 return autoCalculatedIndentifiers;
141 }
142
143 /**
144 * Returns the root of the tree. Returns {@code null} only if the tree has
145 * no nodes.
146 *
147 * @return the root of the tree
148 *
149 * @throws ClassCastException
150 * if {@code root} is not a {@code TreeTableNode}. Even though
151 * subclasses have direct access to {@code root}, they should
152 * avoid accessing it directly.
153 * @see AbstractTreeTableModel#root
154 * @see #setRoot(TreeTableNode)
155 */
156 @Override
157 public TreeTableNode getRoot() {
158 return (TreeTableNode) root;
159 }
160
161 /**
162 * Gets the value for the {@code node} at {@code column}.
163 *
164 * @impl delegates to {@code TreeTableNode.getValueAt(int)}
165 * @param node
166 * the node whose value is to be queried
167 * @param column
168 * the column whose value is to be queried
169 * @return the value Object at the specified cell
170 * @throws IllegalArgumentException
171 * if {@code node} is not an instance of {@code TreeTableNode}
172 * or is not managed by this model, or {@code column} is not a
173 * valid column index
174 */
175 public Object getValueAt(Object node, int column) {
176 if (!isValidTreeTableNode(node)) {
177 throw new IllegalArgumentException(
178 "node must be a valid node managed by this model");
179 }
180
181 if (column < 0 || column >= getColumnCount()) {
182 throw new IllegalArgumentException("column must be a valid index");
183 }
184
185 TreeTableNode ttn = (TreeTableNode) node;
186
187 if (column >= ttn.getColumnCount()) {
188 return null;
189 }
190
191 return ttn.getValueAt(column);
192 }
193
194 /**
195 * {@inheritDoc}
196 */
197 @Override
198 public void setValueAt(Object value, Object node, int column) {
199 if (!isValidTreeTableNode(node)) {
200 throw new IllegalArgumentException(
201 "node must be a valid node managed by this model");
202 }
203
204 if (column < 0 || column >= getColumnCount()) {
205 throw new IllegalArgumentException("column must be a valid index");
206 }
207
208 TreeTableNode ttn = (TreeTableNode) node;
209
210 if (column < ttn.getColumnCount()) {
211 ttn.setValueAt(value, column);
212
213 modelSupport.firePathChanged(new TreePath(getPathToRoot(ttn)));
214 }
215 }
216
217 /**
218 * {@inheritDoc}
219 */
220 public int getColumnCount() {
221 return columnIdentifiers.size();
222 }
223
224 /**
225 * {@inheritDoc}
226 */
227 // Can we make getColumnClass final and avoid the complex DTM copy? -- kgs
228 @Override
229 public String getColumnName(int column) {
230 // Copied from DefaultTableModel.
231 Object id = null;
232
233 // This test is to cover the case when
234 // getColumnCount has been subclassed by mistake ...
235 if (column < columnIdentifiers.size() && (column >= 0)) {
236 id = columnIdentifiers.get(column);
237 }
238
239 return (id == null) ? super.getColumnName(column) : id.toString();
240 }
241
242 /**
243 * {@inheritDoc}
244 */
245 public Object getChild(Object parent, int index) {
246 if (!isValidTreeTableNode(parent)) {
247 throw new IllegalArgumentException(
248 "parent must be a TreeTableNode managed by this model");
249 }
250
251 return ((TreeTableNode) parent).getChildAt(index);
252 }
253
254 /**
255 * {@inheritDoc}
256 */
257 public int getChildCount(Object parent) {
258 if (!isValidTreeTableNode(parent)) {
259 throw new IllegalArgumentException(
260 "parent must be a TreeTableNode managed by this model");
261 }
262
263 return ((TreeTableNode) parent).getChildCount();
264 }
265
266 /**
267 * {@inheritDoc}
268 */
269 public int getIndexOfChild(Object parent, Object child) {
270 if (!isValidTreeTableNode(parent) || !isValidTreeTableNode(child)) {
271 return -1;
272 }
273
274 return ((TreeTableNode) parent).getIndex((TreeTableNode) child);
275 }
276
277 /**
278 * {@inheritDoc}
279 */
280 @Override
281 public boolean isCellEditable(Object node, int column) {
282 if (!isValidTreeTableNode(node)) {
283 throw new IllegalArgumentException(
284 "node must be a valid node managed by this model");
285 }
286
287 if (column < 0 || column >= getColumnCount()) {
288 throw new IllegalArgumentException("column must be a valid index");
289 }
290
291 TreeTableNode ttn = (TreeTableNode) node;
292
293 if (column >= ttn.getColumnCount()) {
294 return false;
295 }
296
297 return ttn.isEditable(column);
298 }
299
300 /**
301 * {@inheritDoc}
302 */
303 @Override
304 public boolean isLeaf(Object node) {
305 if (!isValidTreeTableNode(node)) {
306 throw new IllegalArgumentException(
307 "node must be a TreeTableNode managed by this model");
308 }
309
310 return ((TreeTableNode) node).isLeaf();
311 }
312
313 /**
314 * Gets the path from the root to the specified node.
315 *
316 * @param aNode
317 * the node to query
318 * @return an array of {@code TreeTableNode}s, where
319 * {@code arr[0].equals(getRoot())} and
320 * {@code arr[arr.length - 1].equals(aNode)}, or an empty array if
321 * the node is not found.
322 * @throws NullPointerException
323 * if {@code aNode} is {@code null}
324 */
325 public TreeTableNode[] getPathToRoot(TreeTableNode aNode) {
326 List<TreeTableNode> path = new ArrayList<TreeTableNode>();
327 TreeTableNode node = aNode;
328
329 while (node != root) {
330 path.add(0, node);
331
332 node = (TreeTableNode) node.getParent();
333 }
334
335 if (node == root) {
336 path.add(0, node);
337 }
338
339 return path.toArray(new TreeTableNode[0]);
340 }
341
342 /**
343 * Sets the root for this table model. If no column identifiers have been
344 * specified, this will rebuild the identifier list, using {@code root} as
345 * an examplar of the table.
346 *
347 * @param root
348 * the node to set as root
349 */
350 public void setRoot(TreeTableNode root) {
351 this.root = root;
352
353 if (useAutoCalculatedIdentifiers) {
354 // rebuild the list
355 //this already fires an event don't duplicate
356 setColumnIdentifiers(null);
357 } else {
358 modelSupport.fireNewRoot();
359 }
360 }
361
362 /**
363 * Invoked this to insert newChild at location index in parents children.
364 * This will then message nodesWereInserted to create the appropriate event.
365 * This is the preferred way to add children as it will create the
366 * appropriate event.
367 */
368 public void insertNodeInto(MutableTreeTableNode newChild,
369 MutableTreeTableNode parent, int index) {
370 parent.insert(newChild, index);
371
372 modelSupport.fireChildAdded(new TreePath(getPathToRoot(parent)), index,
373 newChild);
374 }
375
376 /**
377 * Message this to remove node from its parent. This will message
378 * nodesWereRemoved to create the appropriate event. This is the preferred
379 * way to remove a node as it handles the event creation for you.
380 */
381 public void removeNodeFromParent(MutableTreeTableNode node) {
382 MutableTreeTableNode parent = (MutableTreeTableNode) node.getParent();
383
384 if (parent == null) {
385 throw new IllegalArgumentException("node does not have a parent.");
386 }
387
388 int index = parent.getIndex(node);
389 node.removeFromParent();
390
391 modelSupport.fireChildRemoved(new TreePath(getPathToRoot(parent)),
392 index, node);
393 }
394
395 /**
396 * Called when value for the item identified by path has been changed. If
397 * newValue signifies a truly new value the model should post a {@code
398 * treeNodesChanged} event.
399 * <p>
400 * This changes the object backing the {@code TreeTableNode} described by
401 * the path. This change does not alter a nodes children in any way. If you
402 * need to change structure of the node, use one of the provided mutator
403 * methods.
404 *
405 * @param path
406 * path to the node that has changed
407 * @param newValue
408 * the new value
409 * @throws NullPointerException
410 * if {@code path} is {@code null}
411 * @throws IllegalArgumentException
412 * if {@code path} is not a path managed by this model
413 * @throws ClassCastException
414 * if {@code path.getLastPathComponent()} is not a {@code
415 * TreeTableNode}
416 */
417 @Override
418 public void valueForPathChanged(TreePath path, Object newValue) {
419 if (path.getPathComponent(0) != root) {
420 throw new IllegalArgumentException("invalid path");
421 }
422
423 TreeTableNode node = (TreeTableNode) path.getLastPathComponent();
424 node.setUserObject(newValue);
425
426 modelSupport.firePathChanged(path);
427 }
428
429 /**
430 * Sets the user object for a node. Client code must use this method, so
431 * that the model can notify listeners that a change has occurred.
432 * <p>
433 * This method is a convenient cover for
434 * {@link #valueForPathChanged(TreePath, Object)}.
435 *
436 * @param node
437 * the node to modify
438 * @param userObject
439 * the new user object to set
440 * @throws NullPointerException
441 * if {@code node} is {@code null}
442 * @throws IllegalArgumentException
443 * if {@code node} is not a node managed by this model
444 */
445 public void setUserObject(TreeTableNode node, Object userObject) {
446 valueForPathChanged(new TreePath(getPathToRoot(node)), userObject);
447 }
448 }