Bug 653637 - Provide a simple way for addons to have preferences UI - Part 2; r=dtownsend ui-r=jboriss
authorGeoff Lankow <geoff@darktrojan.net>
Wed, 01 Jun 2011 22:35:24 +1200
changeset 70510 e0620f11d5ee24fa8f8c670a9e64b8e9789d4d61
parent 70509 cdfc7faf382230e777f5908f6ac337a93c4ad7f0
child 70511 ddf432ea3249835d1cc34f258299f4d8307f644b
push id20344
push userdtownsend@mozilla.com
push dateFri, 03 Jun 2011 16:11:33 +0000
treeherdermozilla-central@4a6c8415ae8e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdtownsend, jboriss
bugs653637
milestone7.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 653637 - Provide a simple way for addons to have preferences UI - Part 2; r=dtownsend ui-r=jboriss
toolkit/mozapps/extensions/AddonManager.jsm
toolkit/mozapps/extensions/XPIProvider.jsm
toolkit/mozapps/extensions/content/extensions.js
toolkit/mozapps/extensions/content/setting.xml
toolkit/themes/gnomestripe/mozapps/extensions/extensions.css
toolkit/themes/pinstripe/mozapps/extensions/extensions.css
toolkit/themes/winstripe/mozapps/extensions/extensions.css
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -1207,16 +1207,22 @@ var AddonManager = {
   // Indicates that the Addon should not update automatically.
   AUTOUPDATE_DISABLE: 0,
   // Indicates that the Addon should update automatically only if
   // that's the global default.
   AUTOUPDATE_DEFAULT: 1,
   // Indicates that the Addon should update automatically.
   AUTOUPDATE_ENABLE: 2,
 
+  // Constants for how Addon options should be shown.
+  // Options will be opened in a new window
+  OPTIONS_TYPE_DIALOG: 1,
+  // Options will be displayed within the AM detail view
+  OPTIONS_TYPE_INLINE: 2,
+
   getInstallForURL: function AM_getInstallForURL(aUrl, aCallback, aMimetype,
                                                  aHash, aName, aIconURL,
                                                  aVersion, aLoadGroup) {
     AddonManagerInternal.getInstallForURL(aUrl, aCallback, aMimetype, aHash,
                                           aName, aIconURL, aVersion, aLoadGroup);
   },
 
   getInstallForFile: function AM_getInstallForFile(aFile, aCallback, aMimetype) {
--- a/toolkit/mozapps/extensions/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/XPIProvider.jsm
@@ -113,31 +113,31 @@ const PREFIX_ITEM_URI                 = 
 const RDFURI_ITEM_ROOT                = "urn:mozilla:item:root"
 const RDFURI_INSTALL_MANIFEST_ROOT    = "urn:mozilla:install-manifest";
 const PREFIX_NS_EM                    = "http://www.mozilla.org/2004/em-rdf#";
 
 const TOOLKIT_ID                      = "toolkit@mozilla.org";
 
 const BRANCH_REGEXP                   = /^([^\.]+\.[0-9]+[a-z]*).*/gi;
 
-const DB_SCHEMA                       = 4;
+const DB_SCHEMA                       = 5;
 const REQ_VERSION                     = 2;
 
 #ifdef MOZ_COMPATABILITY_NIGHTLY
 const PREF_EM_CHECK_COMPATIBILITY = PREF_EM_CHECK_COMPATIBILITY_BASE +
                                     ".nightly";
 #else
 const PREF_EM_CHECK_COMPATIBILITY = PREF_EM_CHECK_COMPATIBILITY_BASE + "." +
                                     Services.appinfo.version.replace(BRANCH_REGEXP, "$1");
 #endif
 
 // Properties that exist in the install manifest
 const PROP_METADATA      = ["id", "version", "type", "internalName", "updateURL",
-                            "updateKey", "optionsURL", "aboutURL", "iconURL",
-                            "icon64URL"];
+                            "updateKey", "optionsURL", "optionsType", "aboutURL",
+                            "iconURL", "icon64URL"];
 const PROP_LOCALE_SINGLE = ["name", "description", "creator", "homepageURL"];
 const PROP_LOCALE_MULTI  = ["developers", "translators", "contributors"];
 const PROP_TARGETAPP     = ["id", "minVersion", "maxVersion"];
 
 // Properties that only exist in the database
 const DB_METADATA        = ["installDate", "updateDate", "size", "sourceURI",
                             "releaseNotesURI", "applyBackgroundUpdates"];
 const DB_BOOL_METADATA   = ["visible", "active", "userDisabled", "appDisabled",
@@ -695,21 +695,27 @@ function loadManifestFromRDF(aUri, aStre
       throw new Error("Illegal add-on ID " + addon.id);
     if (!addon.version)
       throw new Error("No version in install manifest");
   }
 
   // Only read the bootstrapped property for extensions
   if (addon.type == "extension") {
     addon.bootstrap = getRDFProperty(ds, root, "bootstrap") == "true";
+    if (addon.optionsType &&
+        addon.optionsType != AddonManager.OPTIONS_TYPE_DIALOG &&
+        addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE) {
+      throw new Error("Install manifest specifies unknown type: " + addon.optionsType);
+    }
   }
   else {
-    // Only extensions are allowed to provide an optionsURL or aboutURL. For
+    // Only extensions are allowed to provide an optionsURL, optionsType or aboutURL. For
     // all other types they are silently ignored
     addon.optionsURL = null;
+    addon.optionsType = null;
     addon.aboutURL = null;
 
     if (addon.type == "theme") {
       if (!addon.internalName)
         throw new Error("Themes must include an internalName property");
       addon.skinnable = getRDFProperty(ds, root, "skinnable") == "true";
     }
   }
@@ -3670,21 +3676,21 @@ var XPIProvider = {
 
     // Notify any other providers that this theme is now enabled again.
     if (aAddon.type == "theme" && aAddon.active)
       AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, false);
   }
 };
 
 const FIELDS_ADDON = "internal_id, id, location, version, type, internalName, " +
