Merge fx-team to m-c. a=merge

This commit is contained in:
Ryan VanderMeulen 2015-02-21 16:38:13 -05:00
commit f4781ef545
68 changed files with 2275 additions and 424 deletions

View File

@ -1856,3 +1856,6 @@ pref("dom.ipc.reportProcessHangs", true);
// Disable reader mode by default.
pref("reader.parse-on-load.enabled", false);
// Disable ReadingList by default.
pref("browser.readinglist.enabled", false);

View File

@ -156,7 +156,7 @@
<menuitem id="context-video-fullscreen"
accesskey="&videoFullScreen.accesskey;"
label="&videoFullScreen.label;"
oncommand="gContextMenu.fullScreenVideo();"/>
oncommand="gContextMenu.mediaCommand('fullscreen');"/>
<menuitem id="context-leave-dom-fullscreen"
accesskey="&leaveDOMFullScreen.accesskey;"
label="&leaveDOMFullScreen.label;"

View File

@ -223,6 +223,11 @@
key="key_gotoHistory"
observes="viewHistorySidebar"
label="&historyButton.label;"/>
<menuitem id="menu_readingListSidebar"
key="key_readingListSidebar"
observes="readingListSidebar"
label="&readingList.label;"/>
<!-- Service providers with sidebars are inserted between these two menuseperators -->
<menuseparator hidden="true"/>
<menuseparator class="social-provider-menu" hidden="true"/>

View File

@ -0,0 +1,68 @@
/*
# This Source Code Form is subject to the terms of the Mozilla Public
# 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/.
*/
let ReadingListUI = {
/**
* Initialize the ReadingList UI.
*/
init() {
Preferences.observe("browser.readinglist.enabled", this.updateUI, this);
this.updateUI();
},
/**
* Un-initialize the ReadingList UI.
*/
uninit() {
Preferences.ignore("browser.readinglist.enabled", this.updateUI, this);
},
/**
* Whether the ReadingList feature is enabled or not.
* @type {boolean}
*/
get enabled() {
return Preferences.get("browser.readinglist.enabled", false);
},
/**
* Whether the ReadingList sidebar is currently open or not.
* @type {boolean}
*/
get isSidebarOpen() {
return SidebarUI.isOpen && SidebarUI.currentID == "readingListSidebar";
},
/**
* Update the UI status, ensuring the UI is shown or hidden depending on
* whether the feature is enabled or not.
*/
updateUI() {
let enabled = this.enabled;
if (!enabled) {
this.hideSidebar();
}
document.getElementById("readingListSidebar").setAttribute("hidden", !enabled);
},
/**
* Show the ReadingList sidebar.
* @return {Promise}
*/
showSidebar() {
return SidebarUI.show("readingListSidebar");
},
/**
* Hide the ReadingList sidebar, if it is currently shown.
*/
hideSidebar() {
if (this.isSidebarOpen) {
SidebarUI.hide();
}
},
};

View File

@ -138,7 +138,7 @@
<broadcaster id="Social:PageShareOrMark" disabled="true"/>
<broadcaster id="viewBookmarksSidebar" autoCheck="false" label="&bookmarksButton.label;"
type="checkbox" group="sidebar" sidebarurl="chrome://browser/content/bookmarks/bookmarksPanel.xul"
oncommand="toggleSidebar('viewBookmarksSidebar');"/>
oncommand="SidebarUI.toggle('viewBookmarksSidebar');"/>
<!-- for both places and non-places, the sidebar lives at
chrome://browser/content/history/history-panel.xul so there are no
@ -146,11 +146,16 @@
<broadcaster id="viewHistorySidebar" autoCheck="false" sidebartitle="&historyButton.label;"
type="checkbox" group="sidebar"
sidebarurl="chrome://browser/content/history/history-panel.xul"
oncommand="toggleSidebar('viewHistorySidebar');"/>
oncommand="SidebarUI.toggle('viewHistorySidebar');"/>
<broadcaster id="readingListSidebar" hidden="true" autoCheck="false"
sidebartitle="&readingList.label;" type="checkbox" group="sidebar"
sidebarurl="chrome://browser/content/readinglist/sidebar.xhtml"
oncommand="SidebarUI.toggle('readingListSidebar');"/>
<broadcaster id="viewWebPanelsSidebar" autoCheck="false"
type="checkbox" group="sidebar" sidebarurl="chrome://browser/content/web-panels.xul"
oncommand="toggleSidebar('viewWebPanelsSidebar');"/>
oncommand="SidebarUI.toggle('viewWebPanelsSidebar');"/>
<broadcaster id="bookmarkThisPageBroadcaster"
label="&bookmarkThisPageCmd.label;"
@ -418,6 +423,11 @@
#endif
command="viewHistorySidebar"/>
<key id="key_readingListSidebar"
key="&readingList.sidebar.commandKey;"
modifiers="accel,alt"
command="readingListSidebar"/>
<key id="key_fullZoomReduce" key="&fullZoomReduceCmd.commandkey;" command="cmd_fullZoomReduce" modifiers="accel"/>
<key key="&fullZoomReduceCmd.commandkey2;" command="cmd_fullZoomReduce" modifiers="accel"/>
<key id="key_fullZoomEnlarge" key="&fullZoomEnlargeCmd.commandkey;" command="cmd_fullZoomEnlarge" modifiers="accel"/>

View File

@ -0,0 +1,309 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# 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/.
/**
* SidebarUI controls showing and hiding the browser sidebar.
*
* @note
* Some of these methods take a commandID argument - we expect to find a
* xul:broadcaster element with the specified ID.
* The following attributes on that element may be used and/or modified:
* - id (required) the string to match commandID. The convention
* is to use this naming scheme: 'view<sidebar-name>Sidebar'.
* - sidebarurl (required) specifies the URL to load in this sidebar.
* - sidebartitle or label (in that order) specify the title to
* display on the sidebar.
* - checked indicates whether the sidebar is currently displayed.
* Note that toggleSidebar updates this attribute when
* it changes the sidebar's visibility.
* - group this attribute must be set to "sidebar".
*/
let SidebarUI = {
browser: null,
_box: null,
_title: null,
_splitter: null,
init() {
this._box = document.getElementById("sidebar-box");
this.browser = document.getElementById("sidebar");
this._title = document.getElementById("sidebar-title");
this._splitter = document.getElementById("sidebar-splitter");
if (window.opener && !window.opener.closed &&
window.opener.document.documentURIObject.schemeIs("chrome") &&
PrivateBrowsingUtils.isWindowPrivate(window) == PrivateBrowsingUtils.isWindowPrivate(window.opener)) {
this.adoptFromWindow(window.opener);
} else {
let commandID = this._box.getAttribute("sidebarcommand");
if (commandID) {
let command = document.getElementById(commandID);
if (command) {
this._delayedLoad = true;
this._box.hidden = false;
this._splitter.hidden = false;
command.setAttribute("checked", "true");
} else {
// Remove the |sidebarcommand| attribute, because the element it
// refers to no longer exists, so we should assume this sidebar
// panel has been uninstalled. (249883)
this._box.removeAttribute("sidebarcommand");
}
}
}
},
uninit() {
let enumerator = Services.wm.getEnumerator(null);
enumerator.getNext();
if (!enumerator.hasMoreElements()) {
document.persist("sidebar-box", "sidebarcommand");
document.persist("sidebar-box", "width");
document.persist("sidebar-box", "src");
document.persist("sidebar-title", "value");
}
},
/**
* Adopt the status of the sidebar from another window.
* @param {Window} sourceWindow - Window to use as a source for sidebar status.
*/
adoptFromWindow(sourceWindow) {
// If the opener had a sidebar, open the same sidebar in our window.
// The opener can be the hidden window too, if we're coming from the state
// where no windows are open, and the hidden window has no sidebar box.
let sourceUI = sourceWindow.SidebarUI;
if (!sourceUI || sourceUI._box.hidden) {
return;
}
let commandID = sourceUI._box.getAttribute("sidebarcommand");
let commandElem = document.getElementById(commandID);
// dynamically generated sidebars will fail this check.
if (!commandElem) {
return;
}
this._title.setAttribute("value",
sourceUI._title.getAttribute("value"));
this._box.setAttribute("width", sourceUI._box.boxObject.width);
this._box.setAttribute("sidebarcommand", commandID);
// Note: we're setting 'src' on this._box, which is a <vbox>, not on
// the <browser id="sidebar">. This lets us delay the actual load until
// delayedStartup().
this._box.setAttribute("src", sourceUI.browser.getAttribute("src"));
this._delayedLoad = true;
this._box.hidden = false;
this._splitter.hidden = false;
commandElem.setAttribute("checked", "true");
},
/**
* If loading a sidebar was delayed on startup, start the load now.
*/
startDelayedLoad() {
if (!this._delayedLoad) {
return;
}
this.browser.setAttribute("src", this._box.getAttribute("src"));
},
/**
* Fire a "SidebarFocused" event on the sidebar's |window| to give the sidebar
* a chance to adjust focus as needed. An additional event is needed, because
* we don't want to focus the sidebar when it's opened on startup or in a new
* window, only when the user opens the sidebar.
*/
_fireFocusedEvent() {
let event = new CustomEvent("SidebarFocused", {bubbles: true});
this.browser.contentWindow.dispatchEvent(event);
// Run the original function for backwards compatibility.
fireSidebarFocusedEvent();
},
/**
* True if the sidebar is currently open.
*/
get isOpen() {
return !this._box.hidden;
},
/**
* The ID of the current sidebar (ie, the ID of the broadcaster being used).
* This can be set even if the sidebar is hidden.
*/
get currentID() {
return this._box.getAttribute("sidebarcommand");
},
get title() {
return this._title.value;
},
set title(value) {
this._title.value = value;
},
/**
* Toggle the visibility of the sidebar. If the sidebar is hidden or is open
* with a different commandID, then the sidebar will be opened using the
* specified commandID. Otherwise the sidebar will be hidden.
*
* @param {string} commandID ID of the xul:broadcaster element to use.
* @return {Promise}
*/
toggle(commandID = this.currentID) {
if (this.isOpen && commandID == this.currentID) {
this.hide();
return Promise.resolve();
} else {
return this.show(commandID);
}
},
/**
* Show the sidebar, using the parameters from the specified broadcaster.
* @see SidebarUI note.
*
* @param {string} commandID ID of the xul:broadcaster element to use.
*/
show(commandID) {
return new Promise((resolve, reject) => {
let sidebarBroadcaster = document.getElementById(commandID);
if (!sidebarBroadcaster || sidebarBroadcaster.localName != "broadcaster") {
reject(new Error("Invalid sidebar broadcaster specified"));
return;
}
let broadcasters = document.getElementsByAttribute("group", "sidebar");
for (let broadcaster of broadcasters) {
// skip elements that observe sidebar broadcasters and random
// other elements
if (broadcaster.localName != "broadcaster") {
continue;
}
if (broadcaster != sidebarBroadcaster) {
broadcaster.removeAttribute("checked");
} else {
sidebarBroadcaster.setAttribute("checked", "true");
}
}
this._box.hidden = false;
this._splitter.hidden = false;
this._box.setAttribute("sidebarcommand", sidebarBroadcaster.id);
let title = sidebarBroadcaster.getAttribute("sidebartitle");
if (!title) {
title = sidebarBroadcaster.getAttribute("label");
}
this._title.value = title;
let url = sidebarBroadcaster.getAttribute("sidebarurl");
this.browser.setAttribute("src", url); // kick off async load
// We set this attribute here in addition to setting it on the <browser>
// element itself, because the code in SidebarUI.uninit() persists this
// attribute, not the "src" of the <browser id="sidebar">. The reason it
// does that is that we want to delay sidebar load a bit when a browser
// window opens. See delayedStartup() and SidebarUI.startDelayedLoad().
this._box.setAttribute("src", url);
if (this.browser.contentDocument.location.href != url) {
let onLoad = event => {
this.browser.removeEventListener("load", onLoad, true);
// We're handling the 'load' event before it bubbles up to the usual
// (non-capturing) event handlers. Let it bubble up before firing the
// SidebarFocused event.
setTimeout(() => this._fireFocusedEvent(), 0);
// Run the original function for backwards compatibility.
sidebarOnLoad(event);
resolve();
};
this.browser.addEventListener("load", onLoad, true);
} else {
// Older code handled this case, so we do it too.
this._fireFocusedEvent();
resolve();
}
});
},
/**
* Hide the sidebar.
*/
hide() {
if (!this.isOpen) {
return;
}
let commandID = this._box.getAttribute("sidebarcommand");
let sidebarBroadcaster = document.getElementById(commandID);
if (sidebarBroadcaster.getAttribute("checked") != "true") {
return;
}
// Replace the document currently displayed in the sidebar with about:blank
// so that we can free memory by unloading the page. We need to explicitly
// create a new content viewer because the old one doesn't get destroyed
// until about:blank has loaded (which does not happen as long as the
// element is hidden).
this.browser.setAttribute("src", "about:blank");
this.browser.docShell.createAboutBlankContentViewer(null);
sidebarBroadcaster.removeAttribute("checked");
this._box.setAttribute("sidebarcommand", "");
this._title.value = "";
this._box.hidden = true;
this._splitter.hidden = true;
gBrowser.selectedBrowser.focus();
},
};
/**
* This exists for backards compatibility - it will be called once a sidebar is
* ready, following any request to show it.
*
* @deprecated
*/
function fireSidebarFocusedEvent() {}
/**
* This exists for backards compatibility - it gets called when a sidebar has
* been loaded.
*
* @deprecated
*/
function sidebarOnLoad(event) {}
/**
* This exists for backards compatibility, and is equivilent to
* SidebarUI.toggle() without the forceOpen param. With forceOpen set to true,
* it is equalivent to SidebarUI.show().
*
* @deprecated
*/
function toggleSidebar(commandID, forceOpen = false) {
Deprecated.warning("toggleSidebar() is deprecated, please use SidebarUI.toggle() or SidebarUI.show() instead",
"https://developer.mozilla.org/en-US/Add-ons/Code_snippets/Sidebar");
if (forceOpen) {
SidebarUI.show(commandID);
} else {
SidebarUI.toggle(commandID);
}
}

