001/*
002 * $Id: TreeModelSupport.java 3100 2008-10-14 22:33:10Z rah003 $
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 */
021package org.jdesktop.swingx.tree;
022
023import javax.swing.event.EventListenerList;
024import javax.swing.event.TreeModelEvent;
025import javax.swing.event.TreeModelListener;
026import javax.swing.tree.TreeModel;
027import javax.swing.tree.TreePath;
028
029import org.jdesktop.swingx.util.Contract;
030
031/**
032 * Support for change notification, usable by {@code TreeModel}s.
033 * 
034 * The changed/inserted/removed is expressed in terms of a {@code TreePath},
035 * it's up to the client model to build it as appropriate.
036 * 
037 * This is inspired by {@code AbstractTreeModel} from Christian Kaufhold,
038 * www.chka.de.
039 * 
040 * TODO - implement and test precondition failure of added/removed notification
041 * 
042 * @author JW
043 */
044public final class TreeModelSupport {
045    protected EventListenerList listeners;
046
047    private TreeModel treeModel;
048
049    /**
050     * Creates the support class for the given {@code TreeModel}.
051     * 
052     * @param model the model to support
053     * @throws NullPointerException if {@code model} is {@code null}
054     */
055    public TreeModelSupport(TreeModel model) {
056        if (model == null)
057            throw new NullPointerException("model must not be null");
058        listeners = new EventListenerList();
059        this.treeModel = model;
060    }
061
062//---------------------- structural changes on subtree
063    
064    /** 
065     * Notifies registered TreeModelListeners that the tree's root has
066     * been replaced. Can cope with a null root.
067     */
068    public void fireNewRoot() {
069
070        Object root = treeModel.getRoot();
071
072        /*
073         * Undocumented. I think it is the only reasonable/possible solution to
074         * use use null as path if there is no root. TreeModels without root
075         * aren't important anyway, since JTree doesn't support them (yet).
076         */
077        TreePath path = (root != null) ? new TreePath(root) : null;
078        fireTreeStructureChanged(path);
079    }
080
081    /**
082     * Call when a node has changed its leaf state.<p>
083     * 
084     * PENDING: rename? Do we need it?
085     * @param path the path to the node with changed leaf state.
086     */
087    public void firePathLeafStateChanged(TreePath path) {
088        fireTreeStructureChanged(path);
089    }
090
091    /**
092     * Notifies registered TreeModelListeners that the structure
093     * below the node identified by the given path has been 
094     * completely changed.
095     * <p>
096     * NOTE: the subtree path maybe null if the root is null. 
097     * If not null, it must contain at least one element (the root).
098     * 
099     * @param subTreePath the path to the root of the subtree 
100     *    whose structure was changed. 
101     * @throws NullPointerException if the path is not null but empty
102     *   or contains null elements.  
103     */
104    public void fireTreeStructureChanged(TreePath subTreePath) {
105        if (subTreePath != null) {
106            Contract.asNotNull(subTreePath.getPath(), 
107                    "path must not contain null elements");
108        }
109        Object[] pairs = listeners.getListenerList();
110
111        TreeModelEvent e = null;
112
113        for (int i = pairs.length - 2; i >= 0; i -= 2) {
114            if (pairs[i] == TreeModelListener.class) {
115                if (e == null)
116                    e = createStructureChangedEvent(subTreePath);
117
118                ((TreeModelListener) pairs[i + 1]).treeStructureChanged(e);
119            }
120        }
121    }
122
123//----------------------- node modifications, no mutations
124    
125    /**
126     * Notifies registered TreeModelListeners that the 
127     * the node identified by the given path has been modified.
128     * 
129     * @param path the path to the node that has been modified, 
130     *   must not be null and must not contain null path elements.
131     * 
132     */
133    public void firePathChanged(TreePath path) {
134        Object node = path.getLastPathComponent();
135        TreePath parentPath = path.getParentPath();
136
137        if (parentPath == null)
138            fireChildrenChanged(path, null, null);
139        else {
140            Object parent = parentPath.getLastPathComponent();
141
142            fireChildChanged(parentPath, treeModel
143                    .getIndexOfChild(parent, node), node);
144        }
145    }
146
147    /**
148     * Notifies registered TreeModelListeners that the given child of
149     * the node identified by the given parent path has been modified.
150     * The parent path must not be null, nor empty nor contain null
151     * elements. 
152     * 
153     * @param parentPath the path to the parent of the modified children.
154     * @param index the position of the child 
155     * @param child child node that has been modified, must not be null
156     */
157    public void fireChildChanged(TreePath parentPath, int index, Object child) {
158        fireChildrenChanged(parentPath, new int[] { index },
159                new Object[] { child });
160    }
161    
162    /**
163     * Notifies registered TreeModelListeners that the given children of 
164     * the node identified by the given parent path have been modified.
165     * The parent path must not be null, nor empty nor contain null
166     * elements. Note that the index array must contain the position of the
167     * corresponding child in the the children array. The indices must be in
168     * ascending order. <p>
169     * 
170     * The exception to these rules is if the root itself has been 
171     * modified (which has no parent by definition). In this case  
172     * the path must be the path to the root and both indices and children
173     * arrays must be null.
174     *  
175     * @param parentPath the path to the parent of the modified children.
176     * @param indices the positions of the modified children
177     * @param children the modified children
178     */
179    public void fireChildrenChanged(TreePath parentPath, int[] indices,
180            Object[] children) {
181        Contract.asNotNull(parentPath.getPath(), 
182                "path must not be null and must not contain null elements");
183        Object[] pairs = listeners.getListenerList();
184
185        TreeModelEvent e = null;
186
187        for (int i = pairs.length - 2; i >= 0; i -= 2) {
188            if (pairs[i] == TreeModelListener.class) {
189                if (e == null)
190                    e = createTreeModelEvent(parentPath, indices, children);
191
192                ((TreeModelListener) pairs[i + 1]).treeNodesChanged(e);
193            }
194        }
195    }
196
197
198//------------------------ mutations (insert/remove nodes)
199    
200    
201    /**
202     * Notifies registered TreeModelListeners that the child has been added to
203     * the the node identified by the given parent path at the given position.
204     * The parent path must not be null, nor empty nor contain null elements.
205     * 
206     * @param parentPath the path to the parent of added child.
207     * @param index the position of the added children
208     * @param child the added child
209     */
210    public void fireChildAdded(TreePath parentPath, int index, Object child) {
211        fireChildrenAdded(parentPath, new int[] { index },
212                new Object[] { child });
213    }
214
215    /**
216     * Notifies registered TreeModelListeners that the child has been removed 
217     * from the node identified by the given parent path from the given position.
218     * The parent path must not be null, nor empty nor contain null elements.
219     * 
220     * @param parentPath the path to the parent of removed child.
221     * @param index the position of the removed children before the removal
222     * @param child the removed child
223     */
224    public void fireChildRemoved(TreePath parentPath, int index, Object child) {
225        fireChildrenRemoved(parentPath, new int[] { index },
226                new Object[] { child });
227    }
228
229    /**
230     * Notifies registered TreeModelListeners that the given children have been
231     * added to the the node identified by the given parent path at the given
232     * locations. The parent path and the child array must not be null, nor
233     * empty nor contain null elements. Note that the index array must contain
234     * the position of the corresponding child in the the children array. The
235     * indices must be in ascending order.
236     * <p>
237     * 
238     * @param parentPath the path to the parent of the added children.
239     * @param indices the positions of the added children.
240     * @param children the added children.
241     */
242    public void fireChildrenAdded(TreePath parentPath, int[] indices,
243            Object[] children) {
244        Object[] pairs = listeners.getListenerList();
245
246        TreeModelEvent e = null;
247
248        for (int i = pairs.length - 2; i >= 0; i -= 2) {
249            if (pairs[i] == TreeModelListener.class) {
250                if (e == null)
251                    e = createTreeModelEvent(parentPath, indices, children);
252
253                ((TreeModelListener) pairs[i + 1]).treeNodesInserted(e);
254            }
255        }
256    }
257
258    /**
259     * Notifies registered TreeModelListeners that the given children have been
260     * removed to the the node identified by the given parent path from the
261     * given locations. The parent path and the child array must not be null,
262     * nor empty nor contain null elements. Note that the index array must
263     * contain the position of the corresponding child in the the children
264     * array. The indices must be in ascending order.
265     * <p>
266     * 
267     * @param parentPath the path to the parent of the removed children.
268     * @param indices the positions of the removed children before the removal
269     * @param children the removed children
270     */
271    public void fireChildrenRemoved(TreePath parentPath, int[] indices,
272            Object[] children) {
273        Object[] pairs = listeners.getListenerList();
274
275        TreeModelEvent e = null;
276
277        for (int i = pairs.length - 2; i >= 0; i -= 2) {
278            if (pairs[i] == TreeModelListener.class) {
279                if (e == null)
280                    e = createTreeModelEvent(parentPath, indices, children);
281                ((TreeModelListener) pairs[i + 1]).treeNodesRemoved(e);
282            }
283        }
284    }
285    
286//------------------- factory methods of TreeModelEvents
287    
288    /**
289     * Creates and returns a TreeModelEvent for structureChanged 
290     * event notification. The given path may be null to indicate
291     * setting a null root. In all other cases, the first path element
292     * must contain the root and the last path element the rootNode of the
293     * structural change. Specifically, a TreePath with a single element
294     * (which is the root) denotes a structural change of the complete tree.
295     * 
296     * @param parentPath the path to the root of the changed structure,
297     *   may be null to indicate setting a null root.
298     * @return a TreeModelEvent for structureChanged notification.
299     * 
300     * @see javax.swing.event.TreeModelEvent
301     * @see javax.swing.event.TreeModelListener
302     */
303    private TreeModelEvent createStructureChangedEvent(TreePath parentPath) {
304        return createTreeModelEvent(parentPath, null, null);
305    }
306
307    /**
308     * Creates and returns a TreeModelEvent for changed/inserted/removed
309     * event notification.
310     * 
311     * @param parentPath path to parent of modified node
312     * @param indices the indices of the modified children (before the change)
313     * @param children the array of modified children 
314     * @return a TreeModelEvent for changed/inserted/removed notification
315     * 
316     * @see javax.swing.event.TreeModelEvent
317     * @see javax.swing.event.TreeModelListener
318     */
319    private TreeModelEvent createTreeModelEvent(TreePath parentPath,
320            int[] indices, Object[] children) {
321        return new TreeModelEvent(treeModel, parentPath, indices, children);
322    }
323
324
325//------------------------ handling listeners
326    
327    public void addTreeModelListener(TreeModelListener l) {
328        listeners.add(TreeModelListener.class, l);
329    }
330
331    public TreeModelListener[] getTreeModelListeners() {
332        return listeners.getListeners(TreeModelListener.class);
333    }
334
335    public void removeTreeModelListener(TreeModelListener l) {
336        listeners.remove(TreeModelListener.class, l);
337    }
338}