-                     "updateURL, updateKey, optionsURL, aboutURL, iconURL, " +
-                     "icon64URL, defaultLocale, visible, active, userDisabled, " +
-                     "appDisabled, pendingUninstall, descriptor, installDate, " +
-                     "updateDate, applyBackgroundUpdates, bootstrap, skinnable, " +
-                     "size, sourceURI, releaseNotesURI, softDisabled";
+                     "updateURL, updateKey, optionsURL, optionsType, aboutURL, " +
+                     "iconURL, icon64URL, defaultLocale, visible, active, " +
+                     "userDisabled, appDisabled, pendingUninstall, descriptor, " +
+                     "installDate, updateDate, applyBackgroundUpdates, bootstrap, " +
+                     "skinnable, size, sourceURI, releaseNotesURI, softDisabled";
 
 /**
  * A helper function to log an SQL error.
  *
  * @param  aError
  *         The storage error code associated with the error
  * @param  aErrorString
  *         An error message
@@ -3814,18 +3820,18 @@ var XPIDatabase = {
                             "addon_internal_id=:internal_id",
     _getTargetPlatforms: "SELECT os, abi FROM targetPlatform WHERE " +
                          "addon_internal_id=:internal_id",
     _readLocaleStrings: "SELECT locale_id, type, value FROM locale_strings " +
                         "WHERE locale_id=:id",
 
     addAddonMetadata_addon: "INSERT INTO addon VALUES (NULL, :id, :location, " +
                             ":version, :type, :internalName, :updateURL, " +
-                            ":updateKey, :optionsURL, :aboutURL, :iconURL, " +
-                            ":icon64URL, :locale, :visible, :active, " +
+                            ":updateKey, :optionsURL, :optionsType, :aboutURL, " +
+                            ":iconURL, :icon64URL, :locale, :visible, :active, " +
                             ":userDisabled, :appDisabled, :pendingUninstall, " +
                             ":descriptor, :installDate, :updateDate, " +
                             ":applyBackgroundUpdates, :bootstrap, :skinnable, " +
                             ":size, :sourceURI, :releaseNotesURI, :softDisabled)",
     addAddonMetadata_addon_locale: "INSERT INTO addon_locale VALUES " +
                                    "(:internal_id, :name, :locale)",
     addAddonMetadata_locale: "INSERT INTO locale (name, description, creator, " +
                              "homepageURL) VALUES (:name, :description, " +
@@ -4323,19 +4329,19 @@ var XPIDatabase = {
     this.beginTransaction();
 
     // Any errors in here should rollback the transaction
     try {
       this.connection.createTable("addon",
                                   "internal_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
                                   "id TEXT, location TEXT, version TEXT, " +
                                   "type TEXT, internalName TEXT, updateURL TEXT, " +
-                                  "updateKey TEXT, optionsURL TEXT, aboutURL TEXT, " +
-                                  "iconURL TEXT, icon64URL TEXT, " +
-                                  "defaultLocale INTEGER, " +
+                                  "updateKey TEXT, optionsURL TEXT, " +
+                                  "optionsType TEXT, aboutURL TEXT, iconURL TEXT, " +
+                                  "icon64URL TEXT, defaultLocale INTEGER, " +
                                   "visible INTEGER, active INTEGER, " +
                                   "userDisabled INTEGER, appDisabled INTEGER, " +
                                   "pendingUninstall INTEGER, descriptor TEXT, " +
                                   "installDate INTEGER, updateDate INTEGER, " +
                                   "applyBackgroundUpdates INTEGER, " +
                                   "bootstrap INTEGER, skinnable INTEGER, " +
                                   "size INTEGER, sourceURI TEXT, " +
                                   "releaseNotesURI TEXT, softDisabled INTEGER, " +
@@ -6801,52 +6807,75 @@ function AddonWrapper(aAddon) {
     this.__defineGetter__(aProp, function() {
       if (aAddon._repositoryAddon)
         return aAddon._repositoryAddon[aProp];
 
       return null;
     });
   }, this);
 
-  ["optionsURL", "aboutURL"].forEach(function(aProp) {
-    this.__defineGetter__(aProp, function() {
-      return this.isActive ? aAddon[aProp] : null;
-    });
-  }, this);
+  this.__defineGetter__("aboutURL", function() {
+    return this.isActive ? aAddon["aboutURL"] : null;
+  });
 
   ["installDate", "updateDate"].forEach(function(aProp) {
     this.__defineGetter__(aProp, function() new Date(aAddon[aProp]));
   }, this);
 
   ["sourceURI", "releaseNotesURI"].forEach(function(aProp) {
     this.__defineGetter__(aProp, function() {
       let target = chooseValue(aAddon, aProp)[0];
       if (!target)
         return null;
       return NetUtil.newURI(target);
     });
   }, this);
 
-  // Maps iconURL and icon64URL to the properties of the same name or icon.png
-  // and icon64.png in the add-on's files.
-  ["icon", "icon64"].forEach(function(aProp) {
+  // Maps iconURL, icon64URL and optionsURL to the properties of the same name
+  // or icon.png, icon64.png and options.xul in the add-on's files.
+  ["icon", "icon64", "options"].forEach(function(aProp) {
     this.__defineGetter__(aProp + "URL", function() {
       if (this.isActive && aAddon[aProp + "URL"])
         return aAddon[aProp + "URL"];
 
-      if (this.hasResource(aProp + ".png"))
-        return this.getResourceURI(aProp + ".png").spec;
+      switch (aProp) {
+        case "icon":
+        case "icon64":
+          if (this.hasResource(aProp + ".png"))
+            return this.getResourceURI(aProp + ".png").spec;
+          break;
+        case "options":
+          if (this.isActive && this.hasResource(aProp + ".xul"))
+            return this.getResourceURI(aProp + ".xul").spec;
+          break;
+      }
 
       if (aAddon._repositoryAddon)
         return aAddon._repositoryAddon[aProp + "URL"];
 
       return null;
     }, this);
   }, this);
 
+  this.__defineGetter__("optionsType", function() {
+    if (!this.isActive)
+      return null;
+
+    if (aAddon.optionsType)
+      return aAddon.optionsType;
+
+    if (this.hasResource("options.xul"))
+      return AddonManager.OPTIONS_TYPE_INLINE;
+
+    if (this.optionsURL)
+      return AddonManager.OPTIONS_TYPE_DIALOG;
+
+    return null;
+  }, this);
+
   PROP_LOCALE_SINGLE.forEach(function(aProp) {
     this.__defineGetter__(aProp, function() {
       // Override XPI creator if repository creator is defined
       if (aProp == "creator" &&
           aAddon._repositoryAddon && aAddon._repositoryAddon.creator) {
         return aAddon._repositoryAddon.creator;
       }
 
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -919,21 +919,30 @@ var gViewController = {
         };
         gEventManager.delegateAddonEvent("onCheckingUpdate", [aAddon]);
         aAddon.findUpdates(listener, AddonManager.UPDATE_WHEN_USER_REQUESTED);
       }
     },
 
     cmd_showItemPreferences: {
       isEnabled: function(aAddon) {
-        if (!aAddon)
+        if (!aAddon || !aAddon.isActive || !aAddon.optionsURL)
           return false;
-        return aAddon.isActive && !!aAddon.optionsURL;
+        if (gViewController.currentViewObj == gDetailView &&
+            aAddon.optionsType == AddonManager.OPTIONS_TYPE_INLINE) {
+          return false;
+        }
+        return true;
       },
       doCommand: function(aAddon) {
+        if (gViewController.currentViewObj == gListView &&
+            aAddon.optionsType == AddonManager.OPTIONS_TYPE_INLINE) {
+          gViewController.commands.cmd_showItemDetails.doCommand(aAddon);
+          return;
+        }
         var optionsURL = aAddon.optionsURL;
         var windows = Services.wm.getEnumerator(null);
         while (windows.hasMoreElements()) {
           var win = windows.getNext();
           if (win.document.documentURI == optionsURL) {
             win.focus();
             return;
           }
@@ -2649,28 +2658,31 @@ var gDetailView = {
       this._autoUpdate.value = aAddon.applyBackgroundUpdates;
       let hideFindUpdates = shouldAutoUpdate(this._addon);
       document.getElementById("detail-findUpdates-btn").hidden = hideFindUpdates;
     } else {
       this._autoUpdate.hidden = true;
       document.getElementById("detail-findUpdates-btn").hidden = false;
     }
 
-    document.getElementById("detail-prefs-btn").hidden = !aIsRemote && !aAddon.optionsURL;
+    document.getElementById("detail-prefs-btn").hidden = !aIsRemote &&
+      !gViewController.commands.cmd_showItemPreferences.isEnabled(aAddon);
     
     var gridRows = document.querySelectorAll("#detail-grid rows row");
     for (var i = 0, first = true; i < gridRows.length; ++i) {
       if (first && window.getComputedStyle(gridRows[i], null).getPropertyValue("display") != "none") {
         gridRows[i].setAttribute("first-row", true);
         first = false;
       } else {
         gridRows[i].removeAttribute("first-row");
       }
     }
 
+    this.fillSettingsRows();
+
     this.updateState();
 
     gViewController.updateCommands();
     gViewController.notifyViewChanged();
   },
 
   show: function(aAddonId, aRequest) {
     var self = this;
@@ -2792,34 +2804,101 @@ var gDetailView = {
     if (this._loadingTimer) {
       clearTimeout(this._loadingTimer);
       this._loadingTimer = null;
     }
 
     this.node.removeAttribute("loading-extended");
   },
 
+  emptySettingsRows: function () {
+    var lastRow = document.getElementById("detail-downloads");
+    var rows = lastRow.parentNode;
+    while (lastRow.nextSibling)
+      rows.removeChild(rows.lastChild);
+  },
+
+  fillSettingsRows: function () {
+    this.emptySettingsRows();
+    if (this._addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE)
+      return;
+
+    var rows = document.getElementById("detail-downloads").parentNode;
+
+    var xhr = new XMLHttpRequest();
+    xhr.open("GET", this._addon.optionsURL, false);
+    xhr.send();
+
+    var xml = xhr.responseXML;
+    var settings = xml.querySelectorAll(":root > setting");
+
+    // This horrible piece of code fixes two problems. 1) The menulist binding doesn't apply
+    // correctly when it's moved from one document to another (bug 659163), which is solved 
+    // by manually cloning the menulist. 2) Labels and controls aligned to the top of a row 
+    // looks really bad, so the description is put on a new row to preserve alignment.
+    for (var i = 0; i < settings.length; i++) {
+      var setting = settings[i];
+      if (i == 0)
+        setting.setAttribute("first-row", true);
+
+      // remove menulist controls for replacement later
+      var control = setting.firstElementChild;
+      if (setting.getAttribute("type") == "control" && control && control.localName == "menulist") {
+        setting.removeChild(control);
+        var consoleMessage = Cc["@mozilla.org/scripterror;1"].
+                             createInstance(Ci.nsIScriptError);
+        consoleMessage.init("Menulist is not available in the addons-manager yet, due to bug 659163",
+                            this._addon.optionsURL, null, null, 0, Ci.nsIScriptError.warningFlag, null);
+        Services.console.logMessage(consoleMessage);
+        continue;
+      }
+
+      // remove setting description, for replacement later
+      var desc = setting.textContent.trim();
+      if (desc)
+        setting.textContent = "";
+      if (setting.hasAttribute("desc")) {
+        desc = setting.getAttribute("desc");
+        setting.removeAttribute("desc");
+      }
+
+      rows.appendChild(setting);
+
+      // add a new row containing the description
+      if (desc) {
+        var row = document.createElement("row");
+        var label = document.createElement("label");
+        label.className = "preferences-description";
+        label.textContent = desc;
+        row.appendChild(label);
+        rows.appendChild(row);
+      }
+    }
+  },
+
   getSelectedAddon: function() {
     return this._addon;
   },
 
   onEnabling: function() {
     this.updateState();
   },
 
   onEnabled: function() {
     this.updateState();
+    this.fillSettingsRows();
   },
 
   onDisabling: function() {
     this.updateState();
   },
 
   onDisabled: function() {
     this.updateState();
+    this.emptySettingsRows();
   },
 
   onUninstalling: function() {
     this.updateState();
   },
 
   onUninstalled: function() {
     gViewController.popState();
--- a/toolkit/mozapps/extensions/content/setting.xml
+++ b/toolkit/mozapps/extensions/content/setting.xml
@@ -265,17 +265,17 @@
     <content>
       <xul:vbox class="setting-label">
         <xul:label class="preferences-title" xbl:inherits="value=title" crop="end" flex="1"/>
         <xul:label class="preferences-description" xbl:inherits="value=desc" crop="end" flex="1">
           <children/>
         </xul:label>
       </xul:vbox>
       <xul:hbox anonid="input-container" class="setting-input">
-        <xul:textbox type="number" anonid="input" xbl:inherits="disabled,emptytext,min,max,increment,hidespinbuttons,wraparound" oninput="inputChanged();" oncommand="inputChanged();"/>
+        <xul:textbox type="number" anonid="input" xbl:inherits="disabled,emptytext,min,max,increment,hidespinbuttons,wraparound" oninput="inputChanged();" onchange="inputChanged();"/>
       </xul:hbox>
     </content>
 
     <implementation>
       <method name="valueFromPreference">
         <body>
         <![CDATA[
           let val = Services.prefs.getIntPref(this.pref);
--- a/toolkit/themes/gnomestripe/mozapps/extensions/extensions.css
+++ b/toolkit/themes/gnomestripe/mozapps/extensions/extensions.css
@@ -712,40 +712,71 @@
 #detail-contrib-suggested {
   color: GrayText;
 }
 
 #detail-grid {
   margin-bottom: 2em;
 }
 
+#detail-grid > columns > column:first-child {
+  max-width: 25em;
+}
+
 .detail-row[first-row="true"],
-.detail-row-complex[first-row="true"] {
+.detail-row-complex[first-row="true"],
+setting[first-row="true"] {
   border-top: none;
 }
 
 .detail-row,
-.detail-row-complex {
+.detail-row-complex,
+setting {
   border-top: 1px solid ThreeDShadow;
   -moz-box-align: center;
+  min-height: 33px;
 }
 
 .detail-row-value {
   -moz-margin-start: 0;
 }
 
 #detail-controls {
   margin-bottom: 1em;
 }
 
 #detail-view[active="false"]:not([pending]):not([notification]) {
   background-image: -moz-linear-gradient(rgba(135, 135, 135, 0.1),
                                          rgba(135, 135, 135, 0));
 }
 
+setting[first-row="true"] {
+  margin-top: 2em;
+}
+
+setting {
+  display: -moz-grid-line;
+}
+
+.preferences-description {
+  font-size: 90.9%;
+  color: graytext;
+  margin-top: -2px;
+  -moz-margin-start: 2em;
+}
+
+setting[type="string"] > .setting-input > textbox {
+  -moz-box-flex: 1;
+}
+
+menulist { /* Fixes some styling inconsistencies */
+  font-size: 100%;
+  margin: 1px 5px 2px 5px;
+}
+
 
 /*** creator ***/
 
 .creator > label {
   -moz-margin-start: 0;
   -moz-margin-end: 0;
 }
 
