Bug 936187 - [Australis] UITour: Make the menu panel work except the z-order of annotations. r=Unfocused

This commit is contained in:
Matthew Noorenberghe 2013-12-10 23:35:11 -08:00
Родитель 5e796c882c
Коммит 457ba62364
4 изменённых файлов: 358 добавлений и 83 удалений

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

@ -2,17 +2,22 @@
// 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/.
"use strict";
this.EXPORTED_SYMBOLS = ["UITour"];
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");
XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
"resource://gre/modules/LightweightThemeManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PermissionsUtils",
"resource://gre/modules/PermissionsUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
"resource:///modules/CustomizableUI.jsm");
const UITOUR_PERMISSION = "uitour";
@ -23,20 +28,45 @@ this.UITour = {
originTabs: new WeakMap(),
pinnedTabs: new WeakMap(),
urlbarCapture: new WeakMap(),
appMenuOpenForAnnotation: new Set(),
highlightEffects: ["wobble", "zoom", "color"],
targets: new Map([
["backforward", "#back-button"],
["appmenu", "#PanelUI-menu-button"],
["home", "#home-button"],
["urlbar", "#urlbar"],
["bookmarks", "#bookmarks-menu-button"],
["search", "#searchbar"],
["searchprovider", function UITour_target_searchprovider(aDocument) {
let searchbar = aDocument.getElementById("searchbar");
return aDocument.getAnonymousElementByAttribute(searchbar,
"anonid",
"searchbar-engine-button");
["addons", {query: "#add-ons-button"}],
["appMenu", {query: "#PanelUI-menu-button"}],
["backForward", {
query: "#back-button",
widgetName: "urlbar-container",
}],
["bookmarks", {query: "#bookmarks-menu-button"}],
["customize", {
query: (aDocument) => {
let customizeButton = aDocument.getElementById("PanelUI-customize");
return aDocument.getAnonymousElementByAttribute(customizeButton,
"class",
"toolbarbutton-icon");
},
widgetName: "PanelUI-customize",
}],
["help", {query: "#PanelUI-help"}],
["home", {query: "#home-button"}],
["quit", {query: "#PanelUI-quit"}],
["search", {
query: "#searchbar",
widgetName: "search-container",
}],
["searchProvider", {
query: (aDocument) => {
let searchbar = aDocument.getElementById("searchbar");
return aDocument.getAnonymousElementByAttribute(searchbar,
"anonid",
"searchbar-engine-button");
},
widgetName: "search-container",
}],
["urlbar", {
query: "#urlbar",
widgetName: "urlbar-container",
}],
]),
@ -68,10 +98,14 @@ this.UITour = {
switch (action) {
case "showHighlight": {
let target = this.getTarget(window, data.target);
if (!target)
return false;
this.showHighlight(target);
let targetPromise = this.getTarget(window, data.target);
targetPromise.then(target => {
if (!target.node) {
Cu.reportError("UITour: Target could not be resolved: " + data.target);
return;
}
this.showHighlight(target);
}).then(null, Cu.reportError);
break;
}
@ -81,10 +115,14 @@ this.UITour = {
}
case "showInfo": {
let target = this.getTarget(window, data.target, true);
if (!target)
return false;
this.showInfo(target, data.title, data.text);
let targetPromise = this.getTarget(window, data.target, true);
targetPromise.then(target => {
if (!target.node) {
Cu.reportError("UITour: Target could not be resolved: " + data.target);
return;
}
this.showInfo(target, data.title, data.text);
}).then(null, Cu.reportError);
break;
}
@ -224,6 +262,7 @@ this.UITour = {
if (!aWindowClosing) {
this.hideHighlight(aWindow);
this.hideInfo(aWindow);
aWindow.PanelUI.panel.removeAttribute("noautohide");
}
this.endUrlbarCapture(aWindow);
@ -273,20 +312,93 @@ this.UITour = {
},
getTarget: function(aWindow, aTargetName, aSticky = false) {
if (typeof aTargetName != "string" || !aTargetName)
return null;
let deferred = Promise.defer();
if (typeof aTargetName != "string" || !aTargetName) {
deferred.reject("Invalid target name specified");
return deferred.promise;
}
if (aTargetName == "pinnedtab")
return this.ensurePinnedTab(aWindow, aSticky);
if (aTargetName == "pinnedTab") {
deferred.resolve({node: this.ensurePinnedTab(aWindow, aSticky)});
return deferred.promise;
}
let targetQuery = this.targets.get(aTargetName);
if (!targetQuery)
return null;
let targetObject = this.targets.get(aTargetName);
if (!targetObject) {
deferred.reject("The specified target name is not in the allowed set");
return deferred.promise;
}
if (typeof targetQuery == "function")
return targetQuery(aWindow.document);
let targetQuery = targetObject.query;
aWindow.PanelUI.ensureReady().then(() => {
if (typeof targetQuery == "function") {
deferred.resolve({
node: targetQuery(aWindow.document),
widgetName: targetObject.widgetName,
});
return;
}
deferred.resolve({
node: aWindow.document.querySelector(targetQuery),
widgetName: targetObject.widgetName,
});
}).then(null, Cu.reportError);
return deferred.promise;
},
targetIsInAppMenu: function(aTarget) {
let placement = CustomizableUI.getPlacementOfWidget(aTarget.widgetName || aTarget.node.id);
if (placement && placement.area == CustomizableUI.AREA_PANEL) {
return true;
}
let targetElement = aTarget.node;
// Use the widget for filtering if it exists since the target may be the icon inside.
if (aTarget.widgetName) {
targetElement = aTarget.node.ownerDocument.getElementById(aTarget.widgetName);
}
// Handle the non-customizable buttons at the bottom of the menu which aren't proper widgets.
return targetElement.id.startsWith("PanelUI-")
&& targetElement.id != "PanelUI-menu-button";
},
/**
* Called before opening or after closing a highlight or info panel to see if
* we need to open or close the appMenu to see the annotation's anchor.
*/
_setAppMenuStateForAnnotation: function(aWindow, aAnnotationType, aShouldOpenForHighlight, aCallback = null) {
// If the panel is in the desired state, we're done.
let panelIsOpen = aWindow.PanelUI.panel.state != "closed";
if (aShouldOpenForHighlight == panelIsOpen) {
if (aCallback)
aCallback();
return;
}
// Don't close the menu if it wasn't opened by us (e.g. via showmenu instead).
if (!aShouldOpenForHighlight && !this.appMenuOpenForAnnotation.has(aAnnotationType)) {
if (aCallback)
aCallback();
return;
}
if (aShouldOpenForHighlight) {
this.appMenuOpenForAnnotation.add(aAnnotationType);
} else {
this.appMenuOpenForAnnotation.delete(aAnnotationType);
}
// Actually show or hide the menu
if (this.appMenuOpenForAnnotation.size) {
this.showMenu(aWindow, "appMenu", aCallback);
} else {
this.hideMenu(aWindow, "appMenu");
if (aCallback)
aCallback();
}
return aWindow.document.querySelector(targetQuery);
},
previewTheme: function(aTheme) {
@ -331,24 +443,30 @@ this.UITour = {
},
showHighlight: function(aTarget) {
let highlighter = aTarget.ownerDocument.getElementById("UITourHighlight");
function showHighlightPanel(aTargetEl) {
let highlighter = aTargetEl.ownerDocument.getElementById("UITourHighlight");
let randomEffect = Math.floor(Math.random() * this.highlightEffects.length);
if (randomEffect == this.highlightEffects.length)
randomEffect--; // On the order of 1 in 2^62 chance of this happening.
highlighter.setAttribute("active", this.highlightEffects[randomEffect]);
let randomEffect = Math.floor(Math.random() * this.highlightEffects.length);
if (randomEffect == this.highlightEffects.length)
randomEffect--; // On the order of 1 in 2^62 chance of this happening.
highlighter.setAttribute("active", this.highlightEffects[randomEffect]);
let targetRect = aTarget.getBoundingClientRect();
let targetRect = aTargetEl.getBoundingClientRect();
highlighter.style.height = targetRect.height + "px";
highlighter.style.width = targetRect.width + "px";
highlighter.style.height = targetRect.height + "px";
highlighter.style.width = targetRect.width + "px";
let highlighterRect = highlighter.getBoundingClientRect();
let highlighterRect = highlighter.getBoundingClientRect();
let top = targetRect.top + (targetRect.height / 2) - (highlighterRect.height / 2);
highlighter.style.top = top + "px";
let left = targetRect.left + (targetRect.width / 2) - (highlighterRect.width / 2);
highlighter.style.left = left + "px";
let top = targetRect.top + (targetRect.height / 2) - (highlighterRect.height / 2);
highlighter.style.top = top + "px";
let left = targetRect.left + (targetRect.width / 2) - (highlighterRect.width / 2);
highlighter.style.left = left + "px";
}
this._setAppMenuStateForAnnotation(aTarget.node.ownerDocument.defaultView, "highlight",
this.targetIsInAppMenu(aTarget),
showHighlightPanel.bind(this, aTarget.node));
},
hideHighlight: function(aWindow) {
@ -358,44 +476,66 @@ this.UITour = {
let highlighter = aWindow.document.getElementById("UITourHighlight");
highlighter.removeAttribute("active");
this._setAppMenuStateForAnnotation(aWindow, "highlight", false);
},
showInfo: function(aAnchor, aTitle, aDescription) {
aAnchor.focus();
function showInfoPanel(aAnchorEl) {
aAnchorEl.focus();
let document = aAnchor.ownerDocument;
let tooltip = document.getElementById("UITourTooltip");
let tooltipTitle = document.getElementById("UITourTooltipTitle");
let tooltipDesc = document.getElementById("UITourTooltipDescription");
let document = aAnchorEl.ownerDocument;
let tooltip = document.getElementById("UITourTooltip");
let tooltipTitle = document.getElementById("UITourTooltipTitle");
let tooltipDesc = document.getElementById("UITourTooltipDescription");
tooltip.hidePopup();
tooltip.hidePopup();
tooltipTitle.textContent = aTitle;
tooltipDesc.textContent = aDescription;
tooltipTitle.textContent = aTitle;
tooltipDesc.textContent = aDescription;
let alignment = "bottomcenter topright";
let anchorRect = aAnchor.getBoundingClientRect();
let alignment = "bottomcenter topright";
tooltip.hidden = false;
tooltip.openPopup(aAnchor, alignment);
tooltip.hidden = false;
tooltip.openPopup(aAnchorEl, alignment);
}
this._setAppMenuStateForAnnotation(aAnchor.node.ownerDocument.defaultView, "info",
this.targetIsInAppMenu(aAnchor),
showInfoPanel.bind(this, aAnchor.node));
},
hideInfo: function(aWindow) {
let tooltip = aWindow.document.getElementById("UITourTooltip");
tooltip.hidePopup();
this._setAppMenuStateForAnnotation(aWindow, "info", false);
},
showMenu: function(aWindow, aMenuName) {
showMenu: function(aWindow, aMenuName, aOpenCallback = null) {
function openMenuButton(aId) {
let menuBtn = aWindow.document.getElementById(aId);
if (menuBtn && menuBtn.boxObject)
menuBtn.boxObject.QueryInterface(Ci.nsIMenuBoxObject).openMenu(true);
if (!menuBtn || !menuBtn.boxObject) {
aOpenCallback();
return;
}
if (aOpenCallback)
menuBtn.addEventListener("popupshown", onPopupShown);
menuBtn.boxObject.QueryInterface(Ci.nsIMenuBoxObject).openMenu(true);
}
function onPopupShown(event) {
this.removeEventListener("popupshown", onPopupShown);
aOpenCallback(event);
}
if (aMenuName == "appmenu")
if (aMenuName == "appMenu") {
aWindow.PanelUI.panel.setAttribute("noautohide", "true");
if (aOpenCallback) {
aWindow.PanelUI.panel.addEventListener("popupshown", onPopupShown);
}
aWindow.PanelUI.show();
else if (aMenuName == "bookmarks")
} else if (aMenuName == "bookmarks") {
openMenuButton("bookmarks-menu-button");
}
},
hideMenu: function(aWindow, aMenuName) {
@ -405,10 +545,12 @@ this.UITour = {
menuBtn.boxObject.QueryInterface(Ci.nsIMenuBoxObject).openMenu(false);
}
if (aMenuName == "appmenu")
if (aMenuName == "appMenu") {
aWindow.PanelUI.panel.removeAttribute("noautohide");
aWindow.PanelUI.hide();
else if (aMenuName == "bookmarks")
} else if (aMenuName == "bookmarks") {
closeMenuButton("bookmarks-menu-button");
}
},
startUrlbarCapture: function(aWindow, aExpectedText, aUrl) {

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

@ -1,4 +1,6 @@
[DEFAULT]
support-files =
head.js
[browser_NetworkPrioritizer.js]
[browser_SignInToWebsite.js]

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

@ -27,6 +27,25 @@ function is_element_visible(element, msg) {
ok(!is_hidden(element), msg);
}
function waitForElementToBeVisible(element, nextTest, msg) {
waitForCondition(() => !is_hidden(element),
() => {
ok(true, msg);
nextTest();
},
"Timeout waiting for visibility: " + msg);
}
function waitForPopupAtAnchor(popup, anchorNode, nextTest, msg) {
waitForCondition(() => popup.popupBoxObject.anchorNode == anchorNode,
() => {
ok(true, msg);
is_element_visible(popup);
nextTest();
},
"Timeout waiting for popup at anchor: " + msg);
}
function is_element_hidden(element, msg) {
isnot(element, null, "Element should not be null, when checking visibility");
ok(is_hidden(element), msg);
@ -80,6 +99,8 @@ function test() {
let popup = document.getElementById("UITourTooltip");
isnot(["hidding","closed"].indexOf(popup.state), -1, "Popup should be closed/hidding after UITour tab is closed");
ok(!PanelUI.panel.hasAttribute("noautohide"), "@noautohide on the menu panel should have been cleaned up");
is(UITour.pinnedTabs.get(window), null, "Any pinned tab should be closed after UITour tab is closed");
executeSoon(nextTest);
@ -102,22 +123,22 @@ function test() {
let tests = [
function test_untrusted_host(done) {
loadTestPage(function() {
let highlight = document.getElementById("UITourHighlight");
is_element_hidden(highlight, "Highlight should initially be hidden");
let bookmarksMenu = document.getElementById("bookmarks-menu-button");
ise(bookmarksMenu.open, false, "Bookmark menu should initially be closed");
gContentAPI.showHighlight("urlbar");
is_element_hidden(highlight, "Highlight should not be shown on a untrusted host");
gContentAPI.showMenu("bookmarks");
ise(bookmarksMenu.open, false, "Bookmark menu should not open on a untrusted host");
done();
}, "http://mochi.test:8888/");
},
function test_unsecure_host(done) {
loadTestPage(function() {
let highlight = document.getElementById("UITourHighlight");
is_element_hidden(highlight, "Highlight should initially be hidden");
let bookmarksMenu = document.getElementById("bookmarks-menu-button");
ise(bookmarksMenu.open, false, "Bookmark menu should initially be closed");
gContentAPI.showHighlight("urlbar");
is_element_hidden(highlight, "Highlight should not be shown on a unsecure host");
gContentAPI.showMenu("bookmarks");
ise(bookmarksMenu.open, false, "Bookmark menu should not open on a unsecure host");
done();
}, "http://example.com/");
@ -129,40 +150,78 @@ let tests = [
is_element_hidden(highlight, "Highlight should initially be hidden");
gContentAPI.showHighlight("urlbar");
is_element_visible(highlight, "Highlight should be shown on a unsecure host when override pref is set");
waitForElementToBeVisible(highlight, done, "Highlight should be shown on a unsecure host when override pref is set");
Services.prefs.setBoolPref("browser.uitour.requireSecure", true);
done();
}, "http://example.com/");
},
function test_disabled(done) {
Services.prefs.setBoolPref("browser.uitour.enabled", false);
let highlight = document.getElementById("UITourHighlight");
is_element_hidden(highlight, "Highlight should initially be hidden");
let bookmarksMenu = document.getElementById("bookmarks-menu-button");
ise(bookmarksMenu.open, false, "Bookmark menu should initially be closed");
gContentAPI.showHighlight("urlbar");
is_element_hidden(highlight, "Highlight should not be shown when feature is disabled");
gContentAPI.showMenu("bookmarks");
ise(bookmarksMenu.open, false, "Bookmark menu should not open when feature is disabled");
Services.prefs.setBoolPref("browser.uitour.enabled", true);
done();
},
function test_highlight(done) {
function test_highlight_2() {
let highlight = document.getElementById("UITourHighlight");
gContentAPI.hideHighlight();
is_element_hidden(highlight, "Highlight should be hidden after hideHighlight()");
gContentAPI.showHighlight("urlbar");
waitForElementToBeVisible(highlight, test_highlight_3, "Highlight should be shown after showHighlight()");
}
function test_highlight_3() {
let highlight = document.getElementById("UITourHighlight");
gContentAPI.showHighlight("backForward");
waitForElementToBeVisible(highlight, done, "Highlight should be shown after showHighlight()");
}
let highlight = document.getElementById("UITourHighlight");
is_element_hidden(highlight, "Highlight should initially be hidden");
gContentAPI.showHighlight("urlbar");
is_element_visible(highlight, "Highlight should be shown after showHighlight()");
waitForElementToBeVisible(highlight, test_highlight_2, "Highlight should be shown after showHighlight()");
},
function test_highlight_customize_auto_open_close(done) {
let highlight = document.getElementById("UITourHighlight");
gContentAPI.showHighlight("customize");
waitForElementToBeVisible(highlight, function checkPanelIsOpen() {
isnot(PanelUI.panel.state, "closed", "Panel should have opened");
gContentAPI.hideHighlight();
is_element_hidden(highlight, "Highlight should be hidden after hideHighlight()");
// Move the highlight outside which should close the app menu.
gContentAPI.showHighlight("appMenu");
waitForElementToBeVisible(highlight, function checkPanelIsClosed() {
isnot(PanelUI.panel.state, "open",
"Panel should have closed after the highlight moved elsewhere.");
done();
}, "Highlight should move to the appMenu button");
}, "Highlight should be shown after showHighlight() for fixed panel items");
},
function test_highlight_customize_manual_open_close(done) {
let highlight = document.getElementById("UITourHighlight");
// Manually open the app menu then show a highlight there. The menu should remain open.
gContentAPI.showMenu("appMenu");
isnot(PanelUI.panel.state, "closed", "Panel should have opened");
gContentAPI.showHighlight("customize");
gContentAPI.showHighlight("urlbar");
is_element_visible(highlight, "Highlight should be shown after showHighlight()");
gContentAPI.showHighlight("backforward");
is_element_visible(highlight, "Highlight should be shown after showHighlight()");
waitForElementToBeVisible(highlight, function checkPanelIsStillOpen() {
isnot(PanelUI.panel.state, "closed", "Panel should still be open");
done();
// Move the highlight outside which shouldn't close the app menu since it was manually opened.
gContentAPI.showHighlight("appMenu");
waitForElementToBeVisible(highlight, function () {
isnot(PanelUI.panel.state, "closed",
"Panel should remain open since UITour didn't open it in the first place");
gContentAPI.hideMenu("appMenu");
done();
}, "Highlight should move to the appMenu button");
}, "Highlight should be shown after showHighlight() for fixed panel items");
},
function test_info_1(done) {
let popup = document.getElementById("UITourTooltip");
@ -212,6 +271,54 @@ let tests = [
gContentAPI.showInfo("urlbar", "urlbar title", "urlbar text");
},
function test_info_customize_auto_open_close(done) {
let popup = document.getElementById("UITourTooltip");
gContentAPI.showInfo("customize", "Customization", "Customize me please!");
UITour.getTarget(window, "customize").then((customizeTarget) => {
waitForPopupAtAnchor(popup, customizeTarget.node, function checkPanelIsOpen() {
isnot(PanelUI.panel.state, "closed", "Panel should have opened before the popup anchored");
ok(PanelUI.panel.hasAttribute("noautohide"), "@noautohide on the menu panel should have been set");
// Move the info outside which should close the app menu.
gContentAPI.showInfo("appMenu", "Open Me", "You know you want to");
UITour.getTarget(window, "appMenu").then((target) => {
waitForPopupAtAnchor(popup, target.node, function checkPanelIsClosed() {
isnot(PanelUI.panel.state, "open",
"Panel should have closed after the info moved elsewhere.");
ok(!PanelUI.panel.hasAttribute("noautohide"), "@noautohide on the menu panel should have been cleaned up on close");
done();
}, "Info should move to the appMenu button");
});
}, "Info panel should be anchored to the customize button");
});
},
function test_info_customize_manual_open_close(done) {
let popup = document.getElementById("UITourTooltip");
// Manually open the app menu then show an info panel there. The menu should remain open.
gContentAPI.showMenu("appMenu");
isnot(PanelUI.panel.state, "closed", "Panel should have opened");
ok(PanelUI.panel.hasAttribute("noautohide"), "@noautohide on the menu panel should have been set");
gContentAPI.showInfo("customize", "Customization", "Customize me please!");
UITour.getTarget(window, "customize").then((customizeTarget) => {
waitForPopupAtAnchor(popup, customizeTarget.node, function checkMenuIsStillOpen() {
isnot(PanelUI.panel.state, "closed", "Panel should still be open");
ok(PanelUI.panel.hasAttribute("noautohide"), "@noautohide on the menu panel should still be set");
// Move the info outside which shouldn't close the app menu since it was manually opened.
gContentAPI.showInfo("appMenu", "Open Me", "You know you want to");
UITour.getTarget(window, "appMenu").then((target) => {
waitForPopupAtAnchor(popup, target.node, function checkMenuIsStillOpen() {
isnot(PanelUI.panel.state, "closed",
"Menu should remain open since UITour didn't open it in the first place");
gContentAPI.hideMenu("appMenu");
ok(!PanelUI.panel.hasAttribute("noautohide"), "@noautohide on the menu panel should have been cleaned up on close");
done();
}, "Info should move to the appMenu button");
});
}, "Info should be shown after showInfo() for fixed menu panel items");
});
},
function test_pinnedTab(done) {
is(UITour.pinnedTabs.get(window), null, "Should not already have a pinned tab");

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

@ -0,0 +1,24 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
function waitForCondition(condition, nextTest, errorMsg) {
var tries = 0;
var interval = setInterval(function() {
if (tries >= 30) {
ok(false, errorMsg);
moveOn();
}
var conditionPassed;
try {
conditionPassed = condition();
} catch (e) {
ok(false, e + "\n" + e.stack);
conditionPassed = false;
}
if (conditionPassed) {
moveOn();
}
tries++;
}, 100);
var moveOn = function() { clearInterval(interval); nextTest(); };
}