autocompleting on tagged contacts works now
authorAndrew Sutherland <asutherland@asutherland.org>
Mon, 29 Sep 2008 11:19:28 -0700
changeset 949 373953d7da1f66116791b18fe0cc6ede271ed380
parent 948 655c2444785380713384763e1576b8b08a50c2e6
child 950 0676f5abd3bb932df1f186b31bb95c251ce33898
push idunknown
push userunknown
push dateunknown
autocompleting on tagged contacts works now
components/glautocomp.js
content/glodacomplete.css
content/glodacomplete.xml
modules/datamodel.js
modules/datastore.js
modules/index_ab.js
modules/indexer.js
--- a/components/glautocomp.js
+++ b/components/glautocomp.js
@@ -62,20 +62,29 @@ ResultRowSingle.prototype = {
 };
 
 function ResultRowMulti(aNounID, aCriteriaType, aCriteria, aQuery) {
   this.nounID = aNounID;
   this.nounMeta = Gloda._nounIDToMeta[aNounID];
   this.criteriaType = aCriteriaType;
   this.criteria = aCriteria;
   this.collection = aQuery.getCollection(this);
+  this.renderer = null;
 }
 ResultRowMulti.prototype = {
   multi: true,
   onItemsAdded: function(aItems) {
+    LOG.debug("onItemsAdded");
+    if (this.renderer) {
+      LOG.debug("rendering...");
+      for each (let item in aItems) {
+        LOG.debug(" ..." + item);
+        this.renderer.renderItem(item);
+      }
+    }
   },
   onItemsModified: function(aItems) {
   },
   onItemsRemoved: function(aItems) {
   },
   onQueryCompleted: function() {
   }
 }
