1 /*
2 Wotonomy: OpenStep design patterns for pure Java applications.
3 Copyright (C) 2001 Intersect Software Corporation
4
5 This library is free software; you can redistribute it and/or
6 modify it under the terms of the GNU Lesser General Public
7 License as published by the Free Software Foundation; either
8 version 2.1 of the License, or (at your option) any later version.
9
10 This library is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 Lesser General Public License for more details.
14
15 You should have received a copy of the GNU Lesser General Public
16 License along with this library; if not, see http://www.gnu.org
17 */
18
19 package net.wotonomy.ui.swing;
20
21 import java.awt.Component;
22 import java.lang.ref.Reference;
23 import java.lang.ref.WeakReference;
24 import java.util.Enumeration;
25 import java.util.Iterator;
26 import java.util.LinkedList;
27 import java.util.List;
28 import java.util.Vector;
29
30 import javax.swing.tree.TreePath;
31
32 import net.wotonomy.control.EODataSource;
33 import net.wotonomy.control.EODelayedObserver;
34 import net.wotonomy.control.EOEditingContext;
35 import net.wotonomy.control.EOObjectStore;
36 import net.wotonomy.control.EOObserverCenter;
37 import net.wotonomy.control.EOQualifier;
38 import net.wotonomy.control.PropertyDataSource;
39 import net.wotonomy.foundation.NSArray;
40 import net.wotonomy.foundation.NSDictionary;
41 import net.wotonomy.foundation.NSMutableDictionary;
42 import net.wotonomy.foundation.NSNotification;
43 import net.wotonomy.foundation.internal.ValueConverter;
44 import net.wotonomy.foundation.internal.WotonomyException;
45 import net.wotonomy.ui.EODisplayGroup;
46 import net.wotonomy.ui.swing.TreeModelAssociation.DelegatingTreeDataSource;
47
48 /***
49 * DisplayGroupNodes are used as nodes in the
50 * TreeModelAssociation's implementation of TreeModel,
51 * and is tightly coupled with TreeModelAssociation
52 * and MasterDetailAssociation. <br><br>
53 *
54 * Even though it is no longer package access,
55 * don't rely on this class because we want to
56 * have the option of completely replacing this
57 * approach in the future.
58 *
59 * @author michael@mpowers.net
60 * @author $Author: cgruber $
61 * @version $Revision: 904 $
62 */
63 abstract public class DisplayGroupNode
64 extends EODisplayGroup
65 {
66 protected TreeModelAssociation parentAssociation;
67 protected EODelayedObserver targetObserver;
68 protected NSMutableDictionary childNodes;
69 protected EODisplayGroup parentGroup;
70 protected Object target;
71 protected boolean isFetched;
72 protected boolean isFetchNeeded;
73 protected boolean useParentOrderings;
74 protected boolean useParentQualifier;
75
76 /***
77 * Constructor for all nodes.
78 * Root node must have a null target.
79 */
80 public DisplayGroupNode(
81 TreeModelAssociation aParentAssociation,
82 EODisplayGroup aParentGroup,
83 Object aTarget )
84 {
85 //new net.wotonomy.ui.swing.util.StackTraceInspector( ""+aTarget );
86 //System.out.println( "DisplayGroupNode.new: " + aTarget );
87 parentAssociation = aParentAssociation;
88 target = null;
89 targetObserver = null;
90 parentGroup = aParentGroup;
91 childNodes = new NSMutableDictionary();
92 isFetched = false;
93 isFetchNeeded = false;
94 useParentOrderings = true;
95 useParentQualifier = true;
96
97 EODataSource parentSource = null;
98 if ( parentGroup != null )
99 {
100 parentSource = parentGroup.dataSource();
101 }
102 else
103 if ( parentAssociation.titlesDisplayGroup != null )
104 {
105 parentSource = parentAssociation.titlesDisplayGroup.dataSource();
106 }
107
108 // create child datasource
109 if ( aTarget != null ) // not root node
110 {
111 if ( parentAssociation.childrenKey != null )
112 {
113 if ( parentSource == null )
114 {
115 throw new WotonomyException(
116 "Need a data source when children aspect is bound." );
117 }
118
119 NSArray displayedObjects = parentGroup.displayedObjects();
120 EODataSource childSource = parentSource.dataSourceQualifiedByKey(
121 parentAssociation.childrenKey );
122 childSource.qualifyWithRelationshipKey(
123 parentAssociation.childrenKey, aTarget );
124
125 // create new display group using child data source
126 this.setDataSource( childSource );
127
128 // establish observer for target object
129 setTarget( aTarget );
130 }
131 else // only titles is bound
132 {
133 // establish observer for target object
134 setTarget( aTarget );
135
136 setDataSource( new PropertyDataSource()
137 {
138 public NSArray fetchObjects()
139 {
140 return new NSArray();
141 }
142 } );
143 }
144 }
145 else // else root node
146 {
147 // root node uses PropertyDataSource by default
148 if ( parentSource == null )
149 {
150 setDataSource( new PropertyDataSource()
151 {
152 public NSArray fetchObjects()
153 {
154 if ( parentGroup != null )
155 {
156 return parentGroup.displayedObjects();
157 }
158 return null;
159 }
160 } );
161 }
162 else
163 {
164 // root node uses parent source directly
165 setDataSource( parentSource );
166 }
167 }
168 }
169
170 /***
171 * Overridden to unregister as an editor of the editing context,
172 * since we don't directly present a user interface.
173 */
174 public void setDataSource ( EODataSource aDataSource )
175 {
176 super.setDataSource( aDataSource );
177 if ( ( aDataSource != null )
178 && ( aDataSource.editingContext() != null ) )
179 {
180 aDataSource.editingContext().removeEditor( this );
181 }
182 }
183
184 /***
185 * Returns whether the node should call fetch().
186 */
187 protected boolean isFetched()
188 {
189 if ( isFetchNeeded() )
190 {
191 setFetchNeeded( false );
192 fetch();
193 }
194 return isFetched;
195 }
196
197 /***
198 * Sets whether the node should call fetch().
199 */
200 protected void setFetched( boolean fetched )
201 {
202 //System.out.println( "DisplayGroupNode.setFetched: " + fetched + " : " + this + " : " + target );
203 //net.wotonomy.ui.swing.util.StackTraceInspector.printShortStackTrace();
204 isFetched = fetched;
205 }
206
207 /***
208 * Returns whether the node is in need of a refetch.
209 */
210 protected boolean isFetchNeeded()
211 {
212 return isFetchNeeded;
213 }
214
215 /***
216 * Returns whether the node should call fetch().
217 */
218 protected void setFetchNeeded( boolean fetchNeeded )
219 {
220 //System.out.println( "DisplayGroupNode.setFetchNeeded: " + fetchNeeded + " : " + this + " : " + target );
221 //net.wotonomy.ui.swing.util.StackTraceInspector.printShortStackTrace();
222 isFetchNeeded = fetchNeeded;
223 }
224
225 /***
226 * Subclasses should override this method to fire an appropriate insertion event.
227 */
228 protected void fireNodesInserted( Object[] path, int[] indexes, Object[] objects )
229 {
230 //System.out.println( "fireNodesInserted: " + this );
231 parentAssociation.fireTreeNodesInserted(
232 this, path, indexes, objects );
233 }
234
235 /***
236 * Subclasses should override this method to fire an appropriate change event.
237 */
238 protected void fireNodesChanged( Object[] path, int[] indexes, Object[] objects )
239 {
240 //System.out.println( "fireNodesChanged: " + this );
241 parentAssociation.fireTreeNodesChanged(
242 this, path, indexes, objects );
243 }
244
245 /***
246 * Subclasses should override this method to fire an appropriate deletion event.
247 */
248 protected void fireNodesRemoved( Object[] path, int[] indexes, Object[] objects )
249 {
250 //System.out.println( "fireNodesRemoved: " + this );
251 parentAssociation.fireTreeNodesRemoved(
252 this, path, indexes, objects );
253 }
254
255 /***
256 * Subclasses should override this method to fire an appropriate event.
257 */
258 protected void fireStructureChanged( Object[] path, int[] indexes, Object[] objects )
259 {
260 parentAssociation.fireTreeStructureChanged(
261 this, path, indexes, objects );
262 }
263
264 /***
265 * Overridden to broadcast a tree event after super executes.
266 */
267 public void insertObjectAtIndex ( Object anObject, int anIndex )
268 {
269 int count = getChildCount(); // gets old count
270 if ( target == null )
271 {
272 // if root node, forward to parent:
273 // circumventing delegating data source, if any
274 EODataSource dataSource = parentGroup.dataSource();
275 if ( dataSource instanceof DelegatingTreeDataSource )
276 {
277 parentGroup.setDataSource(
278 ((DelegatingTreeDataSource)dataSource).delegateDataSource );
279 }
280 parentGroup.insertObjectAtIndex( anObject, anIndex );
281 if ( dataSource instanceof DelegatingTreeDataSource )
282 {
283 parentGroup.setDataSource( dataSource );
284 }
285 return; // prevent event from firing (?)
286 }
287 else // not root node
288 {
289 super.insertObjectAtIndex( anObject, anIndex );
290 }
291 }
292
293 /***
294 * Overridden to broadcast a tree event after super executes.
295 */
296 public boolean deleteObjectAtIndex ( int anIndex )
297 {
298 boolean result;
299 Object node = getChildNodeAt( anIndex );
300 if ( target == null )
301 {
302 // if root node, forward to parent:
303 result = parentGroup.deleteObjectAtIndex( anIndex );
304 }
305 else // not root node
306 {
307 result = super.deleteObjectAtIndex( anIndex );
308 }
309
310 return result;
311 }
312
313 /***
314 * Returns the child node that corresponds to the
315 * specified index, creating it if necessary.
316 * The index must be within bounds or an exception
317 * is thrown.
318 */
319 public DisplayGroupNode getChildNodeAt( int anIndex )
320 {
321 boolean wasFetched = isFetched();
322 if ( ! wasFetched ) fetch();
323 Object o = displayedObjects.objectAtIndex( anIndex );
324 DisplayGroupNode result = getChildNodeForObject( o );
325 if ( result == null )
326 {
327 result = createChildNodeForObject( o );
328 }
329 return result;
330 }
331
332 /***
333 * Returns a child node that corresponds to the
334 * specified object, returning null if not found.
335 */
336 protected DisplayGroupNode getChildNodeForObject( Object anObject )
337 {
338 return (DisplayGroupNode)
339 childNodes.objectForKey( new ReferenceKey( anObject ) );
340 }
341
342 /***
343 * Creates a child node that corresponds to the
344 * specified object.
345 */
346 private DisplayGroupNode createChildNodeForObject( Object anObject )
347 {
348 DisplayGroupNode result = parentAssociation.createNode( this, anObject );
349 childNodes.setObjectForKey( result, new ReferenceKey( anObject ) );
350 return result;
351 }
352
353 /***
354 * Returns a tree path of all DisplayGroupNodes leading
355 * to this node, including the root node (but excluding the
356 * titles display group).
357 */
358 public TreePath treePath()
359 {
360 List path = new LinkedList();
361 EODisplayGroup node = this;
362 while ( node instanceof DisplayGroupNode )
363 {
364 // insert at head of list
365 path.add( 0, node );
366 node = ((DisplayGroupNode)node).parentGroup;
367 }
368 return new TreePath( path.toArray() );
369 }
370
371 /***
372 * Overridden to return the parent group's
373 * sort ordering if useParentOrderings is true.
374 * useParentOrderings is true by default.
375 */
376 public NSArray sortOrderings()
377 {
378 if ( ( useParentOrderings )
379 && ( parentGroup != null ) )
380 {
381 return parentGroup.sortOrderings();
382 }
383 return super.sortOrderings();
384 }
385
386 /***
387 * Overridden to set useParentOrderings to false,
388 * or true if aList is null.
389 */
390 public void setSortOrderings ( List aList )
391 {
392 if ( aList == null )
393 {
394 useParentOrderings = true;
395 }
396 else
397 {
398 useParentOrderings = false;
399 super.setSortOrderings( aList );
400 }
401 }
402
403 /***
404 * Overridden to return the parent group's
405 * qualifier if useParentQualifier is true.
406 * useParentQualifier is true by default.
407 */
408 public EOQualifier qualifier()
409 {
410 if ( ( useParentQualifier )
411 && ( parentGroup != null ) )
412 {
413 return parentGroup.qualifier();
414 }
415 return super.qualifier();
416 }
417
418 /***
419 * Overridden to set useParentQualifier to false,
420 * or true if aList is null.
421 */
422 public void setQualifier ( EOQualifier aQualifier )
423 {
424 if ( aQualifier == null )
425 {
426 useParentQualifier = true;
427 }
428 else
429 {
430 useParentQualifier = false;
431 super.setQualifier( aQualifier );
432 }
433 }
434
435 /***
436 * Overridden to set isFetched to true.
437 */
438 public boolean fetch()
439 {
440 //System.out.println( "DisplayGroupNode.fetch: " + this + " : " );
441 //if ( getClass().getName().indexOf( "Activity" ) != -1 )
442 //{
443 // new net.wotonomy.ui.swing.util.StackTraceInspector( this.toString() );
444 //}
445 // set flag
446 setFetched( true );
447
448 // skip root node
449 if ( target == null ) return true;
450
451 // requalify
452 dataSource().qualifyWithRelationshipKey(
453 parentAssociation.childrenKey, target );
454
455 // call to super
456 return super.fetch();
457
458 //boolean result = super.fetch();
459 //System.out.println( displayedObjects() );
460 //return result;
461 }
462
463 /***
464 * Returns the object at the appropriate index
465 * in the parent display group.
466 */
467 public Object object()
468 {
469 // if root node
470 if ( target == null )
471 {
472 return parentAssociation.rootLabel();
473 }
474 return target;
475 }
476
477 /***
478 * Returns the string value of the title property
479 * on the object in the parent display group corresponding
480 * to this index. The tree renderer asks JTrees to
481 * call this method to retrieve a value for display.
482 */
483 public String toString()
484 {
485 Object result = getUserObject();
486 if ( result == null ) result = "[null]";
487 return result.toString();
488 }
489
490 // parts of interface TreeNode
491
492 public int getChildCount()
493 {
494 if ( ! isFetched() ) fetch();
495 //if ( toString().indexOf("154.16406")!=-1){
496 //System.out.println( "getChildCount: " + displayedObjects.count() + " : " + this );
497 //new RuntimeException().printStackTrace();
498 //net.wotonomy.ui.swing.util.StackTraceInspector.printShortStackTrace();
499 //}
500 return displayedObjects.count();
501 }
502
503 public int getIndex(DisplayGroupNode node)
504 {
505 if ( ! isFetched() ) fetch();
506 return displayedObjects.indexOfObject(
507 ((DisplayGroupNode)node).target );
508 }
509
510 public boolean getAllowsChildren()
511 {
512 return true;
513 }
514
515 public boolean isLeaf()
516 {
517 // if not root node and isLeaf aspect is bound
518 if ( ( target != null )
519 && ( parentGroup != null )
520 && ( parentAssociation.leafKey != null ) )
521 {
522 Object value;
523 if ( parentAssociation.leafDisplayGroup != null )
524 {
525 value = parentGroup.valueForObject(
526 target, parentAssociation.leafKey );
527 }
528 else
529 {
530 value = parentAssociation.leafKey;
531 }
532
533 // getBoolean returns true for zero, among other things
534 Object result = ValueConverter.getBoolean( value );
535 if ( result != null )
536 {
537 return ((Boolean)result).booleanValue();
538 }
539 }
540
541 // otherwise, we have to fetch and return count
542 return ( getChildCount() == 0 );
543 }
544
545 public Enumeration children()
546 {
547 int count = getChildCount();
548 Vector v = new Vector();
549 for ( int i = 0; i < count; i++ )
550 {
551 v.add( getChildNodeAt( i ) );
552 }
553 return v.elements();
554 }
555
556 // parts of interface MutableTreeNode
557
558 public void insert(DisplayGroupNode aChild, int anIndex)
559 {
560 insertObjectAtIndex(
561 ((DisplayGroupNode)aChild).object(), anIndex );
562 }
563
564 public void remove(int index)
565 {
566 deleteObjectAtIndex( index );
567 }
568
569 /***
570 * Removes the node at the index corresponding
571 * to the index of the object.
572 */
573 public void remove(DisplayGroupNode node)
574 {
575 remove( getIndex( node ) );
576 }
577
578 /***
579 * Removes our object from the parent display group.
580 */
581 public void removeFromParent()
582 {
583 int index = parentGroup.displayedObjects().indexOfIdenticalObject( target );
584 if ( index != NSArray.NotFound )
585 {
586 parentGroup.deleteObjectAtIndex( index );
587 }
588 else
589 {
590 throw new WotonomyException(
591 "Object not found in parent group: " + target );
592 }
593 }
594
595 /***
596 * Removes our object from the parent display group
597 * and adds it to the end of the specified node's children.
598 */
599 public void setParent(DisplayGroupNode newParent)
600 {
601 removeFromParent();
602 newParent.insertObjectAtIndex(
603 object(), newParent.displayedObjects.size() );
604 }
605
606 /***
607 * Returns the value of the displayed property in the parent display group
608 * at the index that corresponds to the index of this node.
609 */
610 public Object getUserObject()
611 {
612 return valueForKey( parentAssociation.titlesKey );
613 }
614
615 /***
616 * Sets the value of the displayed property in the parent display group
617 * at the index that corresponds to the index of this node.
618 */
619 public void setUserObject( Object aValue )
620 {
621 setValueForKey( aValue, parentAssociation.titlesKey );
622 }
623
624 /***
625 * Returns a value from the object in the parent display group
626 * at the index that corresponds to the index of this node.
627 * For the root node, if the titles key is specified, the root
628 * label is returned, otherwise null is returned.
629 */
630 public Object valueForKey( String aKey )
631 {
632 // if root node
633 if ( target == null )
634 {
635 // compare by ref is okay for strings
636 if ( aKey == parentAssociation.titlesKey )
637 {
638 return parentAssociation.rootLabel();
639 }
640 return null;
641 }
642 return parentGroup.valueForObject( target, aKey );
643 }
644
645 /***
646 * Sets a value on the object in the parent display group
647 * at the index that corresponds to the index of this node.
648 * For the root node, this method only works if aKey is the
649 * titlesAspect's key, otherwise does nothing.
650 */
651 public void setValueForKey(Object aValue, String aKey)
652 {
653 // if root node, return.
654 if ( target == null )
655 {
656 // compare by ref is okay for strings
657 if ( aKey == parentAssociation.titlesKey )
658 {
659 parentAssociation.setRootLabel( aValue );
660
661 // how to handle root node? tree event docs don't say.
662 fireNodesChanged ( treePath().getPath(),
663 new int[] { 0 },
664 new Object[] { this } );
665 }
666 return;
667 }
668
669 parentGroup.setValueForObject(
670 aValue, target, aKey );
671 }
672
673 /***
674 * Perform any clean up in this method.
675 * The node will not be reused after this method is called.
676 * This implementation removes itself from the parent's
677 * set of child nodes, sets target and datasource to null,
678 * and then calls disposeChildNodes().
679 */
680 protected void dispose()
681 { //System.out.println( "dispose: " + this.getClass().getName() + " : " + this );
682 if ( parentGroup != null )
683 {
684 ((DisplayGroupNode)parentGroup).childNodes.remove(
685 new ReferenceKey( target ) );
686 }
687 setTarget( (Object) null );
688 setDataSource( null );
689 disposeChildNodes();
690 }
691
692 /***
693 * Calls dispose() on all child nodes.
694 */
695 protected void disposeChildNodes()
696 {
697 Iterator i = new LinkedList(childNodes.values()).iterator();
698 while ( i.hasNext() )
699 {
700 ((DisplayGroupNode) i.next()).dispose();
701 }
702 }
703
704 /***
705 * Called after the target object posts a change notification.
706 * This implementation re-fetches which triggers
707 * updateDisplayedObjects to broadcast any tree events.
708 * This method marks the parent object as changed if:
709 * (1) this object is not registered in the editing context
710 * of the titles display group's data source (if any), AND
711 * (2) the children key is not in the list of attributes
712 * of the parent object's EOClassDescription.
713 */
714 public void targetChanged()
715 {
716 // if not root node
717 if ( target != null )
718 {
719 // if we're not root and not fetched, stop here.
720 //FIXME: with this, some nodes have old values when moved.
721 //FIXME: without this, nodes are unnecessarily fetched.
722 //FIXME: might have parent modify isFetched of certain child nodes.
723 if ( isFetched() )
724 {
725 fetch();
726 }
727 else // not fetched - just update the display
728 {
729 updateDisplayedObjects();
730 }
731 /*
732 //disabling this for performance reasons:
733 //might reenable later or find an alternate approach
734 // check to see if we need to mark the parent object as changed
735 EOEditingContext context = dataSource().editingContext();
736 if ( ( context == null )
737 || ( context.globalIDForObject( target ) == null ) )
738 {
739 DisplayGroupNode parentNode = (DisplayGroupNode) parentGroup;
740 if ( parentNode.target != null )
741 {
742 // only notify if childrenKey is an attribute of parentDesc
743 // (and therefore not a toOne or toMany relationship)
744 EOClassDescription parentDesc =
745 EOClassDescription.classDescriptionForClass(
746 parentNode.target.getClass() );
747 if ( parentDesc.attributeKeys().contains( parentAssociation.childrenKey ) )
748 {
749 // only notify if no context is already observing the object
750 // and we are an attribute key
751 EOObserverCenter.notifyObserversObjectWillChange( parentNode.target );
752 }
753 }
754 }
755 */
756 }
757 else // root node
758 {
759 setObjectArray( parentAssociation.titlesDisplayGroup.displayedObjects() );
760 }
761
762 // finally, broadcast change event for this node
763 // even though we're not sure if the displayed value changed.
764 fireNodeChanged();
765 }
766
767 /***
768 * Fires a change event for this node.
769 */
770 public void fireNodeChanged()
771 {
772 // if not root node
773 if ( target != null )
774 {
775 int index = ((DisplayGroupNode)parentGroup).getIndex( this );
776 if ( ( index != -1 )
777 && ( treePath().getParentPath() != null ) )
778 {
779 fireNodesChanged (
780 treePath().getParentPath().getPath(),
781 new int[] { index },
782 new Object[] { this } );
783 }
784 }
785 }
786
787 Object[] previouslyDisplayedObjects = new Object[0];
788 /***
789 * Overridden to call to super, fire any tree events, and then
790 * call updateDisplayedObjects on all fetched child nodes.
791 * This method compares this node's displayed objects against
792 * the list of child nodes, synchronizes them, and then broadcasts
793 * only the necessary events to bring the view component up to date.
794 */
795 public void updateDisplayedObjects()
796 {
797 //System.out.println( "updateDisplayedObjects: " + " : " + this );
798 //net.wotonomy.ui.swing.util.StackTraceInspector.printShortStackTrace();
799 //new RuntimeException().printStackTrace();
800 super.updateDisplayedObjects();
801
802 // diff lists
803 boolean proceed = true;
804 Object[] oldObjects = previouslyDisplayedObjects;
805 Object[] newObjects = displayedObjects.toArray();
806 if ( oldObjects.length == newObjects.length )
807 {
808 proceed = false;
809 for ( int i = 0; i < newObjects.length; i++ )
810 {
811 if ( oldObjects[i] != newObjects[i] )
812 {
813 proceed = true;
814 break;
815 }
816 }
817 }
818
819 // this should be set before firing the change events
820 // in case some clients end up calling this again.
821 previouslyDisplayedObjects = newObjects;
822
823 DisplayGroupNode node;
824 Iterator i = childNodes.values().iterator();
825 while ( i.hasNext() )
826 {
827 node = (DisplayGroupNode) i.next();
828 if ( !node.isFetchNeeded() )
829 {
830 node.updateDisplayedObjects();
831 }
832 }
833
834 if ( proceed )
835 {
836 //System.out.println( "DisplayGroupNode.firingEventsForChanges: " );
837 //new RuntimeException().printStackTrace();
838 fireEventsForChanges( oldObjects, newObjects );
839 }
840
841 }
842
843 /***
844 * Called by processRecentChanges to analyze the
845 * differences between the lists and broadcast the
846 * appropriate events.
847 */
848 protected void fireEventsForChanges(
849 Object[] oldObjects, Object[] newObjects )
850 {
851 // structure changed causes havoc while
852 // establishing connection in some cases
853 //if ( oldObjects.length == 0 || newObjects.length == 0 )
854 //{
855 // fireStructureChanged( treePath().getPath(), null, null );
856 // return;
857 //}
858
859 int insertCount = 0;
860 int deleteCount = 0;
861 Object[] inserts = new Object[ newObjects.length ];
862 Object[] deletes = new Object[ oldObjects.length ];
863
864 int i;
865 int n = -1, o = -1; // last match
866 int n1 = 0, o1 = 0; // current match test
867 int n2 = 0, o2 = 0; // scan ahead
868
869 while ( o1 < oldObjects.length && n1 < newObjects.length )
870 {
871 if ( newObjects[n1] == oldObjects[o1] )
872 {
873 // mark as match and continue
874 o = o1;
875 n = n1;
876 }
877 else
878 {
879 // scan ahead for the next match, if any
880 o2 = o1;
881 n2 = n1;
882
883 while ( o2 < oldObjects.length || n2 < newObjects.length )
884 {
885 if ( o2 < oldObjects.length && newObjects[n1] == oldObjects[o2] )
886 {
887 // run o1 to o2: mark as deletes
888 for ( i = o1; i < o2; i++ )
889 { // System.out.println( "delete : " + i );
890 deletes[i] = oldObjects[i];
891 deleteCount++;
892 }
893 o1 = o2; // reset test
894 o = o1; // set match
895 n = n1; // set match
896 break;
897 }
898 if ( n2 < newObjects.length && newObjects[n2] == oldObjects[o1] )
899 {
900 // run n1 to n2: mark as inserts
901 for ( i = n1; i < n2; i++ )
902 { // System.out.println( "insert : " + i );
903 inserts[i] = newObjects[i];
904 insertCount++;
905 }
906 n1 = n2; // reset test
907 n = n1; // set match
908 o = o1; // set match
909 break;
910 }
911 o2++;
912 n2++;
913 }
914 }
915 if (n != n1)
916 {
917 inserts[n1] = newObjects[n1];
918 insertCount++;
919 deletes[o1] = oldObjects[o1];
920 deleteCount++;
921 //increment even though no match:
922 //the new object was marked as inserted and
923 //the old object was marked as deleted.
924 n = n1;
925 o = o1;
926 }
927 o1++;
928 n1++;
929 }
930
931 // run o to end of oldObjects: mark as deletes
932 for ( i = o+1; i < oldObjects.length; i++ )
933 { // System.out.println( "delete : " + i );
934 deletes[i] = oldObjects[i];
935 deleteCount++;
936 }
937
938 // run n to end of newObjects: mark as inserts
939 for ( i = n+1; i < newObjects.length; i++ )
940 { // System.out.println( "insert : " + i );
941 inserts[i] = newObjects[i];
942 insertCount++;
943 }
944
945 //System.out.println( "done : "
946 //+ o + " : " + o1 + " : " + o2 + " :: " + n + " : " + n1 + " : " + n2 );
947 //System.out.println( new NSArray( newObjects ) );
948 //System.out.println( new NSArray( inserts ) );
949 //System.out.println( new NSArray( deletes ) );
950 //System.out.println( new NSArray( oldObjects ) );
951
952 int c;
953 Object[] nodes;
954 int[] indices;
955
956 // broadcast delete event
957 c = 0;
958 nodes = new Object[ deleteCount ];
959 indices = new int[ deleteCount ];
960 for ( i = 0; i < deletes.length; i++ )
961 {
962 if ( deletes[i] != null )
963 {
964 indices[c] = i;
965 nodes[c] = getChildNodeForObject( deletes[i] );
966 c++;
967 }
968 }
969 if ( c > 0 )
970 {
971 // fireNodeChanged(); // force the jtree to get the correct child count
972 fireNodesRemoved( treePath().getPath(), indices, nodes );
973 }
974 deletes = nodes; // retain for dispose check
975
976 // broadcast insert event
977 c = 0;
978 nodes = new Object[ insertCount ];
979 indices = new int[ insertCount ];
980 for ( i = 0; i < inserts.length; i++ )
981 {
982 if ( inserts[i] != null )
983 {
984 indices[c] = i;
985 nodes[c] = getChildNodeForObject( inserts[i] );
986 if ( nodes[c] == null )
987 {
988 nodes[c] = createChildNodeForObject( newObjects[i] );
989 }
990 c++;
991 }
992 }
993 if ( c > 0 )
994 {
995 fireNodesInserted( treePath().getPath(), indices, nodes );
996 }
997
998 // dispose any delete nodes not on insert list
999 int j;
1000 boolean found;
1001 for ( i = 0; i < deletes.length; i++ )
1002 {
1003 for ( j = 0; j < nodes.length; j++ )
1004 {
1005 if ( deletes[i] == nodes[j] ) break;
1006 }
1007
1008 // did not break early, so not found, so dispose
1009 if ( j == nodes.length )
1010 {
1011 ((DisplayGroupNode)deletes[i]).dispose();
1012 }
1013 }
1014 }
1015
1016 /***
1017 * Sets the target object and creates an registers a target observer.
1018 * If target was not previously null, the existing observer is unregistered.
1019 * Protected access so subclasses and TreeModelAssociation can update our target.
1020 */
1021 public void setTarget( Object aTarget )
1022 {
1023 if ( target != null )
1024 {
1025 EOObserverCenter.removeObserver( targetObserver, target );
1026 targetObserver.discardPendingNotification();
1027 }
1028
1029 if ( aTarget != null )
1030 {
1031 target = aTarget;
1032 targetObserver = new TargetObserver( this );
1033 EOObserverCenter.addObserver( targetObserver, target );
1034 }
1035 }
1036
1037 /***
1038 * Returns the parent display group, or null if parent is root.
1039 */
1040 public DisplayGroupNode getParentGroup()
1041 {
1042 if ( parentGroup instanceof DisplayGroupNode )
1043 {
1044 return (DisplayGroupNode)parentGroup;
1045 }
1046 // presumably the root node
1047 return null;
1048 }
1049
1050 /***
1051 * Gets all descendants of the this node.
1052 */
1053 public List getDescendants()
1054 {
1055 return getDescendants( this, true );
1056 }
1057
1058 /***
1059 * Gets only the descendants of the this node
1060 * whose children has been loaded - no fetching
1061 * will occur. Useful for load-on-demand trees.
1062 */
1063 public List getLoadedDescendants()
1064 {
1065 return getDescendants( this, false );
1066 }
1067
1068 // breadth first traversal implementation
1069
1070 /***
1071 * Returns a list of all descendants of the
1072 * specified node. Unfetched nodes are traversed
1073 * only if forceLoad is true.
1074 * This implementation is a breadth-first traversal
1075 * of the nodes starting at the specified node.
1076 */
1077 static private List getDescendants( DisplayGroupNode aNode, boolean forceLoad )
1078 {
1079 if ( !forceLoad && !aNode.isFetched ) return NSArray.EmptyArray;
1080
1081 LinkedList result = new LinkedList();
1082 LinkedList queue = new LinkedList();
1083
1084 queue.add( aNode );
1085 while ( ! queue.isEmpty() )
1086 {
1087 checkNode( (DisplayGroupNode) queue.removeFirst(),
1088 queue, result, forceLoad );
1089 }
1090
1091 return result;
1092 }
1093
1094 /***
1095 * Adds each fetched child node of the specified node to
1096 * the result set (optionally forcing the child node to load)
1097 * and adding child node to the end of the queue.
1098 */
1099 static private void checkNode( DisplayGroupNode aNode,
1100 LinkedList aQueue, LinkedList aResult, boolean forceLoad )
1101 {
1102 DisplayGroupNode child;
1103 int count = aNode.getChildCount();
1104
1105 for ( int i = 0; i < count; i++ )
1106 {
1107 child = aNode.getChildNodeAt( i );
1108
1109 // add to queue if node has fetched children
1110 if ( ( !child.isFetched ) && ( forceLoad ) )
1111 {
1112 child.fetch();
1113 }
1114 if ( child.isFetched )
1115 {
1116 aQueue.addLast( child );
1117 }
1118
1119 aResult.add( child );
1120 }
1121 }
1122
1123 /***
1124 * Overridden to not fetch on InvalidateAllObjectsInStoreNotification
1125 * unless we've already been fetched, preserving the load-on-demand
1126 * functionality.
1127 */
1128 public void objectsInvalidatedInEditingContext( NSNotification aNotification )
1129 {
1130 if ( EOObjectStore.InvalidatedAllObjectsInStoreNotification
1131 .equals( aNotification.name() ) )
1132 {
1133 //System.out.println( "DisplayGroupNode.objectsInvalidatedInEditingContext: " + aNotification.name() );
1134 if ( parentAssociation.isVisible( this ) && targetObserver != null )
1135 {
1136 targetObserver.objectWillChange( target ); // force ui to update
1137 fireNodeChanged();
1138 }
1139 else // make sure we fetch children when we do become visible
1140 setFetchNeeded( true );
1141 return;
1142 }
1143 else
1144 if ( ( EOEditingContext.ObjectsChangedInEditingContextNotification
1145 .equals( aNotification.name() ) )
1146 || ( EOEditingContext.EditingContextDidSaveChangesNotification
1147 .equals( aNotification.name() ) ) )
1148 {
1149 int index;
1150 Enumeration e;
1151 boolean didChange = false;
1152 NSDictionary userInfo = aNotification.userInfo();
1153
1154 // if our target object was deleted
1155 NSArray deletes = (NSArray) userInfo.objectForKey(
1156 EOObjectStore.DeletedKey );
1157 if ( deletes.indexOfIdenticalObject( target ) != NSArray.NotFound )
1158 {
1159 //System.out.println( "DisplayGroupNode.objectsInvalidatedInEditingContext: delete: " + this + " : " + aNotification.name() );
1160 if ( parentAssociation.isVisible( this ) && targetObserver != null )
1161 {
1162 targetObserver.objectWillChange( target ); // force ui to update
1163 fireNodeChanged();
1164 }
1165 else // make sure we fetch children when we do become visible
1166 setFetchNeeded( true );
1167 return;
1168 }
1169
1170 // if our target object was invalidated
1171 NSArray invalidates = (NSArray) userInfo.objectForKey(
1172 EOObjectStore.InvalidatedKey );
1173 if ( invalidates != null &&
1174 invalidates.indexOfIdenticalObject( target ) != NSArray.NotFound )
1175 {
1176 //System.out.println( "DisplayGroupNode.objectsInvalidatedInEditingContext: invalidate: " + this + " : " + aNotification.name() );
1177 if ( parentAssociation.isVisible( this ) && targetObserver != null )
1178 {
1179 targetObserver.objectWillChange( target ); // force ui to update
1180 fireNodeChanged();
1181 }
1182 else // make sure we fetch children when we do become visible
1183 setFetchNeeded( true );
1184 return;
1185 }
1186
1187 // if our target object was updated, set fetchNeeded plus fire changed event
1188 NSArray updates = (NSArray) userInfo.objectForKey(
1189 EOObjectStore.UpdatedKey );
1190 if ( updates.indexOfIdenticalObject( target ) != NSArray.NotFound )
1191 {
1192 if ( parentAssociation.isVisible( this ) && targetObserver != null )
1193 {
1194 targetObserver.objectWillChange( target ); // force ui to update
1195 fireNodeChanged();
1196 if ( object() instanceof Component ) ((Component)object()).repaint();
1197 }
1198 else // make sure we fetch children when we do become visible
1199 setFetchNeeded( true );
1200 return;
1201 }
1202 }
1203
1204 super.objectsInvalidatedInEditingContext( aNotification );
1205
1206 }
1207
1208 // inner classes
1209
1210 /***
1211 * Private class used to force a hashmap to
1212 * perform key comparisons by reference.
1213 */
1214 private class ReferenceKey
1215 {
1216 private int hashCode;
1217 private Object referent;
1218
1219 public ReferenceKey( Object anObject )
1220 {
1221 referent = anObject;
1222 hashCode = anObject.hashCode();
1223 }
1224
1225 /***
1226 * Returns the actual key's hash code.
1227 */
1228 public int hashCode()
1229 {
1230 return hashCode;
1231 }
1232
1233 /***
1234 * Compares by reference.
1235 */
1236 public boolean equals( Object anObject )
1237 {
1238 if ( anObject instanceof ReferenceKey )
1239 {
1240 return ((ReferenceKey)anObject).referent == referent;
1241 }
1242 return false;
1243 }
1244 }
1245
1246 /***
1247 * A private class to observe the target object of this node.
1248 */
1249 private class TargetObserver extends EODelayedObserver
1250 {
1251 Reference ref;
1252
1253 /***
1254 * Pass in the display group node that will be updated
1255 * when the target changes.
1256 */
1257 public TargetObserver( DisplayGroupNode aDisplayGroup )
1258 {
1259 ref = new WeakReference( aDisplayGroup );
1260 }
1261
1262 /***
1263 * Repopulate our display group, and calculate the deltas
1264 * so we can broadcast appropriate events.
1265 */
1266 public void subjectChanged ()
1267 {
1268 DisplayGroupNode node = (DisplayGroupNode) ref.get();
1269 if ( node == null ) return; // node is null if gc'd.
1270 //FIXME: should un-register self from observer center??
1271
1272 node.targetChanged();
1273 }
1274 }
1275
1276 }
1277 /*
1278 * $Log$
1279 * Revision 1.2 2006/02/18 23:19:05 cgruber
1280 * Update imports and maven dependencies.
1281 *
1282 * Revision 1.1 2006/02/16 13:22:22 cgruber
1283 * Check in all sources in eclipse-friendly maven-enabled packages.
1284 *
1285 * Revision 1.64 2003/08/06 23:07:52 chochos
1286 * general code cleanup (mostly, removing unused imports)
1287 *
1288 * Revision 1.63 2003/06/06 14:20:07 mpowers
1289 * getLoadedDescendants was forcing a fetch of the node it was called on.
1290 *
1291 * Revision 1.62 2003/06/03 14:48:33 mpowers
1292 * Clean-up of notification handling for updates/invalidation/etc.
1293 * Now fetching immediately on notification if the node is visible.
1294 * This averts the infamous IndexOutOfBoundsException that occurs
1295 * if fetching happens during repaint, because the BasicTreeUI is
1296 * caching the number of child nodes before painting begins.
1297 *
1298 * Revision 1.61 2003/01/18 23:33:29 mpowers
1299 * Fixing the build.
1300 *
1301 * Revision 1.60 2002/05/31 15:03:10 mpowers
1302 * Fixes for the previous fix. Fat props to yjcheung.
1303 *
1304 * Revision 1.59 2002/05/28 15:31:36 mpowers
1305 * Fix for updateDisplayedObjects for a subtle case where a node appears in
1306 * the position that another node was moved from.
1307 *
1308 * Revision 1.58 2002/05/24 14:42:02 mpowers
1309 * Prevent repeat events from firing if firing events loops back.
1310 *
1311 * Revision 1.57 2002/04/23 19:12:28 mpowers
1312 * Reimplemented fireEventsForChanges. Fitter and happier.
1313 *
1314 * Revision 1.56 2002/04/19 21:18:45 mpowers
1315 * Removed tree event coalescing, which was causing way too many problems.
1316 * The fireChangeEvent algorithm is way faster than before, so we should
1317 * still be better off than before. At least now, we don't have to track
1318 * whether the view component has encountered a particular node.
1319 *
1320 * Revision 1.55 2002/04/19 20:53:22 mpowers
1321 * Now firing event fewer events in fireEventsForChanges.
1322 *
1323 * Revision 1.54 2002/04/15 21:52:50 mpowers
1324 * Tightening up TreeModelAssociation and DisplayGroupNode.
1325 * Now only firing root structure changed once.
1326 * Now disposing of root's children.
1327 * Better event coalescing.
1328 *
1329 * Revision 1.53 2002/04/12 20:35:20 mpowers
1330 * Now correctly setting parent display group and data source on creation.
1331 *
1332 * Revision 1.52 2002/04/10 21:20:04 mpowers
1333 * Better handling for tree nodes when working with editing contexts.
1334 * Better handling for invalidation. No longer broadcasting events
1335 * when nodes have not been "registered" in the tree.
1336 *
1337 * Revision 1.51 2002/04/03 20:13:36 mpowers
1338 * Now differentiating between node instantiation caused by model expansion
1339 * (user initiated) and by modifications to the model.
1340 * Dispose now disposes all children.
1341 *
1342 * Revision 1.50 2002/03/23 16:20:27 mpowers
1343 * Optimized processRecentChanges, minimized tree events.
1344 *
1345 * Revision 1.49 2002/03/11 03:15:06 mpowers
1346 * Optimized processRecentChanges, minimize event firing, coalescing changes.
1347 * Still need a better diff algorithm to avoid removing nodes.
1348 *
1349 * Revision 1.48 2002/03/10 00:59:39 mpowers
1350 * Interim version: coalesces calls to process recent changes.
1351 * Still does not handle rearranged nodes.
1352 *
1353 * Revision 1.47 2002/03/09 17:33:45 mpowers
1354 * Nodes now track their child nodes by reference, not index.
1355 *
1356 * Revision 1.46 2002/03/08 23:19:07 mpowers
1357 * Added getParentGroup to DisplayGroupNode.
1358 *
1359 * Revision 1.45 2002/03/06 13:04:16 mpowers
1360 * Implemented cascading qualifiers in tree nodes.
1361 *
1362 * Revision 1.44 2002/02/27 23:19:17 mpowers
1363 * Refactoring of TreeAssociation to create TreeModelAssociation parent.
1364 *
1365 * Revision 1.42 2002/02/19 22:28:46 mpowers
1366 * DisplayGroupNodes immediately unregister themselves as editors.
1367 *
1368 * Revision 1.41 2002/02/13 16:27:38 mpowers
1369 * Exposing setTarget.
1370 *
1371 * Revision 1.40 2001/11/02 20:55:46 mpowers
1372 * Now using fixed index to send node removed events. This preserves the
1373 * expanded state of the nodes in the corresponding jtree.
1374 *
1375 * Revision 1.39 2001/09/21 21:09:25 mpowers
1376 * Exposed more fields as protected.
1377 *
1378 * Revision 1.38 2001/09/19 15:36:08 mpowers
1379 * Refined behavior for isFetched after notification handling.
1380 *
1381 * Revision 1.37 2001/09/13 14:51:18 mpowers
1382 * DisplayGroupNodes now dispose themselves and mark their parent for update
1383 * when they receive notification that their target has been deleted.
1384 *
1385 * Revision 1.36 2001/09/10 14:10:24 mpowers
1386 * Fix for notification handling.
1387 *
1388 * Revision 1.35 2001/07/30 16:17:01 mpowers
1389 * Minor code cleanup.
1390 *
1391 * Revision 1.34 2001/07/18 22:13:39 mpowers
1392 * getLoadedDescendants now works as advertised.
1393 * Now correctly handling invalidateAllObjects notification.
1394 *
1395 * Revision 1.33 2001/07/18 13:03:32 mpowers
1396 * TreeNodes now refetch only on demand. Previously, once a node had
1397 * been fetched, it was always refetched after an invalidate, even if
1398 * the node was not being displayed.
1399 *
1400 * Revision 1.32 2001/06/18 14:10:28 mpowers
1401 * Cleaned up event firing: no longer firing insert or remove events twice.
1402 *
1403 * Revision 1.31 2001/06/09 16:15:39 mpowers
1404 * Revised the targetChanged scheme because oldObjects and newObjects were
1405 * identical after the target object is invalidated.
1406 *
1407 * Revision 1.30 2001/05/21 22:17:19 mpowers
1408 * Fix for tree out-of-synch problems when nodes are inserted.
1409 *
1410 * Revision 1.29 2001/05/18 21:07:46 mpowers
1411 * Playing with refresh options.
1412 *
1413 * Revision 1.28 2001/05/14 15:25:43 mpowers
1414 * DisplayGroupNodes now only respond to InvalidateAllObjectsInStore
1415 * if they are already fetched.
1416 *
1417 * Revision 1.27 2001/05/08 19:55:58 mpowers
1418 * Fix for node children not refreshing after sibling was inserted.
1419 *
1420 * Revision 1.26 2001/05/08 18:47:34 mpowers
1421 * Minor fixes for d3.
1422 *
1423 * Revision 1.25 2001/05/06 22:22:55 mpowers
1424 * Debugging.
1425 *
1426 * Revision 1.24 2001/05/04 14:42:58 mpowers
1427 * Now getting stored values in KeyValueCoding.
1428 * MasterDetail now marks dirty based on whether it's an attribute
1429 * or relation.
1430 * Implemented editing context marker.
1431 *
1432 * Revision 1.23 2001/05/02 18:00:43 mpowers
1433 * Removed debug code.
1434 *
1435 * Revision 1.22 2001/05/02 17:31:20 mpowers
1436 * DisplayGroupNode now does a better job determining when to mark its
1437 * parent dirty.
1438 *
1439 * Revision 1.21 2001/05/01 00:52:32 mpowers
1440 * Implemented breadth-first traversal of tree for node.
1441 *
1442 * Revision 1.20 2001/04/26 01:15:19 mpowers
1443 * Major clean-up of DisplayGroupNode: fitter, happier, more productive.
1444 *
1445 * Revision 1.19 2001/04/22 23:13:35 mpowers
1446 * Minor bug.
1447 *
1448 * Revision 1.18 2001/04/22 23:05:33 mpowers
1449 * Totally revised DisplayGroupNode so each object gets its own node
1450 * (so the nodes are no longer fixed by index).
1451 *
1452 * Revision 1.17 2001/04/21 23:05:12 mpowers
1453 * A fairly major revisiting. I've decided to scrap the pass-thru approach
1454 * where every node simply represents an index and not an object.
1455 * The next update will have each node correspond to a specific object.
1456 *
1457 * Revision 1.16 2001/04/13 16:37:37 mpowers
1458 * Handling bounds checking.
1459 *
1460 * Revision 1.15 2001/04/03 20:36:01 mpowers
1461 * Fixed refaulting/reverting/invalidating to be self-consistent.
1462 *
1463 * Revision 1.14 2001/03/27 17:45:51 mpowers
1464 * More index bounds checking.
1465 *
1466 * Revision 1.13 2001/03/22 21:25:42 mpowers
1467 * Fixed some nasty issues with jtree's internal state and array bounds.
1468 *
1469 * Revision 1.12 2001/03/19 22:18:58 mpowers
1470 * Root node now mirrors contents of titles display group.
1471 *
1472 * Revision 1.11 2001/03/19 21:38:36 mpowers
1473 * Improved redisplay after edit. Editing nodes off root now works.
1474 *
1475 * Revision 1.10 2001/03/09 22:08:38 mpowers
1476 * Removed unused line.
1477 *
1478 * Revision 1.9 2001/03/07 16:41:04 mpowers
1479 * Now checking size of parent displayed objects array so that we don't
1480 * get array out of bounds execeptions from isLeaf() or object() when
1481 * those messages are called after the TreeAssociation fires a
1482 * nodesDeleted event. I believe that JTree is mistakenly rendering
1483 * those nodes one last time before erasing them.
1484 *
1485 * Revision 1.8 2001/03/06 23:21:27 mpowers
1486 * Now only notifying parent if the object is not registered in the
1487 * editing context, if any.
1488 *
1489 * Revision 1.7 2001/02/20 16:38:55 mpowers
1490 * MasterDetailAssociations now observe their controlled display group's
1491 * objects for changes to that the parent object will be marked as updated.
1492 * Before, only inserts and deletes to an object's items are registered.
1493 * Also, moved ObservableArray to package access.
1494 *
1495 * Revision 1.6 2001/02/17 16:52:05 mpowers
1496 * Changes in imports to support building with jdk1.1 collections.
1497 *
1498 * Revision 1.5 2001/01/31 17:59:52 mpowers
1499 * Fixed isLeaf aspect of TreeAssociation.
1500 *
1501 * Revision 1.4 2001/01/25 02:16:25 mpowers
1502 * TreeAssociation now returns DisplayGroupNode.getUserObject.
1503 *
1504 * Revision 1.3 2001/01/24 18:14:40 mpowers
1505 * Fixed problem with leaving children aspect unspecified.
1506 *
1507 * Revision 1.2 2001/01/24 16:35:37 mpowers
1508 * Improved documentation on TreeAssociation.
1509 * SortOrderings are now inherited from parent nodes.
1510 * Updates after sorting are still lost on TreeController.
1511 *
1512 * Revision 1.1 2001/01/24 14:17:12 mpowers
1513 * Major revision to TreeAssociation. Can now add and remove nodes.
1514 * DisplayGroupNode is now it's own class.
1515 *
1516 *
1517 */
1518