0001: /*
0002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
0003: *
0004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
0005: *
0006: * The contents of this file are subject to the terms of either the GNU
0007: * General Public License Version 2 only ("GPL") or the Common
0008: * Development and Distribution License("CDDL") (collectively, the
0009: * "License"). You may not use this file except in compliance with the
0010: * License. You can obtain a copy of the License at
0011: * http://www.netbeans.org/cddl-gplv2.html
0012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
0013: * specific language governing permissions and limitations under the
0014: * License. When distributing the software, include this License Header
0015: * Notice in each file and include the License file at
0016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
0017: * particular file as subject to the "Classpath" exception as provided
0018: * by Sun in the GPL Version 2 section of the License file that
0019: * accompanied this code. If applicable, add the following below the
0020: * License Header, with the fields enclosed by brackets [] replaced by
0021: * your own identifying information:
0022: * "Portions Copyrighted [year] [name of copyright owner]"
0023: *
0024: * Contributor(s):
0025: *
0026: * The Original Software is NetBeans. The Initial Developer of the Original
0027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun
0028: * Microsystems, Inc. All Rights Reserved.
0029: *
0030: * If you wish your version of this file to be governed by only the CDDL
0031: * or only the GPL Version 2, indicate your decision by adding
0032: * "[Contributor] elects to include this software in this distribution
0033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
0034: * single choice of license, a recipient has the option to distribute
0035: * your version of this file under either the CDDL, the GPL Version 2 or
0036: * to extend the choice of license to its licensees as provided above.
0037: * However, if you add GPL Version 2 code and therefore, elected the GPL
0038: * Version 2 license, then the option applies only if the new code is
0039: * made subject to such option by the copyright holder.
0040: */
0041:
0042: package org.netbeans.swing.outline;
0043:
0044: import java.util.ArrayList;
0045: import java.util.Arrays;
0046: import java.util.List;
0047: import javax.swing.SwingUtilities;
0048: import javax.swing.event.TableModelEvent;
0049: import javax.swing.event.TableModelListener;
0050: import javax.swing.event.TreeExpansionEvent;
0051: import javax.swing.event.TreeExpansionListener;
0052: import javax.swing.event.TreeModelEvent;
0053: import javax.swing.event.TreeModelListener;
0054: import javax.swing.table.TableModel;
0055: import javax.swing.tree.AbstractLayoutCache;
0056: import javax.swing.tree.ExpandVetoException;
0057: import javax.swing.tree.TreeModel;
0058: import javax.swing.tree.TreePath;
0059:
0060: /** Responsible for handling tree model events from the user-supplied treemodel
0061: * portion of a DefaultOutlineModel, translating them into appropriate
0062: * TableModelEvents and refiring these events to listeners on the table model.
0063: * <p>
0064: * This class could be (and originally was) incorporated directly into
0065: * DefaultOutlineModel, but is separated for better readability and separation
0066: * of concerns.
0067: *
0068: * @author Tim Boudreau
0069: */
0070: final class EventBroadcaster implements TableModelListener,
0071: TreeModelListener, ExtTreeWillExpandListener,
0072: TreeExpansionListener {
0073:
0074: /** Debugging constant for whether logging should be enabled */
0075: static boolean log = false;
0076:
0077: /** Debugging message counter to differentiate log entries */
0078: private int logcount = 0;
0079:
0080: /** The model we will proxy */
0081: private DefaultOutlineModel model;
0082:
0083: /** The last event sent to treeWillExpand/Collapse, used to compare against the
0084: * next value sent to treeExpanded/Collapse */
0085: private TreeExpansionEvent inProgressEvent = null;
0086:
0087: /** A TableModelEvent generated in treeWillExpand/Collapse (so, generated when
0088: * data about the rows/columns in the tree model is still in sync with the
0089: * TableModel), which will be fired from treeExpanded/Collapsed if the
0090: * expansion event is not vetoed */
0091: private TableModelEvent pendingExpansionEvent = null;
0092:
0093: /** Are we in the middle of firing multiple TableModelEvents for a single
0094: * TreeModelEvent. */
0095: private boolean inMultiEvent = false;
0096:
0097: //Some constants we use to have a single method handle all translated
0098: //event firing
0099: private static final int NODES_CHANGED = 0;
0100: private static final int NODES_INSERTED = 1;
0101: private static final int NODES_REMOVED = 2;
0102: private static final int STRUCTURE_CHANGED = 3;
0103:
0104: //XXX deleteme - string version of the avoid constants debug output:
0105: private static final String[] types = new String[] {
0106: "nodesChanged", "nodesInserted", "nodesRemoved",
0107: "structureChanged" }; //NOI18N
0108:
0109: /** List of table model listeners */
0110: private List tableListeners = new ArrayList();
0111:
0112: /** List of tree model listeners */
0113: private List treeListeners = new ArrayList();
0114:
0115: /** Creates a new instance of EventBroadcaster which will
0116: * produce events for the passed DefaultOutlineModel model. */
0117: public EventBroadcaster(DefaultOutlineModel model) {
0118: setModel(model);
0119: }
0120:
0121: /** Debug logging */
0122: private void log(String method, Object o) {
0123: if (log) {
0124: if (o instanceof TableModelEvent) {
0125: //TableModelEvents just give their hash code in toString()
0126: o = tableModelEventToString((TableModelEvent) o);
0127: }
0128: System.err
0129: .println("EB-"
0130: + (logcount++)
0131: + " "
0132: + method
0133: + ":"
0134: + (o instanceof String ? (String) o : o
0135: .toString()));
0136: }
0137: }
0138:
0139: //***************** Bean properties/convenience getters & setters ************
0140: /** Flag which is set to true while multiple TableModelEvents generated
0141: * from a single TreeModelEvent are being fired, so clients can avoid
0142: * any model queries until all pending changes have been fired. The
0143: * main thing to avoid is any mid-process repaints, which can only happen
0144: * if the response to an event will be to call paintImmediately().
0145: * <p>
0146: * This value is guaranteed to be true for the first of a group of
0147: * related events, and false if tested in response to the final event.
0148: */
0149: public boolean areMoreEventsPending() {
0150: return inMultiEvent;
0151: }
0152:
0153: /** Get the outline model for which this broadcaster will proxy events*/
0154: private DefaultOutlineModel getModel() {
0155: return model;
0156: }
0157:
0158: /** Set the outline model this broadcaster will proxy events for */
0159: private void setModel(DefaultOutlineModel model) {
0160: this .model = model;
0161: }
0162:
0163: /** Convenience getter for the proxied model's layout cache */
0164: private AbstractLayoutCache getLayout() {
0165: return getModel().getLayout();
0166: }
0167:
0168: /** Convenience getter for the proxied model's TreePathSupport */
0169: private TreePathSupport getTreePathSupport() {
0170: return getModel().getTreePathSupport();
0171: }
0172:
0173: /** Convenience getter for the proxied model's user-supplied TreeModel */
0174: private TreeModel getTreeModel() {
0175: return getModel().getTreeModel();
0176: }
0177:
0178: /** Convenience getter for the proxied model's user-supplied TableModel (in
0179: * practice, an instance of ProxyTableModel driven by the tree model and a
0180: * RowModel) */
0181: private TableModel getTableModel() {
0182: return getModel().getTableModel();
0183: }
0184:
0185: //******************* Event source implementation **************************
0186:
0187: /** Add a table model listener. All events fired by this EventBroadcaster
0188: * will have the OutlineModel as the event source */
0189: public synchronized void addTableModelListener(TableModelListener l) {
0190: tableListeners.add(l);
0191: }
0192:
0193: /** Add a tree model listener. All events fired by this EventBroadcaster
0194: * will have the OutlineModel as the event source */
0195: public synchronized void addTreeModelListener(TreeModelListener l) {
0196: treeListeners.add(l);
0197: }
0198:
0199: /** Remove a table model listener. */
0200: public synchronized void removeTableModelListener(
0201: TableModelListener l) {
0202: tableListeners.remove(l);
0203: }
0204:
0205: /** Remove a tree model listener. */
0206: public synchronized void removeTreeModelListener(TreeModelListener l) {
0207: treeListeners.remove(l);
0208: }
0209:
0210: /** Fire a table change to the list of listeners supplied. The event should
0211: * already have its source set to be the OutlineModel we're proxying for. */
0212: private void fireTableChange(TableModelEvent e,
0213: TableModelListener[] listeners) {
0214: //Event may be null for offscreen info, etc.
0215: if (e == null) {
0216: return;
0217: }
0218:
0219: assert (e.getSource() == getModel());
0220:
0221: log("fireTableChange", e);
0222:
0223: for (int i = 0; i < listeners.length; i++) {
0224: listeners[i].tableChanged(e);
0225: }
0226: }
0227:
0228: /** Convenience method to fire a single table change to all listeners */
0229: private void fireTableChange(TableModelEvent e) {
0230: //Event may be null for offscreen info, etc.
0231: if (e == null) {
0232: return;
0233: }
0234: inMultiEvent = false;
0235: TableModelListener[] listeners = getTableModelListeners();
0236: fireTableChange(e, getTableModelListeners());
0237: }
0238:
0239: /** Fires multiple table model events, setting the inMultiEvent flag
0240: * as appropriate. */
0241: private void fireTableChange(TableModelEvent[] e) {
0242: //Event may be null for offscreen info, etc.
0243: if (e == null || e.length == 0) {
0244: return;
0245: }
0246:
0247: TableModelListener[] listeners = getTableModelListeners();
0248: inMultiEvent = e.length > 1;
0249: try {
0250: for (int i = 0; i < e.length; i++) {
0251: fireTableChange(e[i], listeners);
0252: if (i == e.length - 1) {
0253: inMultiEvent = false;
0254: }
0255: }
0256: } finally {
0257: inMultiEvent = false;
0258: }
0259: }
0260:
0261: /** Fetch an array of the currently registered table model listeners */
0262: private TableModelListener[] getTableModelListeners() {
0263: TableModelListener[] listeners = null;
0264: synchronized (this ) {
0265: listeners = new TableModelListener[tableListeners.size()];
0266:
0267: listeners = (TableModelListener[]) tableListeners
0268: .toArray(listeners);
0269: }
0270: return listeners;
0271: }
0272:
0273: /** Fire the passed TreeModelEvent of the specified type to all
0274: * registered TreeModelListeners. The passed event should already have
0275: * its source set to be the model. */
0276: private synchronized void fireTreeChange(TreeModelEvent e, int type) {
0277: //Event may be null for offscreen info, etc.
0278: if (e == null) {
0279: return;
0280: }
0281: assert (e.getSource() == getModel());
0282:
0283: TreeModelListener[] listeners = null;
0284: synchronized (this ) {
0285: listeners = new TreeModelListener[treeListeners.size()];
0286: listeners = (TreeModelListener[]) treeListeners
0287: .toArray(listeners);
0288: }
0289:
0290: log("fireTreeChange-" + types[type], e);
0291:
0292: //Now refire it to any listeners
0293: for (int i = 0; i < listeners.length; i++) {
0294: switch (type) {
0295: case NODES_CHANGED:
0296: listeners[i].treeNodesChanged(e);
0297: break;
0298: case NODES_INSERTED:
0299: listeners[i].treeNodesInserted(e);
0300: break;
0301: case NODES_REMOVED:
0302: listeners[i].treeNodesRemoved(e);
0303: break;
0304: case STRUCTURE_CHANGED:
0305: listeners[i].treeStructureChanged(e);
0306: break;
0307: default:
0308: assert false;
0309: }
0310: }
0311: }
0312:
0313: //******************* Event listener implementations ************************
0314:
0315: /** Process a change event from the user-supplied tree model. This
0316: * method will throw an assertion failure if it receives any event type
0317: * other than TableModelEvent.UPDATE - the ProxyTableModel should never,
0318: * ever fire structural changes - only the tree model is allowed to do
0319: * that. */
0320: public void tableChanged(TableModelEvent e) {
0321: assert SwingUtilities.isEventDispatchThread();
0322: //The *ONLY* time we should see events here is due to user
0323: //data entry. The ProxyTableModel should never change out
0324: //from under us - all structural changes happen through the
0325: //table model.
0326: assert (e.getType() == e.UPDATE) : "Table model should only fire "
0327: + "updates, never structural changes";
0328:
0329: fireTableChange(translateEvent(e));
0330: }
0331:
0332: /** Process a change event from the user-supplied tree model.
0333: * Order of operations:
0334: * <ol><li>Refire the same tree event with the OutlineModel we're
0335: * proxying as the source</li>
0336: * <li>Create one or more table model events (more than one if the
0337: * incoming event affects discontiguous rows) reflecting the effect
0338: * of the tree change</li>
0339: * <li>Call the method with the same signature as this one on the
0340: * layout cache, so it will update its state appropriately</li>
0341: * <li>Fire the generated TableModelEvent(s)</li></ol>
0342: */
0343: public void treeNodesChanged(TreeModelEvent e) {
0344: assert SwingUtilities.isEventDispatchThread();
0345:
0346: fireTreeChange(translateEvent(e), NODES_CHANGED);
0347:
0348: TableModelEvent[] events = translateEvent(e, NODES_CHANGED);
0349: getLayout().treeNodesChanged(e);
0350: fireTableChange(events);
0351: }
0352:
0353: /** Process a node insertion event from the user-supplied tree model
0354: * Order of operations:
0355: * <ol><li>Refire the same tree event with the OutlineModel we're
0356: * proxying as the source</li>
0357: * <li>Create one or more table model events (more than one if the
0358: * incoming event affects discontiguous rows) reflecting the effect
0359: * of the tree change</li>
0360: * <li>Call the method with the same signature as this one on the
0361: * layout cache, so it will update its state appropriately</li>
0362: * <li>Fire the generated TableModelEvent(s)</li></ol>
0363: */
0364: public void treeNodesInserted(TreeModelEvent e) {
0365: assert SwingUtilities.isEventDispatchThread();
0366:
0367: fireTreeChange(translateEvent(e), NODES_INSERTED);
0368:
0369: TableModelEvent[] events = translateEvent(e, NODES_INSERTED);
0370: getLayout().treeNodesInserted(e);
0371: fireTableChange(events);
0372: }
0373:
0374: /** Process a node removal event from the user-supplied tree model
0375: * Order of operations:
0376: * <ol><li>Refire the same tree event with the OutlineModel we're
0377: * proxying as the source</li>
0378: * <li>Create one or more table model events (more than one if the
0379: * incoming event affects discontiguous rows) reflecting the effect
0380: * of the tree change</li>
0381: * <li>Call the method with the same signature as this one on the
0382: * layout cache, so it will update its state appropriately</li>
0383: * <li>Fire the generated TableModelEvent(s)</li></ol>
0384: */
0385: public void treeNodesRemoved(TreeModelEvent e) {
0386: assert SwingUtilities.isEventDispatchThread();
0387:
0388: fireTreeChange(e, NODES_REMOVED);
0389:
0390: TableModelEvent[] events = translateEvent(e, NODES_REMOVED);
0391: getLayout().treeNodesRemoved(e);
0392: fireTableChange(events);
0393: }
0394:
0395: /** Process a structural change event from the user-supplied tree model.
0396: * This will result in a generic "something changed"
0397: * TableModelEvent being fired. */
0398: public void treeStructureChanged(TreeModelEvent e) {
0399: assert SwingUtilities.isEventDispatchThread();
0400:
0401: getLayout().treeStructureChanged(e);
0402: fireTreeChange(e, STRUCTURE_CHANGED);
0403:
0404: //If it's a structural change, we need to dump all our info about the
0405: //existing tree structure - it can be bogus now. Similar to JTree,
0406: //this will have the effect of collapsing all expanded paths. The
0407: //TreePathSupport takes care of dumping the layout cache's copy of
0408: //such data
0409: getTreePathSupport().clear();
0410:
0411: //We will just fire a "Something happened. Go figure out what." event.
0412: fireTableChange(new TableModelEvent(getModel()));
0413: }
0414:
0415: /** Receives a TreeWillCollapse event and constructs a TableModelEvent
0416: * based on the pending changes while the model still reflects the unchanged
0417: * state */
0418: public void treeWillCollapse(TreeExpansionEvent event)
0419: throws ExpandVetoException {
0420: assert SwingUtilities.isEventDispatchThread();
0421:
0422: log("treeWillCollapse", event);
0423:
0424: //Construct the TableModelEvent here, before data structures have
0425: //changed. We will fire it from TreeCollapsed if the change is
0426: //not vetoed.
0427: pendingExpansionEvent = translateEvent(event, false);
0428: log("treeWillCollapse generated ", pendingExpansionEvent);
0429: inProgressEvent = event;
0430: }
0431:
0432: /** Receives a TreeWillExpand event and constructs a TableModelEvent
0433: * based on the pending changes while the model still reflects the unchanged
0434: * state */
0435: public void treeWillExpand(TreeExpansionEvent event)
0436: throws ExpandVetoException {
0437: assert SwingUtilities.isEventDispatchThread();
0438:
0439: log("treeWillExpand", event);
0440:
0441: //Construct the TableModelEvent here, before data structures have
0442: //changed. We will fire it from TreeExpanded if the change is not
0443: //vetoed
0444: pendingExpansionEvent = translateEvent(event, true);
0445:
0446: log("treeWillExpand generated", pendingExpansionEvent);
0447: inProgressEvent = event;
0448: }
0449:
0450: public void treeCollapsed(TreeExpansionEvent event) {
0451: assert SwingUtilities.isEventDispatchThread();
0452:
0453: log("treeExpanded", event);
0454:
0455: //FixedHeightLayoutCache tests if the event is null.
0456: //Don't know how it could be, but there's probably a reason...
0457: if (event != null) {
0458: TreePath path = event.getPath();
0459:
0460: //Tell the layout about the change
0461: if (path != null && getTreePathSupport().isVisible(path)) {
0462: getLayout().setExpandedState(path, false);
0463: }
0464: }
0465:
0466: log("about to fire", pendingExpansionEvent);
0467:
0468: //Now fire a change on the owning row so its display is updated (it
0469: //may have just become an expandable node)
0470: TreePath path = event.getPath();
0471: int row = getLayout().getRowForPath(path);
0472: TableModelEvent evt = new TableModelEvent(getModel(), row, row,
0473: 0, TableModelEvent.UPDATE);
0474: fireTableChange(new TableModelEvent[] { evt,
0475: pendingExpansionEvent });
0476:
0477: pendingExpansionEvent = null;
0478: inProgressEvent = null;
0479: }
0480:
0481: /** Updates the layout to mark the descendants of the events path as also
0482: * expanded if they were the last it was expanded, then fires a table change. */
0483: public void treeExpanded(TreeExpansionEvent event) {
0484: assert SwingUtilities.isEventDispatchThread();
0485:
0486: log("treeExpanded", event);
0487:
0488: //Mysterious how the event could be null, but JTree tests it
0489: //so we will too.
0490: if (event != null) {
0491: updateExpandedDescendants(event.getPath());
0492: }
0493:
0494: log("about to fire", pendingExpansionEvent);
0495:
0496: //Now fire a change on the owning row so its display is updated (it
0497: //may have just become an expandable node)
0498: TreePath path = event.getPath();
0499: int row = getLayout().getRowForPath(path);
0500: TableModelEvent evt = new TableModelEvent(getModel(), row, row,
0501: 0, TableModelEvent.UPDATE);
0502: fireTableChange(new TableModelEvent[] { evt,
0503: pendingExpansionEvent });
0504:
0505: pendingExpansionEvent = null;
0506: inProgressEvent = null;
0507: }
0508:
0509: /** Messaged if the tree expansion event (for which we will have already
0510: * constructed a TableModelEvent) was vetoed; disposes of the constructed
0511: * TableModelEvent in that circumstance. */
0512: public void treeExpansionVetoed(TreeExpansionEvent event,
0513: ExpandVetoException exception) {
0514: assert SwingUtilities.isEventDispatchThread();
0515:
0516: log("treeExpansionVetoed", exception);
0517:
0518: //Make sure the event that was vetoed is the one we're interested in
0519: if (event == inProgressEvent) {
0520: //If so, delete the expansion event we thought we were going
0521: //to use in treeExpanded/treeCollapsed, so that it doesn't
0522: //stick around forever holding references to objects from the
0523: //model
0524: pendingExpansionEvent = null;
0525: inProgressEvent = null;
0526: }
0527: }
0528:
0529: //******************* Support routines for handling events ******************
0530: //do I date myself by using the word "routines"? :-)
0531:
0532: /** Reëexpand descendants of a newly expanded path which were
0533: * expanded the last time their parent was expanded */
0534: private void updateExpandedDescendants(TreePath path) {
0535: getLayout().setExpandedState(path, true);
0536:
0537: TreePath[] descendants = getTreePathSupport()
0538: .getExpandedDescendants(path);
0539:
0540: if (descendants.length > 0) {
0541: for (int i = 0; i < descendants.length; i++) {
0542: getLayout().setExpandedState(descendants[i], true);
0543: }
0544: }
0545: }
0546:
0547: //******************* Event translation routines ****************************
0548:
0549: /** Creates a TableModelEvent identical to the original except that the
0550: * column index has been shifted by +1. This is used to refire events
0551: * from the ProxyTableModel (generated by RowModel.setValueFor()) as
0552: * change events on the OutlineModel. */
0553: private TableModelEvent translateEvent(TableModelEvent e) {
0554: TableModelEvent nue = new TableModelEvent(getModel(), e
0555: .getFirstRow(), e.getLastRow(), e.getColumn() + 1, e
0556: .getType());
0557: return nue;
0558: }
0559:
0560: /** Creates an identical TreeModelEvent with the model we are proxying
0561: * as the event source */
0562: private TreeModelEvent translateEvent(TreeModelEvent e) {
0563: //Create a new TreeModelEvent with us as the source
0564: TreeModelEvent nue = new TreeModelEvent(getModel(),
0565: e.getPath(), e.getChildIndices(), e.getChildren());
0566: return nue;
0567: }
0568:
0569: /** Tranlates a TreeModelEvent into one or more contiguous TableModelEvents
0570: */
0571: private TableModelEvent[] translateEvent(TreeModelEvent e, int type) {
0572:
0573: TreePath path = e.getTreePath();
0574: int row = getLayout().getRowForPath(path);
0575:
0576: //If the node is not expanded, we simply fire a change
0577: //event for the parent
0578: boolean inClosedNode = !getLayout().isExpanded(path);
0579: if (inClosedNode) {
0580: //If the node is closed, no expensive checks are needed - just
0581: //fire a change on the parent node in case it needs to update
0582: //its display
0583: if (row != -1) {
0584: switch (type) {
0585: case NODES_CHANGED:
0586: case NODES_INSERTED:
0587: case NODES_REMOVED:
0588: return new TableModelEvent[] { new TableModelEvent(
0589: getModel(), row, row, 0,
0590: TableModelEvent.UPDATE) };
0591: default:
0592: assert false : "Unknown event type " + type;
0593: }
0594: }
0595: //In a closed node that is not visible, no event needed
0596: return new TableModelEvent[0];
0597: }
0598:
0599: boolean discontiguous = isDiscontiguous(e);
0600:
0601: Object[] blocks;
0602: if (discontiguous) {
0603: blocks = getContiguousIndexBlocks(e, type == NODES_REMOVED);
0604: log("discontiguous " + types[type] + " event",
0605: blocks.length + " blocks");
0606: } else {
0607: blocks = new Object[] { e.getChildIndices() };
0608: }
0609:
0610: TableModelEvent[] result = new TableModelEvent[blocks.length];
0611: for (int i = 0; i < blocks.length; i++) {
0612:
0613: int[] currBlock = (int[]) blocks[i];
0614: switch (type) {
0615: case NODES_CHANGED:
0616: result[i] = createTableChangeEvent(e, currBlock);
0617: break;
0618: case NODES_INSERTED:
0619: result[i] = createTableInsertionEvent(e, currBlock);
0620: break;
0621: case NODES_REMOVED:
0622: result[i] = createTableDeletionEvent(e, currBlock);
0623: break;
0624: default:
0625: assert false : "Unknown event type: " + type;
0626: }
0627: }
0628: log("translateEvent", e);
0629: log("generated table events", new Integer(result.length));
0630: if (log) {
0631: for (int i = 0; i < result.length; i++) {
0632: log(" Event " + i, result[i]);
0633: }
0634: }
0635: return result;
0636: }
0637:
0638: /** Translates tree expansion event into an appropriate TableModelEvent
0639: * indicating the number of rows added/removed at the appropriate index */
0640: private TableModelEvent translateEvent(TreeExpansionEvent e,
0641: boolean expand) {
0642: //PENDING: This code should be profiled - the descendent paths search
0643: //is not cheap, and it might be less expensive (at least if the table
0644: //does not have expensive painting logic) to simply fire a generic
0645: //"something changed" table model event and be done with it.
0646:
0647: TreePath path = e.getPath();
0648:
0649: //Add one because it is a child of the row.
0650: int firstRow = getLayout().getRowForPath(path) + 1;
0651: if (firstRow == -1) {
0652: //This does not mean nothing happened, it may just be that we are
0653: //a large model tree, and the FixedHeightLayoutCache says the
0654: //change happened in a row that is not showing.
0655:
0656: //TODO: Just to make the table scrollbar adjust itself appropriately,
0657: //we may want to look up the number of children in the model and
0658: //fire an event that says that that many rows were added. Waiting
0659: //to see if anybody actually will use this (i.e. fires changes in
0660: //offscreen nodes as a normal part of usage
0661: return null;
0662: }
0663:
0664: //Get all the expanded descendants of the path that was expanded/collapsed
0665: TreePath[] paths = getTreePathSupport().getExpandedDescendants(
0666: path);
0667:
0668: //Start with the number of children of whatever was expanded/collapsed
0669: int count = getTreeModel().getChildCount(
0670: path.getLastPathComponent());
0671:
0672: //Iterate any of the expanded children, adding in their child counts
0673: for (int i = 0; i < paths.length; i++) {
0674: count += getTreeModel().getChildCount(
0675: paths[i].getLastPathComponent());
0676: }
0677:
0678: //Now we can calculate the last row affected for real
0679: int lastRow = firstRow + count - 1;
0680:
0681: //Construct a table model event reflecting this data
0682: TableModelEvent result = new TableModelEvent(getModel(),
0683: firstRow, lastRow, TableModelEvent.ALL_COLUMNS,
0684: expand ? TableModelEvent.INSERT
0685: : TableModelEvent.DELETE);
0686:
0687: return result;
0688: }
0689:
0690: /** Create a change TableModelEvent for the passed TreeModelEvent and the
0691: * contiguous subrange of the TreeModelEvent's getChildIndices() value */
0692: private TableModelEvent createTableChangeEvent(TreeModelEvent e,
0693: int[] indices) {
0694: TableModelEvent result = null;
0695: TreePath path = e.getTreePath();
0696: int row = getLayout().getRowForPath(path);
0697:
0698: int first = indices[0];
0699: int last = indices[indices.length - 1];
0700:
0701: //TODO - does not need to be ALL_COLUMNS, but we need a way to determine
0702: //which column index is the tree
0703: result = new TableModelEvent(getModel(), first, last,
0704: TableModelEvent.ALL_COLUMNS, TableModelEvent.UPDATE);
0705:
0706: return result;
0707: }
0708:
0709: /** Create an insertion TableModelEvent for the passed TreeModelEvent and the
0710: * contiguous subrange of the TreeModelEvent's getChildIndices() value */
0711: private TableModelEvent createTableInsertionEvent(TreeModelEvent e,
0712: int[] indices) {
0713: TableModelEvent result = null;
0714:
0715: log("createTableInsertionEvent", e);
0716:
0717: TreePath path = e.getTreePath();
0718: int row = getLayout().getRowForPath(path);
0719:
0720: boolean realInsert = getLayout().isExpanded(path);
0721:
0722: if (realInsert) {
0723: if (indices.length == 1) {
0724: //Only one index to change, fire a simple event. It
0725: //will be the first index in the array + the row +
0726: //1 because the 0th child of a node is 1 greater than
0727: //its row index
0728: int affectedRow = row + indices[0] + 1;
0729: result = new TableModelEvent(getModel(), affectedRow,
0730: affectedRow, TableModelEvent.ALL_COLUMNS,
0731: TableModelEvent.INSERT);
0732:
0733: } else {
0734: //Find the first and last indices. Add one since it is at
0735: //minimum the first index after the affected row, since it
0736: //is a child of it.
0737: int lowest = indices[0] + 1;
0738: int highest = indices[indices.length - 1] + 1;
0739: result = new TableModelEvent(getModel(), row + lowest,
0740: row + highest, TableModelEvent.ALL_COLUMNS,
0741: TableModelEvent.INSERT);
0742:
0743: }
0744: } else {
0745: //Nodes were inserted in an unexpanded parent. Just fire
0746: //a change for that row and column so that it gets repainted
0747: //in case the node there changed from leaf to non-leaf
0748: result = new TableModelEvent(getModel(), row, row,
0749: TableModelEvent.ALL_COLUMNS); //TODO - specify only the tree column
0750: }
0751: return result;
0752: }
0753:
0754: /** Create a deletion TableModelEvent for the passed TreeModelEvent and the
0755: * contiguous subrange of the TreeModelEvent's getChildIndices() value */
0756: private TableModelEvent createTableDeletionEvent(TreeModelEvent e,
0757: int[] indices) {
0758: TableModelEvent result = null;
0759:
0760: log("createTableDeletionEvent "
0761: + Arrays.asList(toArrayOfInteger(indices)), e);
0762:
0763: TreePath path = e.getTreePath();
0764: int row = getLayout().getRowForPath(path);
0765: if (row == -1) {
0766: //XXX could calculate based on last visible row?
0767: return null;
0768: }
0769:
0770: int countRemoved = indices.length;
0771:
0772: //Get the subset of the children in the event that correspond
0773: //to the passed indices
0774: Object[] children = getChildrenForIndices(e, indices);
0775:
0776: for (int i = 0; i < children.length; i++) {
0777: TreePath childPath = path.pathByAddingChild(children[i]);
0778: if (getTreePathSupport().isExpanded(childPath)) {
0779:
0780: int visibleChildren = getLayout().getVisibleChildCount(
0781: childPath);
0782:
0783: if (log) {
0784: log(childPath + " has ", new Integer(
0785: visibleChildren));
0786: }
0787:
0788: countRemoved += visibleChildren;
0789: }
0790: getTreePathSupport().removePath(path);
0791: }
0792:
0793: //Add in the first index, and add one to it since the 0th
0794: //will have the row index of its parent + 1
0795: int firstRow = row + indices[0] + 1;
0796:
0797: log("firstRow", new Integer(firstRow));
0798: /*
0799: if (countRemoved == 1) {
0800: System.err.println("Only one removed: " + (row + indices[0] + 1));
0801: result = new TableModelEvent (getModel(), firstRow, firstRow,
0802: TableModelEvent.ALL_COLUMNS,
0803: TableModelEvent.DELETE);
0804: } else {
0805: */
0806: System.err.println("Count removed is " + countRemoved);
0807:
0808: int lastRow = firstRow + (countRemoved - 1);
0809:
0810: System.err.println("TableModelEvent: fromRow: " + firstRow
0811: + " toRow: " + lastRow);
0812:
0813: result = new TableModelEvent(getModel(), firstRow, lastRow,
0814: TableModelEvent.ALL_COLUMNS, TableModelEvent.DELETE);
0815: //}
0816:
0817: /* //old code
0818:
0819: //Okay, one or more nodes was removed. The event's tree path
0820: //will be the parent. Now we need to find out about any children
0821: //that were also removed so we can create a TreeModelEvent with
0822: //the right number of removed rows.
0823:
0824: //Note there is a slight impedance mismatch between TreeModel and
0825: //TableModel here - if we're using a large model layout cache,
0826: //we don't actually know what was offscreen - the data is already
0827: //gone from the model, so even if we know it was expanded, we
0828: //can't find out how many children it had.
0829:
0830: //The only thing this really affects is the scrollbar, and in
0831: //fact, the standard JTable UIs will update it correctly, since
0832: //the scrollbar will read getRowCount() to calculate its position.
0833: //In theory, this could break on a hyper-efficient TableUI that
0834: //attempted to manage scrollbar position *only* based on the
0835: //content of table model events. That's pretty unlikely; but if
0836: //it happens, the solution is for Outline.getPreferredSize() to
0837: //proxy the preferred size from the layout cache
0838:
0839: TreePath path = e.getTreePath();
0840: boolean lastRemoveWasExpanded = getTreePathSupport().isExpanded(path);
0841: int countRemoved = 1;
0842:
0843: //See if it's expanded - if it wasn't we're just going to blow
0844: //away one row anyway
0845: if (lastRemoveWasExpanded) {
0846: Object[] kids = e.getChildren();
0847:
0848: //TranslateEvent uses countRemoved to set the TableModelEvent
0849: countRemoved = kids.length;
0850:
0851: //Iterate the removed children
0852: for (int i=0; i < kids.length; i++) {
0853: //Get the child's path
0854: TreePath childPath = path.pathByAddingChild(kids[i]);
0855:
0856: //If it's not expanded, we don't care
0857: if (getTreePathSupport().isExpanded(childPath)) {
0858: //Find the number of *visible* children. This may not
0859: //be all the children, but it's the best information we have.
0860: int visibleChildren =
0861: getLayout().getVisibleChildCount(childPath);
0862:
0863: //add in the number of visible children
0864: countRemoved += visibleChildren;
0865: }
0866: //Kill any references to the dead path to avoid memory leaks
0867: getTreePathSupport().removePath(childPath);
0868: }
0869: }
0870:
0871: //Tell the layout what happened, now that we've mined it for data
0872: //about the visible children of the removed paths
0873: getLayout().treeNodesRemoved(e);
0874:
0875: boolean realRemove = lastRemoveWasExpanded;//getLayout().isExpanded(path);
0876: if (realRemove) {
0877: System.err.println("Nodes removed from open countainer");
0878: int[] indices = e.getChildIndices();
0879:
0880: //Comments in FixedHeightLayoutCache suggest we cannot
0881: //assume array is sorted, though it should be
0882: Arrays.sort(indices);
0883: if (indices.length == 0) {
0884: //well, that's a little weird
0885: return null;
0886: } else if (countRemoved == 1) {
0887: System.err.println("Only one removed: " + (row + indices[0] + 1));
0888: return new TableModelEvent (this, row + indices[0] + 1,
0889: row + indices[0] + 1, TableModelEvent.ALL_COLUMNS,
0890: TableModelEvent.DELETE);
0891: }
0892: System.err.println("Count removed is " + countRemoved);
0893:
0894: //Add in the first index, and add one to it since the 0th
0895: //will have the row index of its parent + 1
0896: int firstRow = row + indices[0] + 1;
0897: int lastRow = firstRow + (countRemoved - 1);
0898:
0899: System.err.println("TableModelEvent: fromRow: " + firstRow + " toRow: " + lastRow);
0900:
0901: return new TableModelEvent (this, firstRow, lastRow,
0902: TableModelEvent.ALL_COLUMNS, TableModelEvent.DELETE);
0903: } else {
0904: System.err.println("Nodes removed from a closed container. Change for row " + row);
0905: //Nodes were removed in an unexpanded parent. Just fire
0906: //a change for that row and column so that it gets repainted
0907: //in case the node there changed from leaf to non-leaf
0908: TableModelEvent evt = new TableModelEvent (this, row, row, 0); //XXX 0 may not be tree column
0909: System.err.println(" Returning " + evt);
0910: return evt;
0911: }
0912: */
0913:
0914: return result;
0915: }
0916:
0917: //**************** Static utility routines *****************************
0918:
0919: /** Determine if the indices referred to by a TreeModelEvent are
0920: * contiguous. If they are not, we will need to generate multiple
0921: * TableModelEvents for each contiguous block */
0922: private static boolean isDiscontiguous(TreeModelEvent e) {
0923: int[] indices = e.getChildIndices();
0924: if (indices.length == 1) {
0925: return false;
0926: }
0927: Arrays.sort(indices);
0928: int lastVal = indices[0];
0929: for (int i = 1; i < indices.length; i++) {
0930: if (indices[i] != lastVal + 1) {
0931: return true;
0932: } else {
0933: lastVal++;
0934: }
0935: }
0936: return false;
0937: }
0938:
0939: /** Returns an array of int[]s each one representing a contiguous set of
0940: * indices in the tree model events child indices - each of which can be
0941: * fired as a single TableModelEvent. The length of the return value is
0942: * the number of TableModelEvents required to represent this TreeModelEvent.
0943: * If reverseOrder is true (needed for remove events, where the last indices
0944: * must be removed first or the indices of later removals will be changed),
0945: * the returned int[]s will be sorted in reverse order, and the order in
0946: * which they are returned will also be from highest to lowest. */
0947: private static Object[] getContiguousIndexBlocks(TreeModelEvent e,
0948: boolean reverseOrder) {
0949: int[] indices = e.getChildIndices();
0950:
0951: //Quick check if there's only one index
0952: if (indices.length == 1) {
0953: return new Object[] { indices };
0954: }
0955:
0956: //The array of int[]s we'll return
0957: ArrayList al = new ArrayList();
0958:
0959: //Sort the indices as requested
0960: if (reverseOrder) {
0961: inverseSort(indices);
0962: } else {
0963: Arrays.sort(indices);
0964: }
0965:
0966: //The starting block
0967: ArrayList currBlock = new ArrayList(indices.length / 2);
0968: al.add(currBlock);
0969:
0970: //The value we'll check against the previous one to detect the
0971: //end of contiguous segment
0972: int lastVal = -1;
0973:
0974: //Iterate the indices
0975: for (int i = 0; i < indices.length; i++) {
0976: if (i != 0) {
0977: //See if we've hit a discontinuity
0978: boolean newBlock = reverseOrder ? indices[i] != lastVal - 1
0979: : indices[i] != lastVal + 1;
0980:
0981: if (newBlock) {
0982: currBlock = new ArrayList(indices.length - 1);
0983: al.add(currBlock);
0984: }
0985: }
0986: currBlock.add(new Integer(indices[i]));
0987: lastVal = indices[i];
0988: }
0989:
0990: for (int i = 0; i < al.size(); i++) {
0991: ArrayList curr = (ArrayList) al.get(i);
0992: Integer[] ints = (Integer[]) curr.toArray(new Integer[0]);
0993:
0994: al.set(i, toArrayOfInt(ints));
0995: }
0996:
0997: return al.toArray();
0998: }
0999:
1000: /** Get the children from a TreeModelEvent associated with the set of
1001: * indices passed. */
1002: private Object[] getChildrenForIndices(TreeModelEvent e,
1003: int[] indices) {
1004: //XXX performance - better way to do this may be to have
1005: //getContinguousIndexBlocks instead construct sub-treemodelevents -
1006: //that would save having to do these iterations later to extract the
1007: //children.
1008:
1009: //At the same time, discontiguous child removals are relatively rare
1010: //events - optimizing them heavily may not be a good use of time.
1011: Object[] children = e.getChildren();
1012: int[] allIndices = e.getChildIndices();
1013:
1014: ArrayList al = new ArrayList();
1015:
1016: for (int i = 0; i < indices.length; i++) {
1017: int pos = Arrays.binarySearch(allIndices, indices[i]);
1018: if (pos > -1) {
1019: al.add(children[pos]);
1020: }
1021: if (al.size() == indices.length) {
1022: break;
1023: }
1024: }
1025: return al.toArray();
1026: }
1027:
1028: /** Converts an Integer[] to an int[] */
1029: private static int[] toArrayOfInt(Integer[] ints) {
1030: int[] result = new int[ints.length];
1031: for (int i = 0; i < ints.length; i++) {
1032: result[i] = ints[i].intValue();
1033: }
1034: return result;
1035: }
1036:
1037: /** Converts an Integer[] to an int[] */
1038: //XXX deleteme - used for debug logging only
1039: private static Integer[] toArrayOfInteger(int[] ints) {
1040: Integer[] result = new Integer[ints.length];
1041: for (int i = 0; i < ints.length; i++) {
1042: result[i] = new Integer(ints[i]);
1043: }
1044: return result;
1045: }
1046:
1047: /** Sort an array of ints from highest to lowest */
1048: private static void inverseSort(int[] array) {
1049: //XXX replace with a proper sort algorithm at some point -
1050: //this is brute force
1051: for (int i = 0; i < array.length; i++) {
1052: array[i] *= -1;
1053: }
1054: Arrays.sort(array);
1055: for (int i = 0; i < array.length; i++) {
1056: array[i] *= -1;
1057: }
1058: }
1059:
1060: private static String tableModelEventToString(TableModelEvent e) {
1061: StringBuffer sb = new StringBuffer();
1062: sb.append("TableModelEvent ");
1063: switch (e.getType()) {
1064: case TableModelEvent.INSERT:
1065: sb.append("insert ");
1066: break;
1067: case TableModelEvent.DELETE:
1068: sb.append("delete ");
1069: break;
1070: case TableModelEvent.UPDATE:
1071: sb.append("update ");
1072: break;
1073: default:
1074: sb.append("Unknown type " + e.getType());
1075: }
1076: sb.append("from ");
1077: switch (e.getFirstRow()) {
1078: case TableModelEvent.HEADER_ROW:
1079: sb.append("header row ");
1080: break;
1081: default:
1082: sb.append(e.getFirstRow());
1083: sb.append(' ');
1084: }
1085: sb.append("to ");
1086: sb.append(e.getLastRow());
1087: sb.append(" column ");
1088: switch (e.getColumn()) {
1089: case TableModelEvent.ALL_COLUMNS:
1090: sb.append("ALL_COLUMNS");
1091: break;
1092: default:
1093: sb.append(e.getColumn());
1094: }
1095: return sb.toString();
1096: }
1097: }
|