--- a/toolkit/themes/pinstripe/mozapps/extensions/extensions.css
+++ b/toolkit/themes/pinstripe/mozapps/extensions/extensions.css
@@ -889,41 +889,67 @@
   box-shadow: 0 0 6.5px rgba(0, 0, 0, 0.4) inset,
               0 0 2px rgba(0, 0, 0, 0.4) inset;
 }
 
 #detail-grid {
   margin-bottom: 2em;
 }
 
+#detail-grid > columns > column:first-child {
+  max-width: 25em;
+}
+
 .detail-row[first-row="true"],
-.detail-row-complex[first-row="true"] {
+.detail-row-complex[first-row="true"],
+setting[first-row="true"] {
   border-top: none;
 }
 
 .detail-row,
-.detail-row-complex {
+.detail-row-complex,
+setting {
   border-top: 2px solid;
   -moz-border-top-colors: rgba(28, 31, 37, 0.2) rgba(255, 255, 255, 0.2);
   -moz-box-align: center;
+  min-height: 30px;
 }
 
 .detail-row-value {
   -moz-margin-start: 0;
 }
 
 #detail-controls {
   margin-bottom: 1em;
 }
 
 #detail-view[active="false"]:not([pending]):not([notification]) {
   background-image: -moz-linear-gradient(rgba(135, 135, 135, 0.1),
                                          rgba(135, 135, 135, 0));
 }
 
