relation editing for multiple selections
authorRichard Fairhurst <richard@systemed.net>
Fri, 31 Dec 2010 17:38:09 +0000 (17:38 +0000)
committerRichard Fairhurst <richard@systemed.net>
Fri, 31 Dec 2010 17:38:09 +0000 (17:38 +0000)
net/systemeD/halcyon/connection/Entity.as
net/systemeD/halcyon/connection/EntityCollection.as
net/systemeD/potlatch2/RelationSelectPanel.mxml
net/systemeD/potlatch2/TagViewer.mxml

index eac19a3..3e9204d 100644 (file)
@@ -300,6 +300,22 @@ package net.systemeD.halcyon.connection {
             return a;
         }
 
+               public function getRelationMemberships():Array {
+                       var memberships:Array = [];
+                       for each( var rel:Relation in parentRelations ) {
+                               for each( var memberIndex:int in rel.findEntityMemberIndexes(this)) {
+                                       memberships.push({
+                                               relation: rel,
+                                               id: rel.id,
+                                               index: memberIndex,
+                                               role: rel.getMember(memberIndex).role,
+                                               description: rel.getDescription(),
+                                               id_idx: rel.id + "/"+memberIndex });
+                               }
+                       }
+                       return memberships;
+               }
+
         /** How many parents does this entity have that satisfy the "within" constraint? */
         public function countParentObjects(within:Object):uint {
             var count:uint=0;
index 128d276..f0e8405 100644 (file)
@@ -6,31 +6,56 @@ package net.systemeD.halcyon.connection {
        import net.systemeD.halcyon.connection.*;
     import net.systemeD.halcyon.connection.actions.*;
 
+       // ** FIXME:
+       // - Can we rework the ControllerStates so they work with this (rather than just a raw Array)?
+       // - It may be possible to generalise the event timer code into a tidier "event grouping" class of some sort
+
     public class EntityCollection extends Entity {
-               private var entities:Array;
+               private var _entities:Array;
                private var tagChangedTimer:Timer;
-               private var delayedEvent:TagEvent;
+               private var addedToRelationTimer:Timer;
+               private var removedFromRelationTimer:Timer;
+               private var delayedEvents:Array = [];
 
         public function EntityCollection(entities:Array) {
                        super(-1, 0, {}, true, -1, "");
-            this.entities = entities;
+            _entities = entities;
                        
-                       //To avoid firing on every contained entity, we wait some short time before firing the tag changed event
-                       tagChangedTimer = new Timer(50, 1);
-                       tagChangedTimer.addEventListener(TimerEvent.TIMER, onTagChangedTimerFinished);                  
-                       for each(var entity:Entity in entities) {
-                               entity.addEventListener(Connection.TAG_CHANGED, onTagChanged);  
+                       //To avoid firing on every contained entity, we wait some short time before firing the events
+                       tagChangedTimer         = new Timer(50, 1); tagChangedTimer.addEventListener(TimerEvent.TIMER, onTimerFinished, false, 0, true);
+                       addedToRelationTimer    = new Timer(50, 1); addedToRelationTimer.addEventListener(TimerEvent.TIMER, onTimerFinished, false, 0, true);
+                       removedFromRelationTimer= new Timer(50, 1); removedFromRelationTimer.addEventListener(TimerEvent.TIMER, onTimerFinished, false, 0, true);
+                       for each(var entity:Entity in _entities) {
+                               entity.addEventListener(Connection.TAG_CHANGED, onTagChanged, false, 0, true);
+                               entity.addEventListener(Connection.ADDED_TO_RELATION, onAddedToRelation, false, 0, true);
+                               entity.addEventListener(Connection.REMOVED_FROM_RELATION, onRemovedFromRelation, false, 0, true);
                        }
         }
                
+               public function get entities():Array {
+                       return _entities;
+               }
+               
                private function onTagChanged(event:TagEvent):void {
                        if(tagChangedTimer.running) return;
-                       delayedEvent = new TagEvent(Connection.TAG_CHANGED, this, event.key, event.key, event.oldValue, event.newValue)
+                       delayedEvents.push(new TagEvent(Connection.TAG_CHANGED, this, event.key, event.key, event.oldValue, event.newValue));
                        tagChangedTimer.start();
                }
                
-               private function onTagChangedTimerFinished(event:TimerEvent): void {
-                       dispatchEvent(delayedEvent);
+               private function onAddedToRelation(event:RelationMemberEvent):void {
+                       if(addedToRelationTimer.running) return;
+                       delayedEvents.push(new RelationMemberEvent(Connection.ADDED_TO_RELATION, this, event.relation, event.index));
+                       addedToRelationTimer.start();
+               }
+               
+               private function onRemovedFromRelation(event:RelationMemberEvent):void {
+                       if(removedFromRelationTimer.running) return;
+                       delayedEvents.push(new RelationMemberEvent(Connection.REMOVED_FROM_RELATION, this, event.relation, event.index));
+                       removedFromRelationTimer.start();
+               }
+               
+               private function onTimerFinished(event:TimerEvent):void { 
+                       dispatchEvent(delayedEvents.shift());
                }
                
                
@@ -39,8 +64,8 @@ package net.systemeD.halcyon.connection {
                private function getMergedTags():Object {
                        //Builds an object with tags of all entities in this collection. If the value of a tag differs or is not set in all entities, value is marked
                        var differentMarker:String = "<different>";
-                       var mergedTags:Object = entities[0].getTagsCopy();
-                       for each(var entity:Entity in entities) {
+                       var mergedTags:Object = _entities[0].getTagsCopy();
+                       for each(var entity:Entity in _entities) {
                                var entityTags:Object = entity.getTagsHash();
                                for(var key:String in entityTags) {
                                        var value:String = entityTags[key];
@@ -89,7 +114,7 @@ package net.systemeD.halcyon.connection {
                public override function setTag(key:String, value:String, performAction:Function):void {
                        var oldValue:String = getMergedTags()[key];     
                        var undoAction:CompositeUndoableAction = new CompositeUndoableAction("set_tag_entity_collection");
-                       for each (var entity:Entity in entities) {
+                       for each (var entity:Entity in _entities) {
                                undoAction.push(new SetTagAction(entity, key, value));
                        }
             performAction(undoAction);
@@ -97,7 +122,7 @@ package net.systemeD.halcyon.connection {
 
         public override function renameTag(oldKey:String, newKey:String, performAction:Function):void {
                        var undoAction:CompositeUndoableAction = new CompositeUndoableAction("rename_tag_entity_collection");
-                       for each (var entity:Entity in entities) {
+                       for each (var entity:Entity in _entities) {
                                undoAction.push(new SetTagKeyAction(entity, oldKey, newKey));
                        }
             performAction(undoAction);
@@ -124,23 +149,62 @@ package net.systemeD.halcyon.connection {
             return copy;
         }
 
+               public override function get parentRelations():Array {
+                       var relations:Array = [];
+                       for each (var entity:Entity in _entities) {
+                               for each (var relation:Relation in entity.parentRelations) {
+                                       if (relations.indexOf(relation)==-1) relations.push(relation);
+                               }
+                       }
+                       return relations;
+               }
+
+               public override function getRelationMemberships():Array {
+                       var relations:Object = {};
+                       for each (var entity:Entity in _entities) {
+                               for each (var rel:Relation in entity.parentRelations) {
+                                       for each(var memberIndex:int in rel.findEntityMemberIndexes(entity)) {
+                                               var role:String=rel.getMember(memberIndex).role;
+                                               if (!relations[rel.id]) {
+                                                       relations[rel.id]= { role: role, relation: rel, distinctCount: 0};
+                                               } else if (relations[rel.id].role!=role) {
+                                                       relations[rel.id].role="<different>";
+                                               }
+                                       }
+                                       relations[rel.id].distinctCount++;
+                               }
+                       }
+                       var memberships:Array = [];
+                       for (var id:String in relations) {
+                               memberships.push({
+                                       relation: relations[id].relation,
+                                       id: Number(id),
+                                       role: relations[id].role,
+                                       description: relations[id].relation.getDescription(),
+                                       universal: relations[id].distinctCount==_entities.length,
+                                       id_idx: id });
+                       }
+                       return memberships;
+                       // ** FIXME: .universal should be shown in the tag panel
+               }
+
                // Clean/dirty methods
 
         public override function get isDirty():Boolean {
-            for each (var entity:Entity in entities) {
+            for each (var entity:Entity in _entities) {
                                if(entity.isDirty) return true;
                        }
                        return false;
         }
 
         public override function markClean():void {
-             for each (var entity:Entity in entities) {
+             for each (var entity:Entity in _entities) {
                                entity.markClean();
                        }
         }
 
         internal override function markDirty():void {
-            for each (var entity:Entity in entities) {
+            for each (var entity:Entity in _entities) {
                                entity.markDirty();
                        }
         }
@@ -159,7 +223,7 @@ package net.systemeD.halcyon.connection {
 
         public override function getType():String {
                        var entityType:String = '';
-                        for each (var entity:Entity in entities) {
+                        for each (var entity:Entity in _entities) {
                                if(entityType == '') entityType = entity.getType();
                                else if(entityType != entity.getType()) {
                                        entityType = '';
index 574ba41..5c58d66 100644 (file)
         }
         
         public function updateEntityAndClose():void {
-          var relation:Relation = conn.getRelation(relationSelector.selectedItem.relId);
-          relation.appendMember(new RelationMember(entity, ''), MainUndoStack.getGlobalStack().addAction);
-          PopUpManager.removePopUp(this);
+                       var relation:Relation = conn.getRelation(relationSelector.selectedItem.relId);
+                       if (entity is EntityCollection) {
+                               var undo:CompositeUndoableAction = new CompositeUndoableAction("Add to relation");
+                               for each (var e:Entity in EntityCollection(entity).entities) {
+                                       if (relation.findEntityMemberIndex(e)==-1) {
+                                               relation.appendMember(new RelationMember(e, ''), undo.push);
+                                       }
+                               }
+                               MainUndoStack.getGlobalStack().addAction(undo);
+                       } else {
+                               relation.appendMember(new RelationMember(entity, ''), MainUndoStack.getGlobalStack().addAction);
+                       }
+                       PopUpManager.removePopUp(this);
         }
         
         public function closeAndNewRelation():void {
-          var relation:Relation = conn.createRelation(tags, [new RelationMember(entity, '')],
-              MainUndoStack.getGlobalStack().addAction)
-          PopUpManager.removePopUp(this);
-          var panel:RelationEditorPanel = RelationEditorPanel(
-              PopUpManager.createPopUp(Application(Application.application), RelationEditorPanel, true));
-          panel.setRelation(relation);
-          PopUpManager.centerPopUp(panel);
+                       var members:Array=[];
+                       if (entity is EntityCollection) {
+                               for each (var e:Entity in EntityCollection(entity).entities) {
+                                       members.push(new RelationMember(e, ''));
+                               }
+                       } else members.push(new RelationMember(entity, ''));
+                       var relation:Relation = conn.createRelation(tags, members, MainUndoStack.getGlobalStack().addAction);
+
+                       PopUpManager.removePopUp(this);
+                       var panel:RelationEditorPanel = RelationEditorPanel(
+                           PopUpManager.createPopUp(Application(Application.application), RelationEditorPanel, true));
+                       panel.setRelation(relation);
+                       PopUpManager.centerPopUp(panel);
         }
         
     ]]></mx:Script> 
index 8588d27..f9b5891 100644 (file)
@@ -10,6 +10,9 @@
     initialize="loadFeatures()">
 
 <mx:ViewStack id="sidebar" width="100%" height="100%" creationPolicy="all">
+
+  <!-- Drag & drop icon panel -->
+
   <mx:VBox id="dndPanel" width="100%" height="100%" horizontalScrollPolicy="off" styleName="dndPanelVbox">
     <mx:Text id="dndPanelText" text="{dndPrompt}" width="100%" styleName="helpInfo" />
        <mx:Repeater id="dndRep" dataProvider="{MapFeatures.getInstance().getCategoriesForType('point')}" styleName="dndRepeater">
@@ -49,6 +52,8 @@
        </mx:Repeater>
   </mx:VBox>
 
+  <!-- Standard tagging panel -->
+
   <mx:VBox id="tagsPanel" width="100%" height="100%" creationPolicy="auto">
     <mx:ViewStack id="stack" width="100%" height="100%">
       <mx:VBox width="100%" height="100%" label="Simple" id="editorContainer" creationComplete="initEditorStackUIs()" styleName="dndPanelVbox">
          </mx:HBox>
   </mx:VBox>
 
+  <!-- Multiple selection -->
+
   <mx:VBox id="multiplePanel" width="100%" height="100%" horizontalScrollPolicy="off" styleName="dndPanelVbox">
     <potlatch2:TagGrid id="multiAdvancedTagGrid" width="100%" height="75%" />
        <mx:HBox horizontalAlign="right" width="100%">
          <mx:LinkButton label="Delete" click="multiAdvancedTagGrid.removeTag()" enabled="{multiAdvancedTagGrid.selectedItem != null? true : false}"/>
          <mx:LinkButton label="Add" click="multiAdvancedTagGrid.addNewTag()" />
        </mx:HBox>
+
+    <mx:DataGrid editable="true" width="100%" height="25%" id="multiRelationsGrid"
+        doubleClickEnabled="true"
+        itemDoubleClick="editRelation(multiRelationsGrid.selectedItem.id)"
+        doubleClick="if (event.target.parent==multiRelationsGrid) { addToRelation(); }">
+        <mx:columns>
+            <mx:DataGridColumn editable="false" dataField="description" headerText="Relation"/>
+            <mx:DataGridColumn editable="false" dataField="id_idx" headerText="ID"/>
+            <mx:DataGridColumn editable="true" dataField="role" headerText="Role">
+                <mx:itemEditor><mx:Component><mx:TextInput restrict="&#x0020;-&#x10FFFF;" /></mx:Component></mx:itemEditor>
+            </mx:DataGridColumn>
+        </mx:columns>
+    </mx:DataGrid>
+    <mx:HBox horizontalAlign="right" width="100%">
+      <mx:LinkButton label="Remove from" click="removeFromRelation(multiRelationsGrid.selectedItem.id)"
+                      enabled="{multiRelationsGrid.selectedItem != null? true : false}"/>
+      <mx:LinkButton label="Add to" click="addToRelation()"/>
+    </mx:HBox>
+
   </mx:VBox>
 
+  <!-- Multiple selection but items cannot be edited -->
+
   <mx:VBox id="multipleInvalidPanel" width="100%" height="100%" horizontalScrollPolicy="off" styleName="dndPanelVbox">
        <mx:Text id="multipleInvalidPanelText" text="You have selected multiple items." width="100%" styleName="helpInfo" />
   </mx:VBox>
 
+  <!-- Generic marker panel -->
+
   <mx:VBox id="markerPanel" width="100%" height="100%" horizontalScrollPolicy="off" styleName="dndPanelVbox">
     <sidepanel:MarkerPanel id="markerPanelContents" width="100%"/>
   </mx:VBox>
 
+  <!-- Bug editing panel -->
+
   <mx:VBox id="bugPanel" width="100%" height="100%" horizontalScrollPolicy="off" styleName="dndPanelVbox">
     <sidepanel:BugPanel id="bugPanelContents" width="100%"/>
   </mx:VBox>
 
                if (selectedEntity!=firstSelected && selectedEntity!=null) {
                        selectedEntity.removeEventListener(Connection.TAG_CHANGED, tagChanged);
+                       selectedEntity.removeEventListener(Connection.ADDED_TO_RELATION, addedToRelation);
+                       selectedEntity.removeEventListener(Connection.REMOVED_FROM_RELATION, removedFromRelation);
                }
 
                if (entities.length==0) {
                }
 
                removeRelationListeners();
-               if ( selectedEntity != null ) {
-                       selectedEntity.removeEventListener(Connection.ADDED_TO_RELATION, addedToRelation);
-                       selectedEntity.removeEventListener(Connection.REMOVED_FROM_RELATION, removedFromRelation);
-               }
 
                if ( entity == null ) {
                        relationsGrid.dataProvider = null;
 
          private function setupMultiAdvanced(entity:Entity):void {
                multiAdvancedTagGrid.init(entity);
+               resetRelationsGrid(entity);
+               entity.addEventListener(Connection.ADDED_TO_RELATION, addedToRelation, false, 0, true);
+               entity.addEventListener(Connection.REMOVED_FROM_RELATION, removedFromRelation, false, 0, true);
       }
 
          public function addNewTag():void {
               rel.removeEventListener(Connection.RELATION_MEMBER_REMOVED, entityRelationMemberChanged);
           }
           listeningToRelations = [];
-          relationsGrid.removeEventListener(DataGridEvent.ITEM_EDIT_END, relationRoleChanged);
+          if (relationsGrid) relationsGrid.removeEventListener(DataGridEvent.ITEM_EDIT_END, relationRoleChanged);
+          if (multiRelationsGrid) multiRelationsGrid.removeEventListener(DataGridEvent.ITEM_EDIT_END, relationRoleChanged);
       }
 
       private function resetRelationsGrid(entity:Entity):void {
           removeRelationListeners();
-          var relations:Array = [];
-          for each( var rel:Relation in entity.parentRelations ) {
-              for each( var memberIndex:int in rel.findEntityMemberIndexes(entity)) {
-                  var props:Object = {};
-                  props["relation"] = rel;
-                  props["id"] = rel.id;
-                  props["index"] = memberIndex;
-                  props["role"] = rel.getMember(memberIndex).role;
-                  props["description"] = rel.getDescription();
-                  props["id_idx"] = rel.id + "/"+memberIndex;
-
-                  relations.push(props);
-              }
 
-              rel.addEventListener(Connection.TAG_CHANGED, relationTagChanged);
-              rel.addEventListener(Connection.RELATION_MEMBER_ADDED, entityRelationMemberChanged);
-              rel.addEventListener(Connection.RELATION_MEMBER_REMOVED, entityRelationMemberChanged);
-              listeningToRelations.push(rel);
+          var instance:DataGrid=relationsGrid;
+          if (entity is EntityCollection) instance=multiRelationsGrid;
+          var memberships:Array = entity.getRelationMemberships();
+          for each (var m:Object in memberships) {
+              m.relation.addEventListener(Connection.TAG_CHANGED, relationTagChanged);
+              m.relation.addEventListener(Connection.RELATION_MEMBER_ADDED, entityRelationMemberChanged);
+              m.relation.addEventListener(Connection.RELATION_MEMBER_REMOVED, entityRelationMemberChanged);
+              listeningToRelations.push(m.relation);
           }
-          relationsGrid.dataProvider = relations;
-          relationsGrid.addEventListener(DataGridEvent.ITEM_EDIT_END, relationRoleChanged, false, -100);
+          instance.dataProvider = memberships;
+          instance.addEventListener(DataGridEvent.ITEM_EDIT_END, relationRoleChanged, false, -100);
       }
 
       private function relationRoleChanged(event:DataGridEvent):void {
                var props:Object=relations[event.rowIndex];
 
                var relation:Relation=props['relation'];
-               var index:uint=props['index'];
                var newRole:String=event.itemRenderer.data['role'];
 
-               relation.setMember(index, new RelationMember(selectedEntity,newRole));
+               if (selectedEntity is EntityCollection) {
+                       for each (var entity:Entity in EntityCollection(selectedEntity).entities) {
+                               var indexes:Array=relation.findEntityMemberIndexes(entity);
+                               for each (var index:int in indexes) {
+                                       relation.setMember(index, new RelationMember(entity,newRole));
+                               }
+                       }
+               } else {
+                       relation.setMember(props['index'], new RelationMember(selectedEntity,newRole));
+               }
       }
 
       private function relationTagChanged(event:TagEvent):void {
           new RelationSelectPanel().init(selectedEntity,new Object());
       }
 
-      private function removeFromRelation(id:Number, index:int):void {
-          Connection.getConnectionInstance().getRelation(id).removeMemberByIndex(index, MainUndoStack.getGlobalStack().addAction);
+      private function removeFromRelation(id:Number, index:int=-1):void {
+               var rel:Relation=Connection.getConnectionInstance().getRelation(id);
+               if (index>-1) {
+                       rel.removeMemberByIndex(index, MainUndoStack.getGlobalStack().addAction);
+               } else if (selectedEntity is EntityCollection) {
+                       var undo:CompositeUndoableAction=new CompositeUndoableAction("Remove selection from relations");
+                       for each (var e:Entity in EntityCollection(selectedEntity).entities) rel.removeMember(e,undo.push);
+                       MainUndoStack.getGlobalStack().addAction(undo);
+               }
       }
 
       public function initFeatureBox():void {