Bug 568701: Support simultaneous personas and themes application
authorJose E. Bolanos <jose@appcoast.com>
Thu, 29 Jul 2010 11:09:28 -0600
changeset 1313 aeee0b0961a9e8ead3a4320834fb8592c35ddac1
parent 1312 44f126d61cd23781558000e2aa5101de6915be83
child 1314 17f52b01f9d71f4d54452afd0312a6c0bc6447b2
push id844
push userjose@glaxstar.com
push dateThu, 29 Jul 2010 17:09:58 +0000
bugs568701
Bug 568701: Support simultaneous personas and themes application
client/Makefile
client/chrome.manifest.in
client/components/nsPersonasExtensionManager.js
client/content/extensions.css
client/content/extensions.js
client/content/extensions.xml
client/content/extensions.xul
client/locale/en-US/personas.dtd
client/locale/en-US/personas.properties
client/modules/service.js
--- a/client/Makefile
+++ b/client/Makefile
@@ -90,47 +90,47 @@ ifeq ($(channel),dev)
   console_log_level := Debug
   dump_log_level  := Debug
   # Automatically archive chrome in JAR archive when building for this channel.
   jar             := 1
 
 # Release Channel
 else ifeq ($(channel),rel)
   # Release build updates are managed by AMO, which provides its own update.
-  update_name     := 
-  update_url      := 
-  update_url_tag  := 
+  update_name     :=
+  update_url      :=
+  update_url_tag  :=
   package_version := $(version)
   package_name    := $(name)-$(version).xpi
-  package_url     := 
+  package_url     :=
   console_log_level := None
   dump_log_level  := None
   # Automatically archive chrome in JAR archive when building for this channel.
   jar             := 1
 
 # No Channel
 else
   # Builds without a channel don't update.
-  update_name     := 
-  update_url      := 
-  update_url_tag  := 
+  update_name     :=
+  update_url      :=
+  update_url_tag  :=
   package_version := 0
   package_name    := $(name).xpi
