Merge fx-team to central, a=merge
authorWes Kocher <wkocher@mozilla.com>
Wed, 25 May 2016 15:36:21 -0700
changeset 338009 8d0aadfe7da782d415363880008b4ca027686137
parent 337996 1012461fa7bb33adf728ec631020cb18153da6f0 (current diff)
parent 338008 df627da479f868c9704d7ac0b1c6aa76fb298150 (diff)
child 338048 da921523812f62a85d6d7963703af963f99ad9ed
child 338077 9ef45b3ae61d40b772319a314205ddacfe00cff9
push id6249
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 13:59:36 +0000
treeherdermozilla-beta@bad9d4f5bf7e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone49.0a1
first release with
nightly linux32
8d0aadfe7da7 / 49.0a1 / 20160526030223 / files
nightly linux64
8d0aadfe7da7 / 49.0a1 / 20160526030223 / files
nightly mac
8d0aadfe7da7 / 49.0a1 / 20160526030223 / files
nightly win32
8d0aadfe7da7 / 49.0a1 / 20160526030223 / files
nightly win64
8d0aadfe7da7 / 49.0a1 / 20160526030223 / 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 central, a=merge
--- a/build/autoconf/android.m4
+++ b/build/autoconf/android.m4
@@ -202,50 +202,56 @@ AC_DEFUN([MOZ_ANDROID_AAR],[
 
   define([root], $MOZ_BUILD_ROOT/dist/exploded-aar/$1-$2/)
   MOZ_ANDROID_AAR_COMPONENT(concat(local_aar_var, _LIB), concat(root, $1-$2-classes.jar), REQUIRED)
   MOZ_ANDROID_AAR_COMPONENT(concat(local_aar_var, _RES), concat(root, res), REQUIRED)
   MOZ_ANDROID_AAR_COMPONENT(concat(local_aar_var, _INTERNAL_LIB), concat(root, libs/$1-$2-internal_impl-$2.jar), $5)
   MOZ_ANDROID_AAR_COMPONENT(concat(local_aar_var, _ASSETS), concat(root, assets), $6)
 ])
 
