From f23e9321f1f1a73b0456a5fa2e9ee5ba62fd3af8 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Mon, 24 Jan 2011 06:00:48 -0500 Subject: [PATCH 1/7] Only purge all domain data when explicitly asked rather than when history is specified. --- common/locale/en-US/various.xml | 17 +++++++++-------- common/modules/sanitizer.jsm | 29 ++++++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/common/locale/en-US/various.xml b/common/locale/en-US/various.xml index c66f85d6..da02cd66 100644 --- a/common/locale/en-US/various.xml +++ b/common/locale/en-US/various.xml @@ -198,6 +198,7 @@
downloads
Download history
formdata
Saved form and search history
history
Browsing history
+
host
All data from the given host
marks
Local and URL marks
macros
Saved macros
messages
Saved :messages
@@ -225,27 +226,27 @@ The following items are always cleared entirely, regardless of - timeframe: cache, offlineapps, + timeframe: cache, host, offlineapps, passwords, sessions, sitesettings. - Conversely, options are never cleared unless a host is - specified. + Conversely, host and options are never cleared + unless a host is specified.

If host (short name -h) is specified, only items containing a reference to that domain or a subdomain thereof are - cleared. Moreover, if commandline or history is - specified, the invocation of the :sanitize command is - naturally cleared as well. + cleared. Moreover, if either of commandline or + history is specified, the invocation of the + :sanitize command is naturally cleared as well.