-  package_url     := 
+  package_url     :=
   console_log_level := Debug
   dump_log_level  := Debug
 endif
 
 dotin_files       := $(shell find . -type f -name \*.in)
 dotin_files       := $(dotin_files:.in=)
 
 chrome_files      := content/* locale/* skin/*
 
 # FIXME: use a package manifest to determine which files to package.
-package_files     := defaults modules chrome.manifest install.rdf
+package_files     := defaults modules components chrome.manifest install.rdf
 
 ifdef jar
   chrome_path     := jar:chrome.jar!/
   jar_dependency  := chrome.jar
   package_files   += chrome.jar
 else
   chrome_path     :=
   jar_dependency  :=
--- a/client/chrome.manifest.in
+++ b/client/chrome.manifest.in
@@ -1,10 +1,11 @@
-overlay chrome://browser/content/browser.xul      chrome://personas/content/personas.xul   application={ec8030f7-c20a-464f-9b0e-13a3a9e97384}
-overlay chrome://messenger/content/messenger.xul  chrome://personas/content/messenger.xul  application={3550f703-e582-4d05-9a08-453d09bdfdc6}
+overlay chrome://browser/content/browser.xul                chrome://personas/content/personas.xul    application={ec8030f7-c20a-464f-9b0e-13a3a9e97384}
+overlay chrome://mozapps/content/extensions/extensions.xul  chrome://personas/content/extensions.xul  application={ec8030f7-c20a-464f-9b0e-13a3a9e97384}  appversion>=3.6
+overlay chrome://messenger/content/messenger.xul            chrome://personas/content/messenger.xul   application={3550f703-e582-4d05-9a08-453d09bdfdc6}
 
 # Register a resource: protocol alias so we can refer to files in this package
 # with URLs of the form resource://personas/path/to/file,
 # f.e. resource://personas/modules/service.js.
 resource personas   ./
 
 content  personas             @chrome_path@content/
 
new file mode 100755
--- /dev/null
+++ b/client/components/nsPersonasExtensionManager.js
@@ -0,0 +1,468 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Personas.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Jose E. Bolanos <jose@appcoast.com>
+ *   Myk Melez <myk@mozilla.org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+// LightweightThemeManager may not be not available
+// (Firefox < 3.6 and some versions of Thunderbird)
+try { Cu.import("resource://gre/modules/LightweightThemeManager.jsm"); }
+catch (e) { LightweightThemeManager = null; }
+
+const PREF_EM_DSS_ENABLED             = "extensions.dss.enabled";
+const PREF_DSS_SWITCHPENDING          = "extensions.dss.switchPending";
+const PREF_DSS_SKIN_TO_SELECT         = "extensions.lastSelectedSkin";
+const PREF_LWTHEME_TO_SELECT          = "extensions.lwThemeToSelect";
+const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin";
+const PREF_FORCE_SKINNING             = "lightweightThemes.forceSkinning";
+const URI_EXTENSION_MANAGER           = "chrome://mozapps/content/extensions/extensions.xul";
+const FEATURES_EXTENSION_MANAGER      = "chrome,menubar,extra-chrome,toolbar,dialog=no,resizable";
+const PREFIX_NS_EM                    = "http://www.mozilla.org/2004/em-rdf#";
+const PREFIX_ITEM_URI                 = "urn:mozilla:item:";
+
+// ID of the original Extension Manager, "@mozilla.org/extensions/manager;1"
+var EXTENSIONS_MANAGER_ID = "{8A115FAA-7DCB-4e8f-979B-5F53472F51CF}";
+var gOldHandler = null;
+var gRDF = Cc["@mozilla.org/rdf/rdf-service;1"].getService(Ci.nsIRDFService);
+
+const DEFAULT_THEME = "classic/1.0";
+
+//
+// Utility Functions
+//
+
+function stringData(literalOrResource) {
+  if (literalOrResource instanceof Ci.nsIRDFLiteral)
+    return literalOrResource.Value;
+  if (literalOrResource instanceof Ci.nsIRDFResource)
+    return literalOrResource.Value;
+  return undefined;
+}
+
+function intData(literal) {
+  if (literal instanceof Ci.nsIRDFInt)
+    return literal.Value;
+  return undefined;
+}
+
+function getURLSpecFromFile(file) {
+  var ioServ = Cc["@mozilla.org/network/io-service;1"].
+               getService(Ci.nsIIOService);
+  var fph = ioServ.getProtocolHandler("file")
+                  .QueryInterface(Ci.nsIFileProtocolHandler);
+  return fph.getURLSpecFromFile(file);
+}
+
+// Given two instances, copy in all properties from "base"
+// and create forwarding methods for all functions and getters.
+// NOTE: Settable properties are not copied.
+function inheritCurrentInterface(self, base) {
+  function defineGetter(prop) {
+    self.__defineGetter__(prop, function() base[prop]);
+  }
+
+  for(let prop in base) {
+    if(typeof self[prop] === 'undefined')
+      if(typeof base[prop] === 'function') {
+        (function(prop) {
+          self[prop] = function() {
+            return base[prop].apply(base,arguments);
+          };
+        })(prop);
+      }
+      else
+        defineGetter(prop);
+  }
+}
+
+function restartApp() {
+  // Notify all windows that an application quit has been requested.
+  var cancelQuit =
+    Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
+  Observers.notify("quit-application-requested", cancelQuit, "restart");
+
+  // Something aborted the quit process.
+  if (cancelQuit.data)
+    return;
+
+  Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup).
+    quit(Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit);
+}
+
+/**
+ * Overriden Extension Manager object. Handles the lightweight theme observer
+ * notifications to allow personas to be applied over compatible themes.
+ */
+function PersonasExtensionManager() {
+  Cu.import("resource://personas/modules/Observers.js");
+  Cu.import("resource://personas/modules/Preferences.js");
+
+  // Add observers for the lightweight theme topics to override their behavior,
+  // and for the xpcom-shutdown topic to remove them afterwards.
+  Observers.add("xpcom-shutdown", this);
+  Observers.add("lightweight-theme-preview-requested", this);
+  Observers.add("lightweight-theme-change-requested", this);
+}
+
+PersonasExtensionManager.prototype = {
+  classDescription: "Personas Plus Extension Manager",
+  contractID: "@mozilla.org/extensions/manager;1",
+  classID: Components.ID("{DF74077B-EB16-47B0-8C37-12D577A7F1AE}"),
+
+  QueryInterface: function(aIID) {
+    // Retrieve the old nsExtensionsManager anew and then QI to copy the
+    // properties of the requested interface only.
+    delete gOldHandler;
+    gOldHandler =
+      Components.classesByID[EXTENSIONS_MANAGER_ID].
+        getService(Ci.nsIExtensionManager);
+    gOldHandler.QueryInterface(aIID);
+
+    // Remove the original nsExtensionManager listeners for the
+    // lightweight theme topics.
+    Observers.remove("lightweight-theme-preview-requested", gOldHandler);
+    Observers.remove("lightweight-theme-change-requested", gOldHandler);
+
+    inheritCurrentInterface(this, gOldHandler);
+    return this;
+  },
+
+  /* Whether the current theme is compatible with Personas */
+  _currentThemeSkinnable : true,
+
+  /* Personas string bundle */
+  _strings : null,
+  get strings() {
+    if (!this.StringBundle)
+      Cu.import("resource://personas/modules/StringBundle.js", this);
+    if (!this._strings)
+      this._strings = new this.StringBundle("chrome://personas/locale/personas.properties");
+    return this._strings;
+  },
+
+  /**
+   * Overriden method from the original nsExtensionManager.
+   * Handles different observer notifications, and delegates the rest to the
+   * original nsExtensionManager object.
+   */
+  observe: function(subject, topic, data) {
+    let forceSkinning;
+
+    switch (topic) {
+      case "lightweight-theme-preview-requested":
+        forceSkinning = Preferences.get(PREF_FORCE_SKINNING, false);
+
+        // Cancel if a custom theme with no support for personas is set.
+        if (!this._currentThemeSkinnable && !forceSkinning) {
+          let cancel = subject.QueryInterface(Ci.nsISupportsPRBool);
+          cancel.data = true;
+        }
+        break;
+
+      case "lightweight-theme-change-requested":
+        let theme = JSON.parse(data);
+        if (!theme)
+          return;
+
+        // Cancel this topic and prompt to restart only if the persona is being
+        // set over a custom theme with no support for personas.
+        forceSkinning = Preferences.get(PREF_FORCE_SKINNING, false);
+
+        if (!this._currentThemeSkinnable && !forceSkinning) {
+          if (Preferences.get(PREF_EM_DSS_ENABLED, false)) {
+            Preferences.reset(PREF_GENERAL_SKINS_SELECTEDSKIN);
+            return;
+          }
+
+          let cancel = subject.QueryInterface(Ci.nsISupportsPRBool);
+          cancel.data = true;
+
+          Preferences.set(PREF_DSS_SWITCHPENDING, true);
+          Preferences.set(PREF_DSS_SKIN_TO_SELECT, DEFAULT_THEME);
+          Preferences.set(PREF_LWTHEME_TO_SELECT, theme.id);
+
+          // Show notification in the browser to restart.
+          this._showRestartNotification();
+          return;
+        }
+        else {
+          // Cancel any pending theme change and allow the lightweight theme
+          // change to go ahead
+          if (Preferences.isSet(PREF_DSS_SWITCHPENDING))
+            Preferences.reset(PREF_DSS_SWITCHPENDING);
+          if (Preferences.isSet(PREF_DSS_SKIN_TO_SELECT))
+            Preferences.reset(PREF_DSS_SKIN_TO_SELECT);
+        }
+        break;
+
+      case "xpcom-shutdown":
+        // Remove the observers
+        Observers.remove("xpcom-shutdown", this);
+        Observers.remove("lightweight-theme-preview-requested", this);
+        Observers.remove("lightweight-theme-change-requested", this);
+        break;
+
+      case "profile-after-change":
+        try {
+          if (Preferences.get(PREF_DSS_SWITCHPENDING)) {
+            var toSelect = Preferences.get(PREF_DSS_SKIN_TO_SELECT);
+            Preferences.set(PREF_GENERAL_SKINS_SELECTEDSKIN, toSelect);
+            Preferences.reset(PREF_DSS_SWITCHPENDING);
+            Preferences.reset(PREF_DSS_SKIN_TO_SELECT);
+          }
+
+          if (Preferences.isSet(PREF_LWTHEME_TO_SELECT)) {
+            var id = Preferences.get(PREF_LWTHEME_TO_SELECT);
+            if (id) {
+              try {
+                let persona = LightweightThemeManager.getUsedTheme(id);
+                let personas = {};
+                Cu.import("resource://personas/modules/service.js", personas);
+                personas.PersonaService.changeToPersona(persona);
+              } catch (e) {}
+            }
+            else {
+              LightweightThemeManager.currentTheme = null;
+            }
+            Preferences.reset(PREF_LWTHEME_TO_SELECT);
+          }
+        }
+        catch (e) {}
+
+        // Let the original nsExtensionManager perform actions
+        // during "profile-after-change".
+        gOldHandler.observe(subject, topic, data);
+        // Load current theme properties, e.g. "skinnable" property.
+        this._loadThemeProperties();
+        break;
+
+      default:
+        // Let the original nsExtensionManager handle the event.
+        gOldHandler.observe(subject, topic, data);
+        break;
+    }
+  },
+
+  /**
+   * Shows a notification in the browser informing to the user to restart it so
+   * the persona can be applied.
+   */
+  _showRestartNotification : function() {
+    // Obtain most recent window and its notification box
+    let wm =
+      Cc["@mozilla.org/appshell/window-mediator;1"].
+        getService(Ci.nsIWindowMediator);
+
+    let notificationBox =
+      wm.getMostRecentWindow("navigator:browser").
+        getBrowser().getNotificationBox();
+
+    // If there is another notification of the same kind already, remove it.
+    let oldNotification =
+      notificationBox.getNotificationWithValue("lwtheme-restart-notification");
+    if (oldNotification)
+      notificationBox.removeNotification(oldNotification);
+
+    let restartButton = {
+      label     : this.strings.get("notification.restartButton.label"),
+      accessKey : this.strings.get("notification.restartButton.accesskey"),
+      popup     : null,
+      callback  : restartApp
+    };
+
+    let notificationBar =
+      notificationBox.appendNotification(
+        this.strings.get("notification.restartToApply"),
+        "lwtheme-restart-notification", null,
+        notificationBox.PRIORITY_INFO_HIGH, [ restartButton ] );
+    notificationBar.persistence = 1;
+  },
+
+  /**
+   * Loads the current theme properties (i.e. whether it is skinnable).
+   */
+  _loadThemeProperties : function() {
+    // The following code determines whether the current theme is skinnable.
+    // This is true when the current theme is the default one, classic/1.0, or
+    // when the theme has the "skinnable" property set to true in its install.rdf.
+    let currentTheme = Preferences.get(PREF_GENERAL_SKINS_SELECTEDSKIN);
+
+    if (currentTheme == DEFAULT_THEME)
+      this._currentThemeSkinnable = true;
+    else {
+      // Find the current theme and load its install.rdf to read its
+      // "skinnable" property.
+      let themes = this.getItemList(Ci.nsIUpdateItem.TYPE_THEME, { });
+      for (let i = 0; i < themes.length; i++) {
+        let theme = themes[i];
+
+        let internalName = this._getItemProperty(theme.id, "internalName");
+        if (internalName == currentTheme) {
+          this._loadThemeSkinnableProperty(theme.id);
+          break;
+        }
+      }
+    }
+  },
+
+  /**
+   * Loads the "skinnable" property of an installed theme.
+   * @param aThemeId The Id of the theme.
+   */
+  _loadThemeSkinnableProperty : function(aThemeId) {
+    let t = this;
+    let onInstallRDFLoaded = function(aDatasource) {
+      let skinnable = t._getInstallRDFProperty(aDatasource, "skinnable");
+      t._currentThemeSkinnable = (skinnable === "true");
+
+      if (!t._currentThemeSkinnable) {
+        if (!Preferences.get(PREF_FORCE_SKINNING, false)) {
+          LightweightThemeManager.currentTheme = null;
+
+          try {
+            let personas = {};
+            Cu.import("resource://personas/modules/service.js", personas);
+            personas.PersonaService.changeToDefaultPersona();
+          } catch (e) {}
+        }
+      }
+    };
+
+    let location = this.getInstallLocation(aThemeId);
+    let file = location.getItemFile(aThemeId, "install.rdf");
+    this._loadDatasource(file, onInstallRDFLoaded);
+  },
+
+  /**
+   * Loads an RDF datasource file.
+   * @param aDatasourceFile The file path of the datasource.
+   * @param aLoadCallback Callback used to notify the caller when the datasource
+   * has finished loading.
+   */
+  _loadDatasource : function(aDatasourceFile, aLoadCallback) {
+    let ds = gRDF.GetDataSource(getURLSpecFromFile(aDatasourceFile));
+    let remote = ds.QueryInterface(Ci.nsIRDFRemoteDataSource);
+
+    if (remote.loaded)
+      aLoadCallback(ds);
+    else {
+      let observer = {
+        onBeginLoad: function(aSink) {},
+        onInterrupt: function(aSink) {},
+        onResume: function(aSink) {},
+        onError: function(aSink, aStatus, aErrorMsg) {},
+        onEndLoad: function(aSink) {
+          aSink.removeXMLSinkObserver(this);
+          aLoadCallback(ds);
+        }
+      };
+
+      let sink = ds.QueryInterface(Ci.nsIRDFXMLSink);
+      sink.addXMLSinkObserver(observer);
+    }
+  },
+
+  /**
+   * Gets a property of an item (theme) from extensions.rdf
+   * @param aItemId The id of the item.
+   * @param aPropertyName The name of the property to get.
+   * @return The value of the property, or undefined if not found.
+   */
+  _getItemProperty : function(aItemId, aPropertyName) {
+    return this._getDatasourceProperty(
+      this.datasource,
+      PREFIX_ITEM_URI + aItemId,
+      PREFIX_NS_EM + aPropertyName);
+  },
+
+  /**
+   * Gets a property of the given extension install.rdf datasource.
+   * @param aDatasource The install.rdf datasource.
+   * @param aPropertyName The name of the property to get.
+   * @return The value of the property, or undefined if not found.
+   */
+  _getInstallRDFProperty : function(aDatasource, aPropertyName) {
+    return this._getDatasourceProperty(
+      aDatasource,
+      "urn:mozilla:install-manifest",
+      PREFIX_NS_EM + aPropertyName);
+  },
+
+  /**
+   * Gets the value of a property from the given datasource.
+   * @param aDatasource The datasource from which to get the property.
+   * @param aResourceName The name of the resource which contains the property
+   * within the datasource.
+   * @param aPropertyName The name of the property to get.
+   * @return The value of the property, or undefined if not found.
+   */
+  _getDatasourceProperty : function(aDatasource, aResourceName, aPropertyName) {
+    let resource = gRDF.GetResource(aResourceName);
+    let property = gRDF.GetResource(aPropertyName);
+
+    if (!resource || !property)
+      return undefined;
+
+    let target = aDatasource.GetTarget(resource, property, true);
+    let value = stringData(target);
+    if (value === undefined)
+      value = intData(target);
+    return value === undefined ? "" : value;
+  }
+};
+
+var components = [];
+
+// Register this component only on Firefox 3.6.*
+const FIREFOX_ID = "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}";
+var appInfo = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo);
+var versionChecker =
+  Cc["@mozilla.org/xpcom/version-comparator;1"].getService(Ci.nsIVersionComparator);
+
+if (appInfo.ID == FIREFOX_ID &&
+    versionChecker.compare(appInfo.version, "3.6") >= 0 &&
+    versionChecker.compare(appInfo.version, "3.6.*") < 0) {
+  components = [PersonasExtensionManager];
+}
+
+function NSGetModule(compMgr, fileSpec) {
+  return XPCOMUtils.generateModule(components);
+}
new file mode 100644
--- /dev/null
+++ b/client/content/extensions.css
@@ -0,0 +1,39 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Personas.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Jose E. Bolanos <jose@appcoast.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+richlistitem[selected="true"][lwtheme="true"] {
+  -moz-binding: url("chrome://personas/content/extensions.xml#persona-selected");
+}
new file mode 100644
--- /dev/null
+++ b/client/content/extensions.js
@@ -0,0 +1,817 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Personas.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Jose E. Bolanos <jose@appcoast.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+// Run this overlay only on Firefox 3.6.*
+const FIREFOX_ID = "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}";
+var appInfo =
+  Components.classes["@mozilla.org/xre/app-info;1"].
+    getService(Components.interfaces.nsIXULAppInfo);
+var versionChecker =
+  Components.classes["@mozilla.org/xpcom/version-comparator;1"].
+    getService(Components.interfaces.nsIVersionComparator);
+
+if (appInfo.ID == FIREFOX_ID &&
+    versionChecker.compare(appInfo.version, "3.6") >= 0 &&
+    versionChecker.compare(appInfo.version, "3.6.*") < 0)
+{
+  // Load the Personas string bundle, used to set the label of the default persona.
+  Components.utils.import("resource://personas/modules/StringBundle.js", gExtensionsViewController);
+  gExtensionsViewController.getStrings = function() {
+    delete this._strings;
+    return this._strings = new this.StringBundle("chrome://personas/locale/personas.properties");
+  };
+
+  const DEFAULT_PERSONA_ID = 0;
+  const CUSTOM_PERSONA_ID = 1;
+
+  /**
+   * Overriden method from Firefox extensions.js. Enables cmd_useTheme for
+   * Persona entries.
+   */
+  gExtensionsViewController.isCommandEnabled = function (aCommand) {
+    var selectedItem = gExtensionsView.selectedItem;
+    if (!selectedItem)
+      return false;
+
+    if (selectedItem.hasAttribute("downloadURL") &&
+        selectedItem.getAttribute("downloadURL") != "") {
+      if (aCommand == "cmd_uninstall")
+        return true;
+      return false;
+    }
+    switch (aCommand) {
+    case "cmd_installSearchResult":
+      return selectedItem.getAttribute("action") == "" ||
+             selectedItem.getAttribute("action") == "failed";
+
+    // Changed from original extensions.js
+    // Enables the cmd_useTheme command for the "Default Persona"
+    // when another Persona is set.
+    case "cmd_useTheme":
+      if (selectedItem.hasAttribute("lwtheme")) {
+        let personaId = selectedItem.getAttribute("addonID");
+        if (personaId == DEFAULT_PERSONA_ID)
+          return (gLWThemeToSelect != null);
+        return (!gLWThemeToSelect || personaId != gLWThemeToSelect.id);
+      }
+      return selectedItem.type == nsIUpdateItem.TYPE_THEME &&
+             !selectedItem.isDisabled &&
+             selectedItem.opType != OP_NEEDS_UNINSTALL &&
+             gThemeToSelect != selectedItem.getAttribute("internalName");
+
+    case "cmd_options":
+      return selectedItem.type == nsIUpdateItem.TYPE_EXTENSION &&
+             !selectedItem.isDisabled &&
+             !gInSafeMode &&
+             !selectedItem.opType &&
+             selectedItem.getAttribute("optionsURL") != "";
+
+    // Changed from original extensions.js
+    // Disables the cmd_About command for the "Default Persona"
+    case "cmd_about":
+      if (selectedItem.hasAttribute("lwtheme"))
+        return (selectedItem.getAttribute("addonID") != DEFAULT_PERSONA_ID);
+      return selectedItem.opType != OP_NEEDS_INSTALL &&
+             selectedItem.getAttribute("plugin") != "true";
+
+    case "cmd_homepage":
+      return selectedItem.getAttribute("homepageURL") != "";
+
+    // Changed from original extensions.js
+    // Disables the cmd_uninstall command for the "Default Persona"
+    case "cmd_uninstall":
+      if (selectedItem.hasAttribute("lwtheme") &&
+          selectedItem.getAttribute("addonID") != DEFAULT_PERSONA_ID)
+        return true;
+      return (selectedItem.type != nsIUpdateItem.TYPE_THEME ||
+             selectedItem.type == nsIUpdateItem.TYPE_THEME &&
+             selectedItem.getAttribute("internalName") != gDefaultTheme) &&
+             selectedItem.opType != OP_NEEDS_UNINSTALL &&
+             selectedItem.getAttribute("locked") != "true" &&
+             canWriteToLocation(selectedItem) &&
+             !gExtensionsView.hasAttribute("update-operation");
+
+    case "cmd_cancelUninstall":
+      return selectedItem.opType == OP_NEEDS_UNINSTALL;
+    case "cmd_cancelInstall":
+      return selectedItem.getAttribute("action") == "installed" &&
+             gView == "search" || selectedItem.opType == OP_NEEDS_INSTALL;
+    case "cmd_cancelUpgrade":
+      return selectedItem.opType == OP_NEEDS_UPGRADE;
+    case "cmd_checkUpdate":
+      return selectedItem.getAttribute("updateable") != "false" &&
+             !gExtensionsView.hasAttribute("update-operation");
+    case "cmd_installUpdate":
+      return selectedItem.hasAttribute("availableUpdateURL") &&
+             !gExtensionsView.hasAttribute("update-operation");
+    case "cmd_includeUpdate":
+      return selectedItem.hasAttribute("availableUpdateURL") &&
+             !gExtensionsView.hasAttribute("update-operation");
+    case "cmd_reallyEnable":
+    // controls whether to show Enable or Disable in extensions' context menu
+      return selectedItem.isDisabled &&
+             selectedItem.opType != OP_NEEDS_ENABLE ||
+             selectedItem.opType == OP_NEEDS_DISABLE;
+    case "cmd_enable":
+      return selectedItem.type != nsIUpdateItem.TYPE_THEME &&
+             (selectedItem.isDisabled ||
+             (!selectedItem.opType ||
+             selectedItem.opType == OP_NEEDS_DISABLE)) &&
+             !selectedItem.isBlocklisted &&
+             (!gCheckUpdateSecurity || selectedItem.providesUpdatesSecurely) &&
+             (!gCheckCompat || selectedItem.isCompatible) &&
+             selectedItem.satisfiesDependencies &&
+             !gExtensionsView.hasAttribute("update-operation");
+    case "cmd_disable":
+      return selectedItem.type != nsIUpdateItem.TYPE_THEME &&
+             (!selectedItem.isDisabled &&
+             !selectedItem.opType ||
+             selectedItem.opType == OP_NEEDS_ENABLE) &&
+             !selectedItem.isBlocklisted &&
+             selectedItem.satisfiesDependencies &&
+             !gExtensionsView.hasAttribute("update-operation");
+    }
+    return false;
+  };
+
+  /**
+   * Overriden method from Firefox extensions.js. Removes the restriction which
+   * removed personas when themes were applied.
+   */
+  gExtensionsViewController.commands.cmd_useTheme = function (aSelectedItem) {
+    if (aSelectedItem.hasAttribute("lwtheme")) {
+      let newTheme = LightweightThemeManager.getUsedTheme(aSelectedItem.getAttribute("addonID"));
+
+      if (newTheme && newTheme.id == CUSTOM_PERSONA_ID &&
+          LightweightThemeManager.setLocalTheme) {
+        LightweightThemeManager.setLocalTheme(newTheme);
+        gLWThemeToSelect = newTheme;
+      }
+      else
+        LightweightThemeManager.currentTheme = gLWThemeToSelect = newTheme;
+
+      if (gPref.prefHasUserValue(PREF_LWTHEME_TO_SELECT)) {
+        clearRestartMessage();
+        setRestartMessage(aSelectedItem);
+      }
+    }
+    else {
+      gThemeToSelect = aSelectedItem.getAttribute("internalName");
+
+      // If choosing the current skin just reset the pending change
+      if (gThemeToSelect == gCurrentTheme) {
+        if (gPref.prefHasUserValue(PREF_EXTENSIONS_DSS_SWITCHPENDING))
+          gPref.clearUserPref(PREF_EXTENSIONS_DSS_SWITCHPENDING);
+        if (gPref.prefHasUserValue(PREF_DSS_SKIN_TO_SELECT))
+          gPref.clearUserPref(PREF_DSS_SKIN_TO_SELECT);
+        clearRestartMessage();
+      }
+      else {
+        if (gPref.getBoolPref(PREF_EXTENSIONS_DSS_ENABLED)) {
+          gPref.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, gThemeToSelect);
+        }
+        else {
+          // Theme change will happen on next startup, this flag tells
+          // the Theme Manager that it needs to show "This theme will
+          // be selected after a restart" text in the selected theme
+          // item.
+          gPref.setBoolPref(PREF_EXTENSIONS_DSS_SWITCHPENDING, true);
+          gPref.setCharPref(PREF_DSS_SKIN_TO_SELECT, gThemeToSelect);
+          clearRestartMessage();
+          setRestartMessage(aSelectedItem);
+        }
+      }
+    }
+
+    // Flush preference change to disk
+    gPref.QueryInterface(Components.interfaces.nsIPrefService)
+         .savePrefFile(null);
+
+    // disable the useThemeButton
+    gExtensionsViewController.onCommandUpdate();
+  };
+
+  /**
+   * Overriden method from Firefox extensions.js. Includes an additional
+   * persona named "Default", used to remove all personas without removing the
+   * current theme.
+   */
+  function rebuildLWThemeDS() {
+    var rdfCU = Components.classes["@mozilla.org/rdf/container-utils;1"]
+                          .getService(Components.interfaces.nsIRDFContainerUtils);
+    var rootctr = rdfCU.MakeSeq(gLWThemeDS, gRDF.GetResource(RDFURI_ITEM_ROOT));
+    var themes = LightweightThemeManager.usedThemes;
+
+    // Changed from original extensions.js
+    // Manually add a fake persona called "Default", which is used to remove
+    // any persona and leave the current theme intact.
+    let strings = gExtensionsViewController.getStrings();
+
+    let defaultTheme = {
+      "id":DEFAULT_PERSONA_ID,
+      "name":strings.get("Default"),
+      "accentcolor":null,
+      "textcolor":null,
+      "header":null,
+      "footer":null,
+      "category":null,
+      "description":null,
+      "author":null,
+      "username":null,
+      "detailURL":null,
+      "headerURL":null,
+      "footerURL":null,
+      "previewURL":null,
+      "iconURL":"data:image/gif;base64,R0lGODlhIwAjAJEAAKGhoaGhoaGhoaGhoSwAAAAA" +
+                "IwAjAAACIZSPqcvtD6OctNqLs968+w+G4kiW5omm6sq27gvH8kzXBQA7",
+      "dataurl":null
+    };
+    themes.unshift(defaultTheme);
+
+    // Running in a batch stops the template builder from running
+    gLWThemeDS.beginUpdateBatch();
+
+    cleanDataSource(gLWThemeDS, rootctr);
+
+    for (var i = 0; i < themes.length; i++) {
+      var theme = themes[i];
+
+      if (!("id" in theme))
+        continue;
+
+      var themeNode = gRDF.GetResource(PREFIX_LWTHEME_URI + theme.id);
+      rootctr.AppendElement(themeNode);
+      gLWThemeDS.Assert(themeNode,
+                        gRDF.GetResource(PREFIX_NS_EM + "name"),
+                        gRDF.GetLiteral(theme.name || ""),
+                        true);
+      gLWThemeDS.Assert(themeNode,
+                        gRDF.GetResource(PREFIX_NS_EM + "addonID"),
+                        gRDF.GetLiteral(theme.id),
+                        true);
+      gLWThemeDS.Assert(themeNode,
+                        gRDF.GetResource(PREFIX_NS_EM + "isDisabled"),
+                        gRDF.GetLiteral("false"),
+                        true);
+      gLWThemeDS.Assert(themeNode,
+                        gRDF.GetResource(PREFIX_NS_EM + "blocklisted"),
+                        gRDF.GetLiteral("false"),
+                        true);
+      gLWThemeDS.Assert(themeNode,
+                        gRDF.GetResource(PREFIX_NS_EM + "blocklistedsoft"),
+                        gRDF.GetLiteral("false"),
+                        true);
+      gLWThemeDS.Assert(themeNode,
+                        gRDF.GetResource(PREFIX_NS_EM + "compatible"),
+                        gRDF.GetLiteral("true"),
+                        true);
+      gLWThemeDS.Assert(themeNode,
+                        gRDF.GetResource(PREFIX_NS_EM + "lwtheme"),
+                        gRDF.GetLiteral("true"),
+                        true);
+      gLWThemeDS.Assert(themeNode,
+                        gRDF.GetResource(PREFIX_NS_EM + "type"),
+                        gRDF.GetIntLiteral(nsIUpdateItem.TYPE_THEME),
+                        true);
+      if (theme.author) {
+        gLWThemeDS.Assert(themeNode,
+                          gRDF.GetResource(PREFIX_NS_EM + "description"),
+                          gRDF.GetLiteral(getExtensionString("lightweightThemeDescription",
+                                                             [theme.author])),
+                          true);
+        gLWThemeDS.Assert(themeNode,
+                          gRDF.GetResource(PREFIX_NS_EM + "creator"),
+                          gRDF.GetLiteral(theme.author),
+                          true);
+      }
+      if (theme.description) {
+        gLWThemeDS.Assert(themeNode,
+                          gRDF.GetResource(PREFIX_NS_EM + "lwdescription"),
+                          gRDF.GetLiteral(theme.description),
+                          true);
+      }
+      if (theme.homepageURL) {
+        gLWThemeDS.Assert(themeNode,
+                          gRDF.GetResource(PREFIX_NS_EM + "homepageURL"),
+                          gRDF.GetLiteral(theme.homepageURL),
+                          true);
+      }
+      if (theme.previewURL) {
+        gLWThemeDS.Assert(themeNode,
+                          gRDF.GetResource(PREFIX_NS_EM + "previewImage"),
+                          gRDF.GetLiteral(theme.previewURL),
+                          true);
+      }
+      if (theme.iconURL) {
+        gLWThemeDS.Assert(themeNode,
+                          gRDF.GetResource(PREFIX_NS_EM + "iconURL"),
+                          gRDF.GetLiteral(theme.iconURL),
+                          true);
+      }
+    }
+
+    gLWThemeDS.endUpdateBatch();
+  }
+
+  /**
+   * Overriden method from Firefox extensions.js. Adjusts the code to allow for
+   * a new view in the Addons window, separating themes from personas.
+   */
+  function showView(aView) {
+    if (gView == aView)
+      return;
+
+    updateLastSelected(aView);
+    gView = aView;
+
+    // Using disabled to represent add-on state in regards to the EM causes evil
+    // focus behavior when used as an element attribute when the element isn't
+    // really disabled.
+    var bindingList = [ [ ["aboutURL", "?aboutURL"],
+                          ["addonID", "?addonID"],
+                          ["availableUpdateURL", "?availableUpdateURL"],
+                          ["availableUpdateVersion", "?availableUpdateVersion"],
+                          ["blocklisted", "?blocklisted"],
+                          ["blocklistedsoft", "?blocklistedsoft"],
+                          ["outdated", "?outdated"],
+                          ["compatible", "?compatible"],
+                          ["description", "?description"],
+                          ["downloadURL", "?downloadURL"],
+                          ["isDisabled", "?isDisabled"],
+                          ["homepageURL", "?homepageURL"],
+                          ["iconURL", "?iconURL"],
+                          ["internalName", "?internalName"],
+                          ["locked", "?locked"],
+                          ["lwtheme", "?lwtheme"],
+                          ["name", "?name"],
+                          ["optionsURL", "?optionsURL"],
+                          ["opType", "?opType"],
+                          ["plugin", "?plugin"],
+                          ["previewImage", "?previewImage"],
+                          ["satisfiesDependencies", "?satisfiesDependencies"],
+                          ["providesUpdatesSecurely", "?providesUpdatesSecurely"],
+                          ["type", "?type"],
+                          ["updateable", "?updateable"],
+                          ["updateURL", "?updateURL"],
+                          ["version", "?version"] ] ];
+    var displays = [ "richlistitem" ];
+    var direction = "ascending";
+
+    var prefURL;
+    var showInstallFile = true;
+    try {
+      showInstallFile = !gPref.getBoolPref(PREF_EXTENSIONS_HIDE_INSTALL_BTN);
+    }
+    catch (e) { }
+    var showCheckUpdatesAll = true;
+    var showInstallUpdatesAll = false;
+    var showSkip = false;
+    switch (aView) {
+      case "search":
+        var bindingList = [ [ ["action", "?action"],
+                              ["addonID", "?addonID"],
+                              ["description", "?description"],
+                              ["eula", "?eula"],
+                              ["homepageURL", "?homepageURL"],
+                              ["iconURL", "?iconURL"],
+                              ["name", "?name"],
+                              ["previewImage", "?previewImage"],
+                              ["rating", "?rating"],
+                              ["addonType", "?addonType"],
+                              ["thumbnailURL", "?thumbnailURL"],
+                              ["version", "?version"],
+                              ["xpiHash", "?xpiHash"],
+                              ["xpiURL", "?xpiURL"],
+                              ["typeName", "searchResult"] ],
+                          [ ["type", "?type"],
+                              ["typeName", "status"],
+                              ["count", "?count"],
+                              ["link", "?link" ] ] ];
+        var types = [ [ ["searchResult", "true", null] ],
+                      [ ["statusMessage", "true", null] ] ];
+        var displays = [ "richlistitem", "vbox" ];
+        direction = "natural";
+        showCheckUpdatesAll = false;
+        document.getElementById("searchfield").disabled = isOffline("offlineSearchMsg");
+        break;
+      case "extensions":
+        prefURL = PREF_EXTENSIONS_GETMOREEXTENSIONSURL;
+        types = [ [ ["type", nsIUpdateItem.TYPE_EXTENSION, "Integer"] ] ];
+        break;
+
+      // Changed from original extensions.js
+      // Separating themes from personas using the internalName and lwtheme
+      // attributes, respectively.
+      case "themes":
+        prefURL = PREF_EXTENSIONS_GETMORETHEMESURL;
+        types = [ [ ["type", nsIUpdateItem.TYPE_THEME, "Integer"],
+                    ["internalName", "?internalName", null] ] ];
+        break;
+      case "personas":
+        prefURL = null;
+        types = [ [ ["type", nsIUpdateItem.TYPE_THEME, "Integer"],
+                    ["lwtheme", "true", null] ] ];
+        break;
+
+      case "locales":
+        types = [ [ ["type", nsIUpdateItem.TYPE_LOCALE, "Integer"] ] ];
+        break;
+      case "plugins":
+        prefURL = PREF_EXTENSIONS_GETMOREPLUGINSURL;
+        types = [ [ ["plugin", "true", null] ] ];
+        if (!gPluginUpdateUrl)
+          showCheckUpdatesAll = false;
+        break;
+      case "updates":
+        document.getElementById("updates-view").hidden = false;
+        showInstallFile = false;
+        showCheckUpdatesAll = false;
+        showInstallUpdatesAll = true;
+        if (gUpdatesOnly)
+          showSkip = true;
+        bindingList = [ [ ["aboutURL", "?aboutURL"],
+                          ["availableUpdateURL", "?availableUpdateURL"],
+                          ["availableUpdateVersion", "?availableUpdateVersion"],
+                          ["availableUpdateInfo", "?availableUpdateInfo"],
+                          ["blocklisted", "?blocklisted"],
+                          ["blocklistedsoft", "?blocklistedsoft"],
+                          ["homepageURL", "?homepageURL"],
+                          ["iconURL", "?iconURL"],
+                          ["internalName", "?internalName"],
+                          ["locked", "?locked"],
+                          ["name", "?name"],
+                          ["opType", "?opType"],
+                          ["previewImage", "?previewImage"],
+                          ["satisfiesDependencies", "?satisfiesDependencies"],
+                          ["providesUpdatesSecurely", "?providesUpdatesSecurely"],
+                          ["type", "?type"],
+                          ["updateURL", "?updateURL"],
+                          ["version", "?version"],
+                          ["typeName", "update"] ] ];
+        types = [ [ ["availableUpdateVersion", "?availableUpdateVersion", null],
+                    ["updateable", "true", null] ] ];
+        break;
+      case "installs":
+        document.getElementById("installs-view").hidden = false;
+        showInstallFile = false;
+        showCheckUpdatesAll = false;
+        showInstallUpdatesAll = false;
+        bindingList = [ [ ["aboutURL", "?aboutURL"],
+                          ["addonID", "?addonID"],
+                          ["availableUpdateURL", "?availableUpdateURL"],
+                          ["availableUpdateVersion", "?availableUpdateVersion"],
+                          ["blocklisted", "?blocklisted"],
+                          ["blocklistedsoft", "?blocklistedsoft"],
+                          ["compatible", "?compatible"],
+                          ["description", "?description"],
+                          ["downloadURL", "?downloadURL"],
+                          ["incompatibleUpdate", "?incompatibleUpdate"],
+                          ["isDisabled", "?isDisabled"],
+                          ["homepageURL", "?homepageURL"],
+                          ["iconURL", "?iconURL"],
+                          ["internalName", "?internalName"],
+                          ["locked", "?locked"],
+                          ["name", "?name"],
+                          ["optionsURL", "?optionsURL"],
+                          ["opType", "?opType"],
+                          ["previewImage", "?previewImage"],
+                          ["progress", "?progress"],
+                          ["state", "?state"],
+                          ["type", "?type"],
+                          ["updateable", "?updateable"],
+                          ["updateURL", "?updateURL"],
+                          ["version", "?version"],
+                          ["newVersion", "?newVersion"],
+                          ["typeName", "install"] ] ];
+        types = [ [ ["state", "?state", null] ] ];
+        break;
+    }
+
+    var showGetMore = false;
+    var getMore = document.getElementById("getMore");
+    if (prefURL && gPref.getPrefType(prefURL) != nsIPrefBranch2.PREF_INVALID) {
+      try {
+        getMore.setAttribute("value", getMore.getAttribute("value" + aView));
+        var getMoreURL = Components.classes["@mozilla.org/toolkit/URLFormatterService;1"]
+                                   .getService(Components.interfaces.nsIURLFormatter)
+                                   .formatURLPref(prefURL);
+        getMore.setAttribute("getMoreURL", getMoreURL);
+        showGetMore = getMoreURL == "about:blank" ? false : true;
+      }
+      catch (e) { }
+    }
+    getMore.hidden = !showGetMore;
+
+    // Changed from original extensions.js
+    // Include the personas view in the isThemes flag, in order to show
+    // the preview area.
+    var isThemes = (aView == "themes" || aView == "personas");
+
+    // Changed from original extensions.js
+    // Include the personas view in the following block to set the correct
+    // tooltip attribute.
+    if (aView == "themes" || aView == "personas" ||
+        aView == "extensions" || aView == "plugins") {
+      var tooltipAttr = "";
+      if (aView == "extensions")
+        tooltipAttr = "tooltiptextaddons";
+      else if (aView == "personas")
+        tooltipAttr = "tooltiptextthemes";
+      else
+        tooltipAttr = "tooltiptext" + aView;
+
+      var el = document.getElementById("checkUpdatesAllButton");
+      el.setAttribute("tooltiptext", el.getAttribute(tooltipAttr));
+      if (aView != "plugins") {
+        el = document.getElementById("installFileButton");
+        el.setAttribute("tooltiptext", el.getAttribute(tooltipAttr));
+      }
+    }
+
+    document.getElementById("installFileButton").hidden = !showInstallFile;
+    document.getElementById("checkUpdatesAllButton").hidden = !showCheckUpdatesAll;
+    document.getElementById("installUpdatesAllButton").hidden = !showInstallUpdatesAll;
+    document.getElementById("skipDialogButton").hidden = !showSkip;
+    document.getElementById("themePreviewArea").hidden = !isThemes;
+    document.getElementById("themeSplitter").hidden = !isThemes;
+    document.getElementById("showUpdateInfoButton").hidden = aView != "updates";
+    document.getElementById("hideUpdateInfoButton").hidden = true;
+    document.getElementById("searchPanel").hidden = aView != "search";
+
+    gExtensionsView.setAttribute("sortDirection", direction);
+    AddonsViewBuilder.updateView(types, displays, bindingList, null);
+
+    if (aView == "updates" || aView == "installs")
+      gExtensionsView.selectedItem = gExtensionsView.children[0];
+    else if (isThemes)
+      gExtensionsView.selectedItem = getActivedThemeItem();
+
+    if (showSkip) {
+      var button = document.getElementById("installUpdatesAllButton");
+      button.setAttribute("default", "true");
+      window.setTimeout(function () { button.focus(); }, 0);
+    } else
+      document.getElementById("installUpdatesAllButton").removeAttribute("default");
+
+    if (isThemes)
+      onAddonSelect();
+    updateGlobalCommands();
+  }
+
+  /**
+   * Overriden method from Firefox extensions.js. Allows the double-click to
+   * work for personas.
+   */
+  function onViewDoubleClick(aEvent) {
+    if (aEvent.button != 0 || !gExtensionsView.selectedItem)
+      return;
+
+    switch (gView) {
+      case "extensions":
+        gExtensionsViewController.doCommand('cmd_options');
+        break;
+      // Changed from original extensions.js
+      // Include the personas view in the double-click command.
+      case "themes":
+      case "personas":
+        gExtensionsViewController.doCommand('cmd_useTheme');
+        break;
+      case "updates":
+        gExtensionsViewController.doCommand('cmd_includeUpdate');
+        break;
+    }
+  }
+
+  // Changed from original extensions.js
+  // Adds a new item to the add-ons context menu, "Use Persona".
+  gAddonContextMenus.unshift("menuitem_usePersona");
+
+  /**
+   * Overriden method from Firefox extensions.js. Prepares the context menu for
+   * persona items.
+   */
+  function buildContextMenu(aEvent) {
+    var popup = document.getElementById("addonContextMenu");
+    var selectedItem = gExtensionsView.selectedItem;
+    if (aEvent.target !== popup || !selectedItem)
+      return false;
+
+    while (popup.hasChildNodes())
+      popup.removeChild(popup.firstChild);
+
+    switch (gView) {
+    case "search":
+      var menus = gSearchContextMenus;
+      break;
+
+    // Changed from original extensions.js
+    // Include the "personas" view in the switch, to set the add-ons context menu.
+    case "extensions":
+    case "themes":
+    case "personas":
+    case "locales":
+    case "plugins":
+      menus = gAddonContextMenus;
+      break;
+
+    case "updates":
+      menus = gUpdateContextMenus;
+      break;
+    case "installs":
+      menus = gInstallContextMenus;
+      break;
+    }
+
+    for (var i = 0; i < menus.length; ++i) {
+      var clonedMenu = document.getElementById(menus[i]).cloneNode(true);
+      clonedMenu.id = clonedMenu.id + "_clone";
+      popup.appendChild(clonedMenu);
+    }
+
+    // All views (but search and plugins) support about
+    if (gView != "search" && gView != "plugins") {
+      var menuitem_about = document.getElementById("menuitem_about_clone");
+      var name = selectedItem ? selectedItem.getAttribute("name") : "";
+      menuitem_about.setAttribute("label", getExtensionString("aboutAddon", [name]));
+    }
+
+    // Make sure all commands are up to date
+    gExtensionsViewController.onCommandUpdate();
+
+    // Some flags needed later
+    var canCancelInstall = gExtensionsViewController.isCommandEnabled("cmd_cancelInstall");
+    var canCancelUpgrade = gExtensionsViewController.isCommandEnabled("cmd_cancelUpgrade");
+    var canReallyEnable = gExtensionsViewController.isCommandEnabled("cmd_reallyEnable");
+    var canCancelUninstall = gExtensionsViewController.isCommandEnabled("cmd_cancelUninstall");
+
+    /* When an update or install is pending allow canceling the update or install
+       and don't allow uninstall. When an uninstall is pending allow canceling the
+       uninstall.*/
+    if (gView != "updates") {
+      document.getElementById("menuitem_cancelInstall_clone").hidden = !canCancelInstall;
+
+      if (gView != "installs" && gView != "search") {
+        document.getElementById("menuitem_cancelUninstall_clone").hidden = !canCancelUninstall;
+        document.getElementById("menuitem_uninstall_clone").hidden = canCancelUninstall ||
+                                                                     canCancelInstall ||
+                                                                     canCancelUpgrade;
+      }
+
+      if (gView != "search")
+        document.getElementById("menuitem_cancelUpgrade_clone").hidden = !canCancelUpgrade;
+    }
+
+    // Changed from original extensions.js
+    // Manipulation of the new "Use Persona" menu item, shown only for personas.
+    switch (gView) {
+    case "extensions":
+      document.getElementById("menuitem_enable_clone").hidden = !canReallyEnable;
+      document.getElementById("menuitem_disable_clone").hidden = canReallyEnable;
+      document.getElementById("menuitem_useTheme_clone").hidden = true;
+      document.getElementById("menuitem_usePersona_clone").hidden = true;
+      break;
+    case "personas":
+    case "themes":
+      var enableMenu = document.getElementById("menuitem_enable_clone");
+      if (!selectedItem.isCompatible || selectedItem.isBlocklisted ||
+          !selectedItem.satisfiesDependencies || selectedItem.isDisabled)
+        // don't let the user activate incompatible themes, but show a (disabled) Enable
+        // menuitem to give visual feedback; it's disabled because cmd_enable returns false
+        enableMenu.hidden = false;
+      else
+        enableMenu.hidden = true;
+      document.getElementById("menuitem_options_clone").hidden = true;
+      document.getElementById("menuitem_disable_clone").hidden = true;
+      document.getElementById("menuitem_useTheme_clone").hidden = gView != "themes";
+      document.getElementById("menuitem_usePersona_clone").hidden = gView != "personas";
+      break;
+    case "plugins":
+      document.getElementById("menuitem_about_clone").hidden = true;
+      document.getElementById("menuitem_uninstall_clone").hidden = true;
+      document.getElementById("menuitem_checkUpdate_clone").hidden = true;
+    case "locales":
+      document.getElementById("menuitem_enable_clone").hidden = !canReallyEnable;
+      document.getElementById("menuitem_disable_clone").hidden = canReallyEnable;
+      document.getElementById("menuitem_useTheme_clone").hidden = true;
+      document.getElementById("menuitem_usePersona_clone").hidden = true;
+      document.getElementById("menuitem_options_clone").hidden = true;
+      break;
+    case "updates":
+      var includeUpdate = document.getAnonymousElementByAttribute(selectedItem, "anonid", "includeUpdate");
+      var menuitem_includeUpdate = document.getElementById("menuitem_includeUpdate_clone");
+      menuitem_includeUpdate.setAttribute("checked", includeUpdate.checked ? "true" : "false");
+      break;
+    case "installs":
+      // Hides the separator if nothing is below it
+      document.getElementById("menuseparator_1_clone").hidden = !canCancelInstall && !canCancelUpgrade;
+      break;
+    case "search":
+      var canInstall = gExtensionsViewController.isCommandEnabled("cmd_installSearchResult");
+      document.getElementById("menuitem_installSearchResult_clone").hidden = !canInstall;
+      // Hides the separator if nothing is below it
+      document.getElementById("menuseparator_1_clone").hidden = !canCancelInstall;
+      break;
+    }
+
+    return true;
+  }
+
+  /**
+   * Overriden method from Firefox extensions.js. Allows personas to be
+   * previewed in the Addons window.
+   */
+  function onAddonSelect(aEvent) {
+    var viewButton = document.getElementById("viewGroup").selectedItem;
+    if (viewButton.hasAttribute("persist") && gExtensionsView.selectedItem)
+      viewButton.setAttribute("last-selected", gExtensionsView.selectedItem.id);
+
+    if (!document.getElementById("themePreviewArea").hidden) {
+      var previewImageDeck = document.getElementById("previewImageDeck");
+
+      // Changed from original extensions.js
+      // Include the "Personas" view to show the preview image.
+      if (gView == "themes" || gView == "personas") {
+        var previewImage = document.getElementById("previewImage");
+        if (!gExtensionsView.selectedItem) {
+          previewImageDeck.selectedIndex = 0;
+          if (previewImage.hasAttribute("src"))
+            previewImage.removeAttribute("src");
+        }
+        else {
+          var url = gExtensionsView.selectedItem.getAttribute("previewImage");
+          if (url) {
+            previewImageDeck.selectedIndex = 2;
+            previewImage.setAttribute("src", url);
+          }
+          else {
+            previewImageDeck.selectedIndex = 1;
+            if (previewImage.hasAttribute("src"))
+              previewImage.removeAttribute("src");
+          }
+        }
+      }
+      else if (gView == "updates") {
+        UpdateInfoLoader.cancelLoad();
+        if (!gExtensionsView.selectedItem) {
+          previewImageDeck.selectedIndex = 3;
+        }
+        else {
+          var uri = gExtensionsView.selectedItem.getAttribute("availableUpdateInfo");
+          if (isSafeURI(uri))
+            UpdateInfoLoader.loadInfo(uri);
+          else
+            previewImageDeck.selectedIndex = 4;
+        }
+      }
+    }
+  }
+
+  /**
+   * Initializes the Personas icon image in the Extension Manager. The icon is
+   * set only if the other tabs are also showing an image.
+   */
+  function initPersonasIcon() {
+    let radio = document.getElementById("personas-view");
+    radio.removeAttribute("hidden");
+    let style = window.getComputedStyle(radio, null);
+
+    // Set the personas icon ONLY if the other buttons have an icon set.
+    if (style.listStyleImage != "none")
+      radio.style.listStyleImage = 'url("chrome://personas/content/personas_32x32.png")';
+  }
+
+  window.addEventListener("load", initPersonasIcon, false);
+}
new file mode 100644
--- /dev/null
+++ b/client/content/extensions.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0"?>
+
+<!-- ***** BEGIN LICENSE BLOCK *****
+   - Version: MPL 1.1/GPL 2.0/LGPL 2.1
+   -
+   - The contents of this file are subject to the Mozilla Public License Version
+   - 1.1 (the "License"); you may not use this file except in compliance with
+   - the License. You may obtain a copy of the License at
+   - http://www.mozilla.org/MPL/
+   -
+   - Software distributed under the License is distributed on an "AS IS" basis,
+   - WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+   - for the specific language governing rights and limitations under the
+   - License.
+   -
+   - The Original Code is Personas.
+   -
+   - The Initial Developer of the Original Code is Mozilla.
+   - Portions created by the Initial Developer are Copyright (C) 2010
+   - the Initial Developer. All Rights Reserved.
+   -
+   - Contributor(s):
+   -   Jose E. Bolanos <jose@appcoast.com>
+   -
+   - Alternatively, the contents of this file may be used under the terms of
+   - either the GNU General Public License Version 2 or later (the "GPL"), or
+   - the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+   - in which case the provisions of the GPL or the LGPL are applicable instead
+   - of those above. If you wish to allow use of your version of this file only
+   - under the terms of either the GPL or the LGPL, and not to allow others to
+   - use your version of this file under the terms of the MPL, indicate your
+   - decision by deleting the provisions above and replace them with the notice
+   - and other provisions required by the GPL or the LGPL. If you do not delete
+   - the provisions above, a recipient may use your version of this file under
+   - the terms of any one of the MPL, the GPL or the LGPL.
+   -
+   - ***** END LICENSE BLOCK ***** -->
+
+<!DOCTYPE bindings [
+  <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+  <!ENTITY % extensionsDTD SYSTEM "chrome://mozapps/locale/extensions/extensions.dtd" >
+  <!ENTITY % personasDTD SYSTEM "chrome://personas/locale/personas.dtd" >
+  %brandDTD;
+  %extensionsDTD;
+  %personasDTD;
+]>
+
+<bindings id="personasAddonBindings"
+          xmlns="http://www.mozilla.org/xbl"
+          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+          xmlns:xbl="http://www.mozilla.org/xbl">
+
+  <binding id="persona-selected" extends="chrome://mozapps/content/extensions/extensions.xml#addon-selected">
+    <content>
+      <xul:hbox flex="1">
+        <xul:vbox class="addon-icon" xbl:inherits="iconURL"/>
+        <xul:vbox flex="1" class="addonTextBox">
+          <xul:hbox anonid="addonNameVersion" class="addon-name-version" xbl:inherits="name, version"/>
+          <xul:label anonid="addonDescriptionWrap" class="descriptionWrap" xbl:inherits="xbl:text=description"/>
+          <xul:hbox anonid="selectedButtons" flex="1" class="selectedButtons">
+            <xul:button class="uninstallHide themeButton useThemeButton" label="&usePersona.label;"
+                        accesskey="&usePersona.accesskey;" tooltiptext="&usePersona.tooltip;"
+                        command="cmd_useTheme"/>
+            <xul:spacer flex="1"/>
+            <xul:button class="uninstallHide uninstallButton" label="&cmd.uninstall.label;"
+                        accesskey="&cmd.uninstall2.accesskey;" tooltiptext="&cmd.uninstall2.tooltip;"
+                        command="cmd_uninstall"/>
+          </xul:hbox>
+        </xul:vbox>
+      </xul:hbox>
+    </content>
+  </binding>
+
+</bindings>
new file mode 100644
--- /dev/null
+++ b/client/content/extensions.xul
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- ***** BEGIN LICENSE BLOCK *****
+   - Version: MPL 1.1/GPL 2.0/LGPL 2.1
+   -
+   - The contents of this file are subject to the Mozilla Public License Version
+   - 1.1 (the "License"); you may not use this file except in compliance with
+   - the License. You may obtain a copy of the License at
+   - http://www.mozilla.org/MPL/
+   -
+   - Software distributed under the License is distributed on an "AS IS" basis,
+   - WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+   - for the specific language governing rights and limitations under the
+   - License.
+   -
+   - The Original Code is Personas.
+   -
+   - The Initial Developer of the Original Code is Mozilla.
+   - Portions created by the Initial Developer are Copyright (C) 2010
+   - the Initial Developer. All Rights Reserved.
+   -
+   - Contributor(s):
+   -   Jose E. Bolanos <jose@appcoast.com>
+   -
+   - Alternatively, the contents of this file may be used under the terms of
+   - either the GNU General Public License Version 2 or later (the "GPL"), or
+   - the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+   - in which case the provisions of the GPL or the LGPL are applicable instead
+   - of those above. If you wish to allow use of your version of this file only
+   - under the terms of either the GPL or the LGPL, and not to allow others to
+   - use your version of this file under the terms of the MPL, indicate your
+   - decision by deleting the provisions above and replace them with the notice
+   - and other provisions required by the GPL or the LGPL. If you do not delete
+   - the provisions above, a recipient may use your version of this file under
+   - the terms of any one of the MPL, the GPL or the LGPL.
+   -
+   - ***** END LICENSE BLOCK ***** -->
+
+<?xml-stylesheet href="chrome://personas/content/extensions.css" type="text/css"?>
+
+<!DOCTYPE overlay SYSTEM "chrome://personas/locale/personas.dtd">
+
+<overlay id="personas-extensions-overlay"
+         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+  <script type="application/javascript" src="chrome://personas/content/extensions.js"/>
+
+  <vbox id="addonContextMenuPalette">
+    <menuitem id="menuitem_usePersona" default="true" command="cmd_useTheme"
+              label="&usePersona.label;" accesskey="&usePersona.accesskey;"/>
+  </vbox>
+
+  <radiogroup id="viewGroup">
+    <radio id="personas-view" insertafter="themes-view" label="&personas_app_title;"
+           oncommand="showView('personas')" persist="last-selected" hidden="true"/>
+  </radiogroup>
+
+</overlay>
--- a/client/locale/en-US/personas.dtd
+++ b/client/locale/en-US/personas.dtd
@@ -13,8 +13,12 @@
 <!ENTITY accentColorPicker.label  "Accent Color:">
 <!ENTITY useDefaultColor.label    "Use Default">
 <!ENTITY okButton.label           "OK">
 
 <!ENTITY contextHomePage.label      "Home Page">
 <!ENTITY contextPreferences.label   "Settings">
 <!ENTITY contextEdit.label          "Edit">
 <!ENTITY contextApply.label         "Apply">