+ANDROID_SUPPORT_LIBRARY_VERSION="23.0.1"
+AC_SUBST(ANDROID_SUPPORT_LIBRARY_VERSION)
+
+ANDROID_GOOGLE_PLAY_SERVICES_VERSION="8.1.0"
+AC_SUBST(ANDROID_GOOGLE_PLAY_SERVICES_VERSION)
+
 AC_DEFUN([MOZ_ANDROID_GOOGLE_PLAY_SERVICES],
 [
 
 if test -n "$MOZ_NATIVE_DEVICES" ; then
     AC_SUBST(MOZ_NATIVE_DEVICES)
 
-    MOZ_ANDROID_AAR(play-services-base, 8.1.0, google, com/google/android/gms)
-    MOZ_ANDROID_AAR(play-services-basement, 8.1.0, google, com/google/android/gms)
-    MOZ_ANDROID_AAR(play-services-cast, 8.1.0, google, com/google/android/gms)
-    MOZ_ANDROID_AAR(mediarouter-v7, 23.0.1, android, com/android/support, REQUIRED_INTERNAL_IMPL)
+    MOZ_ANDROID_AAR(play-services-base, $ANDROID_GOOGLE_PLAY_SERVICES_VERSION, google, com/google/android/gms)
+    MOZ_ANDROID_AAR(play-services-basement, $ANDROID_GOOGLE_PLAY_SERVICES_VERSION, google, com/google/android/gms)
+    MOZ_ANDROID_AAR(play-services-cast, $ANDROID_GOOGLE_PLAY_SERVICES_VERSION, google, com/google/android/gms)
+    MOZ_ANDROID_AAR(mediarouter-v7, $ANDROID_SUPPORT_LIBRARY_VERSION, android, com/android/support, REQUIRED_INTERNAL_IMPL)
 fi
 
 ])
 
 AC_DEFUN([MOZ_ANDROID_GOOGLE_CLOUD_MESSAGING],
 [
 
 if test -n "$MOZ_ANDROID_GCM" ; then
-    MOZ_ANDROID_AAR(play-services-base, 8.1.0, google, com/google/android/gms)
-    MOZ_ANDROID_AAR(play-services-basement, 8.1.0, google, com/google/android/gms)
-    MOZ_ANDROID_AAR(play-services-gcm, 8.1.0, google, com/google/android/gms)
+    MOZ_ANDROID_AAR(play-services-base, $ANDROID_GOOGLE_PLAY_SERVICES_VERSION, google, com/google/android/gms)
+    MOZ_ANDROID_AAR(play-services-basement, $ANDROID_GOOGLE_PLAY_SERVICES_VERSION, google, com/google/android/gms)
+    MOZ_ANDROID_AAR(play-services-gcm, $ANDROID_GOOGLE_PLAY_SERVICES_VERSION, google, com/google/android/gms)
 fi
 
 ])
 
 AC_DEFUN([MOZ_ANDROID_INSTALL_TRACKING],
 [
 
 if test -n "$MOZ_INSTALL_TRACKING"; then
     AC_SUBST(MOZ_INSTALL_TRACKING)
-    MOZ_ANDROID_AAR(play-services-ads, 8.1.0, google, com/google/android/gms)
-    MOZ_ANDROID_AAR(play-services-analytics, 8.1.0, google, com/google/android/gms)
-    MOZ_ANDROID_AAR(play-services-appindexing, 8.1.0, google, com/google/android/gms)
-    MOZ_ANDROID_AAR(play-services-basement, 8.1.0, google, com/google/android/gms)
+    MOZ_ANDROID_AAR(play-services-ads, $ANDROID_GOOGLE_PLAY_SERVICES_VERSION, google, com/google/android/gms)
+    MOZ_ANDROID_AAR(play-services-analytics, $ANDROID_GOOGLE_PLAY_SERVICES_VERSION, google, com/google/android/gms)
+    MOZ_ANDROID_AAR(play-services-appindexing, $ANDROID_GOOGLE_PLAY_SERVICES_VERSION, google, com/google/android/gms)
+    MOZ_ANDROID_AAR(play-services-basement, $ANDROID_GOOGLE_PLAY_SERVICES_VERSION, google, com/google/android/gms)
 fi
 
 ])
 
 dnl Configure an Android SDK.
 dnl Arg 1: target SDK version, like 22.
 dnl Arg 2: build tools version, like 22.0.1.
 AC_DEFUN([MOZ_ANDROID_SDK],
@@ -329,29 +335,31 @@ case "$target" in
     if test -z "$EMULATOR" -o "$EMULATOR" = ":"; then
       AC_MSG_ERROR([The program emulator was not found.  Try |mach bootstrap|.])
     fi
 
     ANDROID_TARGET_SDK="${android_target_sdk}"
     ANDROID_SDK="${android_sdk}"
     ANDROID_SDK_ROOT="${android_sdk_root}"
     ANDROID_TOOLS="${android_tools}"
+    ANDROID_BUILD_TOOLS_VERSION="$2"
     AC_DEFINE_UNQUOTED(ANDROID_TARGET_SDK,$ANDROID_TARGET_SDK)
     AC_SUBST(ANDROID_TARGET_SDK)
     AC_SUBST(ANDROID_SDK_ROOT)
     AC_SUBST(ANDROID_SDK)
     AC_SUBST(ANDROID_TOOLS)
+    AC_SUBST(ANDROID_BUILD_TOOLS_VERSION)
 
-    MOZ_ANDROID_AAR(appcompat-v7, 23.0.1, android, com/android/support)
-    MOZ_ANDROID_AAR(cardview-v7, 23.0.1, android, com/android/support)
-    MOZ_ANDROID_AAR(design, 23.0.1, android, com/android/support)
-    MOZ_ANDROID_AAR(recyclerview-v7, 23.0.1, android, com/android/support)
-    MOZ_ANDROID_AAR(support-v4, 23.0.1, android, com/android/support, REQUIRED_INTERNAL_IMPL)
+    MOZ_ANDROID_AAR(appcompat-v7, $ANDROID_SUPPORT_LIBRARY_VERSION, android, com/android/support)
+    MOZ_ANDROID_AAR(cardview-v7, $ANDROID_SUPPORT_LIBRARY_VERSION, android, com/android/support)
+    MOZ_ANDROID_AAR(design, $ANDROID_SUPPORT_LIBRARY_VERSION, android, com/android/support)
+    MOZ_ANDROID_AAR(recyclerview-v7, $ANDROID_SUPPORT_LIBRARY_VERSION, android, com/android/support)
+    MOZ_ANDROID_AAR(support-v4, $ANDROID_SUPPORT_LIBRARY_VERSION, android, com/android/support, REQUIRED_INTERNAL_IMPL)
 
-    ANDROID_SUPPORT_ANNOTATIONS_JAR="$ANDROID_SDK_ROOT/extras/android/m2repository/com/android/support/support-annotations/23.0.1/support-annotations-23.0.1.jar"
+    ANDROID_SUPPORT_ANNOTATIONS_JAR="$ANDROID_SDK_ROOT/extras/android/m2repository/com/android/support/support-annotations/$ANDROID_SUPPORT_LIBRARY_VERSION/support-annotations-$ANDROID_SUPPORT_LIBRARY_VERSION.jar"
     AC_MSG_CHECKING([for support-annotations JAR])
     if ! test -e $ANDROID_SUPPORT_ANNOTATIONS_JAR ; then
         AC_MSG_ERROR([You must download the support-annotations lib.  Run the Android SDK tool and install the Android Support Repository under Extras.  See https://developer.android.com/tools/extras/support-library.html for more info. (looked for $ANDROID_SUPPORT_ANNOTATIONS_JAR)])
     fi
     AC_MSG_RESULT([$ANDROID_SUPPORT_ANNOTATIONS_JAR])
     AC_SUBST(ANDROID_SUPPORT_ANNOTATIONS_JAR)
     ANDROID_SUPPORT_ANNOTATIONS_JAR_LIB=$ANDROID_SUPPORT_ANNOTATIONS_JAR
     AC_SUBST(ANDROID_SUPPORT_ANNOTATIONS_JAR_LIB)
--- a/devtools/client/devtools-startup.manifest
+++ b/devtools/client/devtools-startup.manifest
@@ -1,2 +1,8 @@
 component {9e9a9283-0ce9-4e4a-8f1c-ba129a032c32} devtools-startup.js
-contract @mozilla.org/toolkit/console-clh;1 {9e9a9283-0ce9-4e4a-8f1c-ba129a032c32}
+contract @mozilla.org/devtools/startup-clh;1 {9e9a9283-0ce9-4e4a-8f1c-ba129a032c32}
+# We want this to override toolkit's --jsconsole handling, so it must have a
+# a higher priority than the entry in jsconsole-clhandler.manifest.  Higher
+# priority means the "m-devtools" value below needs to be something that sorts
+# before the one in jsconsole-clhandler.manifest.  See details in
+# nsICommandLineHandler.idl.
+category command-line-handler m-devtools @mozilla.org/devtools/startup-clh;1
--- a/devtools/client/framework/test/browser_keybindings_02.js
+++ b/devtools/client/framework/test/browser_keybindings_02.js
@@ -5,16 +5,18 @@
 
 "use strict";
 
 // Test that the toolbox keybindings still work after the host is changed.
 
 const URL = "data:text/html;charset=utf8,test page";
 
 var {Toolbox} = require("devtools/client/framework/toolbox");
+var strings = Services.strings.createBundle(
+  "chrome://devtools/locale/toolbox.properties");
 
 add_task(function* () {
   info("Create a test tab and open the toolbox");
   let tab = yield addTab(URL);
   let target = TargetFactory.forTab(tab);
   let toolbox = yield gDevTools.showToolbox(target, "webconsole");
 
   let {SIDE, BOTTOM} = Toolbox.HostType;
@@ -28,31 +30,31 @@ add_task(function* () {
 
   Services.prefs.clearUserPref("devtools.toolbox.zoomValue");
   Services.prefs.setCharPref("devtools.toolbox.host", BOTTOM);
   yield toolbox.destroy();
   gBrowser.removeCurrentTab();
 });
 
 function zoomWithKey(toolbox, key) {
-  if (!key) {
+  let shortcut = strings.GetStringFromName(key);
+  if (!shortcut) {
     info("Key was empty, skipping zoomWithKey");
     return;
   }
-
   info("Zooming with key: " + key);
   let currentZoom = toolbox.zoomValue;
-  EventUtils.synthesizeKey(key, {accelKey: true}, toolbox.win);
+  synthesizeKeyShortcut(shortcut);
   isnot(toolbox.zoomValue, currentZoom, "The zoom level was changed in the toolbox");
 }
 
 function* checkKeyBindings(toolbox) {
-  zoomWithKey(toolbox, toolbox.doc.getElementById("toolbox-zoom-in-key").getAttribute("key"));
-  zoomWithKey(toolbox, toolbox.doc.getElementById("toolbox-zoom-in-key2").getAttribute("key"));
-  zoomWithKey(toolbox, toolbox.doc.getElementById("toolbox-zoom-in-key3").getAttribute("key"));
+  zoomWithKey(toolbox, "toolbox.zoomIn.key");
+  zoomWithKey(toolbox, "toolbox.zoomIn2.key");
+  zoomWithKey(toolbox, "toolbox.zoomIn3.key");
 
-  zoomWithKey(toolbox, toolbox.doc.getElementById("toolbox-zoom-reset-key").getAttribute("key"));
+  zoomWithKey(toolbox, "toolbox.zoomReset.key");
 
-  zoomWithKey(toolbox, toolbox.doc.getElementById("toolbox-zoom-out-key").getAttribute("key"));
-  zoomWithKey(toolbox, toolbox.doc.getElementById("toolbox-zoom-out-key2").getAttribute("key"));
+  zoomWithKey(toolbox, "toolbox.zoomOut.key");
+  zoomWithKey(toolbox, "toolbox.zoomOut2.key");
 
-  zoomWithKey(toolbox, toolbox.doc.getElementById("toolbox-zoom-reset-key2").getAttribute("key"));
+  zoomWithKey(toolbox, "toolbox.zoomReset2.key");
 }
--- a/devtools/client/framework/test/browser_keybindings_03.js
+++ b/devtools/client/framework/test/browser_keybindings_03.js
@@ -6,42 +6,47 @@
 "use strict";
 
 // Test that the toolbox 'switch to previous host' feature works.
 // Pressing ctrl/cmd+shift+d should switch to the last used host.
 
 const URL = "data:text/html;charset=utf8,test page for toolbox switching";
 
 var {Toolbox} = require("devtools/client/framework/toolbox");
+var strings = Services.strings.createBundle(
+  "chrome://devtools/locale/toolbox.properties");
 
 add_task(function* () {
   info("Create a test tab and open the toolbox");
   let tab = yield addTab(URL);
   let target = TargetFactory.forTab(tab);
   let toolbox = yield gDevTools.showToolbox(target, "webconsole");
 
-  let keyElement = toolbox.doc.getElementById("toolbox-toggle-host-key");
+  let shortcut = strings.GetStringFromName("toolbox.toggleHost.key");
 
   let {SIDE, BOTTOM, WINDOW} = Toolbox.HostType;
   checkHostType(toolbox, BOTTOM, SIDE);
 
   info("Switching from bottom to side");
-  synthesizeKeyElement(keyElement);
-  yield toolbox.once("host-changed");
+  let onHostChanged = toolbox.once("host-changed");
+  synthesizeKeyShortcut(shortcut, toolbox.win);
+  yield onHostChanged;
   checkHostType(toolbox, SIDE, BOTTOM);
 
   info("Switching from side to bottom");
-  synthesizeKeyElement(keyElement);
-  yield toolbox.once("host-changed");
+  onHostChanged = toolbox.once("host-changed");
+  synthesizeKeyShortcut(shortcut, toolbox.win);
+  yield onHostChanged;
   checkHostType(toolbox, BOTTOM, SIDE);
 
   info("Switching to window");
   yield toolbox.switchHost(WINDOW);
   checkHostType(toolbox, WINDOW, BOTTOM);
 
   info("Switching from window to bottom");
-  synthesizeKeyElement(keyElement);
-  yield toolbox.once("host-changed");
+  onHostChanged = toolbox.once("host-changed");
+  synthesizeKeyShortcut(shortcut, toolbox.win);
+  yield onHostChanged;
   checkHostType(toolbox, BOTTOM, WINDOW);
 
   yield toolbox.destroy();
   gBrowser.removeCurrentTab();
 });
--- a/devtools/client/framework/test/browser_toolbox_options.js
+++ b/devtools/client/framework/test/browser_toolbox_options.js
@@ -5,16 +5,18 @@
 
 /* import-globals-from shared-head.js */
 "use strict";
 
 // Tests that changing preferences in the options panel updates the prefs
 // and toggles appropriate things in the toolbox.
 
 var doc = null, toolbox = null, panelWin = null, modifiedPrefs = [];
+var strings = Services.strings.createBundle(
+  "chrome://devtools/locale/toolbox.properties");
 
 add_task(function* () {
   const URL = "data:text/html;charset=utf8,test for dynamically registering " +
               "and unregistering tools";
   registerNewTool();
   let tab = yield addTab(URL);
   let target = TargetFactory.forTab(tab);
   toolbox = yield gDevTools.showToolbox(target);
@@ -53,28 +55,28 @@ function* testSelectTool() {
   ok(true, "Toolbox selected via selectTool method");
 }
 
 function* testOptionsShortcut() {
   info("Selecting another tool, then reselecting options panel with keyboard.");
 
   yield toolbox.selectTool("webconsole");
   is(toolbox.currentToolId, "webconsole", "webconsole is selected");
-  synthesizeKeyFromKeyTag(doc.getElementById("toolbox-options-key"));
+  synthesizeKeyShortcut(strings.GetStringFromName("toolbox.options.key"));
   is(toolbox.currentToolId, "options", "Toolbox selected via shortcut key (1)");
-  synthesizeKeyFromKeyTag(doc.getElementById("toolbox-options-key"));
+  synthesizeKeyShortcut(strings.GetStringFromName("toolbox.options.key"));
   is(toolbox.currentToolId, "webconsole", "webconsole is selected (1)");
 
   yield toolbox.selectTool("webconsole");
   is(toolbox.currentToolId, "webconsole", "webconsole is selected");
-  synthesizeKeyFromKeyTag(doc.getElementById("toolbox-options-key2"));
+  synthesizeKeyShortcut(strings.GetStringFromName("toolbox.help.key"));
   is(toolbox.currentToolId, "options", "Toolbox selected via shortcut key (2)");
-  synthesizeKeyFromKeyTag(doc.getElementById("toolbox-options-key"));
+  synthesizeKeyShortcut(strings.GetStringFromName("toolbox.options.key"));
   is(toolbox.currentToolId, "webconsole", "webconsole is reselected (2)");
-  synthesizeKeyFromKeyTag(doc.getElementById("toolbox-options-key2"));
+  synthesizeKeyShortcut(strings.GetStringFromName("toolbox.help.key"));
   is(toolbox.currentToolId, "options", "Toolbox selected via shortcut key (2)");
 }
 
 function* testOptions() {
   let tool = toolbox.getPanel("options");
   panelWin = tool.panelWin;
   let prefNodes = tool.panelDoc.querySelectorAll(
     "input[type=checkbox][data-pref]");
--- a/devtools/client/framework/test/browser_toolbox_tabsswitch_shortcuts.js
+++ b/devtools/client/framework/test/browser_toolbox_tabsswitch_shortcuts.js
@@ -1,66 +1,66 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 requestLongerTimeout(2);
 
 var {Toolbox} = require("devtools/client/framework/toolbox");
+var strings = Services.strings.createBundle(
+  "chrome://devtools/locale/toolbox.properties");
 
 add_task(function* () {
   let tab = yield addTab("about:blank");
   let target = TargetFactory.forTab(tab);
   yield target.makeRemote();
 
   let toolIDs = gDevTools.getToolDefinitionArray()
                          .filter(def => def.isTargetSupported(target))
                          .map(def => def.id);
 
   let toolbox = yield gDevTools.showToolbox(target, toolIDs[0],
                                             Toolbox.HostType.BOTTOM);
-  let nextKey = toolbox.doc.getElementById("toolbox-next-tool-key")
-                           .getAttribute("key");
-  let prevKey = toolbox.doc.getElementById("toolbox-previous-tool-key")
-                           .getAttribute("key");
+  let nextShortcut = strings.GetStringFromName("toolbox.nextTool.key")
+  let prevShortcut = strings.GetStringFromName("toolbox.previousTool.key")
 
   // Iterate over all tools, starting from options to netmonitor, in normal
   // order.
   for (let i = 1; i < toolIDs.length; i++) {
-    yield testShortcuts(toolbox, i, nextKey, toolIDs);
+    yield testShortcuts(toolbox, i, nextShortcut, toolIDs);
   }
 
   // Iterate again, in the same order, starting from netmonitor (so next one is
   // 0: options).
   for (let i = 0; i < toolIDs.length; i++) {
-    yield testShortcuts(toolbox, i, nextKey, toolIDs);
+    yield testShortcuts(toolbox, i, nextShortcut, toolIDs);
   }
 
   // Iterate over all tools in reverse order, starting from netmonitor to
   // options.
   for (let i = toolIDs.length - 2; i >= 0; i--) {
-    yield testShortcuts(toolbox, i, prevKey, toolIDs);
+    yield testShortcuts(toolbox, i, prevShortcut, toolIDs);
   }
 
   // Iterate again, in reverse order again, starting from options (so next one
   // is length-1: netmonitor).
   for (let i = toolIDs.length - 1; i >= 0; i--) {
-    yield testShortcuts(toolbox, i, prevKey, toolIDs);
+    yield testShortcuts(toolbox, i, prevShortcut, toolIDs);
   }
 
   yield toolbox.destroy();
   gBrowser.removeCurrentTab();
 });
 
-function* testShortcuts(toolbox, index, key, toolIDs) {
+function* testShortcuts(toolbox, index, shortcut, toolIDs) {
   info("Testing shortcut to switch to tool " + index + ":" + toolIDs[index] +
-       " using key " + key);
+       " using shortcut " + shortcut);
 
   let onToolSelected = toolbox.once("select");
-  EventUtils.synthesizeKey(key, {accelKey: true}, toolbox.win);
+  synthesizeKeyShortcut(shortcut);
   let id = yield onToolSelected;
 
   info("toolbox-select event from " + id);
 
   is(toolIDs.indexOf(id), index,
      "Correct tool is selected on pressing the shortcut for " + id);
 }
--- a/devtools/client/framework/test/browser_toolbox_window_reload_target.js
+++ b/devtools/client/framework/test/browser_toolbox_window_reload_target.js
@@ -5,16 +5,18 @@
 
 requestLongerTimeout(10);
 
 const TEST_URL = "data:text/html;charset=utf-8," +
                  "<html><head><title>Test reload</title></head>" +
                  "<body><h1>Testing reload from devtools</h1></body></html>";
 
 var {Toolbox} = require("devtools/client/framework/toolbox");
+var strings = Services.strings.createBundle(
+  "chrome://devtools/locale/toolbox.properties");
 
 var target, toolbox, description, reloadsSent, toolIDs;
 
 function test() {
   addTab(TEST_URL).then(() => {
     target = TargetFactory.forTab(gBrowser.selectedTab);
 
     target.makeRemote().then(() => {
@@ -55,39 +57,38 @@ function startReloadTest(aToolbox) {
   }, toolIDs.length - 1 /* only test 1 tool in docked mode, to cut down test time */);
 }
 
 function testAllTheTools(docked, callback, toolNum = 0) {
   if (toolNum >= toolIDs.length) {
     return callback();
   }
   toolbox.selectTool(toolIDs[toolNum]).then(() => {
-    testReload("toolbox-reload-key", docked, toolIDs[toolNum], () => {
-      testReload("toolbox-reload-key2", docked, toolIDs[toolNum], () => {
-        testReload("toolbox-force-reload-key", docked, toolIDs[toolNum], () => {
-          testReload("toolbox-force-reload-key2", docked, toolIDs[toolNum], () => {
+    testReload("toolbox.reload.key", docked, toolIDs[toolNum], () => {
+      testReload("toolbox.reload2.key", docked, toolIDs[toolNum], () => {
+        testReload("toolbox.forceReload.key", docked, toolIDs[toolNum], () => {
+          testReload("toolbox.forceReload2.key", docked, toolIDs[toolNum], () => {
             testAllTheTools(docked, callback, toolNum + 1);
           });
         });
       });
     });
   });
 }
 
-function testReload(key, docked, toolID, callback) {
+function testReload(shortcut, docked, toolID, callback) {
   let complete = () => {
     gBrowser.selectedBrowser.messageManager.removeMessageListener("devtools:test:load", complete);
     return callback();
   };
   gBrowser.selectedBrowser.messageManager.addMessageListener("devtools:test:load", complete);
 
-  description = docked + " devtools with tool " + toolID + ", key #" + key;
+  description = docked + " devtools with tool " + toolID + ", shortcut #" + shortcut;
   info("Testing reload in " + description);
-  let el = toolbox.doc.getElementById(key);
-  synthesizeKeyElement(el);
+  synthesizeKeyShortcut(strings.GetStringFromName(shortcut), toolbox.win);
   reloadsSent++;
 }
 
 function finishUp() {
   toolbox.destroy().then(() => {
     gBrowser.removeCurrentTab();
 
     target = toolbox = description = reloadsSent = toolIDs = null;
--- a/devtools/client/framework/test/browser_toolbox_zoom.js
+++ b/devtools/client/framework/test/browser_toolbox_zoom.js
@@ -3,16 +3,18 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var modifiers = {
   accelKey: true
 };
 
 var toolbox;
+var strings = Services.strings.createBundle(
+  "chrome://devtools/locale/toolbox.properties");
 
 function test() {
   addTab("about:blank").then(openToolbox);
 }
 
 function openToolbox() {
   let target = TargetFactory.forTab(gBrowser.selectedTab);
 
@@ -20,37 +22,36 @@ function openToolbox() {
     toolbox = aToolbox;
     toolbox.selectTool("styleeditor").then(testZoom);
   });
 }
 
 function testZoom() {
   info("testing zoom keys");
 
-  testZoomLevel("in", 2, 1.2);
-  testZoomLevel("out", 3, 0.9);
-  testZoomLevel("reset", 1, 1);
+  testZoomLevel("In", 2, 1.2);
+  testZoomLevel("Out", 3, 0.9);
+  testZoomLevel("Reset", 1, 1);
 
   tidyUp();
 }
 
 function testZoomLevel(type, times, expected) {
-  sendZoomKey("toolbox-zoom-" + type + "-key", times);
+  sendZoomKey("toolbox.zoom" + type + ".key", times);
 
   let zoom = getCurrentZoom(toolbox);
   is(zoom.toFixed(2), expected, "zoom level correct after zoom " + type);
 
   is(toolbox.zoomValue.toFixed(2), expected,
      "saved zoom level is correct after zoom " + type);
 }
 
-function sendZoomKey(id, times) {
-  let key = toolbox.doc.getElementById(id).getAttribute("key");
+function sendZoomKey(shortcut, times) {
   for (let i = 0; i < times; i++) {
-    EventUtils.synthesizeKey(key, modifiers, toolbox.win);
+    synthesizeKeyShortcut(strings.GetStringFromName(shortcut));
   }
 }
 
 function getCurrentZoom() {
   var contViewer = toolbox.frame.docShell.contentViewer;
   return contViewer.fullZoom;
 }
 
--- a/devtools/client/framework/test/shared-head.js
+++ b/devtools/client/framework/test/shared-head.js
@@ -163,30 +163,32 @@ function synthesizeKeyFromKeyTag(key) {
   EventUtils.synthesizeKey(name, modifiers);
 }
 
 /**
  * Simulate a key event from an electron key shortcut string:
  * https://github.com/electron/electron/blob/master/docs/api/accelerator.md
  *
  * @param {String} key
+ * @param {DOMWindow} target
+ *        Optional window where to fire the key event
  */
-function synthesizeKeyShortcut(key) {
+function synthesizeKeyShortcut(key, target) {
   // parseElectronKey requires any window, just to access `KeyboardEvent`
   let window = Services.appShell.hiddenDOMWindow;
   let shortcut = KeyShortcuts.parseElectronKey(window, key);
 
   info("Synthesizing key shortcut: " + key);
   EventUtils.synthesizeKey(shortcut.key || "", {
     keyCode: shortcut.keyCode,
     altKey: shortcut.alt,
     ctrlKey: shortcut.ctrl,
     metaKey: shortcut.meta,
     shiftKey: shortcut.shift
-  });
+  }, target);
 }
 
 /**
  * Wait for eventName on target to be delivered a number of times.
  *
  * @param {Object} target
  *        An observable object that either supports on/off or
  *        addEventListener/removeEventListener
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -63,16 +63,19 @@ loader.lazyRequireGetter(this, "DevTools
 loader.lazyRequireGetter(this, "showDoorhanger",
   "devtools/client/shared/doorhanger", true);
 loader.lazyRequireGetter(this, "createPerformanceFront",
   "devtools/server/actors/performance", true);
 loader.lazyRequireGetter(this, "system",
   "devtools/shared/system");
 loader.lazyRequireGetter(this, "getPreferenceFront",
   "devtools/server/actors/preference", true);
+loader.lazyRequireGetter(this, "KeyShortcuts",
+  "devtools/client/shared/key-shortcuts", true);
+
 loader.lazyGetter(this, "osString", () => {
   return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
 });
 loader.lazyGetter(this, "registerHarOverlay", () => {
   return require("devtools/client/netmonitor/har/toolbox-overlay").register;
 });
 
 // White-list buttons that can be toggled to prevent adding prefs for
@@ -412,29 +415,30 @@ Toolbox.prototype = {
       let noautohideMenu = this.doc.getElementById("command-button-noautohide");
       noautohideMenu.addEventListener("command", this._toggleAutohide, true);
 
       this.textboxContextMenuPopup =
         this.doc.getElementById("toolbox-textbox-context-popup");
       this.textboxContextMenuPopup.addEventListener("popupshowing",
         this._updateTextboxMenuItems, true);
 
+      var shortcuts = new KeyShortcuts({
+        window: this.doc.defaultView
+      });
       this._buildDockButtons();
-      this._buildOptions();
+      this._buildOptions(shortcuts);
       this._buildTabs();
       this._applyCacheSettings();
       this._applyServiceWorkersTestingSettings();
       this._addKeysToWindow();
-      this._addReloadKeys();
-      this._addHostListeners();
+      this._addReloadKeys(shortcuts);
+      this._addHostListeners(shortcuts);
       this._registerOverlays();
-      if (this._hostOptions && this._hostOptions.zoom === false) {
-        this._disableZoomKeys();
-      } else {
-        this._addZoomKeys();
+      if (!this._hostOptions || this._hostOptions.zoom === true) {
+        this._addZoomKeys(shortcuts);
         this._loadInitialZoom();
       }
       this._setToolbarKeyboardNavigation();
 
       this.webconsolePanel = this.doc.querySelector("#toolbox-panel-webconsole");
       this.webconsolePanel.height = Services.prefs.getIntPref(SPLITCONSOLE_HEIGHT_PREF);
       this.webconsolePanel.addEventListener("resize", this._saveSplitConsoleHeight);
 
@@ -518,44 +522,45 @@ Toolbox.prototype = {
         this._applyCacheSettings();
         break;
       case "devtools.serviceWorkers.testing.enabled":
         this._applyServiceWorkersTestingSettings();
         break;
     }
   },
 
-  _buildOptions: function () {
-    let selectOptions = () => {
+  _buildOptions: function (shortcuts) {
+    let selectOptions = (name, event) => {
       // Flip back to the last used panel if we are already
       // on the options panel.
       if (this.currentToolId === "options" &&
           gDevTools.getToolDefinition(this.lastUsedToolId)) {
         this.selectTool(this.lastUsedToolId);
       } else {
         this.selectTool("options");
       }
+      // Prevent the opening of bookmarks window on toolbox.options.key
+      event.preventDefault();
     };
-    let key = this.doc.getElementById("toolbox-options-key");
-    key.addEventListener("command", selectOptions, true);
-    let key2 = this.doc.getElementById("toolbox-options-key2");
-    key2.addEventListener("command", selectOptions, true);
+    shortcuts.on(toolboxStrings("toolbox.options.key"), selectOptions);
+    shortcuts.on(toolboxStrings("toolbox.help.key"), selectOptions);
   },
 
   _splitConsoleOnKeypress: function (e) {
     if (e.keyCode === e.DOM_VK_ESCAPE) {
       this.toggleSplitConsole();
       // If the debugger is paused, don't let the ESC key stop any pending
       // navigation.
       let jsdebugger = this.getPanel("jsdebugger");
       if (jsdebugger && jsdebugger.panelWin.gThreadClient.state == "paused") {
         e.preventDefault();
       }
     }
   },
+
   /**
    * Add a shortcut key that should work when a split console
    * has focus to the toolbox.
    *
    * @param {element} keyElement
    *        They <key> XUL element describing the shortcut key
    * @param {string} whichTool
    *        The tool the key belongs to. The corresponding command
@@ -569,44 +574,41 @@ Toolbox.prototype = {
       // Only forward the command if the tool is active
       if (this.currentToolId === whichTool && this.isSplitConsoleFocused()) {
         keyElement.doCommand();
       }
     }, true);
     this.doc.getElementById("toolbox-keyset").appendChild(cloned);
   },
 
-  _addReloadKeys: function () {
+  _addReloadKeys: function (shortcuts) {
     [
-      ["toolbox-reload-key", false],
-      ["toolbox-reload-key2", false],
-      ["toolbox-force-reload-key", true],
-      ["toolbox-force-reload-key2", true]
+      ["reload", false],
+      ["reload2", false],
+      ["forceReload", true],
+      ["forceReload2", true]
     ].forEach(([id, force]) => {
-      this.doc.getElementById(id).addEventListener("command", () => {
-        this.reloadTarget(force);
-      }, true);
+      let key = toolboxStrings("toolbox." + id + ".key");
+      shortcuts.on(key, this.reloadTarget.bind(this, force));
     });
   },
 
-  _addHostListeners: function () {
-    let nextKey = this.doc.getElementById("toolbox-next-tool-key");
-    nextKey.addEventListener("command", this.selectNextTool.bind(this), true);
-
-    let prevKey = this.doc.getElementById("toolbox-previous-tool-key");
-    prevKey.addEventListener("command", this.selectPreviousTool.bind(this), true);
+  _addHostListeners: function (shortcuts) {
+    shortcuts.on(toolboxStrings("toolbox.nextTool.key"),
+                 this.selectNextTool.bind(this));
+    shortcuts.on(toolboxStrings("toolbox.previousTool.key"),
+                 this.selectPreviousTool.bind(this));
+    shortcuts.on(toolboxStrings("toolbox.minimize.key"),
+                 this._toggleMinimizeMode.bind(this));
+    shortcuts.on(toolboxStrings("toolbox.toggleHost.key"),
+                 (name, event) => {
+                   this.switchToPreviousHost();
+                   event.preventDefault();
+                 });
 
-    let minimizeKey = this.doc.getElementById("toolbox-minimize-key");
-    minimizeKey.addEventListener("command", this._toggleMinimizeMode, true);
-
-    let toggleKey = this.doc.getElementById("toolbox-toggle-host-key");
-    toggleKey.addEventListener("command", this.switchToPreviousHost.bind(this), true);
-
-    // Split console uses keypress instead of command so the event can be
-    // cancelled with stopPropagation on the keypress, and not preventDefault.
     this.doc.addEventListener("keypress", this._splitConsoleOnKeypress, false);
 
     this.doc.addEventListener("focus", this._onFocus, true);
   },
 
   _registerOverlays: function () {
     registerHarOverlay(this);
   },
@@ -648,88 +650,72 @@ Toolbox.prototype = {
         splitter.setAttribute("hidden", "true");
       }
     }
   },
 
   /**
    * Wire up the listeners for the zoom keys.
    */
-  _addZoomKeys: function () {
-    let inKey = this.doc.getElementById("toolbox-zoom-in-key");
-    inKey.addEventListener("command", this.zoomIn.bind(this), true);
-
-    let inKey2 = this.doc.getElementById("toolbox-zoom-in-key2");
-    inKey2.addEventListener("command", this.zoomIn.bind(this), true);
-
-    let inKey3 = this.doc.getElementById("toolbox-zoom-in-key3");
-    inKey3.addEventListener("command", this.zoomIn.bind(this), true);
-
-    let outKey = this.doc.getElementById("toolbox-zoom-out-key");
-    outKey.addEventListener("command", this.zoomOut.bind(this), true);
-
-    let outKey2 = this.doc.getElementById("toolbox-zoom-out-key2");
-    outKey2.addEventListener("command", this.zoomOut.bind(this), true);
-
-    let resetKey = this.doc.getElementById("toolbox-zoom-reset-key");
-    resetKey.addEventListener("command", this.zoomReset.bind(this), true);
+  _addZoomKeys: function (shortcuts) {
+    shortcuts.on(toolboxStrings("toolbox.zoomIn.key"),
+                 this.zoomIn.bind(this));
+    let zoomIn2 = toolboxStrings("toolbox.zoomIn2.key");
+    if (zoomIn2) {
+      shortcuts.on(zoomIn2, this.zoomIn.bind(this));
+    }
+    let zoomIn3 = toolboxStrings("toolbox.zoomIn2.key");
+    if (zoomIn3) {
+      shortcuts.on(zoomIn3, this.zoomIn.bind(this));
+    }
 
-    let resetKey2 = this.doc.getElementById("toolbox-zoom-reset-key2");
-    resetKey2.addEventListener("command", this.zoomReset.bind(this), true);
-  },
-
-  _disableZoomKeys: function () {
-    let inKey = this.doc.getElementById("toolbox-zoom-in-key");
-    inKey.setAttribute("disabled", "true");
-
-    let inKey2 = this.doc.getElementById("toolbox-zoom-in-key2");
-    inKey2.setAttribute("disabled", "true");
+    shortcuts.on(toolboxStrings("toolbox.zoomOut.key"),
+                 this.zoomOut.bind(this));
+    let zoomOut2 = toolboxStrings("toolbox.zoomOut2.key");
+    if (zoomOut2) {
+      shortcuts.on(zoomOut2, this.zoomOut.bind(this));
+    }
 
-    let inKey3 = this.doc.getElementById("toolbox-zoom-in-key3");
-    inKey3.setAttribute("disabled", "true");
-
-    let outKey = this.doc.getElementById("toolbox-zoom-out-key");
-    outKey.setAttribute("disabled", "true");
-
-    let outKey2 = this.doc.getElementById("toolbox-zoom-out-key2");
-    outKey2.setAttribute("disabled", "true");
-
-    let resetKey = this.doc.getElementById("toolbox-zoom-reset-key");
-    resetKey.setAttribute("disabled", "true");
-
-    let resetKey2 = this.doc.getElementById("toolbox-zoom-reset-key2");
-    resetKey2.setAttribute("disabled", "true");
+    shortcuts.on(toolboxStrings("toolbox.zoomReset.key"),
+                 this.zoomReset.bind(this));
+    let zoomReset2 = toolboxStrings("toolbox.zoomReset2.key");
+    if (zoomReset2) {
+      shortcuts.on(zoomReset2, this.zoomReset.bind(this));
+    }
   },
 
   /**
    * Set zoom on toolbox to whatever the last setting was.
    */
   _loadInitialZoom: function () {
     this.setZoom(this.zoomValue);
   },
 
   /**
    * Increase zoom level of toolbox window - make things bigger.
    */
-  zoomIn: function () {
+  zoomIn: function (name, event) {
     this.setZoom(this.zoomValue + 0.1);
+    event.preventDefault();
   },
 
   /**
    * Decrease zoom level of toolbox window - make things smaller.
    */
-  zoomOut: function () {
+  zoomOut: function (name, event) {
     this.setZoom(this.zoomValue - 0.1);
+    event.preventDefault();
   },
 
   /**
    * Reset zoom level of the toolbox window.
    */
-  zoomReset: function () {
+  zoomReset: function (name, event) {
     this.setZoom(1);
+    event.preventDefault();
   },
 
   /**
    * Set zoom level of the toolbox window.
    *
    * @param {number} zoomValue
    *        Zoom level e.g. 1.2
    */
@@ -895,20 +881,19 @@ Toolbox.prototype = {
         this.switchHost(position);
       });
 
       dockBox.appendChild(button);
     }
   },
 
   _getMinimizeButtonShortcutTooltip: function () {
-    let key = this.doc.getElementById("toolbox-minimize-key")
-                      .getAttribute("key");
-    return "(" + (osString == "Darwin" ? "Cmd+Shift+" : "Ctrl+Shift+") +
-           key.toUpperCase() + ")";
+    let str = toolboxStrings("toolbox.minimize.key");
+    let key = KeyShortcuts.parseElectronKey(this.win, str);
+    return "(" + KeyShortcuts.stringify(key) + ")";
   },
 
   _onBottomHostMinimized: function () {
     let btn = this.doc.querySelector("#toolbox-dock-bottom-minimize");
     btn.className = "minimized";
 
     btn.setAttribute("tooltiptext",
       toolboxStrings("toolboxDockButtons.bottom.maximize") + " " +
--- a/devtools/client/framework/toolbox.xul
+++ b/devtools/client/framework/toolbox.xul
@@ -24,85 +24,17 @@
           src="chrome://global/content/viewSourceUtils.js"/>
 
   <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
   <script type="application/javascript;version=1.8"
           src="chrome://devtools/content/framework/toolbox-init.js"/>
 
   <commandset id="editMenuCommands"/>
   <keyset id="editMenuKeys"/>
-  <keyset id="toolbox-keyset">
-    <key id="toolbox-options-key"
-         key="&toolboxOptionsButton.key;"
-         oncommand="void(0);"
-         modifiers="shift, accel"/>
-    <key id="toolbox-options-key2"
-         keycode="&openHelp.commandkey;"
-         oncommand="void(0);"/>
-    <key id="toolbox-next-tool-key"
-         key="&toolboxNextTool.key;"
-         oncommand="void(0);"
-         modifiers="accel"/>
-    <key id="toolbox-previous-tool-key"
-         key="&toolboxPreviousTool.key;"
-         oncommand="void(0);"
-         modifiers="accel"/>
-    <key id="toolbox-zoom-in-key"
-         key="&fullZoomEnlargeCmd.commandkey;"
-         oncommand="void(0);"
-         modifiers="accel"/>
-    <key id="toolbox-zoom-in-key2"
-         key="&fullZoomEnlargeCmd.commandkey2;"
-         oncommand="void(0);"
-         modifiers="accel"/>
-    <key id="toolbox-zoom-in-key3"
-         key="&fullZoomEnlargeCmd.commandkey3;"
-         oncommand="void(0);"
-         modifiers="accel"/>
-    <key id="toolbox-zoom-out-key"
-         key="&fullZoomReduceCmd.commandkey;"
-         oncommand="void(0);"
-         modifiers="accel"/>
-    <key id="toolbox-zoom-out-key2"
-         key="&fullZoomReduceCmd.commandkey2;"
-         oncommand="void(0);"
-         modifiers="accel"/>
-    <key id="toolbox-zoom-reset-key"
-         key="&fullZoomResetCmd.commandkey;"
-         oncommand="void(0);"
-         modifiers="accel"/>
-    <key id="toolbox-zoom-reset-key2"
-         key="&fullZoomResetCmd.commandkey2;"
-         oncommand="void(0);"
-         modifiers="accel"/>
-    <key id="toolbox-reload-key"
-         key="&toolboxReload.key;"
-         oncommand="void(0);"
-         modifiers="accel"/>
-    <key id="toolbox-force-reload-key"
-         key="&toolboxReload.key;"
-         oncommand="void(0);"
-         modifiers="accel shift"/>
-    <key id="toolbox-reload-key2"
-         keycode="VK_F5"
-         oncommand="void(0);"
-         modifiers=""/>
-    <key id="toolbox-force-reload-key2"
-         keycode="VK_F5"
-         oncommand="void(0);"
-         modifiers="accel"/>
-    <key id="toolbox-minimize-key"
-         key="&toolboxToggleMinimize.key;"
-         oncommand="void(0);"
-         modifiers="shift, accel"/>
-    <key id="toolbox-toggle-host-key"
-         key="&toolboxToggle.key;"
-         oncommand="void(0);"
-         modifiers="accel shift"/>
-  </keyset>
+  <keyset id="toolbox-keyset"/>
 
   <popupset>
     <menupopup id="toolbox-textbox-context-popup">
       <menuitem id="cMenu_undo"/>
       <menuseparator/>
       <menuitem id="cMenu_cut"/>
       <menuitem id="cMenu_copy"/>
       <menuitem id="cMenu_paste"/>
--- a/devtools/client/locales/en-US/toolbox.dtd
+++ b/devtools/client/locales/en-US/toolbox.dtd
@@ -6,41 +6,17 @@
 <!-- LOCALIZATION NOTE : FILE Do not translate key -->
 
 <!ENTITY closeCmd.key  "W">
 <!ENTITY toggleToolbox.key  "I">
 <!ENTITY toggleToolboxF12.keycode          "VK_F12">
 <!ENTITY toggleToolboxF12.keytext          "F12">
 
 <!ENTITY toolboxCloseButton.tooltip    "Close Developer Tools">
-<!ENTITY toolboxOptionsButton.key      "O">
-<!ENTITY toolboxNextTool.key           "]">
-<!ENTITY toolboxPreviousTool.key       "[">
 
-<!-- LOCALIZATION NOTE :
-fullZoomEnlargeCmd.commandkey, fullZoomEnlargeCmd.commandkey2,
-fullZoomEnlargeCmd.commandkey3, fullZoomReduceCmd.commandkey,
-fullZoomReduceCmd.commandkey2, fullZoomResetCmd.commandkey,
-and fullZoomResetCmd.commandkey2 should all match the corresponding
-values from browser.dtd.  -->
-<!ENTITY fullZoomEnlargeCmd.commandkey  "+">
-<!ENTITY fullZoomEnlargeCmd.commandkey2 "=">
-<!ENTITY fullZoomEnlargeCmd.commandkey3 "">
-
-<!ENTITY fullZoomReduceCmd.commandkey   "-">
-<!ENTITY fullZoomReduceCmd.commandkey2  "">
-
-<!ENTITY fullZoomResetCmd.commandkey    "0">
-<!ENTITY fullZoomResetCmd.commandkey2   "">
-
-<!ENTITY toolboxReload.key             "r">
-<!-- This key is used with the accel+shift modifiers to minimize the toolbox -->
-<!ENTITY toolboxToggleMinimize.key     "U">
-
-<!ENTITY toolboxToggle.key             "d">
 <!-- LOCALIZATION NOTE (toolboxFramesButton): This is the label for
   -  the iframes menu list that appears only when the document has some.
   -  It allows you to switch the context of the whole toolbox. -->
 <!ENTITY toolboxFramesTooltip          "Select an iframe as the currently targeted document">
 
 <!-- LOCALIZATION NOTE (toolboxNoAutoHideButton): This is the label for
   -  the button to force the popups/panels to stay visible on blur.
   -  This is only visible in the browser toolbox as it is meant for
--- a/devtools/client/locales/en-US/toolbox.properties
+++ b/devtools/client/locales/en-US/toolbox.properties
@@ -130,8 +130,55 @@ toolbox.viewCssSourceInStyleEditor.label
 
 # LOCALIZATION NOTE (toolbox.viewJsSourceInDebugger.label)
 # Used as a message in either tooltips or contextual menu items to open the
 # corresponding URL as a js file in the Debugger tool.
 # DEV NOTE: Mostly used wherever toolbox.viewSourceInDebugger is used.
 toolbox.viewJsSourceInDebugger.label=Open File in Debugger
 
 toolbox.resumeOrderWarning=Page did not resume after the debugger was attached. To fix this, please close and re-open the toolbox.
+
+# LOCALIZATION NOTE (toolbox.options.key)
+# Key shortcut used to open the options panel
+toolbox.options.key=CmdOrCtrl+Shift+O
+
+# LOCALIZATION NOTE (toolbox.help.key)
+# Key shortcut used to open the options panel
+toolbox.help.key=F1
+
+# LOCALIZATION NOTE (toolbox.nextTool.key)
+# Key shortcut used to select the next tool
+toolbox.nextTool.key=CmdOrCtrl+]
+
+# LOCALIZATION NOTE (toolbox.previousTool.key)
+# Key shortcut used to select the previous tool
+toolbox.previousTool.key=CmdOrCtrl+[
+
+# LOCALIZATION NOTE (toolbox.zoom*.key)
+# Key shortcuts used to zomm in/out or reset the toolbox
+# Should match fullZoom*Cmd.commandkey values from browser.dtd
+toolbox.zoomIn.key=CmdOrCtrl+Plus
+toolbox.zoomIn2.key=CmdOrCtrl+=
+toolbox.zoomIn3.key=
+
+toolbox.zoomOut.key=CmdOrCtrl+-
+toolbox.zoomOut2.key=
+
+toolbox.zoomReset.key=CmdOrCtrl+0
+toolbox.zoomReset2.key=
+
+# LOCALIZATION NOTE (toolbox.reload*.key)
+# Key shortcuts used to reload the page
+toolbox.reload.key=CmdOrCtrl+R
+toolbox.reload2.key=F5
+
+# LOCALIZATION NOTE (toolbox.forceReload*.key)
+# Key shortcuts used to force reload of the page by bypassing caches
+toolbox.forceReload.key=CmdOrCtrl+Shift+R
+toolbox.forceReload2.key=CmdOrCtrl+F5
+
+# LOCALIZATION NOTE (toolbox.minimize.key)
+# Key shortcut used to minimize the toolbox
+toolbox.minimize.key=CmdOrCtrl+Shift+U
+
+# LOCALIZATION NOTE (toolbox.toggleHost.key)
+# Key shortcut used to move the toolbox in bottom or side of the browser window
+toolbox.toggleHost.key=CmdOrCtrl+Shift+D
--- a/devtools/client/shared/key-shortcuts.js
+++ b/devtools/client/shared/key-shortcuts.js
@@ -29,17 +29,16 @@ const ElectronKeysMapping = {
   "F17": "DOM_VK_F17",
   "F18": "DOM_VK_F18",
   "F19": "DOM_VK_F19",
   "F20": "DOM_VK_F20",
   "F21": "DOM_VK_F21",
   "F22": "DOM_VK_F22",
   "F23": "DOM_VK_F23",
   "F24": "DOM_VK_F24",
-  "Plus": "DOM_VK_PLUS",
   "Space": "DOM_VK_SPACE",
   "Backspace": "DOM_VK_BACK_SPACE",
   "Delete": "DOM_VK_DELETE",
   "Insert": "DOM_VK_INSERT",
   "Return": "DOM_VK_RETURN",
   "Enter": "DOM_VK_RETURN",
   "Up": "DOM_VK_UP",
   "Down": "DOM_VK_DOWN",
@@ -117,30 +116,62 @@ KeyShortcuts.parseElectronKey = function
       shortcut.ctrl = true;
     } else if (mod === "Shift") {
       shortcut.shift = true;
     } else {
       throw new Error("Unsupported modifier: " + mod);
     }
   }
 
-  if (typeof (key) === "string" && key.length === 1) {
+  // Plus is a special case. It's a character key and shouldn't be matched
+  // against a keycode as it is only accessible via Shift/Capslock
+  if (key === "Plus") {
+    key = "+";
+  }
+
+  if (typeof key === "string" && key.length === 1) {
     // Match any single character
     shortcut.key = key.toLowerCase();
   } else if (key in ElectronKeysMapping) {
     // Maps the others manually to DOM API DOM_VK_*
     key = ElectronKeysMapping[key];
     shortcut.keyCode = window.KeyboardEvent[key];
+    // Used only to stringify the shortcut
+    shortcut.keyCodeString = key;
   } else {
     throw new Error("Unsupported key: " + key);
   }
 
   return shortcut;
 };
 
+KeyShortcuts.stringify = function (shortcut) {
+  let list = [];
+  if (shortcut.alt) {
+    list.push("Alt");
+  }
+  if (shortcut.ctrl) {
+    list.push("Ctrl");
+  }
+  if (shortcut.meta) {
+    list.push("Cmd");
+  }
+  if (shortcut.shift) {
+    list.push("Shift");
+  }
+  let key;
+  if (shortcut.key) {
+    key = shortcut.key.toUpperCase();
+  } else {
+    key = shortcut.keyCodeString;
+  }
+  list.push(key);
+  return list.join("+");
+};
+
 KeyShortcuts.prototype = {
   destroy() {
     this.window.removeEventListener("keydown", this);
     this.keys.clear();
   },
 
   doesEventMatchShortcut(event, shortcut) {
     if (shortcut.meta != event.metaKey) {
@@ -156,17 +187,22 @@ KeyShortcuts.prototype = {
     // expected key is a special character accessible via shift.
     if (shortcut.shift != event.shiftKey && event.key &&
         event.key.match(/[a-zA-Z]/)) {
       return false;
     }
     if (shortcut.keyCode) {
       return event.keyCode == shortcut.keyCode;
     }
-    return event.key.toLowerCase() == shortcut.key;
+    // For character keys, we match if the final character is the expected one.
+    // But for digits we also accept indirect match to please azerty keyboard,
+    // which requires Shift to be pressed to get digits.
+    return event.key.toLowerCase() == shortcut.key ||
+      (shortcut.key.match(/[0-9]/) &&
+       event.keyCode == shortcut.key.charCodeAt(0));
   },
 
   handleEvent(event) {
     for (let [key, shortcut] of this.keys) {
       if (this.doesEventMatchShortcut(event, shortcut)) {
         this.eventEmitter.emit(key, event);
       }
     }
--- a/devtools/client/shared/test/browser.ini
+++ b/devtools/client/shared/test/browser.ini
@@ -112,16 +112,18 @@ skip-if = e10s # Bug 1221911, bug 122228
 skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
 [browser_graphs-16.js]
 skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
 [browser_html_tooltip-01.js]
 [browser_html_tooltip-02.js]
 [browser_html_tooltip-03.js]
 [browser_html_tooltip-04.js]
 [browser_html_tooltip-05.js]
+[browser_html_tooltip_arrow-01.js]
+[browser_html_tooltip_arrow-02.js]
 [browser_inplace-editor-01.js]
 [browser_inplace-editor-02.js]
 [browser_inplace-editor_maxwidth.js]
 [browser_key_shortcuts.js]
 [browser_layoutHelpers.js]
 skip-if = e10s # Layouthelpers test should not run in a content page.
 [browser_layoutHelpers-getBoxQuads.js]
 skip-if = e10s # Layouthelpers test should not run in a content page.
--- a/devtools/client/shared/test/browser_html_tooltip-02.js
+++ b/devtools/client/shared/test/browser_html_tooltip-02.js
@@ -6,16 +6,17 @@
 
 /**
  * Test the HTMLTooltip is closed when clicking outside of its container.
  */
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
   <?xml-stylesheet href="chrome://global/skin/global.css"?>
+  <?xml-stylesheet href="chrome://devtools/skin/common.css"?>
   <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
    title="Tooltip test">
     <vbox flex="1">
       <hbox id="box1" flex="1">test1</hbox>
       <hbox id="box2" flex="1">test2</hbox>
       <hbox id="box3" flex="1">test3</hbox>
       <hbox id="box4" flex="1">test4</hbox>
     </vbox>
--- a/devtools/client/shared/test/browser_html_tooltip-03.js
+++ b/devtools/client/shared/test/browser_html_tooltip-03.js
@@ -6,16 +6,17 @@
 
 /**
  * Test the HTMLTooltip autofocus configuration option.
  */
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
   <?xml-stylesheet href="chrome://global/skin/global.css"?>
+  <?xml-stylesheet href="chrome://devtools/skin/common.css"?>
   <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
    title="Tooltip test">
     <vbox flex="1">
       <hbox id="box1" flex="1">test1</hbox>
       <hbox id="box2" flex="1">test2</hbox>
       <hbox id="box3" flex="1">test3</hbox>
       <hbox id="box4" flex="1">
         <textbox id="box4-input"></textbox>
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_arrow-01.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip "arrow" type on small anchors. The arrow should remain
+ * aligned with the anchors as much as possible
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const getAnchor = function (position) {
+  return `<html:div class="anchor" style="width:10px;
+                                          height: 10px;
+                                          position: absolute;
+                                          background: red;
+                                          ${position}"></html:div>`;
+};
+
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+  <?xml-stylesheet href="chrome://global/skin/global.css"?>
+  <?xml-stylesheet href="chrome://devtools/skin/common.css"?>
+  <?xml-stylesheet href="chrome://devtools/skin/light-theme.css"?>
+
+  <window class="theme-light"
+          xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+          xmlns:html="http://www.w3.org/1999/xhtml"
+          title="Tooltip test">
+    <vbox flex="1" style="position: relative">
+      ${getAnchor("top: 0; left: 0;")}
+      ${getAnchor("top: 0; left: 25px;")}
+      ${getAnchor("top: 0; left: 50px;")}
+      ${getAnchor("top: 0; left: 75px;")}
+      ${getAnchor("bottom: 0; left: 0;")}
+      ${getAnchor("bottom: 0; left: 25px;")}
+      ${getAnchor("bottom: 0; left: 50px;")}
+      ${getAnchor("bottom: 0; left: 75px;")}
+      ${getAnchor("bottom: 0; right: 0;")}
+      ${getAnchor("bottom: 0; right: 25px;")}
+      ${getAnchor("bottom: 0; right: 50px;")}
+      ${getAnchor("bottom: 0; right: 75px;")}
+      ${getAnchor("top: 0; right: 0;")}
+      ${getAnchor("top: 0; right: 25px;")}
+      ${getAnchor("top: 0; right: 50px;")}
+      ${getAnchor("top: 0; right: 75px;")}
+    </vbox>
+  </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+add_task(function* () {
+  // Force the toolbox to be 200px high;
+  yield pushPref("devtools.toolbox.footer.height", 200);
+
+  yield addTab("about:blank");
+  let [,, doc] = yield createHost("bottom", TEST_URI);
+
+  info("Create HTML tooltip");
+  let tooltip = new HTMLTooltip({doc}, {type: "arrow"});
+  let div = doc.createElementNS(HTML_NS, "div");
+  div.style.height = "100%";
+  yield tooltip.setContent(div, 200, 35);
+
+  let {right: docRight} = doc.documentElement.getBoundingClientRect();
+
+  let elements = [...doc.querySelectorAll(".anchor")];
+  for (let el of elements) {
+    info("Display the tooltip on an anchor.");
+    yield showTooltip(tooltip, el);
+
+    let arrow = tooltip.arrow;
+    ok(arrow, "Tooltip has an arrow");
+
+    // Get the geometry of the anchor, the tooltip frame & arrow.
+    let arrowBounds = arrow.getBoxQuads({relativeTo: doc})[0].bounds;
+    let frameBounds = tooltip.frame.getBoxQuads({relativeTo: doc})[0].bounds;
+    let anchorBounds = el.getBoxQuads({relativeTo: doc})[0].bounds;
+
+    let intersects = arrowBounds.left <= anchorBounds.right &&
+                     arrowBounds.right >= anchorBounds.left;
+    let isBlockedByViewport = arrowBounds.left == 0 ||
+                              arrowBounds.right == docRight;
+    ok(intersects || isBlockedByViewport,
+      "Tooltip arrow is aligned with the anchor, or stuck on viewport's edge.");
+
+    let isInFrame = arrowBounds.left >= frameBounds.left &&
+                    arrowBounds.right <= frameBounds.right;
+    ok(isInFrame,
+      "The tooltip arrow remains inside the tooltip frame horizontally");
+
+    yield hideTooltip(tooltip);
+  }
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_arrow-02.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+
+"use strict";
+
+/**
+ * Test the HTMLTooltip "arrow" type on wide anchors. The arrow should remain
+ * aligned with the anchors as much as possible
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const getAnchor = function (position) {
+  return `<html:div class="anchor" style="height: 5px;
+                                          position: absolute;
+                                          background: red;
+                                          ${position}"></html:div>`;
+};
+
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+  <?xml-stylesheet href="chrome://global/skin/global.css"?>
+  <?xml-stylesheet href="chrome://devtools/skin/common.css"?>
+  <?xml-stylesheet href="chrome://devtools/skin/light-theme.css"?>
+
+  <window class="theme-light"
+          xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+          xmlns:html="http://www.w3.org/1999/xhtml"
+          title="Tooltip test">
+    <vbox flex="1" style="position: relative">
+      ${getAnchor("top:    0; left: 0; width: 50px;")}
+      ${getAnchor("top: 10px; left: 0; width: 100px;")}
+      ${getAnchor("top: 20px; left: 0; width: 150px;")}
+      ${getAnchor("top: 30px; left: 0; width: 200px;")}
+      ${getAnchor("top: 40px; left: 0; width: 250px;")}
+      ${getAnchor("top: 50px; left: 100px; width: 250px;")}
+      ${getAnchor("top: 100px; width:  50px; right: 0;")}
+      ${getAnchor("top: 110px; width: 100px; right: 0;")}
+      ${getAnchor("top: 120px; width: 150px; right: 0;")}
+      ${getAnchor("top: 130px; width: 200px; right: 0;")}
+      ${getAnchor("top: 140px; width: 250px; right: 0;")}
+    </vbox>
+  </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+add_task(function* () {
+  // Force the toolbox to be 200px high;
+  yield pushPref("devtools.toolbox.footer.height", 200);
+
+  yield addTab("about:blank");
+  let [,, doc] = yield createHost("bottom", TEST_URI);
+
+  info("Create HTML tooltip");
+  let tooltip = new HTMLTooltip({doc}, {type: "arrow"});
+  let div = doc.createElementNS(HTML_NS, "div");
+  div.style.height = "100%";
+  yield tooltip.setContent(div, 200, 35);
+
+  let {right: docRight} = doc.documentElement.getBoundingClientRect();
+
+  let elements = [...doc.querySelectorAll(".anchor")];
+  for (let el of elements) {
+    info("Display the tooltip on an anchor.");
+    yield showTooltip(tooltip, el);
+
+    let arrow = tooltip.arrow;
+    ok(arrow, "Tooltip has an arrow");
+
+    // Get the geometry of the anchor, the tooltip frame & arrow.
+    let arrowBounds = arrow.getBoxQuads({relativeTo: doc})[0].bounds;
+    let frameBounds = tooltip.frame.getBoxQuads({relativeTo: doc})[0].bounds;
+    let anchorBounds = el.getBoxQuads({relativeTo: doc})[0].bounds;
+
+    let intersects = arrowBounds.left <= anchorBounds.right &&
+                     arrowBounds.right >= anchorBounds.left;
+    let isBlockedByViewport = arrowBounds.left == 0 ||
+                              arrowBounds.right == docRight;
+    ok(intersects || isBlockedByViewport,
+      "Tooltip arrow is aligned with the anchor, or stuck on viewport's edge.");
+
+    let isInFrame = arrowBounds.left >= frameBounds.left &&
+                    arrowBounds.right <= frameBounds.right;
+    ok(isInFrame,
+      "The tooltip arrow remains inside the tooltip frame horizontally");
+    yield hideTooltip(tooltip);
+  }
+});
--- a/devtools/client/shared/test/browser_key_shortcuts.js
+++ b/devtools/client/shared/test/browser_key_shortcuts.js
@@ -4,17 +4,19 @@
 var isOSX = Services.appinfo.OS === "Darwin";
 
 add_task(function* () {
   let shortcuts = new KeyShortcuts({
     window
   });
   yield testSimple(shortcuts);
   yield testNonLetterCharacter(shortcuts);
+  yield testPlusCharacter(shortcuts);
   yield testMixup(shortcuts);
+  yield testLooseDigits(shortcuts);
   yield testExactModifiers(shortcuts);
   yield testLooseShiftModifier(shortcuts);
   yield testStrictLetterShiftModifier(shortcuts);
   yield testAltModifier(shortcuts);
   yield testCommandOrControlModifier(shortcuts);
   yield testCtrlModifier(shortcuts);
   shortcuts.destroy();
 });
@@ -56,16 +58,30 @@ function testNonLetterCharacter(shortcut
   let onKey = once(shortcuts, "[", (key, event) => {
     is(event.key, "[");
   });
 
   EventUtils.synthesizeKey("[", {}, window);
   yield onKey;
 }
 
+// Plus is special. It's keycode is the one for "=". That's because it requires
+// shift to be pressed and is behind "=" key. So it should be considered as a
+// character key
+function testPlusCharacter(shortcuts) {
+  info("Test 'Plus' key shortcuts");
+
+  let onKey = once(shortcuts, "Plus", (key, event) => {
+    is(event.key, "+");
+  });
+
+  EventUtils.synthesizeKey("+", { keyCode: 61, shiftKey: true }, window);
+  yield onKey;
+}
+
 // Test they listeners are not mixed up between shortcuts
 function testMixup(shortcuts) {
   info("Test possible listener mixup");
 
   let hitFirst = false, hitSecond = false;
   let onFirstKey = once(shortcuts, "0", (key, event) => {
     is(event.key, "0");
     hitFirst = true;
@@ -90,16 +106,50 @@ function testMixup(shortcuts) {
   ok(!hitSecond, "No mixup, second shortcut is still not notified (2/2)");
 
   // Finally dispatch the second shortcut
   EventUtils.synthesizeKey("a", { altKey: true }, window);
   yield onSecondKey;
   ok(hitSecond, "Got the second shortcut notified once it is actually fired");
 }
 
+// On azerty keyboard, digits are only available by pressing Shift/Capslock,
+// but we accept them even if we omit doing that.
+function testLooseDigits(shortcuts) {
+  info("Test Loose digits");
+  let onKey = once(shortcuts, "0", (key, event) => {
+    is(event.key, "à");
+    ok(!event.altKey);
+    ok(!event.ctrlKey);
+    ok(!event.metaKey);
+    ok(!event.shiftKey);
+  });
+  // Simulate a press on the "0" key, without shift pressed on a french
+  // keyboard
+  EventUtils.synthesizeKey(
+    "à",
+    { keyCode: 48 },
+    window);
+  yield onKey;
+
+  onKey = once(shortcuts, "0", (key, event) => {
+    is(event.key, "0");
+    ok(!event.altKey);
+    ok(!event.ctrlKey);
+    ok(!event.metaKey);
+    ok(event.shiftKey);
+  });
+  // Simulate the same press with shift pressed
+  EventUtils.synthesizeKey(
+    "0",
+    { keyCode: 48, shiftKey: true },
+    window);
+  yield onKey;
+}
+
 // Test that shortcuts is notified only when the modifiers match exactly
 function testExactModifiers(shortcuts) {
   info("Test exact modifiers match");
 
   let hit = false;
   let onKey = once(shortcuts, "Alt+A", (key, event) => {
     is(event.key, "a");
     ok(event.altKey);
--- a/devtools/client/shared/test/helper_html_tooltip.js
+++ b/devtools/client/shared/test/helper_html_tooltip.js
@@ -47,20 +47,20 @@ function* hideTooltip(tooltip) {
 /**
  * Forces the reflow of an HTMLTooltip document and waits for the next repaint.
  *
  * @param {HTMLTooltip} the tooltip to reflow
  * @return {Promise} a promise that will resolve after the reflow and repaint
  *         have been executed.
  */
 function waitForReflow(tooltip) {
-  let {document} = tooltip;
+  let {doc} = tooltip;
   return new Promise(resolve => {
-    document.documentElement.offsetWidth;
-    document.defaultView.requestAnimationFrame(resolve);
+    doc.documentElement.offsetWidth;
+    doc.defaultView.requestAnimationFrame(resolve);
   });
 }
 
 /**
  * Test helper designed to check that a tooltip is displayed at the expected
  * position relative to an anchor, given a set of expectations.
  *
  * @param {HTMLTooltip} tooltip
--- a/devtools/client/shared/widgets/HTMLTooltip.js
+++ b/devtools/client/shared/widgets/HTMLTooltip.js
@@ -8,101 +8,153 @@
 
 const EventEmitter = require("devtools/shared/event-emitter");
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 const XHTML_NS = "http://www.w3.org/1999/xhtml";
 
 const IFRAME_URL = "chrome://devtools/content/shared/widgets/tooltip-frame.xhtml";
 const IFRAME_CONTAINER_ID = "tooltip-iframe-container";
 
+const POSITION = {
+  TOP: "top",
+  BOTTOM: "bottom",
+};
+
+module.exports.POSITION = POSITION;
+
+const TYPE = {
+  NORMAL: "normal",
+  ARROW: "arrow",
+};
+
+module.exports.TYPE = TYPE;
+
+const ARROW_WIDTH = 32;
+
+// Default offset between the tooltip's left edge and the tooltip arrow.
+const ARROW_OFFSET = 20;
+
+const EXTRA_HEIGHT = {
+  "normal": 0,
+  // The arrow is 16px tall, but merges on 3px with the panel border
+  "arrow": 13,
+};
+
+const EXTRA_BORDER = {
+  "normal": 0,
+  "arrow": 3,
+};
+
 /**
  * The HTMLTooltip can display HTML content in a tooltip popup.
  *
  * @param {Toolbox} toolbox
  *        The devtools toolbox, needed to get the devtools main window.
  * @param {Object}
  *        - {String} type
- *          Display type of the tooltip. Possible values: "normal"
+ *          Display type of the tooltip. Possible values: "normal", "arrow"
  *        - {Boolean} autofocus
  *          Defaults to true. Should the tooltip be focused when opening it.
  *        - {Boolean} consumeOutsideClicks
  *          Defaults to true. The tooltip is closed when clicking outside.
  *          Should this event be stopped and consumed or not.
  */
 function HTMLTooltip(toolbox,
   {type = "normal", autofocus = true, consumeOutsideClicks = true} = {}) {
   EventEmitter.decorate(this);
 
-  this.document = toolbox.doc;
+  this.doc = toolbox.doc;
   this.type = type;
   this.autofocus = autofocus;
   this.consumeOutsideClicks = consumeOutsideClicks;
 
   // Use the topmost window to listen for click events to close the tooltip
-  this.topWindow = this.document.defaultView.top;
+  this.topWindow = this.doc.defaultView.top;
 
   this._onClick = this._onClick.bind(this);
 
   this.container = this._createContainer();
 
   // Promise that will resolve when the container can be filled with content.
   this.containerReady = new Promise(resolve => {
     if (this._isXUL()) {
       // In XUL context, load a placeholder document in the iframe container.
       let onLoad = () => {
-        this.container.removeEventListener("load", onLoad, true);
+        this.frame.removeEventListener("load", onLoad, true);
         resolve();
       };
 
-      this.container.addEventListener("load", onLoad, true);
-      this.container.setAttribute("src", IFRAME_URL);
+      this.frame.addEventListener("load", onLoad, true);
+      this.frame.setAttribute("src", IFRAME_URL);
+      this.doc.querySelector("window").appendChild(this.container);
     } else {
       // In non-XUL context the container is ready to use as is.
+      this.doc.body.appendChild(this.container);
       resolve();
     }
   });
 }
 
 module.exports.HTMLTooltip = HTMLTooltip;
 
 HTMLTooltip.prototype = {
-  position: {
-    TOP: "top",
-    BOTTOM: "bottom",
+  /**
+   * The tooltip frame is the child of the tooltip container that will only
+   * contain the tooltip content (and not the arrow or any other tooltip styling
+   * element).
+   * In XUL contexts, this is an iframe. In non XUL contexts this is a div,
+   * which also happens to be the tooltip.panel property.
+   */
+  get frame() {
+    return this.container.querySelector(".tooltip-panel");
   },
 
-  get parent() {
-    if (this._isXUL()) {
-      // In XUL context, we are wrapping the HTML content in an iframe.
-      let win = this.container.contentWindow.wrappedJSObject;
-      return win.document.getElementById(IFRAME_CONTAINER_ID);
+  /**
+   * The tooltip panel is the parentNode of the tooltip content provided in
+   * setContent().
+   */
+  get panel() {
+    if (!this._isXUL()) {
+      return this.frame;
     }
-    return this.container;
+    // In XUL context, the content is wrapped in an iframe.
+    let win = this.frame.contentWindow.wrappedJSObject;
+    return win.document.getElementById(IFRAME_CONTAINER_ID);
+  },
+
+  /**
+   * The arrow element. Might be null depending on the tooltip type.
+   */
+  get arrow() {
+    return this.container.querySelector(".tooltip-arrow");
   },
 
   /**
    * Set the tooltip content element. The preferred width/height should also be
    * specified here.
    *
    * @param {Element} content
    *        The tooltip content, should be a HTML element.
    * @param {Number} width
    *        Preferred width for the tooltip container
    * @param {Number} height
    *        Preferred height for the tooltip container
    * @return {Promise} a promise that will resolve when the content has been
    *         added in the tooltip container.
    */
   setContent: function (content, width, height) {
-    this.preferredWidth = width;
-    this.preferredHeight = height;
+    let themeHeight = EXTRA_HEIGHT[this.type] + 2 * EXTRA_BORDER[this.type];
+    let themeWidth = 2 * EXTRA_BORDER[this.type];
+
+    this.preferredWidth = width + themeWidth;
+    this.preferredHeight = height + themeHeight;
 
     return this.containerReady.then(() => {
-      this.parent.innerHTML = "";
-      this.parent.appendChild(content);
+      this.panel.innerHTML = "";
+      this.panel.appendChild(content);
     });
   },
 
   /**
    * Show the tooltip next to the provided anchor element. A preferred position
    * can be set. The event "shown" will be fired after the tooltip is displayed.
    *
    * @param {Element} anchor
@@ -110,186 +162,217 @@ HTMLTooltip.prototype = {
    * @param {Object}
    *        - {String} position: optional, possible values: top|bottom
    *          If layout permits, the tooltip will be displayed on top/bottom
    *          of the anchor. If ommitted, the tooltip will be displayed where
    *          more space is available.
    */
   show: function (anchor, {position} = {}) {
     this.containerReady.then(() => {
-      let {top, left, width, height} = this._findBestPosition(anchor, position);
+      let computedPosition = this._findBestPosition(anchor, position);
+
+      let isTop = computedPosition.position === POSITION.TOP;
+      this.container.classList.toggle("tooltip-top", isTop);
+      this.container.classList.toggle("tooltip-bottom", !isTop);
 
-      if (this._isXUL()) {
-        this.container.setAttribute("width", width);
-        this.container.setAttribute("height", height);
-      } else {
-        this.container.style.width = width + "px";
-        this.container.style.height = height + "px";
+      this.container.style.width = computedPosition.width + "px";
+      this.container.style.height = computedPosition.height + "px";
+      this.container.style.top = computedPosition.top + "px";
+      this.container.style.left = computedPosition.left + "px";
+
+      if (this.type === TYPE.ARROW) {
+        this.arrow.style.left = computedPosition.arrowLeft + "px";
       }
 
-      this.container.style.top = top + "px";
-      this.container.style.left = left + "px";
-      this.container.style.display = "block";
+      this.container.classList.add("tooltip-visible");
 
-      if (this.autofocus) {
-        this.container.focus();
-      }
-
-      this.attachEventsTimer = this.document.defaultView.setTimeout(() => {
+      this.attachEventsTimer = this.doc.defaultView.setTimeout(() => {
+        if (this.autofocus) {
+          this.frame.focus();
+        }
         this.topWindow.addEventListener("click", this._onClick, true);
         this.emit("shown");
       }, 0);
     });
   },
 
   /**
    * Hide the current tooltip. The event "hidden" will be fired when the tooltip
    * is hidden.
    */
   hide: function () {
-    this.document.defaultView.clearTimeout(this.attachEventsTimer);
+    this.doc.defaultView.clearTimeout(this.attachEventsTimer);
 
     if (this.isVisible()) {
       this.topWindow.removeEventListener("click", this._onClick, true);
-      this.container.style.display = "none";
+      this.container.classList.remove("tooltip-visible");
       this.emit("hidden");
     }
   },
 
   /**
    * Check if the tooltip is currently displayed.
    * @return {Boolean} true if the tooltip is visible
    */
   isVisible: function () {
-    let win = this.document.defaultView;
-    return win.getComputedStyle(this.container).display != "none";
+    return this.container.classList.contains("tooltip-visible");
   },
 
   /**
    * Destroy the tooltip instance. Hide the tooltip if displayed, remove the
    * tooltip container from the document.
    */
   destroy: function () {
     this.hide();
     this.container.remove();
   },
 
   _createContainer: function () {
-    let container;
+    let container = this.doc.createElementNS(XHTML_NS, "div");
+    container.setAttribute("type", this.type);
+    container.classList.add("tooltip-container");
+
+    let html;
     if (this._isXUL()) {
-      container = this.document.createElementNS(XHTML_NS, "iframe");
-      container.classList.add("devtools-tooltip-iframe");
-      this.document.querySelector("window").appendChild(container);
+      html = '<iframe class="devtools-tooltip-iframe tooltip-panel"></iframe>';
     } else {
-      container = this.document.createElementNS(XHTML_NS, "div");
-      this.document.body.appendChild(container);
+      html = '<div class="tooltip-panel theme-body"></div>';
     }
 
-    container.classList.add("theme-body");
-    container.classList.add("devtools-htmltooltip-container");
-
+    if (this.type === TYPE.ARROW) {
+      html += '<div class="tooltip-arrow"></div>';
+    }
+    container.innerHTML = html;
     return container;
   },
 
   _onClick: function (e) {
     if (this._isInTooltipContainer(e.target)) {
       return;
     }
 
     this.hide();
     if (this.consumeOutsideClicks) {
       e.preventDefault();
       e.stopPropagation();
     }
   },
 
   _isInTooltipContainer: function (node) {
-    let contentWindow = this.parent.ownerDocument.defaultView;
+    let contentWindow = this.panel.ownerDocument.defaultView;
     let win = node.ownerDocument.defaultView;
 
     if (win === contentWindow) {
       // If node is in the same window as the tooltip, check if the tooltip
       // parent contains node.
-      return this.parent.contains(node);
+      return this.panel.contains(node);
     }
 
     // Otherwise check if the node window is in the tooltip window.
     while (win.parent && win.parent != win) {
       win = win.parent;
       if (win === contentWindow) {
         return true;
       }
     }
     return false;
   },
 
+  /**
+   * Calculates the best possible position to display the tooltip near the
+   * provided anchor. An optional position can be provided, but will be
+   * respected only if it doesn't force the tooltip to be resized.
+   *
+   * If the tooltip has to be resized, the position will be wherever the most
+   * space is available.
+   *
+   */
   _findBestPosition: function (anchor, position) {
-    let top, left;
-    let {TOP, BOTTOM} = this.position;
+    let {TOP, BOTTOM} = POSITION;
 
-    let {left: anchorLeft, top: anchorTop, height: anchorHeight}
-      = this._getRelativeRect(anchor, this.document);
-
-    let {bottom: docBottom, right: docRight} =
-      this.document.documentElement.getBoundingClientRect();
+    // Get anchor geometry
+    let {
+      left: anchorLeft, top: anchorTop,
+      height: anchorHeight, width: anchorWidth
+    } = this._getRelativeRect(anchor, this.doc);
 
-    let height = this.preferredHeight;
-    // Check if the popup can fit above the anchor.
+    // Get document geometry
+    let {bottom: docBottom, right: docRight} =
+      this.doc.documentElement.getBoundingClientRect();
+
+    // Calculate available space for the tooltip.
     let availableTop = anchorTop;
-    let fitsAbove = availableTop >= height;
-    // Check if the popup can fit below the anchor.
-    let availableBelow = docBottom - (anchorTop + anchorHeight);
-    let fitsBelow = availableBelow >= height;
+    let availableBottom = docBottom - (anchorTop + anchorHeight);
 
-    let isPositionSuitable = (fitsAbove && position === TOP)
-      || (fitsBelow && position === BOTTOM);
-    if (!isPositionSuitable) {
-      // If the preferred position does not fit the preferred height,
-      // pick the position offering the most height.
-      position = availableTop > availableBelow ? TOP : BOTTOM;
+    // Find POSITION
+    let keepPosition = false;
+    if (position === TOP) {
+      keepPosition = availableTop >= this.preferredHeight;
+    } else if (position === BOTTOM) {
+      keepPosition = availableBottom >= this.preferredHeight;
+    }
+    if (!keepPosition) {
+      position = availableTop > availableBottom ? TOP : BOTTOM;
     }
 
-    // Calculate height, capped by the maximum height available.
-    height = Math.min(height, Math.max(availableTop, availableBelow));
-    top = position === TOP ? anchorTop - height : anchorTop + anchorHeight;
+    // Calculate HEIGHT.
+    let availableHeight = position === TOP ? availableTop : availableBottom;
+    let height = Math.min(this.preferredHeight, availableHeight);
+    height = Math.floor(height);
 
+    // Calculate TOP.
+    let top = position === TOP ? anchorTop - height : anchorTop + anchorHeight;
+
+    // Calculate WIDTH.
     let availableWidth = docRight;
     let width = Math.min(this.preferredWidth, availableWidth);
 
-    // By default, align the tooltip's left edge with the anchor left edge.
-    if (anchorLeft + width <= docRight) {
-      left = anchorLeft;
-    } else {
-      // If the tooltip cannot fit, shift to the left just enough to fit.
-      left = docRight - width;
+    // Calculate LEFT.
+    // By default the tooltip is aligned with the anchor left edge. Unless this
+    // makes it overflow the viewport, in which case is shifts to the left.
+    let left = Math.min(anchorLeft, docRight - width);
+
+    // Calculate ARROW LEFT (tooltip's LEFT might be updated)
+    let arrowLeft;
+    // Arrow style tooltips may need to be shifted to the left
+    if (this.type === TYPE.ARROW) {
+      let arrowCenter = left + ARROW_OFFSET + ARROW_WIDTH / 2;
+      let anchorCenter = anchorLeft + anchorWidth / 2;
+      // If the anchor is too narrow, align the arrow and the anchor center.
+      if (arrowCenter > anchorCenter) {
+        left = Math.max(0, left - (arrowCenter - anchorCenter));
+      }
+      // Arrow's feft offset relative to the anchor.
+      arrowLeft = Math.min(ARROW_OFFSET, (anchorWidth - ARROW_WIDTH) / 2) | 0;
+      // Translate the coordinate to tooltip container
+      arrowLeft += anchorLeft - left;
+      // Make sure the arrow remains in the tooltip container.
+      arrowLeft = Math.min(arrowLeft, width - ARROW_WIDTH);
+      arrowLeft = Math.max(arrowLeft, 0);
     }
 
-    return {top, left, width, height};
+    return {top, left, width, height, position, arrowLeft};
   },
 
   /**
    * Get the bounding client rectangle for a given node, relative to a custom
    * reference element (instead of the default for getBoundingClientRect which
    * is always the element's ownerDocument).
    */
   _getRelativeRect: function (node, relativeTo) {
     // Width and Height can be taken from the rect.
     let {width, height} = node.getBoundingClientRect();
 
-    // Find the smallest top/left coordinates from all quads.
-    let top = Infinity, left = Infinity;
-    let quads = node.getBoxQuads({relativeTo: relativeTo});
-    for (let quad of quads) {
-      top = Math.min(top, quad.bounds.top);
-      left = Math.min(left, quad.bounds.left);
-    }
+    let quads = node.getBoxQuads({relativeTo});
+    let top = quads[0].bounds.top;
+    let left = quads[0].bounds.left;
 
     // Compute right and bottom coordinates using the rest of the data.
     let right = left + width;
     let bottom = top + height;
 
     return {top, right, bottom, left, width, height};
   },
 
   _isXUL: function () {
-    return this.document.documentElement.namespaceURI === XUL_NS;
+    return this.doc.documentElement.namespaceURI === XUL_NS;
   },
 };
--- a/devtools/client/shared/widgets/tooltip-frame.xhtml
+++ b/devtools/client/shared/widgets/tooltip-frame.xhtml
@@ -10,15 +10,16 @@
   <script type="application/javascript;version=1.8" src="chrome://devtools/content/shared/theme-switching.js"/>
   <style>
     html, body, #tooltip-iframe-container {
       margin: 0;
       padding: 0;
       width: 100%;
       height: 100%;
       overflow: hidden;
+      color: var(--theme-body-color);
     }
   </style>
 </head>
 <body role="application" class="theme-body">
   <div id="tooltip-iframe-container"></div>
 </body>
 </html>
--- a/devtools/client/themes/common.css
+++ b/devtools/client/themes/common.css
@@ -241,20 +241,98 @@
   background-position: 0 0, 10px 10px;
 }
 
 .devtools-tooltip-iframe {
   border: none;
   background: transparent;
 }
 
-.devtools-htmltooltip-container {
+.tooltip-container {
   display: none;
   position: fixed;
   z-index: 9999;
+  display: none;
+  background: transparent;
+}
+
+.tooltip-panel{
+  background-color: var(--theme-tooltip-background);
+}
+
+.tooltip-visible {
+  display: block;
+}
+
+.tooltip-container[type="normal"] > .tooltip-panel {
+  height: 100%;
+  width: 100%;
+}
+
+/* Tooltip : arrow style */
+
+.tooltip-container[type="arrow"] {
+  filter: drop-shadow(0 3px 4px var(--theme-tooltip-shadow));
+}
+
+.tooltip-container[type="arrow"] > .tooltip-panel {
+  position: absolute;
+  box-sizing: border-box;
+  height: calc(100% - 13px);
+  width: 100%;
+
+  border: 3px solid var(--theme-tooltip-border);
+  border-radius: 5px;
+}
+
+.tooltip-top[type="arrow"] .tooltip-panel {
+  top: 0;
+}
+
+.tooltip-bottom[type="arrow"] .tooltip-panel {
+  bottom: 0;
+}
+
+.tooltip-arrow {
+  position: absolute;
+  height: 16px;
+  width: 32px;
+  overflow: hidden;
+}
+
+.tooltip-top .tooltip-arrow {
+  bottom: 0;
+}
+
+.tooltip-bottom .tooltip-arrow {
+  top: 0;
+}
+
+.tooltip-arrow:before {
+  content: "";
+  position: absolute;
+  width: 21px;
+  height: 21px;
+  margin-left: 4px;
+  background: linear-gradient(-45deg,
+    var(--theme-tooltip-background) 50%, transparent 50%);
+  border-color: var(--theme-tooltip-border);
+  border-style: solid;
+  border-width: 0px 3px 3px 0px;
+  border-radius: 3px;
+}
+
+.tooltip-bottom .tooltip-arrow:before {
+  margin-top: 4px;
+  transform: rotate(225deg);
+}
+
+.tooltip-top .tooltip-arrow:before {
+  margin-top: -12px;
+  transform: rotate(45deg);
 }
 
 /* links to source code, like displaying `myfile.js:45` */
 
 .devtools-source-link {
   font-family: var(--monospace-font-family);
   color: var(--theme-highlight-blue);
   cursor: pointer;
--- a/devtools/client/themes/variables.css
+++ b/devtools/client/themes/variables.css
@@ -58,16 +58,21 @@
   --theme-graphs-red: #e57180;
   --theme-graphs-grey: #cccccc;
   --theme-graphs-full-red: #f00;
   --theme-graphs-full-blue: #00f;
 
   /* Images */
   --theme-pane-collapse-image: url(chrome://devtools/skin/images/pane-collapse.svg);
   --theme-pane-expand-image: url(chrome://devtools/skin/images/pane-expand.svg);
+
+  /* Tooltips */
+  --theme-tooltip-border: #d9e1e8;
+  --theme-tooltip-background: rgba(255, 255, 255, .9);
+  --theme-tooltip-shadow: rgba(155, 155, 155, 0.26);
 }
 
 :root.theme-dark {
   --theme-body-background: #393f4c;
   --theme-sidebar-background: #393f4c;
   --theme-contrast-background: #ffb35b;
 
   --theme-tab-toolbar-background: #272b35;
@@ -109,16 +114,21 @@
   --theme-graphs-red: #eb5368;
   --theme-graphs-grey: #757873;
   --theme-graphs-full-red: #f00;
   --theme-graphs-full-blue: #00f;
 
   /* Images */
   --theme-pane-collapse-image: url(chrome://devtools/skin/images/pane-collapse.svg);
   --theme-pane-expand-image: url(chrome://devtools/skin/images/pane-expand.svg);
+
+  /* Tooltips */
+  --theme-tooltip-border: #434850;
+  --theme-tooltip-background: rgba(19, 28, 38, .9);
+  --theme-tooltip-shadow: rgba(25, 25, 25, 0.76);
 }
 
 :root.theme-firebug {
   --theme-body-background: #fcfcfc;
   --theme-sidebar-background: #fcfcfc;
   --theme-contrast-background: #e6b064;
 
   --theme-tab-toolbar-background: #ebeced;
--- a/devtools/server/actors/styleeditor.js
+++ b/devtools/server/actors/styleeditor.js
@@ -27,251 +27,16 @@ var TRANSITION_RULE = "\
 transition-duration: " + TRANSITION_DURATION_MS + "ms !important; \
 transition-delay: 0ms !important;\
 transition-timing-function: ease-out !important;\
 transition-property: all !important;\
 }";
 
 var LOAD_ERROR = "error-load";
 
-types.addActorType("old-stylesheet");
-
-/**
- * Creates a StyleEditorActor. StyleEditorActor provides remote access to the
- * stylesheets of a document.
- */
-var StyleEditorActor = exports.StyleEditorActor = protocol.ActorClass({
-  typeName: "styleeditor",
-
-  /**
-   * The window we work with, taken from the parent actor.
-   */
-  get window() {
-    return this.parentActor.window;
-  },
-
-  /**
-   * The current content document of the window we work with.
-   */
-  get document() {
-    return this.window.document;
-  },
-
-  events: {
-    "document-load" : {
-      type: "documentLoad",
-      styleSheets: Arg(0, "array:old-stylesheet")
-    }
-  },
-
-  form: function ()
-  {
-    return { actor: this.actorID };
-  },
-
-  initialize: function (conn, tabActor) {
-    protocol.Actor.prototype.initialize.call(this, null);
-
-    this.parentActor = tabActor;
-
-    // keep a map of sheets-to-actors so we don't create two actors for one sheet
-    this._sheets = new Map();
-  },
-
-  /**
-   * Destroy the current StyleEditorActor instance.
-   */
-  destroy: function ()
-  {
-    this._sheets.clear();
-  },
-
-  /**
-   * Called by client when target navigates to a new document.
-   * Adds load listeners to document.
-   */
-  newDocument: method(function () {
-    // delete previous document's actors
-    this._clearStyleSheetActors();
-
-    // Note: listening for load won't be necessary once
-    // https://bugzilla.mozilla.org/show_bug.cgi?id=839103 is fixed
-    if (this.document.readyState == "complete") {
-      this._onDocumentLoaded();
-    }
-    else {
-      this.window.addEventListener("load", this._onDocumentLoaded, false);
-    }
-    return {};
-  }),
-
-  /**
-   * Event handler for document loaded event. Add actor for each stylesheet
-   * and send an event notifying of the load
-   */
-  _onDocumentLoaded: function (event) {
-    if (event) {
-      this.window.removeEventListener("load", this._onDocumentLoaded, false);
-    }
-
-    let documents = [this.document];
-    var forms = [];
-    for (let doc of documents) {
-      let sheetForms = this._addStyleSheets(doc.styleSheets);
-      forms = forms.concat(sheetForms);
-      // Recursively handle style sheets of the documents in iframes.
-      for (let iframe of doc.getElementsByTagName("iframe")) {
-        documents.push(iframe.contentDocument);
-      }
-    }
-
-    events.emit(this, "document-load", forms);
-  },
-
-  /**
-   * Add all the stylesheets to the map and create an actor for each one
-   * if not already created. Send event that there are new stylesheets.
-   *
-   * @param {[DOMStyleSheet]} styleSheets
-   *        Stylesheets to add
-   * @return {[object]}
-   *         Array of actors for each StyleSheetActor created
-   */
-  _addStyleSheets: function (styleSheets)
-  {
-    let sheets = [];
-    for (let i = 0; i < styleSheets.length; i++) {
-      let styleSheet = styleSheets[i];
-      sheets.push(styleSheet);
-
-      // Get all sheets, including imported ones
-      let imports = this._getImported(styleSheet);
-      sheets = sheets.concat(imports);
-    }
-    let actors = sheets.map(this._createStyleSheetActor.bind(this));
-
-    return actors;
-  },
-
-  /**
-   * Create a new actor for a style sheet, if it hasn't already been created.
-   *
-   * @param  {DOMStyleSheet} styleSheet
-   *         The style sheet to create an actor for.
-   * @return {StyleSheetActor}
-   *         The actor for this style sheet
-   */
-  _createStyleSheetActor: function (styleSheet)
-  {
-    if (this._sheets.has(styleSheet)) {
-      return this._sheets.get(styleSheet);
-    }
-    let actor = new OldStyleSheetActor(styleSheet, this);
-
-    this.manage(actor);
-    this._sheets.set(styleSheet, actor);
-
-    return actor;
-  },
-
-  /**
-   * Get all the stylesheets @imported from a stylesheet.
-   *
-   * @param  {DOMStyleSheet} styleSheet
-   *         Style sheet to search
-   * @return {array}
-   *         All the imported stylesheets
-   */
-  _getImported: function (styleSheet) {
-    let imported = [];
-
-    for (let i = 0; i < styleSheet.cssRules.length; i++) {
-      let rule = styleSheet.cssRules[i];
-      if (rule.type == Ci.nsIDOMCSSRule.IMPORT_RULE) {
-        // Associated styleSheet may be null if it has already been seen due to
-        // duplicate @imports for the same URL.
-        if (!rule.styleSheet) {
-          continue;
-        }
-        imported.push(rule.styleSheet);
-
-        // recurse imports in this stylesheet as well
-        imported = imported.concat(this._getImported(rule.styleSheet));
-      }
-      else if (rule.type != Ci.nsIDOMCSSRule.CHARSET_RULE) {
-        // @import rules must precede all others except @charset
-        break;
-      }
-    }
-    return imported;
-  },
-
-  /**
-   * Clear all the current stylesheet actors in map.
-   */
-  _clearStyleSheetActors: function () {
-    for (let actor in this._sheets) {
-      this.unmanage(this._sheets[actor]);
-    }
-    this._sheets.clear();
-  },
-
-  /**
-   * Create a new style sheet in the document with the given text.
-   * Return an actor for it.
-   *
-   * @param  {object} request
-   *         Debugging protocol request object, with 'text property'
-   * @return {object}
-   *         Object with 'styelSheet' property for form on new actor.
-   */
-  newStyleSheet: method(function (text) {
-    let parent = this.document.documentElement;
-    let style = this.document.createElementNS("http://www.w3.org/1999/xhtml", "style");
-    style.setAttribute("type", "text/css");
-
-    if (text) {
-      style.appendChild(this.document.createTextNode(text));
-    }
-    parent.appendChild(style);
-
-    let actor = this._createStyleSheetActor(style.sheet);
-    return actor;
-  }, {
-    request: { text: Arg(0, "string") },
-    response: { styleSheet: RetVal("old-stylesheet") }
-  })
-});
-
-/**
- * The corresponding Front object for the StyleEditorActor.
- */
-var StyleEditorFront = protocol.FrontClass(StyleEditorActor, {
-  initialize: function (client, tabForm) {
-    protocol.Front.prototype.initialize.call(this, client);
-    this.actorID = tabForm.styleEditorActor;
-    this.manage(this);
-  },
-
-  getStyleSheets: function () {
-    let deferred = promise.defer();
-
-    events.once(this, "document-load", (styleSheets) => {
-      deferred.resolve(styleSheets);
-    });
-    this.newDocument();
-
-    return deferred.promise;
-  },
-
-  addStyleSheet: function (text) {
-    return this.newStyleSheet(text);
-  }
-});
-
 /**
  * A StyleSheetActor represents a stylesheet on the server.
  */
 var OldStyleSheetActor = protocol.ActorClass({
   typeName: "old-stylesheet",
 
   events: {
     "property-change" : {
@@ -645,27 +410,492 @@ var OldStyleSheetFront = protocol.FrontC
   get styleSheetIndex() {
     return this._form.styleSheetIndex;
   },
   get ruleCount() {
     return this._form.ruleCount;
   }
 });
 
+/**
+ * Creates a StyleEditorActor. StyleEditorActor provides remote access to the
+ * stylesheets of a document.
+ */
+var StyleEditorActor = exports.StyleEditorActor = protocol.ActorClass({
+  typeName: "styleeditor",
+
+  /**
+   * The window we work with, taken from the parent actor.
+   */
+  get window() {
+    return this.parentActor.window;
+  },
+
+  /**
+   * The current content document of the window we work with.
+   */
+  get document() {
+    return this.window.document;
+  },
+
+  events: {
+    "document-load" : {
+      type: "documentLoad",
+      styleSheets: Arg(0, "array:old-stylesheet")
+    }
+  },
+
+  form: function ()
+  {
+    return { actor: this.actorID };
+  },
+
+  initialize: function (conn, tabActor) {
+    protocol.Actor.prototype.initialize.call(this, null);
+
+    this.parentActor = tabActor;
+
+    // keep a map of sheets-to-actors so we don't create two actors for one sheet
+    this._sheets = new Map();
+  },
+
+  /**
+   * Destroy the current StyleEditorActor instance.
+   */
+  destroy: function ()
+  {
+    this._sheets.clear();
+  },
+
+  /**
+   * Called by client when target navigates to a new document.
+   * Adds load listeners to document.
+   */
+  newDocument: method(function () {
+    // delete previous document's actors
+    this._clearStyleSheetActors();
+
+    // Note: listening for load won't be necessary once
+    // https://bugzilla.mozilla.org/show_bug.cgi?id=839103 is fixed
+    if (this.document.readyState == "complete") {
+      this._onDocumentLoaded();
+    }
+    else {
+      this.window.addEventListener("load", this._onDocumentLoaded, false);
+    }
+    return {};
+  }),
+
+  /**
+   * Event handler for document loaded event. Add actor for each stylesheet
+   * and send an event notifying of the load
+   */
+  _onDocumentLoaded: function (event) {
+    if (event) {
+      this.window.removeEventListener("load", this._onDocumentLoaded, false);
+    }
+
+    let documents = [this.document];
+    var forms = [];
+    for (let doc of documents) {
+      let sheetForms = this._addStyleSheets(doc.styleSheets);
+      forms = forms.concat(sheetForms);
+      // Recursively handle style sheets of the documents in iframes.
+      for (let iframe of doc.getElementsByTagName("iframe")) {
+        documents.push(iframe.contentDocument);
+      }
+    }
+
+    events.emit(this, "document-load", forms);
+  },
+
+  /**
+   * Add all the stylesheets to the map and create an actor for each one
+   * if not already created. Send event that there are new stylesheets.
+   *
+   * @param {[DOMStyleSheet]} styleSheets
+   *        Stylesheets to add
+   * @return {[object]}
+   *         Array of actors for each StyleSheetActor created
+   */
+  _addStyleSheets: function (styleSheets)
+  {
+    let sheets = [];
+    for (let i = 0; i < styleSheets.length; i++) {
+      let styleSheet = styleSheets[i];
+      sheets.push(styleSheet);
+
+      // Get all sheets, including imported ones
+      let imports = this._getImported(styleSheet);
+      sheets = sheets.concat(imports);
+    }
+    let actors = sheets.map(this._createStyleSheetActor.bind(this));
+
+    return actors;
+  },
+
+  /**
+   * Create a new actor for a style sheet, if it hasn't already been created.
+   *
+   * @param  {DOMStyleSheet} styleSheet
+   *         The style sheet to create an actor for.
+   * @return {StyleSheetActor}
+   *         The actor for this style sheet
+   */
+  _createStyleSheetActor: function (styleSheet)
+  {
+    if (this._sheets.has(styleSheet)) {
+      return this._sheets.get(styleSheet);
+    }
+    let actor = new OldStyleSheetActor(styleSheet, this);
+
+    this.manage(actor);
+    this._sheets.set(styleSheet, actor);
+
+    return actor;
+  },
+
+  /**
+   * Get all the stylesheets @imported from a stylesheet.
+   *
+   * @param  {DOMStyleSheet} styleSheet
+   *         Style sheet to search
+   * @return {array}
+   *         All the imported stylesheets
+   */
+  _getImported: function (styleSheet) {
+    let imported = [];
+
+    for (let i = 0; i < styleSheet.cssRules.length; i++) {
+      let rule = styleSheet.cssRules[i];
+      if (rule.type == Ci.nsIDOMCSSRule.IMPORT_RULE) {
+        // Associated styleSheet may be null if it has already been seen due to
+        // duplicate @imports for the same URL.
+        if (!rule.styleSheet) {
+          continue;
+        }
+        imported.push(rule.styleSheet);
+
+        // recurse imports in this stylesheet as well
+        imported = imported.concat(this._getImported(rule.styleSheet));
+      }
+      else if (rule.type != Ci.nsIDOMCSSRule.CHARSET_RULE) {
+        // @import rules must precede all others except @charset
+        break;
+      }
+    }
+    return imported;
+  },
+
+  /**
+   * Clear all the current stylesheet actors in map.
+   */
+  _clearStyleSheetActors: function () {
+    for (let actor in this._sheets) {
+      this.unmanage(this._sheets[actor]);
+    }
+    this._sheets.clear();
+  },
+
+  /**
+   * Create a new style sheet in the document with the given text.
+   * Return an actor for it.
+   *
+   * @param  {object} request
+   *         Debugging protocol request object, with 'text property'
+   * @return {object}
+   *         Object with 'styelSheet' property for form on new actor.
+   */
+  newStyleSheet: method(function (text) {
+    let parent = this.document.documentElement;
+    let style = this.document.createElementNS("http://www.w3.org/1999/xhtml", "style");
+    style.setAttribute("type", "text/css");
+
+    if (text) {
+      style.appendChild(this.document.createTextNode(text));
+    }
+    parent.appendChild(style);
+
+    let actor = this._createStyleSheetActor(style.sheet);
+    return actor;
+  }, {
+    request: { text: Arg(0, "string") },
+    response: { styleSheet: RetVal("old-stylesheet") }
+  })
+});
+
+/**
+ * The corresponding Front object for the StyleEditorActor.
+ */
+var StyleEditorFront = protocol.FrontClass(StyleEditorActor, {
+  initialize: function (client, tabForm) {
+    protocol.Front.prototype.initialize.call(this, client);
+    this.actorID = tabForm.styleEditorActor;
+    this.manage(this);
+  },
+
+  getStyleSheets: function () {
+    let deferred = promise.defer();
+
+    events.once(this, "document-load", (styleSheets) => {
+      deferred.resolve(styleSheets);
+    });
+    this.newDocument();
+
+    return deferred.promise;
+  },
+
+  addStyleSheet: function (text) {
+    return this.newStyleSheet(text);
+  }
+});
+
+/**
+ * Creates a StyleEditorActor. StyleEditorActor provides remote access to the
+ * stylesheets of a document.
+ */
+var StyleEditorActor = exports.StyleEditorActor = protocol.ActorClass({
+  typeName: "styleeditor",
+
+  /**
+   * The window we work with, taken from the parent actor.
+   */
+  get window() {
+    return this.parentActor.window;
+  },
+
+  /**
+   * The current content document of the window we work with.
+   */
+  get document() {
+    return this.window.document;
+  },
+
+  events: {
+    "document-load" : {
+      type: "documentLoad",
+      styleSheets: Arg(0, "array:old-stylesheet")
+    }
+  },
+
+  form: function ()
+  {
+    return { actor: this.actorID };
+  },
+
+  initialize: function (conn, tabActor) {
+    protocol.Actor.prototype.initialize.call(this, null);
+
+    this.parentActor = tabActor;
+
+    // keep a map of sheets-to-actors so we don't create two actors for one sheet
+    this._sheets = new Map();
+  },
+
+  /**
+   * Destroy the current StyleEditorActor instance.
+   */
+  destroy: function ()
+  {
+    this._sheets.clear();
+  },
+
+  /**
+   * Called by client when target navigates to a new document.
+   * Adds load listeners to document.
+   */
+  newDocument: method(function () {
+    // delete previous document's actors
+    this._clearStyleSheetActors();
+
+    // Note: listening for load won't be necessary once
+    // https://bugzilla.mozilla.org/show_bug.cgi?id=839103 is fixed
+    if (this.document.readyState == "complete") {
+      this._onDocumentLoaded();
+    }
+    else {
+      this.window.addEventListener("load", this._onDocumentLoaded, false);
+    }
+    return {};
+  }),
+
+  /**
+   * Event handler for document loaded event. Add actor for each stylesheet
+   * and send an event notifying of the load
+   */
+  _onDocumentLoaded: function (event) {
+    if (event) {
+      this.window.removeEventListener("load", this._onDocumentLoaded, false);
+    }
+
+    let documents = [this.document];
+    var forms = [];
+    for (let doc of documents) {
+      let sheetForms = this._addStyleSheets(doc.styleSheets);
+      forms = forms.concat(sheetForms);
+      // Recursively handle style sheets of the documents in iframes.
+      for (let iframe of doc.getElementsByTagName("iframe")) {
+        documents.push(iframe.contentDocument);
+      }
+    }
+
+    events.emit(this, "document-load", forms);
+  },
+
+  /**
+   * Add all the stylesheets to the map and create an actor for each one
+   * if not already created. Send event that there are new stylesheets.
+   *
+   * @param {[DOMStyleSheet]} styleSheets
+   *        Stylesheets to add
+   * @return {[object]}
+   *         Array of actors for each StyleSheetActor created
+   */
+  _addStyleSheets: function (styleSheets)
+  {
+    let sheets = [];
+    for (let i = 0; i < styleSheets.length; i++) {
+      let styleSheet = styleSheets[i];
+      sheets.push(styleSheet);
+
+      // Get all sheets, including imported ones
+      let imports = this._getImported(styleSheet);
+      sheets = sheets.concat(imports);
+    }
+    let actors = sheets.map(this._createStyleSheetActor.bind(this));
+
+    return actors;
+  },
+
+  /**
+   * Create a new actor for a style sheet, if it hasn't already been created.
+   *
+   * @param  {DOMStyleSheet} styleSheet
+   *         The style sheet to create an actor for.
+   * @return {StyleSheetActor}
+   *         The actor for this style sheet
+   */
+  _createStyleSheetActor: function (styleSheet)
+  {
+    if (this._sheets.has(styleSheet)) {
+      return this._sheets.get(styleSheet);
+    }
+    let actor = new OldStyleSheetActor(styleSheet, this);
+
+    this.manage(actor);
+    this._sheets.set(styleSheet, actor);
+
+    return actor;
+  },
+
+  /**
+   * Get all the stylesheets @imported from a stylesheet.
+   *
+   * @param  {DOMStyleSheet} styleSheet
+   *         Style sheet to search
+   * @return {array}
+   *         All the imported stylesheets
+   */
+  _getImported: function (styleSheet) {
+   let imported = [];
+
+   for (let i = 0; i < styleSheet.cssRules.length; i++) {
+      let rule = styleSheet.cssRules[i];
+      if (rule.type == Ci.nsIDOMCSSRule.IMPORT_RULE) {
+        // Associated styleSheet may be null if it has already been seen due to
+        // duplicate @imports for the same URL.
+        if (!rule.styleSheet) {
+          continue;
+        }
+        imported.push(rule.styleSheet);
+
+        // recurse imports in this stylesheet as well
+        imported = imported.concat(this._getImported(rule.styleSheet));
+      }
+      else if (rule.type != Ci.nsIDOMCSSRule.CHARSET_RULE) {
+        // @import rules must precede all others except @charset
+        break;
+      }
+    }
+    return imported;
+  },
+
+  /**
+   * Clear all the current stylesheet actors in map.
+   */
+  _clearStyleSheetActors: function () {
+    for (let actor in this._sheets) {
+      this.unmanage(this._sheets[actor]);
+    }
+    this._sheets.clear();
+  },
+
+  /**
+   * Create a new style sheet in the document with the given text.
+   * Return an actor for it.
+   *
+   * @param  {object} request
+   *         Debugging protocol request object, with 'text property'
+   * @return {object}
+   *         Object with 'styelSheet' property for form on new actor.
+   */
+  newStyleSheet: method(function (text) {
+    let parent = this.document.documentElement;
+    let style = this.document.createElementNS("http://www.w3.org/1999/xhtml", "style");
+    style.setAttribute("type", "text/css");
+
+    if (text) {
+      style.appendChild(this.document.createTextNode(text));
+    }
+    parent.appendChild(style);
+
+    let actor = this._createStyleSheetActor(style.sheet);
+    return actor;
+  }, {
+    request: { text: Arg(0, "string") },
+    response: { styleSheet: RetVal("old-stylesheet") }
+  })
+});
+
+/**
+ * The corresponding Front object for the StyleEditorActor.
+ */
+var StyleEditorFront = protocol.FrontClass(StyleEditorActor, {
+  initialize: function (client, tabForm) {
+    protocol.Front.prototype.initialize.call(this, client);
+    this.actorID = tabForm.styleEditorActor;
+    this.manage(this);
+  },
+
+  getStyleSheets: function () {
+    let deferred = promise.defer();
+
+    events.once(this, "document-load", (styleSheets) => {
+      deferred.resolve(styleSheets);
+    });
+    this.newDocument();
+
+    return deferred.promise;
+  },
+
+  addStyleSheet: function (text) {
+    return this.newStyleSheet(text);
+  }
+});
+
 XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () {
   return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
 });
 
 exports.StyleEditorActor = StyleEditorActor;
 exports.StyleEditorFront = StyleEditorFront;
 
 exports.OldStyleSheetActor = OldStyleSheetActor;
 exports.OldStyleSheetFront = OldStyleSheetFront;
 
-
 /**
  * Normalize multiple relative paths towards the base paths on the right.
  */
 function normalize(...aURLs) {
   let base = Services.io.newURI(aURLs.pop(), null, null);
   let url;
   while ((url = aURLs.pop())) {
     base = Services.io.newURI(url, null, base);
--- a/mobile/android/app/build.gradle
+++ b/mobile/android/app/build.gradle
@@ -1,17 +1,17 @@
 buildDir "${topobjdir}/gradle/build/mobile/android/app"
 
 apply plugin: 'android-sdk-manager' // Must come before 'com.android.*'.
 apply plugin: 'com.android.application'
 apply plugin: 'checkstyle'
 
 android {
     compileSdkVersion 23
-    buildToolsVersion "23.0.1"
+    buildToolsVersion mozconfig.substs.ANDROID_BUILD_TOOLS_VERSION
 
     defaultConfig {
         targetSdkVersion 23
         minSdkVersion 15 
         applicationId mozconfig.substs.ANDROID_PACKAGE_NAME
         testApplicationId 'org.mozilla.roboexample.test'
         testInstrumentationRunner 'org.mozilla.gecko.FennecInstrumentationTestRunner'
         manifestPlaceholders = [
@@ -166,40 +166,40 @@ android {
             // we have tests that start test servers and the bound ports
             // collide.  We'll fix this soon to have much faster test cycles.
             maxParallelForks 1
         }
     }
 }
 
 dependencies {
-    compile 'com.android.support:support-v4:23.0.1'
-    compile 'com.android.support:appcompat-v7:23.0.1'
-    compile 'com.android.support:cardview-v7:23.0.1'
-    compile 'com.android.support:recyclerview-v7:23.0.1'
-    compile 'com.android.support:design:23.0.1'
+    compile "com.android.support:support-v4:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
+    compile "com.android.support:appcompat-v7:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
+    compile "com.android.support:cardview-v7:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
+    compile "com.android.support:recyclerview-v7:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
+    compile "com.android.support:design:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
 
     if (mozconfig.substs.MOZ_NATIVE_DEVICES) {
-        compile 'com.android.support:mediarouter-v7:23.0.1'
-        compile 'com.google.android.gms:play-services-basement:8.1.0'
-        compile 'com.google.android.gms:play-services-base:8.1.0'
-        compile 'com.google.android.gms:play-services-cast:8.1.0'
+        compile "com.android.support:mediarouter-v7:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
+        compile "com.google.android.gms:play-services-basement:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
+        compile "com.google.android.gms:play-services-base:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
+        compile "com.google.android.gms:play-services-cast:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
     }
 
     if (mozconfig.substs.MOZ_INSTALL_TRACKING) {
-        compile 'com.google.android.gms:play-services-ads:8.1.0'
-        compile 'com.google.android.gms:play-services-analytics:8.1.0'
-        compile 'com.google.android.gms:play-services-appindexing:8.1.0'
-        compile 'com.google.android.gms:play-services-basement:8.1.0'
+        compile "com.google.android.gms:play-services-ads:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
+        compile "com.google.android.gms:play-services-analytics:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
+        compile "com.google.android.gms:play-services-appindexing:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
+        compile "com.google.android.gms:play-services-basement:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
     }
 
     if (mozconfig.substs.MOZ_ANDROID_GCM) {
-        compile 'com.google.android.gms:play-services-basement:8.1.0'
-        compile 'com.google.android.gms:play-services-base:8.1.0'
-        compile 'com.google.android.gms:play-services-gcm:8.1.0'
+        compile "com.google.android.gms:play-services-basement:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
+        compile "com.google.android.gms:play-services-base:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
+        compile "com.google.android.gms:play-services-gcm:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
     }
 
     // Gradle based builds include LeakCanary.  Gradle based tests include the no-op version.  Mach
     // based builds only include the no-op version of this library.
     compile 'com.squareup.leakcanary:leakcanary-android:1.4-beta1'
     testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
 
     compile project(':thirdparty')
--- a/mobile/android/bouncer/build.gradle
+++ b/mobile/android/bouncer/build.gradle
@@ -1,15 +1,15 @@
 buildDir "${topobjdir}/gradle/build/mobile/android/bouncer"
 
 apply plugin: 'com.android.application'
 
 android {
     compileSdkVersion 23
-    buildToolsVersion "23.0.1"
+    buildToolsVersion mozconfig.substs.ANDROID_BUILD_TOOLS_VERSION
 
     defaultConfig {
         targetSdkVersion 23
         minSdkVersion 15 
         applicationId mozconfig.substs.ANDROID_PACKAGE_NAME
     }
 
     compileOptions {
--- a/mobile/android/thirdparty/build.gradle
+++ b/mobile/android/thirdparty/build.gradle
@@ -1,15 +1,15 @@
 buildDir "${topobjdir}/gradle/build/mobile/android/thirdparty"
 
 apply plugin: 'com.android.library'
 
 android {
     compileSdkVersion 23
-    buildToolsVersion "23.0.1"
+    buildToolsVersion mozconfig.substs.ANDROID_BUILD_TOOLS_VERSION
 
     defaultConfig {
         targetSdkVersion 23
         minSdkVersion 15
     }
 
     compileOptions {
         sourceCompatibility JavaVersion.VERSION_1_7
@@ -33,17 +33,17 @@ android {
                 // here is only the no-op library for mach-based builds.
                 exclude 'com/squareup/leakcanary/**'
             }
         }
     }
 }
 
 dependencies {
-    compile 'com.android.support:support-v4:23.0.1'
+    compile "com.android.support:support-v4:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
 }
 
 apply plugin: 'idea'
 
 idea {
     module {
         // This is cosmetic.  See the excludes in the root project.
         if (!mozconfig.substs.MOZ_INSTALL_TRACKING) {
--- a/toolkit/components/console/jsconsole-clhandler.manifest
+++ b/toolkit/components/console/jsconsole-clhandler.manifest
@@ -1,3 +1,3 @@
 component {2cd0c310-e127-44d0-88fc-4435c9ab4d4b} jsconsole-clhandler.js
 contract @mozilla.org/toolkit/console-clh;1 {2cd0c310-e127-44d0-88fc-4435c9ab4d4b}
-category command-line-handler b-jsconsole @mozilla.org/toolkit/console-clh;1
+category command-line-handler t-jsconsole @mozilla.org/toolkit/console-clh;1