Bug 1389221 - Add readline navigation bindings to url bar on macOS r=dao
authorrosston <ross.brandes@gmail.com>
Fri, 19 Oct 2018 08:29:27 +0000
changeset 500612 14e469a4365b820bfb1a890c6993edb118a60d4f
parent 500611 53abc33e3e38ea49862e20af8974bb5f8a17f439
child 500613 15fd39e497668c53ee7fecd914510c5f4ac35bf7
push id1864
push userffxbld-merge
push dateMon, 03 Dec 2018 15:51:40 +0000
treeherdermozilla-release@f040763d99ad [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdao
bugs1389221
milestone64.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 1389221 - Add readline navigation bindings to url bar on macOS r=dao Support ctrl-n and ctrl-p for navigating down and up (respectively) in the url bar on macOS. The autocomplete widget will also support the same key bindings. This functionality matches ctrl-n/ctrl-p behavior on the other major macOS browsers. Differential Revision: https://phabricator.services.mozilla.com/D6451
browser/base/content/test/urlbar/browser.ini
browser/base/content/test/urlbar/browser_autocomplete_readline_navigation.js
browser/base/content/urlbarBindings.xml
toolkit/content/widgets/autocomplete.xml
--- a/browser/base/content/test/urlbar/browser.ini
+++ b/browser/base/content/test/urlbar/browser.ini
@@ -15,16 +15,18 @@ support-files =
 [browser_autocomplete_a11y_label.js]
 skip-if = (verify && !debug && (os == 'win'))
 [browser_autocomplete_autoselect.js]
 [browser_autocomplete_cursor.js]
 skip-if = verify
 [browser_autocomplete_edit_completed.js]
 [browser_autocomplete_enter_race.js]
 [browser_autocomplete_no_title.js]
+[browser_autocomplete_readline_navigation.js]
+skip-if = os != "mac" # Mac only feature
 [browser_autocomplete_tag_star_visibility.js]
 [browser_bug1104165-switchtab-decodeuri.js]
 [browser_bug1003461-switchtab-override.js]
 skip-if = (verify && debug && (os == 'win'))
 [browser_bug1024133-switchtab-override-keynav.js]
 [browser_bug1025195_switchToTabHavingURI_aOpenParams.js]
 [browser_bug1070778.js]
 [browser_bug1225194-remotetab.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_autocomplete_readline_navigation.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const ONEOFF_URLBAR_PREF = "browser.urlbar.oneOffSearches";
+
+function repeat(limit, func) {
+  for (let i = 0; i < limit; i++) {
+    func(i);
+  }
+}
+
+function is_selected(index) {
+  is(gURLBar.popup.richlistbox.selectedIndex, index, `Item ${index + 1} should be selected`);
+
+  // This is true because although both the listbox and the one-offs can have
+  // selections, the test doesn't check that.
+  is(gURLBar.popup.oneOffSearchButtons.selectedButton, null,
+     "A result is selected, so the one-offs should not have a selection");
+}
+
+add_task(async function() {
+  let maxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults");
+  Services.prefs.setBoolPref(ONEOFF_URLBAR_PREF, true);
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
+  registerCleanupFunction(async function() {
+    await PlacesUtils.history.clear();
+    Services.prefs.clearUserPref(ONEOFF_URLBAR_PREF);
+    BrowserTestUtils.removeTab(tab);
+  });
+
+  let visits = [];
+  repeat(maxResults, i => {
+    visits.push({
+      uri: makeURI("http://example.com/autocomplete/?" + i),
+    });
+  });
+  await PlacesTestUtils.addVisits(visits);
+
+  await promiseAutocompleteResultPopup("example.com/autocomplete");
+  await waitForAutocompleteResultAt(maxResults - 1);
+
+  let popup = gURLBar.popup;
+  let results = popup.richlistbox.children;
+  is(results.length, maxResults,
+     "Should get maxResults=" + maxResults + " results");
+  is_selected(0);
+
+  info("Ctrl-n to select the next item");
+  EventUtils.synthesizeKey("n", {ctrlKey: true});
+  is_selected(1);
+
+  info("Ctrl-p to select the previous item");
+  EventUtils.synthesizeKey("p", {ctrlKey: true});
+  is_selected(0);
+
+  EventUtils.synthesizeKey("KEY_Escape");
+  await promisePopupHidden(gURLBar.popup);
+});
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -367,18 +367,22 @@ file, You can obtain one at http://mozil
           // keypresses out of order.  All events will be replayed when
           // _deferredKeyEventTimeout fires.
           if (this._deferredKeyEventQueue.length) {
             return true;
           }
 
           // At this point, no events have been deferred for this search, and we
           // need to decide whether `event` is the first one that should be.
+          if (!this._keyCodesToDefer.has(event.keyCode) &&
+              !(/Mac/.test(navigator.platform) &&
+                event.ctrlKey &&
+                (event.key === "n" || event.key === "p") &&
+                this.popupOpen)) {
 
-          if (!this._keyCodesToDefer.has(event.keyCode)) {
             // Not a key that should trigger deferring.
             return false;
           }
 
           let waitedLongEnough =
             this._searchStartDate + this._deferredKeyEventTimeoutMs <= Cu.now();
           if (waitedLongEnough) {
             // This is a key that we would defer, but enough time has passed
--- a/toolkit/content/widgets/autocomplete.xml
+++ b/toolkit/content/widgets/autocomplete.xml
@@ -434,22 +434,23 @@
           if (aEvent.target.localName != "textbox")
             return true; // Let child buttons of autocomplete take input
 
           // Re: urlbarDeferred, see the comment in urlbarBindings.xml.
           if (aEvent.defaultPrevented && !aEvent.urlbarDeferred) {
             return false;
           }
 
+          const isMac = /Mac/.test(navigator.platform);
           var cancel = false;
 
           // Catch any keys that could potentially move the caret. Ctrl can be
           // used in combination with these keys on Windows and Linux; and Alt
           // can be used on OS X, so make sure the unused one isn't used.
-          let metaKey = /Mac/.test(navigator.platform) ? aEvent.ctrlKey : aEvent.altKey;
+          let metaKey = isMac ? aEvent.ctrlKey : aEvent.altKey;
           if (!this.disableKeyNavigation && !metaKey) {
             switch (aEvent.keyCode) {
               case KeyEvent.DOM_VK_LEFT:
               case KeyEvent.DOM_VK_RIGHT:
               case KeyEvent.DOM_VK_HOME:
                 cancel = this.mController.handleKeyNavigation(aEvent.keyCode);
                 break;
             }
@@ -470,54 +471,67 @@
               case KeyEvent.DOM_VK_DOWN:
               case KeyEvent.DOM_VK_PAGE_UP:
               case KeyEvent.DOM_VK_PAGE_DOWN:
                 cancel = this.mController.handleKeyNavigation(aEvent.keyCode);
                 break;
             }
           }
 
+          // Handle readline/emacs-style navigation bindings on Mac.
+          if (isMac &&
+              !this.disableKeyNavigation &&
+              this.popup.popupOpen &&
+              aEvent.ctrlKey &&
+              (aEvent.key === "n" || aEvent.key === "p")) {
+
+            const effectiveKey = (aEvent.key === "p") ?
+                                 KeyEvent.DOM_VK_UP :
+                                 KeyEvent.DOM_VK_DOWN;
+            cancel = this.mController.handleKeyNavigation(effectiveKey);
+          }
+
           // Handle keys we know aren't part of a shortcut, even with Alt or
           // Ctrl.
           switch (aEvent.keyCode) {
             case KeyEvent.DOM_VK_ESCAPE:
               cancel = this.mController.handleEscape();
               break;
             case KeyEvent.DOM_VK_RETURN:
-              if (/Mac/.test(navigator.platform)) {
+              if (isMac) {
                 // Prevent the default action, since it will beep on Mac
                 if (aEvent.metaKey)
                   aEvent.preventDefault();
               }
               if (this.popup.selectedIndex >= 0) {
                 this._selectionDetails = {
                   index: this.popup.selectedIndex,
                   kind: "key",
                 };
               }
               cancel = this.handleEnter(aEvent);
               break;
             case KeyEvent.DOM_VK_DELETE:
-              if (/Mac/.test(navigator.platform) && !aEvent.shiftKey) {
+              if (isMac && !aEvent.shiftKey) {
                 break;
               }
               cancel = this.handleDelete();
               break;
             case KeyEvent.DOM_VK_BACK_SPACE:
-              if (/Mac/.test(navigator.platform) && aEvent.shiftKey) {
+              if (isMac && aEvent.shiftKey) {
                 cancel = this.handleDelete();
               }
               break;
             case KeyEvent.DOM_VK_DOWN:
             case KeyEvent.DOM_VK_UP:
               if (aEvent.altKey)
                 this.toggleHistoryPopup();
               break;
             case KeyEvent.DOM_VK_F4:
-              if (!/Mac/.test(navigator.platform)) {
+              if (!isMac) {
                 this.toggleHistoryPopup();
               }
               break;
           }
 
           if (cancel) {
             aEvent.stopPropagation();
             aEvent.preventDefault();