Bug 1221539 - Add search engine discovery to the page action menu. Part 2: Add the new action. r=Gijs

MozReview-Commit-ID: DEEZBwmV0JD

--HG--
extra : rebase_source : 71b68621198d72c069f505d922e92ed357eefe8b
This commit is contained in:
Drew Willcoxon 2018-03-28 11:28:20 -07:00
parent 36b7f691ee
commit e5e43736bc
16 changed files with 640 additions and 99 deletions

View File

@ -965,10 +965,29 @@ var BrowserPageActionFeedback = {
return this.feedbackLabel = document.getElementById("pageActionFeedbackMessage");
},
show(action, event, textContentOverride) {
this.feedbackLabel.textContent = this.panelNode.getAttribute((textContentOverride || action.id) + "Feedback");
/**
* Shows the feedback popup for an action.
*
* @param action (PageActions.Action, required)
* The action associated with the feedback.
* @param opts (object, optional)
* An object with the following optional properties:
* - event (DOM event): The event that triggered the feedback.
* - textAttributeOverride (string): Normally the feedback text is
* taken from an attribute on the feedback panel. The attribute's
* name is `${action.id}Feedback`. Use this to override the
* action.id part of the name.
* - text (string): The text string. If not given, an attribute on
* panel is assumed to contain the text, as described above.
*/
show(action, opts = {}) {
this.feedbackLabel.textContent =
opts.text ||
this.panelNode.getAttribute((opts.textAttributeOverride || action.id) +
"Feedback");
this.panelNode.hidden = false;
let event = opts.event || null;
let anchor = BrowserPageActions.panelAnchorNodeForAction(action, event);
PanelMultiView.openPopup(this.panelNode, anchor, {
position: "bottomcenter topright",
@ -1019,7 +1038,9 @@ BrowserPageActions.copyURL = {
.getService(Ci.nsIClipboardHelper)
.copyString(gURLBar.makeURIReadable(gBrowser.selectedBrowser.currentURI).displaySpec);
let action = PageActions.actionForID("copyURL");
BrowserPageActionFeedback.show(action, event);
BrowserPageActionFeedback.show(action, {
event,
});
},
};
@ -1096,8 +1117,11 @@ BrowserPageActions.sendToDevice = {
// in", "Learn about Sync", etc. Device items will be .sendtab-target.
if (event.target.classList.contains("sendtab-target")) {
let action = PageActions.actionForID("sendToDevice");
let textOverride = gSync.offline && "sendToDeviceOffline";
BrowserPageActionFeedback.show(action, event, textOverride);
let textAttributeOverride = gSync.offline && "sendToDeviceOffline";
BrowserPageActionFeedback.show(action, {
event,
textAttributeOverride,
});
}
});
return item;
@ -1120,3 +1144,118 @@ BrowserPageActions.sendToDevice = {
}
},
};
// add search engine
BrowserPageActions.addSearchEngine = {
get action() {
return PageActions.actionForID("addSearchEngine");
},
get engines() {
return gBrowser.selectedBrowser.engines || [];
},
get strings() {
delete this.strings;
let uri = "chrome://browser/locale/search.properties";
return this.strings = Services.strings.createBundle(uri);
},
updateEngines() {
// As a slight optimization, if the action isn't in the urlbar, don't do
// anything here except disable it. The action's panel nodes are updated
// when the panel is shown.
this.action.setDisabled(!this.engines.length, window);
if (this.action.shouldShowInUrlbar(window)) {
this._updateTitleAndIcon();
}
},
_updateTitleAndIcon() {
if (!this.engines.length) {
return;
}
let title =
this.engines.length == 1 ?
this.strings.formatStringFromName("searchAddFoundEngine",
[this.engines[0].title], 1) :
this.strings.GetStringFromName("searchAddFoundEngineMenu");
this.action.setTitle(title, window);
this.action.setIconURL(this.engines[0].icon, window);
},
onShowingInPanel() {
this._updateTitleAndIcon();
this.action.setWantsSubview(this.engines.length > 1, window);
let button = BrowserPageActions.panelButtonNodeForActionID(this.action.id);
button.classList.add("badged-button");
button.setAttribute("image", this.engines[0].icon);
button.setAttribute("uri", this.engines[0].uri);
button.setAttribute("crop", "center");
},
onSubviewShowing(panelViewNode) {
let body = panelViewNode.querySelector(".panel-subview-body");
while (body.firstChild) {
body.firstChild.remove();
}
for (let engine of this.engines) {
let button = document.createElement("toolbarbutton");
button.classList.add("subviewbutton", "subviewbutton-iconic");
button.setAttribute("label", engine.title);
button.setAttribute("image", engine.icon);
button.setAttribute("uri", engine.uri);
button.addEventListener("command", event => {
PanelMultiView.hidePopup(BrowserPageActions.panelNode);
this._handleClickOnEngineButton(button);
});
body.appendChild(button);
}
},
onCommand(event, buttonNode) {
if (!buttonNode.closest("panel")) {
// The urlbar button was clicked. It should have a subview if there are
// many engines.
let manyEngines = this.engines.length > 1;
this.action.setWantsSubview(manyEngines, window);
if (manyEngines) {
return;
}
}
this._handleClickOnEngineButton(buttonNode);
},
_handleClickOnEngineButton(button) {
this._installEngine(button.getAttribute("uri"),
button.getAttribute("image"));
},
_installEngine(uri, image) {
Services.search.addEngine(uri, null, image, false, {
onSuccess: engine => {
BrowserPageActionFeedback.show(this.action, {
text: this.strings.GetStringFromName("searchAddedFoundEngine"),
});
},
onError(errorCode) {
if (errorCode != Ci.nsISearchInstallCallback.ERROR_DUPLICATE_ENGINE) {
// Download error is shown by the search service
return;
}
const kSearchBundleURI = "chrome://global/locale/search/search.properties";
let searchBundle = Services.strings.createBundle(kSearchBundleURI);
let brandBundle = document.getElementById("bundle_brand");
let brandName = brandBundle.getString("brandShortName");
let title = searchBundle.GetStringFromName("error_invalid_engine_title");
let text = searchBundle.formatStringFromName("error_duplicate_engine_msg",
[brandName, uri], 2);
Services.prompt.QueryInterface(Ci.nsIPromptFactory);
let prompt = Services.prompt.getPrompt(gBrowser.contentWindow, Ci.nsIPrompt);
prompt.QueryInterface(Ci.nsIWritablePropertyBag2);
prompt.setPropertyAsBool("allowTabModal", true);
prompt.alert(title, text);
},
});
},
};

