Bug 1657676 - Add search mode preview. r=adw

Differential Revision: https://phabricator.services.mozilla.com/D89964
This commit is contained in:
Harry Twyford 2020-09-16 23:18:59 +00:00
Родитель 72ef712fd0
Коммит 8f95a2e763
14 изменённых файлов: 335 добавлений и 37 удалений

Просмотреть файл

@ -4135,9 +4135,11 @@ const BrowserSearch = {
if (delayUpdate && !gURLBar.value) {
// Delays changing the URL Bar placeholder until the user is not going to be
// seeing it, e.g. when there is a value entered in the bar, or if there is
// a tab switch to a tab which has a url loaded.
// a tab switch to a tab which has a url loaded. We delay the update until
// the user is out of search mode since an alternative placeholder is used
// in search mode.
let placeholderUpdateListener = () => {
if (gURLBar.value) {
if (gURLBar.value && !gURLBar.searchMode) {
// By the time the user has switched, they may have changed the engine
// again, so we need to call this function again but with the
// new engine name.

Просмотреть файл

@ -377,7 +377,9 @@ class UrlbarController {
break;
case KeyEvent.DOM_VK_RIGHT:
case KeyEvent.DOM_VK_END:
this.input.maybePromoteKeywordToSearchMode();
this.input.maybePromoteKeywordToSearchMode({
entry: "typed",
});
// Fall through.
case KeyEvent.DOM_VK_LEFT:
case KeyEvent.DOM_VK_HOME:

Просмотреть файл

@ -942,6 +942,15 @@ class UrlbarInput {
// confusing the user, we always enforce it when a result changes our value.
this.setPageProxyState("invalid", true);
// A previous result may have previewed search mode. If we don't expect that
// we might stay in a search mode of some kind, exit it now.
if (
this.searchMode?.isPreview &&
result?.payload.keywordOffer != UrlbarUtils.KEYWORD_OFFER.SHOW
) {
this.setSearchMode({});
}
if (!result) {
// This happens when there's no selection, for example when moving to the
// one-offs search settings button, or to the input field when Top Sites
@ -961,6 +970,20 @@ class UrlbarInput {
} else if (result.autofill) {
let { value, selectionStart, selectionEnd } = result.autofill;
this._autofillValue(value, selectionStart, selectionEnd);
} else if (
result.payload.keywordOffer == UrlbarUtils.KEYWORD_OFFER.SHOW
) {
// Enter search mode without starting a query. This will just preview
// search mode.
let enteredSearchMode = this.maybePromoteKeywordToSearchMode({
result,
checkValue: false,
startQuery: false,
});
if (!enteredSearchMode) {
this._setValue(this._getValueFromResult(result), true);
this.setSearchMode({});
}
} else {
// If the url is trimmed but it's invalid (for example it has an unknown
// single word host, or an unknown domain suffix), trimming
@ -1065,7 +1088,11 @@ class UrlbarInput {
firstResult.heuristic &&
firstResult.payload.keyword &&
!firstResult.payload.keywordOffer &&
this.maybePromoteKeywordToSearchMode(firstResult, false)
this.maybePromoteKeywordToSearchMode({
result: firstResult,
entry: "typed",
checkValue: false,
})
) {
return true;
}
@ -1157,6 +1184,7 @@ class UrlbarInput {
};
if (this.searchMode) {
this.promoteSearchMode();
options.searchMode = this.searchMode;
if (this.searchMode.source) {
options.sources = [this.searchMode.source];
@ -1275,8 +1303,14 @@ class UrlbarInput {
* @param {string} entry
* How search mode was entered. This is recorded in event telemetry. One of
* the values in UrlbarUtils.SEARCH_MODE_ENTRY.
* @param {boolean} [isPreview]
* If true, we will preview search mode. Search mode preview does not record
* telemetry and has slighly different UI behavior. The preview is exited in
* favor of full search mode when a query is executed. False should be
* passed if the caller needs to enter search mode but expects it will not
* be interacted with right away. Defaults to true.
*/
setSearchMode({ engineName, source, entry }) {
setSearchMode({ engineName, source, entry, isPreview = true }) {
// Exit search mode if update2 is disabled or the passed-in engine is
// invalid or hidden.
let engine = Services.search.getEngineByName(engineName);
@ -1357,6 +1391,11 @@ class UrlbarInput {
} else {
this.searchMode.entry = entry;
}
this.searchMode.isPreview = true;
if (!isPreview) {
this.promoteSearchMode();
}
this.toggleAttribute("searchmode", true);
// Clear autofill.
if (this._autofillPlaceholder && this.window.gBrowser.userTypedValue) {
@ -1367,17 +1406,6 @@ class UrlbarInput {
this.value = "";
this.setPageProxyState("invalid", true);
}
this._searchModesByBrowser.set(
this.window.gBrowser.selectedBrowser,
this.searchMode
);
// Record search mode entry in telemetry.
try {
BrowserUsageTelemetry.recordSearchMode(this.searchMode);
} catch (ex) {
Cu.reportError(ex);
}
} else {
this.removeAttribute("searchmode");
this._searchModesByBrowser.delete(this.window.gBrowser.selectedBrowser);
@ -1421,6 +1449,38 @@ class UrlbarInput {
}
}
/**
* Promotes the current search mode from preview mode to full search mode.
*/
promoteSearchMode() {
if (!this.searchMode.isPreview) {
return;
}
this.searchMode.isPreview = false;
let previousSearchMode = this._searchModesByBrowser.get(
this.window.gBrowser.selectedBrowser
);
if (ObjectUtils.deepEqual(previousSearchMode, this.searchMode)) {
// If we already have an exact match of this.searchMode in the cache, then
// we are restoring search mode for this tab. There is no need to
// overwrite _searchModesByBrowser or record telemetry again.
return;
}
this._searchModesByBrowser.set(
this.window.gBrowser.selectedBrowser,
this.searchMode
);
try {
BrowserUsageTelemetry.recordSearchMode(this.searchMode);
} catch (ex) {
Cu.reportError(ex);
}
}
// Getters and Setters below.
get editor() {
@ -1583,30 +1643,41 @@ class UrlbarInput {
* Enters search mode and starts a new search if appropriate for the given
* result. See also _searchModeForResult.
*
* @param {UrlbarResult} result
* The currently selected urlbar result.
* @param {boolean} checkValue
* @param {string} entry
* The search mode entry point. See setSearchMode documentation for details.
* @param {UrlbarResult} [result]
* The result to promote. Defaults to the currently selected result.
* @param {boolean} [checkValue]
* If true, the trimmed input value must equal the result's keyword in order
* to enter search mode.
* @param {boolean} [startQuery]
* If true, start a query after entering search mode. Defaults to true.
* @returns {boolean}
* True if we entered search mode and false if not.
*/
maybePromoteKeywordToSearchMode(
maybePromoteKeywordToSearchMode({
entry,
result = this._resultForCurrentValue,
checkValue = true
) {
if (checkValue && this.value.trim() != result.payload.keyword?.trim()) {
checkValue = true,
startQuery = true,
}) {
if (
!result ||
(checkValue && this.value.trim() != result.payload.keyword?.trim())
) {
return false;
}
let searchMode = this._searchModeForResult(result, "typed");
let searchMode = this._searchModeForResult(result, entry);
if (!searchMode) {
return false;
}
this.setSearchMode(searchMode);
this._setValue(result.payload.query?.trimStart() || "", false);
this.startQuery({ allowAutofill: false });
if (startQuery) {
this.startQuery({ allowAutofill: false });
}
return true;
}
@ -2456,6 +2527,12 @@ class UrlbarInput {
}
this._focusUntrimmedValue = null;
// We exit previewed search mode on blur since the result previewing it is
// implictly unselected.
if (this.searchMode?.isPreview) {
this.setSearchMode({});
}
this.formatValue();
this._resetSearchState();

Просмотреть файл

@ -219,7 +219,7 @@ class ProviderTopSites extends UrlbarProvider {
...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
title: site.title,
keyword: site.title,
keywordOffer: UrlbarUtils.KEYWORD_OFFER.HIDE,
keywordOffer: UrlbarUtils.KEYWORD_OFFER.SHOW,
engine: engine.name,
query: "",
icon: site.favicon,

Просмотреть файл

@ -124,7 +124,10 @@ class UrlbarResult {
case UrlbarUtils.RESULT_TYPE.SEARCH:
switch (this.payload.keywordOffer) {
case UrlbarUtils.KEYWORD_OFFER.SHOW:
return [this.payload.keyword, this.payloadHighlights.keyword];
if (!UrlbarPrefs.get("update2")) {
return [this.payload.keyword, this.payloadHighlights.keyword];
}
// Fall through.
case UrlbarUtils.KEYWORD_OFFER.HIDE:
return ["", []];
}

Просмотреть файл

@ -218,6 +218,12 @@ class UrlbarSearchOneOffs extends SearchOneOffs {
break;
}
case "tab": {
if (params?.inBackground) {
// We will enter search mode in a background tab. We should enter full
// search mode right away so it is not cleared on Urlbar blur.
searchMode.isPreview = false;
}
let newTab = this.input.window.gBrowser.addTrustedTab("about:newtab");
this.input.setSearchModeForBrowser(searchMode, newTab.linkedBrowser);
if (userTypedSearchString) {

Просмотреть файл

@ -160,9 +160,9 @@ var UrlbarUtils = {
// when the user picks them. Depending on the use case, a keyword offer can
// visually show or hide the keyword itself in its result. For example,
// typing "@" by itself will show keyword offers for all engines with @
// aliases, and those results will visually show their keywords -- @google,
// @bing, etc. When a keyword offer is a heuristic -- like an autofilled @
// alias -- usually it hides its keyword since the user is already typing it.
// aliases, and those results will preview their search modes. When a keyword
// offer is a heuristic -- like an autofilled @ alias -- usually it hides
// its keyword since the user is already typing it.
KEYWORD_OFFER: {
SHOW: 1,
HIDE: 2,

Просмотреть файл

@ -420,6 +420,11 @@ var UrlbarTestUtils = {
return;
}
// Default to full search mode for less verbose tests.
if (!expectedSearchMode.hasOwnProperty("isPreview")) {
expectedSearchMode.isPreview = false;
}
this.Assert.deepEqual(
window.gURLBar.searchMode,
expectedSearchMode,
@ -481,7 +486,13 @@ var UrlbarTestUtils = {
// If this is an engine search mode, check that all results are either
// search results with the same engine or have the same host as the engine.
if (expectedSearchMode.engineName && this.isPopupOpen(window)) {
// Search mode preview can show other results since it is not supposed to
// start a query.
if (
expectedSearchMode.engineName &&
!expectedSearchMode.isPreview &&
this.isPopupOpen(window)
) {
let resultCount = this.getResultCount(window);
for (let i = 0; i < resultCount; i++) {
let result = await this.getDetailsOfResultAt(window, i);

Просмотреть файл

@ -156,6 +156,7 @@ support-files =
[browser_searchMode_engineRemoval.js]
[browser_searchMode_no_results.js]
[browser_searchMode_pickResult.js]
[browser_searchMode_preview.js]
[browser_searchMode_setURI.js]
[browser_searchMode_suggestions.js]
support-files =

Просмотреть файл

@ -7,7 +7,20 @@
"use strict";
add_task(async function test() {
await PlacesTestUtils.addVisits("http://example.com/");
for (let i = 0; i < 5; i++) {
await PlacesTestUtils.addVisits("http://example.com/");
}
// Update Top Sites to make sure the last Top Site is a URL. Otherwise, it
// would be a search shortcut and thus would not fill the Urlbar when
// selected.
await updateTopSites(sites => {
return (
sites &&
sites[sites.length - 1] &&
sites[sites.length - 1].url == "http://example.com/"
);
});
registerCleanupFunction(async function() {
await PlacesUtils.history.clear();
});

Просмотреть файл

@ -0,0 +1,168 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests search mode preview.
*/
"use strict";
const TEST_ENGINE_NAME = "Test";
add_task(async function setup() {
await SpecialPowers.pushPrefEnv({
set: [["browser.urlbar.update2", true]],
});
let testEngine = await Services.search.addEngineWithDetails(
TEST_ENGINE_NAME,
{
alias: "@test",
template: "http://example.com/?search={searchTerms}",
}
);
registerCleanupFunction(async () => {
await Services.search.removeEngine(testEngine);
});
});
// Tests that cycling through token alias engines enters search mode preview.
add_task(async function tokenAlias() {
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: "@",
});
let result;
while (gURLBar.searchMode?.engineName != TEST_ENGINE_NAME) {
EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
let index = UrlbarTestUtils.getSelectedRowIndex(window);
result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
let expectedSearchMode = {
engineName: result.searchParams.engine,
isPreview: true,
entry: "keywordoffer",
};
if (UrlbarUtils.WEB_ENGINE_NAMES.has(result.searchParams.engine)) {
expectedSearchMode.source = UrlbarUtils.RESULT_SOURCE.SEARCH;
}
await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode);
}
let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
EventUtils.synthesizeKey("KEY_Enter");
await searchPromise;
// Test that we are in non-preview search mode.
await UrlbarTestUtils.assertSearchMode(window, {
engineName: result.searchParams.engine,
entry: "keywordoffer",
});
await UrlbarTestUtils.exitSearchMode(window);
});
// Tests that starting to type a query exits search mode preview in favour of
// full search mode.
add_task(async function startTyping() {
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: "@",
});
while (gURLBar.searchMode?.engineName != TEST_ENGINE_NAME) {
EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
}
await UrlbarTestUtils.assertSearchMode(window, {
engineName: TEST_ENGINE_NAME,
isPreview: true,
entry: "keywordoffer",
});
let searchPromise = UrlbarTestUtils.promiseSearchComplete(window);
EventUtils.synthesizeKey("M");
await searchPromise;
await UrlbarTestUtils.assertSearchMode(window, {
engineName: TEST_ENGINE_NAME,
entry: "keywordoffer",
});
await UrlbarTestUtils.exitSearchMode(window);
});
// Tests that highlighting a search shortcut Top Site enters search mode
// preview.
add_task(async function topSites() {
// Enable search shortcut Top Sites.
await updateTopSites(
sites => sites && sites[0] && sites[0].searchTopSite,
true
);
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: "",
fireInputEvent: true,
});
// We previously verified that the first Top Site is a search shortcut.
EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
let searchTopSite = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
await UrlbarTestUtils.assertSearchMode(window, {
engineName: searchTopSite.searchParams.engine,
isPreview: true,
entry: "topsites_urlbar",
});
await UrlbarTestUtils.exitSearchMode(window);
});
// Tests that search mode preview is exited when the view is closed.
add_task(async function closeView() {
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: "@",
});
while (gURLBar.searchMode?.engineName != TEST_ENGINE_NAME) {
EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
}
await UrlbarTestUtils.assertSearchMode(window, {
engineName: TEST_ENGINE_NAME,
isPreview: true,
entry: "keywordoffer",
});
// We should close search mode when closing the view.
await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
await UrlbarTestUtils.assertSearchMode(window, null);
// Check search mode isn't re-entered when re-opening the view.
await UrlbarTestUtils.promisePopupOpen(window, () => {
if (gURLBar.getAttribute("pageproxystate") == "invalid") {
gURLBar.handleRevert();
}
EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
});
await UrlbarTestUtils.assertSearchMode(window, null);
await UrlbarTestUtils.promisePopupClose(window);
});
// Tests that search more preview is exited when the user switches tabs.
add_task(async function tabSwitch() {
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: "@",
});
while (gURLBar.searchMode?.engineName != TEST_ENGINE_NAME) {
EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
}
await UrlbarTestUtils.assertSearchMode(window, {
engineName: TEST_ENGINE_NAME,
isPreview: true,
entry: "keywordoffer",
});
// Open a new tab then switch back to the original tab.
let tab1 = gBrowser.selectedTab;
let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser);
await BrowserTestUtils.switchTab(gBrowser, tab1);
await UrlbarTestUtils.assertSearchMode(window, null);
BrowserTestUtils.removeTab(tab2);
});

Просмотреть файл

@ -349,12 +349,22 @@ add_task(async function nonHeuristicAliases() {
window,
tokenEngines.length - 1
);
// Key down to select each result in turn. The urlbar value should be set to
// each alias, and each should be highlighted.
// Key down to select each result in turn. The urlbar should preview search
// mode for each engine.
for (let { tokenAliases } of tokenEngines) {
let alias = tokenAliases[0];
let engineName = (await UrlbarSearchUtils.engineForAlias(alias)).name;
EventUtils.synthesizeKey("KEY_ArrowDown");
assertHighlighted(true, alias);
let expectedSearchMode = {
engineName,
entry: "keywordoffer",
isPreview: true,
};
if (UrlbarUtils.WEB_ENGINE_NAMES.has(engineName)) {
expectedSearchMode.source = UrlbarUtils.RESULT_SOURCE.SEARCH;
}
await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode);
Assert.ok(!gURLBar.value, "The Urlbar should be empty.");
}
await UrlbarTestUtils.promisePopupClose(window, () =>

Просмотреть файл

@ -600,7 +600,7 @@ const tests = [
fireInputEvent: true,
});
while (win.gURLBar.untrimmedValue != "@test") {
while (win.gURLBar.searchMode?.engineName != "Test") {
EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
}
EventUtils.synthesizeKey("VK_RETURN", {}, win);
@ -1241,7 +1241,7 @@ const tests = [
await UrlbarTestUtils.promisePopupOpen(win, () => {
EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
});
while (win.gURLBar.untrimmedValue != "@google") {
while (win.gURLBar.searchMode?.engineName != "Google") {
EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
}
let element = UrlbarTestUtils.getSelectedRow(win);

Просмотреть файл

@ -644,6 +644,11 @@ let BrowserUsageTelemetry = {
* details.
*/
recordSearchMode(searchMode) {
// Search mode preview is not search mode. Recording it would just create
// noise.
if (searchMode.isPreview) {
return;
}
let scalarKey;
if (searchMode.engineName) {
let engine = Services.search.getEngineByName(searchMode.engineName);