View File

@ -12,6 +12,11 @@ Cu.import("resource://gre/modules/NotificationDB.jsm");
Cu.import("resource:///modules/RecentWindow.jsm");
Cu.import("resource://gre/modules/WindowsPrefSync.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
"resource://gre/modules/Preferences.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
"resource://gre/modules/Deprecated.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry",
"resource:///modules/BrowserUITelemetry.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
@ -222,7 +227,9 @@ let gInitialPages = [
#include browser-loop.js
#include browser-places.js
#include browser-plugins.js
#include browser-readinglist.js
#include browser-safebrowsing.js
#include browser-sidebar.js
#include browser-social.js
#include browser-tabview.js
#include browser-thumbnails.js
@ -899,8 +906,6 @@ var gBrowserInit = {
delayedStartupFinished: false,
onLoad: function() {
var mustLoadSidebar = false;
gBrowser.addEventListener("DOMUpdatePageReport", gPopupBlockerObserver, false);
Services.obs.addObserver(gPluginHandler.pluginCrashed, "plugin-crashed", false);
@ -952,61 +957,7 @@ var gBrowserInit = {
// setup history swipe animation
gHistorySwipeAnimation.init();
if (window.opener && !window.opener.closed &&
window.opener.document.documentURIObject.schemeIs("chrome") &&
PrivateBrowsingUtils.isWindowPrivate(window) == PrivateBrowsingUtils.isWindowPrivate(window.opener)) {
let openerSidebarBox = window.opener.document.getElementById("sidebar-box");
// If the opener had a sidebar, open the same sidebar in our window.
// The opener can be the hidden window too, if we're coming from the state
// where no windows are open, and the hidden window has no sidebar box.
if (openerSidebarBox && !openerSidebarBox.hidden) {
let sidebarCmd = openerSidebarBox.getAttribute("sidebarcommand");
let sidebarCmdElem = document.getElementById(sidebarCmd);
// dynamically generated sidebars will fail this check.
if (sidebarCmdElem) {
let sidebarBox = document.getElementById("sidebar-box");
let sidebarTitle = document.getElementById("sidebar-title");
sidebarTitle.setAttribute(
"value", window.opener.document.getElementById("sidebar-title").getAttribute("value"));
sidebarBox.setAttribute("width", openerSidebarBox.boxObject.width);
sidebarBox.setAttribute("sidebarcommand", sidebarCmd);
// Note: we're setting 'src' on sidebarBox, which is a <vbox>, not on
// the <browser id="sidebar">. This lets us delay the actual load until
// delayedStartup().
sidebarBox.setAttribute(
"src", window.opener.document.getElementById("sidebar").getAttribute("src"));
mustLoadSidebar = true;
sidebarBox.hidden = false;
document.getElementById("sidebar-splitter").hidden = false;
sidebarCmdElem.setAttribute("checked", "true");
}
}
}
else {
let box = document.getElementById("sidebar-box");
if (box.hasAttribute("sidebarcommand")) {
let commandID = box.getAttribute("sidebarcommand");
if (commandID) {
let command = document.getElementById(commandID);
if (command) {
mustLoadSidebar = true;
box.hidden = false;
document.getElementById("sidebar-splitter").hidden = false;
command.setAttribute("checked", "true");
}
else {
// Remove the |sidebarcommand| attribute, because the element it
// refers to no longer exists, so we should assume this sidebar
// panel has been uninstalled. (249883)
box.removeAttribute("sidebarcommand");
}
}
}
}
SidebarUI.init();
// Certain kinds of automigration rely on this notification to complete
// their tasks BEFORE the browser window is shown. SessionStore uses it to
@ -1095,7 +1046,7 @@ var gBrowserInit = {
ToolbarIconColor.init();
// Wait until chrome is painted before executing code not critical to making the window visible
this._boundDelayedStartup = this._delayedStartup.bind(this, mustLoadSidebar);
this._boundDelayedStartup = this._delayedStartup.bind(this);
window.addEventListener("MozAfterPaint", this._boundDelayedStartup);
this._loadHandled = true;
@ -1106,7 +1057,7 @@ var gBrowserInit = {
this._boundDelayedStartup = null;
},
_delayedStartup: function(mustLoadSidebar) {
_delayedStartup: function() {
let tmp = {};
Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", tmp);
let TelemetryTimestamps = tmp.TelemetryTimestamps;
@ -1269,11 +1220,7 @@ var gBrowserInit = {
Services.telemetry.getHistogramById("E10S_WINDOW").add(gMultiProcessBrowser);
if (mustLoadSidebar) {
let sidebar = document.getElementById("sidebar");
let sidebarBox = document.getElementById("sidebar-box");
sidebar.setAttribute("src", sidebarBox.getAttribute("src"));
}
SidebarUI.startDelayedLoad();
UpdateUrlbarSearchSplitterState();
@ -1427,6 +1374,7 @@ var gBrowserInit = {
SocialUI.init();
TabView.init();
ReadingListUI.init();
// Telemetry for master-password - we do this after 5 seconds as it
// can cause IO if NSS/PSM has not already initialized.
@ -1535,14 +1483,9 @@ var gBrowserInit = {
gMenuButtonUpdateBadge.uninit();
var enumerator = Services.wm.getEnumerator(null);
enumerator.getNext();
if (!enumerator.hasMoreElements()) {
document.persist("sidebar-box", "sidebarcommand");
document.persist("sidebar-box", "width");
document.persist("sidebar-box", "src");
document.persist("sidebar-title", "value");
}
ReadingListUI.uninit();
SidebarUI.uninit();
// Now either cancel delayedStartup, or clean up the services initialized from
// it.
@ -1769,7 +1712,7 @@ function HandleAppCommandEvent(evt) {
BrowserSearch.webSearch();
break;
case "Bookmarks":
toggleSidebar('viewBookmarksSidebar');
SidebarUI.toggle("viewBookmarksSidebar");
break;
case "Home":
BrowserHome();
@ -3139,23 +3082,19 @@ var PrintPreviewListener = {
else
this._showChrome();
if (this._chromeState.sidebarOpen)
toggleSidebar(this._sidebarCommand);
TabsInTitlebar.allowedBy("print-preview", !gInPrintPreviewMode);
},
_hideChrome: function () {
this._chromeState = {};
var sidebar = document.getElementById("sidebar-box");
this._chromeState.sidebarOpen = !sidebar.hidden;
this._sidebarCommand = sidebar.getAttribute("sidebarcommand");
this._chromeState.sidebarOpen = SidebarUI.isOpen;
this._sidebarCommand = SidebarUI.currentID;
SidebarUI.hide();
var notificationBox = gBrowser.getNotificationBox();
this._chromeState.notificationsOpen = !notificationBox.notificationsHidden;
notificationBox.notificationsHidden = true;
document.getElementById("sidebar").setAttribute("src", "about:blank");
gBrowser.updateWindowResizers();
this._chromeState.findOpen = gFindBarInitialized && !gFindBar.hidden;
@ -3185,6 +3124,9 @@ var PrintPreviewListener = {
if (this._chromeState.syncNotificationsOpen)
document.getElementById("sync-notifications").notificationsHidden = false;
if (this._chromeState.sidebarOpen)
SidebarUI.show(this._sidebarCommand);
}
}
@ -5213,121 +5155,6 @@ function displaySecurityInfo()
BrowserPageInfo(null, "securityTab");
}
/**
* Opens or closes the sidebar identified by commandID.
*
* @param commandID a string identifying the sidebar to toggle; see the
* note below. (Optional if a sidebar is already open.)
* @param forceOpen boolean indicating whether the sidebar should be
* opened regardless of its current state (optional).
* @note
* We expect to find a xul:broadcaster element with the specified ID.
* The following attributes on that element may be used and/or modified:
* - id (required) the string to match commandID. The convention
* is to use this naming scheme: 'view<sidebar-name>Sidebar'.
* - sidebarurl (required) specifies the URL to load in this sidebar.
* - sidebartitle or label (in that order) specify the title to
* display on the sidebar.
* - checked indicates whether the sidebar is currently displayed.
* Note that toggleSidebar updates this attribute when
* it changes the sidebar's visibility.
* - group this attribute must be set to "sidebar".
*/
function toggleSidebar(commandID, forceOpen) {
var sidebarBox = document.getElementById("sidebar-box");
if (!commandID)
commandID = sidebarBox.getAttribute("sidebarcommand");
var sidebarBroadcaster = document.getElementById(commandID);
var sidebar = document.getElementById("sidebar"); // xul:browser
var sidebarTitle = document.getElementById("sidebar-title");
var sidebarSplitter = document.getElementById("sidebar-splitter");
if (sidebarBroadcaster.getAttribute("checked") == "true") {
if (!forceOpen) {
// Replace the document currently displayed in the sidebar with about:blank
// so that we can free memory by unloading the page. We need to explicitly
// create a new content viewer because the old one doesn't get destroyed
// until about:blank has loaded (which does not happen as long as the
// element is hidden).
sidebar.setAttribute("src", "about:blank");
sidebar.docShell.createAboutBlankContentViewer(null);
sidebarBroadcaster.removeAttribute("checked");
sidebarBox.setAttribute("sidebarcommand", "");
sidebarTitle.value = "";
sidebarBox.hidden = true;
sidebarSplitter.hidden = true;
gBrowser.selectedBrowser.focus();
} else {
fireSidebarFocusedEvent();
}
return;
}
// now we need to show the specified sidebar
// ..but first update the 'checked' state of all sidebar broadcasters
var broadcasters = document.getElementsByAttribute("group", "sidebar");
for (let broadcaster of broadcasters) {
// skip elements that observe sidebar broadcasters and random
// other elements
if (broadcaster.localName != "broadcaster")
continue;
if (broadcaster != sidebarBroadcaster)
broadcaster.removeAttribute("checked");
else
sidebarBroadcaster.setAttribute("checked", "true");
}
sidebarBox.hidden = false;
sidebarSplitter.hidden = false;
var url = sidebarBroadcaster.getAttribute("sidebarurl");
var title = sidebarBroadcaster.getAttribute("sidebartitle");
if (!title)
title = sidebarBroadcaster.getAttribute("label");
sidebar.setAttribute("src", url); // kick off async load
sidebarBox.setAttribute("sidebarcommand", sidebarBroadcaster.id);
sidebarTitle.value = title;
// We set this attribute here in addition to setting it on the <browser>
// element itself, because the code in gBrowserInit.onUnload persists this
// attribute, not the "src" of the <browser id="sidebar">. The reason it
// does that is that we want to delay sidebar load a bit when a browser
// window opens. See delayedStartup().
sidebarBox.setAttribute("src", url);
if (sidebar.contentDocument.location.href != url)
sidebar.addEventListener("load", sidebarOnLoad, true);
else // older code handled this case, so we do it too
fireSidebarFocusedEvent();
}
function sidebarOnLoad(event) {
var sidebar = document.getElementById("sidebar");
sidebar.removeEventListener("load", sidebarOnLoad, true);
// We're handling the 'load' event before it bubbles up to the usual
// (non-capturing) event handlers. Let it bubble up before firing the
// SidebarFocused event.
setTimeout(fireSidebarFocusedEvent, 0);
}
/**
* Fire a "SidebarFocused" event on the sidebar's |window| to give the sidebar
* a chance to adjust focus as needed. An additional event is needed, because
* we don't want to focus the sidebar when it's opened on startup or in a new
* window, only when the user opens the sidebar.
*/
function fireSidebarFocusedEvent() {
var sidebar = document.getElementById("sidebar");
var event = document.createEvent("Events");
event.initEvent("SidebarFocused", true, false);
sidebar.contentWindow.dispatchEvent(event);
}
var gHomeButton = {
prefDomain: "browser.startup.homepage",
@ -5474,38 +5301,37 @@ function getBrowserSelection(aCharLen) {
}
var gWebPanelURI;
function openWebPanel(aTitle, aURI)
{
// Ensure that the web panels sidebar is open.
toggleSidebar('viewWebPanelsSidebar', true);
function openWebPanel(title, uri) {
// Ensure that the web panels sidebar is open.
SidebarUI.show("viewWebPanelsSidebar");
// Set the title of the panel.
document.getElementById("sidebar-title").value = aTitle;
// Set the title of the panel.
SidebarUI.title = title;
// Tell the Web Panels sidebar to load the bookmark.
var sidebar = document.getElementById("sidebar");
if (sidebar.docShell && sidebar.contentDocument && sidebar.contentDocument.getElementById('web-panels-browser')) {
sidebar.contentWindow.loadWebPanel(aURI);
if (gWebPanelURI) {
gWebPanelURI = "";
sidebar.removeEventListener("load", asyncOpenWebPanel, true);
}
// Tell the Web Panels sidebar to load the bookmark.
if (SidebarUI.browser.docShell && SidebarUI.browser.contentDocument &&
SidebarUI.browser.contentDocument.getElementById("web-panels-browser")) {
SidebarUI.browser.contentWindow.loadWebPanel(uri);
if (gWebPanelURI) {
gWebPanelURI = "";
SidebarUI.browser.removeEventListener("load", asyncOpenWebPanel, true);
}
else {
// The panel is still being constructed. Attach an onload handler.
if (!gWebPanelURI)
sidebar.addEventListener("load", asyncOpenWebPanel, true);
gWebPanelURI = aURI;
} else {
// The panel is still being constructed. Attach an onload handler.
if (!gWebPanelURI) {
SidebarUI.browser.addEventListener("load", asyncOpenWebPanel, true);
}
gWebPanelURI = uri;
}
}
function asyncOpenWebPanel(event)
{
var sidebar = document.getElementById("sidebar");
if (gWebPanelURI && sidebar.contentDocument && sidebar.contentDocument.getElementById('web-panels-browser'))
sidebar.contentWindow.loadWebPanel(gWebPanelURI);
gWebPanelURI = "";
sidebar.removeEventListener("load", asyncOpenWebPanel, true);
function asyncOpenWebPanel(event) {
if (gWebPanelURI && SidebarUI.browser.contentDocument &&
SidebarUI.browser.contentDocument.getElementById("web-panels-browser")) {
SidebarUI.browser.contentWindow.loadWebPanel(gWebPanelURI);
}
gWebPanelURI = "";
SidebarUI.browser.removeEventListener("load", asyncOpenWebPanel, true);
}
/*

View File

@ -885,7 +885,7 @@
class="subviewbutton"
label="&viewBookmarksSidebar2.label;"
type="checkbox"
oncommand="toggleSidebar('viewBookmarksSidebar');">
oncommand="SidebarUI.toggle('viewBookmarksSidebar');">
<observes element="viewBookmarksSidebar" attribute="checked"/>
</menuitem>
<!-- NB: temporary solution for bug 985024, this should go away soon. -->
@ -1138,10 +1138,10 @@
<sidebarheader id="sidebar-header" align="center">
<label id="sidebar-title" persist="value" flex="1" crop="end" control="sidebar"/>
<image id="sidebar-throbber"/>
<toolbarbutton class="close-icon tabbable" tooltiptext="&sidebarCloseButton.tooltip;" oncommand="toggleSidebar();"/>
<toolbarbutton class="close-icon tabbable" tooltiptext="&sidebarCloseButton.tooltip;" oncommand="SidebarUI.hide();"/>
</sidebarheader>
<browser id="sidebar" flex="1" autoscroll="false" disablehistory="true"
style="min-width: 14em; width: 18em; max-width: 36em;"/>
style="min-width: 14em; width: 18em; max-width: 36em;" tooltip="aHTMLTooltip"/>
</vbox>
<splitter id="sidebar-splitter" class="chromeclass-extrachrome sidebar-splitter" hidden="true"/>

View File

@ -1097,5 +1097,9 @@ addMessageListener("ContextMenu:MediaCommand", (message) => {
message.data.command == "showstats");
media.dispatchEvent(event);
break;
case "fullscreen":
if (content.document.mozFullScreenEnabled)
media.mozRequestFullScreen();
break;
}
});

View File

@ -1078,12 +1078,6 @@ nsContextMenu.prototype = {
mm.addMessageListener("ContextMenu:SaveVideoFrameAsImage:Result", onMessage);
},
fullScreenVideo: function () {
let video = this.target;
if (document.mozFullScreenEnabled)
video.mozRequestFullScreen();
},
leaveDOMFullScreen: function() {
document.mozCancelFullScreen();
},

View File

@ -0,0 +1,70 @@
"use strict";
this.EXPORTED_SYMBOLS = [
"BrowserUITestUtils",
];
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/Timer.jsm");
/**
* Default wait period in millseconds, when waiting for the expected event to occur.
* @type {number}
*/
const DEFAULT_WAIT = 2000;
/**
* Test utility functions for dealing with the browser UI DOM.
*/
this.BrowserUITestUtils = {
/**
* Waits a specified number of miliseconds for a specified event to be
* fired on a specified element.
*
* Usage:
* let receivedEvent = BrowserUITestUtils.waitForEvent(element, "eventName");
* // Do some processing here that will cause the event to be fired
* // ...
* // Now yield until the Promise is fulfilled
* yield receivedEvent;
* if (receivedEvent && !(receivedEvent instanceof Error)) {
* receivedEvent.msg == "eventName";
* // ...
* }
*
* @param {Element} subject - The element that should receive the event.
* @param {string} eventName - The event to wait for.
* @param {number} timeoutMs - The number of miliseconds to wait before giving up.
* @param {Element} target - Expected target of the event.
* @returns {Promise} A Promise that resolves to the received event, or
* rejects with an Error.
*/
waitForEvent(subject, eventName, timeoutMs, target) {
return new Promise((resolve, reject) => {
function listener(event) {
if (target && target !== event.target) {
return;
}
subject.removeEventListener(eventName, listener);
clearTimeout(timerID);
resolve(event);
}
timeoutMs = timeoutMs || DEFAULT_WAIT;
let stack = new Error().stack;
let timerID = setTimeout(() => {
subject.removeEventListener(eventName, listener);
reject(new Error(`${eventName} event timeout at ${stack}`));
}, timeoutMs);
subject.addEventListener(eventName, listener);
});
},
};

View File

@ -4,7 +4,7 @@ function test() {
// XXX This looks a bit odd, but is needed to avoid throwing when removing the
// event listeners below. See bug 310955.
document.getElementById("sidebar").addEventListener("load", delayedOpenUrl, true);
toggleSidebar("viewWebPanelsSidebar", true);
SidebarUI.show("viewWebPanelsSidebar");
}
function delayedOpenUrl() {
@ -75,7 +75,7 @@ function contextMenuClosed()
var sidebar = document.getElementById("sidebar");
sidebar.contentDocument.removeEventListener("popuphidden", contextMenuClosed, false);
toggleSidebar("viewWebPanelsSidebar");
SidebarUI.hide();
ok(document.getElementById("sidebar-box").hidden, "Sidebar successfully hidden");

View File

@ -6,6 +6,10 @@
SPHINX_TREES['sslerrorreport'] = 'content/docs/sslerrorreport'
TESTING_JS_MODULES += [
'content/test/BrowserUITestUtils.jsm',
]
MOCHITEST_MANIFESTS += [
'content/test/general/mochitest.ini',
]

View File

@ -120,6 +120,7 @@ static RedirEntry kRedirMap[] = {
{ "reader", "chrome://global/content/reader/aboutReader.html",
nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
nsIAboutModule::ALLOW_SCRIPT |
nsIAboutModule::URI_MUST_LOAD_IN_CHILD |
nsIAboutModule::HIDE_FROM_ABOUTABOUT },
};
static const int kRedirTotal = ArrayLength(kRedirMap);

View File

@ -64,7 +64,7 @@
type="checkbox"
class="subviewbutton"
key="key_gotoHistory"
oncommand="toggleSidebar('viewHistorySidebar'); PanelUI.hide();">
oncommand="SidebarUI.toggle('viewHistorySidebar'); PanelUI.hide();">
<observes element="viewHistorySidebar" attribute="checked"/>
</toolbarbutton>
<toolbarbutton id="appMenuClearRecentHistory"
@ -108,7 +108,7 @@
label="&viewBookmarksSidebar2.label;"
class="subviewbutton"
key="viewBookmarksSidebarKb"
oncommand="toggleSidebar('viewBookmarksSidebar'); PanelUI.hide();">
oncommand="SidebarUI.toggle('viewBookmarksSidebar'); PanelUI.hide();">
<observes element="viewBookmarksSidebar" attribute="checked"/>
</toolbarbutton>
<toolbarbutton id="panelMenu_viewBookmarksToolbar"

View File

@ -22,7 +22,7 @@ registerCleanupFunction(() => {
// Ensure sidebar is hidden after each test:
if (!document.getElementById("sidebar-box").hidden) {
toggleSidebar("", false);
SidebarUI.hide();
}
});
@ -53,9 +53,13 @@ function removeWidget() {
// Filters out the trailing menuseparators from the sidebar list
function getSidebarList() {
let sidebars = [...gSidebarMenu.children];
while (sidebars[sidebars.length - 1].localName == "menuseparator")
sidebars.pop();
let sidebars = [...gSidebarMenu.children].filter(sidebar => {
if (sidebar.localName == "menuseparator")
return false;
if (sidebar.getAttribute("hidden") == "true")
return false;
return true;
});
return sidebars;
}

View File

@ -15,6 +15,7 @@ DIRS += [
'places',
'preferences',
'privatebrowsing',
'readinglist',
'search',
'sessionstore',
'shell',

View File

@ -16,7 +16,9 @@ add_task(function* test() {
}
let sidebar = yield promiseLoadedSidebar("viewBookmarksSidebar");
registerCleanupFunction(toggleSidebar);
registerCleanupFunction(() => {
SidebarUI.hide();
});
// Focus the tree and check if its controller is returned.
let tree = sidebar.contentDocument.getElementById("bookmarks-view");
@ -59,6 +61,6 @@ function promiseLoadedSidebar(cmd) {
resolve(sidebar);
}, true);
toggleSidebar(cmd, true);
SidebarUI.show(cmd);
});
}

View File

@ -79,7 +79,7 @@ gTests.push({
finish: function() {
// Close window, toggle sidebar and goto next test.
this.window.document.documentElement.cancelDialog();
toggleSidebar(this.sidebar, false);
SidebarUI.hide();
runNextTest();
},
@ -138,7 +138,7 @@ gTests.push({
finish: function() {
this.window.document.documentElement.cancelDialog();
toggleSidebar(this.sidebar, false);
SidebarUI.hide();
runNextTest();
},
@ -232,7 +232,7 @@ gTests.push({
},
finish: function() {
toggleSidebar(this.sidebar, false);
SidebarUI.hide();
runNextTest();
},
@ -291,7 +291,7 @@ gTests.push({
finish: function() {
// Window is already closed.
toggleSidebar(this.sidebar, false);
SidebarUI.hide();
runNextTest();
},
@ -390,7 +390,7 @@ gTests.push({
},
finish: function() {
toggleSidebar(this.sidebar, false);
SidebarUI.hide();
runNextTest();
},
@ -468,7 +468,7 @@ gTests.push({
},
finish: function() {
toggleSidebar(this.sidebar, false);
SidebarUI.hide();
runNextTest();
},
@ -529,7 +529,7 @@ function execute_test_in_sidebar() {
// Need to executeSoon since the tree is initialized on sidebar load.
executeSoon(open_properties_dialog);
}, true);
toggleSidebar(gCurrentTest.sidebar, true);
SidebarUI.show(gCurrentTest.sidebar);
}
function open_properties_dialog() {

View File

@ -44,7 +44,7 @@ function continue_test() {
transition: hs.TRANSITION_TYPED});
}
PlacesTestUtils.addVisits(places).then(() => {
toggleSidebar("viewHistorySidebar", true);
SidebarUI.show("viewHistorySidebar");
});
sidebar.addEventListener("load", function() {
@ -63,7 +63,7 @@ function continue_test() {
check_sidebar_tree_order(pages.length);
// Cleanup.
toggleSidebar("viewHistorySidebar", false);
SidebarUI.hide();
PlacesTestUtils.clearHistory().then(finish);
});
}, true);

View File

@ -18,7 +18,7 @@ function test() {
// If a sidebar is already open, close it.
if (!document.getElementById("sidebar-box").hidden) {
info("Unexpected sidebar found - a previous test failed to cleanup correctly");
toggleSidebar();
SidebarUI.hide();
}
let sidebar = document.getElementById("sidebar");
@ -76,7 +76,7 @@ function test() {
function testPlacesPanel(preFunc, postFunc) {
currentTest.init(function() {
toggleSidebar(currentTest.sidebarName);
SidebarUI.show(currentTest.sidebarName);
});
sidebar.addEventListener("load", function() {
@ -95,7 +95,7 @@ function test() {
aSubject.Dialog.ui.button0.click();
executeSoon(function () {
toggleSidebar(currentTest.sidebarName);
SidebarUI.hide();
currentTest.cleanup(postFunc);
});
}

View File

@ -37,7 +37,7 @@ function openBookmarksSidebar() {
// Need to executeSoon since the tree is initialized on sidebar load.
executeSoon(startTest);
}, true);
toggleSidebar("viewBookmarksSidebar", true);
SidebarUI.show("viewBookmarksSidebar");
}
/**
@ -171,7 +171,7 @@ function startTest() {
*/
function finishTest() {
// Close bookmarks sidebar.
toggleSidebar("viewBookmarksSidebar", false);
SidebarUI.hide();
// Collapse the personal toolbar if needed.
if (wasCollapsed) {

View File

@ -1,6 +1,8 @@
let gMimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
let gHandlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(Ci.nsIHandlerService);
Services.prefs.setBoolPref("browser.preferences.inContent", true);
function setupFakeHandler() {
let info = gMimeSvc.getFromTypeAndExtension("text/plain", "foo.txt");
ok(info.possibleLocalHandlers.length, "Should have at least one known handler");
@ -92,5 +94,6 @@ add_task(function*() {
registerCleanupFunction(function() {
let infoToModify = gMimeSvc.getFromTypeAndExtension("text/x-test-handler", null);
gHandlerSvc.remove(infoToModify);
Services.prefs.clearUserPref("browser.preferences.inContent");
});

View File

@ -33,7 +33,7 @@ function test() {
resolve(win);
}, true);
win.toggleSidebar(sidebarID, true);
win.SidebarUI.show(sidebarID);
return promise;
}

View File

@ -0,0 +1,356 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* 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 = ["ReadingList"];
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/Preferences.jsm");
Cu.import("resource://gre/modules/Log.jsm");
(function() {
let parentLog = Log.repository.getLogger("readinglist");
parentLog.level = Preferences.get("browser.readinglist.logLevel", Log.Level.Warn);
Preferences.observe("browser.readinglist.logLevel", value => {
parentLog.level = value;
});
let formatter = new Log.BasicFormatter();
parentLog.addAppender(new Log.ConsoleAppender(formatter));
parentLog.addAppender(new Log.DumpAppender(formatter));
})();
let log = Log.repository.getLogger("readinglist.api");
/**
* Represents an item in the Reading List.
* @constructor
* @see https://github.com/mozilla-services/readinglist/wiki/API-Design-proposal#data-model
*/
function Item(data) {
this._data = data;
}
Item.prototype = {
/**
* UUID
* @type {string}
*/
get id() {
return this._data.id;
},
/**
* Server timestamp
* @type {string}
*/
get lastModified() {
return this._data.last_modified;
},
/**
* @type {nsIURL}
*/
get originalUrl() {
return Services.io.newURI(this._data.url, null, null);
},
/**
* @type {string}
*/
get originalTitle() {
return this._data.title || "";
},
/**
* @type {nsIURL}
*/
get resolvedUrl() {
return Services.io.newURI(this._data.resolved_url || this._data.url, null, null);
},
/**
* @type {string}
*/
get resolvedTitle() {
return this._data.resolved_title || this.originalTitle;
},
/**
* @type {string}
*/
get excerpt() {
return this._data.excerpt || "";
},
/**
* @type {ItemStates}
*/
get state() {
return ReadingList.ItemStates[this._data.state] || ReadingList.ItemStates.OK;
},
/**
* @type {boolean}
*/
get isFavorite() {
return !!this._data.favorite;
},
/**
* @type {boolean}
*/
get isArticle() {
return !!this._data.is_article;
},
/**
* @type {number}
*/
get wordCount() {
return this._data.word_count || 0;
},
/**
* @type {boolean}
*/
get isUnread() {
return !!this._data.unread;
},
/**
* Device name
* @type {string}
*/
get addedBy() {
return this._data.added_by;
},
/**
* @type {Date}
*/
get addedOn() {
return new Date(this._data.added_on);
},
/**
* @type {Date}
*/
get storedOn() {
return new Date(this._data.stored_on);
},
/**
* Device name
* @type {string}
*/
get markedReadBy() {
return this._data.marked_read_by;
},
/**
* @type {Date}
*/
get markedReadOn() {
return new date(this._data.marked_read_on);
},
/**
* @type {number}
*/
get readPosition() {
return this._data.read_position;
},
// Data not specified by the current server API
/**
* Array of scraped or captured summary images for this page.
* TODO: Implement this.
* @type {[nsIURL]}
*/
get images() {
return [];
},
/**
* Favicon for this site.
* @type {nsIURL}
* TODO: Generate moz-anno: URI for favicon.
*/
get favicon() {
return null;
},
// Helpers
/**
* Alias for resolvedUrl.
* TODO: This url/resolvedUrl alias makes it feel like the server API hasn't got this right.
*/
get url() {
return this.resolvedUrl;
},
/**
* Alias for resolvedTitle
*/
get title() {
return this.resolvedTitle;
},
/**
* Domain portion of the URL, with prefixes stripped. For display purposes.
* @type {string}
*/
get domain() {
let host = this.resolvedUrl.host;
if (host.startsWith("www.")) {
host = host.slice(4);
}
return host;
},
/**
* Convert this Item to a string representation.
*/
toString() {
return `[Item url=${this.url.spec}]`;
},
/**
* Get the value that should be used for a JSON representation of this Item.
*/
toJSON() {
return this._data;
},
};
let ItemStates = {
OK: Symbol("ok"),
ARCHIVED: Symbol("archived"),
DELETED: Symbol("deleted"),
};
this.ReadingList = {
Item: Item,
ItemStates: ItemStates,
_listeners: new Set(),
_items: [],
/**
* Initialize the ReadingList component.
*/
_init() {
log.debug("Init");
// Initialize mock data
let mockData = JSON.parse(Preferences.get("browser.readinglist.mockData", "[]"));
for (let itemData of mockData) {
this._items.push(new Item(itemData));
}
},
/**
* Add an event listener.
* @param {object} listener - Listener object to start notifying.
*/
addListener(listener) {
this._listeners.add(listener);
},
/**
* Remove a specified event listener.
* @param {object} listener - Listener object to stop notifying.
*/
removeListener(listener) {
this._listeners.delete(listener);
},
/**
* Notify all registered event listeners of an event.
* @param {string} eventName - Event name, which will be used as a method name
* on listeners to call.
*/
_notifyListeners(eventName, ...args) {
for (let listener of this._listeners) {
if (typeof listener[eventName] != "function") {
continue;
}
try {
listener[eventName](...args);
} catch (e) {
log.error(`Error calling listener.${eventName}`, e);
}
}
},
/**
* Fetch the number of items that match a set of given conditions.
* TODO: Implement filtering, sorting, etc. Needs backend storage work.
*
* @param {Object} conditions Object specifying a set of conditions for
* filtering items.
* @return {Promise}
* @resolves {number}
*/
getNumItems(conditions = {unread: false}) {
return new Promise((resolve, reject) => {
resolve(this._items.length);
});
},
/**
* Fetch items matching a set of conditions, in a sorted list.
* TODO: Implement filtering, sorting, etc. Needs backend storage work.
*
* @return {Promise}
* @resolves {[Item]}
*/
getItems(options = {sort: "addedOn", conditions: {unread: false}}) {
return new Promise((resolve, reject) => {
resolve([...this._items]);
});
},
/**
* Find an item based on its ID.
* TODO: Implement. Needs backend storage work.
*
* @return {Promise}
* @resolves {Item}
*/
getItemByID(url) {
return new Promise((resolve, reject) => {
resolve(null);
});
},
/**
* Find an item based on its URL.
*
* TODO: Implement. Needs backend storage work.
* TODO: Does this match original or resolved URL, or both?
* TODO: Should this just be a generic findItem API?
*
* @return {Promise}
* @resolves {Item}
*/
getItemByURL(url) {
return new Promise((resolve, reject) => {
resolve(null);
});
},
};
ReadingList._init();

View File

@ -0,0 +1,7 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# 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/.
browser.jar:
content/browser/readinglist/sidebar.xhtml
content/browser/readinglist/sidebar.js

View File

@ -0,0 +1,15 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# 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/.
JAR_MANIFESTS += ['jar.mn']
EXTRA_JS_MODULES.readinglist += [
'ReadingList.jsm',
]
TESTING_JS_MODULES += [
'test/ReadingListTestUtils.jsm',
]
BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']

View File

@ -0,0 +1,403 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* 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";
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:///modules/readinglist/ReadingList.jsm");
let log = Cu.import("resource://gre/modules/Log.jsm", {})
.Log.repository.getLogger("readinglist.sidebar");
let RLSidebar = {
/**
* Container element for all list item elements.
* @type {Element}
*/
list: null,
/**
* <template> element used for constructing list item elements.
* @type {Element}
*/
itemTemplate: null,
/**
* Map of ReadingList Item objects, keyed by their ID.
* @type {Map}
*/
itemsById: new Map(),
/**
* Map of list item elements, keyed by their corresponding Item's ID.
* @type {Map}
*/
itemNodesById: new Map(),
/**
* Initialize the sidebar UI.
*/
init() {
log.debug("Initializing");
addEventListener("unload", () => this.uninit());
this.list = document.getElementById("list");
this.itemTemplate = document.getElementById("item-template");
this.list.addEventListener("click", event => this.onListClick(event));
this.list.addEventListener("mousemove", event => this.onListMouseMove(event));
this.list.addEventListener("keydown", event => this.onListKeyDown(event), true);
this.ensureListItems();
ReadingList.addListener(this);
let initEvent = new CustomEvent("Initialized", {bubbles: true});
document.documentElement.dispatchEvent(initEvent);
},
/**
* Un-initialize the sidebar UI.
*/
uninit() {
log.debug("Shutting down");
ReadingList.removeListener(this);
},
/**
* Handle an item being added to the ReadingList.
* TODO: We may not want to show this new item right now.
* TODO: We should guard against the list growing here.
*
* @param {Readinglist.Item} item - Item that was added.
*/
onItemAdded(item) {
log.trace(`onItemAdded: ${item}`);
let itemNode = document.importNode(this.itemTemplate.content, true).firstElementChild;
this.updateItem(item, itemNode);
this.list.appendChild(itemNode);
this.itemNodesById.set(item.id, itemNode);
this.itemsById.set(item.id, item);
},
/**
* Handle an item being deleted from the ReadingList.
* @param {ReadingList.Item} item - Item that was deleted.
*/
onItemDeleted(item) {
log.trace(`onItemDeleted: ${item}`);
let itemNode = this.itemNodesById.get(item.id);
itemNode.remove();
this.itemNodesById.delete(item.id);
this.itemsById.delete(item.id);
// TODO: ensureListItems doesn't yet cope with needing to add one item.
//this.ensureListItems();
},
/**
* Handle an item in the ReadingList having any of its properties changed.
* @param {ReadingList.Item} item - Item that was updated.
*/
onItemUpdated(item) {
log.trace(`onItemUpdated: ${item}`);
let itemNode = this.itemNodesById.get(item.id);
if (!itemNode)
return;
this.updateItem(item, itemNode);
},
/**
* Update the element representing an item, ensuring it's in sync with the
* underlying data.
* @param {ReadingList.Item} item - Item to use as a source.
* @param {Element} itemNode - Element to update.
*/
updateItem(item, itemNode) {
itemNode.setAttribute("id", "item-" + item.id);
itemNode.setAttribute("title", `${item.title}\n${item.url.spec}`);
itemNode.querySelector(".item-title").textContent = item.title;
itemNode.querySelector(".item-domain").textContent = item.domain;
},
/**
* Ensure that the list is populated with the correct items.
*/
ensureListItems() {
ReadingList.getItems().then(items => {
for (let item of items) {
// TODO: Should be batch inserting via DocumentFragment
try {
this.onItemAdded(item);
} catch (e) {
log.warn("Error adding item", e);
}
}
});
},
/**
* Get the number of items currently displayed in the list.
* @type {number}
*/
get numItems() {
return this.list.childElementCount;
},
/**
* The currently active element in the list.
* @type {Element}
*/
get activeItem() {
return document.querySelector("#list > .item.active");
},
set activeItem(node) {
if (node && node.parentNode != this.list) {
log.error(`Unable to set activeItem to invalid node ${node}`);
return;
}
log.debug(`Setting activeItem: ${node ? node.id : null}`);
if (node) {
if (!node.classList.contains("selected")) {
this.selectedItem = node;
}
if (node.classList.contains("active")) {
return;
}
}
let prevItem = document.querySelector("#list > .item.active");
if (prevItem) {
prevItem.classList.remove("active");
}
if (node) {
node.classList.add("active");
}
let event = new CustomEvent("ActiveItemChanged", {bubbles: true});
this.list.dispatchEvent(event);
},
/**
* The currently selected item in the list.
* @type {Element}
*/
get selectedItem() {
return document.querySelector("#list > .item.selected");
},
set selectedItem(node) {
if (node && node.parentNode != this.list) {
log.error(`Unable to set selectedItem to invalid node ${node}`);
return;
}
log.debug(`Setting activeItem: ${node ? node.id : null}`);
let prevItem = document.querySelector("#list > .item.selected");
if (prevItem) {
prevItem.classList.remove("selected");
}
if (node) {
node.classList.add("selected");
let itemId = this.getItemIdFromNode(node);
this.list.setAttribute("aria-activedescendant", "item-" + itemId);
} else {
this.list.removeAttribute("aria-activedescendant");
}
let event = new CustomEvent("SelectedItemChanged", {bubbles: true});
this.list.dispatchEvent(event);
},
/**
* The index of the currently selected item in the list.
* @type {number}
*/
get selectedIndex() {
for (let i = 0; i < this.numItems; i++) {
let item = this.list.children.item(i);
if (!item) {
break;
}
if (item.classList.contains("selected")) {
return i;
}
}
return -1;
},
set selectedIndex(index) {
log.debug(`Setting selectedIndex: ${index}`);
if (index == -1) {
this.selectedItem = null;
return;
}
let item = this.list.children.item(index);
if (!item) {
log.warn(`Unable to set selectedIndex to invalid index ${index}`);
return;
}
this.selectedItem = item;
},
/**
* Open a given URL. The event is used to determine where it should be opened
* (current tab, new tab, new window).
* @param {string} url - URL to open.
* @param {Event} event - KeyEvent or MouseEvent that triggered this action.
*/
openURL(url, event) {
// TODO: Disabled while working on the listbox mechanics.
log.debug(`Opening page ${url}`);
return;
let mainWindow = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShellTreeItem)
.rootTreeItem
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
mainWindow.openUILink(url, event);
},
/**
* Get the ID of the Item associated with a given list item element.
* @param {element} node - List item element to get an ID for.
* @return {string} Assocated Item ID.
*/
getItemIdFromNode(node) {
let id = node.getAttribute("id");
if (id && id.startsWith("item-")) {
return id.slice(5);
}
return null;
},
/**
* Get the Item associated with a given list item element.
* @param {element} node - List item element to get an Item for.
* @return {string} Associated Item.
*/
getItemFromNode(node) {
let itemId = this.getItemIdFromNode(node);
if (!itemId) {
return null;
}
return this.itemsById.get(itemId);
},
/**
* Open the active item in the list.
* @param {Event} event - Event triggering this.
*/
openActiveItem(event) {
let itemNode = this.activeItem;
if (!itemNode) {
return;
}
let item = this.getItemFromNode(itemNode);
this.openURL(item.url.spec, event);
},
/**
* Find the parent item element, from a given child element.
* @param {Element} node - Child element.
* @return {Element} Element for the item, or null if not found.
*/
findParentItemNode(node) {
while (node && node != this.list && node != document.documentElement &&
!node.classList.contains("item")) {
node = node.parentNode;
}
if (node != this.list && node != document.documentElement) {
return node;
}
return null;
},
/**
* Handle a click event on the list box.
* @param {Event} event - Triggering event.
*/
onListClick(event) {
let itemNode = this.findParentItemNode(event.target);
if (!itemNode)
return;
this.activeItem = itemNode;
this.openActiveItem(event);
},
/**
* Handle a mousemove event over the list box.
* @param {Event} event - Triggering event.
*/
onListMouseMove(event) {
let itemNode = this.findParentItemNode(event.target);
if (!itemNode)
return;
this.selectedItem = itemNode;
},
/**
* Handle a keydown event on the list box.
* @param {Event} event - Triggering event.
*/
onListKeyDown(event) {
if (event.keyCode == KeyEvent.DOM_VK_DOWN) {
// TODO: Refactor this so we pass a direction to a generic method.
// See autocomplete.xml's getNextIndex
event.preventDefault();
let index = this.selectedIndex + 1;
if (index >= this.numItems) {
index = 0;
}
this.selectedIndex = index;
this.selectedItem.focus();
} else if (event.keyCode == KeyEvent.DOM_VK_UP) {
event.preventDefault();
let index = this.selectedIndex - 1;
if (index < 0) {
index = this.numItems - 1;
}
this.selectedIndex = index;
this.selectedItem.focus();
} else if (event.keyCode == KeyEvent.DOM_VK_RETURN) {
let selectedItem = this.selectedItem;
if (selectedItem) {
this.activeItem = this.selectedItem;
this.openActiveItem(event);
}
}
},
};
addEventListener("DOMContentLoaded", () => RLSidebar.init());

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- 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/. -->
<!DOCTYPE html [
<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
%browserDTD;
]>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<script src="chrome://browser/content/readinglist/sidebar.js" type="application/javascript;version=1.8"></script>
<link rel="stylesheet" type="text/css" media="all" href="chrome://browser/skin/readinglist/sidebar.css"/>
<!-- <title>&readingList.label;</title> -->
</head>
<body role="application">
<template id="item-template">
<div class="item" role="option" tabindex="-1">
<div class="item-thumb-container"></div>
<div class="item-summary-container">
<div class="item-title"></div>
<div class="item-domain"></div>
</div>
</div>
</template>
<div id="list" role="listbox" tabindex="1"></div>
</body>
</html>

View File

@ -0,0 +1,159 @@
"use strict";
this.EXPORTED_SYMBOLS = [
"ReadingListTestUtils",
];
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource:///modules/readinglist/ReadingList.jsm");
/** Preference name controlling whether the ReadingList feature is enabled/disabled. */
const PREF_RL_ENABLED = "browser.readinglist.enabled";
/**
* Utilities for testing the ReadingList sidebar.
*/
function SidebarUtils(window, assert) {
this.window = window;
this.Assert = assert;
}
SidebarUtils.prototype = {
/**
* Reference to the RLSidebar object controlling the ReadingList sidebar UI.
* @type {object}
*/
get RLSidebar() {
return this.window.SidebarUI.browser.contentWindow.RLSidebar;
},
/**
* Reference to the list container element in the sidebar.
* @type {Element}
*/
get list() {
return this.RLSidebar.list;
},
/**
* Check that the number of elements in the list matches the expected count.
* @param {number} count - Expected number of items.
*/
expectNumItems(count) {
this.Assert.equal(this.list.childElementCount, count,
"Should have expected number of items in the sidebar list");
},
/**
* Check all items in the sidebar list, ensuring the DOM matches the data.
*/
checkAllItems() {
for (let itemNode of this.list.children) {
this.checkSidebarItem(itemNode);
}
},
/**
* Run a series of sanity checks for an element in the list associated with
* an Item, ensuring the DOM matches the data.
*/
checkItem(node) {
let item = this.RLSidebar.getItemFromNode(node);
this.Assert.ok(node.classList.contains("item"),
"Node should have .item class");
this.Assert.equal(node.id, "item-" + item.id,
"Node should have correct ID");
this.Assert.equal(node.getAttribute("title"), item.title + "\n" + item.url.spec,
"Node should have correct title attribute");
this.Assert.equal(node.querySelector(".item-title").textContent, item.title,
"Node's title element's text should match item title");
this.Assert.equal(node.querySelector(".item-domain").textContent, item.domain,
"Node's domain element's text should match item title");
},
expectSelectedId(itemId) {
let selectedItem = this.RLSidebar.selectedItem;
if (itemId == null) {
this.Assert.equal(selectedItem, null, "Should have no selected item");
} else {
this.Assert.notEqual(selectedItem, null, "selectedItem should not be null");
let selectedId = this.RLSidebar.getItemIdFromNode(selectedItem);
this.Assert.equal(itemId, selectedId, "Should have currect item selected");
}
},
expectActiveId(itemId) {
let activeItem = this.RLSidebar.activeItem;
if (itemId == null) {
this.Assert.equal(activeItem, null, "Should have no active item");
} else {
this.Assert.notEqual(activeItem, null, "activeItem should not be null");
let activeId = this.RLSidebar.getItemIdFromNode(activeItem);
this.Assert.equal(itemId, activeId, "Should have correct item active");
}
},
};
/**
* Utilities for testing the ReadingList.
*/
this.ReadingListTestUtils = {
/**
* Whether the ReadingList feature is enabled or not.
* @type {boolean}
*/
get enabled() {
return Preferences.get(PREF_RL_ENABLED, false);
},
set enabled(value) {
Preferences.set(PREF_RL_ENABLED, !!value);
},
/**
* Utilities for testing the ReadingList sidebar.
*/
SidebarUtils: SidebarUtils,
/**
* Synthetically add an item to the ReadingList.
* @param {object|[object]} data - Object or array of objects to pass to the
* Item constructor.
* @return {Promise} Promise that gets fulfilled with the item or items added.
*/
addItem(data) {
if (Array.isArray(data)) {
let promises = [];
for (let itemData of data) {
promises.push(this.addItem(itemData));
}
return Promise.all(promises);
}
return new Promise(resolve => {
let item = new ReadingList.Item(data);
ReadingList._items.push(item);
ReadingList._notifyListeners("onItemAdded", item);
resolve(item);
});
},
/**
* Cleanup all data, resetting to a blank state.
*/
cleanup() {
return new Promise(resolve => {
ReadingList._items = [];
ReadingList._listeners.clear();
Preferences.reset(PREF_RL_ENABLED);
resolve();
});
},
};

View File

@ -0,0 +1,7 @@
[DEFAULT]
support-files =
head.js
[browser_ui_enable_disable.js]
[browser_sidebar_list.js]
;[browser_sidebar_mouse_nav.js]

View File

@ -0,0 +1,53 @@
/**
* This tests the basic functionality of the sidebar to list items.
*/
add_task(function*() {
registerCleanupFunction(function*() {
ReadingListUI.hideSidebar();
yield RLUtils.cleanup();
});
RLUtils.enabled = true;
yield ReadingListUI.showSidebar();
let RLSidebar = RLSidebarUtils.RLSidebar;
let sidebarDoc = SidebarUI.browser.contentDocument;
Assert.equal(RLSidebar.numItems, 0, "Should start with no items");
Assert.equal(RLSidebar.activeItem, null, "Should start with no active item");
Assert.equal(RLSidebar.activeItem, null, "Should start with no selected item");
info("Adding first item");
yield RLUtils.addItem({
id: "c3502a49-bcef-4a94-b222-d4834463de33",
url: "http://example.com/article1",
title: "Article 1",
});
RLSidebarUtils.expectNumItems(1);
info("Adding more items");
yield RLUtils.addItem([{
id: "e054f5b7-1f4f-463f-bb96-d64c02448c31",
url: "http://example.com/article2",
title: "Article 2",
}, {
id: "4207230b-2364-4e97-9587-01312b0ce4e6",
url: "http://example.com/article3",
title: "Article 3",
}]);
RLSidebarUtils.expectNumItems(3);
info("Closing sidebar");
ReadingListUI.hideSidebar();
info("Adding another item");
yield RLUtils.addItem({
id: "dae0e855-607e-4df3-b27f-73a5e35c94fe",
url: "http://example.com/article4",
title: "Article 4",
});
info("Re-eopning sidebar");
yield ReadingListUI.showSidebar();
RLSidebarUtils.expectNumItems(4);
});

View File

@ -0,0 +1,84 @@
/**
* Test mouse navigation for selecting items in the sidebar.
*/
function mouseInteraction(mouseEvent, responseEvent, itemNode) {
let eventPromise = BrowserUITestUtils.waitForEvent(RLSidebarUtils.list, responseEvent);
let details = {};
if (mouseEvent != "click") {
details.type = mouseEvent;
}
EventUtils.synthesizeMouseAtCenter(itemNode, details, itemNode.ownerDocument.defaultView);
return eventPromise;
}
add_task(function*() {
registerCleanupFunction(function*() {
ReadingListUI.hideSidebar();
yield RLUtils.cleanup();
});
RLUtils.enabled = true;
let itemData = [{
id: "00bd24c7-3629-40b0-acde-37aa81768735",
url: "http://example.com/article1",
title: "Article 1",
}, {
id: "28bf7f19-cf94-4ceb-876a-ac1878342e0d",
url: "http://example.com/article2",
title: "Article 2",
}, {
id: "7e5064ea-f45d-4fc7-8d8c-c067b7781e78",
url: "http://example.com/article3",
title: "Article 3",
}, {
id: "8e72a472-8db8-4904-ba39-9672f029e2d0",
url: "http://example.com/article4",
title: "Article 4",
}, {
id: "8d332744-37bc-4a1a-a26b-e9953b9f7d91",
url: "http://example.com/article5",
title: "Article 5",
}];
info("Adding initial mock data");
yield RLUtils.addItem(itemData);
info("Opening sidebar");
yield ReadingListUI.showSidebar();
RLSidebarUtils.expectNumItems(5);
RLSidebarUtils.expectSelectedId(null);
RLSidebarUtils.expectActiveId(null);
info("Mouse move over item 1");
yield mouseInteraction("mousemove", "SelectedItemChanged", RLSidebarUtils.list.children[0]);
RLSidebarUtils.expectSelectedId(itemData[0].id);
RLSidebarUtils.expectActiveId(null);
info("Mouse move over item 2");
yield mouseInteraction("mousemove", "SelectedItemChanged", RLSidebarUtils.list.children[1]);
RLSidebarUtils.expectSelectedId(itemData[1].id);
RLSidebarUtils.expectActiveId(null);
info("Mouse move over item 5");
yield mouseInteraction("mousemove", "SelectedItemChanged", RLSidebarUtils.list.children[4]);
RLSidebarUtils.expectSelectedId(itemData[4].id);
RLSidebarUtils.expectActiveId(null);
info("Mouse move over item 1 again");
yield mouseInteraction("mousemove", "SelectedItemChanged", RLSidebarUtils.list.children[0]);
RLSidebarUtils.expectSelectedId(itemData[0].id);
RLSidebarUtils.expectActiveId(null);
info("Mouse click on item 1");
yield mouseInteraction("click", "ActiveItemChanged", RLSidebarUtils.list.children[0]);
RLSidebarUtils.expectSelectedId(itemData[0].id);
RLSidebarUtils.expectActiveId(itemData[0].id);
info("Mouse click on item 3");
yield mouseInteraction("click", "ActiveItemChanged", RLSidebarUtils.list.children[2]);
RLSidebarUtils.expectSelectedId(itemData[2].id);
RLSidebarUtils.expectActiveId(itemData[2].id);
});

View File

@ -0,0 +1,49 @@
/**
* Test enabling/disabling the entire ReadingList feature via the
* browser.readinglist.enabled preference.
*/
function checkRLState() {
let enabled = RLUtils.enabled;
info("Checking ReadingList UI is " + (enabled ? "enabled" : "disabled"));
let sidebarBroadcaster = document.getElementById("readingListSidebar");
let sidebarMenuitem = document.getElementById("menu_readingListSidebar");
if (enabled) {
Assert.notEqual(sidebarBroadcaster.getAttribute("hidden"), "true",
"Sidebar broadcaster should not be hidden");
Assert.notEqual(sidebarMenuitem.getAttribute("hidden"), "true",
"Sidebar menuitem should be visible");
} else {
Assert.equal(sidebarBroadcaster.getAttribute("hidden"), "true",
"Sidebar broadcaster should be hidden");
Assert.equal(sidebarMenuitem.getAttribute("hidden"), "true",
"Sidebar menuitem should be hidden");
Assert.equal(ReadingListUI.isSidebarOpen, false,
"ReadingListUI should not think sidebar is open");
}
if (!enabled) {
Assert.equal(SidebarUI.isOpen, false, "Sidebar should not be open");
}
}
add_task(function*() {
info("Start with ReadingList disabled");
RLUtils.enabled = false;
checkRLState();
info("Enabling ReadingList");
RLUtils.enabled = true;
checkRLState();
info("Opening ReadingList sidebar");
yield ReadingListUI.showSidebar();
Assert.ok(SidebarUI.isOpen, "Sidebar should be open");
Assert.equal(SidebarUI.currentID, "readingListSidebar", "Sidebar should have ReadingList loaded");
info("Disabling ReadingList");
RLUtils.enabled = false;
Assert.ok(!SidebarUI.isOpen, "Sidebar should be closed");
checkRLState();
});

View File

@ -0,0 +1,15 @@
XPCOMUtils.defineLazyModuleGetter(this, "ReadingList",
"resource:///modules/readinglist/ReadingList.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ReadingListTestUtils",
"resource://testing-common/ReadingListTestUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITestUtils",
"resource://testing-common/BrowserUITestUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "RLUtils", () => {
return ReadingListTestUtils;
});
XPCOMUtils.defineLazyGetter(this, "RLSidebarUtils", () => {
return new RLUtils.SidebarUtils(window, Assert);
});

View File

@ -2865,7 +2865,7 @@ let SessionStoreInternal = {
}
var sidebar = aWindow.document.getElementById("sidebar-box");
if (sidebar.getAttribute("sidebarcommand") != aSidebar) {
aWindow.toggleSidebar(aSidebar);
aWindow.SidebarUI.show(aSidebar);
}
// since resizing/moving a window brings it to the foreground,
// we might want to re-focus the last focused window

View File

@ -10,6 +10,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
Cu.import("resource://gre/modules/devtools/Console.jsm");
const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
@ -123,26 +124,26 @@ let EventsHandler = {
_onTabNavigated: function(event, {isFrameSwitching}) {
switch (event) {
case "will-navigate": {
Task.spawn(function*() {
// Make sure the backend is prepared to handle WebGL contexts.
if (!isFrameSwitching) {
gFront.setup({ reload: false });
}
// Make sure the backend is prepared to handle WebGL contexts.
if (!isFrameSwitching) {
gFront.setup({ reload: false });
}
// Reset UI.
ShadersListView.empty();
// When switching to an iframe, ensure displaying the reload button.
// As the document has already been loaded without being hooked.
if (isFrameSwitching) {
$("#reload-notice").hidden = false;
$("#waiting-notice").hidden = true;
} else {
$("#reload-notice").hidden = true;
$("#waiting-notice").hidden = false;
}
$("#content").hidden = true;
window.emit(EVENTS.UI_RESET);
// Reset UI.
ShadersListView.empty();
// When switching to an iframe, ensure displaying the reload button.
// As the document has already been loaded without being hooked.
if (isFrameSwitching) {
$("#reload-notice").hidden = false;
$("#waiting-notice").hidden = true;
} else {
$("#reload-notice").hidden = true;
$("#waiting-notice").hidden = false;
}
yield ShadersEditorsView.setText({ vs: "", fs: "" });
$("#content").hidden = true;
}).then(() => window.emit(EVENTS.UI_RESET));
break;
}
case "navigate": {
@ -370,7 +371,7 @@ let ShadersEditorsView = {
*/
destroy: Task.async(function*() {
this._destroyed = true;
this._toggleListeners("off");
yield this._toggleListeners("off");
for (let p of this._editorPromises.values()) {
let editor = yield p;
editor.destroy();
@ -414,9 +415,6 @@ let ShadersEditorsView = {
* Returns a promise that resolves to an editor instance
*/
_getEditor: function(type) {
if ($("#content").hidden) {
return promise.reject(new Error("Shader Editor is still waiting for a WebGL context to be created."));
}
if (this._editorPromises.has(type)) {
return this._editorPromises.get(type);
}

View File

@ -1,13 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
///////////////////
//
// Whitelisting this test.
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
//
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is still waiting for a WebGL context to be created.");
/**
* Tests if the shader editor leaks on initialization and sudden destruction.
* You can also use this initialization format as a template for other tests.

View File

@ -1,13 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
///////////////////
//
// Whitelisting this test.
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
//
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is still waiting for a WebGL context to be created.");
/**
* Tests if the editors contain the correct text when a program
* becomes available.

View File

@ -1,13 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
///////////////////
//
// Whitelisting this test.
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
//
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is still waiting for a WebGL context to be created.");
/**
* Tests if error indicators are shown in the editor's gutter and text area
* when there's a shader compilation error.

View File

@ -1,13 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
///////////////////
//
// Whitelisting this test.
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
//
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is still waiting for a WebGL context to be created.");
/**
* Tests if error tooltips can be opened from the editor's gutter when there's
* a shader compilation error.

View File

@ -1,13 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
///////////////////
//
// Whitelisting this test.
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
//
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is still waiting for a WebGL context to be created.");
/**
* Tests if source editors are lazily initialized.
*/
@ -16,20 +9,6 @@ function ifWebGLSupported() {
let { target, panel } = yield initShaderEditor(SIMPLE_CANVAS_URL);
let { gFront, ShadersEditorsView } = panel.panelWin;
try {
yield ShadersEditorsView._getEditor("vs");
ok(false, "The promise for a vertex shader editor should be rejected.");
} catch (e) {
ok(true, "The vertex shader editors wasn't initialized.");
}
try {
yield ShadersEditorsView._getEditor("fs");
ok(false, "The promise for a fragment shader editor should be rejected.");
} catch (e) {
ok(true, "The fragment shader editors wasn't initialized.");
}
reload(target);
yield once(gFront, "program-linked");

View File

@ -1,13 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
///////////////////
//
// Whitelisting this test.
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
//
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is still waiting for a WebGL context to be created.");
/**
* Tests if the shader editor shows the appropriate UI when opened.
*/

View File

@ -1,13 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
///////////////////
//
// Whitelisting this test.
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
//
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is still waiting for a WebGL context to be created.");
/**
* Tests target navigations are handled correctly in the UI.
*/
@ -48,8 +41,7 @@ function ifWebGLSupported() {
let navigated = once(target, "will-navigate");
navigate(target, "about:blank");
yield navigating;
yield once(panel.panelWin, EVENTS.UI_RESET);
yield promise.all([navigating, once(panel.panelWin, EVENTS.UI_RESET) ]);
is($("#reload-notice").hidden, true,
"The 'reload this page' notice should be hidden while navigating.");
@ -65,18 +57,6 @@ function ifWebGLSupported() {
is(ShadersListView.selectedIndex, -1,
"The shaders list has a negative index.");
yield ShadersEditorsView._getEditor("vs").then(() => {
ok(false, "The promise for a vertex shader editor should be rejected.");
}, () => {
ok(true, "The vertex shader editors wasn't initialized.");
});
yield ShadersEditorsView._getEditor("fs").then(() => {
ok(false, "The promise for a fragment shader editor should be rejected.");
}, () => {
ok(true, "The fragment shader editors wasn't initialized.");
});
yield navigated;
is($("#reload-notice").hidden, true,

View File

@ -1,13 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
///////////////////
//
// Whitelisting this test.
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
//
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is still waiting for a WebGL context to be created.");
/**
* Tests if blackboxing a program works properly.
*/

View File

@ -1,13 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
///////////////////
//
// Whitelisting this test.
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
//
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is still waiting for a WebGL context to be created.");
/**
* Tests if blackboxing a program works properly in tandem with blended
* overlapping geometry.

View File

@ -1,13 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
///////////////////
//
// Whitelisting this test.
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
//
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is still waiting for a WebGL context to be created.");
/**
* Tests if the programs list contains an entry after vertex and fragment
* shaders are linked.

View File

@ -1,13 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
///////////////////
//
// Whitelisting this test.
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
//
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is still waiting for a WebGL context to be created.");
/**
* Tests if editing a vertex and a fragment shader works properly.
*/

View File

@ -1,13 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
///////////////////
//
// Whitelisting this test.
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
//
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is still waiting for a WebGL context to be created.");
/**
* Tests if compile or linkage errors are emitted when a shader source
* gets malformed after being edited.

View File

@ -1,13 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
///////////////////
//
// Whitelisting this test.
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
//
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is still waiting for a WebGL context to be created.");
/**
* Tests if editing a vertex and a fragment shader would permanently store
* their new source on the backend and reshow it in the frontend when required.

View File

@ -838,6 +838,8 @@ just addresses the organization to follow, e.g. "This site is run by " -->
<!ENTITY processHang.terminateProcess.label "Kill Web Process">
<!ENTITY processHang.terminateProcess.accessKey "K">
<!ENTITY readingList.label "Reading List">
<!ENTITY readingList.sidebar.commandKey "R">
<!ENTITY emeLearnMoreContextMenu.label "Learn more about DRM…">
<!ENTITY emeLearnMoreContextMenu.accesskey "D">

View File

@ -28,6 +28,8 @@ let ReaderParent = {
"Reader:Share",
"Reader:SystemUIVisibility",
"Reader:UpdateReaderButton",
"Reader:SetIntPref",
"Reader:SetCharPref",
],
init: function() {
@ -80,6 +82,18 @@ let ReaderParent = {
this.updateReaderButton(browser);
break;
}
case "Reader:SetIntPref": {
if (message.data && message.data.name !== undefined) {
Services.prefs.setIntPref(message.data.name, message.data.value);
}
break;
}
case "Reader:SetCharPref": {
if (message.data && message.data.name !== undefined) {
Services.prefs.setCharPref(message.data.name, message.data.value);
}
break;
}
}
},

View File

@ -91,6 +91,7 @@ browser.jar:
skin/classic/browser/tab-crashed.svg (../shared/incontent-icons/tab-crashed.svg)
skin/classic/browser/welcome-back.svg (../shared/incontent-icons/welcome-back.svg)
skin/classic/browser/reader-mode-16.png (../shared/reader/reader-mode-16.png)
skin/classic/browser/readinglist/sidebar.css (../shared/readinglist/sidebar.css)
skin/classic/browser/webRTC-shareDevice-16.png
skin/classic/browser/webRTC-shareDevice-64.png
skin/classic/browser/webRTC-sharingDevice-16.png (../shared/webrtc/webRTC-sharingDevice-16.png)

View File

@ -142,6 +142,7 @@ browser.jar:
skin/classic/browser/welcome-back.svg (../shared/incontent-icons/welcome-back.svg)
skin/classic/browser/reader-mode-16.png (../shared/reader/reader-mode-16.png)
skin/classic/browser/reader-mode-16@2x.png (../shared/reader/reader-mode-16@2x.png)
skin/classic/browser/readinglist/sidebar.css (../shared/readinglist/sidebar.css)
skin/classic/browser/webRTC-shareDevice-16.png
skin/classic/browser/webRTC-shareDevice-16@2x.png
skin/classic/browser/webRTC-shareDevice-64.png

View File

@ -0,0 +1,72 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* 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/. */
:root, body {
height: 100%;
overflow-x: hidden;
}
body {
margin: 0;
font: message-box;
background: #F8F7F8;
color: #333333;
-moz-user-select: none;
overflow: hidden;
}
#list {
height: 100%;
overflow-x: auto;
}
.item {
display: flex;
flex-flow: row;
cursor: pointer;
padding: 6px;
}
.item.active {
background: #FEFEFE;
}
.item.selected {
background: #FDFDFD;
}
.item-thumb-container {
min-width: 64px;
max-width: 64px;
min-height: 40px;
max-height: 40px;
background: #EBEBEB;
border: 1px solid white;
box-shadow: 0px 1px 2px rgba(0,0,0,.35);
margin: 5px;
}
.item-summary-container {
display: flex;
flex-flow: column;
-moz-padding-start: 4px;
overflow: hidden;
}
.item-title {
overflow: hidden;
height: 2.8em;
}
.item-domain {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-height: 1.4em;
color: #0095DD;
}
.item:hover .item-domain {
color: #008ACB;
}

View File

@ -110,6 +110,7 @@ browser.jar:
skin/classic/browser/tab-crashed.svg (../shared/incontent-icons/tab-crashed.svg)
skin/classic/browser/welcome-back.svg (../shared/incontent-icons/welcome-back.svg)
skin/classic/browser/reader-mode-16.png (../shared/reader/reader-mode-16.png)
skin/classic/browser/readinglist/sidebar.css (../shared/readinglist/sidebar.css)
skin/classic/browser/notification-pluginNormal.png (../shared/plugins/notification-pluginNormal.png)
skin/classic/browser/notification-pluginAlert.png (../shared/plugins/notification-pluginAlert.png)
skin/classic/browser/notification-pluginBlocked.png (../shared/plugins/notification-pluginBlocked.png)
@ -575,6 +576,7 @@ browser.jar:
skin/classic/aero/browser/tab-crashed.svg (../shared/incontent-icons/tab-crashed.svg)
skin/classic/aero/browser/welcome-back.svg (../shared/incontent-icons/welcome-back.svg)
skin/classic/aero/browser/reader-mode-16.png (../shared/reader/reader-mode-16.png)
skin/classic/aero/browser/readinglist/sidebar.css (../shared/readinglist/sidebar.css)
skin/classic/aero/browser/notification-pluginNormal.png (../shared/plugins/notification-pluginNormal.png)
skin/classic/aero/browser/notification-pluginAlert.png (../shared/plugins/notification-pluginAlert.png)
skin/classic/aero/browser/notification-pluginBlocked.png (../shared/plugins/notification-pluginBlocked.png)

View File

@ -10,6 +10,7 @@
#include "mozilla/Preferences.h"
#include "nsLayoutUtils.h"
#include "nsGkAtoms.h"
#include "nsFontMetrics.h"
#include "nsPrintfCString.h"
#include "mozilla/dom/Element.h"
#include "nsRegion.h"
@ -76,6 +77,7 @@ struct EventRadiusPrefs
bool mTouchOnly;
bool mRepositionEventCoords;
bool mTouchClusterDetection;
uint32_t mLimitReadableSize;
};
static EventRadiusPrefs sMouseEventRadiusPrefs;
@ -125,6 +127,9 @@ GetPrefsFor(EventClassID aEventClassID)
nsPrintfCString touchClusterPref("ui.zoomedview.enabled", prefBranch);
Preferences::AddBoolVarCache(&prefs->mTouchClusterDetection, touchClusterPref.get(), false);
nsPrintfCString limitReadableSizePref("ui.zoomedview.limitReadableSize", prefBranch);
Preferences::AddUintVarCache(&prefs->mLimitReadableSize, limitReadableSizePref.get(), 8);
}
return prefs;
@ -382,6 +387,46 @@ GetClosest(nsIFrame* aRoot, const nsPoint& aPointRelativeToRootFrame,
return bestTarget;
}
/*
* Return always true when touch cluster detection is OFF.
* When cluster detection is ON, return true if the text inside
* the frame is readable (by human eyes):
* in this case, the frame is really clickable.
* Frames with a too small size will return false:
* in this case, the frame is considered not clickable.
*/
static bool
IsElementClickableAndReadable(nsIFrame* aFrame, WidgetGUIEvent* aEvent, const EventRadiusPrefs* aPrefs)
{
if (!aPrefs->mTouchClusterDetection) {
return true;
}
if (aEvent->mClass != eMouseEventClass) {
return true;
}
uint32_t limitReadableSize = aPrefs->mLimitReadableSize;
nsSize frameSize = aFrame->GetSize();
nsPresContext* pc = aFrame->PresContext();
nsIPresShell* presShell = pc->PresShell();
gfxSize cumulativeResolution = presShell->GetCumulativeResolution();
if ((pc->AppUnitsToGfxUnits(frameSize.height) * cumulativeResolution.height) < limitReadableSize ||
(pc->AppUnitsToGfxUnits(frameSize.width) * cumulativeResolution.width) < limitReadableSize) {
return false;
}
nsRefPtr<nsFontMetrics> fm;
nsLayoutUtils::GetFontMetricsForFrame(aFrame, getter_AddRefs(fm),
nsLayoutUtils::FontSizeInflationFor(aFrame));
if (fm) {
if ((pc->AppUnitsToGfxUnits(fm->EmHeight()) * cumulativeResolution.height) < limitReadableSize) {
return false;
}
}
return true;
}
nsIFrame*
FindFrameTargetedByInputEvent(WidgetGUIEvent* aEvent,
nsIFrame* aRootFrame,
@ -398,8 +443,15 @@ FindFrameTargetedByInputEvent(WidgetGUIEvent* aEvent,
mozilla::layers::Stringify(aPointRelativeToRootFrame).c_str(), aRootFrame);
const EventRadiusPrefs* prefs = GetPrefsFor(aEvent->mClass);
if (!prefs || !prefs->mEnabled || (target && IsElementClickable(target, nsGkAtoms::body))) {
PET_LOG("Retargeting disabled or target %p is clickable\n", target);
if (!prefs || !prefs->mEnabled) {
PET_LOG("Retargeting disabled\n");
return target;
}
if (target && IsElementClickable(target, nsGkAtoms::body)) {
if (!IsElementClickableAndReadable(target, aEvent, prefs)) {
aEvent->AsMouseEventBase()->hitCluster = true;
}
PET_LOG("Target %p is clickable\n", target);
return target;
}
@ -437,7 +489,8 @@ FindFrameTargetedByInputEvent(WidgetGUIEvent* aEvent,
GetClosest(aRootFrame, aPointRelativeToRootFrame, targetRect, prefs,
restrictToDescendants, candidates, &elementsInCluster);
if (closestClickable) {
if (prefs->mTouchClusterDetection && elementsInCluster > 1) {
if ((prefs->mTouchClusterDetection && elementsInCluster > 1) ||
(!IsElementClickableAndReadable(closestClickable, aEvent, prefs))) {
if (aEvent->mClass == eMouseEventClass) {
WidgetMouseEventBase* mouseEventBase = aEvent->AsMouseEventBase();
mouseEventBase->hitCluster = true;

View File

@ -402,6 +402,7 @@ pref("font.size.inflation.minTwips", 120);
pref("browser.ui.zoom.force-user-scalable", false);
pref("ui.zoomedview.enabled", false);
pref("ui.zoomedview.limitReadableSize", 8); // value in layer pixels
pref("ui.touch.radius.enabled", false);
pref("ui.touch.radius.leftmm", 3);

View File

@ -40,12 +40,13 @@ public class ZoomedView extends FrameLayout implements LayerView.OnMetricsChange
LayerView.ZoomedViewListener, GeckoEventListener {
private static final String LOGTAG = "Gecko" + ZoomedView.class.getSimpleName();
private static final int ZOOM_FACTOR = 2;
private static final int DEFAULT_ZOOM_FACTOR = 3;
private static final int W_CAPTURED_VIEW_IN_PERCENT = 80;
private static final int H_CAPTURED_VIEW_IN_PERCENT = 50;
private static final int MINIMUM_DELAY_BETWEEN_TWO_RENDER_CALLS_NS = 1000000;
private static final int DELAY_BEFORE_NEXT_RENDER_REQUEST_MS = 2000;
private int zoomFactor;
private ImageView zoomedImageView;
private LayerView layerView;
private int viewWidth;
@ -86,6 +87,8 @@ public class ZoomedView extends FrameLayout implements LayerView.OnMetricsChange
if (dragged) {
dragged = false;
} else {
GeckoEvent eClickInZoomedView = GeckoEvent.createBroadcastEvent("Gesture:ClickInZoomedView", "");
GeckoAppShell.sendEventToGecko(eClickInZoomedView);
layerView.dispatchTouchEvent(actionDownEvent);
actionDownEvent.recycle();
PointF convertedPosition = getUnzoomedPositionFromPointInZoomedView(event.getX(), event.getY());
@ -184,7 +187,7 @@ public class ZoomedView extends FrameLayout implements LayerView.OnMetricsChange
final float parentHeight = metrics.getHeight();
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) getLayoutParams();
returnValue.x = (int) ((x / ZOOM_FACTOR) + // Conversion of the x offset inside the zoomed view (using the scale factor)
returnValue.x = (int) ((x / zoomFactor) + // Conversion of the x offset inside the zoomed view (using the scale factor)
offset.x + // The offset of the layerView
@ -192,17 +195,17 @@ public class ZoomedView extends FrameLayout implements LayerView.OnMetricsChange
* Minimum value for the left side of the zoomed view is 0
* and we return 0 after conversion
* Maximum value for the left side of the zoomed view is (parentWidth - offset.x - viewWidth)
* and we return (parentWidth - offset.x - (viewWidth / ZOOM_FACTOR)) after conversion.
* and we return (parentWidth - offset.x - (viewWidth / zoomFactor)) after conversion.
*/
(((float) params.leftMargin) - offset.x) *
((parentWidth - offset.x - (viewWidth / ZOOM_FACTOR)) /
((parentWidth - offset.x - (viewWidth / zoomFactor)) /
(parentWidth - offset.x - viewWidth)));
// Same comments here vertically
returnValue.y = (int) ((y / ZOOM_FACTOR) +
returnValue.y = (int) ((y / zoomFactor) +
offset.y +
(((float) params.topMargin) - offset.y) *
((parentHeight - offset.y - (viewHeight / ZOOM_FACTOR)) /
((parentHeight - offset.y - (viewHeight / zoomFactor)) /
(parentHeight - offset.y - viewHeight)));
return returnValue;
@ -219,7 +222,7 @@ public class ZoomedView extends FrameLayout implements LayerView.OnMetricsChange
final float parentWidth = metrics.getWidth();
final float parentHeight = metrics.getHeight();
returnValue.x = (int) ((((x - (viewWidth / (2 * ZOOM_FACTOR)))) / // Translation to get the left side position of the zoomed view
returnValue.x = (int) ((((x - (viewWidth / (2 * zoomFactor)))) / // Translation to get the left side position of the zoomed view
// centered on x (the value 2 to get the middle).
/* Conversion of the left side position of the zoomed view.
@ -228,14 +231,14 @@ public class ZoomedView extends FrameLayout implements LayerView.OnMetricsChange
* and not in a multiplication to convert the position from
* the LayerView to the ZoomedView.
*/
((parentWidth - offset.x - (viewWidth / ZOOM_FACTOR)) /
((parentWidth - offset.x - (viewWidth / zoomFactor)) /
(parentWidth - offset.x - viewWidth)))
+ offset.x); // The offset of the layerView
// Same comments here vertically
returnValue.y = (int) ((((y - (viewHeight / (2 * ZOOM_FACTOR)))) /
((parentHeight - offset.y - (viewHeight / ZOOM_FACTOR)) /
returnValue.y = (int) ((((y - (viewHeight / (2 * zoomFactor)))) /
((parentHeight - offset.y - (viewHeight / zoomFactor)) /
(parentHeight - offset.y - viewHeight)))
+ offset.y);
@ -296,8 +299,16 @@ public class ZoomedView extends FrameLayout implements LayerView.OnMetricsChange
private void setCapturedSize(ImmutableViewportMetrics metrics) {
float parentMinSize = Math.min(metrics.getWidth(), metrics.getHeight());
viewWidth = (int) (parentMinSize * W_CAPTURED_VIEW_IN_PERCENT / (ZOOM_FACTOR * 100.0)) * ZOOM_FACTOR;
viewHeight = (int) (parentMinSize * H_CAPTURED_VIEW_IN_PERCENT / (ZOOM_FACTOR * 100.0)) * ZOOM_FACTOR;
// For metrics.zoomFactor lower than 1, the zoom factor of the zoomed view is calculated
// to get always the same size for the content in the zoomed view.
// For metrics.zoomFactor greater than 1, the zoom factor is always set to the default
// value DEFAULT_ZOOM_FACTOR, thus the zoomed view is always a zoom of the normal view.
zoomFactor = Math.max(DEFAULT_ZOOM_FACTOR, (int) (DEFAULT_ZOOM_FACTOR / metrics.zoomFactor));
viewWidth = (int) (parentMinSize * W_CAPTURED_VIEW_IN_PERCENT / (zoomFactor * 100.0)) * zoomFactor;
viewHeight = (int) (parentMinSize * H_CAPTURED_VIEW_IN_PERCENT / (zoomFactor * 100.0)) * zoomFactor;
// Display in zoomedview is corrupted when width is an odd number
// More details about this issue here: bug 776906 comment 11
viewWidth &= ~0x1;
}
private void shouldBlockUpdate(boolean shouldBlockUpdate) {
@ -474,7 +485,7 @@ public class ZoomedView extends FrameLayout implements LayerView.OnMetricsChange
final int yPos = (int) (origin.y - offset.y) + lastPosition.y;
GeckoEvent e = GeckoEvent.createZoomedViewEvent(tabId, xPos, yPos, viewWidth,
viewHeight, ZOOM_FACTOR * metrics.zoomFactor, buffer);
viewHeight, zoomFactor * metrics.zoomFactor, buffer);
GeckoAppShell.sendEventToGecko(e);
}

View File

@ -129,6 +129,18 @@ let Reader = {
this.updatePageAction(tab);
break;
}
case "Reader:SetIntPref": {
if (message.data && message.data.name !== undefined) {
Services.prefs.setIntPref(message.data.name, message.data.value);
}
break;
}
case "Reader:SetCharPref": {
if (message.data && message.data.name !== undefined) {
Services.prefs.setCharPref(message.data.name, message.data.value);
}
break;
}
}
},

View File

@ -182,6 +182,8 @@ lazilyLoadedObserverScripts.forEach(function (aScript) {
"Reader:ToolbarHidden",
"Reader:SystemUIVisibility",
"Reader:UpdateReaderButton",
"Reader:SetIntPref",
"Reader:SetCharPref",
], "chrome://browser/content/Reader.js"],
].forEach(aScript => {
let [name, messages, script] = aScript;
@ -4908,8 +4910,10 @@ Tab.prototype = {
var BrowserEventHandler = {
init: function init() {
this._clickInZoomedView = false;
Services.obs.addObserver(this, "Gesture:SingleTap", false);
Services.obs.addObserver(this, "Gesture:CancelTouch", false);
Services.obs.addObserver(this, "Gesture:ClickInZoomedView", false);
Services.obs.addObserver(this, "Gesture:DoubleTap", false);
Services.obs.addObserver(this, "Gesture:Scroll", false);
Services.obs.addObserver(this, "dom-touch-listener-added", false);
@ -5103,6 +5107,10 @@ var BrowserEventHandler = {
this._cancelTapHighlight();
break;
case "Gesture:ClickInZoomedView":
this._clickInZoomedView = true;
break;
case "Gesture:SingleTap": {
try {
// If the element was previously focused, show the caret attached to it.
@ -5120,7 +5128,7 @@ var BrowserEventHandler = {
let data = JSON.parse(aData);
let {x, y} = data;
if (this._inCluster) {
if (this._inCluster && this._clickInZoomedView != true) {
this._clusterClicked(x, y);
} else {
// The _highlightElement was chosen after fluffing the touch events
@ -5130,6 +5138,7 @@ var BrowserEventHandler = {
this._sendMouseEvent("mousedown", x, y);
this._sendMouseEvent("mouseup", x, y);
}
this._clickInZoomedView = false;
// scrollToFocusedInput does its own checks to find out if an element should be zoomed into
BrowserApp.scrollToFocusedInput(BrowserApp.selectedBrowser);

View File

@ -301,7 +301,10 @@ AboutReader.prototype = {
this._fontSize = newFontSize;
htmlClasses.add("font-size" + this._fontSize);
Services.prefs.setIntPref("reader.font_size", this._fontSize);
this._mm.sendAsyncMessage("Reader:SetIntPref", {
name: "reader.font_size",
value: this._fontSize
});
},
_handleDeviceLight: function Reader_handleDeviceLight(newLux) {
@ -392,7 +395,10 @@ AboutReader.prototype = {
this._enableAmbientLighting(colorSchemePref === "auto");
this._setColorScheme(colorSchemePref);
Services.prefs.setCharPref("reader.color_scheme", colorSchemePref);
this._mm.sendAsyncMessage("Reader:SetCharPref", {
name: "reader.color_scheme",
value: colorSchemePref
});
},
_setFontType: function Reader_setFontType(newFontType) {
@ -407,7 +413,10 @@ AboutReader.prototype = {
this._fontType = newFontType;
bodyClasses.add(this._fontType);
Services.prefs.setCharPref("reader.font_type", this._fontType);
this._mm.sendAsyncMessage("Reader:SetCharPref", {
name: "reader.font_type",
value: this._fontType
});
},
_getToolbarVisibility: function Reader_getToolbarVisibility() {

View File

@ -16,6 +16,7 @@ DIRS += [
'security',
'server',
'sourcemap',
'shared',
'styleinspector',
'tern',
'transport',

View File

@ -0,0 +1,193 @@
/**
*
* Adapted from https://github.com/mozilla-b2g/gaia/blob/f09993563fb5fec4393eb71816ce76cb00463190/shared/js/async_storage.js
* (converted to use Promises instead of callbacks).
*
* This file defines an asynchronous version of the localStorage API, backed by
* an IndexedDB database. It creates a global asyncStorage object that has
* methods like the localStorage object.
*
* To store a value use setItem:
*
* asyncStorage.setItem("key", "value");
*
* This returns a promise in case you want confirmation that the value has been stored.
*
* asyncStorage.setItem("key", "newvalue").then(function() {
* console.log("new value stored");
* });
*
* To read a value, call getItem(), but note that you must wait for a promise
* resolution for the value to be retrieved.
*
* asyncStorage.getItem("key").then(function(value) {
* console.log("The value of key is:", value);
* });
*
* Note that unlike localStorage, asyncStorage does not allow you to store and
* retrieve values by setting and querying properties directly. You cannot just
* write asyncStorage.key; you have to explicitly call setItem() or getItem().
*
* removeItem(), clear(), length(), and key() are like the same-named methods of
* localStorage, and all return a promise.
*
* The asynchronous nature of getItem() makes it tricky to retrieve multiple
* values. But unlike localStorage, asyncStorage does not require the values you
* store to be strings. So if you need to save multiple values and want to
* retrieve them together, in a single asynchronous operation, just group the
* values into a single object. The properties of this object may not include
* DOM elements, but they may include things like Blobs and typed arrays.
*
*/
const {Cc, Ci, Cu, Cr} = require("chrome");
const {indexedDB} = require("sdk/indexed-db");
const {Promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
module.exports = (function() {
"use strict";
var DBNAME = "devtools-async-storage";
var DBVERSION = 1;
var STORENAME = "keyvaluepairs";
var db = null;
function withStore(type, onsuccess, onerror) {
if (db) {
var transaction = db.transaction(STORENAME, type);
var store = transaction.objectStore(STORENAME);
onsuccess(store);
} else {
var openreq = indexedDB.open(DBNAME, DBVERSION);
openreq.onerror = function withStoreOnError() {
onerror();
};
openreq.onupgradeneeded = function withStoreOnUpgradeNeeded() {
// First time setup: create an empty object store
openreq.result.createObjectStore(STORENAME);
};
openreq.onsuccess = function withStoreOnSuccess() {
db = openreq.result;
var transaction = db.transaction(STORENAME, type);
var store = transaction.objectStore(STORENAME);
onsuccess(store);
};
}
}
function getItem(key) {
return new Promise((resolve, reject) => {
var req;
withStore("readonly", (store) => {
store.transaction.oncomplete = function onComplete() {
var value = req.result;
if (value === undefined) {
value = null;
}
resolve(value);
};
req = store.get(key);
req.onerror = function getItemOnError() {
reject("Error in asyncStorage.getItem(): ", req.error.name);
};
}, reject);
});
}
function setItem(key, value) {
return new Promise((resolve, reject) => {
withStore("readwrite", (store) => {
store.transaction.oncomplete = resolve;
var req = store.put(value, key);
req.onerror = function setItemOnError() {
reject("Error in asyncStorage.setItem(): ", req.error.name);
};
}, reject);
});
}
function removeItem(key) {
return new Promise((resolve, reject) => {
withStore("readwrite", (store) => {
store.transaction.oncomplete = resolve;
var req = store.delete(key);
req.onerror = function removeItemOnError() {
reject("Error in asyncStorage.removeItem(): ", req.error.name);
};
}, reject);
});
}
function clear() {
return new Promise((resolve, reject) => {
withStore("readwrite", (store) => {
store.transaction.oncomplete = resolve;
var req = store.clear();
req.onerror = function clearOnError() {
reject("Error in asyncStorage.clear(): ", req.error.name);
};
}, reject);
});
}
function length() {
return new Promise((resolve, reject) => {
var req;
withStore("readonly", (store) => {
store.transaction.oncomplete = function onComplete() {
resolve(req.result);
}
req = store.count();
req.onerror = function lengthOnError() {
reject("Error in asyncStorage.length(): ", req.error.name);
};
}, reject);
});
}
function key(n) {
return new Promise((resolve, reject) => {
if (n < 0) {
resolve(null);
return;
}
var req;
withStore("readonly", (store) => {
store.transaction.oncomplete = function onComplete() {
var cursor = req.result;
resolve(cursor ? cursor.key : null);
};
var advanced = false;
req = store.openCursor();
req.onsuccess = function keyOnSuccess() {
var cursor = req.result;
if (!cursor) {
// this means there weren"t enough keys
return;
}
if (n === 0 || advanced) {
// Either 1) we have the first key, return it if that's what they
// wanted, or 2) we"ve got the nth key.
return;
}
// Otherwise, ask the cursor to skip ahead n records
advanced = true;
cursor.advance(n);
};
req.onerror = function keyOnError() {
reject("Error in asyncStorage.key(): ", req.error.name);
};
}, reject);
});
}
return {
getItem: getItem,
setItem: setItem,
removeItem: removeItem,
clear: clear,
length: length,
key: key
};
}());

View File

@ -0,0 +1,11 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# 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/.
BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
EXTRA_JS_MODULES.devtools.shared += [
'async-storage.js'
]

View File

@ -0,0 +1,6 @@
[DEFAULT]
subsuite = devtools
support-files =
../../../server/tests/browser/head.js
[browser_async_storage.js]

View File

@ -0,0 +1,77 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test the basic functionality of async-storage.
// Adapted from https://github.com/mozilla-b2g/gaia/blob/f09993563fb5fec4393eb71816ce76cb00463190/apps/sharedtest/test/unit/async_storage_test.js.
const asyncStorage = require("devtools/toolkit/shared/async-storage");
add_task(function*() {
is(typeof asyncStorage.length, 'function', "API exists.");
is(typeof asyncStorage.key, 'function', "API exists.");
is(typeof asyncStorage.getItem, 'function', "API exists.");
is(typeof asyncStorage.setItem, 'function', "API exists.");
is(typeof asyncStorage.removeItem, 'function', "API exists.");
is(typeof asyncStorage.clear, 'function', "API exists.");
});
add_task(function*() {
yield asyncStorage.setItem('foo', 'bar');
let value = yield asyncStorage.getItem('foo');
is(value, 'bar', 'value is correct');
yield asyncStorage.setItem('foo', 'overwritten');
value = yield asyncStorage.getItem('foo');
is(value, 'overwritten', 'value is correct');
yield asyncStorage.removeItem('foo');
value = yield asyncStorage.getItem('foo');
is(value, null, 'value is correct');
});
add_task(function*() {
var object = {
x: 1,
y: 'foo',
z: true
};
yield asyncStorage.setItem('myobj', object);
let value = yield asyncStorage.getItem('myobj');
is(object.x, value.x, 'value is correct');
is(object.y, value.y, 'value is correct');
is(object.z, value.z, 'value is correct');
yield asyncStorage.removeItem('myobj');
value = yield asyncStorage.getItem('myobj');
is(value, null, 'value is correct');
});
add_task(function*() {
yield asyncStorage.clear();
let len = yield asyncStorage.length();
is(len, 0, 'length is correct');
yield asyncStorage.setItem('key1', 'value1');
len = yield asyncStorage.length();
is(len, 1, 'length is correct');
yield asyncStorage.setItem('key2', 'value2');
len = yield asyncStorage.length();
is(len, 2, 'length is correct');
yield asyncStorage.setItem('key3', 'value3');
len = yield asyncStorage.length();
is(len, 3, 'length is correct');
let key = yield asyncStorage.key(0);
is(key, 'key1', 'key is correct');
key = yield asyncStorage.key(1);
is(key, 'key2', 'key is correct');
key = yield asyncStorage.key(2);
is(key, 'key3', 'key is correct');
key = yield asyncStorage.key(3);
is(key, null, 'key is correct');
yield asyncStorage.clear();
key = yield asyncStorage.key(0);
is(key, null, 'key is correct');
len = yield asyncStorage.length();
is(len, 0, 'length is correct');
});

View File

@ -44,6 +44,20 @@ function run_test() {
regSvc.close();
}
let isLinux = ("@mozilla.org/gio-service;1" in Components.classes);
if (isLinux) {
// Check mailto handler from GIO
// If there isn't one, then we have no mailto handler
let gIOSvc = Cc["@mozilla.org/gio-service;1"].
createInstance(Ci.nsIGIOService);
try {
gIOSvc.getAppForURIScheme("mailto");
noMailto = false;
} catch (ex) {
noMailto = true;
}
}
//**************************************************************************//
// Sample Data
@ -157,7 +171,7 @@ function run_test() {
else
do_check_eq(0, protoInfo.possibleApplicationHandlers.length);
// Win7+ might not have a default mailto: handler
// Win7+ or Linux's GIO might not have a default mailto: handler
if (noMailto)
do_check_true(protoInfo.alwaysAskBeforeHandling);
else
@ -168,7 +182,7 @@ function run_test() {
protoInfo = protoSvc.getProtocolHandlerInfo("mailto");
if (haveDefaultHandlersVersion) {
do_check_eq(2, protoInfo.possibleApplicationHandlers.length);
// Win7+ might not have a default mailto: handler, but on other platforms
// Win7+ or Linux's GIO may have no default mailto: handler. Otherwise
// alwaysAskBeforeHandling is expected to be false here, because although
// the pref is true, the value in RDF is false. The injected mailto handler
// carried over the default pref value, and so when we set the pref above