Mercurial > snowl
changeset 234:2b79c0d8a019
split snowl.xul up into a file for the list view and a file for the browser integration points, and localize both of them; also, localize collections
author | Myk Melez <myk@mozilla.org> |
---|---|
date | Mon, 04 Aug 2008 16:58:54 -0700 |
parents | 3996e2477977 |
children | fea841e2b6fa |
files | chrome.manifest content/browser.xul content/collections.xul content/list.css content/list.js content/list.xul content/snowl.css content/snowl.js content/snowl.xul locale/en-US/browser.dtd locale/en-US/collections.dtd locale/en-US/list.dtd |
diffstat | 12 files changed, 847 insertions(+), 777 deletions(-) [+] |
line wrap: on
line diff
--- a/chrome.manifest Mon Aug 04 16:08:58 2008 -0700 +++ b/chrome.manifest Mon Aug 04 16:58:54 2008 -0700 @@ -1,5 +1,8 @@ content snowl content/ locale snowl en-US locale/en-US/ -overlay chrome://browser/content/browser.xul chrome://snowl/content/snowl.xul + +overlay chrome://browser/content/browser.xul chrome://snowl/content/browser.xul +overlay chrome://browser/content/browser.xul chrome://snowl/content/list.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/content/browser.xul Mon Aug 04 16:58:54 2008 -0700 @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- ***** BEGIN LICENSE BLOCK ***** + - Version: MPL 1.1/GPL 2.0/LGPL 2.1 + - + - The contents of this file are subject to the Mozilla Public License Version + - 1.1 (the "License"); you may not use this file except in compliance with + - the License. You may obtain a copy of the License at + - http://www.mozilla.org/MPL/ + - + - Software distributed under the License is distributed on an "AS IS" basis, + - WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + - for the specific language governing rights and limitations under the + - License. + - + - The Original Code is Snowl. + - + - The Initial Developer of the Original Code is Mozilla. + - Portions created by the Initial Developer are Copyright (C) 2008 + - the Initial Developer. All Rights Reserved. + - + - Contributor(s): + - Myk Melez <myk@mozilla.org> + - + - Alternatively, the contents of this file may be used under the terms of + - either the GNU General Public License Version 2 or later (the "GPL"), or + - the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + - in which case the provisions of the GPL or the LGPL are applicable instead + - of those above. If you wish to allow use of your version of this file only + - under the terms of either the GPL or the LGPL, and not to allow others to + - use your version of this file under the terms of the MPL, indicate your + - decision by deleting the provisions above and replace them with the notice + - and other provisions required by the GPL or the LGPL. If you do not delete + - the provisions above, a recipient may use your version of this file under + - the terms of any one of the MPL, the GPL or the LGPL. + - + - ***** END LICENSE BLOCK ***** --> + +<!DOCTYPE overlay SYSTEM "chrome://snowl/locale/browser.dtd"> + +<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="snowlBrowserOverlay"> + + <menupopup id="menu_viewPopup"> + <!-- Since these both get inserted after viewSidebarMenuMenu, they appear + - in the reverse of the order below. --> + <menuitem insertafter="viewSidebarMenuMenu" + label="&riverView.label;" accesskey="&riverView.accesskey;" + oncommand="gBrowser.selectedTab = gBrowser.addTab('chrome://snowl/content/river.xul')"/> + + <menuitem observes="viewSnowlSidebar" insertafter="viewSidebarMenuMenu" + accesskey="&listView.accesskey;"/> + </menupopup> + + <broadcasterset id="mainBroadcasterSet"> + <broadcaster id="viewSnowlSidebar" + label="&listView.label;" + autoCheck="false" + type="checkbox" + group="sidebar" + sidebarurl="chrome://snowl/content/sidebar.xul" + sidebartitle="&sidebar.label;" + oncommand="toggleSidebar('viewSnowlSidebar')"/> + </broadcasterset> + +</overlay>
--- a/content/collections.xul Mon Aug 04 16:08:58 2008 -0700 +++ b/content/collections.xul Mon Aug 04 16:58:54 2008 -0700 @@ -38,19 +38,17 @@ <?xml-stylesheet href="chrome://snowl/content/collections.css" type"text/css"?> +<!DOCTYPE overlay SYSTEM "chrome://snowl/locale/collections.dtd"> + <overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> <script type="application/x-javascript" src="chrome://snowl/content/collections.js"/> <vbox id="collectionsViewBox"> - <menupopup id="sourcesContextMenu"> - <menuitem label="Unsubscribe" oncommand="SourcesView.unsubscribe(event)"/> - </menupopup> - <tree id="sourcesView" flex="1" context="sourcesContextMenu" editable="true" onselect="SourcesView.onSelect(event)"> <treecols> - <treecol id="nameCol" label="Name" primary="true" flex="1"/> + <treecol id="nameCol" label="&nameCol.label;" primary="true" flex="1"/> </treecols> <treechildren flex="1"/> @@ -59,16 +57,20 @@ <toolbar> <toolbarbutton id="snowlSubscribeButton" image="chrome://snowl/content/icons/add.png" - oncommand="SourcesView.subscribe()"/> + oncommand="SourcesView.subscribe()" + tooltiptext="&subscribeButton.tooltip;"/> <toolbarbutton id="snowlUnsubscribeButton" image="chrome://snowl/content/icons/delete.png" - oncommand="SourcesView.unsubscribe()"/> + oncommand="SourcesView.unsubscribe()" + tooltiptext="&unsubscribeButton.tooltip;"/> <toolbarbutton id="snowlRefreshButton" image="chrome://snowl/content/icons/arrow_refresh_small.png" - oncommand="SnowlService.refreshAllSources()"/> + oncommand="SnowlService.refreshAllSources()" + tooltiptext="&refreshButton.tooltip;"/> <toolbarbutton id="snowlExportOPMLButton" image="chrome://snowl/content/icons/opml-icon-16x16.png" - oncommand="SourcesView.exportOPML()"/> + oncommand="SourcesView.exportOPML()" + tooltiptext="&exportButton.tooltip;"/> </toolbar> </vbox>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/content/list.css Mon Aug 04 16:58:54 2008 -0700 @@ -0,0 +1,66 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Snowl. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Myk Melez <myk@mozilla.org> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +treechildren::-moz-tree-cell-text(unread) { + font-weight: bold !important +} + +/* Don't style the buttons natively because they look ugly on Linux. + * FIXME: test on other platforms and style conditionally as appropriate, + * and figure out how to make them look pretty on Linux. */ +.snowlToolbarButton { + -moz-appearance: none; + min-width: 24px; + min-height: 24px; +} + +/* Hide the button labels, since they would otherwise take up space, + * even if empty, and throw off the centering of the button icons. */ +.snowlToolbarButton .toolbarbutton-text { + display: none; +} + +#snowlViewContainer[placement="top"] #snowlPlacementButton { + list-style-image: url("chrome://snowl/content/icons/application_tile_horizontal.png"); +} + +#snowlViewContainer[placement="side"] #snowlPlacementButton { + list-style-image: url("chrome://snowl/content/icons/application_split.png"); +} + +#snowlView { + height: 12em; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/content/list.js Mon Aug 04 16:58:54 2008 -0700 @@ -0,0 +1,577 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Snowl. + * + * The Initial Developer of the Original Code is Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Myk Melez <myk@mozilla.org> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +Cu.import("resource://snowl/modules/service.js"); +Cu.import("resource://snowl/modules/datastore.js"); +Cu.import("resource://snowl/modules/collection.js"); + +// FIXME: import these into an object to avoid name collisions. +Cu.import("resource://snowl/modules/log4moz.js"); +Cu.import("resource://snowl/modules/URI.js"); +Cu.import("resource://snowl/modules/Preferences.js"); + +let Snowl = { + get _prefs() { + delete this._prefs; + return this._prefs = new Preferences("extensions.snowl."); + }, + + get _version() { + let em = Cc["@mozilla.org/extensions/manager;1"]. + getService(Ci.nsIExtensionManager); + let addon = em.getItemForID("snowl@mozilla.org"); + delete this._version; + return this._version = addon.version; + }, + + init: function() { + let lastVersion = this._prefs.get("lastVersion"); + + if (!lastVersion) { + let url = "chrome://snowl/content/firstrun.html"; + setTimeout(function() { window.openUILinkIn(url, "tab") }, 500); + } + else if (lastVersion != this._version) { + let url = "chrome://snowl/content/update.html?old=" + lastVersion + + "&new=" + this._version; + setTimeout(function() { window.openUILinkIn(url, "tab"); }, 500); + } + + this._prefs.set("lastVersion", this._version); + } +}; + + +let SnowlMessageView = { + _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; + }, + + // Date Formatting Service + get _dfSvc() { + let dfSvc = Cc["@mozilla.org/intl/scriptabledateformat;1"]. + getService(Ci.nsIScriptableDateFormat); + delete this._dfSvc; + this._dfSvc = dfSvc; + return this._dfSvc; + }, + + // Atom Service + get _atomSvc() { + let atomSvc = Cc["@mozilla.org/atom-service;1"]. + getService(Ci.nsIAtomService); + delete this._atomSvc; + this._atomSvc = atomSvc; + return this._atomSvc; + }, + + // The ID of the source to display. The sidebar can set this to the source + // selected by the user. + // FIXME: make this an array of sources, and let the user select multiple + // sources to view multiple sources simultaneously. + sourceID: null, + + get _filter() { + delete this._filter; + return this._filter = document.getElementById("snowlFilter"); + }, + + get _tree() { + delete this._tree; + return this._tree = document.getElementById("snowlView"); + }, + + get _currentButton() { + delete this._currentButton; + return this._currentButton = document.getElementById("snowlCurrentButton"); + }, + + get _unreadButton() { + delete this._unreadButton; + return this._unreadButton = document.getElementById("snowlUnreadButton"); + }, + + // Maps XUL tree column IDs to collection properties. + _columnProperties: { + "snowlAuthorCol": "author", + "snowlSubjectCol": "subject", + "snowlTimestampCol": "timestamp" + }, + + + //**************************************************************************// + // nsITreeView + + get rowCount() { +this._log.info("get rowCount: " + this._collection.messages.length); + return this._collection.messages.length; + }, + + getCellText: function(aRow, aColumn) { + // FIXME: use _columnProperties instead of hardcoding column + // IDs and property names here. + switch(aColumn.id) { + case "snowlAuthorCol": + return this._collection.messages[aRow].author; + case "snowlSubjectCol": + return this._collection.messages[aRow].subject; + case "snowlTimestampCol": + return this._formatTimestamp(new Date(this._collection.messages[aRow].timestamp)); + default: + return null; + } + }, + + _treebox: null, + setTree: function(treebox){ this._treebox = treebox; }, + cycleHeader: function(aColumn) {}, + + isContainer: function(aRow) { return false }, + isSeparator: function(aRow) { return false }, + isSorted: function() { return false }, + getLevel: function(aRow) { return 0 }, + getImageSrc: function(aRow, aColumn) { return null }, + getRowProperties: function (aRow, aProperties) {}, + + getCellProperties: function (aRow, aColumn, aProperties) { + // We have to set this on each cell rather than on the row as a whole + // 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._collection.messages[aRow].read) + aProperties.AppendElement(this._atomSvc.getAtom("unread")); + }, + + getColumnProperties: function(aColumnID, aColumn, aProperties) {}, + + // We could implement inline tagging with an editable "Tags" column + // by making this true, adding editable="true" to the tree tag, and + // then marking only the tags column as editable. + isEditable: function() { return false }, + + + //**************************************************************************// + // Initialization and Destruction + + init: function() { + this._log = Log4Moz.Service.getLogger("Snowl.View"); + this._obsSvc.addObserver(this, "messages:changed", true); + + let container = document.getElementById("snowlViewContainer"); + if (container.getAttribute("placement") == "side") + this.placeOnSide(); + + this._collection = new SnowlCollection(); + this._sort(); + this._tree.view = this; + }, + + destroy: function() { + this._obsSvc.removeObserver(this, "messages:changed"); + }, + + + //**************************************************************************// + // Misc XPCOM Interfaces + + // 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 "messages:changed": + this._onMessagesChanged(); + break; + } + }, + + + //**************************************************************************// + // Event & Notification Handling + + _onMessagesChanged: function() { + // 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(); + this._rebuildView(); + }, + + onFilter: function() { + this._applyFilters(); + }, + + onCommandCurrentButton: function(aEvent) { + this._applyFilters(); + }, + + onCommandUnreadButton: function(aEvent) { + // XXX Instead of rebuilding from scratch each time, when going from + // all to unread, simply hide the ones that are read (f.e. by setting a CSS + // class on read items and then using a CSS rule to hide them)? + this._applyFilters(); + }, + + _applyFilters: function() { + let filters = []; + + if (this._currentButton.checked) + filters.push({ expression: "current = 1", parameters: {} }); + + if (this._unreadButton.checked) + filters.push({ expression: "read = 0", parameters: {} }); + + // FIXME: use a left join here once the SQLite bug breaking left joins to + // virtual tables has been fixed (i.e. after we upgrade to SQLite 3.5.7+). + if (this._filter.value) + filters.push({ expression: "messages.id IN (SELECT messageID FROM parts WHERE content MATCH :filter)", + parameters: { filter: this._filter.value } }); + + this._collection.filters = filters; + this._collection.invalidate(); + this._rebuildView(); + }, + + setCollection: function(collection) { + this._collection = collection; + this._rebuildView(); + }, + + _rebuildView: function() { + // Clear the selection before we rebuild the view, since it won't apply + // to the new data. + this._tree.view.selection.clearSelection(); + + // Since the number of rows might have changed, we rebuild the view + // by reinitializing it instead of merely invalidating the box object + // (which wouldn't accommodate changes to the number of rows). + // XXX Is there a better way to do this? + this._tree.view = this; + + // Scroll back to the top of the tree. + this._tree.boxObject.scrollToRow(this._tree.boxObject.getFirstVisibleRow()); + }, + + // From toolkit/mozapps/update/content/history.js + // XXX Really? ^ + + /** + * Formats a timestamp for human consumption using the date formatting service + * for locale-specific formatting along with some additional smarts for more + * human-readable representations of recent timestamps. + * @param {Date} the timestamp to format + * @returns a human-readable string + */ + _formatTimestamp: function(aTimestamp) { + let formattedString; + + let now = new Date(); + + let yesterday = new Date(now - 24 * 60 * 60 * 1000); + yesterday = new Date(yesterday.getFullYear(), yesterday.getMonth(), yesterday.getDate()); + + let sixDaysAgo = new Date(now - 6 * 24 * 60 * 60 * 1000); + sixDaysAgo = new Date(sixDaysAgo.getFullYear(), sixDaysAgo.getMonth(), sixDaysAgo.getDate()); + + if (aTimestamp.toLocaleDateString() == now.toLocaleDateString()) + formattedString = this._dfSvc.FormatTime("", + this._dfSvc.timeFormatNoSeconds, + aTimestamp.getHours(), + aTimestamp.getMinutes(), + null); + else if (aTimestamp > yesterday) + formattedString = "Yesterday " + this._dfSvc.FormatTime("", + this._dfSvc.timeFormatNoSeconds, + aTimestamp.getHours(), + aTimestamp.getMinutes(), + null); + else if (aTimestamp > sixDaysAgo) + formattedString = this._dfSvc.FormatDateTime("", + this._dfSvc.dateFormatWeekday, + this._dfSvc.timeFormatNoSeconds, + aTimestamp.getFullYear(), + aTimestamp.getMonth() + 1, + aTimestamp.getDate(), + aTimestamp.getHours(), + aTimestamp.getMinutes(), + aTimestamp.getSeconds()); + else + formattedString = this._dfSvc.FormatDateTime("", + this._dfSvc.dateFormatShort, + this._dfSvc.timeFormatNoSeconds, + aTimestamp.getFullYear(), + aTimestamp.getMonth() + 1, + aTimestamp.getDate(), + aTimestamp.getHours(), + aTimestamp.getMinutes(), + aTimestamp.getSeconds()); + + return formattedString; + }, + + switchPlacement: function() { + let container = document.getElementById("snowlViewContainer"); + let appcontent = document.getElementById("appcontent"); + + if (container.parentNode == appcontent) + this.placeOnSide(); + else + this.placeOnTop(); + }, + + placeOnSide: function() { + let browser = document.getElementById("browser"); + let container = document.getElementById("snowlViewContainer"); + let appcontent = document.getElementById("appcontent"); + let splitter = document.getElementById("snowlViewSplitter"); + + browser.insertBefore(container, appcontent); + browser.insertBefore(splitter, appcontent); + splitter.setAttribute("orient", "horizontal"); + container.setAttribute("placement", "side"); + + // Because we've moved the tree, we have to reattach the view to it, + // or we will get the error: "this._tree.boxObject.invalidate is not + // a function" when we switch sources. + this._tree.view = this; + }, + + placeOnTop: function() { + let appcontent = document.getElementById("appcontent"); + let container = document.getElementById("snowlViewContainer"); + let content = document.getElementById("content"); + let splitter = document.getElementById("snowlViewSplitter"); + + appcontent.insertBefore(container, content); + appcontent.insertBefore(splitter, content); + splitter.setAttribute("orient", "vertical"); + container.setAttribute("placement", "top"); + + // Because we've moved the tree, we have to reattach the view to it, + // or we will get the error: "this._tree.boxObject.invalidate is not + // a function" when we switch sources. + this._tree.view = this; + }, + + onSelect: function(aEvent) { + if (this._tree.currentIndex == -1) + return; + + // When we support opening multiple links in the background, + // perhaps use this code: + // http://lxr.mozilla.org/mozilla/source/browser/base/content/browser.js#1482 + + let row = this._tree.currentIndex; + let message = this._collection.messages[row]; + + //window.loadURI(message.link, null, null, false); + let url = "chrome://snowl/content/message/message.xul?id=" + message.id; + window.loadURI(url, null, null, false); + + this._setRead(true); + }, + + onKeyPress: function(aEvent) { + if (aEvent.altKey || aEvent.metaKey || aEvent.ctrlKey) + return; + + // which is either the charCode or the keyCode, depending on which is set. + this._log.info("onKeyPress: which = " + aEvent.which); + + if (aEvent.charCode == "r".charCodeAt(0)) + this._toggleRead(false); + if (aEvent.charCode == "R".charCodeAt(0)) + this._toggleRead(true); + else if (aEvent.charCode == " ".charCodeAt(0)) + this._onSpacePress(aEvent); + }, + + // Based on SpaceHit in mailWindowOverlay.js + _onSpacePress: function(aEvent) { + if (aEvent.shiftKey) { + // if at the start of the message, go to the previous one + if (gBrowser.contentWindow.scrollY > 0) + gBrowser.contentWindow.scrollByPages(-1); + else + this._goToPreviousUnreadMessage(); + } + else { + // if at the end of the message, go to the next one + if (gBrowser.contentWindow.scrollY < gBrowser.contentWindow.scrollMaxY) + gBrowser.contentWindow.scrollByPages(1); + else + this._goToNextUnreadMessage(); + } + }, + + _goToPreviousUnreadMessage: function() { + let currentIndex = this._tree.currentIndex; + let i = currentIndex - 1; + + while (i != currentIndex) { + if (i < 0) { + i = this._collection.messages.length - 1; + continue; + } + + if (!this._collection.messages[i].read) { + this.selection.select(i); + this._tree.treeBoxObject.ensureRowIsVisible(i); + break; + } + + i--; + } + }, + + _goToNextUnreadMessage: function() { + let currentIndex = this._tree.currentIndex; + let i = currentIndex + 1; + + while (i != currentIndex) { + if (i > this._collection.messages.length - 1) { + i = 0; + continue; + } +this._log.info(i); + if (!this._collection.messages[i].read) { + this.selection.select(i); + this._tree.treeBoxObject.ensureRowIsVisible(i); + break; + } + + i++; + } + }, + + _toggleRead: function(aAll) { +this._log.info("_toggleRead: all? " + aAll); + if (this._tree.currentIndex == -1) + return; + + let row = this._tree.currentIndex; + let message = this._collection.messages[row]; + if (aAll) + this._setAllRead(!message.read); + else + this._setRead(!message.read); + }, + + _setRead: function(aRead) { + let row = this._tree.currentIndex; + let message = this._collection.messages[row]; + message.read = aRead; + this._tree.boxObject.invalidateRow(row); + }, + + _setAllRead: function(aRead) { + let ids = this._collection.messages.map(function(v) { return v.id }); + this._collection.messages.forEach(function(v) { v.read = aRead }); + this._tree.boxObject.invalidate(); + }, + + show: function() { + let container = document.getElementById("snowlViewContainer"); + let splitter = document.getElementById("snowlViewSplitter"); + container.hidden = false; + splitter.hidden = false; + }, + + hide: function() { + let container = document.getElementById("snowlViewContainer"); + let splitter = document.getElementById("snowlViewSplitter"); + container.hidden = true; + splitter.hidden = true; + }, + + onClickColumnHeader: function(aEvent) { + let column = aEvent.target; + let property = this._columnProperties[column.id]; + let sortResource = this._tree.getAttribute("sortResource"); + let sortDirection = this._tree.getAttribute("sortDirection"); + + // FIXME: don't sort if the user right- or middle-clicked the header. + + // Determine the sort order. If the user clicked on the header for + // the current sort column, we sort in the reverse of the current order. + // Otherwise we sort in ascending order. + let oldOrder = (sortDirection == "ascending" ? 1 : -1); + let newOrder = (column.id == sortResource ? -oldOrder : 1); + + // Persist the new sort resource and direction. + let direction = (newOrder == 1 ? "ascending" : "descending"); + this._tree.setAttribute("sortResource", column.id); + this._tree.setAttribute("sortDirection", direction); + + // Update the sort indicator to appear on the current column. + let columns = this._tree.getElementsByTagName("treecol"); + for (let i = 0; i < columns.length; i++) + columns[i].removeAttribute("sortDirection"); + column.setAttribute("sortDirection", direction); + + // Perform the sort. + this._sort(); + }, + + _sort: function() { + let resource = this._tree.getAttribute("sortResource"); + let property = this._columnProperties[resource]; + + let direction = this._tree.getAttribute("sortDirection"); + let order = (direction == "ascending" ? 1 : -1); + + // Perform the sort. + this._collection.sort(property, order); + } +}; + +window.addEventListener("load", function() { Snowl.init() }, false); +window.addEventListener("load", function() { SnowlMessageView.init() }, false);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/content/list.xul Mon Aug 04 16:58:54 2008 -0700 @@ -0,0 +1,104 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- ***** BEGIN LICENSE BLOCK ***** + - Version: MPL 1.1/GPL 2.0/LGPL 2.1 + - + - The contents of this file are subject to the Mozilla Public License Version + - 1.1 (the "License"); you may not use this file except in compliance with + - the License. You may obtain a copy of the License at + - http://www.mozilla.org/MPL/ + - + - Software distributed under the License is distributed on an "AS IS" basis, + - WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + - for the specific language governing rights and limitations under the + - License. + - + - The Original Code is Snowl. + - + - The Initial Developer of the Original Code is Mozilla. + - Portions created by the Initial Developer are Copyright (C) 2008 + - the Initial Developer. All Rights Reserved. + - + - Contributor(s): + - Myk Melez <myk@mozilla.org> + - + - Alternatively, the contents of this file may be used under the terms of + - either the GNU General Public License Version 2 or later (the "GPL"), or + - the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + - in which case the provisions of the GPL or the LGPL are applicable instead + - of those above. If you wish to allow use of your version of this file only + - under the terms of either the GPL or the LGPL, and not to allow others to + - use your version of this file under the terms of the MPL, indicate your + - decision by deleting the provisions above and replace them with the notice + - and other provisions required by the GPL or the LGPL. If you do not delete + - the provisions above, a recipient may use your version of this file under + - the terms of any one of the MPL, the GPL or the LGPL. + - + - ***** END LICENSE BLOCK ***** --> + +<!-- XXX Do we need references to the global and browser skins in an overlay? --> +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://snowl/content/list.css" type="text/css"?> + +<!DOCTYPE overlay SYSTEM "chrome://snowl/locale/list.dtd"> + +<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="snowlOverlay"> + + <script type="application/x-javascript" src="list.js"/> + + <vbox id="appcontent"> + <vbox id="snowlViewContainer" insertbefore="content" placement="top" + persist="hidden placement height width" hidden="true"> + <toolbar id="snowlViewToolbar" pack="end" align="center"> + <toolbarbutton id="snowlPlacementButton" class="snowlToolbarButton" + oncommand="SnowlMessageView.switchPlacement()" + tooltiptext="&placementButton.tooltip;"/> + + <toolbarspring/> + + <toolbarbutton id="snowlCurrentButton" class="snowlToolbarButton" + type="checkbox" + image="chrome://snowl/content/icons/newspaper.png" + oncommand="SnowlMessageView.onCommandCurrentButton(event)" + tooltiptext="¤tButton.tooltip;"/> + + <toolbarbutton id="snowlUnreadButton" class="snowlToolbarButton" + type="checkbox" + image="chrome://snowl/content/icons/new.png" + oncommand="SnowlMessageView.onCommandUnreadButton(event)" + tooltiptext="&unreadButton.tooltip;"/> + + <textbox id="snowlFilter" type="timed" timeout="200" + oncommand="SnowlMessageView.onFilter()"/> + </toolbar> + + <tree id="snowlView" flex="1" persist="sortResource sortDirection" + sortResource="snowlTimestampCol" sortDirection="ascending" + enableColumnDrag="true" disableKeyNavigation="true" + onselect="SnowlMessageView.onSelect(event)" + onkeypress="SnowlMessageView.onKeyPress(event)"> + <treecols> + <treecol id="snowlAuthorCol" label="&authorCol.label;" flex="1" + persist="width ordinal hidden sortDirection" + class="sortDirectionIndicator" + onclick="SnowlMessageView.onClickColumnHeader(event)"/> + <splitter class="tree-splitter"/> + <treecol id="snowlSubjectCol" label="&subjectCol.label;" flex="5" + persist="width ordinal hidden sortDirection" + class="sortDirectionIndicator" + onclick="SnowlMessageView.onClickColumnHeader(event)"/> + <splitter class="tree-splitter"/> + <treecol id="snowlTimestampCol" label="×tampCol.label;" flex="1" + persist="width ordinal hidden sortDirection" + class="sortDirectionIndicator" + onclick="SnowlMessageView.onClickColumnHeader(event)"/> + </treecols> + + <treechildren flex="1"/> + </tree> + </vbox> + <splitter id="snowlViewSplitter" insertbefore="content" persist="hidden"/> + </vbox> +</overlay>
--- a/content/snowl.css Mon Aug 04 16:08:58 2008 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,66 +0,0 @@ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Snowl. - * - * The Initial Developer of the Original Code is Mozilla. - * Portions created by the Initial Developer are Copyright (C) 2008 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Myk Melez <myk@mozilla.org> - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -treechildren::-moz-tree-cell-text(unread) { - font-weight: bold !important -} - -/* Don't style the buttons natively because they look ugly on Linux. - * FIXME: test on other platforms and style conditionally as appropriate, - * and figure out how to make them look pretty on Linux. */ -.snowlToolbarButton { - -moz-appearance: none; - min-width: 24px; - min-height: 24px; -} - -/* Hide the button labels, since they would otherwise take up space, - * even if empty, and throw off the centering of the button icons. */ -.snowlToolbarButton .toolbarbutton-text { - display: none; -} - -#snowlViewContainer[placement="top"] #snowlPlacementButton { - list-style-image: url("chrome://snowl/content/icons/application_tile_horizontal.png"); -} - -#snowlViewContainer[placement="side"] #snowlPlacementButton { - list-style-image: url("chrome://snowl/content/icons/application_split.png"); -} - -#snowlView { - height: 12em; -}
--- a/content/snowl.js Mon Aug 04 16:08:58 2008 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,577 +0,0 @@ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Snowl. - * - * The Initial Developer of the Original Code is Mozilla. - * Portions created by the Initial Developer are Copyright (C) 2008 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Myk Melez <myk@mozilla.org> - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -Cu.import("resource://snowl/modules/service.js"); -Cu.import("resource://snowl/modules/datastore.js"); -Cu.import("resource://snowl/modules/collection.js"); - -// FIXME: import these into an object to avoid name collisions. -Cu.import("resource://snowl/modules/log4moz.js"); -Cu.import("resource://snowl/modules/URI.js"); -Cu.import("resource://snowl/modules/Preferences.js"); - -let Snowl = { - get _prefs() { - delete this._prefs; - return this._prefs = new Preferences("extensions.snowl."); - }, - - get _version() { - let em = Cc["@mozilla.org/extensions/manager;1"]. - getService(Ci.nsIExtensionManager); - let addon = em.getItemForID("snowl@mozilla.org"); - delete this._version; - return this._version = addon.version; - }, - - init: function() { - let lastVersion = this._prefs.get("lastVersion"); - - if (!lastVersion) { - let url = "chrome://snowl/content/firstrun.html"; - setTimeout(function() { window.openUILinkIn(url, "tab") }, 500); - } - else if (lastVersion != this._version) { - let url = "chrome://snowl/content/update.html?old=" + lastVersion + - "&new=" + this._version; - setTimeout(function() { window.openUILinkIn(url, "tab"); }, 500); - } - - this._prefs.set("lastVersion", this._version); - } -}; - - -let SnowlMessageView = { - _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; - }, - - // Date Formatting Service - get _dfSvc() { - let dfSvc = Cc["@mozilla.org/intl/scriptabledateformat;1"]. - getService(Ci.nsIScriptableDateFormat); - delete this._dfSvc; - this._dfSvc = dfSvc; - return this._dfSvc; - }, - - // Atom Service - get _atomSvc() { - let atomSvc = Cc["@mozilla.org/atom-service;1"]. - getService(Ci.nsIAtomService); - delete this._atomSvc; - this._atomSvc = atomSvc; - return this._atomSvc; - }, - - // The ID of the source to display. The sidebar can set this to the source - // selected by the user. - // FIXME: make this an array of sources, and let the user select multiple - // sources to view multiple sources simultaneously. - sourceID: null, - - get _filter() { - delete this._filter; - return this._filter = document.getElementById("snowlFilter"); - }, - - get _tree() { - delete this._tree; - return this._tree = document.getElementById("snowlView"); - }, - - get _currentButton() { - delete this._currentButton; - return this._currentButton = document.getElementById("snowlCurrentButton"); - }, - - get _unreadButton() { - delete this._unreadButton; - return this._unreadButton = document.getElementById("snowlUnreadButton"); - }, - - // Maps XUL tree column IDs to collection properties. - _columnProperties: { - "snowlAuthorCol": "author", - "snowlSubjectCol": "subject", - "snowlTimestampCol": "timestamp" - }, - - - //**************************************************************************// - // nsITreeView - - get rowCount() { -this._log.info("get rowCount: " + this._collection.messages.length); - return this._collection.messages.length; - }, - - getCellText: function(aRow, aColumn) { - // FIXME: use _columnProperties instead of hardcoding column - // IDs and property names here. - switch(aColumn.id) { - case "snowlAuthorCol": - return this._collection.messages[aRow].author; - case "snowlSubjectCol": - return this._collection.messages[aRow].subject; - case "snowlTimestampCol": - return this._formatTimestamp(new Date(this._collection.messages[aRow].timestamp)); - default: - return null; - } - }, - - _treebox: null, - setTree: function(treebox){ this._treebox = treebox; }, - cycleHeader: function(aColumn) {}, - - isContainer: function(aRow) { return false }, - isSeparator: function(aRow) { return false }, - isSorted: function() { return false }, - getLevel: function(aRow) { return 0 }, - getImageSrc: function(aRow, aColumn) { return null }, - getRowProperties: function (aRow, aProperties) {}, - - getCellProperties: function (aRow, aColumn, aProperties) { - // We have to set this on each cell rather than on the row as a whole - // 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._collection.messages[aRow].read) - aProperties.AppendElement(this._atomSvc.getAtom("unread")); - }, - - getColumnProperties: function(aColumnID, aColumn, aProperties) {}, - - // We could implement inline tagging with an editable "Tags" column - // by making this true, adding editable="true" to the tree tag, and - // then marking only the tags column as editable. - isEditable: function() { return false }, - - - //**************************************************************************// - // Initialization and Destruction - - init: function() { - this._log = Log4Moz.Service.getLogger("Snowl.View"); - this._obsSvc.addObserver(this, "messages:changed", true); - - let container = document.getElementById("snowlViewContainer"); - if (container.getAttribute("placement") == "side") - this.placeOnSide(); - - this._collection = new SnowlCollection(); - this._sort(); - this._tree.view = this; - }, - - destroy: function() { - this._obsSvc.removeObserver(this, "messages:changed"); - }, - - - //**************************************************************************// - // Misc XPCOM Interfaces - - // 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 "messages:changed": - this._onMessagesChanged(); - break; - } - }, - - - //**************************************************************************// - // Event & Notification Handling - - _onMessagesChanged: function() { - // 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(); - this._rebuildView(); - }, - - onFilter: function() { - this._applyFilters(); - }, - - onCommandCurrentButton: function(aEvent) { - this._applyFilters(); - }, - - onCommandUnreadButton: function(aEvent) { - // XXX Instead of rebuilding from scratch each time, when going from - // all to unread, simply hide the ones that are read (f.e. by setting a CSS - // class on read items and then using a CSS rule to hide them)? - this._applyFilters(); - }, - - _applyFilters: function() { - let filters = []; - - if (this._currentButton.checked) - filters.push({ expression: "current = 1", parameters: {} }); - - if (this._unreadButton.checked) - filters.push({ expression: "read = 0", parameters: {} }); - - // FIXME: use a left join here once the SQLite bug breaking left joins to - // virtual tables has been fixed (i.e. after we upgrade to SQLite 3.5.7+). - if (this._filter.value) - filters.push({ expression: "messages.id IN (SELECT messageID FROM parts WHERE content MATCH :filter)", - parameters: { filter: this._filter.value } }); - - this._collection.filters = filters; - this._collection.invalidate(); - this._rebuildView(); - }, - - setCollection: function(collection) { - this._collection = collection; - this._rebuildView(); - }, - - _rebuildView: function() { - // Clear the selection before we rebuild the view, since it won't apply - // to the new data. - this._tree.view.selection.clearSelection(); - - // Since the number of rows might have changed, we rebuild the view - // by reinitializing it instead of merely invalidating the box object - // (which wouldn't accommodate changes to the number of rows). - // XXX Is there a better way to do this? - this._tree.view = this; - - // Scroll back to the top of the tree. - this._tree.boxObject.scrollToRow(this._tree.boxObject.getFirstVisibleRow()); - }, - - // From toolkit/mozapps/update/content/history.js - // XXX Really? ^ - - /** - * Formats a timestamp for human consumption using the date formatting service - * for locale-specific formatting along with some additional smarts for more - * human-readable representations of recent timestamps. - * @param {Date} the timestamp to format - * @returns a human-readable string - */ - _formatTimestamp: function(aTimestamp) { - let formattedString; - - let now = new Date(); - - let yesterday = new Date(now - 24 * 60 * 60 * 1000); - yesterday = new Date(yesterday.getFullYear(), yesterday.getMonth(), yesterday.getDate()); - - let sixDaysAgo = new Date(now - 6 * 24 * 60 * 60 * 1000); - sixDaysAgo = new Date(sixDaysAgo.getFullYear(), sixDaysAgo.getMonth(), sixDaysAgo.getDate()); - - if (aTimestamp.toLocaleDateString() == now.toLocaleDateString()) - formattedString = this._dfSvc.FormatTime("", - this._dfSvc.timeFormatNoSeconds, - aTimestamp.getHours(), - aTimestamp.getMinutes(), - null); - else if (aTimestamp > yesterday) - formattedString = "Yesterday " + this._dfSvc.FormatTime("", - this._dfSvc.timeFormatNoSeconds, - aTimestamp.getHours(), - aTimestamp.getMinutes(), - null); - else if (aTimestamp > sixDaysAgo) - formattedString = this._dfSvc.FormatDateTime("", - this._dfSvc.dateFormatWeekday, - this._dfSvc.timeFormatNoSeconds, - aTimestamp.getFullYear(), - aTimestamp.getMonth() + 1, - aTimestamp.getDate(), - aTimestamp.getHours(), - aTimestamp.getMinutes(), - aTimestamp.getSeconds()); - else - formattedString = this._dfSvc.FormatDateTime("", - this._dfSvc.dateFormatShort, - this._dfSvc.timeFormatNoSeconds, - aTimestamp.getFullYear(), - aTimestamp.getMonth() + 1, - aTimestamp.getDate(), - aTimestamp.getHours(), - aTimestamp.getMinutes(), - aTimestamp.getSeconds()); - - return formattedString; - }, - - switchPlacement: function() { - let container = document.getElementById("snowlViewContainer"); - let appcontent = document.getElementById("appcontent"); - - if (container.parentNode == appcontent) - this.placeOnSide(); - else - this.placeOnTop(); - }, - - placeOnSide: function() { - let browser = document.getElementById("browser"); - let container = document.getElementById("snowlViewContainer"); - let appcontent = document.getElementById("appcontent"); - let splitter = document.getElementById("snowlViewSplitter"); - - browser.insertBefore(container, appcontent); - browser.insertBefore(splitter, appcontent); - splitter.setAttribute("orient", "horizontal"); - container.setAttribute("placement", "side"); - - // Because we've moved the tree, we have to reattach the view to it, - // or we will get the error: "this._tree.boxObject.invalidate is not - // a function" when we switch sources. - this._tree.view = this; - }, - - placeOnTop: function() { - let appcontent = document.getElementById("appcontent"); - let container = document.getElementById("snowlViewContainer"); - let content = document.getElementById("content"); - let splitter = document.getElementById("snowlViewSplitter"); - - appcontent.insertBefore(container, content); - appcontent.insertBefore(splitter, content); - splitter.setAttribute("orient", "vertical"); - container.setAttribute("placement", "top"); - - // Because we've moved the tree, we have to reattach the view to it, - // or we will get the error: "this._tree.boxObject.invalidate is not - // a function" when we switch sources. - this._tree.view = this; - }, - - onSelect: function(aEvent) { - if (this._tree.currentIndex == -1) - return; - - // When we support opening multiple links in the background, - // perhaps use this code: - // http://lxr.mozilla.org/mozilla/source/browser/base/content/browser.js#1482 - - let row = this._tree.currentIndex; - let message = this._collection.messages[row]; - - //window.loadURI(message.link, null, null, false); - let url = "chrome://snowl/content/message/message.xul?id=" + message.id; - window.loadURI(url, null, null, false); - - this._setRead(true); - }, - - onKeyPress: function(aEvent) { - if (aEvent.altKey || aEvent.metaKey || aEvent.ctrlKey) - return; - - // which is either the charCode or the keyCode, depending on which is set. - this._log.info("onKeyPress: which = " + aEvent.which); - - if (aEvent.charCode == "r".charCodeAt(0)) - this._toggleRead(false); - if (aEvent.charCode == "R".charCodeAt(0)) - this._toggleRead(true); - else if (aEvent.charCode == " ".charCodeAt(0)) - this._onSpacePress(aEvent); - }, - - // Based on SpaceHit in mailWindowOverlay.js - _onSpacePress: function(aEvent) { - if (aEvent.shiftKey) { - // if at the start of the message, go to the previous one - if (gBrowser.contentWindow.scrollY > 0) - gBrowser.contentWindow.scrollByPages(-1); - else - this._goToPreviousUnreadMessage(); - } - else { - // if at the end of the message, go to the next one - if (gBrowser.contentWindow.scrollY < gBrowser.contentWindow.scrollMaxY) - gBrowser.contentWindow.scrollByPages(1); - else - this._goToNextUnreadMessage(); - } - }, - - _goToPreviousUnreadMessage: function() { - let currentIndex = this._tree.currentIndex; - let i = currentIndex - 1; - - while (i != currentIndex) { - if (i < 0) { - i = this._collection.messages.length - 1; - continue; - } - - if (!this._collection.messages[i].read) { - this.selection.select(i); - this._tree.treeBoxObject.ensureRowIsVisible(i); - break; - } - - i--; - } - }, - - _goToNextUnreadMessage: function() { - let currentIndex = this._tree.currentIndex; - let i = currentIndex + 1; - - while (i != currentIndex) { - if (i > this._collection.messages.length - 1) { - i = 0; - continue; - } -this._log.info(i); - if (!this._collection.messages[i].read) { - this.selection.select(i); - this._tree.treeBoxObject.ensureRowIsVisible(i); - break; - } - - i++; - } - }, - - _toggleRead: function(aAll) { -this._log.info("_toggleRead: all? " + aAll); - if (this._tree.currentIndex == -1) - return; - - let row = this._tree.currentIndex; - let message = this._collection.messages[row]; - if (aAll) - this._setAllRead(!message.read); - else - this._setRead(!message.read); - }, - - _setRead: function(aRead) { - let row = this._tree.currentIndex; - let message = this._collection.messages[row]; - message.read = aRead; - this._tree.boxObject.invalidateRow(row); - }, - - _setAllRead: function(aRead) { - let ids = this._collection.messages.map(function(v) { return v.id }); - this._collection.messages.forEach(function(v) { v.read = aRead }); - this._tree.boxObject.invalidate(); - }, - - show: function() { - let container = document.getElementById("snowlViewContainer"); - let splitter = document.getElementById("snowlViewSplitter"); - container.hidden = false; - splitter.hidden = false; - }, - - hide: function() { - let container = document.getElementById("snowlViewContainer"); - let splitter = document.getElementById("snowlViewSplitter"); - container.hidden = true; - splitter.hidden = true; - }, - - onClickColumnHeader: function(aEvent) { - let column = aEvent.target; - let property = this._columnProperties[column.id]; - let sortResource = this._tree.getAttribute("sortResource"); - let sortDirection = this._tree.getAttribute("sortDirection"); - - // FIXME: don't sort if the user right- or middle-clicked the header. - - // Determine the sort order. If the user clicked on the header for - // the current sort column, we sort in the reverse of the current order. - // Otherwise we sort in ascending order. - let oldOrder = (sortDirection == "ascending" ? 1 : -1); - let newOrder = (column.id == sortResource ? -oldOrder : 1); - - // Persist the new sort resource and direction. - let direction = (newOrder == 1 ? "ascending" : "descending"); - this._tree.setAttribute("sortResource", column.id); - this._tree.setAttribute("sortDirection", direction); - - // Update the sort indicator to appear on the current column. - let columns = this._tree.getElementsByTagName("treecol"); - for (let i = 0; i < columns.length; i++) - columns[i].removeAttribute("sortDirection"); - column.setAttribute("sortDirection", direction); - - // Perform the sort. - this._sort(); - }, - - _sort: function() { - let resource = this._tree.getAttribute("sortResource"); - let property = this._columnProperties[resource]; - - let direction = this._tree.getAttribute("sortDirection"); - let order = (direction == "ascending" ? 1 : -1); - - // Perform the sort. - this._collection.sort(property, order); - } -}; - -window.addEventListener("load", function() { Snowl.init() }, false); -window.addEventListener("load", function() { SnowlMessageView.init() }, false);
--- a/content/snowl.xul Mon Aug 04 16:08:58 2008 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,124 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> - -<!-- ***** BEGIN LICENSE BLOCK ***** - - Version: MPL 1.1/GPL 2.0/LGPL 2.1 - - - - The contents of this file are subject to the Mozilla Public License Version - - 1.1 (the "License"); you may not use this file except in compliance with - - the License. You may obtain a copy of the License at - - http://www.mozilla.org/MPL/ - - - - Software distributed under the License is distributed on an "AS IS" basis, - - WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - - for the specific language governing rights and limitations under the - - License. - - - - The Original Code is Snowl. - - - - The Initial Developer of the Original Code is Mozilla. - - Portions created by the Initial Developer are Copyright (C) 2008 - - the Initial Developer. All Rights Reserved. - - - - Contributor(s): - - Myk Melez <myk@mozilla.org> - - - - Alternatively, the contents of this file may be used under the terms of - - either the GNU General Public License Version 2 or later (the "GPL"), or - - the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - - in which case the provisions of the GPL or the LGPL are applicable instead - - of those above. If you wish to allow use of your version of this file only - - under the terms of either the GPL or the LGPL, and not to allow others to - - use your version of this file under the terms of the MPL, indicate your - - decision by deleting the provisions above and replace them with the notice - - and other provisions required by the GPL or the LGPL. If you do not delete - - the provisions above, a recipient may use your version of this file under - - the terms of any one of the MPL, the GPL or the LGPL. - - - - ***** END LICENSE BLOCK ***** --> - -<!-- XXX Do we need references to the global and browser skins in an overlay? --> -<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> -<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?> -<?xml-stylesheet href="chrome://snowl/content/snowl.css" type="text/css"?> - -<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" - id="snowlOverlay"> - - <script type="application/x-javascript" src="snowl.js"/> - - <menupopup id="menu_viewPopup"> - <!-- Since these both get inserted after viewSidebarMenuMenu, they appear - - in the reverse of the order below. --> - <menuitem insertafter="viewSidebarMenuMenu" - label="Message River" accesskey="v" - oncommand="gBrowser.selectedTab = gBrowser.addTab('chrome://snowl/content/river.xul')"/> - - <menuitem observes="viewSnowlSidebar" insertafter="viewSidebarMenuMenu" - accesskey="m"/> - </menupopup> - - <broadcasterset id="mainBroadcasterSet"> - <broadcaster id="viewSnowlSidebar" - label="Message List" - autoCheck="false" - type="checkbox" - group="sidebar" - sidebarurl="chrome://snowl/content/sidebar.xul" - sidebartitle="Snowl" - oncommand="toggleSidebar('viewSnowlSidebar')"/> - </broadcasterset> - - <vbox id="appcontent"> - <vbox id="snowlViewContainer" insertbefore="content" placement="top" - persist="hidden placement height width" hidden="true"> - <toolbar id="snowlViewToolbar" pack="end" align="center"> - <toolbarbutton id="snowlPlacementButton" class="snowlToolbarButton" - oncommand="SnowlMessageView.switchPlacement()" - tooltiptext="Switch placement."/> - - <toolbarspring/> - - <toolbarbutton id="snowlCurrentButton" class="snowlToolbarButton" - type="checkbox" - image="chrome://snowl/content/icons/newspaper.png" - oncommand="SnowlMessageView.onCommandCurrentButton(event)" - tooltiptext="Only show current messages."/> - - <toolbarbutton id="snowlUnreadButton" class="snowlToolbarButton" - type="checkbox" - image="chrome://snowl/content/icons/new.png" - oncommand="SnowlMessageView.onCommandUnreadButton(event)" - tooltiptext="Only show unread messages."/> - - <textbox id="snowlFilter" type="timed" timeout="200" - oncommand="SnowlMessageView.onFilter()"/> - </toolbar> - - <tree id="snowlView" flex="1" persist="sortResource sortDirection" - sortResource="snowlTimestampCol" sortDirection="ascending" - enableColumnDrag="true" disableKeyNavigation="true" - onselect="SnowlMessageView.onSelect(event)" - onkeypress="SnowlMessageView.onKeyPress(event)"> - <treecols> - <treecol id="snowlAuthorCol" label="Author" flex="1" - persist="width ordinal hidden sortDirection" - class="sortDirectionIndicator" - onclick="SnowlMessageView.onClickColumnHeader(event)"/> - <splitter class="tree-splitter"/> - <treecol id="snowlSubjectCol" label="Subject" flex="5" - persist="width ordinal hidden sortDirection" - class="sortDirectionIndicator" - onclick="SnowlMessageView.onClickColumnHeader(event)"/> - <splitter class="tree-splitter"/> - <treecol id="snowlTimestampCol" label="Date" flex="1" - persist="width ordinal hidden sortDirection" - class="sortDirectionIndicator" - onclick="SnowlMessageView.onClickColumnHeader(event)"/> - </treecols> - - <treechildren flex="1"/> - </tree> - </vbox> - <splitter id="snowlViewSplitter" insertbefore="content" persist="hidden"/> - </vbox> -</overlay>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/locale/en-US/browser.dtd Mon Aug 04 16:58:54 2008 -0700 @@ -0,0 +1,6 @@ +<!ENTITY riverView.label "River of Messages"> +<!ENTITY riverView.accesskey "v"> +<!ENTITY listView.label "Message List"> +<!ENTITY listView.accesskey "m"> + +<!ENTITY sidebar.label "Snowl">
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/locale/en-US/collections.dtd Mon Aug 04 16:58:54 2008 -0700 @@ -0,0 +1,6 @@ +<!ENTITY nameCol.label "Name"> + +<!ENTITY subscribeButton.tooltip "Subscribe to message sources."> +<!ENTITY unsubscribeButton.tooltip "Unsubscribe from message sources."> +<!ENTITY refreshButton.tooltip "Refresh sources."> +<!ENTITY exportButton.tooltip "Export sources as OPML.">
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/locale/en-US/list.dtd Mon Aug 04 16:58:54 2008 -0700 @@ -0,0 +1,7 @@ +<!ENTITY placementButton.tooltip "Switch placement."> +<!ENTITY currentButton.tooltip "Only show current messages."> +<!ENTITY unreadButton.tooltip "Only show unread messages."> + +<!ENTITY authorCol.label "Author"> +<!ENTITY subjectCol.label "Subject"> +<!ENTITY timestampCol.label "Date">