View File

@ -1346,6 +1346,7 @@ var gBrowserInit = {
TabletModeUpdater.init();
CombinedStopReload.ensureInitialized();
gPrivateBrowsingUI.init();
BrowserSearch.init();
BrowserPageActions.init();
gAccessibilityServiceIndicator.init();
@ -1876,6 +1877,8 @@ var gBrowserInit = {
LanguagePrompt.uninit();
BrowserSearch.uninit();
// Now either cancel delayedStartup, or clean up the services initialized from
// it.
if (this._boundDelayedStartup) {
@ -3747,6 +3750,83 @@ const DOMEventHandler = {
};
const BrowserSearch = {
init() {
Services.obs.addObserver(this, "browser-search-engine-modified");
},
uninit() {
Services.obs.removeObserver(this, "browser-search-engine-modified");
},
observe(engine, topic, data) {
// There are two kinds of search engine objects, nsISearchEngine objects and
// plain { uri, title, icon } objects. `engine` in this method is the
// former. The browser.engines and browser.hiddenEngines arrays are the
// latter, and they're the engines offered by the the page in the browser.
//
// The two types of engines are currently related by their titles/names,
// although that may change; see bug 335102.
let engineName = engine.wrappedJSObject.name;
switch (data) {
case "engine-removed":
// An engine was removed from the search service. If a page is offering
// the engine, then the engine needs to be added back to the corresponding
// browser's offered engines.
this._addMaybeOfferedEngine(engineName);
break;
case "engine-added":
// An engine was added to the search service. If a page is offering the
// engine, then the engine needs to be removed from the corresponding
// browser's offered engines.
this._removeMaybeOfferedEngine(engineName);
break;
}
},
_addMaybeOfferedEngine(engineName) {
let selectedBrowserOffersEngine = false;
for (let browser of gBrowser.browsers) {
for (let i = 0; i < (browser.hiddenEngines || []).length; i++) {
if (browser.hiddenEngines[i].title == engineName) {
if (!browser.engines) {
browser.engines = [];
}
browser.engines.push(browser.hiddenEngines[i]);
browser.hiddenEngines.splice(i, 1);
if (browser == gBrowser.selectedBrowser) {
selectedBrowserOffersEngine = true;
}
break;
}
}
}
if (selectedBrowserOffersEngine) {
this.updateOpenSearchBadge();
}
},
_removeMaybeOfferedEngine(engineName) {
let selectedBrowserOffersEngine = false;
for (let browser of gBrowser.browsers) {
for (let i = 0; i < (browser.engines || []).length; i++) {
if (browser.engines[i].title == engineName) {
if (!browser.hiddenEngines) {
browser.hiddenEngines = [];
}
browser.hiddenEngines.push(browser.engines[i]);
browser.engines.splice(i, 1);
if (browser == gBrowser.selectedBrowser) {
selectedBrowserOffersEngine = true;
}
break;
}
}
}
if (selectedBrowserOffersEngine) {
this.updateOpenSearchBadge();
}
},
addEngine(browser, engine, uri) {
// Check to see whether we've already added an engine with this title
if (browser.engines) {
@ -3784,6 +3864,8 @@ const BrowserSearch = {
* has search engines.
*/
updateOpenSearchBadge() {
BrowserPageActions.addSearchEngine.updateEngines();
var searchBar = this.searchBar;
if (!searchBar)
return;

View File

@ -42,6 +42,14 @@ skip-if = true # Bug 1315887
[browser_moz_action_link.js]
[browser_new_tab_urlbar_reset.js]
[browser_page_action_menu.js]
[browser_page_action_menu_add_search_engine.js]
support-files =
page_action_menu_add_search_engine_one.html
page_action_menu_add_search_engine_many.html
page_action_menu_add_search_engine_same_names.html
page_action_menu_add_search_engine_0.xml
page_action_menu_add_search_engine_1.xml
page_action_menu_add_search_engine_2.xml
[browser_page_action_menu_clipboard.js]
subsuite = clipboard
[browser_pasteAndGo.js]

View File

@ -0,0 +1,305 @@
"use strict";
// Checks a page that doesn't offer any engines.
add_task(async function none() {
let url = "http://mochi.test:8888/";
await BrowserTestUtils.withNewTab(url, async () => {
// Open the panel.
await promisePageActionPanelOpen();
EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
await promisePageActionPanelHidden();
// The action should not be present.
let actions = PageActions.actionsInPanel(window);
Assert.ok(!actions.some(a => a.id == "addSearchEngine"),
"Action should not be present in panel");
let button =
BrowserPageActions.panelButtonNodeForActionID("addSearchEngine");
Assert.ok(!button, "Action button should not be in panel");
});
});
// Checks a page that offers one engine.
add_task(async function one() {
let url = getRootDirectory(gTestPath) + "page_action_menu_add_search_engine_one.html";
await BrowserTestUtils.withNewTab(url, async () => {
// Open the panel.
await promisePageActionPanelOpen();
// The action should be present.
let actions = PageActions.actionsInPanel(window);
let action = actions.find(a => a.id == "addSearchEngine");
Assert.ok(action, "Action should be present in panel");
let expectedTitle =
"Add \u{201C}page_action_menu_add_search_engine_0\u{201D} to One-Click Search";
Assert.equal(action.getTitle(window), expectedTitle, "Action title");
let button =
BrowserPageActions.panelButtonNodeForActionID("addSearchEngine");
Assert.ok(button, "Button should be in panel");
Assert.equal(button.label, expectedTitle, "Button label");
Assert.equal(button.classList.contains("subviewbutton-nav"), false,
"Button should not expand into a subview");
// Click the action's button.
let enginePromise =
promiseEngine("engine-added", "page_action_menu_add_search_engine_0");
let hiddenPromise = promisePageActionPanelHidden();
let feedbackPromise = promiseFeedbackPanelHidden();
EventUtils.synthesizeMouseAtCenter(button, {});
await hiddenPromise;
let engine = await enginePromise;
let feedbackText = await feedbackPromise;
Assert.equal(feedbackText, "Added to Search Dropdown");
// Open the panel again.
await promisePageActionPanelOpen();
EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
await promisePageActionPanelHidden();
// The action should be gone.
actions = PageActions.actionsInPanel(window);
action = actions.find(a => a.id == "addSearchEngine");
Assert.ok(!action, "Action should not be present in panel");
button = BrowserPageActions.panelButtonNodeForActionID("addSearchEngine");
Assert.ok(!button, "Action button should not be in panel");
// Remove the engine.
enginePromise =
promiseEngine("engine-removed", "page_action_menu_add_search_engine_0");
Services.search.removeEngine(engine);
await enginePromise;
// Open the panel again.
await promisePageActionPanelOpen();
EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
await promisePageActionPanelHidden();
// The action should be present again.
actions = PageActions.actionsInPanel(window);
action = actions.find(a => a.id == "addSearchEngine");
Assert.ok(action, "Action should be present in panel");
Assert.equal(action.getTitle(window), expectedTitle, "Action title");
button = BrowserPageActions.panelButtonNodeForActionID("addSearchEngine");
Assert.ok(button, "Action button should be in panel");
Assert.equal(button.label, expectedTitle, "Button label");
Assert.equal(button.classList.contains("subviewbutton-nav"), false,
"Button should not expand into a subview");
});
});
// Checks a page that offers many engines.
add_task(async function many() {
let url = getRootDirectory(gTestPath) + "page_action_menu_add_search_engine_many.html";
await BrowserTestUtils.withNewTab(url, async () => {
// Open the panel.
await promisePageActionPanelOpen();
// The action should be present.
let actions = PageActions.actionsInPanel(window);
let action = actions.find(a => a.id == "addSearchEngine");
Assert.ok(action, "Action should be present in panel");
let expectedTitle = "Add One-Click Search Engine";
Assert.equal(action.getTitle(window), expectedTitle, "Action title");
let button =
BrowserPageActions.panelButtonNodeForActionID("addSearchEngine");
Assert.ok(button, "Action button should be in panel");
Assert.equal(button.label, expectedTitle, "Button label");
Assert.equal(button.classList.contains("subviewbutton-nav"), true,
"Button should expand into a subview");
// Click the action's button. The subview should be shown.
let viewPromise = promisePageActionViewShown();
EventUtils.synthesizeMouseAtCenter(button, {});
let view = await viewPromise;
let viewID =
BrowserPageActions._panelViewNodeIDForActionID("addSearchEngine", false);
Assert.equal(view.id, viewID, "View ID");
let bodyID = viewID + "-body";
let body = document.getElementById(bodyID);
Assert.deepEqual(
Array.map(body.childNodes, n => n.label),
[
"page_action_menu_add_search_engine_0",
"page_action_menu_add_search_engine_1",
"page_action_menu_add_search_engine_2",
],
"Subview children"
);
// Click the first engine to install it.
let enginePromise =
promiseEngine("engine-added", "page_action_menu_add_search_engine_0");
let hiddenPromise = promisePageActionPanelHidden();
let feedbackPromise = promiseFeedbackPanelHidden();
EventUtils.synthesizeMouseAtCenter(body.childNodes[0], {});
await hiddenPromise;
let engines = [];
let engine = await enginePromise;
engines.push(engine);
let feedbackText = await feedbackPromise;
Assert.equal(feedbackText, "Added to Search Dropdown", "Feedback text");
// Open the panel and show the subview again. The installed engine should
// be gone.
await promisePageActionPanelOpen();
viewPromise = promisePageActionViewShown();
EventUtils.synthesizeMouseAtCenter(button, {});
await viewPromise;
Assert.deepEqual(
Array.map(body.childNodes, n => n.label),
[
"page_action_menu_add_search_engine_1",
"page_action_menu_add_search_engine_2",
],
"Subview children"
);
// Click the next engine to install it.
enginePromise =
promiseEngine("engine-added", "page_action_menu_add_search_engine_1");
hiddenPromise = promisePageActionPanelHidden();
feedbackPromise = promiseFeedbackPanelHidden();
EventUtils.synthesizeMouseAtCenter(body.childNodes[0], {});
await hiddenPromise;
engine = await enginePromise;
engines.push(engine);
feedbackText = await feedbackPromise;
Assert.equal(feedbackText, "Added to Search Dropdown", "Feedback text");
// Open the panel again. This time the action button should show the one
// remaining engine.
await promisePageActionPanelOpen();
actions = PageActions.actionsInPanel(window);
action = actions.find(a => a.id == "addSearchEngine");
Assert.ok(action, "Action should be present in panel");
expectedTitle =
"Add \u{201C}page_action_menu_add_search_engine_2\u{201D} to One-Click Search";
Assert.equal(action.getTitle(window), expectedTitle, "Action title");
button = BrowserPageActions.panelButtonNodeForActionID("addSearchEngine");
Assert.ok(button, "Button should be present in panel");
Assert.equal(button.label, expectedTitle, "Button label");
Assert.equal(button.classList.contains("subviewbutton-nav"), false,
"Button should not expand into a subview");
// Click the button.
enginePromise =
promiseEngine("engine-added", "page_action_menu_add_search_engine_2");
hiddenPromise = promisePageActionPanelHidden();
feedbackPromise = promiseFeedbackPanelHidden();
EventUtils.synthesizeMouseAtCenter(button, {});
await hiddenPromise;
engine = await enginePromise;
engines.push(engine);
feedbackText = await feedbackPromise;
Assert.equal(feedbackText, "Added to Search Dropdown", "Feedback text");
// All engines are installed at this point. Open the panel and make sure
// the action is gone.
await promisePageActionPanelOpen();
EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
await promisePageActionPanelHidden();
actions = PageActions.actionsInPanel(window);
action = actions.find(a => a.id == "addSearchEngine");
Assert.ok(!action, "Action should be gone");
button = BrowserPageActions.panelButtonNodeForActionID("addSearchEngine");
Assert.ok(!button, "Button should not be in panel");
// Remove the first engine.
enginePromise =
promiseEngine("engine-removed", "page_action_menu_add_search_engine_0");
Services.search.removeEngine(engines.shift());
await enginePromise;
// Open the panel again. The action should be present and showing the first
// engine.
await promisePageActionPanelOpen();
EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
await promisePageActionPanelHidden();
actions = PageActions.actionsInPanel(window);
action = actions.find(a => a.id == "addSearchEngine");
Assert.ok(action, "Action should be present in panel");
expectedTitle =
"Add \u{201C}page_action_menu_add_search_engine_0\u{201D} to One-Click Search";
Assert.equal(action.getTitle(window), expectedTitle, "Action title");
button = BrowserPageActions.panelButtonNodeForActionID("addSearchEngine");
Assert.ok(button, "Button should be present in panel");
Assert.equal(button.label, expectedTitle, "Button label");
Assert.equal(button.classList.contains("subviewbutton-nav"), false,
"Button should not expand into a subview");
// Remove the second engine.
enginePromise =
promiseEngine("engine-removed", "page_action_menu_add_search_engine_1");
Services.search.removeEngine(engines.shift());
await enginePromise;
// Open the panel again and check the subview. The subview should be
// present now that there are two offerred engines again.
await promisePageActionPanelOpen();
actions = PageActions.actionsInPanel(window);
action = actions.find(a => a.id == "addSearchEngine");
Assert.ok(action, "Action should be present in panel");
expectedTitle = "Add One-Click Search Engine";
Assert.equal(action.getTitle(window), expectedTitle, "Action title");
button = BrowserPageActions.panelButtonNodeForActionID("addSearchEngine");
Assert.ok(button, "Button should be in panel");
Assert.equal(button.label, expectedTitle, "Button label");
Assert.equal(button.classList.contains("subviewbutton-nav"), true,
"Button should expand into a subview");
viewPromise = promisePageActionViewShown();
EventUtils.synthesizeMouseAtCenter(button, {});
await viewPromise;
body = document.getElementById(bodyID);
Assert.deepEqual(
Array.map(body.childNodes, n => n.label),
[
"page_action_menu_add_search_engine_0",
"page_action_menu_add_search_engine_1",
],
"Subview children"
);
EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
await promisePageActionPanelHidden();
// Remove the third engine.
enginePromise =
promiseEngine("engine-removed", "page_action_menu_add_search_engine_2");
Services.search.removeEngine(engines.shift());
await enginePromise;
// Open the panel again and check the subview.
await promisePageActionPanelOpen();
viewPromise = promisePageActionViewShown();
EventUtils.synthesizeMouseAtCenter(button, {});
await viewPromise;
Assert.deepEqual(
Array.map(body.childNodes, n => n.label),
[
"page_action_menu_add_search_engine_0",
"page_action_menu_add_search_engine_1",
"page_action_menu_add_search_engine_2",
],
"Subview children"
);
EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
await promisePageActionPanelHidden();
});
});
function promiseEngine(expectedData, expectedEngineName) {
return TestUtils.topicObserved("browser-search-engine-modified", (engine, data) => {
return expectedData == data &&
expectedEngineName == engine.wrappedJSObject.name;
}).then(([engine, data]) => engine);
}
function promiseFeedbackPanelHidden() {
return new Promise(resolve => {
BrowserPageActionFeedback.panelNode.addEventListener("popuphidden", event => {
resolve(BrowserPageActionFeedback.feedbackLabel.textContent);
}, {once: true});
});
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
<ShortName>page_action_menu_add_search_engine_0</ShortName>
<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform">
<Param name="terms" value="{searchTerms}"/>
</Url>
</SearchPlugin>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
<ShortName>page_action_menu_add_search_engine_1</ShortName>
<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform">
<Param name="terms" value="{searchTerms}"/>
</Url>
</SearchPlugin>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
<ShortName>page_action_menu_add_search_engine_2</ShortName>
<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform">
<Param name="terms" value="{searchTerms}"/>
</Url>
</SearchPlugin>

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="search" type="application/opensearchdescription+xml" title="page_action_menu_add_search_engine_0" href="http://mochi.test:8888/browser/browser/base/content/test/urlbar/page_action_menu_add_search_engine_0.xml">
<link rel="search" type="application/opensearchdescription+xml" title="page_action_menu_add_search_engine_1" href="http://mochi.test:8888/browser/browser/base/content/test/urlbar/page_action_menu_add_search_engine_1.xml">
<link rel="search" type="application/opensearchdescription+xml" title="page_action_menu_add_search_engine_2" href="http://mochi.test:8888/browser/browser/base/content/test/urlbar/page_action_menu_add_search_engine_2.xml">
</head>
<body></body>
</html>

View File

@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="search" type="application/opensearchdescription+xml" title="page_action_menu_add_search_engine_0" href="http://mochi.test:8888/browser/browser/base/content/test/urlbar/page_action_menu_add_search_engine_0.xml">
</head>
<body></body>
</html>

View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="search" type="application/opensearchdescription+xml" title="page_action_menu_add_search_engine_0" href="http://mochi.test:8888/browser/browser/base/content/test/urlbar/page_action_menu_add_search_engine_0.xml">
<link rel="search" type="application/opensearchdescription+xml" title="page_action_menu_add_search_engine_1" href="http://mochi.test:8888/browser/browser/base/content/test/urlbar/page_action_menu_add_search_engine_0.xml">
</head>
<body></body>
</html>

View File

@ -188,18 +188,6 @@
<parameter name="aVerb"/>
<body><![CDATA[
if (aTopic == "browser-search-engine-modified") {
switch (aVerb) {
case "engine-removed":
this.offerNewEngine(aEngine);
break;
case "engine-added":
this.hideNewEngine(aEngine);
break;
case "engine-changed":
// An engine was removed (or hidden) or added, or an icon was
// changed. Do nothing special.
}
// Make sure the engine list is refetched next time it's needed
this._engines = null;
@ -210,75 +198,6 @@
]]></body>
</method>
<!-- There are two seaprate lists of search engines, whose uses intersect
in this file. The search service (nsIBrowserSearchService and
nsSearchService.js) maintains a list of Engine objects which is used to
populate the searchbox list of available engines and to perform queries.
That list is accessed here via this.SearchService, and it's that sort of
Engine that is passed to this binding's observer as aEngine.
In addition, browser.js fills two lists of autodetected search engines
(browser.engines and browser.hiddenEngines) as properties of
selectedBrowser. Those lists contain unnamed JS objects of the form
{ uri:, title:, icon: }, and that's what the searchbar uses to determine
whether to show any "Add <EngineName>" menu items in the drop-down.
The two types of engines are currently related by their identifying
titles (the Engine object's 'name'), although that may change; see bug
335102. -->
<!-- If the engine that was just removed from the searchbox list was
autodetected on this page, move it to each browser's active list so it
will be offered to be added again. -->
<method name="offerNewEngine">
<parameter name="aEngine"/>
<body><![CDATA[
for (let browser of gBrowser.browsers) {
if (browser.hiddenEngines) {
// XXX This will need to be changed when engines are identified by
// URL rather than title; see bug 335102.
var removeTitle = aEngine.wrappedJSObject.name;
for (var i = 0; i < browser.hiddenEngines.length; i++) {
if (browser.hiddenEngines[i].title == removeTitle) {
if (!browser.engines)
browser.engines = [];
browser.engines.push(browser.hiddenEngines[i]);
browser.hiddenEngines.splice(i, 1);
break;
}
}
}
}
BrowserSearch.updateOpenSearchBadge();
]]></body>
</method>
<!-- If the engine that was just added to the searchbox list was
autodetected on this page, move it to each browser's hidden list so it is
no longer offered to be added. -->
<method name="hideNewEngine">
<parameter name="aEngine"/>
<body><![CDATA[
for (let browser of gBrowser.browsers) {
if (browser.engines) {
// XXX This will need to be changed when engines are identified by
// URL rather than title; see bug 335102.
var removeTitle = aEngine.wrappedJSObject.name;
for (var i = 0; i < browser.engines.length; i++) {
if (browser.engines[i].title == removeTitle) {
if (!browser.hiddenEngines)
browser.hiddenEngines = [];
browser.hiddenEngines.push(browser.engines[i]);
browser.engines.splice(i, 1);
break;
}
}
}
}
BrowserSearch.updateOpenSearchBadge();
]]></body>
</method>
<method name="setIcon">
<parameter name="element"/>
<parameter name="uri"/>

View File

@ -33,6 +33,10 @@ cmd_addFoundEngine=Add “%S”
# grouped in a submenu using cmd_addFoundEngineMenu as a label.
cmd_addFoundEngineMenu=Add search engine
searchAddFoundEngine=Add “%S” to One-Click Search
searchAddFoundEngineMenu=Add One-Click Search Engine
searchAddedFoundEngine=Added to Search Dropdown
# LOCALIZATION NOTE (searchForSomethingWith2):
# This string is used to build the header above the list of one-click
# search providers: "Search for <user-typed string> with:"

View File

@ -1132,6 +1132,25 @@ var gBuiltInActions = [
browserPageActions(buttonNode).emailLink.onCommand(event, buttonNode);
},
},
// add search engine
{
id: "addSearchEngine",
// The title is set in browser-pageActions.js.
title: "",
_transient: true,
onShowingInPanel(buttonNode) {
browserPageActions(buttonNode).addSearchEngine.onShowingInPanel();
},
onCommand(event, buttonNode) {
browserPageActions(buttonNode).addSearchEngine
.onCommand(event, buttonNode);
},
onSubviewShowing(panelViewNode) {
browserPageActions(panelViewNode).addSearchEngine
.onSubviewShowing(panelViewNode);
},
},
];
if (Services.prefs.getBoolPref("identity.fxaccounts.enabled")) {

View File

@ -164,9 +164,9 @@ add_task(async function simple() {
initialActionsInUrlbar,
"Actions in urlbar after adding the action");
// Check the list of all actions.
Assert.deepEqual(PageActions.actions,
initialActions.concat([action]),
// Check the set of all actions.
Assert.deepEqual(new Set(PageActions.actions),
new Set(initialActions.concat([action])),
"All actions after adding the action");
Assert.deepEqual(PageActions.actionForID(action.id), action,
@ -817,15 +817,15 @@ add_task(async function nonBuiltFirst() {
// Check the actions.
Assert.deepEqual(
PageActions.actions.map(a => a.id),
initialActions.map(a => a.id).concat(
new Set(PageActions.actions.map(a => a.id)),
new Set(initialActions.map(a => a.id).concat(
[action.id]
),
)),
"All actions should be in PageActions.actions"
);
Assert.deepEqual(
PageActions._builtInActions.map(a => a.id),
initialActions.map(a => a.id),
initialActions.filter(a => !a.__transient).map(a => a.id),
"PageActions._builtInActions should be initial actions"
);
Assert.deepEqual(
@ -866,7 +866,7 @@ add_task(async function nonBuiltFirst() {
);
Assert.deepEqual(
PageActions._builtInActions.map(a => a.id),
initialActions.map(a => a.id),
initialActions.filter(a => !a.__transient).map(a => a.id),
"PageActions._builtInActions should be initial actions"
);
Assert.deepEqual(

View File

@ -163,6 +163,23 @@
list-style-image: url("chrome://browser/skin/sync.svg");
}
#pageAction-panel-addSearchEngine > .toolbarbutton-badge-stack > .toolbarbutton-icon {
width: 16px;
height: 16px;
}
#pageAction-panel-addSearchEngine > .toolbarbutton-badge-stack > .toolbarbutton-badge {
display: -moz-box;
background: url(chrome://browser/skin/search-indicator-badge-add.svg) no-repeat center;
box-shadow: none;
/* "!important" is necessary to override the rule in toolbarbutton.css */
margin: -4px 0 0 !important;
margin-inline-end: -4px !important;
width: 11px;
height: 11px;
min-width: 11px;
min-height: 11px;
}
/* URL bar and page action buttons */
#page-action-buttons {

View File

@ -6429,7 +6429,7 @@
"expires_in_version": "64",
"kind": "categorical",
"labels": ["bookmark", "pocket", "screenshots", "webcompat", "copyURL", "emailLink",
"sendToDevice", "other"],
"sendToDevice", "other", "addSearchEngine"],
"description": "Count how many times people add items to the url bar"
},
"FX_PAGE_ACTION_REMOVED": {
@ -6439,7 +6439,7 @@
"expires_in_version": "64",
"kind": "categorical",
"labels": ["bookmark", "pocket", "screenshots", "webcompat", "copyURL", "emailLink",
"sendToDevice", "other"],
"sendToDevice", "other", "addSearchEngine"],
"description": "Count how many times people remove items from the url bar"
},
"FX_PAGE_ACTION_MANAGED": {
@ -6449,7 +6449,7 @@
"expires_in_version": "64",
"kind": "categorical",
"labels": ["bookmark", "pocket", "screenshots", "webcompat", "copyURL", "emailLink",
"sendToDevice", "other"],
"sendToDevice", "other", "addSearchEngine"],
"description": "Count how many times people manage extensions via their actions in the url bar"
},
"FX_PAGE_ACTION_URLBAR_USED": {
@ -6459,7 +6459,7 @@
"expires_in_version": "64",
"kind": "categorical",
"labels": ["bookmark", "pocket", "screenshots", "webcompat", "copyURL", "emailLink",
"sendToDevice", "other"],
"sendToDevice", "other", "addSearchEngine"],
"description": "Count how many times people use items in the url bar"
},
"FX_PAGE_ACTION_PANEL_USED": {
@ -6469,7 +6469,7 @@
"expires_in_version": "64",
"kind": "categorical",
"labels": ["bookmark", "pocket", "screenshots", "webcompat", "copyURL", "emailLink",
"sendToDevice", "other"],
"sendToDevice", "other", "addSearchEngine"],
"description": "Count how many times people use items from the main page action button"
},
"INPUT_EVENT_RESPONSE_MS": {