Merge mozilla-central to autoland. r=merge a=merge

This commit is contained in:
Sebastian Hengst 2017-10-26 00:39:55 +02:00
commit 443416f881
8298 changed files with 65203 additions and 366849 deletions

View File

@ -0,0 +1,5 @@
<html xmlns="http://www.w3.org/1999/xhtml">
<body onload="document.body.appendChild(x);">
<table><tbody><div id="x"></div></tbody></table>
</body>
</html>

View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<script>
function f()
{
window.removeEventListener("pagehide", f, false);
var root = document.documentElement;
document.removeChild(root);
document.appendChild(root);
}
function boom()
{
window.addEventListener("pagehide", f, false);
window.location = "data:text/html,4";
}
</script>
</head>
<body onload="boom();"></body>
</html>

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script>
function boom()
{
document.designMode = 'on';
var otherDoc = document.implementation.createDocument("", "", null);
otherDoc.adoptNode(document.getElementById("t"));
}
</script>
</head>
<body onload="setTimeout(boom, 0);"><textarea id="t"></textarea></body>
</html>

View File

@ -1,5 +1,9 @@
load 448064.xhtml # This test instantiates a11y, so be careful about adding tests before it
load 471493.xul
asserts-if(!browserIsRemote,2) load 884202.html
load 893515.html
load 1072792.xhtml
# last_test_to_unload_testsuite.xul MUST be the last test in the list because it
# is responsible for shutting down accessibility service affecting later tests.
load last_test_to_unload_testsuite.xul

View File

@ -61,6 +61,7 @@ subsuite = clipboard
skip-if = true # temporarily disabled for breaking the treeview - bug 658744
[browser_sort_in_library.js]
[browser_stayopenmenu.js]
skip-if = os == "mac" && debug # bug 1400323
[browser_toolbar_drop_text.js]
[browser_toolbar_overflow.js]
[browser_toolbarbutton_menu_context.js]

View File

@ -217,6 +217,8 @@ this.SiteDataManager = {
cookie.host, cookie.name, cookie.path, false, cookie.originAttributes);
}
}
Services.obs.notifyObservers(null, "browser:purge-domain-data", principal.URI.host);
}
},

View File

@ -32,7 +32,8 @@ function promiseStateChangeURI() {
if (!(flags & docStart) || !webProgress.isTopLevel)
return;
if (req.originalURI.spec == "about:blank")
let spec = req.originalURI.spec;
if (spec == "about:blank")
return;
gBrowser.removeProgressListener(listener);
@ -44,7 +45,7 @@ function promiseStateChangeURI() {
req.cancel(Components.results.NS_ERROR_FAILURE);
executeSoon(() => {
resolve(req.originalURI.spec);
resolve(spec);
});
}
};

View File

@ -82,7 +82,8 @@ function promiseStateChangeURI() {
if (!(flags & docStart) || !webProgress.isTopLevel)
return;
if (req.originalURI.spec == "about:blank")
let spec = req.originalURI.spec;
if (spec == "about:blank")
return;
gBrowser.removeProgressListener(listener);
@ -94,7 +95,7 @@ function promiseStateChangeURI() {
req.cancel(Components.results.NS_ERROR_FAILURE);
executeSoon(() => {
resolve(req.originalURI.spec);
resolve(spec);
});
}
};

View File

@ -17,8 +17,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "Utils",
XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel",
"resource://gre/modules/sessionstore/PrivacyLevel.jsm");
// MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision.
const MAX_EXPIRY = Math.pow(2, 62);
const MAX_EXPIRY = Number.MAX_SAFE_INTEGER;
/**
* The external API implemented by the SessionCookies module.

View File

@ -40,7 +40,7 @@ elif CONFIG['OS_TARGET'] in ('FreeBSD', 'OpenBSD', 'NetBSD'):
SOURCES += ['/nsprpub/pr/src/md/unix/%s.c' % CONFIG['OS_TARGET'].lower()]
elif CONFIG['OS_TARGET'] == 'Darwin':
OS_LIBS += ['-framework CoreServices']
if CONFIG['HOST_MAJOR_VERSION'] == '15':
if CONFIG['HOST_MAJOR_VERSION'] >= '15':
DEFINES.update(
HAS_CONNECTX=True,
)

View File

@ -0,0 +1,111 @@
/* 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/. */
/* eslint-env browser */
"use strict";
const { createFactory, createClass, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const Services = require("Services");
const PanelMenu = createFactory(require("./PanelMenu"));
loader.lazyGetter(this, "AddonsPanel",
() => createFactory(require("./addons/Panel")));
loader.lazyGetter(this, "TabsPanel",
() => createFactory(require("./tabs/Panel")));
loader.lazyGetter(this, "WorkersPanel",
() => createFactory(require("./workers/Panel")));
loader.lazyRequireGetter(this, "DebuggerClient",
"devtools/shared/client/debugger-client", true);
loader.lazyRequireGetter(this, "Telemetry",
"devtools/client/shared/telemetry");
const Strings = Services.strings.createBundle(
"chrome://devtools/locale/aboutdebugging.properties");
const panels = [{
id: "addons",
name: Strings.GetStringFromName("addons"),
icon: "chrome://devtools/skin/images/debugging-addons.svg",
component: AddonsPanel
}, {
id: "tabs",
name: Strings.GetStringFromName("tabs"),
icon: "chrome://devtools/skin/images/debugging-tabs.svg",
component: TabsPanel
}, {
id: "workers",
name: Strings.GetStringFromName("workers"),
icon: "chrome://devtools/skin/images/debugging-workers.svg",
component: WorkersPanel
}];
const defaultPanelId = "addons";
module.exports = createClass({
displayName: "AboutDebuggingApp",
propTypes: {
client: PropTypes.instanceOf(DebuggerClient).isRequired,
telemetry: PropTypes.instanceOf(Telemetry).isRequired
},
getInitialState() {
return {
selectedPanelId: defaultPanelId
};
},
componentDidMount() {
window.addEventListener("hashchange", this.onHashChange);
this.onHashChange();
this.props.telemetry.toolOpened("aboutdebugging");
},
componentWillUnmount() {
window.removeEventListener("hashchange", this.onHashChange);
this.props.telemetry.toolClosed("aboutdebugging");
this.props.telemetry.destroy();
},
onHashChange() {
this.setState({
selectedPanelId: window.location.hash.substr(1) || defaultPanelId
});
},
selectPanel(panelId) {
window.location.hash = "#" + panelId;
},
render() {
let { client } = this.props;
let { selectedPanelId } = this.state;
let selectPanel = this.selectPanel;
let selectedPanel = panels.find(p => p.id == selectedPanelId);
let panel;
if (selectedPanel) {
panel = selectedPanel.component({ client, id: selectedPanel.id });
} else {
panel = (
dom.div({ className: "error-page" },
dom.h1({ className: "header-name" },
Strings.GetStringFromName("pageNotFound")
),
dom.h4({ className: "error-page-details" },
Strings.formatStringFromName("doesNotExist", [selectedPanelId], 1))
)
);
}
return dom.div({ className: "app" },
PanelMenu({ panels, selectedPanelId, selectPanel }),
dom.div({ className: "main-content" }, panel)
);
}
});

View File

@ -0,0 +1,41 @@
/* 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 { createClass, createFactory, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const PanelMenuEntry = createFactory(require("./PanelMenuEntry"));
module.exports = createClass({
displayName: "PanelMenu",
propTypes: {
panels: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
component: PropTypes.func.isRequired
})).isRequired,
selectPanel: PropTypes.func.isRequired,
selectedPanelId: PropTypes.string
},
render() {
let { panels, selectedPanelId, selectPanel } = this.props;
let panelLinks = panels.map(({ id, name, icon }) => {
let selected = id == selectedPanelId;
return PanelMenuEntry({
id,
name,
icon,
selected,
selectPanel
});
});
// "categories" id used for styling purposes
return dom.div({ id: "categories", role: "tablist" }, panelLinks);
},
});

View File

@ -1,111 +0,0 @@
/* 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/. */
/* eslint-env browser */
"use strict";
const { createFactory, createClass, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const Services = require("Services");
const PanelMenu = createFactory(require("./panel-menu"));
loader.lazyGetter(this, "AddonsPanel",
() => createFactory(require("./addons/panel")));
loader.lazyGetter(this, "TabsPanel",
() => createFactory(require("./tabs/panel")));
loader.lazyGetter(this, "WorkersPanel",
() => createFactory(require("./workers/panel")));
loader.lazyRequireGetter(this, "DebuggerClient",
"devtools/shared/client/debugger-client", true);
loader.lazyRequireGetter(this, "Telemetry",
"devtools/client/shared/telemetry");
const Strings = Services.strings.createBundle(
"chrome://devtools/locale/aboutdebugging.properties");
const panels = [{
id: "addons",
name: Strings.GetStringFromName("addons"),
icon: "chrome://devtools/skin/images/debugging-addons.svg",
component: AddonsPanel
}, {
id: "tabs",
name: Strings.GetStringFromName("tabs"),
icon: "chrome://devtools/skin/images/debugging-tabs.svg",
component: TabsPanel
}, {
id: "workers",
name: Strings.GetStringFromName("workers"),
icon: "chrome://devtools/skin/images/debugging-workers.svg",
component: WorkersPanel
}];
const defaultPanelId = "addons";
module.exports = createClass({
displayName: "AboutDebuggingApp",
propTypes: {
client: PropTypes.instanceOf(DebuggerClient).isRequired,
telemetry: PropTypes.instanceOf(Telemetry).isRequired
},
getInitialState() {
return {
selectedPanelId: defaultPanelId
};
},
componentDidMount() {
window.addEventListener("hashchange", this.onHashChange);
this.onHashChange();
this.props.telemetry.toolOpened("aboutdebugging");
},
componentWillUnmount() {
window.removeEventListener("hashchange", this.onHashChange);
this.props.telemetry.toolClosed("aboutdebugging");
this.props.telemetry.destroy();
},
onHashChange() {
this.setState({
selectedPanelId: window.location.hash.substr(1) || defaultPanelId
});
},
selectPanel(panelId) {
window.location.hash = "#" + panelId;
},
render() {
let { client } = this.props;
let { selectedPanelId } = this.state;
let selectPanel = this.selectPanel;
let selectedPanel = panels.find(p => p.id == selectedPanelId);
let panel;
if (selectedPanel) {
panel = selectedPanel.component({ client, id: selectedPanel.id });
} else {
panel = (
dom.div({ className: "error-page" },
dom.h1({ className: "header-name" },
Strings.GetStringFromName("pageNotFound")
),
dom.h4({ className: "error-page-details" },
Strings.formatStringFromName("doesNotExist", [selectedPanelId], 1))
)
);
}
return dom.div({ className: "app" },
PanelMenu({ panels, selectedPanelId, selectPanel }),
dom.div({ className: "main-content" }, panel)
);
}
});

View File

@ -0,0 +1,113 @@
/* 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/. */
/* eslint-env browser */
/* globals AddonManager */
"use strict";
loader.lazyImporter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
const { Cc, Ci } = require("chrome");
const { createFactory, createClass, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const Services = require("Services");
const AddonsInstallError = createFactory(require("./InstallError"));
const Strings = Services.strings.createBundle(
"chrome://devtools/locale/aboutdebugging.properties");
const MORE_INFO_URL = "https://developer.mozilla.org/docs/Tools" +
"/about:debugging#Enabling_add-on_debugging";
module.exports = createClass({
displayName: "AddonsControls",
propTypes: {
debugDisabled: PropTypes.bool
},
getInitialState() {
return {
installError: null,
};
},
onEnableAddonDebuggingChange(event) {
let enabled = event.target.checked;
Services.prefs.setBoolPref("devtools.chrome.enabled", enabled);
Services.prefs.setBoolPref("devtools.debugger.remote-enabled", enabled);
},
loadAddonFromFile() {
this.setState({ installError: null });
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
fp.init(window,
Strings.GetStringFromName("selectAddonFromFile2"),
Ci.nsIFilePicker.modeOpen);
fp.open(res => {
if (res == Ci.nsIFilePicker.returnCancel || !fp.file) {
return;
}
let file = fp.file;
// AddonManager.installTemporaryAddon accepts either
// addon directory or final xpi file.
if (!file.isDirectory() && !file.leafName.endsWith(".xpi")) {
file = file.parent;
}
this.installAddon(file);
});
},
retryInstall() {
this.setState({ installError: null });
this.installAddon(this.state.lastInstallErrorFile);
},
installAddon(file) {
AddonManager.installTemporaryAddon(file)
.then(() => {
this.setState({ lastInstallErrorFile: null });
})
.catch(e => {
console.error(e);
this.setState({ installError: e.message, lastInstallErrorFile: file });
});
},
render() {
let { debugDisabled } = this.props;
return dom.div({ className: "addons-top" },
dom.div({ className: "addons-controls" },
dom.div({ className: "addons-options toggle-container-with-text" },
dom.input({
id: "enable-addon-debugging",
type: "checkbox",
checked: !debugDisabled,
onChange: this.onEnableAddonDebuggingChange,
role: "checkbox",
}),
dom.label({
className: "addons-debugging-label",
htmlFor: "enable-addon-debugging",
title: Strings.GetStringFromName("addonDebugging.tooltip")
}, Strings.GetStringFromName("addonDebugging.label")),
dom.a({ href: MORE_INFO_URL, target: "_blank" },
Strings.GetStringFromName("addonDebugging.learnMore")
),
),
dom.button({
id: "load-addon-from-file",
onClick: this.loadAddonFromFile,
}, Strings.GetStringFromName("loadTemporaryAddon"))
),
AddonsInstallError({
error: this.state.installError,
retryInstall: this.retryInstall,
}));
}
});

View File

@ -0,0 +1,180 @@
/* 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 { AddonManager } = require("resource://gre/modules/AddonManager.jsm");
const { Management } = require("resource://gre/modules/Extension.jsm");
const { createFactory, createClass, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const Services = require("Services");
const AddonsControls = createFactory(require("./Controls"));
const AddonTarget = createFactory(require("./Target"));
const PanelHeader = createFactory(require("../PanelHeader"));
const TargetList = createFactory(require("../TargetList"));
loader.lazyRequireGetter(this, "DebuggerClient",
"devtools/shared/client/debugger-client", true);
const Strings = Services.strings.createBundle(
"chrome://devtools/locale/aboutdebugging.properties");
const ExtensionIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
const CHROME_ENABLED_PREF = "devtools.chrome.enabled";
const REMOTE_ENABLED_PREF = "devtools.debugger.remote-enabled";
const WEB_EXT_URL = "https://developer.mozilla.org/Add-ons" +
"/WebExtensions/Getting_started_with_web-ext";
module.exports = createClass({
displayName: "AddonsPanel",
propTypes: {
client: PropTypes.instanceOf(DebuggerClient).isRequired,
id: PropTypes.string.isRequired
},
getInitialState() {
return {
extensions: [],
debugDisabled: false,
};
},
componentDidMount() {
AddonManager.addAddonListener(this);
// Listen to startup since that's when errors and warnings
// get populated on the extension.
Management.on("startup", this.updateAddonsList);
Services.prefs.addObserver(CHROME_ENABLED_PREF,
this.updateDebugStatus);
Services.prefs.addObserver(REMOTE_ENABLED_PREF,
this.updateDebugStatus);
this.updateDebugStatus();
this.updateAddonsList();
},
componentWillUnmount() {
AddonManager.removeAddonListener(this);
Management.off("startup", this.updateAddonsList);
Services.prefs.removeObserver(CHROME_ENABLED_PREF,
this.updateDebugStatus);
Services.prefs.removeObserver(REMOTE_ENABLED_PREF,
this.updateDebugStatus);
},
updateDebugStatus() {
let debugDisabled =
!Services.prefs.getBoolPref(CHROME_ENABLED_PREF) ||
!Services.prefs.getBoolPref(REMOTE_ENABLED_PREF);
this.setState({ debugDisabled });
},
updateAddonsList() {
this.props.client.listAddons()
.then(({addons}) => {
let extensions = addons.filter(addon => addon.debuggable).map(addon => {
return {
name: addon.name,
icon: addon.iconURL || ExtensionIcon,
addonID: addon.id,
addonActor: addon.actor,
temporarilyInstalled: addon.temporarilyInstalled,
url: addon.url,
manifestURL: addon.manifestURL,
warnings: addon.warnings,
};
});
this.setState({ extensions });
}, error => {
throw new Error("Client error while listing addons: " + error);
});
},
/**
* Mandatory callback as AddonManager listener.
*/
onInstalled() {
this.updateAddonsList();
},
/**
* Mandatory callback as AddonManager listener.
*/
onUninstalled() {
this.updateAddonsList();
},
/**
* Mandatory callback as AddonManager listener.
*/
onEnabled() {
this.updateAddonsList();
},
/**
* Mandatory callback as AddonManager listener.
*/
onDisabled() {
this.updateAddonsList();
},
render() {
let { client, id } = this.props;
let { debugDisabled, extensions: targets } = this.state;
let installedName = Strings.GetStringFromName("extensions");
let temporaryName = Strings.GetStringFromName("temporaryExtensions");
let targetClass = AddonTarget;
const installedTargets = targets.filter((target) => !target.temporarilyInstalled);
const temporaryTargets = targets.filter((target) => target.temporarilyInstalled);
return dom.div({
id: id + "-panel",
className: "panel",
role: "tabpanel",
"aria-labelledby": id + "-header"
},
PanelHeader({
id: id + "-header",
name: Strings.GetStringFromName("addons")
}),
AddonsControls({ debugDisabled }),
dom.div({ id: "temporary-addons" },
TargetList({
id: "temporary-extensions",
name: temporaryName,
targets: temporaryTargets,
client,
debugDisabled,
targetClass,
sort: true
}),
dom.div({ className: "addons-tip"},
dom.span({
className: "addons-web-ext-tip",
}, Strings.GetStringFromName("webExtTip")),
dom.a({ href: WEB_EXT_URL, target: "_blank" },
Strings.GetStringFromName("webExtTip.learnMore")
)
)
),
dom.div({ id: "addons" },
TargetList({
id: "extensions",
name: installedName,
targets: installedTargets,
client,
debugDisabled,
targetClass,
sort: true
})
));
}
});

View File

@ -1,113 +0,0 @@
/* 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/. */
/* eslint-env browser */
/* globals AddonManager */
"use strict";
loader.lazyImporter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
const { Cc, Ci } = require("chrome");
const { createFactory, createClass, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const Services = require("Services");
const AddonsInstallError = createFactory(require("./install-error"));
const Strings = Services.strings.createBundle(
"chrome://devtools/locale/aboutdebugging.properties");
const MORE_INFO_URL = "https://developer.mozilla.org/docs/Tools" +
"/about:debugging#Enabling_add-on_debugging";
module.exports = createClass({
displayName: "AddonsControls",
propTypes: {
debugDisabled: PropTypes.bool
},
getInitialState() {
return {
installError: null,
};
},
onEnableAddonDebuggingChange(event) {
let enabled = event.target.checked;
Services.prefs.setBoolPref("devtools.chrome.enabled", enabled);
Services.prefs.setBoolPref("devtools.debugger.remote-enabled", enabled);
},
loadAddonFromFile() {
this.setState({ installError: null });
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
fp.init(window,
Strings.GetStringFromName("selectAddonFromFile2"),
Ci.nsIFilePicker.modeOpen);
fp.open(res => {
if (res == Ci.nsIFilePicker.returnCancel || !fp.file) {
return;
}
let file = fp.file;
// AddonManager.installTemporaryAddon accepts either
// addon directory or final xpi file.
if (!file.isDirectory() && !file.leafName.endsWith(".xpi")) {
file = file.parent;
}
this.installAddon(file);
});
},
retryInstall() {
this.setState({ installError: null });
this.installAddon(this.state.lastInstallErrorFile);
},
installAddon(file) {
AddonManager.installTemporaryAddon(file)
.then(() => {
this.setState({ lastInstallErrorFile: null });
})
.catch(e => {
console.error(e);
this.setState({ installError: e.message, lastInstallErrorFile: file });
});
},
render() {
let { debugDisabled } = this.props;
return dom.div({ className: "addons-top" },
dom.div({ className: "addons-controls" },
dom.div({ className: "addons-options toggle-container-with-text" },
dom.input({
id: "enable-addon-debugging",
type: "checkbox",
checked: !debugDisabled,
onChange: this.onEnableAddonDebuggingChange,
role: "checkbox",
}),
dom.label({
className: "addons-debugging-label",
htmlFor: "enable-addon-debugging",
title: Strings.GetStringFromName("addonDebugging.tooltip")
}, Strings.GetStringFromName("addonDebugging.label")),
dom.a({ href: MORE_INFO_URL, target: "_blank" },
Strings.GetStringFromName("addonDebugging.learnMore")
),
),
dom.button({
id: "load-addon-from-file",
onClick: this.loadAddonFromFile,
}, Strings.GetStringFromName("loadTemporaryAddon"))
),
AddonsInstallError({
error: this.state.installError,
retryInstall: this.retryInstall,
}));
}
});

View File

@ -3,8 +3,8 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DevToolsModules(
'controls.js',
'install-error.js',
'panel.js',
'target.js',
'Controls.js',
'InstallError.js',
'Panel.js',
'Target.js',
)

View File

@ -1,180 +0,0 @@
/* 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 { AddonManager } = require("resource://gre/modules/AddonManager.jsm");
const { Management } = require("resource://gre/modules/Extension.jsm");
const { createFactory, createClass, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const Services = require("Services");
const AddonsControls = createFactory(require("./controls"));
const AddonTarget = createFactory(require("./target"));
const PanelHeader = createFactory(require("../panel-header"));
const TargetList = createFactory(require("../target-list"));
loader.lazyRequireGetter(this, "DebuggerClient",
"devtools/shared/client/debugger-client", true);
const Strings = Services.strings.createBundle(
"chrome://devtools/locale/aboutdebugging.properties");
const ExtensionIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
const CHROME_ENABLED_PREF = "devtools.chrome.enabled";
const REMOTE_ENABLED_PREF = "devtools.debugger.remote-enabled";
const WEB_EXT_URL = "https://developer.mozilla.org/Add-ons" +
"/WebExtensions/Getting_started_with_web-ext";
module.exports = createClass({
displayName: "AddonsPanel",
propTypes: {
client: PropTypes.instanceOf(DebuggerClient).isRequired,
id: PropTypes.string.isRequired
},
getInitialState() {
return {
extensions: [],
debugDisabled: false,
};
},
componentDidMount() {
AddonManager.addAddonListener(this);
// Listen to startup since that's when errors and warnings
// get populated on the extension.
Management.on("startup", this.updateAddonsList);
Services.prefs.addObserver(CHROME_ENABLED_PREF,
this.updateDebugStatus);
Services.prefs.addObserver(REMOTE_ENABLED_PREF,
this.updateDebugStatus);
this.updateDebugStatus();
this.updateAddonsList();
},
componentWillUnmount() {
AddonManager.removeAddonListener(this);
Management.off("startup", this.updateAddonsList);
Services.prefs.removeObserver(CHROME_ENABLED_PREF,
this.updateDebugStatus);
Services.prefs.removeObserver(REMOTE_ENABLED_PREF,
this.updateDebugStatus);
},
updateDebugStatus() {
let debugDisabled =
!Services.prefs.getBoolPref(CHROME_ENABLED_PREF) ||
!Services.prefs.getBoolPref(REMOTE_ENABLED_PREF);
this.setState({ debugDisabled });
},
updateAddonsList() {
this.props.client.listAddons()
.then(({addons}) => {
let extensions = addons.filter(addon => addon.debuggable).map(addon => {
return {
name: addon.name,
icon: addon.iconURL || ExtensionIcon,
addonID: addon.id,
addonActor: addon.actor,
temporarilyInstalled: addon.temporarilyInstalled,
url: addon.url,
manifestURL: addon.manifestURL,
warnings: addon.warnings,
};
});
this.setState({ extensions });
}, error => {
throw new Error("Client error while listing addons: " + error);
});
},
/**
* Mandatory callback as AddonManager listener.
*/
onInstalled() {
this.updateAddonsList();
},
/**
* Mandatory callback as AddonManager listener.
*/
onUninstalled() {
this.updateAddonsList();
},
/**
* Mandatory callback as AddonManager listener.
*/
onEnabled() {
this.updateAddonsList();
},
/**
* Mandatory callback as AddonManager listener.
*/
onDisabled() {
this.updateAddonsList();
},
render() {
let { client, id } = this.props;
let { debugDisabled, extensions: targets } = this.state;
let installedName = Strings.GetStringFromName("extensions");
let temporaryName = Strings.GetStringFromName("temporaryExtensions");
let targetClass = AddonTarget;
const installedTargets = targets.filter((target) => !target.temporarilyInstalled);
const temporaryTargets = targets.filter((target) => target.temporarilyInstalled);
return dom.div({
id: id + "-panel",
className: "panel",
role: "tabpanel",
"aria-labelledby": id + "-header"
},
PanelHeader({
id: id + "-header",
name: Strings.GetStringFromName("addons")
}),
AddonsControls({ debugDisabled }),
dom.div({ id: "temporary-addons" },
TargetList({
id: "temporary-extensions",
name: temporaryName,
targets: temporaryTargets,
client,
debugDisabled,
targetClass,
sort: true
}),
dom.div({ className: "addons-tip"},
dom.span({
className: "addons-web-ext-tip",
}, Strings.GetStringFromName("webExtTip")),
dom.a({ href: WEB_EXT_URL, target: "_blank" },
Strings.GetStringFromName("webExtTip.learnMore")
)
)
),
dom.div({ id: "addons" },
TargetList({
id: "extensions",
name: installedName,
targets: installedTargets,
client,
debugDisabled,
targetClass,
sort: true
})
));
}
});

View File

@ -9,9 +9,9 @@ DIRS += [
]
DevToolsModules(
'aboutdebugging.js',
'panel-header.js',
'panel-menu-entry.js',
'panel-menu.js',
'target-list.js',
'Aboutdebugging.js',
'PanelHeader.js',
'PanelMenu.js',
'PanelMenuEntry.js',
'TargetList.js',
)

View File

