changeset 183:b497c5463801

move the collections view into an overlay to facilitate its reuse inside the river view
author Myk Melez <myk@mozilla.org>
date Sun, 20 Jul 2008 20:21:27 -0700
parents 489b8c2db12d
children e005621994a3
files extension/chrome.manifest extension/content/collections.css extension/content/collections.js extension/content/sidebar.css extension/content/sidebar.js extension/content/sidebar.xul
diffstat 6 files changed, 435 insertions(+), 479 deletions(-) [+]
line wrap: on
line diff
--- a/extension/chrome.manifest	Sun Jul 20 19:13:25 2008 -0700
+++ b/extension/chrome.manifest	Sun Jul 20 20:21:27 2008 -0700
@@ -1,4 +1,5 @@
 content snowl content/
 locale  snowl en-US locale/en-US/
 overlay chrome://browser/content/browser.xul chrome://snowl/content/snowl.xul
+overlay chrome://snowl/content/sidebar.xul chrome://snowl/content/collections.xul
 resource snowl ./
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/extension/content/collections.css	Sun Jul 20 20:21:27 2008 -0700
@@ -0,0 +1,7 @@
+toolbarbutton {
+  -moz-appearance: none;
+}
+
+.toolbarbutton-icon {
+  -moz-margin-end: 0;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/extension/content/collections.js	Sun Jul 20 20:21:27 2008 -0700
@@ -0,0 +1,422 @@
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://snowl/modules/service.js");
+Cu.import("resource://snowl/modules/datastore.js");
+Cu.import("resource://snowl/modules/log4moz.js");
+Cu.import("resource://snowl/modules/source.js");
+Cu.import("resource://snowl/modules/feed.js");
+Cu.import("resource://snowl/modules/URI.js");
+Cu.import("resource://snowl/modules/identity.js");
+Cu.import("resource://snowl/modules/collection.js");
+
+// FIXME: call this SnowlViewWindow to facilitate reuse of this sidebar code
+// in the river view, where the window it will reference will not be a browser
+// window.
+var gBrowserWindow = window.QueryInterface(Ci.nsIInterfaceRequestor).
+                     getInterface(Ci.nsIWebNavigation).
+                     QueryInterface(Ci.nsIDocShellTreeItem).
+                     rootTreeItem.
+                     QueryInterface(Ci.nsIInterfaceRequestor).
+                     getInterface(Ci.nsIDOMWindow);
+
+SourcesView = {
+  _log: null,
+
+  // Observer Service
+  get _obsSvc() {
+    let obsSvc = Cc["@mozilla.org/observer-service;1"].
+                 getService(Ci.nsIObserverService);
+    delete this._obsSvc;
+    this._obsSvc = obsSvc;
+    return this._obsSvc;
+  },
+
+  get _tree() {
+    delete this._tree;
+    return this._tree = document.getElementById("sourcesView");
+  },
+
+  get _children() {
+    delete this._children;
+    return this._children = this._tree.getElementsByTagName("treechildren")[0];
+  },
+
+
+  //**************************************************************************//
+  // Initialization & Destruction
+
+  init: function() {
+    this._log = Log4Moz.Service.getLogger("Snowl.Sidebar");
+    this._obsSvc.addObserver(this, "sources:changed", true);
+    this._getCollections();
+    this._tree.view = this;
+
+    // Add a capturing click listener to the tree so we can find out if the user
+    // clicked on a row that is already selected (in which case we let them edit
+    // the collection name).
+    // FIXME: disable this for names that can't be changed.
+    this._tree.addEventListener("mousedown", function(aEvent) { SourcesView.onClick(aEvent) }, true);
+  },
+
+
+  //**************************************************************************//
+  // nsITreeView
+
+  selection: null,
+
+  get rowCount() {
+    return this._rows.length;
+  },
+
+  // FIXME: consolidate these two references.
+  _treebox: null,
+  setTree: function(treeBox) {
+    this._treeBox = treeBox;
+  },
+
+  getCellText : function(row, column) {
+    return this._rows[row].name;
+  },
+
+  isContainer: function(row) {
+    //this._log.info("isContainer: " + (this._rows[row].groups ? true : false));
+    return (this._rows[row].groups ? true : false);
+  },
+  isContainerOpen: function(row) {
+    //this._log.info("isContainerOpen: " + this._rows[row].isOpen);
+    return this._rows[row].isOpen;
+  },
+  isContainerEmpty: function(row) {
+    //this._log.info("isContainerEmpty: " + row + " " + this._rows[row].groups.length + " " + (this._rows[row].groups.length == 0));
+    return (this._rows[row].groups.length == 0);
+  },
+
+  isSeparator: function(row)         { return false },
+  isSorted: function()               { return false },
+
+  // FIXME: make this return true for collection names that are editable,
+  // and then implement name editing on the new architecture.
+  isEditable: function(row, column)  { return false },
+
+  getParentIndex: function(row) {
+    //this._log.info("getParentIndex: " + row);
+
+    let thisLevel = this.getLevel(row);
+
+    if (this._rows[row].level == 0)
+      return -1;
+    for (let t = row - 1; t >= 0; t--)
+      if (this.getLevel(t) < thisLevel)
+        return t;
+
+    throw "getParentIndex: couldn't figure out parent index for row " + row;
+  },
+
+  getLevel: function(row) {
+    //this._log.info("getLevel: " + row);
+
+    return this._rows[row].level;
+  },
+
+  hasNextSibling: function(idx, after) {
+    //this._log.info("hasNextSibling: " + idx + " " + after);
+
+    let thisLevel = this.getLevel(idx);
+    for (let t = idx + 1; t < this._rows.length; t++) {
+      let nextLevel = this.getLevel(t);
+      if (nextLevel == thisLevel)
+        return true;
+      if (nextLevel < thisLevel)
+        return false;
+    }
+
+    return false;
+  },
+
+  getImageSrc: function(row, column) {
+    if (column.id == "nameCol") {
+      let faviconURI = this._rows[row].faviconURI;
+      if (faviconURI)
+        return faviconURI.spec;
+    }
+
+    return null;
+  },
+
+  toggleOpenState: function(idx) {
+    //this._log.info("toggleOpenState: " + idx);
+
+    let item = this._rows[idx];
+    if (!item.groups)
+      return;
+
+    if (item.isOpen) {
+      item.isOpen = false;
+
+      let thisLevel = this.getLevel(idx);
+      let numToDelete = 0;
+      for (let t = idx + 1; t < this._rows.length; t++) {
+        if (this.getLevel(t) > thisLevel)
+          numToDelete++;
+        else
+          break;
+      }
+      if (numToDelete) {
+        this._rows.splice(idx + 1, numToDelete);
+        this._treeBox.rowCountChanged(idx + 1, -numToDelete);
+      }
+    }
+    else {
+      item.isOpen = true;
+
+      let groups = this._rows[idx].groups;
+      for (let i = 0; i < groups.length; i++)
+        this._rows.splice(idx + 1 + i, 0, groups[i]);
+      this._treeBox.rowCountChanged(idx + 1, groups.length);
+    }
+  },
+
+  getRowProperties: function (aRow, aProperties) {},
+  getCellProperties: function (aRow, aColumn, aProperties) {},
+  getColumnProperties: function(aColumnID, aColumn, aProperties) {},
+
+  setCellText: function(aRow, aCol, aValue) {
+    let statement = SnowlDatastore.createStatement("UPDATE sources SET name = :name WHERE id = :id");
+    statement.params.name = this._rows[aRow].name = aValue;
+    statement.params.id = this._rows[aRow].id;
+
+    try {
+      statement.execute();
+    }
+    finally {
+      statement.reset();
+    }
+  },
+
+
+  //**************************************************************************//
+  // Misc XPCOM Interface Implementations
+
+  // nsISupports
+  QueryInterface: function(aIID) {
+    if (aIID.equals(Ci.nsIObserver) ||
+        aIID.equals(Ci.nsISupportsWeakReference) ||
+        aIID.equals(Ci.nsISupports))
+      return this;
+
+    throw Cr.NS_ERROR_NO_INTERFACE;
+  },
+
+  // nsIObserver
+  observe: function(subject, topic, data) {
+    switch (topic) {
+      case "sources:changed":
+        this._getCollections();
+        // 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;
+        break;
+    }
+  },
+
+  _collections: null,
+  _getCollections: function() {
+    this._collections = [];
+
+    let all = new SnowlCollection();
+    all.name = "All";
+    all.defaultFaviconURI = URI.get("chrome://snowl/content/icons/rainbow.png");
+    this._collections.push(all);
+
+    let grouping = {
+      nameColumn: "sources.name",
+      uriColumn: "sources.humanURI",
+      // the default favicon for sources
+      // FIXME: use a source type-specific favicon.
+      defaultFaviconURI: URI.get("chrome://browser/skin/feeds/feedIcon16.png")
+    }
+    let collection = new SnowlCollection(null, null, grouping);
+    collection.name = "Sources";
+    this._collections.push(collection);
+
+    {
+      let grouping = {
+        nameColumn: "authors.name"
+        // FIXME: get a favicon for people
+      }
+      let collection = new SnowlCollection(null, null, grouping);
+      collection.name = "People";
+      this._collections.push(collection);
+    }
+
+    // Build the list of rows in the tree.  By default, all containers
+    // are closed, so this is the same as the list of collections, although
+    // in the future we might persist and restore the open state.
+    // XXX Should this work be in a separate function?
+    this._rows = [collection for each (collection in this._collections)];
+  },
+
+  onSelect: function(aEvent) {
+    if (this._tree.currentIndex == -1)
+      return;
+
+    let collection = this._rows[this._tree.currentIndex];
+    gBrowserWindow.SnowlView.setCollection(collection);
+  },
+
+  onClick: function(aEvent) {
+this._log.info("on click");
+//this._log.info(Log4Moz.enumerateProperties(aEvent).join("\n"));
+//this._log.info(aEvent.target.nodeName);
+
+  let row = {}, col = {}, child = {};
+  this._tree.treeBoxObject.getCellAt(aEvent.clientX, aEvent.clientY, row, col, child);
+  if (this._tree.view.selection.isSelected(row.value))
+this._log.info(row.value + " is selected");
+else
+this._log.info(row.value + " is not selected");
+  },
+
+  subscribe: function(event) {
+    gBrowserWindow.gBrowser.selectedTab =
+      gBrowserWindow.gBrowser.addTab("chrome://snowl/content/subscribe.xul");
+  },
+
+  unsubscribe: function(aEvent) {
+    let sourceID = this._collections[this._tree.currentIndex].id;
+
+    SnowlDatastore.dbConnection.beginTransaction();
+    try {
+      SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM metadata WHERE messageID IN (SELECT id FROM messages WHERE sourceID = " + sourceID + ")");
+      SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM parts WHERE messageID IN (SELECT id FROM messages WHERE sourceID = " + sourceID + ")");
+      SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM messages WHERE sourceID = " + sourceID);
+      SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM sources WHERE id = " + sourceID);
+      SnowlDatastore.dbConnection.commitTransaction();
+    }
+    catch(ex) {
+      SnowlDatastore.dbConnection.rollbackTransaction();
+      throw ex;
+    }
+
+    this._obsSvc.notifyObservers(null, "sources:changed", null);
+    this._obsSvc.notifyObservers(null, "messages:changed", null);
+  },
+
+
+  //**************************************************************************//
+  // OPML Export
+  // Based on code in Thunderbird's feed-subscriptions.js
+
+  exportOPML: function() {
+    let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+    fp.init(window, "Export feeds as an OPML file", Ci.nsIFilePicker.modeSave);
+    fp.appendFilter("OPML Files", "*.opml");
+    fp.appendFilters(Ci.nsIFilePicker.filterXML | Ci.nsIFilePicker.filterAll);
+    fp.defaultString = "feeds.opml";
+    fp.defaultExtension = "opml";
+
+    let rv = fp.show();
+
+    if (rv == Ci.nsIFilePicker.returnCancel)
+      return;
+
+    let doc = this._createOPMLDocument();
+
+    // Format the document with newlines and indentation so it's easier
+    // for humans to read.
+    this._prettifyNode(doc.documentElement, 0);
+
+    let serializer = new XMLSerializer();
+    let foStream = Cc["@mozilla.org/network/file-output-stream;1"].
+                   createInstance(Ci.nsIFileOutputStream);
+    // default mode:  write | create | truncate
+    let mode = 0x02 | 0x08 | 0x20;
+    foStream.init(fp.file, mode, 0666, 0);
+    serializer.serializeToStream(doc, foStream, "utf-8");
+  },
+
+  _createOPMLDocument: function() {
+    let doc = document.implementation.createDocument("", "opml", null);
+    let root = doc.documentElement;
+    root.setAttribute("version", "1.0");
+
+    // Create the <head> element.
+    let head = doc.createElement("head");
+    root.appendChild(head);
+
+    let title = doc.createElement("title");
+    head.appendChild(title);
+    title.appendChild(doc.createTextNode("Snowl OPML Export"));
+
+    let dt = doc.createElement("dateCreated");
+    head.appendChild(dt);
+    dt.appendChild(doc.createTextNode((new Date()).toGMTString()));
+
+    // Create the <body> element.
+    let body = doc.createElement("body");
+    root.appendChild(body);
+
+    // Populate the <body> element with <outline> elements.
+    let sources = SnowlSource.getAll();
+    for each (let source in sources) {
+      let outline = doc.createElement("outline");
+      // XXX Should we specify the |type| attribute, and should we specify
+      // type="atom" for Atom feeds or just type="rss" for all feeds?
+      // This document says the latter but is three years old:
+      // http://www.therssweblog.com/?guid=20051003145153
+      //outline.setAttribute("type", "rss");
+      outline.setAttribute("text", source.name);
+      outline.setAttribute("url", source.humanURI.spec);
+      outline.setAttribute("xmlUrl", source.machineURI.spec);
+      body.appendChild(outline);
+    }
+
+    return doc;
+  },
+
+  _prettifyNode: function(node, level) {
+    let doc = node.ownerDocument;
+
+    // Create a string containing two spaces for every level deep we are.
+    let indentString = new Array(level + 1).join("  ");
+
+    // Indent the tag.
+    if (level > 0)
+      node.parentNode.insertBefore(doc.createTextNode(indentString), node);
+
+    // Grab the list of nodes to format.  We can't just use node.childNodes
+    // because it'd change under us as we insert formatting nodes.
+    let childNodesToFormat = [];
+    for (let i = 0; i < node.childNodes.length; i++)
+      if (node.childNodes[i].nodeType == node.ELEMENT_NODE)
+        childNodesToFormat.push(node.childNodes[i]);
+
+    if (childNodesToFormat.length > 0) {
+      for each (let childNode in childNodesToFormat)
+        this._prettifyNode(childNode, level + 1);
+
+      // Insert a newline after the opening tag.
+      node.insertBefore(doc.createTextNode("\n"), node.firstChild);
+  
+      // Indent the closing tag.
+      node.appendChild(doc.createTextNode(indentString));
+    }
+
+    // Insert a newline after the tag.
+    if (level > 0) {
+      if (node.nextSibling)
+        node.parentNode.insertBefore(doc.createTextNode("\n"),
+                                     node.nextSibling);
+      else
+        node.parentNode.appendChild(doc.createTextNode("\n"));
+    }
+  }
+
+};
+
+window.addEventListener("load", function() { SourcesView.init() }, false);
--- a/extension/content/sidebar.css	Sun Jul 20 19:13:25 2008 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-
-toolbarbutton {
-  -moz-appearance: none;
-}
-
-.toolbarbutton-icon {
-  -moz-margin-end: 0;
-}
-
-.statusBox > .statusIcon {
-  list-style-image: url("chrome://snowl/content/icons/asterisk_orange.png");
-}
-
-.statusBox[status="active"] > .statusIcon {
-  list-style-image: url("chrome://global/skin/icons/loading_16.png");
-}
-
-.statusBox[status="complete"] > .statusIcon {
-  list-style-image: url("chrome://snowl/content/icons/tick.png");
-}
--- a/extension/content/sidebar.js	Sun Jul 20 19:13:25 2008 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,422 +0,0 @@
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cr = Components.results;
-const Cu = Components.utils;
-
-Cu.import("resource://snowl/modules/service.js");
-Cu.import("resource://snowl/modules/datastore.js");
-Cu.import("resource://snowl/modules/log4moz.js");
-Cu.import("resource://snowl/modules/source.js");
-Cu.import("resource://snowl/modules/feed.js");
-Cu.import("resource://snowl/modules/URI.js");
-Cu.import("resource://snowl/modules/identity.js");
-Cu.import("resource://snowl/modules/collection.js");
-
-// FIXME: call this SnowlViewWindow to facilitate reuse of this sidebar code
-// in the river view, where the window it will reference will not be a browser
-// window.
-var gBrowserWindow = window.QueryInterface(Ci.nsIInterfaceRequestor).
-                     getInterface(Ci.nsIWebNavigation).
-                     QueryInterface(Ci.nsIDocShellTreeItem).
-                     rootTreeItem.
-                     QueryInterface(Ci.nsIInterfaceRequestor).
-                     getInterface(Ci.nsIDOMWindow);
-
-SourcesView = {
-  _log: null,
-
-  // Observer Service
-  get _obsSvc() {
-    let obsSvc = Cc["@mozilla.org/observer-service;1"].
-                 getService(Ci.nsIObserverService);
-    delete this._obsSvc;
-    this._obsSvc = obsSvc;
-    return this._obsSvc;
-  },
-
-  get _tree() {
-    delete this._tree;
-    return this._tree = document.getElementById("sourcesView");
-  },
-
-  get _children() {
-    delete this._children;
-    return this._children = this._tree.getElementsByTagName("treechildren")[0];
-  },
-
-
-  //**************************************************************************//
-  // Initialization & Destruction
-
-  init: function() {
-    this._log = Log4Moz.Service.getLogger("Snowl.Sidebar");
-    this._obsSvc.addObserver(this, "sources:changed", true);
-    this._getCollections();
-    this._tree.view = this;
-
-    // Add a capturing click listener to the tree so we can find out if the user
-    // clicked on a row that is already selected (in which case we let them edit
-    // the collection name).
-    // FIXME: disable this for names that can't be changed.
-    this._tree.addEventListener("mousedown", function(aEvent) { SourcesView.onClick(aEvent) }, true);
-  },
-
-
-  //**************************************************************************//
-  // nsITreeView
-
-  selection: null,
-
-  get rowCount() {
-    return this._rows.length;
-  },
-
-  // FIXME: consolidate these two references.
-  _treebox: null,
-  setTree: function(treeBox) {
-    this._treeBox = treeBox;
-  },
-
-  getCellText : function(row, column) {
-    return this._rows[row].name;
-  },
-
-  isContainer: function(row) {
-    //this._log.info("isContainer: " + (this._rows[row].groups ? true : false));
-    return (this._rows[row].groups ? true : false);
-  },
-  isContainerOpen: function(row) {
-    //this._log.info("isContainerOpen: " + this._rows[row].isOpen);
-    return this._rows[row].isOpen;
-  },
-  isContainerEmpty: function(row) {
-    //this._log.info("isContainerEmpty: " + row + " " + this._rows[row].groups.length + " " + (this._rows[row].groups.length == 0));
-    return (this._rows[row].groups.length == 0);
-  },
-
-  isSeparator: function(row)         { return false },
-  isSorted: function()               { return false },
-
-  // FIXME: make this return true for collection names that are editable,
-  // and then implement name editing on the new architecture.
-  isEditable: function(row, column)  { return false },
-
-  getParentIndex: function(row) {
-    //this._log.info("getParentIndex: " + row);
-
-    let thisLevel = this.getLevel(row);
-
-    if (this._rows[row].level == 0)
-      return -1;
-    for (let t = row - 1; t >= 0; t--)
-      if (this.getLevel(t) < thisLevel)
-        return t;
-
-    throw "getParentIndex: couldn't figure out parent index for row " + row;
-  },
-
-  getLevel: function(row) {
-    //this._log.info("getLevel: " + row);
-
-    return this._rows[row].level;
-  },
-
-  hasNextSibling: function(idx, after) {
-    //this._log.info("hasNextSibling: " + idx + " " + after);
-
-    let thisLevel = this.getLevel(idx);
-    for (let t = idx + 1; t < this._rows.length; t++) {
-      let nextLevel = this.getLevel(t);
-      if (nextLevel == thisLevel)
-        return true;
-      if (nextLevel < thisLevel)
-        return false;
-    }
-
-    return false;
-  },
-
-  getImageSrc: function(row, column) {
-    if (column.id == "nameCol") {
-      let faviconURI = this._rows[row].faviconURI;
-      if (faviconURI)
-        return faviconURI.spec;
-    }
-
-    return null;
-  },
-
-  toggleOpenState: function(idx) {
-    //this._log.info("toggleOpenState: " + idx);
-
-    let item = this._rows[idx];
-    if (!item.groups)
-      return;
-
-    if (item.isOpen) {
-      item.isOpen = false;
-
-      let thisLevel = this.getLevel(idx);
-      let numToDelete = 0;
-      for (let t = idx + 1; t < this._rows.length; t++) {
-        if (this.getLevel(t) > thisLevel)
-          numToDelete++;
-        else
-          break;
-      }
-      if (numToDelete) {
-        this._rows.splice(idx + 1, numToDelete);
-        this._treeBox.rowCountChanged(idx + 1, -numToDelete);
-      }
-    }
-    else {
-      item.isOpen = true;
-
-      let groups = this._rows[idx].groups;
-      for (let i = 0; i < groups.length; i++)
-        this._rows.splice(idx + 1 + i, 0, groups[i]);
-      this._treeBox.rowCountChanged(idx + 1, groups.length);
-    }
-  },
-
-  getRowProperties: function (aRow, aProperties) {},
-  getCellProperties: function (aRow, aColumn, aProperties) {},
-  getColumnProperties: function(aColumnID, aColumn, aProperties) {},
-
-  setCellText: function(aRow, aCol, aValue) {
-    let statement = SnowlDatastore.createStatement("UPDATE sources SET name = :name WHERE id = :id");
-    statement.params.name = this._rows[aRow].name = aValue;
-    statement.params.id = this._rows[aRow].id;
-
-    try {
-      statement.execute();
-    }
-    finally {
-      statement.reset();
-    }
-  },
-
-
-  //**************************************************************************//
-  // Misc XPCOM Interface Implementations
-
-  // nsISupports
-  QueryInterface: function(aIID) {
-    if (aIID.equals(Ci.nsIObserver) ||
-        aIID.equals(Ci.nsISupportsWeakReference) ||
-        aIID.equals(Ci.nsISupports))
-      return this;
-
-    throw Cr.NS_ERROR_NO_INTERFACE;
-  },
-
-  // nsIObserver
-  observe: function(subject, topic, data) {
-    switch (topic) {
-      case "sources:changed":
-        this._getCollections();
-        // 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;
-        break;
-    }
-  },
-
-  _collections: null,
-  _getCollections: function() {
-    this._collections = [];
-
-    let all = new SnowlCollection();
-    all.name = "All";
-    all.defaultFaviconURI = URI.get("chrome://snowl/content/icons/rainbow.png");
-    this._collections.push(all);
-
-    let grouping = {
-      nameColumn: "sources.name",
-      uriColumn: "sources.humanURI",
-      // the default favicon for sources
-      // FIXME: use a source type-specific favicon.
-      defaultFaviconURI: URI.get("chrome://browser/skin/feeds/feedIcon16.png")
-    }
-    let collection = new SnowlCollection(null, null, grouping);
-    collection.name = "Sources";
-    this._collections.push(collection);
-
-    {
-      let grouping = {
-        nameColumn: "authors.name"
-        // FIXME: get a favicon for people
-      }
-      let collection = new SnowlCollection(null, null, grouping);
-      collection.name = "People";
-      this._collections.push(collection);
-    }
-
-    // Build the list of rows in the tree.  By default, all containers
-    // are closed, so this is the same as the list of collections, although
-    // in the future we might persist and restore the open state.
-    // XXX Should this work be in a separate function?
-    this._rows = [collection for each (collection in this._collections)];
-  },
-
-  onSelect: function(aEvent) {
-    if (this._tree.currentIndex == -1)
-      return;
-
-    let collection = this._rows[this._tree.currentIndex];
-    gBrowserWindow.SnowlView.setCollection(collection);
-  },
-
-  onClick: function(aEvent) {
-this._log.info("on click");
-//this._log.info(Log4Moz.enumerateProperties(aEvent).join("\n"));
-//this._log.info(aEvent.target.nodeName);
-
-  let row = {}, col = {}, child = {};
-  this._tree.treeBoxObject.getCellAt(aEvent.clientX, aEvent.clientY, row, col, child);
-  if (this._tree.view.selection.isSelected(row.value))
-this._log.info(row.value + " is selected");
-else
-this._log.info(row.value + " is not selected");
-  },
-
-  subscribe: function(event) {
-    gBrowserWindow.gBrowser.selectedTab =
-      gBrowserWindow.gBrowser.addTab("chrome://snowl/content/subscribe.xul");
-  },
-
-  unsubscribe: function(aEvent) {
-    let sourceID = this._collections[this._tree.currentIndex].id;
-
-    SnowlDatastore.dbConnection.beginTransaction();
-    try {
-      SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM metadata WHERE messageID IN (SELECT id FROM messages WHERE sourceID = " + sourceID + ")");
-      SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM parts WHERE messageID IN (SELECT id FROM messages WHERE sourceID = " + sourceID + ")");
-      SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM messages WHERE sourceID = " + sourceID);
-      SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM sources WHERE id = " + sourceID);
-      SnowlDatastore.dbConnection.commitTransaction();
-    }
-    catch(ex) {
-      SnowlDatastore.dbConnection.rollbackTransaction();
-      throw ex;
-    }
-
-    this._obsSvc.notifyObservers(null, "sources:changed", null);
-    this._obsSvc.notifyObservers(null, "messages:changed", null);
-  },
-
-
-  //**************************************************************************//
-  // OPML Export
-  // Based on code in Thunderbird's feed-subscriptions.js
-
-  exportOPML: function() {
-    let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
-    fp.init(window, "Export feeds as an OPML file", Ci.nsIFilePicker.modeSave);
-    fp.appendFilter("OPML Files", "*.opml");
-    fp.appendFilters(Ci.nsIFilePicker.filterXML | Ci.nsIFilePicker.filterAll);
-    fp.defaultString = "feeds.opml";
-    fp.defaultExtension = "opml";
-
-    let rv = fp.show();
-
-    if (rv == Ci.nsIFilePicker.returnCancel)
-      return;
-
-    let doc = this._createOPMLDocument();
-
-    // Format the document with newlines and indentation so it's easier
-    // for humans to read.
-    this._prettifyNode(doc.documentElement, 0);
-
-    let serializer = new XMLSerializer();
-    let foStream = Cc["@mozilla.org/network/file-output-stream;1"].
-                   createInstance(Ci.nsIFileOutputStream);
-    // default mode:  write | create | truncate
-    let mode = 0x02 | 0x08 | 0x20;
-    foStream.init(fp.file, mode, 0666, 0);
-    serializer.serializeToStream(doc, foStream, "utf-8");
-  },
-
-  _createOPMLDocument: function() {
-    let doc = document.implementation.createDocument("", "opml", null);
-    let root = doc.documentElement;
-    root.setAttribute("version", "1.0");
-
-    // Create the <head> element.
-    let head = doc.createElement("head");
-    root.appendChild(head);
-
-    let title = doc.createElement("title");
-    head.appendChild(title);
-    title.appendChild(doc.createTextNode("Snowl OPML Export"));
-
-    let dt = doc.createElement("dateCreated");
-    head.appendChild(dt);
-    dt.appendChild(doc.createTextNode((new Date()).toGMTString()));
-
-    // Create the <body> element.
-    let body = doc.createElement("body");
-    root.appendChild(body);
-
-    // Populate the <body> element with <outline> elements.
-    let sources = SnowlSource.getAll();
-    for each (let source in sources) {
-      let outline = doc.createElement("outline");
-      // XXX Should we specify the |type| attribute, and should we specify
-      // type="atom" for Atom feeds or just type="rss" for all feeds?
-      // This document says the latter but is three years old:
-      // http://www.therssweblog.com/?guid=20051003145153
-      //outline.setAttribute("type", "rss");
-      outline.setAttribute("text", source.name);
-      outline.setAttribute("url", source.humanURI.spec);
-      outline.setAttribute("xmlUrl", source.machineURI.spec);
-      body.appendChild(outline);
-    }
-
-    return doc;
-  },
-
-  _prettifyNode: function(node, level) {
-    let doc = node.ownerDocument;
-
-    // Create a string containing two spaces for every level deep we are.
-    let indentString = new Array(level + 1).join("  ");
-
-    // Indent the tag.
-    if (level > 0)
-      node.parentNode.insertBefore(doc.createTextNode(indentString), node);
-
-    // Grab the list of nodes to format.  We can't just use node.childNodes
-    // because it'd change under us as we insert formatting nodes.
-    let childNodesToFormat = [];
-    for (let i = 0; i < node.childNodes.length; i++)
-      if (node.childNodes[i].nodeType == node.ELEMENT_NODE)
-        childNodesToFormat.push(node.childNodes[i]);
-
-    if (childNodesToFormat.length > 0) {
-      for each (let childNode in childNodesToFormat)
-        this._prettifyNode(childNode, level + 1);
-
-      // Insert a newline after the opening tag.
-      node.insertBefore(doc.createTextNode("\n"), node.firstChild);
-  
-      // Indent the closing tag.
-      node.appendChild(doc.createTextNode(indentString));
-    }
-
-    // Insert a newline after the tag.
-    if (level > 0) {
-      if (node.nextSibling)
-        node.parentNode.insertBefore(doc.createTextNode("\n"),
-                                     node.nextSibling);
-      else
-        node.parentNode.appendChild(doc.createTextNode("\n"));
-    }
-  }
-
-};
-
-window.addEventListener("load", function() { SourcesView.init() }, false);
--- a/extension/content/sidebar.xul	Sun Jul 20 19:13:25 2008 -0700
+++ b/extension/content/sidebar.xul	Sun Jul 20 20:21:27 2008 -0700
@@ -1,4 +1,5 @@
 <?xml version="1.0"?>
+
 <!-- ***** BEGIN LICENSE BLOCK *****
    - Version: MPL 1.1/GPL 2.0/LGPL 2.1
    -
@@ -36,44 +37,11 @@
    - ***** END LICENSE BLOCK ***** -->
 
 <?xml-stylesheet href="chrome://global/skin/" type"text/css"?>
-<?xml-stylesheet href="chrome://snowl/content/sidebar.css" type"text/css"?>
-
-<page id="snowlSidebar" title="Subscriptions"
-      xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
-
-  <script type="application/x-javascript" src="chrome://snowl/content/sidebar.js"/>
 
-  <toolbar>
-    <!-- FIXME: Note in credits that silk icons licensed
-       - from http://www.famfamfam.com/lab/icons/silk/
-       - under http://creativecommons.org/licenses/by/2.5/ -->
-    <toolbarbutton id="snowlSubscribeButton"
-                   image="chrome://snowl/content/icons/add.png"
-                   oncommand="SourcesView.subscribe()"/>
-    <toolbarbutton id="snowlUnsubscribeButton"
-                   image="chrome://snowl/content/icons/delete.png"
-                   oncommand="SourcesView.unsubscribe()"/>
-    <toolbarbutton id="snowlRefreshButton"
-                   image="chrome://snowl/content/icons/arrow_refresh_small.png"
-                   oncommand="SnowlService.refreshAllSources()"/>
-    <!-- FIXME: Note in credits that OPML icon licensed from opmlicons.com
-       - under http://creativecommons.org/licenses/by-sa/2.5/ -->
-    <toolbarbutton id="snowlExportOPMLButton"
-                   image="chrome://snowl/content/icons/opml-icon-16x16.png"
-                   oncommand="SourcesView.exportOPML()"/>
-  </toolbar>
+<page id="snowlSidebar"
+      xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+      title="Snowl Sidebar">
 
-  <tree id="sourcesView" flex="1" context="sourcesContextMenu" editable="true"
-        onselect="SourcesView.onSelect(event)">
-    <treecols>
-      <treecol id="nameCol" label="Name" primary="true" flex="1"/>
-    </treecols>
-
-    <treechildren flex="1"/>
-  </tree>
-
-  <menupopup id="sourcesContextMenu">
-    <menuitem label="Unsubscribe" oncommand="SourcesView.unsubscribe(event)"/>
-  </menupopup>
+  <vbox id="collectionsViewBox" flex="1"/>
 
 </page>