Bug 879799 - Browser console lacks keyboard access; r=robcee
authorMihai Sucan <mihai.sucan@gmail.com>
Fri, 21 Jun 2013 19:01:02 +0300
changeset 147653 6d5d454fc9b133c65f62ed02d11769fd60bf9290
parent 147652 c8bec69452510bdf8a342966b649570818b2a5c0
child 147654 2fd3d3b584d8a017852b4a9abb7ff2460336c6c1
push id2697
push userbbajaj@mozilla.com
push dateMon, 05 Aug 2013 18:49:53 +0000
treeherdermozilla-beta@dfec938c7b63 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrobcee
bugs879799
milestone24.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 879799 - Browser console lacks keyboard access; r=robcee
browser/devtools/jar.mn
browser/devtools/webconsole/test/Makefile.in
browser/devtools/webconsole/test/browser_console_keyboard_accessibility.js
browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js
browser/devtools/webconsole/webconsole.js
browser/devtools/webconsole/webconsole.xul
browser/locales/en-US/chrome/browser/devtools/webConsole.dtd
browser/locales/en-US/chrome/browser/devtools/webconsole.properties
browser/themes/linux/devtools/common.css
browser/themes/linux/devtools/webconsole.css
browser/themes/osx/devtools/common.css
browser/themes/osx/devtools/webconsole.css
browser/themes/windows/devtools/common.css
browser/themes/windows/devtools/webconsole.css
--- a/browser/devtools/jar.mn
+++ b/browser/devtools/jar.mn
@@ -8,17 +8,17 @@ browser.jar:
     content/browser/devtools/markup-view.xhtml                         (markupview/markup-view.xhtml)
     content/browser/devtools/markup-view.css                           (markupview/markup-view.css)
     content/browser/devtools/netmonitor.xul                            (netmonitor/netmonitor.xul)
     content/browser/devtools/netmonitor.css                            (netmonitor/netmonitor.css)
     content/browser/devtools/netmonitor-controller.js                  (netmonitor/netmonitor-controller.js)
     content/browser/devtools/netmonitor-view.js                        (netmonitor/netmonitor-view.js)
     content/browser/devtools/NetworkPanel.xhtml                        (webconsole/NetworkPanel.xhtml)
     content/browser/devtools/webconsole.js                             (webconsole/webconsole.js)
-    content/browser/devtools/webconsole.xul                            (webconsole/webconsole.xul)
+*   content/browser/devtools/webconsole.xul                            (webconsole/webconsole.xul)
 *   content/browser/devtools/scratchpad.xul                            (scratchpad/scratchpad.xul)
     content/browser/devtools/scratchpad.js                             (scratchpad/scratchpad.js)
     content/browser/devtools/splitview.css                             (shared/splitview.css)
     content/browser/devtools/theme-switching.js                        (shared/theme-switching.js)
     content/browser/devtools/styleeditor.xul                           (styleeditor/styleeditor.xul)
     content/browser/devtools/styleeditor.css                           (styleeditor/styleeditor.css)
     content/browser/devtools/computedview.xhtml                        (styleinspector/computedview.xhtml)
     content/browser/devtools/cssruleview.xhtml                         (styleinspector/cssruleview.xhtml)
--- a/browser/devtools/webconsole/test/Makefile.in
+++ b/browser/devtools/webconsole/test/Makefile.in
@@ -132,16 +132,17 @@ MOCHITEST_BROWSER_FILES = \
 	browser_console_native_getters.js \
 	browser_bug_871156_ctrlw_close_tab.js \
 	browser_console_private_browsing.js \
 	browser_console_nsiconsolemessage.js \
 	browser_webconsole_bug_817834_add_edited_input_to_history.js \
 	browser_console_addonsdk_loader_exception.js \
 	browser_console_error_source_click.js \
 	browser_console_clear_on_reload.js \
