From ce61db5b10f122f5e35cf7af60778c334e09ff81 Mon Sep 17 00:00:00 2001 From: Jon Rietveld Date: Tue, 8 May 2012 19:19:58 -0700 Subject: [PATCH] Bug 733469 - Move the applications preferences to in-content UI. r=jaws r=bmcbride --- .../preferences/in-content/applications.js | 1821 +++++++++++++++++ .../preferences/in-content/applications.xul | 88 + .../components/preferences/in-content/jar.mn | 4 +- .../preferences/in-content/preferences.js | 1 + .../preferences/in-content/preferences.xul | 1 + .../preferences/in-content/preferences.css | 12 + .../preferences/in-content/preferences.css | 12 + .../preferences/in-content/preferences.css | 12 + 8 files changed, 1950 insertions(+), 1 deletion(-) create mode 100644 browser/components/preferences/in-content/applications.js create mode 100644 browser/components/preferences/in-content/applications.xul diff --git a/browser/components/preferences/in-content/applications.js b/browser/components/preferences/in-content/applications.js new file mode 100644 index 000000000000..03c8065979f5 --- /dev/null +++ b/browser/components/preferences/in-content/applications.js @@ -0,0 +1,1821 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//****************************************************************************// +// Constants & Enumeration Values + +const TYPE_MAYBE_FEED = "application/vnd.mozilla.maybe.feed"; +const TYPE_MAYBE_VIDEO_FEED = "application/vnd.mozilla.maybe.video.feed"; +const TYPE_MAYBE_AUDIO_FEED = "application/vnd.mozilla.maybe.audio.feed"; + +const PREF_DISABLED_PLUGIN_TYPES = "plugin.disable_full_page_plugin_for_types"; + +// Preferences that affect which entries to show in the list. +const PREF_SHOW_PLUGINS_IN_LIST = "browser.download.show_plugins_in_list"; +const PREF_HIDE_PLUGINS_WITHOUT_EXTENSIONS = + "browser.download.hide_plugins_without_extensions"; + +/* + * Preferences where we store handling information about the feed type. + * + * browser.feeds.handler + * - "bookmarks", "reader" (clarified further using the .default preference), + * or "ask" -- indicates the default handler being used to process feeds; + * "bookmarks" is obsolete; to specify that the handler is bookmarks, + * set browser.feeds.handler.default to "bookmarks"; + * + * browser.feeds.handler.default + * - "bookmarks", "client" or "web" -- indicates the chosen feed reader used + * to display feeds, either transiently (i.e., when the "use as default" + * checkbox is unchecked, corresponds to when browser.feeds.handler=="ask") + * or more permanently (i.e., the item displayed in the dropdown in Feeds + * preferences) + * + * browser.feeds.handler.webservice + * - the URL of the currently selected web service used to read feeds + * + * browser.feeds.handlers.application + * - nsILocalFile, stores the current client-side feed reading app if one has + * been chosen + */ +const PREF_FEED_SELECTED_APP = "browser.feeds.handlers.application"; +const PREF_FEED_SELECTED_WEB = "browser.feeds.handlers.webservice"; +const PREF_FEED_SELECTED_ACTION = "browser.feeds.handler"; +const PREF_FEED_SELECTED_READER = "browser.feeds.handler.default"; + +const PREF_VIDEO_FEED_SELECTED_APP = "browser.videoFeeds.handlers.application"; +const PREF_VIDEO_FEED_SELECTED_WEB = "browser.videoFeeds.handlers.webservice"; +const PREF_VIDEO_FEED_SELECTED_ACTION = "browser.videoFeeds.handler"; +const PREF_VIDEO_FEED_SELECTED_READER = "browser.videoFeeds.handler.default"; + +const PREF_AUDIO_FEED_SELECTED_APP = "browser.audioFeeds.handlers.application"; +const PREF_AUDIO_FEED_SELECTED_WEB = "browser.audioFeeds.handlers.webservice"; +const PREF_AUDIO_FEED_SELECTED_ACTION = "browser.audioFeeds.handler"; +const PREF_AUDIO_FEED_SELECTED_READER = "browser.audioFeeds.handler.default"; + +// The nsHandlerInfoAction enumeration values in nsIHandlerInfo identify +// the actions the application can take with content of various types. +// But since nsIHandlerInfo doesn't support plugins, there's no value +// identifying the "use plugin" action, so we use this constant instead. +const kActionUsePlugin = 5; + +/* +#ifdef MOZ_WIDGET_GTK2 +*/ +const ICON_URL_APP = "moz-icon://dummy.exe?size=16"; +/* +#else +*/ +const ICON_URL_APP = "chrome://browser/skin/preferences/application.png"; +/* +#endif +*/ + +// For CSS. Can be one of "ask", "save", "plugin" or "feed". If absent, the icon URL +// was set by us to a custom handler icon and CSS should not try to override it. +const APP_ICON_ATTR_NAME = "appHandlerIcon"; + +//****************************************************************************// +// Utilities + +function getFileDisplayName(file) { +#ifdef XP_WIN + if (file instanceof Ci.nsILocalFileWin) { + try { + return file.getVersionInfoField("FileDescription"); + } catch (e) {} + } +#endif +#ifdef XP_MACOSX + if (file instanceof Ci.nsILocalFileMac) { + try { + return file.bundleDisplayName; + } catch (e) {} + } +#endif + return file.leafName; +} + +function getLocalHandlerApp(aFile) { + var localHandlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"]. + createInstance(Ci.nsILocalHandlerApp); + localHandlerApp.name = getFileDisplayName(aFile); + localHandlerApp.executable = aFile; + + return localHandlerApp; +} + +/** + * An enumeration of items in a JS array. + * + * FIXME: use ArrayConverter once it lands (bug 380839). + * + * @constructor + */ +function ArrayEnumerator(aItems) { + this._index = 0; + this._contents = aItems; +} + +ArrayEnumerator.prototype = { + _index: 0, + + hasMoreElements: function() { + return this._index < this._contents.length; + }, + + getNext: function() { + return this._contents[this._index++]; + } +}; + +function isFeedType(t) { + return t == TYPE_MAYBE_FEED || t == TYPE_MAYBE_VIDEO_FEED || t == TYPE_MAYBE_AUDIO_FEED; +} + +//****************************************************************************// +// HandlerInfoWrapper + +/** + * This object wraps nsIHandlerInfo with some additional functionality + * the Applications prefpane needs to display and allow modification of + * the list of handled types. + * + * We create an instance of this wrapper for each entry we might display + * in the prefpane, and we compose the instances from various sources, + * including navigator.plugins and the handler service. + * + * We don't implement all the original nsIHandlerInfo functionality, + * just the stuff that the prefpane needs. + * + * In theory, all of the custom functionality in this wrapper should get + * pushed down into nsIHandlerInfo eventually. + */ +function HandlerInfoWrapper(aType, aHandlerInfo) { + this._type = aType; + this.wrappedHandlerInfo = aHandlerInfo; +} + +HandlerInfoWrapper.prototype = { + // The wrapped nsIHandlerInfo object. In general, this object is private, + // but there are a couple cases where callers access it directly for things + // we haven't (yet?) implemented, so we make it a public property. + wrappedHandlerInfo: null, + + + //**************************************************************************// + // Convenience Utils + + _handlerSvc: Cc["@mozilla.org/uriloader/handler-service;1"]. + getService(Ci.nsIHandlerService), + + _prefSvc: Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch), + + _categoryMgr: Cc["@mozilla.org/categorymanager;1"]. + getService(Ci.nsICategoryManager), + + element: function(aID) { + return document.getElementById(aID); + }, + + + //**************************************************************************// + // nsIHandlerInfo + + // The MIME type or protocol scheme. + _type: null, + get type() { + return this._type; + }, + + get description() { + if (this.wrappedHandlerInfo.description) + return this.wrappedHandlerInfo.description; + + if (this.primaryExtension) { + var extension = this.primaryExtension.toUpperCase(); + return this.element("bundlePreferences").getFormattedString("fileEnding", + [extension]); + } + + return this.type; + }, + + get preferredApplicationHandler() { + return this.wrappedHandlerInfo.preferredApplicationHandler; + }, + + set preferredApplicationHandler(aNewValue) { + this.wrappedHandlerInfo.preferredApplicationHandler = aNewValue; + + // Make sure the preferred handler is in the set of possible handlers. + if (aNewValue) + this.addPossibleApplicationHandler(aNewValue) + }, + + get possibleApplicationHandlers() { + return this.wrappedHandlerInfo.possibleApplicationHandlers; + }, + + addPossibleApplicationHandler: function(aNewHandler) { + var possibleApps = this.possibleApplicationHandlers.enumerate(); + while (possibleApps.hasMoreElements()) { + if (possibleApps.getNext().equals(aNewHandler)) + return; + } + this.possibleApplicationHandlers.appendElement(aNewHandler, false); + }, + + removePossibleApplicationHandler: function(aHandler) { + var defaultApp = this.preferredApplicationHandler; + if (defaultApp && aHandler.equals(defaultApp)) { + // If the app we remove was the default app, we must make sure + // it won't be used anymore + this.alwaysAskBeforeHandling = true; + this.preferredApplicationHandler = null; + } + + var handlers = this.possibleApplicationHandlers; + for (var i = 0; i < handlers.length; ++i) { + var handler = handlers.queryElementAt(i, Ci.nsIHandlerApp); + if (handler.equals(aHandler)) { + handlers.removeElementAt(i); + break; + } + } + }, + + get hasDefaultHandler() { + return this.wrappedHandlerInfo.hasDefaultHandler; + }, + + get defaultDescription() { + return this.wrappedHandlerInfo.defaultDescription; + }, + + // What to do with content of this type. + get preferredAction() { + // If we have an enabled plugin, then the action is to use that plugin. + if (this.plugin && !this.isDisabledPluginType) + return kActionUsePlugin; + + // If the action is to use a helper app, but we don't have a preferred + // handler app, then switch to using the system default, if any; otherwise + // fall back to saving to disk, which is the default action in nsMIMEInfo. + // Note: "save to disk" is an invalid value for protocol info objects, + // but the alwaysAskBeforeHandling getter will detect that situation + // and always return true in that case to override this invalid value. + if (this.wrappedHandlerInfo.preferredAction == Ci.nsIHandlerInfo.useHelperApp && + !gApplicationsPane.isValidHandlerApp(this.preferredApplicationHandler)) { + if (this.wrappedHandlerInfo.hasDefaultHandler) + return Ci.nsIHandlerInfo.useSystemDefault; + else + return Ci.nsIHandlerInfo.saveToDisk; + } + + return this.wrappedHandlerInfo.preferredAction; + }, + + set preferredAction(aNewValue) { + // We don't modify the preferred action if the new action is to use a plugin + // because handler info objects don't understand our custom "use plugin" + // value. Also, leaving it untouched means that we can automatically revert + // to the old setting if the user ever removes the plugin. + + if (aNewValue != kActionUsePlugin) + this.wrappedHandlerInfo.preferredAction = aNewValue; + }, + + get alwaysAskBeforeHandling() { + // If this type is handled only by a plugin, we can't trust the value + // in the handler info object, since it'll be a default based on the absence + // of any user configuration, and the default in that case is to always ask, + // even though we never ask for content handled by a plugin, so special case + // plugin-handled types by returning false here. + if (this.plugin && this.handledOnlyByPlugin) + return false; + + // If this is a protocol type and the preferred action is "save to disk", + // which is invalid for such types, then return true here to override that + // action. This could happen when the preferred action is to use a helper + // app, but the preferredApplicationHandler is invalid, and there isn't + // a default handler, so the preferredAction getter returns save to disk + // instead. + if (!(this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) && + this.preferredAction == Ci.nsIHandlerInfo.saveToDisk) + return true; + + return this.wrappedHandlerInfo.alwaysAskBeforeHandling; + }, + + set alwaysAskBeforeHandling(aNewValue) { + this.wrappedHandlerInfo.alwaysAskBeforeHandling = aNewValue; + }, + + + //**************************************************************************// + // nsIMIMEInfo + + // The primary file extension associated with this type, if any. + // + // XXX Plugin objects contain an array of MimeType objects with "suffixes" + // properties; if this object has an associated plugin, shouldn't we check + // those properties for an extension? + get primaryExtension() { + try { + if (this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo && + this.wrappedHandlerInfo.primaryExtension) + return this.wrappedHandlerInfo.primaryExtension + } catch(ex) {} + + return null; + }, + + + //**************************************************************************// + // Plugin Handling + + // A plugin that can handle this type, if any. + // + // Note: just because we have one doesn't mean it *will* handle the type. + // That depends on whether or not the type is in the list of types for which + // plugin handling is disabled. + plugin: null, + + // Whether or not this type is only handled by a plugin or is also handled + // by some user-configured action as specified in the handler info object. + // + // Note: we can't just check if there's a handler info object for this type, + // because OS and user configuration is mixed up in the handler info object, + // so we always need to retrieve it for the OS info and can't tell whether + // it represents only OS-default information or user-configured information. + // + // FIXME: once handler info records are broken up into OS-provided records + // and user-configured records, stop using this boolean flag and simply + // check for the presence of a user-configured record to determine whether + // or not this type is only handled by a plugin. Filed as bug 395142. + handledOnlyByPlugin: undefined, + + get isDisabledPluginType() { + return this._getDisabledPluginTypes().indexOf(this.type) != -1; + }, + + _getDisabledPluginTypes: function() { + var types = ""; + + if (this._prefSvc.prefHasUserValue(PREF_DISABLED_PLUGIN_TYPES)) + types = this._prefSvc.getCharPref(PREF_DISABLED_PLUGIN_TYPES); + + // Only split if the string isn't empty so we don't end up with an array + // containing a single empty string. + if (types != "") + return types.split(","); + + return []; + }, + + disablePluginType: function() { + var disabledPluginTypes = this._getDisabledPluginTypes(); + + if (disabledPluginTypes.indexOf(this.type) == -1) + disabledPluginTypes.push(this.type); + + this._prefSvc.setCharPref(PREF_DISABLED_PLUGIN_TYPES, + disabledPluginTypes.join(",")); + + // Update the category manager so existing browser windows update. + this._categoryMgr.deleteCategoryEntry("Gecko-Content-Viewers", + this.type, + false); + }, + + enablePluginType: function() { + var disabledPluginTypes = this._getDisabledPluginTypes(); + + var type = this.type; + disabledPluginTypes = disabledPluginTypes.filter(function(v) v != type); + + this._prefSvc.setCharPref(PREF_DISABLED_PLUGIN_TYPES, + disabledPluginTypes.join(",")); + + // Update the category manager so existing browser windows update. + this._categoryMgr. + addCategoryEntry("Gecko-Content-Viewers", + this.type, + "@mozilla.org/content/plugin/document-loader-factory;1", + false, + true); + }, + + + //**************************************************************************// + // Storage + + store: function() { + this._handlerSvc.store(this.wrappedHandlerInfo); + }, + + + //**************************************************************************// + // Icons + + get smallIcon() { + return this._getIcon(16); + }, + + _getIcon: function(aSize) { + if (this.primaryExtension) + return "moz-icon://goat." + this.primaryExtension + "?size=" + aSize; + + if (this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) + return "moz-icon://goat?size=" + aSize + "&contentType=" + this.type; + + // FIXME: consider returning some generic icon when we can't get a URL for + // one (for example in the case of protocol schemes). Filed as bug 395141. + return null; + } + +}; + + +//****************************************************************************// +// Feed Handler Info + +/** + * This object implements nsIHandlerInfo for the feed types. It's a separate + * object because we currently store handling information for the feed type + * in a set of preferences rather than the nsIHandlerService-managed datastore. + * + * This object inherits from HandlerInfoWrapper in order to get functionality + * that isn't special to the feed type. + * + * XXX Should we inherit from HandlerInfoWrapper? After all, we override + * most of that wrapper's properties and methods, and we have to dance around + * the fact that the wrapper expects to have a wrappedHandlerInfo, which we + * don't provide. + */ + +function FeedHandlerInfo(aMIMEType) { + HandlerInfoWrapper.call(this, aMIMEType, null); +} + +FeedHandlerInfo.prototype = { + __proto__: HandlerInfoWrapper.prototype, + + //**************************************************************************// + // Convenience Utils + + _converterSvc: + Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"]. + getService(Ci.nsIWebContentConverterService), + + _shellSvc: +#ifdef HAVE_SHELL_SERVICE + getShellService(), +#else + null, +#endif + + + //**************************************************************************// + // nsIHandlerInfo + + get description() { + return this.element("bundlePreferences").getString(this._appPrefLabel); + }, + + get preferredApplicationHandler() { + switch (this.element(this._prefSelectedReader).value) { + case "client": + var file = this.element(this._prefSelectedApp).value; + if (file) + return getLocalHandlerApp(file); + + return null; + + case "web": + var uri = this.element(this._prefSelectedWeb).value; + if (!uri) + return null; + return this._converterSvc.getWebContentHandlerByURI(this.type, uri); + + case "bookmarks": + default: + // When the pref is set to bookmarks, we handle feeds internally, + // we don't forward them to a local or web handler app, so there is + // no preferred handler. + return null; + } + }, + + set preferredApplicationHandler(aNewValue) { + if (aNewValue instanceof Ci.nsILocalHandlerApp) { + this.element(this._prefSelectedApp).value = aNewValue.executable; + this.element(this._prefSelectedReader).value = "client"; + } + else if (aNewValue instanceof Ci.nsIWebContentHandlerInfo) { + this.element(this._prefSelectedWeb).value = aNewValue.uri; + this.element(this._prefSelectedReader).value = "web"; + // Make the web handler be the new "auto handler" for feeds. + // Note: we don't have to unregister the auto handler when the user picks + // a non-web handler (local app, Live Bookmarks, etc.) because the service + // only uses the "auto handler" when the selected reader is a web handler. + // We also don't have to unregister it when the user turns on "always ask" + // (i.e. preview in browser), since that also overrides the auto handler. + this._converterSvc.setAutoHandler(this.type, aNewValue); + } + }, + + _possibleApplicationHandlers: null, + + get possibleApplicationHandlers() { + if (this._possibleApplicationHandlers) + return this._possibleApplicationHandlers; + + // A minimal implementation of nsIMutableArray. It only supports the two + // methods its callers invoke, namely appendElement and nsIArray::enumerate. + this._possibleApplicationHandlers = { + _inner: [], + _removed: [], + + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIMutableArray) || + aIID.equals(Ci.nsIArray) || + aIID.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + get length() { + return this._inner.length; + }, + + enumerate: function() { + return new ArrayEnumerator(this._inner); + }, + + appendElement: function(aHandlerApp, aWeak) { + this._inner.push(aHandlerApp); + }, + + removeElementAt: function(aIndex) { + this._removed.push(this._inner[aIndex]); + this._inner.splice(aIndex, 1); + }, + + queryElementAt: function(aIndex, aInterface) { + return this._inner[aIndex].QueryInterface(aInterface); + } + }; + + // Add the selected local app if it's different from the OS default handler. + // Unlike for other types, we can store only one local app at a time for the + // feed type, since we store it in a preference that historically stores + // only a single path. But we display all the local apps the user chooses + // while the prefpane is open, only dropping the list when the user closes + // the prefpane, for maximum usability and consistency with other types. + var preferredAppFile = this.element(this._prefSelectedApp).value; + if (preferredAppFile) { + let preferredApp = getLocalHandlerApp(preferredAppFile); + let defaultApp = this._defaultApplicationHandler; + if (!defaultApp || !defaultApp.equals(preferredApp)) + this._possibleApplicationHandlers.appendElement(preferredApp, false); + } + + // Add the registered web handlers. There can be any number of these. + var webHandlers = this._converterSvc.getContentHandlers(this.type); + for each (let webHandler in webHandlers) + this._possibleApplicationHandlers.appendElement(webHandler, false); + + return this._possibleApplicationHandlers; + }, + + __defaultApplicationHandler: undefined, + get _defaultApplicationHandler() { + if (typeof this.__defaultApplicationHandler != "undefined") + return this.__defaultApplicationHandler; + + var defaultFeedReader = null; +#ifdef HAVE_SHELL_SERVICE + try { + defaultFeedReader = this._shellSvc.defaultFeedReader; + } + catch(ex) { + // no default reader or _shellSvc is null + } +#endif + + if (defaultFeedReader) { + let handlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"]. + createInstance(Ci.nsIHandlerApp); + handlerApp.name = getFileDisplayName(defaultFeedReader); + handlerApp.QueryInterface(Ci.nsILocalHandlerApp); + handlerApp.executable = defaultFeedReader; + + this.__defaultApplicationHandler = handlerApp; + } + else { + this.__defaultApplicationHandler = null; + } + + return this.__defaultApplicationHandler; + }, + + get hasDefaultHandler() { +#ifdef HAVE_SHELL_SERVICE + try { + if (this._shellSvc.defaultFeedReader) + return true; + } + catch(ex) { + // no default reader or _shellSvc is null + } +#endif + + return false; + }, + + get defaultDescription() { + if (this.hasDefaultHandler) + return this._defaultApplicationHandler.name; + + // Should we instead return null? + return ""; + }, + + // What to do with content of this type. + get preferredAction() { + switch (this.element(this._prefSelectedAction).value) { + + case "bookmarks": + return Ci.nsIHandlerInfo.handleInternally; + + case "reader": { + let preferredApp = this.preferredApplicationHandler; + let defaultApp = this._defaultApplicationHandler; + + // If we have a valid preferred app, return useSystemDefault if it's + // the default app; otherwise return useHelperApp. + if (gApplicationsPane.isValidHandlerApp(preferredApp)) { + if (defaultApp && defaultApp.equals(preferredApp)) + return Ci.nsIHandlerInfo.useSystemDefault; + + return Ci.nsIHandlerInfo.useHelperApp; + } + + // The pref is set to "reader", but we don't have a valid preferred app. + // What do we do now? Not sure this is the best option (perhaps we + // should direct the user to the default app, if any), but for now let's + // direct the user to live bookmarks. + return Ci.nsIHandlerInfo.handleInternally; + } + + // If the action is "ask", then alwaysAskBeforeHandling will override + // the action, so it doesn't matter what we say it is, it just has to be + // something that doesn't cause the controller to hide the type. + case "ask": + default: + return Ci.nsIHandlerInfo.handleInternally; + } + }, + + set preferredAction(aNewValue) { + switch (aNewValue) { + + case Ci.nsIHandlerInfo.handleInternally: + this.element(this._prefSelectedReader).value = "bookmarks"; + break; + + case Ci.nsIHandlerInfo.useHelperApp: + this.element(this._prefSelectedAction).value = "reader"; + // The controller has already set preferredApplicationHandler + // to the new helper app. + break; + + case Ci.nsIHandlerInfo.useSystemDefault: + this.element(this._prefSelectedAction).value = "reader"; + this.preferredApplicationHandler = this._defaultApplicationHandler; + break; + } + }, + + get alwaysAskBeforeHandling() { + return this.element(this._prefSelectedAction).value == "ask"; + }, + + set alwaysAskBeforeHandling(aNewValue) { + if (aNewValue == true) + this.element(this._prefSelectedAction).value = "ask"; + else + this.element(this._prefSelectedAction).value = "reader"; + }, + + // Whether or not we are currently storing the action selected by the user. + // We use this to suppress notification-triggered updates to the list when + // we make changes that may spawn such updates, specifically when we change + // the action for the feed type, which results in feed preference updates, + // which spawn "pref changed" notifications that would otherwise cause us + // to rebuild the view unnecessarily. + _storingAction: false, + + + //**************************************************************************// + // nsIMIMEInfo + + get primaryExtension() { + return "xml"; + }, + + + //**************************************************************************// + // Storage + + // Changes to the preferred action and handler take effect immediately + // (we write them out to the preferences right as they happen), + // so we when the controller calls store() after modifying the handlers, + // the only thing we need to store is the removal of possible handlers + // XXX Should we hold off on making the changes until this method gets called? + store: function() { + for each (let app in this._possibleApplicationHandlers._removed) { + if (app instanceof Ci.nsILocalHandlerApp) { + let pref = this.element(PREF_FEED_SELECTED_APP); + var preferredAppFile = pref.value; + if (preferredAppFile) { + let preferredApp = getLocalHandlerApp(preferredAppFile); + if (app.equals(preferredApp)) + pref.reset(); + } + } + else { + app.QueryInterface(Ci.nsIWebContentHandlerInfo); + this._converterSvc.removeContentHandler(app.contentType, app.uri); + } + } + this._possibleApplicationHandlers._removed = []; + }, + + + //**************************************************************************// + // Icons + + get smallIcon() { + return this._smallIcon; + } + +}; + +var feedHandlerInfo = { + __proto__: new FeedHandlerInfo(TYPE_MAYBE_FEED), + _prefSelectedApp: PREF_FEED_SELECTED_APP, + _prefSelectedWeb: PREF_FEED_SELECTED_WEB, + _prefSelectedAction: PREF_FEED_SELECTED_ACTION, + _prefSelectedReader: PREF_FEED_SELECTED_READER, + _smallIcon: "chrome://browser/skin/feeds/feedIcon16.png", + _appPrefLabel: "webFeed" +} + +var videoFeedHandlerInfo = { + __proto__: new FeedHandlerInfo(TYPE_MAYBE_VIDEO_FEED), + _prefSelectedApp: PREF_VIDEO_FEED_SELECTED_APP, + _prefSelectedWeb: PREF_VIDEO_FEED_SELECTED_WEB, + _prefSelectedAction: PREF_VIDEO_FEED_SELECTED_ACTION, + _prefSelectedReader: PREF_VIDEO_FEED_SELECTED_READER, + _smallIcon: "chrome://browser/skin/feeds/videoFeedIcon16.png", + _appPrefLabel: "videoPodcastFeed" +} + +var audioFeedHandlerInfo = { + __proto__: new FeedHandlerInfo(TYPE_MAYBE_AUDIO_FEED), + _prefSelectedApp: PREF_AUDIO_FEED_SELECTED_APP, + _prefSelectedWeb: PREF_AUDIO_FEED_SELECTED_WEB, + _prefSelectedAction: PREF_AUDIO_FEED_SELECTED_ACTION, + _prefSelectedReader: PREF_AUDIO_FEED_SELECTED_READER, + _smallIcon: "chrome://browser/skin/feeds/audioFeedIcon16.png", + _appPrefLabel: "audioPodcastFeed" +} + + +//****************************************************************************// +// Prefpane Controller + +var gApplicationsPane = { + // The set of types the app knows how to handle. A hash of HandlerInfoWrapper + // objects, indexed by type. + _handledTypes: {}, + + // The list of types we can show, sorted by the sort column/direction. + // An array of HandlerInfoWrapper objects. We build this list when we first + // load the data and then rebuild it when users change a pref that affects + // what types we can show or change the sort column/direction. + // Note: this isn't necessarily the list of types we *will* show; if the user + // provides a filter string, we'll only show the subset of types in this list + // that match that string. + _visibleTypes: [], + + // A count of the number of times each visible type description appears. + // We use these counts to determine whether or not to annotate descriptions + // with their types to distinguish duplicate descriptions from each other. + // A hash of integer counts, indexed by string description. + _visibleTypeDescriptionCount: {}, + + + //**************************************************************************// + // Convenience & Performance Shortcuts + + // These get defined by init(). + _brandShortName : null, + _prefsBundle : null, + _list : null, + _filter : null, + + _prefSvc : Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch), + + _mimeSvc : Cc["@mozilla.org/mime;1"]. + getService(Ci.nsIMIMEService), + + _helperAppSvc : Cc["@mozilla.org/uriloader/external-helper-app-service;1"]. + getService(Ci.nsIExternalHelperAppService), + + _handlerSvc : Cc["@mozilla.org/uriloader/handler-service;1"]. + getService(Ci.nsIHandlerService), + + _ioSvc : Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService), + + + //**************************************************************************// + // Initialization & Destruction + + init: function() { + // Initialize shortcuts to some commonly accessed elements & values. + this._brandShortName = + document.getElementById("bundleBrand").getString("brandShortName"); + this._prefsBundle = document.getElementById("bundlePreferences"); + this._list = document.getElementById("handlersView"); + this._filter = document.getElementById("filter"); + + // Observe preferences that influence what we display so we can rebuild + // the view when they change. + this._prefSvc.addObserver(PREF_SHOW_PLUGINS_IN_LIST, this, false); + this._prefSvc.addObserver(PREF_HIDE_PLUGINS_WITHOUT_EXTENSIONS, this, false); + this._prefSvc.addObserver(PREF_FEED_SELECTED_APP, this, false); + this._prefSvc.addObserver(PREF_FEED_SELECTED_WEB, this, false); + this._prefSvc.addObserver(PREF_FEED_SELECTED_ACTION, this, false); + this._prefSvc.addObserver(PREF_FEED_SELECTED_READER, this, false); + + this._prefSvc.addObserver(PREF_VIDEO_FEED_SELECTED_APP, this, false); + this._prefSvc.addObserver(PREF_VIDEO_FEED_SELECTED_WEB, this, false); + this._prefSvc.addObserver(PREF_VIDEO_FEED_SELECTED_ACTION, this, false); + this._prefSvc.addObserver(PREF_VIDEO_FEED_SELECTED_READER, this, false); + + this._prefSvc.addObserver(PREF_AUDIO_FEED_SELECTED_APP, this, false); + this._prefSvc.addObserver(PREF_AUDIO_FEED_SELECTED_WEB, this, false); + this._prefSvc.addObserver(PREF_AUDIO_FEED_SELECTED_ACTION, this, false); + this._prefSvc.addObserver(PREF_AUDIO_FEED_SELECTED_READER, this, false); + + + // Listen for window unload so we can remove our preference observers. + window.addEventListener("unload", this, false); + + // Figure out how we should be sorting the list. We persist sort settings + // across sessions, so we can't assume the default sort column/direction. + // XXX should we be using the XUL sort service instead? + if (document.getElementById("actionColumn").hasAttribute("sortDirection")) { + this._sortColumn = document.getElementById("actionColumn"); + // The typeColumn element always has a sortDirection attribute, + // either because it was persisted or because the default value + // from the xul file was used. If we are sorting on the other + // column, we should remove it. + document.getElementById("typeColumn").removeAttribute("sortDirection"); + } + else + this._sortColumn = document.getElementById("typeColumn"); + + // Load the data and build the list of handlers. + // By doing this in a timeout, we let the preferences dialog resize itself + // to an appropriate size before we add a bunch of items to the list. + // Otherwise, if there are many items, and the Applications prefpane + // is the one that gets displayed when the user first opens the dialog, + // the dialog might stretch too much in an attempt to fit them all in. + // XXX Shouldn't we perhaps just set a max-height on the richlistbox? + var _delayedPaneLoad = function(self) { + self._loadData(); + self._rebuildVisibleTypes(); + self._sortVisibleTypes(); + self._rebuildView(); + + // Notify observers that the UI is now ready + Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService). + notifyObservers(window, "app-handler-pane-loaded", null); + } + setTimeout(_delayedPaneLoad, 0, this); + }, + + destroy: function() { + window.removeEventListener("unload", this, false); + this._prefSvc.removeObserver(PREF_SHOW_PLUGINS_IN_LIST, this); + this._prefSvc.removeObserver(PREF_HIDE_PLUGINS_WITHOUT_EXTENSIONS, this); + this._prefSvc.removeObserver(PREF_FEED_SELECTED_APP, this); + this._prefSvc.removeObserver(PREF_FEED_SELECTED_WEB, this); + this._prefSvc.removeObserver(PREF_FEED_SELECTED_ACTION, this); + this._prefSvc.removeObserver(PREF_FEED_SELECTED_READER, this); + + this._prefSvc.removeObserver(PREF_VIDEO_FEED_SELECTED_APP, this); + this._prefSvc.removeObserver(PREF_VIDEO_FEED_SELECTED_WEB, this); + this._prefSvc.removeObserver(PREF_VIDEO_FEED_SELECTED_ACTION, this); + this._prefSvc.removeObserver(PREF_VIDEO_FEED_SELECTED_READER, this); + + this._prefSvc.removeObserver(PREF_AUDIO_FEED_SELECTED_APP, this); + this._prefSvc.removeObserver(PREF_AUDIO_FEED_SELECTED_WEB, this); + this._prefSvc.removeObserver(PREF_AUDIO_FEED_SELECTED_ACTION, this); + this._prefSvc.removeObserver(PREF_AUDIO_FEED_SELECTED_READER, this); + }, + + + //**************************************************************************// + // nsISupports + + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIObserver) || + aIID.equals(Ci.nsIDOMEventListener || + aIID.equals(Ci.nsISupports))) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + //**************************************************************************// + // nsIObserver + + observe: function (aSubject, aTopic, aData) { + // Rebuild the list when there are changes to preferences that influence + // whether or not to show certain entries in the list. + if (aTopic == "nsPref:changed" && !this._storingAction) { + // These two prefs alter the list of visible types, so we have to rebuild + // that list when they change. + if (aData == PREF_SHOW_PLUGINS_IN_LIST || + aData == PREF_HIDE_PLUGINS_WITHOUT_EXTENSIONS) { + this._rebuildVisibleTypes(); + this._sortVisibleTypes(); + } + + // All the prefs we observe can affect what we display, so we rebuild + // the view when any of them changes. + this._rebuildView(); + } + }, + + + //**************************************************************************// + // nsIDOMEventListener + + handleEvent: function(aEvent) { + if (aEvent.type == "unload") { + this.destroy(); + } + }, + + + //**************************************************************************// + // Composed Model Construction + + _loadData: function() { + this._loadFeedHandler(); + this._loadPluginHandlers(); + this._loadApplicationHandlers(); + }, + + _loadFeedHandler: function() { + this._handledTypes[TYPE_MAYBE_FEED] = feedHandlerInfo; + feedHandlerInfo.handledOnlyByPlugin = false; + + this._handledTypes[TYPE_MAYBE_VIDEO_FEED] = videoFeedHandlerInfo; + videoFeedHandlerInfo.handledOnlyByPlugin = false; + + this._handledTypes[TYPE_MAYBE_AUDIO_FEED] = audioFeedHandlerInfo; + audioFeedHandlerInfo.handledOnlyByPlugin = false; + }, + + /** + * Load the set of handlers defined by plugins. + * + * Note: if there's more than one plugin for a given MIME type, we assume + * the last one is the one that the application will use. That may not be + * correct, but it's how we've been doing it for years. + * + * Perhaps we should instead query navigator.mimeTypes for the set of types + * supported by the application and then get the plugin from each MIME type's + * enabledPlugin property. But if there's a plugin for a type, we need + * to know about it even if it isn't enabled, since we're going to give + * the user an option to enable it. + * + * I'll also note that my reading of nsPluginTag::RegisterWithCategoryManager + * suggests that enabledPlugin is only determined during registration + * and does not get updated when plugin.disable_full_page_plugin_for_types + * changes (unless modification of that preference spawns reregistration). + * So even if we could use enabledPlugin to get the plugin that would be used, + * we'd still need to check the pref ourselves to find out if it's enabled. + */ + _loadPluginHandlers: function() { + for (let i = 0; i < navigator.plugins.length; ++i) { + let plugin = navigator.plugins[i]; + for (let j = 0; j < plugin.length; ++j) { + let type = plugin[j].type; + + let handlerInfoWrapper; + if (type in this._handledTypes) + handlerInfoWrapper = this._handledTypes[type]; + else { + let wrappedHandlerInfo = + this._mimeSvc.getFromTypeAndExtension(type, null); + handlerInfoWrapper = new HandlerInfoWrapper(type, wrappedHandlerInfo); + handlerInfoWrapper.handledOnlyByPlugin = true; + this._handledTypes[type] = handlerInfoWrapper; + } + + handlerInfoWrapper.plugin = plugin; + } + } + }, + + /** + * Load the set of handlers defined by the application datastore. + */ + _loadApplicationHandlers: function() { + var wrappedHandlerInfos = this._handlerSvc.enumerate(); + while (wrappedHandlerInfos.hasMoreElements()) { + let wrappedHandlerInfo = + wrappedHandlerInfos.getNext().QueryInterface(Ci.nsIHandlerInfo); + let type = wrappedHandlerInfo.type; + + let handlerInfoWrapper; + if (type in this._handledTypes) + handlerInfoWrapper = this._handledTypes[type]; + else { + handlerInfoWrapper = new HandlerInfoWrapper(type, wrappedHandlerInfo); + this._handledTypes[type] = handlerInfoWrapper; + } + + handlerInfoWrapper.handledOnlyByPlugin = false; + } + }, + + + //**************************************************************************// + // View Construction + + _rebuildVisibleTypes: function() { + // Reset the list of visible types and the visible type description counts. + this._visibleTypes = []; + this._visibleTypeDescriptionCount = {}; + + // Get the preferences that help determine what types to show. + var showPlugins = this._prefSvc.getBoolPref(PREF_SHOW_PLUGINS_IN_LIST); + var hidePluginsWithoutExtensions = + this._prefSvc.getBoolPref(PREF_HIDE_PLUGINS_WITHOUT_EXTENSIONS); + + for (let type in this._handledTypes) { + let handlerInfo = this._handledTypes[type]; + + // Hide plugins without associated extensions if so prefed so we don't + // show a whole bunch of obscure types handled by plugins on Mac. + // Note: though protocol types don't have extensions, we still show them; + // the pref is only meant to be applied to MIME types, since plugins are + // only associated with MIME types. + // FIXME: should we also check the "suffixes" property of the plugin? + // Filed as bug 395135. + if (hidePluginsWithoutExtensions && handlerInfo.handledOnlyByPlugin && + handlerInfo.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo && + !handlerInfo.primaryExtension) + continue; + + // Hide types handled only by plugins if so prefed. + if (handlerInfo.handledOnlyByPlugin && !showPlugins) + continue; + + // We couldn't find any reason to exclude the type, so include it. + this._visibleTypes.push(handlerInfo); + + if (handlerInfo.description in this._visibleTypeDescriptionCount) + this._visibleTypeDescriptionCount[handlerInfo.description]++; + else + this._visibleTypeDescriptionCount[handlerInfo.description] = 1; + } + }, + + _rebuildView: function() { + // Clear the list of entries. + while (this._list.childNodes.length > 1) + this._list.removeChild(this._list.lastChild); + + var visibleTypes = this._visibleTypes; + + // If the user is filtering the list, then only show matching types. + if (this._filter.value) + visibleTypes = visibleTypes.filter(this._matchesFilter, this); + + for each (let visibleType in visibleTypes) { + let item = document.createElement("richlistitem"); + item.setAttribute("type", visibleType.type); + item.setAttribute("typeDescription", this._describeType(visibleType)); + if (visibleType.smallIcon) + item.setAttribute("typeIcon", visibleType.smallIcon); + item.setAttribute("actionDescription", + this._describePreferredAction(visibleType)); + + if (!this._setIconClassForPreferredAction(visibleType, item)) { + item.setAttribute("actionIcon", + this._getIconURLForPreferredAction(visibleType)); + } + + this._list.appendChild(item); + } + + this._selectLastSelectedType(); + }, + + _matchesFilter: function(aType) { + var filterValue = this._filter.value.toLowerCase(); + return this._describeType(aType).toLowerCase().indexOf(filterValue) != -1 || + this._describePreferredAction(aType).toLowerCase().indexOf(filterValue) != -1; + }, + + /** + * Describe, in a human-readable fashion, the type represented by the given + * handler info object. Normally this is just the description provided by + * the info object, but if more than one object presents the same description, + * then we annotate the duplicate descriptions with the type itself to help + * users distinguish between those types. + * + * @param aHandlerInfo {nsIHandlerInfo} the type being described + * @returns {string} a description of the type + */ + _describeType: function(aHandlerInfo) { + if (this._visibleTypeDescriptionCount[aHandlerInfo.description] > 1) + return this._prefsBundle.getFormattedString("typeDescriptionWithType", + [aHandlerInfo.description, + aHandlerInfo.type]); + + return aHandlerInfo.description; + }, + + /** + * Describe, in a human-readable fashion, the preferred action to take on + * the type represented by the given handler info object. + * + * XXX Should this be part of the HandlerInfoWrapper interface? It would + * violate the separation of model and view, but it might make more sense + * nonetheless (f.e. it would make sortTypes easier). + * + * @param aHandlerInfo {nsIHandlerInfo} the type whose preferred action + * is being described + * @returns {string} a description of the action + */ + _describePreferredAction: function(aHandlerInfo) { + // alwaysAskBeforeHandling overrides the preferred action, so if that flag + // is set, then describe that behavior instead. For most types, this is + // the "alwaysAsk" string, but for the feed type we show something special. + if (aHandlerInfo.alwaysAskBeforeHandling) { + if (isFeedType(aHandlerInfo.type)) + return this._prefsBundle.getFormattedString("previewInApp", + [this._brandShortName]); + else + return this._prefsBundle.getString("alwaysAsk"); + } + + switch (aHandlerInfo.preferredAction) { + case Ci.nsIHandlerInfo.saveToDisk: + return this._prefsBundle.getString("saveFile"); + + case Ci.nsIHandlerInfo.useHelperApp: + var preferredApp = aHandlerInfo.preferredApplicationHandler; + var name; + if (preferredApp instanceof Ci.nsILocalHandlerApp) + name = getFileDisplayName(preferredApp.executable); + else + name = preferredApp.name; + return this._prefsBundle.getFormattedString("useApp", [name]); + + case Ci.nsIHandlerInfo.handleInternally: + // For the feed type, handleInternally means live bookmarks. + if (isFeedType(aHandlerInfo.type)) + return this._prefsBundle.getFormattedString("addLiveBookmarksInApp", + [this._brandShortName]); + + // For other types, handleInternally looks like either useHelperApp + // or useSystemDefault depending on whether or not there's a preferred + // handler app. + if (this.isValidHandlerApp(aHandlerInfo.preferredApplicationHandler)) + return aHandlerInfo.preferredApplicationHandler.name; + + return aHandlerInfo.defaultDescription; + + // XXX Why don't we say the app will handle the type internally? + // Is it because the app can't actually do that? But if that's true, + // then why would a preferredAction ever get set to this value + // in the first place? + + case Ci.nsIHandlerInfo.useSystemDefault: + return this._prefsBundle.getFormattedString("useDefault", + [aHandlerInfo.defaultDescription]); + + case kActionUsePlugin: + return this._prefsBundle.getFormattedString("usePluginIn", + [aHandlerInfo.plugin.name, + this._brandShortName]); + } + }, + + _selectLastSelectedType: function() { + // If the list is disabled by the pref.downloads.disable_button.edit_actions + // preference being locked, then don't select the type, as that would cause + // it to appear selected, with a different background and an actions menu + // that makes it seem like you can choose an action for the type. + if (this._list.disabled) + return; + + var lastSelectedType = this._list.getAttribute("lastSelectedType"); + if (!lastSelectedType) + return; + + var item = this._list.getElementsByAttribute("type", lastSelectedType)[0]; + if (!item) + return; + + this._list.selectedItem = item; + }, + + /** + * Whether or not the given handler app is valid. + * + * @param aHandlerApp {nsIHandlerApp} the handler app in question + * + * @returns {boolean} whether or not it's valid + */ + isValidHandlerApp: function(aHandlerApp) { + if (!aHandlerApp) + return false; + + if (aHandlerApp instanceof Ci.nsILocalHandlerApp) + return this._isValidHandlerExecutable(aHandlerApp.executable); + + if (aHandlerApp instanceof Ci.nsIWebHandlerApp) + return aHandlerApp.uriTemplate; + + if (aHandlerApp instanceof Ci.nsIWebContentHandlerInfo) + return aHandlerApp.uri; + + return false; + }, + + _isValidHandlerExecutable: function(aExecutable) { + return aExecutable && + aExecutable.exists() && + aExecutable.isExecutable() && +// XXXben - we need to compare this with the running instance executable +// just don't know how to do that via script... +// XXXmano TBD: can probably add this to nsIShellService +#ifdef XP_WIN +#expand aExecutable.leafName != "__MOZ_APP_NAME__.exe"; +#else +#ifdef XP_MACOSX +#expand aExecutable.leafName != "__MOZ_MACBUNDLE_NAME__"; +#else +#expand aExecutable.leafName != "__MOZ_APP_NAME__-bin"; +#endif +#endif + }, + + /** + * Rebuild the actions menu for the selected entry. Gets called by + * the richlistitem constructor when an entry in the list gets selected. + */ + rebuildActionsMenu: function() { + var typeItem = this._list.selectedItem; + var handlerInfo = this._handledTypes[typeItem.type]; + var menu = + document.getAnonymousElementByAttribute(typeItem, "class", "actionsMenu"); + var menuPopup = menu.menupopup; + + // Clear out existing items. + while (menuPopup.hasChildNodes()) + menuPopup.removeChild(menuPopup.lastChild); + + { + var askMenuItem = document.createElement("menuitem"); + askMenuItem.setAttribute("alwaysAsk", "true"); + let label; + if (isFeedType(handlerInfo.type)) + label = this._prefsBundle.getFormattedString("previewInApp", + [this._brandShortName]); + else + label = this._prefsBundle.getString("alwaysAsk"); + askMenuItem.setAttribute("label", label); + askMenuItem.setAttribute("tooltiptext", label); + askMenuItem.setAttribute(APP_ICON_ATTR_NAME, "ask"); + menuPopup.appendChild(askMenuItem); + } + + // Create a menu item for saving to disk. + // Note: this option isn't available to protocol types, since we don't know + // what it means to save a URL having a certain scheme to disk, nor is it + // available to feeds, since the feed code doesn't implement the capability. + if ((handlerInfo.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) && + !isFeedType(handlerInfo.type)) { + var saveMenuItem = document.createElement("menuitem"); + saveMenuItem.setAttribute("action", Ci.nsIHandlerInfo.saveToDisk); + let label = this._prefsBundle.getString("saveFile"); + saveMenuItem.setAttribute("label", label); + saveMenuItem.setAttribute("tooltiptext", label); + saveMenuItem.setAttribute(APP_ICON_ATTR_NAME, "save"); + menuPopup.appendChild(saveMenuItem); + } + + // If this is the feed type, add a Live Bookmarks item. + if (isFeedType(handlerInfo.type)) { + var internalMenuItem = document.createElement("menuitem"); + internalMenuItem.setAttribute("action", Ci.nsIHandlerInfo.handleInternally); + let label = this._prefsBundle.getFormattedString("addLiveBookmarksInApp", + [this._brandShortName]); + internalMenuItem.setAttribute("label", label); + internalMenuItem.setAttribute("tooltiptext", label); + internalMenuItem.setAttribute(APP_ICON_ATTR_NAME, "feed"); + menuPopup.appendChild(internalMenuItem); + } + + // Add a separator to distinguish these items from the helper app items + // that follow them. + let menuItem = document.createElement("menuseparator"); + menuPopup.appendChild(menuItem); + + // Create a menu item for the OS default application, if any. + if (handlerInfo.hasDefaultHandler) { + var defaultMenuItem = document.createElement("menuitem"); + defaultMenuItem.setAttribute("action", Ci.nsIHandlerInfo.useSystemDefault); + let label = this._prefsBundle.getFormattedString("useDefault", + [handlerInfo.defaultDescription]); + defaultMenuItem.setAttribute("label", label); + defaultMenuItem.setAttribute("tooltiptext", handlerInfo.defaultDescription); + defaultMenuItem.setAttribute("image", this._getIconURLForSystemDefault(handlerInfo)); + + menuPopup.appendChild(defaultMenuItem); + } + + // Create menu items for possible handlers. + let preferredApp = handlerInfo.preferredApplicationHandler; + let possibleApps = handlerInfo.possibleApplicationHandlers.enumerate(); + var possibleAppMenuItems = []; + while (possibleApps.hasMoreElements()) { + let possibleApp = possibleApps.getNext(); + if (!this.isValidHandlerApp(possibleApp)) + continue; + + let menuItem = document.createElement("menuitem"); + menuItem.setAttribute("action", Ci.nsIHandlerInfo.useHelperApp); + let label; + if (possibleApp instanceof Ci.nsILocalHandlerApp) + label = getFileDisplayName(possibleApp.executable); + else + label = possibleApp.name; + label = this._prefsBundle.getFormattedString("useApp", [label]); + menuItem.setAttribute("label", label); + menuItem.setAttribute("tooltiptext", label); + menuItem.setAttribute("image", this._getIconURLForHandlerApp(possibleApp)); + + // Attach the handler app object to the menu item so we can use it + // to make changes to the datastore when the user selects the item. + menuItem.handlerApp = possibleApp; + + menuPopup.appendChild(menuItem); + possibleAppMenuItems.push(menuItem); + } + + // Create a menu item for the plugin. + if (handlerInfo.plugin) { + var pluginMenuItem = document.createElement("menuitem"); + pluginMenuItem.setAttribute("action", kActionUsePlugin); + let label = this._prefsBundle.getFormattedString("usePluginIn", + [handlerInfo.plugin.name, + this._brandShortName]); + pluginMenuItem.setAttribute("label", label); + pluginMenuItem.setAttribute("tooltiptext", label); + pluginMenuItem.setAttribute(APP_ICON_ATTR_NAME, "plugin"); + menuPopup.appendChild(pluginMenuItem); + } + + // Create a menu item for selecting a local application. +#ifdef XP_WIN + // On Windows, selecting an application to open another application + // would be meaningless so we special case executables. + var executableType = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService) + .getTypeFromExtension("exe"); + if (handlerInfo.type != executableType) +#endif + { + let menuItem = document.createElement("menuitem"); + menuItem.setAttribute("oncommand", "gApplicationsPane.chooseApp(event)"); + let label = this._prefsBundle.getString("useOtherApp"); + menuItem.setAttribute("label", label); + menuItem.setAttribute("tooltiptext", label); + menuPopup.appendChild(menuItem); + } + + // Create a menu item for managing applications. + if (possibleAppMenuItems.length) { + let menuItem = document.createElement("menuseparator"); + menuPopup.appendChild(menuItem); + menuItem = document.createElement("menuitem"); + menuItem.setAttribute("oncommand", "gApplicationsPane.manageApp(event)"); + menuItem.setAttribute("label", this._prefsBundle.getString("manageApp")); + menuPopup.appendChild(menuItem); + } + + // Select the item corresponding to the preferred action. If the always + // ask flag is set, it overrides the preferred action. Otherwise we pick + // the item identified by the preferred action (when the preferred action + // is to use a helper app, we have to pick the specific helper app item). + if (handlerInfo.alwaysAskBeforeHandling) + menu.selectedItem = askMenuItem; + else switch (handlerInfo.preferredAction) { + case Ci.nsIHandlerInfo.handleInternally: + menu.selectedItem = internalMenuItem; + break; + case Ci.nsIHandlerInfo.useSystemDefault: + menu.selectedItem = defaultMenuItem; + break; + case Ci.nsIHandlerInfo.useHelperApp: + if (preferredApp) + menu.selectedItem = + possibleAppMenuItems.filter(function(v) v.handlerApp.equals(preferredApp))[0]; + break; + case kActionUsePlugin: + menu.selectedItem = pluginMenuItem; + break; + case Ci.nsIHandlerInfo.saveToDisk: + menu.selectedItem = saveMenuItem; + break; + } + }, + + + //**************************************************************************// + // Sorting & Filtering + + _sortColumn: null, + + /** + * Sort the list when the user clicks on a column header. + */ + sort: function (event) { + var column = event.target; + + // If the user clicked on a new sort column, remove the direction indicator + // from the old column. + if (this._sortColumn && this._sortColumn != column) + this._sortColumn.removeAttribute("sortDirection"); + + this._sortColumn = column; + + // Set (or switch) the sort direction indicator. + if (column.getAttribute("sortDirection") == "ascending") + column.setAttribute("sortDirection", "descending"); + else + column.setAttribute("sortDirection", "ascending"); + + this._sortVisibleTypes(); + this._rebuildView(); + }, + + /** + * Sort the list of visible types by the current sort column/direction. + */ + _sortVisibleTypes: function() { + if (!this._sortColumn) + return; + + var t = this; + + function sortByType(a, b) { + return t._describeType(a).toLowerCase(). + localeCompare(t._describeType(b).toLowerCase()); + } + + function sortByAction(a, b) { + return t._describePreferredAction(a).toLowerCase(). + localeCompare(t._describePreferredAction(b).toLowerCase()); + } + + switch (this._sortColumn.getAttribute("value")) { + case "type": + this._visibleTypes.sort(sortByType); + break; + case "action": + this._visibleTypes.sort(sortByAction); + break; + } + + if (this._sortColumn.getAttribute("sortDirection") == "descending") + this._visibleTypes.reverse(); + }, + + /** + * Filter the list when the user enters a filter term into the filter field. + */ + filter: function() { + this._rebuildView(); + }, + + focusFilterBox: function() { + this._filter.focus(); + this._filter.select(); + }, + + + //**************************************************************************// + // Changes + + onSelectAction: function(aActionItem) { + this._storingAction = true; + + try { + this._storeAction(aActionItem); + } + finally { + this._storingAction = false; + } + }, + + _storeAction: function(aActionItem) { + var typeItem = this._list.selectedItem; + var handlerInfo = this._handledTypes[typeItem.type]; + + if (aActionItem.hasAttribute("alwaysAsk")) { + handlerInfo.alwaysAskBeforeHandling = true; + } + else if (aActionItem.hasAttribute("action")) { + let action = parseInt(aActionItem.getAttribute("action")); + + // Set the plugin state if we're enabling or disabling a plugin. + if (action == kActionUsePlugin) + handlerInfo.enablePluginType(); + else if (handlerInfo.plugin && !handlerInfo.isDisabledPluginType) + handlerInfo.disablePluginType(); + + // Set the preferred application handler. + // We leave the existing preferred app in the list when we set + // the preferred action to something other than useHelperApp so that + // legacy datastores that don't have the preferred app in the list + // of possible apps still include the preferred app in the list of apps + // the user can choose to handle the type. + if (action == Ci.nsIHandlerInfo.useHelperApp) + handlerInfo.preferredApplicationHandler = aActionItem.handlerApp; + + // Set the "always ask" flag. + handlerInfo.alwaysAskBeforeHandling = false; + + // Set the preferred action. + handlerInfo.preferredAction = action; + } + + handlerInfo.store(); + + // Make sure the handler info object is flagged to indicate that there is + // now some user configuration for the type. + handlerInfo.handledOnlyByPlugin = false; + + // Update the action label and image to reflect the new preferred action. + typeItem.setAttribute("actionDescription", + this._describePreferredAction(handlerInfo)); + if (!this._setIconClassForPreferredAction(handlerInfo, typeItem)) { + typeItem.setAttribute("actionIcon", + this._getIconURLForPreferredAction(handlerInfo)); + } + }, + + manageApp: function(aEvent) { + // Don't let the normal "on select action" handler get this event, + // as we handle it specially ourselves. + aEvent.stopPropagation(); + + var typeItem = this._list.selectedItem; + var handlerInfo = this._handledTypes[typeItem.type]; + + document.documentElement.openSubDialog("chrome://browser/content/preferences/applicationManager.xul", + "", handlerInfo); + + // Rebuild the actions menu so that we revert to the previous selection, + // or "Always ask" if the previous default application has been removed + this.rebuildActionsMenu(); + + // update the richlistitem too. Will be visible when selecting another row + typeItem.setAttribute("actionDescription", + this._describePreferredAction(handlerInfo)); + if (!this._setIconClassForPreferredAction(handlerInfo, typeItem)) { + typeItem.setAttribute("actionIcon", + this._getIconURLForPreferredAction(handlerInfo)); + } + }, + + chooseApp: function(aEvent) { + // Don't let the normal "on select action" handler get this event, + // as we handle it specially ourselves. + aEvent.stopPropagation(); + + var handlerApp; + +#ifdef XP_WIN + var params = {}; + var handlerInfo = this._handledTypes[this._list.selectedItem.type]; + + if (isFeedType(handlerInfo.type)) { + // MIME info will be null, create a temp object. + params.mimeInfo = this._mimeSvc.getFromTypeAndExtension(handlerInfo.type, + handlerInfo.primaryExtension); + } else { + params.mimeInfo = handlerInfo.wrappedHandlerInfo; + } + + params.title = this._prefsBundle.getString("fpTitleChooseApp"); + params.description = handlerInfo.description; + params.filename = null; + params.handlerApp = null; + + window.openDialog("chrome://global/content/appPicker.xul", null, + "chrome,modal,centerscreen,titlebar,dialog=yes", + params); + + if (this.isValidHandlerApp(params.handlerApp)) { + handlerApp = params.handlerApp; + + // Add the app to the type's list of possible handlers. + handlerInfo.addPossibleApplicationHandler(handlerApp); + } +#else + var fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + var winTitle = this._prefsBundle.getString("fpTitleChooseApp"); + fp.init(window, winTitle, Ci.nsIFilePicker.modeOpen); + fp.appendFilters(Ci.nsIFilePicker.filterApps); + + // Prompt the user to pick an app. If they pick one, and it's a valid + // selection, then add it to the list of possible handlers. + if (fp.show() == Ci.nsIFilePicker.returnOK && fp.file && + this._isValidHandlerExecutable(fp.file)) { + handlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"]. + createInstance(Ci.nsILocalHandlerApp); + handlerApp.name = getFileDisplayName(fp.file); + handlerApp.executable = fp.file; + + // Add the app to the type's list of possible handlers. + let handlerInfo = this._handledTypes[this._list.selectedItem.type]; + handlerInfo.addPossibleApplicationHandler(handlerApp); + } +#endif + + // Rebuild the actions menu whether the user picked an app or canceled. + // If they picked an app, we want to add the app to the menu and select it. + // If they canceled, we want to go back to their previous selection. + this.rebuildActionsMenu(); + + // If the user picked a new app from the menu, select it. + if (handlerApp) { + let typeItem = this._list.selectedItem; + let actionsMenu = + document.getAnonymousElementByAttribute(typeItem, "class", "actionsMenu"); + let menuItems = actionsMenu.menupopup.childNodes; + for (let i = 0; i < menuItems.length; i++) { + let menuItem = menuItems[i]; + if (menuItem.handlerApp && menuItem.handlerApp.equals(handlerApp)) { + actionsMenu.selectedIndex = i; + this.onSelectAction(menuItem); + break; + } + } + } + }, + + // Mark which item in the list was last selected so we can reselect it + // when we rebuild the list or when the user returns to the prefpane. + onSelectionChanged: function() { + if (this._list.selectedItem) + this._list.setAttribute("lastSelectedType", + this._list.selectedItem.getAttribute("type")); + }, + + _setIconClassForPreferredAction: function(aHandlerInfo, aElement) { + // If this returns true, the attribute that CSS sniffs for was set to something + // so you shouldn't manually set an icon URI. + // This removes the existing actionIcon attribute if any, even if returning false. + aElement.removeAttribute("actionIcon"); + + if (aHandlerInfo.alwaysAskBeforeHandling) { + aElement.setAttribute(APP_ICON_ATTR_NAME, "ask"); + return true; + } + + switch (aHandlerInfo.preferredAction) { + case Ci.nsIHandlerInfo.saveToDisk: + aElement.setAttribute(APP_ICON_ATTR_NAME, "save"); + return true; + + case Ci.nsIHandlerInfo.handleInternally: + if (isFeedType(aHandlerInfo.type)) { + aElement.setAttribute(APP_ICON_ATTR_NAME, "feed"); + return true; + } + break; + + case kActionUsePlugin: + aElement.setAttribute(APP_ICON_ATTR_NAME, "plugin"); + return true; + } + aElement.removeAttribute(APP_ICON_ATTR_NAME); + return false; + }, + + _getIconURLForPreferredAction: function(aHandlerInfo) { + switch (aHandlerInfo.preferredAction) { + case Ci.nsIHandlerInfo.useSystemDefault: + return this._getIconURLForSystemDefault(aHandlerInfo); + + case Ci.nsIHandlerInfo.useHelperApp: + let (preferredApp = aHandlerInfo.preferredApplicationHandler) { + if (this.isValidHandlerApp(preferredApp)) + return this._getIconURLForHandlerApp(preferredApp); + } + break; + + // This should never happen, but if preferredAction is set to some weird + // value, then fall back to the generic application icon. + default: + return ICON_URL_APP; + } + }, + + _getIconURLForHandlerApp: function(aHandlerApp) { + if (aHandlerApp instanceof Ci.nsILocalHandlerApp) + return this._getIconURLForFile(aHandlerApp.executable); + + if (aHandlerApp instanceof Ci.nsIWebHandlerApp) + return this._getIconURLForWebApp(aHandlerApp.uriTemplate); + + if (aHandlerApp instanceof Ci.nsIWebContentHandlerInfo) + return this._getIconURLForWebApp(aHandlerApp.uri) + + // We know nothing about other kinds of handler apps. + return ""; + }, + + _getIconURLForFile: function(aFile) { + var fph = this._ioSvc.getProtocolHandler("file"). + QueryInterface(Ci.nsIFileProtocolHandler); + var urlSpec = fph.getURLSpecFromFile(aFile); + + return "moz-icon://" + urlSpec + "?size=16"; + }, + + _getIconURLForWebApp: function(aWebAppURITemplate) { + var uri = this._ioSvc.newURI(aWebAppURITemplate, null, null); + + // Unfortunately we can't use the favicon service to get the favicon, + // because the service looks in the annotations table for a record with + // the exact URL we give it, and users won't have such records for URLs + // they don't visit, and users won't visit the web app's URL template, + // they'll only visit URLs derived from that template (i.e. with %s + // in the template replaced by the URL of the content being handled). + + if (/^https?/.test(uri.scheme) && this._prefSvc.getBoolPref("browser.chrome.favicons")) + return uri.prePath + "/favicon.ico"; + + return ""; + }, + + _getIconURLForSystemDefault: function(aHandlerInfo) { + // Handler info objects for MIME types on some OSes implement a property bag + // interface from which we can get an icon for the default app, so if we're + // dealing with a MIME type on one of those OSes, then try to get the icon. + if ("wrappedHandlerInfo" in aHandlerInfo) { + let wrappedHandlerInfo = aHandlerInfo.wrappedHandlerInfo; + + if (wrappedHandlerInfo instanceof Ci.nsIMIMEInfo && + wrappedHandlerInfo instanceof Ci.nsIPropertyBag) { + try { + let url = wrappedHandlerInfo.getProperty("defaultApplicationIconURL"); + if (url) + return url + "?size=16"; + } + catch(ex) {} + } + } + + // If this isn't a MIME type object on an OS that supports retrieving + // the icon, or if we couldn't retrieve the icon for some other reason, + // then use a generic icon. + return ICON_URL_APP; + } + +}; diff --git a/browser/components/preferences/in-content/applications.xul b/browser/components/preferences/in-content/applications.xul new file mode 100644 index 000000000000..9757ce8f12af --- /dev/null +++ b/browser/components/preferences/in-content/applications.xul @@ -0,0 +1,88 @@ + + +