+setting[first-row="true"] {
+  margin-top: 2em;
+}
+
+setting {
+  display: -moz-grid-line;
+}
+
+.preferences-description {
+  font-size: 90.9%;
+  color: graytext;
+  margin-top: -2px;
+  -moz-margin-start: 2em;
+}
+
+setting[type="string"] > .setting-input > textbox {
+  -moz-box-flex: 1;
+}
+
 
 /*** creator ***/
 
 .creator > label {
   -moz-margin-start: 0;
   -moz-margin-end: 0;
 }
 
@@ -1077,34 +1103,38 @@
 
 #update-selected {
   margin: 12px;
 }
 
 
 /*** buttons ***/
 
-.addon-control {
+.addon-control,
+setting[type="control"] button,
+setting[type="control"] menulist {
   -moz-appearance: none;
   padding: 1px 4px;
   min-width: 60px;
   border-radius: 3px;
   border: 1px solid rgba(60,73,97,0.5);
   box-shadow: inset 0 1px rgba(255,255,255,0.25), 0 1px rgba(255,255,255,0.25);
   background-image: -moz-linear-gradient(rgba(255,255,255,0.45), rgba(255,255,255,0.2));
   background-clip: padding-box;
   color: #252F3B;
   text-shadow: @loweredShadow@;
 }
 
 .addon-control[disabled="true"] {
   display: none;
 }
 
-.addon-control:active:hover {
+.addon-control:active:hover,
+setting[type="control"] button:hover,
+setting[type="control"] menulist:hover {
   box-shadow: inset 0 1px 3px rgba(0,0,0,.2), 0 1px rgba(255,255,255,0.25);
   background-image: -moz-linear-gradient(rgba(45,54,71,0.3), rgba(45,54,71,0.1));
   border-color: rgba(60,73,97,0.7);
 }
 
 .button-link {
   -moz-appearance: none;
   background: transparent;
--- a/toolkit/themes/winstripe/mozapps/extensions/extensions.css
+++ b/toolkit/themes/winstripe/mozapps/extensions/extensions.css
@@ -914,41 +914,71 @@
               0 0 2px rgba(0, 0, 0, 0.4) inset,
               0 1px 0 rgba(255, 255, 255, 0.4);
 }
 
 #detail-grid {
   margin-bottom: 2em;
 }
 
+#detail-grid > columns > column:first-child {
+  max-width: 25em;
+}
+
 .detail-row[first-row="true"],
