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