From 4d88ccb0368b3697cca433de508cd23ecf15e481 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Mon, 9 Nov 2009 02:39:23 -0500 Subject: [PATCH] Port Xulmus/Muttator. Not tested yet. --- muttator/content/addressbook.js | 272 +++-- muttator/content/config.js | 375 ++++--- muttator/content/mail.js | 1654 +++++++++++++++---------------- xulmus/content/config.js | 16 - xulmus/content/library.js | 103 +- xulmus/content/player.js | 1202 +++++++++++----------- 6 files changed, 1749 insertions(+), 1873 deletions(-) diff --git a/muttator/content/addressbook.js b/muttator/content/addressbook.js index 96c48183..de3f66e8 100644 --- a/muttator/content/addressbook.js +++ b/muttator/content/addressbook.js @@ -3,17 +3,13 @@ // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. -function Addressbook() { //{{{ - //////////////////////////////////////////////////////////////////////////////// - ////////////////////// PRIVATE SECTION ///////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - const abManager = Cc["@mozilla.org/abmanager;1"].getService(Ci.nsIAbManager); - const rdf = Cc["@mozilla.org/rdf/rdf-service;1"].getService(Ci.nsIRDFService); +const Addressbook = Module("addressbook", { + init: function () { + }, // TODO: add option for a format specifier, like: // :set displayname=%l, %f - function generateDisplayName(firstName, lastName) { + generateDisplayName: function (firstName, lastName) { if (firstName && lastName) return lastName + ", " + firstName; else if (firstName) @@ -22,150 +18,134 @@ function Addressbook() { //{{{ return lastName; else return ""; - } + }, - function getDirectoryFromURI(uri) services.get("rdf").GetResource(uri).QueryInterface(Ci.nsIAbDirectory) + getDirectoryFromURI: function (uri) services.get("rdf").GetResource(uri).QueryInterface(Ci.nsIAbDirectory), - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// OPTIONS ///////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ + add: function (address, firstName, lastName, displayName) { + const personalAddressbookURI = "moz-abmdbdirectory://abook.mab"; + let directory = this.getDirectoryFromURI(personalAddressbookURI); + let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(Ci.nsIAbCard); - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// MAPPINGS //////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ + if (!address || !directory || !card) + return false; - var myModes = config.mailModes; + card.primaryEmail = address; + card.firstName = firstName; + card.lastName = lastName; + card.displayName = displayName; - mappings.add(myModes, ["a"], - "Open a prompt to save a new addressbook entry for the sender of the selected message", - function () { - try { - var to = gDBView.hdrForFirstSelectedMessage.mime2DecodedAuthor; + return directory.addCard(card); + }, + + // TODO: add telephone number support + list: function (filter, newMail) { + let addresses = []; + let dirs = abManager.directories; + let lowerFilter = filter.toLowerCase(); + + while (dirs.hasMoreElements()) { + let addrbook = dirs.getNext().QueryInterface(Ci.nsIAbDirectory); + let cards = addrbook.childCards; + while (cards.hasMoreElements()) { + let card = cards.getNext().QueryInterface(Ci.nsIAbCard); + //var mail = card.primaryEmail || ""; //XXX + let displayName = card.displayName; + if (!displayName) + displayName = this.generateDisplayName(card.firstName, card.lastName); + + if (displayName.toLowerCase().indexOf(lowerFilter) > -1 + || card.primaryEmail.toLowerCase().indexOf(lowerFilter) > -1) + addresses.push([displayName, card.primaryEmail]); } - catch (e) { - liberator.beep(); - } - - if (!to) - return; - - let address = to.substring(to.indexOf("<") + 1, to.indexOf(">")); - - let displayName = to.substr(0, to.indexOf("<") - 1); - if (/^\S+\s+\S+\s*$/.test(displayName)) { - let names = displayName.split(/\s+/); - displayName = "-firstname=" + names[0].replace(/"/g, "") - + " -lastname=" + names[1].replace(/"/g, ""); - } - else - displayName = "-name=\"" + displayName.replace(/"/g, "") + "\""; - - commandline.open(":", "contact " + address + " " + displayName, modes.EX); - }); - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// COMMANDS //////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - commands.add(["con[tact]"], - "Add an address book entry", - function (args) { - let mailAddr = args[0]; // TODO: support more than one email address - let firstName = args["-firstname"] || null; - let lastName = args["-lastname"] || null; - let displayName = args["-name"] || null; - if (!displayName) - displayName = generateDisplayName(firstName, lastName); - - if (addressbook.add(mailAddr, firstName, lastName, displayName)) - liberator.echomsg("Added address: " + displayName + " <" + mailAddr + ">", 1, commandline.FORCE_SINGLELINE); - else - liberator.echoerr("Exxx: Could not add contact `" + mailAddr + "'", commandline.FORCE_SINGLELINE); - - }, - { - argCount: "+", - options: [[["-firstname", "-f"], commands.OPTION_STRING], - [["-lastname", "-l"], commands.OPTION_STRING], - [["-name", "-n"], commands.OPTION_STRING]] - }); - - commands.add(["contacts", "addr[essbook]"], - "List or open multiple addresses", - function (args) { addressbook.list(args.string, args.bang); }, - { bang: true }); - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// PUBLIC SECTION ////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - return { - - add: function (address, firstName, lastName, displayName) { - const personalAddressbookURI = "moz-abmdbdirectory://abook.mab"; - let directory = getDirectoryFromURI(personalAddressbookURI); - let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(Ci.nsIAbCard); - - if (!address || !directory || !card) - return false; - - card.primaryEmail = address; - card.firstName = firstName; - card.lastName = lastName; - card.displayName = displayName; - - return directory.addCard(card); - }, - - // TODO: add telephone number support - list: function (filter, newMail) { - let addresses = []; - let dirs = abManager.directories; - let lowerFilter = filter.toLowerCase(); - - while (dirs.hasMoreElements()) { - let addrbook = dirs.getNext().QueryInterface(Ci.nsIAbDirectory); - let cards = addrbook.childCards; - while (cards.hasMoreElements()) { - let card = cards.getNext().QueryInterface(Ci.nsIAbCard); - //var mail = card.primaryEmail || ""; //XXX - let displayName = card.displayName; - if (!displayName) - displayName = generateDisplayName(card.firstName, card.lastName); - - if (displayName.toLowerCase().indexOf(lowerFilter) > -1 - || card.primaryEmail.toLowerCase().indexOf(lowerFilter) > -1) - addresses.push([displayName, card.primaryEmail]); - } - } - - if (addresses.length < 1) { - if (!filter) - liberator.echoerr("Exxx: No contacts", commandline.FORCE_SINGLELINE); - else - liberator.echoerr("Exxx: No contacts matching string '" + filter + "'", commandline.FORCE_SINGLELINE); - return false; - } - - if (newMail) { - // Now we have to create a new message - let args = {}; - args.to = addresses.map( - function (address) "\"" + address[0].replace(/"/g, "") + " <" + address[1] + ">\"" - ).join(", "); - - mail.composeNewMail(args); - } - else { - let list = template.tabular(["Name", "Address"], [], - [[util.clip(address[0], 50), address[1]] for ([, address] in Iterator(addresses))] - ); - commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); - } - return true; } - }; - //}}} -} //}}} + + if (addresses.length < 1) { + if (!filter) + liberator.echoerr("Exxx: No contacts", commandline.FORCE_SINGLELINE); + else + liberator.echoerr("Exxx: No contacts matching string '" + filter + "'", commandline.FORCE_SINGLELINE); + return false; + } + + if (newMail) { + // Now we have to create a new message + let args = {}; + args.to = addresses.map( + function (address) "\"" + address[0].replace(/"/g, "") + " <" + address[1] + ">\"" + ).join(", "); + + mail.composeNewMail(args); + } + else { + let list = template.tabular(["Name", "Address"], [], + [[util.clip(address[0], 50), address[1]] for ([, address] in Iterator(addresses))] + ); + commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); + } + return true; + } +}, { +}, { + commands: function () { + commands.add(["con[tact]"], + "Add an address book entry", + function (args) { + let mailAddr = args[0]; // TODO: support more than one email address + let firstName = args["-firstname"] || null; + let lastName = args["-lastname"] || null; + let displayName = args["-name"] || null; + if (!displayName) + displayName = this.generateDisplayName(firstName, lastName); + + if (addressbook.add(mailAddr, firstName, lastName, displayName)) + liberator.echomsg("Added address: " + displayName + " <" + mailAddr + ">", 1, commandline.FORCE_SINGLELINE); + else + liberator.echoerr("Exxx: Could not add contact `" + mailAddr + "'", commandline.FORCE_SINGLELINE); + + }, + { + argCount: "+", + options: [[["-firstname", "-f"], commands.OPTION_STRING], + [["-lastname", "-l"], commands.OPTION_STRING], + [["-name", "-n"], commands.OPTION_STRING]] + }); + + commands.add(["contacts", "addr[essbook]"], + "List or open multiple addresses", + function (args) { addressbook.list(args.string, args.bang); }, + { bang: true }); + }, + mappings: function () { + var myModes = config.mailModes; + + mappings.add(myModes, ["a"], + "Open a prompt to save a new addressbook entry for the sender of the selected message", + function () { + try { + var to = gDBView.hdrForFirstSelectedMessage.mime2DecodedAuthor; + } + catch (e) { + liberator.beep(); + } + + if (!to) + return; + + let address = to.substring(to.indexOf("<") + 1, to.indexOf(">")); + + let displayName = to.substr(0, to.indexOf("<") - 1); + if (/^\S+\s+\S+\s*$/.test(displayName)) { + let names = displayName.split(/\s+/); + displayName = "-firstname=" + names[0].replace(/"/g, "") + + " -lastname=" + names[1].replace(/"/g, ""); + } + else + displayName = "-name=\"" + displayName.replace(/"/g, "") + "\""; + + commandline.open(":", "contact " + address + " " + displayName, modes.EX); + }); + }, +}); // vim: set fdm=marker sw=4 ts=4 et: diff --git a/muttator/content/config.js b/muttator/content/config.js index 61744c8d..ff31088f 100644 --- a/muttator/content/config.js +++ b/muttator/content/config.js @@ -3,214 +3,185 @@ // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. -const config = (function () { //{{{ - //////////////////////////////////////////////////////////////////////////////// - ////////////////////// PRIVATE SECTION ///////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ +const config = { //{{{ + /*** required options, no checks done if they really exist, so be careful ***/ + name: "Muttator", + hostApplication: "Thunderbird", // TODO: can this be found out otherwise? gBrandBundle.getString("brandShortName"); + // Yes, but it will be localized unlike all other strings. So, it's best left until we i18n liberator. --djk - var name = "Muttator"; - var host = "Thunderbird"; - var tabmail; + get mainWindowId() this.isComposeWindow ? "msgcomposeWindow" : "messengerWindow", - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// PUBLIC SECTION ////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ + /*** optional options, there are checked for existence and a fallback provided ***/ + features: ["hints", "mail", "marks", "addressbook", "tabs"], + defaults: { + guioptions: "frb", + showtabline: 1, + titlestring: "Muttator" + }, - return { - /*** required options, no checks done if they really exist, so be careful ***/ - name: name, - hostApplication: host, // TODO: can this be found out otherwise? gBrandBundle.getString("brandShortName"); - // Yes, but it will be localized unlike all other strings. So, it's best left until we i18n liberator. --djk - get mainWindowId() this.isComposeWindow ? "msgcomposeWindow" : "messengerWindow", + guioptions: { + m: ["MenuBar", ["mail-toolbar-menubar2"]], + T: ["Toolbar" , ["mail-bar2"]], + f: ["Folder list", ["folderPaneBox", "folderpane_splitter"]], + F: ["Folder list header", ["folderPaneHeader"]] + }, - /*** optional options, there are checked for existence and a fallback provided ***/ - features: ["hints", "mail", "marks", "addressbook", "tabs"], - defaults: { - guioptions: "frb", - showtabline: 1, - titlestring: name - }, + get isComposeWindow() window.wintype == "msgcompose", + get browserModes() [modes.MESSAGE], + get mailModes() [modes.NORMAL], + // focusContent() focuses this widget + get mainWidget() this.isComposeWindow ? document.getElementById("content-frame") : GetThreadTree(), + get visualbellWindow() document.getElementById(this.mainWindowId), + styleableChrome: ["chrome://messenger/content/messenger.xul", + "chrome://messenger/content/messengercompose/messengercompose.xul"], - guioptions: { - m: ["MenuBar", ["mail-toolbar-menubar2"]], - T: ["Toolbar" , ["mail-bar2"]], - f: ["Folder list", ["folderPaneBox", "folderpane_splitter"]], - F: ["Folder list header", ["folderPaneHeader"]] - }, + autocommands: [["DOMLoad", "Triggered when a page's DOM content has fully loaded"], + ["FolderLoad", "Triggered after switching folders in Thunderbird"], + ["PageLoadPre", "Triggered after a page load is initiated"], + ["PageLoad", "Triggered when a page gets (re)loaded/opened"], + ["MuttatorEnter", "Triggered after Thunderbird starts"], + ["MuttatorLeave", "Triggered before exiting Thunderbird"], + ["MuttatorLeavePre", "Triggered before exiting Thunderbird"]], + dialogs: [ + ["about", "About Thunderbird", + function () { window.openAboutDialog(); }], + ["addons", "Manage Add-ons", + function () { window.openAddonsMgr(); }], + ["addressbook", "Address book", + function () { window.toAddressBook(); }], + ["checkupdates", "Check for updates", + function () { window.checkForUpdates(); }], + /*["cleardata", "Clear private data", + function () { Cc[GLUE_CID].getService(Ci.nsIBrowserGlue).sanitize(window || null); }],*/ + ["console", "JavaScript console", + function () { window.toJavaScriptConsole(); }], + /*["customizetoolbar", "Customize the Toolbar", + function () { BrowserCustomizeToolbar(); }],*/ + ["dominspector", "DOM Inspector", + function () { window.inspectDOMDocument(content.document); }], + ["downloads", "Manage Downloads", + function () { window.toOpenWindowByType('Download:Manager', 'chrome://mozapps/content/downloads/downloads.xul', 'chrome,dialog=no,resizable'); }], + /*["import", "Import Preferences, Bookmarks, History, etc. from other browsers", + function () { BrowserImport(); }], + ["openfile", "Open the file selector dialog", + function () { BrowserOpenFileWindow(); }], + ["pageinfo", "Show information about the current page", + function () { BrowserPageInfo(); }], + ["pagesource", "View page source", + function () { BrowserViewSourceOfDocument(content.document); }],*/ + ["preferences", "Show Thunderbird preferences dialog", + function () { openOptionsDialog(); }], + /*["printpreview", "Preview the page before printing", + function () { PrintUtils.printPreview(onEnterPrintPreview, onExitPrintPreview); }],*/ + ["printsetup", "Setup the page size and orientation before printing", + function () { PrintUtils.showPageSetup(); }], + ["print", "Show print dialog", + function () { PrintUtils.print(); }], + ["saveframe", "Save frame to disk", + function () { window.saveFrameDocument(); }], + ["savepage", "Save page to disk", + function () { window.saveDocument(window.content.document); }], + /*["searchengines", "Manage installed search engines", + function () { openDialog("chrome://browser/content/search/engineManager.xul", "_blank", "chrome,dialog,modal,centerscreen"); }], + ["selectionsource", "View selection source", + function () { buffer.viewSelectionSource(); }]*/ + ], - get isComposeWindow() window.wintype == "msgcompose", - get browserModes() [modes.MESSAGE], - get mailModes() [modes.NORMAL], - // focusContent() focuses this widget - get mainWidget() this.isComposeWindow ? document.getElementById("content-frame") : GetThreadTree(), - get visualbellWindow() document.getElementById(this.mainWindowId), - styleableChrome: ["chrome://messenger/content/messenger.xul", - "chrome://messenger/content/messengercompose/messengercompose.xul"], - - autocommands: [["DOMLoad", "Triggered when a page's DOM content has fully loaded"], - ["FolderLoad", "Triggered after switching folders in " + host], - ["PageLoadPre", "Triggered after a page load is initiated"], - ["PageLoad", "Triggered when a page gets (re)loaded/opened"], - [name + "Enter", "Triggered after " + host + " starts"], - [name + "Leave", "Triggered before exiting " + host], - [name + "LeavePre", "Triggered before exiting " + host]], - - dialogs: [ - ["about", "About " + host, - function () { window.openAboutDialog(); }], - ["addons", "Manage Add-ons", - function () { window.openAddonsMgr(); }], - ["addressbook", "Address book", - function () { window.toAddressBook(); }], - ["checkupdates", "Check for updates", - function () { window.checkForUpdates(); }], - /*["cleardata", "Clear private data", - function () { Cc[GLUE_CID].getService(Ci.nsIBrowserGlue).sanitize(window || null); }],*/ - ["console", "JavaScript console", - function () { window.toJavaScriptConsole(); }], - /*["customizetoolbar", "Customize the Toolbar", - function () { BrowserCustomizeToolbar(); }],*/ - ["dominspector", "DOM Inspector", - function () { window.inspectDOMDocument(content.document); }], - ["downloads", "Manage Downloads", - function () { window.toOpenWindowByType('Download:Manager', 'chrome://mozapps/content/downloads/downloads.xul', 'chrome,dialog=no,resizable'); }], - /*["import", "Import Preferences, Bookmarks, History, etc. from other browsers", - function () { BrowserImport(); }], - ["openfile", "Open the file selector dialog", - function () { BrowserOpenFileWindow(); }], - ["pageinfo", "Show information about the current page", - function () { BrowserPageInfo(); }], - ["pagesource", "View page source", - function () { BrowserViewSourceOfDocument(content.document); }],*/ - ["preferences", "Show " + host + " preferences dialog", - function () { openOptionsDialog(); }], - /*["printpreview", "Preview the page before printing", - function () { PrintUtils.printPreview(onEnterPrintPreview, onExitPrintPreview); }],*/ - ["printsetup", "Setup the page size and orientation before printing", - function () { PrintUtils.showPageSetup(); }], - ["print", "Show print dialog", - function () { PrintUtils.print(); }], - ["saveframe", "Save frame to disk", - function () { window.saveFrameDocument(); }], - ["savepage", "Save page to disk", - function () { window.saveDocument(window.content.document); }], - /*["searchengines", "Manage installed search engines", - function () { openDialog("chrome://browser/content/search/engineManager.xul", "_blank", "chrome,dialog,modal,centerscreen"); }], - ["selectionsource", "View selection source", - function () { buffer.viewSelectionSource(); }]*/ - ], - - focusChange: function (win) { - // we switch to -- MESSAGE -- mode for Muttator, when the main HTML widget gets focus - if (win && win.document instanceof HTMLDocument || liberator.focus instanceof HTMLAnchorElement) { - if (config.isComposeWindow) - modes.set(modes.INSERT, modes.TEXTAREA); - else if (liberator.mode != modes.MESSAGE) - liberator.mode = modes.MESSAGE; - } - }, - - getBrowser: function () { - if (!tabmail) { - tabmail = { __proto__: document.getElementById("tabmail") }; - tabmail.__defineGetter__("mTabContainer", function () this.tabContainer); - tabmail.__defineGetter__("mTabs", function () this.tabContainer.childNodes); - tabmail.__defineGetter__("mCurrentTab", function () this.tabContainer.selectedItem); - tabmail.__defineGetter__("mStrip", function () this.tabStrip); - tabmail.__defineGetter__("browsers", function () [browser for (browser in Iterator(this.mTabs))]); - } - return tabmail; - }, - - // they are sorted by relevance, not alphabetically - helpFiles: ["intro.html", "version.html"], - - get ignoreKeys() { - delete this.ignoreKeys; - return this.ignoreKeys = { - "": modes.NORMAL | modes.INSERT, - "": modes.NORMAL | modes.INSERT, - "": modes.NORMAL | modes.INSERT, - "": modes.NORMAL | modes.INSERT - } - }, - - modes: [ - ["MESSAGE", { char: "m" }], - ["COMPOSE"] - ], - - // NOTE: as I don't use TB I have no idea how robust this is. --djk - get outputHeight() { - if (!this.isComposeWindow) { - let container = document.getElementById("tabpanelcontainer").boxObject; - let deck = document.getElementById("displayDeck"); - let box = document.getElementById("messagepanebox"); - let splitter = document.getElementById("threadpane-splitter").boxObject; - - if (splitter.width > splitter.height) - return container.height - deck.minHeight - box.minHeight- splitter.height; - else - return container.height - Math.max(deck.minHeight, box.minHeight); - } - else - return document.getElementById("appcontent").boxObject.height; - }, - - scripts: [ - "addressbook.js", - "compose/compose.js", - "mail.js", - "tabs.js" - ], - - // to allow Vim to :set ft=mail automatically - tempFile: "mutt-ator-mail", - - init: function () { - // don't wait too long when selecting new messages - // GetThreadTree()._selectDelay = 300; // TODO: make configurable - - // load Muttator specific modules - if (this.isComposeWindow) - // TODO: this should probably be "composer" - liberator.loadModule("compose", Compose); - else { - liberator.loadModule("mail", Mail); - liberator.loadModule("addressbook", Addressbook); - liberator.loadModule("tabs", Tabs); - liberator.loadModule("marks", Marks); - liberator.loadModule("hints", Hints); - } - - commands.add(["pref[erences]", "prefs"], - "Show " + config.hostApplication + " preferences", - function () { window.openOptionsDialog(); }, - { argCount: "0" }); - - // FIXME: comment obviously incorrect - // 0: never automatically edit externally - // 1: automatically edit externally when message window is shown the first time - // 2: automatically edit externally, once the message text gets focus (not working currently) - options.add(["autoexternal", "ae"], - "Edit message with external editor by default", - "boolean", false); - - options.add(["online"], - "Set the 'work offline' option", - "boolean", true, - { - setter: function (value) { - if (MailOfflineMgr.isOnline() != value) - MailOfflineMgr.toggleOfflineStatus(); - return value; - }, - getter: function () MailOfflineMgr.isOnline() - }); - - //}}} + focusChange: function (win) { + // we switch to -- MESSAGE -- mode for Muttator, when the main HTML widget gets focus + if (win && win.document instanceof HTMLDocument || liberator.focus instanceof HTMLAnchorElement) { + if (config.isComposeWindow) + modes.set(modes.INSERT, modes.TEXTAREA); + else if (liberator.mode != modes.MESSAGE) + liberator.mode = modes.MESSAGE; } - }; //}}} -})(); //}}} + }, + + getBrowser: function () { + var tabmail = { __proto__: document.getElementById("tabmail") }; + tabmail.__defineGetter__("mTabContainer", function () this.tabContainer); + tabmail.__defineGetter__("mTabs", function () this.tabContainer.childNodes); + tabmail.__defineGetter__("mCurrentTab", function () this.tabContainer.selectedItem); + tabmail.__defineGetter__("mStrip", function () this.tabStrip); + tabmail.__defineGetter__("browsers", function () [browser for (browser in Iterator(this.mTabs))]); + config.getBrowser = function () tabmail; + return tabmail; + }, + + // they are sorted by relevance, not alphabetically + helpFiles: ["intro.html", "version.html"], + + get ignoreKeys() { + delete this.ignoreKeys; + return this.ignoreKeys = { + "": modes.NORMAL | modes.INSERT, + "": modes.NORMAL | modes.INSERT, + "": modes.NORMAL | modes.INSERT, + "": modes.NORMAL | modes.INSERT + } + }, + + modes: [ + ["MESSAGE", { char: "m" }], + ["COMPOSE"] + ], + + // NOTE: as I don't use TB I have no idea how robust this is. --djk + get outputHeight() { + if (!this.isComposeWindow) { + let container = document.getElementById("tabpanelcontainer").boxObject; + let deck = document.getElementById("displayDeck"); + let box = document.getElementById("messagepanebox"); + let splitter = document.getElementById("threadpane-splitter").boxObject; + + if (splitter.width > splitter.height) + return container.height - deck.minHeight - box.minHeight- splitter.height; + else + return container.height - Math.max(deck.minHeight, box.minHeight); + } + else + return document.getElementById("appcontent").boxObject.height; + }, + + get scripts() this.isComposeWindow() ? ["compose/compose.js"] : [ + "addressbook.js", + "mail.js", + ], + + // to allow Vim to :set ft=mail automatically + tempFile: "mutt-ator-mail", + + init: function () { + // don't wait too long when selecting new messages + // GetThreadTree()._selectDelay = 300; // TODO: make configurable + + commands.add(["pref[erences]", "prefs"], + "Show " + config.hostApplication + " preferences", + function () { window.openOptionsDialog(); }, + { argCount: "0" }); + + // FIXME: comment obviously incorrect + // 0: never automatically edit externally + // 1: automatically edit externally when message window is shown the first time + // 2: automatically edit externally, once the message text gets focus (not working currently) + options.add(["autoexternal", "ae"], + "Edit message with external editor by default", + "boolean", false); + + options.add(["online"], + "Set the 'work offline' option", + "boolean", true, + { + setter: function (value) { + if (MailOfflineMgr.isOnline() != value) + MailOfflineMgr.toggleOfflineStatus(); + return value; + }, + getter: function () MailOfflineMgr.isOnline() + }); + + //}}} + } +}; //}}} // vim: set fdm=marker sw=4 ts=4 et: diff --git a/muttator/content/mail.js b/muttator/content/mail.js index a6519aad..416b95d4 100644 --- a/muttator/content/mail.js +++ b/muttator/content/mail.js @@ -3,19 +3,22 @@ // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. -function Mail() { //{{{ - //////////////////////////////////////////////////////////////////////////////// - ////////////////////// PRIVATE SECTION ///////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ +const Mail = Module("mail", { + init: function () { + services.add("smtpService", "@mozilla.org/messengercompose/smtp;1", Ci.nsISmtpService); - services.add("smtpService", "@mozilla.org/messengercompose/smtp;1", Ci.nsISmtpService); + // used for asynchronously selecting messages after wrapping folders + this._selectMessageKeys = []; + this._selectMessageCount = 1; + this._selectMessageReverse = false; - // used for asynchronously selecting messages after wrapping folders - var selectMessageKeys = []; - var selectMessageCount = 1; - var selectMessageReverse = false; + this._mailSession = Cc["@mozilla.org/messenger/services/session;1"].getService(Ci.nsIMsgMailSession); + this._nsIFolderListener = Ci.this._nsIFolderListener; + this._notifyFlags = this._nsIFolderListener.intPropertyChanged | this._nsIFolderListener.event; + this._mailSession.AddFolderListener(this._folderListener, this._notifyFlags); + }, - var folderListener = { + _folderListener: { OnItemAdded: function (parentItem, item) {}, OnItemRemoved: function (parentItem, item) {}, OnItemPropertyChanged: function (item, property, oldValue, newValue) {}, @@ -33,17 +36,17 @@ function Mail() { //{{{ // Jump to a message when requested let indices = []; - if (selectMessageKeys.length > 0) { - for (let j = 0; j < selectMessageKeys.length; j++) - indices.push([gDBView.findIndexFromKey(selectMessageKeys[j], true), selectMessageKeys[j]]); + if (this._selectMessageKeys.length > 0) { + for (let j = 0; j < this._selectMessageKeys.length; j++) + indices.push([gDBView.findIndexFromKey(this._selectMessageKeys[j], true), this._selectMessageKeys[j]]); indices.sort(); - let index = selectMessageCount - 1; - if (selectMessageReverse) - index = selectMessageKeys.length - 1 - index; + let index = this._selectMessageCount - 1; + if (this._selectMessageReverse) + index = this._selectMessageKeys.length - 1 - index; gDBView.selectMsgByKey(indices[index][1]); - selectMessageKeys = []; + this._selectMessageKeys = []; } } } @@ -55,23 +58,18 @@ function Mail() { //{{{ else if (eventType == "RenameCompleted") {} else if (eventType == "JunkStatusChanged") {}*/ } - }; + }, - var mailSession = Cc["@mozilla.org/messenger/services/session;1"].getService(Ci.nsIMsgMailSession); - var nsIFolderListener = Ci.nsIFolderListener; - var notifyFlags = nsIFolderListener.intPropertyChanged | nsIFolderListener.event; - mailSession.AddFolderListener(folderListener, notifyFlags); - - function getCurrentFolderIndex() { + _getCurrentFolderIndex: function () { // for some reason, the index is interpreted as a string, therefore the parseInt return parseInt(gFolderTreeView.getIndexOfFolder(gFolderTreeView.getSelectedFolders()[0])); - } + }, - function getRSSUrl() { + _getRSSUrl: function () { return gDBView.hdrForFirstSelectedMessage.messageId.replace(/(#.*)?@.*$/, ""); - } + }, - function moveOrCopy(copy, destinationFolder, operateOnThread) { + _moveOrCopy: function (copy, destinationFolder, operateOnThread) { let folders = mail.getFolders(destinationFolder); if (folders.length == 0) return void liberator.echoerr("Exxx: No matching folder for " + destinationFolder); @@ -86,9 +84,9 @@ function Mail() { //{{{ setTimeout(function () { liberator.echomsg(count + " message(s) " + (copy ? "copied" : "moved") + " to " + folders[0].prettyName, 1); }, 100); - } + }, - function parentIndex(index) { + _parentIndex: function (index) { let parent = index; let tree = GetThreadTree(); @@ -100,13 +98,13 @@ function Mail() { //{{{ break; } return parent; - } + }, // does not wrap yet, intentional? - function selectUnreadFolder(backwards, count) { + _selectUnreadFolder: function (backwards, count) { count = Math.max(1, count); let direction = backwards ? -1 : 1; - let c = getCurrentFolderIndex(); + let c = this._getCurrentFolderIndex(); let i = direction; let folder; while (count > 0 && (c + i) < gFolderTreeView.rowCount && (c + i) >= 0) { @@ -121,844 +119,826 @@ function Mail() { //{{{ liberator.beep(); else gFolderTreeView.selection.timedSelect(c + folder, 500); - } + }, - function escapeRecipient(recipient) { + _escapeRecipient: function (recipient) { // strip all ": recipient = recipient.replace(/"/g, ""); return "\"" + recipient + "\""; - } - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// OPTIONS ///////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - // FIXME: why does this default to "Archive", I don't have one? The default - // value won't validate now. mst please fix. --djk - options.add(["archivefolder"], - "Set the archive folder", - "string", "Archive", - { - completer: function (context) completion.mailFolder(context), - validator: Option.validateCompleter - }); - - // TODO: generate the possible values dynamically from the menu - options.add(["layout"], - "Set the layout of the mail window", - "string", "inherit", - { - setter: function (value) { - switch (value) { - case "classic": ChangeMailLayout(0); break; - case "wide": ChangeMailLayout(1); break; - case "vertical": ChangeMailLayout(2); break; - // case "inherit" just does nothing - } - - return value; - }, - completer: function (context) [ - ["inherit", "Default View"], // FIXME: correct description? - ["classic", "Classic View"], - ["wide", "Wide View"], - ["vertical", "Vertical View"] - ], - validator: Option.validateCompleter - }); - - options.add(["smtpserver", "smtp"], - "Set the default SMTP server", - "string", services.get("smtpService").defaultServer.key, // TODO: how should we handle these persistent external defaults - "inherit" or null? - { - getter: function () services.get("smtpService").defaultServer.key, - setter: function (value) { - let server = mail.smtpServers.filter(function (s) s.key == value)[0]; - services.get("smtpService").defaultServer = server; - return value; - }, - completer: function (context) [[s.key, s.serverURI] for ([, s] in Iterator(mail.smtpServers))], - validator: Option.validateCompleter - }); - - /*options.add(["threads"], - "Use threading to group messages", - "boolean", true, - { - setter: function (value) { - if (value) - MsgSortThreaded(); - else - MsgSortUnthreaded(); - - return value; - } - });*/ - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// MAPPINGS //////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - var myModes = config.mailModes; - - mappings.add(myModes, ["", "i"], - "Inspect (focus) message", - function () { content.focus(); }); - - mappings.add(myModes, ["I"], - "Open the message in new tab", - function () { - if (gDBView && gDBView.selection.count < 1) - return void liberator.beep(); - - MsgOpenNewTabForMessage(); - }); - - /*mappings.add([modes.NORMAL], - ["o"], "Open a message", - function () { commandline.open(":", "open ", modes.EX); });*/ - - mappings.add(myModes, [""], - "Scroll message or select next unread one", - function () true, - { route: true }); - - mappings.add(myModes, ["t"], - "Select thread", - function () { gDBView.ExpandAndSelectThreadByIndex(GetThreadTree().currentIndex, false); }); - - mappings.add(myModes, ["d", ""], - "Move mail to Trash folder", - function () { window.goDoCommand("cmd_delete"); }); - - mappings.add(myModes, ["j", ""], - "Select next message", - function (count) { mail.selectMessage(function (msg) true, false, false, false, count); }, - { count: true }); - - mappings.add(myModes, ["gj"], - "Select next message, including closed threads", - function (count) { mail.selectMessage(function (msg) true, false, true, false, count); }, - { count: true }); - - mappings.add(myModes, ["J", ""], - "Select next unread message", - function (count) { mail.selectMessage(function (msg) !msg.isRead, true, true, false, count); }, - { count: true }); - - mappings.add(myModes, ["k", ""], - "Select previous message", - function (count) { mail.selectMessage(function (msg) true, false, false, true, count); }, - { count: true }); - - mappings.add(myModes, ["gk"], - "Select previous message", - function (count) { mail.selectMessage(function (msg) true, false, true, true, count); }, - { count: true }); - - mappings.add(myModes, ["K"], - "Select previous unread message", - function (count) { mail.selectMessage(function (msg) !msg.isRead, true, true, true, count); }, - { count: true }); - - mappings.add(myModes, ["*"], - "Select next message from the same sender", - function (count) { - try { - let author = gDBView.hdrForFirstSelectedMessage.mime2DecodedAuthor.toLowerCase(); - mail.selectMessage(function (msg) msg.mime2DecodedAuthor.toLowerCase().indexOf(author) == 0, true, true, false, count); - } - catch (e) { liberator.beep(); } - }, - { count: true }); - - mappings.add(myModes, ["#"], - "Select previous message from the same sender", - function (count) { - try { - let author = gDBView.hdrForFirstSelectedMessage.mime2DecodedAuthor.toLowerCase(); - mail.selectMessage(function (msg) msg.mime2DecodedAuthor.toLowerCase().indexOf(author) == 0, true, true, true, count); - } - catch (e) { liberator.beep(); } - }, - { count: true }); - - // SENDING MESSAGES - mappings.add(myModes, ["m"], - "Compose a new message", - function () { commandline.open(":", "mail -subject=", modes.EX); }); - - mappings.add(myModes, ["M"], - "Compose a new message to the sender of selected mail", - function () { - try { - let to = escapeRecipient(gDBView.hdrForFirstSelectedMessage.mime2DecodedAuthor); - commandline.open(":", "mail " + to + " -subject=", modes.EX); - } - catch (e) { - liberator.beep(); - } - }); - - mappings.add(myModes, ["r"], - "Reply to sender", - function () { window.goDoCommand("cmd_reply"); }); - - mappings.add(myModes, ["R"], - "Reply to all", - function () { window.goDoCommand("cmd_replyall"); }); - - mappings.add(myModes, ["f"], - "Forward message", - function () { window.goDoCommand("cmd_forward"); }); - - mappings.add(myModes, ["F"], - "Forward message inline", - function () { window.goDoCommand("cmd_forwardInline"); }); - - // SCROLLING - mappings.add(myModes, [""], - "Scroll message down", - function (count) { buffer.scrollLines(Math.max(count, 1)); }, - { count: true }); - - mappings.add(myModes, [""], - "Scroll message up", - function (count) { buffer.scrollLines(-Math.max(count, 1)); }, - { count: true }); - - mappings.add([modes.MESSAGE], [""], - "Select previous message", - function (count) { mail.selectMessage(function (msg) true, false, false, true, count); }, - { count: true }); - - mappings.add([modes.MESSAGE], [""], - "Select next message", - function (count) { mail.selectMessage(function (msg) true, false, false, false, count); }, - { count: true }); - - // UNDO/REDO - mappings.add(myModes, ["u"], - "Undo", - function () { - if (messenger.canUndo()) - messenger.undo(msgWindow); - else - liberator.beep(); - }); - mappings.add(myModes, [""], - "Redo", - function () { - if (messenger.canRedo()) - messenger.redo(msgWindow); - else - liberator.beep(); - }); - - // GETTING MAIL - mappings.add(myModes, ["gm"], - "Get new messages", - function () { mail.getNewMessages(); }); - - mappings.add(myModes, ["gM"], - "Get new messages for current account only", - function () { mail.getNewMessages(true); }); - - // MOVING MAIL - mappings.add(myModes, ["c"], - "Change folders", - function () { commandline.open(":", "goto ", modes.EX); }); - - mappings.add(myModes, ["s"], - "Move selected messages", - function () { commandline.open(":", "moveto ", modes.EX); }); - - mappings.add(myModes, ["S"], - "Copy selected messages", - function () { commandline.open(":", "copyto ", modes.EX); }); - - mappings.add(myModes, [""], - "Archive message", - function () { moveOrCopy(false, options["archivefolder"]); }); - - mappings.add(myModes, ["]s"], - "Select next starred message", - function (count) { mail.selectMessage(function (msg) msg.isFlagged, true, true, false, count); }, - { count: true }); - - mappings.add(myModes, ["[s"], - "Select previous starred message", - function (count) { mail.selectMessage(function (msg) msg.isFlagged, true, true, true, count); }, - { count: true }); - - mappings.add(myModes, ["]a"], - "Select next message with an attachment", - function (count) { mail.selectMessage(function (msg) gDBView.db.HasAttachments(msg.messageKey), true, true, false, count); }, - { count: true }); - - mappings.add(myModes, ["[a"], - "Select previous message with an attachment", - function (count) { mail.selectMessage(function (msg) gDBView.db.HasAttachments(msg.messageKey), true, true, true, count); }, - { count: true }); - - // FOLDER SWITCHING - mappings.add(myModes, ["gi"], - "Go to inbox", - function (count) { - let folder = mail.getFolders("Inbox", false, true)[(count > 0) ? (count - 1) : 0]; - if (folder) - SelectFolder(folder.URI); - else - liberator.beep(); - }, - { count: true }); - - mappings.add(myModes, [""], - "Select next folder", - function (count) { - count = Math.max(1, count); - let newPos = getCurrentFolderIndex() + count; - if (newPos >= gFolderTreeView.rowCount) { - newPos = newPos % gFolderTreeView.rowCount; - commandline.echo("search hit BOTTOM, continuing at TOP", commandline.HL_WARNINGMSG, commandline.APPEND_TO_MESSAGES); - } - gFolderTreeView.selection.timedSelect(newPos, 500); - }, - { count: true }); - - mappings.add(myModes, [""], - "Go to next mailbox with unread messages", - function (count) { - selectUnreadFolder(false, count); - }, - { count: true }); - - mappings.add(myModes, [""], - "Select previous folder", - function (count) { - count = Math.max(1, count); - let newPos = getCurrentFolderIndex() - count; - if (newPos < 0) { - newPos = (newPos % gFolderTreeView.rowCount) + gFolderTreeView.rowCount; - commandline.echo("search hit TOP, continuing at BOTTOM", commandline.HL_WARNINGMSG, commandline.APPEND_TO_MESSAGES); - } - gFolderTreeView.selection.timedSelect(newPos, 500); - }, - { count: true }); - - mappings.add(myModes, [""], - "Go to previous mailbox with unread messages", - function (count) { - selectUnreadFolder(true, count); - }, - { count: true }); - - // THREADING - mappings.add(myModes, ["za"], - "Toggle thread collapsed/expanded", - function () { if (!mail.expandThread()) mail.collapseThread(); }); - - mappings.add(myModes, ["zc"], - "Collapse thread", - function () { mail.collapseThread(); }); - - mappings.add(myModes, ["zo"], - "Open thread", - function () { mail.expandThread(); }); - - mappings.add(myModes, ["zr", "zR"], - "Expand all threads", - function () { window.goDoCommand("cmd_expandAllThreads"); }); - - mappings.add(myModes, ["zm", "zM"], - "Collapse all threads", - function () { window.goDoCommand("cmd_collapseAllThreads"); }); - - mappings.add(myModes, [""], - "Go forward", - function (count) { if (count < 1) count = 1; while (count--) GoNextMessage(nsMsgNavigationType.forward, true); }, - { count: true }); - - mappings.add(myModes, [""], - "Go back", - function (count) { if (count < 1) count = 1; while (count--) GoNextMessage(nsMsgNavigationType.back, true); }, - { count: true }); - - mappings.add(myModes, ["gg"], - "Select first message", - function (count) { if (count < 1) count = 1; while (count--) GoNextMessage(nsMsgNavigationType.firstMessage, true); }, - { count: true }); - - mappings.add(myModes, ["G"], - "Select last message", - function (count) { if (count < 1) count = 1; while (count--) GoNextMessage(nsMsgNavigationType.lastMessage, false); }, - { count: true }); - - // tagging messages - mappings.add(myModes, ["l"], - "Label message", - function (arg) { - if (!GetSelectedMessages()) - return void liberator.beep(); - - switch (arg) { - case "r": MsgMarkMsgAsRead(); break; - case "s": MsgMarkAsFlagged(); break; - case "i": ToggleMessageTagKey(1); break; // Important - case "w": ToggleMessageTagKey(2); break; // Work - case "p": ToggleMessageTagKey(3); break; // Personal - case "t": ToggleMessageTagKey(4); break; // TODO - case "l": ToggleMessageTagKey(5); break; // Later - default: liberator.beep(); - } - }, - { - arg: true - }); - - // TODO: change binding? - mappings.add(myModes, ["T"], - "Mark current folder as read", - function () { - if (mail.currentFolder.isServer) - return liberator.beep(); - - mail.currentFolder.markAllMessagesRead(msgWindow); - }); - - mappings.add(myModes, [""], - "Mark all messages as read", - function () { - mail.getFolders("", false).forEach(function (folder) { folder.markAllMessagesRead(msgWindow); }); - }); - - // DISPLAY OPTIONS - mappings.add(myModes, ["h"], - "Toggle displayed headers", - function () { - let value = gPrefBranch.getIntPref("mail.show_headers", 2); - gPrefBranch.setIntPref("mail.show_headers", value == 2 ? 1 : 2); - ReloadMessage(); - }); - - mappings.add(myModes, ["x"], - "Toggle HTML message display", - function () { - let wantHtml = (gPrefBranch.getIntPref("mailnews.display.html_as", 1) == 1); - mail.setHTML(wantHtml ? 1 : 0); - }); - - // YANKING TEXT - mappings.add(myModes, ["Y"], - "Yank subject", - function () { - try { - let subject = gDBView.hdrForFirstSelectedMessage.mime2DecodedSubject; - util.copyToClipboard(subject, true); - } - catch (e) { liberator.beep(); } - }); - - mappings.add(myModes, ["y"], - "Yank sender or feed URL", - function () { - try { - if (mail.currentAccount.server.type == "rss") - util.copyToClipboard(getRSSUrl(), true); - else - util.copyToClipboard(gDBView.hdrForFirstSelectedMessage.mime2DecodedAuthor, true); - } - catch (e) { liberator.beep(); } - }); - - // RSS specific mappings - mappings.add(myModes, ["p"], - "Open RSS message in browser", - function () { - try { - if (mail.currentAccount.server.type == "rss") - messenger.launchExternalURL(getRSSUrl()); - // TODO: what to do for non-rss message? - } - catch (e) { - liberator.beep(); - } - }); - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// COMMANDS //////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - commands.add(["go[to]"], - "Select a folder", - function (args) { - let count = Math.max(0, args.count - 1); - let arg = args.literalArg || "Inbox"; - - let folder = mail.getFolders(arg, true, true)[count]; - if (!folder) - liberator.echoerr("Exxx: Folder \"" + arg + "\" does not exist"); - else if (liberator.forceNewTab) - MsgOpenNewTabForFolder(folder.URI); - else - SelectFolder(folder.URI); - }, - { - argCount: "?", - completer: function (context) completion.mailFolder(context), - count: true, - literal: 0 - }); - - commands.add(["m[ail]"], - "Write a new message", - function (args) { - let mailargs = {}; - mailargs.to = args.join(", "); - mailargs.subject = args["-subject"]; - mailargs.bcc = args["-bcc"]; - mailargs.cc = args["-cc"]; - mailargs.body = args["-text"]; - mailargs.attachments = args["-attachment"] || []; - - let addresses = args; - if (mailargs.bcc) - addresses = addresses.concat(mailargs.bcc); - if (mailargs.cc) - addresses = addresses.concat(mailargs.cc); - - // TODO: is there a better way to check for validity? - if (addresses.some(function (recipient) !(/\S@\S+\.\S/.test(recipient)))) - return void liberator.echoerr("Exxx: Invalid e-mail address"); - - mail.composeNewMail(mailargs); - }, - { - options: [[["-subject", "-s"], commands.OPTION_STRING], - [["-attachment", "-a"], commands.OPTION_LIST], - [["-bcc", "-b"], commands.OPTION_STRING], - [["-cc", "-c"], commands.OPTION_STRING], - [["-text", "-t"], commands.OPTION_STRING]] - }); - - commands.add(["copy[to]"], - "Copy selected messages", - function (args) { moveOrCopy(true, args.literalArg); }, - { - argCount: 1, - completer: function (context) completion.mailFolder(context), - literal: 0 - }); - - commands.add(["move[to]"], - "Move selected messages", - function (args) { moveOrCopy(false, args.literalArg); }, - { - argCount: 1, - completer: function (context) completion.mailFolder(context), - literal: 0 - }); - - commands.add(["empty[trash]"], - "Empty trash of the current account", - function () { window.goDoCommand("cmd_emptyTrash"); }, - { argCount: "0" }); - - commands.add(["get[messages]"], - "Check for new messages", - function (args) mail.getNewMessages(!args.bang), - { - argCount: "0", - bang: true, - }); - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// COMPLETIONS ///////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - completion.mailFolder = function mailFolder(context) { - let folders = mail.getFolders(context.filter); - context.anchored = false; - context.quote = false; - context.completions = folders.map(function (folder) - [folder.server.prettyName + ": " + folder.name, - "Unread: " + folder.getNumUnread(false)]); - }; - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// PUBLIC SECTION ////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - return { - - get currentAccount() this.currentFolder.rootFolder, - - get currentFolder() gFolderTreeView.getSelectedFolders()[0], - - /** @property {nsISmtpServer[]} The list of configured SMTP servers. */ - get smtpServers() { - let servers = services.get("smtpService").smtpServers; - let ret = []; - - while (servers.hasMoreElements()) { - let server = servers.getNext(); - if (server instanceof Ci.nsISmtpServer) - ret.push(server); - } - - return ret; - }, - - composeNewMail: function (args) { - let params = Cc["@mozilla.org/messengercompose/composeparams;1"].createInstance(Ci.nsIMsgComposeParams); - params.composeFields = Cc["@mozilla.org/messengercompose/composefields;1"].createInstance(Ci.nsIMsgCompFields); - - if (args) { - if (args.originalMsg) - params.originalMsgURI = args.originalMsg; - if (args.to) - params.composeFields.to = args.to; - if (args.cc) - params.composeFields.cc = args.cc; - if (args.bcc) - params.composeFields.bcc = args.bcc; - if (args.newsgroups) - params.composeFields.newsgroups = args.newsgroups; - if (args.subject) - params.composeFields.subject = args.subject; - if (args.body) - params.composeFields.body = args.body; - - if (args.attachments) { - while (args.attachments.length > 0) { - let url = args.attachments.pop(); - let file = io.getFile(url); - if (!file.exists()) - return void liberator.echoerr("Exxx: Could not attach file `" + url + "'", commandline.FORCE_SINGLELINE); - - attachment = Cc["@mozilla.org/messengercompose/attachment;1"].createInstance(Ci.nsIMsgAttachment); - attachment.url = "file://" + file.path; - params.composeFields.addAttachment(attachment); - } + }, + + get currentAccount() this.currentFolder.rootFolder, + + get currentFolder() gFolderTreeView.getSelectedFolders()[0], + + /** @property {nsISmtpServer[]} The list of configured SMTP servers. */ + get smtpServers() { + let servers = services.get("smtpService").smtpServers; + let ret = []; + + while (servers.hasMoreElements()) { + let server = servers.getNext(); + if (server instanceof Ci.nsISmtpServer) + ret.push(server); + } + + return ret; + }, + + composeNewMail: function (args) { + let params = Cc["@mozilla.org/messengercompose/composeparams;1"].createInstance(Ci.nsIMsgComposeParams); + params.composeFields = Cc["@mozilla.org/messengercompose/composefields;1"].createInstance(Ci.nsIMsgCompFields); + + if (args) { + if (args.originalMsg) + params.originalMsgURI = args.originalMsg; + if (args.to) + params.composeFields.to = args.to; + if (args.cc) + params.composeFields.cc = args.cc; + if (args.bcc) + params.composeFields.bcc = args.bcc; + if (args.newsgroups) + params.composeFields.newsgroups = args.newsgroups; + if (args.subject) + params.composeFields.subject = args.subject; + if (args.body) + params.composeFields.body = args.body; + + if (args.attachments) { + while (args.attachments.length > 0) { + let url = args.attachments.pop(); + let file = io.getFile(url); + if (!file.exists()) + return void liberator.echoerr("Exxx: Could not attach file `" + url + "'", commandline.FORCE_SINGLELINE); + + attachment = Cc["@mozilla.org/messengercompose/attachment;1"].createInstance(Ci.nsIMsgAttachment); + attachment.url = "file://" + file.path; + params.composeFields.addAttachment(attachment); } } + } - params.type = Ci.nsIMsgCompType.New; + params.type = Ci.nsIMsgCompType.New; - const msgComposeService = Cc["@mozilla.org/messengercompose;1"].getService(); - msgComposeService = msgComposeService.QueryInterface(Ci.nsIMsgComposeService); - msgComposeService.OpenComposeWindowWithParams(null, params); - }, + const msgComposeService = Cc["@mozilla.org/messengercompose;1"].getService(); + msgComposeService = msgComposeService.QueryInterface(Ci.nsIMsgComposeService); + msgComposeService.OpenComposeWindowWithParams(null, params); + }, - // returns an array of nsIMsgFolder objects - getFolders: function (filter, includeServers, includeMsgFolders) { - let folders = []; - if (!filter) - filter = ""; - else - filter = filter.toLowerCase(); + // returns an array of nsIMsgFolder objects + getFolders: function (filter, includeServers, includeMsgFolders) { + let folders = []; + if (!filter) + filter = ""; + else + filter = filter.toLowerCase(); - if (includeServers === undefined) - includeServers = false; - if (includeMsgFolders === undefined) - includeMsgFolders = true; + if (includeServers === undefined) + includeServers = false; + if (includeMsgFolders === undefined) + includeMsgFolders = true; - for (let i = 0; i < gFolderTreeView.rowCount; i++) { - let resource = gFolderTreeView._rowMap[i]._folder; - if ((resource.isServer && !includeServers) || (!resource.isServer && !includeMsgFolders)) - continue; + for (let i = 0; i < gFolderTreeView.rowCount; i++) { + let resource = gFolderTreeView._rowMap[i]._folder; + if ((resource.isServer && !includeServers) || (!resource.isServer && !includeMsgFolders)) + continue; - let folderString = resource.server.prettyName + ": " + resource.name; + let folderString = resource.server.prettyName + ": " + resource.name; - if (resource.prettiestName.toLowerCase().indexOf(filter) >= 0) - folders.push(resource); - else if (folderString.toLowerCase().indexOf(filter) >= 0) - folders.push(resource); + if (resource.prettiestName.toLowerCase().indexOf(filter) >= 0) + folders.push(resource); + else if (folderString.toLowerCase().indexOf(filter) >= 0) + folders.push(resource); + } + return folders; + }, + + getNewMessages: function (currentAccountOnly) { + if (currentAccountOnly) + MsgGetMessagesForAccount(); + else + GetMessagesForAllAuthenticatedAccounts(); + }, + + getStatistics: function (currentAccountOnly) { + let accounts = currentAccountOnly ? [this.currentAccount] + : this.getFolders("", true, false); + + let unreadCount = 0, totalCount = 0, newCount = 0; + for (let i = 0; i < accounts.length; i++) { + let account = accounts[i]; + unreadCount += account.getNumUnread(true); // true == deep (includes subfolders) + totalCount += account.getTotalMessages(true); + newCount += account.getNumUnread(true); + } + + return { numUnread: unreadCount, numTotal: totalCount, numNew: newCount }; + }, + + collapseThread: function () { + let tree = GetThreadTree(); + if (tree) { + let parent = this._parentIndex(tree.currentIndex); + if (tree.changeOpenState(parent, false)) { + tree.view.selection.select(parent); + tree.treeBoxObject.ensureRowIsVisible(parent); + return true; } - return folders; - }, + } + return false; + }, - getNewMessages: function (currentAccountOnly) { - if (currentAccountOnly) - MsgGetMessagesForAccount(); - else - GetMessagesForAllAuthenticatedAccounts(); - }, + expandThread: function () { + let tree = GetThreadTree(); + if (tree) { + let row = tree.currentIndex; + if (row >= 0 && tree.changeOpenState(row, true)) + return true; + } + return false; + }, - getStatistics: function (currentAccountOnly) { - let accounts = currentAccountOnly ? [this.currentAccount] - : this.getFolders("", true, false); + /** + * General-purpose method to find messages. + * + * @param {function(nsIMsgDBHdr):boolean} validatorFunc Return + * true/false whether msg should be selected or not. + * @param {boolean} canWrap When true, wraps around folders. + * @param {boolean} openThreads Should we open closed threads? + * @param {boolean} reverse Change direction of searching. + */ + selectMessage: function (validatorFunc, canWrap, openThreads, reverse, count) { + function currentIndex() { + let index = gDBView.selection.currentIndex; + if (index < 0) + index = 0; + return index; + } - let unreadCount = 0, totalCount = 0, newCount = 0; - for (let i = 0; i < accounts.length; i++) { - let account = accounts[i]; - unreadCount += account.getNumUnread(true); // true == deep (includes subfolders) - totalCount += account.getTotalMessages(true); - newCount += account.getNumUnread(true); - } + function closedThread(index) { + if (!(gDBView.viewFlags & nsMsgViewFlagsType.kThreadedDisplay)) + return false; - return { numUnread: unreadCount, numTotal: totalCount, numNew: newCount }; - }, + index = (typeof index == "number") ? index : currentIndex(); + return !gDBView.isContainerOpen(index) && !gDBView.isContainerEmpty(index); + } - collapseThread: function () { - let tree = GetThreadTree(); - if (tree) { - let parent = parentIndex(tree.currentIndex); - if (tree.changeOpenState(parent, false)) { - tree.view.selection.select(parent); - tree.treeBoxObject.ensureRowIsVisible(parent); - return true; - } - } - return false; - }, + if (typeof validatorFunc != "function") + return; - expandThread: function () { - let tree = GetThreadTree(); - if (tree) { - let row = tree.currentIndex; - if (row >= 0 && tree.changeOpenState(row, true)) - return true; - } - return false; - }, + if (typeof count != "number" || count < 1) + count = 1; - /** - * General-purpose method to find messages. - * - * @param {function(nsIMsgDBHdr):boolean} validatorFunc Return - * true/false whether msg should be selected or not. - * @param {boolean} canWrap When true, wraps around folders. - * @param {boolean} openThreads Should we open closed threads? - * @param {boolean} reverse Change direction of searching. - */ - selectMessage: function (validatorFunc, canWrap, openThreads, reverse, count) { - function currentIndex() { - let index = gDBView.selection.currentIndex; - if (index < 0) - index = 0; - return index; - } + // first try to find in current folder + if (gDBView) { + for (let i = currentIndex() + (reverse ? -1 : (openThreads && closedThread() ? 0 : 1)); + reverse ? (i >= 0) : (i < gDBView.rowCount); + reverse ? i-- : i++) { + let key = gDBView.getKeyAt(i); + let msg = gDBView.db.GetMsgHdrForKey(key); - function closedThread(index) { - if (!(gDBView.viewFlags & nsMsgViewFlagsType.kThreadedDisplay)) - return false; + // a closed thread + if (openThreads && closedThread(i)) { + let thread = gDBView.db.GetThreadContainingMsgHdr(msg); + let originalCount = count; - index = (typeof index == "number") ? index : currentIndex(); - return !gDBView.isContainerOpen(index) && !gDBView.isContainerEmpty(index); - } - - if (typeof validatorFunc != "function") - return; - - if (typeof count != "number" || count < 1) - count = 1; - - // first try to find in current folder - if (gDBView) { - for (let i = currentIndex() + (reverse ? -1 : (openThreads && closedThread() ? 0 : 1)); - reverse ? (i >= 0) : (i < gDBView.rowCount); - reverse ? i-- : i++) { - let key = gDBView.getKeyAt(i); - let msg = gDBView.db.GetMsgHdrForKey(key); - - // a closed thread - if (openThreads && closedThread(i)) { - let thread = gDBView.db.GetThreadContainingMsgHdr(msg); - let originalCount = count; - - for (let j = (i == currentIndex() && !reverse) ? 1 : (reverse ? thread.numChildren - 1 : 0); - reverse ? (j >= 0) : (j < thread.numChildren); - reverse ? j-- : j++) { - msg = thread.getChildAt(j); - if (validatorFunc(msg) && --count == 0) { - // this hack is needed to get the correct message, because getChildAt() does not - // necessarily return the messages in the order they are displayed - gDBView.selection.timedSelect(i, GetThreadTree()._selectDelay || 500); - GetThreadTree().treeBoxObject.ensureRowIsVisible(i); - if (j > 0) { - GetThreadTree().changeOpenState(i, true); - this.selectMessage(validatorFunc, false, false, false, originalCount); - } - return; - } - } - } - else { // simple non-threaded message + for (let j = (i == currentIndex() && !reverse) ? 1 : (reverse ? thread.numChildren - 1 : 0); + reverse ? (j >= 0) : (j < thread.numChildren); + reverse ? j-- : j++) { + msg = thread.getChildAt(j); if (validatorFunc(msg) && --count == 0) { + // this hack is needed to get the correct message, because getChildAt() does not + // necessarily return the messages in the order they are displayed gDBView.selection.timedSelect(i, GetThreadTree()._selectDelay || 500); GetThreadTree().treeBoxObject.ensureRowIsVisible(i); + if (j > 0) { + GetThreadTree().changeOpenState(i, true); + this.selectMessage(validatorFunc, false, false, false, originalCount); + } return; } } } - } - - // then in other folders - if (canWrap) { - selectMessageReverse = reverse; - - let folders = this.getFolders("", true, true); - let ci = getCurrentFolderIndex(); - for (let i = 1; i < folders.length; i++) { - let index = (i + ci) % folders.length; - if (reverse) - index = folders.length - 1 - index; - - let folder = folders[index]; - if (folder.isServer) - continue; - - selectMessageCount = count; - selectMessageKeys = []; - - // sometimes folder.getMessages can fail with an exception - // TODO: find out why, and solve the problem - try { - var msgs = folder.messages; - } - catch (e) { - msgs = folder.getMessages(msgWindow); // for older thunderbirds - liberator.dump("WARNING: " + folder.prettyName + " failed to getMessages, trying old API"); - //continue; - } - - while (msgs.hasMoreElements()) { - let msg = msgs.getNext().QueryInterface(Ci.nsIMsgDBHdr); - if (validatorFunc(msg)) { - count--; - selectMessageKeys.push(msg.messageKey); - } - } - - if (count <= 0) { - // SelectFolder is asynchronous, message is selected in folderListener - SelectFolder(folder.URI); + else { // simple non-threaded message + if (validatorFunc(msg) && --count == 0) { + gDBView.selection.timedSelect(i, GetThreadTree()._selectDelay || 500); + GetThreadTree().treeBoxObject.ensureRowIsVisible(i); return; } } } - - // TODO: finally for the "rest" of the current folder - - liberator.beep(); - }, - - setHTML: function (value) { - let values = [[true, 1, gDisallow_classes_no_html], // plaintext - [false, 0, 0], // HTML - [false, 3, gDisallow_classes_no_html]]; // sanitized/simple HTML - - if (typeof value != "number" || value < 0 || value > 2) - value = 1; - - gPrefBranch.setBoolPref("mailnews.display.prefer_plaintext", values[value][0]); - gPrefBranch.setIntPref("mailnews.display.html_as", values[value][1]); - gPrefBranch.setIntPref("mailnews.display.disallow_mime_handlers", values[value][2]); - ReloadMessage(); } - }; - //}}} -} //}}} + + // then in other folders + if (canWrap) { + this._selectMessageReverse = reverse; + + let folders = this.getFolders("", true, true); + let ci = this._getCurrentFolderIndex(); + for (let i = 1; i < folders.length; i++) { + let index = (i + ci) % folders.length; + if (reverse) + index = folders.length - 1 - index; + + let folder = folders[index]; + if (folder.isServer) + continue; + + this._selectMessageCount = count; + this._selectMessageKeys = []; + + // sometimes folder.getMessages can fail with an exception + // TODO: find out why, and solve the problem + try { + var msgs = folder.messages; + } + catch (e) { + msgs = folder.getMessages(msgWindow); // for older thunderbirds + liberator.dump("WARNING: " + folder.prettyName + " failed to getMessages, trying old API"); + //continue; + } + + while (msgs.hasMoreElements()) { + let msg = msgs.getNext().QueryInterface(Ci.nsIMsgDBHdr); + if (validatorFunc(msg)) { + count--; + this._selectMessageKeys.push(msg.messageKey); + } + } + + if (count <= 0) { + // SelectFolder is asynchronous, message is selected in this._folderListener + SelectFolder(folder.URI); + return; + } + } + } + + // TODO: finally for the "rest" of the current folder + + liberator.beep(); + }, + + setHTML: function (value) { + let values = [[true, 1, gDisallow_classes_no_html], // plaintext + [false, 0, 0], // HTML + [false, 3, gDisallow_classes_no_html]]; // sanitized/simple HTML + + if (typeof value != "number" || value < 0 || value > 2) + value = 1; + + gPrefBranch.setBoolPref("mailnews.display.prefer_plaintext", values[value][0]); + gPrefBranch.setIntPref("mailnews.display.html_as", values[value][1]); + gPrefBranch.setIntPref("mailnews.display.disallow_mime_handlers", values[value][2]); + ReloadMessage(); + } +}, { +}, { + commands: function () { + commands.add(["go[to]"], + "Select a folder", + function (args) { + let count = Math.max(0, args.count - 1); + let arg = args.literalArg || "Inbox"; + + let folder = mail.getFolders(arg, true, true)[count]; + if (!folder) + liberator.echoerr("Exxx: Folder \"" + arg + "\" does not exist"); + else if (liberator.forceNewTab) + MsgOpenNewTabForFolder(folder.URI); + else + SelectFolder(folder.URI); + }, + { + argCount: "?", + completer: function (context) completion.mailFolder(context), + count: true, + literal: 0 + }); + + commands.add(["m[ail]"], + "Write a new message", + function (args) { + let mailargs = {}; + mailargs.to = args.join(", "); + mailargs.subject = args["-subject"]; + mailargs.bcc = args["-bcc"]; + mailargs.cc = args["-cc"]; + mailargs.body = args["-text"]; + mailargs.attachments = args["-attachment"] || []; + + let addresses = args; + if (mailargs.bcc) + addresses = addresses.concat(mailargs.bcc); + if (mailargs.cc) + addresses = addresses.concat(mailargs.cc); + + // TODO: is there a better way to check for validity? + if (addresses.some(function (recipient) !(/\S@\S+\.\S/.test(recipient)))) + return void liberator.echoerr("Exxx: Invalid e-mail address"); + + mail.composeNewMail(mailargs); + }, + { + options: [[["-subject", "-s"], commands.OPTION_STRING], + [["-attachment", "-a"], commands.OPTION_LIST], + [["-bcc", "-b"], commands.OPTION_STRING], + [["-cc", "-c"], commands.OPTION_STRING], + [["-text", "-t"], commands.OPTION_STRING]] + }); + + commands.add(["copy[to]"], + "Copy selected messages", + function (args) { this._moveOrCopy(true, args.literalArg); }, + { + argCount: 1, + completer: function (context) completion.mailFolder(context), + literal: 0 + }); + + commands.add(["move[to]"], + "Move selected messages", + function (args) { this._moveOrCopy(false, args.literalArg); }, + { + argCount: 1, + completer: function (context) completion.mailFolder(context), + literal: 0 + }); + + commands.add(["empty[trash]"], + "Empty trash of the current account", + function () { window.goDoCommand("cmd_emptyTrash"); }, + { argCount: "0" }); + + commands.add(["get[messages]"], + "Check for new messages", + function (args) mail.getNewMessages(!args.bang), + { + argCount: "0", + bang: true, + }); + }, + completions: function () { + completion.mailFolder = function mailFolder(context) { + let folders = mail.getFolders(context.filter); + context.anchored = false; + context.quote = false; + context.completions = folders.map(function (folder) + [folder.server.prettyName + ": " + folder.name, + "Unread: " + folder.getNumUnread(false)]); + }; + }, + mappings: function () { + var myModes = config.mailModes; + + mappings.add(myModes, ["", "i"], + "Inspect (focus) message", + function () { content.focus(); }); + + mappings.add(myModes, ["I"], + "Open the message in new tab", + function () { + if (gDBView && gDBView.selection.count < 1) + return void liberator.beep(); + + MsgOpenNewTabForMessage(); + }); + + /*mappings.add([modes.NORMAL], + ["o"], "Open a message", + function () { commandline.open(":", "open ", modes.EX); });*/ + + mappings.add(myModes, [""], + "Scroll message or select next unread one", + function () true, + { route: true }); + + mappings.add(myModes, ["t"], + "Select thread", + function () { gDBView.ExpandAndSelectThreadByIndex(GetThreadTree().currentIndex, false); }); + + mappings.add(myModes, ["d", ""], + "Move mail to Trash folder", + function () { window.goDoCommand("cmd_delete"); }); + + mappings.add(myModes, ["j", ""], + "Select next message", + function (count) { mail.selectMessage(function (msg) true, false, false, false, count); }, + { count: true }); + + mappings.add(myModes, ["gj"], + "Select next message, including closed threads", + function (count) { mail.selectMessage(function (msg) true, false, true, false, count); }, + { count: true }); + + mappings.add(myModes, ["J", ""], + "Select next unread message", + function (count) { mail.selectMessage(function (msg) !msg.isRead, true, true, false, count); }, + { count: true }); + + mappings.add(myModes, ["k", ""], + "Select previous message", + function (count) { mail.selectMessage(function (msg) true, false, false, true, count); }, + { count: true }); + + mappings.add(myModes, ["gk"], + "Select previous message", + function (count) { mail.selectMessage(function (msg) true, false, true, true, count); }, + { count: true }); + + mappings.add(myModes, ["K"], + "Select previous unread message", + function (count) { mail.selectMessage(function (msg) !msg.isRead, true, true, true, count); }, + { count: true }); + + mappings.add(myModes, ["*"], + "Select next message from the same sender", + function (count) { + try { + let author = gDBView.hdrForFirstSelectedMessage.mime2DecodedAuthor.toLowerCase(); + mail.selectMessage(function (msg) msg.mime2DecodedAuthor.toLowerCase().indexOf(author) == 0, true, true, false, count); + } + catch (e) { liberator.beep(); } + }, + { count: true }); + + mappings.add(myModes, ["#"], + "Select previous message from the same sender", + function (count) { + try { + let author = gDBView.hdrForFirstSelectedMessage.mime2DecodedAuthor.toLowerCase(); + mail.selectMessage(function (msg) msg.mime2DecodedAuthor.toLowerCase().indexOf(author) == 0, true, true, true, count); + } + catch (e) { liberator.beep(); } + }, + { count: true }); + + // SENDING MESSAGES + mappings.add(myModes, ["m"], + "Compose a new message", + function () { commandline.open(":", "mail -subject=", modes.EX); }); + + mappings.add(myModes, ["M"], + "Compose a new message to the sender of selected mail", + function () { + try { + let to = this._escapeRecipient(gDBView.hdrForFirstSelectedMessage.mime2DecodedAuthor); + commandline.open(":", "mail " + to + " -subject=", modes.EX); + } + catch (e) { + liberator.beep(); + } + }); + + mappings.add(myModes, ["r"], + "Reply to sender", + function () { window.goDoCommand("cmd_reply"); }); + + mappings.add(myModes, ["R"], + "Reply to all", + function () { window.goDoCommand("cmd_replyall"); }); + + mappings.add(myModes, ["f"], + "Forward message", + function () { window.goDoCommand("cmd_forward"); }); + + mappings.add(myModes, ["F"], + "Forward message inline", + function () { window.goDoCommand("cmd_forwardInline"); }); + + // SCROLLING + mappings.add(myModes, [""], + "Scroll message down", + function (count) { buffer.scrollLines(Math.max(count, 1)); }, + { count: true }); + + mappings.add(myModes, [""], + "Scroll message up", + function (count) { buffer.scrollLines(-Math.max(count, 1)); }, + { count: true }); + + mappings.add([modes.MESSAGE], [""], + "Select previous message", + function (count) { mail.selectMessage(function (msg) true, false, false, true, count); }, + { count: true }); + + mappings.add([modes.MESSAGE], [""], + "Select next message", + function (count) { mail.selectMessage(function (msg) true, false, false, false, count); }, + { count: true }); + + // UNDO/REDO + mappings.add(myModes, ["u"], + "Undo", + function () { + if (messenger.canUndo()) + messenger.undo(msgWindow); + else + liberator.beep(); + }); + mappings.add(myModes, [""], + "Redo", + function () { + if (messenger.canRedo()) + messenger.redo(msgWindow); + else + liberator.beep(); + }); + + // GETTING MAIL + mappings.add(myModes, ["gm"], + "Get new messages", + function () { mail.getNewMessages(); }); + + mappings.add(myModes, ["gM"], + "Get new messages for current account only", + function () { mail.getNewMessages(true); }); + + // MOVING MAIL + mappings.add(myModes, ["c"], + "Change folders", + function () { commandline.open(":", "goto ", modes.EX); }); + + mappings.add(myModes, ["s"], + "Move selected messages", + function () { commandline.open(":", "moveto ", modes.EX); }); + + mappings.add(myModes, ["S"], + "Copy selected messages", + function () { commandline.open(":", "copyto ", modes.EX); }); + + mappings.add(myModes, [""], + "Archive message", + function () { this._moveOrCopy(false, options["archivefolder"]); }); + + mappings.add(myModes, ["]s"], + "Select next starred message", + function (count) { mail.selectMessage(function (msg) msg.isFlagged, true, true, false, count); }, + { count: true }); + + mappings.add(myModes, ["[s"], + "Select previous starred message", + function (count) { mail.selectMessage(function (msg) msg.isFlagged, true, true, true, count); }, + { count: true }); + + mappings.add(myModes, ["]a"], + "Select next message with an attachment", + function (count) { mail.selectMessage(function (msg) gDBView.db.HasAttachments(msg.messageKey), true, true, false, count); }, + { count: true }); + + mappings.add(myModes, ["[a"], + "Select previous message with an attachment", + function (count) { mail.selectMessage(function (msg) gDBView.db.HasAttachments(msg.messageKey), true, true, true, count); }, + { count: true }); + + // FOLDER SWITCHING + mappings.add(myModes, ["gi"], + "Go to inbox", + function (count) { + let folder = mail.getFolders("Inbox", false, true)[(count > 0) ? (count - 1) : 0]; + if (folder) + SelectFolder(folder.URI); + else + liberator.beep(); + }, + { count: true }); + + mappings.add(myModes, [""], + "Select next folder", + function (count) { + count = Math.max(1, count); + let newPos = this._getCurrentFolderIndex() + count; + if (newPos >= gFolderTreeView.rowCount) { + newPos = newPos % gFolderTreeView.rowCount; + commandline.echo("search hit BOTTOM, continuing at TOP", commandline.HL_WARNINGMSG, commandline.APPEND_TO_MESSAGES); + } + gFolderTreeView.selection.timedSelect(newPos, 500); + }, + { count: true }); + + mappings.add(myModes, [""], + "Go to next mailbox with unread messages", + function (count) { + this._selectUnreadFolder(false, count); + }, + { count: true }); + + mappings.add(myModes, [""], + "Select previous folder", + function (count) { + count = Math.max(1, count); + let newPos = this._getCurrentFolderIndex() - count; + if (newPos < 0) { + newPos = (newPos % gFolderTreeView.rowCount) + gFolderTreeView.rowCount; + commandline.echo("search hit TOP, continuing at BOTTOM", commandline.HL_WARNINGMSG, commandline.APPEND_TO_MESSAGES); + } + gFolderTreeView.selection.timedSelect(newPos, 500); + }, + { count: true }); + + mappings.add(myModes, [""], + "Go to previous mailbox with unread messages", + function (count) { + this._selectUnreadFolder(true, count); + }, + { count: true }); + + // THREADING + mappings.add(myModes, ["za"], + "Toggle thread collapsed/expanded", + function () { if (!mail.expandThread()) mail.collapseThread(); }); + + mappings.add(myModes, ["zc"], + "Collapse thread", + function () { mail.collapseThread(); }); + + mappings.add(myModes, ["zo"], + "Open thread", + function () { mail.expandThread(); }); + + mappings.add(myModes, ["zr", "zR"], + "Expand all threads", + function () { window.goDoCommand("cmd_expandAllThreads"); }); + + mappings.add(myModes, ["zm", "zM"], + "Collapse all threads", + function () { window.goDoCommand("cmd_collapseAllThreads"); }); + + mappings.add(myModes, [""], + "Go forward", + function (count) { if (count < 1) count = 1; while (count--) GoNextMessage(nsMsgNavigationType.forward, true); }, + { count: true }); + + mappings.add(myModes, [""], + "Go back", + function (count) { if (count < 1) count = 1; while (count--) GoNextMessage(nsMsgNavigationType.back, true); }, + { count: true }); + + mappings.add(myModes, ["gg"], + "Select first message", + function (count) { if (count < 1) count = 1; while (count--) GoNextMessage(nsMsgNavigationType.firstMessage, true); }, + { count: true }); + + mappings.add(myModes, ["G"], + "Select last message", + function (count) { if (count < 1) count = 1; while (count--) GoNextMessage(nsMsgNavigationType.lastMessage, false); }, + { count: true }); + + // tagging messages + mappings.add(myModes, ["l"], + "Label message", + function (arg) { + if (!GetSelectedMessages()) + return void liberator.beep(); + + switch (arg) { + case "r": MsgMarkMsgAsRead(); break; + case "s": MsgMarkAsFlagged(); break; + case "i": ToggleMessageTagKey(1); break; // Important + case "w": ToggleMessageTagKey(2); break; // Work + case "p": ToggleMessageTagKey(3); break; // Personal + case "t": ToggleMessageTagKey(4); break; // TODO + case "l": ToggleMessageTagKey(5); break; // Later + default: liberator.beep(); + } + }, + { + arg: true + }); + + // TODO: change binding? + mappings.add(myModes, ["T"], + "Mark current folder as read", + function () { + if (mail.currentFolder.isServer) + return liberator.beep(); + + mail.currentFolder.markAllMessagesRead(msgWindow); + }); + + mappings.add(myModes, [""], + "Mark all messages as read", + function () { + mail.getFolders("", false).forEach(function (folder) { folder.markAllMessagesRead(msgWindow); }); + }); + + // DISPLAY OPTIONS + mappings.add(myModes, ["h"], + "Toggle displayed headers", + function () { + let value = gPrefBranch.getIntPref("mail.show_headers", 2); + gPrefBranch.setIntPref("mail.show_headers", value == 2 ? 1 : 2); + ReloadMessage(); + }); + + mappings.add(myModes, ["x"], + "Toggle HTML message display", + function () { + let wantHtml = (gPrefBranch.getIntPref("mailnews.display.html_as", 1) == 1); + mail.setHTML(wantHtml ? 1 : 0); + }); + + // YANKING TEXT + mappings.add(myModes, ["Y"], + "Yank subject", + function () { + try { + let subject = gDBView.hdrForFirstSelectedMessage.mime2DecodedSubject; + util.copyToClipboard(subject, true); + } + catch (e) { liberator.beep(); } + }); + + mappings.add(myModes, ["y"], + "Yank sender or feed URL", + function () { + try { + if (mail.currentAccount.server.type == "rss") + util.copyToClipboard(this._getRSSUrl(), true); + else + util.copyToClipboard(gDBView.hdrForFirstSelectedMessage.mime2DecodedAuthor, true); + } + catch (e) { liberator.beep(); } + }); + + // RSS specific mappings + mappings.add(myModes, ["p"], + "Open RSS message in browser", + function () { + try { + if (mail.currentAccount.server.type == "rss") + messenger.launchExternalURL(this._getRSSUrl()); + // TODO: what to do for non-rss message? + } + catch (e) { + liberator.beep(); + } + }); + }, + options: function () { + // FIXME: why does this default to "Archive", I don't have one? The default + // value won't validate now. mst please fix. --djk + options.add(["archivefolder"], + "Set the archive folder", + "string", "Archive", + { + completer: function (context) completion.mailFolder(context), + validator: Option.validateCompleter + }); + + // TODO: generate the possible values dynamically from the menu + options.add(["layout"], + "Set the layout of the mail window", + "string", "inherit", + { + setter: function (value) { + switch (value) { + case "classic": ChangeMailLayout(0); break; + case "wide": ChangeMailLayout(1); break; + case "vertical": ChangeMailLayout(2); break; + // case "inherit" just does nothing + } + + return value; + }, + completer: function (context) [ + ["inherit", "Default View"], // FIXME: correct description? + ["classic", "Classic View"], + ["wide", "Wide View"], + ["vertical", "Vertical View"] + ], + validator: Option.validateCompleter + }); + + options.add(["smtpserver", "smtp"], + "Set the default SMTP server", + "string", services.get("smtpService").defaultServer.key, // TODO: how should we handle these persistent external defaults - "inherit" or null? + { + getter: function () services.get("smtpService").defaultServer.key, + setter: function (value) { + let server = mail.smtpServers.filter(function (s) s.key == value)[0]; + services.get("smtpService").defaultServer = server; + return value; + }, + completer: function (context) [[s.key, s.serverURI] for ([, s] in Iterator(mail.smtpServers))], + validator: Option.validateCompleter + }); + + /*options.add(["threads"], + "Use threading to group messages", + "boolean", true, + { + setter: function (value) { + if (value) + MsgSortThreaded(); + else + MsgSortUnthreaded(); + + return value; + } + });*/ + }, +}); // vim: set fdm=marker sw=4 ts=4 et: diff --git a/xulmus/content/config.js b/xulmus/content/config.js index 63436dcd..c743ab93 100644 --- a/xulmus/content/config.js +++ b/xulmus/content/config.js @@ -221,18 +221,6 @@ const config = { //{{{ context.completions = displayPanes; // FIXME: useful description etc }; - // load Xulmus specific modules - liberator.loadModule("browser", Browser); - liberator.loadModule("finder", Finder); - liberator.loadModule("bookmarks", Bookmarks); - liberator.loadModule("history", History); - liberator.loadModule("tabs", Tabs); - liberator.loadModule("marks", Marks); - liberator.loadModule("quickmarks", QuickMarks); - liberator.loadModule("hints", Hints); - liberator.loadModule("player", Player); - liberator.loadModule("library", Library); - //////////////////////////////////////////////////////////////////////////////// ////////////////////// STYLES ////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ @@ -251,10 +239,6 @@ const config = { //{{{ delete img; }; - //////////////////////////////////////////////////////////////////////////////// - ////////////////////// MAPPINGS //////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - /////////////////////////////////////////////////////////////////////////////}}} ////////////////////// COMMANDS //////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ diff --git a/xulmus/content/library.js b/xulmus/content/library.js index 0b9ed885..9b633bdb 100644 --- a/xulmus/content/library.js +++ b/xulmus/content/library.js @@ -4,72 +4,55 @@ // given in the LICENSE.txt file included with this file. -function Library() { //{{{ +const Library = Module("library", { + init: function () { + this.MAIN_LIBRARY = LibraryUtils.mainLibrary; + }, - //////////////////////////////////////////////////////////////////////////////// - ////////////////////// PRIVATE SECTION ///////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - const MAIN_LIBRARY = LibraryUtils.mainLibrary; - - function toJSArray(enum) ArrayConverter.JSArray(enum) - - function getArtistsArray() { - return toJSArray(MAIN_LIBRARY.getDistinctValuesForProperty(SBProperties.artistName)); - } - - // Get the artist names before hand. - let artists = getArtistsArray(); - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// PUBLIC SECTION ////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ + _toJSArray: function (enum) ArrayConverter.JSArray(enum), // TODO: return some actually useful objects. ;-) - return { + /** + * Returns an array of all the artist names in the main library. + * + * @returns {string[]} + */ + getArtists: function getArtists() this._toJSArray(this.MAIN_LIBRARY.getDistinctValuesForProperty(SBProperties.artistName)), - /** - * Returns an array of all the artist names in the main library. - * - * @returns {string[]} - */ - getArtists: function getArtists() artists, + // FIXME: ken do we really want to remove duplicates? If so, why not tracks too? --djk + /** + * Returns an array of all the album names for artist in the + * main library. + * + * @param {param} artist The artist name. + * @returns {string[]} + */ + getAlbums: function getAlbums(artist) { + let albums = this._toJSArray(this.MAIN_LIBRARY.getItemsByProperty(SBProperties.artistName, artist)) + .map(function (track) track.getProperty(SBProperties.albumName)); + return util.Array.uniq(albums); + }, - // FIXME: ken do we really want to remove duplicates? If so, why not tracks too? --djk - /** - * Returns an array of all the album names for artist in the - * main library. - * - * @param {param} artist The artist name. - * @returns {string[]} - */ - getAlbums: function getAlbums(artist) { - let albums = toJSArray(MAIN_LIBRARY.getItemsByProperty(SBProperties.artistName, artist)) - .map(function (track) track.getProperty(SBProperties.albumName)); - return util.Array.uniq(albums); - }, + /** + * Returns an array of all the track names for artist and + * album in the main library. + * + * @param {param} artist The artist name. + * @param {param} album The album name. + * @returns {string[]} + */ + getTracks: function getTracks(artist, album) { + const properties = Cc["@songbirdnest.com/Songbird/Properties/MutablePropertyArray;1"] + .createInstance(Ci.sbIMutablePropertyArray); - /** - * Returns an array of all the track names for artist and - * album in the main library. - * - * @param {param} artist The artist name. - * @param {param} album The album name. - * @returns {string[]} - */ - getTracks: function getTracks(artist, album) { - const properties = Cc["@songbirdnest.com/Songbird/Properties/MutablePropertyArray;1"] - .createInstance(Ci.sbIMutablePropertyArray); + properties.appendProperty(SBProperties.artistName, artist); + properties.appendProperty(SBProperties.albumName, album); - properties.appendProperty(SBProperties.artistName, artist); - properties.appendProperty(SBProperties.albumName, album); - - return toJSArray(MAIN_LIBRARY.getItemsByProperties(properties)) - .map(function (track) track.getProperty(SBProperties.trackName)); - } - - }; - //}}} -} //}}} + return this._toJSArray(this.MAIN_LIBRARY.getItemsByProperties(properties)) + .map(function (track) track.getProperty(SBProperties.trackName)); + } +}, { +}, { +}); // vim: set fdm=marker sw=4 ts=4 et: diff --git a/xulmus/content/player.js b/xulmus/content/player.js index dd04ff66..f3ccdb7f 100644 --- a/xulmus/content/player.js +++ b/xulmus/content/player.js @@ -4,38 +4,32 @@ // given in the LICENSE.txt file included with this file. -function Player() { //{{{ +const Player = Module("player", { + init: function () { + this._lastSearchString = ""; + this._lastSearchIndex = 0; + this._lastSearchView = _SBGetCurrentView(); - //////////////////////////////////////////////////////////////////////////////// - ////////////////////// PRIVATE SECTION ///////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ + // Get the focus to the visible playlist first + //window._SBShowMainLibrary(); - let lastSearchString = ""; - let lastSearchIndex = 0; - let lastSearchView = _SBGetCurrentView(); - - // Get the focus to the visible playlist first - //window._SBShowMainLibrary(); - - services.add("mediaPageManager", "@songbirdnest.com/Songbird/MediaPageManager;1", Ci.sbIMediaPageManager); - services.add("propertyManager","@songbirdnest.com/Songbird/Properties/PropertyManager;1", Ci.sbIPropertyManager); - - // Register Callbacks for searching. - commandline.registerCallback("change", modes.SEARCH_VIEW_FORWARD, function (str) { player.onSearchKeyPress(str); }); - commandline.registerCallback("submit", modes.SEARCH_VIEW_FORWARD, function (str) { player.onSearchSubmit(str); }); - commandline.registerCallback("cancel", modes.SEARCH_VIEW_FORWARD, function () { player.onSearchCancel(); }); + gMM.addListener(this._mediaCoreListener); + }, + destroy: function () { + gMM.removeListener(this._mediaCoreListener); + }, // interval (milliseconds) - function seek(interval, direction) { + _seek: function (interval, direction) { let position = gMM.playbackControl ? gMM.playbackControl.position : 0; player.seekTo(position + (direction ? interval : -interval)); - } + }, - function focusTrack(mediaItem) { + _focusTrack: function (mediaItem) { SBGetBrowser().mediaTab.mediaPage.highlightItem(_SBGetCurrentView().getIndexForItem(mediaItem)); - } + }, - var mediaCoreListener = { + _mediaCoreListener: { onMediacoreEvent: function (event) { switch (event.type) { case Ci.sbIMediacoreEvent.BEFORE_TRACK_CHANGE: @@ -71,667 +65,651 @@ function Player() { //{{{ break; } } - }; + }, + // TODO: check bounds and round, 0 - 1 or 0 - 100? + /** + * @property {string} The player volume as a percentage. + */ + get volume() gMM.volumeControl.volume, + set volume(value) { + gMM.volumeControl.volume = value; + }, - gMM.addListener(mediaCoreListener); - liberator.registerObserver("shutdown", function () { - gMM.removeListener(mediaCoreListener); - }); + // FIXME: can't be called from non-media tabs since 840e78 + play: function play() { + // Check if there is any selection in place, else play first item of the visible view. + if (_SBGetCurrentView().selection.count != 0) { + // Play the selection. + gMM.sequencer.playView(_SBGetCurrentView(), _SBGetCurrentView().getIndexForItem(_SBGetCurrentView().selection.currentMediaItem)); + this._focusTrack(gMM.sequencer.currentItem); + } + else { + gMM.sequencer.playView(SBGetBrowser().currentMediaListView, 0); + this._focusTrack(gMM.sequencer.currentItem); + } + }, - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// OPTIONS ///////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ + stop: function stop() { + gMM.sequencer.stop(); + }, - options.add(["repeat"], - "Set the playback repeat mode", - "number", 0, - { - setter: function (value) gMM.sequencer.repeatMode = value, - getter: function () gMM.sequencer.repeatMode, - completer: function (context) [ - ["0", "Repeat none"], - ["1", "Repeat one"], - ["2", "Repeat all"] - ], - validator: Option.validateCompleter - }); + next: function next() { + gSongbirdWindowController.doCommand("cmd_control_next"); + gSongbirdWindowController.doCommand("cmd_find_current_track"); + }, - options.add(["shuffle"], - "Play tracks in shuffled order", - "boolean", false, - { - setter: function (value) value ? gMM.sequencer.mode = gMM.sequencer.MODE_SHUFFLE : - gMM.sequencer.mode = gMM.sequencer.MODE_FORWARD, - getter: function () gMM.sequencer.mode == gMM.sequencer.MODE_SHUFFLE - }); + previous: function previous() { + gSongbirdWindowController.doCommand("cmd_control_previous"); + gSongbirdWindowController.doCommand("cmd_find_current_track"); + }, - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// MAPPINGS //////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ + togglePlayPause: function togglePlayPause() { + gSongbirdWindowController.doCommand("cmd_control_playpause"); + this._focusTrack(gMM.sequencer.currentItem); + }, - mappings.add([modes.PLAYER], - ["x"], "Play track", - function () { player.play(); }); + toggleShuffle: function toggleShuffle() { + if (gMM.sequencer.mode != gMM.sequencer.MODE_SHUFFLE) + gMM.sequencer.mode = gMM.sequencer.MODE_SHUFFLE; + else + gMM.sequencer.mode = gMM.sequencer.MODE_FORWARD; + }, - mappings.add([modes.PLAYER], - ["z"], "Previous track", - function () { player.previous(); }); + // FIXME: not really toggling - good enough for now. + toggleRepeat: function toggleRepeat() { + switch (gMM.sequencer.repeatMode) { + case gMM.sequencer.MODE_REPEAT_NONE: + gMM.sequencer.repeatMode = gMM.sequencer.MODE_REPEAT_ONE; + break; + case gMM.sequencer.MODE_REPEAT_ONE: + gMM.sequencer.repeatMode = gMM.sequencer.MODE_REPEAT_ALL; + break; + case gMM.sequencer.MODE_REPEAT_ALL: + gMM.sequencer.repeatMode = gMM.sequencer.MODE_REPEAT_NONE; + break; + default: + gMM.sequencer.repeatMode = gMM.sequencer.MODE_REPEAT_NONE; + break; + } + }, - mappings.add([modes.PLAYER], - ["c"], "Pause/unpause track", - function () { player.togglePlayPause(); }); + /** + * Seek forward interval milliseconds in the currently playing + * track. + * + * @param {number} interval The time interval (ms) to advance the + * current track. + */ + seekForward: function seekForward(interval) { + this._seek(interval, true); + }, - mappings.add([modes.PLAYER], - ["b"], "Next track", - function () { player.next(); }); + /** + * Seek backwards interval milliseconds in the currently + * playing track. + * + * @param {number} interval The time interval (ms) to rewind the + * current track. + */ + seekBackward: function seekBackward(interval) { + this._seek(interval, false); + }, - mappings.add([modes.PLAYER], - ["v"], "Stop track", - function () { player.stop(); }); + /** + * Seek to a specific position in the currently playing track. + * + * @param {number} The new position (ms) in the track. + */ + seekTo: function seekTo(position) { + // FIXME: if not playing + if (!gMM.playbackControl) + this.play(); - mappings.add([modes.PLAYER], - ["Q"], "Queue tracks by artist/album/track", - function () { commandline.open(":", "queue ", modes.EX); }); + let min = 0; + let max = gMM.playbackControl.duration - 5000; // TODO: 5s buffer like cmus desirable? - mappings.add([modes.PLAYER], - ["f"], "Loads current view filtered by the keywords", - function () { commandline.open(":", "filter ", modes.EX); }); + gMM.playbackControl.position = util.Math.constrain(position, min, max); + }, - mappings.add([modes.PLAYER], - ["i"], "Select current track", - function () { gSongbirdWindowController.doCommand("cmd_find_current_track"); }); + // FIXME: 10% ? + // I think just general increments of say 0.05 might be better --djk + increaseVolume: function increaseVolume() { + gMM.volumeControl.volume = gMM.volumeControl.volume * 1.1; + }, - mappings.add([modes.PLAYER], - ["s"], "Toggle shuffle", - function () { player.toggleShuffle(); }); + decreaseVolume: function decreaseVolume() { + if (gMM.volumeControl.volume == 0) + gMM.volumeControl.volume = 0.1; + else + gMM.volumeControl.volume = gMM.volumeControl.volume * 0.9; + }, - mappings.add([modes.PLAYER], - ["r"], "Toggle repeat", - function () { player.toggleRepeat(); }); + focusPlayingTrack :function focusPlayingTrack() { + this._focusTrack(gMM.sequencer.currentItem); + }, - mappings.add([modes.PLAYER], - ["h", ""], "Seek -10s", - function (count) { player.seekBackward(Math.max(1, count) * 10000); }, - { count: true }); + listTracks: function listTracks(view) { + //let myView = LibraryUtils.createStandardMediaListView(LibraryUtils.mainLibrary, args); + let length = view.length; + let tracksList = []; - mappings.add([modes.PLAYER], - ["l", ""], "Seek +10s", - function (count) { player.seekForward(Math.max(1, count) * 10000); }, - { count: true }); + for (let i = 0; i < length; i++) { + let mediaItem = view.getItemByIndex(i); + let trackName = mediaItem.getProperty(SBProperties.trackName); + let albumName = mediaItem.getProperty(SBProperties.albumName); + let artistName = mediaItem.getProperty(SBProperties.artistName); - mappings.add([modes.PLAYER], - ["H", ""], "Seek -1m", - function (count) { player.seekBackward(Math.max(1, count) * 60000); }, - { count: true }); + tracksList[i] = [trackName, "Album : " + albumName + " Artist : " + artistName]; + } - mappings.add([modes.PLAYER], - ["L", ""], "Seek +1m", - function (count) { player.seekForward(Math.max(1, count) * 60000); }, - { count: true }); + return tracksList; + }, - mappings.add([modes.PLAYER], - ["=", "+"], "Increase volume by 10%", - function () { player.increaseVolume(); }); + searchView: function searchView(args) { + let currentView = _SBGetCurrentView(); + let mediaItemList = currentView.mediaList; + let search = _getSearchString(currentView); + let searchString = ""; - mappings.add([modes.PLAYER], - ["-"], "Decrease volume by 10%", - function () { player.decreaseVolume(); }); + if (search != "") + searchString = args + " " + search; + else + searchString = args; - mappings.add([modes.PLAYER], - ["/"], "Search forward for a track", - function (args) { commandline.open("/", "", modes.SEARCH_VIEW_FORWARD); }); + this._lastSearchString = searchString; - mappings.add([modes.PLAYER], - ["n"], "Find the next track", - function () { player.searchViewAgain(false);}); + let mySearchView = LibraryUtils.createStandardMediaListView(mediaItemList, searchString); - mappings.add([modes.PLAYER], - ["N"], "Find the previous track", - function () { player.searchViewAgain(true);}); + if (mySearchView.length) { + this._lastSearchView = mySearchView; + this._lastSearchIndex = 0; + this._focusTrack(mySearchView.getItemByIndex(this._lastSearchIndex)); + } + else + liberator.echoerr("E486 Pattern not found: " + searchString, commandline.FORCE_SINGLELINE); + }, - for (let i in util.range(0, 6)) { - let (rating = i) { - mappings.add([modes.PLAYER], - [""], "Rate the current media item " + rating, - function () { player.rateMediaItem(rating); }); + searchViewAgain: function searchViewAgain(reverse) { + function echo(str) { + setTimeout(function () { + commandline.echo(str, commandline.HL_WARNINGMSG, commandline.APPEND_TO_MESSAGES | commandline.FORCE_SINGLELINE); + }, 0); + } + + if (reverse) { + if (this._lastSearchIndex == 0) { + this._lastSearchIndex = this._lastSearchView.length - 1; + echo("Search hit TOP, continuing at BOTTOM"); + } + else + this._lastSearchIndex = this._lastSearchIndex - 1; + } + else { + if (this._lastSearchIndex == (this._lastSearchView.length - 1)) { + this._lastSearchIndex = 0; + echo("Search hit BOTTOM, continuing at TOP"); + } + else + this._lastSearchIndex = this._lastSearchIndex + 1; + } + + // FIXME: Implement for "?" --ken + commandline.echo("/" + this._lastSearchString, null, commandline.FORCE_SINGLELINE); + this._focusTrack(this._lastSearchView.getItemByIndex(this._lastSearchIndex)); + + }, + + /** + * The search dialog keypress callback. + * + * @param {string} str The contents of the search dialog. + */ + onSearchKeyPress: function (str) { + if (options["incsearch"]) + this.searchView(str); + }, + + /** + * The search dialog submit callback. + * + * @param {string} str The contents of the search dialog. + */ + onSearchSubmit: function (str) { + this.searchView(str); + }, + + /** + * The search dialog cancel callback. + */ + onSearchCancel: function () { + // TODO: restore the view state if altered by an 'incsearch' search + }, + + getPlaylists: function getPlaylists() { + let mainLibrary = LibraryUtils.mainLibrary; + let playlists = [mainLibrary]; + let listener = { + onEnumerationBegin: function () { }, + onEnumerationEnd: function () { }, + onEnumeratedItem: function (list, item) { + // FIXME: why are there null items and duplicates? + if (!playlists.some(function (list) list.name == item.name) && item.name != null) + playlists.push(item); + return Ci.sbIMediaListEnumerationListener.CONTINUE; + } }; + + mainLibrary.enumerateItemsByProperty("http://songbirdnest.com/data/1.0#isList", "1", listener); + + return playlists; + }, + + // Play track at 'row' in 'playlist' + playPlaylist: function playPlaylist(playlist, row) { + gMM.sequencer.playView(playlist.createView(), row); + }, + + getMediaPages: function getMediaPages() { + let list = gBrowser.currentMediaPage.mediaListView.mediaList; + let pages = services.get("mediaPageManager").getAvailablePages(list); + return ArrayConverter.JSArray(pages).map(function (page) page.QueryInterface(Ci.sbIMediaPageInfo)); + }, + + loadMediaPage: function loadMediaList(page, list, view) { + services.get("mediaPageManager").setPage(list, page); + gBrowser.loadMediaList(list, null, null, view, null); + }, + + rateMediaItem: function rateMediaItem(rating) { + if (gMM.sequencer.currentItem) + gMM.sequencer.currentItem.setProperty(SBProperties.rating, rating); + }, + + getUserViewable: function getUserViewable() { + let propManager = services.get("propertyManager"); + let propEnumerator = propManager.propertyIDs; + let properties = []; + + while (propEnumerator.hasMore()) { + let propertyId = propEnumerator.getNext(); + + if (propManager.getPropertyInfo(propertyId).userViewable) { + //liberator.dump("propertyId - " + propManager.getPropertyInfo(propertyId).id); + properties.push(propManager.getPropertyInfo(propertyId).displayName); + } + } + + return properties; + }, + + sortBy: function sortBy(property, order) { + let pa = Cc["@songbirdnest.com/Songbird/Properties/MutablePropertyArray;1"].createInstance(Ci.sbIMutablePropertyArray); + liberator.dump("Property: " + property); + + switch (property.string) { + case "#": + case "Title": + pa.appendProperty(SBProperties.trackName, "a"); + break; + case "Rating": + pa.appendProperty(SBProperties.rating, 1); + break; + case "Album": + pa.appendProperty(SBProperties.albumName, "a"); + break; + default: + pa.appendProperty(SBProperties.trackName, "a"); + break; + } + + _SBGetCurrentView().setSort(pa); } +}, { +}, { + commandline: function () { + commandline.registerCallback("change", modes.SEARCH_VIEW_FORWARD, function (str) { player.onSearchKeyPress(str); }); + commandline.registerCallback("submit", modes.SEARCH_VIEW_FORWARD, function (str) { player.onSearchSubmit(str); }); + commandline.registerCallback("cancel", modes.SEARCH_VIEW_FORWARD, function () { player.onSearchCancel(); }); + }, + commands: function () { + commands.add(["f[ilter]"], + "Filter tracks based on keywords {genre/artist/album/track}", + function (args) { + let library = LibraryUtils.mainLibrary; + let view = LibraryUtils.createStandardMediaListView(LibraryUtils.mainLibrary, args.literalArg); - ////////////////// ///////////////////////////////////////////////////////////}}} - ////////////////////// COMMANDS //////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ + if (view.length == 0) + liberator.echoerr("No Tracks matching the keywords"); + else { + SBGetBrowser().loadMediaList(LibraryUtils.mainLibrary, null, null, view, + "chrome://songbird/content/mediapages/filtersPage.xul"); + // TODO: make this this._focusTrack work ? + this._focusTrack(view.getItemByIndex(0)); + } + }, + { + argCount: "1", + literal: 0 + //completer: function (context, args) completion.tracks(context, args); + }); - commands.add(["f[ilter]"], - "Filter tracks based on keywords {genre/artist/album/track}", + commands.add(["load"], + "Load a playlist", function (args) { - let library = LibraryUtils.mainLibrary; - let view = LibraryUtils.createStandardMediaListView(LibraryUtils.mainLibrary, args.literalArg); + let arg = args.literalArg; - if (view.length == 0) - liberator.echoerr("No Tracks matching the keywords"); + if (arg) { + // load the selected playlist/smart playlist + let playlists = player.getPlaylists(); + + for ([i, list] in Iterator(playlists)) { + if (util.compareIgnoreCase(arg, list.name) == 0) { + SBGetBrowser().loadMediaList(playlists[i]); + this._focusTrack(_SBGetCurrentView().getItemByIndex(0)); + return; + } + } + + liberator.echoerr("E475: Invalid argument: " + arg); + } else { - SBGetBrowser().loadMediaList(LibraryUtils.mainLibrary, null, null, view, - "chrome://songbird/content/mediapages/filtersPage.xul"); - // TODO: make this focusTrack work ? - focusTrack(view.getItemByIndex(0)); + // load main library if there are no args + _SBShowMainLibrary(); + } + }, + { + argCount: "?", + completer: function (context, args) completion.playlist(context, args), + literal: 0 + }); + + // TODO: better off as a single command (:player play) or cmus compatible (:player-play)? --djk + commands.add(["playerp[lay]"], + "Play track", + function () { player.play(); }); + + commands.add(["playerpa[use]"], + "Pause/unpause track", + function () { player.togglePlayPause(); }); + + commands.add(["playern[ext]"], + "Play next track", + function () { player.next(); }); + + commands.add(["playerpr[ev]"], + "Play previous track", + function () { player.previous(); }); + + commands.add(["players[top]"], + "Stop track", + function () { player.stop(); }); + + commands.add(["see[k]"], + "Seek to a track position", + function (args) { + let arg = args[0]; + + // intentionally supports 999:99:99 + if (!/^[+-]?(\d+[smh]?|(\d+:\d\d:|\d+:)?\d{2})$/.test(arg)) + return void liberator.echoerr("E475: Invalid argument: " + arg); + + function ms(t, m) Math.abs(parseInt(t, 10) * { s: 1000, m: 60000, h: 3600000 }[m]) + + if (/:/.test(arg)) { + let [seconds, minutes, hours] = arg.split(":").reverse(); + hours = hours || 0; + var value = ms(seconds, "s") + ms(minutes, "m") + ms(hours, "h"); + } + else { + if (!/[smh]/.test(arg.substr(-1))) + arg += "s"; // default to seconds + + value = ms(arg.substring(arg, arg.length - 1), arg.substr(-1)); + } + + if (/^[-+]/.test(arg)) + arg[0] == "-" ? player.seekBackward(value) : player.seekForward(value); + else + player.seekTo(value); + + }, + { argCount: "1" }); + + commands.add(["mediav[iew]"], + "Change the current media view", + function (args) { + // FIXME: is this a SB restriction? --djk + if (!gBrowser.currentMediaPage) + return void liberator.echoerr("Exxx: Can only set the media view from the media tab"); // XXX + + let arg = args[0]; + + if (arg) { + let pages = player.getMediaPages(); + + for ([, page] in Iterator(pages)) { + if (util.compareIgnoreCase(arg, page.contentTitle) == 0) { + player.loadMediaPage(page, gBrowser.currentMediaListView.mediaList, gBrowser.currentMediaListView); + return; + } + } + + liberator.echoerr("E475: Invalid argument: " + arg); } }, { argCount: "1", + completer: function (context) completion.mediaView(context), literal: 0 - //completer: function (context, args) completion.tracks(context, args); }); - commands.add(["load"], - "Load a playlist", - function (args) { - let arg = args.literalArg; + commands.add(["sort[view]"], + "Sort the current media view", + function (args) { + player.sortBy(args, true); - if (arg) { - // load the selected playlist/smart playlist - let playlists = player.getPlaylists(); + }); - for ([i, list] in Iterator(playlists)) { - if (util.compareIgnoreCase(arg, list.name) == 0) { - SBGetBrowser().loadMediaList(playlists[i]); - focusTrack(_SBGetCurrentView().getItemByIndex(0)); - return; - } - } - - liberator.echoerr("E475: Invalid argument: " + arg); - } - else { - // load main library if there are no args - _SBShowMainLibrary(); - } - }, - { - argCount: "?", - completer: function (context, args) completion.playlist(context, args), - literal: 0 - }); - - // TODO: better off as a single command (:player play) or cmus compatible (:player-play)? --djk - commands.add(["playerp[lay]"], - "Play track", - function () { player.play(); }); - - commands.add(["playerpa[use]"], - "Pause/unpause track", - function () { player.togglePlayPause(); }); - - commands.add(["playern[ext]"], - "Play next track", - function () { player.next(); }); - - commands.add(["playerpr[ev]"], - "Play previous track", - function () { player.previous(); }); - - commands.add(["players[top]"], - "Stop track", - function () { player.stop(); }); - - commands.add(["see[k]"], - "Seek to a track position", - function (args) { - let arg = args[0]; - - // intentionally supports 999:99:99 - if (!/^[+-]?(\d+[smh]?|(\d+:\d\d:|\d+:)?\d{2})$/.test(arg)) - return void liberator.echoerr("E475: Invalid argument: " + arg); - - function ms(t, m) Math.abs(parseInt(t, 10) * { s: 1000, m: 60000, h: 3600000 }[m]) - - if (/:/.test(arg)) { - let [seconds, minutes, hours] = arg.split(":").reverse(); - hours = hours || 0; - var value = ms(seconds, "s") + ms(minutes, "m") + ms(hours, "h"); - } - else { - if (!/[smh]/.test(arg.substr(-1))) - arg += "s"; // default to seconds - - value = ms(arg.substring(arg, arg.length - 1), arg.substr(-1)); - } - - if (/^[-+]/.test(arg)) - arg[0] == "-" ? player.seekBackward(value) : player.seekForward(value); - else - player.seekTo(value); - - }, - { argCount: "1" }); - - commands.add(["mediav[iew]"], - "Change the current media view", - function (args) { - // FIXME: is this a SB restriction? --djk - if (!gBrowser.currentMediaPage) - return void liberator.echoerr("Exxx: Can only set the media view from the media tab"); // XXX - - let arg = args[0]; - - if (arg) { - let pages = player.getMediaPages(); - - for ([, page] in Iterator(pages)) { - if (util.compareIgnoreCase(arg, page.contentTitle) == 0) { - player.loadMediaPage(page, gBrowser.currentMediaListView.mediaList, gBrowser.currentMediaListView); - return; - } - } - - liberator.echoerr("E475: Invalid argument: " + arg); - } - }, - { - argCount: "1", - completer: function (context) completion.mediaView(context), - literal: 0 - }); - - commands.add(["sort[view]"], - "Sort the current media view", + // FIXME: use :add -q like cmus? (not very vim-like are it's multi-option commands) --djk + commands.add(["qu[eue]"], + "Queue tracks by artist/album/track", function (args) { - player.sortBy(args, true); + // Store the old view + // let prev_view = gMM.status.view; + let library = LibraryUtils.mainLibrary; + let mainView = library.createView(); + let customProps = Cc["@songbirdnest.com/Songbird/Properties/MutablePropertyArray;1"] + .createInstance(Ci.sbIMutablePropertyArray); + // args + switch (args.length) { + case 3: + customProps.appendProperty(SBProperties.trackName, args[2]); + case 2: + customProps.appendProperty(SBProperties.albumName, args[1]); + case 1: + customProps.appendProperty(SBProperties.artistName, args[0]); + break; + default: + break; + } + + gMM.sequencer.playView(mainView, mainView.getIndexForItem(library.getItemsByProperties(customProps).queryElementAt(0, Ci.sbIMediaItem))); + player.focusPlayingTrack(); + }, + { + argCount: "+", + completer: function (context, args) completion.song(context, args) }); - // FIXME: use :add -q like cmus? (not very vim-like are it's multi-option commands) --djk - commands.add(["qu[eue]"], - "Queue tracks by artist/album/track", - function (args) { - // Store the old view - // let prev_view = gMM.status.view; - let library = LibraryUtils.mainLibrary; - let mainView = library.createView(); - let customProps = Cc["@songbirdnest.com/Songbird/Properties/MutablePropertyArray;1"] - .createInstance(Ci.sbIMutablePropertyArray); + // TODO: maybe :vol! could toggle mute on/off? --djk + commands.add(["vol[ume]"], + "Set the volume", + function (args) { + let arg = args[0]; - // args - switch (args.length) { - case 3: - customProps.appendProperty(SBProperties.trackName, args[2]); - case 2: - customProps.appendProperty(SBProperties.albumName, args[1]); - case 1: - customProps.appendProperty(SBProperties.artistName, args[0]); - break; - default: - break; + if (!/^[+-]?\d+$/.test(arg)) + return void liberator.echoerr("E488: Trailing characters"); + + let level = parseInt(arg, 10) / 100; + + if (/^[+-]/.test(arg)) + level = player.volume + level; + + player.volume = util.Math.constrain(level, 0, 1); + }, + { argCount: "1" }); + }, + completions: function () { + completion.song = function song(context, args) { + // TODO: useful descriptions? + function map(list) list.map(function (i) [i, ""]); + let [artist, album] = [args[0], args[1]]; + + if (args.completeArg == 0) { + context.title = ["Artists"]; + context.completions = map(library.getArtists()); } - - gMM.sequencer.playView(mainView, mainView.getIndexForItem(library.getItemsByProperties(customProps).queryElementAt(0, Ci.sbIMediaItem))); - player.focusPlayingTrack(); - }, - { - argCount: "+", - completer: function (context, args) completion.song(context, args) - }); - - // TODO: maybe :vol! could toggle mute on/off? --djk - commands.add(["vol[ume]"], - "Set the volume", - function (args) { - let arg = args[0]; - - if (!/^[+-]?\d+$/.test(arg)) - return void liberator.echoerr("E488: Trailing characters"); - - let level = parseInt(arg, 10) / 100; - - if (/^[+-]/.test(arg)) - level = player.volume + level; - - player.volume = util.Math.constrain(level, 0, 1); - }, - { argCount: "1" }); - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// COMPLETIONS ///////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - completion.song = function song(context, args) { - // TODO: useful descriptions? - function map(list) list.map(function (i) [i, ""]); - let [artist, album] = [args[0], args[1]]; - - if (args.completeArg == 0) { - context.title = ["Artists"]; - context.completions = map(library.getArtists()); - } - else if (args.completeArg == 1) { - context.title = ["Albums by " + artist]; - context.completions = map(library.getAlbums(artist)); - } - else if (args.completeArg == 2) { - context.title = ["Tracks from " + album + " by " + artist]; - context.completions = map(library.getTracks(artist, album)); - } - }; - - completion.playlist = function playlist(context, args) { - context.title = ["Playlist", "Type"]; - context.keys = { text: "name", description: "type" }; - context.completions = player.getPlaylists(); - }; - - completion.mediaView = function mediaView(context) { - context.title = ["Media View", "URL"]; - context.anchored = false; - context.keys = { text: "contentTitle", description: "contentUrl" }; - context.completions = player.getMediaPages(); - }; - - /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// PUBLIC SECTION ////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////{{{ - - return { - - // TODO: check bounds and round, 0 - 1 or 0 - 100? - /** - * @property {string} The player volume as a percentage. - */ - get volume() gMM.volumeControl.volume, - set volume(value) { - gMM.volumeControl.volume = value; - }, - - // FIXME: can't be called from non-media tabs since 840e78 - play: function play() { - // Check if there is any selection in place, else play first item of the visible view. - if (_SBGetCurrentView().selection.count != 0) { - // Play the selection. - gMM.sequencer.playView(_SBGetCurrentView(), _SBGetCurrentView().getIndexForItem(_SBGetCurrentView().selection.currentMediaItem)); - focusTrack(gMM.sequencer.currentItem); + else if (args.completeArg == 1) { + context.title = ["Albums by " + artist]; + context.completions = map(library.getAlbums(artist)); } - else { - gMM.sequencer.playView(SBGetBrowser().currentMediaListView, 0); - focusTrack(gMM.sequencer.currentItem); + else if (args.completeArg == 2) { + context.title = ["Tracks from " + album + " by " + artist]; + context.completions = map(library.getTracks(artist, album)); } - }, + }; - stop: function stop() { - gMM.sequencer.stop(); - }, + completion.playlist = function playlist(context, args) { + context.title = ["Playlist", "Type"]; + context.keys = { text: "name", description: "type" }; + context.completions = player.getPlaylists(); + }; - next: function next() { - gSongbirdWindowController.doCommand("cmd_control_next"); - gSongbirdWindowController.doCommand("cmd_find_current_track"); - }, + completion.mediaView = function mediaView(context) { + context.title = ["Media View", "URL"]; + context.anchored = false; + context.keys = { text: "contentTitle", description: "contentUrl" }; + context.completions = player.getMediaPages(); + }; + }, + mappings: function () { + mappings.add([modes.PLAYER], + ["x"], "Play track", + function () { player.play(); }); - previous: function previous() { - gSongbirdWindowController.doCommand("cmd_control_previous"); - gSongbirdWindowController.doCommand("cmd_find_current_track"); - }, + mappings.add([modes.PLAYER], + ["z"], "Previous track", + function () { player.previous(); }); - togglePlayPause: function togglePlayPause() { - gSongbirdWindowController.doCommand("cmd_control_playpause"); - focusTrack(gMM.sequencer.currentItem); - }, + mappings.add([modes.PLAYER], + ["c"], "Pause/unpause track", + function () { player.togglePlayPause(); }); - toggleShuffle: function toggleShuffle() { - if (gMM.sequencer.mode != gMM.sequencer.MODE_SHUFFLE) - gMM.sequencer.mode = gMM.sequencer.MODE_SHUFFLE; - else - gMM.sequencer.mode = gMM.sequencer.MODE_FORWARD; - }, + mappings.add([modes.PLAYER], + ["b"], "Next track", + function () { player.next(); }); - // FIXME: not really toggling - good enough for now. - toggleRepeat: function toggleRepeat() { - switch (gMM.sequencer.repeatMode) { - case gMM.sequencer.MODE_REPEAT_NONE: - gMM.sequencer.repeatMode = gMM.sequencer.MODE_REPEAT_ONE; - break; - case gMM.sequencer.MODE_REPEAT_ONE: - gMM.sequencer.repeatMode = gMM.sequencer.MODE_REPEAT_ALL; - break; - case gMM.sequencer.MODE_REPEAT_ALL: - gMM.sequencer.repeatMode = gMM.sequencer.MODE_REPEAT_NONE; - break; - default: - gMM.sequencer.repeatMode = gMM.sequencer.MODE_REPEAT_NONE; - break; - } - }, + mappings.add([modes.PLAYER], + ["v"], "Stop track", + function () { player.stop(); }); - /** - * Seek forward interval milliseconds in the currently playing - * track. - * - * @param {number} interval The time interval (ms) to advance the - * current track. - */ - seekForward: function seekForward(interval) { - seek(interval, true); - }, + mappings.add([modes.PLAYER], + ["Q"], "Queue tracks by artist/album/track", + function () { commandline.open(":", "queue ", modes.EX); }); - /** - * Seek backwards interval milliseconds in the currently - * playing track. - * - * @param {number} interval The time interval (ms) to rewind the - * current track. - */ - seekBackward: function seekBackward(interval) { - seek(interval, false); - }, + mappings.add([modes.PLAYER], + ["f"], "Loads current view filtered by the keywords", + function () { commandline.open(":", "filter ", modes.EX); }); - /** - * Seek to a specific position in the currently playing track. - * - * @param {number} The new position (ms) in the track. - */ - seekTo: function seekTo(position) { - // FIXME: if not playing - if (!gMM.playbackControl) - this.play(); + mappings.add([modes.PLAYER], + ["i"], "Select current track", + function () { gSongbirdWindowController.doCommand("cmd_find_current_track"); }); - let min = 0; - let max = gMM.playbackControl.duration - 5000; // TODO: 5s buffer like cmus desirable? + mappings.add([modes.PLAYER], + ["s"], "Toggle shuffle", + function () { player.toggleShuffle(); }); - gMM.playbackControl.position = util.Math.constrain(position, min, max); - }, + mappings.add([modes.PLAYER], + ["r"], "Toggle repeat", + function () { player.toggleRepeat(); }); - // FIXME: 10% ? - // I think just general increments of say 0.05 might be better --djk - increaseVolume: function increaseVolume() { - gMM.volumeControl.volume = gMM.volumeControl.volume * 1.1; - }, + mappings.add([modes.PLAYER], + ["h", ""], "Seek -10s", + function (count) { player.seekBackward(Math.max(1, count) * 10000); }, + { count: true }); - decreaseVolume: function decreaseVolume() { - if (gMM.volumeControl.volume == 0) - gMM.volumeControl.volume = 0.1; - else - gMM.volumeControl.volume = gMM.volumeControl.volume * 0.9; - }, + mappings.add([modes.PLAYER], + ["l", ""], "Seek +10s", + function (count) { player.seekForward(Math.max(1, count) * 10000); }, + { count: true }); - focusPlayingTrack :function focusPlayingTrack() { - focusTrack(gMM.sequencer.currentItem); - }, + mappings.add([modes.PLAYER], + ["H", ""], "Seek -1m", + function (count) { player.seekBackward(Math.max(1, count) * 60000); }, + { count: true }); - listTracks: function listTracks(view) { - //let myView = LibraryUtils.createStandardMediaListView(LibraryUtils.mainLibrary, args); - let length = view.length; - let tracksList = []; + mappings.add([modes.PLAYER], + ["L", ""], "Seek +1m", + function (count) { player.seekForward(Math.max(1, count) * 60000); }, + { count: true }); - for (let i = 0; i < length; i++) { - let mediaItem = view.getItemByIndex(i); - let trackName = mediaItem.getProperty(SBProperties.trackName); - let albumName = mediaItem.getProperty(SBProperties.albumName); - let artistName = mediaItem.getProperty(SBProperties.artistName); + mappings.add([modes.PLAYER], + ["=", "+"], "Increase volume by 10%", + function () { player.increaseVolume(); }); - tracksList[i] = [trackName, "Album : " + albumName + " Artist : " + artistName]; - } + mappings.add([modes.PLAYER], + ["-"], "Decrease volume by 10%", + function () { player.decreaseVolume(); }); - return tracksList; - }, + mappings.add([modes.PLAYER], + ["/"], "Search forward for a track", + function (args) { commandline.open("/", "", modes.SEARCH_VIEW_FORWARD); }); - searchView: function searchView(args) { - let currentView = _SBGetCurrentView(); - let mediaItemList = currentView.mediaList; - let search = _getSearchString(currentView); - let searchString = ""; + mappings.add([modes.PLAYER], + ["n"], "Find the next track", + function () { player.searchViewAgain(false);}); - if (search != "") - searchString = args + " " + search; - else - searchString = args; + mappings.add([modes.PLAYER], + ["N"], "Find the previous track", + function () { player.searchViewAgain(true);}); - lastSearchString = searchString; - - let mySearchView = LibraryUtils.createStandardMediaListView(mediaItemList, searchString); - - if (mySearchView.length) { - lastSearchView = mySearchView; - lastSearchIndex = 0; - focusTrack(mySearchView.getItemByIndex(lastSearchIndex)); - } - else - liberator.echoerr("E486 Pattern not found: " + searchString, commandline.FORCE_SINGLELINE); - }, - - searchViewAgain: function searchViewAgain(reverse) { - function echo(str) { - setTimeout(function () { - commandline.echo(str, commandline.HL_WARNINGMSG, commandline.APPEND_TO_MESSAGES | commandline.FORCE_SINGLELINE); - }, 0); - } - - if (reverse) { - if (lastSearchIndex == 0) { - lastSearchIndex = lastSearchView.length - 1; - echo("Search hit TOP, continuing at BOTTOM"); - } - else - lastSearchIndex = lastSearchIndex - 1; - } - else { - if (lastSearchIndex == (lastSearchView.length - 1)) { - lastSearchIndex = 0; - echo("Search hit BOTTOM, continuing at TOP"); - } - else - lastSearchIndex = lastSearchIndex + 1; - } - - // FIXME: Implement for "?" --ken - commandline.echo("/" + lastSearchString, null, commandline.FORCE_SINGLELINE); - focusTrack(lastSearchView.getItemByIndex(lastSearchIndex)); - - }, - - /** - * The search dialog keypress callback. - * - * @param {string} str The contents of the search dialog. - */ - onSearchKeyPress: function (str) { - if (options["incsearch"]) - this.searchView(str); - }, - - /** - * The search dialog submit callback. - * - * @param {string} str The contents of the search dialog. - */ - onSearchSubmit: function (str) { - this.searchView(str); - }, - - /** - * The search dialog cancel callback. - */ - onSearchCancel: function () { - // TODO: restore the view state if altered by an 'incsearch' search - }, - - getPlaylists: function getPlaylists() { - let mainLibrary = LibraryUtils.mainLibrary; - let playlists = [mainLibrary]; - let listener = { - onEnumerationBegin: function () { }, - onEnumerationEnd: function () { }, - onEnumeratedItem: function (list, item) { - // FIXME: why are there null items and duplicates? - if (!playlists.some(function (list) list.name == item.name) && item.name != null) - playlists.push(item); - return Ci.sbIMediaListEnumerationListener.CONTINUE; - } + for (let i in util.range(0, 6)) { + let (rating = i) { + mappings.add([modes.PLAYER], + [""], "Rate the current media item " + rating, + function () { player.rateMediaItem(rating); }); }; - - mainLibrary.enumerateItemsByProperty("http://songbirdnest.com/data/1.0#isList", "1", listener); - - return playlists; - }, - - // Play track at 'row' in 'playlist' - playPlaylist: function playPlaylist(playlist, row) { - gMM.sequencer.playView(playlist.createView(), row); - }, - - getMediaPages: function getMediaPages() { - let list = gBrowser.currentMediaPage.mediaListView.mediaList; - let pages = services.get("mediaPageManager").getAvailablePages(list); - return ArrayConverter.JSArray(pages).map(function (page) page.QueryInterface(Ci.sbIMediaPageInfo)); - }, - - loadMediaPage: function loadMediaList(page, list, view) { - services.get("mediaPageManager").setPage(list, page); - gBrowser.loadMediaList(list, null, null, view, null); - }, - - rateMediaItem: function rateMediaItem(rating) { - if (gMM.sequencer.currentItem) - gMM.sequencer.currentItem.setProperty(SBProperties.rating, rating); - }, - - getUserViewable: function getUserViewable() { - let propManager = services.get("propertyManager"); - let propEnumerator = propManager.propertyIDs; - let properties = []; - - while (propEnumerator.hasMore()) { - let propertyId = propEnumerator.getNext(); - - if (propManager.getPropertyInfo(propertyId).userViewable) { - //liberator.dump("propertyId - " + propManager.getPropertyInfo(propertyId).id); - properties.push(propManager.getPropertyInfo(propertyId).displayName); - } - } - - return properties; - }, - - sortBy: function sortBy(property, order) { - let pa = Cc["@songbirdnest.com/Songbird/Properties/MutablePropertyArray;1"].createInstance(Ci.sbIMutablePropertyArray); - liberator.dump("Property: " + property); - - switch (property.string) { - case "#": - case "Title": - pa.appendProperty(SBProperties.trackName, "a"); - break; - case "Rating": - pa.appendProperty(SBProperties.rating, 1); - break; - case "Album": - pa.appendProperty(SBProperties.albumName, "a"); - break; - default: - pa.appendProperty(SBProperties.trackName, "a"); - break; - } - - _SBGetCurrentView().setSort(pa); } + }, + options: function () { + options.add(["repeat"], + "Set the playback repeat mode", + "number", 0, + { + setter: function (value) gMM.sequencer.repeatMode = value, + getter: function () gMM.sequencer.repeatMode, + completer: function (context) [ + ["0", "Repeat none"], + ["1", "Repeat one"], + ["2", "Repeat all"] + ], + validator: Option.validateCompleter + }); - }; - //}}} -} //}}} + options.add(["shuffle"], + "Play tracks in shuffled order", + "boolean", false, + { + setter: function (value) value ? gMM.sequencer.mode = gMM.sequencer.MODE_SHUFFLE : + gMM.sequencer.mode = gMM.sequencer.MODE_FORWARD, + getter: function () gMM.sequencer.mode == gMM.sequencer.MODE_SHUFFLE + }); + }, + services: function () { + services.add("mediaPageManager", "@songbirdnest.com/Songbird/MediaPageManager;1", Ci.sbIMediaPageManager); + services.add("propertyManager","@songbirdnest.com/Songbird/Properties/PropertyManager;1", Ci.sbIPropertyManager); + }, +}); // vim: set fdm=marker sw=4 ts=4 et: