changeset 49:090d5ab4a449

refactor code for retrieving and maintaining a collection of messages into a SnowlCollection object so it can be reused by multiple views
author Myk Melez <myk@mozilla.org>
date Fri, 02 May 2008 17:32:05 -0700
parents 241f8a8ad7a8
children f83981ab4c88
files extension/content/snowl.js extension/modules/collection.js
diffstat 2 files changed, 83 insertions(+), 179 deletions(-) [+]
line wrap: on
line diff
--- a/extension/content/snowl.js	Thu May 01 18:42:35 2008 -0700
+++ b/extension/content/snowl.js	Fri May 02 17:32:05 2008 -0700
@@ -1,5 +1,6 @@
 Cu.import("resource://snowl/modules/service.js");
 Cu.import("resource://snowl/modules/datastore.js");
+Cu.import("resource://snowl/modules/collection.js");
 Cu.import("resource://snowl/modules/log4moz.js");
 
 let SnowlView = {
@@ -59,8 +60,8 @@
   // nsITreeView
 
   get rowCount() {
-    this._log.info("get rowCount: " + this._model.length);
-    return this._model.length;
+    this._log.info("get rowCount: " + this._collection.messages.length);
+    return this._collection.messages.length;
   },
 
   getCellText: function(aRow, aColumn) {
@@ -68,11 +69,11 @@
     // IDs and property names here.
     switch(aColumn.id) {
       case "snowlAuthorCol":
-        return this._model[aRow].author;
+        return this._collection.messages[aRow].author;
       case "snowlSubjectCol":
-        return this._model[aRow].subject;
+        return this._collection.messages[aRow].subject;
       case "snowlTimestampCol":
-        return this._formatTimestamp(new Date(this._model[aRow].timestamp));
+        return this._formatTimestamp(new Date(this._collection.messages[aRow].timestamp));
       default:
         return null;
     }
@@ -94,7 +95,7 @@
     // because the styling we apply to unread messages (bold text) has to be
     // specified by the ::-moz-tree-cell-text pseudo-element, which inherits
     // only the cell's properties.
-    if (!this._model[aRow].read)
+    if (!this._collection.messages[aRow].read)
       aProperties.AppendElement(this._atomSvc.getAtom("unread"));
   },
 
@@ -107,132 +108,6 @@
 
 
   //**************************************************************************//
-  // Model Generation
-
-  // A JavaScript data structure storing the data that appears in the view.
-  //
-  // Another way of doing this would be to select the data into a temporary
-  // table or view (either in the normal database or in the in-memory database).
-  // I'm not sure which approach is more memory-efficient or faster.
-  //
-  // Also, it's inaccurate to call this the model, since the model is really
-  // the database itself.  This is rather a view on the model, but I can't think
-  // of a good name for that.
-  _model: null,
-
-  _rebuildModel: function() {
-    let conditions = [];
-
-    // Step one: generate the query string.
-
-    // FIXME: use a left join here again like we used to do before we hit
-    // the bug with left joins to virtual tables that has been fixed
-    // with the upgrade to 3.5.8 on April 17.
-    if (this._filter.value)
-      conditions.push("messages.id IN (SELECT messageID FROM parts WHERE content MATCH :filter)");
-
-    if (this.sourceID)
-      conditions.push("sourceID = :sourceID");
-
-    let statementString = 
-      //"SELECT sources.title AS sourceTitle, subject, author, link, timestamp, content \
-      // FROM sources JOIN messages ON sources.id = messages.sourceID \
-      // LEFT JOIN parts on messages.id = parts.messageID";
-      "SELECT sources.title AS sourceTitle, messages.id AS id, " +
-             "subject, author, link, timestamp, read " +
-      "FROM sources JOIN messages ON sources.id = messages.sourceID";
-
-    if (conditions.length > 0)
-      statementString += " WHERE " + conditions.join(" AND ");
-
-    // XXX Would it be faster to let the DB engine sort the model?
-    //statementString += " ORDER BY timestamp DESC";
-
-
-    // Step two: create the statement and bind parameters to it.
-
-    let statement = SnowlDatastore.createStatement(statementString);
-
-    if (this._filter.value)
-      statement.params.filter = this._filter.value;
-
-    if (this.sourceID)
-      statement.params.sourceID = this.sourceID;
-
-
-    // Step three: execute the query and retrieve its results.
-    this._model = [];
-    try {
-      while (statement.step()) {
-        this._model.push({ id:          statement.row.id,
-                           sourceTitle: statement.row.sourceTitle,
-                           subject:     statement.row.subject,
-                           author:      statement.row.author,
-                           link:        statement.row.link,
-                           timestamp:   statement.row.timestamp,
-                           read:        (statement.row.read ? true : false)
-                           //,content: statement.row.content
-                      });
-      }
-    }
-    catch(ex) {
-      this._log.error(statementString + ": " + ex + ": " + SnowlDatastore.dbConnection.lastErrorString + "\n");
-      throw ex;
-    }
-    finally {
-      statement.reset();
-    }
-
-
-    // Step four: sort the results by the current sort property and direction.
-
-    let sortResource = this._tree.getAttribute("sortResource");
-    let property = this._columnToPropertyTranslations[sortResource];
-    let sortDirection = this._tree.getAttribute("sortDirection");
-    let order = sortDirection == "descending" ? -1 : 1;
-    this._sortModel(property, order);
-  },
-
-  _sortModel: function(aProperty, aOrder) {
-    let compare = function(a, b) {
-      if (SnowlView._prepareObjectForComparison(a[aProperty]) >
-          SnowlView._prepareObjectForComparison(b[aProperty]))
-        return 1 * aOrder;
-      if (SnowlView._prepareObjectForComparison(a[aProperty]) <
-          SnowlView._prepareObjectForComparison(b[aProperty]))
-        return -1 * aOrder;
-
-      // Fall back on the "subject" property.
-      if (aProperty != "subject") {
-        if (SnowlView._prepareObjectForComparison(a.subject) >
-            SnowlView._prepareObjectForComparison(b.subject))
-          return 1 * aOrder;
-        if (SnowlView._prepareObjectForComparison(a.subject) <
-            SnowlView._prepareObjectForComparison(b.subject))
-          return -1 * aOrder;
-      }
-
-      // Return an inconclusive result.
-      return 0;
-    };
-
-    this._model.sort(compare);
-  },
-
-  _prepareObjectForComparison: function(aObject) {
-    if (typeof aObject == "string")
-      return aObject.toLowerCase();
-
-    // Null values are neither greater than nor less than strings, so we
-    // convert them into empty strings, which is how they appear to users.
-    if (aObject == null)
-      return "";
-
-    return aObject;
-  },
-
-
-  //**************************************************************************//
   // Initialization and Destruction
 
   init: function() {
@@ -243,7 +118,7 @@
     if (container.getAttribute("placement") == "side")
       this.placeOnSide();
 
-    this._rebuildModel();
+    this._collection = new SnowlCollection();
     this._tree.view = this;
   },
 
@@ -279,20 +154,35 @@
   // Event & Notification Handling
 
   _onMessagesChanged: function() {
-this._log.info("_onMessagesChanged");
-    this._rebuildModel();
+    // FIXME: make the collection listen for message changes and invalidate
+    // itself, then rebuild the view in a timeout to give the collection time
+    // to do so.
+    this._collection.invalidate();
 
-    // The number of rows might have changed, so we need to let the tree know
-    // about that, and the simplest way to do it is to reinitialize the view.
+    // Rebuild the view to reflect the new collection of messages.
+    // Since the number of rows might have changed, we do this by reinitializing
+    // the view instead of merely invalidating the box object (which doesn't
+    // expect changes to the number of rows).
     this._tree.view = this;
   },
 
   onFilter: function() {
-this._log.info("onFilter");
-    this._rebuildModel();
+    this._collection.filter = this._filter.value;
 
-    // The number of rows might have changed, so we need to let the tree know
-    // about that, and the simplest way to do it is to reinitialize the view.
+    // Rebuild the view to reflect the new collection of messages.
+    // Since the number of rows might have changed, we do this by reinitializing
+    // the view instead of merely invalidating the box object (which doesn't
+    // expect changes to the number of rows).
+    this._tree.view = this;
+  },
+
+  setSource: function(aSourceID) {
+    this._collection.sourceID = aSourceID;
+
+    // Rebuild the view to reflect the new collection of messages.
+    // Since the number of rows might have changed, we do this by reinitializing
+    // the view instead of merely invalidating the box object (which doesn't
+    // expect changes to the number of rows).
     this._tree.view = this;
   },
 
@@ -405,7 +295,7 @@
     // http://lxr.mozilla.org/mozilla/source/browser/base/content/browser.js#1482
 
     let row = this._tree.currentIndex;
-    let message = this._model[row];
+    let message = this._collection.messages[row];
 
     window.loadURI(message.link, null, null, false);
     this._setRead(true);
@@ -450,11 +340,11 @@
 
     while (i != currentIndex) {
       if (i < 0) {
-        i = this._model.length - 1;
+        i = this._collection.messages.length - 1;
         continue;
       }
 
-      if (!this._model[i].read) {
+      if (!this._collection.messages[i].read) {
         this.selection.select(i);
         this._tree.treeBoxObject.ensureRowIsVisible(i);
         break;
@@ -469,12 +359,12 @@
     let i = currentIndex + 1;
 
     while (i != currentIndex) {
-      if (i > this._model.length - 1) {
+      if (i > this._collection.messages.length - 1) {
         i = 0;
         continue;
       }
 this._log.info(i);
-      if (!this._model[i].read) {
+      if (!this._collection.messages[i].read) {
         this.selection.select(i);
         this._tree.treeBoxObject.ensureRowIsVisible(i);
         break;
@@ -490,7 +380,7 @@
       return;
 
     let row = this._tree.currentIndex;
-    let message = this._model[row];
+    let message = this._collection.messages[row];
     if (aAll)
       this._setAllRead(!message.read);
     else
@@ -499,7 +389,7 @@
 
   _setRead: function(aRead) {
     let row = this._tree.currentIndex;
-    let message = this._model[row];
+    let message = this._collection.messages[row];
 
 try {
     SnowlDatastore.dbConnection.executeSimpleSQL("UPDATE messages SET read = " +
@@ -516,24 +406,14 @@
 
   _setAllRead: function(aRead) {
 this._log.info("_setAllRead: aRead? " + aRead);
-    let ids = this._model.map(function(v) { return v.id });
+    let ids = this._collection.messages.map(function(v) { return v.id });
     SnowlDatastore.dbConnection.executeSimpleSQL("UPDATE messages SET read = " +
                                                  (aRead ? "1" : "0") +
                                                  " WHERE id IN (" + ids.join(",") + ")");
-    this._model.forEach(function(v) { v.read = aRead });
+    this._collection.messages.forEach(function(v) { v.read = aRead });
     this._tree.boxObject.invalidate();
   },
 
-  setSource: function(aSourceID) {
-this._log.info("setSource: " + aSourceID);
-    this.sourceID = aSourceID;
-    this._rebuildModel();
-
-    // The number of rows might have changed, so we need to let the tree know
-    // about that, and the simplest way to do it is to reinitialize the view.
-    this._tree.view = this;
-  },
-
   toggle: function() {
     let container = document.getElementById("snowlViewContainer");
     let splitter = document.getElementById("snowlViewSplitter");
@@ -564,7 +444,7 @@
       order = sortDirection == "ascending" ? -1 : 1;
 
     // Perform the sort.
-    this._sortModel(property, order);
+    this._collection.sort(property, order);
 
     // Persist the sort options.
     let direction = order == 1 ? "ascending" : "descending";
--- a/extension/modules/collection.js	Thu May 01 18:42:35 2008 -0700
+++ b/extension/modules/collection.js	Fri May 02 17:32:05 2008 -0700
@@ -5,19 +5,39 @@
 const Cr = Components.results;
 const Cu = Components.utils;
 
+Cu.import("resource://snowl/modules/datastore.js");
 Cu.import("resource://snowl/modules/message.js");
 
 /**
  * A group of messages.
  */
 function SnowlCollection(aSourceID, aFilter) {
-  this.sourceID = aSourceID;
-  this.filter = aFilter;
+  this._sourceID = aSourceID;
+  this._filter = aFilter;
 }
 
 SnowlCollection.prototype = {
-  sourceID: null,
-  filter: null,
+  _sourceID: null,
+
+  get sourceID() {
+    return this._sourceID;
+  },
+
+  set sourceID(newVal) {
+    this._sourceID = newVal;
+    this.invalidate();
+  },
+
+  _filter: null,
+
+  get filter() {
+    return this._filter;
+  },
+
+  set filter(newVal) {
+    this._filter = newVal;
+    this.invalidate();
+  },
 
   sortProperty: "timestamp",
   sortOrder: 1,
@@ -45,11 +65,15 @@
       statement.reset();
     }
 
-    this._sort();
+    this.sort(this.sortProperty, this.sortOrder);
 
     return this._messages;
   },
 
+  invalidate: function() {
+    this._messages = null;
+  },
+
   _generateStatement: function() {
     let query = 
       //"SELECT sources.title AS sourceTitle, subject, author, link, timestamp, content \
@@ -83,26 +107,26 @@
     return statement;
   },
 
-  _sort: function() {
-    let property = this.sortProperty;
-    let order = this.sortOrder;
+  sort: function(aProperty, aOrder) {
+    this.sortProperty = aProperty;
+    this.sortOrder = aOrder;
 
     let compare = function(a, b) {
-      if (prepareObjectForComparison(a[property]) >
-          prepareObjectForComparison(b[property]))
-        return 1 * order;
-      if (prepareObjectForComparison(a[property]) <
-          prepareObjectForComparison(b[property]))
-        return -1 * order;
+      if (prepareObjectForComparison(a[aProperty]) >
+          prepareObjectForComparison(b[aProperty]))
+        return 1 * aOrder;
+      if (prepareObjectForComparison(a[aProperty]) <
+          prepareObjectForComparison(b[aProperty]))
+        return -1 * aOrder;
 
-      // Fall back on the "subject" property.
-      if (property != "subject") {
+      // Fall back on the "subject" aProperty.
+      if (aProperty != "subject") {
         if (prepareObjectForComparison(a.subject) >
             prepareObjectForComparison(b.subject))
-          return 1 * order;
+          return 1 * aOrder;
         if (prepareObjectForComparison(a.subject) <
             prepareObjectForComparison(b.subject))
-          return -1 * order;
+          return -1 * aOrder;
       }
 
       // Return an inconclusive result.