Bug 1119133 - Add a keyboard shortcut to toggle devtools docking mode between last two positions;r=pbrosset
authorBrian Grinstead <bgrinstead@mozilla.com>
Tue, 09 Jun 2015 14:59:27 -0700
changeset 249079 547e848ba27a83023d594471f9c7b96632c9d96d
parent 249078 dabc93a5eff16053e7ea5759ac5cfd52e2f6b1fd
child 249080 12ab70b3668555fbea5037906733a1fc1bd496f8
push id61139
push usercbook@mozilla.com
push dateTue, 16 Jun 2015 14:53:44 +0000
treeherdermozilla-inbound@7299b8b5a8a1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspbrosset
bugs1119133
milestone41.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1119133 - Add a keyboard shortcut to toggle devtools docking mode between last two positions;r=pbrosset
browser/app/profile/firefox.js
browser/devtools/framework/test/browser.ini
browser/devtools/framework/test/browser_keybindings_03.js
browser/devtools/framework/test/browser_toolbox_hosts.js
browser/devtools/framework/test/browser_toolbox_window_reload_target.js
browser/devtools/framework/test/head.js
browser/devtools/framework/test/shared-head.js
browser/devtools/framework/toolbox.js
browser/devtools/framework/toolbox.xul
browser/locales/en-US/chrome/browser/devtools/toolbox.dtd
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1373,16 +1373,17 @@ pref("devtools.appmanager.manifestEditor
 
 // Enable DevTools WebIDE by default
 pref("devtools.webide.enabled", true);
 
 // Toolbox preferences
 pref("devtools.toolbox.footer.height", 250);
 pref("devtools.toolbox.sidebar.width", 500);
 pref("devtools.toolbox.host", "bottom");
+pref("devtools.toolbox.previousHost", "side");
 pref("devtools.toolbox.selectedTool", "webconsole");
 pref("devtools.toolbox.toolbarSpec", '["splitconsole", "paintflashing toggle","tilt toggle","scratchpad","resize toggle","eyedropper","screenshot --fullpage", "rulers"]');
 pref("devtools.toolbox.sideEnabled", true);
 pref("devtools.toolbox.zoomValue", "1");
 pref("devtools.toolbox.splitconsoleEnabled", false);
 pref("devtools.toolbox.splitconsoleHeight", 100);
 
 // Toolbox Button preferences
--- a/browser/devtools/framework/test/browser.ini
+++ b/browser/devtools/framework/test/browser.ini
@@ -17,16 +17,17 @@ support-files =
   serviceworker.js
 
 [browser_devtools_api.js]
 [browser_devtools_api_destroy.js]
 [browser_dynamic_tool_enabling.js]
 [browser_ignore_toolbox_network_requests.js]
 [browser_keybindings_01.js]
 [browser_keybindings_02.js]
+[browser_keybindings_03.js]
 [browser_new_activation_workflow.js]
 [browser_target_events.js]
 [browser_target_remote.js]
 [browser_target_support.js]
 [browser_two_tabs.js]
 [browser_toolbox_dynamic_registration.js]
 [browser_toolbox_getpanelwhenready.js]
 [browser_toolbox_highlight.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/framework/test/browser_keybindings_03.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"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";
+
+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 {SIDE, BOTTOM, WINDOW} = devtools.Toolbox.HostType;
+  checkHostType(toolbox, BOTTOM, SIDE);
+
+  info ("Switching from bottom to side");
+  synthesizeKeyElement(keyElement);
+  yield toolbox.once("host-changed");
+  checkHostType(toolbox, SIDE, BOTTOM);
+
+  info ("Switching from side to bottom");
+  synthesizeKeyElement(keyElement);
+  yield toolbox.once("host-changed");
+  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");
+  checkHostType(toolbox, BOTTOM, WINDOW);
+
+  yield toolbox.destroy();
+  gBrowser.removeCurrentTab();
+});
--- a/browser/devtools/framework/test/browser_toolbox_hosts.js
+++ b/browser/devtools/framework/test/browser_toolbox_hosts.js
@@ -1,130 +1,137 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-let temp = {}
-Cu.import("resource:///modules/devtools/gDevTools.jsm", temp);
-let DevTools = temp.DevTools;
+"use strict";
 
