Mercurial > snowl
view extension/content/collections.js @ 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 | extension/content/sidebar.js@33e517403dc8 |
children | e005621994a3 |
line wrap: on
line source
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);