+
+<!ENTITY usePersona.label      "Use Persona">
+<!ENTITY usePersona.accesskey  "P">
+<!ENTITY usePersona.tooltip    "Use this Persona">
--- a/client/locale/en-US/personas.properties
+++ b/client/locale/en-US/personas.properties
@@ -33,16 +33,26 @@ viewMore          = %1$S More from %2$S.
 #  %2$2 = the name of the author of the persona (f.e. Lee.Tom)
 notification.personaWasSelected     = You selected the persona "%1$S" by %2$S.
 
 # Strings used for the Undo button in the notification shown when a persona
 # is selected for the first time.
 notification.revertButton.label     = Undo
 notification.revertButton.accesskey = U
 
+# The text of the notification shown when the browser needs to be restarted in
+# order to apply a persona.
+notification.restartToApply          = The current theme is incompatible with Personas. Restart to apply.
+
+# Strings used for the Restart button in the notification shown when the browser
+# needs to be restarted in order to apply a persona. The brand name "Firefox"
+# can be used safely because this notification is shown only in Firefox [3.6, 4.0[
+notification.restartButton.label     = Restart Firefox
+notification.restartButton.accesskey = R
+
 # LOCALIZATION NOTE (randomPersona): a label that identifies the current
 # randomly-selected persona.
 #   %1$S = the category to which the persona belongs
 #   %2$S = the name of the persona
 # f.e. "Random Selection from Scenery > Yosemite"
 randomPersona     = Random Selection from %1$S > %2$S
 
 
--- a/client/modules/service.js
+++ b/client/modules/service.js
@@ -15,16 +15,17 @@
  *
  * The Initial Developer of the Original Code is Mozilla.
  * Portions created by the Initial Developer are Copyright (C) 2007
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
  *   Chris Beard <cbeard@mozilla.org>
  *   Myk Melez <myk@mozilla.org>
+ *   Jose E. Bolanos <jose@appcoast.com>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -714,17 +715,21 @@ let PersonaService = {
     if (previousPersona) {
       this.currentPersona = JSON.parse(previousPersona);
       this._revertRecent();
       this.selected = "current";
 
       if (LightweightThemeManager) {
         // forget the lightweight theme too
         LightweightThemeManager.forgetUsedTheme(undonePersonaId);
-        LightweightThemeManager.currentTheme = this.currentPersona;
+
+        if (this.currentPersona.custom && LightweightThemeManager.setLocalTheme)
+          LightweightThemeManager.setLocalTheme(this.currentPersona);
+        else
+          LightweightThemeManager.currentTheme = this.currentPersona;
       }
       else
         this.resetPersona();
     }
   },
 
   /**
    * Shows a notification displaying the currently selected persona and button