This only applies to commandline, cookies, history, marks, messages, options, and sitesettings. All other - domain-specific data is cleared only along with history, + domain-specific data is cleared only along with host, when a request is made to &dactyl.host; to purge all data for - host. Included in this purge are all matching history + host. Included in this purge are all matching history entries, cookies, closed tabs, form data, and location bar entries. diff --git a/common/modules/sanitizer.jsm b/common/modules/sanitizer.jsm index b4cd74f6..47f402e8 100644 --- a/common/modules/sanitizer.jsm +++ b/common/modules/sanitizer.jsm @@ -78,7 +78,6 @@ var Sanitizer = Module("sanitizer", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakRef this.addItem("cache", { builtin: true, description: "Cache" }); this.addItem("downloads", { builtin: true, description: "Download history" }); this.addItem("formdata", { builtin: true, description: "Saved form and search history" }); - this.addItem("history", { builtin: true, description: "Browsing history", sessionHistory: true }); this.addItem("offlineapps", { builtin: true, description: "Offline website data" }); this.addItem("passwords", { builtin: true, description: "Saved passwords" }); this.addItem("sessions", { builtin: true, description: "Authenticated sessions" }); @@ -96,6 +95,32 @@ var Sanitizer = Module("sanitizer", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakRef }, override: true }); + this.addItem("history", { + builtin: true, + description: "Browsing history", + persistent: true, + sessionHistory: true, + action: function (range, host) { + if (host) + services.history.removePagesFromHost(host, true); + else + services.history.removeVisitsByTimeframe(this.range.min, this.range.max); + + if (!host) + services.observer.notifyObservers(null, "browser:purge-session-history", ""); + + if (!host || util.isDomainURL(prefs.get("general.open_location.last_url"), host)) + prefs.reset("general.open_location.last_url"); + }, + override: true + }); + this.addItem("host", { + description: "All data from the given host", + action: function (range, host) { + if (host) + services.privateBrowsing.removeDataFromDomain(host); + } + }); this.addItem("sitesettings", { builtin: true, description: "Site preferences", @@ -398,8 +423,6 @@ var Sanitizer = Module("sanitizer", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakRef if (args["-host"]) { args["-host"].forEach(function (host) { sanitizer.sanitizing = true; - if (items.indexOf("history") > -1) - services.privateBrowsing.removeDataFromDomain(host); sanitizer.sanitizeItems(items, range, host) }); } From fb4aafc21e1057e0614e6c2058dcdf5bbddcf4b2 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Mon, 24 Jan 2011 03:56:27 -0500 Subject: [PATCH 2/7] Update :extensions with live update and buttons like :downloads. Rename to :addons. --HG-- extra : transplant_source : %CD%8A%D4L%5D%DD%5E6A%1A%02Gm%22%28%0E3%A6%B4%85 --- common/content/dactyl.js | 257 ----------------- common/locale/en-US/gui.xml | 19 +- common/modules/addons.jsm | 515 +++++++++++++++++++++++++++++++++++ common/modules/config.jsm | 17 ++ common/modules/downloads.jsm | 5 +- common/modules/overlay.jsm | 3 +- common/modules/services.jsm | 80 +----- pentadactyl/NEWS | 1 + 8 files changed, 539 insertions(+), 358 deletions(-) create mode 100644 common/modules/addons.jsm diff --git a/common/content/dactyl.js b/common/content/dactyl.js index a8922422..8c65f810 100644 --- a/common/content/dactyl.js +++ b/common/content/dactyl.js @@ -1527,14 +1527,6 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), { }, commands: function () { - commands.add(["addo[ns]"], - "Manage available Extensions and Themes", - function () { - dactyl.open("chrome://mozapps/content/extensions/extensions.xul", - { from: "addons" }); - }, - { argCount: "0" }); - commands.add(["dia[log]"], "Open a " + config.appName + " dialog", function (args) { @@ -1592,255 +1584,6 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), { literal: 0 }); - /////////////////////////////////////////////////////////////////////////// - - const addonErrors = array.toObject([ - [AddonManager.ERROR_NETWORK_FAILURE, "A network error occurred"], - [AddonManager.ERROR_INCORRECT_HASH, "The downloaded file did not match the expected hash"], - [AddonManager.ERROR_CORRUPT_FILE, "The file appears to be corrupt"], - [AddonManager.ERROR_FILE_ACCESS, "There was an error accessing the filesystem"]]); - - function listener(action, event) - function addonListener(install) { - if (typeof dactyl !== "undefined") - dactyl[install.error ? "echoerr" : "echomsg"]( - "Add-on " + action + " " + event + ": " + (install.name || install.sourceURI.spec) + - (install.error ? ": " + addonErrors[install.error] : "")); - } - const addonListener = { - onNewInstall: function (install) {}, - onExternalInstall: function (addon, existingAddon, needsRestart) {}, - onDownloadStarted: listener("download", "started"), - onDownloadEnded: listener("download", "complete"), - onDownloadCancelled: listener("download", "cancelled"), - onDownloadFailed: listener("download", "failed"), - onDownloadProgress: function (install) {}, - onInstallStarted: function (install) {}, - onInstallEnded: listener("installation", "complete"), - onInstallCancelled: listener("installation", "cancelled"), - onInstallFailed: listener("installation", "failed") - }; - - const updateAddons = Class("UpgradeListener", { - init: function init(addons) { - dactyl.assert(!addons.length || addons[0].findUpdates, - "Not available on " + config.host + " " + services.runtime.version); - this.remaining = addons; - this.upgrade = []; - dactyl.echomsg("Checking updates for addons: " + addons.map(function (a) a.name).join(", ")); - for (let addon in values(addons)) - addon.findUpdates(this, AddonManager.UPDATE_WHEN_USER_REQUESTED, null, null); - }, - addonListener: { - __proto__: addonListener, - onDownloadStarted: function () {}, - onDownloadEnded: function () {} - }, - onUpdateAvailable: function (addon, install) { - this.upgrade.push(addon); - install.addListener(this.addonListener); - install.install(); - }, - onUpdateFinished: function (addon, error) { - this.remaining = this.remaining.filter(function (a) a != addon); - if (!this.remaining.length) - dactyl.echomsg( - this.upgrade.length - ? "Installing updates for addons: " + this.upgrade.map(function (i) i.name).join(", ") - : "No addon updates found"); - } - }); - - /////////////////////////////////////////////////////////////////////////// - - function callResult(method) { - let args = Array.slice(arguments, 1); - return function (result) { result[method].apply(result, args); }; - } - - commands.add(["exta[dd]"], - "Install an extension", - function (args) { - let url = args[0]; - let file = io.File(url); - function install(addonInstall) { - addonInstall.addListener(addonListener); - addonInstall.install(); - } - - if (!file.exists()) - AddonManager.getInstallForURL(url, install, "application/x-xpinstall"); - else if (file.isReadable() && file.isFile()) - AddonManager.getInstallForFile(file, install, "application/x-xpinstall"); - else if (file.isDirectory()) - dactyl.echoerr("Cannot install a directory: " + file.path.quote()); - else - dactyl.echoerr("E484: Can't open file " + file.path); - }, { - argCount: "1", - completer: function (context) { - context.filters.push(function ({ item }) item.isDirectory() || /\.xpi$/.test(item.leafName)); - completion.file(context); - }, - literal: 0 - }); - - // TODO: handle extension dependencies - [ - { - name: "extde[lete]", - description: "Uninstall an extension", - action: callResult("uninstall"), - perm: "uninstall" - }, - { - name: "exte[nable]", - description: "Enable an extension", - action: function (addon) addon.userDisabled = false, - filter: function ({ item }) item.userDisabled, - perm: "enable" - }, - { - name: "extd[isable]", - description: "Disable an extension", - action: function (addon) addon.userDisabled = true, - filter: function ({ item }) !item.userDisabled, - perm: "disable" - }, - { - name: "extr[ehash]", - description: "Reload an extension", - action: function (addon) { - dactyl.assert(dactyl.has("Gecko2"), "This command is not useful in this version of " + config.host); - util.timeout(function () { - addon.userDisabled = true; - addon.userDisabled = false; - }); - }, - get filter() { - let ids = set(keys(JSON.parse(prefs.get("extensions.bootstrappedAddons", "{}")))); - return function ({ item }) !item.userDisabled && set.has(ids, item.id); - }, - perm: "disable" - }, - { - name: "extt[oggle]", - description: "Toggle an extension's enabled status", - action: function (addon) addon.userDisabled = !addon.userDisabled - }, - { - name: "extu[pdate]", - description: "Update an extension", - actions: updateAddons, - perm: "upgrade" - } - ].forEach(function (command) { - let perm = command.perm && AddonManager["PERM_CAN_" + command.perm.toUpperCase()]; - function ok(addon) !perm || addon.permissions & perm; - commands.add([command.name], - command.description, - function (args) { - let name = args[0]; - if (args.bang) - dactyl.assert(!name, "E488: Trailing characters"); - else - dactyl.assert(name, "E471: Argument required"); - - AddonManager.getAddonsByTypes(["extension"], dactyl.wrapCallback(function (list) { - if (!args.bang) { - list = list.filter(function (extension) extension.name == name); - if (list.length == 0) - return void dactyl.echoerr("E475: Invalid argument: " + name); - if (!list.every(ok)) - return void dactyl.echoerr("Permission denied"); - } - if (command.actions) - command.actions(list); - else - list.forEach(command.action); - })); - }, { - argCount: "?", // FIXME: should be "1" - bang: true, - completer: function (context) { - completion.extension(context); - context.filters.push(function ({ item }) ok(item)); - if (command.filter) - context.filters.push(command.filter); - }, - literal: 0 - }); - }); - - commands.add(["exto[ptions]", "extp[references]"], - "Open an extension's preference dialog", - function (args) { - AddonManager.getAddonsByTypes(["extension"], dactyl.wrapCallback(function (list) { - list = list.filter(function (extension) extension.name == args[0]); - if (!list.length || !list[0].optionsURL) - dactyl.echoerr("E474: Invalid argument"); - else if (args.bang) - window.openDialog(list[0].optionsURL, "_blank", "chrome"); - else - dactyl.open(list[0].optionsURL, { from: "extoptions" }); - })); - }, { - argCount: "1", - bang: true, - completer: function (context) { - completion.extension(context); - context.filters.push(function ({ item }) item.isActive && item.optionsURL); - }, - literal: 0 - }); - - commands.add(["extens[ions]", "exts"], - "List available extensions", - function (args) { - function addonExtra(e) { - let extra; - if (e.pendingOperations & AddonManager.PENDING_UNINSTALL) - extra = ["Disabled", "uninstalled"]; - else if (e.pendingOperations & AddonManager.PENDING_DISABLE) - extra = ["Disabled", "disabled"]; - else if (e.pendingOperations & AddonManager.PENDING_INSTALL) - extra = ["Enabled", "installed"]; - else if (e.pendingOperations & AddonManager.PENDING_ENABLE) - extra = ["Enabled", "enabled"]; - else if (e.pendingOperations & AddonManager.PENDING_UPGRADE) - extra = ["Enabled", "upgraded"]; - if (extra) - return <> ({extra[1]} -  on restart); - return <>; - } - let waiting = true; - AddonManager.getAddonsByTypes(["extension"], function (extensions) { - if (args[0]) - extensions = extensions.filter(function (extension) extension.name.indexOf(args[0]) >= 0); - extensions.sort(function (a, b) String.localeCompare(a.name, b.name)); - - if (extensions.length > 0) - commandline.commandOutput( - template.tabular(["Name", "Version", "Status", "Description"], [], - ([template.icon({ icon: e.iconURL }, e.name), - e.version, - (e.isActive ? enabled - : disabled) + - addonExtra(e), - e.description] - for ([, e] in Iterator(extensions))))); - else if (filter) - dactyl.echoerr("Exxx: No extension matching " + filter.quote()); - else - dactyl.echoerr("No extensions installed"); - waiting = false; - }); - if (commandline.savingOutput) - util.waitFor(function () !waiting); - }, - { argCount: "?" }); - [ { name: "h[elp]", diff --git a/common/locale/en-US/gui.xml b/common/locale/en-US/gui.xml index fa1daf5d..e7054858 100644 --- a/common/locale/en-US/gui.xml +++ b/common/locale/en-US/gui.xml @@ -41,16 +41,11 @@

Dialogs

- :addo :addons + :ao :addo :addons :addons -

- Opens the &dactyl.host; addon manager, where extensions and themes - may be installed, removed, disabled, and configured. See also - :extensions, :extadd, :extoptions, - :extenable, :extdisable, and :extdelete. -

+

Opens the add-on list.

@@ -129,16 +124,6 @@ - - :exts :extens :extensions - :extensions - :exts - - -

List all installed extensions.

-
-
- :exto :extoptions :extoptions! extension diff --git a/common/modules/addons.jsm b/common/modules/addons.jsm new file mode 100644 index 00000000..4964d938 --- /dev/null +++ b/common/modules/addons.jsm @@ -0,0 +1,515 @@ +// Copyright (c) 2011 by Kris Maglione +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. +"use strict"; + +try { + +Components.utils.import("resource://dactyl/bootstrap.jsm"); +defineModule("addons", { + exports: ["AddonManager", "Addons", "Addon", "addons"], + require: ["services"], + use: ["config", "io", "prefs", "template", "util"] +}, this); + +var callResult = function callResult(method) { + let args = Array.slice(arguments, 1); + return function (result) { result[method].apply(result, args); }; +} + +var listener = function listener(action, event) + function addonListener(install) { + this.dactyl[install.error ? "echoerr" : "echomsg"]( + "Add-on " + action + " " + event + ": " + (install.name || install.sourceURI.spec) + + (install.error ? ": " + addonErrors[install.error] : "")); + } + +var AddonListener = Class("AddonListener", { + init: function init(modules) { + this.dactyl = modules.dactyl; + }, + + onNewInstall: function (install) {}, + onExternalInstall: function (addon, existingAddon, needsRestart) {}, + onDownloadStarted: listener("download", "started"), + onDownloadEnded: listener("download", "complete"), + onDownloadCancelled: listener("download", "cancelled"), + onDownloadFailed: listener("download", "failed"), + onDownloadProgress: function (install) {}, + onInstallStarted: function (install) {}, + onInstallEnded: listener("installation", "complete"), + onInstallCancelled: listener("installation", "cancelled"), + onInstallFailed: listener("installation", "failed") +}); + +var updateAddons = Class("UpgradeListener", AddonListener, { + init: function init(addons, modules) { + init.supercall(this, modules); + + util.assert(!addons.length || addons[0].findUpdates, + "Not available on " + config.host + " " + services.runtime.version); + + this.remaining = addons; + this.upgrade = []; + this.dactyl.echomsg("Checking updates for addons: " + addons.map(function (a) a.name).join(", ")); + for (let addon in values(addons)) + addon.findUpdates(this, AddonManager.UPDATE_WHEN_USER_REQUESTED, null, null); + + }, + onUpdateAvailable: function (addon, install) { + this.upgrade.push(addon); + install.addListener(this); + install.install(); + }, + onUpdateFinished: function (addon, error) { + this.remaining = this.remaining.filter(function (a) a != addon); + if (!this.remaining.length) + this.dactyl.echomsg( + this.upgrade.length + ? "Installing updates for addons: " + this.upgrade.map(function (i) i.name).join(", ") + : "No addon updates found"); + } +}); + +var actions = { + delete: { + name: "extde[lete]", + description: "Uninstall an extension", + action: callResult("uninstall"), + perm: "uninstall" + }, + enable: { + name: "exte[nable]", + description: "Enable an extension", + action: function (addon) addon.userDisabled = false, + filter: function ({ item }) item.userDisabled, + perm: "enable" + }, + disable: { + name: "extd[isable]", + description: "Disable an extension", + action: function (addon) addon.userDisabled = true, + filter: function ({ item }) !item.userDisabled, + perm: "disable" + }, + rehash: { + name: "extr[ehash]", + description: "Reload an extension", + action: function (addon) { + dactyl.assert(dactyl.has("Gecko2"), "This command is not useful in this version of " + config.host); + util.timeout(function () { + addon.userDisabled = true; + addon.userDisabled = false; + }); + }, + get filter() { + let ids = set(keys(JSON.parse(prefs.get("extensions.bootstrappedAddons", "{}")))); + return function ({ item }) !item.userDisabled && set.has(ids, item.id); + }, + perm: "disable" + }, + toggle: { + name: "extt[oggle]", + description: "Toggle an extension's enabled status", + action: function (addon) addon.userDisabled = !addon.userDisabled + }, + update: { + name: "extu[pdate]", + description: "Update an extension", + actions: updateAddons, + perm: "upgrade" + } +}; + + +var Addon = Class("Addon", { + init: function init(addon, list) { + this.addon = addon; + this.instance = this; + this.list = list; + + this.nodes = { + commandTarget: this + }; + util.xmlToDom( +
  • + + + + + + On  + Off + Del + Upd + + +
  • , + this.list.document, this.nodes); + + this.update(); + }, + + commandAllowed: function commandAllowed(cmd) { + util.assert(set.has(actions, cmd), "Unknown command"); + + let action = actions[cmd]; + if ("perm" in action && !(this.permissions & AddonManager["PERM_CAN_" + action.perm.toUpperCase()])) + return false; + if ("filter" in action && !action.filter({ item: this })) + return false; + return true; + }, + + command: function command(cmd) { + util.assert(this.commandAllowed(cmd), "Command not allowed"); + + let action = actions[cmd]; + if (action.action) + action.action.call(this.modules, this); + else + action.actions([this], this.list.modules); + }, + + compare: function compare(other) String.localeCompare(this.name, other.name), + + get statusInfo() { + let info = this.isActive ? enabled + : disabled; + + let pending; + if (this.pendingOperations & AddonManager.PENDING_UNINSTALL) + pending = ["Disabled", "uninstalled"]; + else if (this.pendingOperations & AddonManager.PENDING_DISABLE) + pending = ["Disabled", "disabled"]; + else if (this.pendingOperations & AddonManager.PENDING_INSTALL) + pending = ["Enabled", "installed"]; + else if (this.pendingOperations & AddonManager.PENDING_ENABLE) + pending = ["Enabled", "enabled"]; + else if (this.pendingOperations & AddonManager.PENDING_UPGRADE) + pending = ["Enabled", "upgraded"]; + if (pending) + return <>{info} ({pending[1]} +  on restart); + return info; + }, + + update: function callee() { + let self = this; + function update(key, xml) { + let node = self.nodes[key]; + while (node.firstChild) + node.removeChild(node.firstChild); + node.appendChild(util.xmlToDom(<>{xml}, self.list.document)); + } + + update("name", template.icon({ icon: this.iconURL }, this.name)); + this.nodes.version.textContent = this.version; + update("status", this.statusInfo); + this.nodes.description.textContent = this.description; + + for (let node in values(this.nodes)) + if (node.update && node.update !== callee) + node.update(); + } +}); + +["cancelUninstall", "findUpdates", "getResourceURI", "hasResource", + "isCompatibleWith", "uninstall"].forEach(function (prop) { + Addon.prototype[prop] = function proxy() this.addon[prop].apply(this.addon, arguments); +}); + +["aboutURL", "appDisabled", "applyBackgroundUpdates", "blocklistState", + "contributors", "creator", "description", "developers", "homepageURL", + "iconURL", "id", "install", "installDate", "isActive", "isCompatible", + "isPlatformCompatible", "name", "operationsRequiringRestart", + "optionsURL", "pendingOperations", "pendingUpgrade", "permissions", + "providesUpdatesSecurely", "releaseNotesURI", "scope", "screenshots", + "size", "sourceURI", "translators", "type", "updateDate", + "userDisabled", "version"].forEach(function (prop) { + Object.defineProperty(Addon.prototype, prop, { + get: function get_proxy() this.addon[prop], + set: function set_proxy(val) this.addon[prop] = val + }); +}); + +var AddonList = Class("AddonList", { + init: function init(modules, types) { + this.modules = modules; + this.nodes = {}; + this.addons = []; + this.ready = false; + + AddonManager.getAddonsByTypes(types, this.closure(function (addons) { + addons.forEach(this.closure.addAddon); + this.ready = true; + this.update(); + })); + AddonManager.addAddonListener(this); + }, + cleanup: function cleanup() { + AddonManager.removeAddonListener(this); + }, + + message: Class.memoize(function () { + + util.xmlToDom(
      +
    • + Name + Version + Status + + Description +
    • +
    , this.document, this.nodes); + + return this.nodes.list; + }), + + addAddon: function addAddon(addon) { + if (addon.id in this.addons) + this.update(addon); + else { + addon = Addon(addon, this); + this.addons[addon.id] = addon; + + let index = values(this.addons).sort(function (a, b) a.compare(b)) + .indexOf(addon); + + this.nodes.list.insertBefore(addon.nodes.row, + this.nodes.list.childNodes[index + 1]); + this.update(); + } + }, + removeAddon: function removeAddon(addon) { + if (addon.id in this.addons) { + this.nodes.list.removeChild(this.addons[addon.id].nodes.row); + delete this.addons[addon.id]; + this.update(); + } + }, + + leave: function leave(stack) { + if (stack.pop) + this.cleanup(); + }, + + update: function update(addon) { + if (addon && addon.id in this.addons) + this.addons[addon.id].update(); + if (this.ready) + this.modules.commandline.updateOutputHeight(false); + }, + + onDisabled: function (addon) { this.update(addon); }, + onDisabling: function (addon) { this.update(addon); }, + onEnabled: function (addon) { this.update(addon); }, + onEnabling: function (addon) { this.update(addon); }, + onInstalled: function (addon) { this.addAddon(addon); }, + onInstalling: function (addon) { this.update(addon); }, + onUninstalled: function (addon) { this.removeAddon(addon); }, + onUninstalling: function (addon) { this.update(addon); }, + onOperationCancelled: function (addon) { this.update(addon); }, + onPropertyChanged: function onPropertyChanged(addon, properties) {} +}); + +var Addons = Module("addons", { +}, { +}, { + commands: function (dactyl, modules, window) { + const { commands, completion } = modules; + + let addonListener = AddonListener(modules); + + commands.add(["exta[dd]"], + "Install an extension", + function (args) { + let url = args[0]; + let file = io.File(url); + function install(addonInstall) { + addonInstall.addListener(addonListener); + addonInstall.install(); + } + + if (!file.exists()) + AddonManager.getInstallForURL(url, install, "application/x-xpinstall"); + else if (file.isReadable() && file.isFile()) + AddonManager.getInstallForFile(file, install, "application/x-xpinstall"); + else if (file.isDirectory()) + dactyl.echoerr("Cannot install a directory: " + file.path.quote()); + else + dactyl.echoerr("E484: Can't open file " + file.path); + }, { + argCount: "1", + completer: function (context) { + context.filters.push(function ({ item }) item.isDirectory() || /\.xpi$/.test(item.leafName)); + completion.file(context); + }, + literal: 0 + }); + + // TODO: handle extension dependencies + values(actions).forEach(function (command) { + let perm = command.perm && AddonManager["PERM_CAN_" + command.perm.toUpperCase()]; + function ok(addon) !perm || addon.permissions & perm; + + commands.add([command.name], + command.description, + function (args) { + let name = args[0]; + if (args.bang) + dactyl.assert(!name, "E488: Trailing characters"); + else + dactyl.assert(name, "E471: Argument required"); + + AddonManager.getAddonsByTypes(["extension"], dactyl.wrapCallback(function (list) { + if (!args.bang) { + list = list.filter(function (extension) extension.name == name); + if (list.length == 0) + return void dactyl.echoerr("E475: Invalid argument: " + name); + if (!list.every(ok)) + return void dactyl.echoerr("Permission denied"); + } + if (command.actions) + command.actions(list, this.modules); + else + list.forEach(command.action, this.modules); + })); + }, { + argCount: "?", // FIXME: should be "1" + bang: true, + completer: function (context) { + completion.extension(context); + context.filters.push(function ({ item }) ok(item)); + if (command.filter) + context.filters.push(command.filter); + }, + literal: 0 + }); + }); + + commands.add(["exto[ptions]", "extp[references]"], + "Open an extension's preference dialog", + function (args) { + AddonManager.getAddonsByTypes(["extension"], dactyl.wrapCallback(function (list) { + list = list.filter(function (extension) extension.name == args[0]); + if (!list.length || !list[0].optionsURL) + dactyl.echoerr("E474: Invalid argument"); + else if (args.bang) + window.openDialog(list[0].optionsURL, "_blank", "chrome"); + else + dactyl.open(list[0].optionsURL, { from: "extoptions" }); + })); + }, { + argCount: "1", + bang: true, + completer: function (context) { + completion.extension(context); + context.filters.push(function ({ item }) item.isActive && item.optionsURL); + }, + literal: 0 + }); + + commands.add(["addo[ns]", "ao"], + "List installed extensions", + function (args) { + let addons = AddonList(modules, ["extension"]); + modules.commandline.echo(addons); + + if (modules.commandline.savingOutput) + util.waitFor(function () addons.ready); + }); + } +}); + +if (!Ci.nsIExtensionManager || !services.extensionManager) + Components.utils.import("resource://gre/modules/AddonManager.jsm"); +else + var AddonManager = { + getAddonByID: function (id, callback) { + callback = callback || util.identity; + let addon = id; + if (!isObject(addon)) + addon = services.extensionManager.getItemForID(id); + if (!addon) + return callback(null); + addon = Object.create(addon); + + function getRdfProperty(item, property) { + let resource = services.rdf.GetResource("urn:mozilla:item:" + item.id); + let value = ""; + + if (resource) { + let target = services.extensionManager.datasource.GetTarget(resource, + services.rdf.GetResource("http://www.mozilla.org/2004/em-rdf#" + property), true); + if (target && target instanceof Ci.nsIRDFLiteral) + value = target.Value; + } + + return value; + } + + ["aboutURL", "creator", "description", "developers", + "homepageURL", "installDate", "optionsURL", + "releaseNotesURI", "updateDate"].forEach(function (item) { + memoize(addon, item, function (item) getRdfProperty(this, item)); + }); + + update(addon, { + + appDisabled: false, + + installLocation: Class.memoize(function () services.extensionManager.getInstallLocation(this.id)), + getResourceURI: function getResourceURI(path) { + let file = this.installLocation.getItemFile(this.id, path); + return services.io.newFileURI(file); + }, + + isActive: getRdfProperty(addon, "isDisabled") != "true", + + uninstall: function uninstall() { + services.extensionManager.uninstallItem(this.id); + }, + + get userDisabled() getRdfProperty(addon, "userDisabled") === "true", + set userDisabled(val) { + services.extensionManager[val ? "disableItem" : "enableItem"](this.id); + } + }); + + return callback(addon); + }, + getAddonsByTypes: function (types, callback) { + let res = []; + for (let [, type] in Iterator(types)) + for (let [, item] in Iterator(services.extensionManager + .getItemList(Ci.nsIUpdateItem["TYPE_" + type.toUpperCase()], {}))) + res.push(this.getAddonByID(item)); + return (callback || util.identity)(res); + }, + getInstallForFile: function (file, callback, mimetype) { + callback({ + addListener: function () {}, + install: function () { + services.extensionManager.installItemFromFile(file, "app-profile"); + } + }); + }, + getInstallForURL: function (url, callback, mimetype) { + dactyl.assert(false, "Install by URL not implemented"); + }, + }; + +var addonErrors = array.toObject([ + [AddonManager.ERROR_NETWORK_FAILURE, "A network error occurred"], + [AddonManager.ERROR_INCORRECT_HASH, "The downloaded file did not match the expected hash"], + [AddonManager.ERROR_CORRUPT_FILE, "The file appears to be corrupt"], + [AddonManager.ERROR_FILE_ACCESS, "There was an error accessing the filesystem"]]); + + +endModule(); + +} catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); } + +// vim: set fdm=marker sw=4 ts=4 et ft=javascript: diff --git a/common/modules/config.jsm b/common/modules/config.jsm index c0ff190b..9b13b7d7 100644 --- a/common/modules/config.jsm +++ b/common/modules/config.jsm @@ -506,6 +506,8 @@ var ConfigBase = Class("ConfigBase", { Download display: table-row; Download:not([active]) color: gray; + Buttons + DownloadCell display: table-cell; padding: 0 1ex; DownloadButtons;;;DownloadCell DownloadPercent;;;DownloadCell @@ -517,6 +519,21 @@ var ConfigBase = Class("ConfigBase", { DownloadTime;;;DownloadCell DownloadTitle;;;DownloadCell,URL + AddonCell display: table-cell; padding: 0 1ex; + + Addons display: table; margin: 0; padding: 0; + AddonHead;;;CompTitle display: table-row; + AddonHead>*;;;AddonCell + + Addon display: table-row; + + Addon>*;;;AddonCell + AddonButtons + AddonDescription + AddonName + AddonStatus + AddonVersion + // ]]>, / /g, "\n")), diff --git a/common/modules/downloads.jsm b/common/modules/downloads.jsm index aff156fe..138efaac 100644 --- a/common/modules/downloads.jsm +++ b/common/modules/downloads.jsm @@ -36,7 +36,7 @@ var Download = Class("Download", { - + Pause Remove Resume @@ -285,9 +285,6 @@ var Downloads = Module("downloads", { function (args) { let downloads = DownloadList(modules); modules.commandline.echo(downloads); - - if (modules.commandline.savingOutput) - util.waitFor(function () downloads.document); }); }, dactyl: function (dactyl, modules, window) { diff --git a/common/modules/overlay.jsm b/common/modules/overlay.jsm index 391553c8..72eb99a9 100644 --- a/common/modules/overlay.jsm +++ b/common/modules/overlay.jsm @@ -148,7 +148,8 @@ var Overlay = Module("Overlay", { let prefix = [BASE, "resource://dactyl-local-content/"]; defineModule.time("load", null, function _load() { - ["base", + ["addons", + "base", "completion", "config", "downloads", diff --git a/common/modules/services.jsm b/common/modules/services.jsm index 71e002e1..48629933 100644 --- a/common/modules/services.jsm +++ b/common/modules/services.jsm @@ -9,7 +9,7 @@ try { var global = this; Components.utils.import("resource://dactyl/bootstrap.jsm"); defineModule("services", { - exports: ["AddonManager", "services"], + exports: ["services"], use: ["util"] }, this); @@ -86,84 +86,6 @@ var Services = Module("Services", { this.addClass("Xmlhttp", "@mozilla.org/xmlextras/xmlhttprequest;1", Ci.nsIXMLHttpRequest); this.addClass("ZipReader", "@mozilla.org/libjar/zip-reader;1", Ci.nsIZipReader, "open"); this.addClass("ZipWriter", "@mozilla.org/zipwriter;1", Ci.nsIZipWriter); - - if (!Ci.nsIExtensionManager || !this.extensionManager) - Components.utils.import("resource://gre/modules/AddonManager.jsm"); - else - global.AddonManager = { - getAddonByID: function (id, callback) { - callback = callback || util.identity; - let addon = id; - if (!isObject(addon)) - addon = services.extensionManager.getItemForID(id); - if (!addon) - return callback(null); - addon = Object.create(addon); - - function getRdfProperty(item, property) { - let resource = services.rdf.GetResource("urn:mozilla:item:" + item.id); - let value = ""; - - if (resource) { - let target = services.extensionManager.datasource.GetTarget(resource, - services.rdf.GetResource("http://www.mozilla.org/2004/em-rdf#" + property), true); - if (target && target instanceof Ci.nsIRDFLiteral) - value = target.Value; - } - - return value; - } - - ["aboutURL", "creator", "description", "developers", - "homepageURL", "installDate", "optionsURL", - "releaseNotesURI", "updateDate"].forEach(function (item) { - memoize(addon, item, function (item) getRdfProperty(this, item)); - }); - - update(addon, { - - appDisabled: false, - - installLocation: Class.memoize(function () services.extensionManager.getInstallLocation(this.id)), - getResourceURI: function getResourceURI(path) { - let file = this.installLocation.getItemFile(this.id, path); - return services.io.newFileURI(file); - }, - - isActive: getRdfProperty(addon, "isDisabled") != "true", - - uninstall: function uninstall() { - services.extensionManager.uninstallItem(this.id); - }, - - get userDisabled() getRdfProperty(addon, "userDisabled") === "true", - set userDisabled(val) { - services.extensionManager[val ? "disableItem" : "enableItem"](this.id); - } - }); - - return callback(addon); - }, - getAddonsByTypes: function (types, callback) { - let res = []; - for (let [, type] in Iterator(types)) - for (let [, item] in Iterator(services.extensionManager - .getItemList(Ci.nsIUpdateItem["TYPE_" + type.toUpperCase()], {}))) - res.push(this.getAddonByID(item)); - return (callback || util.identity)(res); - }, - getInstallForFile: function (file, callback, mimetype) { - callback({ - addListener: function () {}, - install: function () { - services.extensionManager.installItemFromFile(file, "app-profile"); - } - }); - }, - getInstallForURL: function (url, callback, mimetype) { - dactyl.assert(false, "Install by URL not implemented"); - }, - }; }, reinit: function () {}, diff --git a/pentadactyl/NEWS b/pentadactyl/NEWS index 578f3d62..abf2e334 100644 --- a/pentadactyl/NEWS +++ b/pentadactyl/NEWS @@ -66,6 +66,7 @@ and linking to source code locations). - :downloads now opens a download list in the multi-line output buffer. + - :extensions has been replaced with a more powerful :addons - Added :cookies command. * :extadd now supports remote URLs as well as local files on Firefox 4. * Added :if/:elseif/:else/:endif conditionals. From 74b671048468c290772895ccd678bc8775484410 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Mon, 24 Jan 2011 05:05:57 -0500 Subject: [PATCH 3/7] Add filtering to :addons and :downloads. Add -type flag to :addons. --HG-- extra : transplant_source : %06i65%11-C%E7b%12%C0%8E%8C%8Ed%C9%ED8I%13 --- common/content/commands.js | 8 +++++-- common/modules/addons.jsm | 44 +++++++++++++++++++++++++++++++---- common/modules/base.jsm | 4 ++-- common/modules/completion.jsm | 7 ++++++ common/modules/config.jsm | 4 +++- common/modules/downloads.jsm | 17 ++++++++++---- 6 files changed, 70 insertions(+), 14 deletions(-) diff --git a/common/content/commands.js b/common/content/commands.js index 173b59e3..44164529 100644 --- a/common/content/commands.js +++ b/common/content/commands.js @@ -916,7 +916,7 @@ var Commands = Module("commands", { } } - if (arg != null || opt.type == CommandOption.NOARG) + if (arg != null || opt.type == CommandOption.NOARG) { // option allowed multiple times if (opt.multiple) args[opt.names[0]] = (args[opt.names[0]] || []).concat(arg); @@ -924,6 +924,7 @@ var Commands = Module("commands", { Class.replaceProperty(args, opt.names[0], opt.type == CommandOption.NOARG || arg); args.explicitOpts[opt.names[0]] = args[opt.names[0]]; + } i += optname.length + count; if (i == str.length) @@ -989,14 +990,17 @@ var Commands = Module("commands", { if (args.completeOpt) { let opt = args.completeOpt; let context = complete.fork(opt.names[0], args.completeStart); - let arg = args[opt.names[0]]; + let arg = args.explicitOpts[opt.names[0]]; context.filter = args.completeFilter; + if (isArray(arg)) context.filters.push(function (item) arg.indexOf(item.text) === -1); + if (typeof opt.completer == "function") var compl = opt.completer(context, args); else compl = opt.completer || []; + context.title = [opt.names[0]]; context.quote = args.quote; if (compl) diff --git a/common/modules/addons.jsm b/common/modules/addons.jsm index 4964d938..a1bd8272 100644 --- a/common/modules/addons.jsm +++ b/common/modules/addons.jsm @@ -10,7 +10,7 @@ Components.utils.import("resource://dactyl/bootstrap.jsm"); defineModule("addons", { exports: ["AddonManager", "Addons", "Addon", "addons"], require: ["services"], - use: ["config", "io", "prefs", "template", "util"] + use: ["completion", "config", "io", "prefs", "template", "util"] }, this); var callResult = function callResult(method) { @@ -235,8 +235,9 @@ var Addon = Class("Addon", { }); var AddonList = Class("AddonList", { - init: function init(modules, types) { + init: function init(modules, types, filter) { this.modules = modules; + this.filter = filter && filter.toLowerCase(); this.nodes = {}; this.addons = []; this.ready = false; @@ -271,6 +272,9 @@ var AddonList = Class("AddonList", { if (addon.id in this.addons) this.update(addon); else { + if (this.filter && addon.name.toLowerCase().indexOf(this.filter) === -1) + return; + addon = Addon(addon, this); this.addons[addon.id] = addon; @@ -318,7 +322,7 @@ var Addons = Module("addons", { }, { }, { commands: function (dactyl, modules, window) { - const { commands, completion } = modules; + const { CommandOption, commands, completion } = modules; let addonListener = AddonListener(modules); @@ -414,12 +418,44 @@ var Addons = Module("addons", { commands.add(["addo[ns]", "ao"], "List installed extensions", function (args) { - let addons = AddonList(modules, ["extension"]); + let addons = AddonList(modules, args["-types"], args[0]); modules.commandline.echo(addons); if (modules.commandline.savingOutput) util.waitFor(function () addons.ready); + }, + { + argCount: "?", + options: [ + { + names: ["-types", "-type", "-t"], + description: "The add-on types to list", + default: ["extension"], + completer: function (context, args) completion.addonType(context), + type: CommandOption.LIST + } + ] }); + }, + completion: function (dactyl, modules, window) { + completion.addonType = function addonType(context) { + let base = ["extension", "theme"]; + function update(types) { + context.completions = types.map(function (t) [t, util.capitalize(t)]); + } + + context.generate = function generate() { + update(base); + if (AddonManager.getAllAddons) { + context.incomplete = true; + AddonManager.getAllAddons(function (addons) { + context.incomplete = false; + update(array.uniq(base.concat(addons.map(function (a) a.type)), + true)); + }); + } + } + } } }); diff --git a/common/modules/base.jsm b/common/modules/base.jsm index caaab70b..6e1a4138 100644 --- a/common/modules/base.jsm +++ b/common/modules/base.jsm @@ -297,11 +297,11 @@ function deprecated(alternative, fn) { * @param {object} obj The object to inspect. * @returns {Generator} */ -function keys(obj) { +function keys(obj) iter(function keys() { for (var k in obj) if (hasOwnProperty.call(obj, k)) yield k; -} +}()); /** * Iterates over all of the top-level, iterable property values of an * object. diff --git a/common/modules/completion.jsm b/common/modules/completion.jsm index 7c352d1b..8594deeb 100644 --- a/common/modules/completion.jsm +++ b/common/modules/completion.jsm @@ -481,6 +481,13 @@ var CompletionContext = Class("CompletionContext", { let filtered = this.filterFunc(this._cache.constructed); if (this.maxItems) filtered = filtered.slice(0, this.maxItems); + if (/types/.test(this.name)) { + let self = this; + util.dump(this.filters[1]); + util.dump("FILTERED", this._cache.constructed.map(function (item) [item.text.quote(), self.filters[0].call(self, item.text)])); + util.dump("FILTERED", this._cache.constructed.map(function (item) + self.filters.map(function (filter) filter.call(self, item)))); + } // Sorting if (this.sortResults && this.compare) diff --git a/common/modules/config.jsm b/common/modules/config.jsm index 9b13b7d7..42434f5f 100644 --- a/common/modules/config.jsm +++ b/common/modules/config.jsm @@ -424,6 +424,7 @@ var ConfigBase = Class("ConfigBase", { Link position: relative; padding-right: 2em; Link:not(:hover)>LinkInfo opacity: 0; left: 0; width: 1px; height: 1px; overflow: hidden; LinkInfo { + color: black; position: absolute; left: 100%; padding: 1ex; @@ -518,6 +519,7 @@ var ConfigBase = Class("ConfigBase", { DownloadState;;;DownloadCell DownloadTime;;;DownloadCell DownloadTitle;;;DownloadCell,URL + DownloadTitle>Link>a max-width: 48ex; overflow: hidden; display: inline-block; AddonCell display: table-cell; padding: 0 1ex; @@ -530,7 +532,7 @@ var ConfigBase = Class("ConfigBase", { Addon>*;;;AddonCell AddonButtons AddonDescription - AddonName + AddonName max-width: 48ex; overflow: hidden; AddonStatus AddonVersion diff --git a/common/modules/downloads.jsm b/common/modules/downloads.jsm index 138efaac..33fcb2cc 100644 --- a/common/modules/downloads.jsm +++ b/common/modules/downloads.jsm @@ -178,9 +178,10 @@ var DownloadList = Class("DownloadList", XPCOM([Ci.nsIDownloadProgressListener, Ci.nsIObserver, Ci.nsISupportsWeakReference]), { - init: function init(modules) { + init: function init(modules, filter) { this.modules = modules; this.nodes = {}; + this.filter = filter && filter.toLowerCase(); this.downloads = {}; }, cleanup: function cleanup() { @@ -213,12 +214,15 @@ var DownloadList = Class("DownloadList", addDownload: function addDownload(id) { if (!(id in this.downloads)) { - this.downloads[id] = Download(id, this); + let download = Download(id, this); + if (this.filter && download.displayName.indexOf(this.filter) === -1) + return; + this.downloads[id] = download; let index = values(this.downloads).sort(function (a, b) a.compare(b)) - .indexOf(this.downloads[id]); + .indexOf(download); - this.nodes.list.insertBefore(this.downloads[id].nodes.row, + this.nodes.list.insertBefore(download.nodes.row, this.nodes.list.childNodes[index + 1]); } }, @@ -283,8 +287,11 @@ var Downloads = Module("downloads", { commands.add(["downl[oads]", "dl"], "Display the downloads list", function (args) { - let downloads = DownloadList(modules); + let downloads = DownloadList(modules, args[0]); modules.commandline.echo(downloads); + }, + { + argCount: "?" }); }, dactyl: function (dactyl, modules, window) { From 295654835264e31c70a83f708c89f189aa140bb6 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Mon, 24 Jan 2011 05:11:17 -0500 Subject: [PATCH 4/7] Remove dump statements. --HG-- extra : transplant_source : %27%D3%96%E0%3B%BB%D8%A2%AA%C1U%DC%E5%C5%19%E2h%19%EF%C8 --- common/modules/completion.jsm | 7 ------- 1 file changed, 7 deletions(-) diff --git a/common/modules/completion.jsm b/common/modules/completion.jsm index 8594deeb..7c352d1b 100644 --- a/common/modules/completion.jsm +++ b/common/modules/completion.jsm @@ -481,13 +481,6 @@ var CompletionContext = Class("CompletionContext", { let filtered = this.filterFunc(this._cache.constructed); if (this.maxItems) filtered = filtered.slice(0, this.maxItems); - if (/types/.test(this.name)) { - let self = this; - util.dump(this.filters[1]); - util.dump("FILTERED", this._cache.constructed.map(function (item) [item.text.quote(), self.filters[0].call(self, item.text)])); - util.dump("FILTERED", this._cache.constructed.map(function (item) - self.filters.map(function (filter) filter.call(self, item)))); - } // Sorting if (this.sortResults && this.compare) From 8e270cdb0cb258add2396834880b10ba22b6acb0 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Sun, 23 Jan 2011 23:25:08 -0500 Subject: [PATCH 5/7] Move :downloads button logic into binding classes. --HG-- extra : transplant_source : %CF.%29Y%88%B5%0AU%18%E0%B9%EA%85%94%F1%99%16b9%F3 --- common/content/commandline.js | 1 - common/modules/downloads.jsm | 26 ++--------- common/modules/template.jsm | 81 +++++++++++++++++++++++++++++++++++ common/modules/util.jsm | 16 ++++--- 4 files changed, 95 insertions(+), 29 deletions(-) diff --git a/common/content/commandline.js b/common/content/commandline.js index 63d86d07..0b103997 100644 --- a/common/content/commandline.js +++ b/common/content/commandline.js @@ -1117,7 +1117,6 @@ var CommandLine = Module("commandline", { // TODO: Wouldn't multiple handlers be cleaner? --djk if (event.type == "click" && event.target instanceof HTMLAnchorElement) { - util.dump(event.getPreventDefault(), event.target); if (event.getPreventDefault()) return; diff --git a/common/modules/downloads.jsm b/common/modules/downloads.jsm index 33fcb2cc..de11b6b3 100644 --- a/common/modules/downloads.jsm +++ b/common/modules/downloads.jsm @@ -25,7 +25,9 @@ var Download = Class("Download", { this.instance = this; this.list = list; - this.nodes = {}; + this.nodes = { + commandTarget: self + }; util.xmlToDom(
  • @@ -54,22 +56,6 @@ var Download = Class("Download", {
  • , this.list.document, this.nodes); - for (let [key, node] in Iterator(this.nodes)) { - node.dactylDownload = self; - if (node.getAttributeNS(NS, "highlight") == "Button") { - node.setAttributeNS(NS, "command", "download.command"); - update(node, { - set collapsed(collapsed) { - if (collapsed) - this.setAttribute("collapsed", "true"); - else - this.removeAttribute("collapsed"); - }, - get collapsed() !!this.getAttribute("collapsed") - }); - } - } - self.updateStatus(); return self; }, @@ -293,12 +279,6 @@ var Downloads = Module("downloads", { { argCount: "?" }); - }, - dactyl: function (dactyl, modules, window) { - dactyl.commands["download.command"] = function (event) { - let elem = event.originalTarget; - elem.dactylDownload.command(elem.getAttribute("key")); - } } }); diff --git a/common/modules/template.jsm b/common/modules/template.jsm index e1f3fec4..178d0259 100644 --- a/common/modules/template.jsm +++ b/common/modules/template.jsm @@ -13,6 +13,68 @@ defineModule("template", { default xml namespace = XHTML; +var Binding = Class("Binding", { + init: function (node) { + this.node = node; + node.dactylBinding = this; + + Object.defineProperties(node, this.constructor.properties); + + for (let [event, handler] in values(this.constructor.events)) + node.addEventListener(event, handler, false); + }, + + set collapsed(collapsed) { + if (collapsed) + this.setAttribute("collapsed", "true"); + else + this.removeAttribute("collapsed"); + }, + get collapsed() !!this.getAttribute("collapsed"), + + __noSuchMethod__: function __noSuchMethod__(meth, args) { + return this.node[meth].apply(this.node, args); + } +}, { + get bindings() { + let bindingProto = Object.getPrototypeOf(Binding.prototype); + for (let obj = this.prototype; obj !== bindingProto; obj = Object.getPrototypeOf(obj)) + yield obj; + }, + + bind: function bind(func) function bound() { + try { + return func.apply(this.dactylBinding, arguments); + } + catch (e) { + util.reportError(e); + throw e; + } + }, + + events: Class.memoize(function () { + let res = []; + for (let obj in this.bindings) + if (Object.getOwnPropertyDescriptor(obj, "events")) + for (let [event, handler] in Iterator(obj.events)) + res.push([event, this.bind(handler)]); + return res; + }), + + properties: Class.memoize(function () { + let res = {}; + for (let obj in this.bindings) + for (let prop in properties(obj)) { + let desc = Object.getOwnPropertyDescriptor(obj, prop); + for (let k in values(["get", "set", "value"])) + if (typeof desc[k] === "function") + desc[k] = this.bind(desc[k]); + res[prop] = desc; + } + return res; + }) +}); + var Template = Module("Template", { add: function add(a, b) a + b, join: function join(c) function (a, b) a + c + b, @@ -36,6 +98,25 @@ var Template = Module("Template", { return ret; }, + bindings: { + Button: Class("Button", Binding, { + init: function init(node, params) { + init.supercall(this, node); + + this.target = params.commandTarget; + if (callable(this.target)) + this.target = { command: this.target } + }, + + events: { + "click": function onClick(event) { + event.preventDefault(); + this.target.command(this.getAttribute("key")); + } + } + }) + }, + bookmarkDescription: function (item, text) <> { diff --git a/common/modules/util.jsm b/common/modules/util.jsm index 18925beb..a58a116e 100644 --- a/common/modules/util.jsm +++ b/common/modules/util.jsm @@ -1587,27 +1587,33 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), XML.prettyPrinting = false; if (typeof node === "string") // Sandboxes can't currently pass us XML objects. node = XML(node); + if (node.length() != 1) { let domnode = doc.createDocumentFragment(); for each (let child in node) domnode.appendChild(xmlToDom(child, doc, nodes)); return domnode; } + switch (node.nodeKind()) { case "text": return doc.createTextNode(String(node)); case "element": let domnode = doc.createElementNS(node.namespace(), node.localName()); - for each (let attr in node.@*::*) - if (attr.name() != "highlight") - domnode.setAttributeNS(attr.namespace(), attr.localName(), String(attr)); - else - highlight.highlightNode(domnode, String(attr)); for each (let child in node.*::*) domnode.appendChild(xmlToDom(child, doc, nodes)); if (nodes && node.@key) nodes[node.@key] = domnode; + + for each (let attr in node.@*::*) + if (attr.name() != "highlight") + domnode.setAttributeNS(attr.namespace(), attr.localName(), String(attr)); + else { + highlight.highlightNode(domnode, String(attr)); + if (attr in template.bindings) + template.bindings[attr](domnode, nodes); + } return domnode; default: return null; From bd431b92b51f7fe4aa1c1483b96fb85e843d6fde Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Mon, 24 Jan 2011 00:37:32 -0500 Subject: [PATCH 6/7] Add Clear button to :downloads. --HG-- extra : transplant_source : %14%3E%7Dk%3F%D0Q%DF%D4BdO%D0q%1D%A4D%E5%09%E9 --- common/content/commandline.js | 13 ++++---- common/modules/config.jsm | 20 ++++++------ common/modules/downloads.jsm | 59 +++++++++++++++++++++++++++-------- common/modules/template.jsm | 23 ++++++++++++-- 4 files changed, 84 insertions(+), 31 deletions(-) diff --git a/common/content/commandline.js b/common/content/commandline.js index 0b103997..075e3810 100644 --- a/common/content/commandline.js +++ b/common/content/commandline.js @@ -893,20 +893,21 @@ var CommandLine = Module("commandline", { */ input: function _input(prompt, callback, extra) { extra = extra || {}; + let closure = extra.closure || extra; this._input = { - submit: callback || extra.onAccept, - change: extra.onChange, - complete: extra.completer, - cancel: extra.onCancel + submit: callback || closure.onAccept, + change: closure.onChange, + complete: closure.completer, + cancel: closure.onCancel }; modes.push(modes.COMMAND_LINE, modes.PROMPT | extra.extended, update(Object.create(extra), { - onEvent: extra.onEvent || this.closure.onEvent, + onEvent: closure.onEvent || this.closure.onEvent, leave: function leave(stack) { commandline.leave(stack); - leave.supercall(this, stack); + leave.supercall(extra, stack); }, keyModes: [extra.extended, modes.PROMPT] })); diff --git a/common/modules/config.jsm b/common/modules/config.jsm index 42434f5f..994d39c0 100644 --- a/common/modules/config.jsm +++ b/common/modules/config.jsm @@ -500,25 +500,27 @@ var ConfigBase = Class("ConfigBase", { Button::after content: "]"; color: gray; text-decoration: none !important; Button:not([collapsed]) ~ Button:not([collapsed])::before content: "/["; + DownloadCell display: table-cell; padding: 0 1ex; + Downloads display: table; margin: 0; padding: 0; DownloadHead;;;CompTitle display: table-row; - DownloadHead>*;;;DownloadCell display: table-cell; + DownloadHead>*;;;DownloadCell Download display: table-row; Download:not([active]) color: gray; Buttons - DownloadCell display: table-cell; padding: 0 1ex; - DownloadButtons;;;DownloadCell - DownloadPercent;;;DownloadCell - DownloadProgress;;;DownloadCell + Download>*;;;DownloadCell + DownloadButtons + DownloadPercent + DownloadProgress DownloadProgressHave DownloadProgressTotal - DownloadSource;;;DownloadCell,URL - DownloadState;;;DownloadCell - DownloadTime;;;DownloadCell - DownloadTitle;;;DownloadCell,URL + DownloadSource + DownloadState + DownloadTime + DownloadTitle DownloadTitle>Link>a max-width: 48ex; overflow: hidden; display: inline-block; AddonCell display: table-cell; padding: 0 1ex; diff --git a/common/modules/downloads.jsm b/common/modules/downloads.jsm index de11b6b3..1eb3acd1 100644 --- a/common/modules/downloads.jsm +++ b/common/modules/downloads.jsm @@ -66,7 +66,7 @@ var Download = Class("Download", { get alive() this.inState(["downloading", "notstarted", "paused", "queued", "scanning"]), - allowed: Class.memoize(function () let (self = this) ({ + allowedCommands: Class.memoize(function () let (self = this) ({ get cancel() self.cancelable && self.inState(["downloading", "paused", "starting"]), get delete() !this.cancel && self.targetFile.exists(), get launch() self.targetFile.exists() && self.inState(["finished"]), @@ -78,13 +78,10 @@ var Download = Class("Download", { })), command: function command(name) { - util.assert(set.has(this.allowed, name), "Unknown command"); - util.assert(this.allowed[name], "Command not allowed"); + util.assert(set.has(this.allowedCommands, name), "Unknown command"); + util.assert(this.allowedCommands[name], "Command not allowed"); - if (set.has(this.commands, name)) - this.commands[name].call(this); - else - services.downloadManager[name + "Download"](this.id); + services.downloadManager[name + "Download"](this.id); }, commands: { @@ -154,8 +151,11 @@ var Download = Class("Download", { this.nodes.row.setAttribute("status", this.status); this.nodes.state.textContent = util.capitalize(this.status); - for (let [command, enabled] in Iterator(this.allowed)) - this.nodes[command].collapsed = !enabled; + + for (let node in values(this.nodes)) + if (node.update) + node.update(); + this.updateProgress(); } }); @@ -166,7 +166,9 @@ var DownloadList = Class("DownloadList", Ci.nsISupportsWeakReference]), { init: function init(modules, filter) { this.modules = modules; - this.nodes = {}; + this.nodes = { + commandTarget: this + }; this.filter = filter && filter.toLowerCase(); this.downloads = {}; }, @@ -181,17 +183,30 @@ var DownloadList = Class("DownloadList",
  • Title Status - + Progress - + Time remaining Source
  • +
  • +
  • + Totals: + + + Clear + + + + + +
  • , this.document, this.nodes); for (let row in iter(services.downloadManager.DBConnection .createStatement("SELECT id FROM moz_downloads"))) this.addDownload(row.id); + this.update(); util.addObserver(this); services.downloadManager.addListener(this); @@ -224,14 +239,31 @@ var DownloadList = Class("DownloadList", this.cleanup(); }, + allowedCommands: Class.memoize(function () let (self = this) ({ + get clear() values(self.downloads).some(function (dl) dl.allowedCommands.remove) + })), + + commands: { + clear: function () { + services.downloadManager.cleanUp(); + } + }, + + update: function update() { + for (let node in values(this.nodes)) + if (node.update && node.update != update) + node.update(); + }, + observers: { "download-manager-remove-download": function (id) { if (id == null) - id = [k for ([k, dl] in iter(this.downloads)) if (dl.allowed.remove)]; + id = [k for ([k, dl] in iter(this.downloads)) if (dl.allowedCommands.remove)]; else id = [id.QueryInterface(Ci.nsISupportsPRUint32).data]; Array.concat(id).map(this.closure.removeDownload); + this.update(); } }, @@ -245,6 +277,7 @@ var DownloadList = Class("DownloadList", this.modules.commandline.updateOutputHeight(false); this.nodes.list.scrollIntoView(false); } + this.update(); } catch (e) { util.reportError(e); diff --git a/common/modules/template.jsm b/common/modules/template.jsm index 178d0259..d9db8cb6 100644 --- a/common/modules/template.jsm +++ b/common/modules/template.jsm @@ -104,15 +104,32 @@ var Template = Module("Template", { init.supercall(this, node); this.target = params.commandTarget; - if (callable(this.target)) - this.target = { command: this.target } }, + get command() this.getAttribute("command") || this.getAttribute("key"), + events: { "click": function onClick(event) { event.preventDefault(); - this.target.command(this.getAttribute("key")); + if (this.commandAllowed) { + if (set.has(this.target.commands || {}, this.command)) + this.target.commands[this.command].call(this.target); + else + this.target.command(this.command); + } } + }, + + get commandAllowed() { + if (set.has(this.target.allowedCommands || {}, this.command)) + return this.target.allowedCommands[this.command]; + if ("commandAllowed" in this.target) + return this.target.commandAllowed(this.command); + return true; + }, + + update: function update() { + this.collapsed = !this.commandAllowed; } }) }, From 198879bc047dc61d51ef65344db4375b25aa9791 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Mon, 24 Jan 2011 06:15:48 -0500 Subject: [PATCH 7/7] Fix gathering of config.version from hg. --- common/modules/config.jsm | 2 +- common/modules/io.jsm | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/common/modules/config.jsm b/common/modules/config.jsm index 994d39c0..29708a70 100644 --- a/common/modules/config.jsm +++ b/common/modules/config.jsm @@ -113,7 +113,7 @@ var ConfigBase = Class("ConfigBase", { io.pathSearch("hg")) { return io.system(["hg", "-R", uri.file.parent.path, "log", "-r.", - "--template=hg{rev} ({date|isodate})"]); + "--template=hg{rev} ({date|isodate})"]).output; } } let version = this.addon.version; diff --git a/common/modules/io.jsm b/common/modules/io.jsm index 58d2fb6b..0aaa3c9e 100644 --- a/common/modules/io.jsm +++ b/common/modules/io.jsm @@ -436,7 +436,7 @@ var IO = Module("io", { * * @param {string} command The command to run. * @param {string} input Any input to be provided to the command on stdin. - * @returns {string} + * @returns {object} */ system: function (command, input) { util.dactyl.echomsg("Calling shell to execute: " + command, 4); @@ -472,7 +472,8 @@ var IO = Module("io", { __noSuchMethod__: function (meth, args) this.output[meth].apply(this.output, args), valueOf: function () this.output, output: stdout.read().replace(/^(.*)\n$/, "$1"), - returnValue: res + returnValue: res, + toString: function () this.output }; }) || ""; },