-.detail-row-complex[first-row="true"] {
+.detail-row-complex[first-row="true"],
+setting[first-row="true"] {
   border-top: none;
 }
 
 .detail-row,
-.detail-row-complex {
+.detail-row-complex,
+setting {
   border-top: 2px solid;
   -moz-border-top-colors: rgba(28, 31, 37, 0.1) rgba(255, 255, 255, 0.1);
   -moz-box-align: center;
+  min-height: 30px;
 }
 
 .detail-row-value {
   -moz-margin-start: 0;
 }
 
 #detail-controls {
   margin-bottom: 1em;
 }
 
 #detail-view[active="false"]:not([pending]):not([notification]) {
   background-image: -moz-linear-gradient(rgba(135, 135, 135, 0.1),
                                          rgba(135, 135, 135, 0));
 }
 
+setting[first-row="true"] {
+  margin-top: 2em;
+}
+
+setting {
+  display: -moz-grid-line;
+}
+
+.preferences-description {
+  font-size: 90.9%;
+  color: graytext;
+  margin-top: -2px;
+  -moz-margin-start: 2em;
+}
+
+setting[type="string"] > .setting-input > textbox {
+  -moz-box-flex: 1;
+}
+
+menulist { /* Fixes some styling inconsistencies */
+  margin: 1px 5px 2px 5px;
+}
+
 /*** creator ***/
 
 .creator > label {
   -moz-margin-start: 0;
   -moz-margin-end: 0;
 }
 
 .creator > .text-link {
@@ -1120,37 +1150,42 @@
 
 #update-selected {
   margin: 12px;
 }
 
 
 /*** buttons ***/
 
-.addon-control {
+.addon-control,
+setting[type="control"] button,
+setting[type="control"] menulist {
   -moz-appearance: none;
   color: black;
   padding: 0 5px;
   background: -moz-linear-gradient(rgba(251, 252, 253, 0.95), rgba(246, 247, 248, 0) 49%, 
                                    rgba(211, 212, 213, 0.45) 51%, rgba(225, 226, 229, 0.3));
   background-clip: padding-box;
   border-radius: 3px;
   border: 1px solid rgba(31, 64, 100, 0.4);
   border-top-color: rgba(31, 64, 100, 0.3);
   box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.25) inset,
               0 0 2px 1px rgba(255, 255, 255, 0.25) inset;
 }
 
-.addon-control:active:hover {
+.addon-control:active:hover,
+setting[type="control"] button:hover,
+setting[type="control"] menulist:hover {
   background-color: rgba(61, 76, 92, 0.2);
   border-color: rgba(39, 53, 68, 0.5);
   box-shadow: 0 0 3px 1px rgba(39, 53, 68, 0.2) inset;
 }
 
-.addon-control > .button-box {
+.addon-control > .button-box,
+setting[type="control"] button > .button-box {
   padding: 1px;
 }
 
 .addon-control[disabled="true"] {
   display: none;
 }
 
 .button-link {