Bug 1068284 - UI Tour: Add ability to highlight search provider in search menu. r=MattN

--HG--
extra : transplant_source : %AA%DA%ED%B6%24F%92%F0%D3k%F6%D34%2054%C4%23%D8%F0
This commit is contained in:
Blair McBride 2014-10-15 13:48:49 +13:00
Родитель ab7777e096
Коммит 1b1bee23f5
4 изменённых файлов: 227 добавлений и 43 удалений

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

@ -11,6 +11,7 @@ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
"resource://gre/modules/LightweightThemeManager.jsm");
@ -39,6 +40,9 @@ const BUCKET_TIMESTEPS = [
// Time after which seen Page IDs expire.
const SEENPAGEID_EXPIRY = 8 * 7 * 24 * 60 * 60 * 1000; // 8 weeks.
// Prefix for any target matching a search engine.
const TARGET_SEARCHENGINE_PREFIX = "searchEngine-";
this.UITour = {
url: null,
@ -375,7 +379,10 @@ this.UITour = {
}
case "showMenu": {
this.showMenu(window, data.name);
this.showMenu(window, data.name, () => {
if (typeof data.showCallbackID == "string")
this.sendPageCallback(contentDocument, data.showCallbackID);
});
break;
}
@ -685,6 +692,11 @@ this.UITour = {
return deferred.promise;
}
if (aTargetName.startsWith(TARGET_SEARCHENGINE_PREFIX)) {
let engineID = aTargetName.slice(TARGET_SEARCHENGINE_PREFIX.length);
return this.getSearchEngineTarget(aWindow, engineID);
}
let targetObject = this.targets.get(aTargetName);
if (!targetObject) {
deferred.reject("The specified target name is not in the allowed set");
@ -817,8 +829,22 @@ this.UITour = {
* @see UITour.highlightEffects
*/
showHighlight: function(aTarget, aEffect = "none") {
function showHighlightPanel(aTargetEl) {
let highlighter = aTargetEl.ownerDocument.getElementById("UITourHighlight");
let window = aTarget.node.ownerDocument.defaultView;
function showHighlightPanel() {
if (aTarget.targetName.startsWith(TARGET_SEARCHENGINE_PREFIX)) {
// This won't affect normal higlights done via the panel, so we need to
// manually hide those.
this.hideHighlight(window);
aTarget.node.setAttribute("_moz-menuactive", true);
return;
}
// Conversely, highlights for search engines are highlighted via CSS
// rather than a panel, so need to be manually removed.
this._hideSearchEngineHighlight(window);
let highlighter = aTarget.node.ownerDocument.getElementById("UITourHighlight");
let effect = aEffect;
if (effect == "random") {
@ -830,12 +856,12 @@ this.UITour = {
}
// Toggle the effect attribute to "none" and flush layout before setting it so the effect plays.
highlighter.setAttribute("active", "none");
aTargetEl.ownerDocument.defaultView.getComputedStyle(highlighter).animationName;
aTarget.node.ownerDocument.defaultView.getComputedStyle(highlighter).animationName;
highlighter.setAttribute("active", effect);
highlighter.parentElement.setAttribute("targetName", aTarget.targetName);
highlighter.parentElement.hidden = false;
let targetRect = aTargetEl.getBoundingClientRect();
let targetRect = aTarget.node.getBoundingClientRect();
let highlightHeight = targetRect.height;
let highlightWidth = targetRect.width;
let minDimension = Math.min(highlightHeight, highlightWidth);
@ -859,7 +885,7 @@ this.UITour = {
}
/* The "overlap" position anchors from the top-left but we want to centre highlights at their
minimum size. */
let highlightWindow = aTargetEl.ownerDocument.defaultView;
let highlightWindow = aTarget.node.ownerDocument.defaultView;
let containerStyle = highlightWindow.getComputedStyle(highlighter.parentElement);
let paddingTopPx = 0 - parseFloat(containerStyle.paddingTop);
let paddingLeftPx = 0 - parseFloat(containerStyle.paddingLeft);
@ -870,9 +896,8 @@ this.UITour = {
- (Math.max(0, highlightWidthWithMin - targetRect.width) / 2);
let offsetY = paddingLeftPx
- (Math.max(0, highlightHeightWithMin - targetRect.height) / 2);
this._addAnnotationPanelMutationObserver(highlighter.parentElement);
highlighter.parentElement.openPopup(aTargetEl, "overlap", offsetX, offsetY);
highlighter.parentElement.openPopup(aTarget.node, "overlap", offsetX, offsetY);
}
// Prevent showing a panel at an undefined position.
@ -881,7 +906,7 @@ this.UITour = {
this._setAppMenuStateForAnnotation(aTarget.node.ownerDocument.defaultView, "highlight",
this.targetIsInAppMenu(aTarget),
showHighlightPanel.bind(this, aTarget.node));
showHighlightPanel.bind(this));
},
hideHighlight: function(aWindow) {
@ -895,6 +920,24 @@ this.UITour = {
highlighter.removeAttribute("active");
this._setAppMenuStateForAnnotation(aWindow, "highlight", false);
this._hideSearchEngineHighlight(aWindow);
},
_hideSearchEngineHighlight: function(aWindow) {
// We special case highlighting items in the search engines dropdown,
// so just blindly remove any highlight there.
let searchMenuBtn = null;
try {
searchMenuBtn = this.targets.get("searchProvider").query(aWindow.document);
} catch (e) { /* This is ok to fail. */ }
if (searchMenuBtn) {
let searchPopup = aWindow.document
.getAnonymousElementByAttribute(searchMenuBtn,
"anonid",
"searchbar-popup");
for (let menuItem of searchPopup.children)
menuItem.removeAttribute("_moz-menuactive");
}
},
/**
@ -994,6 +1037,11 @@ this.UITour = {
if (!this.isElementVisible(aAnchor.node))
return;
// Due to a platform limitation, we can't anchor a panel to an element in a
// <menupopup>. So we can't support showing info panels for search engines.
if (aAnchor.targetName.startsWith(TARGET_SEARCHENGINE_PREFIX))
return;
this._setAppMenuStateForAnnotation(aAnchor.node.ownerDocument.defaultView, "info",
this.targetIsInAppMenu(aAnchor),
showInfoPanel.bind(this, aAnchor.node));
@ -1013,15 +1061,15 @@ this.UITour = {
},
showMenu: function(aWindow, aMenuName, aOpenCallback = null) {
function openMenuButton(aID) {
let menuBtn = aWindow.document.getElementById(aID);
if (!menuBtn || !menuBtn.boxObject) {
aOpenCallback();
function openMenuButton(aMenuBtn) {
if (!aMenuBtn || !aMenuBtn.boxObject || aMenuBtn.open) {
if (aOpenCallback)
aOpenCallback();
return;
}
if (aOpenCallback)
menuBtn.addEventListener("popupshown", onPopupShown);
menuBtn.boxObject.QueryInterface(Ci.nsIMenuBoxObject).openMenu(true);
aMenuBtn.addEventListener("popupshown", onPopupShown);
aMenuBtn.boxObject.QueryInterface(Ci.nsIMenuBoxObject).openMenu(true);
}
function onPopupShown(event) {
this.removeEventListener("popupshown", onPopupShown);
@ -1041,15 +1089,19 @@ this.UITour = {
}
aWindow.PanelUI.show();
} else if (aMenuName == "bookmarks") {
openMenuButton("bookmarks-menu-button");
let menuBtn = aWindow.document.getElementById("bookmarks-menu-button");
openMenuButton(menuBtn);
} else if (aMenuName == "searchEngines") {
this.getTarget(aWindow, "searchProvider").then(target => {
openMenuButton(target.node);
}).catch(Cu.reportError);
}
},
hideMenu: function(aWindow, aMenuName) {
function closeMenuButton(aID) {
let menuBtn = aWindow.document.getElementById(aID);
if (menuBtn && menuBtn.boxObject)
menuBtn.boxObject.QueryInterface(Ci.nsIMenuBoxObject).openMenu(false);
function closeMenuButton(aMenuBtn) {
if (aMenuBtn && aMenuBtn.boxObject)
aMenuBtn.boxObject.QueryInterface(Ci.nsIMenuBoxObject).openMenu(false);
}
if (aMenuName == "appMenu") {
@ -1057,7 +1109,11 @@ this.UITour = {
aWindow.PanelUI.hide();
this.recreatePopup(aWindow.PanelUI.panel);
} else if (aMenuName == "bookmarks") {
closeMenuButton("bookmarks-menu-button");
let menuBtn = aWindow.document.getElementById("bookmarks-menu-button");
closeMenuButton(menuBtn);
} else if (aMenuName == "searchEngines") {
let menuBtn = this.targets.get("searchProvider").query(aWindow.document);
closeMenuButton(menuBtn);
}
},
@ -1158,31 +1214,39 @@ this.UITour = {
},
getAvailableTargets: function(aContentDocument, aCallbackID) {
let window = this.getChromeWindow(aContentDocument);
let data = this.availableTargetsCache.get(window);
if (data) {
this.sendPageCallback(aContentDocument, aCallbackID, data);
return;
}
Task.spawn(function*() {
let window = this.getChromeWindow(aContentDocument);
let data = this.availableTargetsCache.get(window);
if (data) {
this.sendPageCallback(aContentDocument, aCallbackID, data);
return;
}
let promises = [];
for (let targetName of this.targets.keys()) {
promises.push(this.getTarget(window, targetName));
}
let targetObjects = yield Promise.all(promises);
let promises = [];
for (let targetName of this.targets.keys()) {
promises.push(this.getTarget(window, targetName));
}
Promise.all(promises).then((targetObjects) => {
let targetNames = [
"pinnedTab",
];
for (let targetObject of targetObjects) {
if (targetObject.node)
targetNames.push(targetObject.targetName);
}
let data = {
targetNames = targetNames.concat(
yield this.getAvailableSearchEngineTargets(window)
);
data = {
targets: targetNames,
};
this.availableTargetsCache.set(window, data);
this.sendPageCallback(aContentDocument, aCallbackID, data);
}, (err) => {
}.bind(this)).catch(err => {
Cu.reportError(err);
this.sendPageCallback(aContentDocument, aCallbackID, {
targets: [],
@ -1248,6 +1312,55 @@ this.UITour = {
return;
}
},
getAvailableSearchEngineTargets(aWindow) {
return new Promise(resolve => {
this.getTarget(aWindow, "search").then(searchTarget => {
if (!searchTarget.node || this.targetIsInAppMenu(searchTarget))
return resolve([]);
Services.search.init(() => {
let engines = Services.search.getVisibleEngines();
resolve([TARGET_SEARCHENGINE_PREFIX + engine.identifier
for (engine of engines)
if (engine.identifier)]);
});
}).catch(() => resolve([]));
});
},
// We only allow matching based on a search engine's identifier - this gives
// us a non-changing ID and guarentees we only match against app-provided
// engines.
getSearchEngineTarget(aWindow, aIdentifier) {
return new Promise((resolve, reject) => {
Task.spawn(function*() {
let searchTarget = yield this.getTarget(aWindow, "search");
// We're not supporting having the searchbar in the app-menu, because
// popups within popups gets crazy. This restriction should be lifted
// once bug 988151 is implemented, as the page can then be responsible
// for opening each menu when appropriate.
if (!searchTarget.node || this.targetIsInAppMenu(searchTarget))
return reject("Search engine not available");
yield Services.search.init();
let searchPopup = searchTarget.node._popup;
for (let engineNode of searchPopup.children) {
let engine = engineNode.engine;
if (engine && engine.identifier == aIdentifier) {
return resolve({
targetName: TARGET_SEARCHENGINE_PREFIX + engine.identifier,
node: engineNode,
});
}
}
reject("Search engine not available");
}.bind(this)).catch(() => {
reject("Search engine not available");
});
});
}
};
this.UITour.init();

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

@ -195,6 +195,53 @@ let tests = [
gContentAPI.showHighlight("urlbar");
waitForElementToBeVisible(highlight, checkDefaultEffect, "Highlight should be shown after showHighlight()");
},
function test_highlight_search_engine(done) {
let highlight = document.getElementById("UITourHighlight");
gContentAPI.showHighlight("urlbar");
waitForElementToBeVisible(highlight, () => {
gContentAPI.showMenu("searchEngines", function() {
let searchbar = document.getElementById("searchbar");
isnot(searchbar, null, "Should have found searchbar");
let searchPopup = document.getAnonymousElementByAttribute(searchbar,
"anonid",
"searchbar-popup");
isnot(searchPopup, null, "Should have found search popup");
function getEngineNode(identifier) {
let engineNode = null;
for (let node of searchPopup.children) {
if (node.engine.identifier == identifier) {
engineNode = node;
break;
}
}
isnot(engineNode, null, "Should have found search engine node in popup");
return engineNode;
}
let googleEngineNode = getEngineNode("google");
let bingEngineNode = getEngineNode("bing");
gContentAPI.showHighlight("searchEngine-google");
waitForCondition(() => googleEngineNode.getAttribute("_moz-menuactive") == "true", function() {
is_element_hidden(highlight, "Highlight panel should be hidden by highlighting search engine");
gContentAPI.showHighlight("searchEngine-bing");
waitForCondition(() => bingEngineNode.getAttribute("_moz-menuactive") == "true", function() {
isnot(googleEngineNode.getAttribute("_moz-menuactive"), "true", "Previous engine should no longer be highlighted");
gContentAPI.hideHighlight();
waitForCondition(() => bingEngineNode.getAttribute("_moz-menuactive") != "true", function() {
gContentAPI.hideMenu("searchEngines");
waitForCondition(() => searchPopup.state == "closed", function() {
done();
}, "Search dropdown should close");
}, "Menu item should get attribute removed");
}, "Menu item should get attribute to make it look active");
});
});
});
},
function test_highlight_effect_unsupported(done) {
function checkUnsupportedEffect() {
is(highlight.getAttribute("active"), "none", "No effect should be used when an unsupported effect is requested");

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

@ -14,6 +14,13 @@ function test() {
UITourTest();
}
function searchEngineTargets() {
let engines = Services.search.getVisibleEngines();
return ["searchEngine-" + engine.identifier
for (engine of engines)
if (engine.identifier)];
}
let tests = [
function test_availableTargets(done) {
gContentAPI.getConfiguration("availableTargets", (data) => {
@ -33,7 +40,7 @@ let tests = [
"search",
"searchProvider",
"urlbar",
]);
].concat(searchEngineTargets()));
ok(UITour.availableTargetsCache.has(window),
"Targets should now be cached");
done();
@ -60,7 +67,7 @@ let tests = [
"search",
"searchProvider",
"urlbar",
]);
].concat(searchEngineTargets()));
ok(UITour.availableTargetsCache.has(window),
"Targets should now be cached again");
CustomizableUI.reset();

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

@ -2,15 +2,13 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// Copied from the proposed JS library for Bedrock (ie, www.mozilla.org).
// create namespace
if (typeof Mozilla == 'undefined') {
var Mozilla = {};
}
(function($) {
'use strict';
;(function($) {
'use strict';
// create namespace
if (typeof Mozilla.UITour == 'undefined') {
@ -60,6 +58,9 @@ if (typeof Mozilla == 'undefined') {
Mozilla.UITour.DEFAULT_THEME_CYCLE_DELAY = 10 * 1000;
Mozilla.UITour.CONFIGNAME_SYNC = "sync";
Mozilla.UITour.CONFIGNAME_AVAILABLETARGETS = "availableTargets";
Mozilla.UITour.registerPageID = function(pageID) {
_sendEvent('registerPageID', {
pageID: pageID
@ -86,7 +87,7 @@ if (typeof Mozilla == 'undefined') {
icon: buttons[i].icon,
style: buttons[i].style,
callbackID: _waitForCallback(buttons[i].callback)
});
});
}
}
@ -156,9 +157,14 @@ if (typeof Mozilla == 'undefined') {
_sendEvent('removePinnedTab');
};
Mozilla.UITour.showMenu = function(name) {
Mozilla.UITour.showMenu = function(name, callback) {
var showCallbackID;
if (callback)
showCallbackID = _waitForCallback(callback);
_sendEvent('showMenu', {
name: name
name: name,
showCallbackID: showCallbackID,
});
};
@ -168,6 +174,17 @@ if (typeof Mozilla == 'undefined') {
});
};
Mozilla.UITour.startUrlbarCapture = function(text, url) {
_sendEvent('startUrlbarCapture', {
text: text,
url: url
});
};
Mozilla.UITour.endUrlbarCapture = function() {
_sendEvent('endUrlbarCapture');
};
Mozilla.UITour.getConfiguration = function(configName, callback) {
_sendEvent('getConfiguration', {
callbackID: _waitForCallback(callback),