Bug 935815 - [Australis] UITour: Allow adding a button with an action to the info panel. r=MattN

This commit is contained in:
Blair McBride 2014-01-06 12:27:25 +13:00
Родитель 311b5a3e1d
Коммит 97236f2f4c
10 изменённых файлов: 399 добавлений и 26 удалений

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

@ -202,8 +202,16 @@
align="start" align="start"
orient="vertical" orient="vertical"
role="alert"> role="alert">
<hbox>
<vbox>
<image id="UITourTooltipIcon"/>
</vbox>
<vbox flex="1">
<label id="UITourTooltipTitle" flex="1"/> <label id="UITourTooltipTitle" flex="1"/>
<description id="UITourTooltipDescription" flex="1"/> <description id="UITourTooltipDescription" flex="1"/>
<hbox id="UITourTooltipButtons" flex="1" align="end"/>
</vbox>
</hbox>
</panel> </panel>
<panel id="UITourHighlightContainer" <panel id="UITourHighlightContainer"
hidden="true" hidden="true"

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

@ -22,6 +22,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
const UITOUR_PERMISSION = "uitour"; const UITOUR_PERMISSION = "uitour";
const PREF_PERM_BRANCH = "browser.uitour."; const PREF_PERM_BRANCH = "browser.uitour.";
const MAX_BUTTONS = 4;
this.UITour = { this.UITour = {
@ -137,7 +138,34 @@ this.UITour = {
Cu.reportError("UITour: Target could not be resolved: " + data.target); Cu.reportError("UITour: Target could not be resolved: " + data.target);
return; return;
} }
this.showInfo(target, data.title, data.text);
let iconURL = null;
if (typeof data.icon == "string")
iconURL = this.resolveURL(contentDocument, data.icon);
let buttons = [];
if (Array.isArray(data.buttons) && data.buttons.length > 0) {
for (let buttonData of data.buttons) {
if (typeof buttonData == "object" &&
typeof buttonData.label == "string" &&
typeof buttonData.callbackID == "string") {
let button = {
label: buttonData.label,
callbackID: buttonData.callbackID,
};
if (typeof buttonData.icon == "string")
button.iconURL = this.resolveURL(contentDocument, buttonData.icon);
buttons.push(button);
if (buttons.length == MAX_BUTTONS)
break;
}
}
}
this.showInfo(contentDocument, target, data.title, data.text, iconURL, buttons);
}).then(null, Cu.reportError); }).then(null, Cu.reportError);
break; break;
} }
@ -315,11 +343,7 @@ this.UITour = {
if (uri.schemeIs("chrome")) if (uri.schemeIs("chrome"))
return true; return true;
let allowedSchemes = new Set(["https"]); if (!this.isSafeScheme(uri))
if (!Services.prefs.getBoolPref("browser.uitour.requireSecure"))
allowedSchemes.add("http");
if (!allowedSchemes.has(uri.scheme))
return false; return false;
this.importPermissions(); this.importPermissions();
@ -327,6 +351,50 @@ this.UITour = {
return permission == Services.perms.ALLOW_ACTION; return permission == Services.perms.ALLOW_ACTION;
}, },
isSafeScheme: function(aURI) {
let allowedSchemes = new Set(["https"]);
if (!Services.prefs.getBoolPref("browser.uitour.requireSecure"))
allowedSchemes.add("http");
if (!allowedSchemes.has(aURI.scheme))
return false;
return true;
},
resolveURL: function(aDocument, aURL) {
try {
let uri = Services.io.newURI(aURL, null, aDocument.documentURIObject);
if (!this.isSafeScheme(uri))
return null;
return uri.spec;
} catch (e) {}
return null;
},
sendPageCallback: function(aDocument, aCallbackID, aData = {}) {
let detail = Cu.createObjectIn(aDocument.defaultView);
detail.data = Cu.createObjectIn(detail);
for (let key of Object.keys(aData))
detail.data[key] = aData[key];
Cu.makeObjectPropsNormal(detail.data);
Cu.makeObjectPropsNormal(detail);
detail.callbackID = aCallbackID;
let event = new aDocument.defaultView.CustomEvent("mozUITourResponse", {
bubbles: true,
detail: detail
});
aDocument.dispatchEvent(event);
},
getTarget: function(aWindow, aTargetName, aSticky = false) { getTarget: function(aWindow, aTargetName, aSticky = false) {
let deferred = Promise.defer(); let deferred = Promise.defer();
if (typeof aTargetName != "string" || !aTargetName) { if (typeof aTargetName != "string" || !aTargetName) {
@ -522,7 +590,7 @@ this.UITour = {
this._setAppMenuStateForAnnotation(aWindow, "highlight", false); this._setAppMenuStateForAnnotation(aWindow, "highlight", false);
}, },
showInfo: function(aAnchor, aTitle, aDescription) { showInfo: function(aContentDocument, aAnchor, aTitle = "", aDescription = "", aIconURL = "", aButtons = []) {
function showInfoPanel(aAnchorEl) { function showInfoPanel(aAnchorEl) {
aAnchorEl.focus(); aAnchorEl.focus();
@ -530,13 +598,37 @@ this.UITour = {
let tooltip = document.getElementById("UITourTooltip"); let tooltip = document.getElementById("UITourTooltip");
let tooltipTitle = document.getElementById("UITourTooltipTitle"); let tooltipTitle = document.getElementById("UITourTooltipTitle");
let tooltipDesc = document.getElementById("UITourTooltipDescription"); let tooltipDesc = document.getElementById("UITourTooltipDescription");
let tooltipIcon = document.getElementById("UITourTooltipIcon");
let tooltipButtons = document.getElementById("UITourTooltipButtons");
if (tooltip.state == "open") { if (tooltip.state == "open") {
tooltip.hidePopup(); tooltip.hidePopup();
} }
tooltipTitle.textContent = aTitle; tooltipTitle.textContent = aTitle || "";
tooltipDesc.textContent = aDescription; tooltipDesc.textContent = aDescription || "";
tooltipIcon.src = aIconURL || "";
tooltipIcon.hidden = !aIconURL;
while (tooltipButtons.firstChild)
tooltipButtons.firstChild.remove();
for (let button of aButtons) {
let el = document.createElement("button");
el.setAttribute("label", button.label);
if (button.iconURL)
el.setAttribute("image", button.iconURL);
let callbackID = button.callbackID;
el.addEventListener("command", event => {
tooltip.hidePopup();
this.sendPageCallback(aContentDocument, callbackID);
});
tooltipButtons.appendChild(el);
}
tooltipButtons.hidden = !aButtons.length;
tooltip.hidden = false; tooltip.hidden = false;
let alignment = "bottomcenter topright"; let alignment = "bottomcenter topright";
@ -553,9 +645,15 @@ this.UITour = {
}, },
hideInfo: function(aWindow) { hideInfo: function(aWindow) {
let tooltip = aWindow.document.getElementById("UITourTooltip"); let document = aWindow.document;
let tooltip = document.getElementById("UITourTooltip");
tooltip.hidePopup(); tooltip.hidePopup();
this._setAppMenuStateForAnnotation(aWindow, "info", false); this._setAppMenuStateForAnnotation(aWindow, "info", false);
let tooltipButtons = document.getElementById("UITourTooltipButtons");
while (tooltipButtons.firstChild)
tooltipButtons.firstChild.remove();
}, },
showMenu: function(aWindow, aMenuName, aOpenCallback = null) { showMenu: function(aWindow, aMenuName, aOpenCallback = null) {

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

@ -1,13 +1,14 @@
[DEFAULT] [DEFAULT]
support-files = support-files =
head.js head.js
uitour.*
image.png
[browser_NetworkPrioritizer.js] [browser_NetworkPrioritizer.js]
[browser_SignInToWebsite.js] [browser_SignInToWebsite.js]
[browser_UITour.js] [browser_UITour.js]
support-files = uitour.*
skip-if = os == "linux" # Intermittent failures, bug 951965 skip-if = os == "linux" # Intermittent failures, bug 951965
[browser_UITour2.js] [browser_UITour2.js]
support-files = uitour.* [browser_UITour3.js]
[browser_taskbar_preview.js] [browser_taskbar_preview.js]
run-if = os == "win" run-if = os == "win"

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

@ -5,6 +5,7 @@
let gTestTab; let gTestTab;
let gContentAPI; let gContentAPI;
let gContentWindow;
Components.utils.import("resource:///modules/UITour.jsm"); Components.utils.import("resource:///modules/UITour.jsm");
@ -21,10 +22,10 @@ function loadTestPage(callback, host = "https://example.com/") {
gTestTab.linkedBrowser.addEventListener("load", function onLoad() { gTestTab.linkedBrowser.addEventListener("load", function onLoad() {
gTestTab.linkedBrowser.removeEventListener("load", onLoad); gTestTab.linkedBrowser.removeEventListener("load", onLoad);
let contentWindow = Components.utils.waiveXrays(gTestTab.linkedBrowser.contentDocument.defaultView); gContentWindow = Components.utils.waiveXrays(gTestTab.linkedBrowser.contentDocument.defaultView);
gContentAPI = contentWindow.Mozilla.UITour; gContentAPI = gContentWindow.Mozilla.UITour;
waitForFocus(callback, contentWindow); waitForFocus(callback, gContentWindow);
}, true); }, true);
} }
@ -37,6 +38,7 @@ function test() {
registerCleanupFunction(function() { registerCleanupFunction(function() {
delete window.UITour; delete window.UITour;
delete window.gContentWindow;
delete window.gContentAPI; delete window.gContentAPI;
if (gTestTab) if (gTestTab)
gBrowser.removeTab(gTestTab); gBrowser.removeTab(gTestTab);
@ -240,11 +242,16 @@ let tests = [
let popup = document.getElementById("UITourTooltip"); let popup = document.getElementById("UITourTooltip");
let title = document.getElementById("UITourTooltipTitle"); let title = document.getElementById("UITourTooltipTitle");
let desc = document.getElementById("UITourTooltipDescription"); let desc = document.getElementById("UITourTooltipDescription");
let icon = document.getElementById("UITourTooltipIcon");
let buttons = document.getElementById("UITourTooltipButtons");
popup.addEventListener("popupshown", function onPopupShown() { popup.addEventListener("popupshown", function onPopupShown() {
popup.removeEventListener("popupshown", onPopupShown); popup.removeEventListener("popupshown", onPopupShown);
is(popup.popupBoxObject.anchorNode, document.getElementById("urlbar"), "Popup should be anchored to the urlbar"); is(popup.popupBoxObject.anchorNode, document.getElementById("urlbar"), "Popup should be anchored to the urlbar");
is(title.textContent, "test title", "Popup should have correct title"); is(title.textContent, "test title", "Popup should have correct title");
is(desc.textContent, "test text", "Popup should have correct description text"); is(desc.textContent, "test text", "Popup should have correct description text");
is(icon.src, "", "Popup should have no icon");
is(buttons.hasChildNodes(), false, "Popup should have no buttons");
popup.addEventListener("popuphidden", function onPopupHidden() { popup.addEventListener("popuphidden", function onPopupHidden() {
popup.removeEventListener("popuphidden", onPopupHidden); popup.removeEventListener("popuphidden", onPopupHidden);
@ -266,11 +273,16 @@ let tests = [
let popup = document.getElementById("UITourTooltip"); let popup = document.getElementById("UITourTooltip");
let title = document.getElementById("UITourTooltipTitle"); let title = document.getElementById("UITourTooltipTitle");
let desc = document.getElementById("UITourTooltipDescription"); let desc = document.getElementById("UITourTooltipDescription");
let icon = document.getElementById("UITourTooltipIcon");
let buttons = document.getElementById("UITourTooltipButtons");
popup.addEventListener("popupshown", function onPopupShown() { popup.addEventListener("popupshown", function onPopupShown() {
popup.removeEventListener("popupshown", onPopupShown); popup.removeEventListener("popupshown", onPopupShown);
is(popup.popupBoxObject.anchorNode, document.getElementById("urlbar"), "Popup should be anchored to the urlbar"); is(popup.popupBoxObject.anchorNode, document.getElementById("urlbar"), "Popup should be anchored to the urlbar");
is(title.textContent, "urlbar title", "Popup should have correct title"); is(title.textContent, "urlbar title", "Popup should have correct title");
is(desc.textContent, "urlbar text", "Popup should have correct description text"); is(desc.textContent, "urlbar text", "Popup should have correct description text");
is(icon.src, "", "Popup should have no icon");
is(buttons.hasChildNodes(), false, "Popup should have no buttons");
gContentAPI.showInfo("search", "search title", "search text"); gContentAPI.showInfo("search", "search title", "search text");
executeSoon(function() { executeSoon(function() {

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

@ -5,6 +5,7 @@
let gTestTab; let gTestTab;
let gContentAPI; let gContentAPI;
let gContentWindow;
Components.utils.import("resource:///modules/UITour.jsm"); Components.utils.import("resource:///modules/UITour.jsm");
@ -21,10 +22,10 @@ function loadTestPage(callback, host = "https://example.com/") {
gTestTab.linkedBrowser.addEventListener("load", function onLoad() { gTestTab.linkedBrowser.addEventListener("load", function onLoad() {
gTestTab.linkedBrowser.removeEventListener("load", onLoad); gTestTab.linkedBrowser.removeEventListener("load", onLoad);
let contentWindow = Components.utils.waiveXrays(gTestTab.linkedBrowser.contentDocument.defaultView); gContentWindow = Components.utils.waiveXrays(gTestTab.linkedBrowser.contentDocument.defaultView);
gContentAPI = contentWindow.Mozilla.UITour; gContentAPI = gContentWindow.Mozilla.UITour;
waitForFocus(callback, contentWindow); waitForFocus(callback, gContentWindow);
}, true); }, true);
} }
@ -37,6 +38,7 @@ function test() {
registerCleanupFunction(function() { registerCleanupFunction(function() {
delete window.UITour; delete window.UITour;
delete window.gContentWindow;
delete window.gContentAPI; delete window.gContentAPI;
if (gTestTab) if (gTestTab)
gBrowser.removeTab(gTestTab); gBrowser.removeTab(gTestTab);

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

@ -0,0 +1,191 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
let gTestTab;
let gContentAPI;
let gContentWindow;
Components.utils.import("resource:///modules/UITour.jsm");
function loadTestPage(callback, host = "https://example.com/") {
if (gTestTab)
gBrowser.removeTab(gTestTab);
let url = getRootDirectory(gTestPath) + "uitour.html";
url = url.replace("chrome://mochitests/content/", host);
gTestTab = gBrowser.addTab(url);
gBrowser.selectedTab = gTestTab;
gTestTab.linkedBrowser.addEventListener("load", function onLoad() {
gTestTab.linkedBrowser.removeEventListener("load", onLoad);
gContentWindow = Components.utils.waiveXrays(gTestTab.linkedBrowser.contentDocument.defaultView);
gContentAPI = gContentWindow.Mozilla.UITour;
waitForFocus(callback, gContentWindow);
}, true);
}
function test() {
Services.prefs.setBoolPref("browser.uitour.enabled", true);
let testUri = Services.io.newURI("http://example.com", null, null);
Services.perms.add(testUri, "uitour", Services.perms.ALLOW_ACTION);
waitForExplicitFinish();
registerCleanupFunction(function() {
delete window.UITour;
delete window.gContentWindow;
delete window.gContentAPI;
if (gTestTab)
gBrowser.removeTab(gTestTab);
delete window.gTestTab;
Services.prefs.clearUserPref("browser.uitour.enabled", true);
Services.perms.remove("example.com", "uitour");
});
function done() {
if (gTestTab)
gBrowser.removeTab(gTestTab);
gTestTab = null;
let highlight = document.getElementById("UITourHighlightContainer");
is_element_hidden(highlight, "Highlight should be closed/hidden after UITour tab is closed");
let tooltip = document.getElementById("UITourTooltip");
is_element_hidden(tooltip, "Tooltip should be closed/hidden 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);
}
function nextTest() {
if (tests.length == 0) {
finish();
return;
}
let test = tests.shift();
info("Starting " + test.name);
loadTestPage(function() {
test(done);
});
}
nextTest();
}
let tests = [
function test_info_icon(done) {
let popup = document.getElementById("UITourTooltip");
let title = document.getElementById("UITourTooltipTitle");
let desc = document.getElementById("UITourTooltipDescription");
let icon = document.getElementById("UITourTooltipIcon");
let buttons = document.getElementById("UITourTooltipButtons");
popup.addEventListener("popupshown", function onPopupShown() {
popup.removeEventListener("popupshown", onPopupShown);
is(title.textContent, "a title", "Popup should have correct title");
is(desc.textContent, "some text", "Popup should have correct description text");
let imageURL = getRootDirectory(gTestPath) + "image.png";
imageURL = imageURL.replace("chrome://mochitests/content/", "https://example.com/");
is(icon.src, imageURL, "Popup should have correct icon shown");
is(buttons.hasChildNodes(), false, "Popup should have no buttons");
done();
});
gContentAPI.showInfo("urlbar", "a title", "some text", "image.png");
},
function test_info_buttons_1(done) {
let popup = document.getElementById("UITourTooltip");
let title = document.getElementById("UITourTooltipTitle");
let desc = document.getElementById("UITourTooltipDescription");
let icon = document.getElementById("UITourTooltipIcon");
popup.addEventListener("popupshown", function onPopupShown() {
popup.removeEventListener("popupshown", onPopupShown);
is(title.textContent, "another title", "Popup should have correct title");
is(desc.textContent, "moar text", "Popup should have correct description text");
let imageURL = getRootDirectory(gTestPath) + "image.png";
imageURL = imageURL.replace("chrome://mochitests/content/", "https://example.com/");
is(icon.src, imageURL, "Popup should have correct icon shown");
let buttons = document.getElementById("UITourTooltipButtons");
is(buttons.childElementCount, 2, "Popup should have two buttons");
is(buttons.childNodes[0].getAttribute("label"), "Button 1", "First button should have correct label");
is(buttons.childNodes[0].getAttribute("image"), "", "First button should have no image");
is(buttons.childNodes[1].getAttribute("label"), "Button 2", "Second button should have correct label");
is(buttons.childNodes[1].getAttribute("image"), imageURL, "Second button should have correct image");
popup.addEventListener("popuphidden", function onPopupHidden() {
popup.removeEventListener("popuphidden", onPopupHidden);
ok(true, "Popup should close automatically");
executeSoon(function() {
is(gContentWindow.callbackResult, "button1", "Correct callback should have been called");
done();
});
});
EventUtils.synthesizeMouseAtCenter(buttons.childNodes[0], {}, window);
});
let buttons = gContentWindow.makeButtons();
gContentAPI.showInfo("urlbar", "another title", "moar text", "./image.png", buttons);
},
function test_info_buttons_2(done) {
let popup = document.getElementById("UITourTooltip");
let title = document.getElementById("UITourTooltipTitle");
let desc = document.getElementById("UITourTooltipDescription");
let icon = document.getElementById("UITourTooltipIcon");
popup.addEventListener("popupshown", function onPopupShown() {
popup.removeEventListener("popupshown", onPopupShown);
is(title.textContent, "another title", "Popup should have correct title");
is(desc.textContent, "moar text", "Popup should have correct description text");
let imageURL = getRootDirectory(gTestPath) + "image.png";
imageURL = imageURL.replace("chrome://mochitests/content/", "https://example.com/");
is(icon.src, imageURL, "Popup should have correct icon shown");
let buttons = document.getElementById("UITourTooltipButtons");
is(buttons.childElementCount, 2, "Popup should have two buttons");
is(buttons.childNodes[0].getAttribute("label"), "Button 1", "First button should have correct label");
is(buttons.childNodes[0].getAttribute("image"), "", "First button should have no image");
is(buttons.childNodes[1].getAttribute("label"), "Button 2", "Second button should have correct label");
is(buttons.childNodes[1].getAttribute("image"), imageURL, "Second button should have correct image");
popup.addEventListener("popuphidden", function onPopupHidden() {
popup.removeEventListener("popuphidden", onPopupHidden);
ok(true, "Popup should close automatically");
executeSoon(function() {
is(gContentWindow.callbackResult, "button2", "Correct callback should have been called");
done();
});
});
EventUtils.synthesizeMouseAtCenter(buttons.childNodes[1], {}, window);
});
let buttons = gContentWindow.makeButtons();
gContentAPI.showInfo("urlbar", "another title", "moar text", "./image.png", buttons);
},
];

Двоичные данные
browser/modules/test/image.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 55 KiB

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

@ -5,6 +5,22 @@
<title>UITour test</title> <title>UITour test</title>
<script type="application/javascript" src="uitour.js"> <script type="application/javascript" src="uitour.js">
</script> </script>
<script type="application/javascript">
var callbackResult;
function makeCallback(name) {
return (function() {
callbackResult = name;
});
}
// Defined in content to avoid weird issues when crossing between chrome/content.
function makeButtons() {
return [
{label: "Button 1", callback: makeCallback("button1")},
{label: "Button 2", callback: makeCallback("button2"), icon: "image.png"}
];
}
</script>
</head> </head>
<body> <body>
<h1>UITour tests</h1> <h1>UITour tests</h1>

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

@ -25,7 +25,6 @@ if (typeof Mozilla == 'undefined') {
} }
} }
function _sendEvent(action, data) { function _sendEvent(action, data) {
var event = new CustomEvent('mozUITour', { var event = new CustomEvent('mozUITour', {
bubbles: true, bubbles: true,
@ -34,10 +33,31 @@ if (typeof Mozilla == 'undefined') {
data: data || {} data: data || {}
} }
}); });
console.log("Sending mozUITour event: ", event);
document.dispatchEvent(event); document.dispatchEvent(event);
} }
function _generateCallbackID() {
return Math.random().toString(36).replace(/[^a-z]+/g, '');
}
function _waitForCallback(callback) {
var id = _generateCallbackID();
function listener(event) {
if (typeof event.detail != "object")
return;
if (event.detail.callbackID != id)
return;
document.removeEventListener("mozUITourResponse", listener);
callback(event.detail.data);
}
document.addEventListener("mozUITourResponse", listener);
return id;
}
Mozilla.UITour.DEFAULT_THEME_CYCLE_DELAY = 10 * 1000; Mozilla.UITour.DEFAULT_THEME_CYCLE_DELAY = 10 * 1000;
Mozilla.UITour.showHighlight = function(target, effect) { Mozilla.UITour.showHighlight = function(target, effect) {
@ -51,11 +71,24 @@ if (typeof Mozilla == 'undefined') {
_sendEvent('hideHighlight'); _sendEvent('hideHighlight');
}; };
Mozilla.UITour.showInfo = function(target, title, text) { Mozilla.UITour.showInfo = function(target, title, text, icon, buttons) {
var buttonData = [];
if (Array.isArray(buttons)) {
for (var i = 0; i < buttons.length; i++) {
buttonData.push({
label: buttons[i].label,
icon: buttons[i].icon,
callbackID: _waitForCallback(buttons[i].callback)
});
}
}
_sendEvent('showInfo', { _sendEvent('showInfo', {
target: target, target: target,
title: title, title: title,
text: text text: text,
icon: icon,
buttons: buttonData
}); });
}; };

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

@ -22,8 +22,10 @@
min-width: 32px; min-width: 32px;
} }
#UITourTooltip { #UITourTooltipIcon {
max-width: 20em; width: 48px;
height: 48px;
padding: 8px;
} }
#UITourTooltipTitle { #UITourTooltipTitle {
@ -35,3 +37,13 @@
#UITourTooltipDescription { #UITourTooltipDescription {
max-width: 20em; max-width: 20em;
} }
#UITourTooltipButtons {
height: 5em;
}
#UITourTooltipButtons > button[image] > .button-box > .button-icon {
width: 16px;
height: 16px;
-moz-margin-end: 5px;
}