-Cu.import("resource://gre/modules/devtools/Loader.jsm", temp);
-let devtools = temp.devtools;
-
-let Toolbox = devtools.Toolbox;
-
+let {SIDE, BOTTOM, WINDOW} = devtools.Toolbox.HostType;
 let toolbox, target;
 
-function test()
-{
-  gBrowser.selectedTab = gBrowser.addTab();
-  target = TargetFactory.forTab(gBrowser.selectedTab);
+const URL = "data:text/html;charset=utf8,test for opening toolbox in different hosts";
+
+add_task(function* runTest() {
+  info("Create a test tab and open the toolbox");
+  let tab = yield addTab(URL);
+  target = TargetFactory.forTab(tab);
+  toolbox = yield gDevTools.showToolbox(target, "webconsole");
 
-  gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
-    gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
-    gDevTools.showToolbox(target)
-             .then(testBottomHost, console.error)
-             .then(null, console.error);
-  }, true);
+  yield testBottomHost();
+  yield testSidebarHost();
+  yield testWindowHost();
+  yield testToolSelect();
+  yield testDestroy();
+  yield testRememberHost();
+  yield testPreviousHost();
 
-  content.location = "data:text/html,test for opening toolbox in different hosts";
-}
+  yield toolbox.destroy();
 
-function testBottomHost(aToolbox)
-{
-  toolbox = aToolbox;
+  toolbox = target = null;
+  gBrowser.removeCurrentTab();
+});
 
-  checkHostType(Toolbox.HostType.BOTTOM);
+function* testBottomHost() {
+  checkHostType(toolbox, BOTTOM);
 
   // test UI presence
   let nbox = gBrowser.getNotificationBox();
   let iframe = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-bottom-iframe");
   ok(iframe, "toolbox bottom iframe exists");
 
   checkToolboxLoaded(iframe);
-
-  toolbox.switchHost(Toolbox.HostType.SIDE).then(testSidebarHost);
 }
 