+	browser_console_keyboard_accessibility.js \
 	head.js \
 	$(NULL)
 
 ifeq ($(OS_ARCH), Darwin)
 MOCHITEST_BROWSER_FILES += \
 	browser_webconsole_bug_804845_ctrl_key_nav.js \
         $(NULL)
 endif
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_console_keyboard_accessibility.js
@@ -0,0 +1,69 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Check that basic keyboard shortcuts work in the web console.
+
+function test()
+{
+  const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
+  let hud = null;
+
+  addTab(TEST_URI);
+
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole(null, consoleOpened);
+  }, true);
+
+  function consoleOpened(aHud)
+  {
+    hud = aHud;
+    ok(hud, "Web Console opened");
+
+    content.console.log("foobarz1");
+    waitForMessages({
+      webconsole: hud,
+      messages: [{
+        text: "foobarz1",
+        category: CATEGORY_WEBDEV,
+        severity: SEVERITY_LOG,
+      }],
+    }).then(onConsoleMessage);
+  }
+
+  function onConsoleMessage()
+  {
+    hud.jsterm.once("messages-cleared", onClear);
+    info("try ctrl-k to clear output");
+    EventUtils.synthesizeKey("K", { accelKey: true });
+  }
+
+  function onClear()
+  {
+    is(hud.outputNode.textContent.indexOf("foobarz1"), -1, "output cleared");
+    is(hud.jsterm.inputNode.getAttribute("focused"), "true",
+       "jsterm input is focused");
+
+    info("try ctrl-f to focus filter");
+    EventUtils.synthesizeKey("F", { accelKey: true });
+    ok(!hud.jsterm.inputNode.getAttribute("focused"),
+       "jsterm input is not focused");
+    is(hud.ui.filterBox.getAttribute("focused"), "true",
+       "filter input is focused");
+
+    if (Services.appinfo.OS == "Darwin") {
+      EventUtils.synthesizeKey("t", { ctrlKey: true });
+    }
+    else {
+      EventUtils.synthesizeKey("N", { altKey: true });
+    }
+
+    let net = hud.ui.document.querySelector("toolbarbutton[category=net]");
+    is(hud.ui.document.activeElement, net,
+       "accesskey for Network category focuses the Net button");
+
+    finishTest();
+  }
+}
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js
@@ -1,37 +1,39 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const TEST_URI = "data:text/html;charset=utf-8,<p>bug 585991 - autocomplete popup keyboard usage test";
-let HUD;
+let HUD, popup, jsterm, inputNode, completeNode;
 
 function test() {
   addTab(TEST_URI);
   browser.addEventListener("load", function onLoad() {
     browser.removeEventListener("load", onLoad, true);
     openConsole(null, consoleOpened);
   }, true);
 }
 
 function consoleOpened(aHud) {
   HUD = aHud;
+  info("web console opened");
 
   content.wrappedJSObject.foobarBug585991 = {
     "item0": "value0",
     "item1": "value1",
     "item2": "value2",
     "item3": "value3",
   };
 
-  let jsterm = HUD.jsterm;
-  let popup = jsterm.autocompletePopup;
-  let completeNode = jsterm.completeNode;
+  jsterm = HUD.jsterm;
+  popup = jsterm.autocompletePopup;
+  completeNode = jsterm.completeNode;
+  inputNode = jsterm.inputNode;
 
   ok(!popup.isOpen, "popup is not open");
 
   popup._panel.addEventListener("popupshown", function onShown() {
     popup._panel.removeEventListener("popupshown", onShown, false);
 
     ok(popup.isOpen, "popup is open");
 
@@ -85,33 +87,30 @@ function consoleOpened(aHud) {
 
     EventUtils.synthesizeKey("VK_UP", {});
 
     is(popup.selectedIndex, 0, "index 0 is selected");
     is(popup.selectedItem.label, "watch", "watch is selected");
     is(completeNode.value, prefix + "watch",
         "completeNode.value holds watch");
 
-    popup._panel.addEventListener("popuphidden", autocompletePopupHidden, false);
-
+    info("press Tab and wait for popup to hide");
+    popup._panel.addEventListener("popuphidden", popupHideAfterTab, false);
     EventUtils.synthesizeKey("VK_TAB", {});
   }, false);
 
+  info("wait for completion: window.foobarBug585991.");
   jsterm.setInputValue("window.foobarBug585991");
   EventUtils.synthesizeKey(".", {});
 }
 
-function autocompletePopupHidden()
+function popupHideAfterTab()
 {
-  let jsterm = HUD.jsterm;
-  let popup = jsterm.autocompletePopup;
-  let completeNode = jsterm.completeNode;
-  let inputNode = jsterm.inputNode;
-
-  popup._panel.removeEventListener("popuphidden", autocompletePopupHidden, false);
+  // At this point the completion suggestion should be accepted.
+  popup._panel.removeEventListener("popuphidden", popupHideAfterTab, false);
 
   ok(!popup.isOpen, "popup is not open");
 
   is(inputNode.value, "window.foobarBug585991.watch",
      "completion was successful after VK_TAB");
 
   ok(!completeNode.value, "completeNode is empty");
 
@@ -141,34 +140,31 @@ function autocompletePopupHidden()
       is(inputNode.value, "window.foobarBug585991.",
          "completion was cancelled");
 
       ok(!completeNode.value, "completeNode is empty");
 
       executeSoon(testReturnKey);
     }, false);
 
+    info("press Escape to close the popup");
     executeSoon(function() {
       EventUtils.synthesizeKey("VK_ESCAPE", {});
     });
   }, false);
 
+  info("wait for completion: window.foobarBug585991.");
   executeSoon(function() {
     jsterm.setInputValue("window.foobarBug585991");
     EventUtils.synthesizeKey(".", {});
   });
 }
 
 function testReturnKey()
 {
-  let jsterm = HUD.jsterm;
-  let popup = jsterm.autocompletePopup;
-  let completeNode = jsterm.completeNode;
-  let inputNode = jsterm.inputNode;
-
   popup._panel.addEventListener("popupshown", function onShown() {
     popup._panel.removeEventListener("popupshown", onShown, false);
 
     ok(popup.isOpen, "popup is open");
 
     is(popup.itemCount, 18, "popup.itemCount is correct");
 
     is(popup.selectedIndex, 17, "First index from bottom is selected");
@@ -182,63 +178,106 @@ function testReturnKey()
     is(completeNode.value, prefix + "watch",
         "completeNode.value holds watch");
 
     EventUtils.synthesizeKey("VK_DOWN", {});
 
     is(popup.selectedIndex, 1, "index 1 is selected");
     is(popup.selectedItem.label, "valueOf", "valueOf is selected");
     is(completeNode.value, prefix + "valueOf",
-        "completeNode.value holds valueOf");
+       "completeNode.value holds valueOf");
 
     popup._panel.addEventListener("popuphidden", function onHidden() {
       popup._panel.removeEventListener("popuphidden", onHidden, false);
 
       ok(!popup.isOpen, "popup is not open after VK_RETURN");
 
-      // modified by bug 873250
-      is(inputNode.value, "", "no completion after VK_RETURN");
-      isnot(jsterm.lastInputValue, "window.foobarBug585991.valueOf",
-        "lastInputValue is not window.foobarBug585991.valueOf");
-      EventUtils.synthesizeKey("VK_UP", {});
-      is(inputNode.value, jsterm.lastInputValue, "previous entry was lastInputNode")
+      is(inputNode.value, "window.foobarBug585991.valueOf",
+         "completion was successful after VK_RETURN");
 
       ok(!completeNode.value, "completeNode is empty");
 
       dontShowArrayNumbers();
     }, false);
 
-    EventUtils.synthesizeKey("VK_RETURN", {});
+    info("press Return to accept suggestion. wait for popup to hide");
+
+    executeSoon(() => EventUtils.synthesizeKey("VK_RETURN", {}));
   }, false);
 