@@ -140,21 +149,21 @@ nsAutoCompleteGlodaResult.prototype = {
     let thing = this._results[aIndex];
     if (thing.value) // identity
       return thing.contact.name;
     else
       return thing.name || thing.subject;
   },
   // rich uses this to be the "type"
   getStyleAt: function(aIndex) {
-    let thing = this._results[aIndex];
-    if (thing.multi)
-      return "gloda-multi-" + thing.nounMeta.name;
+    let row = this._results[aIndex];
+    if (row.multi)
+      return "gloda-multi";
     else
-      return "gloda-single-" + thing.nounMeta.name;
+      return "gloda-single-" + row.nounMeta.name;
   },
   // rich uses this to be the icon
   getImageAt: function(aIndex) {
     let thing = this._results[aIndex];
     if (!thing.value)
       return null;
 
     let md5hash = GlodaUtils.md5HashString(thing.value);
@@ -325,21 +334,24 @@ ContactTagCompleter.prototype = {
   },
   complete: function ContactTagCompleter_complete(aResult, aString) {
     // now is not the best time to do this; have onFreeTagAdded use a timer.
     if (this.suffixTreeDirty)
       this._buildSuffixTree();
     
     if (aString.length < 2)
       return false; // no async mechanism that will add new rows
-      
+    
+    LOG.debug("Completing on tags...");
+    
     tags = this._suffixTree.findMatches(aString.toLowerCase());
     let rows = [];
     for each (let tag in tags) {
       let query = Gloda.newQuery(Gloda.NOUN_CONTACT);
+      LOG.debug("  checking for tag: " + tag.name);
       query.freeTags(tag);
       let resRow = new ResultRowMulti(Gloda.NOUN_CONTACT, "tag", tag.name,
                                       query);
       rows.push(resRow);
     }
     aResult.addRows(rows);
     
     return false; // no async mechanism that will add new rows
--- a/content/glodacomplete.css
+++ b/content/glodacomplete.css
@@ -17,15 +17,21 @@ panel[type="glodacomplete-richlistbox"] 
 }
 
 .autocomplete-richlistitem[type="gloda-single-identity"] {
   -moz-binding: url("chrome://gloda/content/glodacomplete.xml#gloda-single-identity-item");
   -moz-box-orient: vertical;
   overflow: -moz-hidden-unscrollable;
 }
 
-.autocomplete-richlistitem[type="gloda-multi-identity"] {
-  -moz-binding: url("chrome://gloda/content/glodacomplete.xml#gloda-multi-identity-item");
+richlistitem[type="gloda-contact-chunk"] {
+  -moz-binding: url("chrome://gloda/content/glodacomplete.xml#gloda-contact-chunk");
+  -moz-box-orient: vertical;
+  overflow: -moz-hidden-unscrollable;
+}
+
+.autocomplete-richlistitem[type="gloda-multi"] {
+  -moz-binding: url("chrome://gloda/content/glodacomplete.xml#gloda-multi-item");
   -moz-box-orient: vertical;
   overflow: -moz-hidden-unscrollable;
 }
 
 /* .autocomplete-history-dropmarker wants to be optional, but we don't care */
\ No newline at end of file
--- a/content/glodacomplete.xml
+++ b/content/glodacomplete.xml
@@ -98,17 +98,16 @@
             // set these attributes before we set the class
             // so that we can use them from the contructor
             var row = result.getObjectAt(this._currentIndex);
             var obj = row.item;
             item.setAttribute("text", trimmedSearchString);
             item.setAttribute("type", result.getStyleAt(this._currentIndex));
 
             item.row = row;
-            item.obj = obj;
 
             if (this._currentIndex < existingItemsCount) {
               // re-use the existing item
               item._adjustAcItem();
               item.collapsed = false;
             }
             else {
               // set the class at the end so we can use the attributes
@@ -124,39 +123,41 @@
           setTimeout(function (self) { self._appendCurrentResult(); }, 0, this);
         ]]>
         </body>
       </method>
     </implementation>
   </binding>
 
   <binding id="gloda-single-identity-item" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-richlistitem">
-    <content orient="horizontal">
-      <xul:image anonid="picture"/>
-      <xul:vbox>
-        <xul:hbox>
-          <xul:hbox anonid="name-box" class="ac-title" flex="1"
-                    onunderflow="_doUnderflow('_name');">
-            <xul:description anonid="name" class="ac-normal-text ac-comment"
-                             xbl:inherits="selected"/>
-          </xul:hbox>
-          <xul:label anonid="name-overflow-ellipsis" xbl:inherits="selected"
-	                 class="ac-ellipsis-after ac-comment" hidden="true"/>
-        </xul:hbox>
-        <xul:hbox>
-          <xul:hbox anonid="identity-box" class="ac-url" flex="1"
-                    onunderflow="_doUnderflow('_identity');">
-            <xul:description anonid="identity" class="ac-normal-text ac-url-text"
-                             xbl:inherits="selected"/>
-          </xul:hbox>
-          <xul:label anonid="identity-overflow-ellipsis" xbl:inherits="selected"
-                     class="ac-ellipsis-after ac-url-text" hidden="true"/>
-          <xul:image anonid="type-image" class="ac-type-icon"/>
-        </xul:hbox>
-      </xul:vbox>
+    <content>
+      <xul:hbox>
+	    <xul:image anonid="picture"/>
+	    <xul:vbox>
+	      <xul:hbox>
+	        <xul:hbox anonid="name-box" class="ac-title" flex="1"
+	                  onunderflow="_doUnderflow('_name');">
+	          <xul:description anonid="name" class="ac-normal-text ac-comment"
+	                           xbl:inherits="selected"/>
+	        </xul:hbox>
+	        <xul:label anonid="name-overflow-ellipsis" xbl:inherits="selected"
+	                   class="ac-ellipsis-after ac-comment" hidden="true"/>
+	      </xul:hbox>
+	      <xul:hbox>
+	        <xul:hbox anonid="identity-box" class="ac-url" flex="1"
+	                  onunderflow="_doUnderflow('_identity');">
+	          <xul:description anonid="identity" class="ac-normal-text ac-url-text"
+	                           xbl:inherits="selected"/>
+	        </xul:hbox>
+	        <xul:label anonid="identity-overflow-ellipsis" xbl:inherits="selected"
+	                   class="ac-ellipsis-after ac-url-text" hidden="true"/>
+	        <xul:image anonid="type-image" class="ac-type-icon"/>
+	      </xul:hbox>
+	    </xul:vbox>
+	  </xul:hbox>
     </content>
     <implementation implements="nsIDOMXULSelectControlItemElement">
       <constructor>
         <![CDATA[
             let ellipsis = "\u2026";
             try {
               ellipsis = Components.classes["@mozilla.org/preferences-service;1"].
                 getService(Components.interfaces.nsIPrefBranch).
@@ -184,26 +185,26 @@
 
             this._adjustAcItem();
           ]]>
       </constructor>
       
       <property name="label" readonly="true">
         <getter>
           <![CDATA[
-            var item = this.getAttribute("item");
-            return item.accessibleLabel;
+            var identity = this.row.item;
+            return identity.accessibleLabel;
           ]]>
         </getter>
       </property>
       
       <method name="_adjustAcItem">
         <body>
           <![CDATA[
-          var identity = this.obj;
+          var identity = this.row.item;
           
           if (identity == null)
             return;
           
           // I guess we should get the picture size from CSS or something?
           this._picture.src = identity.pictureURL(32);
           
           // Emphasize the matching search terms for the description
@@ -212,13 +213,172 @@
 
           // Set up overflow on a timeout because the contents of the box
           // might not have a width yet even though we just changed them
           setTimeout(this._setUpOverflow, 0, this._nameBox, this._nameOverflowEllipsis);
           setTimeout(this._setUpOverflow, 0, this._identityBox, this._identityOverflowEllipsis);
           ]]>
         </body>
       </method>
-
     </implementation>
   </binding>
 
+  <binding id="gloda-contact-chunk" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-richlistitem">
+    <content orient="horizontal">
+      <xul:image anonid="picture"/>
+      <xul:vbox>
+        <xul:hbox>
+          <xul:hbox anonid="name-box" class="ac-title" flex="1"
+                    onunderflow="_doUnderflow('_name');">
+            <xul:description anonid="name" class="ac-normal-text ac-comment"
+                             xbl:inherits="selected"/>
+          </xul:hbox>
+          <xul:label anonid="name-overflow-ellipsis" xbl:inherits="selected"
+                     class="ac-ellipsis-after ac-comment" hidden="true"/>
+        </xul:hbox>
+        <xul:hbox>
+          <xul:hbox anonid="identity-box" class="ac-url" flex="1"
+                    onunderflow="_doUnderflow('_identity');">
+            <xul:description anonid="identity" class="ac-normal-text ac-url-text"
+                             xbl:inherits="selected"/>
+          </xul:hbox>
+          <xul:label anonid="identity-overflow-ellipsis" xbl:inherits="selected"
+                     class="ac-ellipsis-after ac-url-text" hidden="true"/>
+          <xul:image anonid="type-image" class="ac-type-icon"/>
+        </xul:hbox>
+      </xul:vbox>
+    </content>
+    <implementation>
+      <constructor>
+        <![CDATA[
+            let ellipsis = "\u2026";
+            try {
+              ellipsis = Components.classes["@mozilla.org/preferences-service;1"].
+                getService(Components.interfaces.nsIPrefBranch).
+                getComplexValue("intl.ellipsis",
+                  Components.interfaces.nsIPrefLocalizedString).data;
+            } catch (ex) {
+              // Do nothing.. we already have a default
+            }
+
+            this._identityOverflowEllipsis = document.getAnonymousElementByAttribute(this, "anonid", "identity-overflow-ellipsis");
+            this._nameOverflowEllipsis = document.getAnonymousElementByAttribute(this, "anonid", "name-overflow-ellipsis");
+
+            this._identityOverflowEllipsis.value = ellipsis;
+            this._nameOverflowEllipsis.value = ellipsis;
+
+            this._typeImage = document.getAnonymousElementByAttribute(this, "anonid", "type-image");
+
+            this._identityBox = document.getAnonymousElementByAttribute(this, "anonid", "identity-box");
+            this._identity = document.getAnonymousElementByAttribute(this, "anonid", "identity");
+
+            this._nameBox = document.getAnonymousElementByAttribute(this, "anonid", "name-box");
+            this._name = document.getAnonymousElementByAttribute(this, "anonid", "name");
+            
+            this._picture = document.getAnonymousElementByAttribute(this, "anonid", "picture");
+
+            this._adjustAcItem();
+          ]]>
+      </constructor>
+      
+      <property name="label" readonly="true">
+        <getter>
+          <![CDATA[
+            var identity = this.obj;
+            return identity.accessibleLabel;
+          ]]>
+        </getter>
+      </property>
+      
+      <method name="_adjustAcItem">
+        <body>
+          <![CDATA[
+          var contact = this.obj;
+          
+          if (contact == null)
+            return;
+          
+          var identity = contact.identities[0];
+          
+          // I guess we should get the picture size from CSS or something?
+          this._picture.src = identity.pictureURL(32);
+          
+          // Emphasize the matching search terms for the description
+          this._setUpDescription(this._name, contact.name);
+          this._setUpDescription(this._identity, identity.value);
+
+          // Set up overflow on a timeout because the contents of the box
+          // might not have a width yet even though we just changed them
+          setTimeout(this._setUpOverflow, 0, this._nameBox, this._nameOverflowEllipsis);
+          setTimeout(this._setUpOverflow, 0, this._identityBox, this._identityOverflowEllipsis);
+          ]]>
+        </body>
+      </method>
+    </implementation>
+  </binding>
+
+  <binding id="gloda-multi-item" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-richlistitem">
+    <content orient="vertical">
+      <xul:description anonid="explanation"/>
+      <xul:hbox anonid="identity-holder" flex="1">
+      </xul:hbox>
+    </content>
+    <implementation implements="nsIDOMXULSelectControlItemElement">
+      <constructor>
+        <![CDATA[
+            this._explanation = document.getAnonymousElementByAttribute(this, "anonid", "explanation");
+            this._identityHolder = document.getAnonymousElementByAttribute(this, "anonid", "identity-holder");
+
+            this._adjustAcItem();
+          ]]>
+      </constructor>
+      
+      <property name="label" readonly="true">
+        <getter>
+          <![CDATA[
+            return this._explanation.value;
+          ]]>
+        </getter>
+      </property>
+      
+      <method name="renderItem">
+        <parameter name="aObj"/>
+        <body>
+          var node = document.createElementNS(
+            "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+            "richlistitem");
+          
+          node.obj = aObj;
+          node.setAttribute("type",
+                            "gloda-" + this.row.nounMeta.name + "-chunk");
+          
+          this._identityHolder.appendChild(node);
+        </body>
+      </method>
+      
+      <method name="_adjustAcItem">
+        <body>
+          <![CDATA[
+          // clear out any lingering children
+          while (this._identityHolder.hasChildNodes())
+            this._identityHolder.removeChild(this._identityHolder.firstChild);
+          
+          var row = this.row;
+          if (row == null)
+            return;
+          
+          this._explanation.value = row.nounMeta.name + "s " +
+            row.criteriaType + "ed " + row.criteria;
+          
+          // render anyone already in there
+          for each (let item in row.collection.items) {
+            this.renderItem(item);
+          }
+          // listen up, yo.
+          row.renderer = this;
+          ]]>
+        </body>
+      </method>
+    </implementation>
+  </binding>
+
+
 </bindings>
\ No newline at end of file
--- a/modules/datamodel.js
+++ b/modules/datamodel.js
@@ -87,16 +87,18 @@ GlodaAttributeDef.prototype = {
   get objectNounMeta() { return this._objectNounMeta; },
 
   get isBound() { return this._boundName !== null; },
   get boundName() { return this._boundName; },
   get singular() { return this._singular; },
 
   get special() { return this._special; },
   get specialColumnName() { return this._specialColumnName; },
+  
+  get parameterBindings() { return this._parameterBindings; },
 
   /**
    * Bind a parameter value to the attribute definition, allowing use of the
    *  attribute-parameter as an attribute.
    *
    * @return
    */
   bindParameter: function gloda_attr_bindParameter(aValue) {
--- a/modules/datastore.js
+++ b/modules/datastore.js
@@ -2080,18 +2080,17 @@ var GlodaDatastore = {
           else
             attributeID = APV[0].id;
           if (attributeID != lastAttributeID) {
             valueTests = [];
             if (APV[0].special == kSpecialColumn ||
                 APV[0].special == kSpecialString)
               attrValueTests.push(["", valueTests]);
             else
-              attrValueTests.push(["attributeID = " + attributeID + " AND ",
-                                   valueTests]);
+              attrValueTests.push(["attributeID = " + attributeID, valueTests]);
             lastAttributeID = attributeID;
           }
 
           // straight value match?
           if (APV.length == 3) {
             if (APV[2] != null)
               valueTests.push(valueColumnName + " = " + valueQuoter(APV[2]));
           }
@@ -2125,17 +2124,21 @@ var GlodaDatastore = {
               }
               valueTests.push(valueColumnName + " LIKE ? ESCAPE '/'");
               boundArgs.push(likePayload);
             }
           }
         }
         let select = "SELECT " + idColumnName + " FROM " + tableName +
                      " WHERE " +
-                     [("(" + avt[0] + "(" + avt[1].join(" OR ") + "))")
+                     [("(" + avt[0] +
+                       (avt[1].length ? ((avt[0] ? " AND " : "") + "(" 
+                            + avt[1].join(" OR ") + ")") :
+                          "")
+                       + ")")
                       for each (avt in attrValueTests)].join(" OR ");
         selects.push(select);
       }
 
       if (selects.length)
         whereClauses.push("id IN (" + selects.join(" INTERSECT ") + " )");
     }
 
--- a/modules/index_ab.js
+++ b/modules/index_ab.js
@@ -218,17 +218,18 @@ var GlodaABAttrs = {
                         singular: false,
                         subjectNouns: [Gloda.NOUN_CONTACT],
                         objectNoun: Gloda.lookupNoun("freetag"),
                         parameterNoun: null,
                         explanation: null,
                         }); // not-tested
     // we need to find any existing bound freetag attributes, and use them to
     //  populate to FreeTagNoun's understanding
-    for (let freeTagName in this._attrFreeTag._bindings) {
+    for (let freeTagName in this._attrFreeTag.parameterBindings) {
+      this._log.debug("Telling FreeTagNoun about: " + freeTagName);
       FreeTagNoun.getFreeTag(freeTagName);
     }
   },
   
   process: function(aContact, aCard) {
     if (aContact.NOUN_ID != Gloda.NOUN_CONTACT) {
       this._log.warning("Somehow got a non-contact: " + aContact);
       return [];
--- a/modules/indexer.js
+++ b/modules/indexer.js
@@ -714,17 +714,17 @@ var GlodaIndexer = {
     // if leave folder was't cleared first, remove the listener; everyone else
     //  will be nulled out in the exception handler below if things go south
     //  on this folder.
     if (this._indexingFolder !== null) {
       this._indexingDatabase.RemoveListener(this._databaseAnnouncerListener);
     }
     
     let folderURI = GlodaDatastore._mapFolderID(aFolderID);
-    this._log.debug("Active Folder URI: " + folderURI);
+    //this._log.debug("Active Folder URI: " + folderURI);
   
     let rdfService = Cc['@mozilla.org/rdf/rdf-service;1'].
                      getService(Ci.nsIRDFService);
     let folder = rdfService.GetResource(folderURI);
     folder.QueryInterface(Ci.nsIMsgFolder); // (we want to explode in the try
     // if this guy wasn't what we wanted)
     this._indexingFolder = folder;
     this._indexingFolderID = aFolderID;
@@ -977,22 +977,22 @@ var GlodaIndexer = {
       
       this._curIndexingJob = null;
       this._indexingDesired = false;
       this._indexingJobCount = 0;
       this._indexingJobGoal = 0;
       return false;
     }
 
-    this._log.debug("++ Pulling job from queue of size " +
-                    this._indexQueue.length);
+    //this._log.debug("++ Pulling job from queue of size " +
+    //                this._indexQueue.length);
     let job = this._curIndexingJob = this._indexQueue.shift();
     this._indexingJobCount++;
-    this._log.debug("++ Pulled job: " + job.jobType + ", " +
-                    job.deltaType + ", " + job.id);
+    //this._log.debug("++ Pulled job: " + job.jobType + ", " +
+    //                job.deltaType + ", " + job.id);
     let generator = null;
     
     if (job.jobType == "sweep") {
       this._actualWorker = this._worker_indexingSweep(job);
     }
     else if (job.jobType == "folder") {
       this._actualWorker = this._worker_folderIndex(job);
     }