-function testSidebarHost()
-{
-  checkHostType(Toolbox.HostType.SIDE);
+function* testSidebarHost() {
+  yield toolbox.switchHost(SIDE);
+  checkHostType(toolbox, SIDE);
 
   // test UI presence
   let nbox = gBrowser.getNotificationBox();
   let bottom = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-bottom-iframe");
   ok(!bottom, "toolbox bottom iframe doesn't exist");
 
   let iframe = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-side-iframe");
   ok(iframe, "toolbox side iframe exists");
 
   checkToolboxLoaded(iframe);
-
-  toolbox.switchHost(Toolbox.HostType.WINDOW).then(testWindowHost);
 }
 
-function testWindowHost()
-{
-  checkHostType(Toolbox.HostType.WINDOW);
+function* testWindowHost() {
+  yield toolbox.switchHost(WINDOW);
+  checkHostType(toolbox, WINDOW);
 
   let nbox = gBrowser.getNotificationBox();
   let sidebar = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-side-iframe");
   ok(!sidebar, "toolbox sidebar iframe doesn't exist");
 
   let win = Services.wm.getMostRecentWindow("devtools:toolbox");
   ok(win, "toolbox separate window exists");
 
   let iframe = win.document.getElementById("toolbox-iframe");
   checkToolboxLoaded(iframe);
-
-  testToolSelect();
 }
 
-function testToolSelect()
-{
+function* testToolSelect() {
   // make sure we can load a tool after switching hosts
-  toolbox.selectTool("inspector").then(testDestroy);
+  yield toolbox.selectTool("inspector");
 }
 
-function testDestroy()
-{
-  toolbox.destroy().then(function() {
-    target = TargetFactory.forTab(gBrowser.selectedTab);
-    gDevTools.showToolbox(target).then(testRememberHost);
-  });
+function* testDestroy() {
+  yield toolbox.destroy();
+  target = TargetFactory.forTab(gBrowser.selectedTab);
+  toolbox = yield gDevTools.showToolbox(target);
 }
 
-function testRememberHost(aToolbox)
-{
-  toolbox = aToolbox;
+function* testRememberHost() {
   // last host was the window - make sure it's the same when re-opening
-  is(toolbox.hostType, Toolbox.HostType.WINDOW, "host remembered");
+  is(toolbox.hostType, WINDOW, "host remembered");
 
   let win = Services.wm.getMostRecentWindow("devtools:toolbox");
   ok(win, "toolbox separate window exists");
-
-  cleanup();
 }
 
-function checkHostType(hostType)
-{
-  is(toolbox.hostType, hostType, "host type is " + hostType);
+function* testPreviousHost() {
+  // last host was the window - make sure it's the same when re-opening
+  is(toolbox.hostType, WINDOW, "host remembered");
+
+  info("Switching to side");
+  yield toolbox.switchHost(SIDE);
+  checkHostType(toolbox, SIDE, WINDOW);
+
+  info("Switching to bottom");
+  yield toolbox.switchHost(BOTTOM);
+  checkHostType(toolbox, BOTTOM, SIDE);
+
+  info("Switching from bottom to side");
+  yield toolbox.switchToPreviousHost();
+  checkHostType(toolbox, SIDE, BOTTOM);
+
+  info("Switching from side to bottom");
+  yield toolbox.switchToPreviousHost();
+  checkHostType(toolbox, BOTTOM, SIDE);
 
-  let pref = Services.prefs.getCharPref("devtools.toolbox.host");
-  is(pref, hostType, "host pref is " + hostType);
+  info("Switching to window");
+  yield toolbox.switchHost(WINDOW);
+  checkHostType(toolbox, WINDOW, BOTTOM);
+
+  info("Switching from window to bottom");
+  yield toolbox.switchToPreviousHost();
+  checkHostType(toolbox, BOTTOM, WINDOW);
+
+  info("Forcing the previous host to match the current (bottom)")
+  Services.prefs.setCharPref("devtools.toolbox.previousHost", BOTTOM);
+
+  info("Switching from bottom to side (since previous=current=bottom");
+  yield toolbox.switchToPreviousHost();
+  checkHostType(toolbox, SIDE, BOTTOM);
+
+  info("Forcing the previous host to match the current (side)")
+  Services.prefs.setCharPref("devtools.toolbox.previousHost", SIDE);
+  info("Switching from side to bottom (since previous=current=side");
+  yield toolbox.switchToPreviousHost();
+  checkHostType(toolbox, BOTTOM, SIDE);
 }
 
-function checkToolboxLoaded(iframe)
-{
+function checkToolboxLoaded(iframe) {
   let tabs = iframe.contentDocument.getElementById("toolbox-tabs");
   ok(tabs, "toolbox UI has been loaded into iframe");
 }
-
-function cleanup()
-{
-  Services.prefs.setCharPref("devtools.toolbox.host", Toolbox.HostType.BOTTOM);
-
-  toolbox.destroy().then(function() {
-    DevTools = Toolbox = toolbox = target = null;
-    gBrowser.removeCurrentTab();
-    finish();
-  });
- }
--- a/browser/devtools/framework/test/browser_toolbox_window_reload_target.js
+++ b/browser/devtools/framework/test/browser_toolbox_window_reload_target.js
@@ -63,35 +63,27 @@ function testAllTheTools(docked, callbac
             testAllTheTools(docked, callback, toolNum+1);
           });
         });
       });
     });
   });
 }
 
-function synthesizeKeyForToolbox(keyId) {
-  let el = toolbox.doc.getElementById(keyId);
-  let key = el.getAttribute("key") || el.getAttribute("keycode");
-  let mod = {};
-  el.getAttribute("modifiers").split(" ").forEach((m) => mod[m+"Key"] = true);
-  info("Synthesizing: key="+key+", mod="+JSON.stringify(mod));
-  EventUtils.synthesizeKey(key, mod, toolbox.doc.defaultView);
-}
-
 function testReload(key, 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;
   info("Testing reload in "+description);
-  synthesizeKeyForToolbox(key);
+  let el = toolbox.doc.getElementById(key);
+  synthesizeKeyElement(el);
   reloadsSent++;
 }
 
 function finishUp() {
   toolbox.destroy().then(() => {
     gBrowser.removeCurrentTab();
 
     target = toolbox = description = reloadsSent = toolIDs = null;
--- a/browser/devtools/framework/test/head.js
+++ b/browser/devtools/framework/test/head.js
@@ -104,8 +104,41 @@ function executeInContent(name, data={},
 
   mm.sendAsyncMessage(name, data, objects);
   if (expectResponse) {
     return waitForContentMessage(name);
   } else {
     return promise.resolve();
   }
 }
+
+/**
+ * Synthesize a keypress from a <key> element, taking into account
+ * any modifiers.
+ * @param {Element} el the <key> element to synthesize
+ */
+function synthesizeKeyElement(el) {
+  let key = el.getAttribute("key") || el.getAttribute("keycode");
+  let mod = {};
+  el.getAttribute("modifiers").split(" ").forEach((m) => mod[m+"Key"] = true);
+  info(`Synthesizing: key=${key}, mod=${JSON.stringify(mod)}`);
+  EventUtils.synthesizeKey(key, mod, el.ownerDocument.defaultView);
+}
+
+/* Check the toolbox host type and prefs to make sure they match the
+ * expected values
+ * @param {Toolbox}
+ * @param {HostType} hostType
+ *        One of {SIDE, BOTTOM, WINDOW} from devtools.Toolbox.HostType
+ * @param {HostType} Optional previousHostType
+ *        The host that will be switched to when calling switchToPreviousHost
+ */
+function checkHostType(toolbox, hostType, previousHostType) {
+  is(toolbox.hostType, hostType, "host type is " + hostType);
+
+  let pref = Services.prefs.getCharPref("devtools.toolbox.host");
+  is(pref, hostType, "host pref is " + hostType);
+
+  if (previousHostType) {
+    is (Services.prefs.getCharPref("devtools.toolbox.previousHost"),
+      previousHostType, "The previous host is correct");
+  }
+}
--- a/browser/devtools/framework/test/shared-head.js
+++ b/browser/devtools/framework/test/shared-head.js
@@ -25,19 +25,21 @@ function getFrameScript() {
   mm.loadFrameScript(frameURL, false);
   SimpleTest.registerCleanupFunction(() => {
     mm = null;
   });
   return mm;
 }
 
 gDevTools.testing = true;
-SimpleTest.registerCleanupFunction(() => {
+registerCleanupFunction(() => {
   gDevTools.testing = false;
   Services.prefs.clearUserPref("devtools.dump.emit");
+  Services.prefs.clearUserPref("devtools.toolbox.host");
+  Services.prefs.clearUserPref("devtools.toolbox.previousHost");
 });
 
 registerCleanupFunction(function cleanup() {
   while (gBrowser.tabs.length > 1) {
     gBrowser.removeCurrentTab();
   }
 });
 
--- a/browser/devtools/framework/toolbox.js
+++ b/browser/devtools/framework/toolbox.js
@@ -183,17 +183,18 @@ Toolbox.HostType = {
 };
 
 Toolbox.prototype = {
   _URL: "chrome://browser/content/devtools/framework/toolbox.xul",
 
   _prefs: {
     LAST_HOST: "devtools.toolbox.host",
     LAST_TOOL: "devtools.toolbox.selectedTool",
-    SIDE_ENABLED: "devtools.toolbox.sideEnabled"
+    SIDE_ENABLED: "devtools.toolbox.sideEnabled",
+    PREVIOUS_HOST: "devtools.toolbox.previousHost"
   },
 
   currentToolId: null,
 
   /**
    * Returns a *copy* of the _toolPanels collection.
    *
    * @return {Map} panels
@@ -492,22 +493,26 @@ Toolbox.prototype = {
         this.reloadTarget(force);
       }, true);
     });
   },
 
   _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);
 
     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);
   },
 
   _saveSplitConsoleHeight: function() {
@@ -1575,16 +1580,36 @@ Toolbox.prototype = {
 
     // clean up the toolbox if its window is closed
     let newHost = new Hosts[hostType](this.target.tab, options);
     newHost.on("window-closed", this.destroy);
     return newHost;
   },
 
   /**
+   * Switch to the last used host for the toolbox UI.
+   * This is determined by the devtools.toolbox.previousHost pref.
+   */
+  switchToPreviousHost: function() {
+    let hostType = Services.prefs.getCharPref(this._prefs.PREVIOUS_HOST);
+
+    // Handle the case where the previous host happens to match the current
+    // host. If so, switch to bottom if it's not already used, and side if not.
+    if (hostType === this._host.type) {
+      if (hostType === Toolbox.HostType.BOTTOM) {
+        hostType = Toolbox.HostType.SIDE;
+      } else {
+        hostType = Toolbox.HostType.BOTTOM;
+      }
+    }
+
+    return this.switchHost(hostType);
+  },
+
+  /**
    * Switch to a new host for the toolbox UI. E.g. bottom, sidebar, window,
    * and focus the window when done.
    *
    * @param {string} hostType
    *        The host type of the new host object
    */
   switchHost: function(hostType) {
     if (hostType == this._host.type || !this._target.isLocalTab) {
@@ -1602,20 +1627,22 @@ Toolbox.prototype = {
       // See bug 1022726, most probably because of swapFrameLoaders we need to
       // first focus the window here, and then once again further below to make
       // sure focus actually happens.
       this.frame.contentWindow.focus();
 
       this._host.off("window-closed", this.destroy);
       this.destroyHost();
 
+      let prevHostType = this._host.type;
       this._host = newHost;
 
       if (this.hostType != Toolbox.HostType.CUSTOM) {
         Services.prefs.setCharPref(this._prefs.LAST_HOST, this._host.type);
+        Services.prefs.setCharPref(this._prefs.PREVIOUS_HOST, prevHostType);
       }
 
       this._buildDockButtons();
       this._addKeysToWindow();
 
       // Focus the contentWindow to make sure keyboard shortcuts work straight
       // away.
       this.frame.contentWindow.focus();
--- a/browser/devtools/framework/toolbox.xul
+++ b/browser/devtools/framework/toolbox.xul
@@ -69,16 +69,20 @@
     <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>
 
   <popupset>
     <menupopup id="toolbox-textbox-context-popup">
       <menuitem id="cMenu_undo"/>
       <menuseparator/>
       <menuitem id="cMenu_cut"/>
       <menuitem id="cMenu_copy"/>
--- a/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd
@@ -19,16 +19,17 @@
 <!ENTITY toolboxZoomIn.key2            "="> <!-- + is above this key on many keyboards -->
 <!ENTITY toolboxZoomOut.key            "-">
 <!ENTITY toolboxZoomReset.key          "0">
 
 <!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 (browserToolboxErrorMessage): This is the label
   -  shown next to error details when the Browser Toolbox is unable to open. -->
 <!ENTITY browserToolboxErrorMessage          "Error opening Browser Toolbox:">