зеркало из https://github.com/mozilla/gecko-dev.git
merge autoland to mozilla-central. r=merge a=merge
MozReview-Commit-ID: 60XtziNG2CK
This commit is contained in:
Коммит
8da0763166
|
@ -238,7 +238,7 @@ var gMenuBuilder = {
|
|||
|
||||
item.tabManager.addActiveTabPermission();
|
||||
|
||||
let tab = item.tabManager.convert(contextData.tab);
|
||||
let tab = contextData.tab && item.tabManager.convert(contextData.tab);
|
||||
let info = item.getClickInfo(contextData, wasChecked);
|
||||
|
||||
const map = {shiftKey: "Shift", altKey: "Alt", metaKey: "Command", ctrlKey: "Ctrl"};
|
||||
|
@ -673,7 +673,8 @@ this.menusInternal = class extends ExtensionAPI {
|
|||
|
||||
onClicked: new EventManager(context, "menusInternal.onClicked", fire => {
|
||||
let listener = (event, info, tab) => {
|
||||
context.withPendingBrowser(tab.linkedBrowser,
|
||||
let {linkedBrowser} = tab || tabTracker.activeTab;
|
||||
context.withPendingBrowser(linkedBrowser,
|
||||
() => fire.sync(info, tab));
|
||||
};
|
||||
|
||||
|
|
|
@ -36,6 +36,9 @@ let extData = {
|
|||
id: "clickme-page",
|
||||
title: "Click me!",
|
||||
contexts: ["all"],
|
||||
onclick(info, tab) {
|
||||
browser.test.sendMessage("menu-click", tab);
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
@ -59,7 +62,11 @@ add_task(async function sidebar_contextmenu() {
|
|||
let contentAreaContextMenu = await openContextMenuInSidebar();
|
||||
let item = contentAreaContextMenu.getElementsByAttribute("label", "Click me!");
|
||||
is(item.length, 1, "contextMenu item for page was found");
|
||||
|
||||
item[0].click();
|
||||
await closeContextMenu(contentAreaContextMenu);
|
||||
let tab = await extension.awaitMessage("menu-click");
|
||||
is(tab, null, "tab argument is optional, and missing in clicks from sidebars");
|
||||
|
||||
await extension.unload();
|
||||
});
|
||||
|
|
|
@ -313,10 +313,12 @@ class AutofillRecords {
|
|||
add(record, {sourceSync = false} = {}) {
|
||||
this.log.debug("add:", record);
|
||||
|
||||
let recordToSave = this._cloneAndCleanUp(record);
|
||||
|
||||
if (sourceSync) {
|
||||
// Remove tombstones for incoming items that were changed on another
|
||||
// device. Local deletions always lose to avoid data loss.
|
||||
let index = this._findIndexByGUID(record.guid, {
|
||||
let index = this._findIndexByGUID(recordToSave.guid, {
|
||||
includeDeleted: true,
|
||||
});
|
||||
if (index > -1) {
|
||||
|
@ -324,31 +326,24 @@ class AutofillRecords {
|
|||
if (existing.deleted) {
|
||||
this.data.splice(index, 1);
|
||||
} else {
|
||||
throw new Error(`Record ${record.guid} already exists`);
|
||||
throw new Error(`Record ${recordToSave.guid} already exists`);
|
||||
}
|
||||
}
|
||||
let recordToSave = this._clone(record);
|
||||
return this._saveRecord(recordToSave, {sourceSync});
|
||||
} else if (!recordToSave.deleted) {
|
||||
this._normalizeRecord(recordToSave);
|
||||
|
||||
recordToSave.guid = this._generateGUID();
|
||||
recordToSave.version = this.version;
|
||||
|
||||
// Metadata
|
||||
let now = Date.now();
|
||||
recordToSave.timeCreated = now;
|
||||
recordToSave.timeLastModified = now;
|
||||
recordToSave.timeLastUsed = 0;
|
||||
recordToSave.timesUsed = 0;
|
||||
}
|
||||
|
||||
if (record.deleted) {
|
||||
return this._saveRecord(record);
|
||||
}
|
||||
|
||||
let recordToSave = this._clone(record);
|
||||
this._normalizeRecord(recordToSave);
|
||||
|
||||
recordToSave.guid = this._generateGUID();
|
||||
recordToSave.version = this.version;
|
||||
|
||||
// Metadata
|
||||
let now = Date.now();
|
||||
recordToSave.timeCreated = now;
|
||||
recordToSave.timeLastModified = now;
|
||||
recordToSave.timeLastUsed = 0;
|
||||
recordToSave.timesUsed = 0;
|
||||
|
||||
return this._saveRecord(recordToSave);
|
||||
return this._saveRecord(recordToSave, {sourceSync});
|
||||
}
|
||||
|
||||
_saveRecord(record, {sourceSync = false} = {}) {
|
||||
|
@ -428,7 +423,7 @@ class AutofillRecords {
|
|||
newValue = oldValue;
|
||||
}
|
||||
|
||||
if (!newValue) {
|
||||
if (newValue === undefined || newValue === "") {
|
||||
delete recordFound[field];
|
||||
} else {
|
||||
recordFound[field] = newValue;
|
||||
|
|
|
@ -39,7 +39,7 @@ const TEST_ADDRESS_4 = {
|
|||
organization: "World Wide Web Consortium",
|
||||
};
|
||||
|
||||
const TEST_ADDRESS_FOR_UPDATE = {
|
||||
const TEST_ADDRESS_WITH_EMPTY_FIELD = {
|
||||
"name": "Tim Berners",
|
||||
"street-address": "",
|
||||
};
|
||||
|
@ -299,6 +299,12 @@ add_task(async function test_add() {
|
|||
do_check_eq(addresses[0].timeLastUsed, 0);
|
||||
do_check_eq(addresses[0].timesUsed, 0);
|
||||
|
||||
// Empty string should be deleted before saving.
|
||||
profileStorage.addresses.add(TEST_ADDRESS_WITH_EMPTY_FIELD);
|
||||
let address = profileStorage.addresses.data[2];
|
||||
do_check_eq(address.name, TEST_ADDRESS_WITH_EMPTY_FIELD.name);
|
||||
do_check_eq(address["street-address"], undefined);
|
||||
|
||||
Assert.throws(() => profileStorage.addresses.add(TEST_ADDRESS_WITH_INVALID_FIELD),
|
||||
/"invalidField" is not a valid field\./);
|
||||
});
|
||||
|
@ -333,7 +339,7 @@ add_task(async function test_update() {
|
|||
do_check_eq(getSyncChangeCounter(profileStorage.addresses, guid), 1);
|
||||
|
||||
// Test preserveOldProperties parameter and field with empty string.
|
||||
profileStorage.addresses.update(guid, TEST_ADDRESS_FOR_UPDATE, true);
|
||||
profileStorage.addresses.update(guid, TEST_ADDRESS_WITH_EMPTY_FIELD, true);
|
||||
await onChanged;
|
||||
await profileStorage._saveImmediately();
|
||||
|
||||
|
@ -348,6 +354,12 @@ add_task(async function test_update() {
|
|||
do_check_neq(address.timeLastModified, timeLastModified);
|
||||
do_check_eq(getSyncChangeCounter(profileStorage.addresses, guid), 2);
|
||||
|
||||
// Empty string should be deleted while updating.
|
||||
profileStorage.addresses.update(profileStorage.addresses.data[0].guid, TEST_ADDRESS_WITH_EMPTY_FIELD);
|
||||
address = profileStorage.addresses.data[0];
|
||||
do_check_eq(address.name, TEST_ADDRESS_WITH_EMPTY_FIELD.name);
|
||||
do_check_eq(address["street-address"], undefined);
|
||||
|
||||
Assert.throws(
|
||||
() => profileStorage.addresses.update("INVALID_GUID", TEST_ADDRESS_3),
|
||||
/No matching record\./
|
||||
|
|
|
@ -28,6 +28,12 @@ const TEST_CREDIT_CARD_3 = {
|
|||
"cc-exp-year": 2000,
|
||||
};
|
||||
|
||||
const TEST_CREDIT_CARD_WITH_EMPTY_FIELD = {
|
||||
"cc-name": "",
|
||||
"cc-number": "1234123412341234",
|
||||
"cc-exp-month": 1,
|
||||
};
|
||||
|
||||
const TEST_CREDIT_CARD_WITH_2_DIGITS_YEAR = {
|
||||
"cc-number": "1234123412341234",
|
||||
"cc-exp-month": 1,
|
||||
|
@ -170,6 +176,12 @@ add_task(async function test_add() {
|
|||
do_check_eq(creditCards[0].timeLastUsed, 0);
|
||||
do_check_eq(creditCards[0].timesUsed, 0);
|
||||
|
||||
// Empty string should be deleted before saving.
|
||||
profileStorage.creditCards.add(TEST_CREDIT_CARD_WITH_EMPTY_FIELD);
|
||||
let creditCard = profileStorage.creditCards.data[2];
|
||||
do_check_eq(creditCard["cc-exp-month"], TEST_CREDIT_CARD_WITH_EMPTY_FIELD["cc-exp-month"]);
|
||||
do_check_eq(creditCard["cc-name"], undefined);
|
||||
|
||||
Assert.throws(() => profileStorage.creditCards.add(TEST_CREDIT_CARD_WITH_INVALID_FIELD),
|
||||
/"invalidField" is not a valid field\./);
|
||||
});
|
||||
|
@ -202,6 +214,12 @@ add_task(async function test_update() {
|
|||
do_check_neq(creditCard.timeLastModified, timeLastModified);
|
||||
do_check_credit_card_matches(creditCard, TEST_CREDIT_CARD_3);
|
||||
|
||||
// Empty string should be deleted while updating.
|
||||
profileStorage.creditCards.update(profileStorage.creditCards.data[0].guid, TEST_CREDIT_CARD_WITH_EMPTY_FIELD);
|
||||
creditCard = profileStorage.creditCards.data[0];
|
||||
do_check_eq(creditCard["cc-exp-month"], TEST_CREDIT_CARD_WITH_EMPTY_FIELD["cc-exp-month"]);
|
||||
do_check_eq(creditCard["cc-name"], undefined);
|
||||
|
||||
Assert.throws(
|
||||
() => profileStorage.creditCards.update("INVALID_GUID", TEST_CREDIT_CARD_3),
|
||||
/No matching record\./
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { createFactory, createClass, DOM: dom, PropTypes } =
|
||||
const { createFactory, Component, DOM: dom, PropTypes } =
|
||||
require("devtools/client/shared/vendor/react");
|
||||
const Services = require("Services");
|
||||
|
||||
|
@ -46,41 +46,46 @@ const panels = [{
|
|||
|
||||
const defaultPanelId = "addons";
|
||||
|
||||
module.exports = createClass({
|
||||
displayName: "AboutDebuggingApp",
|
||||
|
||||
propTypes: {
|
||||
client: PropTypes.instanceOf(DebuggerClient).isRequired,
|
||||
telemetry: PropTypes.instanceOf(Telemetry).isRequired
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
class AboutDebuggingApp extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
client: PropTypes.instanceOf(DebuggerClient).isRequired,
|
||||
telemetry: PropTypes.instanceOf(Telemetry).isRequired
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
selectedPanelId: defaultPanelId
|
||||
};
|
||||
},
|
||||
|
||||
this.onHashChange = this.onHashChange.bind(this);
|
||||
this.selectPanel = this.selectPanel.bind(this);
|
||||
}
|
||||
|
||||
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;
|
||||
|
@ -108,4 +113,6 @@ module.exports = createClass({
|
|||
dom.div({ className: "main-content" }, panel)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = AboutDebuggingApp;
|
||||
|
|
|
@ -4,21 +4,23 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { createClass, DOM: dom, PropTypes } =
|
||||
const { Component, DOM: dom, PropTypes } =
|
||||
require("devtools/client/shared/vendor/react");
|
||||
|
||||
module.exports = createClass({
|
||||
displayName: "PanelHeader",
|
||||
|
||||
propTypes: {
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired
|
||||
},
|
||||
class PanelHeader extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
let { name, id } = this.props;
|
||||
|
||||
return dom.div({ className: "header" },
|
||||
dom.h1({ id, className: "header-name" }, name));
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PanelHeader;
|
||||
|
|
|
@ -4,23 +4,23 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { createClass, createFactory, DOM: dom, PropTypes } =
|
||||
const { Component, 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
|
||||
},
|
||||
class PanelMenu extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
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;
|
||||
|
@ -37,5 +37,7 @@ module.exports = createClass({
|
|||
|
||||
// "categories" id used for styling purposes
|
||||
return dom.div({ id: "categories", role: "tablist" }, panelLinks);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PanelMenu;
|
||||
|
|
|
@ -4,23 +4,28 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { createClass, DOM: dom, PropTypes } =
|
||||
const { Component, DOM: dom, PropTypes } =
|
||||
require("devtools/client/shared/vendor/react");
|
||||
|
||||
module.exports = createClass({
|
||||
displayName: "PanelMenuEntry",
|
||||
class PanelMenuEntry extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
icon: PropTypes.string.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
selected: PropTypes.bool,
|
||||
selectPanel: PropTypes.func.isRequired
|
||||
};
|
||||
}
|
||||
|
||||
propTypes: {
|
||||
icon: PropTypes.string.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
selected: PropTypes.bool,
|
||||
selectPanel: PropTypes.func.isRequired
|
||||
},
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onClick = this.onClick.bind(this);
|
||||
}
|
||||
|
||||
onClick() {
|
||||
this.props.selectPanel(this.props.id);
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
let { id, name, icon, selected } = this.props;
|
||||
|
@ -38,4 +43,6 @@ module.exports = createClass({
|
|||
dom.img({ className: "category-icon", src: icon, role: "presentation" }),
|
||||
dom.div({ className: "category-name" }, name));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = PanelMenuEntry;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { createClass, DOM: dom, PropTypes } =
|
||||
const { Component, DOM: dom, PropTypes } =
|
||||
require("devtools/client/shared/vendor/react");
|
||||
const Services = require("Services");
|
||||
|
||||
|
@ -18,19 +18,19 @@ const LocaleCompare = (a, b) => {
|
|||
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
||||
};
|
||||
|
||||
module.exports = createClass({
|
||||
displayName: "TargetList",
|
||||
|
||||
propTypes: {
|
||||
client: PropTypes.instanceOf(DebuggerClient).isRequired,
|
||||
debugDisabled: PropTypes.bool,
|
||||
error: PropTypes.node,
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string,
|
||||
sort: PropTypes.bool,
|
||||
targetClass: PropTypes.func.isRequired,
|
||||
targets: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
},
|
||||
class TargetList extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
client: PropTypes.instanceOf(DebuggerClient).isRequired,
|
||||
debugDisabled: PropTypes.bool,
|
||||
error: PropTypes.node,
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string,
|
||||
sort: PropTypes.bool,
|
||||
targetClass: PropTypes.func.isRequired,
|
||||
targets: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
let { client, debugDisabled, error, targetClass, targets, sort } = this.props;
|
||||
|
@ -52,5 +52,7 @@ module.exports = createClass({
|
|||
|
||||
return dom.div({ id: this.props.id, className: "targets" },
|
||||
dom.h2(null, this.props.name), content);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TargetList;
|
||||
|
|
|
@ -11,7 +11,7 @@ loader.lazyImporter(this, "AddonManager",
|
|||
"resource://gre/modules/AddonManager.jsm");
|
||||
|
||||
const { Cc, Ci } = require("chrome");
|
||||
const { createFactory, createClass, DOM: dom, PropTypes } =
|
||||
const { createFactory, Component, DOM: dom, PropTypes } =
|
||||
require("devtools/client/shared/vendor/react");
|
||||
const Services = require("Services");
|
||||
const AddonsInstallError = createFactory(require("./InstallError"));
|
||||
|
@ -22,24 +22,31 @@ const Strings = Services.strings.createBundle(
|
|||
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() {
|
||||
class AddonsControls extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
debugDisabled: PropTypes.bool
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
installError: null,
|
||||
};
|
||||
},
|
||||
|
||||
this.onEnableAddonDebuggingChange = this.onEnableAddonDebuggingChange.bind(this);
|
||||
this.loadAddonFromFile = this.loadAddonFromFile.bind(this);
|
||||
this.retryInstall = this.retryInstall.bind(this);
|
||||
this.installAddon = this.installAddon.bind(this);
|
||||
}
|
||||
|
||||
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 });
|
||||
|
@ -60,12 +67,12 @@ module.exports = createClass({
|
|||
|
||||
this.installAddon(file);
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
retryInstall() {
|
||||
this.setState({ installError: null });
|
||||
this.installAddon(this.state.lastInstallErrorFile);
|
||||
},
|
||||
}
|
||||
|
||||
installAddon(file) {
|
||||
AddonManager.installTemporaryAddon(file)
|
||||
|
@ -76,7 +83,7 @@ module.exports = createClass({
|
|||
console.error(e);
|
||||
this.setState({ installError: e.message, lastInstallErrorFile: file });
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
let { debugDisabled } = this.props;
|
||||
|
@ -110,4 +117,6 @@ module.exports = createClass({
|
|||
retryInstall: this.retryInstall,
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = AddonsControls;
|
||||
|
|
|
@ -5,20 +5,20 @@
|
|||
/* eslint-env browser */
|
||||
"use strict";
|
||||
|
||||
const { createClass, DOM: dom, PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
const { Component, DOM: dom, PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
|
||||
const Services = require("Services");
|
||||
|
||||
const Strings = Services.strings.createBundle(
|
||||
"chrome://devtools/locale/aboutdebugging.properties");
|
||||
|
||||
module.exports = createClass({
|
||||
displayName: "AddonsInstallError",
|
||||
|
||||
propTypes: {
|
||||
error: PropTypes.string,
|
||||
retryInstall: PropTypes.func,
|
||||
},
|
||||
class AddonsInstallError extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
error: PropTypes.string,
|
||||
retryInstall: PropTypes.func,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.error) {
|
||||
|
@ -36,4 +36,6 @@ module.exports = createClass({
|
|||
{ className: "addons-install-retry", onClick: this.props.retryInstall },
|
||||
Strings.GetStringFromName("retryTemporaryInstall")));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = AddonsInstallError;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
const { AddonManager } = require("resource://gre/modules/AddonManager.jsm");
|
||||
const { Management } = require("resource://gre/modules/Extension.jsm");
|
||||
const { createFactory, createClass, DOM: dom, PropTypes } =
|
||||
const { createFactory, Component, DOM: dom, PropTypes } =
|
||||
require("devtools/client/shared/vendor/react");
|
||||
const Services = require("Services");
|
||||
|
||||
|
@ -27,20 +27,29 @@ 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() {
|
||||
class AddonsPanel extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
client: PropTypes.instanceOf(DebuggerClient).isRequired,
|
||||
id: PropTypes.string.isRequired
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
extensions: [],
|
||||
debugDisabled: false,
|
||||
};
|
||||
},
|
||||
|
||||
this.updateDebugStatus = this.updateDebugStatus.bind(this);
|
||||
this.updateAddonsList = this.updateAddonsList.bind(this);
|
||||
this.onInstalled = this.onInstalled.bind(this);
|
||||
this.onUninstalled = this.onUninstalled.bind(this);
|
||||
this.onEnabled = this.onEnabled.bind(this);
|
||||
this.onDisabled = this.onDisabled.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
AddonManager.addAddonListener(this);
|
||||
|
@ -55,7 +64,7 @@ module.exports = createClass({
|
|||
|
||||
this.updateDebugStatus();
|
||||
this.updateAddonsList();
|
||||
},
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
AddonManager.removeAddonListener(this);
|
||||
|
@ -65,7 +74,7 @@ module.exports = createClass({
|
|||
this.updateDebugStatus);
|
||||
Services.prefs.removeObserver(REMOTE_ENABLED_PREF,
|
||||
this.updateDebugStatus);
|
||||
},
|
||||
}
|
||||
|
||||
updateDebugStatus() {
|
||||
let debugDisabled =
|
||||
|
@ -73,7 +82,7 @@ module.exports = createClass({
|
|||
!Services.prefs.getBoolPref(REMOTE_ENABLED_PREF);
|
||||
|
||||
this.setState({ debugDisabled });
|
||||
},
|
||||
}
|
||||
|
||||
updateAddonsList() {
|
||||
this.props.client.listAddons()
|
||||
|
@ -95,35 +104,35 @@ module.exports = createClass({
|
|||
}, 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;
|
||||
|
@ -177,4 +186,6 @@ module.exports = createClass({
|
|||
})
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = AddonsPanel;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { createClass, DOM: dom, PropTypes } =
|
||||
const { Component, DOM: dom, PropTypes } =
|
||||
require("devtools/client/shared/vendor/react");
|
||||
const { debugAddon, isTemporaryID, parseFileUri, uninstallAddon } =
|
||||
require("../../modules/addon");
|
||||
|
@ -122,32 +122,39 @@ function warningMessages(warnings = []) {
|
|||
});
|
||||
}
|
||||
|
||||
module.exports = createClass({
|
||||
displayName: "AddonTarget",
|
||||
class AddonTarget extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
client: PropTypes.instanceOf(DebuggerClient).isRequired,
|
||||
debugDisabled: PropTypes.bool,
|
||||
target: PropTypes.shape({
|
||||
addonActor: PropTypes.string.isRequired,
|
||||
addonID: PropTypes.string.isRequired,
|
||||
icon: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
temporarilyInstalled: PropTypes.bool,
|
||||
url: PropTypes.string,
|
||||
warnings: PropTypes.array,
|
||||
}).isRequired
|
||||
};
|
||||
}
|
||||
|
||||
propTypes: {
|
||||
client: PropTypes.instanceOf(DebuggerClient).isRequired,
|
||||
debugDisabled: PropTypes.bool,
|
||||
target: PropTypes.shape({
|
||||
addonActor: PropTypes.string.isRequired,
|
||||
addonID: PropTypes.string.isRequired,
|
||||
icon: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
temporarilyInstalled: PropTypes.bool,
|
||||
url: PropTypes.string,
|
||||
warnings: PropTypes.array,
|
||||
}).isRequired
|
||||
},
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.debug = this.debug.bind(this);
|
||||
this.uninstall = this.uninstall.bind(this);
|
||||
this.reload = this.reload.bind(this);
|
||||
}
|
||||
|
||||
debug() {
|
||||
let { target } = this.props;
|
||||
debugAddon(target.addonID);
|
||||
},
|
||||
}
|
||||
|
||||
uninstall() {
|
||||
let { target } = this.props;
|
||||
uninstallAddon(target.addonID);
|
||||
},
|
||||
}
|
||||
|
||||
reload() {
|
||||
let { client, target } = this.props;
|
||||
|
@ -160,7 +167,7 @@ module.exports = createClass({
|
|||
throw new Error(
|
||||
"Error reloading addon " + target.addonID + ": " + error);
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
let { target, debugDisabled } = this.props;
|
||||
|
@ -205,4 +212,6 @@ module.exports = createClass({
|
|||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = AddonTarget;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { createClass, createFactory, DOM: dom, PropTypes } =
|
||||
const { Component, createFactory, DOM: dom, PropTypes } =
|
||||
require("devtools/client/shared/vendor/react");
|
||||
const Services = require("Services");
|
||||
|
||||
|
@ -20,30 +20,34 @@ loader.lazyRequireGetter(this, "DebuggerClient",
|
|||
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() {
|
||||
class TabsPanel extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
client: PropTypes.instanceOf(DebuggerClient).isRequired,
|
||||
id: PropTypes.string.isRequired
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
tabs: []
|
||||
};
|
||||
},
|
||||
|
||||
this.update = this.update.bind(this);
|
||||
}
|
||||
|
||||
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 }) => {
|
||||
|
@ -68,7 +72,7 @@ module.exports = createClass({
|
|||
});
|
||||
this.setState({ tabs });
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
let { client, id } = this.props;
|
||||
|
@ -95,4 +99,6 @@ module.exports = createClass({
|
|||
})
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = TabsPanel;
|
||||
|
|
|
@ -6,29 +6,34 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { createClass, DOM: dom, PropTypes } =
|
||||
const { Component, DOM: dom, PropTypes } =
|
||||
require("devtools/client/shared/vendor/react");
|
||||
const Services = require("Services");
|
||||
|
||||
const Strings = Services.strings.createBundle(
|
||||
"chrome://devtools/locale/aboutdebugging.properties");
|
||||
|
||||
module.exports = createClass({
|
||||
displayName: "TabTarget",
|
||||
class TabTarget extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
target: PropTypes.shape({
|
||||
icon: PropTypes.string,
|
||||
outerWindowID: PropTypes.number.isRequired,
|
||||
title: PropTypes.string,
|
||||
url: PropTypes.string.isRequired
|
||||
}).isRequired
|
||||
};
|
||||
}
|
||||
|
||||
propTypes: {
|
||||
target: PropTypes.shape({
|
||||
icon: PropTypes.string,
|
||||
outerWindowID: PropTypes.number.isRequired,
|
||||
title: PropTypes.string,
|
||||
url: PropTypes.string.isRequired
|
||||
}).isRequired
|
||||
},
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.debug = this.debug.bind(this);
|
||||
}
|
||||
|
||||
debug() {
|
||||
let { target } = this.props;
|
||||
window.open("about:devtools-toolbox?type=tab&id=" + target.outerWindowID);
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
let { target } = this.props;
|
||||
|
@ -50,4 +55,6 @@ module.exports = createClass({
|
|||
}, Strings.GetStringFromName("debug"))
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = TabTarget;
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
loader.lazyImporter(this, "PrivateBrowsingUtils",
|
||||
"resource://gre/modules/PrivateBrowsingUtils.jsm");
|
||||
const { createClass, DOM: dom } =
|
||||
const { Component, DOM: dom } =
|
||||
require("devtools/client/shared/vendor/react");
|
||||
const Services = require("Services");
|
||||
const { Ci } = require("chrome");
|
||||
|
@ -22,8 +22,11 @@ loader.lazyRequireGetter(this, "DebuggerClient",
|
|||
const Strings = Services.strings.createBundle("chrome://devtools/locale/aboutdebugging.properties");
|
||||
const MULTI_OPT_OUT_PREF = "dom.ipc.multiOptOut";
|
||||
|
||||
module.exports = createClass({
|
||||
displayName: "multiE10SWarning",
|
||||
class multiE10SWarning extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onUpdatePreferenceClick = this.onUpdatePreferenceClick.bind(this);
|
||||
}
|
||||
|
||||
onUpdatePreferenceClick() {
|
||||
let message = Strings.GetStringFromName("multiProcessWarningConfirmUpdate2");
|
||||
|
@ -34,7 +37,7 @@ module.exports = createClass({
|
|||
// Restart the browser.
|
||||
Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
return dom.div(
|
||||
|
@ -58,5 +61,7 @@ module.exports = createClass({
|
|||
Strings.GetStringFromName("multiProcessWarningUpdateLink2")
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = multiE10SWarning;
|
||||
|
|
|
@ -8,7 +8,7 @@ loader.lazyImporter(this, "PrivateBrowsingUtils",
|
|||
"resource://gre/modules/PrivateBrowsingUtils.jsm");
|
||||
|
||||
const { Ci } = require("chrome");
|
||||
const { createClass, createFactory, DOM: dom, PropTypes } =
|
||||
const { Component, createFactory, DOM: dom, PropTypes } =
|
||||
require("devtools/client/shared/vendor/react");
|
||||
const { getWorkerForms } = require("../../modules/worker");
|
||||
const Services = require("Services");
|
||||
|
@ -34,24 +34,25 @@ const MORE_INFO_URL = "https://developer.mozilla.org/en-US/docs/Tools/about%3Ade
|
|||
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() {
|
||||
class WorkersPanel extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
workers: {
|
||||
service: [],
|
||||
shared: [],
|
||||
other: []
|
||||
},
|
||||
processCount: 1,
|
||||
client: PropTypes.instanceOf(DebuggerClient).isRequired,
|
||||
id: PropTypes.string.isRequired
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.updateMultiE10S = this.updateMultiE10S.bind(this);
|
||||
this.updateWorkers = this.updateWorkers.bind(this);
|
||||
this.getRegistrationForWorker = this.getRegistrationForWorker.bind(this);
|
||||
this.isE10S = this.isE10S.bind(this);
|
||||
this.renderServiceWorkersError = this.renderServiceWorkersError.bind(this);
|
||||
|
||||
this.state = this.initialState;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let client = this.props.client;
|
||||
|
@ -77,7 +78,7 @@ module.exports = createClass({
|
|||
|
||||
this.updateMultiE10S();
|
||||
this.updateWorkers();
|
||||
},
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
let client = this.props.client;
|
||||
|
@ -88,17 +89,28 @@ module.exports = createClass({
|
|||
|
||||
Services.prefs.removeObserver(PROCESS_COUNT_PREF, this.updateMultiE10S);
|
||||
Services.prefs.removeObserver(MULTI_OPTOUT_PREF, this.updateMultiE10S);
|
||||
},
|
||||
}
|
||||
|
||||
get initialState() {
|
||||
return {
|
||||
workers: {
|
||||
service: [],
|
||||
shared: [],
|
||||
other: []
|
||||
},
|
||||
processCount: 1,
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
let workers = this.initialState.workers;
|
||||
|
||||
getWorkerForms(this.props.client).then(forms => {
|
||||
forms.registrations.forEach(form => {
|
||||
|
@ -156,7 +168,7 @@ module.exports = createClass({
|
|||
|
||||
this.setState({ workers });
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
getRegistrationForWorker(form, registrations) {
|
||||
for (let registration of registrations) {
|
||||
|
@ -165,11 +177,11 @@ module.exports = createClass({
|
|||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
}
|
||||
|
||||
isE10S() {
|
||||
return Services.appinfo.browserTabsRemoteAutostart;
|
||||
},
|
||||
}
|
||||
|
||||
renderServiceWorkersError() {
|
||||
let isWindowPrivate = PrivateBrowsingUtils.isContentWindowPrivate(window);
|
||||
|
@ -200,7 +212,7 @@ module.exports = createClass({
|
|||
Strings.GetStringFromName("configurationIsNotCompatible.learnMore")
|
||||
),
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
let { client, id } = this.props;
|
||||
|
@ -255,4 +267,6 @@ module.exports = createClass({
|
|||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = WorkersPanel;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { createClass, DOM: dom, PropTypes } =
|
||||
const { Component, DOM: dom, PropTypes } =
|
||||
require("devtools/client/shared/vendor/react");
|
||||
const { debugWorker } = require("../../modules/worker");
|
||||
const Services = require("Services");
|
||||
|
@ -17,36 +17,50 @@ loader.lazyRequireGetter(this, "DebuggerClient",
|
|||
const Strings = Services.strings.createBundle(
|
||||
"chrome://devtools/locale/aboutdebugging.properties");
|
||||
|
||||
module.exports = createClass({
|
||||
displayName: "ServiceWorkerTarget",
|
||||
|
||||
propTypes: {
|
||||
client: PropTypes.instanceOf(DebuggerClient).isRequired,
|
||||
debugDisabled: PropTypes.bool,
|
||||
target: PropTypes.shape({
|
||||
active: PropTypes.bool,
|
||||
fetch: PropTypes.bool.isRequired,
|
||||
icon: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
url: PropTypes.string,
|
||||
scope: PropTypes.string.isRequired,
|
||||
// registrationActor can be missing in e10s.
|
||||
registrationActor: PropTypes.string,
|
||||
workerActor: PropTypes.string
|
||||
}).isRequired
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
class ServiceWorkerTarget extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
client: PropTypes.instanceOf(DebuggerClient).isRequired,
|
||||
debugDisabled: PropTypes.bool,
|
||||
target: PropTypes.shape({
|
||||
active: PropTypes.bool,
|
||||
fetch: PropTypes.bool.isRequired,
|
||||
icon: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
url: PropTypes.string,
|
||||
scope: PropTypes.string.isRequired,
|
||||
// registrationActor can be missing in e10s.
|
||||
registrationActor: PropTypes.string,
|
||||
workerActor: PropTypes.string
|
||||
}).isRequired
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
pushSubscription: null
|
||||
};
|
||||
},
|
||||
|
||||
this.debug = this.debug.bind(this);
|
||||
this.push = this.push.bind(this);
|
||||
this.start = this.start.bind(this);
|
||||
this.unregister = this.unregister.bind(this);
|
||||
this.onPushSubscriptionModified = this.onPushSubscriptionModified.bind(this);
|
||||
this.updatePushSubscription = this.updatePushSubscription.bind(this);
|
||||
this.isRunning = this.isRunning.bind(this);
|
||||
this.isActive = this.isActive.bind(this);
|
||||
this.getServiceWorkerStatus = this.getServiceWorkerStatus.bind(this);
|
||||
this.renderButtons = this.renderButtons.bind(this);
|
||||
this.renderUnregisterLink = this.renderUnregisterLink.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let { client } = this.props;
|
||||
client.addListener("push-subscription-modified", this.onPushSubscriptionModified);
|
||||
this.updatePushSubscription();
|
||||
},
|
||||
}
|
||||
|
||||
componentDidUpdate(oldProps, oldState) {
|
||||
let wasActive = oldProps.target.active;
|
||||
|
@ -56,12 +70,12 @@ module.exports = createClass({
|
|||
// subscription change by updating it now.
|
||||
this.updatePushSubscription();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
let { client } = this.props;
|
||||
client.removeListener("push-subscription-modified", this.onPushSubscriptionModified);
|
||||
},
|
||||
}
|
||||
|
||||
debug() {
|
||||
if (!this.isRunning()) {
|
||||
|
@ -71,7 +85,7 @@ module.exports = createClass({
|
|||
|
||||
let { client, target } = this.props;
|
||||
debugWorker(client, target.workerActor);
|
||||
},
|
||||
}
|
||||
|
||||
push() {
|
||||
if (!this.isActive() || !this.isRunning()) {
|
||||
|
@ -86,7 +100,7 @@ module.exports = createClass({
|
|||
to: target.workerActor,
|
||||
type: "push"
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
start() {
|
||||
if (!this.isActive() || this.isRunning()) {
|
||||
|
@ -99,7 +113,7 @@ module.exports = createClass({
|
|||
to: target.registrationActor,
|
||||
type: "start"
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
unregister() {
|
||||
let { client, target } = this.props;
|
||||
|
@ -107,14 +121,14 @@ module.exports = createClass({
|
|||
to: target.registrationActor,
|
||||
type: "unregister"
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
onPushSubscriptionModified(type, data) {
|
||||
let { target } = this.props;
|
||||
if (data.from === target.registrationActor) {
|
||||
this.updatePushSubscription();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
updatePushSubscription() {
|
||||
if (!this.props.target.registrationActor) {
|
||||
|
@ -129,16 +143,16 @@ module.exports = createClass({
|
|||
}, ({ subscription }) => {
|
||||
this.setState({ pushSubscription: subscription });
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
isRunning() {
|
||||
// We know the target is running if it has a worker actor.
|
||||
return !!this.props.target.workerActor;
|
||||
},
|
||||
}
|
||||
|
||||
isActive() {
|
||||
return this.props.target.active;
|
||||
},
|
||||
}
|
||||
|
||||
getServiceWorkerStatus() {
|
||||
if (this.isActive() && this.isRunning()) {
|
||||
|
@ -150,7 +164,7 @@ module.exports = createClass({
|
|||
// ACTIVE state. Unable to know the actual state ("installing", "waiting"), we
|
||||
// display a custom state "registering" for now. See Bug 1153292.
|
||||
return "registering";
|
||||
},
|
||||
}
|
||||
|
||||
renderButtons() {
|
||||
let pushButton = dom.button({
|
||||
|
@ -179,7 +193,7 @@ module.exports = createClass({
|
|||
return debugButton;
|
||||
}
|
||||
return startButton;
|
||||
},
|
||||
}
|
||||
|
||||
renderUnregisterLink() {
|
||||
if (!this.isActive()) {
|
||||
|
@ -191,7 +205,7 @@ module.exports = createClass({
|
|||
onClick: this.unregister,
|
||||
className: "unregister-link",
|
||||
}, Strings.GetStringFromName("unregister"));
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
let { target } = this.props;
|
||||
|
@ -240,4 +254,6 @@ module.exports = createClass({
|
|||
this.renderButtons()
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = ServiceWorkerTarget;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { createClass, DOM: dom, PropTypes } =
|
||||
const { Component, DOM: dom, PropTypes } =
|
||||
require("devtools/client/shared/vendor/react");
|
||||
const { debugWorker } = require("../../modules/worker");
|
||||
const Services = require("Services");
|
||||
|
@ -17,23 +17,28 @@ loader.lazyRequireGetter(this, "DebuggerClient",
|
|||
const Strings = Services.strings.createBundle(
|
||||
"chrome://devtools/locale/aboutdebugging.properties");
|
||||
|
||||
module.exports = createClass({
|
||||
displayName: "WorkerTarget",
|
||||
class WorkerTarget extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
client: PropTypes.instanceOf(DebuggerClient).isRequired,
|
||||
debugDisabled: PropTypes.bool,
|
||||
target: PropTypes.shape({
|
||||
icon: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
workerActor: PropTypes.string
|
||||
}).isRequired
|
||||
};
|
||||
}
|
||||
|
||||
propTypes: {
|
||||
client: PropTypes.instanceOf(DebuggerClient).isRequired,
|
||||
debugDisabled: PropTypes.bool,
|
||||
target: PropTypes.shape({
|
||||
icon: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
workerActor: PropTypes.string
|
||||
}).isRequired
|
||||
},
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.debug = this.debug.bind(this);
|
||||
}
|
||||
|
||||
debug() {
|
||||
let { client, target } = this.props;
|
||||
debugWorker(client, target.workerActor);
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
let { target, debugDisabled } = this.props;
|
||||
|
@ -54,4 +59,6 @@ module.exports = createClass({
|
|||
}, Strings.GetStringFromName("debug"))
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = WorkerTarget;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
const { assert } = require("devtools/shared/DevToolsUtils");
|
||||
const { appinfo } = require("Services");
|
||||
const { DOM: dom, createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
const { DOM: dom, Component, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
const { connect } = require("devtools/client/shared/vendor/react-redux");
|
||||
const { censusDisplays, labelDisplays, treeMapDisplays, diffingState, viewState } = require("./constants");
|
||||
const { toggleRecordingAllocationStacks } = require("./actions/allocations");
|
||||
|
@ -51,20 +51,30 @@ const SnapshotListItem = createFactory(require("./components/SnapshotListItem"))
|
|||
const Heap = createFactory(require("./components/Heap"));
|
||||
const { app: appModel } = require("./models");
|
||||
|
||||
const MemoryApp = createClass({
|
||||
displayName: "MemoryApp",
|
||||
class MemoryApp extends Component {
|
||||
static get propTypes() {
|
||||
return appModel;
|
||||
}
|
||||
|
||||
propTypes: appModel,
|
||||
static get childContextTypes() {
|
||||
return {
|
||||
front: PropTypes.any,
|
||||
heapWorker: PropTypes.any,
|
||||
toolbox: PropTypes.any,
|
||||
};
|
||||
}
|
||||
|
||||
childContextTypes: {
|
||||
front: PropTypes.any,
|
||||
heapWorker: PropTypes.any,
|
||||
toolbox: PropTypes.any,
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
static get defaultProps() {
|
||||
return {};
|
||||
},
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onKeyDown = this.onKeyDown.bind(this);
|
||||
this._getCensusDisplays = this._getCensusDisplays.bind(this);
|
||||
this._getLabelDisplays = this._getLabelDisplays.bind(this);
|
||||
this._getTreeMapDisplays = this._getTreeMapDisplays.bind(this);
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
|
@ -72,18 +82,18 @@ const MemoryApp = createClass({
|
|||
heapWorker: this.props.heapWorker,
|
||||
toolbox: this.props.toolbox,
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Attach the keydown listener directly to the window. When an element that
|
||||
// has the focus (such as a tree node) is removed from the DOM, the focus
|
||||
// falls back to the body.
|
||||
window.addEventListener("keydown", this.onKeyDown);
|
||||
},
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("keydown", this.onKeyDown);
|
||||
},
|
||||
}
|
||||
|
||||
onKeyDown(e) {
|
||||
let { snapshots, dispatch, heapWorker } = this.props;
|
||||
|
@ -106,7 +116,7 @@ const MemoryApp = createClass({
|
|||
let nextSnapshotId = snapshots[nextIndex].id;
|
||||
dispatch(selectSnapshotAndRefresh(heapWorker, nextSnapshotId));
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
_getCensusDisplays() {
|
||||
const customDisplays = getCustomCensusDisplays();
|
||||
|
@ -120,7 +130,7 @@ const MemoryApp = createClass({
|
|||
censusDisplays.allocationStack,
|
||||
censusDisplays.invertedAllocationStack,
|
||||
].concat(custom);
|
||||
},
|
||||
}
|
||||
|
||||
_getLabelDisplays() {
|
||||
const customDisplays = getCustomLabelDisplays();
|
||||
|
@ -133,7 +143,7 @@ const MemoryApp = createClass({
|
|||
labelDisplays.coarseType,
|
||||
labelDisplays.allocationStack,
|
||||
].concat(custom);
|
||||
},
|
||||
}
|
||||
|
||||
_getTreeMapDisplays() {
|
||||
const customDisplays = getCustomTreeMapDisplays();
|
||||
|
@ -145,7 +155,7 @@ const MemoryApp = createClass({
|
|||
return [
|
||||
treeMapDisplays.coarseType
|
||||
].concat(custom);
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
let {
|
||||
|
@ -317,8 +327,8 @@ const MemoryApp = createClass({
|
|||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Passed into react-redux's `connect` method that is called on store change
|
||||
|
|
|
@ -4,24 +4,24 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
|
||||
const { Component, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
|
||||
const Tree = createFactory(require("devtools/client/shared/components/Tree"));
|
||||
const CensusTreeItem = createFactory(require("./CensusTreeItem"));
|
||||
const { TREE_ROW_HEIGHT } = require("../constants");
|
||||
const { censusModel, diffingModel } = require("../models");
|
||||
|
||||
module.exports = createClass({
|
||||
displayName: "Census",
|
||||
|
||||
propTypes: {
|
||||
census: censusModel,
|
||||
onExpand: PropTypes.func.isRequired,
|
||||
onCollapse: PropTypes.func.isRequired,
|
||||
onFocus: PropTypes.func.isRequired,
|
||||
onViewSourceInDebugger: PropTypes.func.isRequired,
|
||||
onViewIndividuals: PropTypes.func.isRequired,
|
||||
diffing: diffingModel,
|
||||
},
|
||||
class Census extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
census: censusModel,
|
||||
onExpand: PropTypes.func.isRequired,
|
||||
onCollapse: PropTypes.func.isRequired,
|
||||
onFocus: PropTypes.func.isRequired,
|
||||
onViewSourceInDebugger: PropTypes.func.isRequired,
|
||||
onViewIndividuals: PropTypes.func.isRequired,
|
||||
diffing: diffingModel,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
let {
|
||||
|
@ -77,4 +77,6 @@ module.exports = createClass({
|
|||
itemHeight: TREE_ROW_HEIGHT,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = Census;
|
||||
|
|
|
@ -4,16 +4,16 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { DOM: dom, createClass } = require("devtools/client/shared/vendor/react");
|
||||
const { DOM: dom, Component } = require("devtools/client/shared/vendor/react");
|
||||
const { L10N } = require("../utils");
|
||||
const models = require("../models");
|
||||
|
||||
module.exports = createClass({
|
||||
displayName: "CensusHeader",
|
||||
|
||||
propTypes: {
|
||||
diffing: models.diffingModel,
|
||||
},
|
||||
class CensusHeader extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
diffing: models.diffingModel,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
let individualsCell;
|
||||
|
@ -71,4 +71,6 @@ module.exports = createClass({
|
|||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = CensusHeader;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
const { isSavedFrame } = require("devtools/shared/DevToolsUtils");
|
||||
const {
|
||||
DOM: dom,
|
||||
createClass,
|
||||
Component,
|
||||
createFactory,
|
||||
PropTypes
|
||||
} = require("devtools/client/shared/vendor/react");
|
||||
|
@ -15,22 +15,27 @@ const Frame = createFactory(require("devtools/client/shared/components/Frame"));
|
|||
const { TREE_ROW_HEIGHT } = require("../constants");
|
||||
const models = require("../models");
|
||||
|
||||
module.exports = createClass({
|
||||
displayName: "CensusTreeItem",
|
||||
class CensusTreeItem extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
arrow: PropTypes.any,
|
||||
depth: PropTypes.number.isRequired,
|
||||
diffing: models.app.diffing,
|
||||
expanded: PropTypes.bool.isRequired,
|
||||
focused: PropTypes.bool.isRequired,
|
||||
getPercentBytes: PropTypes.func.isRequired,
|
||||
getPercentCount: PropTypes.func.isRequired,
|
||||
inverted: PropTypes.bool,
|
||||
item: PropTypes.object.isRequired,
|
||||
onViewIndividuals: PropTypes.func.isRequired,
|
||||
onViewSourceInDebugger: PropTypes.func.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
propTypes: {
|
||||
arrow: PropTypes.any,
|
||||
depth: PropTypes.number.isRequired,
|
||||
diffing: models.app.diffing,
|
||||
expanded: PropTypes.bool.isRequired,
|
||||
focused: PropTypes.bool.isRequired,
|
||||
getPercentBytes: PropTypes.func.isRequired,
|
||||
getPercentCount: PropTypes.func.isRequired,
|
||||
inverted: PropTypes.bool,
|
||||
item: PropTypes.object.isRequired,
|
||||
onViewIndividuals: PropTypes.func.isRequired,
|
||||
onViewSourceInDebugger: PropTypes.func.isRequired,
|
||||
},
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.toLabel = this.toLabel.bind(this);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return this.props.item != nextProps.item
|
||||
|
@ -38,7 +43,7 @@ module.exports = createClass({
|
|||
|| this.props.expanded != nextProps.expanded
|
||||
|| this.props.focused != nextProps.focused
|
||||
|| this.props.diffing != nextProps.diffing;
|
||||
},
|
||||
}
|
||||
|
||||
toLabel(name, linkToDebugger) {
|
||||
if (isSavedFrame(name)) {
|
||||
|
@ -63,7 +68,7 @@ module.exports = createClass({
|
|||
}
|
||||
|
||||
return String(name);
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
let {
|
||||
|
@ -150,5 +155,7 @@ module.exports = createClass({
|
|||
this.toLabel(item.name, onViewSourceInDebugger)
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CensusTreeItem;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { DOM: dom, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
|
||||
const { DOM: dom, Component, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
|
||||
const { assert } = require("devtools/shared/DevToolsUtils");
|
||||
const { createParentMap } = require("devtools/shared/heapsnapshot/CensusUtils");
|
||||
const Tree = createFactory(require("devtools/client/shared/components/Tree"));
|
||||
|
@ -20,18 +20,18 @@ const DOMINATOR_TREE_AUTO_EXPAND_DEPTH = 3;
|
|||
* A throbber that represents a subtree in the dominator tree that is actively
|
||||
* being incrementally loaded and fetched from the `HeapAnalysesWorker`.
|
||||
*/
|
||||
const DominatorTreeSubtreeFetching = createFactory(createClass({
|
||||
displayName: "DominatorTreeSubtreeFetching",
|
||||
|
||||
propTypes: {
|
||||
depth: PropTypes.number.isRequired,
|
||||
focused: PropTypes.bool.isRequired,
|
||||
},
|
||||
class DominatorTreeSubtreeFetchingClass extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
depth: PropTypes.number.isRequired,
|
||||
focused: PropTypes.bool.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return this.props.depth !== nextProps.depth
|
||||
|| this.props.focused !== nextProps.focused;
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
let {
|
||||
|
@ -51,26 +51,26 @@ const DominatorTreeSubtreeFetching = createFactory(createClass({
|
|||
})
|
||||
);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* A link to fetch and load more siblings in the dominator tree, when there are
|
||||
* already many loaded above.
|
||||
*/
|
||||
const DominatorTreeSiblingLink = createFactory(createClass({
|
||||
displayName: "DominatorTreeSiblingLink",
|
||||
|
||||
propTypes: {
|
||||
depth: PropTypes.number.isRequired,
|
||||
focused: PropTypes.bool.isRequired,
|
||||
item: PropTypes.instanceOf(DominatorTreeLazyChildren).isRequired,
|
||||
onLoadMoreSiblings: PropTypes.func.isRequired,
|
||||
},
|
||||
class DominatorTreeSiblingLinkClass extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
depth: PropTypes.number.isRequired,
|
||||
focused: PropTypes.bool.isRequired,
|
||||
item: PropTypes.instanceOf(DominatorTreeLazyChildren).isRequired,
|
||||
onLoadMoreSiblings: PropTypes.func.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return this.props.depth !== nextProps.depth
|
||||
|| this.props.focused !== nextProps.focused;
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
let {
|
||||
|
@ -100,22 +100,19 @@ const DominatorTreeSiblingLink = createFactory(createClass({
|
|||
)
|
||||
);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* The actual dominator tree rendered as an expandable and collapsible tree.
|
||||
*/
|
||||
module.exports = createClass({
|
||||
displayName: "DominatorTree",
|
||||
|
||||
propTypes: {
|
||||
dominatorTree: dominatorTreeModel.isRequired,
|
||||
onLoadMoreSiblings: PropTypes.func.isRequired,
|
||||
onViewSourceInDebugger: PropTypes.func.isRequired,
|
||||
onExpand: PropTypes.func.isRequired,
|
||||
onCollapse: PropTypes.func.isRequired,
|
||||
onFocus: PropTypes.func.isRequired,
|
||||
},
|
||||
class DominatorTree extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
dominatorTree: dominatorTreeModel.isRequired,
|
||||
onLoadMoreSiblings: PropTypes.func.isRequired,
|
||||
onViewSourceInDebugger: PropTypes.func.isRequired,
|
||||
onExpand: PropTypes.func.isRequired,
|
||||
onCollapse: PropTypes.func.isRequired,
|
||||
onFocus: PropTypes.func.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
// Safe to use referential equality here because all of our mutations on
|
||||
|
@ -125,7 +122,7 @@ module.exports = createClass({
|
|||
// mutations to the expanded set occur. Because of the re-allocations, we
|
||||
// can continue using referential equality here.
|
||||
return this.props.dominatorTree !== nextProps.dominatorTree;
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dominatorTree, onViewSourceInDebugger, onLoadMoreSiblings } = this.props;
|
||||
|
@ -216,4 +213,9 @@ module.exports = createClass({
|
|||
itemHeight: TREE_ROW_HEIGHT,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const DominatorTreeSubtreeFetching = createFactory(DominatorTreeSubtreeFetchingClass);
|
||||
const DominatorTreeSiblingLink = createFactory(DominatorTreeSiblingLinkClass);
|
||||
|
||||
module.exports = DominatorTree;
|
||||
|
|
|
@ -4,13 +4,13 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { DOM: dom, createClass } = require("devtools/client/shared/vendor/react");
|
||||
const { DOM: dom, Component } = require("devtools/client/shared/vendor/react");
|
||||
const { L10N } = require("../utils");
|
||||
|
||||
module.exports = createClass({
|
||||
displayName: "DominatorTreeHeader",
|
||||
|
||||
propTypes: { },
|
||||
class DominatorTreeHeader extends Component {
|
||||
static get propTypes() {
|
||||
return { };
|
||||
}
|
||||
|
||||
render() {
|
||||
return dom.div(
|
||||
|
@ -43,4 +43,6 @@ module.exports = createClass({
|
|||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = DominatorTreeHeader;
|
||||
|
|
|
@ -5,38 +5,38 @@
|
|||
"use strict";
|
||||
|
||||
const { assert, isSavedFrame } = require("devtools/shared/DevToolsUtils");
|
||||
const { DOM: dom, createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
const { DOM: dom, Component, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
const { L10N, formatNumber, formatPercent } = require("../utils");
|
||||
const Frame = createFactory(require("devtools/client/shared/components/Frame"));
|
||||
const { TREE_ROW_HEIGHT } = require("../constants");
|
||||
|
||||
const Separator = createFactory(createClass({
|
||||
displayName: "Separator",
|
||||
|
||||
class SeparatorClass extends Component {
|
||||
render() {
|
||||
return dom.span({ className: "separator" }, "›");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
module.exports = createClass({
|
||||
displayName: "DominatorTreeItem",
|
||||
const Separator = createFactory(SeparatorClass);
|
||||
|
||||
propTypes: {
|
||||
item: PropTypes.object.isRequired,
|
||||
depth: PropTypes.number.isRequired,
|
||||
arrow: PropTypes.object,
|
||||
expanded: PropTypes.bool.isRequired,
|
||||
focused: PropTypes.bool.isRequired,
|
||||
getPercentSize: PropTypes.func.isRequired,
|
||||
onViewSourceInDebugger: PropTypes.func.isRequired,
|
||||
},
|
||||
class DominatorTreeItem extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
item: PropTypes.object.isRequired,
|
||||
depth: PropTypes.number.isRequired,
|
||||
arrow: PropTypes.object,
|
||||
expanded: PropTypes.bool.isRequired,
|
||||
focused: PropTypes.bool.isRequired,
|
||||
getPercentSize: PropTypes.func.isRequired,
|
||||
onViewSourceInDebugger: PropTypes.func.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return this.props.item != nextProps.item
|
||||
|| this.props.depth != nextProps.depth
|
||||
|| this.props.expanded != nextProps.expanded
|
||||
|| this.props.focused != nextProps.focused;
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
let {
|
||||
|
@ -141,5 +141,7 @@ module.exports = createClass({
|
|||
`@ 0x${item.nodeId.toString(16)}`)
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DominatorTreeItem;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { DOM: dom, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
|
||||
const { DOM: dom, Component, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
|
||||
const { assert, safeErrorString } = require("devtools/shared/DevToolsUtils");
|
||||
const Census = createFactory(require("./Census"));
|
||||
const CensusHeader = createFactory(require("./CensusHeader"));
|
||||
|
@ -180,29 +180,41 @@ function getError(snapshot, diffing, individuals) {
|
|||
* state of only a button to take a snapshot, loading states, the census view
|
||||
* tree, the dominator tree, etc.
|
||||
*/
|
||||
module.exports = createClass({
|
||||
displayName: "Heap",
|
||||
class Heap extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
onSnapshotClick: PropTypes.func.isRequired,
|
||||
onLoadMoreSiblings: PropTypes.func.isRequired,
|
||||
onCensusExpand: PropTypes.func.isRequired,
|
||||
onCensusCollapse: PropTypes.func.isRequired,
|
||||
onDominatorTreeExpand: PropTypes.func.isRequired,
|
||||
onDominatorTreeCollapse: PropTypes.func.isRequired,
|
||||
onCensusFocus: PropTypes.func.isRequired,
|
||||
onDominatorTreeFocus: PropTypes.func.isRequired,
|
||||
onShortestPathsResize: PropTypes.func.isRequired,
|
||||
snapshot: snapshotModel,
|
||||
onViewSourceInDebugger: PropTypes.func.isRequired,
|
||||
onPopView: PropTypes.func.isRequired,
|
||||
individuals: models.individuals,
|
||||
onViewIndividuals: PropTypes.func.isRequired,
|
||||
onFocusIndividual: PropTypes.func.isRequired,
|
||||
diffing: diffingModel,
|
||||
view: models.view.isRequired,
|
||||
sizes: PropTypes.object.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
propTypes: {
|
||||
onSnapshotClick: PropTypes.func.isRequired,
|
||||
onLoadMoreSiblings: PropTypes.func.isRequired,
|
||||
onCensusExpand: PropTypes.func.isRequired,
|
||||
onCensusCollapse: PropTypes.func.isRequired,
|
||||
onDominatorTreeExpand: PropTypes.func.isRequired,
|
||||
onDominatorTreeCollapse: PropTypes.func.isRequired,
|
||||
onCensusFocus: PropTypes.func.isRequired,
|
||||
onDominatorTreeFocus: PropTypes.func.isRequired,
|
||||
onShortestPathsResize: PropTypes.func.isRequired,
|
||||
snapshot: snapshotModel,
|
||||
onViewSourceInDebugger: PropTypes.func.isRequired,
|
||||
onPopView: PropTypes.func.isRequired,
|
||||
individuals: models.individuals,
|
||||
onViewIndividuals: PropTypes.func.isRequired,
|
||||
onFocusIndividual: PropTypes.func.isRequired,
|
||||
diffing: diffingModel,
|
||||
view: models.view.isRequired,
|
||||
sizes: PropTypes.object.isRequired,
|
||||
},
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._renderHeapView = this._renderHeapView.bind(this);
|
||||
this._renderInitial = this._renderInitial.bind(this);
|
||||
this._renderStatus = this._renderStatus.bind(this);
|
||||
this._renderError = this._renderError.bind(this);
|
||||
this._renderCensus = this._renderCensus.bind(this);
|
||||
this._renderTreeMap = this._renderTreeMap.bind(this);
|
||||
this._renderIndividuals = this._renderIndividuals.bind(this);
|
||||
this._renderDominatorTree = this._renderDominatorTree.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the heap view's container panel with the given contents inside of
|
||||
|
@ -225,7 +237,7 @@ module.exports = createClass({
|
|||
...contents
|
||||
)
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
_renderInitial(onSnapshotClick) {
|
||||
return this._renderHeapView("initial", dom.button(
|
||||
|
@ -236,7 +248,7 @@ module.exports = createClass({
|
|||
},
|
||||
L10N.getStr("take-snapshot")
|
||||
));
|
||||
},
|
||||
}
|
||||
|
||||
_renderStatus(state, statusText, diffing) {
|
||||
let throbber = "";
|
||||
|
@ -250,7 +262,7 @@ module.exports = createClass({
|
|||
},
|
||||
statusText
|
||||
));
|
||||
},
|
||||
}
|
||||
|
||||
_renderError(state, statusText, error) {
|
||||
return this._renderHeapView(
|
||||
|
@ -258,7 +270,7 @@ module.exports = createClass({
|
|||
dom.span({ className: "snapshot-status error" }, statusText),
|
||||
dom.pre({}, safeErrorString(error))
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
_renderCensus(state, census, diffing, onViewSourceInDebugger, onViewIndividuals) {
|
||||
assert(census.report, "Should not render census that does not have a report");
|
||||
|
@ -293,14 +305,14 @@ module.exports = createClass({
|
|||
}));
|
||||
|
||||
return this._renderHeapView(state, ...contents);
|
||||
},
|
||||
}
|
||||
|
||||
_renderTreeMap(state, treeMap) {
|
||||
return this._renderHeapView(
|
||||
state,
|
||||
TreeMap({ treeMap })
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
_renderIndividuals(state, individuals, dominatorTree, onViewSourceInDebugger) {
|
||||
assert(individuals.state === individualsState.FETCHED,
|
||||
|
@ -355,7 +367,7 @@ module.exports = createClass({
|
|||
onResize: this.props.onShortestPathsResize,
|
||||
})
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
_renderDominatorTree(state, onViewSourceInDebugger, dominatorTree, onLoadMoreSiblings) {
|
||||
const tree = dom.div(
|
||||
|
@ -391,7 +403,7 @@ module.exports = createClass({
|
|||
onResize: this.props.onShortestPathsResize,
|
||||
})
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
let {
|
||||
|
@ -454,5 +466,7 @@ module.exports = createClass({
|
|||
return this._renderDominatorTree(state, onViewSourceInDebugger,
|
||||
snapshot.dominatorTree,
|
||||
onLoadMoreSiblings);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Heap;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
|
||||
const { Component, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
|
||||
const Tree = createFactory(require("devtools/client/shared/components/Tree"));
|
||||
const DominatorTreeItem = createFactory(require("./DominatorTreeItem"));
|
||||
const { TREE_ROW_HEIGHT } = require("../constants");
|
||||
|
@ -13,15 +13,15 @@ const models = require("../models");
|
|||
/**
|
||||
* The list of individuals in a census group.
|
||||
*/
|
||||
module.exports = createClass({
|
||||
displayName: "Individuals",
|
||||
|
||||
propTypes: {
|
||||
onViewSourceInDebugger: PropTypes.func.isRequired,
|
||||
onFocus: PropTypes.func.isRequired,
|
||||
individuals: models.individuals,
|
||||
dominatorTree: models.dominatorTreeModel,
|
||||
},
|
||||
class Individuals extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
onViewSourceInDebugger: PropTypes.func.isRequired,
|
||||
onFocus: PropTypes.func.isRequired,
|
||||
individuals: models.individuals,
|
||||
dominatorTree: models.dominatorTreeModel,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
|
@ -57,4 +57,6 @@ module.exports = createClass({
|
|||
itemHeight: TREE_ROW_HEIGHT,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = Individuals;
|
||||
|
|
|
@ -4,13 +4,13 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { DOM: dom, createClass } = require("devtools/client/shared/vendor/react");
|
||||
const { DOM: dom, Component } = require("devtools/client/shared/vendor/react");
|
||||
const { L10N } = require("../utils");
|
||||
|
||||
module.exports = createClass({
|
||||
displayName: "IndividualsHeader",
|
||||
|
||||
propTypes: { },
|
||||
class IndividualsHeader extends Component {
|
||||
static get propTypes() {
|
||||
return { };
|
||||
}
|
||||
|
||||
render() {
|
||||
return dom.div(
|
||||
|
@ -43,4 +43,6 @@ module.exports = createClass({
|
|||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = IndividualsHeader;
|
||||
|
|
|
@ -4,21 +4,21 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
const { DOM: dom, Component, PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
|
||||
/**
|
||||
* Generic list component that takes another react component to represent
|
||||
* the children nodes as `itemComponent`, and a list of items to render
|
||||
* as that component with a click handler.
|
||||
*/
|
||||
module.exports = createClass({
|
||||
displayName: "List",
|
||||
|
||||
propTypes: {
|
||||
itemComponent: PropTypes.any.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
items: PropTypes.array.isRequired,
|
||||
},
|
||||
class List extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
itemComponent: PropTypes.any.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
items: PropTypes.array.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
let { items, onClick, itemComponent: Item } = this.props;
|
||||
|
@ -34,4 +34,6 @@ module.exports = createClass({
|
|||
}))
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = List;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
const {
|
||||
DOM: dom,
|
||||
createClass,
|
||||
Component,
|
||||
PropTypes,
|
||||
} = require("devtools/client/shared/vendor/react");
|
||||
const { isSavedFrame } = require("devtools/shared/DevToolsUtils");
|
||||
|
@ -50,41 +50,43 @@ function stringifyLabel(label, id) {
|
|||
return `${sanitized.join(" › ")} @ 0x${id.toString(16)}`;
|
||||
}
|
||||
|
||||
module.exports = createClass({
|
||||
displayName: "ShortestPaths",
|
||||
class ShortestPaths extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
graph: PropTypes.shape({
|
||||
nodes: PropTypes.arrayOf(PropTypes.object),
|
||||
edges: PropTypes.arrayOf(PropTypes.object),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
propTypes: {
|
||||
graph: PropTypes.shape({
|
||||
nodes: PropTypes.arrayOf(PropTypes.object),
|
||||
edges: PropTypes.arrayOf(PropTypes.object),
|
||||
}),
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return { zoom: null };
|
||||
},
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { zoom: null };
|
||||
this._renderGraph = this._renderGraph.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.graph) {
|
||||
this._renderGraph(this.refs.container, this.props.graph);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return this.props.graph != nextProps.graph;
|
||||
},
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.props.graph) {
|
||||
this._renderGraph(this.refs.container, this.props.graph);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.state.zoom) {
|
||||
this.state.zoom.on("zoom", null);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
_renderGraph(container, { nodes, edges }) {
|
||||
if (!container.firstChild) {
|
||||
|
@ -144,7 +146,7 @@ module.exports = createClass({
|
|||
|
||||
const layout = dagreD3.layout();
|
||||
renderer.layout(layout).run(graph, target);
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
let contents;
|
||||
|
@ -182,5 +184,7 @@ module.exports = createClass({
|
|||
),
|
||||
contents
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ShortestPaths;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
const { DOM: dom, Component, PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
const {
|
||||
L10N,
|
||||
getSnapshotTitle,
|
||||
|
@ -16,17 +16,17 @@ const {
|
|||
const { diffingState } = require("../constants");
|
||||
const { snapshot: snapshotModel, app: appModel } = require("../models");
|
||||
|
||||
module.exports = createClass({
|
||||
displayName: "SnapshotListItem",
|
||||
|
||||
propTypes: {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
item: snapshotModel.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
diffing: appModel.diffing,
|
||||
},
|
||||
class SnapshotListItem extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
item: snapshotModel.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
diffing: appModel.diffing,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
let { item: snapshot, onClick, onSave, onDelete, diffing } = this.props;
|
||||
|
@ -112,4 +112,6 @@ module.exports = createClass({
|
|||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = SnapshotListItem;
|
||||
|
|
|
@ -4,46 +4,46 @@
|
|||
"use strict";
|
||||
|
||||
const { assert } = require("devtools/shared/DevToolsUtils");
|
||||
const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
const { DOM: dom, Component, PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
const { L10N } = require("../utils");
|
||||
const models = require("../models");
|
||||
const { viewState } = require("../constants");
|
||||
|
||||
module.exports = createClass({
|
||||
displayName: "Toolbar",
|
||||
|
||||
propTypes: {
|
||||
censusDisplays: PropTypes.arrayOf(PropTypes.shape({
|
||||
displayName: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
censusDisplay: PropTypes.shape({
|
||||
displayName: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
onTakeSnapshotClick: PropTypes.func.isRequired,
|
||||
onImportClick: PropTypes.func.isRequired,
|
||||
onClearSnapshotsClick: PropTypes.func.isRequired,
|
||||
onCensusDisplayChange: PropTypes.func.isRequired,
|
||||
onToggleRecordAllocationStacks: PropTypes.func.isRequired,
|
||||
allocations: models.allocations,
|
||||
filterString: PropTypes.string,
|
||||
setFilterString: PropTypes.func.isRequired,
|
||||
diffing: models.diffingModel,
|
||||
onToggleDiffing: PropTypes.func.isRequired,
|
||||
view: models.view.isRequired,
|
||||
onViewChange: PropTypes.func.isRequired,
|
||||
labelDisplays: PropTypes.arrayOf(PropTypes.shape({
|
||||
displayName: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
labelDisplay: PropTypes.shape({
|
||||
displayName: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
onLabelDisplayChange: PropTypes.func.isRequired,
|
||||
treeMapDisplays: PropTypes.arrayOf(PropTypes.shape({
|
||||
displayName: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
onTreeMapDisplayChange: PropTypes.func.isRequired,
|
||||
snapshots: PropTypes.arrayOf(models.snapshot).isRequired,
|
||||
},
|
||||
class Toolbar extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
censusDisplays: PropTypes.arrayOf(PropTypes.shape({
|
||||
displayName: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
censusDisplay: PropTypes.shape({
|
||||
displayName: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
onTakeSnapshotClick: PropTypes.func.isRequired,
|
||||
onImportClick: PropTypes.func.isRequired,
|
||||
onClearSnapshotsClick: PropTypes.func.isRequired,
|
||||
onCensusDisplayChange: PropTypes.func.isRequired,
|
||||
onToggleRecordAllocationStacks: PropTypes.func.isRequired,
|
||||
allocations: models.allocations,
|
||||
filterString: PropTypes.string,
|
||||
setFilterString: PropTypes.func.isRequired,
|
||||
diffing: models.diffingModel,
|
||||
onToggleDiffing: PropTypes.func.isRequired,
|
||||
view: models.view.isRequired,
|
||||
onViewChange: PropTypes.func.isRequired,
|
||||
labelDisplays: PropTypes.arrayOf(PropTypes.shape({
|
||||
displayName: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
labelDisplay: PropTypes.shape({
|
||||
displayName: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
onLabelDisplayChange: PropTypes.func.isRequired,
|
||||
treeMapDisplays: PropTypes.arrayOf(PropTypes.shape({
|
||||
displayName: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
onTreeMapDisplayChange: PropTypes.func.isRequired,
|
||||
snapshots: PropTypes.arrayOf(models.snapshot).isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
let {
|
||||
|
@ -298,4 +298,6 @@ module.exports = createClass({
|
|||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = Toolbar;
|
||||
|
|
|
@ -4,33 +4,36 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { DOM: dom, createClass } = require("devtools/client/shared/vendor/react");
|
||||
const { DOM: dom, Component } = require("devtools/client/shared/vendor/react");
|
||||
const { treeMapModel } = require("../models");
|
||||
const startVisualization = require("./tree-map/start");
|
||||
|
||||
module.exports = createClass({
|
||||
displayName: "TreeMap",
|
||||
class TreeMap extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
treeMap: treeMapModel
|
||||
};
|
||||
}
|
||||
|
||||
propTypes: {
|
||||
treeMap: treeMapModel
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {};
|
||||
},
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
this._stopVisualization = this._stopVisualization.bind(this);
|
||||
this._startVisualization = this._startVisualization.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { treeMap } = this.props;
|
||||
if (treeMap && treeMap.report) {
|
||||
this._startVisualization();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
const oldTreeMap = this.props.treeMap;
|
||||
const newTreeMap = nextProps.treeMap;
|
||||
return oldTreeMap !== newTreeMap;
|
||||
},
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
this._stopVisualization();
|
||||
|
@ -38,27 +41,27 @@ module.exports = createClass({
|
|||
if (this.props.treeMap && this.props.treeMap.report) {
|
||||
this._startVisualization();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.state.stopVisualization) {
|
||||
this.state.stopVisualization();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
_stopVisualization() {
|
||||
if (this.state.stopVisualization) {
|
||||
this.state.stopVisualization();
|
||||
this.setState({ stopVisualization: null });
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
_startVisualization() {
|
||||
const { container } = this.refs;
|
||||
const { report } = this.props.treeMap;
|
||||
const stopVisualization = startVisualization(container, report);
|
||||
this.setState({ stopVisualization });
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
return dom.div(
|
||||
|
@ -68,4 +71,6 @@ module.exports = createClass({
|
|||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = TreeMap;
|
||||
|
|
|
@ -4,48 +4,55 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
const { DOM: dom, Component, PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
|
||||
module.exports = createClass({
|
||||
displayName: "AutocompletePopup",
|
||||
class AutocompletePopup extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
/**
|
||||
* autocompleteProvider takes search-box's entire input text as `filter` argument
|
||||
* ie. "is:cached pr"
|
||||
* returned value is array of objects like below
|
||||
* [{value: "is:cached protocol", displayValue: "protocol"}[, ...]]
|
||||
* `value` is used to update the search-box input box for given item
|
||||
* `displayValue` is used to render the autocomplete list
|
||||
*/
|
||||
autocompleteProvider: PropTypes.func.isRequired,
|
||||
filter: PropTypes.string.isRequired,
|
||||
onItemSelected: PropTypes.func.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
propTypes: {
|
||||
/**
|
||||
* autocompleteProvider takes search-box's entire input text as `filter` argument
|
||||
* ie. "is:cached pr"
|
||||
* returned value is array of objects like below
|
||||
* [{value: "is:cached protocol", displayValue: "protocol"}[, ...]]
|
||||
* `value` is used to update the search-box input box for given item
|
||||
* `displayValue` is used to render the autocomplete list
|
||||
*/
|
||||
autocompleteProvider: PropTypes.func.isRequired,
|
||||
filter: PropTypes.string.isRequired,
|
||||
onItemSelected: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return this.computeState(this.props);
|
||||
},
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = this.computeState(props);
|
||||
this.computeState = this.computeState.bind(this);
|
||||
this.jumpToTop = this.jumpToTop.bind(this);
|
||||
this.jumpToBottom = this.jumpToBottom.bind(this);
|
||||
this.jumpBy = this.jumpBy.bind(this);
|
||||
this.select = this.select.bind(this);
|
||||
this.onMouseDown = this.onMouseDown.bind(this);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.filter === nextProps.filter) {
|
||||
return;
|
||||
}
|
||||
this.setState(this.computeState(nextProps));
|
||||
},
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.refs.selected) {
|
||||
this.refs.selected.scrollIntoView(false);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
computeState({ autocompleteProvider, filter }) {
|
||||
let list = autocompleteProvider(filter);
|
||||
let selectedIndex = list.length == 1 ? 0 : -1;
|
||||
|
||||
return { list, selectedIndex };
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this method to select the top-most item
|
||||
|
@ -53,7 +60,7 @@ module.exports = createClass({
|
|||
*/
|
||||
jumpToTop() {
|
||||
this.setState({ selectedIndex: 0 });
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this method to select the bottom-most item
|
||||
|
@ -61,7 +68,7 @@ module.exports = createClass({
|
|||
*/
|
||||
jumpToBottom() {
|
||||
this.setState({ selectedIndex: this.state.list.length - 1 });
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the selected index with the provided increment value. Will cycle to the
|
||||
|
@ -81,7 +88,7 @@ module.exports = createClass({
|
|||
nextIndex = nextIndex < 0 ? list.length - 1 : nextIndex;
|
||||
}
|
||||
this.setState({selectedIndex: nextIndex});
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the currently selected item to the onItemSelected callback
|
||||
|
@ -91,12 +98,12 @@ module.exports = createClass({
|
|||
if (this.refs.selected) {
|
||||
this.props.onItemSelected(this.refs.selected.dataset.value);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
onMouseDown(e) {
|
||||
e.preventDefault();
|
||||
this.setState({ selectedIndex: Number(e.target.dataset.index) }, this.select);
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
let { list } = this.state;
|
||||
|
@ -124,4 +131,6 @@ module.exports = createClass({
|
|||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = AutocompletePopup;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
const { DOM: dom, Component, PropTypes } = require("devtools/client/shared/vendor/react");
|
||||
const { getSourceNames, parseURL,
|
||||
isScratchpadScheme, getSourceMappedFile } = require("devtools/client/shared/source-utils");
|
||||
const { LocalizationHelper } = require("devtools/shared/l10n");
|
||||
|
@ -12,34 +12,34 @@ const { LocalizationHelper } = require("devtools/shared/l10n");
|
|||
const l10n = new LocalizationHelper("devtools/client/locales/components.properties");
|
||||
const webl10n = new LocalizationHelper("devtools/client/locales/webconsole.properties");
|
||||
|
||||
module.exports = createClass({
|
||||
displayName: "Frame",
|
||||
class Frame extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
// SavedFrame, or an object containing all the required properties.
|
||||
frame: PropTypes.shape({
|
||||
functionDisplayName: PropTypes.string,
|
||||
source: PropTypes.string.isRequired,
|
||||
line: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
|
||||
column: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
|
||||
}).isRequired,
|
||||
// Clicking on the frame link -- probably should link to the debugger.
|
||||
onClick: PropTypes.func.isRequired,
|
||||
// Option to display a function name before the source link.
|
||||
showFunctionName: PropTypes.bool,
|
||||
// Option to display a function name even if it's anonymous.
|
||||
showAnonymousFunctionName: PropTypes.bool,
|
||||
// Option to display a host name after the source link.
|
||||
showHost: PropTypes.bool,
|
||||
// Option to display a host name if the filename is empty or just '/'
|
||||
showEmptyPathAsHost: PropTypes.bool,
|
||||
// Option to display a full source instead of just the filename.
|
||||
showFullSourceUrl: PropTypes.bool,
|
||||
// Service to enable the source map feature for console.
|
||||
sourceMapService: PropTypes.object,
|
||||
};
|
||||
}
|
||||
|
||||
propTypes: {
|
||||
// SavedFrame, or an object containing all the required properties.
|
||||
frame: PropTypes.shape({
|
||||
functionDisplayName: PropTypes.string,
|
||||
source: PropTypes.string.isRequired,
|
||||
line: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
|
||||
column: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
|
||||
}).isRequired,
|
||||
// Clicking on the frame link -- probably should link to the debugger.
|
||||
onClick: PropTypes.func.isRequired,
|
||||
// Option to display a function name before the source link.
|
||||
showFunctionName: PropTypes.bool,
|
||||
// Option to display a function name even if it's anonymous.
|
||||
showAnonymousFunctionName: PropTypes.bool,
|
||||
// Option to display a host name after the source link.
|
||||
showHost: PropTypes.bool,
|
||||
// Option to display a host name if the filename is empty or just '/'
|
||||
showEmptyPathAsHost: PropTypes.bool,
|
||||
// Option to display a full source instead of just the filename.
|
||||
showFullSourceUrl: PropTypes.bool,
|
||||
// Service to enable the source map feature for console.
|
||||
sourceMapService: PropTypes.object,
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
static get defaultProps() {
|
||||
return {
|
||||
showFunctionName: false,
|
||||
showAnonymousFunctionName: false,
|
||||
|
@ -47,7 +47,13 @@ module.exports = createClass({
|
|||
showEmptyPathAsHost: false,
|
||||
showFullSourceUrl: false,
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._locationChanged = this._locationChanged.bind(this);
|
||||
this.getSourceForClick = this.getSourceForClick.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
if (this.props.sourceMapService) {
|
||||
|
@ -55,7 +61,7 @@ module.exports = createClass({
|
|||
this.props.sourceMapService.subscribe(source, line, column,
|
||||
this._locationChanged);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.props.sourceMapService) {
|
||||
|
@ -63,7 +69,7 @@ module.exports = createClass({
|
|||
this.props.sourceMapService.unsubscribe(source, line, column,
|
||||
this._locationChanged);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
_locationChanged(isSourceMapped, url, line, column) {
|
||||
let newState = {
|
||||
|
@ -79,7 +85,7 @@ module.exports = createClass({
|
|||
}
|
||||
|
||||
this.setState(newState);
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to convert the Frame object model to the
|
||||
|
@ -95,7 +101,7 @@ module.exports = createClass({
|
|||
column,
|
||||
functionDisplayName: this.props.frame.functionDisplayName,
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
let frame, isSourceMapped;
|
||||
|
@ -235,4 +241,6 @@ module.exports = createClass({
|
|||
|
||||
return dom.span(attributes, ...elements);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = Frame;
|
||||
|
|
|
@ -25,61 +25,67 @@
|
|||
|
||||
const {
|
||||
DOM: dom,
|
||||
createClass,
|
||||
Component,
|
||||
PropTypes,
|
||||
} = require("devtools/client/shared/vendor/react");
|
||||
const { assert } = require("devtools/shared/DevToolsUtils");
|
||||
|
||||
module.exports = createClass({
|
||||
displayName: "HSplitBox",
|
||||
class HSplitBox extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
// The contents of the start pane.
|
||||
start: PropTypes.any.isRequired,
|
||||
|
||||
propTypes: {
|
||||
// The contents of the start pane.
|
||||
start: PropTypes.any.isRequired,
|
||||
// The contents of the end pane.
|
||||
end: PropTypes.any.isRequired,
|
||||
|
||||
// The contents of the end pane.
|
||||
end: PropTypes.any.isRequired,
|
||||
// The relative width of the start pane, expressed as a number between 0 and
|
||||
// 1. The relative width of the end pane is 1 - startWidth. For example,
|
||||
// with startWidth = .5, both panes are of equal width; with startWidth =
|
||||
// .25, the start panel will take up 1/4 width and the end panel will take
|
||||
// up 3/4 width.
|
||||
startWidth: PropTypes.number,
|
||||
|
||||
// The relative width of the start pane, expressed as a number between 0 and
|
||||
// 1. The relative width of the end pane is 1 - startWidth. For example,
|
||||
// with startWidth = .5, both panes are of equal width; with startWidth =
|
||||
// .25, the start panel will take up 1/4 width and the end panel will take
|
||||
// up 3/4 width.
|
||||
startWidth: PropTypes.number,
|
||||
// A minimum css width value for the start and end panes.
|
||||
minStartWidth: PropTypes.any,
|
||||
minEndWidth: PropTypes.any,
|
||||
|
||||
// A minimum css width value for the start and end panes.
|
||||
minStartWidth: PropTypes.any,
|
||||
minEndWidth: PropTypes.any,
|
||||
// A callback fired when the user drags the splitter to resize the relative
|
||||
// pane widths. The function is passed the startWidth value that would put
|
||||
// the splitter underneath the users mouse.
|
||||
onResize: PropTypes.func.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
// A callback fired when the user drags the splitter to resize the relative
|
||||
// pane widths. The function is passed the startWidth value that would put
|
||||
// the splitter underneath the users mouse.
|
||||
onResize: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
static get defaultProps() {
|
||||
return {
|
||||
startWidth: 0.5,
|
||||
minStartWidth: "20px",
|
||||
minEndWidth: "20px",
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
mouseDown: false
|
||||
};
|
||||
},
|
||||
|
||||
this._onMouseDown = this._onMouseDown.bind(this);
|
||||
this._onMouseUp = this._onMouseUp.bind(this);
|
||||
this._onMouseMove = this._onMouseMove.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.defaultView.top.addEventListener("mouseup", this._onMouseUp);
|
||||
document.defaultView.top.addEventListener("mousemove", this._onMouseMove);
|
||||
},
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.defaultView.top.removeEventListener("mouseup", this._onMouseUp);
|
||||
document.defaultView.top.removeEventListener("mousemove", this._onMouseMove);
|
||||
},
|
||||
}
|
||||
|
||||
_onMouseDown(event) {
|
||||
if (event.button !== 0) {
|
||||
|
@ -88,7 +94,7 @@ module.exports = createClass({
|
|||
|
||||
this.setState({ mouseDown: true });
|
||||
event.preventDefault();
|
||||
},
|
||||
}
|
||||
|
||||
_onMouseUp(event) {
|
||||
if (event.button !== 0 || !this.state.mouseDown) {
|
||||
|
@ -97,7 +103,7 @@ module.exports = createClass({
|
|||
|
||||
this.setState({ mouseDown: false });
|
||||
event.preventDefault();
|
||||
},
|
||||
}
|
||||
|
||||
_onMouseMove(event) {
|
||||
if (!this.state.mouseDown) {
|
||||
|
@ -113,7 +119,7 @@ module.exports = createClass({
|
|||
this.props.onResize(relative / width);
|
||||
|
||||
event.preventDefault();
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
/* eslint-disable no-shadow */
|
||||
|
@ -149,4 +155,6 @@ module.exports = createClass({
|
|||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = HSplitBox;
|
||||
|
|
|
@ -10,7 +10,7 @@ const { LocalizationHelper } = require("devtools/shared/l10n");
|
|||
const l10n = new LocalizationHelper("devtools/client/locales/components.properties");
|
||||
|
||||
// Shortcuts
|
||||
const { PropTypes, createClass, DOM } = React;
|
||||
const { PropTypes, Component, DOM } = React;
|
||||
const { div, span, button } = DOM;
|
||||
|
||||
// Priority Levels
|
||||
|
@ -34,72 +34,81 @@ const PriorityLevels = {
|
|||
* See also MDN for more info about <xul:notificationbox>:
|
||||
* https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/notificationbox
|
||||
*/
|
||||
var NotificationBox = createClass({
|
||||
displayName: "NotificationBox",
|
||||
|
||||
propTypes: {
|
||||
// List of notifications appended into the box.
|
||||
notifications: PropTypes.arrayOf(PropTypes.shape({
|
||||
// label to appear on the notification.
|
||||
label: PropTypes.string.isRequired,
|
||||
|
||||
// Value used to identify the notification
|
||||
value: PropTypes.string.isRequired,
|
||||
|
||||
// URL of image to appear on the notification. If "" then an icon
|
||||
// appropriate for the priority level is used.
|
||||
image: PropTypes.string.isRequired,
|
||||
|
||||
// Notification priority; see Priority Levels.
|
||||
priority: PropTypes.number.isRequired,
|
||||
|
||||
// Array of button descriptions to appear on the notification.
|
||||
buttons: PropTypes.arrayOf(PropTypes.shape({
|
||||
// Function to be called when the button is activated.
|
||||
// This function is passed three arguments:
|
||||
// 1) the NotificationBox component the button is associated with
|
||||
// 2) the button description as passed to appendNotification.
|
||||
// 3) the element which was the target of the button press event.
|
||||
// If the return value from this function is not True, then the
|
||||
// notification is closed. The notification is also not closed
|
||||
// if an error is thrown.
|
||||
callback: PropTypes.func.isRequired,
|
||||
|
||||
// The label to appear on the button.
|
||||
class NotificationBox extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
// List of notifications appended into the box.
|
||||
notifications: PropTypes.arrayOf(PropTypes.shape({
|
||||
// label to appear on the notification.
|
||||
label: PropTypes.string.isRequired,
|
||||
|
||||
// The accesskey attribute set on the <button> element.
|
||||
accesskey: PropTypes.string,
|
||||
// Value used to identify the notification
|
||||
value: PropTypes.string.isRequired,
|
||||
|
||||
// URL of image to appear on the notification. If "" then an icon
|
||||
// appropriate for the priority level is used.
|
||||
image: PropTypes.string.isRequired,
|
||||
|
||||
// Notification priority; see Priority Levels.
|
||||
priority: PropTypes.number.isRequired,
|
||||
|
||||
// Array of button descriptions to appear on the notification.
|
||||
buttons: PropTypes.arrayOf(PropTypes.shape({
|
||||
// Function to be called when the button is activated.
|
||||
// This function is passed three arguments:
|
||||
// 1) the NotificationBox component the button is associated with
|
||||
// 2) the button description as passed to appendNotification.
|
||||
// 3) the element which was the target of the button press event.
|
||||
// If the return value from this function is not True, then the
|
||||
// notification is closed. The notification is also not closed
|
||||
// if an error is thrown.
|
||||
callback: PropTypes.func.isRequired,
|
||||
|
||||
// The label to appear on the button.
|
||||
label: PropTypes.string.isRequired,
|
||||
|
||||
// The accesskey attribute set on the <button> element.
|
||||
accesskey: PropTypes.string,
|
||||
})),
|
||||
|
||||
// A function to call to notify you of interesting things that happen
|
||||
// with the notification box.
|
||||
eventCallback: PropTypes.func,
|
||||
})),
|
||||
|
||||
// A function to call to notify you of interesting things that happen
|
||||
// with the notification box.
|
||||
eventCallback: PropTypes.func,
|
||||
})),
|
||||
// Message that should be shown when hovering over the close button
|
||||
closeButtonTooltip: PropTypes.string
|
||||
};
|
||||
}
|
||||
|
||||
// Message that should be shown when hovering over the close button
|
||||
closeButtonTooltip: PropTypes.string
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
static get defaultProps() {
|
||||
return {
|
||||
closeButtonTooltip: l10n.getStr("notificationBox.closeTooltip")
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
notifications: new Immutable.OrderedMap()
|
||||
};
|
||||
},
|
||||
|
||||
this.appendNotification = this.appendNotification.bind(this);
|
||||
this.removeNotification = this.removeNotification.bind(this);
|
||||
this.getNotificationWithValue = this.getNotificationWithValue.bind(this);
|
||||
this.getCurrentNotification = this.getCurrentNotification.bind(this);
|
||||
this.close = this.close.bind(this);
|
||||
this.renderButton = this.renderButton.bind(this);
|
||||
this.renderNotification = this.renderNotification.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new notification and display it. If another notification is
|
||||
* already present with a higher priority, the new notification will be
|
||||
* added behind it. See `propTypes` for arguments description.
|
||||
*/
|
||||
appendNotification(label, value, image, priority, buttons = [],
|
||||
eventCallback) {
|
||||
appendNotification(label, value, image, priority, buttons = [], eventCallback) {
|
||||
// Priority level must be within expected interval
|
||||
// (see priority levels at the top of this file).
|
||||
if (priority < PriorityLevels.PRIORITY_INFO_LOW ||
|
||||
|
@ -137,14 +146,14 @@ var NotificationBox = createClass({
|
|||
this.setState({
|
||||
notifications: notifications
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove specific notification from the list.
|
||||
*/
|
||||
removeNotification(notification) {
|
||||
this.close(this.state.notifications.get(notification.value));
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object that represents a notification. It can be
|
||||
|
@ -163,11 +172,11 @@ var NotificationBox = createClass({
|
|||
this.close(notification);
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
getCurrentNotification() {
|
||||
return this.state.notifications.first();
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Close specified notification.
|
||||
|
@ -184,7 +193,7 @@ var NotificationBox = createClass({
|
|||
this.setState({
|
||||
notifications: this.state.notifications.remove(notification.value)
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a button. A notification can have a set of custom buttons.
|
||||
|
@ -210,7 +219,7 @@ var NotificationBox = createClass({
|
|||
props.label
|
||||
)
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a notification.
|
||||
|
@ -241,7 +250,7 @@ var NotificationBox = createClass({
|
|||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the top (highest priority) notification. Only one
|
||||
|
@ -256,8 +265,8 @@ var NotificationBox = createClass({
|
|||
return div({className: "notificationbox"},
|
||||
content
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.NotificationBox = NotificationBox;
|
||||
module.exports.PriorityLevels = PriorityLevels;
|
||||
|
|
|
@ -6,31 +6,36 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { DOM: dom, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
|
||||
const { DOM: dom, Component, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
|
||||
const KeyShortcuts = require("devtools/client/shared/key-shortcuts");
|
||||
const AutocompletePopup = createFactory(require("devtools/client/shared/components/AutoCompletePopup"));
|
||||
|
||||
/**
|
||||
* A generic search box component for use across devtools
|
||||
*/
|
||||
module.exports = createClass({
|
||||
displayName: "SearchBox",
|
||||
|
||||
propTypes: {
|
||||
delay: PropTypes.number,
|
||||
keyShortcut: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
placeholder: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
autocompleteProvider: PropTypes.func,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
class SearchBox extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
delay: PropTypes.number,
|
||||
keyShortcut: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
placeholder: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
autocompleteProvider: PropTypes.func,
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
value: "",
|
||||
focused: false,
|
||||
};
|
||||
},
|
||||
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onClearButtonClick = this.onClearButtonClick.bind(this);
|
||||
this.onFocus = this.onFocus.bind(this);
|
||||
this.onBlur = this.onBlur.bind(this);
|
||||
this.onKeyDown = this.onKeyDown.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.keyShortcut) {
|
||||
|
@ -44,7 +49,7 @@ module.exports = createClass({
|
|||
event.preventDefault();
|
||||
this.refs.input.focus();
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.shortcuts) {
|
||||
|
@ -55,7 +60,7 @@ module.exports = createClass({
|
|||
if (this.searchTimeout) {
|
||||
clearTimeout(this.searchTimeout);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
onChange() {
|
||||
if (this.state.value !== this.refs.input.value) {
|
||||
|
@ -81,20 +86,20 @@ module.exports = createClass({
|
|||
this.searchTimeout = null;
|
||||
this.props.onChange(this.state.value);
|
||||
}, this.props.delay);
|
||||
},
|
||||
}
|
||||
|
||||
onClearButtonClick() {
|
||||
this.refs.input.value = "";
|
||||
this.onChange();
|
||||
},
|
||||
}
|
||||
|
||||
onFocus() {
|
||||
this.setState({ focused: true });
|
||||
},
|
||||
}
|
||||
|
||||
onBlur() {
|
||||
this.setState({ focused: false });
|
||||
},
|
||||
}
|
||||
|
||||
onKeyDown(e) {
|
||||
let { autocomplete } = this.refs;
|
||||
|
@ -131,7 +136,7 @@ module.exports = createClass({
|
|||
autocomplete.jumpToBottom();
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
let {
|
||||
|
@ -175,4 +180,6 @@ module.exports = createClass({
|
|||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = SearchBox;
|
||||
|
|
|
@ -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 { button } = DOM;
|
||||
|
@ -15,35 +15,39 @@ const { button } = DOM;
|
|||
* Sidebar toggle button. This button is used to exapand
|
||||
* and collapse Sidebar.
|
||||
*/
|
||||
var SidebarToggle = createClass({
|
||||
displayName: "SidebarToggle",
|
||||
|
||||
propTypes: {
|
||||
// Set to true if collapsed.
|
||||
collapsed: PropTypes.bool.isRequired,
|
||||
// Tooltip text used when the button indicates expanded state.
|
||||
collapsePaneTitle: PropTypes.string.isRequired,
|
||||
// Tooltip text used when the button indicates collapsed state.
|
||||
expandPaneTitle: PropTypes.string.isRequired,
|
||||
// Click callback
|
||||
onClick: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function () {
|
||||
class SidebarToggle extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
collapsed: this.props.collapsed,
|
||||
// Set to true if collapsed.
|
||||
collapsed: PropTypes.bool.isRequired,
|
||||
// Tooltip text used when the button indicates expanded state.
|
||||
collapsePaneTitle: PropTypes.string.isRequired,
|
||||
// Tooltip text used when the button indicates collapsed state.
|
||||
expandPaneTitle: PropTypes.string.isRequired,
|
||||
// Click callback
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
collapsed: props.collapsed,
|
||||
};
|
||||
|
||||
this.onClick = this.onClick.bind(this);
|
||||
}
|
||||
|
||||
// Events
|
||||
|
||||
onClick: function (event) {
|
||||
onClick(event) {
|
||||
this.props.onClick(event);
|
||||
},
|
||||
}
|
||||
|
||||
// Rendering
|
||||
|
||||
render: function () {
|
||||
render() {
|
||||
let title = this.state.collapsed ?
|
||||
this.props.expandPaneTitle :
|
||||
this.props.collapsePaneTitle;
|
||||
|
@ -61,6 +65,6 @@ var SidebarToggle = createClass({
|
|||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = SidebarToggle;
|
||||
|
|
|
@ -5,18 +5,18 @@
|
|||
"use strict";
|
||||
|
||||
const React = require("devtools/client/shared/vendor/react");
|
||||
const { DOM: dom, createClass, createFactory, PropTypes } = React;
|
||||
const { DOM: dom, Component, createFactory, PropTypes } = React;
|
||||
const { LocalizationHelper } = require("devtools/shared/l10n");
|
||||
const Frame = createFactory(require("./Frame"));
|
||||
|
||||
const l10n = new LocalizationHelper("devtools/client/locales/webconsole.properties");
|
||||
|
||||
const AsyncFrame = createFactory(createClass({
|
||||
displayName: "AsyncFrame",
|
||||
|
||||
propTypes: {
|
||||
asyncCause: PropTypes.string.isRequired
|
||||
},
|
||||
class AsyncFrameClass extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
asyncCause: PropTypes.string.isRequired
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
let { asyncCause } = this.props;
|
||||
|
@ -26,18 +26,18 @@ const AsyncFrame = createFactory(createClass({
|
|||
l10n.getFormatStr("stacktrace.asyncStack", asyncCause)
|
||||
);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
const StackTrace = createClass({
|
||||
displayName: "StackTrace",
|
||||
|
||||
propTypes: {
|
||||
stacktrace: PropTypes.array.isRequired,
|
||||
onViewSourceInDebugger: PropTypes.func.isRequired,
|
||||
onViewSourceInScratchpad: PropTypes.func,
|
||||
// Service to enable the source map feature.
|
||||
sourceMapService: PropTypes.object,
|
||||
},
|
||||
class StackTrace extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
stacktrace: PropTypes.array.isRequired,
|
||||
onViewSourceInDebugger: PropTypes.func.isRequired,
|
||||
onViewSourceInScratchpad: PropTypes.func,
|
||||
// Service to enable the source map feature.
|
||||
sourceMapService: PropTypes.object,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
let {
|
||||
|
@ -77,6 +77,8 @@ const StackTrace = createClass({
|
|||
|
||||
return dom.div({ className: "stack-trace" }, frames);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const AsyncFrame = createFactory(AsyncFrameClass);
|
||||
|
||||
module.exports = StackTrace;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"use strict";
|
||||
|
||||
const React = require("devtools/client/shared/vendor/react");
|
||||
const { DOM: dom, createClass, createFactory, PropTypes } = React;
|
||||
const { DOM: dom, Component, createFactory, PropTypes } = React;
|
||||
|
||||
const AUTO_EXPAND_DEPTH = 0;
|
||||
const NUMBER_OF_OFFSCREEN_ITEMS = 1;
|
||||
|
@ -97,158 +97,176 @@ const NUMBER_OF_OFFSCREEN_ITEMS = 1;
|
|||
* }
|
||||
* });
|
||||
*/
|
||||
module.exports = createClass({
|
||||
displayName: "Tree",
|
||||
class Tree extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
// Required props
|
||||
|
||||
propTypes: {
|
||||
// Required props
|
||||
// A function to get an item's parent, or null if it is a root.
|
||||
//
|
||||
// Type: getParent(item: Item) -> Maybe<Item>
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // The parent of this item is stored in its `parent` property.
|
||||
// getParent: item => item.parent
|
||||
getParent: PropTypes.func.isRequired,
|
||||
|
||||
// A function to get an item's parent, or null if it is a root.
|
||||
//
|
||||
// Type: getParent(item: Item) -> Maybe<Item>
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // The parent of this item is stored in its `parent` property.
|
||||
// getParent: item => item.parent
|
||||
getParent: PropTypes.func.isRequired,
|
||||
// A function to get an item's children.
|
||||
//
|
||||
// Type: getChildren(item: Item) -> [Item]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // This item's children are stored in its `children` property.
|
||||
// getChildren: item => item.children
|
||||
getChildren: PropTypes.func.isRequired,
|
||||
|
||||
// A function to get an item's children.
|
||||
//
|
||||
// Type: getChildren(item: Item) -> [Item]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // This item's children are stored in its `children` property.
|
||||
// getChildren: item => item.children
|
||||
getChildren: PropTypes.func.isRequired,
|
||||
// A function which takes an item and ArrowExpander component instance and
|
||||
// returns a component, or text, or anything else that React considers
|
||||
// renderable.
|
||||
//
|
||||
// Type: renderItem(item: Item,
|
||||
// depth: Number,
|
||||
// isFocused: Boolean,
|
||||
// arrow: ReactComponent,
|
||||
// isExpanded: Boolean) -> ReactRenderable
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// renderItem: (item, depth, isFocused, arrow, isExpanded) => {
|
||||
// let className = "my-tree-item";
|
||||
// if (isFocused) {
|
||||
// className += " focused";
|
||||
// }
|
||||
// return dom.div(
|
||||
// {
|
||||
// className,
|
||||
// style: { marginLeft: depth * 10 + "px" }
|
||||
// },
|
||||
// arrow,
|
||||
// dom.span({ className: "my-tree-item-label" }, item.label)
|
||||
// );
|
||||
// },
|
||||
renderItem: PropTypes.func.isRequired,
|
||||
|
||||
// A function which takes an item and ArrowExpander component instance and
|
||||
// returns a component, or text, or anything else that React considers
|
||||
// renderable.
|
||||
//
|
||||
// Type: renderItem(item: Item,
|
||||
// depth: Number,
|
||||
// isFocused: Boolean,
|
||||
// arrow: ReactComponent,
|
||||
// isExpanded: Boolean) -> ReactRenderable
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// renderItem: (item, depth, isFocused, arrow, isExpanded) => {
|
||||
// let className = "my-tree-item";
|
||||
// if (isFocused) {
|
||||
// className += " focused";
|
||||
// }
|
||||
// return dom.div(
|
||||
// {
|
||||
// className,
|
||||
// style: { marginLeft: depth * 10 + "px" }
|
||||
// },
|
||||
// arrow,
|
||||
// dom.span({ className: "my-tree-item-label" }, item.label)
|
||||
// );
|
||||
// },
|
||||
renderItem: PropTypes.func.isRequired,
|
||||
// A function which returns the roots of the tree (forest).
|
||||
//
|
||||
// Type: getRoots() -> [Item]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // In this case, we only have one top level, root item. You could
|
||||
// // return multiple items if you have many top level items in your
|
||||
// // tree.
|
||||
// getRoots: () => [this.props.rootOfMyTree]
|
||||
getRoots: PropTypes.func.isRequired,
|
||||
|
||||
// A function which returns the roots of the tree (forest).
|
||||
//
|
||||
// Type: getRoots() -> [Item]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // In this case, we only have one top level, root item. You could
|
||||
// // return multiple items if you have many top level items in your
|
||||
// // tree.
|
||||
// getRoots: () => [this.props.rootOfMyTree]
|
||||
getRoots: PropTypes.func.isRequired,
|
||||
// A function to get a unique key for the given item. This helps speed up
|
||||
// React's rendering a *TON*.
|
||||
//
|
||||
// Type: getKey(item: Item) -> String
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getKey: item => `my-tree-item-${item.uniqueId}`
|
||||
getKey: PropTypes.func.isRequired,
|
||||
|
||||
// A function to get a unique key for the given item. This helps speed up
|
||||
// React's rendering a *TON*.
|
||||
//
|
||||
// Type: getKey(item: Item) -> String
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getKey: item => `my-tree-item-${item.uniqueId}`
|
||||
getKey: PropTypes.func.isRequired,
|
||||
// A function to get whether an item is expanded or not. If an item is not
|
||||
// expanded, then it must be collapsed.
|
||||
//
|
||||
// Type: isExpanded(item: Item) -> Boolean
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// isExpanded: item => item.expanded,
|
||||
isExpanded: PropTypes.func.isRequired,
|
||||
|
||||
// A function to get whether an item is expanded or not. If an item is not
|
||||
// expanded, then it must be collapsed.
|
||||
//
|
||||
// Type: isExpanded(item: Item) -> Boolean
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// isExpanded: item => item.expanded,
|
||||
isExpanded: PropTypes.func.isRequired,
|
||||
// The height of an item in the tree including margin and padding, in
|
||||
// pixels.
|
||||
itemHeight: PropTypes.number.isRequired,
|
||||
|
||||
// The height of an item in the tree including margin and padding, in
|
||||
// pixels.
|
||||
itemHeight: PropTypes.number.isRequired,
|
||||
// Optional props
|
||||
|
||||
// Optional props
|
||||
// The currently focused item, if any such item exists.
|
||||
focused: PropTypes.any,
|
||||
|
||||
// The currently focused item, if any such item exists.
|
||||
focused: PropTypes.any,
|
||||
// Handle when a new item is focused.
|
||||
onFocus: PropTypes.func,
|
||||
|
||||
// Handle when a new item is focused.
|
||||
onFocus: PropTypes.func,
|
||||
// The depth to which we should automatically expand new items.
|
||||
autoExpandDepth: PropTypes.number,
|
||||
|
||||
// The depth to which we should automatically expand new items.
|
||||
autoExpandDepth: PropTypes.number,
|
||||
// Note: the two properties below are mutually exclusive. Only one of the
|
||||
// label properties is necessary.
|
||||
// ID of an element whose textual content serves as an accessible label for
|
||||
// a tree.
|
||||
labelledby: PropTypes.string,
|
||||
// Accessibility label for a tree widget.
|
||||
label: PropTypes.string,
|
||||
|
||||
// Note: the two properties below are mutually exclusive. Only one of the
|
||||
// label properties is necessary.
|
||||
// ID of an element whose textual content serves as an accessible label for
|
||||
// a tree.
|
||||
labelledby: PropTypes.string,
|
||||
// Accessibility label for a tree widget.
|
||||
label: PropTypes.string,
|
||||
// Optional event handlers for when items are expanded or collapsed. Useful
|
||||
// for dispatching redux events and updating application state, maybe lazily
|
||||
// loading subtrees from a worker, etc.
|
||||
//
|
||||
// Type:
|
||||
// onExpand(item: Item)
|
||||
// onCollapse(item: Item)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// onExpand: item => dispatchExpandActionToRedux(item)
|
||||
onExpand: PropTypes.func,
|
||||
onCollapse: PropTypes.func,
|
||||
};
|
||||
}
|
||||
|
||||
// Optional event handlers for when items are expanded or collapsed. Useful
|
||||
// for dispatching redux events and updating application state, maybe lazily
|
||||
// loading subtrees from a worker, etc.
|
||||
//
|
||||
// Type:
|
||||
// onExpand(item: Item)
|
||||
// onCollapse(item: Item)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// onExpand: item => dispatchExpandActionToRedux(item)
|
||||
onExpand: PropTypes.func,
|
||||
onCollapse: PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
static get defaultProps() {
|
||||
return {
|
||||
autoExpandDepth: AUTO_EXPAND_DEPTH,
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
scroll: 0,
|
||||
height: window.innerHeight,
|
||||
seen: new Set(),
|
||||
};
|
||||
},
|
||||
|
||||
this._onExpand = oncePerAnimationFrame(this._onExpand).bind(this);
|
||||
this._onCollapse = oncePerAnimationFrame(this._onCollapse).bind(this);
|
||||
this._onScroll = oncePerAnimationFrame(this._onScroll).bind(this);
|
||||
this._focusPrevNode = oncePerAnimationFrame(this._focusPrevNode).bind(this);
|
||||
this._focusNextNode = oncePerAnimationFrame(this._focusNextNode).bind(this);
|
||||
this._focusParentNode = oncePerAnimationFrame(this._focusParentNode).bind(this);
|
||||
|
||||
this._autoExpand = this._autoExpand.bind(this);
|
||||
this._preventArrowKeyScrolling = this._preventArrowKeyScrolling.bind(this);
|
||||
this._updateHeight = this._updateHeight.bind(this);
|
||||
this._dfs = this._dfs.bind(this);
|
||||
this._dfsFromRoots = this._dfsFromRoots.bind(this);
|
||||
this._focus = this._focus.bind(this);
|
||||
this._onBlur = this._onBlur.bind(this);
|
||||
this._onKeyDown = this._onKeyDown.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener("resize", this._updateHeight);
|
||||
this._autoExpand();
|
||||
this._updateHeight();
|
||||
},
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this._autoExpand();
|
||||
this._updateHeight();
|
||||
},
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("resize", this._updateHeight);
|
||||
},
|
||||
}
|
||||
|
||||
_autoExpand() {
|
||||
if (!this.props.autoExpandDepth) {
|
||||
|
@ -279,7 +297,7 @@ module.exports = createClass({
|
|||
for (let i = 0; i < length; i++) {
|
||||
autoExpand(roots[i], 0);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
_preventArrowKeyScrolling(e) {
|
||||
switch (e.key) {
|
||||
|
@ -298,7 +316,7 @@ module.exports = createClass({
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the state's height based on clientHeight.
|
||||
|
@ -307,7 +325,7 @@ module.exports = createClass({
|
|||
this.setState({
|
||||
height: this.refs.tree.clientHeight
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a pre-order depth-first search from item.
|
||||
|
@ -332,7 +350,7 @@ module.exports = createClass({
|
|||
}
|
||||
|
||||
return traversal;
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a pre-order depth-first search over the whole forest.
|
||||
|
@ -347,7 +365,7 @@ module.exports = createClass({
|
|||
}
|
||||
|
||||
return traversal;
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands current row.
|
||||
|
@ -355,7 +373,7 @@ module.exports = createClass({
|
|||
* @param {Object} item
|
||||
* @param {Boolean} expandAllChildren
|
||||
*/
|
||||
_onExpand: oncePerAnimationFrame(function (item, expandAllChildren) {
|
||||
_onExpand(item, expandAllChildren) {
|
||||
if (this.props.onExpand) {
|
||||
this.props.onExpand(item);
|
||||
|
||||
|
@ -367,18 +385,18 @@ module.exports = createClass({
|
|||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapses current row.
|
||||
*
|
||||
* @param {Object} item
|
||||
*/
|
||||
_onCollapse: oncePerAnimationFrame(function (item) {
|
||||
_onCollapse(item) {
|
||||
if (this.props.onCollapse) {
|
||||
this.props.onCollapse(item);
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the passed in item to be the focused item.
|
||||
|
@ -411,14 +429,14 @@ module.exports = createClass({
|
|||
if (this.props.onFocus) {
|
||||
this.props.onFocus(item);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the state to have no focused item.
|
||||
*/
|
||||
_onBlur() {
|
||||
this._focus(0, undefined);
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired on a scroll within the tree's container, updates
|
||||
|
@ -426,12 +444,12 @@ module.exports = createClass({
|
|||
*
|
||||
* @param {Event} e
|
||||
*/
|
||||
_onScroll: oncePerAnimationFrame(function (e) {
|
||||
_onScroll(e) {
|
||||
this.setState({
|
||||
scroll: Math.max(this.refs.tree.scrollTop, 0),
|
||||
height: this.refs.tree.clientHeight
|
||||
});
|
||||
}),
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles key down events in the tree's container.
|
||||
|
@ -476,12 +494,12 @@ module.exports = createClass({
|
|||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the previous node relative to the currently focused item, to focused.
|
||||
*/
|
||||
_focusPrevNode: oncePerAnimationFrame(function () {
|
||||
_focusPrevNode() {
|
||||
// Start a depth first search and keep going until we reach the currently
|
||||
// focused node. Focus the previous node in the DFS, if it exists. If it
|
||||
// doesn't exist, we're at the first node already.
|
||||
|
@ -505,13 +523,13 @@ module.exports = createClass({
|
|||
}
|
||||
|
||||
this._focus(prevIndex, prev);
|
||||
}),
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the down arrow key which will focus either the next child
|
||||
* or sibling row.
|
||||
*/
|
||||
_focusNextNode: oncePerAnimationFrame(function () {
|
||||
_focusNextNode() {
|
||||
// Start a depth first search and keep going until we reach the currently
|
||||
// focused node. Focus the next node in the DFS, if it exists. If it
|
||||
// doesn't exist, we're at the last node already.
|
||||
|
@ -530,13 +548,13 @@ module.exports = createClass({
|
|||
if (i + 1 < traversal.length) {
|
||||
this._focus(i + 1, traversal[i + 1].item);
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the left arrow key, going back up to the current rows'
|
||||
* parent row.
|
||||
*/
|
||||
_focusParentNode: oncePerAnimationFrame(function () {
|
||||
_focusParentNode() {
|
||||
const parent = this.props.getParent(this.props.focused);
|
||||
if (!parent) {
|
||||
return;
|
||||
|
@ -552,7 +570,7 @@ module.exports = createClass({
|
|||
}
|
||||
|
||||
this._focus(parentIndex, parent);
|
||||
}),
|
||||
}
|
||||
|
||||
render() {
|
||||
const traversal = this._dfsFromRoots();
|
||||
|
@ -656,28 +674,28 @@ module.exports = createClass({
|
|||
nodes
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* An arrow that displays whether its node is expanded (▼) or collapsed
|
||||
* (▶). When its node has no children, it is hidden.
|
||||
*/
|
||||
const ArrowExpander = createFactory(createClass({
|
||||
displayName: "ArrowExpander",
|
||||
|
||||
propTypes: {
|
||||
item: PropTypes.any.isRequired,
|
||||
visible: PropTypes.bool.isRequired,
|
||||
expanded: PropTypes.bool.isRequired,
|
||||
onCollapse: PropTypes.func.isRequired,
|
||||
onExpand: PropTypes.func.isRequired,
|
||||
},
|
||||
class ArrowExpanderClass extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
item: PropTypes.any.isRequired,
|
||||
visible: PropTypes.bool.isRequired,
|
||||
expanded: PropTypes.bool.isRequired,
|
||||
onCollapse: PropTypes.func.isRequired,
|
||||
onExpand: PropTypes.func.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return this.props.item !== nextProps.item
|
||||
|| this.props.visible !== nextProps.visible
|
||||
|| this.props.expanded !== nextProps.expanded;
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
const attrs = {
|
||||
|
@ -699,24 +717,26 @@ const ArrowExpander = createFactory(createClass({
|
|||
|
||||
return dom.div(attrs);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
const TreeNode = createFactory(createClass({
|
||||
propTypes: {
|
||||
id: PropTypes.any.isRequired,
|
||||
focused: PropTypes.bool.isRequired,
|
||||
item: PropTypes.any.isRequired,
|
||||
expanded: PropTypes.bool.isRequired,
|
||||
hasChildren: PropTypes.bool.isRequired,
|
||||
onExpand: PropTypes.func.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
first: PropTypes.bool,
|
||||
last: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
onCollapse: PropTypes.func.isRequired,
|
||||
depth: PropTypes.number.isRequired,
|
||||
renderItem: PropTypes.func.isRequired,
|
||||
},
|
||||
class TreeNodeClass extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
id: PropTypes.any.isRequired,
|
||||
focused: PropTypes.bool.isRequired,
|
||||
item: PropTypes.any.isRequired,
|
||||
expanded: PropTypes.bool.isRequired,
|
||||
hasChildren: PropTypes.bool.isRequired,
|
||||
onExpand: PropTypes.func.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
first: PropTypes.bool,
|
||||
last: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
onCollapse: PropTypes.func.isRequired,
|
||||
depth: PropTypes.number.isRequired,
|
||||
renderItem: PropTypes.func.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const arrow = ArrowExpander({
|
||||
|
@ -769,7 +789,10 @@ const TreeNode = createFactory(createClass({
|
|||
this.props.expanded),
|
||||
);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
const ArrowExpander = createFactory(ArrowExpanderClass);
|
||||
const TreeNode = createFactory(TreeNodeClass);
|
||||
|
||||
/**
|
||||
* Create a function that calls the given function `fn` only once per animation
|
||||
|
@ -794,3 +817,5 @@ function oncePerAnimationFrame(fn) {
|
|||
});
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Tree;
|
||||
|
|
|
@ -6,18 +6,25 @@
|
|||
|
||||
const React = require("devtools/client/shared/vendor/react");
|
||||
const ReactDOM = require("devtools/client/shared/vendor/react-dom");
|
||||
const { DOM: dom, PropTypes } = React;
|
||||
const { Component, DOM: dom, PropTypes } = React;
|
||||
|
||||
const Draggable = React.createClass({
|
||||
displayName: "Draggable",
|
||||
class Draggable extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
onMove: PropTypes.func.isRequired,
|
||||
onStart: PropTypes.func,
|
||||
onStop: PropTypes.func,
|
||||
style: PropTypes.object,
|
||||
className: PropTypes.string
|
||||
};
|
||||
}
|
||||
|
||||
propTypes: {
|
||||
onMove: PropTypes.func.isRequired,
|
||||
onStart: PropTypes.func,
|
||||
onStop: PropTypes.func,
|
||||
style: PropTypes.object,
|
||||
className: PropTypes.string
|
||||
},
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.startDragging = this.startDragging.bind(this);
|
||||
this.onMove = this.onMove.bind(this);
|
||||
this.onUp = this.onUp.bind(this);
|
||||
}
|
||||
|
||||
startDragging(ev) {
|
||||
ev.preventDefault();
|
||||
|
@ -25,14 +32,14 @@ const Draggable = React.createClass({
|
|||
doc.addEventListener("mousemove", this.onMove);
|
||||
doc.addEventListener("mouseup", this.onUp);
|
||||
this.props.onStart && this.props.onStart();
|
||||
},
|
||||
}
|
||||
|
||||
onMove(ev) {
|
||||
ev.preventDefault();
|
||||
// Use viewport coordinates so, moving mouse over iframes
|
||||
// doesn't mangle (relative) coordinates.
|
||||
this.props.onMove(ev.clientX, ev.clientY);
|
||||
},
|
||||
}
|
||||
|
||||
onUp(ev) {
|
||||
ev.preventDefault();
|
||||
|
@ -40,7 +47,7 @@ const Draggable = React.createClass({
|
|||
doc.removeEventListener("mousemove", this.onMove);
|
||||
doc.removeEventListener("mouseup", this.onUp);
|
||||
this.props.onStop && this.props.onStop();
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
return dom.div({
|
||||
|
@ -49,6 +56,6 @@ const Draggable = React.createClass({
|
|||
onMouseDown: this.startDragging
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = Draggable;
|
||||
|
|
|
@ -7,62 +7,68 @@
|
|||
const React = require("devtools/client/shared/vendor/react");
|
||||
const ReactDOM = require("devtools/client/shared/vendor/react-dom");
|
||||
const Draggable = React.createFactory(require("devtools/client/shared/components/splitter/Draggable"));
|
||||
const { DOM: dom, PropTypes } = React;
|
||||
const { Component, DOM: dom, PropTypes } = React;
|
||||
|
||||
/**
|
||||
* This component represents a Splitter. The splitter supports vertical
|
||||
* as well as horizontal mode.
|
||||
*/
|
||||
const SplitBox = React.createClass({
|
||||
displayName: "SplitBox",
|
||||
class SplitBox extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
// Custom class name. You can use more names separated by a space.
|
||||
className: PropTypes.string,
|
||||
// Initial size of controlled panel.
|
||||
initialSize: PropTypes.string,
|
||||
// Initial width of controlled panel.
|
||||
initialWidth: PropTypes.string,
|
||||
// Initial height of controlled panel.
|
||||
initialHeight: PropTypes.string,
|
||||
// Left/top panel
|
||||
startPanel: PropTypes.any,
|
||||
// Min panel size.
|
||||
minSize: PropTypes.string,
|
||||
// Max panel size.
|
||||
maxSize: PropTypes.string,
|
||||
// Right/bottom panel
|
||||
endPanel: PropTypes.any,
|
||||
// True if the right/bottom panel should be controlled.
|
||||
endPanelControl: PropTypes.bool,
|
||||
// Size of the splitter handle bar.
|
||||
splitterSize: PropTypes.string,
|
||||
// True if the splitter bar is vertical (default is vertical).
|
||||
vert: PropTypes.bool,
|
||||
// Style object.
|
||||
style: PropTypes.object,
|
||||
};
|
||||
}
|
||||
|
||||
propTypes: {
|
||||
// Custom class name. You can use more names separated by a space.
|
||||
className: PropTypes.string,
|
||||
// Initial size of controlled panel.
|
||||
initialSize: PropTypes.string,
|
||||
// Initial width of controlled panel.
|
||||
initialWidth: PropTypes.string,
|
||||
// Initial height of controlled panel.
|
||||
initialHeight: PropTypes.string,
|
||||
// Left/top panel
|
||||
startPanel: PropTypes.any,
|
||||
// Min panel size.
|
||||
minSize: PropTypes.string,
|
||||
// Max panel size.
|
||||
maxSize: PropTypes.string,
|
||||
// Right/bottom panel
|
||||
endPanel: PropTypes.any,
|
||||
// True if the right/bottom panel should be controlled.
|
||||
endPanelControl: PropTypes.bool,
|
||||
// Size of the splitter handle bar.
|
||||
splitterSize: PropTypes.string,
|
||||
// True if the splitter bar is vertical (default is vertical).
|
||||
vert: PropTypes.bool,
|
||||
// Style object.
|
||||
style: PropTypes.object,
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
static get defaultProps() {
|
||||
return {
|
||||
splitterSize: 5,
|
||||
vert: true,
|
||||
endPanelControl: false
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* The state stores the current orientation (vertical or horizontal)
|
||||
* and the current size (width/height). All these values can change
|
||||
* during the component's life time.
|
||||
*/
|
||||
getInitialState() {
|
||||
return {
|
||||
vert: this.props.vert,
|
||||
width: this.props.initialWidth || this.props.initialSize,
|
||||
height: this.props.initialHeight || this.props.initialSize
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
/**
|
||||
* The state stores the current orientation (vertical or horizontal)
|
||||
* and the current size (width/height). All these values can change
|
||||
* during the component's life time.
|
||||
*/
|
||||
this.state = {
|
||||
vert: props.vert,
|
||||
width: props.initialWidth || props.initialSize,
|
||||
height: props.initialHeight || props.initialSize
|
||||
};
|
||||
},
|
||||
|
||||
this.onStartMove = this.onStartMove.bind(this);
|
||||
this.onStopMove = this.onStopMove.bind(this);
|
||||
this.onMove = this.onMove.bind(this);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
let { vert } = nextProps;
|
||||
|
@ -70,7 +76,7 @@ const SplitBox = React.createClass({
|
|||
if (vert !== this.props.vert) {
|
||||
this.setState({ vert });
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return nextState.width != this.state.width ||
|
||||
|
@ -82,7 +88,7 @@ const SplitBox = React.createClass({
|
|||
nextProps.minSize != this.props.minSize ||
|
||||
nextProps.maxSize != this.props.maxSize ||
|
||||
nextProps.splitterSize != this.props.splitterSize;
|
||||
},
|
||||
}
|
||||
|
||||
// Dragging Events
|
||||
|
||||
|
@ -102,7 +108,7 @@ const SplitBox = React.createClass({
|
|||
this.setState({
|
||||
defaultCursor: defaultCursor
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
onStopMove() {
|
||||
const splitBox = ReactDOM.findDOMNode(this);
|
||||
|
@ -110,7 +116,7 @@ const SplitBox = React.createClass({
|
|||
doc.documentElement.style.cursor = this.state.defaultCursor;
|
||||
|
||||
splitBox.classList.remove("dragging");
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust size of the controlled panel. Depending on the current
|
||||
|
@ -149,7 +155,7 @@ const SplitBox = React.createClass({
|
|||
height: size
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Rendering
|
||||
|
||||
|
@ -226,6 +232,6 @@ const SplitBox = React.createClass({
|
|||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = SplitBox;
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { DOM, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
|
||||
const { DOM, Component, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
|
||||
const Tabs = createFactory(require("devtools/client/shared/components/tabs/Tabs").Tabs);
|
||||
|
||||
const Menu = require("devtools/client/framework/menu");
|
||||
|
@ -20,37 +20,50 @@ const { div } = DOM;
|
|||
/**
|
||||
* Renders Tabbar component.
|
||||
*/
|
||||
let Tabbar = createClass({
|
||||
displayName: "Tabbar",
|
||||
class Tabbar extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
children: PropTypes.array,
|
||||
menuDocument: PropTypes.object,
|
||||
onSelect: PropTypes.func,
|
||||
showAllTabsMenu: PropTypes.bool,
|
||||
activeTabId: PropTypes.string,
|
||||
renderOnlySelected: PropTypes.bool,
|
||||
};
|
||||
}
|
||||
|
||||
propTypes: {
|
||||
children: PropTypes.array,
|
||||
menuDocument: PropTypes.object,
|
||||
onSelect: PropTypes.func,
|
||||
showAllTabsMenu: PropTypes.bool,
|
||||
activeTabId: PropTypes.string,
|
||||
renderOnlySelected: PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function () {
|
||||
static get defaultProps() {
|
||||
return {
|
||||
menuDocument: window.parent.document,
|
||||
showAllTabsMenu: false,
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
getInitialState: function () {
|
||||
let { activeTabId, children = [] } = this.props;
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
let { activeTabId, children = [] } = props;
|
||||
let tabs = this.createTabs(children);
|
||||
let activeTab = tabs.findIndex((tab, index) => tab.id === activeTabId);
|
||||
|
||||
return {
|
||||
this.state = {
|
||||
activeTab: activeTab === -1 ? 0 : activeTab,
|
||||
tabs,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function (nextProps) {
|
||||
this.createTabs = this.createTabs.bind(this);
|
||||
this.addTab = this.addTab.bind(this);
|
||||
this.toggleTab = this.toggleTab.bind(this);
|
||||
this.removeTab = this.removeTab.bind(this);
|
||||
this.select = this.select.bind(this);
|
||||
this.getTabIndex = this.getTabIndex.bind(this);
|
||||
this.getTabId = this.getTabId.bind(this);
|
||||
this.getCurrentTabId = this.getCurrentTabId.bind(this);
|
||||
this.onTabChanged = this.onTabChanged.bind(this);
|
||||
this.onAllTabsMenuClick = this.onAllTabsMenuClick.bind(this);
|
||||
this.renderTab = this.renderTab.bind(this);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
let { activeTabId, children = [] } = nextProps;
|
||||
let tabs = this.createTabs(children);
|
||||
let activeTab = tabs.findIndex((tab, index) => tab.id === activeTabId);
|
||||
|
@ -62,9 +75,9 @@ let Tabbar = createClass({
|
|||
tabs,
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
createTabs: function (children) {
|
||||
createTabs(children) {
|
||||
return children
|
||||
.filter((panel) => panel)
|
||||
.map((panel, index) =>
|
||||
|
@ -74,11 +87,11 @@ let Tabbar = createClass({
|
|||
title: panel.props.title,
|
||||
})
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
// Public API
|
||||
|
||||
addTab: function (id, title, selected = false, panel, url, index = -1) {
|
||||
addTab(id, title, selected = false, panel, url, index = -1) {
|
||||
let tabs = this.state.tabs.slice();
|
||||
|
||||
if (index >= 0) {
|
||||
|
@ -100,9 +113,9 @@ let Tabbar = createClass({
|
|||
this.props.onSelect(id);
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
toggleTab: function (tabId, isVisible) {
|
||||
toggleTab(tabId, isVisible) {
|
||||
let index = this.getTabIndex(tabId);
|
||||
if (index < 0) {
|
||||
return;
|
||||
|
@ -116,9 +129,9 @@ let Tabbar = createClass({
|
|||
this.setState(Object.assign({}, this.state, {
|
||||
tabs: tabs,
|
||||
}));
|
||||
},
|
||||
}
|
||||
|
||||
removeTab: function (tabId) {
|
||||
removeTab(tabId) {
|
||||
let index = this.getTabIndex(tabId);
|
||||
if (index < 0) {
|
||||
return;
|
||||
|
@ -137,9 +150,9 @@ let Tabbar = createClass({
|
|||
tabs,
|
||||
activeTab,
|
||||
}));
|
||||
},
|
||||
}
|
||||
|
||||
select: function (tabId) {
|
||||
select(tabId) {
|
||||
let index = this.getTabIndex(tabId);
|
||||
if (index < 0) {
|
||||
return;
|
||||
|
@ -154,11 +167,11 @@ let Tabbar = createClass({
|
|||
this.props.onSelect(tabId);
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
getTabIndex: function (tabId) {
|
||||
getTabIndex(tabId) {
|
||||
let tabIndex = -1;
|
||||
this.state.tabs.forEach((tab, index) => {
|
||||
if (tab.id === tabId) {
|
||||
|
@ -166,19 +179,19 @@ let Tabbar = createClass({
|
|||
}
|
||||
});
|
||||
return tabIndex;
|
||||
},
|
||||
}
|
||||
|
||||
getTabId: function (index) {
|
||||
getTabId(index) {
|
||||
return this.state.tabs[index].id;
|
||||
},
|
||||
}
|
||||
|
||||
getCurrentTabId: function () {
|
||||
getCurrentTabId() {
|
||||
return this.state.tabs[this.state.activeTab].id;
|
||||
},
|
||||
}
|
||||
|
||||
// Event Handlers
|
||||
|
||||
onTabChanged: function (index) {
|
||||
onTabChanged(index) {
|
||||
this.setState({
|
||||
activeTab: index
|
||||
});
|
||||
|
@ -186,9 +199,9 @@ let Tabbar = createClass({
|
|||
if (this.props.onSelect) {
|
||||
this.props.onSelect(this.state.tabs[index].id);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
onAllTabsMenuClick: function (event) {
|
||||
onAllTabsMenuClick(event) {
|
||||
let menu = new Menu();
|
||||
let target = event.target;
|
||||
|
||||
|
@ -214,11 +227,11 @@ let Tabbar = createClass({
|
|||
{ doc: this.props.menuDocument });
|
||||
|
||||
return menu;
|
||||
},
|
||||
}
|
||||
|
||||
// Rendering
|
||||
|
||||
renderTab: function (tab) {
|
||||
renderTab(tab) {
|
||||
if (typeof tab.panel === "function") {
|
||||
return tab.panel({
|
||||
key: tab.id,
|
||||
|
@ -229,9 +242,9 @@ let Tabbar = createClass({
|
|||
}
|
||||
|
||||
return tab.panel;
|
||||
},
|
||||
}
|
||||
|
||||
render: function () {
|
||||
render() {
|
||||
let tabs = this.state.tabs.map((tab) => this.renderTab(tab));
|
||||
|
||||
return (
|
||||
|
@ -247,7 +260,7 @@ let Tabbar = createClass({
|
|||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Tabbar;
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
define(function (require, exports, module) {
|
||||
const React = require("devtools/client/shared/vendor/react");
|
||||
const { DOM } = React;
|
||||
const { Component, DOM } = React;
|
||||
const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
|
||||
|
||||
/**
|
||||
|
@ -31,43 +31,45 @@ define(function (require, exports, module) {
|
|||
* </div>
|
||||
* <div>
|
||||
*/
|
||||
let Tabs = React.createClass({
|
||||
displayName: "Tabs",
|
||||
class Tabs extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
className: React.PropTypes.oneOfType([
|
||||
React.PropTypes.array,
|
||||
React.PropTypes.string,
|
||||
React.PropTypes.object
|
||||
]),
|
||||
tabActive: React.PropTypes.number,
|
||||
onMount: React.PropTypes.func,
|
||||
onBeforeChange: React.PropTypes.func,
|
||||
onAfterChange: React.PropTypes.func,
|
||||
children: React.PropTypes.oneOfType([
|
||||
React.PropTypes.array,
|
||||
React.PropTypes.element
|
||||
]).isRequired,
|
||||
showAllTabsMenu: React.PropTypes.bool,
|
||||
onAllTabsMenuClick: React.PropTypes.func,
|
||||
|
||||
propTypes: {
|
||||
className: React.PropTypes.oneOfType([
|
||||
React.PropTypes.array,
|
||||
React.PropTypes.string,
|
||||
React.PropTypes.object
|
||||
]),
|
||||
tabActive: React.PropTypes.number,
|
||||
onMount: React.PropTypes.func,
|
||||
onBeforeChange: React.PropTypes.func,
|
||||
onAfterChange: React.PropTypes.func,
|
||||
children: React.PropTypes.oneOfType([
|
||||
React.PropTypes.array,
|
||||
React.PropTypes.element
|
||||
]).isRequired,
|
||||
showAllTabsMenu: React.PropTypes.bool,
|
||||
onAllTabsMenuClick: React.PropTypes.func,
|
||||
// Set true will only render selected panel on DOM. It's complete
|
||||
// opposite of the created array, and it's useful if panels content
|
||||
// is unpredictable and update frequently.
|
||||
renderOnlySelected: React.PropTypes.bool,
|
||||
};
|
||||
}
|
||||
|
||||
// Set true will only render selected panel on DOM. It's complete
|
||||
// opposite of the created array, and it's useful if panels content
|
||||
// is unpredictable and update frequently.
|
||||
renderOnlySelected: React.PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function () {
|
||||
static get defaultProps() {
|
||||
return {
|
||||
tabActive: 0,
|
||||
showAllTabsMenu: false,
|
||||
renderOnlySelected: false,
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
getInitialState: function () {
|
||||
return {
|
||||
tabActive: this.props.tabActive,
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
tabActive: props.tabActive,
|
||||
|
||||
// This array is used to store an information whether a tab
|
||||
// at specific index has already been created (e.g. selected
|
||||
|
@ -82,9 +84,17 @@ define(function (require, exports, module) {
|
|||
// True if tabs can't fit into available horizontal space.
|
||||
overflow: false,
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function () {
|
||||
this.onOverflow = this.onOverflow.bind(this);
|
||||
this.onUnderflow = this.onUnderflow.bind(this);
|
||||
this.onKeyDown = this.onKeyDown.bind(this);
|
||||
this.onClickTab = this.onClickTab.bind(this);
|
||||
this.setActive = this.setActive.bind(this);
|
||||
this.renderMenuItems = this.renderMenuItems.bind(this);
|
||||
this.renderPanels = this.renderPanels.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let node = findDOMNode(this);
|
||||
node.addEventListener("keydown", this.onKeyDown);
|
||||
|
||||
|
@ -101,9 +111,9 @@ define(function (require, exports, module) {
|
|||
if (this.props.onMount) {
|
||||
this.props.onMount(index);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
componentWillReceiveProps: function (nextProps) {
|
||||
componentWillReceiveProps(nextProps) {
|
||||
let { children, tabActive } = nextProps;
|
||||
|
||||
// Check type of 'tabActive' props to see if it's valid
|
||||
|
@ -123,9 +133,9 @@ define(function (require, exports, module) {
|
|||
tabActive,
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
componentWillUnmount: function () {
|
||||
componentWillUnmount() {
|
||||
let node = findDOMNode(this);
|
||||
node.removeEventListener("keydown", this.onKeyDown);
|
||||
|
||||
|
@ -133,27 +143,27 @@ define(function (require, exports, module) {
|
|||
node.removeEventListener("overflow", this.onOverflow);
|
||||
node.removeEventListener("underflow", this.onUnderflow);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// DOM Events
|
||||
|
||||
onOverflow: function (event) {
|
||||
onOverflow(event) {
|
||||
if (event.target.classList.contains("tabs-menu")) {
|
||||
this.setState({
|
||||
overflow: true
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
onUnderflow: function (event) {
|
||||
onUnderflow(event) {
|
||||
if (event.target.classList.contains("tabs-menu")) {
|
||||
this.setState({
|
||||
overflow: false
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
onKeyDown: function (event) {
|
||||
onKeyDown(event) {
|
||||
// Bail out if the focus isn't on a tab.
|
||||
if (!event.target.closest(".tabs-menu-item")) {
|
||||
return;
|
||||
|
@ -174,19 +184,19 @@ define(function (require, exports, module) {
|
|||
if (this.state.tabActive != tabActive) {
|
||||
this.setActive(tabActive);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
onClickTab: function (index, event) {
|
||||
onClickTab(index, event) {
|
||||
this.setActive(index);
|
||||
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// API
|
||||
|
||||
setActive: function (index) {
|
||||
setActive(index) {
|
||||
let onAfterChange = this.props.onAfterChange;
|
||||
let onBeforeChange = this.props.onBeforeChange;
|
||||
|
||||
|
@ -217,11 +227,11 @@ define(function (require, exports, module) {
|
|||
onAfterChange(index);
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
// Rendering
|
||||
|
||||
renderMenuItems: function () {
|
||||
renderMenuItems() {
|
||||
if (!this.props.children) {
|
||||
throw new Error("There must be at least one Tab");
|
||||
}
|
||||
|
@ -299,9 +309,9 @@ define(function (require, exports, module) {
|
|||
allTabsMenu
|
||||
)
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
renderPanels: function () {
|
||||
renderPanels() {
|
||||
let { children, renderOnlySelected } = this.props;
|
||||
|
||||
if (!children) {
|
||||
|
@ -359,38 +369,38 @@ define(function (require, exports, module) {
|
|||
panels
|
||||
)
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
render: function () {
|
||||
render() {
|
||||
return (
|
||||
DOM.div({ className: ["tabs", this.props.className].join(" ") },
|
||||
this.renderMenuItems(),
|
||||
this.renderPanels()
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders simple tab 'panel'.
|
||||
*/
|
||||
let Panel = React.createClass({
|
||||
displayName: "Panel",
|
||||
class Panel extends Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
title: React.PropTypes.string.isRequired,
|
||||
children: React.PropTypes.oneOfType([
|
||||
React.PropTypes.array,
|
||||
React.PropTypes.element
|
||||
]).isRequired
|
||||
};
|
||||
}
|
||||
|
||||
propTypes: {
|
||||
title: React.PropTypes.string.isRequired,
|
||||
children: React.PropTypes.oneOfType([
|
||||
React.PropTypes.array,
|
||||
React.PropTypes.element
|
||||
]).isRequired
|
||||
},
|
||||
|
||||
render: function () {
|
||||
render() {
|
||||
return DOM.div({className: "tab-panel"},
|
||||
this.props.children
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Exports from this module
|
||||
exports.TabPanel = Panel;
|
||||
|
|
|
@ -26,8 +26,8 @@ Test all-tabs menu.
|
|||
window.onload = Task.async(function* () {
|
||||
try {
|
||||
const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
|
||||
const React = browserRequire("devtools/client/shared/vendor/react");
|
||||
const Tabbar = React.createFactory(browserRequire("devtools/client/shared/components/tabs/TabBar"));
|
||||
const { Component, createFactory, DOM } = browserRequire("devtools/client/shared/vendor/react");
|
||||
const Tabbar = createFactory(browserRequire("devtools/client/shared/components/tabs/TabBar"));
|
||||
|
||||
// Create container for the TabBar. Set smaller width
|
||||
// to ensure that tabs won't fit and the all-tabs menu
|
||||
|
@ -45,12 +45,14 @@ window.onload = Task.async(function* () {
|
|||
|
||||
const tabbarReact = ReactDOM.render(tabbar, tabBarBox);
|
||||
|
||||
// Test panel.
|
||||
let TabPanel = React.createFactory(React.createClass({
|
||||
render: function () {
|
||||
return React.DOM.div({}, "content");
|
||||
class TabPanelClass extends Component {
|
||||
render() {
|
||||
return DOM.div({}, "content");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Test panel.
|
||||
let TabPanel = createFactory(TabPanelClass);
|
||||
|
||||
// Create a few panels.
|
||||
yield addTabWithPanel(1);
|
||||
|
|
|
@ -8,26 +8,23 @@
|
|||
// Make this available to both AMD and CJS environments
|
||||
define(function (require, exports, module) {
|
||||
// ReactJS
|
||||
const React = require("devtools/client/shared/vendor/react");
|
||||
|
||||
// Shortcuts
|
||||
const { td, span } = React.DOM;
|
||||
const PropTypes = React.PropTypes;
|
||||
const { Component, DOM: dom, PropTypes } =
|
||||
require("devtools/client/shared/vendor/react");
|
||||
|
||||
/**
|
||||
* Render the default cell used for toggle buttons
|
||||
*/
|
||||
let LabelCell = React.createClass({
|
||||
displayName: "LabelCell",
|
||||
|
||||
class LabelCell extends Component {
|
||||
// See the TreeView component for details related
|
||||
// to the 'member' object.
|
||||
propTypes: {
|
||||
id: PropTypes.string.isRequired,
|
||||
member: PropTypes.object.isRequired
|
||||
},
|
||||
static get propTypes() {
|
||||
return {
|
||||
id: PropTypes.string.isRequired,
|
||||
member: PropTypes.object.isRequired
|
||||
};
|
||||
}
|
||||
|
||||
render: function () {
|
||||
render() {
|
||||
let id = this.props.id;
|
||||
let member = this.props.member;
|
||||
let level = member.level || 0;
|
||||
|
@ -49,16 +46,16 @@ define(function (require, exports, module) {
|
|||
}
|
||||
|
||||
return (
|
||||
td({
|
||||
dom.td({
|
||||
className: "treeLabelCell",
|
||||
key: "default",
|
||||
style: rowStyle,
|
||||
role: "presentation"},
|
||||
span({
|
||||
dom.span({
|
||||
className: iconClassList.join(" "),
|
||||
role: "presentation"
|
||||
}),
|
||||
span({
|
||||
dom.span({
|
||||
className: "treeLabel " + member.type + "Label",
|
||||
"aria-labelledby": id,
|
||||
"data-level": level
|
||||
|
@ -66,7 +63,7 @@ define(function (require, exports, module) {
|
|||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Exports from this module
|
||||
module.exports = LabelCell;
|
||||
|
|
|
@ -8,45 +8,48 @@
|
|||
// Make this available to both AMD and CJS environments
|
||||
define(function (require, exports, module) {
|
||||
const React = require("devtools/client/shared/vendor/react");
|
||||
|
||||
// Shortcuts
|
||||
const { Component, PropTypes } = React;
|
||||
const { input, span, td } = React.DOM;
|
||||
const PropTypes = React.PropTypes;
|
||||
|
||||
/**
|
||||
* This template represents a cell in TreeView row. It's rendered
|
||||
* using <td> element (the row is <tr> and the entire tree is <table>).
|
||||
*/
|
||||
let TreeCell = React.createClass({
|
||||
displayName: "TreeCell",
|
||||
|
||||
class TreeCell extends Component {
|
||||
// See TreeView component for detailed property explanation.
|
||||
propTypes: {
|
||||
value: PropTypes.any,
|
||||
decorator: PropTypes.object,
|
||||
id: PropTypes.string.isRequired,
|
||||
member: PropTypes.object.isRequired,
|
||||
renderValue: PropTypes.func.isRequired,
|
||||
enableInput: PropTypes.bool,
|
||||
},
|
||||
|
||||
getInitialState: function () {
|
||||
static get propTypes() {
|
||||
return {
|
||||
value: PropTypes.any,
|
||||
decorator: PropTypes.object,
|
||||
id: PropTypes.string.isRequired,
|
||||
member: PropTypes.object.isRequired,
|
||||
renderValue: PropTypes.func.isRequired,
|
||||
enableInput: PropTypes.bool,
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
inputEnabled: false,
|
||||
};
|
||||
},
|
||||
|
||||
this.getCellClass = this.getCellClass.bind(this);
|
||||
this.updateInputEnabled = this.updateInputEnabled.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimize cell rendering. Rerender cell content only if
|
||||
* the value or expanded state changes.
|
||||
*/
|
||||
shouldComponentUpdate: function (nextProps, nextState) {
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return (this.props.value != nextProps.value) ||
|
||||
(this.state !== nextState) ||
|
||||
(this.props.member.open != nextProps.member.open);
|
||||
},
|
||||
}
|
||||
|
||||
getCellClass: function (object, id) {
|
||||
getCellClass(object, id) {
|
||||
let decorator = this.props.decorator;
|
||||
if (!decorator || !decorator.getCellClass) {
|
||||
return [];
|
||||
|
@ -63,15 +66,15 @@ define(function (require, exports, module) {
|
|||
}
|
||||
|
||||
return classNames;
|
||||
},
|
||||
}
|
||||
|
||||
updateInputEnabled: function (evt) {
|
||||
updateInputEnabled(evt) {
|
||||
this.setState(Object.assign({}, this.state, {
|
||||
inputEnabled: evt.target.nodeName.toLowerCase() !== "input",
|
||||
}));
|
||||
},
|
||||
}
|
||||
|
||||
render: function () {
|
||||
render() {
|
||||
let {
|
||||
member,
|
||||
id,
|
||||
|
@ -127,7 +130,7 @@ define(function (require, exports, module) {
|
|||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Default value rendering.
|
||||
let defaultRenderValue = props => {
|
||||
|
|
|
@ -7,39 +7,41 @@
|
|||
|
||||
// Make this available to both AMD and CJS environments
|
||||
define(function (require, exports, module) {
|
||||
// ReactJS
|
||||
const React = require("devtools/client/shared/vendor/react");
|
||||
|
||||
// Shortcuts
|
||||
const { Component, PropTypes } = React;
|
||||
const { thead, tr, td, div } = React.DOM;
|
||||
const PropTypes = React.PropTypes;
|
||||
|
||||
/**
|
||||
* This component is responsible for rendering tree header.
|
||||
* It's based on <thead> element.
|
||||
*/
|
||||
let TreeHeader = React.createClass({
|
||||
displayName: "TreeHeader",
|
||||
|
||||
class TreeHeader extends Component {
|
||||
// See also TreeView component for detailed info about properties.
|
||||
propTypes: {
|
||||
// Custom tree decorator
|
||||
decorator: PropTypes.object,
|
||||
// True if the header should be visible
|
||||
header: PropTypes.bool,
|
||||
// Array with column definition
|
||||
columns: PropTypes.array
|
||||
},
|
||||
static get propTypes() {
|
||||
return {
|
||||
// Custom tree decorator
|
||||
decorator: PropTypes.object,
|
||||
// True if the header should be visible
|
||||
header: PropTypes.bool,
|
||||
// Array with column definition
|
||||
columns: PropTypes.array
|
||||
};
|
||||
}
|
||||
|
||||
getDefaultProps: function () {
|
||||
static get defaultProps() {
|
||||
return {
|
||||
columns: [{
|
||||
id: "default"
|
||||
}]
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
getHeaderClass: function (colId) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.getHeaderClass = this.getHeaderClass.bind(this);
|
||||
}
|
||||
|
||||
getHeaderClass(colId) {
|
||||
let decorator = this.props.decorator;
|
||||
if (!decorator || !decorator.getHeaderClass) {
|
||||
return [];
|
||||
|
@ -56,9 +58,9 @@ define(function (require, exports, module) {
|
|||
}
|
||||
|
||||
return classNames;
|
||||
},
|
||||
}
|
||||
|
||||
render: function () {
|
||||
render() {
|
||||
let cells = [];
|
||||
let visible = this.props.header;
|
||||
|
||||
|
@ -97,7 +99,7 @@ define(function (require, exports, module) {
|
|||
}, cells))
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Exports from this module
|
||||
module.exports = TreeHeader;
|
||||
|
|
|
@ -7,54 +7,56 @@
|
|||
|
||||
// Make this available to both AMD and CJS environments
|
||||
define(function (require, exports, module) {
|
||||
// ReactJS
|
||||
const React = require("devtools/client/shared/vendor/react");
|
||||
const ReactDOM = require("devtools/client/shared/vendor/react-dom");
|
||||
const { Component, createFactory, PropTypes } = React;
|
||||
const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
|
||||
const { tr } = React.DOM;
|
||||
|
||||
// Tree
|
||||
const TreeCell = React.createFactory(require("./TreeCell"));
|
||||
const LabelCell = React.createFactory(require("./LabelCell"));
|
||||
const TreeCell = createFactory(require("./TreeCell"));
|
||||
const LabelCell = createFactory(require("./LabelCell"));
|
||||
|
||||
// Scroll
|
||||
const { scrollIntoViewIfNeeded } = require("devtools/client/shared/scroll");
|
||||
|
||||
// Shortcuts
|
||||
const { tr } = React.DOM;
|
||||
const PropTypes = React.PropTypes;
|
||||
|
||||
/**
|
||||
* This template represents a node in TreeView component. It's rendered
|
||||
* using <tr> element (the entire tree is one big <table>).
|
||||
*/
|
||||
let TreeRow = React.createClass({
|
||||
displayName: "TreeRow",
|
||||
|
||||
class TreeRow extends Component {
|
||||
// See TreeView component for more details about the props and
|
||||
// the 'member' object.
|
||||
propTypes: {
|
||||
member: PropTypes.shape({
|
||||
object: PropTypes.obSject,
|
||||
name: PropTypes.sring,
|
||||
type: PropTypes.string.isRequired,
|
||||
rowClass: PropTypes.string.isRequired,
|
||||
level: PropTypes.number.isRequired,
|
||||
hasChildren: PropTypes.bool,
|
||||
value: PropTypes.any,
|
||||
open: PropTypes.bool.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
hidden: PropTypes.bool,
|
||||
selected: PropTypes.bool,
|
||||
}),
|
||||
decorator: PropTypes.object,
|
||||
renderCell: PropTypes.object,
|
||||
renderLabelCell: PropTypes.object,
|
||||
columns: PropTypes.array.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
provider: PropTypes.object.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
onMouseOver: PropTypes.func,
|
||||
onMouseOut: PropTypes.func
|
||||
},
|
||||
static get propTypes() {
|
||||
return {
|
||||
member: PropTypes.shape({
|
||||
object: PropTypes.obSject,
|
||||
name: PropTypes.sring,
|
||||
type: PropTypes.string.isRequired,
|
||||
rowClass: PropTypes.string.isRequired,
|
||||
level: PropTypes.number.isRequired,
|
||||
hasChildren: PropTypes.bool,
|
||||
value: PropTypes.any,
|
||||
open: PropTypes.bool.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
hidden: PropTypes.bool,
|
||||
selected: PropTypes.bool,
|
||||
}),
|
||||
decorator: PropTypes.object,
|
||||
renderCell: PropTypes.object,
|
||||
renderLabelCell: PropTypes.object,
|
||||
columns: PropTypes.array.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
provider: PropTypes.object.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
onMouseOver: PropTypes.func,
|
||||
onMouseOut: PropTypes.func
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.getRowClass = this.getRowClass.bind(this);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
// I don't like accessing the underlying DOM elements directly,
|
||||
|
@ -64,16 +66,16 @@ define(function (require, exports, module) {
|
|||
// The important part is that DOM elements don't need to be
|
||||
// re-created when they should appear again.
|
||||
if (nextProps.member.hidden != this.props.member.hidden) {
|
||||
let row = ReactDOM.findDOMNode(this);
|
||||
let row = findDOMNode(this);
|
||||
row.classList.toggle("hidden");
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimize row rendering. If props are the same do not render.
|
||||
* This makes the rendering a lot faster!
|
||||
*/
|
||||
shouldComponentUpdate: function (nextProps) {
|
||||
shouldComponentUpdate(nextProps) {
|
||||
let props = ["name", "open", "value", "loading", "selected", "hasChildren"];
|
||||
for (let p in props) {
|
||||
if (nextProps.member[props[p]] != this.props.member[props[p]]) {
|
||||
|
@ -82,20 +84,20 @@ define(function (require, exports, module) {
|
|||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
}
|
||||
|
||||
componentDidUpdate: function () {
|
||||
componentDidUpdate() {
|
||||
if (this.props.member.selected) {
|
||||
let row = ReactDOM.findDOMNode(this);
|
||||
let row = findDOMNode(this);
|
||||
// Because this is called asynchronously, context window might be
|
||||
// already gone.
|
||||
if (row.ownerDocument.defaultView) {
|
||||
scrollIntoViewIfNeeded(row);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
getRowClass: function (object) {
|
||||
getRowClass(object) {
|
||||
let decorator = this.props.decorator;
|
||||
if (!decorator || !decorator.getRowClass) {
|
||||
return [];
|
||||
|
@ -112,9 +114,9 @@ define(function (require, exports, module) {
|
|||
}
|
||||
|
||||
return classNames;
|
||||
},
|
||||
}
|
||||
|
||||
render: function () {
|
||||
render() {
|
||||
let member = this.props.member;
|
||||
let decorator = this.props.decorator;
|
||||
let props = {
|
||||
|
@ -198,7 +200,7 @@ define(function (require, exports, module) {
|
|||
tr(props, cells)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
|
|
|
@ -7,17 +7,22 @@
|
|||
|
||||
// Make this available to both AMD and CJS environments
|
||||
define(function (require, exports, module) {
|
||||
// ReactJS
|
||||
const React = require("devtools/client/shared/vendor/react");
|
||||
const { cloneElement, Component, createFactory, DOM: dom, PropTypes } =
|
||||
require("devtools/client/shared/vendor/react");
|
||||
|
||||
// Reps
|
||||
const { ObjectProvider } = require("./ObjectProvider");
|
||||
const TreeRow = React.createFactory(require("./TreeRow"));
|
||||
const TreeHeader = React.createFactory(require("./TreeHeader"));
|
||||
const TreeRow = createFactory(require("./TreeRow"));
|
||||
const TreeHeader = createFactory(require("./TreeHeader"));
|
||||
|
||||
// Shortcuts
|
||||
const DOM = React.DOM;
|
||||
const PropTypes = React.PropTypes;
|
||||
const defaultProps = {
|
||||
object: null,
|
||||
renderRow: null,
|
||||
provider: ObjectProvider,
|
||||
expandedNodes: new Set(),
|
||||
expandableStrings: true,
|
||||
columns: []
|
||||
};
|
||||
|
||||
/**
|
||||
* This component represents a tree view with expandable/collapsible nodes.
|
||||
|
@ -53,88 +58,96 @@ define(function (require, exports, module) {
|
|||
* renderLabelCell: function(object);
|
||||
* }
|
||||
*/
|
||||
let TreeView = React.createClass({
|
||||
displayName: "TreeView",
|
||||
|
||||
class TreeView extends Component {
|
||||
// The only required property (not set by default) is the input data
|
||||
// object that is used to puputate the tree.
|
||||
propTypes: {
|
||||
// The input data object.
|
||||
object: PropTypes.any,
|
||||
className: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
// Data provider (see also the interface above)
|
||||
provider: PropTypes.shape({
|
||||
getChildren: PropTypes.func,
|
||||
hasChildren: PropTypes.func,
|
||||
getLabel: PropTypes.func,
|
||||
getValue: PropTypes.func,
|
||||
getKey: PropTypes.func,
|
||||
getType: PropTypes.func,
|
||||
}).isRequired,
|
||||
// Tree decorator (see also the interface above)
|
||||
decorator: PropTypes.shape({
|
||||
getRowClass: PropTypes.func,
|
||||
getCellClass: PropTypes.func,
|
||||
getHeaderClass: PropTypes.func,
|
||||
renderValue: PropTypes.func,
|
||||
static get propTypes() {
|
||||
return {
|
||||
// The input data object.
|
||||
object: PropTypes.any,
|
||||
className: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
// Data provider (see also the interface above)
|
||||
provider: PropTypes.shape({
|
||||
getChildren: PropTypes.func,
|
||||
hasChildren: PropTypes.func,
|
||||
getLabel: PropTypes.func,
|
||||
getValue: PropTypes.func,
|
||||
getKey: PropTypes.func,
|
||||
getType: PropTypes.func,
|
||||
}).isRequired,
|
||||
// Tree decorator (see also the interface above)
|
||||
decorator: PropTypes.shape({
|
||||
getRowClass: PropTypes.func,
|
||||
getCellClass: PropTypes.func,
|
||||
getHeaderClass: PropTypes.func,
|
||||
renderValue: PropTypes.func,
|
||||
renderRow: PropTypes.func,
|
||||
renderCell: PropTypes.func,
|
||||
renderLabelCell: PropTypes.func,
|
||||
}),
|
||||
// Custom tree row (node) renderer
|
||||
renderRow: PropTypes.func,
|
||||
// Custom cell renderer
|
||||
renderCell: PropTypes.func,
|
||||
// Custom value renderef
|
||||
renderValue: PropTypes.func,
|
||||
// Custom tree label (including a toggle button) renderer
|
||||
renderLabelCell: PropTypes.func,
|
||||
}),
|
||||
// Custom tree row (node) renderer
|
||||
renderRow: PropTypes.func,
|
||||
// Custom cell renderer
|
||||
renderCell: PropTypes.func,
|
||||
// Custom value renderef
|
||||
renderValue: PropTypes.func,
|
||||
// Custom tree label (including a toggle button) renderer
|
||||
renderLabelCell: PropTypes.func,
|
||||
// Set of expanded nodes
|
||||
expandedNodes: PropTypes.object,
|
||||
// Custom filtering callback
|
||||
onFilter: PropTypes.func,
|
||||
// Custom sorting callback
|
||||
onSort: PropTypes.func,
|
||||
// A header is displayed if set to true
|
||||
header: PropTypes.bool,
|
||||
// Long string is expandable by a toggle button
|
||||
expandableStrings: PropTypes.bool,
|
||||
// Array of columns
|
||||
columns: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
width: PropTypes.string
|
||||
}))
|
||||
},
|
||||
|
||||
getDefaultProps: function () {
|
||||
return {
|
||||
object: null,
|
||||
renderRow: null,
|
||||
provider: ObjectProvider,
|
||||
expandedNodes: new Set(),
|
||||
expandableStrings: true,
|
||||
columns: []
|
||||
// Set of expanded nodes
|
||||
expandedNodes: PropTypes.object,
|
||||
// Custom filtering callback
|
||||
onFilter: PropTypes.func,
|
||||
// Custom sorting callback
|
||||
onSort: PropTypes.func,
|
||||
// A header is displayed if set to true
|
||||
header: PropTypes.bool,
|
||||
// Long string is expandable by a toggle button
|
||||
expandableStrings: PropTypes.bool,
|
||||
// Array of columns
|
||||
columns: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
width: PropTypes.string
|
||||
}))
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
getInitialState: function () {
|
||||
return {
|
||||
expandedNodes: this.props.expandedNodes,
|
||||
columns: ensureDefaultColumn(this.props.columns),
|
||||
static get defaultProps() {
|
||||
return defaultProps;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
expandedNodes: props.expandedNodes,
|
||||
columns: ensureDefaultColumn(props.columns),
|
||||
selected: null
|
||||
};
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function (nextProps) {
|
||||
this.toggle = this.toggle.bind(this);
|
||||
this.isExpanded = this.isExpanded.bind(this);
|
||||
this.onKeyDown = this.onKeyDown.bind(this);
|
||||
this.onKeyUp = this.onKeyUp.bind(this);
|
||||
this.onClickRow = this.onClickRow.bind(this);
|
||||
this.getSelectedRow = this.getSelectedRow.bind(this);
|
||||
this.selectRow = this.selectRow.bind(this);
|
||||
this.isSelected = this.isSelected.bind(this);
|
||||
this.onFilter = this.onFilter.bind(this);
|
||||
this.onSort = this.onSort.bind(this);
|
||||
this.getMembers = this.getMembers.bind(this);
|
||||
this.renderRows = this.renderRows.bind(this);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
let { expandedNodes } = nextProps;
|
||||
this.setState(Object.assign({}, this.state, {
|
||||
expandedNodes,
|
||||
}));
|
||||
},
|
||||
}
|
||||
|
||||
componentDidUpdate: function () {
|
||||
componentDidUpdate() {
|
||||
let selected = this.getSelectedRow(this.rows);
|
||||
if (!selected && this.rows.length > 0) {
|
||||
// TODO: Do better than just selecting the first row again. We want to
|
||||
|
@ -142,11 +155,58 @@ define(function (require, exports, module) {
|
|||
// row is removed.
|
||||
this.selectRow(this.rows[0].props.member.path);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
static subPath(path, subKey) {
|
||||
return path + "/" + String(subKey).replace(/[\\/]/g, "\\$&");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a set with the paths of the nodes that should be expanded by default
|
||||
* according to the passed options.
|
||||
* @param {Object} The root node of the tree.
|
||||
* @param {Object} [optional] An object with the following optional parameters:
|
||||
* - maxLevel: nodes nested deeper than this level won't be expanded.
|
||||
* - maxNodes: maximum number of nodes that can be expanded. The traversal is
|
||||
breadth-first, so expanding nodes nearer to the root will be preferred.
|
||||
Sibling nodes will either be all expanded or none expanded.
|
||||
* }
|
||||
*/
|
||||
static getExpandedNodes(rootObj, { maxLevel = Infinity, maxNodes = Infinity } = {}) {
|
||||
let expandedNodes = new Set();
|
||||
let queue = [{
|
||||
object: rootObj,
|
||||
level: 1,
|
||||
path: ""
|
||||
}];
|
||||
while (queue.length) {
|
||||
let {object, level, path} = queue.shift();
|
||||
if (Object(object) !== object) {
|
||||
continue;
|
||||
}
|
||||
let keys = Object.keys(object);
|
||||
if (expandedNodes.size + keys.length > maxNodes) {
|
||||
// Avoid having children half expanded.
|
||||
break;
|
||||
}
|
||||
for (let key of keys) {
|
||||
let nodePath = TreeView.subPath(path, key);
|
||||
expandedNodes.add(nodePath);
|
||||
if (level < maxLevel) {
|
||||
queue.push({
|
||||
object: object[key],
|
||||
level: level + 1,
|
||||
path: nodePath
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return expandedNodes;
|
||||
}
|
||||
|
||||
// Node expand/collapse
|
||||
|
||||
toggle: function (nodePath) {
|
||||
toggle(nodePath) {
|
||||
let nodes = this.state.expandedNodes;
|
||||
if (this.isExpanded(nodePath)) {
|
||||
nodes.delete(nodePath);
|
||||
|
@ -158,22 +218,22 @@ define(function (require, exports, module) {
|
|||
this.setState(Object.assign({}, this.state, {
|
||||
expandedNodes: nodes
|
||||
}));
|
||||
},
|
||||
}
|
||||
|
||||
isExpanded: function (nodePath) {
|
||||
isExpanded(nodePath) {
|
||||
return this.state.expandedNodes.has(nodePath);
|
||||
},
|
||||
}
|
||||
|
||||
// Event Handlers
|
||||
|
||||
onKeyDown: function (event) {
|
||||
onKeyDown(event) {
|
||||
if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(
|
||||
event.key)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
onKeyUp: function (event) {
|
||||
onKeyUp(event) {
|
||||
let row = this.getSelectedRow(this.rows);
|
||||
if (!row) {
|
||||
return;
|
||||
|
@ -209,33 +269,33 @@ define(function (require, exports, module) {
|
|||
}
|
||||
|
||||
event.preventDefault();
|
||||
},
|
||||
}
|
||||
|
||||
onClickRow: function (nodePath, event) {
|
||||
onClickRow(nodePath, event) {
|
||||
event.stopPropagation();
|
||||
let cell = event.target.closest("td");
|
||||
if (cell && cell.classList.contains("treeLabelCell")) {
|
||||
this.toggle(nodePath);
|
||||
}
|
||||
this.selectRow(nodePath);
|
||||
},
|
||||
}
|
||||
|
||||
getSelectedRow: function (rows) {
|
||||
getSelectedRow(rows) {
|
||||
if (!this.state.selected || rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return rows.find(row => this.isSelected(row.props.member.path));
|
||||
},
|
||||
}
|
||||
|
||||
selectRow: function (nodePath) {
|
||||
selectRow(nodePath) {
|
||||
this.setState(Object.assign({}, this.state, {
|
||||
selected: nodePath
|
||||
}));
|
||||
},
|
||||
}
|
||||
|
||||
isSelected: function (nodePath) {
|
||||
isSelected(nodePath) {
|
||||
return nodePath === this.state.selected;
|
||||
},
|
||||
}
|
||||
|
||||
// Filtering & Sorting
|
||||
|
||||
|
@ -243,15 +303,15 @@ define(function (require, exports, module) {
|
|||
* Filter out nodes that don't correspond to the current filter.
|
||||
* @return {Boolean} true if the node should be visible otherwise false.
|
||||
*/
|
||||
onFilter: function (object) {
|
||||
onFilter(object) {
|
||||
let onFilter = this.props.onFilter;
|
||||
return onFilter ? onFilter(object) : true;
|
||||
},
|
||||
}
|
||||
|
||||
onSort: function (parent, children) {
|
||||
onSort(parent, children) {
|
||||
let onSort = this.props.onSort;
|
||||
return onSort ? onSort(parent, children) : children;
|
||||
},
|
||||
}
|
||||
|
||||
// Members
|
||||
|
||||
|
@ -259,7 +319,7 @@ define(function (require, exports, module) {
|
|||
* Return children node objects (so called 'members') for given
|
||||
* parent object.
|
||||
*/
|
||||
getMembers: function (parent, level, path) {
|
||||
getMembers(parent, level, path) {
|
||||
// Strings don't have children. Note that 'long' strings are using
|
||||
// the expander icon (+/-) to display the entire original value,
|
||||
// but there are no child items.
|
||||
|
@ -320,12 +380,12 @@ define(function (require, exports, module) {
|
|||
selected: this.isSelected(nodePath)
|
||||
};
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Render tree rows/nodes.
|
||||
*/
|
||||
renderRows: function (parent, level = 0, path = "") {
|
||||
renderRows(parent, level = 0, path = "") {
|
||||
let rows = [];
|
||||
let decorator = this.props.decorator;
|
||||
let renderRow = this.props.renderRow || TreeRow;
|
||||
|
@ -367,7 +427,7 @@ define(function (require, exports, module) {
|
|||
if (!Array.isArray(childRows)) {
|
||||
let lastIndex = rows.length - 1;
|
||||
props.member.loading = true;
|
||||
rows[lastIndex] = React.cloneElement(rows[lastIndex], props);
|
||||
rows[lastIndex] = cloneElement(rows[lastIndex], props);
|
||||
} else {
|
||||
rows = rows.concat(childRows);
|
||||
}
|
||||
|
@ -375,9 +435,9 @@ define(function (require, exports, module) {
|
|||
});
|
||||
|
||||
return rows;
|
||||
},
|
||||
}
|
||||
|
||||
render: function () {
|
||||
render() {
|
||||
let root = this.props.object;
|
||||
let classNames = ["treeTable"];
|
||||
this.rows = [];
|
||||
|
@ -403,7 +463,7 @@ define(function (require, exports, module) {
|
|||
});
|
||||
|
||||
return (
|
||||
DOM.table({
|
||||
dom.table({
|
||||
className: classNames.join(" "),
|
||||
role: "tree",
|
||||
tabIndex: 0,
|
||||
|
@ -414,62 +474,13 @@ define(function (require, exports, module) {
|
|||
cellPadding: 0,
|
||||
cellSpacing: 0},
|
||||
TreeHeader(props),
|
||||
DOM.tbody({
|
||||
dom.tbody({
|
||||
role: "presentation"
|
||||
}, rows)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
TreeView.subPath = function (path, subKey) {
|
||||
return path + "/" + String(subKey).replace(/[\\/]/g, "\\$&");
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a set with the paths of the nodes that should be expanded by default
|
||||
* according to the passed options.
|
||||
* @param {Object} The root node of the tree.
|
||||
* @param {Object} [optional] An object with the following optional parameters:
|
||||
* - maxLevel: nodes nested deeper than this level won't be expanded.
|
||||
* - maxNodes: maximum number of nodes that can be expanded. The traversal is
|
||||
breadth-first, so expanding nodes nearer to the root will be preferred.
|
||||
Sibling nodes will either be all expanded or none expanded.
|
||||
* }
|
||||
*/
|
||||
TreeView.getExpandedNodes = function (rootObj,
|
||||
{ maxLevel = Infinity, maxNodes = Infinity } = {}
|
||||
) {
|
||||
let expandedNodes = new Set();
|
||||
let queue = [{
|
||||
object: rootObj,
|
||||
level: 1,
|
||||
path: ""
|
||||
}];
|
||||
while (queue.length) {
|
||||
let {object, level, path} = queue.shift();
|
||||
if (Object(object) !== object) {
|
||||
continue;
|
||||
}
|
||||
let keys = Object.keys(object);
|
||||
if (expandedNodes.size + keys.length > maxNodes) {
|
||||
// Avoid having children half expanded.
|
||||
break;
|
||||
}
|
||||
for (let key of keys) {
|
||||
let nodePath = TreeView.subPath(path, key);
|
||||
expandedNodes.add(nodePath);
|
||||
if (level < maxLevel) {
|
||||
queue.push({
|
||||
object: object[key],
|
||||
level: level + 1,
|
||||
path: nodePath
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return expandedNodes;
|
||||
};
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
|
|
|
@ -70,6 +70,17 @@ public:
|
|||
}
|
||||
}
|
||||
|
||||
void NotifyEvent(MediaStreamGraph* aGraph, MediaStreamGraphEvent aEvent) override
|
||||
{
|
||||
if (aEvent == MediaStreamGraphEvent::EVENT_REMOVED) {
|
||||
EndStream();
|
||||
mSourceStream->EndAllTrackAndFinish();
|
||||
|
||||
MutexAutoLock lock(mMutex);
|
||||
mImage = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
~StreamListener() { }
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include "AudioStreamTrack.h"
|
||||
#include "Layers.h"
|
||||
#include "MediaStreamGraph.h"
|
||||
#include "MediaStreamGraphImpl.h"
|
||||
#include "MediaStreamListener.h"
|
||||
#include "VideoStreamTrack.h"
|
||||
#include "mozilla/dom/AudioNode.h"
|
||||
|
@ -20,6 +21,7 @@
|
|||
#include "mozilla/dom/LocalMediaStreamBinding.h"
|
||||
#include "mozilla/dom/MediaStreamBinding.h"
|
||||
#include "mozilla/dom/MediaStreamTrackEvent.h"
|
||||
#include "mozilla/dom/Promise.h"
|
||||
#include "mozilla/dom/VideoTrack.h"
|
||||
#include "mozilla/dom/VideoTrackList.h"
|
||||
#include "mozilla/media/MediaUtils.h"
|
||||
|
@ -27,6 +29,7 @@
|
|||
#include "nsIScriptError.h"
|
||||
#include "nsIUUIDGenerator.h"
|
||||
#include "nsPIDOMWindow.h"
|
||||
#include "nsProxyRelease.h"
|
||||
#include "nsRFPService.h"
|
||||
#include "nsServiceManagerUtils.h"
|
||||
|
||||
|
@ -338,9 +341,8 @@ public:
|
|||
explicit PlaybackTrackListener(DOMMediaStream* aStream) :
|
||||
mStream(aStream) {}
|
||||
|
||||
NS_DECL_ISUPPORTS_INHERITED
|
||||
NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(PlaybackTrackListener,
|
||||
MediaStreamTrackConsumer)
|
||||
NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(PlaybackTrackListener)
|
||||
NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(PlaybackTrackListener)
|
||||
|
||||
void NotifyEnded(MediaStreamTrack* aTrack) override
|
||||
{
|
||||
|
@ -364,15 +366,9 @@ protected:
|
|||
RefPtr<DOMMediaStream> mStream;
|
||||
};
|
||||
|
||||
NS_IMPL_ADDREF_INHERITED(DOMMediaStream::PlaybackTrackListener,
|
||||
MediaStreamTrackConsumer)
|
||||
NS_IMPL_RELEASE_INHERITED(DOMMediaStream::PlaybackTrackListener,
|
||||
MediaStreamTrackConsumer)
|
||||
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMMediaStream::PlaybackTrackListener)
|
||||
NS_INTERFACE_MAP_END_INHERITING(MediaStreamTrackConsumer)
|
||||
NS_IMPL_CYCLE_COLLECTION_INHERITED(DOMMediaStream::PlaybackTrackListener,
|
||||
MediaStreamTrackConsumer,
|
||||
mStream)
|
||||
NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(DOMMediaStream::PlaybackTrackListener, AddRef)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(DOMMediaStream::PlaybackTrackListener, Release)
|
||||
NS_IMPL_CYCLE_COLLECTION(DOMMediaStream::PlaybackTrackListener, mStream)
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_CLASS(DOMMediaStream)
|
||||
|
||||
|
@ -580,6 +576,68 @@ DOMMediaStream::CurrentTime()
|
|||
StreamTimeToSeconds(mPlaybackStream->GetCurrentTime() - mLogicalStreamStartTime));
|
||||
}
|
||||
|
||||
already_AddRefed<Promise>
|
||||
DOMMediaStream::CountUnderlyingStreams(const GlobalObject& aGlobal, ErrorResult& aRv)
|
||||
{
|
||||
nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aGlobal.GetAsSupports());
|
||||
if (!window) {
|
||||
aRv.Throw(NS_ERROR_UNEXPECTED);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIGlobalObject> go = do_QueryInterface(aGlobal.GetAsSupports());
|
||||
if (!go) {
|
||||
aRv.Throw(NS_ERROR_UNEXPECTED);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RefPtr<Promise> p = Promise::Create(go, aRv);
|
||||
if (aRv.Failed()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
MediaStreamGraph* graph = MediaStreamGraph::GetInstanceIfExists(window);
|
||||
if (!graph) {
|
||||
p->MaybeResolve(0);
|
||||
return p.forget();
|
||||
}
|
||||
|
||||
auto* graphImpl = static_cast<MediaStreamGraphImpl*>(graph);
|
||||
|
||||
class Counter : public ControlMessage
|
||||
{
|
||||
public:
|
||||
Counter(MediaStreamGraphImpl* aGraph,
|
||||
const RefPtr<Promise>& aPromise)
|
||||
: ControlMessage(nullptr)
|
||||
, mGraph(aGraph)
|
||||
, mPromise(MakeAndAddRef<nsMainThreadPtrHolder<Promise>>("DOMMediaStream::Counter::mPromise", aPromise))
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
}
|
||||
|
||||
void Run() override
|
||||
{
|
||||
nsMainThreadPtrHandle<Promise>& promise = mPromise;
|
||||
uint32_t streams = mGraph->mStreams.Length() +
|
||||
mGraph->mSuspendedStreams.Length();
|
||||
mGraph->DispatchToMainThreadAfterStreamStateUpdate(
|
||||
NewRunnableFrom([promise, streams]() mutable {
|
||||
promise->MaybeResolve(streams);
|
||||
return NS_OK;
|
||||
}));
|
||||
}
|
||||
|
||||
private:
|
||||
// mGraph owns this Counter instance and decides its lifetime.
|
||||
MediaStreamGraphImpl* mGraph;
|
||||
nsMainThreadPtrHandle<Promise> mPromise;
|
||||
};
|
||||
graphImpl->AppendMessage(MakeUnique<Counter>(graphImpl, p));
|
||||
|
||||
return p.forget();
|
||||
}
|
||||
|
||||
void
|
||||
DOMMediaStream::GetId(nsAString& aID) const
|
||||
{
|
||||
|
|
|
@ -360,6 +360,9 @@ public:
|
|||
|
||||
double CurrentTime();
|
||||
|
||||
static already_AddRefed<dom::Promise>
|
||||
CountUnderlyingStreams(const dom::GlobalObject& aGlobal, ErrorResult& aRv);
|
||||
|
||||
void GetId(nsAString& aID) const;
|
||||
|
||||
void GetAudioTracks(nsTArray<RefPtr<AudioStreamTrack> >& aTracks) const;
|
||||
|
|
|
@ -59,6 +59,8 @@ enum SourceMediaStream::TrackCommands : uint32_t {
|
|||
|
||||
/**
|
||||
* A hash table containing the graph instances, one per document.
|
||||
*
|
||||
* The key is a hash of nsPIDOMWindowInner, see `WindowToHash`.
|
||||
*/
|
||||
static nsDataHashtable<nsUint32HashKey, MediaStreamGraphImpl*> gGraphs;
|
||||
|
||||
|
@ -1589,6 +1591,19 @@ public:
|
|||
// teardown and just leak, for safety.
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
for (MediaStream* stream : mGraph->AllStreams()) {
|
||||
// Clean up all MediaSegments since we cannot release Images too
|
||||
// late during shutdown. Also notify listeners that they were removed
|
||||
// so they can clean up any gfx resources.
|
||||
if (SourceMediaStream* source = stream->AsSourceStream()) {
|
||||
// Finishing a SourceStream prevents new data from being appended.
|
||||
source->Finish();
|
||||
}
|
||||
stream->GetStreamTracks().Clear();
|
||||
stream->RemoveAllListenersImpl();
|
||||
}
|
||||
|
||||
mGraph->mForceShutdownTicket = nullptr;
|
||||
|
||||
// We can't block past the final LIFECYCLE_WAITING_FOR_STREAM_DESTRUCTION
|
||||
|
@ -1601,21 +1616,11 @@ public:
|
|||
mGraph->Destroy();
|
||||
} else {
|
||||
// The graph is not empty. We must be in a forced shutdown, or a
|
||||
// non-realtime graph that has finished processing. Some later
|
||||
// AppendMessage will detect that the manager has been emptied, and
|
||||
// non-realtime graph that has finished processing. Some later
|
||||
// AppendMessage will detect that the graph has been emptied, and
|
||||
// delete it.
|
||||
NS_ASSERTION(mGraph->mForceShutDown || !mGraph->mRealtime,
|
||||
"Not in forced shutdown?");
|
||||
for (MediaStream* stream : mGraph->AllStreams()) {
|
||||
// Clean up all MediaSegments since we cannot release Images too
|
||||
// late during shutdown.
|
||||
if (SourceMediaStream* source = stream->AsSourceStream()) {
|
||||
// Finishing a SourceStream prevents new data from being appended.
|
||||
source->Finish();
|
||||
}
|
||||
stream->GetStreamTracks().Clear();
|
||||
}
|
||||
|
||||
mGraph->mLifecycleState =
|
||||
MediaStreamGraphImpl::LIFECYCLE_WAITING_FOR_STREAM_DESTRUCTION;
|
||||
}
|
||||
|
@ -2074,11 +2079,23 @@ MediaStream::EnsureTrack(TrackID aTrackId)
|
|||
void
|
||||
MediaStream::RemoveAllListenersImpl()
|
||||
{
|
||||
for (int32_t i = mListeners.Length() - 1; i >= 0; --i) {
|
||||
RefPtr<MediaStreamListener> listener = mListeners[i].forget();
|
||||
listener->NotifyEvent(GraphImpl(), MediaStreamGraphEvent::EVENT_REMOVED);
|
||||
GraphImpl()->AssertOnGraphThreadOrNotRunning();
|
||||
|
||||
auto streamListeners(mListeners);
|
||||
for (auto& l : streamListeners) {
|
||||
l->NotifyEvent(GraphImpl(), MediaStreamGraphEvent::EVENT_REMOVED);
|
||||
}
|
||||
mListeners.Clear();
|
||||
|
||||
auto trackListeners(mTrackListeners);
|
||||
for (auto& l : trackListeners) {
|
||||
l.mListener->NotifyRemoved();
|
||||
}
|
||||
mTrackListeners.Clear();
|
||||
|
||||
if (SourceMediaStream* source = AsSourceStream()) {
|
||||
source->RemoveAllDirectListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -3119,6 +3136,18 @@ SourceMediaStream::EndAllTrackAndFinish()
|
|||
// we will call NotifyEvent() to let GetUserMedia know
|
||||
}
|
||||
|
||||
void
|
||||
SourceMediaStream::RemoveAllDirectListeners()
|
||||
{
|
||||
GraphImpl()->AssertOnGraphThreadOrNotRunning();
|
||||
|
||||
auto directListeners(mDirectTrackListeners);
|
||||
for (auto& l : directListeners) {
|
||||
l.mListener->NotifyDirectListenerUninstalled();
|
||||
}
|
||||
mDirectTrackListeners.Clear();
|
||||
}
|
||||
|
||||
SourceMediaStream::~SourceMediaStream()
|
||||
{
|
||||
}
|
||||
|
@ -3494,21 +3523,28 @@ uint32_t WindowToHash(nsPIDOMWindowInner* aWindow)
|
|||
return hashkey;
|
||||
}
|
||||
|
||||
MediaStreamGraph*
|
||||
MediaStreamGraph::GetInstanceIfExists(nsPIDOMWindowInner* aWindow)
|
||||
{
|
||||
NS_ASSERTION(NS_IsMainThread(), "Main thread only");
|
||||
|
||||
uint32_t hashkey = WindowToHash(aWindow);
|
||||
|
||||
MediaStreamGraphImpl* graph = nullptr;
|
||||
gGraphs.Get(hashkey, &graph);
|
||||
return graph;
|
||||
}
|
||||
|
||||
MediaStreamGraph*
|
||||
MediaStreamGraph::GetInstance(MediaStreamGraph::GraphDriverType aGraphDriverRequested,
|
||||
nsPIDOMWindowInner* aWindow)
|
||||
{
|
||||
NS_ASSERTION(NS_IsMainThread(), "Main thread only");
|
||||
|
||||
MediaStreamGraphImpl* graph = nullptr;
|
||||
MediaStreamGraphImpl* graph =
|
||||
static_cast<MediaStreamGraphImpl*>(GetInstanceIfExists(aWindow));
|
||||
|
||||
// We hash the nsPIDOMWindowInner to form a key to the gloabl
|
||||
// MediaStreamGraph hashtable. Effectively, this means there is a graph per
|
||||
// document.
|
||||
|
||||
uint32_t hashkey = WindowToHash(aWindow);
|
||||
|
||||
if (!gGraphs.Get(hashkey, &graph)) {
|
||||
if (!graph) {
|
||||
if (!gMediaStreamGraphShutdownBlocker) {
|
||||
|
||||
class Blocker : public media::ShutdownBlocker
|
||||
|
@ -3556,6 +3592,7 @@ MediaStreamGraph::GetInstance(MediaStreamGraph::GraphDriverType aGraphDriverRequ
|
|||
CubebUtils::PreferredSampleRate(),
|
||||
mainThread);
|
||||
|
||||
uint32_t hashkey = WindowToHash(aWindow);
|
||||
gGraphs.Put(hashkey, graph);
|
||||
|
||||
LOG(LogLevel::Debug,
|
||||
|
|
|
@ -784,6 +784,12 @@ public:
|
|||
*/
|
||||
void EndAllTrackAndFinish();
|
||||
|
||||
/**
|
||||
* Removes all direct listeners and signals to them that they have been
|
||||
* uninstalled.
|
||||
*/
|
||||
void RemoveAllDirectListeners();
|
||||
|
||||
void RegisterForAudioMixing();
|
||||
|
||||
/**
|
||||
|
@ -1264,6 +1270,7 @@ public:
|
|||
static const uint32_t AUDIO_CALLBACK_DRIVER_SHUTDOWN_TIMEOUT = 20*1000;
|
||||
|
||||
// Main thread only
|
||||
static MediaStreamGraph* GetInstanceIfExists(nsPIDOMWindowInner* aWindow);
|
||||
static MediaStreamGraph* GetInstance(GraphDriverType aGraphDriverRequested,
|
||||
nsPIDOMWindowInner* aWindow);
|
||||
static MediaStreamGraph* CreateNonRealtimeInstance(
|
||||
|
|
|
@ -57,14 +57,6 @@ MediaStreamTrackSource::ApplyConstraints(
|
|||
return p.forget();
|
||||
}
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaStreamTrackConsumer)
|
||||
NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaStreamTrackConsumer)
|
||||
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaStreamTrackConsumer)
|
||||
NS_INTERFACE_MAP_ENTRY(nsISupports)
|
||||
NS_INTERFACE_MAP_END
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_0(MediaStreamTrackConsumer)
|
||||
|
||||
/**
|
||||
* PrincipalHandleListener monitors changes in PrincipalHandle of the media flowing
|
||||
* through the MediaStreamGraph.
|
||||
|
@ -187,7 +179,6 @@ NS_IMPL_CYCLE_COLLECTION_CLASS(MediaStreamTrack)
|
|||
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(MediaStreamTrack,
|
||||
DOMEventTargetHelper)
|
||||
tmp->Destroy();
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK(mConsumers)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwningStream)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK(mSource)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK(mOriginalTrack)
|
||||
|
@ -197,7 +188,6 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_END
|
|||
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(MediaStreamTrack,
|
||||
DOMEventTargetHelper)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mConsumers)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwningStream)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSource)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOriginalTrack)
|
||||
|
@ -379,10 +369,14 @@ MediaStreamTrack::NotifyEnded()
|
|||
{
|
||||
MOZ_ASSERT(mReadyState == MediaStreamTrackState::Ended);
|
||||
|
||||
for (int32_t i = mConsumers.Length() - 1; i >= 0; --i) {
|
||||
// Loop backwards by index in case the consumer removes itself in the
|
||||
// callback.
|
||||
mConsumers[i]->NotifyEnded(this);
|
||||
auto consumers(mConsumers);
|
||||
for (const auto& consumer : consumers) {
|
||||
if (consumer) {
|
||||
consumer->NotifyEnded(this);
|
||||
} else {
|
||||
MOZ_ASSERT_UNREACHABLE("A consumer was not explicitly removed");
|
||||
mConsumers.RemoveElement(consumer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -405,12 +399,22 @@ MediaStreamTrack::AddConsumer(MediaStreamTrackConsumer* aConsumer)
|
|||
{
|
||||
MOZ_ASSERT(!mConsumers.Contains(aConsumer));
|
||||
mConsumers.AppendElement(aConsumer);
|
||||
|
||||
// Remove destroyed consumers for cleanliness
|
||||
while (mConsumers.RemoveElement(nullptr)) {
|
||||
MOZ_ASSERT_UNREACHABLE("A consumer was not explicitly removed");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
MediaStreamTrack::RemoveConsumer(MediaStreamTrackConsumer* aConsumer)
|
||||
{
|
||||
mConsumers.RemoveElement(aConsumer);
|
||||
|
||||
// Remove destroyed consumers for cleanliness
|
||||
while (mConsumers.RemoveElement(nullptr)) {
|
||||
MOZ_ASSERT_UNREACHABLE("A consumer was not explicitly removed");
|
||||
}
|
||||
}
|
||||
|
||||
already_AddRefed<MediaStreamTrack>
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
#include "mozilla/dom/MediaStreamTrackBinding.h"
|
||||
#include "mozilla/dom/MediaTrackSettingsBinding.h"
|
||||
#include "mozilla/media/MediaUtils.h"
|
||||
#include "mozilla/WeakPtr.h"
|
||||
#include "nsError.h"
|
||||
#include "nsID.h"
|
||||
#include "nsIPrincipal.h"
|
||||
|
@ -221,11 +222,11 @@ protected:
|
|||
* Base class that consumers of a MediaStreamTrack can use to get notifications
|
||||
* about state changes in the track.
|
||||
*/
|
||||
class MediaStreamTrackConsumer : public nsISupports
|
||||
class MediaStreamTrackConsumer
|
||||
: public SupportsWeakPtr<MediaStreamTrackConsumer>
|
||||
{
|
||||
public:
|
||||
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
|
||||
NS_DECL_CYCLE_COLLECTION_CLASS(MediaStreamTrackConsumer)
|
||||
MOZ_DECLARE_WEAKREFERENCE_TYPENAME(MediaStreamTrackConsumer)
|
||||
|
||||
/**
|
||||
* Called when the track's readyState transitions to "ended".
|
||||
|
@ -233,9 +234,6 @@ public:
|
|||
* including MediaStreamTrack::Stop().
|
||||
*/
|
||||
virtual void NotifyEnded(MediaStreamTrack* aTrack) {};
|
||||
|
||||
protected:
|
||||
virtual ~MediaStreamTrackConsumer() {}
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -456,7 +454,7 @@ protected:
|
|||
|
||||
nsTArray<PrincipalChangeObserver<MediaStreamTrack>*> mPrincipalChangeObservers;
|
||||
|
||||
nsTArray<RefPtr<MediaStreamTrackConsumer>> mConsumers;
|
||||
nsTArray<WeakPtr<MediaStreamTrackConsumer>> mConsumers;
|
||||
|
||||
RefPtr<DOMMediaStream> mOwningStream;
|
||||
TrackID mTrackID;
|
||||
|
|
|
@ -632,7 +632,11 @@ MediaEncoder::CreateEncoder(TaskQueue* aEncoderThread,
|
|||
audioEncoder = MakeAndAddRef<OpusTrackEncoder>(aTrackRate);
|
||||
NS_ENSURE_TRUE(audioEncoder, nullptr);
|
||||
}
|
||||
videoEncoder = MakeAndAddRef<VP8TrackEncoder>(aTrackRate);
|
||||
if (Preferences::GetBool("media.recorder.video.frame_drops", true)) {
|
||||
videoEncoder = MakeAndAddRef<VP8TrackEncoder>(aTrackRate, FrameDroppingMode::ALLOW);
|
||||
} else {
|
||||
videoEncoder = MakeAndAddRef<VP8TrackEncoder>(aTrackRate, FrameDroppingMode::DISALLOW);
|
||||
}
|
||||
writer = MakeUnique<WebMWriter>(aTrackTypes);
|
||||
NS_ENSURE_TRUE(writer, nullptr);
|
||||
NS_ENSURE_TRUE(videoEncoder, nullptr);
|
||||
|
|
|
@ -385,10 +385,15 @@ protected:
|
|||
bool mDirectConnected;
|
||||
};
|
||||
|
||||
enum class FrameDroppingMode {
|
||||
ALLOW, // Allowed to drop frames to keep up under load
|
||||
DISALLOW, // Must not drop any frames, even if it means we will OOM
|
||||
};
|
||||
|
||||
class VideoTrackEncoder : public TrackEncoder
|
||||
{
|
||||
public:
|
||||
explicit VideoTrackEncoder(TrackRate aTrackRate)
|
||||
VideoTrackEncoder(TrackRate aTrackRate, FrameDroppingMode aFrameDroppingMode)
|
||||
: TrackEncoder(aTrackRate)
|
||||
, mFrameWidth(0)
|
||||
, mFrameHeight(0)
|
||||
|
@ -396,6 +401,7 @@ public:
|
|||
, mDisplayHeight(0)
|
||||
, mEncodedTicks(0)
|
||||
, mVideoBitrate(0)
|
||||
, mFrameDroppingMode(aFrameDroppingMode)
|
||||
{
|
||||
mLastChunk.mDuration = 0;
|
||||
}
|
||||
|
@ -551,6 +557,12 @@ protected:
|
|||
TimeStamp mSuspendTime;
|
||||
|
||||
uint32_t mVideoBitrate;
|
||||
|
||||
/**
|
||||
* ALLOW to drop frames under load.
|
||||
* DISALLOW to encode all frames, mainly for testing.
|
||||
*/
|
||||
FrameDroppingMode mFrameDroppingMode;
|
||||
};
|
||||
|
||||
} // namespace mozilla
|
||||
|
|
|
@ -56,8 +56,9 @@ GetSourceSurface(already_AddRefed<Image> aImg)
|
|||
return surf.forget();
|
||||
}
|
||||
|
||||
VP8TrackEncoder::VP8TrackEncoder(TrackRate aTrackRate)
|
||||
: VideoTrackEncoder(aTrackRate)
|
||||
VP8TrackEncoder::VP8TrackEncoder(TrackRate aTrackRate,
|
||||
FrameDroppingMode aFrameDroppingMode)
|
||||
: VideoTrackEncoder(aTrackRate, aFrameDroppingMode)
|
||||
, mEncodedTimestamp(0)
|
||||
, mVPXContext(new vpx_codec_ctx_t())
|
||||
, mVPXImageWrapper(new vpx_image_t())
|
||||
|
@ -576,6 +577,10 @@ VP8TrackEncoder::EncodeOperation
|
|||
VP8TrackEncoder::GetNextEncodeOperation(TimeDuration aTimeElapsed,
|
||||
StreamTime aProcessedDuration)
|
||||
{
|
||||
if (mFrameDroppingMode == FrameDroppingMode::DISALLOW) {
|
||||
return ENCODE_NORMAL_FRAME;
|
||||
}
|
||||
|
||||
int64_t durationInUsec =
|
||||
FramesToUsecs(aProcessedDuration, mTrackRate).value();
|
||||
if (aTimeElapsed.ToMicroseconds() > (durationInUsec * SKIP_FRAME_RATIO)) {
|
||||
|
|
|
@ -27,8 +27,9 @@ class VP8TrackEncoder : public VideoTrackEncoder
|
|||
ENCODE_I_FRAME, // The next frame will be encoded as I-Frame.
|
||||
SKIP_FRAME, // Skip the next frame.
|
||||
};
|
||||
|
||||
public:
|
||||
explicit VP8TrackEncoder(TrackRate aTrackRate);
|
||||
VP8TrackEncoder(TrackRate aTrackRate, FrameDroppingMode aFrameDroppingMode);
|
||||
virtual ~VP8TrackEncoder();
|
||||
|
||||
already_AddRefed<TrackMetadataBase> GetMetadata() final override;
|
||||
|
|
|
@ -192,7 +192,7 @@ class TestVP8TrackEncoder: public VP8TrackEncoder
|
|||
{
|
||||
public:
|
||||
explicit TestVP8TrackEncoder(TrackRate aTrackRate = VIDEO_TRACK_RATE)
|
||||
: VP8TrackEncoder(aTrackRate) {}
|
||||
: VP8TrackEncoder(aTrackRate, FrameDroppingMode::DISALLOW) {}
|
||||
|
||||
::testing::AssertionResult TestInit(const InitParam &aParam)
|
||||
{
|
||||
|
|
|
@ -31,7 +31,7 @@ class WebMVP8TrackEncoder: public VP8TrackEncoder
|
|||
{
|
||||
public:
|
||||
explicit WebMVP8TrackEncoder(TrackRate aTrackRate = 90000)
|
||||
: VP8TrackEncoder(aTrackRate) {}
|
||||
: VP8TrackEncoder(aTrackRate, FrameDroppingMode::DISALLOW) {}
|
||||
|
||||
bool TestVP8Creation(int32_t aWidth, int32_t aHeight, int32_t aDisplayWidth,
|
||||
int32_t aDisplayHeight)
|
||||
|
|
|
@ -63,6 +63,7 @@ skip-if = toolkit == 'android' # no windowshare on android
|
|||
[test_getUserMedia_bug1223696.html]
|
||||
[test_getUserMedia_constraints.html]
|
||||
[test_getUserMedia_callbacks.html]
|
||||
[test_getUserMedia_GC_MediaStream.html]
|
||||
[test_getUserMedia_getTrackById.html]
|
||||
[test_getUserMedia_gumWithinGum.html]
|
||||
[test_getUserMedia_loadedmetadata.html]
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<script type="application/javascript" src="mediaStreamPlayback.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<pre id="test">
|
||||
<script type="application/javascript">
|
||||
"use strict";
|
||||
|
||||
createHTML({
|
||||
title: "MediaStreams can be garbage collected",
|
||||
bug: "1407542"
|
||||
});
|
||||
|
||||
let SpecialStream = SpecialPowers.wrap(MediaStream);
|
||||
|
||||
async function testGC(stream, numCopies, copy) {
|
||||
let startStreams = await SpecialStream.countUnderlyingStreams();
|
||||
|
||||
let copies = new Array(numCopies).fill(0).map(() => copy(stream));
|
||||
ok(await SpecialStream.countUnderlyingStreams() > startStreams,
|
||||
"MediaStream constructor creates more underlying streams");
|
||||
|
||||
copies = [];
|
||||
await new Promise(r => SpecialPowers.exactGC(r));
|
||||
is(await SpecialStream.countUnderlyingStreams(), startStreams,
|
||||
"MediaStreams should have been collected");
|
||||
}
|
||||
|
||||
runTest(async () => {
|
||||
let gUMStream = await getUserMedia({video: true});
|
||||
info("Testing GC of copy constructor");
|
||||
await testGC(gUMStream, 10, s => new MediaStream(s));
|
||||
|
||||
info("Testing GC of track-array constructor");
|
||||
await testGC(gUMStream, 10, s => new MediaStream(s.getTracks()));
|
||||
|
||||
info("Testing GC of empty constructor plus addTrack");
|
||||
await testGC(gUMStream, 10, s => {
|
||||
let s2 = new MediaStream();
|
||||
s.getTracks().forEach(t => s2.addTrack(t));
|
||||
return s2;
|
||||
});
|
||||
|
||||
info("Testing GC of track-array constructor with cloned tracks");
|
||||
await testGC(gUMStream, 10, s => new MediaStream(s.getTracks().map(t => t.clone())));
|
||||
|
||||
info("Testing GC of empty constructor plus addTrack with cloned tracks");
|
||||
await testGC(gUMStream, 10, s => {
|
||||
let s2 = new MediaStream();
|
||||
s.getTracks().forEach(t => s2.addTrack(t.clone()));
|
||||
return s2;
|
||||
});
|
||||
|
||||
info("Testing GC of cloned stream");
|
||||
await testGC(gUMStream, 10, s => s.clone());
|
||||
|
||||
info("Testing GC of gUM stream");
|
||||
gUMStream = null;
|
||||
await new Promise(r => SpecialPowers.exactGC(r));
|
||||
is(await SpecialStream.countUnderlyingStreams(), 0,
|
||||
"Original gUM stream should be collectable");
|
||||
});
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
|
@ -40,4 +40,7 @@ interface MediaStream : EventTarget {
|
|||
attribute EventHandler onaddtrack;
|
||||
// attribute EventHandler onremovetrack;
|
||||
readonly attribute double currentTime;
|
||||
|
||||
[ChromeOnly, Throws]
|
||||
static Promise<long> countUnderlyingStreams();
|
||||
};
|
||||
|
|
|
@ -44,7 +44,7 @@ html|input.empty {
|
|||
color: graytext;
|
||||
}
|
||||
|
||||
@media (-moz-windows-default-theme) {
|
||||
@media (-moz-windows-default-theme) and (-moz-os-version: windows-win7) {
|
||||
:root:not(.winxp) html|input.empty {
|
||||
font-style: italic;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
fails-if(Android) fails-if(stylo&&winWidget) skip-if(browserIsRemote&&winWidget) == empty-1.xul empty-ref.xul # Windows: bug 1239170, stylo: bug 1411460
|
||||
fails-if(Android) skip-if(browserIsRemote&&winWidget) == empty-1.xul empty-ref.xul # Windows: bug 1239170
|
||||
!= empty-2.xul empty-ref.xul
|
||||
# There is no way to simulate an autocomplete textbox in windows XP/Vista/7/8/10 default theme using CSS.
|
||||
# Therefore, the equlity tests below should be marked as failing.
|
||||
|
|
|
@ -203,6 +203,7 @@ XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_WRONG_SIGNATURE , "The JAR's signature is wr
|
|||
XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_ENTRY_TOO_LARGE , "An entry in the JAR is too large.")
|
||||
XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_ENTRY_INVALID , "An entry in the JAR is invalid.")
|
||||
XPC_MSG_DEF(NS_ERROR_SIGNED_JAR_MANIFEST_INVALID , "The JAR's manifest or signature file is invalid.")
|
||||
XPC_MSG_DEF(NS_ERROR_CMS_VERIFY_NOT_SIGNED , "The PKCS#7 information is not signed.")
|
||||
|
||||
/* Codes related to signed manifests */
|
||||
XPC_MSG_DEF(NS_ERROR_SIGNED_APP_MANIFEST_INVALID , "The signed app manifest or signature file is invalid.")
|
||||
|
|
|
@ -833,7 +833,8 @@ class ConfigureCodec {
|
|||
mHardwareH264Supported(false),
|
||||
mSoftwareH264Enabled(false),
|
||||
mH264Enabled(false),
|
||||
mVP9Enabled(false),
|
||||
mVP9Enabled(true),
|
||||
mVP9Preferred(false),
|
||||
mH264Level(13), // minimum suggested for WebRTC spec
|
||||
mH264MaxBr(0), // Unlimited
|
||||
mH264MaxMbps(0), // Unlimited
|
||||
|
@ -859,6 +860,9 @@ class ConfigureCodec {
|
|||
branch->GetBoolPref("media.peerconnection.video.vp9_enabled",
|
||||
&mVP9Enabled);
|
||||
|
||||
branch->GetBoolPref("media.peerconnection.video.vp9_preferred",
|
||||
&mVP9Preferred);
|
||||
|
||||
branch->GetIntPref("media.navigator.video.max_fs", &mVP8MaxFs);
|
||||
if (mVP8MaxFs <= 0) {
|
||||
mVP8MaxFs = 12288; // We must specify something other than 0
|
||||
|
@ -930,9 +934,14 @@ class ConfigureCodec {
|
|||
} else if (videoCodec.mName == "ulpfec") {
|
||||
videoCodec.mEnabled = mRedUlpfecEnabled;
|
||||
} else if (videoCodec.mName == "VP8" || videoCodec.mName == "VP9") {
|
||||
if (videoCodec.mName == "VP9" && !mVP9Enabled) {
|
||||
videoCodec.mEnabled = false;
|
||||
break;
|
||||
if (videoCodec.mName == "VP9") {
|
||||
if (!mVP9Enabled) {
|
||||
videoCodec.mEnabled = false;
|
||||
break;
|
||||
}
|
||||
if (mVP9Preferred) {
|
||||
videoCodec.mStronglyPreferred = true;
|
||||
}
|
||||
}
|
||||
videoCodec.mConstraints.maxFs = mVP8MaxFs;
|
||||
videoCodec.mConstraints.maxFps = mVP8MaxFr;
|
||||
|
@ -959,6 +968,7 @@ class ConfigureCodec {
|
|||
bool mSoftwareH264Enabled;
|
||||
bool mH264Enabled;
|
||||
bool mVP9Enabled;
|
||||
bool mVP9Preferred;
|
||||
int32_t mH264Level;
|
||||
int32_t mH264MaxBr;
|
||||
int32_t mH264MaxMbps;
|
||||
|
|
|
@ -18,7 +18,10 @@ public class BookmarkUtils {
|
|||
* full bookmark management features(full-page dialog, bookmark/folder modification, etc.)
|
||||
*/
|
||||
public static boolean isEnabled(Context context) {
|
||||
return AppConstants.NIGHTLY_BUILD &&
|
||||
SwitchBoard.isInExperiment(context, Experiments.FULL_BOOKMARK_MANAGEMENT);
|
||||
final boolean initialized = SwitchBoard.hasExperimentValues(context, Experiments.FULL_BOOKMARK_MANAGEMENT);
|
||||
if (!initialized) {
|
||||
return true;
|
||||
}
|
||||
return SwitchBoard.isInExperiment(context, Experiments.FULL_BOOKMARK_MANAGEMENT);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -485,6 +485,7 @@ pref("media.navigator.video.h264.max_br", 0);
|
|||
pref("media.navigator.video.h264.max_mbps", 0);
|
||||
pref("media.peerconnection.video.h264_enabled", false);
|
||||
pref("media.peerconnection.video.vp9_enabled", true);
|
||||
pref("media.peerconnection.video.vp9_preferred", false);
|
||||
pref("media.getusermedia.aec", 1);
|
||||
pref("media.getusermedia.browser.enabled", false);
|
||||
pref("media.getusermedia.channels", 0);
|
||||
|
@ -616,6 +617,13 @@ pref("media.webspeech.synth.enabled", false);
|
|||
pref("media.encoder.webm.enabled", true);
|
||||
#endif
|
||||
|
||||
// Whether to allow recording of AudioNodes with MediaRecorder
|
||||
pref("media.recorder.audio_node.enabled", false);
|
||||
|
||||
// Whether MediaRecorder's video encoder should allow dropping frames in order
|
||||
// to keep up under load. Useful for tests but beware of memory consumption!
|
||||
pref("media.recorder.video.frame_drops", true);
|
||||
|
||||
// Whether to autostart a media element with an |autoplay| attribute
|
||||
pref("media.autoplay.enabled", true);
|
||||
|
||||
|
|
|
@ -10,15 +10,17 @@
|
|||
#include "CryptoTask.h"
|
||||
#include "NSSCertDBTrustDomain.h"
|
||||
#include "ScopedNSSTypes.h"
|
||||
#include "SharedCertVerifier.h"
|
||||
#include "certdb.h"
|
||||
#include "cms.h"
|
||||
#include "mozilla/Base64.h"
|
||||
#include "mozilla/Casting.h"
|
||||
#include "mozilla/Logging.h"
|
||||
#include "mozilla/RefPtr.h"
|
||||
#include "mozilla/UniquePtr.h"
|
||||
#include "mozilla/Unused.h"
|
||||
#include "nsCOMPtr.h"
|
||||
#include "nsComponentManagerUtils.h"
|
||||
#include "nsDataSignatureVerifier.h"
|
||||
#include "nsDependentString.h"
|
||||
#include "nsHashKeys.h"
|
||||
#include "nsIDirectoryEnumerator.h"
|
||||
|
@ -139,8 +141,8 @@ ReadStream(const nsCOMPtr<nsIInputStream>& stream, /*out*/ SECItem& buf)
|
|||
}
|
||||
|
||||
// Finds exactly one (signature metadata) JAR entry that matches the given
|
||||
// search pattern, and then load it. Fails if there are no matches or if
|
||||
// there is more than one match. If bugDigest is not null then on success
|
||||
// search pattern, and then loads it. Fails if there are no matches or if
|
||||
// there is more than one match. If bufDigest is not null then on success
|
||||
// bufDigest will contain the digeset of the entry using the given digest
|
||||
// algorithm.
|
||||
nsresult
|
||||
|
@ -465,10 +467,10 @@ CheckManifestVersion(const char* & nextLineStart,
|
|||
|
||||
// Parses a signature file (SF) based on the JDK 8 JAR Specification.
|
||||
//
|
||||
// The SF file must contain a SHA1-Digest-Manifest (or, preferrably,
|
||||
// SHA256-Digest-Manifest) attribute in the main section. All other sections are
|
||||
// ignored. This means that this will NOT parse old-style signature files that
|
||||
// have separate digests per entry.
|
||||
// The SF file must contain a SHA*-Digest-Manifest attribute in the main
|
||||
// section (where the * is either 1 or 256, depending on the given digest
|
||||
// algorithm). All other sections are ignored. This means that this will NOT
|
||||
// parse old-style signature files that have separate digests per entry.
|
||||
// The JDK8 x-Digest-Manifest variant is better because:
|
||||
//
|
||||
// (1) It allows us to follow the principle that we should minimize the
|
||||
|
@ -480,11 +482,24 @@ CheckManifestVersion(const char* & nextLineStart,
|
|||
// x-Digest-Manifest instead of multiple x-Digest values.
|
||||
//
|
||||
// filebuf must be null-terminated. On output, mfDigest will contain the
|
||||
// decoded value of SHA1-Digest-Manifest or SHA256-Digest-Manifest, if found,
|
||||
// as well as an identifier indicating which algorithm was found.
|
||||
// decoded value of the appropriate SHA*-DigestManifest, if found.
|
||||
nsresult
|
||||
ParseSF(const char* filebuf, /*out*/ DigestWithAlgorithm& mfDigest)
|
||||
ParseSF(const char* filebuf, SECOidTag digestAlgorithm,
|
||||
/*out*/ nsAutoCString& mfDigest)
|
||||
{
|
||||
const char* digestNameToFind = nullptr;
|
||||
switch (digestAlgorithm) {
|
||||
case SEC_OID_SHA256:
|
||||
digestNameToFind = "sha256-digest-manifest";
|
||||
break;
|
||||
case SEC_OID_SHA1:
|
||||
digestNameToFind = "sha1-digest-manifest";
|
||||
break;
|
||||
default:
|
||||
MOZ_ASSERT_UNREACHABLE("bad argument to ParseSF");
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
const char* nextLineStart = filebuf;
|
||||
nsresult rv = CheckManifestVersion(nextLineStart,
|
||||
NS_LITERAL_CSTRING(JAR_SF_HEADER));
|
||||
|
@ -492,9 +507,6 @@ ParseSF(const char* filebuf, /*out*/ DigestWithAlgorithm& mfDigest)
|
|||
return rv;
|
||||
}
|
||||
|
||||
nsAutoCString savedSHA1Digest;
|
||||
// Search for SHA256-Digest-Manifest and SHA1-Digest-Manifest. Prefer the
|
||||
// former.
|
||||
for (;;) {
|
||||
nsAutoCString curLine;
|
||||
rv = ReadLine(nextLineStart, curLine);
|
||||
|
@ -503,13 +515,8 @@ ParseSF(const char* filebuf, /*out*/ DigestWithAlgorithm& mfDigest)
|
|||
}
|
||||
|
||||
if (curLine.Length() == 0) {
|
||||
// End of main section (blank line or end-of-file). We didn't find
|
||||
// SHA256-Digest-Manifest, but maybe we found SHA1-Digest-Manifest.
|
||||
if (!savedSHA1Digest.IsEmpty()) {
|
||||
mfDigest.mDigest.Assign(savedSHA1Digest);
|
||||
mfDigest.mAlgorithm = SEC_OID_SHA1;
|
||||
return NS_OK;
|
||||
}
|
||||
// End of main section (blank line or end-of-file). We didn't find the
|
||||
// SHA*-Digest-Manifest we were looking for.
|
||||
return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID;
|
||||
}
|
||||
|
||||
|
@ -520,12 +527,11 @@ ParseSF(const char* filebuf, /*out*/ DigestWithAlgorithm& mfDigest)
|
|||
return rv;
|
||||
}
|
||||
|
||||
if (attrName.LowerCaseEqualsLiteral("sha256-digest-manifest")) {
|
||||
rv = Base64Decode(attrValue, mfDigest.mDigest);
|
||||
if (attrName.EqualsIgnoreCase(digestNameToFind)) {
|
||||
rv = Base64Decode(attrValue, mfDigest);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
mfDigest.mAlgorithm = SEC_OID_SHA256;
|
||||
|
||||
// There could be multiple SHA*-Digest-Manifest attributes, which
|
||||
// would be an error, but it's better to just skip any erroneous
|
||||
|
@ -538,18 +544,11 @@ ParseSF(const char* filebuf, /*out*/ DigestWithAlgorithm& mfDigest)
|
|||
return NS_OK;
|
||||
}
|
||||
|
||||
if (attrName.LowerCaseEqualsLiteral("sha1-digest-manifest") &&
|
||||
savedSHA1Digest.IsEmpty()) {
|
||||
rv = Base64Decode(attrValue, savedSHA1Digest);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
|
||||
// ignore unrecognized attributes
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
MOZ_ASSERT_UNREACHABLE("somehow exited loop in ParseSF without returning");
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
// Parses MANIFEST.MF. The filenames of all entries will be returned in
|
||||
|
@ -702,23 +701,16 @@ ParseMF(const char* filebuf, nsIZipReader* zip, SECOidTag digestAlgorithm,
|
|||
return NS_OK;
|
||||
}
|
||||
|
||||
struct VerifyCertificateContext {
|
||||
AppTrustedRoot trustedRoot;
|
||||
UniqueCERTCertList& builtChain;
|
||||
};
|
||||
|
||||
nsresult
|
||||
VerifyCertificate(CERTCertificate* signerCert, void* voidContext, void* pinArg)
|
||||
VerifyCertificate(CERTCertificate* signerCert, AppTrustedRoot trustedRoot,
|
||||
/*out*/ UniqueCERTCertList& builtChain)
|
||||
{
|
||||
// TODO: null pinArg is tolerated.
|
||||
if (NS_WARN_IF(!signerCert) || NS_WARN_IF(!voidContext)) {
|
||||
if (NS_WARN_IF(!signerCert)) {
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
const VerifyCertificateContext& context =
|
||||
*static_cast<const VerifyCertificateContext*>(voidContext);
|
||||
|
||||
AppTrustDomain trustDomain(context.builtChain, pinArg);
|
||||
nsresult rv = trustDomain.SetTrustedRoot(context.trustedRoot);
|
||||
// TODO: pinArg is null.
|
||||
AppTrustDomain trustDomain(builtChain, nullptr);
|
||||
nsresult rv = trustDomain.SetTrustedRoot(trustedRoot);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
|
@ -763,23 +755,151 @@ VerifyCertificate(CERTCertificate* signerCert, void* voidContext, void* pinArg)
|
|||
return NS_OK;
|
||||
}
|
||||
|
||||
// Given a SECOidTag representing a digest algorithm (either SEC_OID_SHA1 or
|
||||
// SEC_OID_SHA256), returns the first signerInfo in the given signedData that
|
||||
// purports to have been created using that digest algorithm, or nullptr if
|
||||
// there is none.
|
||||
// The returned signerInfo is owned by signedData, so the caller must ensure
|
||||
// that the lifetime of the signerInfo is contained by the lifetime of the
|
||||
// signedData.
|
||||
NSSCMSSignerInfo*
|
||||
GetSignerInfoForDigestAlgorithm(NSSCMSSignedData* signedData,
|
||||
SECOidTag digestAlgorithm)
|
||||
{
|
||||
MOZ_ASSERT(digestAlgorithm == SEC_OID_SHA1 ||
|
||||
digestAlgorithm == SEC_OID_SHA256);
|
||||
if (digestAlgorithm != SEC_OID_SHA1 && digestAlgorithm != SEC_OID_SHA256) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int numSigners = NSS_CMSSignedData_SignerInfoCount(signedData);
|
||||
if (numSigners < 1) {
|
||||
return nullptr;
|
||||
}
|
||||
for (int i = 0; i < numSigners; i++) {
|
||||
NSSCMSSignerInfo* signerInfo =
|
||||
NSS_CMSSignedData_GetSignerInfo(signedData, i);
|
||||
// NSS_CMSSignerInfo_GetDigestAlgTag isn't exported from NSS.
|
||||
SECOidData* digestAlgOID = SECOID_FindOID(&signerInfo->digestAlg.algorithm);
|
||||
if (!digestAlgOID) {
|
||||
continue;
|
||||
}
|
||||
if (digestAlgorithm == digestAlgOID->offset) {
|
||||
return signerInfo;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
nsresult
|
||||
VerifySignature(AppTrustedRoot trustedRoot, const SECItem& buffer,
|
||||
const SECItem& detachedDigest,
|
||||
const SECItem& detachedSHA1Digest,
|
||||
const SECItem& detachedSHA256Digest,
|
||||
/*out*/ SECOidTag& digestAlgorithm,
|
||||
/*out*/ UniqueCERTCertList& builtChain)
|
||||
{
|
||||
// Currently, this function is only called within the CalculateResult() method
|
||||
// of CryptoTasks. As such, NSS should not be shut down at this point and the
|
||||
// CryptoTask implementation should already hold a nsNSSShutDownPreventionLock.
|
||||
// We acquire a nsNSSShutDownPreventionLock here solely to prove we did to
|
||||
// VerifyCMSDetachedSignatureIncludingCertificate().
|
||||
nsNSSShutDownPreventionLock locker;
|
||||
VerifyCertificateContext context = { trustedRoot, builtChain };
|
||||
// XXX: missing pinArg
|
||||
return VerifyCMSDetachedSignatureIncludingCertificate(buffer, detachedDigest,
|
||||
VerifyCertificate,
|
||||
&context, nullptr,
|
||||
locker);
|
||||
|
||||
if (NS_WARN_IF(!buffer.data || buffer.len == 0 || !detachedSHA1Digest.data ||
|
||||
detachedSHA1Digest.len == 0 || !detachedSHA256Digest.data ||
|
||||
detachedSHA256Digest.len == 0)) {
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
UniqueNSSCMSMessage
|
||||
cmsMsg(NSS_CMSMessage_CreateFromDER(const_cast<SECItem*>(&buffer), nullptr,
|
||||
nullptr, nullptr, nullptr, nullptr,
|
||||
nullptr));
|
||||
if (!cmsMsg) {
|
||||
return NS_ERROR_CMS_VERIFY_NOT_SIGNED;
|
||||
}
|
||||
|
||||
if (!NSS_CMSMessage_IsSigned(cmsMsg.get())) {
|
||||
return NS_ERROR_CMS_VERIFY_NOT_SIGNED;
|
||||
}
|
||||
|
||||
NSSCMSContentInfo* cinfo = NSS_CMSMessage_ContentLevel(cmsMsg.get(), 0);
|
||||
if (!cinfo) {
|
||||
return NS_ERROR_CMS_VERIFY_NO_CONTENT_INFO;
|
||||
}
|
||||
|
||||
// We're expecting this to be a PKCS#7 signedData content info.
|
||||
if (NSS_CMSContentInfo_GetContentTypeTag(cinfo)
|
||||
!= SEC_OID_PKCS7_SIGNED_DATA) {
|
||||
return NS_ERROR_CMS_VERIFY_NO_CONTENT_INFO;
|
||||
}
|
||||
|
||||
// signedData is non-owning
|
||||
NSSCMSSignedData* signedData =
|
||||
static_cast<NSSCMSSignedData*>(NSS_CMSContentInfo_GetContent(cinfo));
|
||||
if (!signedData) {
|
||||
return NS_ERROR_CMS_VERIFY_NO_CONTENT_INFO;
|
||||
}
|
||||
|
||||
// Parse the certificates into CERTCertificate objects held in memory so
|
||||
// verifyCertificate will be able to find them during path building.
|
||||
UniqueCERTCertList certs(CERT_NewCertList());
|
||||
if (!certs) {
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
if (signedData->rawCerts) {
|
||||
for (size_t i = 0; signedData->rawCerts[i]; ++i) {
|
||||
UniqueCERTCertificate
|
||||
cert(CERT_NewTempCertificate(CERT_GetDefaultCertDB(),
|
||||
signedData->rawCerts[i], nullptr, false,
|
||||
true));
|
||||
// Skip certificates that fail to parse
|
||||
if (!cert) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (CERT_AddCertToListTail(certs.get(), cert.get()) != SECSuccess) {
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
Unused << cert.release(); // Ownership transferred to the cert list.
|
||||
}
|
||||
}
|
||||
|
||||
NSSCMSSignerInfo* signerInfo =
|
||||
GetSignerInfoForDigestAlgorithm(signedData, SEC_OID_SHA256);
|
||||
const SECItem* detachedDigest = &detachedSHA256Digest;
|
||||
digestAlgorithm = SEC_OID_SHA256;
|
||||
if (!signerInfo) {
|
||||
signerInfo = GetSignerInfoForDigestAlgorithm(signedData, SEC_OID_SHA1);
|
||||
if (!signerInfo) {
|
||||
return NS_ERROR_CMS_VERIFY_NOT_SIGNED;
|
||||
}
|
||||
detachedDigest = &detachedSHA1Digest;
|
||||
digestAlgorithm = SEC_OID_SHA1;
|
||||
}
|
||||
|
||||
// Get the end-entity certificate.
|
||||
CERTCertificate* signerCert =
|
||||
NSS_CMSSignerInfo_GetSigningCertificate(signerInfo,
|
||||
CERT_GetDefaultCertDB());
|
||||
if (!signerCert) {
|
||||
return NS_ERROR_CMS_VERIFY_ERROR_PROCESSING;
|
||||
}
|
||||
|
||||
nsresult rv = VerifyCertificate(signerCert, trustedRoot, builtChain);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
// Ensure that the PKCS#7 data OID is present as the PKCS#9 contentType.
|
||||
const char* pkcs7DataOidString = "1.2.840.113549.1.7.1";
|
||||
ScopedAutoSECItem pkcs7DataOid;
|
||||
if (SEC_StringToOID(nullptr, &pkcs7DataOid, pkcs7DataOidString, 0)
|
||||
!= SECSuccess) {
|
||||
return NS_ERROR_CMS_VERIFY_ERROR_PROCESSING;
|
||||
}
|
||||
|
||||
return MapSECStatus(
|
||||
NSS_CMSSignerInfo_Verify(signerInfo, const_cast<SECItem*>(detachedDigest),
|
||||
&pkcs7DataOid));
|
||||
}
|
||||
|
||||
nsresult
|
||||
|
@ -818,24 +938,39 @@ OpenSignedAppFile(AppTrustedRoot aTrustedRoot, nsIFile* aJarFile,
|
|||
// Signature (SF) file
|
||||
nsAutoCString sfFilename;
|
||||
ScopedAutoSECItem sfBuffer;
|
||||
Digest sfCalculatedDigest;
|
||||
rv = FindAndLoadOneEntry(zip, NS_LITERAL_CSTRING(JAR_SF_SEARCH_STRING),
|
||||
sfFilename, sfBuffer, SEC_OID_SHA1,
|
||||
&sfCalculatedDigest);
|
||||
sfFilename, sfBuffer);
|
||||
if (NS_FAILED(rv)) {
|
||||
return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID;
|
||||
}
|
||||
|
||||
sigBuffer.type = siBuffer;
|
||||
UniqueCERTCertList builtChain;
|
||||
rv = VerifySignature(aTrustedRoot, sigBuffer, sfCalculatedDigest.get(),
|
||||
builtChain);
|
||||
// Calculate both the SHA-1 and SHA-256 hashes of the signature file - we
|
||||
// don't know what algorithm the PKCS#7 signature used.
|
||||
Digest sfCalculatedSHA1Digest;
|
||||
rv = sfCalculatedSHA1Digest.DigestBuf(SEC_OID_SHA1, sfBuffer.data,
|
||||
sfBuffer.len - 1);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
Digest sfCalculatedSHA256Digest;
|
||||
rv = sfCalculatedSHA256Digest.DigestBuf(SEC_OID_SHA256, sfBuffer.data,
|
||||
sfBuffer.len - 1);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
DigestWithAlgorithm mfDigest;
|
||||
rv = ParseSF(BitwiseCast<char*, unsigned char*>(sfBuffer.data), mfDigest);
|
||||
sigBuffer.type = siBuffer;
|
||||
UniqueCERTCertList builtChain;
|
||||
SECOidTag digestToUse;
|
||||
rv = VerifySignature(aTrustedRoot, sigBuffer, sfCalculatedSHA1Digest.get(),
|
||||
sfCalculatedSHA256Digest.get(), digestToUse, builtChain);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
nsAutoCString mfDigest;
|
||||
rv = ParseSF(BitwiseCast<char*, unsigned char*>(sfBuffer.data), digestToUse,
|
||||
mfDigest);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
|
@ -845,7 +980,7 @@ OpenSignedAppFile(AppTrustedRoot aTrustedRoot, nsIFile* aJarFile,
|
|||
ScopedAutoSECItem manifestBuffer;
|
||||
Digest mfCalculatedDigest;
|
||||
rv = FindAndLoadOneEntry(zip, NS_LITERAL_CSTRING(JAR_MF_SEARCH_STRING),
|
||||
mfFilename, manifestBuffer, mfDigest.mAlgorithm,
|
||||
mfFilename, manifestBuffer, digestToUse,
|
||||
&mfCalculatedDigest);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
|
@ -853,7 +988,7 @@ OpenSignedAppFile(AppTrustedRoot aTrustedRoot, nsIFile* aJarFile,
|
|||
|
||||
nsDependentCSubstring calculatedDigest(
|
||||
DigestToDependentString(mfCalculatedDigest));
|
||||
if (!mfDigest.mDigest.Equals(calculatedDigest)) {
|
||||
if (!mfDigest.Equals(calculatedDigest)) {
|
||||
return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID;
|
||||
}
|
||||
|
||||
|
@ -865,7 +1000,7 @@ OpenSignedAppFile(AppTrustedRoot aTrustedRoot, nsIFile* aJarFile,
|
|||
nsTHashtable<nsCStringHashKey> items;
|
||||
|
||||
rv = ParseMF(BitwiseCast<char*, unsigned char*>(manifestBuffer.data), zip,
|
||||
mfDigest.mAlgorithm, items, buf);
|
||||
digestToUse, items, buf);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
|
@ -1393,25 +1528,40 @@ VerifySignedDirectory(AppTrustedRoot aTrustedRoot,
|
|||
+ NS_LITERAL_STRING("sf"));
|
||||
|
||||
ScopedAutoSECItem sfBuffer;
|
||||
Digest sfCalculatedDigest;
|
||||
rv = LoadOneMetafile(metaDir, sfFilename, sfBuffer, SEC_OID_SHA1,
|
||||
&sfCalculatedDigest);
|
||||
rv = LoadOneMetafile(metaDir, sfFilename, sfBuffer);
|
||||
if (NS_FAILED(rv)) {
|
||||
return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID;
|
||||
}
|
||||
|
||||
// Calculate both the SHA-1 and SHA-256 hashes of the signature file - we
|
||||
// don't know what algorithm the PKCS#7 signature used.
|
||||
Digest sfCalculatedSHA1Digest;
|
||||
rv = sfCalculatedSHA1Digest.DigestBuf(SEC_OID_SHA1, sfBuffer.data,
|
||||
sfBuffer.len - 1);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
Digest sfCalculatedSHA256Digest;
|
||||
rv = sfCalculatedSHA256Digest.DigestBuf(SEC_OID_SHA256, sfBuffer.data,
|
||||
sfBuffer.len - 1);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
sigBuffer.type = siBuffer;
|
||||
UniqueCERTCertList builtChain;
|
||||
rv = VerifySignature(aTrustedRoot, sigBuffer, sfCalculatedDigest.get(),
|
||||
builtChain);
|
||||
SECOidTag digestToUse;
|
||||
rv = VerifySignature(aTrustedRoot, sigBuffer, sfCalculatedSHA1Digest.get(),
|
||||
sfCalculatedSHA256Digest.get(), digestToUse, builtChain);
|
||||
if (NS_FAILED(rv)) {
|
||||
return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID;
|
||||
}
|
||||
|
||||
// Get the expected manifest hash from the signed .sf file
|
||||
|
||||
DigestWithAlgorithm mfDigest;
|
||||
rv = ParseSF(BitwiseCast<char*, unsigned char*>(sfBuffer.data), mfDigest);
|
||||
nsAutoCString mfDigest;
|
||||
rv = ParseSF(BitwiseCast<char*, unsigned char*>(sfBuffer.data), digestToUse,
|
||||
mfDigest);
|
||||
if (NS_FAILED(rv)) {
|
||||
return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID;
|
||||
}
|
||||
|
@ -1421,7 +1571,7 @@ VerifySignedDirectory(AppTrustedRoot aTrustedRoot,
|
|||
nsAutoString mfFilename(NS_LITERAL_STRING("manifest.mf"));
|
||||
ScopedAutoSECItem manifestBuffer;
|
||||
Digest mfCalculatedDigest;
|
||||
rv = LoadOneMetafile(metaDir, mfFilename, manifestBuffer, mfDigest.mAlgorithm,
|
||||
rv = LoadOneMetafile(metaDir, mfFilename, manifestBuffer, digestToUse,
|
||||
&mfCalculatedDigest);
|
||||
if (NS_FAILED(rv)) {
|
||||
return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID;
|
||||
|
@ -1429,7 +1579,7 @@ VerifySignedDirectory(AppTrustedRoot aTrustedRoot,
|
|||
|
||||
nsDependentCSubstring calculatedDigest(
|
||||
DigestToDependentString(mfCalculatedDigest));
|
||||
if (!mfDigest.mDigest.Equals(calculatedDigest)) {
|
||||
if (!mfDigest.Equals(calculatedDigest)) {
|
||||
return NS_ERROR_SIGNED_JAR_MANIFEST_INVALID;
|
||||
}
|
||||
|
||||
|
@ -1442,7 +1592,7 @@ VerifySignedDirectory(AppTrustedRoot aTrustedRoot,
|
|||
|
||||
nsTHashtable<nsStringHashKey> items;
|
||||
rv = ParseMFUnpacked(BitwiseCast<char*, unsigned char*>(manifestBuffer.data),
|
||||
aDirectory, mfDigest.mAlgorithm, items, buf);
|
||||
aDirectory, digestToUse, items, buf);
|
||||
if (NS_FAILED(rv)){
|
||||
return rv;
|
||||
}
|
||||
|
|
|
@ -5,22 +5,12 @@
|
|||
#include "nsDataSignatureVerifier.h"
|
||||
|
||||
#include "ScopedNSSTypes.h"
|
||||
#include "SharedCertVerifier.h"
|
||||
#include "cms.h"
|
||||
#include "cryptohi.h"
|
||||
#include "keyhi.h"
|
||||
#include "mozilla/Base64.h"
|
||||
#include "mozilla/Casting.h"
|
||||
#include "mozilla/Unused.h"
|
||||
#include "nsCOMPtr.h"
|
||||
#include "nsNSSComponent.h"
|
||||
#include "nsString.h"
|
||||
#include "pkix/pkixnss.h"
|
||||
#include "pkix/pkixtypes.h"
|
||||
#include "secerr.h"
|
||||
|
||||
using namespace mozilla;
|
||||
using namespace mozilla::pkix;
|
||||
using namespace mozilla::psm;
|
||||
|
||||
SEC_ASN1_MKSUB(SECOID_AlgorithmIDTemplate)
|
||||
|
@ -134,117 +124,3 @@ nsDataSignatureVerifier::VerifyData(const nsACString& aData,
|
|||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
nsresult
|
||||
VerifyCMSDetachedSignatureIncludingCertificate(
|
||||
const SECItem& buffer, const SECItem& detachedDigest,
|
||||
nsresult (*verifyCertificate)(CERTCertificate* cert, void* context,
|
||||
void* pinArg),
|
||||
void* verifyCertificateContext, void* pinArg,
|
||||
const nsNSSShutDownPreventionLock& /*proofOfLock*/)
|
||||
{
|
||||
// XXX: missing pinArg is tolerated.
|
||||
if (NS_WARN_IF(!buffer.data && buffer.len > 0) ||
|
||||
NS_WARN_IF(!detachedDigest.data && detachedDigest.len > 0) ||
|
||||
(!verifyCertificate) ||
|
||||
NS_WARN_IF(!verifyCertificateContext)) {
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
UniqueNSSCMSMessage
|
||||
cmsMsg(NSS_CMSMessage_CreateFromDER(const_cast<SECItem*>(&buffer), nullptr,
|
||||
nullptr, nullptr, nullptr, nullptr,
|
||||
nullptr));
|
||||
if (!cmsMsg) {
|
||||
return NS_ERROR_CMS_VERIFY_ERROR_PROCESSING;
|
||||
}
|
||||
|
||||
if (!NSS_CMSMessage_IsSigned(cmsMsg.get())) {
|
||||
return NS_ERROR_CMS_VERIFY_NOT_SIGNED;
|
||||
}
|
||||
|
||||
NSSCMSContentInfo* cinfo = NSS_CMSMessage_ContentLevel(cmsMsg.get(), 0);
|
||||
if (!cinfo) {
|
||||
return NS_ERROR_CMS_VERIFY_NO_CONTENT_INFO;
|
||||
}
|
||||
|
||||
// We're expecting this to be a PKCS#7 signedData content info.
|
||||
if (NSS_CMSContentInfo_GetContentTypeTag(cinfo)
|
||||
!= SEC_OID_PKCS7_SIGNED_DATA) {
|
||||
return NS_ERROR_CMS_VERIFY_NO_CONTENT_INFO;
|
||||
}
|
||||
|
||||
// signedData is non-owning
|
||||
NSSCMSSignedData* signedData =
|
||||
static_cast<NSSCMSSignedData*>(NSS_CMSContentInfo_GetContent(cinfo));
|
||||
if (!signedData) {
|
||||
return NS_ERROR_CMS_VERIFY_NO_CONTENT_INFO;
|
||||
}
|
||||
|
||||
// Set digest value.
|
||||
if (NSS_CMSSignedData_SetDigestValue(signedData, SEC_OID_SHA1,
|
||||
const_cast<SECItem*>(&detachedDigest))) {
|
||||
return NS_ERROR_CMS_VERIFY_BAD_DIGEST;
|
||||
}
|
||||
|
||||
// Parse the certificates into CERTCertificate objects held in memory so
|
||||
// verifyCertificate will be able to find them during path building.
|
||||
UniqueCERTCertList certs(CERT_NewCertList());
|
||||
if (!certs) {
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
if (signedData->rawCerts) {
|
||||
for (size_t i = 0; signedData->rawCerts[i]; ++i) {
|
||||
UniqueCERTCertificate
|
||||
cert(CERT_NewTempCertificate(CERT_GetDefaultCertDB(),
|
||||
signedData->rawCerts[i], nullptr, false,
|
||||
true));
|
||||
// Skip certificates that fail to parse
|
||||
if (!cert) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (CERT_AddCertToListTail(certs.get(), cert.get()) != SECSuccess) {
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
Unused << cert.release(); // Ownership transferred to the cert list.
|
||||
}
|
||||
}
|
||||
|
||||
// Get the end-entity certificate.
|
||||
int numSigners = NSS_CMSSignedData_SignerInfoCount(signedData);
|
||||
if (NS_WARN_IF(numSigners != 1)) {
|
||||
return NS_ERROR_CMS_VERIFY_ERROR_PROCESSING;
|
||||
}
|
||||
// signer is non-owning.
|
||||
NSSCMSSignerInfo* signer = NSS_CMSSignedData_GetSignerInfo(signedData, 0);
|
||||
if (NS_WARN_IF(!signer)) {
|
||||
return NS_ERROR_CMS_VERIFY_ERROR_PROCESSING;
|
||||
}
|
||||
CERTCertificate* signerCert =
|
||||
NSS_CMSSignerInfo_GetSigningCertificate(signer, CERT_GetDefaultCertDB());
|
||||
if (!signerCert) {
|
||||
return NS_ERROR_CMS_VERIFY_ERROR_PROCESSING;
|
||||
}
|
||||
|
||||
nsresult rv = verifyCertificate(signerCert, verifyCertificateContext, pinArg);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
// See NSS_CMSContentInfo_GetContentTypeOID, which isn't exported from NSS.
|
||||
SECOidData* contentTypeOidData =
|
||||
SECOID_FindOID(&signedData->contentInfo.contentType);
|
||||
if (!contentTypeOidData) {
|
||||
return NS_ERROR_CMS_VERIFY_ERROR_PROCESSING;
|
||||
}
|
||||
|
||||
return MapSECStatus(NSS_CMSSignerInfo_Verify(signer,
|
||||
const_cast<SECItem*>(&detachedDigest),
|
||||
&contentTypeOidData->oid));
|
||||
}
|
||||
|
||||
} // namespace mozilla
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
#ifndef nsDataSignatureVerifier_h
|
||||
#define nsDataSignatureVerifier_h
|
||||
|
||||
#include "certt.h"
|
||||
#include "nsIDataSignatureVerifier.h"
|
||||
#include "nsNSSShutDown.h"
|
||||
|
||||
|
@ -33,15 +32,4 @@ private:
|
|||
virtual void virtualDestroyNSSReference() override {}
|
||||
};
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
nsresult VerifyCMSDetachedSignatureIncludingCertificate(
|
||||
const SECItem& buffer, const SECItem& detachedDigest,
|
||||
nsresult (*verifyCertificate)(CERTCertificate* cert, void* context,
|
||||
void* pinArg),
|
||||
void* verifyCertificateContext, void* pinArg,
|
||||
const nsNSSShutDownPreventionLock& proofOfLock);
|
||||
|
||||
} // namespace mozilla
|
||||
|
||||
#endif // nsDataSignatureVerifier_h
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
#include "sslproto.h"
|
||||
|
||||
using namespace mozilla;
|
||||
using namespace mozilla::pkix;
|
||||
using namespace mozilla::psm;
|
||||
|
||||
extern LazyLogModule gPIPNSSLog;
|
||||
|
|
|
@ -50,6 +50,7 @@ const SEC_ERROR_UNTRUSTED_CERT = SEC_ERROR_BASE + 21;
|
|||
const SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE = SEC_ERROR_BASE + 30;
|
||||
const SEC_ERROR_CA_CERT_INVALID = SEC_ERROR_BASE + 36;
|
||||
const SEC_ERROR_UNKNOWN_CRITICAL_EXTENSION = SEC_ERROR_BASE + 41;
|
||||
const SEC_ERROR_PKCS7_BAD_SIGNATURE = SEC_ERROR_BASE + 47;
|
||||
const SEC_ERROR_INADEQUATE_KEY_USAGE = SEC_ERROR_BASE + 90;
|
||||
const SEC_ERROR_INADEQUATE_CERT_TYPE = SEC_ERROR_BASE + 91;
|
||||
const SEC_ERROR_CERT_NOT_IN_NAME_SPACE = SEC_ERROR_BASE + 112;
|
||||
|
|
|
@ -10,14 +10,20 @@ the desired properties.
|
|||
|
||||
The specification format is as follows:
|
||||
|
||||
hash:<hex string>
|
||||
sha1:<hex string>
|
||||
sha256:<hex string>
|
||||
signer:
|
||||
<pycert specification>
|
||||
|
||||
hash is the value that will be put in the messageDigest attribute in
|
||||
each SignerInfo of the signerInfos field of the SignedData.
|
||||
Eith or both of sha1 and sha256 may be specified. The value of
|
||||
each hash directive is what will be put in the messageDigest
|
||||
attribute of the SignerInfo that corresponds to the signature
|
||||
algorithm defined by the hash algorithm and key type of the
|
||||
default key. Together, these comprise the signerInfos field of
|
||||
the SignedData. If neither hash is specified, the signerInfos
|
||||
will be an empty SET (i.e. there will be no actual signature
|
||||
information).
|
||||
The certificate specification must come last.
|
||||
Currently only SHA-1 is supported.
|
||||
"""
|
||||
|
||||
from pyasn1.codec.der import decoder
|
||||
|
@ -52,7 +58,8 @@ class CMS(object):
|
|||
generating a CMS message"""
|
||||
|
||||
def __init__(self, paramStream):
|
||||
self.hash = ''
|
||||
self.sha1 = ''
|
||||
self.sha256 = ''
|
||||
signerSpecification = StringIO.StringIO()
|
||||
readingSignerSpecification = False
|
||||
for line in paramStream.readlines():
|
||||
|
@ -60,8 +67,10 @@ class CMS(object):
|
|||
print >>signerSpecification, line.strip()
|
||||
elif line.strip() == 'signer:':
|
||||
readingSignerSpecification = True
|
||||
elif line.startswith('hash:'):
|
||||
self.hash = line.strip()[len('hash:'):]
|
||||
elif line.startswith('sha1:'):
|
||||
self.sha1 = line.strip()[len('sha1:'):]
|
||||
elif line.startswith('sha256:'):
|
||||
self.sha256 = line.strip()[len('sha256:'):]
|
||||
else:
|
||||
raise UnknownDirectiveError(line.strip())
|
||||
signerSpecification.seek(0)
|
||||
|
@ -97,11 +106,15 @@ class CMS(object):
|
|||
"""Given a pykey hash algorithm identifier, builds an
|
||||
AlgorithmIdentifier for use with pyasn1."""
|
||||
if pykeyHash == pykey.HASH_SHA1:
|
||||
algorithmIdentifier = rfc2459.AlgorithmIdentifier()
|
||||
algorithmIdentifier['algorithm'] = univ.ObjectIdentifier('1.3.14.3.2.26')
|
||||
algorithmIdentifier['parameters'] = univ.Null()
|
||||
return algorithmIdentifier
|
||||
raise pykey.UnknownHashAlgorithmError(pykeyHash)
|
||||
oidString = '1.3.14.3.2.26'
|
||||
elif pykeyHash == pykey.HASH_SHA256:
|
||||
oidString = '2.16.840.1.101.3.4.2.1'
|
||||
else:
|
||||
raise pykey.UnknownHashAlgorithmError(pykeyHash)
|
||||
algorithmIdentifier = rfc2459.AlgorithmIdentifier()
|
||||
algorithmIdentifier['algorithm'] = univ.ObjectIdentifier(oidString)
|
||||
algorithmIdentifier['parameters'] = univ.Null()
|
||||
return algorithmIdentifier
|
||||
|
||||
def buildSignerInfo(self, certificate, pykeyHash, digestValue):
|
||||
"""Given a pyasn1 certificate, a pykey hash identifier
|
||||
|
@ -157,7 +170,12 @@ class CMS(object):
|
|||
|
||||
signerInfos = rfc2315.SignerInfos()
|
||||
|
||||
signerInfos[0] = self.buildSignerInfo(certificate, pykey.HASH_SHA1, self.hash)
|
||||
if len(self.sha1) > 0:
|
||||
signerInfos[len(signerInfos)] = self.buildSignerInfo(certificate,
|
||||
pykey.HASH_SHA1, self.sha1)
|
||||
if len(self.sha256) > 0:
|
||||
signerInfos[len(signerInfos)] = self.buildSignerInfo(certificate,
|
||||
pykey.HASH_SHA256, self.sha256)
|
||||
signedData['signerInfos'] = signerInfos
|
||||
|
||||
encoded = encoder.encode(signedData)
|
||||
|
|
|
@ -33,14 +33,15 @@ def walkDirectory(directory):
|
|||
return paths
|
||||
|
||||
def signZip(appDirectory, outputFile, issuerName, manifestHashes,
|
||||
signatureHashes, doSign):
|
||||
signatureHashes, pkcs7Hashes, doSign):
|
||||
"""Given a directory containing the files to package up,
|
||||
an output filename to write to, the name of the issuer of
|
||||
the signing certificate, a list of hash algorithms to use in
|
||||
the manifest file, a similar list for the signature file,
|
||||
and whether or not to actually sign the resulting package,
|
||||
packages up the files in the directory and creates the
|
||||
output as appropriate."""
|
||||
a similar list for the pkcs#7 signature, and whether or not
|
||||
to actually sign the resulting package, packages up the
|
||||
files in the directory and creates the output as
|
||||
appropriate."""
|
||||
mfEntries = []
|
||||
|
||||
with zipfile.ZipFile(outputFile, 'w') as outZip:
|
||||
|
@ -66,7 +67,12 @@ def signZip(appDirectory, outputFile, issuerName, manifestHashes,
|
|||
base64hash = b64encode(hashFunc(mfContents).digest())
|
||||
sfContents += '%s-Digest-Manifest: %s\n' % (name, base64hash)
|
||||
|
||||
cmsSpecification = 'hash:%s\nsigner:\n' % sha1(sfContents).hexdigest() + \
|
||||
cmsSpecification = ''
|
||||
for name in pkcs7Hashes:
|
||||
hashFunc, _ = hashNameToFunctionAndIdentifier(name)
|
||||
cmsSpecification += '%s:%s\n' % (name,
|
||||
hashFunc(sfContents).hexdigest())
|
||||
cmsSpecification += 'signer:\n' + \
|
||||
'issuer:%s\n' % issuerName + \
|
||||
'subject:xpcshell signed app test signer\n' + \
|
||||
'extension:keyUsage:digitalSignature'
|
||||
|
@ -118,12 +124,20 @@ def main(outputFile, appPath, *args):
|
|||
parser.add_argument('-s', '--signature-hash', action='append',
|
||||
help='Hash algorithms to use in signature file',
|
||||
default=[])
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument('-p', '--pkcs7-hash', action='append',
|
||||
help='Hash algorithms to use in PKCS#7 signature',
|
||||
default=[])
|
||||
group.add_argument('-e', '--empty-signerInfos', action='store_true',
|
||||
help='Emit pkcs#7 SignedData with empty signerInfos')
|
||||
parsed = parser.parse_args(args)
|
||||
if len(parsed.manifest_hash) == 0:
|
||||
parsed.manifest_hash.append('sha256')
|
||||
if len(parsed.signature_hash) == 0:
|
||||
parsed.signature_hash.append('sha256')
|
||||
if len(parsed.pkcs7_hash) == 0 and not parsed.empty_signerInfos:
|
||||
parsed.pkcs7_hash.append('sha256')
|
||||
signZip(appPath, outputFile, parsed.issuer,
|
||||
map(hashNameToFunctionAndIdentifier, parsed.manifest_hash),
|
||||
map(hashNameToFunctionAndIdentifier, parsed.signature_hash),
|
||||
not parsed.no_sign)
|
||||
parsed.pkcs7_hash, not parsed.no_sign)
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
// This test attempts to ensure that PSM doesn't deadlock or crash when shutting
|
||||
// down NSS while a background thread is attempting to use NSS.
|
||||
// Uses test_signed_apps/signed_app.zip from test_signed_apps.js.
|
||||
// Uses test_signed_apps/app_mf-1_sf-1_p7-1.zip from test_signed_apps.js.
|
||||
|
||||
function startAsyncNSSOperation(certdb, appFile) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@ -28,7 +28,7 @@ add_task(async function() {
|
|||
.QueryInterface(Ci.nsIObserver);
|
||||
let certdb = Cc["@mozilla.org/security/x509certdb;1"]
|
||||
.getService(Ci.nsIX509CertDB);
|
||||
let appFile = do_get_file("test_signed_apps/signed_app.zip");
|
||||
let appFile = do_get_file("test_signed_apps/app_mf-1_sf-1_p7-1.zip");
|
||||
|
||||
let promises = [];
|
||||
for (let i = 0; i < 25; i++) {
|
||||
|
|
|
@ -1,27 +1,18 @@
|
|||
"use strict";
|
||||
/* To regenerate the certificates and apps for this test:
|
||||
|
||||
cd security/manager/ssl/tests/unit/test_signed_apps
|
||||
PATH=$NSS/bin:$NSS/lib:$PATH ./generate.sh
|
||||
cd ../../../../../..
|
||||
make -C $OBJDIR/security/manager/ssl/tests
|
||||
// Tests the API nsIX509CertDB.openSignedAppFileAsync, which backs add-on
|
||||
// signature verification. Testcases include various ways of tampering with
|
||||
// add-ons as well as different hash algorithms used in the various
|
||||
// signature/metadata files.
|
||||
|
||||
$NSS is the path to NSS binaries and libraries built for the host platform.
|
||||
If you get error messages about "CertUtil" on Windows, then it means that
|
||||
the Windows CertUtil.exe is ahead of the NSS certutil.exe in $PATH.
|
||||
|
||||
Check in the generated files. These steps are not done as part of the build
|
||||
because we do not want to add a build-time dependency on the OpenSSL or NSS
|
||||
tools or libraries built for the host platform.
|
||||
*/
|
||||
|
||||
// XXX from prio.h
|
||||
// from prio.h
|
||||
const PR_RDWR = 0x04;
|
||||
const PR_CREATE_FILE = 0x08;
|
||||
const PR_TRUNCATE = 0x20;
|
||||
|
||||
do_get_profile(); // must be called before getting nsIX509CertDB
|
||||
const certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(Ci.nsIX509CertDB);
|
||||
const certdb = Cc["@mozilla.org/security/x509certdb;1"]
|
||||
.getService(Ci.nsIX509CertDB);
|
||||
|
||||
// Creates a new app package based in the inFilePath package, with a set of
|
||||
// modifications (including possibly deletions) applied to the existing entries,
|
||||
|
@ -134,71 +125,85 @@ function tampered_app_path(test_name) {
|
|||
return FileUtils.getFile("TmpD", ["test_signed_app-" + test_name + ".zip"]);
|
||||
}
|
||||
|
||||
add_test(function () {
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot, original_app_path("signed_app"),
|
||||
check_open_result("valid", Cr.NS_OK));
|
||||
});
|
||||
var hashTestcases = [
|
||||
// SHA-256 in PKCS#7 + SHA-256 present elsewhere => OK
|
||||
{ name: "app_mf-1-256_sf-1-256_p7-1-256",
|
||||
expectedResult: Cr.NS_OK },
|
||||
{ name: "app_mf-1-256_sf-1-256_p7-256",
|
||||
expectedResult: Cr.NS_OK },
|
||||
{ name: "app_mf-1-256_sf-256_p7-1-256",
|
||||
expectedResult: Cr.NS_OK },
|
||||
{ name: "app_mf-1-256_sf-256_p7-256",
|
||||
expectedResult: Cr.NS_OK },
|
||||
{ name: "app_mf-256_sf-1-256_p7-1-256",
|
||||
expectedResult: Cr.NS_OK },
|
||||
{ name: "app_mf-256_sf-1-256_p7-256",
|
||||
expectedResult: Cr.NS_OK },
|
||||
{ name: "app_mf-256_sf-256_p7-1-256",
|
||||
expectedResult: Cr.NS_OK },
|
||||
{ name: "app_mf-256_sf-256_p7-256",
|
||||
expectedResult: Cr.NS_OK },
|
||||
|
||||
add_test(function () {
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot, original_app_path("signed_app_sha256"),
|
||||
check_open_result("valid with sha256 hashes in manifest", Cr.NS_OK));
|
||||
});
|
||||
// SHA-1 in PKCS#7 + SHA-1 present elsewhere => OK
|
||||
{ name: "app_mf-1-256_sf-1-256_p7-1",
|
||||
expectedResult: Cr.NS_OK },
|
||||
{ name: "app_mf-1-256_sf-1_p7-1",
|
||||
expectedResult: Cr.NS_OK },
|
||||
{ name: "app_mf-1_sf-1-256_p7-1",
|
||||
expectedResult: Cr.NS_OK },
|
||||
{ name: "app_mf-1_sf-1_p7-1",
|
||||
expectedResult: Cr.NS_OK },
|
||||
|
||||
// SHA-256 in PKCS#7 + SHA-256 not present elsewhere => INVALID
|
||||
{ name: "app_mf-1-256_sf-1_p7-1-256",
|
||||
expectedResult: Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID },
|
||||
{ name: "app_mf-1-256_sf-1_p7-256",
|
||||
expectedResult: Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID },
|
||||
{ name: "app_mf-1_sf-1-256_p7-1-256",
|
||||
expectedResult: Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID },
|
||||
{ name: "app_mf-1_sf-1-256_p7-256",
|
||||
expectedResult: Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID },
|
||||
{ name: "app_mf-1_sf-1_p7-1-256",
|
||||
expectedResult: Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID },
|
||||
{ name: "app_mf-1_sf-1_p7-256",
|
||||
expectedResult: Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID },
|
||||
{ name: "app_mf-1_sf-256_p7-1-256",
|
||||
expectedResult: Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID },
|
||||
{ name: "app_mf-1_sf-256_p7-256",
|
||||
expectedResult: Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID },
|
||||
{ name: "app_mf-256_sf-1_p7-1-256",
|
||||
expectedResult: Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID },
|
||||
{ name: "app_mf-256_sf-1_p7-256",
|
||||
expectedResult: Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID },
|
||||
|
||||
// SHA-1 in PKCS#7 + SHA-1 not present elsewhere => INVALID
|
||||
{ name: "app_mf-1-256_sf-256_p7-1",
|
||||
expectedResult: Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID },
|
||||
{ name: "app_mf-1_sf-256_p7-1",
|
||||
expectedResult: Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID },
|
||||
{ name: "app_mf-256_sf-1-256_p7-1",
|
||||
expectedResult: Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID },
|
||||
{ name: "app_mf-256_sf-1_p7-1",
|
||||
expectedResult: Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID },
|
||||
{ name: "app_mf-256_sf-256_p7-1",
|
||||
expectedResult: Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID },
|
||||
];
|
||||
|
||||
for (let testcase of hashTestcases) {
|
||||
add_test(function () {
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot,
|
||||
original_app_path(testcase.name),
|
||||
check_open_result(testcase.name, testcase.expectedResult));
|
||||
});
|
||||
}
|
||||
|
||||
add_test(function () {
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot,
|
||||
original_app_path("signed_app_sha1_and_sha256"),
|
||||
check_open_result("valid with sha1 and sha256 hashes in manifest", Cr.NS_OK));
|
||||
});
|
||||
|
||||
add_test(function () {
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot,
|
||||
original_app_path("signed_app_sha256_manifest"),
|
||||
check_open_result("sha256 hashes in manifest, but only a sha1 hash in the signature file",
|
||||
Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID));
|
||||
});
|
||||
|
||||
add_test(function () {
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot,
|
||||
original_app_path("signed_app_sha256_signature_file"),
|
||||
check_open_result("sha256 hash in the signature file, but only sha1 hashes in the manifest",
|
||||
Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID));
|
||||
});
|
||||
|
||||
add_test(function () {
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot,
|
||||
original_app_path("sha1_and_sha256_manifest_sha1_signature_file"),
|
||||
check_open_result("sha1 and sha256 hashes in the manifest, but only sha1 hash in the signature file",
|
||||
Cr.NS_OK));
|
||||
});
|
||||
|
||||
add_test(function () {
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot,
|
||||
original_app_path("sha1_and_sha256_manifest_sha256_signature_file"),
|
||||
check_open_result("sha1 and sha256 hashes in the manifest, but only sha256 hash in the signature file",
|
||||
Cr.NS_OK));
|
||||
});
|
||||
|
||||
add_test(function () {
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot,
|
||||
original_app_path("sha1_manifest_sha1_and_sha256_signature_file"),
|
||||
check_open_result("only sha1 in the manifest, sha1 and sha256 hashes in the signature file",
|
||||
Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID));
|
||||
});
|
||||
|
||||
add_test(function () {
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot,
|
||||
original_app_path("sha256_manifest_sha1_and_sha256_signature_file"),
|
||||
check_open_result("only sha256 in the manifest, sha1 and sha256 hashes in the signature file",
|
||||
Cr.NS_OK));
|
||||
original_app_path("empty_signerInfos"),
|
||||
check_open_result("the signerInfos in the PKCS#7 signature is empty",
|
||||
Cr.NS_ERROR_CMS_VERIFY_NOT_SIGNED));
|
||||
});
|
||||
|
||||
add_test(function () {
|
||||
|
@ -217,15 +222,16 @@ add_test(function () {
|
|||
// Sanity check to ensure a no-op tampering gives a valid result
|
||||
add_test(function () {
|
||||
let tampered = tampered_app_path("identity_tampering");
|
||||
tamper(original_app_path("signed_app"), tampered, { }, []);
|
||||
tamper(original_app_path("app_mf-1_sf-1_p7-1"), tampered, { }, []);
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot, original_app_path("signed_app"),
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot, original_app_path("app_mf-1_sf-1_p7-1"),
|
||||
check_open_result("identity_tampering", Cr.NS_OK));
|
||||
});
|
||||
|
||||
add_test(function () {
|
||||
let tampered = tampered_app_path("missing_rsa");
|
||||
tamper(original_app_path("signed_app"), tampered, { "META-INF/A.RSA": removeEntry }, []);
|
||||
tamper(original_app_path("app_mf-1_sf-1_p7-1"), tampered,
|
||||
{ "META-INF/A.RSA": removeEntry }, []);
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot, tampered,
|
||||
check_open_result("missing_rsa", Cr.NS_ERROR_SIGNED_JAR_NOT_SIGNED));
|
||||
|
@ -233,7 +239,8 @@ add_test(function () {
|
|||
|
||||
add_test(function () {
|
||||
let tampered = tampered_app_path("missing_sf");
|
||||
tamper(original_app_path("signed_app"), tampered, { "META-INF/A.SF": removeEntry }, []);
|
||||
tamper(original_app_path("app_mf-1_sf-1_p7-1"), tampered,
|
||||
{ "META-INF/A.SF": removeEntry }, []);
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot, tampered,
|
||||
check_open_result("missing_sf", Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID));
|
||||
|
@ -241,7 +248,8 @@ add_test(function () {
|
|||
|
||||
add_test(function () {
|
||||
let tampered = tampered_app_path("missing_manifest_mf");
|
||||
tamper(original_app_path("signed_app"), tampered, { "META-INF/MANIFEST.MF": removeEntry }, []);
|
||||
tamper(original_app_path("app_mf-1_sf-1_p7-1"), tampered,
|
||||
{ "META-INF/MANIFEST.MF": removeEntry }, []);
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot, tampered,
|
||||
check_open_result("missing_manifest_mf",
|
||||
|
@ -250,7 +258,8 @@ add_test(function () {
|
|||
|
||||
add_test(function () {
|
||||
let tampered = tampered_app_path("missing_entry");
|
||||
tamper(original_app_path("signed_app"), tampered, { "manifest.json": removeEntry }, []);
|
||||
tamper(original_app_path("app_mf-1_sf-1_p7-1"), tampered,
|
||||
{ "manifest.json": removeEntry }, []);
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot, tampered,
|
||||
check_open_result("missing_entry", Cr.NS_ERROR_SIGNED_JAR_ENTRY_MISSING));
|
||||
|
@ -258,15 +267,47 @@ add_test(function () {
|
|||
|
||||
add_test(function () {
|
||||
let tampered = tampered_app_path("truncated_entry");
|
||||
tamper(original_app_path("signed_app"), tampered, { "manifest.json": truncateEntry }, []);
|
||||
tamper(original_app_path("app_mf-1_sf-1_p7-1"), tampered,
|
||||
{ "manifest.json": truncateEntry }, []);
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot, tampered,
|
||||
check_open_result("truncated_entry", Cr.NS_ERROR_SIGNED_JAR_MODIFIED_ENTRY));
|
||||
check_open_result("truncated_entry",
|
||||
Cr.NS_ERROR_SIGNED_JAR_MODIFIED_ENTRY));
|
||||
});
|
||||
|
||||
add_test(function () {
|
||||
let tampered = tampered_app_path("truncated_manifestFile");
|
||||
tamper(original_app_path("app_mf-1_sf-1_p7-1"), tampered,
|
||||
{ "META-INF/MANIFEST.MF": truncateEntry }, []);
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot, tampered,
|
||||
check_open_result("truncated_manifestFile",
|
||||
Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID));
|
||||
});
|
||||
|
||||
add_test(function () {
|
||||
let tampered = tampered_app_path("truncated_signatureFile");
|
||||
tamper(original_app_path("app_mf-1_sf-1_p7-1"), tampered,
|
||||
{ "META-INF/A.SF": truncateEntry }, []);
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot, tampered,
|
||||
check_open_result("truncated_signatureFile",
|
||||
getXPCOMStatusFromNSS(SEC_ERROR_PKCS7_BAD_SIGNATURE)));
|
||||
});
|
||||
|
||||
add_test(function () {
|
||||
let tampered = tampered_app_path("truncated_pkcs7File");
|
||||
tamper(original_app_path("app_mf-1_sf-1_p7-1"), tampered,
|
||||
{ "META-INF/A.RSA": truncateEntry }, []);
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot, tampered,
|
||||
check_open_result("truncated_pkcs7File",
|
||||
Cr.NS_ERROR_CMS_VERIFY_NOT_SIGNED));
|
||||
});
|
||||
|
||||
add_test(function () {
|
||||
let tampered = tampered_app_path("unsigned_entry");
|
||||
tamper(original_app_path("signed_app"), tampered, {},
|
||||
tamper(original_app_path("app_mf-1_sf-1_p7-1"), tampered, {},
|
||||
[ { "name": "unsigned.txt", "content": "unsigned content!" } ]);
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot, tampered,
|
||||
|
@ -275,7 +316,7 @@ add_test(function () {
|
|||
|
||||
add_test(function () {
|
||||
let tampered = tampered_app_path("unsigned_metainf_entry");
|
||||
tamper(original_app_path("signed_app"), tampered, {},
|
||||
tamper(original_app_path("app_mf-1_sf-1_p7-1"), tampered, {},
|
||||
[ { name: "META-INF/unsigned.txt", content: "unsigned content!" } ]);
|
||||
certdb.openSignedAppFileAsync(
|
||||
Ci.nsIX509CertDB.AppXPCShellRoot, tampered,
|
||||
|
|
Двоичный файл не отображается.
Двоичные данные
security/manager/ssl/tests/unit/test_signed_apps/app_mf-1-256_sf-1-256_p7-1.zip
Normal file
Двоичные данные
security/manager/ssl/tests/unit/test_signed_apps/app_mf-1-256_sf-1-256_p7-1.zip
Normal file
Двоичный файл не отображается.
Двоичные данные
security/manager/ssl/tests/unit/test_signed_apps/app_mf-1-256_sf-1-256_p7-256.zip
Normal file
Двоичные данные
security/manager/ssl/tests/unit/test_signed_apps/app_mf-1-256_sf-1-256_p7-256.zip
Normal file
Двоичный файл не отображается.
Двоичные данные
security/manager/ssl/tests/unit/test_signed_apps/app_mf-1-256_sf-1_p7-1-256.zip
Normal file
Двоичные данные
security/manager/ssl/tests/unit/test_signed_apps/app_mf-1-256_sf-1_p7-1-256.zip
Normal file
Двоичный файл не отображается.
Двоичный файл не отображается.
Двоичные данные
security/manager/ssl/tests/unit/test_signed_apps/app_mf-1-256_sf-1_p7-256.zip
Normal file
Двоичные данные
security/manager/ssl/tests/unit/test_signed_apps/app_mf-1-256_sf-1_p7-256.zip
Normal file
Двоичный файл не отображается.
Двоичные данные
security/manager/ssl/tests/unit/test_signed_apps/app_mf-1-256_sf-256_p7-1-256.zip
Normal file
Двоичные данные
security/manager/ssl/tests/unit/test_signed_apps/app_mf-1-256_sf-256_p7-1-256.zip
Normal file
Двоичный файл не отображается.
Двоичный файл не отображается.
Двоичные данные
security/manager/ssl/tests/unit/test_signed_apps/app_mf-1-256_sf-256_p7-256.zip
Normal file
Двоичные данные
security/manager/ssl/tests/unit/test_signed_apps/app_mf-1-256_sf-256_p7-256.zip
Normal file
Двоичный файл не отображается.
Двоичные данные
security/manager/ssl/tests/unit/test_signed_apps/app_mf-1_sf-1-256_p7-1-256.zip
Normal file
Двоичные данные
security/manager/ssl/tests/unit/test_signed_apps/app_mf-1_sf-1-256_p7-1-256.zip
Normal file
Двоичный файл не отображается.
Двоичный файл не отображается.
Двоичные данные
security/manager/ssl/tests/unit/test_signed_apps/app_mf-1_sf-1-256_p7-256.zip
Normal file
Двоичные данные
security/manager/ssl/tests/unit/test_signed_apps/app_mf-1_sf-1-256_p7-256.zip
Normal file
Двоичный файл не отображается.
Двоичные данные
security/manager/ssl/tests/unit/test_signed_apps/app_mf-1_sf-1_p7-1-256.zip
Normal file
Двоичные данные
security/manager/ssl/tests/unit/test_signed_apps/app_mf-1_sf-1_p7-1-256.zip
Normal file
Двоичный файл не отображается.
Двоичный файл не отображается.
Двоичные данные
security/manager/ssl/tests/unit/test_signed_apps/app_mf-1_sf-1_p7-256.zip
Normal file
Двоичные данные
security/manager/ssl/tests/unit/test_signed_apps/app_mf-1_sf-1_p7-256.zip
Normal file
Двоичный файл не отображается.
Двоичные данные
security/manager/ssl/tests/unit/test_signed_apps/app_mf-1_sf-256_p7-1-256.zip
Normal file
Двоичные данные
security/manager/ssl/tests/unit/test_signed_apps/app_mf-1_sf-256_p7-1-256.zip
Normal file
Двоичный файл не отображается.
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче