Merge fx-team to m-c.
authorRyan VanderMeulen <ryanvm@gmail.com>
Wed, 11 Dec 2013 22:02:30 -0500
changeset 175956 1ad9af3a2ab828dfca41414959d6cf149915b21f
parent 175946 361eef20662bcd5379b5543b27b1d19adad188e9 (current diff)
parent 175955 96c4b3ea0540c690dd49db57ccebdb33accec523 (diff)
child 176096 30dd3830c3bf6b51c8db71172dd03a002f297a19
child 176111 b9da6cfa552ad94ee617c24959d7dba271aa7ec5
child 176134 3a25cf9c343ff04bb985ed17aff2868b0f9ffab1
push id3343
push userffxbld
push dateMon, 17 Mar 2014 21:55:32 +0000
treeherdermozilla-beta@2f7d3415f79f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone29.0a1
first release with
nightly linux32
1ad9af3a2ab8 / 29.0a1 / 20131212030202 / files
nightly linux64
1ad9af3a2ab8 / 29.0a1 / 20131212030202 / files
nightly mac
1ad9af3a2ab8 / 29.0a1 / 20131212030202 / files
nightly win32
1ad9af3a2ab8 / 29.0a1 / 20131212030202 / files
nightly win64
1ad9af3a2ab8 / 29.0a1 / 20131212030202 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c.
--- a/browser/base/content/aboutDialog.js
+++ b/browser/base/content/aboutDialog.js
@@ -101,54 +101,52 @@ function appUpdater()
                                      "nsIUpdateChecker");
   XPCOMUtils.defineLazyServiceGetter(this, "um",
                                      "@mozilla.org/updates/update-manager;1",
                                      "nsIUpdateManager");
 
   this.bundle = Services.strings.
                 createBundle("chrome://browser/locale/browser.properties");
 
-  this.updateBtn = document.getElementById("updateButton");
-
-  // The button label value must be set so its height is correct.
-  this.setupUpdateButton("update.checkInsideButton");
-
   let manualURL = Services.urlFormatter.formatURLPref("app.update.url.manual");
   let manualLink = document.getElementById("manualLink");
   manualLink.value = manualURL;
   manualLink.href = manualURL;
   document.getElementById("failedLink").href = manualURL;
 
   if (this.updateDisabledAndLocked) {
     this.selectPanel("adminDisabled");
     return;
   }
 
   if (this.isPending || this.isApplied) {
-    this.setupUpdateButton("update.restart." +
-                           (this.isMajor ? "upgradeButton" : "updateButton"));
+    this.selectPanel("apply");
     return;
   }
 
   if (this.aus.isOtherInstanceHandlingUpdates) {
     this.selectPanel("otherInstanceHandlingUpdates");
     return;
   }
 
   if (this.isDownloading) {
     this.startDownload();
+    // selectPanel("downloading") is called from setupDownloadingUI().
     return;
   }
 
-  if (this.updateEnabled && this.updateAuto) {
-    this.selectPanel("checkingForUpdates");
-    this.isChecking = true;
-    this.checker.checkForUpdates(this.updateCheckListener, true);
-    return;
-  }
+  // If app.update.enabled is false, we don't pop up an update dialog
+  // automatically, but opening the About dialog is considered manually
+  // checking for updates, so we always check.
+  // If app.update.auto is false, we ask before downloading though,
+  // in onCheckComplete.
+  this.selectPanel("checkingForUpdates");
+  this.isChecking = true;
+  this.checker.checkForUpdates(this.updateCheckListener, true);
+  // after checking, onCheckComplete() is called
 }
 
 appUpdater.prototype =
 {
   // true when there is an update check in progress.
   isChecking: false,
 
   // true when there is an update already staged / ready to be applied.
@@ -175,23 +173,16 @@ appUpdater.prototype =
   // true when there is an update download in progress.
   get isDownloading() {
     if (this.update)
       return this.update.state == "downloading";
     return this.um.activeUpdate &&
            this.um.activeUpdate.state == "downloading";
   },
 
-  // true when the update type is major.
-  get isMajor() {
-    if (this.update)
-      return this.update.type == "major";
-    return this.um.activeUpdate.type == "major";
-  },
-
   // true when updating is disabled by an administrator.
   get updateDisabledAndLocked() {
     return !this.updateEnabled &&
            Services.prefs.prefIsLocked("app.update.enabled");
   },
 
   // true when updating is enabled.
   get updateEnabled() {
@@ -213,46 +204,64 @@ appUpdater.prototype =
     try {
       return Services.prefs.getBoolPref("app.update.auto");
     }
     catch (e) { }
     return true; // Firefox default is true
   },
 
   /**
-   * Sets the deck's selected panel.
+   * Sets the panel of the updateDeck.
    *
    * @param  aChildID
-   *         The id of the deck's child to select.
+   *         The id of the deck's child to select, e.g. "apply".
    */
   selectPanel: function(aChildID) {
-    this.updateDeck.selectedPanel = document.getElementById(aChildID);
-    this.updateBtn.disabled = (aChildID != "updateButtonBox");
+    let panel = document.getElementById(aChildID);
+
+    let button = panel.querySelector("button");
+    if (button) {
+      if (aChildID == "downloadAndInstall") {
+        let updateVersion = gAppUpdater.update.displayVersion;
+        button.label = this.bundle.formatStringFromName("update.downloadAndInstallButton.label", [updateVersion], 1);
+        button.accessKey = this.bundle.GetStringFromName("update.downloadAndInstallButton.accesskey");
+      }
+      this.updateDeck.selectedPanel = panel;
+      if (!document.commandDispatcher.focusedElement || // don't steal the focus
+          document.commandDispatcher.focusedElement.localName == "button") // except from the other buttons
+        button.focus();
+
+    } else {
+      this.updateDeck.selectedPanel = panel;
+    }
   },
 
   /**
-   * Sets the update button's label and accesskey.
-   *
-   * @param  aKeyPrefix
-   *         The prefix for the properties file entry to use for setting the
-   *         label and accesskey.
+   * Check for addon compat, or start the download right away
    */
-  setupUpdateButton: function(aKeyPrefix) {
-    this.updateBtn.label = this.bundle.GetStringFromName(aKeyPrefix + ".label");
-    this.updateBtn.accessKey = this.bundle.GetStringFromName(aKeyPrefix + ".accesskey");
-    if (!document.commandDispatcher.focusedElement ||
-        document.commandDispatcher.focusedElement == this.updateBtn)
-      this.updateBtn.focus();
+  doUpdate: function() {
+    // skip the compatibility check if the update doesn't provide appVersion,
+    // or the appVersion is unchanged, e.g. nightly update
+    if (!this.update.appVersion ||
+        Services.vc.compare(gAppUpdater.update.appVersion,
+                            Services.appinfo.version) == 0) {
+      this.startDownload();
+    } else {
+      this.checkAddonCompatibility();
+    }
   },
 
   /**
-   * Handles oncommand for the update button.
+   * Handles oncommand for the "Restart to Update" button
+   * which is presented after the download has been downloaded.
    */
-  buttonOnCommand: function() {
-    if (this.isPending || this.isApplied) {
+  buttonRestartAfterDownload: function() {
+    if (!this.isPending && !this.isApplied)
+      return;
+
       // Notify all windows that an application quit has been requested.
       let cancelQuit = Components.classes["@mozilla.org/supports-PRBool;1"].
                        createInstance(Components.interfaces.nsISupportsPRBool);
       Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart");
 
       // Something aborted the quit process.
       if (cancelQuit.data)
         return;
@@ -263,37 +272,31 @@ appUpdater.prototype =
       // If already in safe mode restart in safe mode (bug 327119)
       if (Services.appinfo.inSafeMode) {
         appStartup.restartInSafeMode(Components.interfaces.nsIAppStartup.eAttemptQuit);
         return;
       }
 
       appStartup.quit(Components.interfaces.nsIAppStartup.eAttemptQuit |
                       Components.interfaces.nsIAppStartup.eRestart);
-      return;
-    }
+    },
 
+  /**
+   * Handles oncommand for the "Apply Update…" button
+   * which is presented if we need to show the billboard or license.
+   */
+  buttonApplyBillboard: function() {
     const URI_UPDATE_PROMPT_DIALOG = "chrome://mozapps/content/update/updates.xul";
-    // Firefox no longer displays a license for updates and the licenseURL check
-    // is just in case a distibution does.
-    if (this.update && (this.update.billboardURL || this.update.licenseURL ||
-        this.addons.length != 0)) {
-      var ary = null;
-      ary = Components.classes["@mozilla.org/supports-array;1"].
-            createInstance(Components.interfaces.nsISupportsArray);
-      ary.AppendElement(this.update);
-      var openFeatures = "chrome,centerscreen,dialog=no,resizable=no,titlebar,toolbar=no";
-      Services.ww.openWindow(null, URI_UPDATE_PROMPT_DIALOG, "", openFeatures, ary);
-      window.close();
-      return;
-    }
-
-    this.selectPanel("checkingForUpdates");
-    this.isChecking = true;
-    this.checker.checkForUpdates(this.updateCheckListener, true);
+    var ary = null;
+    ary = Components.classes["@mozilla.org/supports-array;1"].
+          createInstance(Components.interfaces.nsISupportsArray);
+    ary.AppendElement(this.update);
+    var openFeatures = "chrome,centerscreen,dialog=no,resizable=no,titlebar,toolbar=no";
+    Services.ww.openWindow(null, URI_UPDATE_PROMPT_DIALOG, "", openFeatures, ary);
+    window.close(); // close the "About" window; updates.xul takes over.
   },
 
   /**
    * Implements nsIUpdateCheckListener. The methods implemented by
    * nsIUpdateCheckListener are in a different scope from nsIIncrementalDownload
    * to make it clear which are used by each interface.
    */
   updateCheckListener: {
@@ -321,31 +324,24 @@ appUpdater.prototype =
       if (!gAppUpdater.aus.canApplyUpdates) {
         gAppUpdater.selectPanel("manualUpdate");
         return;
       }
 
       // Firefox no longer displays a license for updates and the licenseURL
       // check is just in case a distibution does.
       if (gAppUpdater.update.billboardURL || gAppUpdater.update.licenseURL) {
-        gAppUpdater.selectPanel("updateButtonBox");
-        gAppUpdater.setupUpdateButton("update.openUpdateUI." +
-                                      (this.isMajor ? "upgradeButton"
-                                                    : "applyButton"));
+        gAppUpdater.selectPanel("applyBillboard");
         return;
       }
 
-      if (!gAppUpdater.update.appVersion ||
-          Services.vc.compare(gAppUpdater.update.appVersion,
-                              Services.appinfo.version) == 0) {
-        gAppUpdater.startDownload();
-        return;
-      }
-
-      gAppUpdater.checkAddonCompatibility();
+      if (gAppUpdater.updateAuto) // automatically download and install
+        gAppUpdater.doUpdate();
+      else // ask
+        gAppUpdater.selectPanel("downloadAndInstall");
     },
 
     /**
      * See nsIUpdateService.idl
      */
     onError: function(aRequest, aUpdate) {
       // Errors in the update check are treated as no updates found. If the
       // update check fails repeatedly without a success the user will be
@@ -469,19 +465,17 @@ appUpdater.prototype =
       return;
 
     if (this.addons.length == 0) {
       // Compatibility updates or new version updates were found for all add-ons
       this.startDownload();
       return;
     }
 
-    this.selectPanel("updateButtonBox");
-    this.setupUpdateButton("update.openUpdateUI." +
-                           (this.isMajor ? "upgradeButton" : "applyButton"));
+    this.selectPanel("apply");
   },
 
   /**
    * Starts the download of an update mar.
    */
   startDownload: function() {
     if (!this.update)
       this.update = this.um.activeUpdate;
@@ -548,46 +542,41 @@ appUpdater.prototype =
         let update = this.um.activeUpdate;
         let self = this;
         Services.obs.addObserver(function (aSubject, aTopic, aData) {
           // Update the UI when the background updater is finished
           let status = aData;
           if (status == "applied" || status == "applied-service" ||
               status == "pending" || status == "pending-service") {
             // If the update is successfully applied, or if the updater has
-            // fallen back to non-staged updates, show the Restart to Update
+            // fallen back to non-staged updates, show the "Restart to Update"
             // button.
-            self.selectPanel("updateButtonBox");
-            self.setupUpdateButton("update.restart." +
-                                   (self.isMajor ? "upgradeButton" : "updateButton"));
+            self.selectPanel("apply");
           } else if (status == "failed") {
             // Background update has failed, let's show the UI responsible for
             // prompting the user to update manually.
             self.selectPanel("downloadFailed");
           } else if (status == "downloading") {
             // We've fallen back to downloading the full update because the
             // partial update failed to get staged in the background.
             // Therefore we need to keep our observer.
             self.setupDownloadingUI();
             return;
           }
           Services.obs.removeObserver(arguments.callee, "update-staged");
         }, "update-staged", false);
       } else {
-        this.selectPanel("updateButtonBox");
-        this.setupUpdateButton("update.restart." +
-                               (this.isMajor ? "upgradeButton" : "updateButton"));
+        this.selectPanel("apply");
       }
       break;
     default:
       this.removeDownloadListener();
       this.selectPanel("downloadFailed");
       break;
     }
-
   },
 
   /**
    * See nsIProgressEventSink.idl
    */
   onStatus: function(aRequest, aContext, aStatus, aStatusArg) {
   },
 
--- a/browser/base/content/aboutDialog.xul
+++ b/browser/base/content/aboutDialog.xul
@@ -45,27 +45,39 @@
 #expand <label id="version">__MOZ_APP_VERSION__</label>
         <label id="distribution" class="text-blurb"/>
         <label id="distributionId" class="text-blurb"/>
 
         <vbox id="detailsBox">
           <vbox id="updateBox">
 #ifdef MOZ_UPDATER
             <deck id="updateDeck" orient="vertical">
-              <hbox id="updateButtonBox" align="center">
+              <hbox id="downloadAndInstall" align="center">
+                <button id="downloadAndInstallButton" align="start"
+                        oncommand="gAppUpdater.doUpdate();"/>
+                        <!-- label and accesskey will be filled by JS -->
+                <spacer flex="1"/>
+              </hbox>
+              <hbox id="apply" align="center">
                 <button id="updateButton" align="start"
-                        oncommand="gAppUpdater.buttonOnCommand();"/>
+                        label="&update.updateButton.label;"
+                        accesskey="&update.updateButton.accesskey;"
+                        oncommand="gAppUpdater.buttonRestartAfterDownload();"/>
+                <spacer flex="1"/>
+              </hbox>
+              <hbox id="applyBillboard" align="center">
+                <button id="applyButtonBillboard" align="start"
+                        label="&update.applyButtonBillboard.label;"
+                        accesskey="&update.applyButtonBillboard.accesskey;"
+                        oncommand="gAppUpdater.buttonApplyBillboard();"/>
                 <spacer flex="1"/>
               </hbox>
               <hbox id="checkingForUpdates" align="center">
                 <image class="update-throbber"/><label>&update.checkingForUpdates;</label>
               </hbox>
-              <hbox id="checkingAddonCompat" align="center">
-                <image class="update-throbber"/><label>&update.checkingAddonCompat;</label>
-              </hbox>
               <hbox id="downloading" align="center">
                 <image class="update-throbber"/><label>&update.downloading.start;</label><label id="downloadStatus"/><label>&update.downloading.end;</label>
               </hbox>
               <hbox id="applying" align="center">
                 <image class="update-throbber"/><label>&update.applying;</label>
               </hbox>
               <hbox id="downloadFailed" align="center">
                 <label>&update.failed.start;</label><label id="failedLink" class="text-link">&update.failed.linkText;</label><label>&update.failed.end;</label>
--- a/browser/components/customizableui/src/CustomizableUI.jsm
+++ b/browser/components/customizableui/src/CustomizableUI.jsm
@@ -452,20 +452,27 @@ let CustomizableUIInternal = {
         let [provider, node] = this.getWidgetNode(id, window);
         if (!node) {
           LOG("Unknown widget: " + id);
           continue;
         }
 
         // If the placements have items in them which are (now) no longer removable,
         // we shouldn't be moving them:
-        if (node.parentNode != container && !this.isWidgetRemovable(node)) {
+        if (provider == CustomizableUI.PROVIDER_API) {
+          let widgetInfo = gPalette.get(id);
+          if (!widgetInfo.removable && aArea != widgetInfo.defaultArea) {
+            placementsToRemove.add(id);
+            continue;
+          }
+        } else if (provider == CustomizableUI.PROVIDER_XUL &&
+                   node.parentNode != container && !this.isWidgetRemovable(node)) {
           placementsToRemove.add(id);
           continue;
-        }
+        } // Special widgets are always removable, so no need to check them
 
         if (inPrivateWindow && provider == CustomizableUI.PROVIDER_API) {
           let widget = gPalette.get(id);
           if (!widget.showInPrivateBrowsing && inPrivateWindow) {
             continue;
           }
         }
 
@@ -1731,17 +1738,17 @@ let CustomizableUIInternal = {
 
   //XXXunf Log some warnings here, when the data provided isn't up to scratch.
   normalizeWidget: function(aData, aSource) {
     let widget = {
       implementation: aData,
       source: aSource || "addon",
       instances: new Map(),
       currentArea: null,
-      removable: false,
+      removable: true,
       overflows: true,
       defaultArea: null,
       shortcutId: null,
       tooltiptext: null,
       showInPrivateBrowsing: true,
     };
 
     if (typeof aData.id != "string" || !/^[a-z0-9-_]{1,}$/i.test(aData.id)) {
@@ -1773,16 +1780,21 @@ let CustomizableUIInternal = {
     for (let prop of kOptBoolProps) {
       if (typeof aData[prop] == "boolean") {
         widget[prop] = aData[prop];
       }
     }
 
     if (aData.defaultArea && gAreas.has(aData.defaultArea)) {
       widget.defaultArea = aData.defaultArea;
+    } else if (!widget.removable) {
+      ERROR("Widget '" + widget.id + "' is not removable but does not specify " +
+            "a valid defaultArea. That's not possible; it must specify a " +
+            "valid defaultArea as well.");
+      return null;
     }
 
     if ("type" in aData && gSupportedWidgetTypes.has(aData.type)) {
       widget.type = aData.type;
     } else {
       widget.type = "button";
     }
 
@@ -2378,21 +2390,23 @@ this.CustomizableUI = {
    * - onClick(aEvt): Attached to all widgets; a function that will be invoked
    *                  when the user clicks the widget.
    * - onViewShowing(aEvt): Only useful for views; a function that will be
    *                  invoked when a user shows your view.
    * - onViewHiding(aEvt): Only useful for views; a function that will be
    *                  invoked when a user hides your view.
    * - tooltiptext:   string to use for the tooltip of the widget
    * - label:         string to use for the label of the widget
-   * - removable:     whether the widget is removable (optional, default: false)
+   * - removable:     whether the widget is removable (optional, default: true)
+   *                  NB: if you specify false here, you must provide a
+   *                  defaultArea, too.
    * - overflows:     whether widget can overflow when in an overflowable
    *                  toolbar (optional, default: true)
    * - defaultArea:   default area to add the widget to
-   *                  (optional, default: none)
+   *                  (optional, default: none; required if non-removable)
    * - shortcutId:    id of an element that has a shortcut for this widget
    *                  (optional, default: null). This is only used to display
    *                  the shortcut as part of the tooltip for builtin widgets
    *                  (which have strings inside
    *                  customizableWidgets.properties). If you're in an add-on,
    *                  you should not set this property.
    * - showInPrivateBrowsing: whether to show the widget in private browsing
    *                          mode (optional, default: true)
--- a/browser/components/customizableui/src/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/src/CustomizableWidgets.jsm
@@ -56,17 +56,16 @@ function updateCombinedWidgetStyle(aNode
   }
 }
 
 const CustomizableWidgets = [{
     id: "history-panelmenu",
     type: "view",
     viewId: "PanelUI-history",
     shortcutId: "key_gotoHistory",
-    removable: true,
     defaultArea: CustomizableUI.AREA_PANEL,
     onViewShowing: function(aEvent) {
       // Populate our list of history
       const kMaxResults = 15;
       let doc = aEvent.detail.ownerDocument;
 
       let options = PlacesUtils.history.getNewQueryOptions();
       options.excludeQueries = true;
@@ -143,71 +142,66 @@ const CustomizableWidgets = [{
       separator.hidden = !windowsFragment.childElementCount;
       recentlyClosedWindows.appendChild(windowsFragment);
     },
     onViewHiding: function(aEvent) {
       LOG("History view is being hidden!");
     }
   }, {
     id: "privatebrowsing-button",
-    removable: true,
     shortcutId: "key_privatebrowsing",
     defaultArea: CustomizableUI.AREA_PANEL,
     onCommand: function(e) {
       if (e.target && e.target.ownerDocument && e.target.ownerDocument.defaultView) {
         let win = e.target.ownerDocument.defaultView;
         if (typeof win.OpenBrowserWindow == "function") {
           win.OpenBrowserWindow({private: true});
         }
       }
     }
   }, {
     id: "save-page-button",
-    removable: true,
     shortcutId: "key_savePage",
     defaultArea: CustomizableUI.AREA_PANEL,
     onCommand: function(aEvent) {
       let win = aEvent.target &&
                 aEvent.target.ownerDocument &&
                 aEvent.target.ownerDocument.defaultView;
       if (win && typeof win.saveDocument == "function") {
         win.saveDocument(win.content.document);
       }
     }
   }, {
     id: "find-button",
-    removable: true,
     shortcutId: "key_find",
     defaultArea: CustomizableUI.AREA_PANEL,
     onCommand: function(aEvent) {
       let win = aEvent.target &&
                 aEvent.target.ownerDocument &&
                 aEvent.target.ownerDocument.defaultView;
       if (win && win.gFindBar) {
         win.gFindBar.onFindCommand();
       }
     }
   }, {
     id: "open-file-button",
-    removable: true,
     shortcutId: "openFileKb",
     defaultArea: CustomizableUI.AREA_PANEL,
     onCommand: function(aEvent) {
       let win = aEvent.target
                 && aEvent.target.ownerDocument
                 && aEvent.target.ownerDocument.defaultView;
       if (win && typeof win.BrowserOpenFileWindow == "function") {
         win.BrowserOpenFileWindow();
       }
     }
   }, {
     id: "developer-button",
     type: "view",
     viewId: "PanelUI-developer",
-    removable: true,
     shortcutId: "key_devToolboxMenuItem",
     defaultArea: CustomizableUI.AREA_PANEL,
     onViewShowing: function(aEvent) {
       // Populate the subview with whatever menuitems are in the developer
       // menu. We skip menu elements, because the menu panel has no way
       // of dealing with those right now.
       let doc = aEvent.target.ownerDocument;
       let win = doc.defaultView;
@@ -260,47 +254,44 @@ const CustomizableWidgets = [{
       }
 
       parent.appendChild(items);
       aEvent.target.removeEventListener("command",
                                         win.PanelUI.onCommandHandler);
     }
   }, {
     id: "add-ons-button",
-    removable: true,
     shortcutId: "key_openAddons",
     defaultArea: CustomizableUI.AREA_PANEL,
     onCommand: function(aEvent) {
       let win = aEvent.target &&
                 aEvent.target.ownerDocument &&
                 aEvent.target.ownerDocument.defaultView;
       if (win && typeof win.BrowserOpenAddonsMgr == "function") {
         win.BrowserOpenAddonsMgr();
       }
     }
   }, {
     id: "preferences-button",
-    removable: true,
     defaultArea: CustomizableUI.AREA_PANEL,
 #ifdef XP_WIN
     label: "preferences-button.labelWin",
     tooltiptext: "preferences-button.tooltipWin",
 #endif
     onCommand: function(aEvent) {
       let win = aEvent.target &&
                 aEvent.target.ownerDocument &&
                 aEvent.target.ownerDocument.defaultView;
       if (win && typeof win.openPreferences == "function") {
         win.openPreferences();
       }
     }
   }, {
     id: "zoom-controls",
     type: "custom",
-    removable: true,
     defaultArea: CustomizableUI.AREA_PANEL,
     onBuild: function(aDocument) {
       const kPanelId = "PanelUI-popup";
       let inPanel = (this.currentArea == CustomizableUI.AREA_PANEL);
       let noautoclose = inPanel ? "true" : null;
       let cls = inPanel ? "panel-combined-button" : "toolbarbutton-1";
 
       if (!this.currentArea)
@@ -436,17 +427,16 @@ const CustomizableWidgets = [{
       };
       CustomizableUI.addListener(listener);
 
       return node;
     }
   }, {
     id: "edit-controls",
     type: "custom",
-    removable: true,
     defaultArea: CustomizableUI.AREA_PANEL,
     onBuild: function(aDocument) {
       let inPanel = (this.currentArea == CustomizableUI.AREA_PANEL);
       let cls = inPanel ? "panel-combined-button" : "toolbarbutton-1";
 
       if (!this.currentArea)
         cls = null;
 
@@ -531,17 +521,16 @@ const CustomizableWidgets = [{
 
       return node;
     }
   },
   {
     id: "feed-button",
     type: "view",
     viewId: "PanelUI-feeds",
-    removable: true,
     defaultArea: CustomizableUI.AREA_PANEL,
     onClick: function(aEvent) {
       let win = aEvent.target.ownerDocument.defaultView;
       let feeds = win.gBrowser.selectedBrowser.feeds;
 
       // Here, we only care about the case where we have exactly 1 feed and the
       // user clicked...
       let isClick = (aEvent.button == 0 || aEvent.button == 1);
@@ -571,17 +560,16 @@ const CustomizableWidgets = [{
       if (!feeds || !feeds.length) {
         node.setAttribute("disabled", "true");
       }
     }
   }, {
     id: "characterencoding-button",
     type: "view",
     viewId: "PanelUI-characterEncodingView",
-    removable: true,
     defaultArea: CustomizableUI.AREA_PANEL,
     maybeDisableMenu: function(aDocument) {
       let window = aDocument.defaultView;
       return !(window.gBrowser &&
                window.gBrowser.docShell &&
                window.gBrowser.docShell.mayEnableCharacterEncodingMenu);
     },
     getCharsetList: function(aSection, aDocument) {
@@ -778,17 +766,16 @@ const CustomizableWidgets = [{
           let panel = aDoc.getElementById(kPanelId);
           panel.removeEventListener("popupshowing", updateButton);
         }
       };
       CustomizableUI.addListener(listener);
     }
   }, {
     id: "email-link-button",
-    removable: true,
     onCommand: function(aEvent) {
       let win = aEvent.view;
       win.MailIntegration.sendLinkForWindow(win.content);
     }
   }];
 
 #ifdef XP_WIN
 #ifdef MOZ_METRO
@@ -796,17 +783,16 @@ if (Services.sysinfo.getProperty("hasWin
   let widgetArgs = {tooltiptext: "switch-to-metro-button2.tooltiptext"};
   let brandShortName = BrandBundle.GetStringFromName("brandShortName");
   let metroTooltip = CustomizableUI.getLocalizedProperty(widgetArgs, "tooltiptext",
                                                          [brandShortName]);
   CustomizableWidgets.push({
     id: "switch-to-metro-button",
     label: "switch-to-metro-button2.label",
     tooltiptext: metroTooltip,
-    removable: true,
     defaultArea: CustomizableUI.AREA_PANEL,
     showInPrivateBrowsing: false, /* See bug 928068 */
     onCommand: function(aEvent) {
       let win = aEvent.view;
       if (win && typeof win.SwitchToMetro == "function") {
         win.SwitchToMetro();
       }
     }
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -40,9 +40,10 @@ skip-if = os == "mac"
 [browser_938980_navbar_collapsed.js]
 [browser_938995_indefaultstate_nonremovable.js]
 [browser_940013_registerToolbarNode_calls_registerArea.js]
 [browser_940946_removable_from_navbar_customizemode.js]
 [browser_941083_invalidate_wrapper_cache_createWidget.js]
 [browser_942581_unregisterArea_keeps_placements.js]
 [browser_943683_migration_test.js]
 [browser_944887_destroyWidget_should_destroy_in_palette.js]
+[browser_947987_removable_default.js]
 [browser_panel_toggle.js]
--- a/browser/components/customizableui/test/browser_938995_indefaultstate_nonremovable.js
+++ b/browser/components/customizableui/test/browser_938995_indefaultstate_nonremovable.js
@@ -5,26 +5,27 @@
 const kWidgetId = "test-non-removable-widget";
 let gTests = [
   {
     desc: "Adding non-removable items to a toolbar or the panel shouldn't change inDefaultState",
     run: function() {
       let navbar = document.getElementById("nav-bar");
       ok(CustomizableUI.inDefaultState, "Should start in default state");
 
-      CustomizableUI.createWidget({id: kWidgetId, removable: false, label: "Test"});
+      let button = createDummyXULButton(kWidgetId, "Test non-removable inDefaultState handling");
       CustomizableUI.addWidgetToArea(kWidgetId, CustomizableUI.AREA_NAVBAR);
+      button.setAttribute("removable", "false");
       ok(CustomizableUI.inDefaultState, "Should still be in default state after navbar addition");
-      CustomizableUI.destroyWidget(kWidgetId);
+      button.remove();
 
-      CustomizableUI.createWidget({id: kWidgetId, removable: false, label: "Test"});
+      button = createDummyXULButton(kWidgetId, "Test non-removable inDefaultState handling");
       CustomizableUI.addWidgetToArea(kWidgetId, CustomizableUI.AREA_PANEL);
+      button.setAttribute("removable", "false");
       ok(CustomizableUI.inDefaultState, "Should still be in default state after panel addition");
-      CustomizableUI.destroyWidget(kWidgetId);
-
+      button.remove();
       ok(CustomizableUI.inDefaultState, "Should be in default state after destroying both widgets");
     },
     teardown: null
   },
 ];
 
 function test() {
   waitForExplicitFinish();
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_947987_removable_default.js
@@ -0,0 +1,79 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+let kWidgetId = "test-removable-widget-default";
+const kNavBar = CustomizableUI.AREA_NAVBAR;
+let widgetCounter = 0;
+let gTests = [
+  {
+    desc: "Sanity checks",
+    run: function() {
+      let brokenSpec = {id: kWidgetId + (widgetCounter++), removable: false};
+      SimpleTest.doesThrow(function() CustomizableUI.createWidget(brokenSpec),
+                           "Creating non-removable widget without defaultArea should throw.");
+
+      // Widget without removable set should be removable:
+      let wrapper = CustomizableUI.createWidget({id: kWidgetId + (widgetCounter++)});
+      ok(CustomizableUI.isWidgetRemovable(wrapper.id), "Should be removable by default.");
+      CustomizableUI.destroyWidget(wrapper.id);
+    }
+  },
+  {
+    desc: "Test non-removable widget with defaultArea",
+    run: function() {
+      // Non-removable widget with defaultArea should work:
+      let spec = {id: kWidgetId + (widgetCounter++), removable: false,
+                  defaultArea: kNavBar};
+      let widgetWrapper;
+      try {
+        widgetWrapper = CustomizableUI.createWidget(spec);
+      } catch (ex) {
+        ok(false, "Creating a non-removable widget with a default area should not throw.");
+        return;
+      }
+
+      let placement = CustomizableUI.getPlacementOfWidget(spec.id);
+      ok(placement, "Widget should be placed.");
+      is(placement.area, kNavBar, "Widget should be in navbar");
+      let singleWrapper = widgetWrapper.forWindow(window);
+      ok(singleWrapper, "Widget should exist in window.");
+      ok(singleWrapper.node, "Widget node should exist in window.");
+      let expectedParent = CustomizableUI.getCustomizeTargetForArea(kNavBar, window);
+      is(singleWrapper.node.parentNode, expectedParent, "Widget should be in navbar.");
+
+      let otherWin = yield openAndLoadWindow(true);
+      placement = CustomizableUI.getPlacementOfWidget(spec.id);
+      ok(placement, "Widget should be placed.");
+      is(placement && placement.area, kNavBar, "Widget should be in navbar");
+
+      singleWrapper = widgetWrapper.forWindow(otherWin);
+      ok(singleWrapper, "Widget should exist in other window.");
+      if (singleWrapper) {
+        ok(singleWrapper.node, "Widget node should exist in other window.");
+        if (singleWrapper.node) {
+          let expectedParent = CustomizableUI.getCustomizeTargetForArea(kNavBar, otherWin);
+          is(singleWrapper.node.parentNode, expectedParent,
+             "Widget should be in navbar in other window.");
+        }
+      }
+      otherWin.close();
+    }
+  },
+];
+
+function asyncCleanup() {
+  yield resetCustomization();
+}
+
+function cleanup() {
+  removeCustomToolbars();
+}
+
+function test() {
+  waitForExplicitFinish();
+  registerCleanupFunction(cleanup);
+  runTests(gTests, asyncCleanup);
+}
+
+
--- a/browser/devtools/shared/test/browser.ini
+++ b/browser/devtools/shared/test/browser.ini
@@ -26,8 +26,9 @@ support-files =
 [browser_telemetry_toolboxtabs_options.js]
 [browser_telemetry_toolboxtabs_styleeditor.js]
 [browser_telemetry_toolboxtabs_webconsole.js]
 [browser_templater_basic.js]
 [browser_toolbar_basic.js]
 [browser_toolbar_tooltip.js]
 [browser_toolbar_webconsole_errors_count.js]
 [browser_spectrum.js]
+[browser_csstransformpreview.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_csstransformpreview.js
@@ -0,0 +1,139 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the spectrum color picker works correctly
+
+const TEST_URI = "data:text/html;charset=utf-8,<div></div>";
+const {CSSTransformPreviewer} = devtools.require("devtools/shared/widgets/CSSTransformPreviewer");
+
+let doc, root;
+
+function test() {
+  waitForExplicitFinish();
+  addTab(TEST_URI, () => {
+    doc = content.document;
+    root = doc.querySelector("div");
+    startTests();
+  });
+}
+
+function endTests() {
+  doc = root = null;
+  gBrowser.removeCurrentTab();
+  finish();
+}
+
+function startTests() {
+  testCreateAndDestroyShouldAppendAndRemoveElements();
+}
+
+function testCreateAndDestroyShouldAppendAndRemoveElements() {
+  ok(root, "We have the root node to append the preview to");
+  is(root.childElementCount, 0, "Root node is empty");
+
+  let p = new CSSTransformPreviewer(root);
+  p.preview("matrix(1, -0.2, 0, 1, 0, 0)");
+  ok(root.childElementCount > 0, "Preview has appended elements");
+  ok(root.querySelector("canvas"), "Canvas preview element is here");
+
+  p.destroy();
+  is(root.childElementCount, 0, "Destroying preview removed all nodes");
+
+  testCanvasDimensionIsConstrainedByMaxDim();
+}
+
+function testCanvasDimensionIsConstrainedByMaxDim() {
+  let p = new CSSTransformPreviewer(root);
+  p.MAX_DIM = 500;
+  p.preview("scale(1)", "center", 1000, 1000);
+
+  let canvas = root.querySelector("canvas");
+  is(canvas.width, 500, "Canvas width is correct");
+  is(canvas.height, 500, "Canvas height is correct");
+
+  p.destroy();
+
+  testCallingPreviewSeveralTimesReusesTheSameCanvas();
+}
+
+function testCallingPreviewSeveralTimesReusesTheSameCanvas() {
+  let p = new CSSTransformPreviewer(root);
+
+  p.preview("scale(1)", "center", 1000, 1000);
+  let canvas = root.querySelector("canvas");
+
+  p.preview("rotate(90deg)");
+  let canvases = root.querySelectorAll("canvas");
+  is(canvases.length, 1, "Still one canvas element");
+  is(canvases[0], canvas, "Still the same canvas element");
+  p.destroy();
+
+  testCanvasDimensionAreCorrect();
+}
+
+function testCanvasDimensionAreCorrect() {
+  // Only test a few simple transformations
+  let p = new CSSTransformPreviewer(root);
+
+  // Make sure we have a square
+  let w = 200, h = w;
+  p.MAX_DIM = w;
+
+  // We can't test the content of the canvas here, just that, given a max width
+  // the aspect ratio of the canvas seems correct.
+
+  // Translate a square by its width, should be a rectangle
+  p.preview("translateX(200px)", "center", w, h);
+  let canvas = root.querySelector("canvas");
+  is(canvas.width, w, "width is correct");
+  is(canvas.height, h/2, "height is half of the width");
+
+  // Rotate on the top right corner, should be a rectangle
+  p.preview("rotate(-90deg)", "top right", w, h);
+  is(canvas.width, w, "width is correct");
+  is(canvas.height, h/2, "height is half of the width");
+
+  // Rotate on the bottom left corner, should be a rectangle
+  p.preview("rotate(90deg)", "top right", w, h);
+  is(canvas.width, w/2, "width is half of the height");
+  is(canvas.height, h, "height is correct");
+
+  // Scale from center, should still be a square
+  p.preview("scale(2)", "center", w, h);
+  is(canvas.width, w, "width is correct");
+  is(canvas.height, h, "height is correct");
+
+  // Skew from center, 45deg, should be a rectangle
+  p.preview("skew(45deg)", "center", w, h);
+  is(canvas.width, w, "width is correct");
+  is(canvas.height, h/2, "height is half of the height");
+
+  p.destroy();
+
+  testPreviewingInvalidTransformReturnsFalse();
+}
+
+function testPreviewingInvalidTransformReturnsFalse() {
+  let p = new CSSTransformPreviewer(root);
+  ok(!p.preview("veryWow(muchPx) suchTransform(soDeg)"), "Returned false for invalid transform");
+  ok(!p.preview("rotae(3deg)"), "Returned false for invalid transform");
+
+  // Verify the canvas is empty by checking the image data
+  let canvas = root.querySelector("canvas"), ctx = canvas.getContext("2d");
+  let data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
+  for (let i = 0, n = data.length; i < n; i += 4) {
+    // Let's not log 250*250*4 asserts! Instead, just log when it fails
+    let red = data[i];
+    let green = data[i + 1];
+    let blue = data[i + 2];
+    let alpha = data[i + 3];
+    if (red !== 0 || green !== 0 || blue !== 0 || alpha !== 0) {
+      ok(false, "Image data is not empty after an invalid transformed was previewed");
+      break;
+    }
+  }
+
+  is(p.preview("translateX(30px)"), true, "Returned true for a valid transform");
+  endTests();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/widgets/CSSTransformPreviewer.js
@@ -0,0 +1,389 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * The CSSTransformPreview module displays, using a <canvas> a rectangle, with
+ * a given width and height and its transformed version, given a css transform
+ * property and origin. It also displays arrows from/to each corner.
+ *
+ * It is useful to visualize how a css transform affected an element. It can
+ * help debug tricky transformations. It is used today in a tooltip, and this
+ * tooltip is shown when hovering over a css transform declaration in the rule
+ * and computed view panels.
+ *
+ * TODO: For now, it multiplies matrices itself to calculate the coordinates of
+ * the transformed box, but that should be removed as soon as we can get access
+ * to getQuads().
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+/**
+ * The TransformPreview needs an element to output a canvas tag.
+ *
+ * Usage example:
+ *
+ * let t = new CSSTransformPreviewer(myRootElement);
+ * t.preview("rotate(45deg)", "top left", 200, 400);
+ * t.preview("skew(19deg)", "center", 100, 500);
+ * t.preview("matrix(1, -0.2, 0, 1, 0, 0)");
+ * t.destroy();
+ *
+ * @param {nsIDOMElement} parentEl
+ *        Where the canvas will go
+ */
+function CSSTransformPreviewer(parentEl) {
+  this.parentEl = parentEl;
+  this.doc = this.parentEl.ownerDocument;
+  this.canvas = null;
+  this.ctx = null;
+}
+
+module.exports.CSSTransformPreviewer = CSSTransformPreviewer;
+
+CSSTransformPreviewer.prototype = {
+  /**
+   * The preview look-and-feel can be changed using these properties
+   */
+  MAX_DIM: 250,
+  PAD: 5,
+  ORIGINAL_FILL: "#1F303F",
+  ORIGINAL_STROKE: "#B2D8FF",
+  TRANSFORMED_FILL: "rgba(200, 200, 200, .5)",
+  TRANSFORMED_STROKE: "#B2D8FF",
+  ARROW_STROKE: "#329AFF",
+  ORIGIN_STROKE: "#329AFF",
+  ARROW_TIP_HEIGHT: 10,
+  ARROW_TIP_WIDTH: 8,
+  CORNER_SIZE_RATIO: 6,
+
+  /**
+   * Destroy removes the canvas from the parentelement passed in the constructor
+   */
+  destroy: function() {
+    if (this.canvas) {
+      this.parentEl.removeChild(this.canvas);
+    }
+    if (this._hiddenDiv) {
+      this.parentEl.removeChild(this._hiddenDiv);
+    }
+    this.parentEl = this.canvas = this.ctx = this.doc = null;
+  },
+
+  _createMarkup: function() {
+    this.canvas = this.doc.createElementNS(HTML_NS, "canvas");
+
+    this.canvas.setAttribute("id", "canvas");
+    this.canvas.setAttribute("width", this.MAX_DIM);
+    this.canvas.setAttribute("height", this.MAX_DIM);
+    this.canvas.style.position = "relative";
+    this.parentEl.appendChild(this.canvas);
+
+    this.ctx = this.canvas.getContext("2d");
+  },
+
+  _getComputed: function(name, value, width, height) {
+    if (!this._hiddenDiv) {
+      // Create a hidden element to apply the style to
+      this._hiddenDiv = this.doc.createElementNS(HTML_NS, "div");
+      this._hiddenDiv.style.visibility = "hidden";
+      this._hiddenDiv.style.position = "absolute";
+      this.parentEl.appendChild(this._hiddenDiv);
+    }
+
+    // Camelcase the name
+    name = name.replace(/-([a-z]{1})/g, (m, letter) => letter.toUpperCase());
+
+    // Apply width and height to make sure computation is made correctly
+    this._hiddenDiv.style.width = width + "px";
+    this._hiddenDiv.style.height = height + "px";
+
+    // Show the hidden div, apply the style, read the computed style, hide the
+    // hidden div again
+    this._hiddenDiv.style.display = "block";
+    this._hiddenDiv.style[name] = value;
+    let computed = this.doc.defaultView.getComputedStyle(this._hiddenDiv);
+    let computedValue = computed[name];
+    this._hiddenDiv.style.display = "none";
+
+    return computedValue;
+  },
+
+  _getMatrixFromTransformString: function(transformStr) {
+    let matrix = transformStr.substring(0, transformStr.length - 1).
+      substring(transformStr.indexOf("(") + 1).split(",");
+
+    matrix.forEach(function(value, index) {
+      matrix[index] = parseFloat(value, 10);
+    });
+
+    let transformMatrix = null;
+
+    if (matrix.length === 6) {
+      // 2d transform
+      transformMatrix = [
+        [matrix[0], matrix[2], matrix[4], 0],
+        [matrix[1], matrix[3], matrix[5], 0],
+        [0,     0,     1,     0],
+        [0,     0,     0,     1]
+      ];
+    } else {
+      // 3d transform
+      transformMatrix = [
+        [matrix[0], matrix[4], matrix[8],  matrix[12]],
+        [matrix[1], matrix[5], matrix[9],  matrix[13]],
+        [matrix[2], matrix[6], matrix[10], matrix[14]],
+        [matrix[3], matrix[7], matrix[11], matrix[15]]
+      ];
+    }
+
+    return transformMatrix;
+  },
+
+  _getOriginFromOriginString: function(originStr) {
+    let offsets = originStr.split(" ");
+    offsets.forEach(function(item, index) {
+      offsets[index] = parseInt(item, 10);
+    });
+
+    return offsets;
+  },
+
+  _multiply: function(m1, m2) {
+    let m = [];
+    for (let m1Line = 0; m1Line < m1.length; m1Line++) {
+      m[m1Line] = 0;
+      for (let m2Col = 0; m2Col < m2.length; m2Col++) {
+        m[m1Line] += m1[m1Line][m2Col] * m2[m2Col];
+      }
+    }
+    return [m[0], m[1]];
+  },
+
+  _getTransformedPoint: function(matrix, point, origin) {
+    let pointMatrix = [point[0] - origin[0], point[1] - origin[1], 1, 1];
+    return this._multiply(matrix, pointMatrix);
+  },
+
+  _getTransformedPoints: function(matrix, rect, origin) {
+    return rect.map(point => {
+      let tPoint = this._getTransformedPoint(matrix, [point[0], point[1]], origin);
+      return [tPoint[0] + origin[0], tPoint[1] + origin[1]];
+    });
+  },
+
+  /**
+   * For canvas to avoid anti-aliasing
+   */
+  _round: x => Math.round(x) + .5,
+
+  _drawShape: function(points, fillStyle, strokeStyle) {
+    this.ctx.save();
+
+    this.ctx.lineWidth = 1;
+    this.ctx.strokeStyle = strokeStyle;
+    this.ctx.fillStyle = fillStyle;
+
+    this.ctx.beginPath();
+    this.ctx.moveTo(this._round(points[0][0]), this._round(points[0][1]));
+    for (var i = 1; i < points.length; i++) {
+      this.ctx.lineTo(this._round(points[i][0]), this._round(points[i][1]));
+    }
+    this.ctx.lineTo(this._round(points[0][0]), this._round(points[0][1]));
+    this.ctx.fill();
+    this.ctx.stroke();
+
+    this.ctx.restore();
+  },
+
+  _drawArrow: function(x1, y1, x2, y2) {
+    // do not draw if the line is too small
+    if (Math.abs(x2-x1) < 20 && Math.abs(y2-y1) < 20) {
+      return;
+    }
+
+    this.ctx.save();
+
+    this.ctx.strokeStyle = this.ARROW_STROKE;
+    this.ctx.fillStyle = this.ARROW_STROKE;
+    this.ctx.lineWidth = 1;
+
+    this.ctx.beginPath();
+    this.ctx.moveTo(this._round(x1), this._round(y1));
+    this.ctx.lineTo(this._round(x2), this._round(y2));
+    this.ctx.stroke();
+
+    this.ctx.beginPath();
+    this.ctx.translate(x2, y2);
+    let radians = Math.atan((y1 - y2) / (x1 - x2));
+    radians += ((x1 >= x2) ? -90 : 90) * Math.PI / 180;
+    this.ctx.rotate(radians);
+    this.ctx.moveTo(0, 0);
+    this.ctx.lineTo(this.ARROW_TIP_WIDTH / 2, this.ARROW_TIP_HEIGHT);
+    this.ctx.lineTo(-this.ARROW_TIP_WIDTH / 2, this.ARROW_TIP_HEIGHT);
+    this.ctx.closePath();
+    this.ctx.fill();
+
+    this.ctx.restore();
+  },
+
+  _drawOrigin: function(x, y) {
+    this.ctx.save();
+
+    this.ctx.strokeStyle = this.ORIGIN_STROKE;
+    this.ctx.fillStyle = this.ORIGIN_STROKE;
+
+    this.ctx.beginPath();
+    this.ctx.arc(x, y, 4, 0, 2 * Math.PI, false);
+    this.ctx.stroke();
+    this.ctx.fill();
+
+    this.ctx.restore();
+  },
+
+  /**
+   * Computes the largest width and height of all the given shapes and changes
+   * all of the shapes' points (by reference) so they fit into the configured
+   * MAX_DIM - 2*PAD area.
+   * @return {Object} A {w, h} giving the size the canvas should be
+   */
+  _fitAllShapes: function(allShapes) {
+    let allXs = [], allYs = [];
+    for (let shape of allShapes) {
+      for (let point of shape) {
+        allXs.push(point[0]);
+        allYs.push(point[1]);
+      }
+    }
+    let minX = Math.min.apply(Math, allXs);
+    let maxX = Math.max.apply(Math, allXs);
+    let minY = Math.min.apply(Math, allYs);
+    let maxY = Math.max.apply(Math, allYs);
+
+    let spanX = maxX - minX;
+    let spanY = maxY - minY;
+    let isWide = spanX > spanY;
+
+    let cw = isWide ? this.MAX_DIM :
+      this.MAX_DIM * Math.min(spanX, spanY) / Math.max(spanX, spanY);
+    let ch = !isWide ? this.MAX_DIM :
+      this.MAX_DIM * Math.min(spanX, spanY) / Math.max(spanX, spanY);
+
+    let mapX = x => this.PAD + ((cw - 2 * this.PAD) / (maxX - minX)) * (x - minX);
+    let mapY = y => this.PAD + ((ch - 2 * this.PAD) / (maxY - minY)) * (y - minY);
+
+    for (let shape of allShapes) {
+      for (let point of shape) {
+        point[0] = mapX(point[0]);
+        point[1] = mapY(point[1]);
+      }
+    }
+
+    return {w: cw, h: ch};
+  },
+
+  _drawShapes: function(shape, corner, transformed, transformedCorner) {
+    this._drawOriginal(shape);
+    this._drawOriginalCorner(corner);
+    this._drawTransformed(transformed);
+    this._drawTransformedCorner(transformedCorner);
+  },
+
+  _drawOriginal: function(points) {
+    this._drawShape(points, this.ORIGINAL_FILL, this.ORIGINAL_STROKE);
+  },
+
+  _drawTransformed: function(points) {
+    this._drawShape(points, this.TRANSFORMED_FILL, this.TRANSFORMED_STROKE);
+  },
+
+  _drawOriginalCorner: function(points) {
+    this._drawShape(points, this.ORIGINAL_STROKE, this.ORIGINAL_STROKE);
+  },
+
+  _drawTransformedCorner: function(points) {
+    this._drawShape(points, this.TRANSFORMED_STROKE, this.TRANSFORMED_STROKE);
+  },
+
+  _drawArrows: function(shape, transformed) {
+    this._drawArrow(shape[0][0], shape[0][1], transformed[0][0], transformed[0][1]);
+    this._drawArrow(shape[1][0], shape[1][1], transformed[1][0], transformed[1][1]);
+    this._drawArrow(shape[2][0], shape[2][1], transformed[2][0], transformed[2][1]);
+    this._drawArrow(shape[3][0], shape[3][1], transformed[3][0], transformed[3][1]);
+  },
+
+  /**
+   * Draw a transform preview
+   *
+   * @param {String} transform
+   *        The css transform value as a string, as typed by the user, as long
+   *        as it can be computed by the browser
+   * @param {String} origin
+   *        Same as above for the transform-origin value. Defaults to "center"
+   * @param {Number} width
+   *        The width of the container. Defaults to 200
+   * @param {Number} height
+   *        The height of the container. Defaults to 200
+   * @return {Boolean} Whether or not the preview could be created. Will return
+   *         false for instance if the transform is invalid
+   */
+  preview: function(transform, origin="center", width=200, height=200) {
+    // Create/clear the canvas
+    if (!this.canvas) {
+      this._createMarkup();
+    }
+    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
+
+    // Get computed versions of transform and origin
+    transform = this._getComputed("transform", transform, width, height);
+    if (transform && transform !== "none") {
+      origin = this._getComputed("transform-origin", origin, width, height);
+
+      // Get the matrix, origin and width height data for the previewed element
+      let originData = this._getOriginFromOriginString(origin);
+      let matrixData = this._getMatrixFromTransformString(transform);
+
+      // Compute the original box rect and transformed box rect
+      let shapePoints = [
+        [0, 0],
+        [width, 0],
+        [width, height],
+        [0, height]
+      ];
+      let transformedPoints = this._getTransformedPoints(matrixData, shapePoints, originData);
+
+      // Do the same for the corner triangle shape
+      let cornerSize = Math.min(shapePoints[2][1] - shapePoints[1][1],
+        shapePoints[1][0] - shapePoints[0][0]) / this.CORNER_SIZE_RATIO;
+      let cornerPoints = [
+        [shapePoints[1][0], shapePoints[1][1]],
+        [shapePoints[1][0], shapePoints[1][1] + cornerSize],
+        [shapePoints[1][0] - cornerSize, shapePoints[1][1]]
+      ];
+      let transformedCornerPoints = this._getTransformedPoints(matrixData, cornerPoints, originData);
+
+      // Resize points to fit everything in the canvas
+      let {w, h} = this._fitAllShapes([
+        shapePoints,
+        transformedPoints,
+        cornerPoints,
+        transformedCornerPoints,
+        [originData]
+      ]);
+
+      this.canvas.setAttribute("width", w);
+      this.canvas.setAttribute("height", h);
+
+      this._drawShapes(shapePoints, cornerPoints, transformedPoints, transformedCornerPoints)
+      this._drawArrows(shapePoints, transformedPoints);
+      this._drawOrigin(originData[0], originData[1]);
+
+      return true;
+    } else {
+      return false;
+    }
+  }
+};
--- a/browser/devtools/shared/widgets/Tooltip.js
+++ b/browser/devtools/shared/widgets/Tooltip.js
@@ -7,16 +7,17 @@
 const {Cc, Cu, Ci} = require("chrome");
 const promise = require("sdk/core/promise");
 const IOService = Cc["@mozilla.org/network/io-service;1"]
   .getService(Ci.nsIIOService);
 const {Spectrum} = require("devtools/shared/widgets/Spectrum");
 const EventEmitter = require("devtools/shared/event-emitter");
 const {colorUtils} = require("devtools/css-color");
 const Heritage = require("sdk/core/heritage");
+const {CSSTransformPreviewer} = require("devtools/shared/widgets/CSSTransformPreviewer");
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "setNamedTimeout",
   "resource:///modules/devtools/ViewHelpers.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "clearNamedTimeout",
   "resource:///modules/devtools/ViewHelpers.jsm");
@@ -95,17 +96,16 @@ let PanelFactory = {
    *        An options store to get some configuration from
    */
   get: function(doc, options) {
     // Create the tooltip
     let panel = doc.createElement("panel");
     panel.setAttribute("hidden", true);
     panel.setAttribute("ignorekeys", true);
 
-    // Prevent the click used to close the panel from being consumed
     panel.setAttribute("consumeoutsideclicks", options.get("consumeOutsideClick"));
     panel.setAttribute("noautofocus", options.get("noAutoFocus"));
     panel.setAttribute("type", "arrow");
     panel.setAttribute("level", "top");
 
     panel.setAttribute("class", "devtools-tooltip theme-tooltip-panel");
     doc.querySelector("window").appendChild(panel);
 
@@ -224,16 +224,20 @@ Tooltip.prototype = {
     this.panel.hidden = true;
     this.panel.hidePopup();
   },
 
   isShown: function() {
     return this.panel.state !== "closed" && this.panel.state !== "hiding";
   },
 
+  setSize: function(width, height) {
+    this.panel.sizeTo(width, height);
+  },
+
   /**
    * Empty the tooltip's content
    */
   empty: function() {
     while (this.panel.hasChildNodes()) {
       this.panel.removeChild(this.panel.firstChild);
     }
   },
@@ -299,25 +303,26 @@ Tooltip.prototype = {
    *
    * Note that if you call this function a second time, it will itself call
    * stopTogglingOnHover before adding mouse tracking listeners again.
    *
    * @param {node} baseNode
    *        The container for all target nodes
    * @param {Function} targetNodeCb
    *        A function that accepts a node argument and returns true or false
-   *        to signify if the tooltip should be shown on that node or not.
+   *        (or a promise that resolves or rejects) to signify if the tooltip
+   *        should be shown on that node or not.
    *        Additionally, the function receives a second argument which is the
    *        tooltip instance itself, to be used to add/modify the content of the
    *        tooltip if needed. If omitted, the tooltip will be shown everytime.
    * @param {Number} showDelay
    *        An optional delay that will be observed before showing the tooltip.
    *        Defaults to this.defaultShowDelay.
    */
-  startTogglingOnHover: function(baseNode, targetNodeCb, showDelay = this.defaultShowDelay) {
+  startTogglingOnHover: function(baseNode, targetNodeCb, showDelay=this.defaultShowDelay) {
     if (this._basedNode) {
       this.stopTogglingOnHover();
     }
 
     this._basedNode = baseNode;
     this._showDelay = showDelay;
     this._targetNodeCb = targetNodeCb || (() => true);
 
@@ -352,17 +357,22 @@ Tooltip.prototype = {
       this._lastHovered = event.target;
       setNamedTimeout(this.uid, this._showDelay, () => {
         this._showOnHover(event.target);
       });
     }
   },
 
   _showOnHover: function(target) {
-    if (this._targetNodeCb(target, this)) {
+    let res = this._targetNodeCb(target, this);
+    if (res && res.then) {
+      res.then(() => {
+        this.show(target);
+      });
+    } else if (res) {
       this.show(target);
     }
   },
 
   _onBaseNodeMouseLeave: function() {
     clearNamedTimeout(this.uid);
     this._lastHovered = null;
     this.hide();
@@ -522,16 +532,18 @@ Tooltip.prototype = {
     imgObj.src = imageUrl;
     imgObj.onload = () => {
       imgObj.onload = null;
 
       // Display dimensions
       let w = options.naturalWidth || imgObj.naturalWidth;
       let h = options.naturalHeight || imgObj.naturalHeight;
       label.textContent = w + " x " + h;
+
+      this.setSize(vbox.width, vbox.height);
     }
   },
 
   /**
    * Exactly the same as the `image` function but takes a css background image
    * value instead : url(....)
    */
   setCssBackgroundImageContent: function(cssBackground, sheetHref, maxDim=400) {
@@ -579,16 +591,64 @@ Tooltip.prototype = {
     }
     iframe.addEventListener("load", onLoad, true);
     iframe.setAttribute("src", SPECTRUM_FRAME);
 
     // Put the iframe in the tooltip
     this.content = iframe;
 
     return def.promise;
+  },
+
+  /**
+   * Set the content of the tooltip to be the result of CSSTransformPreviewer.
+   * Meaning a canvas previewing a css transformation.
+   *
+   * @param {String} transform
+   *        The CSS transform value (e.g. "rotate(45deg) translateX(50px)")
+   * @param {PageStyleActor} pageStyle
+   *        An instance of the PageStyleActor that will be used to retrieve
+   *        computed styles
+   * @param {NodeActor} node
+   *        The NodeActor for the currently selected node
+   * @return A promise that resolves when the tooltip content is ready, or
+   *         rejects if no transform is provided or is invalid
+   */
+  setCssTransformContent: function(transform, pageStyle, node) {
+    let def = promise.defer();
+
+    if (transform) {
+      // Look into the computed styles to find the width and height and possibly
+      // the origin if it hadn't been provided
+      pageStyle.getComputed(node, {
+        filter: "user",
+        markMatched: false,
+        onlyMatched: false
+      }).then(styles => {
+        let origin = styles["transform-origin"].value;
+        let width = parseInt(styles["width"].value);
+        let height = parseInt(styles["height"].value);
+
+        let root = this.doc.createElementNS(XHTML_NS, "div");
+        let previewer = new CSSTransformPreviewer(root);
+        this.content = root;
+        if (!previewer.preview(transform, origin, width, height)) {
+          // If the preview didn't work, reject the promise
+          def.reject();
+        } else {
+          // Else, make sure the tooltip has the right size and resolve
+          this.setSize(previewer.canvas.width, previewer.canvas.height);
+          def.resolve();
+        }
+      });
+    } else {
+      def.reject();
+    }
+
+    return def.promise;
   }
 };
 
 /**
  * Base class for all (color, gradient, ...)-swatch based value editors inside
  * tooltips
  *
  * @param {XULDocument} doc
--- a/browser/devtools/styleinspector/computed-view.js
+++ b/browser/devtools/styleinspector/computed-view.js
@@ -502,31 +502,37 @@ CssHtmlTree.prototype = {
   },
 
   /**
    * Verify that target is indeed a css value we want a tooltip on, and if yes
    * prepare some content for the tooltip
    */
   _buildTooltipContent: function(target)
   {
-    // If the hovered element is not a property view and is not a background
-    // image, then don't show a tooltip
-    let isPropertyValue = target.classList.contains("property-value");
-    if (!isPropertyValue) {
-      return false;
-    }
-    let propName = target.parentNode.querySelector(".property-name");
-    let isBackgroundImage = propName.textContent === "background-image";
-    if (!isBackgroundImage) {
-      return false;
+    // Test for image url
+    if (target.classList.contains("theme-link")) {
+      let propValue = target.parentNode;
+      let propName = propValue.parentNode.querySelector(".property-name");
+      if (propName.textContent === "background-image") {
+        this.tooltip.setCssBackgroundImageContent(propValue.textContent);
+        return true;
+      }
     }
 
-    // Fill some content
-    this.tooltip.setCssBackgroundImageContent(target.textContent);
-    return true;
+    // Test for css transform
+    if (target.classList.contains("property-value")) {
+      let def = promise.defer();
+      let propValue = target;
+      let propName = target.parentNode.querySelector(".property-name");
+      if (propName.textContent === "transform") {
+        this.tooltip.setCssTransformContent(propValue.textContent,
+          this.pageStyle, this.viewedElement).then(def.resolve);
+        return def.promise;
+      }
+    }
   },
 
   /**
    * Create a context menu.
    */
   _buildContextMenu: function()
   {
     let doc = this.styleDocument.defaultView.parent.document;
--- a/browser/devtools/styleinspector/rule-view.js
+++ b/browser/devtools/styleinspector/rule-view.js
@@ -1149,37 +1149,42 @@ CssRuleView.prototype = {
     popupset.appendChild(this._contextmenu);
   },
 
   /**
    * Verify that target is indeed a css value we want a tooltip on, and if yes
    * prepare some content for the tooltip
    */
   _buildTooltipContent: function(target) {
-    let isImageHref = target.classList.contains("theme-link") &&
-      target.parentNode.classList.contains("ruleview-propertyvalue");
+    let property = target.textProperty, def = promise.defer(), hasTooltip = false;
 
-    // If the inplace-editor is visible or if this is not a background image
-    // don't show the tooltip
-    if (!isImageHref) {
-      return false;
+    // Test for css transform
+    if (property && property.name === "transform") {
+      this.previewTooltip.setCssTransformContent(property.value, this.pageStyle,
+        this._viewedElement).then(def.resolve);
+      hasTooltip = true;
     }
 
-    // Retrieve the TextProperty for the hovered element
-    let property = target.parentNode.textProperty;
-    let href = property.rule.domRule.href;
+    // Test for image
+    let isImageHref = target.classList.contains("theme-link") &&
+      target.parentNode.classList.contains("ruleview-propertyvalue");
+    if (isImageHref) {
+      property = target.parentNode.textProperty;
+      this.previewTooltip.setCssBackgroundImageContent(property.value,
+        property.rule.domRule.href);
+      def.resolve();
+      hasTooltip = true;
+    }
 
-    // Fill some content
-    this.previewTooltip.setCssBackgroundImageContent(property.value, href);
+    if (hasTooltip) {
+      this.colorPicker.revert();
+      this.colorPicker.hide();
+    }
 
-    // Hide the color picker tooltip if shown and revert changes
-    this.colorPicker.revert();
-    this.colorPicker.hide();
-
-    return true;
+    return def.promise;
   },
 
   /**
    * Update the context menu. This means enabling or disabling menuitems as
    * appropriate.
    */
   _contextMenuUpdate: function() {
     let win = this.doc.defaultView;
@@ -1325,17 +1330,17 @@ CssRuleView.prototype = {
     }
 
     this.popup.destroy();
   },
 
   /**
    * Update the highlighted element.
    *
-   * @param {nsIDOMElement} aElement
+   * @param {NodeActor} aElement
    *        The node whose style rules we'll inspect.
    */
   highlight: function CssRuleView_highlight(aElement)
   {
     if (this._viewedElement === aElement) {
       return promise.resolve(undefined);
     }
 
--- a/browser/devtools/styleinspector/test/browser.ini
+++ b/browser/devtools/styleinspector/test/browser.ini
@@ -48,16 +48,18 @@ support-files =
 [browser_bug894376_css_value_completion_existing_property_value_pair.js]
 [browser_ruleview_bug_902966_revert_value_on_ESC.js]
 [browser_ruleview_pseudoelement.js]
 support-files = browser_ruleview_pseudoelement.html
 [browser_computedview_bug835808_keyboard_nav.js]
 [browser_bug913014_matched_expand.js]
 [browser_bug765105_background_image_tooltip.js]
 [browser_bug889638_rule_view_color_picker.js]
+[browser_bug726427_csstransform_tooltip.js]
+
 [browser_bug940500_rule_view_pick_gradient_color.js]
 [browser_ruleview_original_source_link.js]
 support-files =
   sourcemaps.html
   sourcemaps.css
   sourcemaps.css.map
   sourcemaps.scss
 [browser_computedview_original_source_link.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_bug726427_csstransform_tooltip.js
@@ -0,0 +1,206 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let contentDoc;
+let inspector;
+let ruleView;
+let computedView;
+
+const PAGE_CONTENT = [
+  '<style type="text/css">',
+  '  #testElement {',
+  '    width: 500px;',
+  '    height: 300px;',
+  '    background: red;',
+  '    transform: skew(16deg);',
+  '  }',
+  '  .test-element {',
+  '    transform-origin: top left;',
+  '    transform: rotate(45deg);',
+  '  }',
+  '  div {',
+  '    transform: scaleX(1.5);',
+  '    transform-origin: bottom right;',
+  '  }',
+  '  [attr] {',
+  '  }',
+  '</style>',
+  '<div id="testElement" class="test-element" attr="value">transformed element</div>'
+].join("\n");
+
+function test() {
+  waitForExplicitFinish();
+
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function(evt) {
+    gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee, true);
+    contentDoc = content.document;
+    waitForFocus(createDocument, content);
+  }, true);
+
+  content.location = "data:text/html,rule view css transform tooltip test";
+}
+
+function createDocument() {
+  contentDoc.body.innerHTML = PAGE_CONTENT;
+
+  openRuleView((aInspector, aRuleView) => {
+    inspector = aInspector;
+    ruleView = aRuleView;
+    startTests();
+  });
+}
+
+function startTests() {
+  inspector.selection.setNode(contentDoc.querySelector("#testElement"));
+  inspector.once("inspector-updated", testTransformTooltipOnIDSelector);
+}
+
+function endTests() {
+  contentDoc = inspector = ruleView = computedView = null;
+  gBrowser.removeCurrentTab();
+  finish();
+}
+
+function testTransformTooltipOnIDSelector() {
+  info("Testing that a transform tooltip appears on the #ID rule");
+
+  let panel = ruleView.previewTooltip.panel;
+  ok(panel, "The XUL panel exists for the rule-view preview tooltips");
+
+  let {valueSpan} = getRuleViewProperty("#testElement", "transform");
+  assertTooltipShownOn(ruleView.previewTooltip, valueSpan, () => {
+    // The transform preview is canvas, so there's not much we can test, so for
+    // now, let's just be happy with the fact that the tooltips is shown!
+    ok(true, "Tooltip shown on the transform property of the #ID rule");
+    ruleView.previewTooltip.hide();
+    executeSoon(testTransformTooltipOnClassSelector);
+  });
+}
+
+function testTransformTooltipOnClassSelector() {
+  info("Testing that a transform tooltip appears on the .class rule");
+
+  let {valueSpan} = getRuleViewProperty(".test-element", "transform");
+  assertTooltipShownOn(ruleView.previewTooltip, valueSpan, () => {
+    // The transform preview is canvas, so there's not much we can test, so for
+    // now, let's just be happy with the fact that the tooltips is shown!
+    ok(true, "Tooltip shown on the transform property of the .class rule");
+    ruleView.previewTooltip.hide();
+    executeSoon(testTransformTooltipOnTagSelector);
+  });
+}
+
+function testTransformTooltipOnTagSelector() {
+  info("Testing that a transform tooltip appears on the tag rule");
+
+  let {valueSpan} = getRuleViewProperty("div", "transform");
+  assertTooltipShownOn(ruleView.previewTooltip, valueSpan, () => {
+    // The transform preview is canvas, so there's not much we can test, so for
+    // now, let's just be happy with the fact that the tooltips is shown!
+    ok(true, "Tooltip shown on the transform property of the tag rule");
+    ruleView.previewTooltip.hide();
+    executeSoon(testTransformTooltipNotShownOnInvalidTransform);
+  });
+}
+
+function testTransformTooltipNotShownOnInvalidTransform() {
+  info("Testing that a transform tooltip does not appear for invalid values");
+
+  let ruleEditor;
+  for (let rule of ruleView._elementStyle.rules) {
+    if (rule.matchedSelectors[0] === "[attr]") {
+      ruleEditor = rule.editor;
+    }
+  }
+  ruleEditor.addProperty("transform", "muchTransform(suchAngle)", "");
+
+  let {valueSpan} = getRuleViewProperty("[attr]", "transform");
+  assertTooltipNotShownOn(ruleView.previewTooltip, valueSpan, () => {
+    executeSoon(testTransformTooltipOnComputedView);
+  });
+}
+
+function testTransformTooltipOnComputedView() {
+  info("Testing that a transform tooltip appears in the computed view too");
+
+  inspector.sidebar.select("computedview");
+  computedView = inspector.sidebar.getWindowForTab("computedview").computedview.view;
+  let doc = computedView.styleDocument;
+
+  let panel = computedView.tooltip.panel;
+  let {valueSpan} = getComputedViewProperty("transform");
+
+  assertTooltipShownOn(computedView.tooltip, valueSpan, () => {
+    // The transform preview is canvas, so there's not much we can test, so for
+    // now, let's just be happy with the fact that the tooltips is shown!
+    ok(true, "Tooltip shown on the computed transform property");
+    computedView.tooltip.hide();
+    executeSoon(endTests);
+  });
+}
+
+function assertTooltipShownOn(tooltip, element, cb) {
+  // If there is indeed a show-on-hover on element, the xul panel will be shown
+  tooltip.panel.addEventListener("popupshown", function shown() {
+    tooltip.panel.removeEventListener("popupshown", shown, true);
+    cb();
+  }, true);
+  tooltip._showOnHover(element);
+}
+
+function assertTooltipNotShownOn(tooltip, element, cb) {
+  // The only way to make sure the tooltip is not shown is try and show it, wait
+  // for a given amount of time, and then check if it's shown or not
+  tooltip._showOnHover(element);
+  setTimeout(() => {
+    ok(!tooltip.isShown(), "The tooltip did not appear on hover of the element");
+    cb();
+  }, tooltip.defaultShowDelay + 100);
+}
+
+function getRule(selectorText) {
+  let rule;
+
+  [].forEach.call(ruleView.doc.querySelectorAll(".ruleview-rule"), aRule => {
+    let selector = aRule.querySelector(".ruleview-selector-matched");
+    if (selector && selector.textContent === selectorText) {
+      rule = aRule;
+    }
+  });
+
+  return rule;
+}
+
+function getRuleViewProperty(selectorText, propertyName) {
+  let prop;
+
+  let rule = getRule(selectorText);
+  if (rule) {
+    // Look for the propertyName in that rule element
+    [].forEach.call(rule.querySelectorAll(".ruleview-property"), property => {
+      let nameSpan = property.querySelector(".ruleview-propertyname");
+      let valueSpan = property.querySelector(".ruleview-propertyvalue");
+
+      if (nameSpan.textContent === propertyName) {
+        prop = {nameSpan: nameSpan, valueSpan: valueSpan};
+      }
+    });
+  }
+
+  return prop;
+}
+
+function getComputedViewProperty(name) {
+  let prop;
+  [].forEach.call(computedView.styleDocument.querySelectorAll(".property-view"), property => {
+    let nameSpan = property.querySelector(".property-name");
+    let valueSpan = property.querySelector(".property-value");
+
+    if (nameSpan.textContent === name) {
+      prop = {nameSpan: nameSpan, valueSpan: valueSpan};
+    }
+  });
+  return prop;
+}
--- a/browser/devtools/styleinspector/test/browser_bug765105_background_image_tooltip.js
+++ b/browser/devtools/styleinspector/test/browser_bug765105_background_image_tooltip.js
@@ -137,18 +137,19 @@ function testComputedView() {
   info("Testing tooltips in the computed view");
 
   inspector.sidebar.select("computedview");
   computedView = inspector.sidebar.getWindowForTab("computedview").computedview.view;
   let doc = computedView.styleDocument;
 
   let panel = computedView.tooltip.panel;
   let {valueSpan} = getComputedViewProperty("background-image");
+  let uriSpan = valueSpan.querySelector(".theme-link");
 
-  assertTooltipShownOn(computedView.tooltip, valueSpan, () => {
+  assertTooltipShownOn(computedView.tooltip, uriSpan, () => {
     let images = panel.getElementsByTagName("image");
     is(images.length, 1, "Tooltip contains an image");
     ok(images[0].src === "chrome://global/skin/icons/warning-64.png");
 
     computedView.tooltip.hide();
 
     endTests();
   });
--- a/browser/locales/en-US/chrome/browser/aboutDialog.dtd
+++ b/browser/locales/en-US/chrome/browser/aboutDialog.dtd
@@ -1,13 +1,24 @@
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/.  -->
 <!ENTITY aboutDialog.title          "About &brandFullName;">
 
+<!-- LOCALIZATION NOTE update.applyButton.*, update.upgradeButton.*):
+# Only one button is present at a time.
+# The button when displayed is located directly under the Firefox version in
+# the about dialog (see bug 596813 for screenshots).
+-->
+<!ENTITY update.updateButton.label                "Restart to Update">
+<!ENTITY update.updateButton.accesskey            "R">
+<!ENTITY update.applyButtonBillboard.label        "Apply Update…">
+<!ENTITY update.applyButtonBillboard.accesskey    "A">
+
+
 <!-- LOCALIZATION NOTE (warningDesc.version): This is a warning about the experimental nature of Nightly and Aurora builds. It is only shown in those versions. -->
 <!ENTITY warningDesc.version        "&brandShortName; is experimental and may be unstable.">
 <!-- LOCALIZATION NOTE (warningDesc.telemetryDesc): This is a notification that Nightly/Aurora builds automatically send Telemetry data back to Mozilla. It is only shown in those versions. "It" refers to brandShortName. -->
 <!ENTITY warningDesc.telemetryDesc  "It automatically sends information about performance, hardware, usage and customizations back to &vendorShortName; to help make &brandShortName; better.">
 
 <!-- LOCALIZATION NOTE (community.exp.*) This paragraph is shown in "experimental" builds, i.e. Nightly and Aurora builds, instead of the other "community.*" strings below. -->
 <!ENTITY community.exp.start        "">
 <!-- LOCALIZATION NOTE (community.exp.mozillaLink): This is a link title that links to http://www.mozilla.org/. -->
@@ -36,18 +47,16 @@
 <!-- LOCALIZATION NOTE (bottomLinks.rights): This is a link title that links to about:rights. -->
 <!ENTITY bottomLinks.rights         "End-User Rights">
 
 <!-- LOCALIZATION NOTE (bottomLinks.privacy): This is a link title that links to https://www.mozilla.org/legal/privacy/. -->
 <!ENTITY bottomLinks.privacy        "Privacy Policy">
 
 <!-- LOCALIZATION NOTE (update.checkingForUpdates): try to make the localized text short (see bug 596813 for screenshots). -->
 <!ENTITY update.checkingForUpdates  "Checking for updates…">
-<!-- LOCALIZATION NOTE (update.checkingAddonCompat): try to make the localized text short (see bug 596813 for screenshots). -->
-<!ENTITY update.checkingAddonCompat "Checking Add-on compatibility…">
 <!-- LOCALIZATION NOTE (update.noUpdatesFound): try to make the localized text short (see bug 596813 for screenshots). -->
 <!ENTITY update.noUpdatesFound      "&brandShortName; is up to date">
 <!-- LOCALIZATION NOTE (update.adminDisabled): try to make the localized text short (see bug 596813 for screenshots). -->
 <!ENTITY update.adminDisabled       "Updates disabled by your system administrator">
 <!-- LOCALIZATION NOTE (update.otherInstanceHandlingUpdates): try to make the localized text short -->
 <!ENTITY update.otherInstanceHandlingUpdates "&brandShortName; is being updated by another instance">
 
 <!-- LOCALIZATION NOTE (update.failed.start,update.failed.linkText,update.failed.end):
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -186,34 +186,20 @@ sanitizeButtonClearing=Clearing
 # "Time range to clear" is set to "Everything" in Clear Recent History dialog,
 # provided that the user has not modified the default set of history items to clear.
 sanitizeEverythingWarning2=All history will be cleared.
 # LOCALIZATION NOTE (sanitizeSelectedWarning): Warning that appears when
 # "Time range to clear" is set to "Everything" in Clear Recent History dialog,
 # provided that the user has modified the default set of history items to clear.
 sanitizeSelectedWarning=All selected items will be cleared.
 
-# Check for Updates in the About Dialog - button labels and accesskeys
-# LOCALIZATION NOTE - all of the following update buttons labels will only be
-# displayed one at a time. So, if a button is displayed nothing else will
-# be displayed alongside of the button. The button when displayed is located
-# directly under the Firefox version in the about dialog (see bug 596813 for
-# screenshots).
-update.checkInsideButton.label=Check for Updates
-update.checkInsideButton.accesskey=C
-update.resumeButton.label=Resume Downloading %S…
-update.resumeButton.accesskey=D
-update.openUpdateUI.applyButton.label=Apply Update…
-update.openUpdateUI.applyButton.accesskey=A
-update.restart.updateButton.label=Restart to Update
-update.restart.updateButton.accesskey=R
-update.openUpdateUI.upgradeButton.label=Upgrade Now…
-update.openUpdateUI.upgradeButton.accesskey=U
-update.restart.upgradeButton.label=Upgrade Now
-update.restart.upgradeButton.accesskey=U
+# LOCALIZATION NOTE (downloadAndInstallButton.label): %S is replaced by the
+# version of the update: "Update to 28.0".
+update.downloadAndInstallButton.label=Update to %S
+update.downloadAndInstallButton.accesskey=U
 
 # RSS Pretty Print
 feedShowFeedNew=Subscribe to '%S'…
 
 menuOpenAllInTabs.label=Open All in Tabs
 
 # History menu
 menuRestoreAllTabs.label=Restore All Tabs
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -161,17 +161,17 @@ pref("browser.formfill.enable", true);
 /* spellcheck */
 pref("layout.spellcheckDefault", 0);
 
 /* new html5 forms */
 pref("dom.experimental_forms", true);
 pref("dom.forms.number", true);
 // Don't enable <input type=color> yet as we don't have a color picker
 // implemented for Android (bug 875750)
-pref("dom.forms.color", false);
+pref("dom.forms.color", true);
 
 /* extension manager and xpinstall */
 pref("xpinstall.whitelist.add", "addons.mozilla.org");
 pref("xpinstall.whitelist.add.180", "marketplace.firefox.com");
 
 pref("extensions.enabledScopes", 1);
 pref("extensions.autoupdate.enabled", true);
 pref("extensions.autoupdate.interval", 86400);
--- a/mobile/android/base/GeckoAppShell.java
+++ b/mobile/android/base/GeckoAppShell.java
@@ -1510,28 +1510,36 @@ public class GeckoAppShell
             }
         });
     }
 
     @WrapElementForJNI
     public static boolean isNetworkLinkUp() {
         ConnectivityManager cm = (ConnectivityManager)
            getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
-        NetworkInfo info = cm.getActiveNetworkInfo();
-        if (info == null || !info.isConnected())
+        try {
+            NetworkInfo info = cm.getActiveNetworkInfo();
+            if (info == null || !info.isConnected())
+                return false;
+        } catch (SecurityException se) {
             return false;
+        }
         return true;
     }
 
     @WrapElementForJNI
     public static boolean isNetworkLinkKnown() {
         ConnectivityManager cm = (ConnectivityManager)
             getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
-        if (cm.getActiveNetworkInfo() == null)
+        try {
+            if (cm.getActiveNetworkInfo() == null)
+                return false;
+        } catch (SecurityException se) {
             return false;
+        }
         return true;
     }
 
     @WrapElementForJNI
     public static int networkLinkType() {
         ConnectivityManager cm = (ConnectivityManager)
             getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
         NetworkInfo info = cm.getActiveNetworkInfo();
--- a/mobile/android/base/GeckoNetworkManager.java
+++ b/mobile/android/base/GeckoNetworkManager.java
@@ -215,17 +215,21 @@ public class GeckoNetworkManager extends
     private static NetworkType getNetworkType() {
         ConnectivityManager cm =
             (ConnectivityManager)sInstance.mApplicationContext.getSystemService(Context.CONNECTIVITY_SERVICE);
         if (cm == null) {
             Log.e(LOGTAG, "Connectivity service does not exist");
             return NetworkType.NETWORK_NONE;
         }
 
-        NetworkInfo ni = cm.getActiveNetworkInfo();
+        NetworkInfo ni = null;
+        try {
+            ni = cm.getActiveNetworkInfo();
+        } catch (SecurityException se) {} // if we don't have the permission, fall through to null check
+
         if (ni == null) {
             return NetworkType.NETWORK_NONE;
         }
 
         switch (ni.getType()) {
         case ConnectivityManager.TYPE_ETHERNET:
             return NetworkType.NETWORK_ETHERNET;
         case ConnectivityManager.TYPE_WIFI:
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -261,16 +261,17 @@ gbjar.sources += [
     'preferences/LinkPreference.java',
     'preferences/MultiChoicePreference.java',
     'preferences/PrivateDataPreference.java',
     'preferences/SearchEnginePreference.java',
     'preferences/SearchPreferenceCategory.java',
     'preferences/SyncPreference.java',
     'PrefsHelper.java',
     'PrivateTab.java',
+    'prompts/ColorPickerInput.java',
     'prompts/IconGridInput.java',
     'prompts/Prompt.java',
     'prompts/PromptInput.java',
     'prompts/PromptService.java',
     'ReaderModeUtils.java',
     'ReferrerReceiver.java',
     'RemoteTabs.java',
     'Restarter.java',
@@ -309,16 +310,17 @@ gbjar.sources += [
     'updater/UpdateServiceHelper.java',
     'VideoPlayer.java',
     'WebAppAllocator.java',
     'WebAppImpl.java',
     'widget/ActivityChooserModel.java',
     'widget/AllCapsTextView.java',
     'widget/AnimatedHeightLayout.java',
     'widget/ArrowPopup.java',
+    'widget/BasicColorPicker.java',
     'widget/ButtonToast.java',
     'widget/CheckableLinearLayout.java',
     'widget/ClickableWhenDisabledEditText.java',
     'widget/DateTimePicker.java',
     'widget/Divider.java',
     'widget/FaviconView.java',
     'widget/FlowLayout.java',
     'widget/GeckoActionProvider.java',
@@ -425,16 +427,17 @@ ANDROID_RESFILES += [
     'resources/drawable-hdpi/alert_download.png',
     'resources/drawable-hdpi/alert_mic.png',
     'resources/drawable-hdpi/alert_mic_camera.png',
     'resources/drawable-hdpi/arrow_popup_bg.9.png',
     'resources/drawable-hdpi/blank.png',
     'resources/drawable-hdpi/bookmark_folder_closed.png',
     'resources/drawable-hdpi/bookmark_folder_opened.png',
     'resources/drawable-hdpi/close.png',
+    'resources/drawable-hdpi/color_picker_row_bg.9.png',
     'resources/drawable-hdpi/copy.png',
     'resources/drawable-hdpi/cut.png',
     'resources/drawable-hdpi/favicon.png',
     'resources/drawable-hdpi/find_close.png',
     'resources/drawable-hdpi/find_next.png',
     'resources/drawable-hdpi/find_prev.png',
     'resources/drawable-hdpi/folder.png',
     'resources/drawable-hdpi/grid_icon_bg_activated.9.png',
@@ -573,16 +576,17 @@ ANDROID_RESFILES += [
     'resources/drawable-mdpi/arrow_popup_bg.9.png',
     'resources/drawable-mdpi/autocomplete_list_bg.9.png',
     'resources/drawable-mdpi/blank.png',
     'resources/drawable-mdpi/bookmark_folder_closed.png',
     'resources/drawable-mdpi/bookmark_folder_opened.png',
     'resources/drawable-mdpi/bookmarkdefaults_favicon_addons.png',
     'resources/drawable-mdpi/bookmarkdefaults_favicon_support.png',
     'resources/drawable-mdpi/close.png',
+    'resources/drawable-mdpi/color_picker_row_bg.9.png',
     'resources/drawable-mdpi/copy.png',
     'resources/drawable-mdpi/cut.png',
     'resources/drawable-mdpi/desktop_notification.png',
     'resources/drawable-mdpi/favicon.png',
     'resources/drawable-mdpi/find_close.png',
     'resources/drawable-mdpi/find_next.png',
     'resources/drawable-mdpi/find_prev.png',
     'resources/drawable-mdpi/folder.png',
@@ -713,16 +717,17 @@ ANDROID_RESFILES += [
     'resources/drawable-xhdpi/alert_download.png',
     'resources/drawable-xhdpi/alert_mic.png',
     'resources/drawable-xhdpi/alert_mic_camera.png',
     'resources/drawable-xhdpi/arrow_popup_bg.9.png',
     'resources/drawable-xhdpi/blank.png',
     'resources/drawable-xhdpi/bookmark_folder_closed.png',
     'resources/drawable-xhdpi/bookmark_folder_opened.png',
     'resources/drawable-xhdpi/close.png',
+    'resources/drawable-xhdpi/color_picker_row_bg.9.png',
     'resources/drawable-xhdpi/copy.png',
     'resources/drawable-xhdpi/cut.png',
     'resources/drawable-xhdpi/favicon.png',
     'resources/drawable-xhdpi/find_close.png',
     'resources/drawable-xhdpi/find_next.png',
     'resources/drawable-xhdpi/find_prev.png',
     'resources/drawable-xhdpi/folder.png',
     'resources/drawable-xhdpi/grid_icon_bg_activated.9.png',
@@ -813,16 +818,17 @@ ANDROID_RESFILES += [
     'resources/drawable-xlarge-mdpi-v11/ic_menu_bookmark_add.png',
     'resources/drawable-xlarge-mdpi-v11/ic_menu_bookmark_remove.png',
     'resources/drawable-xlarge-v11/home_history_tabs_indicator.xml',
     'resources/drawable-xlarge-xhdpi-v11/ic_menu_bookmark_add.png',
     'resources/drawable-xlarge-xhdpi-v11/ic_menu_bookmark_remove.png',
     'resources/drawable/action_bar_button.xml',
     'resources/drawable/action_bar_button_inverse.xml',
     'resources/drawable/bookmark_folder.xml',
+    'resources/drawable/color_picker_checkmark.xml',
     'resources/drawable/divider_horizontal.xml',
     'resources/drawable/divider_vertical.xml',
     'resources/drawable/handle_end_level.xml',
     'resources/drawable/handle_start_level.xml',
     'resources/drawable/home_banner.xml',
     'resources/drawable/home_history_tabs_indicator.xml',
     'resources/drawable/home_page_title_background.xml',
     'resources/drawable/ic_menu_back.xml',
@@ -862,21 +868,23 @@ ANDROID_RESFILES += [
     'resources/layout-xlarge-v11/home_history_page.xml',
     'resources/layout-xlarge-v11/home_history_tabs_indicator.xml',
     'resources/layout-xlarge-v11/remote_tabs_child.xml',
     'resources/layout-xlarge-v11/remote_tabs_group.xml',
     'resources/layout/actionbar.xml',
     'resources/layout/arrow_popup.xml',
     'resources/layout/autocomplete_list.xml',
     'resources/layout/autocomplete_list_item.xml',
+    'resources/layout/basic_color_picker_dialog.xml',
     'resources/layout/bookmark_edit.xml',
     'resources/layout/bookmark_folder_row.xml',
     'resources/layout/bookmark_item_row.xml',
     'resources/layout/browser_search.xml',
     'resources/layout/browser_toolbar.xml',
+    'resources/layout/color_picker_row.xml',
     'resources/layout/datetime_picker.xml',
     'resources/layout/doorhanger.xml',
     'resources/layout/doorhanger_button.xml',
     'resources/layout/find_in_page_content.xml',
     'resources/layout/font_size_preference.xml',
     'resources/layout/gecko_app.xml',
     'resources/layout/home_banner.xml',
     'resources/layout/home_bookmarks_page.xml',
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/prompts/ColorPickerInput.java
@@ -0,0 +1,63 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.prompts;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.widget.BasicColorPicker;
+
+import org.json.JSONObject;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.view.View;
+import android.view.LayoutInflater;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.LinearLayout.LayoutParams;
+
+public class ColorPickerInput extends PromptInput {
+    public static final String INPUT_TYPE = "color";
+    public static final String LOGTAG = "GeckoColorPickerInput";
+
+    private boolean mShowAdvancedButton = true;
+    private int mInitialColor;
+
+    public ColorPickerInput(JSONObject obj) {
+        super(obj);
+        String init = obj.optString("value");
+        mInitialColor = Color.rgb(Integer.parseInt(init.substring(1,3), 16),
+                                  Integer.parseInt(init.substring(3,5), 16),
+                                  Integer.parseInt(init.substring(5,7), 16));
+    }
+
+    @Override
+    public View getView(Context context) throws UnsupportedOperationException {
+        LayoutInflater inflater = LayoutInflater.from(context);
+        mView = inflater.inflate(R.layout.basic_color_picker_dialog, null);
+
+        BasicColorPicker cp = (BasicColorPicker) mView.findViewById(R.id.colorpicker);
+        cp.setColor(mInitialColor);
+
+        return mView;
+    }
+
+    @Override
+    public String getValue() {
+        BasicColorPicker cp = (BasicColorPicker) mView.findViewById(R.id.colorpicker);
+        int color = cp.getColor();
+        return "#" + Integer.toHexString(color).substring(2);
+    }
+
+    @Override
+    public boolean getScrollable() {
+        return true;
+    }
+
+    @Override
+    public boolean canApplyInputStyle() {
+	return false;
+    }
+}
--- a/mobile/android/base/prompts/Prompt.java
+++ b/mobile/android/base/prompts/Prompt.java
@@ -4,16 +4,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.prompts;
 
 import org.mozilla.gecko.util.GeckoEventResponder;
 import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.widget.DateTimePicker;
+import org.mozilla.gecko.prompts.ColorPickerInput;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.GeckoAppShell;
 
 import org.json.JSONArray;
 import org.json.JSONObject;
 import org.json.JSONException;
 
 import android.app.AlertDialog;
@@ -103,18 +104,21 @@ public class Prompt implements OnClickLi
             mIconTextPadding = (int) (res.getDimension(R.dimen.prompt_service_icon_text_padding));
             mIconSize = (int) (res.getDimension(R.dimen.prompt_service_icon_size));
             mInputPaddingSize = (int) (res.getDimension(R.dimen.prompt_service_inputs_padding));
             mMinRowSize = (int) (res.getDimension(R.dimen.prompt_service_min_list_item_height));
             mInitialized = true;
         }
     }
 
-    private View applyInputStyle(View view) {
-        view.setPadding(mInputPaddingSize, 0, mInputPaddingSize, 0);
+    private View applyInputStyle(View view, PromptInput input) {
+        // Don't add padding to color picker views
+        if (input.canApplyInputStyle()) {
+            view.setPadding(mInputPaddingSize, 0, mInputPaddingSize, 0);
+	}
         return view;
     }
 
     public void show(JSONObject message) {
         processMessage(message);
     }
 
     public void show(String title, String text, PromptListItem[] listItems, boolean multipleSelection) {
@@ -328,34 +332,36 @@ public class Prompt implements OnClickLi
         }
 
         try {
             View root = null;
             boolean scrollable = false; // If any of the innuts are scrollable, we won't wrap this in a ScrollView
 
             if (length == 1) {
                 root = mInputs[0].getView(mContext);
+                applyInputStyle(root, mInputs[0]);
                 scrollable |= mInputs[0].getScrollable();
             } else if (length > 1) {
                 LinearLayout linearLayout = new LinearLayout(mContext);
                 linearLayout.setOrientation(LinearLayout.VERTICAL);
                 for (int i = 0; i < length; i++) {
                     View content = mInputs[i].getView(mContext);
+                    applyInputStyle(content, mInputs[i]);
                     linearLayout.addView(content);
                     scrollable |= mInputs[i].getScrollable();
                 }
                 root = linearLayout;
             }
 
             if (scrollable) {
-                builder.setView(applyInputStyle(root));
+                builder.setView(root);
             } else {
                 ScrollView view = new ScrollView(mContext);
                 view.addView(root);
-                builder.setView(applyInputStyle(view));
+                builder.setView(view);
             }
         } catch(Exception ex) {
             Log.e(LOGTAG, "Error showing prompt inputs", ex);
             // We cannot display these input widgets with this sdk version,
             // do not display any dialog and finish the prompt now.
             cancelDialog();
             return false;
         }
--- a/mobile/android/base/prompts/PromptInput.java
+++ b/mobile/android/base/prompts/PromptInput.java
@@ -345,16 +345,18 @@ public class PromptInput {
         } else if (CheckboxInput.INPUT_TYPE.equals(type)) {
             return new CheckboxInput(obj);
         } else if (MenulistInput.INPUT_TYPE.equals(type)) {
             return new MenulistInput(obj);
         } else if (LabelInput.INPUT_TYPE.equals(type)) {
             return new LabelInput(obj);
         } else if (IconGridInput.INPUT_TYPE.equals(type)) {
             return new IconGridInput(obj);
+        } else if (ColorPickerInput.INPUT_TYPE.equals(type)) {
+            return new ColorPickerInput(obj);
         } else {
             for (String dtType : DateTimeInput.INPUT_TYPES) {
                 if (dtType.equals(type)) {
                     return new DateTimeInput(obj);
                 }
             }
         }
         return null;
@@ -370,9 +372,13 @@ public class PromptInput {
 
     public String getValue() {
         return "";
     }
 
     public boolean getScrollable() {
         return false;
     }
+
+    public boolean canApplyInputStyle() {
+	return true;
+    }
 }
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..13202759e1bf087b719bb00ccb59ae3b0d04964a
GIT binary patch
literal 218
zc%17D@N?(olHy`uVBq!ia0vp^%s|Y~!3HE1UzHvKQfx`y?k)`bLGT}kaTid8v%n*=
zn1O*?7=#%aX3dcR3bL1Y`ns~;<>40AQEJ-q!w4uOn;8;O;+&tGo0?a`00PcMsfi`2
zDGKG8B^e6tp1uJoda3L{aXU{J#}JO|r9B&Y84Lvu&3R*Qs4!8f>aXd-%?1a28O<(i
zW4L>u>+JOfJRP5#mw#i+l)JON)HE|^((z+r#;g{XFKZ<&b(CKXvcl8V&t;ucLK6VQ
C0zmQr
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ca51b54ca9e021640f6ddefd1d3e05f78d7ac727
GIT binary patch
literal 212
zc%17D@N?(olHy`uVBq!ia0vp^%s|Y;!3HFk*Rf{-DYhhUcNd2JAo!2NxC<!4S>O>_
z%)r1c48n{Iv*t(u1=&kHeO=k_@^B05Foa*(c?~Een;8;O;+&tGo0?a`00PcMsfi`2
zDGKG8B^e6tp1uJoda3L{aSKlu#}JO|rDqKJ8XS0-5AOW^zg?Isc+LbRmfMlpvz-%^
vJr$%^@3M6ZKWlI%JXf~(Z*ClWs?ZL`b?J&p=dIO>L56#}`njxgN@xNAPjxxk
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0047cbad40b5faf8f2b12854495d37c869a21387
GIT binary patch
literal 224
zc%17D@N?(olHy`uVBq!ia0vp^%s?!{!3HEfez(s7Qfx`y?k)`bLGT}kaTid8v%n*=
zn1O*?7=#%aX3dcR3bL1Y`ns~;<>40A<u$1Lq6HL^%?ybsan8@pP0cG|00HNs)Wnk1
z6ovB4k_-iRPv3wPy;OFfxQnNYV~E7%-hM~E0}4EcdH(<ZI4PCg;Wb~!)3C@=5eZHQ
z_DL)XvKjl%DtT1#uNDd~dV3=L!Q85UTRdaD|8tgTEj?GqAb-v>Gc`cJ7-W&BtDnm{
Hr-UW|wZuSv
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/color_picker_checkmark.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="ring"
+       android:innerRadius="15dip"
+       android:thickness="4dip"
+       android:useLevel="false">
+    <solid android:color="@android:color/white"/>
+</shape>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/basic_color_picker_dialog.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_height="wrap_content"
+              android:layout_width="fill_parent"
+              android:orientation="vertical">
+
+  <org.mozilla.gecko.widget.BasicColorPicker android:id="@+id/colorpicker"
+                                             android:layout_height="0dip"
+                                             android:layout_weight="1"
+                                             android:drawSelectorOnTop="true"
+                                             android:choiceMode="singleChoice"
+                                             android:divider="@android:color/transparent"
+                                             android:dividerHeight="0dip"
+                                             android:listSelector="#22FFFFFF"
+                                             android:layout_width="fill_parent"/>
+
+</LinearLayout>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/color_picker_row.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
+                 android:id="@+id/text"
+                 android:layout_width="match_parent"
+                 android:layout_height="wrap_content"
+                 android:textAppearance="@style/TextAppearance.Widget.TextView"
+                 style="@style/Widget.ListItem"
+                 android:background="@drawable/color_picker_row_bg"
+                 android:checkMark="@android:color/transparent"/>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/widget/BasicColorPicker.java
@@ -0,0 +1,139 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.widget;
+
+import org.mozilla.gecko.R;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.ArrayAdapter;
+import android.widget.AdapterView;
+import android.widget.CheckedTextView;
+import android.widget.ListView;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+
+public class BasicColorPicker extends ListView {
+    private final static String LOGTAG = "GeckoBasicColorPicker";
+    private final static List<Integer> DEFAULT_COLORS = Arrays.asList(Color.rgb(215,57,32),
+                                                                      Color.rgb(255,134,5),
+                                                                      Color.rgb(255,203,19),
+                                                                      Color.rgb(95,173,71),
+                                                                      Color.rgb(84,201,168),
+                                                                      Color.rgb(33,161,222),
+                                                                      Color.rgb(16,36,87),
+                                                                      Color.rgb(91,32,103),
+                                                                      Color.rgb(212,221,228),
+                                                                      Color.BLACK);
+
+    private static Drawable mCheckDrawable = null;
+    private int mSelected = 0;
+    final private ColorPickerListAdapter mAdapter;
+
+    public BasicColorPicker(Context context) {
+        this(context, null);
+    }
+
+    public BasicColorPicker(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public BasicColorPicker(Context context, AttributeSet attrs, int style) {
+        this(context, attrs, style, DEFAULT_COLORS);
+    }
+
+    public BasicColorPicker(Context context, AttributeSet attrs, int style, List<Integer> colors) {
+        super(context, attrs, style);
+        mAdapter = new ColorPickerListAdapter(context, new ArrayList<Integer>(colors));
+        setAdapter(mAdapter);
+
+        setOnItemClickListener(new AdapterView.OnItemClickListener() {
+            @Override
+            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+                mSelected = position;
+                mAdapter.notifyDataSetChanged();
+            }
+        });
+    }
+
+    public int getColor() {
+        return mAdapter.getItem(mSelected);
+    }
+
+    public void setColor(int color) {
+        if (!DEFAULT_COLORS.contains(color)) {
+            mSelected = mAdapter.getCount();
+            mAdapter.add(color);
+        } else {
+            mSelected = DEFAULT_COLORS.indexOf(color);
+        }
+
+        setSelection(mSelected);
+        mAdapter.notifyDataSetChanged();
+    }
+
+    private Drawable getCheckDrawable() {
+        if (mCheckDrawable == null) {
+            Resources res = getContext().getResources();
+
+            TypedValue typedValue = new TypedValue();
+            getContext().getTheme().resolveAttribute(android.R.attr.listPreferredItemHeight, typedValue, true);
+            DisplayMetrics metrics = new android.util.DisplayMetrics();
+            ((WindowManager)getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getMetrics(metrics);
+            int height = (int) typedValue.getDimension(metrics);
+
+            Drawable background = res.getDrawable(R.drawable.color_picker_row_bg);
+            Rect r = new Rect();
+            background.getPadding(r);
+            height -= r.top + r.bottom;
+
+            mCheckDrawable = res.getDrawable(R.drawable.color_picker_checkmark);
+            mCheckDrawable.setBounds(0, 0, height, height);
+        }
+
+        return mCheckDrawable;
+    }
+
+   private class ColorPickerListAdapter extends ArrayAdapter<Integer> {
+        private final List<Integer> mColors;
+
+        public ColorPickerListAdapter(Context context, List<Integer> colors) {
+            super(context, R.layout.color_picker_row, colors);
+            mColors = colors;
+        }
+
+        public View getView(int position, View convertView, ViewGroup parent) {
+            View v = super.getView(position, convertView, parent);
+
+            Drawable d = v.getBackground();
+            d.setColorFilter(getItem(position), PorterDuff.Mode.MULTIPLY);
+            v.setBackground(d);
+
+            Drawable check = null;
+            CheckedTextView checked = ((CheckedTextView) v);
+            if (mSelected == position) {
+                check = getCheckDrawable();
+            }
+
+            checked.setCompoundDrawables(check, null, null, null);
+            checked.setText("");
+
+            return v;
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/components/ColorPicker.js
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cc = Components.classes;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Prompt.jsm");
+
+function ColorPicker() {
+}
+
+ColorPicker.prototype = {
+  _initial: 0,
+  _domWin: null,
+  _title: "",
+
+  get strings() {
+    delete this.strings;
+    return this.strings = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+  },
+
+  init: function(aParent, aTitle, aInitial) {
+    this._domWin = aParent;
+    this._initial = aInitial;
+    this._title = aTitle;
+  },
+
+  open: function(aCallback) {
+    let p = new Prompt({ title: this._title,
+                         buttons: [
+                            this.strings.GetStringFromName("inputWidgetHelper.set"),
+                            this.strings.GetStringFromName("inputWidgetHelper.cancel")
+                         ] })
+                      .addColorPicker({ value: this._initial })
+                      .show((data) => {
+      if (data.button == 0)
+        aCallback.done(data.color0);
+      else
+        aCallback.done(this._initial);
+    });
+  },
+
+  classID: Components.ID("{430b987f-bb9f-46a3-99a5-241749220b29}"),
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIColorPicker])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ColorPicker]);
--- a/mobile/android/components/MobileComponents.manifest
+++ b/mobile/android/components/MobileComponents.manifest
@@ -100,8 +100,12 @@ contract @mozilla.org/payment/ui-glue;1 
 component {18a4e042-7c7c-424b-a583-354e68553a7f} FilePicker.js
 contract @mozilla.org/filepicker;1 {18a4e042-7c7c-424b-a583-354e68553a7f}
 
 # Snippets.js
 component {a78d7e59-b558-4321-a3d6-dffe2f1e76dd} Snippets.js
 contract @mozilla.org/snippets;1 {a78d7e59-b558-4321-a3d6-dffe2f1e76dd}
 category profile-after-change Snippets @mozilla.org/snippets;1
 category update-timer Snippets @mozilla.org/snippets;1,getService,snippets-update-timer,browser.snippets.updateInterval,86400
+
+# ColorPicker.js
+component {430b987f-bb9f-46a3-99a5-241749220b29} ColorPicker.js
+contract @mozilla.org/colorpicker;1 {430b987f-bb9f-46a3-99a5-241749220b29}
--- a/mobile/android/components/moz.build
+++ b/mobile/android/components/moz.build
@@ -8,16 +8,17 @@ XPIDL_SOURCES += [
     'SessionStore.idl',
 ]
 
 XPIDL_MODULE = 'MobileComponents'
 
 EXTRA_COMPONENTS += [
     'AddonUpdateService.js',
     'BlocklistPrompt.js',
+    'ColorPicker.js',
     'ContentDispatchChooser.js',
     'ContentPermissionPrompt.js',
     'DownloadManagerUI.js',
     'FilePicker.js',
     'LoginManagerPrompter.js',
     'NSSDialogService.js',
     'PaymentsUI.js',
     'PromptService.js',
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -553,16 +553,17 @@ bin/components/@DLL_PREFIX@nkgnomevfs@DL
 [mobile]
 @BINPATH@/chrome/icons/
 @BINPATH@/chrome/chrome@JAREXT@
 @BINPATH@/chrome/chrome.manifest
 @BINPATH@/components/AboutRedirector.js
 @BINPATH@/components/AddonUpdateService.js
 @BINPATH@/components/BlocklistPrompt.js
 @BINPATH@/components/BrowserCLH.js
+@BINPATH@/components/ColorPicker.js
 @BINPATH@/components/ContentDispatchChooser.js
 @BINPATH@/components/ContentPermissionPrompt.js
 @BINPATH@/components/DirectoryProvider.js
 @BINPATH@/components/DownloadManagerUI.js
 @BINPATH@/components/FilePicker.js
 @BINPATH@/components/HelperAppDialog.js
 @BINPATH@/components/LoginManagerPrompter.js
 @BINPATH@/components/MobileComponents.manifest
--- a/mobile/android/modules/Prompt.jsm
+++ b/mobile/android/modules/Prompt.jsm
@@ -112,16 +112,24 @@ Prompt.prototype = {
   addDatePicker: function(aOptions) {
     return this._addInput({
       type: aOptions.type || "date",
       value: aOptions.value,
       id: aOptions.id
     });
   },
 
+  addColorPicker: function(aOptions) {
+    return this._addInput({
+      type: "color",
+      value: aOptions.value,
+      id: aOptions.id
+    });
+  },
+
   addLabel: function(aOptions) {
     return this._addInput({
       type: "label",
       label: aOptions.label,
       id: aOptions.id
     });
   },
 
--- a/widget/android/nsLookAndFeel.cpp
+++ b/widget/android/nsLookAndFeel.cpp
@@ -392,16 +392,20 @@ nsLookAndFeel::GetIntImpl(IntID aID, int
         case eIntID_ScrollSliderStyle:
             aResult = eScrollThumbStyle_Proportional;
             break;
 
         case eIntID_TouchEnabled:
             aResult = 1;
             break;
 
+        case eIntID_ColorPickerAvailable:
+            aResult = 1;
+            break;
+
         case eIntID_WindowsDefaultTheme:
         case eIntID_WindowsThemeIdentifier:
         case eIntID_OperatingSystemVersionIdentifier:
             aResult = 0;
             rv = NS_ERROR_NOT_IMPLEMENTED;
             break;
 
         case eIntID_SpellCheckerUnderlineStyle: