basic AutoComplete component and sample support. Needs much more work on the P2 logic...
authorRichard Fairhurst <richard@systemed.net>
Mon, 6 Sep 2010 09:51:07 +0000 (09:51 +0000)
committerRichard Fairhurst <richard@systemed.net>
Mon, 6 Sep 2010 09:51:07 +0000 (09:51 +0000)
net/systemeD/controls/AutoComplete.as [new file with mode: 0755]
net/systemeD/potlatch2/EditController.as
net/systemeD/potlatch2/TagViewer.mxml
net/systemeD/potlatch2/mapfeatures/MapFeatures.as

diff --git a/net/systemeD/controls/AutoComplete.as b/net/systemeD/controls/AutoComplete.as
new file mode 100755 (executable)
index 0000000..e164f1a
--- /dev/null
@@ -0,0 +1,371 @@
+/*
+       AutoComplete component
+       based on Adobe original but heavily bug-fixed and stripped down
+       http://www.adobe.com/cfusion/exchange/index.cfm?event=extensionDetail&extid=1047291
+       
+       Enhancements to do:
+       - up/down when field empty should show everything
+       - up (to 0) when dropdown displayed should cause it to reset to previous typed value
+       - down (past only item) when dropdown displayed should paste it
+       - shouldn't be able to leave empty fields, or those which already exist
+*/
+
+package net.systemeD.controls {
+       import flash.events.KeyboardEvent;
+       import flash.events.Event;
+       import flash.events.FocusEvent;
+       import flash.events.MouseEvent;
+       import flash.net.SharedObject;
+       import flash.ui.Keyboard;
+       import mx.core.UIComponent;
+       import mx.controls.ComboBox;
+       import mx.controls.DataGrid;
+       import mx.controls.listClasses.ListBase;
+       import mx.collections.ArrayCollection;
+       import mx.collections.ListCollectionView;
+       import mx.events.DropdownEvent;
+       import mx.events.ListEvent;
+       import mx.events.FlexEvent;
+       import mx.managers.IFocusManagerComponent;
+
+       [Event(name="filterFunctionChange", type="flash.events.Event")]
+       [Event(name="typedTextChange", type="flash.events.Event")]
+
+       [Exclude(name="editable", kind="property")]
+
+       /**
+        *      The AutoComplete control is an enhanced 
+        *      TextInput control which pops up a list of suggestions 
+        *      based on characters entered by the user. These suggestions
+        *      are to be provided by setting the <code>dataProvider
+        *      </code> property of the control.
+        *      @mxml
+        *
+        *      <p>The <code>&lt;fc:AutoComplete&gt;</code> tag inherits all the tag attributes
+        *      of its superclass, and adds the following tag attributes:</p>
+        *
+        *      <pre>
+        *      &lt;fc:AutoComplete
+        *        <b>Properties</b>
+        *        keepLocalHistory="false"
+        *        typedText=""
+        *        filterFunction="<i>Internal filter function</i>"
+        *
+        *        <b>Events</b>
+        *        filterFunctionChange="<i>No default</i>"
+        *        typedTextChange="<i>No default</i>"
+        *      /&gt;
+        *      </pre>
+        *
+        *      @includeExample ../../../../../../docs/com/adobe/flex/extras/controls/example/AutoCompleteCountriesData/AutoCompleteCountriesData.mxml
+        *
+        *      @see mx.controls.ComboBox
+        *
+        */
+       public class AutoComplete extends ComboBox 
+       {
+
+               //--------------------------------------------------------------------------
+               //      Constructor
+               //--------------------------------------------------------------------------
+
+               public function AutoComplete() {
+                       super();
+
+                       //Make ComboBox look like a normal text field
+                       editable = true;
+
+                       setStyle("arrowButtonWidth",0);
+                       setStyle("fontWeight","normal");
+                       setStyle("cornerRadius",0);
+                       setStyle("paddingLeft",0);
+                       setStyle("paddingRight",0);
+                       rowCount = 7;
+               }
+               
+               //--------------------------------------------------------------------------
+               //      Variables
+               //--------------------------------------------------------------------------
+
+               private var cursorPosition:Number=0;
+               private var prevIndex:Number = -1;
+               private var showDropdown:Boolean=false;
+               private var showingDropdown:Boolean=false;
+               private var tempCollection:Object;
+               private var dropdownClosed:Boolean=true;
+
+               //--------------------------------------------------------------------------
+               //      Overridden Properties
+               //--------------------------------------------------------------------------
+
+               override public function set editable(value:Boolean):void {
+                       //This is done to prevent user from resetting the value to false
+                       super.editable = true;
+               }
+               override public function set dataProvider(value:Object):void {
+                       super.dataProvider = value;
+                       tempCollection = value;
+
+                       // Big bug in Flex 3.5:
+                       //  http://www.newtriks.com/?p=935
+                       //  http://forums.adobe.com/message/2952677
+                       //  https://bugs.adobe.com/jira/browse/SDK-25567
+                       //  https://bugs.adobe.com/jira/browse/SDK-25705
+                       //  http://stackoverflow.com/questions/3006291/adobe-flex-combobox-dataprovider
+                       // We can remove this workaround if we ever move to Flex 3.6 or Flex 4
+                       var newDropDown:ListBase = dropdown;
+                       if(newDropDown) {
+                               validateSize(true);
+                               newDropDown.dataProvider = super.dataProvider;
+
+                               dropdown.addEventListener(ListEvent.ITEM_CLICK, itemClickHandler, false, 0, true);
+                       }
+               }
+
+               override public function set labelField(value:String):void {
+                       super.labelField = value;
+                       invalidateProperties();
+                       invalidateDisplayList();
+               }
+
+
+               //--------------------------------------------------------------------------
+               //      Properties
+               //--------------------------------------------------------------------------
+
+               private var _typedText:String="";                       // text changed by user
+               private var typedTextChanged:Boolean;
+
+               [Bindable("typedTextChange")]
+               [Inspectable(category="Data")]
+               public function get typedText():String { return _typedText; }
+
+               public function set typedText(input:String):void {
+                       _typedText = input;
+                       typedTextChanged = true;
+                       
+                       invalidateProperties();
+                       invalidateDisplayList();
+                       dispatchEvent(new Event("typedTextChange"));
+               }
+
+               //--------------------------------------------------------------------------
+               //      New event listener to restore item-click
+               //--------------------------------------------------------------------------
+
+               protected function itemClickHandler(event:ListEvent):void {
+                       typedTextChanged=false;
+                       textInput.text=itemToLabel(collection[event.rowIndex]);
+                       selectNextField();
+               }
+
+               protected function selectNextField():void {
+                       if (this.parent.parent is DataGrid) {
+                               this.parent.parent.dispatchEvent(new FocusEvent("keyFocusChange",true,true,null,false,9));
+                       } else {
+                               focusManager.getNextFocusManagerComponent(true).setFocus();
+                       }
+               }
+
+               //--------------------------------------------------------------------------
+               //      Overridden methods
+               //--------------------------------------------------------------------------
+
+               override protected function commitProperties():void {
+                       super.commitProperties();
+
+                       if (dropdown) {
+                               if (typedTextChanged) {
+                                       cursorPosition = textInput.selectionBeginIndex;
+                                       updateDataProvider();
+
+                                       if( collection.length==0 || typedText=="" || typedText==null ) {
+                                               // no suggestions, so no dropdown
+                                               dropdownClosed=true;
+                                               showDropdown=false;
+                                               showingDropdown=false;
+                                       } else {
+                                               // show dropdown
+                                               showDropdown = true;
+                                               selectedIndex = 0;
+                                       }
+                               }
+                       } else {
+                               selectedIndex=-1
+                       }
+               }
+               
+               override protected function updateDisplayList(unscaledWidth:Number, 
+                                                                 unscaledHeight:Number):void {
+
+                       super.updateDisplayList(unscaledWidth, unscaledHeight);
+                       
+                       if(selectedIndex == -1 && typedTextChanged && textInput.text!=typedText) { 
+                               // not in menu
+                               // trace("not in menu"); trace("- restoring to "+typedText);
+                               textInput.text = typedText;
+                               textInput.setSelection(textInput.text.length, textInput.text.length);
+                       } else if (dropdown && typedTextChanged && textInput.text!=typedText) {
+                               // in menu, but user has typed
+                               // trace("in menu, but user has typed"); trace("- restoring to "+typedText);
+                               textInput.text = typedText;
+                               textInput.setSelection(cursorPosition, cursorPosition);
+                       } else if (showingDropdown && textInput.text==selectedLabel) {
+                               // force update if Flex has fucked up again
+                               // trace("should force update");
+                               textInput.htmlText=selectedLabel;
+                               textInput.validateNow();
+                       } else if (showingDropdown && textInput.text!=selectedLabel && !typedTextChanged) {
+                               // in menu, user has navigated with cursor keys/mouse
+                               // trace("in menu, user has navigated with cursor keys/mouse");
+                               textInput.text = selectedLabel;
+                               textInput.setSelection(0, textInput.text.length);
+                       } else if (textInput.text!="") {
+                               textInput.setSelection(cursorPosition, cursorPosition);
+                       }
+
+                       if (showDropdown && !dropdown.visible) {
+                               // controls the open duration of the dropdown
+                               super.open();
+                               showDropdown = false;
+                               showingDropdown = true;
+                               dropdownClosed = false;
+                       }
+               }
+       
+               override protected function keyDownHandler(event:KeyboardEvent):void {
+                       super.keyDownHandler(event);
+
+                       if (event.keyCode==Keyboard.UP || event.keyCode==Keyboard.DOWN) {
+                               typedTextChanged=false;
+                       }
+
+                       if (event.keyCode==Keyboard.ESCAPE && showingDropdown) {
+                               // ESCAPE cancels dropdown
+                               textInput.text = typedText;
+                               textInput.setSelection(textInput.text.length, textInput.text.length);
+                               showingDropdown = false;
+                               dropdownClosed=true;
+
+                       } else if (event.keyCode == Keyboard.ENTER) {
+                               // ENTER pressed, so select the topmost item (if it exists)
+                               if (selectedIndex>-1) { textInput.text = selectedLabel; }
+                               dropdownClosed=true;
+                               
+                               // and move on to the next field
+                               event.stopImmediatePropagation();
+                               selectNextField();
+
+                       } else if (event.ctrlKey && event.keyCode == Keyboard.UP) {
+                               dropdownClosed=true;
+                       }
+               
+                       prevIndex = selectedIndex;
+               }
+       
+               override public function getStyle(styleProp:String):* {
+                       if (styleProp != "openDuration") {
+                               return super.getStyle(styleProp);
+                       } else {
+                               if (dropdownClosed) return super.getStyle(styleProp);
+                               else return 0;
+                       }
+               }
+
+               override protected function textInput_changeHandler(event:Event):void {
+                       super.textInput_changeHandler(event);
+                       typedText = text;
+                       typedTextChanged = true;
+               }
+
+               override protected function measure():void {
+                       super.measure();
+                       measuredWidth = mx.core.UIComponent.DEFAULT_MEASURED_WIDTH;
+               }
+
+               override public function set selectedIndex(value:int):void {
+                       var prevtext:String=text;
+                       super.selectedIndex=value;
+                       text=prevtext;
+               }
+
+
+               //----------------------------------
+               //      filterFunction
+               //----------------------------------
+               /**
+                *      A function that is used to select items that match the
+                *      function's criteria. 
+                *      A filterFunction is expected to have the following signature:
+                *
+                *      <pre>f(item:~~, text:String):Boolean</pre>
+                *
+                *      where the return value is <code>true</code> if the specified item
+                *      should displayed as a suggestion. 
+                *      Whenever there is a change in text in the AutoComplete control, this 
+                *      filterFunction is run on each item in the <code>dataProvider</code>.
+                *      
+                *      <p>The default implementation for filterFunction works as follows:<br>
+                *      If "AB" has been typed, it will display all the items matching 
+                *      "AB~~" (ABaa, ABcc, abAc etc.).</p>
+                *
+                *      <p>An example usage of a customized filterFunction is when text typed
+                *      is a regular expression and we want to display all the
+                *      items which come in the set.</p>
+                *
+                *      @example
+                *      <pre>
+                *      public function myFilterFunction(item:~~, text:String):Boolean
+                *      {
+                *         public var regExp:RegExp = new RegExp(text,"");
+                *         return regExp.test(item);
+                *      }
+                *      </pre>
+                *
+                */
+
+               private var _filterFunction:Function = defaultFilterFunction;
+               private var filterFunctionChanged:Boolean = true;
+
+               [Bindable("filterFunctionChange")]
+               [Inspectable(category="General")]
+
+               public function get filterFunction():Function {
+                       return _filterFunction;
+               }
+
+               public function set filterFunction(value:Function):void {
+                       //An empty filterFunction is allowed but not a null filterFunction
+                       if(value!=null) {
+                               _filterFunction = value;
+                               filterFunctionChanged = true;
+
+                               invalidateProperties();
+                               invalidateDisplayList();
+       
+                               dispatchEvent(new Event("filterFunctionChange"));
+                       } else {
+                               _filterFunction = defaultFilterFunction;
+                       }
+               }
+                               
+               private function defaultFilterFunction(element:*, text:String):Boolean {
+                       var label:String = itemToLabel(element);
+                       return (label.toLowerCase().substring(0,text.length) == text.toLowerCase());
+               }
+
+               private function templateFilterFunction(element:*):Boolean {
+                       var flag:Boolean=false;
+                       if(filterFunction!=null)
+                               flag=filterFunction(element,typedText);
+                       return flag;
+               }
+
+               // Updates the dataProvider used for showing suggestions
+               private function updateDataProvider():void {
+                       dataProvider = tempCollection;
+                       collection.filterFunction = templateFilterFunction;
+                       collection.refresh();
+               }
+       }       
+}
index f6cdb3f..2ca88c3 100644 (file)
@@ -77,7 +77,7 @@ package net.systemeD.potlatch2 {
                }
 
         private function mapMouseEvent(event:MouseEvent):void {
-            map.stage.focus = map.parent;
+            if (event.type!=MouseEvent.ROLL_OVER) map.stage.focus = map.parent;
             if (event.type==MouseEvent.MOUSE_UP && map.dragstate==map.DRAGGING) { return; }
             
             var mapLoc:Point = map.globalToLocal(new Point(event.stageX, event.stageY));
@@ -89,7 +89,7 @@ package net.systemeD.potlatch2 {
         }
         
         public function entityMouseEvent(event:MouseEvent, entity:Entity):void {
-            map.stage.focus = map.parent;
+            if (event.type!=MouseEvent.ROLL_OVER) map.stage.focus = map.parent;
             //if ( event.type == MouseEvent.MOUSE_DOWN )
             event.stopPropagation();
                 
index 244e1d4..38e526f 100644 (file)
@@ -2,6 +2,7 @@
 <mx:VBox
        xmlns:mx="http://www.adobe.com/2006/mxml"
        xmlns:flexlib="flexlib.containers.*"
+       xmlns:controls="net.systemeD.controls.*"
        horizontalScrollPolicy="off"
     backgroundColor="white"
     initialize="loadFeatures()">
             doubleClickEnabled="true"
             doubleClick="if (event.target.parent==advancedTagGrid) { addNewTag(); }">
                 <mx:columns>
-                    <mx:DataGridColumn editable="true" dataField="key" headerText="Key"/>
-                    <mx:DataGridColumn editable="true" dataField="value" headerText="Value"/>
+                    <mx:DataGridColumn editable="true" dataField="key" headerText="Key">
+                                               <mx:itemEditor>
+                                                       <mx:Component>
+                                                               <controls:AutoComplete 
+                                                                 dataProvider="{autoCompleteKeys()}" 
+                                                                 labelField="name" 
+                                                                 rowCount="10"
+                                                                 typedText="{getSelectedKey()}">
+                                                                       <mx:Script><![CDATA[
+                                                                               import net.systemeD.potlatch2.mapfeatures.MapFeatures;
+                                                                               import mx.collections.ArrayCollection;
+                                                                               import mx.controls.DataGrid;
+
+                                                                               public function autoCompleteKeys():Array {
+                                                                                       var a:Array=[];
+                                                                                       for each (var k:String in MapFeatures.getInstance().getAutoCompleteKeys()) {
+                                                                                               a.push( { name: k } );
+                                                                                       }
+                                                                                       return a;
+                                                                               }
+
+                                                                               internal function getSelectedKey():String {
+                                                                                       return DataGrid(this.parent.parent).selectedItem.key;
+                                                                               }
+                                                                       ]]></mx:Script>
+                                                               </controls:AutoComplete>
+                                                       </mx:Component>
+                                               </mx:itemEditor>
+                                       </mx:DataGridColumn>
+                    <mx:DataGridColumn editable="true" dataField="value" headerText="Value" />
                 </mx:columns>
         </mx:DataGrid>
 
       private var editorStackAccordion:Accordion;
       [Bindable] private var editorStack:Container;
 
-      private var mapFeatures:MapFeatures;
+      public var mapFeatures:MapFeatures;
       private var selectedEntity:Entity;
       private var tagDataProvider:ArrayCollection;
       private var tw:CategorySelector = null;
index d9f64e5..9319190 100644 (file)
@@ -7,6 +7,9 @@ package net.systemeD.potlatch2.mapfeatures {
        import flash.system.Security;
        import flash.net.*;
 
+       import mx.core.UIComponent;
+       import mx.controls.DataGrid;
+
     import net.systemeD.halcyon.connection.*;
        import net.systemeD.halcyon.DebugURLRequest;
 
@@ -21,11 +24,11 @@ package net.systemeD.potlatch2.mapfeatures {
             return instance;
         }
 
-
-
         private var xml:XML = null;
         private var _features:Array = null;
         private var _categories:Array = null;
+               private var _keys:Array = null;
+               private var _tags:Object = null;
 
         protected function loadFeatures():void {
             var request:DebugURLRequest = new DebugURLRequest("map_features.xml");
@@ -39,12 +42,23 @@ package net.systemeD.potlatch2.mapfeatures {
         }
         
         private function onFeatureLoad(event:Event):void {
+                       var f:Feature;
+
             xml = new XML(URLLoader(event.target).data);
-            
-            _features = new Array();
+            _features = [];
+                       _keys = [];
+                       _tags = {};
+
             for each(var feature:XML in xml.feature) {
-                _features.push(new Feature(this, feature));
+                f=new Feature(this,feature);
+                               _features.push(f);
+                               for each (var tag:Object in f.tags) {
+                                       if (!_tags[tag.k]) { _tags[tag.k]=new Object; _keys.push(tag.k); }
+                                       _tags[tag.k][tag.v]=true;
+                               }
             }            
+                       _keys.sort();
+
             _categories = new Array();
             for each(var catXML:XML in xml.category) {
                 if ( catXML.child("category").length() == 0 )
@@ -128,6 +142,19 @@ package net.systemeD.potlatch2.mapfeatures {
             }
             return pois;
         }
+
+               [Bindable(event="featuresLoaded")]
+               public function getAutoCompleteKeys():Array {
+                       return _keys;
+               }
+               
+               public function getAutoCompleteValues(key:String):Array {
+                       var list:Array=[];
+                       for (var v:String in _tags[key]) { list.push(v); }
+                       list.sort();
+                       return list;
+               }
+               
     }
 
 }