+  info("wait for completion suggestions: window.foobarBug585991.");
+
   executeSoon(function() {
     jsterm.setInputValue("window.foobarBug58599");
     EventUtils.synthesizeKey("1", {});
     EventUtils.synthesizeKey(".", {});
   });
 }
 
 function dontShowArrayNumbers()
 {
+  info("dontShowArrayNumbers");
   content.wrappedJSObject.foobarBug585991 = ["Sherlock Holmes"];
 
   let jsterm = HUD.jsterm;
   let popup = jsterm.autocompletePopup;
   let completeNode = jsterm.completeNode;
 
   popup._panel.addEventListener("popupshown", function onShown() {
     popup._panel.removeEventListener("popupshown", onShown, false);
 
     let sameItems = popup.getItems().map(function(e) {return e.label;});
     ok(!sameItems.some(function(prop, index) { prop === "0"; }),
        "Completing on an array doesn't show numbers.");
 
-    popup._panel.addEventListener("popuphidden", consoleOpened, false);
+    popup._panel.addEventListener("popuphidden", testReturnWithNoSelection, false);
 
-    EventUtils.synthesizeKey("VK_TAB", {});
-
-    executeSoon(finishTest);
+    info("wait for popup to hide");
+    executeSoon(() => EventUtils.synthesizeKey("VK_ESCAPE", {}));
   }, false);
 
-  jsterm.setInputValue("window.foobarBug585991");
-  EventUtils.synthesizeKey(".", {});
+  info("wait for popup to show");
+  executeSoon(() => {
+    jsterm.setInputValue("window.foobarBug585991");
+    EventUtils.synthesizeKey(".", {});
+  });
 }
 
+function testReturnWithNoSelection()
+{
+  popup._panel.removeEventListener("popuphidden", testReturnWithNoSelection, false);
+
+  info("test pressing return with open popup, but no selection, see bug 873250");
+  content.wrappedJSObject.testBug873250a = "hello world";
+  content.wrappedJSObject.testBug873250b = "hello world 2";
+
+  popup._panel.addEventListener("popupshown", function onShown() {
+    popup._panel.removeEventListener("popupshown", onShown);
+
+    ok(popup.isOpen, "popup is open");
+    is(popup.itemCount, 2, "popup.itemCount is correct");
+    isnot(popup.selectedIndex, -1, "popup.selectedIndex is correct");
+
+    info("press Return and wait for popup to hide");
+    popup._panel.addEventListener("popuphidden", popupHideAfterReturnWithNoSelection);
+    executeSoon(() => EventUtils.synthesizeKey("VK_RETURN", {}));
+  });
+
+  executeSoon(() => {
+    info("wait for popup to show");
+    jsterm.setInputValue("window.testBu");
+    EventUtils.synthesizeKey("g", {});
+  });
+}
+
+function popupHideAfterReturnWithNoSelection()
+{
+  popup._panel.removeEventListener("popuphidden", popupHideAfterReturnWithNoSelection);
+
+  ok(!popup.isOpen, "popup is not open after VK_RETURN");
+
+  is(inputNode.value, "", "inputNode is empty after VK_RETURN");
+  is(completeNode.value, "", "completeNode is empty");
+  is(jsterm.history[jsterm.history.length-1], "window.testBug",
+     "jsterm history is correct");
+
+  executeSoon(finishTest);
+}
--- a/browser/devtools/webconsole/webconsole.js
+++ b/browser/devtools/webconsole/webconsole.js
@@ -2849,24 +2849,32 @@ JSTerm.prototype = {
 
   /**
    * Last input value.
    * @type string
    */
   lastInputValue: "",
 
   /**
-   * Indicate input node changed since last focus.
+   * Tells if the input node changed since the last focus.
    *
    * @private
    * @type boolean
    */
   _inputChanged: false,
 
   /**
+   * Tells if the autocomplete popup was navigated since the last open.
+   *
+   * @private
+   * @type boolean
+   */
+  _autocompletePopupNavigated: false,
+
+  /**
    * History of code that was executed.
    * @type array
    */
   history: null,
   autocompletePopup: null,
   inputNode: null,
   completeNode: null,
 
@@ -3055,18 +3063,16 @@ JSTerm.prototype = {
    * @param function [aCallback]
    *        Optional function to invoke when the result is displayed.
    */
   execute: function JST_execute(aExecuteString, aCallback)
   {
     // attempt to execute the content of the inputNode
     aExecuteString = aExecuteString || this.inputNode.value;
     if (!aExecuteString) {
-      this.writeOutput(l10n.getStr("executeEmptyInput"), CATEGORY_OUTPUT,
-                       SEVERITY_LOG);
       return;
     }
 
     let node = this.writeOutput(aExecuteString, CATEGORY_INPUT, SEVERITY_LOG);
     let onResult = this._executeResultCallback.bind(this, node, aCallback);
 
     let options = { frame: this.SELECTED_FRAME };
     this.requestEvaluation(aExecuteString, options).then(onResult, onResult);
@@ -3593,16 +3599,18 @@ JSTerm.prototype = {
     node._outputAfterNode = aNodeAfter;
     this.hud.outputMessage(aCategory, node);
     return node;
   },
 
   /**
    * Clear the Web Console output.
    *
+   * This method emits the "messages-cleared" notification.
+   *
    * @param boolean aClearStorage
    *        True if you want to clear the console messages storage associated to
    *        this Web Console.
    */
   clearOutput: function JST_clearOutput(aClearStorage)
   {
     let hud = this.hud;
     let outputNode = hud.outputNode;
@@ -3615,20 +3623,24 @@ JSTerm.prototype = {
     hud._outputQueue.forEach(hud._pruneItemFromQueue, hud);
     hud._outputQueue = [];
     hud._networkRequests = {};
     hud._repeatNodes = {};
 
     if (aClearStorage) {
       this.webConsoleClient.clearMessagesCache();
     }
+
+    this.emit("messages-cleared");
   },
 
   /**
    * Remove all of the private messages from the Web Console output.
+   *
+   * This method emits the "private-messages-cleared" notification.
    */
   clearPrivateMessages: function JST_clearPrivateMessages()
   {
     let nodes = this.hud.outputNode.querySelectorAll("richlistitem[private]");
     for (let node of nodes) {
       this.hud.removeOutputMessage(node);
     }
     this.emit("private-messages-cleared");
@@ -3689,41 +3701,45 @@ JSTerm.prototype = {
   /**
    * The inputNode "keypress" event handler.
    *
    * @private
    * @param nsIDOMEvent aEvent
    */
   _keyPress: function JST__keyPress(aEvent)
   {
+    let inputNode = this.inputNode;
+    let inputUpdated = false;
+
     if (aEvent.ctrlKey) {
-      let inputNode = this.inputNode;
-      let closePopup = false;
       switch (aEvent.charCode) {
         case 97:
           // control-a
+          this.clearCompletion();
+
           if (Services.appinfo.OS == "WINNT") {
-            closePopup = true;
+            // Allow Select All on Windows.
             break;
           }
+
           let lineBeginPos = 0;
           if (this.hasMultilineInput()) {
             // find index of closest newline <= to cursor
             for (let i = inputNode.selectionStart-1; i >= 0; i--) {
               if (inputNode.value.charAt(i) == "\r" ||
                   inputNode.value.charAt(i) == "\n") {
                 lineBeginPos = i+1;
                 break;
               }
             }
           }
           inputNode.setSelectionRange(lineBeginPos, lineBeginPos);
           aEvent.preventDefault();
-          closePopup = true;
           break;
+
         case 101:
           // control-e
           if (Services.appinfo.OS == "WINNT") {
             break;
           }
           let lineEndPos = inputNode.value.length;
           if (this.hasMultilineInput()) {
             // find index of closest newline >= cursor
@@ -3732,99 +3748,144 @@ JSTerm.prototype = {
                   inputNode.value.charAt(i) == "\n") {
                 lineEndPos = i;
                 break;
               }
             }
           }
           inputNode.setSelectionRange(lineEndPos, lineEndPos);
           aEvent.preventDefault();
+          this.clearCompletion();
           break;
+
         case 110:
           // Control-N differs from down arrow: it ignores autocomplete state.
           // Note that we preserve the default 'down' navigation within
           // multiline text.
           if (Services.appinfo.OS == "Darwin" &&
               this.canCaretGoNext() &&
               this.historyPeruse(HISTORY_FORWARD)) {
             aEvent.preventDefault();
+            // Ctrl-N is also used to focus the Network category button on MacOSX.
+            // The preventDefault() call doesn't prevent the focus from moving
+            // away from the input.
+            inputNode.focus();
           }
-          closePopup = true;
+          this.clearCompletion();
           break;
+
         case 112:
           // Control-P differs from up arrow: it ignores autocomplete state.
           // Note that we preserve the default 'up' navigation within
           // multiline text.
           if (Services.appinfo.OS == "Darwin" &&
               this.canCaretGoPrevious() &&
               this.historyPeruse(HISTORY_BACK)) {
             aEvent.preventDefault();
+            // Ctrl-P may also be used to focus some category button on MacOSX.
+            // The preventDefault() call doesn't prevent the focus from moving
+            // away from the input.
+            inputNode.focus();
           }
-          closePopup = true;
+          this.clearCompletion();
           break;
         default:
           break;
       }
-      if (closePopup) {
-        if (this.autocompletePopup.isOpen) {
-          this.clearCompletion();
-        }
-      }
       return;
     }
     else if (aEvent.shiftKey &&
         aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN) {
       // shift return
       // TODO: expand the inputNode height by one line
       return;
     }
 
-    let inputUpdated = false;
-
-    switch(aEvent.keyCode) {
+    switch (aEvent.keyCode) {
       case Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE:
         if (this.autocompletePopup.isOpen) {
           this.clearCompletion();
           aEvent.preventDefault();
         }
         else if (this.sidebar) {
           this._sidebarDestroy();
+          aEvent.preventDefault();
         }
         break;
 
-      // Bug 873250 - always enter, ignore autocomplete
       case Ci.nsIDOMKeyEvent.DOM_VK_RETURN:
-        this.execute();
+        if (this._autocompletePopupNavigated &&
+            this.autocompletePopup.isOpen &&
+            this.autocompletePopup.selectedIndex > -1) {
+          this.acceptProposedCompletion();
+        }
+        else {
+          this.execute();
+          this._inputChanged = false;
+        }
         aEvent.preventDefault();
         break;
 
       case Ci.nsIDOMKeyEvent.DOM_VK_UP:
         if (this.autocompletePopup.isOpen) {
           inputUpdated = this.complete(this.COMPLETE_BACKWARD);
+          if (inputUpdated) {
+            this._autocompletePopupNavigated = true;
+          }
         }
         else if (this.canCaretGoPrevious()) {
           inputUpdated = this.historyPeruse(HISTORY_BACK);
         }
         if (inputUpdated) {
           aEvent.preventDefault();
         }
         break;
 
       case Ci.nsIDOMKeyEvent.DOM_VK_DOWN:
         if (this.autocompletePopup.isOpen) {
           inputUpdated = this.complete(this.COMPLETE_FORWARD);
+          if (inputUpdated) {
+            this._autocompletePopupNavigated = true;
+          }
         }
         else if (this.canCaretGoNext()) {
           inputUpdated = this.historyPeruse(HISTORY_FORWARD);
         }
         if (inputUpdated) {
           aEvent.preventDefault();
         }
         break;
 
+      case Ci.nsIDOMKeyEvent.DOM_VK_HOME:
+      case Ci.nsIDOMKeyEvent.DOM_VK_END:
+      case Ci.nsIDOMKeyEvent.DOM_VK_LEFT:
+        if (this.autocompletePopup.isOpen || this.lastCompletion.value) {
+          this.clearCompletion();
+        }
+        break;
+
+      case Ci.nsIDOMKeyEvent.DOM_VK_RIGHT: {
+        let cursorAtTheEnd = this.inputNode.selectionStart ==
+                             this.inputNode.selectionEnd &&
+                             this.inputNode.selectionStart ==
+                             this.inputNode.value.length;
+        let haveSuggestion = this.autocompletePopup.isOpen ||
+                             this.lastCompletion.value;
+        let useCompletion = cursorAtTheEnd || this._autocompletePopupNavigated;
+        if (haveSuggestion && useCompletion &&
+            this.complete(this.COMPLETE_HINT_ONLY) &&
+            this.lastCompletion.value &&
+            this.acceptProposedCompletion()) {
+          aEvent.preventDefault();
+        }
+        if (this.autocompletePopup.isOpen) {
+          this.clearCompletion();
+        }
+        break;
+      }
       case Ci.nsIDOMKeyEvent.DOM_VK_TAB:
         // Generate a completion and accept the first proposed value.
         if (this.complete(this.COMPLETE_HINT_ONLY) &&
             this.lastCompletion &&
             this.acceptProposedCompletion()) {
           aEvent.preventDefault();
         }
         else if (this._inputChanged) {
@@ -4091,19 +4152,21 @@ JSTerm.prototype = {
     let completionType = this.lastCompletion.completionType;
     this.lastCompletion = {
       value: inputValue,
       matchProp: lastPart,
     };
 
     if (items.length > 1 && !popup.isOpen) {
       popup.openPopup(inputNode);
+      this._autocompletePopupNavigated = false;
     }
     else if (items.length < 2 && popup.isOpen) {
       popup.hidePopup();
+      this._autocompletePopupNavigated = false;
     }
 
     if (items.length == 1) {
       popup.selectedIndex = 0;
     }
 
     this.onAutocompleteSelect();
 
@@ -4139,16 +4202,17 @@ JSTerm.prototype = {
    */
   clearCompletion: function JSTF_clearCompletion()
   {
     this.autocompletePopup.clearItems();
     this.lastCompletion = { value: null };
     this.updateCompleteNode("");
     if (this.autocompletePopup.isOpen) {
       this.autocompletePopup.hidePopup();
+      this._autocompletePopupNavigated = false;
     }
   },
 
   /**
    * Accept the proposed input completion.
    *
    * @return boolean
    *         True if there was a selected completion item and the input value
@@ -4406,20 +4470,22 @@ CommandController.prototype = {
         // Only enable "copy" if nodes are selected.
         return this.owner.outputNode.selectedCount > 0;
       case "consoleCmd_openURL":
       case "consoleCmd_copyURL": {
         // Only enable URL-related actions if node is Net Activity.
         let selectedItem = this.owner.outputNode.selectedItem;
         return selectedItem && "url" in selectedItem;
       }
+      case "consoleCmd_clearOutput":
       case "cmd_fontSizeEnlarge":
       case "cmd_fontSizeReduce":
       case "cmd_fontSizeReset":
       case "cmd_selectAll":
+      case "cmd_find":
         return true;
       case "cmd_close":
         return this.owner.owner._browserConsole;
     }
     return false;
   },
 
   doCommand: function CommandController_doCommand(aCommand)
@@ -4429,16 +4495,22 @@ CommandController.prototype = {
         this.copy();
         break;
       case "consoleCmd_openURL":
         this.openURL();
         break;
       case "consoleCmd_copyURL":
         this.copyURL();
         break;
+      case "consoleCmd_clearOutput":
+        this.owner.jsterm.clearOutput(true);
+        break;
+      case "cmd_find":
+        this.owner.filterBox.focus();
+        break;
       case "cmd_selectAll":
         this.selectAll();
         break;
       case "cmd_fontSizeEnlarge":
         this.owner.changeFontSize("+");
         break;
       case "cmd_fontSizeReduce":
         this.owner.changeFontSize("-");
--- a/browser/devtools/webconsole/webconsole.xul
+++ b/browser/devtools/webconsole/webconsole.xul
@@ -29,29 +29,34 @@
   <commandset id="consoleCommands"
               commandupdater="true"
               events="richlistbox-select"
               oncommandupdate="goUpdateConsoleCommands();">
     <command id="consoleCmd_openURL"
              oncommand="goDoCommand('consoleCmd_openURL');"/>
     <command id="consoleCmd_copyURL"
              oncommand="goDoCommand('consoleCmd_copyURL');"/>
+    <command id="consoleCmd_clearOutput"
+             oncommand="goDoCommand('consoleCmd_clearOutput');"/>
+    <command id="cmd_find" oncommand="goDoCommand('cmd_find');"/>
     <command id="cmd_fullZoomEnlarge" oncommand="goDoCommand('cmd_fontSizeEnlarge');"/>
     <command id="cmd_fullZoomReduce" oncommand="goDoCommand('cmd_fontSizeReduce');"/>
     <command id="cmd_fullZoomReset" oncommand="goDoCommand('cmd_fontSizeReset');"/>
     <command id="cmd_close" oncommand="goDoCommand('cmd_close');" disabled="true"/>
   </commandset>
   <keyset id="consoleKeys">
     <key id="key_fullZoomReduce"  key="&fullZoomReduceCmd.commandkey;" command="cmd_fullZoomReduce"  modifiers="accel"/>
     <key key="&fullZoomReduceCmd.commandkey2;"  command="cmd_fullZoomReduce" modifiers="accel"/>
     <key id="key_fullZoomEnlarge" key="&fullZoomEnlargeCmd.commandkey;" command="cmd_fullZoomEnlarge" modifiers="accel"/>
     <key key="&fullZoomEnlargeCmd.commandkey2;" command="cmd_fullZoomEnlarge" modifiers="accel"/>
     <key key="&fullZoomEnlargeCmd.commandkey3;" command="cmd_fullZoomEnlarge" modifiers="accel"/>
     <key id="key_fullZoomReset" key="&fullZoomResetCmd.commandkey;" command="cmd_fullZoomReset" modifiers="accel"/>
     <key key="&fullZoomResetCmd.commandkey2;" command="cmd_fullZoomReset" modifiers="accel"/>
+    <key key="&findCmd.key;" command="cmd_find" modifiers="accel"/>
+    <key key="&clearOutputCmd.key;" command="consoleCmd_clearOutput" modifiers="accel"/>
     <key key="&closeCmd.key;" command="cmd_close" modifiers="accel"/>
   </keyset>
   <keyset id="editMenuKeys"/>
 
   <popupset id="mainPopupSet">
     <menupopup id="output-contextmenu"
                onpopupshowing="ConsoleContextMenu.build(event);">
       <menuitem id="saveBodiesContextMenu" type="checkbox" label="&saveBodies.label;"
@@ -67,90 +72,106 @@
     </menupopup>
   </popupset>
 
   <box class="hud-outer-wrapper devtools-responsive-container" flex="1">
     <vbox class="hud-console-wrapper" flex="1">
       <toolbar class="hud-console-filter-toolbar devtools-toolbar" mode="full">
         <toolbarbutton label="&btnPageNet.label;" type="menu-button"
                        category="net" class="devtools-toolbarbutton webconsole-filter-button"
-                       tooltiptext="&btnPageNet.tooltip;">
+                       tooltiptext="&btnPageNet.tooltip;"
+#ifdef XP_MACOSX
+                       accesskey="&btnPageNet.accesskeyMacOSX;"
+#else
+                       accesskey="&btnPageNet.accesskey;"
+#endif
+                       tabindex="3">
           <menupopup>
             <menuitem label="&btnConsoleErrors;" type="checkbox" autocheck="false"
                       prefKey="network"/>
             <menuitem label="&btnConsoleLog;" type="checkbox" autocheck="false"
                       prefKey="networkinfo"/>
             <menuseparator id="saveBodiesSeparator" />
             <menuitem id="saveBodies" type="checkbox" label="&saveBodies.label;"
                       accesskey="&saveBodies.accesskey;"/>
           </menupopup>
         </toolbarbutton>
         <toolbarbutton label="&btnPageCSS.label;" type="menu-button"
                        category="css" class="devtools-toolbarbutton webconsole-filter-button"
-                       tooltiptext="&btnPageCSS.tooltip;">
+                       tooltiptext="&btnPageCSS.tooltip;"
+                       accesskey="&btnPageCSS.accesskey;"
+                       tabindex="4">
           <menupopup>
             <menuitem label="&btnConsoleErrors;" type="checkbox" autocheck="false"
                       prefKey="csserror"/>
             <menuitem label="&btnConsoleWarnings;" type="checkbox"
                       autocheck="false" prefKey="cssparser"/>
           </menupopup>
         </toolbarbutton>
         <toolbarbutton label="&btnPageJS.label;" type="menu-button"
                        category="js" class="devtools-toolbarbutton webconsole-filter-button"
-                       tooltiptext="&btnPageJS.tooltip;">
+                       tooltiptext="&btnPageJS.tooltip;"
+                       accesskey="&btnPageJS.accesskey;"
+                       tabindex="5">
           <menupopup>
             <menuitem label="&btnConsoleErrors;" type="checkbox"
                       autocheck="false" prefKey="exception"/>
             <menuitem label="&btnConsoleWarnings;" type="checkbox"
                       autocheck="false" prefKey="jswarn"/>
           </menupopup>
         </toolbarbutton>
         <toolbarbutton label="&btnPageSecurity.label;" type="menu-button"
                        category="security" class="devtools-toolbarbutton webconsole-filter-button"
-                       tooltiptext="&btnPageSecurity.tooltip;">
+                       tooltiptext="&btnPageSecurity.tooltip;"
+                       accesskey="&btnPageSecurity.accesskey;"
+                       tabindex="6">
           <menupopup>
             <menuitem label="&btnConsoleErrors;" type="checkbox"
                       autocheck="false" prefKey="secerror"/>
             <menuitem label="&btnConsoleWarnings;" type="checkbox"
                       autocheck="false" prefKey="secwarn"/>
           </menupopup>
         </toolbarbutton>
         <toolbarbutton label="&btnPageLogging.label;" type="menu-button"
                        category="logging" class="devtools-toolbarbutton webconsole-filter-button"
-                       tooltiptext="&btnPageLogging.tooltip;">
+                       tooltiptext="&btnPageLogging.tooltip;"
+                       accesskey="&btnPageLogging.accesskey;"
+                       tabindex="7">
           <menupopup>
             <menuitem label="&btnConsoleErrors;" type="checkbox"
                       autocheck="false" prefKey="error"/>
             <menuitem label="&btnConsoleWarnings;" type="checkbox"
                       autocheck="false" prefKey="warn"/>
             <menuitem label="&btnConsoleInfo;" type="checkbox" autocheck="false"
                       prefKey="info"/>
             <menuitem label="&btnConsoleLog;" type="checkbox" autocheck="false"
                       prefKey="log"/>
           </menupopup>
         </toolbarbutton>
 
         <toolbarbutton class="webconsole-clear-console-button devtools-toolbarbutton"
-                       label="&btnClear.label;" tooltiptext="&btnClear.tooltip;"/>
+                       label="&btnClear.label;" tooltiptext="&btnClear.tooltip;"
+                       accesskey="&btnClear.accesskey;"
+                       tabindex="8"/>
 
         <spacer flex="1"/>
 
         <textbox class="compact hud-filter-box devtools-searchinput" type="search"
-                 placeholder="&filterOutput.placeholder;"/>
+                 placeholder="&filterOutput.placeholder;" tabindex="2"/>
       </toolbar>
 
       <richlistbox class="hud-output-node" orient="vertical" flex="1"
                    seltype="multiple" context="output-contextmenu"
-                   style="direction:ltr;"/>
+                   style="direction:ltr;" tabindex="1"/>
 
       <hbox class="jsterm-input-container" style="direction:ltr">
         <stack class="jsterm-stack-node" flex="1">
           <textbox class="jsterm-complete-node" multiline="true" rows="1"
                    tabindex="-1"/>
-          <textbox class="jsterm-input-node" multiline="true" rows="1"/>
+          <textbox class="jsterm-input-node" multiline="true" rows="1" tabindex="0"/>
         </stack>
       </hbox>
     </vbox>
 
     <splitter class="devtools-side-splitter"/>
 
     <tabbox id="webconsole-sidebar" class="devtools-sidebar-tabs" hidden="true" width="300">
       <tabs/>
--- a/browser/locales/en-US/chrome/browser/devtools/webConsole.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/webConsole.dtd
@@ -40,44 +40,58 @@
 <!ENTITY openURL.accesskey "T">
 
 <!-- LOCALIZATION NOTE (btnPageNet.label): This string is used for the menu
   -  button that allows users to toggle the network logging output.
   -  This string and the following strings toggle various kinds of output
   -  filters. -->
 <!ENTITY btnPageNet.label   "Net">
 <!ENTITY btnPageNet.tooltip "Log network access">
+<!ENTITY btnPageNet.accesskey "N">
+<!-- LOCALIZATION NOTE (btnPageNet.accesskeyMacOSX): This string is used as
+  -  access key for the menu button that allows users to toggle the network
+  -  logging output. On MacOSX accesskeys are available with Ctrl-*. Please make
+  -  sure you do not use the following letters: A, E, N and P. These are used
+  -  for editing commands in text inputs. -->
+<!ENTITY btnPageNet.accesskeyMacOSX "t">
 <!ENTITY btnPageCSS.label   "CSS">
 <!ENTITY btnPageCSS.tooltip "Log CSS parsing errors">
+<!ENTITY btnPageCSS.accesskey "C">
 <!ENTITY btnPageJS.label    "JS">
 <!ENTITY btnPageJS.tooltip  "Log JavaScript exceptions">
+<!ENTITY btnPageJS.accesskey  "J">
 <!ENTITY btnPageSecurity.label "Security">
 <!ENTITY btnPageSecurity.tooltip "Log security errors and warnings">
+<!ENTITY btnPageSecurity.accesskey "S">
 
 <!-- LOCALIZATION NOTE (btnPageLogging): This is used as the text of the
   -  the toolbar. It shows or hides messages that the web developer inserted on
   -  the page for debugging purposes, using calls such console.log() and
   -  console.error(). -->
 <!ENTITY btnPageLogging.label   "Logging">
 <!ENTITY btnPageLogging.tooltip "Log messages sent to the window.console object">
+<!ENTITY btnPageLogging.accesskey "L">
 <!ENTITY btnConsoleErrors       "Errors">
 <!ENTITY btnConsoleInfo         "Info">
 <!ENTITY btnConsoleWarnings     "Warnings">
 <!ENTITY btnConsoleLog          "Log">
 
 <!ENTITY filterOutput.placeholder "Filter output">
 <!ENTITY btnClear.label        "Clear">
 <!ENTITY btnClear.tooltip      "Clear the Web Console output">
+<!ENTITY btnClear.accesskey    "r">
 
 <!ENTITY fullZoomEnlargeCmd.commandkey  "+">
 <!ENTITY fullZoomEnlargeCmd.commandkey2 "="> <!-- + is above this key on many keyboards -->
 <!ENTITY fullZoomEnlargeCmd.commandkey3 "">
 
 <!ENTITY fullZoomReduceCmd.commandkey   "-">
 <!ENTITY fullZoomReduceCmd.commandkey2  "">
 
 <!ENTITY fullZoomResetCmd.commandkey    "0">
 <!ENTITY fullZoomResetCmd.commandkey2   "">
 
 <!ENTITY copyURLCmd.label     "Copy Link Location">
 <!ENTITY copyURLCmd.accesskey "a">
 
-<!ENTITY closeCmd.key  "W">
+<!ENTITY closeCmd.key         "W">
+<!ENTITY findCmd.key          "F">
+<!ENTITY clearOutputCmd.key   "K">
--- a/browser/locales/en-US/chrome/browser/devtools/webconsole.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/webconsole.properties
@@ -155,20 +155,16 @@ ToolboxWebconsole.tooltip=Web Console
 # string. This string is clickable such that the rest of the string is
 # retrieved from the server.
 longStringEllipsis=[…]
 
 # LOCALIZATION NOTE (longStringTooLong): the string displayed after the user
 # tries to expand a long string.
 longStringTooLong=The string you are trying to view is too long to be displayed by the Web Console.
 
-# LOCALIZATION NOTE (executeEmptyInput): the string displayed when the user
-# tries to execute code, but the input is empty.
-executeEmptyInput=No value to execute.
-
 # LOCALIZATION NOTE (NetworkPanel.fetchRemainingResponseContentLink): the
 # string  displayed in the network panel when the response body is only
 # partially available. Parameters: %S is the amount of bytes that need to be
 # fetched.
 NetworkPanel.fetchRemainingResponseContentLink=Fetch the remaining %S bytes
 
 # LOCALIZATION NOTE (NetworkPanel.fetchRemainingRequestContentLink): the
 # string displayed in the network panel when the request body is only
--- a/browser/themes/linux/devtools/common.css
+++ b/browser/themes/linux/devtools/common.css
@@ -41,17 +41,17 @@
 }
 
 .devtools-toolbarbutton > .toolbarbutton-menubutton-button {
   -moz-box-orient: horizontal;
 }
 
 .devtools-menulist:-moz-focusring,
 .devtools-toolbarbutton:-moz-focusring {
-  outline: 1px dotted hsla(210,30%,85%,0.4);
+  outline: 1px dotted hsla(210,30%,85%,0.7);
   outline-offset: -4px;
 }
 
 .devtools-toolbarbutton:not([label]) {
   min-width: 32px;
 }
 
 .devtools-toolbarbutton:not([label]) > .toolbarbutton-text {
--- a/browser/themes/linux/devtools/webconsole.css
+++ b/browser/themes/linux/devtools/webconsole.css
@@ -106,16 +106,20 @@
   display: block;
   visibility: hidden;
   height: 0;
   overflow: hidden;
 }
 
 /* WebConsole colored drops */
 
+.webconsole-filter-button {
+  -moz-user-focus: normal;
+}
+
 .webconsole-filter-button[checked] {
   color: white !important;
 }
 
 .webconsole-filter-button > .toolbarbutton-menubutton-button:before {
   content: "";
   display: inline-block;
   height: 8px;
--- a/browser/themes/osx/devtools/common.css
+++ b/browser/themes/osx/devtools/common.css
@@ -45,17 +45,17 @@
 }
 
 .devtools-menulist {
   margin: 0 2px;
 }
 
 .devtools-menulist:-moz-focusring,
 .devtools-toolbarbutton:-moz-focusring {
-  outline: 1px dotted hsla(210,30%,85%,0.4);
+  outline: 1px dotted hsla(210,30%,85%,0.7);
   outline-offset: -4px;
 }
 
 .devtools-toolbarbutton > .toolbarbutton-text {
   margin: 1px 6px;
 }
 
 .devtools-toolbarbutton:not([label]) {
--- a/browser/themes/osx/devtools/webconsole.css
+++ b/browser/themes/osx/devtools/webconsole.css
@@ -98,16 +98,20 @@
   display: block;
   visibility: hidden;
   height: 0;
   overflow: hidden;
 }
 
 /* WebConsole colored drops */
 
+.webconsole-filter-button {
+  -moz-user-focus: normal;
+}
+
 .webconsole-filter-button[checked] {
   color: white !important;
 }
 
 .webconsole-filter-button > .toolbarbutton-menubutton-button:before {
   content: "";
   display: inline-block;
   height: 8px;
--- a/browser/themes/windows/devtools/common.css
+++ b/browser/themes/windows/devtools/common.css
@@ -41,17 +41,17 @@
 }
 
 .devtools-toolbarbutton > .toolbarbutton-menubutton-button {
   -moz-box-orient: horizontal;
 }
 
 .devtools-menulist:-moz-focusring,
 .devtools-toolbarbutton:-moz-focusring {
-  outline: 1px dotted hsla(210,30%,85%,0.4);
+  outline: 1px dotted hsla(210,30%,85%,0.7);
   outline-offset: -4px;
 }
 
 .devtools-toolbarbutton > .toolbarbutton-icon {
   margin: 0;
 }
 
 .devtools-toolbarbutton:not([label]) {
--- a/browser/themes/windows/devtools/webconsole.css
+++ b/browser/themes/windows/devtools/webconsole.css
@@ -96,16 +96,20 @@
   display: block;
   visibility: hidden;
   height: 0;
   overflow: hidden;
 }
 
 /* WebConsole colored drops */
 
+.webconsole-filter-button {
+  -moz-user-focus: normal;
+}
+
 .webconsole-filter-button[checked] {
   color: white !important;
 }
 
 .webconsole-filter-button > .toolbarbutton-menubutton-button:before {
   content: "";
   display: inline-block;
   height: 8px;