@ -1,41 +0,0 @@
/* 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 { createClass, createFactory, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const PanelMenuEntry = createFactory(require("./panel-menu-entry"));
module.exports = createClass({
displayName: "PanelMenu",
propTypes: {
panels: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
component: PropTypes.func.isRequired
})).isRequired,
selectPanel: PropTypes.func.isRequired,
selectedPanelId: PropTypes.string
},
render() {
let { panels, selectedPanelId, selectPanel } = this.props;
let panelLinks = panels.map(({ id, name, icon }) => {
let selected = id == selectedPanelId;
return PanelMenuEntry({
id,
name,
icon,
selected,
selectPanel
});
});
// "categories" id used for styling purposes
return dom.div({ id: "categories", role: "tablist" }, panelLinks);
},
});

View File

@ -0,0 +1,98 @@
/* 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/. */
/* eslint-env browser */
"use strict";
const { createClass, createFactory, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const Services = require("Services");
const PanelHeader = createFactory(require("../PanelHeader"));
const TargetList = createFactory(require("../TargetList"));
const TabTarget = createFactory(require("./Target"));
loader.lazyRequireGetter(this, "DebuggerClient",
"devtools/shared/client/debugger-client", true);
const Strings = Services.strings.createBundle(
"chrome://devtools/locale/aboutdebugging.properties");
module.exports = createClass({
displayName: "TabsPanel",
propTypes: {
client: PropTypes.instanceOf(DebuggerClient).isRequired,
id: PropTypes.string.isRequired
},
getInitialState() {
return {
tabs: []
};
},
componentDidMount() {
let { client } = this.props;
client.addListener("tabListChanged", this.update);
this.update();
},
componentWillUnmount() {
let { client } = this.props;
client.removeListener("tabListChanged", this.update);
},
update() {
this.props.client.mainRoot.listTabs().then(({ tabs }) => {
// Filter out closed tabs (represented as `null`).
tabs = tabs.filter(tab => !!tab);
tabs.forEach(tab => {
// FIXME Also try to fetch low-res favicon. But we should use actor
// support for this to get the high-res one (bug 1061654).
let url = new URL(tab.url);
if (url.protocol.startsWith("http")) {
let prePath = url.origin;
let idx = url.pathname.lastIndexOf("/");
if (idx === -1) {
prePath += url.pathname;
} else {
prePath += url.pathname.substr(0, idx);
}
tab.icon = prePath + "/favicon.ico";
} else {
tab.icon = "chrome://devtools/skin/images/globe.svg";
}
});
this.setState({ tabs });
});
},
render() {
let { client, id } = this.props;
let { tabs } = this.state;
return dom.div({
id: id + "-panel",
className: "panel",
role: "tabpanel",
"aria-labelledby": id + "-header"
},
PanelHeader({
id: id + "-header",
name: Strings.GetStringFromName("tabs")
}),
dom.div({},
TargetList({
client,
id: "tabs",
name: Strings.GetStringFromName("tabs"),
sort: false,
targetClass: TabTarget,
targets: tabs
})
));
}
});

View File

@ -3,6 +3,6 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DevToolsModules(
'panel.js',
'target.js',
'Panel.js',
'Target.js',
)

View File

@ -1,98 +0,0 @@
/* 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/. */
/* eslint-env browser */
"use strict";
const { createClass, createFactory, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const Services = require("Services");
const PanelHeader = createFactory(require("../panel-header"));
const TargetList = createFactory(require("../target-list"));
const TabTarget = createFactory(require("./target"));
loader.lazyRequireGetter(this, "DebuggerClient",
"devtools/shared/client/debugger-client", true);
const Strings = Services.strings.createBundle(
"chrome://devtools/locale/aboutdebugging.properties");
module.exports = createClass({
displayName: "TabsPanel",
propTypes: {
client: PropTypes.instanceOf(DebuggerClient).isRequired,
id: PropTypes.string.isRequired
},
getInitialState() {
return {
tabs: []
};
},
componentDidMount() {
let { client } = this.props;
client.addListener("tabListChanged", this.update);
this.update();
},
componentWillUnmount() {
let { client } = this.props;
client.removeListener("tabListChanged", this.update);
},
update() {
this.props.client.mainRoot.listTabs().then(({ tabs }) => {
// Filter out closed tabs (represented as `null`).
tabs = tabs.filter(tab => !!tab);
tabs.forEach(tab => {
// FIXME Also try to fetch low-res favicon. But we should use actor
// support for this to get the high-res one (bug 1061654).
let url = new URL(tab.url);
if (url.protocol.startsWith("http")) {
let prePath = url.origin;
let idx = url.pathname.lastIndexOf("/");
if (idx === -1) {
prePath += url.pathname;
} else {
prePath += url.pathname.substr(0, idx);
}
tab.icon = prePath + "/favicon.ico";
} else {
tab.icon = "chrome://devtools/skin/images/globe.svg";
}
});
this.setState({ tabs });
});
},
render() {
let { client, id } = this.props;
let { tabs } = this.state;
return dom.div({
id: id + "-panel",
className: "panel",
role: "tabpanel",
"aria-labelledby": id + "-header"
},
PanelHeader({
id: id + "-header",
name: Strings.GetStringFromName("tabs")
}),
dom.div({},
TargetList({
client,
id: "tabs",
name: Strings.GetStringFromName("tabs"),
sort: false,
targetClass: TabTarget,
targets: tabs
})
));
}
});

View File

@ -0,0 +1,258 @@
/* 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/. */
/* globals window */
"use strict";
loader.lazyImporter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
const { Ci } = require("chrome");
const { createClass, createFactory, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const { getWorkerForms } = require("../../modules/worker");
const Services = require("Services");
const PanelHeader = createFactory(require("../PanelHeader"));
const TargetList = createFactory(require("../TargetList"));
const WorkerTarget = createFactory(require("./Target"));
const MultiE10SWarning = createFactory(require("./MultiE10sWarning"));
const ServiceWorkerTarget = createFactory(require("./ServiceWorkerTarget"));
loader.lazyImporter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
loader.lazyRequireGetter(this, "DebuggerClient",
"devtools/shared/client/debugger-client", true);
const Strings = Services.strings.createBundle(
"chrome://devtools/locale/aboutdebugging.properties");
const WorkerIcon = "chrome://devtools/skin/images/debugging-workers.svg";
const MORE_INFO_URL = "https://developer.mozilla.org/en-US/docs/Tools/about%3Adebugging" +
"#Service_workers_not_compatible";
const PROCESS_COUNT_PREF = "dom.ipc.processCount";
const MULTI_OPTOUT_PREF = "dom.ipc.multiOptOut";
module.exports = createClass({
displayName: "WorkersPanel",
propTypes: {
client: PropTypes.instanceOf(DebuggerClient).isRequired,
id: PropTypes.string.isRequired
},
getInitialState() {
return {
workers: {
service: [],
shared: [],
other: []
},
processCount: 1,
};
},
componentDidMount() {
let client = this.props.client;
client.addListener("workerListChanged", this.updateWorkers);
client.addListener("serviceWorkerRegistrationListChanged", this.updateWorkers);
client.addListener("processListChanged", this.updateWorkers);
client.addListener("registration-changed", this.updateWorkers);
// Some notes about these observers:
// - nsIPrefBranch.addObserver observes prefixes. In reality, watching
// PROCESS_COUNT_PREF watches two separate prefs:
// dom.ipc.processCount *and* dom.ipc.processCount.web. Because these
// are the two ways that we control the number of content processes,
// that works perfectly fine.
// - The user might opt in or out of multi by setting the multi opt out
// pref. That affects whether we need to show our warning, so we need to
// update our state when that pref changes.
// - In all cases, we don't have to manually check which pref changed to
// what. The platform code in nsIXULRuntime.maxWebProcessCount does all
// of that for us.
Services.prefs.addObserver(PROCESS_COUNT_PREF, this.updateMultiE10S);
Services.prefs.addObserver(MULTI_OPTOUT_PREF, this.updateMultiE10S);
this.updateMultiE10S();
this.updateWorkers();
},
componentWillUnmount() {
let client = this.props.client;
client.removeListener("processListChanged", this.updateWorkers);
client.removeListener("serviceWorkerRegistrationListChanged", this.updateWorkers);
client.removeListener("workerListChanged", this.updateWorkers);
client.removeListener("registration-changed", this.updateWorkers);
Services.prefs.removeObserver(PROCESS_COUNT_PREF, this.updateMultiE10S);
Services.prefs.removeObserver(MULTI_OPTOUT_PREF, this.updateMultiE10S);
},
updateMultiE10S() {
// We watch the pref but set the state based on
// nsIXULRuntime.maxWebProcessCount.
let processCount = Services.appinfo.maxWebProcessCount;
this.setState({ processCount });
},
updateWorkers() {
let workers = this.getInitialState().workers;
getWorkerForms(this.props.client).then(forms => {
forms.registrations.forEach(form => {
workers.service.push({
icon: WorkerIcon,
name: form.url,
url: form.url,
scope: form.scope,
fetch: form.fetch,
registrationActor: form.actor,
active: form.active
});
});
forms.workers.forEach(form => {
let worker = {
icon: WorkerIcon,
name: form.url,
url: form.url,
workerActor: form.actor
};
switch (form.type) {
case Ci.nsIWorkerDebugger.TYPE_SERVICE:
let registration = this.getRegistrationForWorker(form, workers.service);
if (registration) {
// XXX: Race, sometimes a ServiceWorkerRegistrationInfo doesn't
// have a scriptSpec, but its associated WorkerDebugger does.
if (!registration.url) {
registration.name = registration.url = form.url;
}
registration.workerActor = form.actor;
} else {
worker.fetch = form.fetch;
// If a service worker registration could not be found, this means we are in
// e10s, and registrations are not forwarded to other processes until they
// reach the activated state. Augment the worker as a registration worker to
// display it in aboutdebugging.
worker.scope = form.scope;
worker.active = false;
workers.service.push(worker);
}
break;
case Ci.nsIWorkerDebugger.TYPE_SHARED:
workers.shared.push(worker);
break;
default:
workers.other.push(worker);
}
});
// XXX: Filter out the service worker registrations for which we couldn't
// find the scriptSpec.
workers.service = workers.service.filter(reg => !!reg.url);
this.setState({ workers });
});
},
getRegistrationForWorker(form, registrations) {
for (let registration of registrations) {
if (registration.scope === form.scope) {
return registration;
}
}
return null;
},
isE10S() {
return Services.appinfo.browserTabsRemoteAutostart;
},
renderServiceWorkersError() {
let isWindowPrivate = PrivateBrowsingUtils.isContentWindowPrivate(window);
let isPrivateBrowsingMode = PrivateBrowsingUtils.permanentPrivateBrowsing;
let isServiceWorkerDisabled = !Services.prefs
.getBoolPref("dom.serviceWorkers.enabled");
let isDisabled = isWindowPrivate || isPrivateBrowsingMode || isServiceWorkerDisabled;
if (!isDisabled) {
return "";
}
return dom.p(
{
className: "service-worker-disabled"
},
dom.div({ className: "warning" }),
dom.span(
{
className: "service-worker-disabled-label",
},
Strings.GetStringFromName("configurationIsNotCompatible.label")
),
dom.a(
{
href: MORE_INFO_URL,
target: "_blank"
},
Strings.GetStringFromName("configurationIsNotCompatible.learnMore")
),
);
},
render() {
let { client, id } = this.props;
let { workers, processCount } = this.state;
let isE10S = Services.appinfo.browserTabsRemoteAutostart;
let isMultiE10S = isE10S && processCount > 1;
return dom.div(
{
id: id + "-panel",
className: "panel",
role: "tabpanel",
"aria-labelledby": id + "-header"
},
PanelHeader({
id: id + "-header",
name: Strings.GetStringFromName("workers")
}),
isMultiE10S ? MultiE10SWarning() : "",
dom.div(
{
id: "workers",
className: "inverted-icons"
},
TargetList({
client,
debugDisabled: isMultiE10S,
error: this.renderServiceWorkersError(),
id: "service-workers",
name: Strings.GetStringFromName("serviceWorkers"),
sort: true,
targetClass: ServiceWorkerTarget,
targets: workers.service
}),
TargetList({
client,
id: "shared-workers",
name: Strings.GetStringFromName("sharedWorkers"),
sort: true,
targetClass: WorkerTarget,
targets: workers.shared
}),
TargetList({
client,
id: "other-workers",
name: Strings.GetStringFromName("otherWorkers"),
sort: true,
targetClass: WorkerTarget,
targets: workers.other
})
)
);
}
});

View File

@ -3,8 +3,8 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DevToolsModules(
'multi-e10s-warning.js',
'panel.js',
'service-worker-target.js',
'target.js',
'MultiE10sWarning.js',
'Panel.js',
'ServiceWorkerTarget.js',
'Target.js',
)

View File

@ -1,258 +0,0 @@
/* 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/. */
/* globals window */
"use strict";
loader.lazyImporter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
const { Ci } = require("chrome");
const { createClass, createFactory, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const { getWorkerForms } = require("../../modules/worker");
const Services = require("Services");
const PanelHeader = createFactory(require("../panel-header"));
const TargetList = createFactory(require("../target-list"));
const WorkerTarget = createFactory(require("./target"));
const MultiE10SWarning = createFactory(require("./multi-e10s-warning"));
const ServiceWorkerTarget = createFactory(require("./service-worker-target"));
loader.lazyImporter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
loader.lazyRequireGetter(this, "DebuggerClient",
"devtools/shared/client/debugger-client", true);
const Strings = Services.strings.createBundle(
"chrome://devtools/locale/aboutdebugging.properties");
const WorkerIcon = "chrome://devtools/skin/images/debugging-workers.svg";
const MORE_INFO_URL = "https://developer.mozilla.org/en-US/docs/Tools/about%3Adebugging" +
"#Service_workers_not_compatible";
const PROCESS_COUNT_PREF = "dom.ipc.processCount";
const MULTI_OPTOUT_PREF = "dom.ipc.multiOptOut";
module.exports = createClass({
displayName: "WorkersPanel",
propTypes: {
client: PropTypes.instanceOf(DebuggerClient).isRequired,
id: PropTypes.string.isRequired
},
getInitialState() {
return {
workers: {
service: [],
shared: [],
other: []
},
processCount: 1,
};
},
componentDidMount() {
let client = this.props.client;
client.addListener("workerListChanged", this.updateWorkers);
client.addListener("serviceWorkerRegistrationListChanged", this.updateWorkers);
client.addListener("processListChanged", this.updateWorkers);
client.addListener("registration-changed", this.updateWorkers);
// Some notes about these observers:
// - nsIPrefBranch.addObserver observes prefixes. In reality, watching
// PROCESS_COUNT_PREF watches two separate prefs:
// dom.ipc.processCount *and* dom.ipc.processCount.web. Because these
// are the two ways that we control the number of content processes,
// that works perfectly fine.
// - The user might opt in or out of multi by setting the multi opt out
// pref. That affects whether we need to show our warning, so we need to
// update our state when that pref changes.
// - In all cases, we don't have to manually check which pref changed to
// what. The platform code in nsIXULRuntime.maxWebProcessCount does all
// of that for us.
Services.prefs.addObserver(PROCESS_COUNT_PREF, this.updateMultiE10S);
Services.prefs.addObserver(MULTI_OPTOUT_PREF, this.updateMultiE10S);
this.updateMultiE10S();
this.updateWorkers();
},
componentWillUnmount() {
let client = this.props.client;
client.removeListener("processListChanged", this.updateWorkers);
client.removeListener("serviceWorkerRegistrationListChanged", this.updateWorkers);
client.removeListener("workerListChanged", this.updateWorkers);
client.removeListener("registration-changed", this.updateWorkers);
Services.prefs.removeObserver(PROCESS_COUNT_PREF, this.updateMultiE10S);
Services.prefs.removeObserver(MULTI_OPTOUT_PREF, this.updateMultiE10S);
},
updateMultiE10S() {
// We watch the pref but set the state based on
// nsIXULRuntime.maxWebProcessCount.
let processCount = Services.appinfo.maxWebProcessCount;
this.setState({ processCount });
},
updateWorkers() {
let workers = this.getInitialState().workers;
getWorkerForms(this.props.client).then(forms => {
forms.registrations.forEach(form => {
workers.service.push({
icon: WorkerIcon,
name: form.url,
url: form.url,
scope: form.scope,
fetch: form.fetch,
registrationActor: form.actor,
active: form.active
});
});
forms.workers.forEach(form => {
let worker = {
icon: WorkerIcon,
name: form.url,
url: form.url,
workerActor: form.actor
};
switch (form.type) {
case Ci.nsIWorkerDebugger.TYPE_SERVICE:
let registration = this.getRegistrationForWorker(form, workers.service);
if (registration) {
// XXX: Race, sometimes a ServiceWorkerRegistrationInfo doesn't
// have a scriptSpec, but its associated WorkerDebugger does.
if (!registration.url) {
registration.name = registration.url = form.url;
}
registration.workerActor = form.actor;
} else {
worker.fetch = form.fetch;
// If a service worker registration could not be found, this means we are in
// e10s, and registrations are not forwarded to other processes until they
// reach the activated state. Augment the worker as a registration worker to
// display it in aboutdebugging.
worker.scope = form.scope;
worker.active = false;
workers.service.push(worker);
}
break;
case Ci.nsIWorkerDebugger.TYPE_SHARED:
workers.shared.push(worker);
break;
default:
workers.other.push(worker);
}
});
// XXX: Filter out the service worker registrations for which we couldn't
// find the scriptSpec.
workers.service = workers.service.filter(reg => !!reg.url);
this.setState({ workers });
});
},
getRegistrationForWorker(form, registrations) {
for (let registration of registrations) {
if (registration.scope === form.scope) {
return registration;
}
}
return null;
},
isE10S() {
return Services.appinfo.browserTabsRemoteAutostart;
},
renderServiceWorkersError() {
let isWindowPrivate = PrivateBrowsingUtils.isContentWindowPrivate(window);
let isPrivateBrowsingMode = PrivateBrowsingUtils.permanentPrivateBrowsing;
let isServiceWorkerDisabled = !Services.prefs
.getBoolPref("dom.serviceWorkers.enabled");
let isDisabled = isWindowPrivate || isPrivateBrowsingMode || isServiceWorkerDisabled;
if (!isDisabled) {
return "";
}
return dom.p(
{
className: "service-worker-disabled"
},
dom.div({ className: "warning" }),
dom.span(
{
className: "service-worker-disabled-label",
},
Strings.GetStringFromName("configurationIsNotCompatible.label")
),
dom.a(
{
href: MORE_INFO_URL,
target: "_blank"
},
Strings.GetStringFromName("configurationIsNotCompatible.learnMore")
),
);
},
render() {
let { client, id } = this.props;
let { workers, processCount } = this.state;
let isE10S = Services.appinfo.browserTabsRemoteAutostart;
let isMultiE10S = isE10S && processCount > 1;
return dom.div(
{
id: id + "-panel",
className: "panel",
role: "tabpanel",
"aria-labelledby": id + "-header"
},
PanelHeader({
id: id + "-header",
name: Strings.GetStringFromName("workers")
}),
isMultiE10S ? MultiE10SWarning() : "",
dom.div(
{
id: "workers",
className: "inverted-icons"
},
TargetList({
client,
debugDisabled: isMultiE10S,
error: this.renderServiceWorkersError(),
id: "service-workers",
name: Strings.GetStringFromName("serviceWorkers"),
sort: true,
targetClass: ServiceWorkerTarget,
targets: workers.service
}),
TargetList({
client,
id: "shared-workers",
name: Strings.GetStringFromName("sharedWorkers"),
sort: true,
targetClass: WorkerTarget,
targets: workers.shared
}),
TargetList({
client,
id: "other-workers",
name: Strings.GetStringFromName("otherWorkers"),
sort: true,
targetClass: WorkerTarget,
targets: workers.other
})
)
);
}
});

View File

@ -29,7 +29,7 @@ const { require } = BrowserLoader({
const { createFactory } = require("devtools/client/shared/vendor/react");
const { render, unmountComponentAtNode } = require("devtools/client/shared/vendor/react-dom");
const AboutDebuggingApp = createFactory(require("./components/aboutdebugging"));
const AboutDebuggingApp = createFactory(require("./components/Aboutdebugging"));
var AboutDebugging = {
init() {

View File

@ -4,8 +4,12 @@
"use strict";
const { addons, createClass, createFactory, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const {
createFactory,
DOM: dom,
PropTypes,
PureComponent,
} = require("devtools/client/shared/vendor/react");
const BoxModelInfo = createFactory(require("./BoxModelInfo"));
const BoxModelMain = createFactory(require("./BoxModelMain"));
@ -13,22 +17,24 @@ const BoxModelProperties = createFactory(require("./BoxModelProperties"));
const Types = require("../types");
module.exports = createClass({
class BoxModel extends PureComponent {
static get propTypes() {
return {
boxModel: PropTypes.shape(Types.boxModel).isRequired,
setSelectedNode: PropTypes.func.isRequired,
showBoxModelProperties: PropTypes.bool.isRequired,
onHideBoxModelHighlighter: PropTypes.func.isRequired,
onShowBoxModelEditor: PropTypes.func.isRequired,
onShowBoxModelHighlighter: PropTypes.func.isRequired,
onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
onToggleGeometryEditor: PropTypes.func.isRequired,
};
}
displayName: "BoxModel",
propTypes: {
boxModel: PropTypes.shape(Types.boxModel).isRequired,
setSelectedNode: PropTypes.func.isRequired,
showBoxModelProperties: PropTypes.bool.isRequired,
onHideBoxModelHighlighter: PropTypes.func.isRequired,
onShowBoxModelEditor: PropTypes.func.isRequired,
onShowBoxModelHighlighter: PropTypes.func.isRequired,
onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
onToggleGeometryEditor: PropTypes.func.isRequired,
},
mixins: [ addons.PureRenderMixin ],
constructor(props) {
super(props);
this.onKeyDown = this.onKeyDown.bind(this);
}
onKeyDown(event) {
let { target } = event;
@ -36,7 +42,7 @@ module.exports = createClass({
if (target == this.boxModelContainer) {
this.boxModelMain.onKeyDown(event);
}
},
}
render() {
let {
@ -83,5 +89,7 @@ module.exports = createClass({
:
null
);
},
});
}
}
module.exports = BoxModel;

View File

@ -5,10 +5,12 @@
"use strict";
const Services = require("Services");
const { addons, createClass, createFactory, PropTypes } =
require("devtools/client/shared/vendor/react");
const {
createFactory,
PropTypes,
PureComponent,
} = require("devtools/client/shared/vendor/react");
const { connect } = require("devtools/client/shared/vendor/react-redux");
const { LocalizationHelper } = require("devtools/shared/l10n");
const Accordion =
@ -22,22 +24,19 @@ const BOXMODEL_L10N = new LocalizationHelper(BOXMODEL_STRINGS_URI);
const BOXMODEL_OPENED_PREF = "devtools.computed.boxmodel.opened";
const BoxModelApp = createClass({
displayName: "BoxModelApp",
propTypes: {
boxModel: PropTypes.shape(Types.boxModel).isRequired,
setSelectedNode: PropTypes.func.isRequired,
showBoxModelProperties: PropTypes.bool.isRequired,
onHideBoxModelHighlighter: PropTypes.func.isRequired,
onShowBoxModelEditor: PropTypes.func.isRequired,
onShowBoxModelHighlighter: PropTypes.func.isRequired,
onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
onToggleGeometryEditor: PropTypes.func.isRequired,
},
mixins: [ addons.PureRenderMixin ],
class BoxModelApp extends PureComponent {
static get propTypes() {
return {
boxModel: PropTypes.shape(Types.boxModel).isRequired,
setSelectedNode: PropTypes.func.isRequired,
showBoxModelProperties: PropTypes.bool.isRequired,
onHideBoxModelHighlighter: PropTypes.func.isRequired,
onShowBoxModelEditor: PropTypes.func.isRequired,
onShowBoxModelHighlighter: PropTypes.func.isRequired,
onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
onToggleGeometryEditor: PropTypes.func.isRequired,
};
}
render() {
return Accordion({
@ -54,8 +53,7 @@ const BoxModelApp = createClass({
}
]
});
},
});
}
}
module.exports = connect(state => state)(BoxModelApp);

View File

@ -4,27 +4,27 @@
"use strict";
const { addons, createClass, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const {
DOM: dom,
PropTypes,
PureComponent,
} = require("devtools/client/shared/vendor/react");
const { editableItem } = require("devtools/client/shared/inplace-editor");
const LONG_TEXT_ROTATE_LIMIT = 3;
module.exports = createClass({
displayName: "BoxModelEditable",
propTypes: {
box: PropTypes.string.isRequired,
direction: PropTypes.string,
focusable: PropTypes.bool.isRequired,
level: PropTypes.string,
property: PropTypes.string.isRequired,
textContent: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
onShowBoxModelEditor: PropTypes.func.isRequired,
},
mixins: [ addons.PureRenderMixin ],
class BoxModelEditable extends PureComponent {
static get propTypes() {
return {
box: PropTypes.string.isRequired,
direction: PropTypes.string,
focusable: PropTypes.bool.isRequired,
level: PropTypes.string,
property: PropTypes.string.isRequired,
textContent: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
onShowBoxModelEditor: PropTypes.func.isRequired,
};
}
componentDidMount() {
let { property, onShowBoxModelEditor } = this.props;
@ -34,7 +34,7 @@ module.exports = createClass({
}, (element, event) => {
onShowBoxModelEditor(element, event, property);
});
},
}
render() {
let {
@ -70,6 +70,7 @@ module.exports = createClass({
textContent
)
);
},
}
}
});
module.exports = BoxModelEditable;

View File

@ -4,9 +4,12 @@
"use strict";
const {
DOM: dom,
PropTypes,
PureComponent,
} = require("devtools/client/shared/vendor/react");
const { LocalizationHelper } = require("devtools/shared/l10n");
const { addons, createClass, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const Types = require("../types");
@ -16,20 +19,22 @@ const BOXMODEL_L10N = new LocalizationHelper(BOXMODEL_STRINGS_URI);
const SHARED_STRINGS_URI = "devtools/client/locales/shared.properties";
const SHARED_L10N = new LocalizationHelper(SHARED_STRINGS_URI);
module.exports = createClass({
class BoxModelInfo extends PureComponent {
static get propTypes() {
return {
boxModel: PropTypes.shape(Types.boxModel).isRequired,
onToggleGeometryEditor: PropTypes.func.isRequired,
};
}
displayName: "BoxModelInfo",
propTypes: {
boxModel: PropTypes.shape(Types.boxModel).isRequired,
onToggleGeometryEditor: PropTypes.func.isRequired,
},
mixins: [ addons.PureRenderMixin ],
constructor(props) {
super(props);
this.onToggleGeometryEditor = this.onToggleGeometryEditor.bind(this);
}
onToggleGeometryEditor(e) {
this.props.onToggleGeometryEditor();
},
}
render() {
let { boxModel } = this.props;
@ -76,6 +81,7 @@ module.exports = createClass({
)
)
);
},
}
}
});
module.exports = BoxModelInfo;

View File

@ -4,11 +4,14 @@
"use strict";
const { addons, createClass, createFactory, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const {
createFactory,
DOM: dom,
PropTypes,
PureComponent,
} = require("devtools/client/shared/vendor/react");
const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
const { KeyCodes } = require("devtools/client/shared/keycodes");
const { LocalizationHelper } = require("devtools/shared/l10n");
const BoxModelEditable = createFactory(require("./BoxModelEditable"));
@ -21,26 +24,39 @@ const BOXMODEL_L10N = new LocalizationHelper(BOXMODEL_STRINGS_URI);
const SHARED_STRINGS_URI = "devtools/client/locales/shared.properties";
const SHARED_L10N = new LocalizationHelper(SHARED_STRINGS_URI);
module.exports = createClass({
displayName: "BoxModelMain",
propTypes: {
boxModel: PropTypes.shape(Types.boxModel).isRequired,
boxModelContainer: PropTypes.object,
onHideBoxModelHighlighter: PropTypes.func.isRequired,
onShowBoxModelEditor: PropTypes.func.isRequired,
onShowBoxModelHighlighter: PropTypes.func.isRequired,
},
mixins: [ addons.PureRenderMixin ],
getInitialState() {
class BoxModelMain extends PureComponent {
static get propTypes() {
return {
boxModel: PropTypes.shape(Types.boxModel).isRequired,
boxModelContainer: PropTypes.object,
onHideBoxModelHighlighter: PropTypes.func.isRequired,
onShowBoxModelEditor: PropTypes.func.isRequired,
onShowBoxModelHighlighter: PropTypes.func.isRequired,
};
}
constructor(props) {
super(props);
this.state = {
activeDescendant: null,
focusable: false,
};
},
this.getAriaActiveDescendant = this.getAriaActiveDescendant.bind(this);
this.getBorderOrPaddingValue = this.getBorderOrPaddingValue.bind(this);
this.getContextBox = this.getContextBox.bind(this);
this.getDisplayPosition = this.getDisplayPosition.bind(this);
this.getHeightValue = this.getHeightValue.bind(this);
this.getMarginValue = this.getMarginValue.bind(this);
this.getPositionValue = this.getPositionValue.bind(this);
this.getWidthValue = this.getWidthValue.bind(this);
this.moveFocus = this.moveFocus.bind(this);
this.setAriaActive = this.setAriaActive.bind(this);
this.onHighlightMouseOver = this.onHighlightMouseOver.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
this.onLevelClick = this.onLevelClick.bind(this);
}
componentDidUpdate() {
let displayPosition = this.getDisplayPosition();
@ -83,7 +99,7 @@ module.exports = createClass({
["click", this.contentLayout]
])
};
},
}
getAriaActiveDescendant() {
let { activeDescendant } = this.state;
@ -96,12 +112,12 @@ module.exports = createClass({
}
return activeDescendant;
},
}
getBorderOrPaddingValue(property) {
let { layout } = this.props.boxModel;
return layout[property] ? parseFloat(layout[property]) : "-";
},
}
/**
* Returns true if the layout box sizing is context box and false otherwise.
@ -109,7 +125,7 @@ module.exports = createClass({
getContextBox() {
let { layout } = this.props.boxModel;
return layout["box-sizing"] == "content-box";
},
}
/**
* Returns true if the position is displayed and false otherwise.
@ -117,7 +133,7 @@ module.exports = createClass({
getDisplayPosition() {
let { layout } = this.props.boxModel;
return layout.position && layout.position != "static";
},
}
getHeightValue(property) {
if (property == undefined) {
@ -133,23 +149,7 @@ module.exports = createClass({
property = parseFloat(property.toPrecision(6));
return property;
},
getWidthValue(property) {
if (property == undefined) {
return "-";
}
let { layout } = this.props.boxModel;
property -= parseFloat(layout["border-left-width"]) +
parseFloat(layout["border-right-width"]) +
parseFloat(layout["padding-left"]) +
parseFloat(layout["padding-right"]);
property = parseFloat(property.toPrecision(6));
return property;
},
}
getMarginValue(property, direction) {
let { layout } = this.props.boxModel;
@ -172,7 +172,7 @@ module.exports = createClass({
}
return value;
},
}
getPositionValue(property) {
let { layout } = this.props.boxModel;
@ -192,7 +192,23 @@ module.exports = createClass({
}
return value;
},
}
getWidthValue(property) {
if (property == undefined) {
return "-";
}
let { layout } = this.props.boxModel;
property -= parseFloat(layout["border-left-width"]) +
parseFloat(layout["border-right-width"]) +
parseFloat(layout["padding-left"]) +
parseFloat(layout["padding-right"]);
property = parseFloat(property.toPrecision(6));
return property;
}
/**
* Move the focus to the next/previous editable element of the current layout.
@ -204,7 +220,7 @@ module.exports = createClass({
* @param {String} level
* Current active layout
*/
moveFocus: function ({ target, shiftKey }, level) {
moveFocus({ target, shiftKey }, level) {
let editBoxes = [
...findDOMNode(this).querySelectorAll(`[data-box="${level}"].boxmodel-editable`)
];
@ -227,7 +243,7 @@ module.exports = createClass({
if (editingMode) {
editBox.click();
}
},
}
/**
* Active aria-level set to current layout.
@ -246,7 +262,7 @@ module.exports = createClass({
this.setState({
activeDescendant: nextLayout.getAttribute("data-box"),
});
},
}
onHighlightMouseOver(event) {
let region = event.target.getAttribute("data-box");
@ -271,7 +287,7 @@ module.exports = createClass({
showOnly: region,
onlyRegionArea: true,
});
},
}
/**
* Handle keyboard navigation and focus for box model layouts.
@ -334,7 +350,7 @@ module.exports = createClass({
default:
break;
}
},
}
/**
* Update aria-active on mouse click.
@ -360,7 +376,7 @@ module.exports = createClass({
if (target && target._editable) {
target.blur();
}
},
}
render() {
let {
@ -686,6 +702,7 @@ module.exports = createClass({
}),
contentBox
);
},
}
}
});
module.exports = BoxModelMain;

View File

@ -4,9 +4,12 @@
"use strict";
const { addons, createClass, createFactory, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const {
createFactory,
DOM: dom,
PropTypes,
PureComponent,
} = require("devtools/client/shared/vendor/react");
const { LocalizationHelper } = require("devtools/shared/l10n");
const ComputedProperty = createFactory(require("./ComputedProperty"));
@ -16,24 +19,26 @@ const Types = require("../types");
const BOXMODEL_STRINGS_URI = "devtools/client/locales/boxmodel.properties";
const BOXMODEL_L10N = new LocalizationHelper(BOXMODEL_STRINGS_URI);
module.exports = createClass({
displayName: "BoxModelProperties",
propTypes: {
boxModel: PropTypes.shape(Types.boxModel).isRequired,
setSelectedNode: PropTypes.func.isRequired,
onHideBoxModelHighlighter: PropTypes.func.isRequired,
onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
},
mixins: [ addons.PureRenderMixin ],
getInitialState() {
class BoxModelProperties extends PureComponent {
static get propTypes() {
return {
boxModel: PropTypes.shape(Types.boxModel).isRequired,
setSelectedNode: PropTypes.func.isRequired,
onHideBoxModelHighlighter: PropTypes.func.isRequired,
onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
};
}
constructor(props) {
super(props);
this.state = {
isOpen: true,
};
},
this.getReferenceElement = this.getReferenceElement.bind(this);
this.onToggleExpander = this.onToggleExpander.bind(this);
}
/**
* Various properties can display a reference element. E.g. position displays an offset
@ -59,13 +64,13 @@ module.exports = createClass({
}
return {};
},
}
onToggleExpander() {
this.setState({
isOpen: !this.state.isOpen,
});
},
}
render() {
let {
@ -121,6 +126,7 @@ module.exports = createClass({
properties
)
);
},
}
}
});
module.exports = BoxModelProperties;

View File

@ -4,26 +4,63 @@
"use strict";
const { addons, createClass, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const {
DOM: dom,
PropTypes,
PureComponent,
} = require("devtools/client/shared/vendor/react");
const { REPS, MODE } = require("devtools/client/shared/components/reps/reps");
const { Rep } = REPS;
module.exports = createClass({
class ComputedProperty extends PureComponent {
static get propTypes() {
return {
name: PropTypes.string.isRequired,
referenceElement: PropTypes.object,
referenceElementType: PropTypes.string,
setSelectedNode: PropTypes.func.isRequired,
value: PropTypes.string,
onHideBoxModelHighlighter: PropTypes.func.isRequired,
onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
};
}
displayName: "ComputedProperty",
constructor(props) {
super(props);
this.renderReferenceElementPreview = this.renderReferenceElementPreview.bind(this);
this.translateNodeFrontToGrip = this.translateNodeFrontToGrip.bind(this);
this.onFocus = this.onFocus.bind(this);
}
propTypes: {
name: PropTypes.string.isRequired,
value: PropTypes.string,
referenceElement: PropTypes.object,
referenceElementType: PropTypes.string,
setSelectedNode: PropTypes.func.isRequired,
onHideBoxModelHighlighter: PropTypes.func.isRequired,
onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
},
renderReferenceElementPreview() {
let {
referenceElement,
referenceElementType,
setSelectedNode,
onShowBoxModelHighlighterForNode,
onHideBoxModelHighlighter
} = this.props;
mixins: [ addons.PureRenderMixin ],
if (!referenceElement) {
return null;
}
return dom.div(
{
className: "reference-element"
},
dom.span({ className: "reference-element-type" }, referenceElementType),
Rep({
defaultRep: referenceElement,
mode: MODE.TINY,
object: this.translateNodeFrontToGrip(referenceElement),
onInspectIconClick: () => setSelectedNode(referenceElement, "box-model"),
onDOMNodeMouseOver: () => onShowBoxModelHighlighterForNode(referenceElement),
onDOMNodeMouseOut: () => onHideBoxModelHighlighter(),
})
);
}
/**
* While waiting for a reps fix in https://github.com/devtools-html/reps/issues/92,
@ -56,40 +93,11 @@ module.exports = createClass({
isConnected: true,
}
};
},
}
onFocus() {
this.container.focus();
},
renderReferenceElementPreview() {
let {
referenceElement,
referenceElementType,
setSelectedNode,
onShowBoxModelHighlighterForNode,
onHideBoxModelHighlighter
} = this.props;
if (!referenceElement) {
return null;
}
return dom.div(
{
className: "reference-element"
},
dom.span({ className: "reference-element-type" }, referenceElementType),
Rep({
defaultRep: referenceElement,
mode: MODE.TINY,
object: this.translateNodeFrontToGrip(referenceElement),
onInspectIconClick: () => setSelectedNode(referenceElement, "box-model"),
onDOMNodeMouseOver: () => onShowBoxModelHighlighterForNode(referenceElement),
onDOMNodeMouseOut: () => onHideBoxModelHighlighter(),
})
);
},
}
render() {
const { name, value } = this.props;
@ -133,6 +141,7 @@ module.exports = createClass({
this.renderReferenceElementPreview()
)
);
},
}
}
});
module.exports = ComputedProperty;

View File

@ -6,7 +6,7 @@
"use strict";
const { DOM, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
const { DOM, Component, PropTypes } = require("devtools/client/shared/vendor/react");
// Shortcuts
const { div } = DOM;
@ -16,25 +16,25 @@ const { div } = DOM;
* as the content. It's used by Sidebar as well as SplitBox
* components.
*/
var InspectorTabPanel = createClass({
displayName: "InspectorTabPanel",
class InspectorTabPanel extends Component {
static get propTypes() {
return {
// ID of the node that should be rendered as the content.
id: PropTypes.string.isRequired,
// Optional prefix for panel IDs.
idPrefix: PropTypes.string,
// Optional mount callback
onMount: PropTypes.func,
};
}
propTypes: {
// ID of the node that should be rendered as the content.
id: PropTypes.string.isRequired,
// Optional prefix for panel IDs.
idPrefix: PropTypes.string,
// Optional mount callback
onMount: PropTypes.func,
},
getDefaultProps: function () {
static get defaultProps() {
return {
idPrefix: "",
};
},
}
componentDidMount: function () {
componentDidMount() {
let doc = this.refs.content.ownerDocument;
let panel = doc.getElementById(this.props.idPrefix + this.props.id);
@ -44,17 +44,17 @@ var InspectorTabPanel = createClass({
if (this.props.onMount) {
this.props.onMount(this.refs.content, this.props);
}
},
}
componentWillUnmount: function () {
componentWillUnmount() {
let doc = this.refs.content.ownerDocument;
let panels = doc.getElementById("tabpanels");
// Move panel's content node back into list of tab panels.
panels.appendChild(this.refs.content.firstChild);
},
}
render: function () {
render() {
return (
div({
ref: "content",
@ -62,6 +62,6 @@ var InspectorTabPanel = createClass({
})
);
}
});
}
module.exports = InspectorTabPanel;

View File

@ -5,12 +5,11 @@
"use strict";
const {
addons,
createClass, createFactory,
createFactory,
DOM: dom,
PropTypes,
PureComponent,
} = require("devtools/client/shared/vendor/react");
const { connect } = require("devtools/client/shared/vendor/react-redux");
const ObjectTreeView = createFactory(require("./ObjectTreeView"));
@ -24,15 +23,13 @@ const ObjectTreeView = createFactory(require("./ObjectTreeView"));
*
* TODO: implement the ExtensionPage viewMode.
*/
const ExtensionSidebar = createClass({
displayName: "ExtensionSidebar",
propTypes: {
id: PropTypes.string.isRequired,
extensionsSidebar: PropTypes.object.isRequired,
},
mixins: [ addons.PureRenderMixin ],
class ExtensionSidebar extends PureComponent {
static get propTypes() {
return {
id: PropTypes.string.isRequired,
extensionsSidebar: PropTypes.object.isRequired,
};
}
render() {
const { id, extensionsSidebar } = this.props;
@ -58,6 +55,6 @@ const ExtensionSidebar = createClass({
return dom.div({ id, className }, sidebarContentEl);
}
});
}
module.exports = connect(state => state)(ExtensionSidebar);

View File

@ -5,9 +5,9 @@
"use strict";
const {
addons,
createClass, createFactory,
createFactory,
PropTypes,
PureComponent,
} = require("devtools/client/shared/vendor/react");
const { REPS, MODE } = require("devtools/client/shared/components/reps/reps");
@ -19,14 +19,12 @@ const TreeView = createFactory(TreeViewClass);
* The ObjectTreeView React Component is used in the ExtensionSidebar component to provide
* a UI viewMode which shows a tree view of the passed JavaScript object.
*/
const ObjectTreeView = createClass({
displayName: "ObjectTreeView",
propTypes: {
object: PropTypes.object.isRequired,
},
mixins: [ addons.PureRenderMixin ],
class ObjectTreeView extends PureComponent {
static get propTypes() {
return {
object: PropTypes.object.isRequired,
};
}
render() {
const { object } = this.props;
@ -55,7 +53,7 @@ const ObjectTreeView = createClass({
maxLevel: 1, maxNodes: 1,
}),
});
},
});
}
}
module.exports = ObjectTreeView;

View File

@ -4,8 +4,12 @@
"use strict";
const { addons, createClass, createFactory, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const {
createFactory,
DOM: dom,
PropTypes,
PureComponent,
} = require("devtools/client/shared/vendor/react");
const { connect } = require("devtools/client/shared/vendor/react-redux");
const SearchBox = createFactory(require("devtools/client/shared/components/SearchBox"));
@ -16,17 +20,14 @@ const Types = require("../types");
const PREVIEW_UPDATE_DELAY = 150;
const App = createClass({
displayName: "App",
propTypes: {
fonts: PropTypes.arrayOf(PropTypes.shape(Types.font)).isRequired,
onPreviewFonts: PropTypes.func.isRequired,
onShowAllFont: PropTypes.func.isRequired,
},
mixins: [ addons.PureRenderMixin ],
class App extends PureComponent {
static get propTypes() {
return {
fonts: PropTypes.arrayOf(PropTypes.shape(Types.font)).isRequired,
onPreviewFonts: PropTypes.func.isRequired,
onShowAllFont: PropTypes.func.isRequired,
};
}
render() {
let {
@ -71,6 +72,6 @@ const App = createClass({
)
);
}
});
}
module.exports = connect(state => state)(App);

View File

@ -4,18 +4,29 @@
"use strict";
const { addons, createClass, DOM: dom, PropTypes } = require("devtools/client/shared/vendor/react");
const {
DOM: dom,
PropTypes,
PureComponent,
} = require("devtools/client/shared/vendor/react");
const { getStr } = require("../utils/l10n");
const Types = require("../types");
module.exports = createClass({
class Font extends PureComponent {
static get propTypes() {
return PropTypes.shape(Types.font).isRequired;
}
displayName: "Font",
propTypes: PropTypes.shape(Types.font).isRequired,
mixins: [ addons.PureRenderMixin ],
constructor(props) {
super(props);
this.getSectionClasses = this.getSectionClasses.bind(this);
this.renderFontCSS = this.renderFontCSS.bind(this);
this.renderFontCSSCode = this.renderFontCSSCode.bind(this);
this.renderFontFormatURL = this.renderFontFormatURL.bind(this);
this.renderFontName = this.renderFontName.bind(this);
this.renderFontPreview = this.renderFontPreview.bind(this);
}
getSectionClasses() {
let { font } = this.props;
@ -28,7 +39,7 @@ module.exports = createClass({
}
return classes.join(" ");
},
}
renderFontCSS(cssFamilyName) {
return dom.p(
@ -48,7 +59,7 @@ module.exports = createClass({
),
"\""
);
},
}
renderFontCSSCode(rule, ruleText) {
return dom.pre(
@ -57,7 +68,7 @@ module.exports = createClass({
},
rule ? ruleText : null
);
},
}
renderFontFormatURL(url, format) {
return dom.p(
@ -88,7 +99,7 @@ module.exports = createClass({
format
)
);
},
}
renderFontName(name) {
return dom.h1(
@ -97,7 +108,7 @@ module.exports = createClass({
},
name
);
},
}
renderFontPreview(previewUrl) {
return dom.div(
@ -111,7 +122,7 @@ module.exports = createClass({
}
)
);
},
}
render() {
let { font } = this.props;
@ -153,4 +164,6 @@ module.exports = createClass({
)
);
}
});
}
module.exports = Font;

View File

@ -4,22 +4,23 @@
"use strict";
const { addons, createClass, createFactory, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const {
createFactory,
DOM: dom,
PropTypes,
PureComponent,
} = require("devtools/client/shared/vendor/react");
const Font = createFactory(require("./Font"));
const Types = require("../types");
module.exports = createClass({
displayName: "FontList",
propTypes: {
fonts: PropTypes.arrayOf(PropTypes.shape(Types.font)).isRequired
},
mixins: [ addons.PureRenderMixin ],
class FontList extends PureComponent {
static get propTypes() {
return {
fonts: PropTypes.arrayOf(PropTypes.shape(Types.font)).isRequired
};
}
render() {
let { fonts } = this.props;
@ -38,6 +39,7 @@ module.exports = createClass({
}))
)
);
},
}
}
});
module.exports = FontList;

View File

@ -4,8 +4,12 @@
"use strict";
const { addons, createClass, createFactory, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const {
createFactory,
DOM: dom,
PropTypes,
PureComponent,
} = require("devtools/client/shared/vendor/react");
const GridDisplaySettings = createFactory(require("./GridDisplaySettings"));
const GridList = createFactory(require("./GridList"));
@ -14,28 +18,25 @@ const GridOutline = createFactory(require("./GridOutline"));
const Types = require("../types");
const { getStr } = require("../utils/l10n");
module.exports = createClass({
displayName: "Grid",
propTypes: {
getSwatchColorPickerTooltip: PropTypes.func.isRequired,
grids: PropTypes.arrayOf(PropTypes.shape(Types.grid)).isRequired,
highlighterSettings: PropTypes.shape(Types.highlighterSettings).isRequired,
setSelectedNode: PropTypes.func.isRequired,
onHideBoxModelHighlighter: PropTypes.func.isRequired,
onSetGridOverlayColor: PropTypes.func.isRequired,
onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
onShowGridAreaHighlight: PropTypes.func.isRequired,
onShowGridCellHighlight: PropTypes.func.isRequired,
onShowGridLineNamesHighlight: PropTypes.func.isRequired,
onToggleGridHighlighter: PropTypes.func.isRequired,
onToggleShowGridAreas: PropTypes.func.isRequired,
onToggleShowGridLineNumbers: PropTypes.func.isRequired,
onToggleShowInfiniteLines: PropTypes.func.isRequired,
},
mixins: [ addons.PureRenderMixin ],
class Grid extends PureComponent {
static get propTypes() {
return {
getSwatchColorPickerTooltip: PropTypes.func.isRequired,
grids: PropTypes.arrayOf(PropTypes.shape(Types.grid)).isRequired,
highlighterSettings: PropTypes.shape(Types.highlighterSettings).isRequired,
setSelectedNode: PropTypes.func.isRequired,
onHideBoxModelHighlighter: PropTypes.func.isRequired,
onSetGridOverlayColor: PropTypes.func.isRequired,
onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
onShowGridAreaHighlight: PropTypes.func.isRequired,
onShowGridCellHighlight: PropTypes.func.isRequired,
onShowGridLineNamesHighlight: PropTypes.func.isRequired,
onToggleGridHighlighter: PropTypes.func.isRequired,
onToggleShowGridAreas: PropTypes.func.isRequired,
onToggleShowGridLineNumbers: PropTypes.func.isRequired,
onToggleShowInfiniteLines: PropTypes.func.isRequired,
};
}
render() {
let {
@ -92,6 +93,7 @@ module.exports = createClass({
},
getStr("layout.noGridsOnThisPage")
);
},
}
}
});
module.exports = Grid;

View File

@ -4,24 +4,33 @@
"use strict";
const { addons, createClass, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const {
DOM: dom,
PropTypes,
PureComponent,
} = require("devtools/client/shared/vendor/react");
const Types = require("../types");
const { getStr } = require("../utils/l10n");
module.exports = createClass({
class GridDisplaySettings extends PureComponent {
static get propTypes() {
return {
highlighterSettings: PropTypes.shape(Types.highlighterSettings).isRequired,
onToggleShowGridAreas: PropTypes.func.isRequired,
onToggleShowGridLineNumbers: PropTypes.func.isRequired,
onToggleShowInfiniteLines: PropTypes.func.isRequired,
};
}
displayName: "GridDisplaySettings",
propTypes: {
highlighterSettings: PropTypes.shape(Types.highlighterSettings).isRequired,
onToggleShowGridAreas: PropTypes.func.isRequired,
onToggleShowGridLineNumbers: PropTypes.func.isRequired,
onToggleShowInfiniteLines: PropTypes.func.isRequired,
},
mixins: [ addons.PureRenderMixin ],
constructor(props) {
super(props);
this.onShowGridAreasCheckboxClick = this.onShowGridAreasCheckboxClick.bind(this);
this.onShowGridLineNumbersCheckboxClick =
this.onShowGridLineNumbersCheckboxClick.bind(this);
this.onShowInfiniteLinesCheckboxClick =
this.onShowInfiniteLinesCheckboxClick.bind(this);
}
onShowGridAreasCheckboxClick() {
let {
@ -30,7 +39,7 @@ module.exports = createClass({
} = this.props;
onToggleShowGridAreas(!highlighterSettings.showGridAreasOverlay);
},
}
onShowGridLineNumbersCheckboxClick() {
let {
@ -39,7 +48,7 @@ module.exports = createClass({
} = this.props;
onToggleShowGridLineNumbers(!highlighterSettings.showGridLineNumbers);
},
}
onShowInfiniteLinesCheckboxClick() {
let {
@ -48,7 +57,7 @@ module.exports = createClass({
} = this.props;
onToggleShowInfiniteLines(!highlighterSettings.showInfiniteLines);
},
}
render() {
let {
@ -118,6 +127,7 @@ module.exports = createClass({
)
)
);
},
}
}
});
module.exports = GridDisplaySettings;

View File

@ -4,7 +4,11 @@
"use strict";
const { addons, createClass, DOM: dom, PropTypes } = require("devtools/client/shared/vendor/react");
const {
DOM: dom,
PropTypes,
PureComponent,
} = require("devtools/client/shared/vendor/react");
const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
// Reps
@ -14,25 +18,29 @@ const ElementNode = REPS.ElementNode;
const Types = require("../types");
module.exports = createClass({
class GridItem extends PureComponent {
static get propTypes() {
return {
getSwatchColorPickerTooltip: PropTypes.func.isRequired,
grid: PropTypes.shape(Types.grid).isRequired,
setSelectedNode: PropTypes.func.isRequired,
onHideBoxModelHighlighter: PropTypes.func.isRequired,
onSetGridOverlayColor: PropTypes.func.isRequired,
onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
onToggleGridHighlighter: PropTypes.func.isRequired,
};
}
displayName: "GridItem",
propTypes: {
getSwatchColorPickerTooltip: PropTypes.func.isRequired,
grid: PropTypes.shape(Types.grid).isRequired,
setSelectedNode: PropTypes.func.isRequired,
onHideBoxModelHighlighter: PropTypes.func.isRequired,
onSetGridOverlayColor: PropTypes.func.isRequired,
onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
onToggleGridHighlighter: PropTypes.func.isRequired,
},
mixins: [ addons.PureRenderMixin ],
constructor(props) {
super(props);
this.setGridColor = this.setGridColor.bind(this);
this.translateNodeFrontToGrip = this.translateNodeFrontToGrip.bind(this);
this.onGridCheckboxClick = this.onGridCheckboxClick.bind(this);
}
componentDidMount() {
let tooltip = this.props.getSwatchColorPickerTooltip();
let swatchEl = findDOMNode(this).querySelector(".grid-color-swatch");
let tooltip = this.props.getSwatchColorPickerTooltip();
let previousColor;
tooltip.addSwatch(swatchEl, {
@ -45,18 +53,18 @@ module.exports = createClass({
previousColor = this.props.grid.color;
},
});
},
}
componentWillUnmount() {
let tooltip = this.props.getSwatchColorPickerTooltip();
let swatchEl = findDOMNode(this).querySelector(".grid-color-swatch");
let tooltip = this.props.getSwatchColorPickerTooltip();
tooltip.removeSwatch(swatchEl);
},
}
setGridColor() {
let color = findDOMNode(this).querySelector(".grid-color-value").textContent;
this.props.onSetGridOverlayColor(this.props.grid.nodeFront, color);
},
}
/**
* While waiting for a reps fix in https://github.com/devtools-html/reps/issues/92,
@ -88,7 +96,7 @@ module.exports = createClass({
nodeType: nodeFront.nodeType,
}
};
},
}
onGridCheckboxClick(e) {
// If the click was on the svg icon to select the node in the inspector, bail out.
@ -106,14 +114,14 @@ module.exports = createClass({
} = this.props;
onToggleGridHighlighter(grid.nodeFront);
},
}
render() {
let {
grid,
setSelectedNode,
onHideBoxModelHighlighter,
onShowBoxModelHighlighterForNode,
setSelectedNode,
} = this.props;
let { nodeFront } = grid;
@ -123,9 +131,9 @@ module.exports = createClass({
{},
dom.input(
{
checked: grid.highlighted,
type: "checkbox",
value: grid.id,
checked: grid.highlighted,
onChange: this.onGridCheckboxClick,
}
),
@ -160,6 +168,7 @@ module.exports = createClass({
grid.color
)
);
},
}
}
});
module.exports = GridItem;

View File

@ -4,29 +4,30 @@
"use strict";
const { addons, createClass, createFactory, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const {
createFactory,
DOM: dom,
PropTypes,
PureComponent,
} = require("devtools/client/shared/vendor/react");
const GridItem = createFactory(require("./GridItem"));
const Types = require("../types");
const { getStr } = require("../utils/l10n");
module.exports = createClass({
displayName: "GridList",
propTypes: {
getSwatchColorPickerTooltip: PropTypes.func.isRequired,
grids: PropTypes.arrayOf(PropTypes.shape(Types.grid)).isRequired,
setSelectedNode: PropTypes.func.isRequired,
onHideBoxModelHighlighter: PropTypes.func.isRequired,
onSetGridOverlayColor: PropTypes.func.isRequired,
onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
onToggleGridHighlighter: PropTypes.func.isRequired,
},
mixins: [ addons.PureRenderMixin ],
class GridList extends PureComponent {
static get propTypes() {
return {
getSwatchColorPickerTooltip: PropTypes.func.isRequired,
grids: PropTypes.arrayOf(PropTypes.shape(Types.grid)).isRequired,
setSelectedNode: PropTypes.func.isRequired,
onHideBoxModelHighlighter: PropTypes.func.isRequired,
onSetGridOverlayColor: PropTypes.func.isRequired,
onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
onToggleGridHighlighter: PropTypes.func.isRequired,
};
}
render() {
let {
@ -64,6 +65,7 @@ module.exports = createClass({
}))
)
);
},
}
}
});
module.exports = GridList;

View File

@ -4,10 +4,13 @@
"use strict";
const { addons, createClass, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const Services = require("Services");
const {
DOM: dom,
PropTypes,
PureComponent,
} = require("devtools/client/shared/vendor/react");
const Types = require("../types");
const { getStr } = require("../utils/l10n");
@ -31,26 +34,37 @@ const GRID_CELL_SCALE_FACTOR = 50;
const VIEWPORT_MIN_HEIGHT = 100;
const VIEWPORT_MAX_HEIGHT = 150;
module.exports = createClass({
displayName: "GridOutline",
propTypes: {
grids: PropTypes.arrayOf(PropTypes.shape(Types.grid)).isRequired,
onShowGridAreaHighlight: PropTypes.func.isRequired,
onShowGridCellHighlight: PropTypes.func.isRequired,
},
mixins: [ addons.PureRenderMixin ],
getInitialState() {
class GridOutline extends PureComponent {
static get propTypes() {
return {
grids: PropTypes.arrayOf(PropTypes.shape(Types.grid)).isRequired,
onShowGridAreaHighlight: PropTypes.func.isRequired,
onShowGridCellHighlight: PropTypes.func.isRequired,
};
}
constructor(props) {
super(props);
this.state = {
height: 0,
selectedGrid: null,
showOutline: true,
width: 0,
};
},
this.doHighlightCell = this.doHighlightCell.bind(this);
this.getGridAreaName = this.getGridAreaName.bind(this);
this.getHeight = this.getHeight.bind(this);
this.getTotalWidthAndHeight = this.getTotalWidthAndHeight.bind(this);
this.renderCannotShowOutlineText = this.renderCannotShowOutlineText.bind(this);
this.renderGrid = this.renderGrid.bind(this);
this.renderGridCell = this.renderGridCell.bind(this);
this.renderGridOutline = this.renderGridOutline.bind(this);
this.renderGridOutlineBorder = this.renderGridOutlineBorder.bind(this);
this.renderOutline = this.renderOutline.bind(this);
this.onHighlightCell = this.onHighlightCell.bind(this);
}
componentWillReceiveProps({ grids }) {
let selectedGrid = grids.find(grid => grid.highlighted);
@ -73,90 +87,7 @@ module.exports = createClass({
}
this.setState({ height, width, selectedGrid, showOutline });
},
/**
* Get the width and height of a given grid.
*
* @param {Object} grid
* A single grid container in the document.
* @return {Object} An object like { width, height }
*/
getTotalWidthAndHeight(grid) {
// TODO: We are drawing the first fragment since only one is currently being stored.
// In the future we will need to iterate over all fragments of a grid.
const { gridFragments } = grid;
const { rows, cols } = gridFragments[0];
let height = 0;
for (let i = 0; i < rows.lines.length - 1; i++) {
height += GRID_CELL_SCALE_FACTOR * (rows.tracks[i].breadth / 100);
}
let width = 0;
for (let i = 0; i < cols.lines.length - 1; i++) {
width += GRID_CELL_SCALE_FACTOR * (cols.tracks[i].breadth / 100);
}
return { width, height };
},
/**
* Returns the grid area name if the given grid cell is part of a grid area, otherwise
* null.
*
* @param {Number} columnNumber
* The column number of the grid cell.
* @param {Number} rowNumber
* The row number of the grid cell.
* @param {Array} areas
* Array of grid areas data stored in the grid fragment.
* @return {String} If there is a grid area return area name, otherwise null.
*/
getGridAreaName(columnNumber, rowNumber, areas) {
const gridArea = areas.find(area =>
(area.rowStart <= rowNumber && area.rowEnd > rowNumber) &&
(area.columnStart <= columnNumber && area.columnEnd > columnNumber)
);
if (!gridArea) {
return null;
}
return gridArea.name;
},
/**
* Returns the height of the grid outline ranging between a minimum and maximum height.
*
* @return {Number} The height of the grid outline.
*/
getHeight() {
const { height } = this.state;
if (height >= VIEWPORT_MAX_HEIGHT) {
return VIEWPORT_MAX_HEIGHT;
} else if (height <= VIEWPORT_MIN_HEIGHT) {
return VIEWPORT_MIN_HEIGHT;
}
return height;
},
onHighlightCell({ target, type }) {
// Debounce the highlighting of cells.
// This way we don't end up sending many requests to the server for highlighting when
// cells get hovered in a rapid succession We only send a request if the user settles
// on a cell for some time.
if (this.highlightTimeout) {
clearTimeout(this.highlightTimeout);
}
this.highlightTimeout = setTimeout(() => {
this.doHighlightCell(target, type === "mouseleave");
this.highlightTimeout = null;
}, GRID_HIGHLIGHTING_DEBOUNCE);
},
}
doHighlightCell(target, hide) {
const {
@ -186,7 +117,75 @@ module.exports = createClass({
onShowGridCellHighlight(grids[id].nodeFront, color, fragmentIndex,
rowNumber, columnNumber);
}
},
}
/**
* Returns the grid area name if the given grid cell is part of a grid area, otherwise
* null.
*
* @param {Number} columnNumber
* The column number of the grid cell.
* @param {Number} rowNumber
* The row number of the grid cell.
* @param {Array} areas
* Array of grid areas data stored in the grid fragment.
* @return {String} If there is a grid area return area name, otherwise null.
*/
getGridAreaName(columnNumber, rowNumber, areas) {
const gridArea = areas.find(area =>
(area.rowStart <= rowNumber && area.rowEnd > rowNumber) &&
(area.columnStart <= columnNumber && area.columnEnd > columnNumber)
);
if (!gridArea) {
return null;
}
return gridArea.name;
}
/**
* Returns the height of the grid outline ranging between a minimum and maximum height.
*
* @return {Number} The height of the grid outline.
*/
getHeight() {
const { height } = this.state;
if (height >= VIEWPORT_MAX_HEIGHT) {
return VIEWPORT_MAX_HEIGHT;
} else if (height <= VIEWPORT_MIN_HEIGHT) {
return VIEWPORT_MIN_HEIGHT;
}
return height;
}
/**
* Get the width and height of a given grid.
*
* @param {Object} grid
* A single grid container in the document.
* @return {Object} An object like { width, height }
*/
getTotalWidthAndHeight(grid) {
// TODO: We are drawing the first fragment since only one is currently being stored.
// In the future we will need to iterate over all fragments of a grid.
const { gridFragments } = grid;
const { rows, cols } = gridFragments[0];
let height = 0;
for (let i = 0; i < rows.lines.length - 1; i++) {
height += GRID_CELL_SCALE_FACTOR * (rows.tracks[i].breadth / 100);
}
let width = 0;
for (let i = 0; i < cols.lines.length - 1; i++) {
width += GRID_CELL_SCALE_FACTOR * (cols.tracks[i].breadth / 100);
}
return { width, height };
}
/**
* Displays a message text "Cannot show outline for this grid".
@ -204,7 +203,7 @@ module.exports = createClass({
),
getStr("layout.cannotShowGridOutline")
);
},
}
/**
* Renders the grid outline for the given grid container object.
@ -253,7 +252,7 @@ module.exports = createClass({
rectangles.unshift(border);
return rectangles;
},
}
/**
* Renders the grid cell of a grid fragment.
@ -297,7 +296,7 @@ module.exports = createClass({
onMouseLeave: this.onHighlightCell,
}
);
},
}
renderGridOutline(grid) {
let { color } = grid;
@ -311,7 +310,7 @@ module.exports = createClass({
},
this.renderGrid(grid)
);
},
}
renderGridOutlineBorder(borderWidth, borderHeight, color) {
return dom.rect(
@ -324,7 +323,7 @@ module.exports = createClass({
height: borderHeight
}
);
},
}
renderOutline() {
const {
@ -346,7 +345,22 @@ module.exports = createClass({
)
:
this.renderCannotShowOutlineText();
},
}
onHighlightCell({ target, type }) {
// Debounce the highlighting of cells.
// This way we don't end up sending many requests to the server for highlighting when
// cells get hovered in a rapid succession We only send a request if the user settles
// on a cell for some time.
if (this.highlightTimeout) {
clearTimeout(this.highlightTimeout);
}
this.highlightTimeout = setTimeout(() => {
this.doHighlightCell(target, type === "mouseleave");
this.highlightTimeout = null;
}, GRID_HIGHLIGHTING_DEBOUNCE);
}
render() {
const { selectedGrid } = this.state;
@ -361,6 +375,7 @@ module.exports = createClass({
)
:
null;
},
}
}
});
module.exports = GridOutline;

View File

@ -55,8 +55,8 @@ function GridInspector(inspector, window) {
this.getSwatchColorPickerTooltip = this.getSwatchColorPickerTooltip.bind(this);
this.updateGridPanel = this.updateGridPanel.bind(this);
this.onNavigate = this.onNavigate.bind(this);
this.onHighlighterChange = this.onHighlighterChange.bind(this);
this.onNavigate = this.onNavigate.bind(this);
this.onReflow = throttle(this.onReflow, 500, this);
this.onSetGridOverlayColor = this.onSetGridOverlayColor.bind(this);
this.onShowGridAreaHighlight = this.onShowGridAreaHighlight.bind(this);
@ -215,6 +215,32 @@ GridInspector.prototype = {
return this.swatchColorPickerTooltip;
},
/**
* Given a list of new grid fronts, and if we have a currently highlighted grid, check
* if its fragments have changed.
*
* @param {Array} newGridFronts
* A list of GridFront objects.
* @return {Boolean}
*/
haveCurrentFragmentsChanged(newGridFronts) {
const currentNode = this.highlighters.gridHighlighterShown;
if (!currentNode) {
return false;
}
const newGridFront = newGridFronts.find(g => g.containerNodeFront === currentNode);
if (!newGridFront) {
return false;
}
const { grids } = this.store.getState();
const oldFragments = grids.find(g => g.nodeFront === currentNode).gridFragments;
const newFragments = newGridFront.gridFragments;
return !compareFragmentsGeometry(oldFragments, newFragments);
},
/**
* Returns true if the layout panel is visible, and false otherwise.
*/
@ -317,16 +343,6 @@ GridInspector.prototype = {
this.store.dispatch(updateGrids(grids));
}),
/**
* Handler for "new-root" event fired by the inspector, which indicates a page
* navigation. Updates grid panel contents.
*/
onNavigate() {
if (this.isPanelVisible()) {
this.updateGridPanel();
}
},
/**
* Handler for "grid-highlighter-shown" and "grid-highlighter-hidden" events emitted
* from the HighlightersOverlay. Updates the NodeFront's grid highlighted state.
@ -361,29 +377,13 @@ GridInspector.prototype = {
},
/**
* Given a list of new grid fronts, and if we have a currently highlighted grid, check
* if its fragments have changed.
*
* @param {Array} newGridFronts
* A list of GridFront objects.
* @return {Boolean}
* Handler for "new-root" event fired by the inspector, which indicates a page
* navigation. Updates grid panel contents.
*/
haveCurrentFragmentsChanged(newGridFronts) {
const currentNode = this.highlighters.gridHighlighterShown;
if (!currentNode) {
return false;
onNavigate() {
if (this.isPanelVisible()) {
this.updateGridPanel();
}
const newGridFront = newGridFronts.find(g => g.containerNodeFront === currentNode);
if (!newGridFront) {
return false;
}
const { grids } = this.store.getState();
const oldFragments = grids.find(g => g.nodeFront === currentNode).gridFragments;
const newFragments = newGridFront.gridFragments;
return !compareFragmentsGeometry(oldFragments, newFragments);
},
/**

View File

@ -10,25 +10,30 @@
"use strict";
const React = require("devtools/client/shared/vendor/react");
const { DOM: dom, PropTypes } = React;
const { PureComponent, DOM: dom, PropTypes } = React;
const { div, span } = dom;
const Accordion = React.createClass({
displayName: "Accordion",
class Accordion extends PureComponent {
static get propTypes() {
return {
items: PropTypes.array
};
}
propTypes: {
items: PropTypes.array
},
constructor(props) {
super(props);
mixins: [ React.addons.PureRenderMixin ],
this.state = {
opened: props.items.map(item => item.opened),
created: []
};
getInitialState: function () {
return { opened: this.props.items.map(item => item.opened),
created: [] };
},
this.handleHeaderClick = this.handleHeaderClick.bind(this);
this.renderContainer = this.renderContainer.bind(this);
}
handleHeaderClick: function (i) {
handleHeaderClick(i) {
const opened = [...this.state.opened];
const created = [...this.state.created];
const item = this.props.items[i];
@ -45,9 +50,9 @@ const Accordion = React.createClass({
}
this.setState({ opened, created });
},
}
renderContainer: function (item, i) {
renderContainer(item, i) {
const { opened, created } = this.state;
const containerClassName =
item.header.toLowerCase().replace(/\s/g, "-") + "-pane";
@ -75,14 +80,14 @@ const Accordion = React.createClass({
) :
null
);
},
}
render: function () {
render() {
return div(
{ className: "accordion" },
this.props.items.map(this.renderContainer)
);
}
});
}
module.exports = Accordion;

View File

@ -5,10 +5,13 @@
"use strict";
const Services = require("Services");
const { addons, createClass, createFactory, DOM: dom, PropTypes } =
require("devtools/client/shared/vendor/react");
const {
createFactory,
DOM: dom,
PropTypes,
PureComponent,
} = require("devtools/client/shared/vendor/react");
const { connect } = require("devtools/client/shared/vendor/react-redux");
const { LocalizationHelper } = require("devtools/shared/l10n");
const BoxModel = createFactory(require("devtools/client/inspector/boxmodel/components/BoxModel"));
@ -28,28 +31,25 @@ const LAYOUT_L10N = new LocalizationHelper(LAYOUT_STRINGS_URI);
const BOXMODEL_OPENED_PREF = "devtools.layout.boxmodel.opened";
const GRID_OPENED_PREF = "devtools.layout.grid.opened";
const App = createClass({
displayName: "App",
propTypes: {
boxModel: PropTypes.shape(BoxModelTypes.boxModel).isRequired,
getSwatchColorPickerTooltip: PropTypes.func.isRequired,
grids: PropTypes.arrayOf(PropTypes.shape(GridTypes.grid)).isRequired,
highlighterSettings: PropTypes.shape(GridTypes.highlighterSettings).isRequired,
setSelectedNode: PropTypes.func.isRequired,
showBoxModelProperties: PropTypes.bool.isRequired,
onHideBoxModelHighlighter: PropTypes.func.isRequired,
onSetGridOverlayColor: PropTypes.func.isRequired,
onShowBoxModelEditor: PropTypes.func.isRequired,
onShowBoxModelHighlighter: PropTypes.func.isRequired,
onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
onToggleGridHighlighter: PropTypes.func.isRequired,
onToggleShowGridLineNumbers: PropTypes.func.isRequired,
onToggleShowInfiniteLines: PropTypes.func.isRequired,
},
mixins: [ addons.PureRenderMixin ],
class App extends PureComponent {
static get propTypes() {
return {
boxModel: PropTypes.shape(BoxModelTypes.boxModel).isRequired,
getSwatchColorPickerTooltip: PropTypes.func.isRequired,
grids: PropTypes.arrayOf(PropTypes.shape(GridTypes.grid)).isRequired,
highlighterSettings: PropTypes.shape(GridTypes.highlighterSettings).isRequired,
setSelectedNode: PropTypes.func.isRequired,
showBoxModelProperties: PropTypes.bool.isRequired,
onHideBoxModelHighlighter: PropTypes.func.isRequired,
onSetGridOverlayColor: PropTypes.func.isRequired,
onShowBoxModelEditor: PropTypes.func.isRequired,
onShowBoxModelHighlighter: PropTypes.func.isRequired,
onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
onToggleGridHighlighter: PropTypes.func.isRequired,
onToggleShowGridLineNumbers: PropTypes.func.isRequired,
onToggleShowInfiniteLines: PropTypes.func.isRequired,
};
}
render() {
return dom.div(
@ -59,9 +59,9 @@ const App = createClass({
Accordion({
items: [
{
header: LAYOUT_L10N.getStr("layout.header"),
component: Grid,
componentProps: this.props,
header: LAYOUT_L10N.getStr("layout.header"),
opened: Services.prefs.getBoolPref(GRID_OPENED_PREF),
onToggled: () => {
let opened = Services.prefs.getBoolPref(GRID_OPENED_PREF);
@ -69,9 +69,9 @@ const App = createClass({
}
},
{
header: BOXMODEL_L10N.getStr("boxmodel.title"),
component: BoxModel,
componentProps: this.props,
header: BOXMODEL_L10N.getStr("boxmodel.title"),
opened: Services.prefs.getBoolPref(BOXMODEL_OPENED_PREF),
onToggled: () => {
let opened = Services.prefs.getBoolPref(BOXMODEL_OPENED_PREF);
@ -81,8 +81,7 @@ const App = createClass({
]
})
);
},
});
}
}
module.exports = connect(state => state)(App);

View File

@ -8,7 +8,6 @@
const PREF_WHITELIST = [
"devtools",
"layout.css.grid.enabled"
];
const acceptLine = function (line) {

View File

@ -44,7 +44,7 @@ const EventEmitter = require("devtools-modules/src/utils/event-emitter");
EventEmitter.decorate(window);
const { configureStore } = require("./src/utils/create-store");
const App = require("./src/components/app");
const App = require("./src/components/App");
const { Connector } = require("./src/connector/index");
const connector = new Connector();
const store = configureStore(connector);

View File

@ -27,7 +27,6 @@ const {LocalizationHelper} = require("devtools/shared/l10n");
const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES);
const HTML_NS = "http://www.w3.org/1999/xhtml";
const CSS_GRID_ENABLED_PREF = "layout.css.grid.enabled";
const CSS_SHAPES_ENABLED_PREF = "devtools.inspector.shapesHighlighter.enabled";
const CSS_SHAPE_OUTSIDE_ENABLED_PREF = "layout.css.shape-outside.enabled";
@ -371,8 +370,7 @@ OutputParser.prototype = {
if (options.expectCubicBezier &&
BEZIER_KEYWORDS.indexOf(token.text) >= 0) {
this._appendCubicBezier(token.text, options);
} else if (Services.prefs.getBoolPref(CSS_GRID_ENABLED_PREF) &&
this._isDisplayGrid(text, token, options)) {
} else if (this._isDisplayGrid(text, token, options)) {
this._appendGrid(token.text, options);
} else if (colorOK() &&
colorUtils.isValidCSSColor(token.text, this.cssColor4)) {

View File

@ -707,10 +707,11 @@ HighlighterEnvironment.prototype = {
register("BoxModelHighlighter", "box-model");
register("CssGridHighlighter", "css-grid");
register("CssTransformHighlighter", "css-transform");
register("SelectorHighlighter", "selector");
register("GeometryEditorHighlighter", "geometry-editor");
register("RulersHighlighter", "rulers");
register("MeasuringToolHighlighter", "measuring-tool");
register("EyeDropper", "eye-dropper");
register("FlexboxHighlighter", "flexbox");
register("GeometryEditorHighlighter", "geometry-editor");
register("MeasuringToolHighlighter", "measuring-tool");
register("PausedDebuggerOverlay", "paused-debugger");
register("RulersHighlighter", "rulers");
register("SelectorHighlighter", "selector");
register("ShapesHighlighter", "shapes");

View File

@ -6,34 +6,37 @@
const Services = require("Services");
const { AutoRefreshHighlighter } = require("./auto-refresh");
const {
CANVAS_SIZE,
drawBubbleRect,
drawLine,
drawRect,
drawRoundedRect,
getBoundsFromPoints,
getCanvasPosition,
getCurrentMatrix,
getPathDescriptionFromPoints,
getPointsFromDiagonal,
updateCanvasElement,
} = require("./utils/canvas");
const {
CanvasFrameAnonymousContentHelper,
createNode,
createSVGNode,
moveInfobar,
} = require("./utils/markup");
const { apply } = require("devtools/shared/layout/dom-matrix-2d");
const {
getCurrentZoom,
getDisplayPixelRatio,
setIgnoreLayoutChanges,
getViewportDimensions,
} = require("devtools/shared/layout/utils");
const {
identity,
apply,
translate,
multiply,
scale,
isIdentity,
getNodeTransformationMatrix,
} = require("devtools/shared/layout/dom-matrix-2d");
const { stringifyGridFragments } = require("devtools/server/actors/utils/css-grid-utils");
const { LocalizationHelper } = require("devtools/shared/l10n");
const LAYOUT_STRINGS_URI = "devtools/client/locales/layout.properties";
const LAYOUT_L10N = new LocalizationHelper(LAYOUT_STRINGS_URI);
const CSS_GRID_ENABLED_PREF = "layout.css.grid.enabled";
const NEGATIVE_LINE_NUMBERS_PREF = "devtools.gridinspector.showNegativeLineNumbers";
const DEFAULT_GRID_COLOR = "#4B0082";
@ -75,263 +78,6 @@ const GRID_GAP_ALPHA = 0.5;
*/
const gCachedGridPattern = new Map();
// We create a <canvas> element that has always 4096x4096 physical pixels, to displays
// our grid's overlay.
// Then, we move the element around when needed, to give the perception that it always
// covers the screen (See bug 1345434).
//
// This canvas size value is the safest we can use because most GPUs can handle it.
// It's also far from the maximum canvas memory allocation limit (4096x4096x4 is
// 67.108.864 bytes, where the limit is 500.000.000 bytes, see:
// http://searchfox.org/mozilla-central/source/gfx/thebes/gfxPrefs.h#401).
//
// Note:
// Once bug 1232491 lands, we could try to refactor this code to use the values from
// the displayport API instead.
//
// Using a fixed value should also solve bug 1348293.
const CANVAS_SIZE = 4096;
/**
* Returns an array containing the four coordinates of a rectangle, given its diagonal
* as input; optionally applying a matrix, and a function to each of the coordinates'
* value.
*
* @param {Number} x1
* The x-axis coordinate of the rectangle's diagonal start point.
* @param {Number} y1
* The y-axis coordinate of the rectangle's diagonal start point.
* @param {Number} x2
* The x-axis coordinate of the rectangle's diagonal end point.
* @param {Number} y2
* The y-axis coordinate of the rectangle's diagonal end point.
* @param {Array} [matrix=identity()]
* A transformation matrix to apply.
* @return {Array}
* The rect four corners' points transformed by the matrix given.
*/
function getPointsFromDiagonal(x1, y1, x2, y2, matrix = identity()) {
return [
[x1, y1],
[x2, y1],
[x2, y2],
[x1, y2]
].map(point => {
let transformedPoint = apply(matrix, point);
return {x: transformedPoint[0], y: transformedPoint[1]};
});
}
/**
* Takes an array of four points and returns a DOMRect-like object, represent the
* boundaries defined by the points given.
*
* @param {Array} points
* The four points.
* @return {Object}
* A DOMRect-like object.
*/
function getBoundsFromPoints(points) {
let bounds = {};
bounds.left = Math.min(points[0].x, points[1].x, points[2].x, points[3].x);
bounds.right = Math.max(points[0].x, points[1].x, points[2].x, points[3].x);
bounds.top = Math.min(points[0].y, points[1].y, points[2].y, points[3].y);
bounds.bottom = Math.max(points[0].y, points[1].y, points[2].y, points[3].y);
bounds.x = bounds.left;
bounds.y = bounds.top;
bounds.width = bounds.right - bounds.left;
bounds.height = bounds.bottom - bounds.top;
return bounds;
}
/**
* Takes an array of four points and returns a string represent a path description.
*
* @param {Array} points
* The four points.
* @return {String}
* A Path Description that can be used in svg's <path> element.
*/
function getPathDescriptionFromPoints(points) {
return "M" + points[0].x + "," + points[0].y + " " +
"L" + points[1].x + "," + points[1].y + " " +
"L" + points[2].x + "," + points[2].y + " " +
"L" + points[3].x + "," + points[3].y;
}
/**
* Draws a line to the context given, applying a transformation matrix if passed.
*
* @param {CanvasRenderingContext2D} ctx
* The 2d canvas context.
* @param {Number} x1
* The x-axis of the coordinate for the begin of the line.
* @param {Number} y1
* The y-axis of the coordinate for the begin of the line.
* @param {Number} x2
* The x-axis of the coordinate for the end of the line.
* @param {Number} y2
* The y-axis of the coordinate for the end of the line.
* @param {Object} [options]
* The options object.
* @param {Array} [options.matrix=identity()]
* The transformation matrix to apply.
* @param {Array} [options.extendToBoundaries]
* If set, the line will be extended to reach the boundaries specified.
*/
function drawLine(ctx, x1, y1, x2, y2, options) {
let matrix = options.matrix || identity();
let p1 = apply(matrix, [x1, y1]);
let p2 = apply(matrix, [x2, y2]);
x1 = p1[0];
y1 = p1[1];
x2 = p2[0];
y2 = p2[1];
if (options.extendToBoundaries) {
if (p1[1] === p2[1]) {
x1 = options.extendToBoundaries[0];
x2 = options.extendToBoundaries[2];
} else {
y1 = options.extendToBoundaries[1];
x1 = (p2[0] - p1[0]) * (y1 - p1[1]) / (p2[1] - p1[1]) + p1[0];
y2 = options.extendToBoundaries[3];
x2 = (p2[0] - p1[0]) * (y2 - p1[1]) / (p2[1] - p1[1]) + p1[0];
}
}
ctx.moveTo(Math.round(x1), Math.round(y1));
ctx.lineTo(Math.round(x2), Math.round(y2));
}
/**
* Draws a rect to the context given, applying a transformation matrix if passed.
* The coordinates are the start and end points of the rectangle's diagonal.
*
* @param {CanvasRenderingContext2D} ctx
* The 2d canvas context.
* @param {Number} x1
* The x-axis coordinate of the rectangle's diagonal start point.
* @param {Number} y1
* The y-axis coordinate of the rectangle's diagonal start point.
* @param {Number} x2
* The x-axis coordinate of the rectangle's diagonal end point.
* @param {Number} y2
* The y-axis coordinate of the rectangle's diagonal end point.
* @param {Array} [matrix=identity()]
* The transformation matrix to apply.
*/
function drawRect(ctx, x1, y1, x2, y2, matrix = identity()) {
let p = getPointsFromDiagonal(x1, y1, x2, y2, matrix);
ctx.beginPath();
ctx.moveTo(Math.round(p[0].x), Math.round(p[0].y));
ctx.lineTo(Math.round(p[1].x), Math.round(p[1].y));
ctx.lineTo(Math.round(p[2].x), Math.round(p[2].y));
ctx.lineTo(Math.round(p[3].x), Math.round(p[3].y));
ctx.closePath();
}
/**
* Utility method to draw a rounded rectangle in the provided canvas context.
*
* @param {CanvasRenderingContext2D} ctx
* The 2d canvas context.
* @param {Number} x
* The x-axis origin of the rectangle.
* @param {Number} y
* The y-axis origin of the rectangle.
* @param {Number} width
* The width of the rectangle.
* @param {Number} height
* The height of the rectangle.
* @param {Number} radius
* The radius of the rounding.
*/
function drawRoundedRect(ctx, x, y, width, height, radius) {
ctx.beginPath();
ctx.moveTo(x, y + radius);
ctx.lineTo(x, y + height - radius);
ctx.arcTo(x, y + height, x + radius, y + height, radius);
ctx.lineTo(x + width - radius, y + height);
ctx.arcTo(x + width, y + height, x + width, y + height - radius, radius);
ctx.lineTo(x + width, y + radius);
ctx.arcTo(x + width, y, x + width - radius, y, radius);
ctx.lineTo(x + radius, y);
ctx.arcTo(x, y, x, y + radius, radius);
ctx.stroke();
ctx.fill();
}
/**
* Utility method to draw an arrow-bubble rectangle in the provided canvas context.
*
* @param {CanvasRenderingContext2D} ctx
* The 2d canvas context.
* @param {Number} x
* The x-axis origin of the rectangle.
* @param {Number} y
* The y-axis origin of the rectangle.
* @param {Number} width
* The width of the rectangle.
* @param {Number} height
* The height of the rectangle.
* @param {Number} radius
* The radius of the rounding.
* @param {Number} margin
* The distance of the origin point from the pointer.
* @param {Number} arrowSize
* The size of the arrow.
* @param {String} alignment
* The alignment of the rectangle in relation to its position to the grid.
*/
function drawBubbleRect(ctx, x, y, width, height, radius, margin, arrowSize, alignment) {
let angle = 0;
if (alignment === "bottom") {
angle = 180;
} else if (alignment === "right") {
angle = 90;
[width, height] = [height, width];
} else if (alignment === "left") {
[width, height] = [height, width];
angle = 270;
}
let originX = x;
let originY = y;
ctx.save();
ctx.translate(originX, originY);
ctx.rotate(angle * (Math.PI / 180));
ctx.translate(-originX, -originY);
ctx.translate(-width / 2, -height - arrowSize - margin);
ctx.beginPath();
ctx.moveTo(x, y + radius);
ctx.lineTo(x, y + height - radius);
ctx.arcTo(x, y + height, x + radius, y + height, radius);
ctx.lineTo(x + width / 2 - arrowSize, y + height);
ctx.lineTo(x + width / 2, y + height + arrowSize);
ctx.lineTo(x + width / 2 + arrowSize, y + height);
ctx.arcTo(x + width, y + height, x + width, y + height - radius, radius);
ctx.lineTo(x + width, y + radius);
ctx.arcTo(x + width, y, x + width - radius, y, radius);
ctx.lineTo(x + radius, y);
ctx.arcTo(x, y, x, y + radius, radius);
ctx.stroke();
ctx.fill();
ctx.restore();
}
/**
* The CssGridHighlighter is the class that overlays a visual grid on top of
* display:[inline-]grid elements.
@ -430,9 +176,12 @@ class CssGridHighlighter extends AutoRefreshHighlighter {
y: 0
};
// Calling `calculateCanvasPosition` anyway since the highlighter could be initialized
// Calling `getCanvasPosition` anyway since the highlighter could be initialized
// on a page that has scrolled already.
this.calculateCanvasPosition();
let { canvasX, canvasY } = getCanvasPosition(this._canvasPosition, this._scroll,
this.win, this._winDimensions);
this._canvasPosition.x = canvasX;
this._canvasPosition.y = canvasY;
}
_buildMarkup() {
@ -760,7 +509,7 @@ class CssGridHighlighter extends AutoRefreshHighlighter {
}
_show() {
if (Services.prefs.getBoolPref(CSS_GRID_ENABLED_PREF) && !this.isGrid()) {
if (!this.isGrid()) {
this.hide();
return false;
}
@ -901,14 +650,17 @@ class CssGridHighlighter extends AutoRefreshHighlighter {
// Updates the <canvas> element's position and size.
// It also clear the <canvas>'s drawing context.
this.updateCanvasElement();
updateCanvasElement(this.canvas, this._canvasPosition, this.win.devicePixelRatio);
// Clear the grid area highlights.
this.clearGridAreas();
this.clearGridCell();
// Update the current matrix used in our canvas' rendering
this.updateCurrentMatrix();
let { currentMatrix, hasNodeTransformations } = getCurrentMatrix(this.currentNode,
this.win);
this.currentMatrix = currentMatrix;
this.hasNodeTransformations = hasNodeTransformations;
// Start drawing the grid fragments.
for (let i = 0; i < this.gridData.length; i++) {
@ -936,7 +688,7 @@ class CssGridHighlighter extends AutoRefreshHighlighter {
this._showGridElements();
root.setAttribute("style",
`position:absolute; width:${width}px;height:${height}px; overflow:hidden`);
`position: absolute; width: ${width}px; height: ${height}px; overflow: hidden`);
setIgnoreLayoutChanges(false, this.highlighterEnv.document.documentElement);
return true;
@ -1020,134 +772,14 @@ class CssGridHighlighter extends AutoRefreshHighlighter {
* to give the illusion that it always covers the viewport.
*/
_scrollUpdate() {
let hasPositionChanged = this.calculateCanvasPosition();
let { hasUpdated } = getCanvasPosition(this._canvasPosition, this._scroll, this.win,
this._winDimensions);
if (hasPositionChanged) {
if (hasUpdated) {
this._update();
}
}
/**
* This method is responsible to do the math that updates the <canvas>'s position,
* in accordance with the page's scroll, document's size, canvas size, and
* viewport's size.
* It's called when a page's scroll is detected.
*
* @return {Boolean} `true` if the <canvas> position was updated, `false` otherwise.
*/
calculateCanvasPosition() {
let cssCanvasSize = CANVAS_SIZE / this.win.devicePixelRatio;
let viewportSize = getViewportDimensions(this.win);
let documentSize = this._winDimensions;
let pageX = this._scroll.x;
let pageY = this._scroll.y;
let canvasWidth = cssCanvasSize;
let canvasHeight = cssCanvasSize;
let hasUpdated = false;
// Those values indicates the relative horizontal and vertical space the page can
// scroll before we have to reposition the <canvas>; they're 1/4 of the delta between
// the canvas' size and the viewport's size: that's because we want to consider both
// sides (top/bottom, left/right; so 1/2 for each side) and also we don't want to
// shown the edges of the canvas in case of fast scrolling (to avoid showing undraw
// areas, therefore another 1/2 here).
let bufferSizeX = (canvasWidth - viewportSize.width) >> 2;
let bufferSizeY = (canvasHeight - viewportSize.height) >> 2;
let { x, y } = this._canvasPosition;
// Defines the boundaries for the canvas.
let topBoundary = 0;
let bottomBoundary = documentSize.height - canvasHeight;
let leftBoundary = 0;
let rightBoundary = documentSize.width - canvasWidth;
// Defines the thresholds that triggers the canvas' position to be updated.
let topThreshold = pageY - bufferSizeY;
let bottomThreshold = pageY - canvasHeight + viewportSize.height + bufferSizeY;
let leftThreshold = pageX - bufferSizeX;
let rightThreshold = pageX - canvasWidth + viewportSize.width + bufferSizeX;
if (y < bottomBoundary && y < bottomThreshold) {
this._canvasPosition.y = Math.min(topThreshold, bottomBoundary);
hasUpdated = true;
} else if (y > topBoundary && y > topThreshold) {
this._canvasPosition.y = Math.max(bottomThreshold, topBoundary);
hasUpdated = true;
}
if (x < rightBoundary && x < rightThreshold) {
this._canvasPosition.x = Math.min(leftThreshold, rightBoundary);
hasUpdated = true;
} else if (x > leftBoundary && x > leftThreshold) {
this._canvasPosition.x = Math.max(rightThreshold, leftBoundary);
hasUpdated = true;
}
return hasUpdated;
}
/**
* Updates the <canvas> element's style in accordance with the current window's
* devicePixelRatio, and the position calculated in `calculateCanvasPosition`; it also
* clears the drawing context.
*/
updateCanvasElement() {
let size = CANVAS_SIZE / this.win.devicePixelRatio;
let { x, y } = this._canvasPosition;
// Resize the canvas taking the dpr into account so as to have crisp lines, and
// translating it to give the perception that it always covers the viewport.
this.canvas.setAttribute("style",
`width:${size}px;height:${size}px; transform: translate(${x}px, ${y}px);`);
this.ctx.clearRect(0, 0, CANVAS_SIZE, CANVAS_SIZE);
}
/**
* Updates the current matrices for both canvas drawing and SVG, taking in account the
* following transformations, in this order:
* 1. The scale given by the display pixel ratio.
* 2. The translation to the top left corner of the element.
* 3. The scale given by the current zoom.
* 4. The translation given by the top and left padding of the element.
* 5. Any CSS transformation applied directly to the element (only 2D
* transformation; the 3D transformation are flattened, see `dom-matrix-2d` module
* for further details.)
*
* The transformations of the element's ancestors are not currently computed (see
* bug 1355675).
*/
updateCurrentMatrix() {
let computedStyle = this.currentNode.ownerGlobal.getComputedStyle(this.currentNode);
let paddingTop = parseFloat(computedStyle.paddingTop);
let paddingLeft = parseFloat(computedStyle.paddingLeft);
let borderTop = parseFloat(computedStyle.borderTopWidth);
let borderLeft = parseFloat(computedStyle.borderLeftWidth);
let nodeMatrix = getNodeTransformationMatrix(this.currentNode,
this.win.document.documentElement);
let m = identity();
// First, we scale based on the device pixel ratio.
m = multiply(m, scale(this.win.devicePixelRatio));
// Then, we apply the current node's transformation matrix, relative to the
// inspected window's root element, but only if it's not a identity matrix.
if (isIdentity(nodeMatrix)) {
this.hasNodeTransformations = false;
} else {
m = multiply(m, nodeMatrix);
this.hasNodeTransformations = true;
}
// Finally, we translate the origin based on the node's padding and border values.
m = multiply(m, translate(paddingLeft + borderLeft, paddingTop + borderTop));
this.currentMatrix = m;
}
getFirstRowLinePos(fragment) {
return fragment.rows.lines[0].start;
}

View File

@ -0,0 +1,199 @@
/* 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 { AutoRefreshHighlighter } = require("./auto-refresh");
const {
CANVAS_SIZE,
getCanvasPosition,
getCurrentMatrix,
updateCanvasElement,
} = require("./utils/canvas");
const {
CanvasFrameAnonymousContentHelper,
createNode,
} = require("./utils/markup");
const {
setIgnoreLayoutChanges,
} = require("devtools/shared/layout/utils");
class FlexboxHighlighter extends AutoRefreshHighlighter {
constructor(highlighterEnv) {
super(highlighterEnv);
this.ID_CLASS_PREFIX = "flexbox-";
this.markup = new CanvasFrameAnonymousContentHelper(this.highlighterEnv,
this._buildMarkup.bind(this));
this.onPageHide = this.onPageHide.bind(this);
this.onWillNavigate = this.onWillNavigate.bind(this);
this.highlighterEnv.on("will-navigate", this.onWillNavigate);
let { pageListenerTarget } = highlighterEnv;
pageListenerTarget.addEventListener("pagehide", this.onPageHide);
// Initialize the <canvas> position to the top left corner of the page
this._canvasPosition = {
x: 0,
y: 0
};
// Calling `getCanvasPosition` anyway since the highlighter could be initialized
// on a page that has scrolled already.
let { canvasX, canvasY } = getCanvasPosition(this._canvasPosition, this._scroll,
this.win, this._winDimensions);
this._canvasPosition.x = canvasX;
this._canvasPosition.y = canvasY;
}
_buildMarkup() {
let container = createNode(this.win, {
attributes: {
"class": "highlighter-container"
}
});
let root = createNode(this.win, {
parent: container,
attributes: {
"id": "root",
"class": "root"
},
prefix: this.ID_CLASS_PREFIX
});
// We use a <canvas> element because there is an arbitrary number of items and texts
// to draw which wouldn't be possible with HTML or SVG without having to insert and
// remove the whole markup on every update.
createNode(this.win, {
parent: root,
nodeType: "canvas",
attributes: {
"id": "canvas",
"class": "canvas",
"hidden": "true",
"width": CANVAS_SIZE,
"height": CANVAS_SIZE
},
prefix: this.ID_CLASS_PREFIX
});
return container;
}
destroy() {
let { highlighterEnv } = this;
highlighterEnv.off("will-navigate", this.onWillNavigate);
let { pageListenerTarget } = highlighterEnv;
if (pageListenerTarget) {
pageListenerTarget.removeEventListener("pagehide", this.onPageHide);
}
this.markup.destroy();
AutoRefreshHighlighter.prototype.destroy.call(this);
}
get canvas() {
return this.getElement("canvas");
}
get ctx() {
return this.canvas.getCanvasContext("2d");
}
getElement(id) {
return this.markup.getElement(this.ID_CLASS_PREFIX + id);
}
_hide() {
setIgnoreLayoutChanges(true);
this._hideFlexbox();
setIgnoreLayoutChanges(false, this.highlighterEnv.document.documentElement);
}
_hideFlexbox() {
this.getElement("canvas").setAttribute("hidden", "true");
}
/**
* The <canvas>'s position needs to be updated if the page scrolls too much, in order
* to give the illusion that it always covers the viewport.
*/
_scrollUpdate() {
let { hasUpdated } = getCanvasPosition(this._canvasPosition, this._scroll, this.win,
this._winDimensions);
if (hasUpdated) {
this._update();
}
}
_show() {
this._hide();
return this._update();
}
_showFlexbox() {
this.getElement("canvas").removeAttribute("hidden");
}
/**
* If a page hide event is triggered for current window's highlighter, hide the
* highlighter.
*/
onPageHide({ target }) {
if (target.defaultView === this.win) {
this.hide();
}
}
/**
* Called when the page will-navigate. Used to hide the flexbox highlighter and clear
* the cached gap patterns and avoid using DeadWrapper obejcts as gap patterns the
* next time.
*/
onWillNavigate({ isTopLevel }) {
if (isTopLevel) {
this.hide();
}
}
_update() {
setIgnoreLayoutChanges(true);
let root = this.getElement("root");
// Hide the root element and force the reflow in order to get the proper window's
// dimensions without increasing them.
root.setAttribute("style", "display: none");
this.win.document.documentElement.offsetWidth;
let { width, height } = this._winDimensions;
// Updates the <canvas> element's position and size.
// It also clear the <canvas>'s drawing context.
updateCanvasElement(this.canvas, this._canvasPosition, this.win.devicePixelRatio);
// Update the current matrix used in our canvas' rendering
let { currentMatrix, hasNodeTransformations } = getCurrentMatrix(this.currentNode,
this.win);
this.currentMatrix = currentMatrix;
this.hasNodeTransformations = hasNodeTransformations;
this._showFlexbox();
root.setAttribute("style",
`position: absolute; width: ${width}px; height: ${height}px; overflow: hidden`);
setIgnoreLayoutChanges(false, this.highlighterEnv.document.documentElement);
return true;
}
}
exports.FlexboxHighlighter = FlexboxHighlighter;

View File

@ -14,6 +14,7 @@ DevToolsModules(
'css-grid.js',
'css-transform.js',
'eye-dropper.js',
'flexbox.js',
'geometry-editor.js',
'measuring-tool.js',
'paused-debugger.js',

View File

@ -0,0 +1,440 @@
/* 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 {
apply,
getNodeTransformationMatrix,
identity,
isIdentity,
multiply,
scale,
translate,
} = require("devtools/shared/layout/dom-matrix-2d");
const { getViewportDimensions } = require("devtools/shared/layout/utils");
// A set of utility functions for highlighters that render their content to a <canvas>
// element.
// We create a <canvas> element that has always 4096x4096 physical pixels, to displays
// our grid's overlay.
// Then, we move the element around when needed, to give the perception that it always
// covers the screen (See bug 1345434).
//
// This canvas size value is the safest we can use because most GPUs can handle it.
// It's also far from the maximum canvas memory allocation limit (4096x4096x4 is
// 67.108.864 bytes, where the limit is 500.000.000 bytes, see:
// http://searchfox.org/mozilla-central/source/gfx/thebes/gfxPrefs.h#401).
//
// Note:
// Once bug 1232491 lands, we could try to refactor this code to use the values from
// the displayport API instead.
//
// Using a fixed value should also solve bug 1348293.
const CANVAS_SIZE = 4096;
/**
* Draws an arrow-bubble rectangle in the provided canvas context.
*
* @param {CanvasRenderingContext2D} ctx
* The 2D canvas context.
* @param {Number} x
* The x-axis origin of the rectangle.
* @param {Number} y
* The y-axis origin of the rectangle.
* @param {Number} width
* The width of the rectangle.
* @param {Number} height
* The height of the rectangle.
* @param {Number} radius
* The radius of the rounding.
* @param {Number} margin
* The distance of the origin point from the pointer.
* @param {Number} arrowSize
* The size of the arrow.
* @param {String} alignment
* The alignment of the rectangle in relation to its position to the grid.
*/
function drawBubbleRect(ctx, x, y, width, height, radius, margin, arrowSize, alignment) {
let angle = 0;
if (alignment === "bottom") {
angle = 180;
} else if (alignment === "right") {
angle = 90;
[width, height] = [height, width];
} else if (alignment === "left") {
[width, height] = [height, width];
angle = 270;
}
let originX = x;
let originY = y;
ctx.save();
ctx.translate(originX, originY);
ctx.rotate(angle * (Math.PI / 180));
ctx.translate(-originX, -originY);
ctx.translate(-width / 2, -height - arrowSize - margin);
ctx.beginPath();
ctx.moveTo(x, y + radius);
ctx.lineTo(x, y + height - radius);
ctx.arcTo(x, y + height, x + radius, y + height, radius);
ctx.lineTo(x + width / 2 - arrowSize, y + height);
ctx.lineTo(x + width / 2, y + height + arrowSize);
ctx.lineTo(x + width / 2 + arrowSize, y + height);
ctx.arcTo(x + width, y + height, x + width, y + height - radius, radius);
ctx.lineTo(x + width, y + radius);
ctx.arcTo(x + width, y, x + width - radius, y, radius);
ctx.lineTo(x + radius, y);
ctx.arcTo(x, y, x, y + radius, radius);
ctx.stroke();
ctx.fill();
ctx.restore();
}
/**
* Draws a line to the context given and applies a transformation matrix if passed.
*
* @param {CanvasRenderingContext2D} ctx
* The 2D canvas context.
* @param {Number} x1
* The x-axis of the coordinate for the begin of the line.
* @param {Number} y1
* The y-axis of the coordinate for the begin of the line.
* @param {Number} x2
* The x-axis of the coordinate for the end of the line.
* @param {Number} y2
* The y-axis of the coordinate for the end of the line.
* @param {Object} [options]
* The options object.
* @param {Array} [options.matrix=identity()]
* The transformation matrix to apply.
* @param {Array} [options.extendToBoundaries]
* If set, the line will be extended to reach the boundaries specified.
*/
function drawLine(ctx, x1, y1, x2, y2, options) {
let matrix = options.matrix || identity();
let p1 = apply(matrix, [x1, y1]);
let p2 = apply(matrix, [x2, y2]);
x1 = p1[0];
y1 = p1[1];
x2 = p2[0];
y2 = p2[1];
if (options.extendToBoundaries) {
if (p1[1] === p2[1]) {
x1 = options.extendToBoundaries[0];
x2 = options.extendToBoundaries[2];
} else {
y1 = options.extendToBoundaries[1];
x1 = (p2[0] - p1[0]) * (y1 - p1[1]) / (p2[1] - p1[1]) + p1[0];
y2 = options.extendToBoundaries[3];
x2 = (p2[0] - p1[0]) * (y2 - p1[1]) / (p2[1] - p1[1]) + p1[0];
}
}
ctx.moveTo(Math.round(x1), Math.round(y1));
ctx.lineTo(Math.round(x2), Math.round(y2));
}
/**
* Draws a rect to the context given and applies a transformation matrix if passed.
* The coordinates are the start and end points of the rectangle's diagonal.
*
* @param {CanvasRenderingContext2D} ctx
* The 2D canvas context.
* @param {Number} x1
* The x-axis coordinate of the rectangle's diagonal start point.
* @param {Number} y1
* The y-axis coordinate of the rectangle's diagonal start point.
* @param {Number} x2
* The x-axis coordinate of the rectangle's diagonal end point.
* @param {Number} y2
* The y-axis coordinate of the rectangle's diagonal end point.
* @param {Array} [matrix=identity()]
* The transformation matrix to apply.
*/
function drawRect(ctx, x1, y1, x2, y2, matrix = identity()) {
let p = getPointsFromDiagonal(x1, y1, x2, y2, matrix);
ctx.beginPath();
ctx.moveTo(Math.round(p[0].x), Math.round(p[0].y));
ctx.lineTo(Math.round(p[1].x), Math.round(p[1].y));
ctx.lineTo(Math.round(p[2].x), Math.round(p[2].y));
ctx.lineTo(Math.round(p[3].x), Math.round(p[3].y));
ctx.closePath();
}
/**
* Draws a rounded rectangle in the provided canvas context.
*
* @param {CanvasRenderingContext2D} ctx
* The 2D canvas context.
* @param {Number} x
* The x-axis origin of the rectangle.
* @param {Number} y
* The y-axis origin of the rectangle.
* @param {Number} width
* The width of the rectangle.
* @param {Number} height
* The height of the rectangle.
* @param {Number} radius
* The radius of the rounding.
*/
function drawRoundedRect(ctx, x, y, width, height, radius) {
ctx.beginPath();
ctx.moveTo(x, y + radius);
ctx.lineTo(x, y + height - radius);
ctx.arcTo(x, y + height, x + radius, y + height, radius);
ctx.lineTo(x + width - radius, y + height);
ctx.arcTo(x + width, y + height, x + width, y + height - radius, radius);
ctx.lineTo(x + width, y + radius);
ctx.arcTo(x + width, y, x + width - radius, y, radius);
ctx.lineTo(x + radius, y);
ctx.arcTo(x, y, x, y + radius, radius);
ctx.stroke();
ctx.fill();
}
/**
* Given an array of four points and returns a DOMRect-like object representing the
* boundaries defined by the four points.
*
* @param {Array} points
* An array with 4 pointer objects {x, y} representing the box quads.
* @return {Object} DOMRect-like object of the 4 points.
*/
function getBoundsFromPoints(points) {
let bounds = {};
bounds.left = Math.min(points[0].x, points[1].x, points[2].x, points[3].x);
bounds.right = Math.max(points[0].x, points[1].x, points[2].x, points[3].x);
bounds.top = Math.min(points[0].y, points[1].y, points[2].y, points[3].y);
bounds.bottom = Math.max(points[0].y, points[1].y, points[2].y, points[3].y);
bounds.x = bounds.left;
bounds.y = bounds.top;
bounds.width = bounds.right - bounds.left;
bounds.height = bounds.bottom - bounds.top;
return bounds;
}
/**
* Calculates and returns the <canvas>'s position in accordance with the page's scroll,
* document's size, canvas size, and viewport's size. This is called when a page's scroll
* is detected.
*
* @param {Object} canvasPosition
* A pointer object {x, y} representing the <canvas> position to the top left
* corner of the page.
* @param {Object} scrollPosition
* A pointer object {x, y} representing the window's pageXOffset and pageYOffset.
* @param {Window} window
* The window object.
* @param {Object} windowDimensions
* An object {width, height} representing the window's dimensions for the
* `window` given.
* @return {Object} An object with the following properties:
* - {Boolean} hasUpdated
* true if the <canvas> position was updated and false otherwise.
* - {Number} canvasX
* The canvas' x position.
* - {Number} canvasY
* The canvas' y position.
*/
function getCanvasPosition(canvasPosition, scrollPosition, window, windowDimensions) {
let { x: canvasX, y: canvasY } = canvasPosition;
let { x: scrollX, y: scrollY } = scrollPosition;
let cssCanvasSize = CANVAS_SIZE / window.devicePixelRatio;
let viewportSize = getViewportDimensions(window);
let { height, width } = windowDimensions;
let canvasWidth = cssCanvasSize;
let canvasHeight = cssCanvasSize;
let hasUpdated = false;
// Those values indicates the relative horizontal and vertical space the page can
// scroll before we have to reposition the <canvas>; they're 1/4 of the delta between
// the canvas' size and the viewport's size: that's because we want to consider both
// sides (top/bottom, left/right; so 1/2 for each side) and also we don't want to
// shown the edges of the canvas in case of fast scrolling (to avoid showing undraw
// areas, therefore another 1/2 here).
let bufferSizeX = (canvasWidth - viewportSize.width) >> 2;
let bufferSizeY = (canvasHeight - viewportSize.height) >> 2;
// Defines the boundaries for the canvas.
let leftBoundary = 0;
let rightBoundary = width - canvasWidth;
let topBoundary = 0;
let bottomBoundary = height - canvasHeight;
// Defines the thresholds that triggers the canvas' position to be updated.
let leftThreshold = scrollX - bufferSizeX;
let rightThreshold = scrollX - canvasWidth + viewportSize.width + bufferSizeX;
let topThreshold = scrollY - bufferSizeY;
let bottomThreshold = scrollY - canvasHeight + viewportSize.height + bufferSizeY;
if (canvasX < rightBoundary && canvasX < rightThreshold) {
canvasX = Math.min(leftThreshold, rightBoundary);
hasUpdated = true;
} else if (canvasX > leftBoundary && canvasX > leftThreshold) {
canvasX = Math.max(rightThreshold, leftBoundary);
hasUpdated = true;
}
if (canvasY < bottomBoundary && canvasY < bottomThreshold) {
canvasY = Math.min(topThreshold, bottomBoundary);
hasUpdated = true;
} else if (canvasY > topBoundary && canvasY > topThreshold) {
canvasY = Math.max(bottomThreshold, topBoundary);
hasUpdated = true;
}
return { canvasX, canvasY, hasUpdated };
}
/**
* Returns the current matrices for both canvas drawing and SVG taking into account the
* following transformations, in this order:
* 1. The scale given by the display pixel ratio.
* 2. The translation to the top left corner of the element.
* 3. The scale given by the current zoom.
* 4. The translation given by the top and left padding of the element.
* 5. Any CSS transformation applied directly to the element (only 2D
* transformation; the 3D transformation are flattened, see `dom-matrix-2d` module
* for further details.)
*
* The transformations of the element's ancestors are not currently computed (see
* bug 1355675).
*
* @param {Element} element
* The current element.
* @param {Window} window
* The window object.
* @return {Object} An object with the following properties:
* - {Array} currentMatrix
* The current matrix.
* - {Boolean} hasNodeTransformations
* true if the node has transformed and false otherwise.
*/
function getCurrentMatrix(element, window) {
let computedStyle = element.ownerGlobal.getComputedStyle(element);
let paddingTop = parseFloat(computedStyle.paddingTop);
let paddingLeft = parseFloat(computedStyle.paddingLeft);
let borderTop = parseFloat(computedStyle.borderTopWidth);
let borderLeft = parseFloat(computedStyle.borderLeftWidth);
let nodeMatrix = getNodeTransformationMatrix(element, window.document.documentElement);
let currentMatrix = identity();
let hasNodeTransformations = false;
// First, we scale based on the device pixel ratio.
currentMatrix = multiply(currentMatrix, scale(window.devicePixelRatio));
// Then, we apply the current node's transformation matrix, relative to the
// inspected window's root element, but only if it's not a identity matrix.
if (isIdentity(nodeMatrix)) {
hasNodeTransformations = false;
} else {
currentMatrix = multiply(currentMatrix, nodeMatrix);
hasNodeTransformations = true;
}
// Finally, we translate the origin based on the node's padding and border values.
currentMatrix = multiply(currentMatrix,
translate(paddingLeft + borderLeft, paddingTop + borderTop));
return { currentMatrix, hasNodeTransformations };
}
/**
* Given an array of four points, returns a string represent a path description.
*
* @param {Array} points
* An array with 4 pointer objects {x, y} representing the box quads.
* @return {String} a Path Description that can be used in svg's <path> element.
*/
function getPathDescriptionFromPoints(points) {
return "M" + points[0].x + "," + points[0].y + " " +
"L" + points[1].x + "," + points[1].y + " " +
"L" + points[2].x + "," + points[2].y + " " +
"L" + points[3].x + "," + points[3].y;
}
/**
* Given the rectangle's diagonal start and end coordinates, returns an array containing
* the four coordinates of a rectangle. If a matrix is provided, applies the matrix
* function to each of the coordinates' value.
*
* @param {Number} x1
* The x-axis coordinate of the rectangle's diagonal start point.
* @param {Number} y1
* The y-axis coordinate of the rectangle's diagonal start point.
* @param {Number} x2
* The x-axis coordinate of the rectangle's diagonal end point.
* @param {Number} y2
* The y-axis coordinate of the rectangle's diagonal end point.
* @param {Array} [matrix=identity()]
* A transformation matrix to apply.
* @return {Array} the four coordinate points of the given rectangle transformed by the
* matrix given.
*/
function getPointsFromDiagonal(x1, y1, x2, y2, matrix = identity()) {
return [
[x1, y1],
[x2, y1],
[x2, y2],
[x1, y2]
].map(point => {
let transformedPoint = apply(matrix, point);
return { x: transformedPoint[0], y: transformedPoint[1] };
});
}
/**
* Updates the <canvas> element's style in accordance with the current window's
* devicePixelRatio, and the position calculated in `getCanvasPosition`. It also
* clears the drawing context. This is called on canvas update after a scroll event where
* `getCanvasPosition` updates the new canvasPosition.
*
* @param {Canvas} canvas
* The <canvas> element.
* @param {Object} canvasPosition
* A pointer object {x, y} representing the <canvas> position to the top left
* corner of the page.
* @param {Number} devicePixelRatio
* The device pixel ratio.
*/
function updateCanvasElement(canvas, canvasPosition, devicePixelRatio) {
let { x, y } = canvasPosition;
let size = CANVAS_SIZE / devicePixelRatio;
// Resize the canvas taking the dpr into account so as to have crisp lines, and
// translating it to give the perception that it always covers the viewport.
canvas.setAttribute("style",
`width: ${size}px; height: ${size}px; transform: translate(${x}px, ${y}px);`);
canvas.getCanvasContext("2d").clearRect(0, 0, CANVAS_SIZE, CANVAS_SIZE);
}
exports.CANVAS_SIZE = CANVAS_SIZE;
exports.drawBubbleRect = drawBubbleRect;
exports.drawLine = drawLine;
exports.drawRect = drawRect;
exports.drawRoundedRect = drawRoundedRect;
exports.getBoundsFromPoints = getBoundsFromPoints;
exports.getCanvasPosition = getCanvasPosition;
exports.getCurrentMatrix = getCurrentMatrix;
exports.getPathDescriptionFromPoints = getPathDescriptionFromPoints;
exports.getPointsFromDiagonal = getPointsFromDiagonal;
exports.updateCanvasElement = updateCanvasElement;

View File

@ -5,5 +5,6 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DevToolsModules(
'canvas.js',
'markup.js'
)

View File

@ -0,0 +1,31 @@
<html xmlns="http://www.w3.org/1999/xhtml">
<script>
<![CDATA[
function boom()
{
var p = document.body;
var t1 = document.createTextNode("1");
p.appendChild(t1);
var t2 = document.createTextNode("2");
p.appendChild(t2);
var t3 = document.createTextNode("3");
p.appendChild(t3);
function f() {
};
window.addEventListener("DOMSubtreeModified", f, false);
function g() {
window.removeEventListener("DOMNodeRemoved", g, false);
p.removeChild(t3);
};
window.addEventListener("DOMNodeRemoved", g, false);
document.normalize();
}
]]>
</script>
<body onload="boom();"></body>
</html>

View File

@ -126,6 +126,7 @@ load 646184.html
load 658845-1.svg
load 666869.html
load 667336-1.html
load 675516.xhtml
load 675621-1.html
load 677194.html
load 679459.html

View File

@ -346,7 +346,9 @@ BroadcastChannel::Constructor(const GlobalObject& aGlobal,
// Register this component to PBackground.
PBackgroundChild* actorChild = BackgroundChild::GetOrCreateForCurrentThread();
if (NS_WARN_IF(!actorChild)) {
MOZ_CRASH("Failed to create a PBackgroundChild actor!");
// Firefox is probably shutting down. Let's return a 'generic' error.
aRv.Throw(NS_ERROR_FAILURE);
return nullptr;
}
PBroadcastChannelChild* actor =

View File

@ -466,8 +466,9 @@ MutableBlobStorage::Append(const void* aData, uint32_t aLength)
// If eInMemory is the current Storage state, we could maybe migrate to
// a temporary file.
if (mStorageState == eInMemory && ShouldBeTemporaryStorage(aLength)) {
MaybeCreateTemporaryFile();
if (mStorageState == eInMemory && ShouldBeTemporaryStorage(aLength) &&
!MaybeCreateTemporaryFile()) {
return NS_ERROR_FAILURE;
}
// If we are already in the temporaryFile mode, we have to dispatch a
@ -547,7 +548,7 @@ MutableBlobStorage::ShouldBeTemporaryStorage(uint64_t aSize) const
return bufferSize.value() >= mMaxMemory;
}
void
bool
MutableBlobStorage::MaybeCreateTemporaryFile()
{
MOZ_ASSERT(NS_IsMainThread());
@ -557,7 +558,7 @@ MutableBlobStorage::MaybeCreateTemporaryFile()
mozilla::ipc::PBackgroundChild* actorChild =
mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread();
if (NS_WARN_IF(!actorChild)) {
MOZ_CRASH("Failed to create a PBackgroundChild actor!");
return false;
}
mActor = new TemporaryIPCBlobChild(this);
@ -569,6 +570,8 @@ MutableBlobStorage::MaybeCreateTemporaryFile()
mActor.get()->AddRef();
// The actor will call us when the FileDescriptor is received.
return true;
}
void

View File

@ -83,7 +83,7 @@ private:
bool ShouldBeTemporaryStorage(uint64_t aSize) const;
void MaybeCreateTemporaryFile();
bool MaybeCreateTemporaryFile();
void DispatchToIOThread(already_AddRefed<nsIRunnable> aRunnable);

View File

@ -140,7 +140,8 @@ FileSystemTaskChildBase::Start()
mozilla::ipc::PBackgroundChild* actor =
mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread();
if (NS_WARN_IF(!actor)) {
MOZ_CRASH("Failed to create a PBackgroundChild actor!");
// We are probably shutting down.
return;
}
nsAutoString serialization;

View File

@ -142,14 +142,15 @@ GamepadManager::AddListener(nsGlobalWindow* aWindow)
if (mChannelChildren.IsEmpty()) {
PBackgroundChild* actor = BackgroundChild::GetOrCreateForCurrentThread();
if (NS_WARN_IF(!actor)) {
MOZ_CRASH("Failed to create a PBackgroundChild actor!");
// We are probably shutting down.
return;
}
GamepadEventChannelChild *child = new GamepadEventChannelChild();
PGamepadEventChannelChild *initedChild =
actor->SendPGamepadEventChannelConstructor(child);
if (NS_WARN_IF(!initedChild)) {
MOZ_CRASH("Failed to create a PBackgroundChild actor!");
// We are probably shutting down.
return;
}

View File

@ -1141,7 +1141,8 @@ ContentChild::InitXPCOM(const XPCOMInitData& aXPCOMInit,
PBackgroundChild* actorChild = BackgroundChild::GetOrCreateForCurrentThread();
if (NS_WARN_IF(!actorChild)) {
MOZ_CRASH("Failed to create PBackgroundChild!");
MOZ_ASSERT_UNREACHABLE("PBackground init can't fail at this point");
return;
}
nsCOMPtr<nsIConsoleService> svc(do_GetService(NS_CONSOLESERVICE_CONTRACTID));

View File

@ -311,7 +311,10 @@ MessagePort::Initialize(const nsID& aUUID,
}
if (mState == eStateEntangling) {
ConnectToPBackground();
if (!ConnectToPBackground()) {
aRv.Throw(NS_ERROR_FAILURE);
return;
}
} else {
MOZ_ASSERT(mState == eStateUnshippedEntangled);
}
@ -781,7 +784,11 @@ MessagePort::CloneAndDisentangle(MessagePortIdentifier& aIdentifier)
MOZ_ASSERT(mMessagesForTheOtherPort.IsEmpty());
// Disconnect the entangled port and connect it to PBackground.
mUnshippedEntangledPort->ConnectToPBackground();
if (!mUnshippedEntangledPort->ConnectToPBackground()) {
// We are probably shutting down. We cannot proceed.
return;
}
mUnshippedEntangledPort = nullptr;
// In this case, we don't need to be connected to the PBackground service.
@ -794,7 +801,10 @@ MessagePort::CloneAndDisentangle(MessagePortIdentifier& aIdentifier)
}
// Register this component to PBackground.
ConnectToPBackground();
if (!ConnectToPBackground()) {
// We are probably shutting down. We cannot proceed.
return;
}
mState = eStateEntanglingForDisentangle;
return;
@ -827,7 +837,7 @@ MessagePort::Closed()
UpdateMustKeepAlive();
}
void
bool
MessagePort::ConnectToPBackground()
{
mState = eStateEntangling;
@ -835,18 +845,22 @@ MessagePort::ConnectToPBackground()
mozilla::ipc::PBackgroundChild* actorChild =
mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread();
if (NS_WARN_IF(!actorChild)) {
MOZ_CRASH("Failed to create a PBackgroundChild actor!");
return false;
}
PMessagePortChild* actor =
actorChild->SendPMessagePortConstructor(mIdentifier->uuid(),
mIdentifier->destinationUuid(),
mIdentifier->sequenceId());
if (NS_WARN_IF(!actor)) {
return false;
}
mActor = static_cast<MessagePortChild*>(actor);
MOZ_ASSERT(mActor);
mActor->SetPort(this);
return true;
}
void

View File

@ -141,7 +141,7 @@ private:
uint32_t aSequenceID, bool mNeutered, State aState,
ErrorResult& aRv);
void ConnectToPBackground();
bool ConnectToPBackground();
// Dispatch events from the Message Queue using a nsRunnable.
void Dispatch();

View File

@ -226,7 +226,11 @@ OpenFileNameIPC::AddToOfn(LPOPENFILENAMEW aLpofn) const
aLpofn->nMaxCustFilter = 0;
}
aLpofn->nFilterIndex = mFilterIndex;
wcscpy(aLpofn->lpstrFile, mFile.c_str());
if (mNMaxFile > 0) {
wcsncpy(aLpofn->lpstrFile, mFile.c_str(),
std::min(static_cast<uint32_t>(mFile.size()+1), mNMaxFile));
aLpofn->lpstrFile[mNMaxFile - 1] = L'\0';
}
aLpofn->nMaxFile = mNMaxFile;
aLpofn->nMaxFileTitle = mNMaxFileTitle;
if (mHasInitialDir) {
@ -252,7 +256,7 @@ OpenFileNameIPC::AllocateOfnStrings(LPOPENFILENAMEW aLpofn) const
}
if (mHasCustomFilter) {
aLpofn->lpstrCustomFilter =
static_cast<LPTSTR>(moz_xmalloc(sizeof(wchar_t) * (mCustomFilterIn.size() + 1) + mNMaxCustFilterOut));
static_cast<LPTSTR>(moz_xmalloc(sizeof(wchar_t) * (mCustomFilterIn.size() + 1 + mNMaxCustFilterOut)));
}
aLpofn->lpstrFile =
static_cast<LPTSTR>(moz_xmalloc(sizeof(wchar_t) * mNMaxFile));

View File

@ -104,12 +104,6 @@ U2FManager::ClearTransaction()
}
mTransaction.reset();
if (mChild) {
RefPtr<U2FTransactionChild> c;
mChild.swap(c);
c->Send__delete__(c);
}
}
void
@ -126,8 +120,8 @@ U2FManager::RejectTransaction(const nsresult& aError)
void
U2FManager::CancelTransaction(const nsresult& aError)
{
if (mChild) {
mChild->SendRequestCancel();
if (!NS_WARN_IF(!mChild || mTransaction.isNothing())) {
mChild->SendRequestCancel(mTransaction.ref().mId);
}
RejectTransaction(aError);
@ -140,20 +134,26 @@ U2FManager::~U2FManager()
if (mTransaction.isSome()) {
RejectTransaction(NS_ERROR_ABORT);
}
if (mChild) {
RefPtr<U2FTransactionChild> c;
mChild.swap(c);
c->Send__delete__(c);
}
}
void
U2FManager::GetOrCreateBackgroundActor()
bool
U2FManager::MaybeCreateBackgroundActor()
{
MOZ_ASSERT(NS_IsMainThread());
if (mChild) {
return;
return true;
}
PBackgroundChild* actorChild = BackgroundChild::GetOrCreateForCurrentThread();
if (NS_WARN_IF(!actorChild)) {
MOZ_CRASH("Failed to create a PBackgroundChild actor!");
return false;
}
RefPtr<U2FTransactionChild> mgr(new U2FTransactionChild());
@ -161,11 +161,13 @@ U2FManager::GetOrCreateBackgroundActor()
actorChild->SendPWebAuthnTransactionConstructor(mgr);
if (NS_WARN_IF(!constructedMgr)) {
return;
return false;
}
MOZ_ASSERT(constructedMgr == mgr);
mChild = mgr.forget();
return true;
}
//static
@ -263,6 +265,10 @@ U2FManager::Register(nsPIDOMWindowInner* aParent, const nsCString& aRpId,
return U2FPromise::CreateAndReject(ErrorCode::OTHER_ERROR, __func__).forget();
}
if (!MaybeCreateBackgroundActor()) {
return U2FPromise::CreateAndReject(ErrorCode::OTHER_ERROR, __func__).forget();
}
ListenForVisibilityEvents(aParent, this);
// Always blank for U2F
@ -275,15 +281,9 @@ U2FManager::Register(nsPIDOMWindowInner* aParent, const nsCString& aRpId,
extensions);
MOZ_ASSERT(mTransaction.isNothing());
mTransaction = Some(U2FTransaction(aParent, Move(info), aClientDataJSON));
GetOrCreateBackgroundActor();
if (mChild) {
mChild->SendRequestRegister(mTransaction.ref().mInfo);
}
mChild->SendRequestRegister(mTransaction.ref().mId, mTransaction.ref().mInfo);
return mTransaction.ref().mPromise.Ensure(__func__);
}
@ -307,6 +307,10 @@ U2FManager::Sign(nsPIDOMWindowInner* aParent,
return U2FPromise::CreateAndReject(ErrorCode::OTHER_ERROR, __func__).forget();
}
if (!MaybeCreateBackgroundActor()) {
return U2FPromise::CreateAndReject(ErrorCode::OTHER_ERROR, __func__).forget();
}
ListenForVisibilityEvents(aParent, this);
// Always blank for U2F
@ -321,22 +325,18 @@ U2FManager::Sign(nsPIDOMWindowInner* aParent,
MOZ_ASSERT(mTransaction.isNothing());
mTransaction = Some(U2FTransaction(aParent, Move(info), aClientDataJSON));
GetOrCreateBackgroundActor();
if (mChild) {
mChild->SendRequestSign(mTransaction.ref().mInfo);
}
mChild->SendRequestSign(mTransaction.ref().mId, mTransaction.ref().mInfo);
return mTransaction.ref().mPromise.Ensure(__func__);
}
void
U2FManager::FinishRegister(nsTArray<uint8_t>& aRegBuffer)
U2FManager::FinishRegister(const uint64_t& aTransactionId,
nsTArray<uint8_t>& aRegBuffer)
{
MOZ_ASSERT(NS_IsMainThread());
// Check for a valid transaction.
if (mTransaction.isNothing()) {
if (mTransaction.isNothing() || mTransaction.ref().mId != aTransactionId) {
return;
}
@ -381,13 +381,14 @@ U2FManager::FinishRegister(nsTArray<uint8_t>& aRegBuffer)
}
void
U2FManager::FinishSign(nsTArray<uint8_t>& aCredentialId,
U2FManager::FinishSign(const uint64_t& aTransactionId,
nsTArray<uint8_t>& aCredentialId,
nsTArray<uint8_t>& aSigBuffer)
{
MOZ_ASSERT(NS_IsMainThread());
// Check for a valid transaction.
if (mTransaction.isNothing()) {
if (mTransaction.isNothing() || mTransaction.ref().mId != aTransactionId) {
return;
}
@ -440,11 +441,12 @@ U2FManager::FinishSign(nsTArray<uint8_t>& aCredentialId,
}
void
U2FManager::RequestAborted(const nsresult& aError)
U2FManager::RequestAborted(const uint64_t& aTransactionId,
const nsresult& aError)
{
MOZ_ASSERT(NS_IsMainThread());
if (mTransaction.isSome()) {
if (mTransaction.isSome() && mTransaction.ref().mId == aTransactionId) {
RejectTransaction(aError);
}
}
@ -454,7 +456,6 @@ U2FManager::HandleEvent(nsIDOMEvent* aEvent)
{
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(aEvent);
MOZ_ASSERT(mChild);
nsAutoString type;
aEvent->GetType(type);
@ -464,9 +465,11 @@ U2FManager::HandleEvent(nsIDOMEvent* aEvent)
nsCOMPtr<nsIDocument> doc =
do_QueryInterface(aEvent->InternalDOMEvent()->GetTarget());
MOZ_ASSERT(doc);
if (NS_WARN_IF(!doc)) {
return NS_ERROR_FAILURE;
}
if (doc && doc->Hidden()) {
if (doc->Hidden()) {
MOZ_LOG(gU2FManagerLog, LogLevel::Debug,
("Visibility change: U2F window is hidden, cancelling job."));
@ -479,6 +482,7 @@ U2FManager::HandleEvent(nsIDOMEvent* aEvent)
void
U2FManager::ActorDestroyed()
{
MOZ_ASSERT(NS_IsMainThread());
mChild = nullptr;
}

View File

@ -61,7 +61,10 @@ public:
: mParent(aParent)
, mInfo(aInfo)
, mClientData(aClientData)
{ }
, mId(NextId())
{
MOZ_ASSERT(mId > 0);
}
// Parent of the context we're running the transaction in.
nsCOMPtr<nsPIDOMWindowInner> mParent;
@ -75,6 +78,18 @@ public:
// Client data used to assemble reply objects.
nsCString mClientData;
// Unique transaction id.
uint64_t mId;
private:
// Generates a unique id for new transactions. This doesn't have to be unique
// forever, it's sufficient to differentiate between temporally close
// transactions, where messages can intersect. Can overflow.
static uint64_t NextId() {
static uint64_t id = 0;
return ++id;
}
};
class U2FManager final : public nsIDOMEventListener
@ -97,10 +112,12 @@ public:
const uint32_t& aTimeoutMillis,
const nsTArray<WebAuthnScopedCredentialDescriptor>& aKeyList);
void FinishRegister(nsTArray<uint8_t>& aRegBuffer);
void FinishSign(nsTArray<uint8_t>& aCredentialId,
void FinishRegister(const uint64_t& aTransactionId,
nsTArray<uint8_t>& aRegBuffer);
void FinishSign(const uint64_t& aTransactionId,
nsTArray<uint8_t>& aCredentialId,
nsTArray<uint8_t>& aSigBuffer);
void RequestAborted(const nsresult& aError);
void RequestAborted(const uint64_t& aTransactionId, const nsresult& aError);
// XXX This is exposed only until we fix bug 1410346.
void MaybeCancelTransaction(const nsresult& aError) {
@ -129,11 +146,9 @@ private:
// parent) and rejects it by calling RejectTransaction().
void CancelTransaction(const nsresult& aError);
typedef MozPromise<nsresult, nsresult, false> BackgroundActorPromise;
bool MaybeCreateBackgroundActor();
void GetOrCreateBackgroundActor();
// IPC Channel for the current transaction.
// IPC Channel to the parent process.
RefPtr<U2FTransactionChild> mChild;
// The current transaction, if any.

View File

@ -18,30 +18,33 @@ U2FTransactionChild::U2FTransactionChild()
}
mozilla::ipc::IPCResult
U2FTransactionChild::RecvConfirmRegister(nsTArray<uint8_t>&& aRegBuffer)
U2FTransactionChild::RecvConfirmRegister(const uint64_t& aTransactionId,
nsTArray<uint8_t>&& aRegBuffer)
{
RefPtr<U2FManager> mgr = U2FManager::Get();
MOZ_ASSERT(mgr);
mgr->FinishRegister(aRegBuffer);
mgr->FinishRegister(aTransactionId, aRegBuffer);
return IPC_OK();
}
mozilla::ipc::IPCResult
U2FTransactionChild::RecvConfirmSign(nsTArray<uint8_t>&& aCredentialId,
U2FTransactionChild::RecvConfirmSign(const uint64_t& aTransactionId,
nsTArray<uint8_t>&& aCredentialId,
nsTArray<uint8_t>&& aBuffer)
{
RefPtr<U2FManager> mgr = U2FManager::Get();
MOZ_ASSERT(mgr);
mgr->FinishSign(aCredentialId, aBuffer);
mgr->FinishSign(aTransactionId, aCredentialId, aBuffer);
return IPC_OK();
}
mozilla::ipc::IPCResult
U2FTransactionChild::RecvAbort(const nsresult& aError)
U2FTransactionChild::RecvAbort(const uint64_t& aTransactionId,
const nsresult& aError)
{
RefPtr<U2FManager> mgr = U2FManager::Get();
MOZ_ASSERT(mgr);
mgr->RequestAborted(aError);
mgr->RequestAborted(aTransactionId, aError);
return IPC_OK();
}

View File

@ -23,11 +23,21 @@ class U2FTransactionChild final : public PWebAuthnTransactionChild
public:
NS_INLINE_DECL_REFCOUNTING(U2FTransactionChild);
U2FTransactionChild();
mozilla::ipc::IPCResult RecvConfirmRegister(nsTArray<uint8_t>&& aRegBuffer) override;
mozilla::ipc::IPCResult RecvConfirmSign(nsTArray<uint8_t>&& aCredentialId,
nsTArray<uint8_t>&& aBuffer) override;
mozilla::ipc::IPCResult RecvAbort(const nsresult& aError) override;
mozilla::ipc::IPCResult
RecvConfirmRegister(const uint64_t& aTransactionId,
nsTArray<uint8_t>&& aRegBuffer) override;
mozilla::ipc::IPCResult
RecvConfirmSign(const uint64_t& aTransactionId,
nsTArray<uint8_t>&& aCredentialId,
nsTArray<uint8_t>&& aBuffer) override;
mozilla::ipc::IPCResult
RecvAbort(const uint64_t& aTransactionId, const nsresult& aError) override;
void ActorDestroy(ActorDestroyReason why) override;
private:
~U2FTransactionChild() = default;
};

View File

@ -12,29 +12,31 @@ namespace mozilla {
namespace dom {
mozilla::ipc::IPCResult
U2FTransactionParent::RecvRequestRegister(const WebAuthnTransactionInfo& aTransactionInfo)
U2FTransactionParent::RecvRequestRegister(const uint64_t& aTransactionId,
const WebAuthnTransactionInfo& aTransactionInfo)
{
AssertIsOnBackgroundThread();
U2FTokenManager* mgr = U2FTokenManager::Get();
mgr->Register(this, aTransactionInfo);
mgr->Register(this, aTransactionId, aTransactionInfo);
return IPC_OK();
}
mozilla::ipc::IPCResult
U2FTransactionParent::RecvRequestSign(const WebAuthnTransactionInfo& aTransactionInfo)
U2FTransactionParent::RecvRequestSign(const uint64_t& aTransactionId,
const WebAuthnTransactionInfo& aTransactionInfo)
{
AssertIsOnBackgroundThread();
U2FTokenManager* mgr = U2FTokenManager::Get();
mgr->Sign(this, aTransactionInfo);
mgr->Sign(this, aTransactionId, aTransactionInfo);
return IPC_OK();
}
mozilla::ipc::IPCResult
U2FTransactionParent::RecvRequestCancel()
U2FTransactionParent::RecvRequestCancel(const uint64_t& aTransactionId)
{
AssertIsOnBackgroundThread();
U2FTokenManager* mgr = U2FTokenManager::Get();
mgr->Cancel(this);
mgr->Cancel(this, aTransactionId);
return IPC_OK();
}

View File

@ -23,12 +23,20 @@ class U2FTransactionParent final : public PWebAuthnTransactionParent
public:
NS_INLINE_DECL_REFCOUNTING(U2FTransactionParent);
U2FTransactionParent() = default;
virtual mozilla::ipc::IPCResult
RecvRequestRegister(const WebAuthnTransactionInfo& aTransactionInfo) override;
RecvRequestRegister(const uint64_t& aTransactionId,
const WebAuthnTransactionInfo& aTransactionInfo) override;
virtual mozilla::ipc::IPCResult
RecvRequestSign(const WebAuthnTransactionInfo& aTransactionInfo) override;
virtual mozilla::ipc::IPCResult RecvRequestCancel() override;
RecvRequestSign(const uint64_t& aTransactionId,
const WebAuthnTransactionInfo& aTransactionInfo) override;
virtual mozilla::ipc::IPCResult
RecvRequestCancel(const uint64_t& aTransactionId) override;
virtual void ActorDestroy(ActorDestroyReason aWhy) override;
private:
~U2FTransactionParent() = default;
};

View File

@ -39,13 +39,13 @@ async protocol PWebAuthnTransaction {
manager PBackground;
parent:
async __delete__();
async RequestRegister(WebAuthnTransactionInfo aTransactionInfo);
async RequestSign(WebAuthnTransactionInfo aTransactionInfo);
async RequestCancel();
async RequestRegister(uint64_t aTransactionId, WebAuthnTransactionInfo aTransactionInfo);
async RequestSign(uint64_t aTransactionId, WebAuthnTransactionInfo aTransactionInfo);
async RequestCancel(uint64_t aTransactionId);
child:
async ConfirmRegister(uint8_t[] RegBuffer);
async ConfirmSign(uint8_t[] CredentialID, uint8_t[] ReplyBuffer);
async Abort(nsresult Error);
async ConfirmRegister(uint64_t aTransactionId, uint8_t[] RegBuffer);
async ConfirmSign(uint64_t aTransactionId, uint8_t[] CredentialID, uint8_t[] ReplyBuffer);
async Abort(uint64_t aTransactionId, nsresult Error);
};
}

View File

@ -120,7 +120,7 @@ NS_IMPL_ISUPPORTS(U2FPrefManager, nsIObserver);
U2FTokenManager::U2FTokenManager()
: mTransactionParent(nullptr)
, mTransactionId(0)
, mLastTransactionId(0)
{
MOZ_ASSERT(XRE_IsParentProcess());
// Create on the main thread to make sure ClearOnShutdown() works.
@ -158,9 +158,10 @@ U2FTokenManager::Get()
}
void
U2FTokenManager::AbortTransaction(const nsresult& aError)
U2FTokenManager::AbortTransaction(const uint64_t& aTransactionId,
const nsresult& aError)
{
Unused << mTransactionParent->SendAbort(aError);
Unused << mTransactionParent->SendAbort(aTransactionId, aError);
ClearTransaction();
}
@ -183,8 +184,8 @@ U2FTokenManager::ClearTransaction()
// Forget promises, if necessary.
mRegisterPromise.DisconnectIfExists();
mSignPromise.DisconnectIfExists();
// Bump transaction id.
mTransactionId++;
// Clear transaction id.
mLastTransactionId = 0;
}
RefPtr<U2FTokenTransport>
@ -218,6 +219,7 @@ U2FTokenManager::GetTokenManagerImpl()
void
U2FTokenManager::Register(PWebAuthnTransactionParent* aTransactionParent,
const uint64_t& aTransactionId,
const WebAuthnTransactionInfo& aTransactionInfo)
{
MOZ_LOG(gU2FTokenManagerLog, LogLevel::Debug, ("U2FAuthRegister"));
@ -227,7 +229,7 @@ U2FTokenManager::Register(PWebAuthnTransactionParent* aTransactionParent,
mTokenManagerImpl = GetTokenManagerImpl();
if (!mTokenManagerImpl) {
AbortTransaction(NS_ERROR_DOM_NOT_ALLOWED_ERR);
AbortTransaction(aTransactionId, NS_ERROR_DOM_NOT_ALLOWED_ERR);
return;
}
@ -237,11 +239,11 @@ U2FTokenManager::Register(PWebAuthnTransactionParent* aTransactionParent,
if ((aTransactionInfo.RpIdHash().Length() != SHA256_LENGTH) ||
(aTransactionInfo.ClientDataHash().Length() != SHA256_LENGTH)) {
AbortTransaction(NS_ERROR_DOM_UNKNOWN_ERR);
AbortTransaction(aTransactionId, NS_ERROR_DOM_UNKNOWN_ERR);
return;
}
uint64_t tid = mTransactionId;
uint64_t tid = mLastTransactionId = aTransactionId;
mozilla::TimeStamp startTime = mozilla::TimeStamp::Now();
mTokenManagerImpl->Register(aTransactionInfo.Descriptors(),
aTransactionInfo.RpIdHash(),
@ -270,36 +272,31 @@ U2FTokenManager::Register(PWebAuthnTransactionParent* aTransactionParent,
}
void
U2FTokenManager::MaybeConfirmRegister(uint64_t aTransactionId,
U2FTokenManager::MaybeConfirmRegister(const uint64_t& aTransactionId,
U2FRegisterResult& aResult)
{
if (mTransactionId != aTransactionId) {
return;
}
MOZ_ASSERT(mLastTransactionId == aTransactionId);
mRegisterPromise.Complete();
nsTArray<uint8_t> registration;
aResult.ConsumeRegistration(registration);
Unused << mTransactionParent->SendConfirmRegister(registration);
Unused << mTransactionParent->SendConfirmRegister(aTransactionId, registration);
ClearTransaction();
}
void
U2FTokenManager::MaybeAbortRegister(uint64_t aTransactionId,
U2FTokenManager::MaybeAbortRegister(const uint64_t& aTransactionId,
const nsresult& aError)
{
if (mTransactionId != aTransactionId) {
return;
}
MOZ_ASSERT(mLastTransactionId == aTransactionId);
mRegisterPromise.Complete();
AbortTransaction(aError);
AbortTransaction(aTransactionId, aError);
}
void
U2FTokenManager::Sign(PWebAuthnTransactionParent* aTransactionParent,
const uint64_t& aTransactionId,
const WebAuthnTransactionInfo& aTransactionInfo)
{
MOZ_LOG(gU2FTokenManagerLog, LogLevel::Debug, ("U2FAuthSign"));
@ -309,17 +306,17 @@ U2FTokenManager::Sign(PWebAuthnTransactionParent* aTransactionParent,
mTokenManagerImpl = GetTokenManagerImpl();
if (!mTokenManagerImpl) {
AbortTransaction(NS_ERROR_DOM_NOT_ALLOWED_ERR);
AbortTransaction(aTransactionId, NS_ERROR_DOM_NOT_ALLOWED_ERR);
return;
}
if ((aTransactionInfo.RpIdHash().Length() != SHA256_LENGTH) ||
(aTransactionInfo.ClientDataHash().Length() != SHA256_LENGTH)) {
AbortTransaction(NS_ERROR_DOM_UNKNOWN_ERR);
AbortTransaction(aTransactionId, NS_ERROR_DOM_UNKNOWN_ERR);
return;
}
uint64_t tid = mTransactionId;
uint64_t tid = mLastTransactionId = aTransactionId;
mozilla::TimeStamp startTime = mozilla::TimeStamp::Now();
mTokenManagerImpl->Sign(aTransactionInfo.Descriptors(),
aTransactionInfo.RpIdHash(),
@ -348,13 +345,10 @@ U2FTokenManager::Sign(PWebAuthnTransactionParent* aTransactionParent,
}
void
U2FTokenManager::MaybeConfirmSign(uint64_t aTransactionId,
U2FTokenManager::MaybeConfirmSign(const uint64_t& aTransactionId,
U2FSignResult& aResult)
{
if (mTransactionId != aTransactionId) {
return;
}
MOZ_ASSERT(mLastTransactionId == aTransactionId);
mSignPromise.Complete();
nsTArray<uint8_t> keyHandle;
@ -362,25 +356,24 @@ U2FTokenManager::MaybeConfirmSign(uint64_t aTransactionId,
nsTArray<uint8_t> signature;
aResult.ConsumeSignature(signature);
Unused << mTransactionParent->SendConfirmSign(keyHandle, signature);
Unused << mTransactionParent->SendConfirmSign(aTransactionId, keyHandle, signature);
ClearTransaction();
}
void
U2FTokenManager::MaybeAbortSign(uint64_t aTransactionId, const nsresult& aError)
U2FTokenManager::MaybeAbortSign(const uint64_t& aTransactionId,
const nsresult& aError)
{
if (mTransactionId != aTransactionId) {
return;
}
MOZ_ASSERT(mLastTransactionId == aTransactionId);
mSignPromise.Complete();
AbortTransaction(aError);
AbortTransaction(aTransactionId, aError);
}
void
U2FTokenManager::Cancel(PWebAuthnTransactionParent* aParent)
U2FTokenManager::Cancel(PWebAuthnTransactionParent* aParent,
const uint64_t& aTransactionId)
{
if (mTransactionParent != aParent) {
if (mTransactionParent != aParent || mLastTransactionId != aTransactionId) {
return;
}

View File

@ -31,23 +31,25 @@ public:
NS_INLINE_DECL_REFCOUNTING(U2FTokenManager)
static U2FTokenManager* Get();
void Register(PWebAuthnTransactionParent* aTransactionParent,
const uint64_t& aTransactionId,
const WebAuthnTransactionInfo& aTransactionInfo);
void Sign(PWebAuthnTransactionParent* aTransactionParent,
const uint64_t& aTransactionId,
const WebAuthnTransactionInfo& aTransactionInfo);
void Cancel(PWebAuthnTransactionParent* aTransactionParent);
void Cancel(PWebAuthnTransactionParent* aTransactionParent,
const uint64_t& aTransactionId);
void MaybeClearTransaction(PWebAuthnTransactionParent* aParent);
static void Initialize();
private:
U2FTokenManager();
~U2FTokenManager();
RefPtr<U2FTokenTransport> GetTokenManagerImpl();
void AbortTransaction(const nsresult& aError);
void AbortTransaction(const uint64_t& aTransactionId, const nsresult& aError);
void ClearTransaction();
void MaybeConfirmRegister(uint64_t aTransactionId,
U2FRegisterResult& aResult);
void MaybeAbortRegister(uint64_t aTransactionId, const nsresult& aError);
void MaybeConfirmSign(uint64_t aTransactionId, U2FSignResult& aResult);
void MaybeAbortSign(uint64_t aTransactionId, const nsresult& aError);
void MaybeConfirmRegister(const uint64_t& aTransactionId, U2FRegisterResult& aResult);
void MaybeAbortRegister(const uint64_t& aTransactionId, const nsresult& aError);
void MaybeConfirmSign(const uint64_t& aTransactionId, U2FSignResult& aResult);
void MaybeAbortSign(const uint64_t& aTransactionId, const nsresult& aError);
// Using a raw pointer here, as the lifetime of the IPC object is managed by
// the PBackground protocol code. This means we cannot be left holding an
// invalid IPC protocol object after the transaction is finished.
@ -55,10 +57,10 @@ private:
RefPtr<U2FTokenTransport> mTokenManagerImpl;
MozPromiseRequestHolder<U2FRegisterPromise> mRegisterPromise;
MozPromiseRequestHolder<U2FSignPromise> mSignPromise;
// Guards the asynchronous promise resolution of token manager impls.
// We don't need to protect this with a lock as it will only be modified
// and checked on the PBackground thread in the parent process.
uint64_t mTransactionId;
// The last transaction id, non-zero if there's an active transaction. This
// guards any cancel messages to ensure we don't cancel newer transactions
// due to a stale message.
uint64_t mLastTransactionId;
};
} // namespace dom

View File

@ -199,12 +199,6 @@ WebAuthnManager::ClearTransaction()
}
mTransaction.reset();
if (mChild) {
RefPtr<WebAuthnTransactionChild> c;
mChild.swap(c);
c->Send__delete__(c);
}
}
void
@ -220,8 +214,8 @@ WebAuthnManager::RejectTransaction(const nsresult& aError)
void
WebAuthnManager::CancelTransaction(const nsresult& aError)
{
if (mChild) {
mChild->SendRequestCancel();
if (!NS_WARN_IF(!mChild || mTransaction.isNothing())) {
mChild->SendRequestCancel(mTransaction.ref().mId);
}
RejectTransaction(aError);
@ -234,20 +228,26 @@ WebAuthnManager::~WebAuthnManager()
if (mTransaction.isSome()) {
RejectTransaction(NS_ERROR_ABORT);
}
if (mChild) {
RefPtr<WebAuthnTransactionChild> c;
mChild.swap(c);
c->Send__delete__(c);
}
}
void
WebAuthnManager::GetOrCreateBackgroundActor()
bool
WebAuthnManager::MaybeCreateBackgroundActor()
{
MOZ_ASSERT(NS_IsMainThread());
if (mChild) {
return;
return true;
}
PBackgroundChild* actor = BackgroundChild::GetOrCreateForCurrentThread();
if (NS_WARN_IF(!actor)) {
MOZ_CRASH("Failed to create a PBackgroundChild actor!");
return false;
}
RefPtr<WebAuthnTransactionChild> mgr(new WebAuthnTransactionChild());
@ -255,12 +255,13 @@ WebAuthnManager::GetOrCreateBackgroundActor()
actor->SendPWebAuthnTransactionConstructor(mgr);
if (NS_WARN_IF(!constructedMgr)) {
MOZ_CRASH("Failed to create a PBackgroundChild actor!");
return;
return false;
}
MOZ_ASSERT(constructedMgr == mgr);
mChild = mgr.forget();
return true;
}
//static
@ -458,6 +459,11 @@ WebAuthnManager::MakeCredential(nsPIDOMWindowInner* aParent,
excludeList.AppendElement(c);
}
if (!MaybeCreateBackgroundActor()) {
promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
return promise.forget();
}
// TODO: Add extension list building
nsTArray<WebAuthnExtension> extensions;
@ -475,11 +481,7 @@ WebAuthnManager::MakeCredential(nsPIDOMWindowInner* aParent,
Move(info),
Move(clientDataJSON)));
GetOrCreateBackgroundActor();
if (mChild) {
mChild->SendRequestRegister(mTransaction.ref().mInfo);
}
mChild->SendRequestRegister(mTransaction.ref().mId, mTransaction.ref().mInfo);
return promise.forget();
}
@ -601,6 +603,11 @@ WebAuthnManager::GetAssertion(nsPIDOMWindowInner* aParent,
allowList.AppendElement(c);
}
if (!MaybeCreateBackgroundActor()) {
promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
return promise.forget();
}
// TODO: Add extension list building
// If extensions was specified, process any extensions supported by this
// client platform, to produce the extension data that needs to be sent to the
@ -623,11 +630,7 @@ WebAuthnManager::GetAssertion(nsPIDOMWindowInner* aParent,
Move(info),
Move(clientDataJSON)));
GetOrCreateBackgroundActor();
if (mChild) {
mChild->SendRequestSign(mTransaction.ref().mInfo);
}
mChild->SendRequestSign(mTransaction.ref().mId, mTransaction.ref().mInfo);
return promise.forget();
}
@ -655,12 +658,13 @@ WebAuthnManager::Store(nsPIDOMWindowInner* aParent,
}
void
WebAuthnManager::FinishMakeCredential(nsTArray<uint8_t>& aRegBuffer)
WebAuthnManager::FinishMakeCredential(const uint64_t& aTransactionId,
nsTArray<uint8_t>& aRegBuffer)
{
MOZ_ASSERT(NS_IsMainThread());
// Check for a valid transaction.
if (mTransaction.isNothing()) {
if (mTransaction.isNothing() || mTransaction.ref().mId != aTransactionId) {
return;
}
@ -782,13 +786,14 @@ WebAuthnManager::FinishMakeCredential(nsTArray<uint8_t>& aRegBuffer)
}
void
WebAuthnManager::FinishGetAssertion(nsTArray<uint8_t>& aCredentialId,
WebAuthnManager::FinishGetAssertion(const uint64_t& aTransactionId,
nsTArray<uint8_t>& aCredentialId,
nsTArray<uint8_t>& aSigBuffer)
{
MOZ_ASSERT(NS_IsMainThread());
// Check for a valid transaction.
if (mTransaction.isNothing()) {
if (mTransaction.isNothing() || mTransaction.ref().mId != aTransactionId) {
return;
}
@ -867,11 +872,12 @@ WebAuthnManager::FinishGetAssertion(nsTArray<uint8_t>& aCredentialId,
}
void
WebAuthnManager::RequestAborted(const nsresult& aError)
WebAuthnManager::RequestAborted(const uint64_t& aTransactionId,
const nsresult& aError)
{
MOZ_ASSERT(NS_IsMainThread());
if (mTransaction.isSome()) {
if (mTransaction.isSome() && mTransaction.ref().mId == aTransactionId) {
RejectTransaction(aError);
}
}
@ -890,9 +896,11 @@ WebAuthnManager::HandleEvent(nsIDOMEvent* aEvent)
nsCOMPtr<nsIDocument> doc =
do_QueryInterface(aEvent->InternalDOMEvent()->GetTarget());
MOZ_ASSERT(doc);
if (NS_WARN_IF(!doc)) {
return NS_ERROR_FAILURE;
}
if (doc && doc->Hidden()) {
if (doc->Hidden()) {
MOZ_LOG(gWebAuthnManagerLog, LogLevel::Debug,
("Visibility change: WebAuthn window is hidden, cancelling job."));

View File

@ -71,7 +71,10 @@ public:
, mPromise(aPromise)
, mInfo(aInfo)
, mClientData(aClientData)
{ }
, mId(NextId())
{
MOZ_ASSERT(mId > 0);
}
// Parent of the context we're running the transaction in.
nsCOMPtr<nsPIDOMWindowInner> mParent;
@ -85,6 +88,18 @@ public:
// Client data used to assemble reply objects.
nsCString mClientData;
// Unique transaction id.
uint64_t mId;
private:
// Generates a unique id for new transactions. This doesn't have to be unique
// forever, it's sufficient to differentiate between temporally close
// transactions, where messages can intersect. Can overflow.
static uint64_t NextId() {
static uint64_t id = 0;
return ++id;
}
};
class WebAuthnManager final : public nsIDOMEventListener
@ -108,14 +123,16 @@ public:
Store(nsPIDOMWindowInner* aParent, const Credential& aCredential);
void
FinishMakeCredential(nsTArray<uint8_t>& aRegBuffer);
FinishMakeCredential(const uint64_t& aTransactionId,
nsTArray<uint8_t>& aRegBuffer);
void
FinishGetAssertion(nsTArray<uint8_t>& aCredentialId,
FinishGetAssertion(const uint64_t& aTransactionId,
nsTArray<uint8_t>& aCredentialId,
nsTArray<uint8_t>& aSigBuffer);
void
RequestAborted(const nsresult& aError);
RequestAborted(const uint64_t& aTransactionId, const nsresult& aError);
void ActorDestroyed();
@ -131,11 +148,9 @@ private:
// parent) and rejects it by calling RejectTransaction().
void CancelTransaction(const nsresult& aError);
typedef MozPromise<nsresult, nsresult, false> BackgroundActorPromise;
bool MaybeCreateBackgroundActor();
void GetOrCreateBackgroundActor();
// IPC Channel for the current transaction.
// IPC Channel to the parent process.
RefPtr<WebAuthnTransactionChild> mChild;
// The current transaction, if any.

View File

@ -5,7 +5,6 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "mozilla/dom/WebAuthnTransactionChild.h"
#include "mozilla/dom/WebAuthnManager.h"
namespace mozilla {
namespace dom {
@ -19,30 +18,33 @@ WebAuthnTransactionChild::WebAuthnTransactionChild()
}
mozilla::ipc::IPCResult
WebAuthnTransactionChild::RecvConfirmRegister(nsTArray<uint8_t>&& aRegBuffer)
WebAuthnTransactionChild::RecvConfirmRegister(const uint64_t& aTransactionId,
nsTArray<uint8_t>&& aRegBuffer)
{
RefPtr<WebAuthnManager> mgr = WebAuthnManager::Get();
MOZ_ASSERT(mgr);
mgr->FinishMakeCredential(aRegBuffer);
mgr->FinishMakeCredential(aTransactionId, aRegBuffer);
return IPC_OK();
}
mozilla::ipc::IPCResult
WebAuthnTransactionChild::RecvConfirmSign(nsTArray<uint8_t>&& aCredentialId,
WebAuthnTransactionChild::RecvConfirmSign(const uint64_t& aTransactionId,
nsTArray<uint8_t>&& aCredentialId,
nsTArray<uint8_t>&& aBuffer)
{
RefPtr<WebAuthnManager> mgr = WebAuthnManager::Get();
MOZ_ASSERT(mgr);
mgr->FinishGetAssertion(aCredentialId, aBuffer);
mgr->FinishGetAssertion(aTransactionId, aCredentialId, aBuffer);
return IPC_OK();
}
mozilla::ipc::IPCResult
WebAuthnTransactionChild::RecvAbort(const nsresult& aError)
WebAuthnTransactionChild::RecvAbort(const uint64_t& aTransactionId,
const nsresult& aError)
{
RefPtr<WebAuthnManager> mgr = WebAuthnManager::Get();
MOZ_ASSERT(mgr);
mgr->RequestAborted(aError);
mgr->RequestAborted(aTransactionId, aError);
return IPC_OK();
}

View File

@ -24,11 +24,21 @@ class WebAuthnTransactionChild final : public PWebAuthnTransactionChild
public:
NS_INLINE_DECL_REFCOUNTING(WebAuthnTransactionChild);
WebAuthnTransactionChild();
mozilla::ipc::IPCResult RecvConfirmRegister(nsTArray<uint8_t>&& aRegBuffer) override;
mozilla::ipc::IPCResult RecvConfirmSign(nsTArray<uint8_t>&& aCredentialId,
nsTArray<uint8_t>&& aBuffer) override;
mozilla::ipc::IPCResult RecvAbort(const nsresult& aError) override;
mozilla::ipc::IPCResult
RecvConfirmRegister(const uint64_t& aTransactionId,
nsTArray<uint8_t>&& aRegBuffer) override;
mozilla::ipc::IPCResult
RecvConfirmSign(const uint64_t& aTransactionId,
nsTArray<uint8_t>&& aCredentialId,
nsTArray<uint8_t>&& aBuffer) override;
mozilla::ipc::IPCResult
RecvAbort(const uint64_t& aTransactionId, const nsresult& aError) override;
void ActorDestroy(ActorDestroyReason why) override;
private:
~WebAuthnTransactionChild() = default;
};

View File

@ -12,29 +12,31 @@ namespace mozilla {
namespace dom {
mozilla::ipc::IPCResult
WebAuthnTransactionParent::RecvRequestRegister(const WebAuthnTransactionInfo& aTransactionInfo)
WebAuthnTransactionParent::RecvRequestRegister(const uint64_t& aTransactionId,
const WebAuthnTransactionInfo& aTransactionInfo)
{
AssertIsOnBackgroundThread();
U2FTokenManager* mgr = U2FTokenManager::Get();
mgr->Register(this, aTransactionInfo);
mgr->Register(this, aTransactionId, aTransactionInfo);
return IPC_OK();
}
mozilla::ipc::IPCResult
WebAuthnTransactionParent::RecvRequestSign(const WebAuthnTransactionInfo& aTransactionInfo)
WebAuthnTransactionParent::RecvRequestSign(const uint64_t& aTransactionId,
const WebAuthnTransactionInfo& aTransactionInfo)
{
AssertIsOnBackgroundThread();
U2FTokenManager* mgr = U2FTokenManager::Get();
mgr->Sign(this, aTransactionInfo);
mgr->Sign(this, aTransactionId, aTransactionInfo);
return IPC_OK();
}
mozilla::ipc::IPCResult
WebAuthnTransactionParent::RecvRequestCancel()
WebAuthnTransactionParent::RecvRequestCancel(const uint64_t& aTransactionId)
{
AssertIsOnBackgroundThread();
U2FTokenManager* mgr = U2FTokenManager::Get();
mgr->Cancel(this);
mgr->Cancel(this, aTransactionId);
return IPC_OK();
}

View File

@ -23,12 +23,20 @@ class WebAuthnTransactionParent final : public PWebAuthnTransactionParent
public:
NS_INLINE_DECL_REFCOUNTING(WebAuthnTransactionParent);
WebAuthnTransactionParent() = default;
virtual mozilla::ipc::IPCResult
RecvRequestRegister(const WebAuthnTransactionInfo& aTransactionInfo) override;
RecvRequestRegister(const uint64_t& aTransactionId,
const WebAuthnTransactionInfo& aTransactionInfo) override;
virtual mozilla::ipc::IPCResult
RecvRequestSign(const WebAuthnTransactionInfo& aTransactionInfo) override;
virtual mozilla::ipc::IPCResult RecvRequestCancel() override;
RecvRequestSign(const uint64_t& aTransactionId,
const WebAuthnTransactionInfo& aTransactionInfo) override;
virtual mozilla::ipc::IPCResult
RecvRequestCancel(const uint64_t& aTransactionId) override;
virtual void ActorDestroy(ActorDestroyReason aWhy) override;
private:
~WebAuthnTransactionParent() = default;
};

View File

@ -232,20 +232,9 @@ function* run_test_2(generator)
// succeeded.
do_check_false(do_get_backup_file(profile).exists());
// Synchronously read in the first cookie. This will cause it to go into the
// cookie table, whereupon it will be written out during database rebuild.
do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 1);
// Wait for the asynchronous read to choke, at which point the backup file
// will be created and the database rebuilt.
new _observer(sub_generator, "cookie-db-rebuilding");
yield;
do_execute_soon(function() { do_run_generator(sub_generator); });
yield;
// At this point, the cookies should still be in memory.
do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 1);
do_check_eq(do_count_cookies(), 1);
// Recreate a new database since it was corrupted
do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 0);
do_check_eq(do_count_cookies(), 0);
// Close the profile.
do_close_profile(sub_generator);
@ -255,13 +244,11 @@ function* run_test_2(generator)
do_check_true(do_get_backup_file(profile).exists());
do_check_eq(do_get_backup_file(profile).fileSize, size);
let db = Services.storage.openDatabase(do_get_cookie_file(profile));
do_check_eq(do_count_cookies_in_db(db, "0.com"), 1);
db.close();
// Load the profile, and check that it contains the new cookie.
do_load_profile();
do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 1);
do_check_eq(do_count_cookies(), 1);
do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 0);
do_check_eq(do_count_cookies(), 0);
// Close the profile.
do_close_profile(sub_generator);
@ -310,25 +297,16 @@ function* run_test_3(generator)
// succeeded.
do_check_false(do_get_backup_file(profile).exists());
// Synchronously read in the cookies for our two domains. The first should
// succeed, but the second should fail midway through, resulting in none of
// those cookies being present.
do_check_eq(Services.cookiemgr.countCookiesFromHost("hither.com"), 10);
// Recreate a new database since it was corrupted
do_check_eq(Services.cookiemgr.countCookiesFromHost("hither.com"), 0);
do_check_eq(Services.cookiemgr.countCookiesFromHost("haithur.com"), 0);
// Wait for the backup file to be created and the database rebuilt.
do_check_false(do_get_backup_file(profile).exists());
new _observer(sub_generator, "cookie-db-rebuilding");
yield;
do_execute_soon(function() { do_run_generator(sub_generator); });
yield;
// Close the profile.
do_close_profile(sub_generator);
yield;
let db = Services.storage.openDatabase(do_get_cookie_file(profile));
do_check_eq(do_count_cookies_in_db(db, "hither.com"), 10);
do_check_eq(do_count_cookies_in_db(db), 10);
do_check_eq(do_count_cookies_in_db(db, "hither.com"), 0);
do_check_eq(do_count_cookies_in_db(db), 0);
db.close();
// Check that the original database was renamed.
@ -346,13 +324,6 @@ function* run_test_3(generator)
// Synchronously read in everything.
do_check_eq(do_count_cookies(), 0);
// Wait for the backup file to be created and the database rebuilt.
do_check_false(do_get_backup_file(profile).exists());
new _observer(sub_generator, "cookie-db-rebuilding");
yield;
do_execute_soon(function() { do_run_generator(sub_generator); });
yield;
// Close the profile.
do_close_profile(sub_generator);
yield;
@ -397,26 +368,17 @@ function* run_test_4(generator)
// succeeded.
do_check_false(do_get_backup_file(profile).exists());
// Synchronously read in the first cookie. This will cause it to go into the
// cookie table, whereupon it will be written out during database rebuild.
do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 1);
// Recreate a new database since it was corrupted
do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 0);
// Queue up an INSERT for the same base domain. This should also go into
// memory and be written out during database rebuild.
let uri = NetUtil.newURI("http://0.com/");
Services.cookies.setCookieString(uri, null, "oh2=hai; max-age=1000", null);
// Wait for the asynchronous read to choke and the insert to fail shortly
// thereafter, at which point the backup file will be created and the database
// rebuilt.
new _observer(sub_generator, "cookie-db-rebuilding");
yield;
do_execute_soon(function() { do_run_generator(sub_generator); });
yield;
// At this point, the cookies should still be in memory.
do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 2);
do_check_eq(do_count_cookies(), 2);
do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 1);
do_check_eq(do_count_cookies(), 1);
// Close the profile.
do_close_profile(sub_generator);
@ -425,14 +387,11 @@ function* run_test_4(generator)
// Check that the original database was renamed.
do_check_true(do_get_backup_file(profile).exists());
do_check_eq(do_get_backup_file(profile).fileSize, size);
let db = Services.storage.openDatabase(do_get_cookie_file(profile));
do_check_eq(do_count_cookies_in_db(db, "0.com"), 2);
db.close();
// Load the profile, and check that it contains the new cookie.
do_load_profile();
do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 2);
do_check_eq(do_count_cookies(), 2);
do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 1);
do_check_eq(do_count_cookies(), 1);
// Close the profile.
do_close_profile(sub_generator);
@ -474,22 +433,10 @@ function* run_test_5(generator)
// succeeded.
do_check_false(do_get_backup_file(profile).exists());
// Synchronously read in the first two cookies. This will cause them to go
// into the cookie table, whereupon it will be written out during database
// rebuild.
do_check_eq(Services.cookiemgr.countCookiesFromHost("bar.com"), 1);
do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 1);
// Wait for the asynchronous read to choke, at which point the backup file
// will be created and a new connection opened.
new _observer(sub_generator, "cookie-db-rebuilding");
yield;
// At this point, the cookies should still be in memory. (Note that these
// calls are re-entrant into the cookie service, but it's OK!)
do_check_eq(Services.cookiemgr.countCookiesFromHost("bar.com"), 1);
do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 1);
do_check_eq(do_count_cookies(), 2);
// Recreate a new database since it was corrupted
do_check_eq(Services.cookiemgr.countCookiesFromHost("bar.com"), 0);
do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 0);
do_check_eq(do_count_cookies(), 0);
do_check_true(do_get_backup_file(profile).exists());
do_check_eq(do_get_backup_file(profile).fileSize, size);
do_check_false(do_get_rebuild_backup_file(profile).exists());
@ -502,39 +449,24 @@ function* run_test_5(generator)
do_check_eq(do_count_cookies_in_db(db.db), 1);
db.close();
// Wait for the rebuild to bail and the database to be closed.
new _observer(sub_generator, "cookie-db-closed");
yield;
// Check that the original backup and the database itself are gone.
do_check_true(do_get_rebuild_backup_file(profile).exists());
do_check_true(do_get_backup_file(profile).exists());
do_check_eq(do_get_backup_file(profile).fileSize, size);
do_check_false(do_get_cookie_file(profile).exists());
// Check that the rebuild backup has the original bar.com cookie, and possibly
// a 0.com cookie depending on whether it got written out first or second.
db = new CookieDatabaseConnection(do_get_rebuild_backup_file(profile), 4);
do_check_eq(do_count_cookies_in_db(db.db, "bar.com"), 1);
let count = do_count_cookies_in_db(db.db);
do_check_true(count == 1 ||
count == 2 && do_count_cookies_in_db(db.db, "0.com") == 1);
db.close();
do_check_eq(Services.cookiemgr.countCookiesFromHost("bar.com"), 1);
do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 1);
do_check_eq(do_count_cookies(), 2);
do_check_eq(Services.cookiemgr.countCookiesFromHost("bar.com"), 0);
do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 0);
do_check_eq(do_count_cookies(), 0);
// Close the profile. We do not need to wait for completion, because the
// database has already been closed.
do_close_profile();
// database has already been closed. Ensure the cookie file is unlocked.
do_close_profile(sub_generator);
yield;
// Clean up.
do_get_cookie_file(profile).remove(false);
do_get_backup_file(profile).remove(false);
do_get_rebuild_backup_file(profile).remove(false);
do_check_false(do_get_cookie_file(profile).exists());
do_check_false(do_get_backup_file(profile).exists());
do_check_false(do_get_rebuild_backup_file(profile).exists());
do_run_generator(generator);
}

View File

@ -27,7 +27,8 @@ function* do_run_test() {
Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
// Start the cookieservice, to force creation of a database.
Services.cookies;
// Get the sessionEnumerator to join the initialization in cookie thread
Services.cookiemgr.sessionEnumerator;
// Open a database connection now, after synchronous initialization has
// completed. We may not be able to open one later once asynchronous writing

View File

@ -22,6 +22,17 @@ function* do_run_test() {
// Set up a profile.
let profile = do_get_profile();
// Start the cookieservice, to force creation of a database.
// Get the sessionEnumerator to join the initialization in cookie thread
Services.cookiemgr.sessionEnumerator;
// Close the profile.
do_close_profile(test_generator);
yield;
// Remove the cookie file in order to create another database file.
do_get_cookie_file(profile).remove(false);
// Create a schema 2 database.
let schema2db = new CookieDatabaseConnection(do_get_cookie_file(profile), 2);
@ -79,6 +90,8 @@ function* do_run_test() {
// Load the database, forcing migration to the current schema version. Then
// test the expected set of cookies:
do_load_profile();
// 1) All unexpired, unique cookies exist.
do_check_eq(Services.cookiemgr.countCookiesFromHost("foo.com"), 20);

View File

@ -22,6 +22,17 @@ function* do_run_test() {
// Set up a profile.
let profile = do_get_profile();
// Start the cookieservice, to force creation of a database.
// Get the sessionEnumerator to join the initialization in cookie thread
Services.cookiemgr.sessionEnumerator;
// Close the profile.
do_close_profile(test_generator);
yield;
// Remove the cookie file in order to create another database file.
do_get_cookie_file(profile).remove(false);
// Create a schema 3 database.
let schema3db = new CookieDatabaseConnection(do_get_cookie_file(profile), 3);
@ -79,6 +90,8 @@ function* do_run_test() {
// Load the database, forcing migration to the current schema version. Then
// test the expected set of cookies:
do_load_profile();
// 1) All unexpired, unique cookies exist.
do_check_eq(Services.cookiemgr.countCookiesFromHost("foo.com"), 20);

View File

@ -248,14 +248,15 @@ enum class OpenMode : uint8_t {
OPEN_NONE = 0,
OPEN_READ = 0x1,
OPEN_WRITE = 0x2,
OPEN_READ_WRITE = OPEN_READ|OPEN_WRITE,
OPEN_READ_ONLY = OPEN_READ,
OPEN_WRITE_ONLY = OPEN_WRITE,
// This is only used in conjunction with OMTP to indicate that the DrawTarget
// that is being borrowed will be painted asynchronously, and so will outlive
// the write lock.
OPEN_ASYNC_WRITE = 0x04
OPEN_ASYNC_WRITE = 0x04,
OPEN_READ_WRITE = OPEN_READ|OPEN_WRITE,
OPEN_READ_ASYNC_WRITE = OPEN_READ|OPEN_WRITE|OPEN_ASYNC_WRITE,
OPEN_READ_ONLY = OPEN_READ,
OPEN_WRITE_ONLY = OPEN_WRITE,
};
MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(OpenMode)

View File

@ -30,7 +30,7 @@ struct MOZ_STACK_CLASS AutoCapturedPaintSetup
{
AutoCapturedPaintSetup(CapturedPaintState* aState, CompositorBridgeChild* aBridge)
: mState(aState)
, mTarget(aState->mTarget)
, mTarget(aState->mTargetDual)
, mRestorePermitsSubpixelAA(mTarget->GetPermitSubpixelAA())
, mOldTransform(mTarget->GetTransform())
, mBridge(aBridge)
@ -186,7 +186,7 @@ PaintThread::AsyncPaintContents(CompositorBridgeChild* aBridge,
MOZ_ASSERT(IsOnPaintThread());
MOZ_ASSERT(aState);
DrawTarget* target = aState->mTarget;
DrawTarget* target = aState->mTargetDual;
DrawTargetCapture* capture = aState->mCapture;
AutoCapturedPaintSetup setup(aState, aBridge);

View File

@ -28,12 +28,14 @@ class CapturedPaintState {
NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CapturedPaintState)
public:
CapturedPaintState(nsIntRegion& aRegionToDraw,
gfx::DrawTarget* aTargetDual,
gfx::DrawTarget* aTarget,
gfx::DrawTarget* aTargetOnWhite,
const gfx::Matrix& aTargetTransform,
SurfaceMode aSurfaceMode,
gfxContentType aContentType)
: mRegionToDraw(aRegionToDraw)
, mTargetDual(aTargetDual)
, mTarget(aTarget)
, mTargetOnWhite(aTargetOnWhite)
, mTargetTransform(aTargetTransform)
@ -45,6 +47,7 @@ public:
RefPtr<TextureClient> mTextureClient;
RefPtr<TextureClient> mTextureClientOnWhite;
RefPtr<gfx::DrawTargetCapture> mCapture;
RefPtr<gfx::DrawTarget> mTargetDual;
RefPtr<gfx::DrawTarget> mTarget;
RefPtr<gfx::DrawTarget> mTargetOnWhite;
gfx::Matrix mTargetTransform;

View File

@ -24,6 +24,7 @@
#include "mozilla/gfx/Types.h" // for ExtendMode::ExtendMode::CLAMP, etc
#include "mozilla/layers/ShadowLayers.h" // for ShadowableLayer
#include "mozilla/layers/TextureClient.h" // for TextureClient
#include "mozilla/Move.h" // for Move
#include "mozilla/gfx/Point.h" // for IntSize
#include "gfx2DGlue.h"
#include "nsLayoutUtils.h" // for invalidation debugging
@ -35,6 +36,20 @@ using namespace gfx;
namespace layers {
void
BorrowDrawTarget::ReturnDrawTarget(gfx::DrawTarget*& aReturned)
{
MOZ_ASSERT(mLoanedDrawTarget);
MOZ_ASSERT(aReturned == mLoanedDrawTarget);
if (mLoanedDrawTarget) {
if (mSetTransform) {
mLoanedDrawTarget->SetTransform(mLoanedTransform);
}
mLoanedDrawTarget = nullptr;
}
aReturned = nullptr;
}
IntRect
RotatedBuffer::GetQuadrantRectangle(XSide aXSide, YSide aYSide) const
{
@ -182,23 +197,7 @@ RotatedBuffer::DrawBufferWithRotation(gfx::DrawTarget *aTarget, ContextSource aS
DrawBufferQuadrant(aTarget, RIGHT, BOTTOM, aSource, aOpacity, aOperator,aMask, aMaskTransform);
}
already_AddRefed<SourceSurface>
SourceRotatedBuffer::GetSourceSurface(ContextSource aSource) const
{
RefPtr<SourceSurface> surf;
if (aSource == BUFFER_BLACK) {
surf = mSource;
} else {
MOZ_ASSERT(aSource == BUFFER_WHITE);
surf = mSourceOnWhite;
}
MOZ_ASSERT(surf);
return surf.forget();
}
/* static */ bool
RotatedContentBuffer::IsClippingCheap(DrawTarget* aTarget, const nsIntRegion& aRegion)
bool IsClippingCheap(gfx::DrawTarget* aTarget, const nsIntRegion& aRegion)
{
// Assume clipping is cheap if the draw target just has an integer
// translation, and the visible region is simple.
@ -207,17 +206,13 @@ RotatedContentBuffer::IsClippingCheap(DrawTarget* aTarget, const nsIntRegion& aR
}
void
RotatedContentBuffer::DrawTo(PaintedLayer* aLayer,
DrawTarget* aTarget,
float aOpacity,
CompositionOp aOp,
SourceSurface* aMask,
const Matrix* aMaskTransform)
RotatedBuffer::DrawTo(PaintedLayer* aLayer,
DrawTarget* aTarget,
float aOpacity,
CompositionOp aOp,
SourceSurface* aMask,
const Matrix* aMaskTransform)
{
if (!EnsureBuffer()) {
return;
}
bool clipped = false;
// If the entire buffer is valid, we can just draw the whole thing,
@ -243,12 +238,162 @@ RotatedContentBuffer::DrawTo(PaintedLayer* aLayer,
}
}
void
RotatedBuffer::UpdateDestinationFrom(const RotatedBuffer& aSource,
const gfx::IntRect& aUpdateRect)
{
DrawIterator iter;
while (DrawTarget* destDT =
BorrowDrawTargetForQuadrantUpdate(aUpdateRect, BUFFER_BLACK, &iter)) {
bool isClippingCheap = IsClippingCheap(destDT, iter.mDrawRegion);
if (isClippingCheap) {
gfxUtils::ClipToRegion(destDT, iter.mDrawRegion);
}
aSource.DrawBufferWithRotation(destDT, BUFFER_BLACK, 1.0, CompositionOp::OP_SOURCE);
if (isClippingCheap) {
destDT->PopClip();
}
// Flush the destination before the sources become inaccessible (Unlock).
destDT->Flush();
ReturnDrawTarget(destDT);
}
if (aSource.HaveBufferOnWhite() && HaveBufferOnWhite()) {
DrawIterator whiteIter;
while (DrawTarget* destDT =
BorrowDrawTargetForQuadrantUpdate(aUpdateRect, BUFFER_WHITE, &whiteIter)) {
bool isClippingCheap = IsClippingCheap(destDT, whiteIter.mDrawRegion);
if (isClippingCheap) {
gfxUtils::ClipToRegion(destDT, whiteIter.mDrawRegion);
}
aSource.DrawBufferWithRotation(destDT, BUFFER_WHITE, 1.0, CompositionOp::OP_SOURCE);
if (isClippingCheap) {
destDT->PopClip();
}
// Flush the destination before the sources become inaccessible (Unlock).
destDT->Flush();
ReturnDrawTarget(destDT);
}
}
}
static void
WrapRotationAxis(int32_t* aRotationPoint, int32_t aSize)
{
if (*aRotationPoint < 0) {
*aRotationPoint += aSize;
} else if (*aRotationPoint >= aSize) {
*aRotationPoint -= aSize;
}
}
bool
RotatedBuffer::AdjustTo(const gfx::IntRect& aDestBufferRect,
const gfx::IntRect& aDrawBounds,
bool aCanHaveRotation,
bool aCanDrawRotated)
{
IntRect keepArea;
if (keepArea.IntersectRect(aDestBufferRect, mBufferRect)) {
// Set mBufferRotation so that the pixels currently in mDTBuffer
// will still be rendered in the right place when mBufferRect
// changes to aDestBufferRect.
IntPoint newRotation = mBufferRotation +
(aDestBufferRect.TopLeft() - mBufferRect.TopLeft());
WrapRotationAxis(&newRotation.x, mBufferRect.Width());
WrapRotationAxis(&newRotation.y, mBufferRect.Height());
NS_ASSERTION(gfx::IntRect(gfx::IntPoint(0,0), mBufferRect.Size()).Contains(newRotation),
"newRotation out of bounds");
int32_t xBoundary = aDestBufferRect.XMost() - newRotation.x;
int32_t yBoundary = aDestBufferRect.YMost() - newRotation.y;
bool drawWrapsBuffer = (aDrawBounds.x < xBoundary && xBoundary < aDrawBounds.XMost()) ||
(aDrawBounds.y < yBoundary && yBoundary < aDrawBounds.YMost());
if ((drawWrapsBuffer && !aCanDrawRotated) ||
(newRotation != IntPoint(0,0) && !aCanHaveRotation)) {
// The stuff we need to redraw will wrap around an edge of the
// buffer (and the caller doesn't know how to support that), so
// move the pixels we can keep into a position that lets us
// redraw in just one quadrant.
RefPtr<gfx::DrawTarget> dtBuffer = GetDTBuffer();
RefPtr<gfx::DrawTarget> dtBufferOnWhite = GetDTBufferOnWhite();
if (mBufferRotation == IntPoint(0,0)) {
IntRect srcRect(IntPoint(0, 0), mBufferRect.Size());
IntPoint dest = mBufferRect.TopLeft() - aDestBufferRect.TopLeft();
MOZ_ASSERT(dtBuffer && dtBuffer->IsValid());
dtBuffer->CopyRect(srcRect, dest);
if (HaveBufferOnWhite()) {
MOZ_ASSERT(dtBufferOnWhite && dtBufferOnWhite->IsValid());
dtBufferOnWhite->CopyRect(srcRect, dest);
}
mDidSelfCopy = true;
mBufferRect = aDestBufferRect;
} else {
// With azure and a data surface perform an buffer unrotate
// (SelfCopy).
unsigned char* data;
IntSize size;
int32_t stride;
SurfaceFormat format;
if (dtBuffer->LockBits(&data, &size, &stride, &format)) {
uint8_t bytesPerPixel = BytesPerPixel(format);
BufferUnrotate(data,
size.width * bytesPerPixel,
size.height, stride,
newRotation.x * bytesPerPixel, newRotation.y);
dtBuffer->ReleaseBits(data);
if (HaveBufferOnWhite()) {
MOZ_ASSERT(dtBufferOnWhite && dtBufferOnWhite->IsValid());
dtBufferOnWhite->LockBits(&data, &size, &stride, &format);
uint8_t bytesPerPixel = BytesPerPixel(format);
BufferUnrotate(data,
size.width * bytesPerPixel,
size.height, stride,
newRotation.x * bytesPerPixel, newRotation.y);
dtBufferOnWhite->ReleaseBits(data);
}
// Buffer unrotate moves all the pixels
mDidSelfCopy = true;
mBufferRect = aDestBufferRect;
mBufferRotation = IntPoint(0, 0);
}
if (!mDidSelfCopy) {
// We couldn't unrotate the buffer, so we need to create a
// new one and start from scratch
return false;
}
}
} else {
mBufferRect = aDestBufferRect;
mBufferRotation = newRotation;
}
} else {
// No pixels are going to be kept. The whole visible region
// will be redrawn, so we don't need to copy anything, so we don't
// set destBuffer.
mBufferRect = aDestBufferRect;
mBufferRotation = IntPoint(0,0);
}
return true;
}
DrawTarget*
RotatedContentBuffer::BorrowDrawTargetForQuadrantUpdate(const IntRect& aBounds,
ContextSource aSource,
DrawIterator* aIter,
bool aSetTransform,
Matrix* aOutMatrix)
RotatedBuffer::BorrowDrawTargetForQuadrantUpdate(const IntRect& aBounds,
ContextSource aSource,
DrawIterator* aIter,
bool aSetTransform,
Matrix* aOutMatrix)
{
IntRect bounds = aBounds;
if (aIter) {
@ -273,25 +418,18 @@ RotatedContentBuffer::BorrowDrawTargetForQuadrantUpdate(const IntRect& aBounds,
bounds = aIter->mDrawRegion.GetBounds();
}
if (!EnsureBuffer()) {
return nullptr;
}
gfx::DrawTarget* dtBuffer = GetDTBuffer();
gfx::DrawTarget* dtBufferOnWhite = GetDTBufferOnWhite();
MOZ_ASSERT(!mLoanedDrawTarget, "draw target has been borrowed and not returned");
if (aSource == BUFFER_BOTH && HaveBufferOnWhite()) {
if (!EnsureBufferOnWhite()) {
return nullptr;
}
MOZ_ASSERT(mDTBuffer && mDTBuffer->IsValid() && mDTBufferOnWhite && mDTBufferOnWhite->IsValid());
mLoanedDrawTarget = Factory::CreateDualDrawTarget(mDTBuffer, mDTBufferOnWhite);
MOZ_ASSERT(dtBuffer && dtBuffer->IsValid() && dtBufferOnWhite && dtBufferOnWhite->IsValid());
mLoanedDrawTarget = Factory::CreateDualDrawTarget(dtBuffer, dtBufferOnWhite);
} else if (aSource == BUFFER_WHITE) {
if (!EnsureBufferOnWhite()) {
return nullptr;
}
mLoanedDrawTarget = mDTBufferOnWhite;
mLoanedDrawTarget = dtBufferOnWhite;
} else {
// BUFFER_BLACK, or BUFFER_BOTH with a single buffer.
mLoanedDrawTarget = mDTBuffer;
mLoanedDrawTarget = dtBuffer;
}
// Figure out which quadrant to draw in
@ -318,553 +456,155 @@ RotatedContentBuffer::BorrowDrawTargetForQuadrantUpdate(const IntRect& aBounds,
return mLoanedDrawTarget;
}
void
BorrowDrawTarget::ReturnDrawTarget(gfx::DrawTarget*& aReturned)
gfx::SurfaceFormat
RemoteRotatedBuffer::GetFormat() const
{
MOZ_ASSERT(mLoanedDrawTarget);
MOZ_ASSERT(aReturned == mLoanedDrawTarget);
if (mLoanedDrawTarget) {
if (mSetTransform) {
mLoanedDrawTarget->SetTransform(mLoanedTransform);
}
mLoanedDrawTarget = nullptr;
}
aReturned = nullptr;
}
gfxContentType
RotatedContentBuffer::BufferContentType()
{
if (mBufferProvider || (mDTBuffer && mDTBuffer->IsValid())) {
SurfaceFormat format = SurfaceFormat::B8G8R8A8;
if (mBufferProvider) {
format = mBufferProvider->GetFormat();
} else if (mDTBuffer && mDTBuffer->IsValid()) {
format = mDTBuffer->GetFormat();
}
return ContentForFormat(format);
}
return gfxContentType::SENTINEL;
return mClient->GetFormat();
}
bool
RotatedContentBuffer::BufferSizeOkFor(const IntSize& aSize)
RemoteRotatedBuffer::IsLocked()
{
return (aSize == mBufferRect.Size() ||
(SizedToVisibleBounds != mBufferSizePolicy &&
aSize < mBufferRect.Size()));
return mClient->IsLocked();
}
bool
RotatedContentBuffer::EnsureBuffer()
RemoteRotatedBuffer::Lock(OpenMode aMode)
{
NS_ASSERTION(!mLoanedDrawTarget, "Loaned draw target must be returned");
if (!mDTBuffer || !mDTBuffer->IsValid()) {
if (mBufferProvider) {
mDTBuffer = mBufferProvider->BorrowDrawTarget();
}
MOZ_ASSERT(!mTarget);
MOZ_ASSERT(!mTargetOnWhite);
bool locked = mClient->Lock(aMode) &&
(!mClientOnWhite || mClientOnWhite->Lock(aMode));
if (!locked) {
Unlock();
return false;
}
NS_WARNING_ASSERTION(mDTBuffer && mDTBuffer->IsValid(), "no buffer");
return !!mDTBuffer;
}
bool
RotatedContentBuffer::EnsureBufferOnWhite()
{
NS_ASSERTION(!mLoanedDrawTarget, "Loaned draw target must be returned");
if (!mDTBufferOnWhite) {
if (mBufferProviderOnWhite) {
mDTBufferOnWhite =
mBufferProviderOnWhite->BorrowDrawTarget();
}
mTarget = mClient->BorrowDrawTarget();
if (!mTarget || !mTarget->IsValid()) {
gfxCriticalNote << "Invalid draw target " << hexa(mTarget)
<< "in RemoteRotatedBuffer::Lock";
Unlock();
return false;
}
NS_WARNING_ASSERTION(mDTBufferOnWhite, "no buffer");
return !!mDTBufferOnWhite;
}
bool
RotatedContentBuffer::HaveBuffer() const
{
return mBufferProvider || (mDTBuffer && mDTBuffer->IsValid());
}
bool
RotatedContentBuffer::HaveBufferOnWhite() const
{
return mBufferProviderOnWhite || (mDTBufferOnWhite && mDTBufferOnWhite->IsValid());
}
static void
WrapRotationAxis(int32_t* aRotationPoint, int32_t aSize)
{
if (*aRotationPoint < 0) {
*aRotationPoint += aSize;
} else if (*aRotationPoint >= aSize) {
*aRotationPoint -= aSize;
}
}
static IntRect
ComputeBufferRect(const IntRect& aRequestedRect)
{
IntRect rect(aRequestedRect);
// Set a minimum width to guarantee a minimum size of buffers we
// allocate (and work around problems on some platforms with smaller
// dimensions). 64 used to be the magic number needed to work around
// a rendering glitch on b2g (see bug 788411). Now that we don't support
// this device anymore we should be fine with 8 pixels as the minimum.
rect.SetWidth(std::max(aRequestedRect.Width(), 8));
return rect;
}
void
RotatedContentBuffer::FlushBuffers()
{
if (mDTBuffer) {
mDTBuffer->Flush();
}
if (mDTBufferOnWhite) {
mDTBufferOnWhite->Flush();
}
}
RotatedContentBuffer::PaintState
RotatedContentBuffer::BeginPaint(PaintedLayer* aLayer,
uint32_t aFlags)
{
PaintState result;
// We need to disable rotation if we're going to be resampled when
// drawing, because we might sample across the rotation boundary.
// Also disable buffer rotation when using webrender.
bool canHaveRotation = gfxPlatform::BufferRotationEnabled() &&
!(aFlags & (PAINT_WILL_RESAMPLE | PAINT_NO_ROTATION)) &&
!(aLayer->Manager()->AsWebRenderLayerManager());
nsIntRegion validRegion = aLayer->GetValidRegion();
bool canUseOpaqueSurface = aLayer->CanUseOpaqueSurface();
ContentType layerContentType =
canUseOpaqueSurface ? gfxContentType::COLOR :
gfxContentType::COLOR_ALPHA;
SurfaceMode mode;
nsIntRegion neededRegion;
IntRect destBufferRect;
bool canReuseBuffer = HaveBuffer();
while (true) {
mode = aLayer->GetSurfaceMode();
neededRegion = aLayer->GetVisibleRegion().ToUnknownRegion();
canReuseBuffer &= BufferSizeOkFor(neededRegion.GetBounds().Size());
result.mContentType = layerContentType;
if (canReuseBuffer) {
if (mBufferRect.Contains(neededRegion.GetBounds())) {
// We don't need to adjust mBufferRect.
destBufferRect = mBufferRect;
} else if (neededRegion.GetBounds().Size() <= mBufferRect.Size()) {
// The buffer's big enough but doesn't contain everything that's
// going to be visible. We'll move it.
destBufferRect = IntRect(neededRegion.GetBounds().TopLeft(), mBufferRect.Size());
} else {
destBufferRect = neededRegion.GetBounds();
}
} else {
// We won't be reusing the buffer. Compute a new rect.
destBufferRect = ComputeBufferRect(neededRegion.GetBounds());
}
if (mode == SurfaceMode::SURFACE_COMPONENT_ALPHA) {
#if defined(MOZ_GFX_OPTIMIZE_MOBILE)
mode = SurfaceMode::SURFACE_SINGLE_CHANNEL_ALPHA;
#else
if (!aLayer->GetParent() ||
!aLayer->GetParent()->SupportsComponentAlphaChildren() ||
!aLayer->AsShadowableLayer() ||
!aLayer->AsShadowableLayer()->HasShadow()) {
mode = SurfaceMode::SURFACE_SINGLE_CHANNEL_ALPHA;
} else {
result.mContentType = gfxContentType::COLOR;
}
#endif
}
if ((aFlags & PAINT_WILL_RESAMPLE) &&
(!neededRegion.GetBounds().IsEqualInterior(destBufferRect) ||
neededRegion.GetNumRects() > 1))
{
// The area we add to neededRegion might not be painted opaquely.
if (mode == SurfaceMode::SURFACE_OPAQUE) {
result.mContentType = gfxContentType::COLOR_ALPHA;
mode = SurfaceMode::SURFACE_SINGLE_CHANNEL_ALPHA;
}
// We need to validate the entire buffer, to make sure that only valid
// pixels are sampled.
neededRegion = destBufferRect;
}
// If we have an existing buffer, but the content type has changed or we
// have transitioned into/out of component alpha, then we need to recreate it.
if (canReuseBuffer &&
(result.mContentType != BufferContentType() ||
(mode == SurfaceMode::SURFACE_COMPONENT_ALPHA) != HaveBufferOnWhite()))
{
// Restart the decision process; we won't re-enter since we guard on
// being able to re-use the buffer.
canReuseBuffer = false;
continue;
}
break;
}
if (HaveBuffer() &&
(result.mContentType != BufferContentType() ||
(mode == SurfaceMode::SURFACE_COMPONENT_ALPHA) != HaveBufferOnWhite()))
{
// We're effectively clearing the valid region, so we need to draw
// the entire needed region now.
canReuseBuffer = false;
result.mRegionToInvalidate = aLayer->GetValidRegion();
validRegion.SetEmpty();
Clear();
#if defined(MOZ_DUMP_PAINTING)
if (nsLayoutUtils::InvalidationDebuggingIsEnabled()) {
if (result.mContentType != BufferContentType()) {
printf_stderr("Invalidating entire rotated buffer (layer %p): content type changed\n", aLayer);
} else if ((mode == SurfaceMode::SURFACE_COMPONENT_ALPHA) != HaveBufferOnWhite()) {
printf_stderr("Invalidating entire rotated buffer (layer %p): component alpha changed\n", aLayer);
}
}
#endif
}
NS_ASSERTION(destBufferRect.Contains(neededRegion.GetBounds()),
"Destination rect doesn't contain what we need to paint");
result.mRegionToDraw.Sub(neededRegion, validRegion);
if (result.mRegionToDraw.IsEmpty())
return result;
if (HaveBuffer()) {
if (LockBuffers()) {
// Do not modify result.mRegionToDraw or result.mContentType after this call.
// Do not modify mBufferRect, mBufferRotation, or mDidSelfCopy,
// or call CreateBuffer before this call.
FinalizeFrame(result.mRegionToDraw);
} else {
// Abandon everything and redraw it all. Ideally we'd reallocate and copy
// the old to the new and then call FinalizeFrame on the new buffer so that
// we only need to draw the latest bits, but we need a big refactor to support
// that ordering.
result.mRegionToDraw = neededRegion;
canReuseBuffer = false;
Clear();
}
}
IntRect drawBounds = result.mRegionToDraw.GetBounds();
RefPtr<DrawTarget> destDTBuffer;
RefPtr<DrawTarget> destDTBufferOnWhite;
uint32_t bufferFlags = 0;
if (mode == SurfaceMode::SURFACE_COMPONENT_ALPHA) {
bufferFlags |= BUFFER_COMPONENT_ALPHA;
}
if (canReuseBuffer) {
if (!EnsureBuffer()) {
return result;
}
IntRect keepArea;
if (keepArea.IntersectRect(destBufferRect, mBufferRect)) {
// Set mBufferRotation so that the pixels currently in mDTBuffer
// will still be rendered in the right place when mBufferRect
// changes to destBufferRect.
IntPoint newRotation = mBufferRotation +
(destBufferRect.TopLeft() - mBufferRect.TopLeft());
WrapRotationAxis(&newRotation.x, mBufferRect.Width());
WrapRotationAxis(&newRotation.y, mBufferRect.Height());
NS_ASSERTION(gfx::IntRect(gfx::IntPoint(0,0), mBufferRect.Size()).Contains(newRotation),
"newRotation out of bounds");
int32_t xBoundary = destBufferRect.XMost() - newRotation.x;
int32_t yBoundary = destBufferRect.YMost() - newRotation.y;
bool drawWrapsBuffer = (drawBounds.x < xBoundary && xBoundary < drawBounds.XMost()) ||
(drawBounds.y < yBoundary && yBoundary < drawBounds.YMost());
if ((drawWrapsBuffer && !(aFlags & PAINT_CAN_DRAW_ROTATED)) ||
(newRotation != IntPoint(0,0) && !canHaveRotation)) {
// The stuff we need to redraw will wrap around an edge of the
// buffer (and the caller doesn't know how to support that), so
// move the pixels we can keep into a position that lets us
// redraw in just one quadrant.
if (mBufferRotation == IntPoint(0,0)) {
IntRect srcRect(IntPoint(0, 0), mBufferRect.Size());
IntPoint dest = mBufferRect.TopLeft() - destBufferRect.TopLeft();
MOZ_ASSERT(mDTBuffer && mDTBuffer->IsValid());
mDTBuffer->CopyRect(srcRect, dest);
if (mode == SurfaceMode::SURFACE_COMPONENT_ALPHA) {
if (!EnsureBufferOnWhite()) {
return result;
}
MOZ_ASSERT(mDTBufferOnWhite && mDTBufferOnWhite->IsValid());
mDTBufferOnWhite->CopyRect(srcRect, dest);
}
result.mDidSelfCopy = true;
mDidSelfCopy = true;
// Don't set destBuffer; we special-case self-copies, and
// just did the necessary work above.
mBufferRect = destBufferRect;
} else {
// With azure and a data surface perform an buffer unrotate
// (SelfCopy).
unsigned char* data;
IntSize size;
int32_t stride;
SurfaceFormat format;
if (mDTBuffer->LockBits(&data, &size, &stride, &format)) {
uint8_t bytesPerPixel = BytesPerPixel(format);
BufferUnrotate(data,
size.width * bytesPerPixel,
size.height, stride,
newRotation.x * bytesPerPixel, newRotation.y);
mDTBuffer->ReleaseBits(data);
if (mode == SurfaceMode::SURFACE_COMPONENT_ALPHA) {
if (!EnsureBufferOnWhite()) {
return result;
}
MOZ_ASSERT(mDTBufferOnWhite && mDTBufferOnWhite->IsValid());
mDTBufferOnWhite->LockBits(&data, &size, &stride, &format);
uint8_t bytesPerPixel = BytesPerPixel(format);
BufferUnrotate(data,
size.width * bytesPerPixel,
size.height, stride,
newRotation.x * bytesPerPixel, newRotation.y);
mDTBufferOnWhite->ReleaseBits(data);
}
// Buffer unrotate moves all the pixels, note that
// we self copied for SyncBackToFrontBuffer
result.mDidSelfCopy = true;
mDidSelfCopy = true;
mBufferRect = destBufferRect;
mBufferRotation = IntPoint(0, 0);
}
if (!result.mDidSelfCopy) {
destBufferRect = ComputeBufferRect(neededRegion.GetBounds());
CreateBuffer(result.mContentType, destBufferRect, bufferFlags,
&destDTBuffer, &destDTBufferOnWhite);
if (!destDTBuffer ||
(!destDTBufferOnWhite && (bufferFlags & BUFFER_COMPONENT_ALPHA))) {
if (Factory::ReasonableSurfaceSize(IntSize(destBufferRect.Width(), destBufferRect.Height()))) {
gfxCriticalNote << "Failed 1 buffer db=" << hexa(destDTBuffer.get()) << " dw=" << hexa(destDTBufferOnWhite.get()) << " for " << destBufferRect.x << ", " << destBufferRect.y << ", " << destBufferRect.Width() << ", " << destBufferRect.Height();
}
return result;
}
}
}
} else {
mBufferRect = destBufferRect;
mBufferRotation = newRotation;
}
} else {
// No pixels are going to be kept. The whole visible region
// will be redrawn, so we don't need to copy anything, so we don't
// set destBuffer.
mBufferRect = destBufferRect;
mBufferRotation = IntPoint(0,0);
}
} else {
// The buffer's not big enough, so allocate a new one
CreateBuffer(result.mContentType, destBufferRect, bufferFlags,
&destDTBuffer, &destDTBufferOnWhite);
if (!destDTBuffer ||
(!destDTBufferOnWhite && (bufferFlags & BUFFER_COMPONENT_ALPHA))) {
if (Factory::ReasonableSurfaceSize(IntSize(destBufferRect.Width(), destBufferRect.Height()))) {
gfxCriticalNote << "Failed 2 buffer db=" << hexa(destDTBuffer.get()) << " dw=" << hexa(destDTBufferOnWhite.get()) << " for " << destBufferRect.x << ", " << destBufferRect.y << ", " << destBufferRect.Width() << ", " << destBufferRect.Height();
}
return result;
}
}
NS_ASSERTION(!(aFlags & PAINT_WILL_RESAMPLE) || destBufferRect == neededRegion.GetBounds(),
"If we're resampling, we need to validate the entire buffer");
// If we have no buffered data already, then destBuffer will be a fresh buffer
// and we do not need to clear it below.
bool isClear = !HaveBuffer();
if (destDTBuffer) {
if (!isClear && (mode != SurfaceMode::SURFACE_COMPONENT_ALPHA || HaveBufferOnWhite())) {
// Copy the bits
IntPoint offset = -destBufferRect.TopLeft();
Matrix mat = Matrix::Translation(offset.x, offset.y);
destDTBuffer->SetTransform(mat);
if (!EnsureBuffer()) {
return result;
}
MOZ_ASSERT(mDTBuffer && mDTBuffer->IsValid(), "Have we got a Thebes buffer for some reason?");
DrawBufferWithRotation(destDTBuffer, BUFFER_BLACK, 1.0, CompositionOp::OP_SOURCE);
destDTBuffer->SetTransform(Matrix());
if (mode == SurfaceMode::SURFACE_COMPONENT_ALPHA) {
if (!destDTBufferOnWhite || !EnsureBufferOnWhite()) {
return result;
}
MOZ_ASSERT(mDTBufferOnWhite && mDTBufferOnWhite->IsValid(), "Have we got a Thebes buffer for some reason?");
destDTBufferOnWhite->SetTransform(mat);
DrawBufferWithRotation(destDTBufferOnWhite, BUFFER_WHITE, 1.0, CompositionOp::OP_SOURCE);
destDTBufferOnWhite->SetTransform(Matrix());
}
}
mDTBuffer = destDTBuffer.forget();
mDTBufferOnWhite = destDTBufferOnWhite.forget();
mBufferRect = destBufferRect;
mBufferRotation = IntPoint(0,0);
}
NS_ASSERTION(canHaveRotation || mBufferRotation == IntPoint(0,0),
"Rotation disabled, but we have nonzero rotation?");
nsIntRegion invalidate;
invalidate.Sub(aLayer->GetValidRegion(), destBufferRect);
result.mRegionToInvalidate.Or(result.mRegionToInvalidate, invalidate);
result.mClip = DrawRegionClip::DRAW;
result.mMode = mode;
return result;
}
RefPtr<CapturedPaintState>
RotatedContentBuffer::BorrowDrawTargetForRecording(PaintState& aPaintState,
DrawIterator* aIter,
bool aSetTransform)
{
if (aPaintState.mMode == SurfaceMode::SURFACE_NONE) {
return nullptr;
}
Matrix transform;
DrawTarget* result = BorrowDrawTargetForQuadrantUpdate(aPaintState.mRegionToDraw.GetBounds(),
BUFFER_BOTH, aIter,
aSetTransform,
&transform);
if (!result) {
return nullptr;
}
nsIntRegion regionToDraw =
ExpandDrawRegion(aPaintState, aIter, result->GetBackendType());
RefPtr<CapturedPaintState> state =
new CapturedPaintState(regionToDraw,
result,
mDTBufferOnWhite,
transform,
aPaintState.mMode,
aPaintState.mContentType);
return state;
}
/*static */ bool
RotatedContentBuffer::PrepareDrawTargetForPainting(CapturedPaintState* aState)
{
MOZ_ASSERT(aState);
RefPtr<DrawTarget> target = aState->mTarget;
RefPtr<DrawTarget> whiteTarget = aState->mTargetOnWhite;
if (aState->mSurfaceMode == SurfaceMode::SURFACE_COMPONENT_ALPHA) {
if (!target || !target->IsValid() ||
!whiteTarget || !whiteTarget->IsValid()) {
// This can happen in release builds if allocating one of the two buffers
// failed. This in turn can happen if unreasonably large textures are
// requested.
if (mClientOnWhite) {
mTargetOnWhite = mClientOnWhite->BorrowDrawTarget();
if (!mTargetOnWhite || !mTargetOnWhite->IsValid()) {
gfxCriticalNote << "Invalid draw target(s) " << hexa(mTarget)
<< " and " << hexa(mTargetOnWhite)
<< "in RemoteRotatedBuffer::Lock";
Unlock();
return false;
}
for (auto iter = aState->mRegionToDraw.RectIter(); !iter.Done(); iter.Next()) {
const IntRect& rect = iter.Get();
target->FillRect(Rect(rect.x, rect.y, rect.Width(), rect.Height()),
ColorPattern(Color(0.0, 0.0, 0.0, 1.0)));
whiteTarget->FillRect(Rect(rect.x, rect.y, rect.Width(), rect.Height()),
ColorPattern(Color(1.0, 1.0, 1.0, 1.0)));
}
} else if (aState->mContentType == gfxContentType::COLOR_ALPHA &&
target->IsValid()) {
// HaveBuffer() => we have an existing buffer that we must clear
for (auto iter = aState->mRegionToDraw.RectIter(); !iter.Done(); iter.Next()) {
const IntRect& rect = iter.Get();
target->ClearRect(Rect(rect.x, rect.y, rect.Width(), rect.Height()));
}
}
return true;
}
nsIntRegion
RotatedContentBuffer::ExpandDrawRegion(PaintState& aPaintState,
DrawIterator* aIter,
BackendType aBackendType)
void
RemoteRotatedBuffer::Unlock()
{
nsIntRegion* drawPtr = &aPaintState.mRegionToDraw;
if (aIter) {
// The iterators draw region currently only contains the bounds of the region,
// this makes it the precise region.
aIter->mDrawRegion.And(aIter->mDrawRegion, aPaintState.mRegionToDraw);
drawPtr = &aIter->mDrawRegion;
mTarget = nullptr;
mTargetOnWhite = nullptr;
if (mClient->IsLocked()) {
mClient->Unlock();
}
if (aBackendType == BackendType::DIRECT2D ||
aBackendType == BackendType::DIRECT2D1_1) {
// Simplify the draw region to avoid hitting expensive drawing paths
// for complex regions.
drawPtr->SimplifyOutwardByArea(100 * 100);
if (mClientOnWhite && mClientOnWhite->IsLocked()) {
mClientOnWhite->Unlock();
}
return *drawPtr;
}
DrawTarget*
RotatedContentBuffer::BorrowDrawTargetForPainting(PaintState& aPaintState,
DrawIterator* aIter /* = nullptr */)
void
RemoteRotatedBuffer::SyncWithObject(SyncObjectClient* aSyncObject)
{
RefPtr<CapturedPaintState> capturedState =
BorrowDrawTargetForRecording(aPaintState, aIter, true);
if (!capturedState) {
return nullptr;
mClient->SyncWithObject(aSyncObject);
if (mClientOnWhite) {
mClientOnWhite->SyncWithObject(aSyncObject);
}
}
if (!RotatedContentBuffer::PrepareDrawTargetForPainting(capturedState)) {
return nullptr;
void
RemoteRotatedBuffer::Clear()
{
MOZ_ASSERT(!mTarget && !mTargetOnWhite);
mClient = nullptr;
mClientOnWhite = nullptr;
}
already_AddRefed<gfx::SourceSurface>
RemoteRotatedBuffer::GetSourceSurface(ContextSource aSource) const
{
if (aSource == ContextSource::BUFFER_BLACK) {
return mTarget->Snapshot();
} else {
MOZ_ASSERT(aSource == ContextSource::BUFFER_WHITE);
return mTargetOnWhite->Snapshot();
}
}
return capturedState->mTarget;
gfx::DrawTarget*
RemoteRotatedBuffer::GetDTBuffer() const
{
return mTarget;
}
gfx::DrawTarget*
RemoteRotatedBuffer::GetDTBufferOnWhite() const
{
return mTargetOnWhite;
}
gfx::SurfaceFormat
DrawTargetRotatedBuffer::GetFormat() const
{
return mTarget->GetFormat();
}
already_AddRefed<gfx::SourceSurface>
DrawTargetRotatedBuffer::GetSourceSurface(ContextSource aSource) const
{
if (aSource == ContextSource::BUFFER_BLACK) {
return mTarget->Snapshot();
} else {
MOZ_ASSERT(aSource == ContextSource::BUFFER_WHITE);
return mTargetOnWhite->Snapshot();
}
}
gfx::DrawTarget*
DrawTargetRotatedBuffer::GetDTBuffer() const
{
return mTarget;
}
gfx::DrawTarget*
DrawTargetRotatedBuffer::GetDTBufferOnWhite() const
{
return mTargetOnWhite;
}
gfx::SurfaceFormat
SourceRotatedBuffer::GetFormat() const
{
return mSource->GetFormat();
}
already_AddRefed<SourceSurface>
RotatedContentBuffer::GetSourceSurface(ContextSource aSource) const
SourceRotatedBuffer::GetSourceSurface(ContextSource aSource) const
{
if (!mDTBuffer || !mDTBuffer->IsValid()) {
gfxCriticalNote << "Invalid buffer in RotatedContentBuffer::GetSourceSurface " << gfx::hexa(mDTBuffer);
return nullptr;
RefPtr<SourceSurface> surf;
if (aSource == BUFFER_BLACK) {
surf = mSource;
} else {
MOZ_ASSERT(aSource == BUFFER_WHITE);
surf = mSourceOnWhite;
}
if (aSource == BUFFER_BLACK) {
return mDTBuffer->Snapshot();
} else {
if (!mDTBufferOnWhite || !mDTBufferOnWhite->IsValid()) {
gfxCriticalNote << "Invalid buffer on white in RotatedContentBuffer::GetSourceSurface " << gfx::hexa(mDTBufferOnWhite);
return nullptr;
}
MOZ_ASSERT(aSource == BUFFER_WHITE);
return mDTBufferOnWhite->Snapshot();
}
MOZ_ASSERT(surf);
return surf.forget();
}
} // namespace layers

Some files were not shown because too many files have changed in this diff Show More