--- a/browser/base/content/browser-pageActions.js
+++ b/browser/base/content/browser-pageActions.js
@@ -960,20 +960,39 @@ var BrowserPageActionFeedback = {
return this.feedbackAnimationBox = document.getElementById("pageActionFeedbackAnimatableBox");
},
get feedbackLabel() {
delete this.feedbackLabel;
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",
triggerEvent: event,
}).catch(Cu.reportError);
this.panelNode.addEventListener("popupshown", () => {
this.feedbackAnimationBox.setAttribute("animate", "true");
@@ -1014,17 +1033,19 @@ BrowserPageActions.copyURL = {
},
onCommand(event, buttonNode) {
PanelMultiView.hidePopup(BrowserPageActions.panelNode);
Cc["@mozilla.org/widget/clipboardhelper;1"]
.getService(Ci.nsIClipboardHelper)
.copyString(gURLBar.makeURIReadable(gBrowser.selectedBrowser.currentURI).displaySpec);
let action = PageActions.actionForID("copyURL");
- BrowserPageActionFeedback.show(action, event);
+ BrowserPageActionFeedback.show(action, {
+ event,
+ });
},
};
// email link
BrowserPageActions.emailLink = {
onPlacedInPanel(buttonNode) {
let action = PageActions.actionForID("emailLink");
BrowserPageActions.takeActionTitleFromPanel(action);
@@ -1091,18 +1112,21 @@ BrowserPageActions.sendToDevice = {
item.addEventListener("command", event => {
if (panelNode) {
PanelMultiView.hidePopup(panelNode);
}
// There are items in the subview that don't represent devices: "Sign
// 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;
});
bodyNode.removeAttribute("state");
// In the first ~10 sec after startup, Sync may not be loaded and the list
// of devices will be empty.
@@ -1115,8 +1139,123 @@ BrowserPageActions.sendToDevice = {
// anyway to avoid infinite looping.
if (!window.closed && !gSync.syncConfiguredAndLoading) {
this.onShowingSubview(panelViewNode);
}
});
}
},
};
+
+// 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);
+ },
+ });
+ },
+};
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1341,16 +1341,17 @@ var gBrowserInit = {
// adjust browser UI for popups
gURLBar.setAttribute("readonly", "true");
}
// Misc. inits.
TabletModeUpdater.init();
CombinedStopReload.ensureInitialized();
gPrivateBrowsingUI.init();
+ BrowserSearch.init();
BrowserPageActions.init();
gAccessibilityServiceIndicator.init();
gRemoteControl.updateVisualCue(Marionette.running);
// If we are given a tab to swap in, take care of it before first paint to
// avoid an about:blank flash.
let tabToOpen = window.arguments && window.arguments[0];
@@ -1871,16 +1872,18 @@ var gBrowserInit = {
SidebarUI.uninit();
DownloadsButton.uninit();
gAccessibilityServiceIndicator.uninit();
LanguagePrompt.uninit();
+ BrowserSearch.uninit();
+
// Now either cancel delayedStartup, or clean up the services initialized from
// it.
if (this._boundDelayedStartup) {
this._cancelDelayedStartup();
} else {
if (Win7Features)
Win7Features.onCloseWindow();
@@ -3742,16 +3745,93 @@ const DOMEventHandler = {
if (!tab)
return;
BrowserSearch.addEngine(aBrowser, aEngine, makeURI(aURL));
},
};
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) {
if (browser.engines.some(e => e.title == engine.title))
return;
}
var hidden = false;
@@ -3779,16 +3859,18 @@ const BrowserSearch = {
},
/**
* Update the browser UI to show whether or not additional engines are
* available when a page is loaded or the user switches tabs to a page that
* has search engines.
*/
updateOpenSearchBadge() {
+ BrowserPageActions.addSearchEngine.updateEngines();
+
var searchBar = this.searchBar;
if (!searchBar)
return;
var engines = gBrowser.selectedBrowser.engines;
if (engines && engines.length > 0)
searchBar.setAttribute("addengines", "true");
else
--- a/browser/base/content/test/urlbar/browser.ini
+++ b/browser/base/content/test/urlbar/browser.ini
@@ -37,16 +37,24 @@ support-files =
[browser_canonizeURL.js]
[browser_dragdropURL.js]
[browser_locationBarCommand.js]
[browser_locationBarExternalLoad.js]
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]
subsuite = clipboard
[browser_removeUnsafeProtocolsFromURLBarPaste.js]
subsuite = clipboard
[browser_search_favicon.js]
[browser_tabMatchesInAwesomebar.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_page_action_menu_add_search_engine.js
@@ -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});
+ });
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/urlbar/page_action_menu_add_search_engine_0.xml
@@ -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>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/urlbar/page_action_menu_add_search_engine_1.xml
@@ -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>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/urlbar/page_action_menu_add_search_engine_2.xml
@@ -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>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/urlbar/page_action_menu_add_search_engine_many.html
@@ -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>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/urlbar/page_action_menu_add_search_engine_one.html
@@ -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>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/urlbar/page_action_menu_add_search_engine_same_names.html
@@ -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>
--- a/browser/components/search/content/search.xml
+++ b/browser/components/search/content/search.xml
@@ -183,107 +183,26 @@
</method>
<method name="observe">
<parameter name="aEngine"/>
<parameter name="aTopic"/>
<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;
// Update the popup header and update the display after any modification.
this._textbox.popup.updateHeader();
this.updateDisplay();
}
]]></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"/>
<body><![CDATA[
element.setAttribute("src", uri);
]]></body>
</method>
--- a/browser/locales/en-US/chrome/browser/search.properties
+++ b/browser/locales/en-US/chrome/browser/search.properties
@@ -28,16 +28,20 @@ cmd_showSuggestions_accesskey=S
# menuitem at the bottom of the search panel.
cmd_addFoundEngine=Add ā%Sā
# LOCALIZATION NOTE (cmd_addFoundEngineMenu): When more than 5 engines
# are offered by a web page, instead of listing all of them in the
# search panel using the cmd_addFoundEngine string, they will be
# 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:"
searchForSomethingWith2=Search for %S with:
# LOCALIZATION NOTE (searchWithHeader):
# The wording of this string should be as close as possible to
# searchForSomethingWith2. This string will be used when the user
--- a/browser/modules/PageActions.jsm
+++ b/browser/modules/PageActions.jsm
@@ -1127,16 +1127,35 @@ var gBuiltInActions = [
title: "emailLink-title",
onPlacedInPanel(buttonNode) {
browserPageActions(buttonNode).emailLink.onPlacedInPanel(buttonNode);
},
onCommand(event, buttonNode) {
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")) {
gBuiltInActions.push(
// send to device
{
id: "sendToDevice",
title: "sendToDevice-title",
--- a/browser/modules/test/browser/browser_PageActions.js
+++ b/browser/modules/test/browser/browser_PageActions.js
@@ -159,19 +159,19 @@ add_task(async function simple() {
"Actions in panel after adding the action");
// The actions in the urlbar should be the same since the test action isn't
// shown there.
Assert.deepEqual(PageActions.actionsInUrlbar(window),
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,
"actionForID should be action");
Assert.ok(PageActions._persistedActions.ids.includes(action.id),
"PageActions should record action in its list of seen actions");
@@ -812,25 +812,25 @@ add_task(async function nonBuiltFirst()
// Now add back all the actions.
for (let a of initialActions) {
PageActions.addAction(a);
}
// 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(
PageActions._nonBuiltInActions.map(a => a.id),
[action.id],
"PageActions._nonBuiltInActions should contain action"
);
@@ -861,17 +861,17 @@ add_task(async function nonBuiltFirst()
// Check the actions.
Assert.deepEqual(
PageActions.actions.map(a => a.id),
initialActions.map(a => a.id),
"Action should no longer 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(
PageActions._nonBuiltInActions.map(a => a.id),
[],
"Action should no longer be in PageActions._nonBuiltInActions"
);
--- a/browser/themes/shared/urlbar-searchbar.inc.css
+++ b/browser/themes/shared/urlbar-searchbar.inc.css
@@ -158,16 +158,33 @@
}
.pageAction-sendToDevice-device.signintosync,
#pageAction-panel-sendToDevice-fxa,
#pageAction-urlbar-sendToDevice-fxa {
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 {
-moz-box-align: center;
}
#pageActionSeparator {
/* This draws the separator the same way that #urlbar-display-box draws its
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -6424,57 +6424,57 @@
},
"FX_PAGE_ACTION_ADDED": {
"record_in_processes": ["main"],
"alert_emails": ["gijs@mozilla.com"],
"bug_numbers": [1393843],
"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": {
"record_in_processes": ["main"],
"alert_emails": ["gijs@mozilla.com"],
"bug_numbers": [1393843],
"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": {
"record_in_processes": ["main"],
"alert_emails": ["gijs@mozilla.com"],
"bug_numbers": [1393843],
"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": {
"record_in_processes": ["main"],
"alert_emails": ["gijs@mozilla.com"],
"bug_numbers": [1393843],
"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": {
"record_in_processes": ["main"],
"alert_emails": ["gijs@mozilla.com"],
"bug_numbers": [1393843],
"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": {
"record_in_processes": ["main", "content"],
"alert_emails": ["perf-telemetry-alerts@mozilla.com"],
"bug_numbers": [1235908],
"expires_in_version": "never",
"kind": "exponential",