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     */
021    package org.jdesktop.swingx.tree;
022    
023    import javax.swing.event.EventListenerList;
024    import javax.swing.event.TreeModelEvent;
025    import javax.swing.event.TreeModelListener;
026    import javax.swing.tree.TreeModel;
027    import javax.swing.tree.TreePath;
028    
029    import 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     */
044    public 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    }