merge fx-team to mozilla-central a=merge

This commit is contained in:
Carsten "Tomcat" Book 2016-09-22 11:54:05 +02:00
commit cce863628d
104 changed files with 2850 additions and 704 deletions

View File

@ -82,6 +82,7 @@ devtools/client/debugger/**
devtools/client/eyedropper/**
devtools/client/framework/**
!devtools/client/framework/selection.js
!devtools/client/framework/toolbox.js
devtools/client/jsonview/lib/**
devtools/client/memory/**
devtools/client/netmonitor/test/**

View File

@ -84,3 +84,9 @@ addMessageListener("Test:WaitForObserverCall", ({data}) => {
}
}, topic, false);
});
addMessageListener("Test:WaitForMessage", () => {
content.addEventListener("message", ({data}) => {
sendAsyncMessage("Test:MessageReceived", data);
}, {once: true});
});

View File

@ -246,21 +246,23 @@ function promiseTodoObserverNotCalled(aTopic) {
}
function promiseMessage(aMessage, aAction) {
let deferred = Promise.defer();
content.addEventListener("message", function messageListener(event) {
content.removeEventListener("message", messageListener);
is(event.data, aMessage, "received " + aMessage);
if (event.data == aMessage)
deferred.resolve();
else
deferred.reject();
let promise = new Promise((resolve, reject) => {
let mm = _mm();
mm.addMessageListener("Test:MessageReceived", function listener({data}) {
is(data, aMessage, "received " + aMessage);
if (data == aMessage)
resolve();
else
reject();
mm.removeMessageListener("Test:MessageReceived", listener);
});
mm.sendAsyncMessage("Test:WaitForMessage");
});
if (aAction)
aAction();
return deferred.promise;
return promise;
}
function promisePopupNotificationShown(aName, aAction) {

View File

@ -292,7 +292,6 @@ function prompt(aBrowser, aRequest) {
requestTypes: requestTypes} = aRequest;
let uri = Services.io.newURI(aRequest.documentURI, null, null);
let host = getHost(uri);
let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
let chromeDoc = aBrowser.ownerDocument;
let chromeWin = chromeDoc.defaultView;
let stringBundle = chromeWin.gNavigatorBundle;
@ -388,14 +387,12 @@ function prompt(aBrowser, aRequest) {
if (micPerm == perms.PROMPT_ACTION)
micPerm = perms.UNKNOWN_ACTION;
let camPermanentPerm = perms.testExactPermanentPermission(principal, "camera");
let camPerm = perms.testExactPermission(uri, "camera");
// Session approval given but never used to allocate a camera, remove
// and ask again
if (camPerm && !camPermanentPerm) {
perms.remove(uri, "camera");
camPerm = perms.UNKNOWN_ACTION;
let mediaManagerPerm =
perms.testExactPermission(uri, "MediaManagerVideo");
if (mediaManagerPerm) {
perms.remove(uri, "MediaManagerVideo");
}
if (camPerm == perms.PROMPT_ACTION)
@ -534,10 +531,12 @@ function prompt(aBrowser, aRequest) {
allowedDevices.push(videoDeviceIndex);
// Session permission will be removed after use
// (it's really one-shot, not for the entire session)
perms.add(uri, "camera", perms.ALLOW_ACTION,
aRemember ? perms.EXPIRE_NEVER : perms.EXPIRE_SESSION);
} else if (aRemember) {
perms.add(uri, "camera", perms.DENY_ACTION);
perms.add(uri, "MediaManagerVideo", perms.ALLOW_ACTION,
perms.EXPIRE_SESSION);
}
if (aRemember) {
perms.add(uri, "camera",
allowCamera ? perms.ALLOW_ACTION : perms.DENY_ACTION);
}
}
if (audioDevices.length) {

View File

@ -24,8 +24,7 @@ module.exports = createClass({
targets = targets.sort(LocaleCompare);
}
targets = targets.map(target => {
let key = target.name || target.url || target.title;
return targetClass({ client, key, target, debugDisabled });
return targetClass({ client, target, debugDisabled });
});
let content = "";

View File

@ -11,6 +11,8 @@
// is selected, animations will be displayed in the timeline, so the timeline
// play/resume button will be displayed
add_task(function* () {
requestLongerTimeout(2);
yield addTab(URL_ROOT + "doc_simple_animation.html");
let {panel, window} = yield openAnimationInspector();
let {playTimelineButtonEl} = panel;

View File

@ -189,32 +189,16 @@
<key id="resumeKey"
keycode="&debuggerUI.stepping.resume1;"
command="resumeCommand"/>
<key id="resumeKey2"
keycode="&debuggerUI.stepping.resume2;"
modifiers="accel"
command="resumeCommand"/>
<key id="stepOverKey"
keycode="&debuggerUI.stepping.stepOver1;"
command="stepOverCommand"/>
<key id="stepOverKey2"
keycode="&debuggerUI.stepping.stepOver2;"
modifiers="accel"
command="stepOverCommand"/>
<key id="stepInKey"
keycode="&debuggerUI.stepping.stepIn1;"
command="stepInCommand"/>
<key id="stepInKey2"
keycode="&debuggerUI.stepping.stepIn2;"
modifiers="accel"
command="stepInCommand"/>
<key id="stepOutKey"
keycode="&debuggerUI.stepping.stepOut1;"
modifiers="shift"
command="stepOutCommand"/>
<key id="stepOutKey2"
keycode="&debuggerUI.stepping.stepOut2;"
modifiers="accel shift"
command="stepOutCommand"/>
<key id="fileSearchKey"
key="&debuggerUI.searchFile.key;"
modifiers="accel"

View File

@ -3,29 +3,27 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { Task } = require("devtools/shared/task");
function DebuggerPanel(iframeWindow, toolbox) {
this.panelWin = iframeWindow;
this.toolbox = toolbox;
}
DebuggerPanel.prototype = {
open: function() {
let targetPromise;
open: Task.async(function*() {
if (!this.toolbox.target.isRemote) {
targetPromise = this.toolbox.target.makeRemote();
} else {
targetPromise = Promise.resolve(this.toolbox.target);
yield this.toolbox.target.makeRemote();
}
return targetPromise.then(() => {
this.panelWin.Debugger.bootstrap({
threadClient: this.toolbox.threadClient,
tabTarget: this.toolbox.target
});
this.isReady = true;
return this;
yield this.panelWin.Debugger.bootstrap({
threadClient: this.toolbox.threadClient,
tabTarget: this.toolbox.target
});
},
this.isReady = true;
return this;
}),
_store: function() {
return this.panelWin.Debugger.store;

View File

@ -54,12 +54,16 @@ DebuggerPanel.prototype = {
this._toolbox.on("host-changed", this.handleHostChanged);
// Add keys from this document's keyset to the toolbox, so they
// can work when the split console is focused.
let keysToClone = ["resumeKey", "resumeKey2", "stepOverKey",
"stepOverKey2", "stepInKey", "stepInKey2",
"stepOutKey", "stepOutKey2"];
let keysToClone = ["resumeKey", "stepOverKey", "stepInKey", "stepOutKey"];
for (let key of keysToClone) {
let elm = this.panelWin.document.getElementById(key);
this._toolbox.useKeyWithSplitConsole(elm, "jsdebugger");
let keycode = elm.getAttribute("keycode");
let modifiers = elm.getAttribute("modifiers");
let command = elm.getAttribute("command");
let handler = this._view.Toolbar.getCommandHandler(command);
let keyShortcut = this.translateToKeyShortcut(keycode, modifiers);
this._toolbox.useKeyWithSplitConsole(keyShortcut, handler, "jsdebugger");
}
this.isReady = true;
this.emit("ready");
@ -70,6 +74,40 @@ DebuggerPanel.prototype = {
});
},
/**
* Translate a VK_ keycode, with modifiers, to a key shortcut that can be used with
* shared/key-shortcut.
*
* @param {String} keycode
* The VK_* keycode to translate
* @param {String} modifiers
* The list (blank-space separated) of modifiers applying to this keycode.
* @return {String} a key shortcut ready to be used with shared/key-shortcut.js
*/
translateToKeyShortcut: function (keycode, modifiers) {
// Remove the VK_ prefix.
keycode = keycode.replace("VK_", "");
// Translate modifiers
if (modifiers.includes("shift")) {
keycode = "Shift+" + keycode;
}
if (modifiers.includes("alt")) {
keycode = "Alt+" + keycode;
}
if (modifiers.includes("control")) {
keycode = "Ctrl+" + keycode;
}
if (modifiers.includes("meta")) {
keycode = "Cmd+" + keycode;
}
if (modifiers.includes("accel")) {
keycode = "CmdOrCtrl+" + keycode;
}
return keycode;
},
// DevToolPanel API
get target() {

View File

@ -207,7 +207,7 @@ skip-if = e10s && debug
[browser_dbg_pretty-print-10.js]
skip-if = e10s && debug
[browser_dbg_pretty-print-11.js]
skip-if = e10s && debug || true
skip-if = e10s && debug
[browser_dbg_pretty-print-12.js]
skip-if = e10s && debug
[browser_dbg_pretty-print-13.js]

View File

@ -101,13 +101,35 @@ ToolbarView.prototype = {
*/
_addCommands: function () {
XULUtils.addCommands(document.getElementById("debuggerCommands"), {
resumeCommand: () => this._onResumePressed(),
stepOverCommand: () => this._onStepOverPressed(),
stepInCommand: () => this._onStepInPressed(),
stepOutCommand: () => this._onStepOutPressed()
resumeCommand: this.getCommandHandler("resumeCommand"),
stepOverCommand: this.getCommandHandler("stepOverCommand"),
stepInCommand: this.getCommandHandler("stepInCommand"),
stepOutCommand: this.getCommandHandler("stepOutCommand")
});
},
/**
* Retrieve the callback associated with the provided debugger command.
*
* @param {String} command
* The debugger command id.
* @return {Function} the corresponding callback.
*/
getCommandHandler: function (command) {
switch (command) {
case "resumeCommand":
return () => this._onResumePressed();
case "stepOverCommand":
return () => this._onStepOverPressed();
case "stepInCommand":
return () => this._onStepInPressed();
case "stepOutCommand":
return () => this._onStepOutPressed();
default:
return () => {};
}
},
/**
* Display a warning when trying to resume a debuggee while another is paused.
* Debuggees must be unpaused in a Last-In-First-Out order.

View File

@ -60,7 +60,7 @@ Tools.inspector = {
modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift",
icon: "chrome://devtools/skin/images/tool-inspector.svg",
invertIconForDarkTheme: true,
url: "chrome://devtools/content/inspector/inspector.xul",
url: "chrome://devtools/content/inspector/inspector.xhtml",
label: l10n("inspector.label"),
panelLabel: l10n("inspector.panelLabel"),
get tooltip() {

View File

@ -3,6 +3,8 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Tests that these toolbox split console APIs work:
// * toolbox.useKeyWithSplitConsole()
// * toolbox.isSplitConsoleFocused
@ -11,7 +13,6 @@ let gToolbox = null;
let panelWin = null;
const URL = "data:text/html;charset=utf8,test split console key delegation";
const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
// Force the old debugger UI since it's directly used (see Bug 1301705)
Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
@ -45,18 +46,15 @@ function* testIsSplitConsoleFocused() {
function* testUseKeyWithSplitConsole() {
let commandCalled = false;
let keyElm = panelWin.document.createElementNS(XULNS, "key");
keyElm.setAttribute("keycode", "VK_F3");
keyElm.addEventListener("command", () => {commandCalled = true;}, false);
panelWin.document.getElementsByTagName("keyset")[0].appendChild(keyElm);
info("useKeyWithSplitConsole on debugger while debugger is focused");
gToolbox.useKeyWithSplitConsole(keyElm, "jsdebugger");
gToolbox.useKeyWithSplitConsole("F3", () => {
commandCalled = true;
}, "jsdebugger");
info("synthesizeKey with the console focused");
let consoleInput = gToolbox.getPanel("webconsole").hud.jsterm.inputNode;
consoleInput.focus();
synthesizeKeyElement(keyElm);
synthesizeKeyShortcut("F3", panelWin);
ok(commandCalled, "Shortcut key should trigger the command");
}
@ -65,18 +63,15 @@ function* testUseKeyWithSplitConsole() {
function* testUseKeyWithSplitConsoleWrongTool() {
let commandCalled = false;
let keyElm = panelWin.document.createElementNS(XULNS, "key");
keyElm.setAttribute("keycode", "VK_F4");
keyElm.addEventListener("command", () => {commandCalled = true;}, false);
panelWin.document.getElementsByTagName("keyset")[0].appendChild(keyElm);
info("useKeyWithSplitConsole on inspector while debugger is focused");
gToolbox.useKeyWithSplitConsole(keyElm, "inspector");
gToolbox.useKeyWithSplitConsole("F4", () => {
commandCalled = true;
}, "inspector");
info("synthesizeKey with the console focused");
let consoleInput = gToolbox.getPanel("webconsole").hud.jsterm.inputNode;
consoleInput.focus();
synthesizeKeyElement(keyElm);
synthesizeKeyShortcut("F4", panelWin);
ok(!commandCalled, "Shortcut key shouldn't trigger the command");
}

View File

@ -9,6 +9,14 @@ add_task(function* () {
let toolbox = yield openNewTabAndToolbox(URL, "inspector");
let textboxContextMenu = toolbox.textboxContextMenuPopup;
emptyClipboard();
// Make sure the focus is predictable.
let inspector = toolbox.getPanel("inspector");
let onFocus = once(inspector.searchBox, "focus");
inspector.searchBox.focus();
yield onFocus;
ok(textboxContextMenu, "The textbox context menu is loaded in the toolbox");
let cmdUndo = textboxContextMenu.querySelector("[command=cmd_undo]");
@ -26,10 +34,17 @@ add_task(function* () {
is(cmdUndo.getAttribute("disabled"), "true", "cmdUndo is disabled");
is(cmdDelete.getAttribute("disabled"), "true", "cmdDelete is disabled");
is(cmdSelectAll.getAttribute("disabled"), "", "cmdSelectAll is enabled");
is(cmdCut.getAttribute("disabled"), "true", "cmdCut is disabled");
is(cmdCopy.getAttribute("disabled"), "true", "cmdCopy is disabled");
is(cmdPaste.getAttribute("disabled"), "true", "cmdPaste is disabled");
is(cmdSelectAll.getAttribute("disabled"), "true", "cmdSelectAll is disabled");
// Cut/Copy items are enabled in context menu even if there
// is no selection. See also Bug 1303033
is(cmdCut.getAttribute("disabled"), "", "cmdCut is enabled");
is(cmdCopy.getAttribute("disabled"), "", "cmdCopy is enabled");
if (isWindows()) {
// emptyClipboard only works on Windows (666254), assert paste only for this OS.
is(cmdPaste.getAttribute("disabled"), "true", "cmdPaste is disabled");
}
yield cleanup(toolbox);
});

View File

@ -84,6 +84,7 @@ function testReload(shortcut, docked, toolID, callback) {
description = docked + " devtools with tool " + toolID + ", shortcut #" + shortcut;
info("Testing reload in " + description);
toolbox.win.focus();
synthesizeKeyShortcut(L10N.getStr(shortcut), toolbox.win);
reloadsSent++;
}

View File

@ -559,3 +559,20 @@ function stopRecordingTelemetryLogs(Telemetry) {
delete Telemetry.prototype._oldlogKeyed;
delete Telemetry.prototype.telemetryInfo;
}
/**
* Clean the logical clipboard content. This method only clears the OS clipboard on
* Windows (see Bug 666254).
*/
function emptyClipboard() {
let clipboard = Cc["@mozilla.org/widget/clipboard;1"]
.getService(SpecialPowers.Ci.nsIClipboard);
clipboard.emptyClipboard(clipboard.kGlobalClipboard);
}
/**
* Check if the current operating system is Windows.
*/
function isWindows() {
return Services.appinfo.OS === "WINNT";
}

View File

@ -14,7 +14,7 @@ const SCREENSIZE_HISTOGRAM = "DEVTOOLS_SCREEN_RESOLUTION_ENUMERATED_PER_USER";
const HTML_NS = "http://www.w3.org/1999/xhtml";
const { SourceMapService } = require("./source-map-service");
var {Cc, Ci, Cu} = require("chrome");
var {Ci, Cu} = require("chrome");
var promise = require("promise");
var defer = require("devtools/shared/defer");
var Services = require("Services");
@ -235,8 +235,8 @@ Toolbox.prototype = {
if (panel) {
deferred.resolve(panel);
} else {
this.on(id + "-ready", (e, panel) => {
deferred.resolve(panel);
this.on(id + "-ready", (e, initializedPanel) => {
deferred.resolve(initializedPanel);
});
}
@ -406,17 +406,17 @@ Toolbox.prototype = {
this.textboxContextMenuPopup.addEventListener("popupshowing",
this._updateTextboxMenuItems, true);
var shortcuts = new KeyShortcuts({
this.shortcuts = new KeyShortcuts({
window: this.doc.defaultView
});
this._buildDockButtons();
this._buildOptions(shortcuts);
this._buildOptions();
this._buildTabs();
this._applyCacheSettings();
this._applyServiceWorkersTestingSettings();
this._addKeysToWindow();
this._addReloadKeys(shortcuts);
this._addHostListeners(shortcuts);
this._addReloadKeys();
this._addHostListeners();
this._registerOverlays();
if (!this._hostOptions || this._hostOptions.zoom === true) {
ZoomKeys.register(this.win);
@ -502,7 +502,8 @@ Toolbox.prototype = {
this._telemetry.logOncePerBrowserVersion(OS_HISTOGRAM, system.getOSCPU());
this._telemetry.logOncePerBrowserVersion(OS_IS_64_BITS,
Services.appinfo.is64Bit ? 1 : 0);
this._telemetry.logOncePerBrowserVersion(SCREENSIZE_HISTOGRAM, system.getScreenDimensions());
this._telemetry.logOncePerBrowserVersion(SCREENSIZE_HISTOGRAM,
system.getScreenDimensions());
this._telemetry.log(HOST_HISTOGRAM, this._getTelemetryHostId());
},
@ -529,7 +530,7 @@ Toolbox.prototype = {
}
},
_buildOptions: function (shortcuts) {
_buildOptions: function () {
let selectOptions = (name, event) => {
// Flip back to the last used panel if we are already
// on the options panel.
@ -542,8 +543,8 @@ Toolbox.prototype = {
// Prevent the opening of bookmarks window on toolbox.options.key
event.preventDefault();
};
shortcuts.on(L10N.getStr("toolbox.options.key"), selectOptions);
shortcuts.on(L10N.getStr("toolbox.help.key"), selectOptions);
this.shortcuts.on(L10N.getStr("toolbox.options.key"), selectOptions);
this.shortcuts.on(L10N.getStr("toolbox.help.key"), selectOptions);
},
_splitConsoleOnKeypress: function (e) {
@ -561,26 +562,24 @@ Toolbox.prototype = {
* Add a shortcut key that should work when a split console
* has focus to the toolbox.
*
* @param {element} keyElement
* They <key> XUL element describing the shortcut key
* @param {string} whichTool
* The tool the key belongs to. The corresponding command
* will only trigger if this tool is active.
* @param {String} key
* The electron key shortcut.
* @param {Function} handler
* The callback that should be called when the provided key shortcut is pressed.
* @param {String} whichTool
* The tool the key belongs to. The corresponding handler will only be triggered
* if this tool is active.
*/
useKeyWithSplitConsole: function (keyElement, whichTool) {
let cloned = keyElement.cloneNode();
cloned.setAttribute("oncommand", "void(0)");
cloned.removeAttribute("command");
cloned.addEventListener("command", (e) => {
// Only forward the command if the tool is active
useKeyWithSplitConsole: function (key, handler, whichTool) {
this.shortcuts.on(key, (name, event) => {
if (this.currentToolId === whichTool && this.isSplitConsoleFocused()) {
keyElement.doCommand();
handler();
event.preventDefault();
}
}, true);
this.doc.getElementById("toolbox-keyset").appendChild(cloned);
});
},
_addReloadKeys: function (shortcuts) {
_addReloadKeys: function () {
[
["reload", false],
["reload2", false],
@ -588,7 +587,7 @@ Toolbox.prototype = {
["forceReload2", true]
].forEach(([id, force]) => {
let key = L10N.getStr("toolbox." + id + ".key");
shortcuts.on(key, (name, event) => {
this.shortcuts.on(key, (name, event) => {
this.reloadTarget(force);
// Prevent Firefox shortcuts from reloading the page
@ -597,23 +596,23 @@ Toolbox.prototype = {
});
},
_addHostListeners: function (shortcuts) {
shortcuts.on(L10N.getStr("toolbox.nextTool.key"),
_addHostListeners: function () {
this.shortcuts.on(L10N.getStr("toolbox.nextTool.key"),
(name, event) => {
this.selectNextTool();
event.preventDefault();
});
shortcuts.on(L10N.getStr("toolbox.previousTool.key"),
this.shortcuts.on(L10N.getStr("toolbox.previousTool.key"),
(name, event) => {
this.selectPreviousTool();
event.preventDefault();
});
shortcuts.on(L10N.getStr("toolbox.minimize.key"),
this.shortcuts.on(L10N.getStr("toolbox.minimize.key"),
(name, event) => {
this._toggleMinimizeMode();
event.preventDefault();
});
shortcuts.on(L10N.getStr("toolbox.toggleHost.key"),
this.shortcuts.on(L10N.getStr("toolbox.toggleHost.key"),
(name, event) => {
this.switchToPreviousHost();
event.preventDefault();
@ -976,16 +975,16 @@ Toolbox.prototype = {
this._requisition = requisition;
const spec = CommandUtils.getCommandbarSpec("devtools.toolbox.toolbarSpec");
return CommandUtils.createButtons(spec, this.target, this.doc,
requisition).then(buttons => {
let container = this.doc.getElementById("toolbox-buttons");
buttons.forEach(button=> {
if (button) {
container.appendChild(button);
}
});
this.setToolboxButtonsVisibility();
});
return CommandUtils.createButtons(spec, this.target, this.doc, requisition)
.then(buttons => {
let container = this.doc.getElementById("toolbox-buttons");
buttons.forEach(button => {
if (button) {
container.appendChild(button);
}
});
this.setToolboxButtonsVisibility();
});
});
},
@ -996,7 +995,8 @@ Toolbox.prototype = {
_buildPickerButton: function () {
this._pickerButton = this.doc.createElementNS(HTML_NS, "button");
this._pickerButton.id = "command-button-pick";
this._pickerButton.className = "command-button command-button-invertable devtools-button";
this._pickerButton.className =
"command-button command-button-invertable devtools-button";
this._pickerButton.setAttribute("title", L10N.getStr("pickButton.tooltip"));
this._pickerButton.setAttribute("hidden", "true");
@ -1082,7 +1082,9 @@ Toolbox.prototype = {
let on = true;
try {
on = Services.prefs.getBoolPref(visibilityswitch);
} catch (ex) { }
} catch (ex) {
// Do nothing.
}
on = on && isTargetSupported(this.target);
@ -1226,8 +1228,8 @@ Toolbox.prototype = {
if (panel) {
deferred.resolve(panel);
} else {
this.once(id + "-ready", panel => {
deferred.resolve(panel);
this.once(id + "-ready", initializedPanel => {
deferred.resolve(initializedPanel);
});
}
return deferred.promise;
@ -1598,7 +1600,7 @@ Toolbox.prototype = {
// Returns an instance of the preference actor
get _preferenceFront() {
return this.target.root.then(rootForm => {
return new getPreferenceFront(this.target.client, rootForm);
return getPreferenceFront(this.target.client, rootForm);
});
},
@ -1950,9 +1952,9 @@ Toolbox.prototype = {
if (!this._initInspector) {
this._initInspector = Task.spawn(function* () {
this._inspector = InspectorFront(this._target.client, this._target.form);
this._walker = yield this._inspector.getWalker(
{showAllAnonymousContent: Services.prefs.getBoolPref("devtools.inspector.showAllAnonymousContent")}
);
let pref = "devtools.inspector.showAllAnonymousContent";
let showAllAnonymousContent = Services.prefs.getBoolPref(pref);
this._walker = yield this._inspector.getWalker({ showAllAnonymousContent });
this._selection = new Selection(this._walker);
if (this.highlighterUtils.isRemoteHighlightable()) {
@ -1976,7 +1978,7 @@ Toolbox.prototype = {
return this._destroyingInspector;
}
return this._destroyingInspector = Task.spawn(function* () {
this._destroyingInspector = Task.spawn(function* () {
if (!this._inspector) {
return;
}
@ -1988,7 +1990,9 @@ Toolbox.prototype = {
if (this._walker && !this.walker.traits.autoReleased) {
try {
yield this._walker.release();
} catch (e) {}
} catch (e) {
// Do nothing;
}
}
yield this.highlighterUtils.stopPicker();
@ -2018,6 +2022,7 @@ Toolbox.prototype = {
this._selection = null;
this._walker = null;
}.bind(this));
return this._destroyingInspector;
},
/**
@ -2233,7 +2238,7 @@ Toolbox.prototype = {
// If target does not have profiler actor (addons), do not
// even register the shared performance connection.
if (!this.target.hasActor("profiler")) {
return;
return promise.resolve();
}
if (this._performanceFrontConnection) {
@ -2272,10 +2277,11 @@ Toolbox.prototype = {
}),
/**
* Called when any event comes from the PerformanceFront. If the performance tool is already
* loaded when the first event comes in, immediately unbind this handler, as this is
* only used to queue up observed recordings before the performance tool can handle them,
* which will only occur when `console.profile()` recordings are started before the tool loads.
* Called when any event comes from the PerformanceFront. If the performance tool is
* already loaded when the first event comes in, immediately unbind this handler, as
* this is only used to queue up observed recordings before the performance tool can
* handle them, which will only occur when `console.profile()` recordings are started
* before the tool loads.
*/
_onPerformanceFrontEvent: Task.async(function* (eventName, recording) {
if (this.getPanel("performance")) {
@ -2283,7 +2289,8 @@ Toolbox.prototype = {
return;
}
let recordings = this._performanceQueuedRecordings = this._performanceQueuedRecordings || [];
this._performanceQueuedRecordings = this._performanceQueuedRecordings || [];
let recordings = this._performanceQueuedRecordings;
// Before any console recordings, we'll get a `console-profile-start` event
// warning us that a recording will come later (via `recording-started`), so

View File

@ -31,7 +31,6 @@
<commandset id="editMenuCommands"/>
<keyset id="editMenuKeys"/>
<keyset id="toolbox-keyset"/>
<popupset>
<menupopup id="toolbox-textbox-context-popup">

View File

@ -12,19 +12,31 @@ const { DOM, createClass, PropTypes } = require("devtools/client/shared/vendor/r
const { div } = DOM;
/**
* Side panel for the Inspector panel.
* This side panel is using an existing DOM node as a content.
* Helper panel component that is using an existing DOM node
* as the content. It's used by Sidebar as well as SplitBox
* components.
*/
var InspectorTabPanel = createClass({
displayName: "InspectorTabPanel",
propTypes: {
// ID of the node that should be rendered as the content.
id: PropTypes.string.isRequired,
// Optional prefix for panel IDs.
idPrefix: PropTypes.string,
// Optional mount callback
onMount: PropTypes.func,
},
getDefaultProps: function () {
return {
idPrefix: "",
};
},
componentDidMount: function () {
let doc = this.refs.content.ownerDocument;
let panel = doc.getElementById("sidebar-panel-" + this.props.id);
let panel = doc.getElementById(this.props.idPrefix + this.props.id);
// Append existing DOM node into panel's content.
this.refs.content.appendChild(panel);

View File

@ -29,6 +29,13 @@ add_task(function* () {
let cmdPaste = searchContextMenu.querySelector("[command=cmd_paste]");
info("Opening context menu");
emptyClipboard();
let onFocus = once(searchField, "focus");
searchField.focus();
yield onFocus;
let onContextMenuPopup = once(searchContextMenu, "popupshowing");
EventUtils.synthesizeMouse(searchField, 2, 2,
{type: "contextmenu", button: 2}, win);
@ -36,10 +43,17 @@ add_task(function* () {
is(cmdUndo.getAttribute("disabled"), "true", "cmdUndo is disabled");
is(cmdDelete.getAttribute("disabled"), "true", "cmdDelete is disabled");
is(cmdSelectAll.getAttribute("disabled"), "", "cmdSelectAll is enabled");
is(cmdCut.getAttribute("disabled"), "true", "cmdCut is disabled");
is(cmdCopy.getAttribute("disabled"), "true", "cmdCopy is disabled");
is(cmdPaste.getAttribute("disabled"), "true", "cmdPaste is disabled");
is(cmdSelectAll.getAttribute("disabled"), "true", "cmdSelectAll is disabled");
// Cut/Copy items are enabled in context menu even if there
// is no selection. See also Bug 1303033
is(cmdCut.getAttribute("disabled"), "", "cmdCut is enabled");
is(cmdCopy.getAttribute("disabled"), "", "cmdCopy is enabled");
if (isWindows()) {
// emptyClipboard only works on Windows (666254), assert paste only for this OS.
is(cmdPaste.getAttribute("disabled"), "true", "cmdPaste is disabled");
}
info("Closing context menu");
let onContextMenuHidden = once(searchContextMenu, "popuphidden");

View File

@ -31,10 +31,18 @@ const {ToolSidebar} = require("devtools/client/inspector/toolsidebar");
const {ViewHelpers} = require("devtools/client/shared/widgets/view-helpers");
const clipboardHelper = require("devtools/shared/platform/clipboard");
const {LocalizationHelper} = require("devtools/shared/l10n");
const {LocalizationHelper, localizeMarkup} = require("devtools/shared/l10n");
const INSPECTOR_L10N = new LocalizationHelper("devtools/locale/inspector.properties");
const TOOLBOX_L10N = new LocalizationHelper("devtools/locale/toolbox.properties");
// Sidebar dimensions
const INITIAL_SIDEBAR_SIZE = 350;
const MIN_SIDEBAR_SIZE = 50;
// If the toolbox width is smaller than given amount of pixels,
// the sidebar automatically switches from 'landscape' to 'portrait' mode.
const PORTRAIT_MODE_WIDTH = 700;
/**
* Represents an open instance of the Inspector for a tab.
* The inspector controls the breadcrumbs, the markup view, and the sidebar
@ -94,6 +102,9 @@ function InspectorPanel(iframeWindow, toolbox) {
this.onDetached = this.onDetached.bind(this);
this.onPaneToggleButtonClicked = this.onPaneToggleButtonClicked.bind(this);
this._onMarkupFrameLoad = this._onMarkupFrameLoad.bind(this);
this.onPanelWindowResize = this.onPanelWindowResize.bind(this);
this.onSidebarShown = this.onSidebarShown.bind(this);
this.onSidebarHidden = this.onSidebarHidden.bind(this);
this._target.on("will-navigate", this._onBeforeNavigate);
this._detectingActorFeatures = this._detectActorFeatures();
@ -108,6 +119,9 @@ InspectorPanel.prototype = {
* open is effectively an asynchronous constructor
*/
open: Task.async(function* () {
// Localize all the nodes containing a data-localization attribute.
localizeMarkup(this.panelDoc);
this._cssPropertiesLoaded = initCssProperties(this.toolbox);
yield this._cssPropertiesLoaded;
yield this.target.makeRemote();
@ -400,6 +414,98 @@ InspectorPanel.prototype = {
return this._toolbox.browserRequire;
},
get InspectorTabPanel() {
if (!this._InspectorTabPanel) {
this._InspectorTabPanel =
this.React.createFactory(this.browserRequire(
"devtools/client/inspector/components/inspector-tab-panel"));
}
return this._InspectorTabPanel;
},
/**
* Build Splitter located between the main and side area of
* the Inspector panel.
*/
setupSplitter: function () {
let SplitBox = this.React.createFactory(this.browserRequire(
"devtools/client/shared/components/splitter/split-box"));
this.panelWin.addEventListener("resize", this.onPanelWindowResize, true);
let splitter = SplitBox({
className: "inspector-sidebar-splitter",
initialWidth: INITIAL_SIDEBAR_SIZE,
initialHeight: INITIAL_SIDEBAR_SIZE,
minSize: MIN_SIDEBAR_SIZE,
splitterSize: 1,
endPanelControl: true,
startPanel: this.InspectorTabPanel({
id: "inspector-main-content"
}),
endPanel: this.InspectorTabPanel({
id: "inspector-sidebar-container"
})
});
this._splitter = this.ReactDOM.render(splitter,
this.panelDoc.getElementById("inspector-splitter-box"));
// Persist splitter state in preferences.
this.sidebar.on("show", this.onSidebarShown);
this.sidebar.on("hide", this.onSidebarHidden);
this.sidebar.on("destroy", this.onSidebarHidden);
},
/**
* Splitter clean up.
*/
teardownSplitter: function () {
this.panelWin.removeEventListener("resize", this.onPanelWindowResize, true);
this.sidebar.off("show", this.onSidebarShown);
this.sidebar.off("hide", this.onSidebarHidden);
this.sidebar.off("destroy", this.onSidebarHidden);
},
/**
* If Toolbox width is less than 600 px, the splitter changes its mode
* to `horizontal` to support portrait view.
*/
onPanelWindowResize: function () {
let box = this.panelDoc.getElementById("inspector-splitter-box");
this._splitter.setState({
vert: (box.clientWidth > PORTRAIT_MODE_WIDTH)
});
},
onSidebarShown: function () {
let width;
let height;
// Initialize splitter size from preferences.
try {
width = Services.prefs.getIntPref("devtools.toolsidebar-width.inspector");
height = Services.prefs.getIntPref("devtools.toolsidebar-height.inspector");
} catch (e) {
// Set width and height of the splitter. Only one
// value is really useful at a time depending on the current
// orientation (vertical/horizontal).
// Having both is supported by the splitter component.
width = INITIAL_SIDEBAR_SIZE;
height = INITIAL_SIDEBAR_SIZE;
}
this._splitter.setState({width, height});
},
onSidebarHidden: function () {
// Store the current splitter size to preferences.
let state = this._splitter.state;
Services.prefs.setIntPref("devtools.toolsidebar-width.inspector", state.width);
Services.prefs.setIntPref("devtools.toolsidebar-height.inspector", state.height);
},
/**
* Build the sidebar.
*/
@ -455,56 +561,13 @@ InspectorPanel.prototype = {
this.sidebar.toggleTab(true, "fontinspector");
}
this.setupSidebarSize();
// Setup the splitter before the sidebar is displayed so,
// we don't miss any events.
this.setupSplitter();
this.sidebar.show(defaultTab);
},
/**
* Sidebar size is currently driven by vbox.inspector-sidebar-container
* element, which is located at the left/bottom side of the side bar splitter.
* Its size is changed by the splitter and stored into preferences.
* As soon as bug 1260552 is fixed and new HTML based splitter in place
* the size can be driven by div.inspector-sidebar element. This element
* represents the ToolSidebar and so, the entire logic related to size
* persistence can be done inside the ToolSidebar.
*/
setupSidebarSize: function () {
let sidePaneContainer = this.panelDoc.querySelector(
"#inspector-sidebar-container");
this.sidebar.on("show", () => {
try {
sidePaneContainer.width = Services.prefs.getIntPref(
"devtools.toolsidebar-width.inspector");
sidePaneContainer.height = Services.prefs.getIntPref(
"devtools.toolsidebar-height.inspector");
} catch (e) {
// The default width is the min-width set in CSS
// for #inspector-sidebar-container
// Set width and height of the sidebar container. Only one
// value is really useful at a time depending on the current
// toolbox orientation and having both doesn't break anything.
sidePaneContainer.width = 450;
sidePaneContainer.height = 450;
}
});
this.sidebar.on("hide", () => {
Services.prefs.setIntPref("devtools.toolsidebar-width.inspector",
sidePaneContainer.width);
Services.prefs.setIntPref("devtools.toolsidebar-height.inspector",
sidePaneContainer.height);
});
this.sidebar.on("destroy", () => {
Services.prefs.setIntPref("devtools.toolsidebar-width.inspector",
sidePaneContainer.width);
Services.prefs.setIntPref("devtools.toolsidebar-height.inspector",
sidePaneContainer.height);
});
},
setupToolbar: function () {
this.teardownToolbar();
@ -798,6 +861,9 @@ InspectorPanel.prototype = {
this.sidebar.off("select", this._setDefaultSidebar);
let sidebarDestroyer = this.sidebar.destroy();
this.teardownSplitter();
this.sidebar = null;
this.teardownToolbar();
@ -1251,7 +1317,8 @@ InspectorPanel.prototype = {
* state and tooltip.
*/
onPaneToggleButtonClicked: function (e) {
let sidePaneContainer = this.panelDoc.querySelector("#inspector-sidebar-container");
let sidePaneContainer = this.panelDoc.querySelector(
"#inspector-splitter-box .controlled");
let isVisible = !this._sidebarToggle.state.collapsed;
// Make sure the sidebar has width and height attributes before collapsing

View File

@ -16,73 +16,77 @@
<?xml-stylesheet href="resource://devtools/client/shared/components/tabs/tabbar.css" type="text/css"?>
<?xml-stylesheet href="resource://devtools/client/inspector/components/side-panel.css" type="text/css"?>
<?xml-stylesheet href="resource://devtools/client/inspector/components/inspector-tab-panel.css" type="text/css"?>
<?xml-stylesheet href="resource://devtools/client/shared/components/splitter/split-box.css" type="text/css"?>
<!DOCTYPE window [
<!ENTITY % inspectorDTD SYSTEM "chrome://devtools/locale/inspector.dtd"> %inspectorDTD;
<!ENTITY % styleinspectorDTD SYSTEM "chrome://devtools/locale/styleinspector.dtd"> %styleinspectorDTD;
<!ENTITY % fontinspectorDTD SYSTEM "chrome://devtools/locale/font-inspector.dtd"> %fontinspectorDTD;
<!ENTITY % layoutviewDTD SYSTEM "chrome://devtools/locale/layoutview.dtd"> %layoutviewDTD;
]>
<!DOCTYPE window>
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:html="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<script type="application/javascript;version=1.8"
src="chrome://devtools/content/shared/theme-switching.js"/>
src="chrome://devtools/content/shared/theme-switching.js"></script>
</head>
<body class="theme-body devtools-monospace" role="application">
<html:div class="inspector-responsive-container theme-body inspector">
<box flex="1" class="devtools-responsive-container theme-body">
<vbox flex="1" class="devtools-main-content">
<html:div id="inspector-toolbar"
class="devtools-toolbar"
nowindowdrag="true">
<html:button id="inspector-element-add-button"
title="&inspectorAddNode.label;"
class="devtools-button" />
<!-- Main Panel Content -->
<html:div id="inspector-main-content" class="devtools-main-content">
<html:div id="inspector-toolbar" class="devtools-toolbar" nowindowdrag="true"
data-localization-bundle="devtools/locale/inspector.properties">
<html:button id="inspector-element-add-button" class="devtools-button"
data-localization="title=inspectorAddNode.label"/>
<html:div class="devtools-toolbar-spacer" />
<html:span id="inspector-searchlabel" />
<html:div id="inspector-search" class="devtools-searchbox has-clear-btn">
<html:input id="inspector-searchbox" class="devtools-searchinput"
type="search"
placeholder="&inspectorSearchHTML.label3;"/>
data-localization="placeholder=inspectorSearchHTML.label3"/>
<html:button id="inspector-searchinput-clear" class="devtools-searchinput-clear" tabindex="-1"></html:button>
</html:div>
<html:button id="inspector-eyedropper-toggle"
title="&inspectorEyeDropper.label;"
data-localization="title=inspector.eyedropper.label"
class="devtools-button command-button-invertable" />
<div xmlns="http://www.w3.org/1999/xhtml"
id="inspector-sidebar-toggle-box" />
<html:div id="inspector-sidebar-toggle-box" />
</html:div>
<vbox flex="1" id="markup-box">
</vbox>
<html:div id="markup-box" />
<html:div id="inspector-breadcrumbs-toolbar" class="devtools-toolbar">
<html:div id="inspector-breadcrumbs" class="breadcrumbs-widget-container"
role="group" aria-label="&inspectorBreadcrumbsGroup;" tabindex="0" />
role="group" data-localization="aria-label=inspector.breadcrumbs.label" tabindex="0" />
</html:div>
</vbox>
<splitter class="devtools-side-splitter"/>
<vbox id="inspector-sidebar-container">
<!-- Specify the XHTML namespace explicitly
otherwise the layout is broken. -->
<div xmlns="http://www.w3.org/1999/xhtml"
id="inspector-sidebar"
hidden="true" />
</vbox>
</html:div>
<!-- Splitter -->
<html:div
xmlns="http://www.w3.org/1999/xhtml"
id="inspector-splitter-box">
</html:div>
<!-- Sidebar Container -->
<html:div id="inspector-sidebar-container">
<html:div
xmlns="http://www.w3.org/1999/xhtml"
id="inspector-sidebar"
hidden="true" />
</html:div>
<!-- Sidebar panel definitions -->
<html:div xmlns="http://www.w3.org/1999/xhtml" id="tabpanels" style="visibility:collapse">
<html:div id="sidebar-panel-ruleview" class="devtools-monospace theme-sidebar inspector-tabpanel">
<html:div id="tabpanels" style="visibility:collapse">
<html:div id="sidebar-panel-ruleview" class="devtools-monospace theme-sidebar inspector-tabpanel"
data-localization-bundle="devtools/locale/inspector.properties">
<html:div id="ruleview-toolbar-container" class="devtools-toolbar">
<html:div id="ruleview-toolbar">
<html:div class="devtools-searchbox has-clear-btn">
<html:input id="ruleview-searchbox"
class="devtools-filterinput devtools-rule-searchbox"
type="search"
placeholder="&filterStylesPlaceholder;"/>
data-localization="placeholder=inspector.filterStyles.placeholder"/>
<html:button id="ruleview-searchinput-clear" class="devtools-searchinput-clear"></html:button>
</html:div>
<html:div id="ruleview-command-toolbar">
<html:button id="ruleview-add-rule-button" title="&addRuleButtonTooltip;" class="devtools-button"></html:button>
<html:button id="pseudo-class-panel-toggle" title="&togglePseudoClassPanel;" class="devtools-button"></html:button>
<html:button id="ruleview-add-rule-button" data-localization="title=inspector.addRule.tooltip" class="devtools-button"></html:button>
<html:button id="pseudo-class-panel-toggle" data-localization="title=inspector.togglePseudo.tooltip" class="devtools-button"></html:button>
</html:div>
</html:div>
<html:div id="pseudo-class-panel" hidden="true">
@ -98,39 +102,41 @@
</html:div>
</html:div>
<html:div id="sidebar-panel-computedview" class="devtools-monospace theme-sidebar inspector-tabpanel">
<html:div id="sidebar-panel-computedview" class="devtools-monospace theme-sidebar inspector-tabpanel"
data-localization-bundle="devtools/locale/inspector.properties">
<html:div id="computedview-toolbar" class="devtools-toolbar">
<html:div class="devtools-searchbox has-clear-btn">
<html:input id="computedview-searchbox"
class="devtools-filterinput devtools-rule-searchbox"
type="search"
placeholder="&filterStylesPlaceholder;"/>
data-localization="placeholder=inspector.filterStyles.placeholder"/>
<html:button id="computedview-searchinput-clear" class="devtools-searchinput-clear"></html:button>
</html:div>
<html:label id="browser-style-checkbox-label" for="browser-style-checkbox">
<html:input id="browser-style-checkbox"
type="checkbox"
class="includebrowserstyles"
label="&browserStylesLabel;"/>&browserStylesLabel;</html:label>
<html:input id="browser-style-checkbox"
type="checkbox"
class="includebrowserstyles"/>
<html:label id="browser-style-checkbox-label" for="browser-style-checkbox"
data-localization="content=inspector.browserStyles.label"/>
</html:div>
<html:div id="computedview-container">
<html:div id="computedview-container-focusable" tabindex="-1">
<html:div id="boxmodel-wrapper" tabindex="0">
<html:div id="boxmodel-wrapper" tabindex="0"
data-localization-bundle="devtools/locale/boxmodel.properties">
<html:div id="boxmodel-header">
<html:div id="boxmodel-expander" class="expander theme-twisty expandable" open=""></html:div>
<html:span>&layoutViewTitle;</html:span>
<html:span data-localization="content=boxmodel.title"/>
</html:div>
<html:div id="boxmodel-container">
<html:div id="boxmodel-main">
<html:span class="boxmodel-legend" data-box="margin" title="&margin.tooltip;">&margin.tooltip;</html:span>
<html:div id="boxmodel-margins" data-box="margin" title="&margin.tooltip;">
<html:span class="boxmodel-legend" data-box="border" title="&border.tooltip;">&border.tooltip;</html:span>
<html:div id="boxmodel-borders" data-box="border" title="&border.tooltip;">
<html:span class="boxmodel-legend" data-box="padding" title="&padding.tooltip;">&padding.tooltip;</html:span>
<html:div id="boxmodel-padding" data-box="padding" title="&padding.tooltip;">
<html:div id="boxmodel-content" data-box="content" title="&content.tooltip;">
<html:span class="boxmodel-legend" data-box="margin" data-localization="content=boxmodel.margin;title=boxmodel.margin"/>
<html:div id="boxmodel-margins" data-box="margin" data-localization="title=boxmodel.margin">
<html:span class="boxmodel-legend" data-box="border" data-localization="content=boxmodel.border;title=boxmodel.border"/>
<html:div id="boxmodel-borders" data-box="border" data-localization="title=boxmodel.border">
<html:span class="boxmodel-legend" data-box="padding" data-localization="content=boxmodel.padding;title=boxmodel.padding"/>
<html:div id="boxmodel-padding" data-box="padding" data-localization="title=boxmodel.padding">
<html:div id="boxmodel-content" data-box="content" data-localization="title=boxmodel.content">
</html:div>
</html:div>
</html:div>
@ -151,13 +157,16 @@
<html:p class="boxmodel-padding boxmodel-bottom"><html:span data-box="padding" class="boxmodel-editable" title="padding-bottom"></html:span></html:p>
<html:p class="boxmodel-padding boxmodel-left"><html:span data-box="padding" class="boxmodel-editable" title="padding-left"></html:span></html:p>
<html:p class="boxmodel-size"><html:span data-box="content" title="&content.tooltip;"></html:span></html:p>
<html:p class="boxmodel-size">
<html:span data-box="content" data-localization="title=boxmodel.content"></html:span>
</html:p>
</html:div>
<html:div id="boxmodel-info">
<html:span id="boxmodel-element-size"></html:span>
<html:section id="boxmodel-position-group">
<html:button class="devtools-button" id="layout-geometry-editor" title="&geometry.button.tooltip;"></html:button>
<html:button class="devtools-button" id="layout-geometry-editor"
data-localization="title=boxmodel.geometryButton.tooltip"></html:button>
<html:span id="boxmodel-element-position"></html:span>
</html:section>
</html:div>
@ -171,22 +180,21 @@
<html:div id="propertyContainer" class="theme-separator" tabindex="0">
</html:div>
<html:div id="computedview-no-results" hidden="">
&noPropertiesFound;
</html:div>
<html:div id="computedview-no-results" hidden="" data-localization="content=inspector.noProperties"/>
</html:div>
</html:div>
</html:div>
<html:div id="sidebar-panel-fontinspector" class="devtools-monospace theme-sidebar inspector-tabpanel">
<html:div id="sidebar-panel-fontinspector" class="devtools-monospace theme-sidebar inspector-tabpanel"
data-localization-bundle="devtools/locale/font-inspector.properties">
<html:div class="devtools-toolbar">
<html:div class="devtools-searchbox">
<html:input id="font-preview-text-input"
class="devtools-textinput"
type="search"
placeholder="&previewHint;"/>
<html:input id="font-preview-text-input" class="devtools-textinput" type="search"
data-localization="placeholder=fontinspector.previewText"/>
</html:div>
<html:label id="font-showall" class="theme-link" title="&showAllFonts;">&showAllFontsUsed;</html:label>
<html:label id="font-showall" class="theme-link"
data-localization="content=fontinspector.seeAll;
title=fontinspector.seeAll.tooltip"/>
</html:div>
<html:div id="font-container">
@ -200,13 +208,15 @@
</html:div>
<html:div class="font-info">
<html:h1 class="font-name"></html:h1>
<html:span class="font-is-local">&system;</html:span>
<html:span class="font-is-remote">&remote;</html:span>
<html:span class="font-is-local" data-localization="content=fontinspector.system"/>
<html:span class="font-is-remote" data-localization="content=fontinspector.remote"/>
<html:p class="font-format-url">
<html:input readonly="readonly" class="font-url"></html:input>
<html:span class="font-format"></html:span>
</html:p>
<html:p class="font-css">&usedAs; "<html:span class="font-css-name"></html:span>"</html:p>
<html:p class="font-css">
<html:span data-localization="content=fontinspector.usedAs"/> "<html:span class="font-css-name"></html:span>"
</html:p>
<html:pre class="font-css-code"></html:pre>
</html:div>
</html:section>
@ -218,5 +228,6 @@
</html:div>
</html:div>
</box>
</window>
</html:div>
</body>
</html>

View File

@ -16,6 +16,7 @@ DevToolsModules(
'inspector-commands.js',
'inspector-panel.js',
'inspector-search.js',
'inspector.xhtml',
'toolsidebar.js',
)

View File

@ -38,6 +38,7 @@ add_task(function* () {
"Should have cancelled creating a new text property.");
ok(!elementRuleEditor.propertyList.hasChildNodes(),
"Should not have any properties.");
is(view.styleDocument.activeElement, view.styleDocument.documentElement,
is(view.styleDocument.activeElement, view.styleDocument.body,
"Correct element has focus");
});

View File

@ -23,7 +23,7 @@ add_task(function* () {
info("Test creating a new property and escaping");
yield addProperty(view, 1, "color", "red", "VK_ESCAPE", false);
is(view.styleDocument.documentElement, view.styleDocument.activeElement,
is(view.styleDocument.activeElement, view.styleDocument.body,
"Correct element has focus");
let elementRuleEditor = getRuleViewRuleEditor(view, 1);

View File

@ -38,6 +38,6 @@ add_task(function* () {
is(elementRuleEditor.rule.textProps.length, 1,
"Should have canceled creating a new text property.");
is(view.styleDocument.documentElement, view.styleDocument.activeElement,
is(view.styleDocument.activeElement, view.styleDocument.body,
"Correct element has focus");
});

View File

@ -51,7 +51,7 @@ add_task(function* () {
let onHidden = cPicker.tooltip.once("hidden");
// Validating the color change ends up updating the rule view twice
let onRuleViewChanged = waitForNEvents(view, "ruleview-changed", 2);
EventUtils.sendKey("RETURN", spectrum.element.ownerDocument.defaultView);
focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN");
yield onHidden;
yield onRuleViewChanged;

View File

@ -56,7 +56,7 @@ function* basicTest(view, name, result) {
let onHidden = cPicker.tooltip.once("hidden");
// Validating the color change ends up updating the rule view twice
let onRuleViewChanged = waitForNEvents(view, "ruleview-changed", 2);
EventUtils.sendKey("RETURN", spectrum.element.ownerDocument.defaultView);
focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN");
yield onHidden;
yield onRuleViewChanged;

View File

@ -48,7 +48,7 @@ function* testImageTooltipAfterColorChange(swatch, url, ruleView) {
let spectrum = picker.spectrum;
let onHidden = picker.tooltip.once("hidden");
let onModifications = ruleView.once("ruleview-changed");
EventUtils.sendKey("RETURN", spectrum.element.ownerDocument.defaultView);
focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN");
yield onHidden;
yield onModifications;

View File

@ -46,7 +46,7 @@ function* testColorChangeIsntRevertedWhenOtherTooltipIsShown(ruleView) {
let onModifications = waitForNEvents(ruleView, "ruleview-changed", 2);
let onHidden = picker.tooltip.once("hidden");
EventUtils.sendKey("RETURN", spectrum.element.ownerDocument.defaultView);
focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN");
yield onHidden;
yield onModifications;

View File

@ -47,7 +47,7 @@ function* testPressingEnterCommitsChanges(swatch, ruleView) {
let onModified = ruleView.once("ruleview-changed");
let spectrum = cPicker.spectrum;
let onHidden = cPicker.tooltip.once("hidden");
EventUtils.sendKey("RETURN", spectrum.element.ownerDocument.defaultView);
focusAndSendKey(spectrum.element.ownerDocument.defaultView, "RETURN");
yield onHidden;
yield onModified;

View File

@ -53,7 +53,7 @@ function* testPressingEnterCommitsChanges(swatch, ruleView) {
// Pressing RETURN ends up doing 2 rule-view updates, one for the preview and
// one for the commit when the tooltip closes.
let onRuleViewChanged = waitForNEvents(ruleView, "ruleview-changed", 2);
EventUtils.sendKey("RETURN", widget.parent.ownerDocument.defaultView);
focusAndSendKey(widget.parent.ownerDocument.defaultView, "RETURN");
yield onRuleViewChanged;
let style = yield getComputedStyleProperty("body", null,

View File

@ -94,7 +94,7 @@ function* escapeTooltip(view) {
let widget = yield bezierTooltip.widget;
let onHidden = bezierTooltip.tooltip.once("hidden");
let onModifications = view.once("ruleview-changed");
EventUtils.sendKey("ESCAPE", widget.parent.ownerDocument.defaultView);
focusAndSendKey(widget.parent.ownerDocument.defaultView, "ESCAPE");
yield onHidden;
yield onModifications;
}

View File

@ -64,6 +64,8 @@ add_task(function* () {
tooltip.hide();
yield onHidden;
ok(!tooltip.isVisible(), "color picker tooltip is closed");
yield waitForTick();
});
function* testESC(swatch, inspector, testActor) {

View File

@ -29,4 +29,6 @@ add_task(function* () {
"The inplace editor wasn't shown as a result of the filter swatch click");
yield hideTooltipAndWaitForRuleViewChanged(filterTooltip, view);
yield waitForTick();
});

View File

@ -28,6 +28,13 @@ add_task(function* () {
let cmdPaste = searchContextMenu.querySelector("[command=cmd_paste]");
info("Opening context menu");
emptyClipboard();
let onFocus = once(searchField, "focus");
searchField.focus();
yield onFocus;
let onContextMenuPopup = once(searchContextMenu, "popupshowing");
EventUtils.synthesizeMouse(searchField, 2, 2,
{type: "contextmenu", button: 2}, win);
@ -35,10 +42,17 @@ add_task(function* () {
is(cmdUndo.getAttribute("disabled"), "true", "cmdUndo is disabled");
is(cmdDelete.getAttribute("disabled"), "true", "cmdDelete is disabled");
is(cmdSelectAll.getAttribute("disabled"), "", "cmdSelectAll is enabled");
is(cmdCut.getAttribute("disabled"), "true", "cmdCut is disabled");
is(cmdCopy.getAttribute("disabled"), "true", "cmdCopy is disabled");
is(cmdPaste.getAttribute("disabled"), "true", "cmdPaste is disabled");
is(cmdSelectAll.getAttribute("disabled"), "true", "cmdSelectAll is disabled");
// Cut/Copy items are enabled in context menu even if there
// is no selection. See also Bug 1303033
is(cmdCut.getAttribute("disabled"), "", "cmdCut is enabled");
is(cmdCopy.getAttribute("disabled"), "", "cmdCopy is enabled");
if (isWindows()) {
// emptyClipboard only works on Windows (666254), assert paste only for this OS.
is(cmdPaste.getAttribute("disabled"), "true", "cmdPaste is disabled");
}
info("Closing context menu");
let onContextMenuHidden = once(searchContextMenu, "popuphidden");

View File

@ -58,7 +58,7 @@ const TEST_DATA = [
];
add_task(function* () {
requestLongerTimeout(2);
requestLongerTimeout(4);
info("Starting the test with the pref set to true before toolbox is opened");
yield setUserAgentStylesPref(true);

View File

@ -634,7 +634,9 @@ var togglePropStatus = Task.async(function* (view, textProp) {
*/
var focusNewRuleViewProperty = Task.async(function* (ruleEditor) {
info("Clicking on a close ruleEditor brace to start editing a new property");
ruleEditor.closeBrace.scrollIntoView();
// Use bottom alignment to avoid scrolling out of the parent element area.
ruleEditor.closeBrace.scrollIntoView(false);
let editor = yield focusEditableField(ruleEditor.ruleView,
ruleEditor.closeBrace);
@ -814,3 +816,13 @@ function waitForStyleModification(inspector) {
inspector.on("markupmutation", checkForStyleModification);
});
}
/**
* Make sure window is properly focused before sending a key event.
* @param {Window} win
* @param {Event} key
*/
function focusAndSendKey(win, key) {
win.document.documentElement.focus();
EventUtils.sendKey(key, win);
}

View File

@ -130,7 +130,6 @@ subsuite = clipboard
[browser_inspector_pane-toggle-01.js]
[browser_inspector_pane-toggle-02.js]
[browser_inspector_pane-toggle-03.js]
[browser_inspector_pane-toggle-04.js]
[browser_inspector_pane-toggle-05.js]
skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control in System Keyboard
[browser_inspector_picker-stop-on-destroy.js]

View File

@ -32,6 +32,10 @@ const NODES = [
];
add_task(function* () {
// This test needs specific initial size of the sidebar.
yield pushPref("devtools.toolsidebar-width.inspector", 350);
yield pushPref("devtools.toolsidebar-height.inspector", 150);
let { inspector, toolbox } = yield openInspectorForURL(TEST_URI);
// No way to wait for scrolling to end (Bug 1172171)

View File

@ -11,7 +11,8 @@ add_task(function* () {
info("Open the inspector in a side toolbox host");
let {toolbox, inspector} = yield openInspectorForURL("about:blank", "side");
let panel = inspector.panelDoc.querySelector("#inspector-sidebar-container");
let panel = inspector.panelDoc.querySelector("#inspector-splitter-box .controlled");
let button = inspector.panelDoc.querySelector(".sidebar-toggle");
ok(!panel.classList.contains("pane-collapsed"), "The panel is in expanded state");

View File

@ -10,7 +10,7 @@ add_task(function* () {
let {inspector} = yield openInspectorForURL("about:blank");
let button = inspector.panelDoc.querySelector(".sidebar-toggle");
let panel = inspector.panelDoc.querySelector("#inspector-sidebar-container");
let panel = inspector.panelDoc.querySelector("#inspector-splitter-box .controlled");
ok(!button.classList.contains("pane-collapsed"), "The button is in expanded state");

View File

@ -1,65 +0,0 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
let { Toolbox } = require("devtools/client/framework/toolbox");
// Test that the dimensions of the collapsed inspector panel are not modified
// when switching from horizontal to vertical layout, which is mandatory to make
// sure the panel remains visually hidden (using negative margins).
add_task(function* () {
info("Set temporary preferences to ensure a small sidebar width.");
yield new Promise(resolve => {
let options = {"set": [
["devtools.toolsidebar-width.inspector", 200]
]};
SpecialPowers.pushPrefEnv(options, resolve);
});
let { inspector, toolbox } = yield openInspectorForURL("about:blank");
let button = inspector.panelDoc.querySelector(".sidebar-toggle");
let panel = inspector.panelDoc.querySelector("#inspector-sidebar-container");
info("Changing toolbox host to a window.");
yield toolbox.switchHost(Toolbox.HostType.WINDOW);
let hostWindow = toolbox._host._window;
let originalWidth = hostWindow.outerWidth;
let originalHeight = hostWindow.outerHeight;
info("Resizing window to switch to the horizontal layout.");
hostWindow.resizeTo(800, 300);
// Check the sidebar is expanded when the test starts.
ok(!panel.classList.contains("pane-collapsed"), "The panel is in expanded state");
info("Collapse the inspector sidebar.");
let onTransitionEnd = once(panel, "transitionend");
EventUtils.synthesizeMouseAtCenter(button, {},
inspector.panelDoc.defaultView);
yield onTransitionEnd;
ok(panel.classList.contains("pane-collapsed"), "The panel is in collapsed state");
let currentPanelHeight = panel.getBoundingClientRect().height;
let currentPanelMarginBottom = panel.style.marginBottom;
info("Resizing window to switch to the vertical layout.");
hostWindow.resizeTo(300, 800);
// Check the panel is collapsed, and still has the same dimensions.
ok(panel.classList.contains("pane-collapsed"), "The panel is still collapsed");
is(panel.getBoundingClientRect().height, currentPanelHeight,
"The panel height has not been modified when changing the layout.");
is(panel.style.marginBottom, currentPanelMarginBottom,
"The panel margin-bottom has not been modified when changing the layout.");
info("Restoring window original size.");
hostWindow.resizeTo(originalWidth, originalHeight);
});
registerCleanupFunction(function () {
// Restore the host type for other tests.
Services.prefs.clearUserPref("devtools.toolbox.host");
});

View File

@ -10,7 +10,8 @@
add_task(function* () {
let {inspector} = yield openInspectorForURL("about:blank", "side");
let panel = inspector.panelDoc.querySelector("#inspector-sidebar-container");
let panel = inspector.panelDoc.querySelector("#inspector-splitter-box .controlled");
let button = inspector.panelDoc.querySelector(".sidebar-toggle");
ok(!panel.classList.contains("pane-collapsed"), "The panel is in expanded state");

View File

@ -26,7 +26,13 @@ add_task(function* () {
let cmdCopy = searchContextMenu.querySelector("[command=cmd_copy]");
let cmdPaste = searchContextMenu.querySelector("[command=cmd_paste]");
emptyClipboard();
info("Opening context menu");
let onFocus = once(searchBox, "focus");
searchBox.focus();
yield onFocus;
let onContextMenuPopup = once(searchContextMenu, "popupshowing");
EventUtils.synthesizeMouse(searchBox, 2, 2,
{type: "contextmenu", button: 2}, win);
@ -34,10 +40,17 @@ add_task(function* () {
is(cmdUndo.getAttribute("disabled"), "true", "cmdUndo is disabled");
is(cmdDelete.getAttribute("disabled"), "true", "cmdDelete is disabled");
is(cmdSelectAll.getAttribute("disabled"), "", "cmdSelectAll is enabled");
is(cmdCut.getAttribute("disabled"), "true", "cmdCut is disabled");
is(cmdCopy.getAttribute("disabled"), "true", "cmdCopy is disabled");
is(cmdPaste.getAttribute("disabled"), "true", "cmdPaste is disabled");
is(cmdSelectAll.getAttribute("disabled"), "true", "cmdSelectAll is disabled");
// Cut/Copy items are enabled in context menu even if there
// is no selection. See also Bug 1303033
is(cmdCut.getAttribute("disabled"), "", "cmdCut is enabled");
is(cmdCopy.getAttribute("disabled"), "", "cmdCopy is enabled");
if (isWindows()) {
// emptyClipboard only works on Windows (666254), assert paste only for this OS.
is(cmdPaste.getAttribute("disabled"), "true", "cmdPaste is disabled");
}
info("Closing context menu");
let onContextMenuHidden = once(searchContextMenu, "popuphidden");
@ -47,6 +60,7 @@ add_task(function* () {
info("Copy text in search field using the context menu");
searchBox.value = TEST_INPUT;
searchBox.select();
searchBox.focus();
EventUtils.synthesizeMouse(searchBox, 2, 2,
{type: "contextmenu", button: 2}, win);
yield onContextMenuPopup;

View File

@ -66,12 +66,7 @@ ToolSidebar.prototype = {
},
get InspectorTabPanel() {
if (!this._InspectorTabPanel) {
this._InspectorTabPanel =
this.React.createFactory(this.browserRequire(
"devtools/client/inspector/components/inspector-tab-panel"));
}
return this._InspectorTabPanel;
return this._toolPanel.InspectorTabPanel;
},
// Rendering
@ -90,7 +85,14 @@ ToolSidebar.prototype = {
},
addExistingTab: function (id, title, selected) {
this._tabbar.addTab(id, title, selected, this.InspectorTabPanel);
let panel = this.InspectorTabPanel({
id: id,
idPrefix: this.TABPANEL_ID_PREFIX,
key: id,
title: title,
});
this._tabbar.addTab(id, title, selected, panel);
this.emit("new-tab-registered", id);
},
@ -105,6 +107,7 @@ ToolSidebar.prototype = {
addFrameTab: function (id, title, url, selected) {
let panel = this.InspectorTabPanel({
id: id,
idPrefix: this.TABPANEL_ID_PREFIX,
key: id,
title: title,
url: url,

View File

@ -118,7 +118,7 @@ devtools.jar:
content/framework/dev-edition-promo/dev-edition-promo.xul (framework/dev-edition-promo/dev-edition-promo.xul)
* content/framework/dev-edition-promo/dev-edition-promo.css (framework/dev-edition-promo/dev-edition-promo.css)
content/framework/dev-edition-promo/dev-edition-logo.png (framework/dev-edition-promo/dev-edition-logo.png)
content/inspector/inspector.xul (inspector/inspector.xul)
content/inspector/inspector.xhtml (inspector/inspector.xhtml)
content/framework/connect/connect.xhtml (framework/connect/connect.xhtml)
content/framework/connect/connect.css (framework/connect/connect.css)
content/framework/connect/connect.js (framework/connect/connect.js)

View File

@ -0,0 +1,37 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# LOCALIZATION NOTE : FILE This file contains the Layout View strings.
# The Layout View is a panel displayed in the computed view tab of the Inspector sidebar.
# LOCALIZATION NOTE : FILE The correct localization of this file might be to
# keep it in English, or another language commonly spoken among web developers.
# You want to make that choice consistent across the developer tools.
# A good criteria is the language in which you'd find the best
# documentation on web development on the web.
# LOCALIZATION NOTE (boxmodel.title) This is the title of the box model panel and is
# displayed as a label.
boxmodel.title=Box Model
# LOCALIZATION NOTE (boxmodel.margin) This refers to the margin in the box model and
# might be displayed as a label or as a tooltip.
boxmodel.margin=margin
# LOCALIZATION NOTE (boxmodel.border) This refers to the border in the box model and
# might be displayed as a label or as a tooltip.
boxmodel.border=border
# LOCALIZATION NOTE (boxmodel.padding) This refers to the padding in the box model and
# might be displayed as a label or as a tooltip.
boxmodel.padding=padding
# LOCALIZATION NOTE (boxmodel.content) This refers to the content in the box model and
# might be displayed as a label or as a tooltip.
boxmodel.content=content
# LOCALIZATION NOTE: (boxmodel.geometryButton.tooltip) This label is displayed as a
# tooltip that appears when hovering over the button that allows users to edit the
# position of an element in the page.
boxmodel.geometryButton.tooltip=Edit position

View File

@ -197,13 +197,9 @@
- control the stepping commands in the debugger (continue, step over,
- step in and step out). -->
<!ENTITY debuggerUI.stepping.resume1 "VK_F8">
<!ENTITY debuggerUI.stepping.resume2 "VK_SLASH">
<!ENTITY debuggerUI.stepping.stepOver1 "VK_F10">
<!ENTITY debuggerUI.stepping.stepOver2 "VK_QUOTE">
<!ENTITY debuggerUI.stepping.stepIn1 "VK_F11">
<!ENTITY debuggerUI.stepping.stepIn2 "VK_SEMICOLON">
<!ENTITY debuggerUI.stepping.stepOut1 "VK_F11">
<!ENTITY debuggerUI.stepping.stepOut2 "VK_SEMICOLON">
<!-- LOCALIZATION NOTE (debuggerUI.context.newTab): This is the label
- for the Open in New Tab menu item displayed in the context menu of the

View File

@ -1,16 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<!-- LOCALIZATION NOTE : FILE This file contains the Font Inspector strings.
- The Font Inspector is the panel accessible in the Inspector sidebar. -->
<!ENTITY showAllFonts "See all the fonts used in the page">
<!ENTITY showAllFontsUsed "Show all fonts used">
<!ENTITY usedAs "Used as: ">
<!ENTITY system "system">
<!ENTITY remote "remote">
<!-- LOCALIZATION NOTE (previewHint): This is the label shown as the
placeholder in font inspector preview text box. -->
<!ENTITY previewHint "Preview Text">

View File

@ -0,0 +1,29 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# LOCALIZATION NOTE This file contains the Font Inspector strings.
# The Font Inspector is a panel accessible in the Inspector sidebar.
# LOCALIZATION NOTE (fontinspector.seeAll) This is the label of a link that will show all
# the fonts used in the page, instead of the ones related to the inspected element.
fontinspector.seeAll=Show all fonts used
# LOCALIZATION NOTE (fontinspector.seeAll.tooltip) see fontinspector.seeAll.
fontinspector.seeAll.tooltip=See all the fonts used in the page
# LOCALIZATION NOTE (fontinspector.usedAs) This label introduces the name used to refer to
# the font in a stylesheet.
fontinspector.usedAs=Used as:
# LOCALIZATION NOTE (fontinspector.system) This label indicates that the font is a local
# system font.
fontinspector.system=system
# LOCALIZATION NOTE (fontinspector.remote) This label indicates that the font is a remote
# font.
fontinspector.remote=remote
# LOCALIZATION NOTE (previewHint):
# This is the label shown as the placeholder in font inspector preview text box.
fontinspector.previewText=Preview Text

View File

@ -1,27 +0,0 @@
<!-- LOCALIZATION NOTE (inspectorRemoveAttribute.label): This is the label shown in
the inspector contextual-menu for the item that lets users delete attribute
from current node -->
<!ENTITY inspectorRemoveAttribute.label "Remove Attribute">
<!ENTITY inspectorRemoveAttribute.accesskey "R">
<!ENTITY inspector.selectButton.tooltip "Select element with mouse">
<!-- LOCALIZATION NOTE (inspectorSearchHTML.label3): This is the label that is
shown as the placeholder for the markup view search in the inspector. -->
<!ENTITY inspectorSearchHTML.label3 "Search HTML">
<!-- LOCALIZATION NOTE (inspectorAddNode.label): This is the label shown in
the inspector toolbar for the button that lets users add elements to the
DOM (as children of the currently selected element). -->
<!ENTITY inspectorAddNode.label "Create New Node">
<!ENTITY inspectorAddNode.accesskey "C">
<!-- LOCALIZATION NOTE (inspectorEyeDropper.label): A string displayed as the tooltip of
a button in the inspector which toggles the Eyedropper tool -->
<!ENTITY inspectorEyeDropper.label "Grab a color from the page">
<!-- LOCALIZATION NOTE (inspectorBreadcrumbsGroup): A string visible only to a
screen reader and is used to label (using aria-label attribute) a container
for inspector breadcrumbs -->
<!ENTITY inspectorBreadcrumbsGroup "Breadcrumbs">

View File

@ -313,12 +313,39 @@ inspector.sidebar.ruleViewTitle=Rules
# used in the page.
inspector.sidebar.computedViewTitle=Computed
# LOCALIZATION NOTE (inspector.sidebar.layoutViewTitle):
# This is the title shown in a tab in the side panel of the Inspector panel
# that corresponds to the tool displaying box model of the selected element.
inspector.sidebar.layoutViewTitle=Box Model
# LOCALIZATION NOTE (inspector.sidebar.animationInspectorTitle):
# This is the title shown in a tab in the side panel of the Inspector panel
# that corresponds to the tool displaying animations defined in the page.
inspector.sidebar.animationInspectorTitle=Animations
# LOCALIZATION NOTE (inspector.eyedropper.label): A string displayed as the tooltip of
# a button in the inspector which toggles the Eyedropper tool
inspector.eyedropper.label=Grab a color from the page
# LOCALIZATION NOTE (inspector.breadcrumbs.label): A string visible only to a screen reader and
# is used to label (using aria-label attribute) a container for inspector breadcrumbs
inspector.breadcrumbs.label=Breadcrumbs
# LOCALIZATION NOTE (inspector.browserStyles.label): This is the label for the checkbox
# that specifies whether the styles that are not from the user's stylesheet should be
# displayed or not.
inspector.browserStyles.label=Browser styles
# LOCALIZATION NOTE (inspector.filterStyles.placeholder): This is the placeholder that
# goes in the search box when no search term has been entered.
inspector.filterStyles.placeholder=Filter Styles
# LOCALIZATION NOTE (inspector.addRule.tooltip): This is the tooltip shown when
# hovering the `Add new rule` button in the rules view toolbar. This should
# match ruleView.contextmenu.addNewRule in styleinspector.properties
inspector.addRule.tooltip=Add new rule
# LOCALIZATION NOTE (inspector.togglePseudo.tooltip): This is the tooltip
# shown when hovering over the `Toggle Pseudo Class Panel` button in the
# rule view toolbar.
inspector.togglePseudo.tooltip=Toggle pseudo-classes
# LOCALIZATION NOTE (inspector.noProperties): In the case where there are no CSS
# properties to display e.g. due to search criteria this message is
# displayed.
inspector.noProperties=No CSS properties found.

View File

@ -1,28 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<!-- LOCALIZATION NOTE : FILE This file contains the Layout View strings.
- The Layout View is the panel accessible at the bottom of the Inspector
- sidebar. -->
<!-- LOCALIZATION NOTE : FILE The correct localization of this file might be to
- keep it in English, or another language commonly spoken among web developers.
- You want to make that choice consistent across the developer tools.
- A good criteria is the language in which you'd find the best
- documentation on web development on the web. -->
<!-- LOCALIZATION NOTE (*.tooltip): These tooltips are not regular tooltips.
- The text appears on the bottom right corner of the layout view when
- the corresponding box is hovered. -->
<!ENTITY layoutViewTitle "Box Model">
<!ENTITY margin.tooltip "margin">
<!ENTITY border.tooltip "border">
<!ENTITY padding.tooltip "padding">
<!ENTITY content.tooltip "content">
<!-- LOCALIZATION NOTE: This label is displayed as a tooltip that appears when
- hovering over the button that allows users to edit the position of an
- element in the page. -->
<!ENTITY geometry.button.tooltip "Edit position">

View File

@ -1,38 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<!-- LOCALIZATION NOTE : FILE The correct localization of this file might be to
- keep it in English, or another language commonly spoken among web developers.
- You want to make that choice consistent across the developer tools.
- A good criteria is the language in which you'd find the best
- documentation on web development on the web. -->
<!-- LOCALIZATION NOTE (browserStylesLabel): This is the label for the checkbox
- that specifies whether the styles that are not from the user's stylesheet
- should be displayed or not. -->
<!ENTITY browserStylesLabel "Browser styles">
<!-- LOCALIZATION NOTE (filterStylesPlaceholder): This is the placeholder that goes in
- the search box when no search term has been entered. -->
<!ENTITY filterStylesPlaceholder "Filter Styles">
<!-- LOCALIZATION NOTE (addRuleButtonTooltip): This is the tooltip shown when
- hovering the `Add new rule` button in the rules view toolbar. This should
- match ruleView.contextmenu.addNewRule in styleinspector.properties -->
<!ENTITY addRuleButtonTooltip "Add new rule">
<!-- LOCALIZATION NOTE (selectedElementLabel): This is the label for the path of
- the highlighted element in the web page. This path is based on the document
- tree. -->
<!ENTITY selectedElementLabel "Selected element:">
<!-- LOCALIZATION NOTE (togglePseudoClassPanel): This is the tooltip
- shown when hovering over the `Toggle Pseudo Class Panel` button in the
- rule view toolbar. -->
<!ENTITY togglePseudoClassPanel "Toggle pseudo-classes">
<!-- LOCALIZATION NOTE (noPropertiesFound): In the case where there are no CSS
- properties to display e.g. due to search criteria this message is
- displayed. -->
<!ENTITY noPropertiesFound "No CSS properties found.">

View File

@ -6,6 +6,7 @@
DIRS += [
'reps',
'splitter',
'tabs',
'tree'
]

View File

@ -0,0 +1,54 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const React = require("devtools/client/shared/vendor/react");
const ReactDOM = require("devtools/client/shared/vendor/react-dom");
const { DOM: dom, PropTypes } = React;
const Draggable = React.createClass({
displayName: "Draggable",
propTypes: {
onMove: PropTypes.func.isRequired,
onStart: PropTypes.func,
onStop: PropTypes.func,
style: PropTypes.object,
className: PropTypes.string
},
startDragging(ev) {
ev.preventDefault();
const doc = ReactDOM.findDOMNode(this).ownerDocument;
doc.addEventListener("mousemove", this.onMove);
doc.addEventListener("mouseup", this.onUp);
this.props.onStart && this.props.onStart();
},
onMove(ev) {
ev.preventDefault();
// Use screen coordinates so, moving mouse over iframes
// doesn't mangle (relative) coordinates.
this.props.onMove(ev.screenX, ev.screenY);
},
onUp(ev) {
ev.preventDefault();
const doc = ReactDOM.findDOMNode(this).ownerDocument;
doc.removeEventListener("mousemove", this.onMove);
doc.removeEventListener("mouseup", this.onUp);
this.props.onStop && this.props.onStop();
},
render() {
return dom.div({
style: this.props.style,
className: this.props.className,
onMouseDown: this.startDragging
});
}
});
module.exports = Draggable;

View File

@ -0,0 +1,11 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DevToolsModules(
'draggable.js',
'split-box.css',
'split-box.js',
)

View File

@ -0,0 +1,88 @@
/* vim:set ts=2 sw=2 sts=2 et: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
.split-box {
display: flex;
flex: 1;
min-width: 0;
height: 100%;
width: 100%;
}
.split-box.vert {
flex-direction: row;
}
.split-box.horz {
flex-direction: column;
}
.split-box > .uncontrolled {
display: flex;
flex: 1;
min-width: 0;
overflow: auto;
}
.split-box > .controlled {
display: flex;
overflow: auto;
}
.split-box > .splitter {
background-image: none;
border: 0;
border-style: solid;
border-color: transparent;
background-color: var(--theme-splitter-color);
background-clip: content-box;
position: relative;
box-sizing: border-box;
/* Positive z-index positions the splitter on top of its siblings and makes
it clickable on both sides. */
z-index: 1;
}
.split-box.vert > .splitter {
min-width: calc(var(--devtools-splitter-inline-start-width) +
var(--devtools-splitter-inline-end-width) + 1px);
border-inline-start-width: var(--devtools-splitter-inline-start-width);
border-inline-end-width: var(--devtools-splitter-inline-end-width);
margin-inline-start: calc(-1 * var(--devtools-splitter-inline-start-width) - 1px);
margin-inline-end: calc(-1 * var(--devtools-splitter-inline-end-width));
cursor: ew-resize;
}
.split-box.horz > .splitter {
min-height: calc(var(--devtools-splitter-top-width) +
var(--devtools-splitter-bottom-width) + 1px);
border-top-width: var(--devtools-splitter-top-width);
border-bottom-width: var(--devtools-splitter-bottom-width);
margin-top: calc(-1 * var(--devtools-splitter-top-width) - 1px);
margin-bottom: calc(-1 * var(--devtools-splitter-bottom-width));
cursor: ns-resize;
}
.split-box.disabled {
pointer-events: none;
}
/**
* Make sure splitter panels are not processing any mouse
* events. This is good for performance during splitter
* bar dragging.
*/
.split-box.dragging > .controlled,
.split-box.dragging > .uncontrolled {
pointer-events: none;
}

View File

@ -0,0 +1,207 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const React = require("devtools/client/shared/vendor/react");
const ReactDOM = require("devtools/client/shared/vendor/react-dom");
const Draggable = React.createFactory(require("devtools/client/shared/components/splitter/draggable"));
const { DOM: dom, PropTypes } = React;
/**
* This component represents a Splitter. The splitter supports vertical
* as well as horizontal mode.
*/
const SplitBox = React.createClass({
displayName: "SplitBox",
propTypes: {
// Custom class name. You can use more names separated by a space.
className: PropTypes.string,
// Initial size of controlled panel.
initialSize: PropTypes.number,
// Left/top panel
startPanel: PropTypes.any,
// Min panel size.
minSize: PropTypes.number,
// Max panel size.
maxSize: PropTypes.number,
// 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.number,
// True if the splitter bar is vertical (default is vertical).
vert: PropTypes.bool
},
getDefaultProps() {
return {
splitterSize: 5,
vert: true,
endPanelControl: false
};
},
/**
* The state stores the current orientation (vertical or horizontal)
* and the current size (width/height). All these values can change
* during the component's life time.
*/
getInitialState() {
return {
vert: this.props.vert,
width: this.props.initialWidth || this.props.initialSize,
height: this.props.initialHeight || this.props.initialSize
};
},
// Dragging Events
/**
* Set 'resizing' cursor on entire document during splitter dragging.
* This avoids cursor-flickering that happens when the mouse leaves
* the splitter bar area (happens frequently).
*/
onStartMove() {
const splitBox = ReactDOM.findDOMNode(this);
const doc = splitBox.ownerDocument;
let defaultCursor = doc.documentElement.style.cursor;
doc.documentElement.style.cursor = (this.state.vert ? "ew-resize" : "ns-resize");
splitBox.classList.add("dragging");
this.setState({
defaultCursor: defaultCursor
});
},
onStopMove() {
const splitBox = ReactDOM.findDOMNode(this);
const doc = splitBox.ownerDocument;
doc.documentElement.style.cursor = this.state.defaultCursor;
splitBox.classList.remove("dragging");
},
/**
* Adjust size of the controlled panel. Depending on the current
* orientation we either remember the width or height of
* the splitter box.
*/
onMove(x, y) {
const node = ReactDOM.findDOMNode(this);
const doc = node.ownerDocument;
const win = doc.defaultView;
let size;
let { endPanelControl } = this.props;
if (this.state.vert) {
// Switch the control flag in case of RTL. Note that RTL
// has impact on vertical splitter only.
let dir = win.getComputedStyle(doc.documentElement).direction;
if (dir == "rtl") {
endPanelControl = !endPanelControl;
}
let innerOffset = x - win.mozInnerScreenX;
size = endPanelControl ?
(node.offsetLeft + node.offsetWidth) - innerOffset :
innerOffset - node.offsetLeft;
this.setState({
width: size
});
} else {
let innerOffset = y - win.mozInnerScreenY;
size = endPanelControl ?
(node.offsetTop + node.offsetHeight) - innerOffset :
innerOffset - node.offsetTop;
this.setState({
height: size
});
}
},
// Rendering
render() {
const vert = this.state.vert;
const { startPanel, endPanel, endPanelControl, minSize,
maxSize, splitterSize } = this.props;
let style = Object.assign({}, this.props.style);
// Calculate class names list.
let classNames = ["split-box"];
classNames.push(vert ? "vert" : "horz");
if (this.props.className) {
classNames = classNames.concat(this.props.className.split(" "));
}
let leftPanelStyle;
let rightPanelStyle;
// Set proper size for panels depending on the current state.
if (vert) {
leftPanelStyle = {
maxWidth: endPanelControl ? null : maxSize,
minWidth: endPanelControl ? null : minSize,
width: endPanelControl ? null : this.state.width
};
rightPanelStyle = {
maxWidth: endPanelControl ? maxSize : null,
minWidth: endPanelControl ? minSize : null,
width: endPanelControl ? this.state.width : null
};
} else {
leftPanelStyle = {
maxHeight: endPanelControl ? null : maxSize,
minHeight: endPanelControl ? null : minSize,
height: endPanelControl ? null : this.state.height
};
rightPanelStyle = {
maxHeight: endPanelControl ? maxSize : null,
minHeight: endPanelControl ? minSize : null,
height: endPanelControl ? this.state.height : null
};
}
// Calculate splitter size
let splitterStyle = {
flex: "0 0 " + splitterSize + "px"
};
return (
dom.div({
className: classNames.join(" "),
style: style },
startPanel ?
dom.div({
className: endPanelControl ? "uncontrolled" : "controlled",
style: leftPanelStyle},
startPanel
) : null,
Draggable({
className: "splitter",
style: splitterStyle,
onStart: this.onStartMove,
onStop: this.onStopMove,
onMove: this.onMove
}),
endPanel ?
dom.div({
className: endPanelControl ? "controlled" : "uncontrolled",
style: rightPanelStyle},
endPanel
) : null
)
);
}
});
module.exports = SplitBox;

View File

@ -57,7 +57,11 @@ window.onload = Task.async(function* () {
document.body.appendChild(panel);
return setState(tabbarReact, Object.assign({}, tabbarReact.state, {
tabs: tabbarReact.state.tabs.concat({id: `${tabId}`, title: `tab-${tabId}`, panel: InspectorTabPanel}),
tabs: tabbarReact.state.tabs.concat({
id: `sidebar-panel-${tabId}`,
title: `tab-${tabId}`,
panel: InspectorTabPanel
}),
}));
}
} catch(e) {

View File

@ -37,10 +37,6 @@ Object.defineProperty(this, "EVENTS", {
writable: false
});
// Maximum number of character visible in any cell in the table. This is to
// avoid making the cell take up all the space in a row.
const MAX_VISIBLE_STRING_SIZE = 100;
/**
* A table widget with various features like resizble/toggleable columns,
* sorting, keyboard navigation etc.
@ -1510,11 +1506,6 @@ Cell.prototype = {
value = span;
}
if (!(value instanceof Node) &&
value.length > MAX_VISIBLE_STRING_SIZE) {
value = value .substr(0, MAX_VISIBLE_STRING_SIZE) + "\u2026";
}
if (value instanceof Node) {
this.label.removeAttribute("value");

View File

@ -28,6 +28,7 @@
#computedview-toolbar {
display: flex;
align-items: center;
}
#browser-style-checkbox {
@ -36,15 +37,11 @@
an extra space after. */
margin-inline-start: 5px;
margin-inline-end: 5px;
}
#browser-style-checkbox-label {
margin-right: 5px;
/* Vertically center the 'Browser styles' checkbox in the
Computed panel with its label. */
display: flex;
align-items: center;
}
#propertyContainer {

View File

@ -11,9 +11,47 @@
--eyedropper-image: url(images/firebug/command-eyedropper.svg);
}
/* Make sure to hide scroll bars for the parent window */
window {
overflow: hidden;
}
/* The main Inspector panel container. */
.inspector-responsive-container {
height: 100vh;
}
/* The main panel layout. This area consists of a toolbar, markup view
and breadcrumbs bar. */
.devtools-main-content {
/* Subtract 1 pixel from the panel height. It's puzzling why this
is needed, but if not presented the entire Inspector panel
content jumps 1 pixel up when the Toolbox is opened. */
height: calc(100% - 1px);
display: flex;
flex-direction: column;
flex: 1 1 auto;
}
/* Inspector Panel Splitter */
#inspector-splitter-box {
height: 100vh;
width: 100vw;
}
/* Minimum width for the Inspector main (uncontrolled) area. */
#inspector-splitter-box .uncontrolled {
min-width: 275px;
}
#inspector-splitter-box .controlled.pane-collapsed {
visibility: collapse;
}
/* Use flex layout for the Inspector toolbar. For now, it's done
specifically for the Inspector toolbar since general rule applied
on .devtools-toolbar breaks breadcrubs and also toolbars in other
on .devtools-toolbar breaks breadcrumbs and also toolbars in other
panels (e.g. webconsole, debugger), these are not ready for HTML
layout yet. */
#inspector-toolbar.devtools-toolbar {
@ -124,8 +162,9 @@
is fixed and the all-tabs-menu is available again. */
#inspector-sidebar-container {
overflow: hidden;
min-width: 300px;
min-width: 50px;
position: relative;
height: 100%;
}
#inspector-sidebar {
@ -149,3 +188,20 @@
text-align: center;
margin: 5px;
}
/* Markup Box */
iframe {
border: 0;
}
#markup-box {
width: 100%;
flex: 1 1 auto;
}
#markup-box > iframe {
height: 100%;
width: 100%;
}

View File

@ -378,6 +378,7 @@ JSTerm.prototype = {
if (this.hud.NEW_CONSOLE_OUTPUT_ENABLED) {
this.hud.newConsoleOutput.dispatchMessageAdd(response);
// @TODO figure out what to do about the callback.
callback && callback();
return;
}
let msg = new Messages.JavaScriptEvalOutput(response,
@ -423,12 +424,17 @@ JSTerm.prototype = {
*/
execute: function (executeString, callback) {
let deferred = promise.defer();
let resultCallback = function (msg) {
deferred.resolve(msg);
if (callback) {
callback(msg);
}
};
let resultCallback;
if (this.hud.NEW_CONSOLE_OUTPUT_ENABLED) {
resultCallback = () => deferred.resolve();
} else {
resultCallback = (msg) => {
deferred.resolve(msg);
if (callback) {
callback(msg);
}
};
}
// attempt to execute the content of the inputNode
executeString = executeString || this.getInputValue();

View File

@ -12,13 +12,12 @@ const {
DOM: dom,
PropTypes
} = require("devtools/client/shared/vendor/react");
const { ConsoleCommand: ConsoleCommandType } = require("devtools/client/webconsole/new-console-output/types");
const MessageIcon = createFactory(require("devtools/client/webconsole/new-console-output/components/message-icon").MessageIcon);
ConsoleCommand.displayName = "ConsoleCommand";
ConsoleCommand.propTypes = {
message: PropTypes.instanceOf(ConsoleCommandType).isRequired,
message: PropTypes.object.isRequired,
};
/**

View File

@ -26,6 +26,14 @@ function EvaluationResult(props) {
const {source, type, level} = message;
const icon = MessageIcon({level});
let messageBody;
if (message.messageText) {
messageBody = message.messageText;
} else {
messageBody = GripMessageBody({grip: message.parameters});
}
const classes = ["message", "cm-s-mozilla"];
classes.push(source);
@ -41,7 +49,7 @@ function EvaluationResult(props) {
dom.span({ className: "message-body-wrapper" },
dom.span({ className: "message-flex-body" },
dom.span({ className: "message-body devtools-monospace" },
GripMessageBody({grip: message.parameters})
messageBody
)
)
)

View File

@ -16,7 +16,7 @@ const {openVariablesView} = require("devtools/client/webconsole/new-console-outp
VariablesViewLink.displayName = "VariablesViewLink";
VariablesViewLink.propTypes = {
object: PropTypes.object.required
object: PropTypes.object.isRequired
};
function VariablesViewLink(props) {

View File

@ -2,29 +2,36 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
const { EvaluationResult } = require("devtools/client/webconsole/new-console-output/components/message-types/evaluation-result");
// Test utils.
const expect = require("expect");
const { render } = require("enzyme");
const {
renderComponent
} = require("devtools/client/webconsole/new-console-output/test/helpers");
// React
const { createFactory } = require("devtools/client/shared/vendor/react");
// Components under test.
const EvaluationResult = createFactory(require("devtools/client/webconsole/new-console-output/components/message-types/evaluation-result").EvaluationResult);
// Test fakes.
const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
describe("EvaluationResult component:", () => {
it("renders a grip result", () => {
const message = stubPreparedMessages.get("new Date(0)");
const props = {
message
};
const rendered = renderComponent(EvaluationResult, props);
const wrapper = render(EvaluationResult({ message }));
const messageBody = getMessageBody(rendered);
expect(messageBody.textContent).toBe("Date 1970-01-01T00:00:00.000Z");
expect(wrapper.find(".message-body").text()).toBe("Date 1970-01-01T00:00:00.000Z");
expect(wrapper.find(".message.log").length).toBe(1);
});
it("renders an error", () => {
const message = stubPreparedMessages.get("asdf()");
const wrapper = render(EvaluationResult({ message }));
expect(wrapper.find(".message-body").text())
.toBe("ReferenceError: asdf is not defined");
expect(wrapper.find(".message.error").length).toBe(1);
});
});
function getMessageBody(rendered) {
const queryPath = "div.message span.message-body-wrapper span.message-body";
return rendered.querySelector(queryPath);
}

View File

@ -49,7 +49,8 @@ console.timeEnd("bar");
// Evaluation Result
const evaluationResultCommands = [
"new Date(0)"
"new Date(0)",
"asdf()"
];
let evaluationResult = new Map(evaluationResultCommands.map(cmd => [cmd, cmd]));

View File

@ -19,7 +19,6 @@ stubPreparedMessages.set("new Date(0)", new ConsoleMessage({
"source": "javascript",
"type": "result",
"level": "log",
"messageText": null,
"parameters": {
"type": "object",
"actor": "server1.conn0.child1/obj30",
@ -33,7 +32,23 @@ stubPreparedMessages.set("new Date(0)", new ConsoleMessage({
}
},
"repeat": 1,
"repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"javascript\",\"type\":\"result\",\"level\":\"log\",\"messageText\":null,\"parameters\":{\"type\":\"object\",\"actor\":\"server1.conn0.child1/obj30\",\"class\":\"Date\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":0,\"preview\":{\"timestamp\":0}},\"repeatId\":null,\"stacktrace\":null,\"frame\":null}",
"repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"javascript\",\"type\":\"result\",\"level\":\"log\",\"parameters\":{\"type\":\"object\",\"actor\":\"server1.conn0.child1/obj30\",\"class\":\"Date\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":0,\"preview\":{\"timestamp\":0}},\"repeatId\":null,\"stacktrace\":null,\"frame\":null}",
"stacktrace": null,
"frame": null
}));
stubPreparedMessages.set("asdf()", new ConsoleMessage({
"id": "1",
"allowRepeating": true,
"source": "javascript",
"type": "result",
"level": "error",
"messageText": "ReferenceError: asdf is not defined",
"parameters": {
"type": "undefined"
},
"repeat": 1,
"repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"javascript\",\"type\":\"result\",\"level\":\"error\",\"messageText\":\"ReferenceError: asdf is not defined\",\"parameters\":{\"type\":\"undefined\"},\"repeatId\":null,\"stacktrace\":null,\"frame\":null}",
"stacktrace": null,
"frame": null
}));
@ -54,11 +69,41 @@ stubPackets.set("new Date(0)", {
"timestamp": 0
}
},
"timestamp": 1471886229652,
"timestamp": 1474405330863,
"exception": null,
"helperResult": null
});
stubPackets.set("asdf()", {
"from": "server1.conn0.child1/consoleActor2",
"input": "asdf()",
"result": {
"type": "undefined"
},
"timestamp": 1474405330881,
"exception": {
"type": "object",
"actor": "server1.conn0.child1/obj32",
"class": "Error",
"extensible": true,
"frozen": false,
"sealed": false,
"ownPropertyLength": 4,
"preview": {
"kind": "Error",
"name": "ReferenceError",
"message": "asdf is not defined",
"stack": "@debugger eval code:1:1\n",
"fileName": "debugger eval code",
"lineNumber": 1,
"columnNumber": 1
}
},
"exceptionMessage": "ReferenceError: asdf is not defined",
"exceptionDocURL": "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_defined?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default",
"helperResult": null
});
module.exports = {
stubPreparedMessages,

View File

@ -7,3 +7,7 @@ support-files =
test-console.html
[browser_webconsole_init.js]
[browser_webconsole_input_focus.js]
[browser_webconsole_observer_notifications.js]
[browser_webconsole_vview_close_on_esc_key.js]

View File

@ -33,36 +33,3 @@ add_task(function* () {
yield receievedMessages;
});
/**
* Wait for messages in the web console output, resolving once they are receieved.
*
* @param object options
* - hud: the webconsole
* - messages: Array[Object]. An array of messages to match. Current supported options:
* - text: Exact text match in .message-body
*/
function waitForMessages({ hud, messages }) {
return new Promise(resolve => {
let numMatched = 0;
let receivedLog = hud.ui.on("new-messages", function messagesReceieved(e, newMessage) {
for (let message of messages) {
if (message.matched) {
continue;
}
if (newMessage.node.querySelector(".message-body").textContent == message.text) {
numMatched++;
message.matched = true;
info("Matched a message with text: " + message.text + ", still waiting for " + (messages.length - numMatched) + " messages");
}
if (numMatched === messages.length) {
hud.ui.off("new-messages", messagesReceieved);
resolve();
return;
}
}
});
});
}

View File

@ -0,0 +1,18 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests that the input field is focused when the console is opened.
"use strict";
const TEST_URI = "data:text/html;charset=utf-8,Test input focus";
add_task(function* () {
let hud = yield openNewTabAndConsole(TEST_URI);
hud.jsterm.clearOutput();
let inputNode = hud.jsterm.inputNode;
ok(inputNode.getAttribute("focused"), "input node is focused");
});

View File

@ -0,0 +1,47 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const TEST_URI = "data:text/html;charset=utf-8,<p>Web Console test for " +
"obeserver notifications";
let created = false;
let destroyed = false;
add_task(function* () {
setupObserver();
yield openNewTabAndConsole(TEST_URI);
yield waitFor(() => created);
yield closeTabAndToolbox(gBrowser.selectedTab);
yield waitFor(() => destroyed);
});
function setupObserver() {
const observer = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
observe: function observe(subject, topic) {
subject = subject.QueryInterface(Ci.nsISupportsString);
switch (topic) {
case "web-console-created":
ok(HUDService.getHudReferenceById(subject.data), "We have a hud reference");
Services.obs.removeObserver(observer, "web-console-created");
created = true;
break;
case "web-console-destroyed":
ok(!HUDService.getHudReferenceById(subject.data), "We do not have a hud reference");
Services.obs.removeObserver(observer, "web-console-destroyed");
destroyed = true;
break;
}
},
};
Services.obs.addObserver(observer, "web-console-created", false);
Services.obs.addObserver(observer, "web-console-destroyed", false);
}

View File

@ -0,0 +1,46 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// Check that the variables view sidebar can be closed by pressing Escape in the
// web console.
"use strict";
const TEST_URI =
"data:text/html;charset=utf8,<script>let fooObj = {testProp: 'testValue'}</script>";
add_task(function* () {
let hud = yield openNewTabAndConsole(TEST_URI);
let jsterm = hud.jsterm;
let vview;
yield openSidebar("fooObj", 'testProp: "testValue"');
vview.window.focus();
let sidebarClosed = jsterm.once("sidebar-closed");
EventUtils.synthesizeKey("VK_ESCAPE", {});
yield sidebarClosed;
function* openSidebar(objName, expectedText) {
yield jsterm.execute(objName);
info("JSTerm executed");
let msg = yield waitFor(() => findMessage(hud, "Object"));
ok(msg, "Message found");
let anchor = msg.querySelector("a");
let body = msg.querySelector(".message-body");
ok(anchor, "object anchor");
ok(body, "message body");
ok(body.textContent.includes(expectedText), "message text check");
msg.scrollIntoView();
yield EventUtils.synthesizeMouse(anchor, 2, 2, {}, hud.iframeWindow);
let vviewVar = yield jsterm.once("variablesview-fetched");
vview = vviewVar._variablesView;
ok(vview, "variables view object exists");
}
});

View File

@ -13,6 +13,102 @@ Services.scriptloader.loadSubScript(
this);
Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", true);
registerCleanupFunction(() => {
registerCleanupFunction(function* () {
Services.prefs.clearUserPref("devtools.webconsole.new-frontend-enabled");
let browserConsole = HUDService.getBrowserConsole();
if (browserConsole) {
if (browserConsole.jsterm) {
browserConsole.jsterm.clearOutput(true);
}
yield HUDService.toggleBrowserConsole();
}
});
/**
* Add a new tab and open the toolbox in it, and select the webconsole.
*
* @param string url
* The URL for the tab to be opened.
* @return Promise
* Resolves when the tab has been added, loaded and the toolbox has been opened.
* Resolves to the toolbox.
*/
var openNewTabAndConsole = Task.async(function* (url) {
let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole");
let hud = toolbox.getCurrentPanel().hud;
hud.jsterm._lazyVariablesView = false;
return hud;
});
/**
* Wait for messages in the web console output, resolving once they are receieved.
*
* @param object options
* - hud: the webconsole
* - messages: Array[Object]. An array of messages to match. Current supported options:
* - text: Exact text match in .message-body
*/
function waitForMessages({ hud, messages }) {
return new Promise(resolve => {
let numMatched = 0;
let receivedLog = hud.ui.on("new-messages", function messagesReceieved(e, newMessage) {
for (let message of messages) {
if (message.matched) {
continue;
}
if (newMessage.node.querySelector(".message-body").textContent == message.text) {
numMatched++;
message.matched = true;
info("Matched a message with text: " + message.text + ", still waiting for " + (messages.length - numMatched) + " messages");
}
if (numMatched === messages.length) {
hud.ui.off("new-messages", messagesReceieved);
resolve(receivedLog);
return;
}
}
});
});
}
/**
* Wait for a predicate to return a result.
*
* @param function condition
* Invoked once in a while until it returns a truthy value. This should be an
* idempotent function, since we have to run it a second time after it returns
* true in order to return the value.
* @param string message [optional]
* A message to output if the condition failes.
* @param number interval [optional]
* How often the predicate is invoked, in milliseconds.
* @return object
* A promise that is resolved with the result of the condition.
*/
function* waitFor(condition, message = "waitFor", interval = 100, maxTries = 50) {
return new Promise(resolve => {
BrowserTestUtils.waitForCondition(condition, message, interval, maxTries)
.then(resolve(condition()));
});
}
/**
* Find a message in the output.
*
* @param object hud
* The web console.
* @param string text
* A substring that can be found in the message.
* @param selector [optional]
* The selector to use in finding the message.
*/
function findMessage(hud, text, selector = ".message") {
const elements = Array.prototype.filter.call(
hud.ui.experimentalOutputNode.querySelectorAll(selector),
(el) => el.textContent.includes(text)
);
return elements.pop();
}

View File

@ -83,11 +83,11 @@ function transformPacket(packet) {
break;
}
const frame = {
source: message.filename || null,
line: message.lineNumber || null,
column: message.columnNumber || null
};
const frame = message.filename ? {
source: message.filename,
line: message.lineNumber,
column: message.columnNumber,
} : null;
return new ConsoleMessage({
source: MESSAGE_SOURCE.CONSOLE_API,
@ -119,11 +119,11 @@ function transformPacket(packet) {
level = MESSAGE_LEVEL.INFO;
}
const frame = {
const frame = pageError.sourceName ? {
source: pageError.sourceName,
line: pageError.lineNumber,
column: pageError.columnNumber
};
} : null;
return new ConsoleMessage({
source: MESSAGE_SOURCE.JAVASCRIPT,
@ -148,13 +148,18 @@ function transformPacket(packet) {
case "evaluationResult":
default: {
let { result } = packet;
let {
exceptionMessage: messageText,
result: parameters
} = packet;
const level = messageText ? MESSAGE_LEVEL.ERROR : MESSAGE_LEVEL.LOG;
return new ConsoleMessage({
source: MESSAGE_SOURCE.JAVASCRIPT,
type: MESSAGE_TYPE.RESULT,
level: MESSAGE_LEVEL.LOG,
parameters: result,
level,
messageText,
parameters,
});
}
}

View File

@ -12,6 +12,9 @@
*
* Once JSTerm is also written in React/Redux, these will be actions.
*/
exports.openVariablesView = (object) => {
window.jsterm.openVariablesView({objectActor: object});
exports.openVariablesView = (objectActor) => {
window.jsterm.openVariablesView({
objectActor,
autofocus: true,
});
};

View File

@ -582,6 +582,7 @@ WebConsoleFrame.prototype = {
// XXX: We should actually stop output from happening on old output
// panel, but for now let's just hide it.
this.experimentalOutputNode = this.outputNode.cloneNode();
this.experimentalOutputNode.removeAttribute("tabindex");
this.outputNode.hidden = true;
this.outputNode.parentNode.appendChild(this.experimentalOutputNode);
// @TODO Once the toolbox has been converted to React, see if passing

View File

@ -406,7 +406,10 @@ TabSources.prototype = {
* @return Promise of a SourceMapConsumer
*/
fetchSourceMap: function (aSource) {
if (this._sourceMaps.has(aSource)) {
if (!this._useSourceMaps) {
return resolve(null);
}
else if (this._sourceMaps.has(aSource)) {
return this._sourceMaps.get(aSource);
}
else if (!aSource || !aSource.sourceMapURL) {
@ -457,10 +460,10 @@ TabSources.prototype = {
* them from aScriptURL.
*/
_fetchSourceMap: function (aAbsSourceMapURL, aSourceURL) {
if (!this._useSourceMaps) {
return resolve(null);
}
else if (this._sourceMapCache[aAbsSourceMapURL]) {
assert(this._useSourceMaps,
"Cannot fetch sourcemaps if they are disabled");
if (this._sourceMapCache[aAbsSourceMapURL]) {
return this._sourceMapCache[aAbsSourceMapURL];
}

View File

@ -0,0 +1,58 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Check getSources functionality with sourcemaps.
*/
const {SourceNode} = require("source-map");
function run_test() {
run_test_with_server(DebuggerServer, function () {
// Bug 1304144 - This test does not run in a worker because the
// `rpc` method which talks to the main thread does not work.
// run_test_with_server(WorkerDebuggerServer, do_test_finished);
do_test_finished();
});
do_test_pending();
}
function run_test_with_server(server, cb) {
Task.spawn(function*() {
initTestDebuggerServer(server);
const debuggee = addTestGlobal("test-sources", server);
const client = new DebuggerClient(server.connectPipe());
yield client.connect();
const [,,threadClient] = yield attachTestTabAndResume(client, "test-sources");
yield threadClient.reconfigure({ useSourceMaps: true });
addSources(debuggee);
threadClient.getSources(Task.async(function* (res) {
do_check_true(res.sources.length === 3, "3 sources exist");
yield threadClient.reconfigure({ useSourceMaps: false });
threadClient.getSources(function(res) {
do_check_true(res.sources.length === 1, "1 source exist");
client.close().then(cb);
});
}));
});
}
function addSources(debuggee) {
let { code, map } = (new SourceNode(null, null, null, [
new SourceNode(1, 0, "a.js", "function a() { return 'a'; }\n"),
new SourceNode(1, 0, "b.js", "function b() { return 'b'; }\n"),
new SourceNode(1, 0, "c.js", "function c() { return 'c'; }\n"),
])).toStringWithSourceMap({
file: "abc.js",
sourceRoot: "http://example.com/www/js/"
});
code += "//# sourceMappingURL=data:text/json;base64," + btoa(map.toString());
Components.utils.evalInSandbox(code, debuggee, "1.8",
"http://example.com/www/js/abc.js", 1);
}

View File

@ -165,6 +165,7 @@ skip-if = toolkit != "gonk"
[test_listsources-01.js]
[test_listsources-02.js]
[test_listsources-03.js]
[test_listsources-04.js]
[test_new_source-01.js]
[test_sourcemaps-01.js]
[test_sourcemaps-02.js]

View File

@ -53,12 +53,6 @@ rule.pseudoElement=Pseudo-elements
# pseudo element header
rule.selectedElement=This Element
# LOCALIZATION NOTE (helpLinkTitle): For each style property
# the user can hover it and get a help link button which allows one to
# quickly jump to the documentation from the Mozilla Developer Network site.
# This is the link title shown in the hover tooltip.
helpLinkTitle=Read the documentation for this property
# LOCALIZATION NOTE (rule.warning.title): When an invalid property value is
# entered into the rule view a warning icon is displayed. This text is used for
# the title attribute of the warning icon.
@ -142,7 +136,7 @@ styleinspector.contextmenu.showMdnDocs.accessKey=D
# LOCALIZATION NOTE (styleinspector.contextmenu.addNewRule): Text displayed in the
# rule view context menu for adding a new rule to the element.
# This should match addRuleButton.tooltip in styleinspector.dtd
# This should match inspector.addRule.tooltip in inspector.properties
styleinspector.contextmenu.addNewRule=Add New Rule
# LOCALIZATION NOTE (styleinspector.contextmenu.addRule.accessKey): Access key for

View File

@ -707,9 +707,8 @@ static bool
HasCameraPermission(const nsCString& aOrigin)
{
// Name used with nsIPermissionManager
static const char* cameraPermission = "camera";
static const char* cameraPermission = "MediaManagerVideo";
bool allowed = false;
bool permanent = false;
nsresult rv;
nsCOMPtr<nsIPermissionManager> mgr =
do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv);
@ -728,19 +727,9 @@ HasCameraPermission(const nsCString& aOrigin)
&video);
if (NS_SUCCEEDED(rv)) {
allowed = (video == nsIPermissionManager::ALLOW_ACTION);
// Was allowed, now see if this is a persistent permission
// or a session one.
if (allowed) {
rv = mgr->TestExactPermanentPermission(principal,
cameraPermission,
&video);
if (NS_SUCCEEDED(rv)) {
permanent = (video == nsIPermissionManager::ALLOW_ACTION);
}
}
}
// Session permissions are removed after one use.
if (allowed && !permanent) {
if (allowed) {
mgr->RemoveFromPrincipal(principal, cameraPermission);
}
}

View File

@ -5,6 +5,7 @@
package org.mozilla.gecko.media;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.Telemetry;
import android.content.ComponentName;
import android.content.Context;
@ -139,9 +140,14 @@ public final class RemoteManager implements IBinder.DeathRecipient {
}
}
private static final String MEDIA_DECODING_PROCESS_CRASH = "MEDIA_DECODING_PROCESS_CRASH";
private void reportDecodingProcessCrash() {
Telemetry.addToHistogram(MEDIA_DECODING_PROCESS_CRASH, 1);
}
@Override
public void binderDied() {
Log.e(LOGTAG, "remote codec is dead");
reportDecodingProcessCrash();
handleRemoteDeath();
}

View File

@ -32,7 +32,7 @@ function PageAction(options, extension) {
this.options = {
title: options.default_title || extension.name,
id: extension.id,
id: `{${extension.uuid}}`,
clickCallback: () => {
if (this.popupUrl) {
let win = Services.wm.getMostRecentWindow("navigator:browser");

View File

@ -6,10 +6,10 @@ const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/PageActions.jsm");
function isPageActionShown(extensionId) {
return PageActions.isShown(extensionId);
function isPageActionShown(uuid) {
return PageActions.isShown(uuid);
}
function clickPageAction(extensionId) {
PageActions.synthesizeClick(extensionId);
function clickPageAction(uuid) {
PageActions.synthesizeClick(uuid);
}

View File

@ -41,10 +41,15 @@ function background() {
browser.test.sendMessage("page-action-clicked");
});
browser.test.sendMessage("ready");
let extensionInfo = {
// Extract the assigned uuid from the background page url.
uuid: `{${window.location.hostname}}`,
};
browser.test.sendMessage("ready", extensionInfo);
}
add_task(function* test_contentscript() {
add_task(function* test_pageAction() {
let extension = ExtensionTestUtils.loadExtension({
background,
manifest: {
@ -55,6 +60,11 @@ add_task(function* test_contentscript() {
"18": "extension.png",
},
},
"applications": {
"gecko": {
"id": "foo@bar.com",
},
},
},
files: {
"extension.png": IMAGE_ARRAYBUFFER,
@ -62,26 +72,26 @@ add_task(function* test_contentscript() {
});
yield extension.startup();
yield extension.awaitMessage("ready");
let {uuid} = yield extension.awaitMessage("ready");
extension.sendMessage("pageAction-show");
yield extension.awaitMessage("page-action-shown");
ok(isPageActionShown(extension.id), "The PageAction should be shown");
ok(isPageActionShown(uuid), "The PageAction should be shown");
extension.sendMessage("pageAction-hide");
yield extension.awaitMessage("page-action-hidden");
ok(!isPageActionShown(extension.id), "The PageAction should be hidden");
ok(!isPageActionShown(uuid), "The PageAction should be hidden");
extension.sendMessage("pageAction-show");
yield extension.awaitMessage("page-action-shown");
ok(isPageActionShown(extension.id), "The PageAction should be shown");
ok(isPageActionShown(uuid), "The PageAction should be shown");
clickPageAction(extension.id);
clickPageAction(uuid);
yield extension.awaitMessage("page-action-clicked");
ok(isPageActionShown(extension.id), "The PageAction should still be shown after being clicked");
ok(isPageActionShown(uuid), "The PageAction should still be shown after being clicked");
yield extension.unload();
ok(!isPageActionShown(extension.id), "The PageAction should be removed after unload");
ok(!isPageActionShown(uuid), "The PageAction should be removed after unload");
});
</script>

View File

@ -54,7 +54,12 @@ add_task(function* test_contentscript() {
browser.test.sendMessage("page-action-onClicked-fired");
});
browser.test.sendMessage("ready");
let extensionInfo = {
// Extract the assigned uuid from the background page url.
uuid: `{${window.location.hostname}}`,
};
browser.test.sendMessage("ready", extensionInfo);
}
function popupScript() {
@ -107,7 +112,7 @@ add_task(function* test_contentscript() {
});
};
function* testPopup(name) {
function* testPopup(name, uuid) {
// We don't need to set the popup when testing default_popup.
if (name != "default.html") {
extension.sendMessage("page-action-set-popup", {name});
@ -124,7 +129,7 @@ add_task(function* test_contentscript() {
extension.sendMessage("page-action-enable-onClicked-listener");
yield extension.awaitMessage("page-action-onClicked-listener-enabled");
clickPageAction(extension.id);
clickPageAction(uuid);
yield extension.awaitMessage("page-action-onClicked-fired");
extension.sendMessage("page-action-disable-onClicked-listener");
@ -132,7 +137,7 @@ add_task(function* test_contentscript() {
} else {
ok(url.includes(name), "Calling pageAction.getPopup should return the correct popup URL when the popup is set.");
clickPageAction(extension.id);
clickPageAction(uuid);
let location = yield extension.awaitMessage("page-action-from-popup");
ok(location.includes(name), "The popup with the correct URL should be shown.");
@ -144,19 +149,19 @@ add_task(function* test_contentscript() {
}
yield extension.startup();
yield extension.awaitMessage("ready");
let {uuid} = yield extension.awaitMessage("ready");
extension.sendMessage("page-action-show");
yield extension.awaitMessage("page-action-shown");
ok(isPageActionShown(extension.id), "The PageAction should be shown.");
ok(isPageActionShown(uuid), "The PageAction should be shown.");
yield testPopup("default.html");
yield testPopup("a.html");
yield testPopup("");
yield testPopup("b.html");
yield testPopup("default.html", uuid);
yield testPopup("a.html", uuid);
yield testPopup("", uuid);
yield testPopup("b.html", uuid);
yield extension.unload();
ok(!isPageActionShown(extension.id), "The PageAction should be removed after unload.");
ok(!isPageActionShown(uuid), "The PageAction should be removed after unload.");
});
</script>

View File

@ -6330,7 +6330,14 @@
"kind": "boolean",
"description": "Reports whether a decoder for an HTTP Live Streaming media type was created when requested.",
"bug_numbers": [1262659]
},
"MEDIA_DECODING_PROCESS_CRASH": {
"alert_emails": ["bwu@mozilla.com", "jolin@mozilla.com", "jacheng@mozilla.com"],
"expires_in_version": "57",
"kind": "count",
"bug_numbers": [1297556, 1257777],
"description": "Records a value each time Fennec remote decoding process crashes unexpected while decoding media content.",
"releaseChannelCollection": "opt-out"
},
"VIDEO_MFT_OUTPUT_NULL_SAMPLES": {
"alert_emails": ["cpearce@mozilla.com"],

View File

@ -17,6 +17,7 @@ struct ScalarInfo {
uint32_t name_offset;
uint32_t expiration_offset;
uint32_t dataset;
bool keyed;
const char *name() const;
const char *expiration() const;

View File

@ -71,6 +71,58 @@ telemetry.test:
- telemetry-client-dev@mozilla.com
release_channel_collection: opt-out
keyed_release_optin:
bug_numbers:
- 1277806
description: A testing scalar; not meant to be touched.
expires: never
kind: uint
keyed: true
notification_emails:
- telemetry-client-dev@mozilla.com
release_channel_collection: opt-in
keyed_release_optout:
bug_numbers:
- 1277806
description: A testing scalar; not meant to be touched.
expires: never
kind: uint
keyed: true
notification_emails:
- telemetry-client-dev@mozilla.com
release_channel_collection: opt-out
keyed_expired:
bug_numbers:
- 1277806
description: This is an expired testing scalar; not meant to be touched.
expires: 4.0a1
kind: uint
keyed: true
notification_emails:
- telemetry-client-dev@mozilla.com
keyed_unsigned_int:
bug_numbers:
- 1277806
description: A testing keyed uint scalar; not meant to be touched.
expires: never
kind: uint
keyed: true
notification_emails:
- telemetry-client-dev@mozilla.com
keyed_boolean_kind:
bug_numbers:
- 1277806
description: A testing keyed boolean scalar; not meant to be touched.
expires: never
kind: boolean
keyed: true
notification_emails:
- telemetry-client-dev@mozilla.com
# The following section contains the browser engagement scalars.
browser.engagement:
max_concurrent_tab_count:

View File

@ -2341,6 +2341,35 @@ TelemetryImpl::SnapshotScalars(unsigned int aDataset, bool aClearScalars, JSCont
return TelemetryScalar::CreateSnapshots(aDataset, aClearScalars, aCx, optional_argc, aResult);
}
NS_IMETHODIMP
TelemetryImpl::KeyedScalarAdd(const nsACString& aName, const nsAString& aKey,
JS::HandleValue aVal, JSContext* aCx)
{
return TelemetryScalar::Add(aName, aKey, aVal, aCx);
}
NS_IMETHODIMP
TelemetryImpl::KeyedScalarSet(const nsACString& aName, const nsAString& aKey,
JS::HandleValue aVal, JSContext* aCx)
{
return TelemetryScalar::Set(aName, aKey, aVal, aCx);
}
NS_IMETHODIMP
TelemetryImpl::KeyedScalarSetMaximum(const nsACString& aName, const nsAString& aKey,
JS::HandleValue aVal, JSContext* aCx)
{
return TelemetryScalar::SetMaximum(aName, aKey, aVal, aCx);
}
NS_IMETHODIMP
TelemetryImpl::SnapshotKeyedScalars(unsigned int aDataset, bool aClearScalars, JSContext* aCx,
uint8_t optional_argc, JS::MutableHandleValue aResult)
{
return TelemetryScalar::CreateKeyedSnapshots(aDataset, aClearScalars, aCx, optional_argc,
aResult);
}
NS_IMETHODIMP
TelemetryImpl::ClearScalars()
{
@ -2967,53 +2996,59 @@ void DestroyStatisticsRecorder()
// Scalar API C++ Endpoints
/**
* Adds the value to the given scalar.
*
* @param aId The scalar enum id.
* @param aValue The unsigned value to add to the scalar.
*/
void
ScalarAdd(mozilla::Telemetry::ScalarID aId, uint32_t aVal)
{
TelemetryScalar::Add(aId, aVal);
}
/**
* Sets the scalar to the given value.
*
* @param aId The scalar enum id.
* @param aValue The numeric, unsigned value to set the scalar to.
*/
void
ScalarSet(mozilla::Telemetry::ScalarID aId, uint32_t aVal)
{
TelemetryScalar::Set(aId, aVal);
}
/**
* Sets the scalar to the given value.
*
* @param aId The scalar enum id.
* @param aValue The string value to set the scalar to.
*/
void
ScalarSet(mozilla::Telemetry::ScalarID aId, bool aVal)
{
TelemetryScalar::Set(aId, aVal);
}
void
ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aVal)
{
TelemetryScalar::Set(aId, aVal);
}
/**
* Sets the scalar to the maximum of the current and the passed value.
*
* @param aId The scalar enum id.
* @param aValue The unsigned value to set the scalar to.
*/
void
ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, uint32_t aVal)
{
TelemetryScalar::SetMaximum(aId, aVal);
}
void
ScalarAdd(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aVal)
{
TelemetryScalar::Add(aId, aKey, aVal);
}
void
ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aVal)
{
TelemetryScalar::Set(aId, aKey, aVal);
}
void
ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, bool aVal)
{
TelemetryScalar::Set(aId, aKey, aVal);
}
void
ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aVal)
{
TelemetryScalar::SetMaximum(aId, aKey, aVal);
}
} // namespace Telemetry
} // namespace mozilla

View File

@ -363,7 +363,7 @@ void WriteFailedProfileLock(nsIFile* aProfileDir);
* Adds the value to the given scalar.
*
* @param aId The scalar enum id.
* @param aValue The unsigned value to add to the scalar.
* @param aValue The value to add to the scalar.
*/
void ScalarAdd(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
@ -371,7 +371,7 @@ void ScalarAdd(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
* Sets the scalar to the given value.
*
* @param aId The scalar enum id.
* @param aValue The numeric, unsigned value to set the scalar to.
* @param aValue The value to set the scalar to.
*/
void ScalarSet(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
@ -379,7 +379,15 @@ void ScalarSet(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
* Sets the scalar to the given value.
*
* @param aId The scalar enum id.
* @param aValue The string value to set the scalar to, truncated to
* @param aValue The value to set the scalar to.
*/
void ScalarSet(mozilla::Telemetry::ScalarID aId, bool aValue);
/**
* Sets the scalar to the given value.
*
* @param aId The scalar enum id.
* @param aValue The value to set the scalar to, truncated to
* 50 characters if exceeding that length.
*/
void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aValue);
@ -388,11 +396,48 @@ void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aValue);
* Sets the scalar to the maximum of the current and the passed value.
*
* @param aId The scalar enum id.
* @param aValue The unsigned value the scalar is set to if its greater
* @param aValue The value the scalar is set to if its greater
* than the current value.
*/
void ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
/**
* Adds the value to the given scalar.
*
* @param aId The scalar enum id.
* @param aKey The scalar key.
* @param aValue The value to add to the scalar.
*/
void ScalarAdd(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue);
/**
* Sets the scalar to the given value.
*
* @param aId The scalar enum id.
* @param aKey The scalar key.
* @param aValue The value to set the scalar to.
*/
void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue);
/**
* Sets the scalar to the given value.
*
* @param aId The scalar enum id.
* @param aKey The scalar key.
* @param aValue The value to set the scalar to.
*/
void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, bool aValue);
/**
* Sets the scalar to the maximum of the current and the passed value.
*
* @param aId The scalar enum id.
* @param aKey The scalar key.
* @param aValue The value the scalar is set to if its greater
* than the current value.
*/
void ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue);
} // namespace Telemetry
} // namespace mozilla

View File

@ -70,6 +70,8 @@ using mozilla::Telemetry::Common::IsInDataset;
namespace {
const uint32_t kMaximumNumberOfKeys = 100;
const uint32_t kMaximumKeyStringLength = 70;
const uint32_t kMaximumStringValueLength = 50;
const uint32_t kScalarCount =
static_cast<uint32_t>(mozilla::Telemetry::ScalarID::ScalarCount);
@ -81,6 +83,9 @@ enum class ScalarResult : uint8_t {
OperationNotSupported,
InvalidType,
InvalidValue,
// Keyed Scalar Errors
KeyTooLong,
TooManyKeys,
// String Scalar Errors
StringTooLong,
// Unsigned Scalar Errors
@ -111,7 +116,10 @@ MapToNsResult(ScalarResult aSr)
return NS_OK;
case ScalarResult::InvalidType:
case ScalarResult::InvalidValue:
case ScalarResult::KeyTooLong:
return NS_ERROR_ILLEGAL_VALUE;
case ScalarResult::TooManyKeys:
return NS_ERROR_FAILURE;
case ScalarResult::UnsignedNegativeValue:
case ScalarResult::UnsignedTruncatedValue:
// We shouldn't throw if trying to set a negative number or are truncated,
@ -471,8 +479,236 @@ ScalarBoolean::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const
return aMallocSizeOf(this);
}
/**
* Allocate a scalar class given the scalar info.
*
* @param aInfo The informations for the scalar coming from the definition file.
* @return nullptr if the scalar type is unknown, otherwise a valid pointer to the
* scalar type.
*/
ScalarBase*
internal_ScalarAllocate(uint32_t aScalarKind)
{
ScalarBase* scalar = nullptr;
switch (aScalarKind) {
case nsITelemetry::SCALAR_COUNT:
scalar = new ScalarUnsigned();
break;
case nsITelemetry::SCALAR_STRING:
scalar = new ScalarString();
break;
case nsITelemetry::SCALAR_BOOLEAN:
scalar = new ScalarBoolean();
break;
default:
MOZ_ASSERT(false, "Invalid scalar type");
}
return scalar;
}
/**
* The implementation for the keyed scalar type.
*/
class KeyedScalar
{
public:
typedef mozilla::Pair<nsCString, nsCOMPtr<nsIVariant>> KeyValuePair;
explicit KeyedScalar(uint32_t aScalarKind) : mScalarKind(aScalarKind) {};
~KeyedScalar() {};
// Set, Add and SetMaximum functions as described in the Telemetry IDL.
// These methods implicitly instantiate a Scalar[*] for each key.
ScalarResult SetValue(const nsAString& aKey, nsIVariant* aValue);
ScalarResult AddValue(const nsAString& aKey, nsIVariant* aValue);
ScalarResult SetMaximum(const nsAString& aKey, nsIVariant* aValue);
// Convenience methods used by the C++ API.
void SetValue(const nsAString& aKey, uint32_t aValue);
void SetValue(const nsAString& aKey, bool aValue);
void AddValue(const nsAString& aKey, uint32_t aValue);
void SetMaximum(const nsAString& aKey, uint32_t aValue);
// GetValue is used to get the key-value pairs stored in the keyed scalar
// when persisting it to JS.
nsresult GetValue(nsTArray<KeyValuePair>& aValues) const;
// To measure the memory stats.
size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf);
private:
typedef nsClassHashtable<nsCStringHashKey, ScalarBase> ScalarKeysMapType;
ScalarKeysMapType mScalarKeys;
const uint32_t mScalarKind;
ScalarResult GetScalarForKey(const nsAString& aKey, ScalarBase** aRet);
};
ScalarResult
KeyedScalar::SetValue(const nsAString& aKey, nsIVariant* aValue)
{
ScalarBase* scalar = nullptr;
ScalarResult sr = GetScalarForKey(aKey, &scalar);
if (sr != ScalarResult::Ok) {
return sr;
}
return scalar->SetValue(aValue);
}
ScalarResult
KeyedScalar::AddValue(const nsAString& aKey, nsIVariant* aValue)
{
ScalarBase* scalar = nullptr;
ScalarResult sr = GetScalarForKey(aKey, &scalar);
if (sr != ScalarResult::Ok) {
return sr;
}
return scalar->AddValue(aValue);
}
ScalarResult
KeyedScalar::SetMaximum(const nsAString& aKey, nsIVariant* aValue)
{
ScalarBase* scalar = nullptr;
ScalarResult sr = GetScalarForKey(aKey, &scalar);
if (sr != ScalarResult::Ok) {
return sr;
}
return scalar->SetMaximum(aValue);
}
void
KeyedScalar::SetValue(const nsAString& aKey, uint32_t aValue)
{
ScalarBase* scalar = nullptr;
ScalarResult sr = GetScalarForKey(aKey, &scalar);
if (sr != ScalarResult::Ok) {
MOZ_ASSERT(false, "Key too long or too many keys are recorded in the scalar.");
return;
}
return scalar->SetValue(aValue);
}
void
KeyedScalar::SetValue(const nsAString& aKey, bool aValue)
{
ScalarBase* scalar = nullptr;
ScalarResult sr = GetScalarForKey(aKey, &scalar);
if (sr != ScalarResult::Ok) {
MOZ_ASSERT(false, "Key too long or too many keys are recorded in the scalar.");
return;
}
return scalar->SetValue(aValue);
}
void
KeyedScalar::AddValue(const nsAString& aKey, uint32_t aValue)
{
ScalarBase* scalar = nullptr;
ScalarResult sr = GetScalarForKey(aKey, &scalar);
if (sr != ScalarResult::Ok) {
MOZ_ASSERT(false, "Key too long or too many keys are recorded in the scalar.");
return;
}
return scalar->AddValue(aValue);
}
void
KeyedScalar::SetMaximum(const nsAString& aKey, uint32_t aValue)
{
ScalarBase* scalar = nullptr;
ScalarResult sr = GetScalarForKey(aKey, &scalar);
if (sr != ScalarResult::Ok) {
MOZ_ASSERT(false, "Key too long or too many keys are recorded in the scalar.");
return;
}
return scalar->SetMaximum(aValue);
}
/**
* Get a key-value array with the values for the Keyed Scalar.
* @param aValue The array that will hold the key-value pairs.
* @return {nsresult} NS_OK or an error value as reported by the
* the specific scalar objects implementations (e.g.
* ScalarUnsigned).
*/
nsresult
KeyedScalar::GetValue(nsTArray<KeyValuePair>& aValues) const
{
for (auto iter = mScalarKeys.ConstIter(); !iter.Done(); iter.Next()) {
ScalarBase* scalar = static_cast<ScalarBase*>(iter.Data());
// Get the scalar value.
nsCOMPtr<nsIVariant> scalarValue;
nsresult rv = scalar->GetValue(scalarValue);
if (NS_FAILED(rv)) {
return rv;
}
// Append it to value list.
aValues.AppendElement(mozilla::MakePair(nsCString(iter.Key()), scalarValue));
}
return NS_OK;
}
/**
* Get the scalar for the referenced key.
* If there's no such key, instantiate a new Scalar object with the
* same type of the Keyed scalar and create the key.
*/
ScalarResult
KeyedScalar::GetScalarForKey(const nsAString& aKey, ScalarBase** aRet)
{
if (aKey.Length() >= kMaximumKeyStringLength) {
return ScalarResult::KeyTooLong;
}
if (mScalarKeys.Count() >= kMaximumNumberOfKeys) {
return ScalarResult::TooManyKeys;
}
NS_ConvertUTF16toUTF8 utf8Key(aKey);
ScalarBase* scalar = nullptr;
if (mScalarKeys.Get(utf8Key, &scalar)) {
*aRet = scalar;
return ScalarResult::Ok;
}
scalar = internal_ScalarAllocate(mScalarKind);
if (!scalar) {
return ScalarResult::InvalidType;
}
mScalarKeys.Put(utf8Key, scalar);
*aRet = scalar;
return ScalarResult::Ok;
}
size_t
KeyedScalar::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf)
{
size_t n = aMallocSizeOf(this);
for (auto iter = mScalarKeys.Iter(); !iter.Done(); iter.Next()) {
ScalarBase* scalar = static_cast<ScalarBase*>(iter.Data());
n += scalar->SizeOfIncludingThis(aMallocSizeOf);
}
return n;
}
typedef nsUint32HashKey ScalarIDHashKey;
typedef nsClassHashtable<ScalarIDHashKey, ScalarBase> ScalarStorageMapType;
typedef nsClassHashtable<ScalarIDHashKey, KeyedScalar> KeyedScalarStorageMapType;
} // namespace
@ -495,6 +731,9 @@ ScalarMapType gScalarNameIDMap(kScalarCount);
// the scalar instance and takes care of deallocating them when they
// get removed from the map.
ScalarStorageMapType gScalarStorageMap;
// The ID -> Keyed Scalar Object map. As for plain scalars, this is
// nsClassHashtable. See above.
KeyedScalarStorageMapType gKeyedScalarStorageMap;
} // namespace
@ -551,12 +790,20 @@ internal_LogToBrowserConsole(uint32_t aLogLevel, const nsAString& aMsg)
bool
internal_ShouldLogError(ScalarResult aSr)
{
if (aSr == ScalarResult::StringTooLong ||
aSr == ScalarResult::UnsignedNegativeValue ||
aSr == ScalarResult::UnsignedTruncatedValue) {
switch (aSr) {
case ScalarResult::StringTooLong: MOZ_FALLTHROUGH;
case ScalarResult::KeyTooLong: MOZ_FALLTHROUGH;
case ScalarResult::TooManyKeys: MOZ_FALLTHROUGH;
case ScalarResult::UnsignedNegativeValue: MOZ_FALLTHROUGH;
case ScalarResult::UnsignedTruncatedValue:
// Intentional fall-through.
return true;
default:
return false;
}
// It should never reach this point.
return false;
}
@ -577,6 +824,12 @@ internal_LogScalarError(const nsACString& aScalarName, ScalarResult aSr)
case ScalarResult::StringTooLong:
errorMessage.Append(NS_LITERAL_STRING(" - Truncating scalar value to 50 characters."));
break;
case ScalarResult::KeyTooLong:
errorMessage.Append(NS_LITERAL_STRING(" - The key length must be limited to 70 characters."));
break;
case ScalarResult::TooManyKeys:
errorMessage.Append(NS_LITERAL_STRING(" - Keyed scalars cannot have more than 100 keys."));
break;
case ScalarResult::UnsignedNegativeValue:
errorMessage.Append(NS_LITERAL_STRING(" - Trying to set an unsigned scalar to a negative number."));
break;
@ -618,6 +871,18 @@ internal_InfoForScalarID(mozilla::Telemetry::ScalarID aId)
return gScalars[static_cast<uint32_t>(aId)];
}
/**
* Check if the given scalar is a keyed scalar.
*
* @param aId The scalar enum.
* @return true if aId refers to a keyed scalar, false otherwise.
*/
bool
internal_IsKeyedScalar(mozilla::Telemetry::ScalarID aId)
{
return internal_InfoForScalarID(aId).keyed;
}
bool
internal_CanRecordForScalarID(mozilla::Telemetry::ScalarID aId)
{
@ -640,33 +905,6 @@ internal_CanRecordForScalarID(mozilla::Telemetry::ScalarID aId)
return true;
}
/**
* Allocate a scalar class given the scalar info.
*
* @param aInfo The informations for the scalar coming from the definition file.
* @return nullptr if the scalar type is unknown, otherwise a valid pointer to the
* scalar type.
*/
ScalarBase*
internal_ScalarAllocate(const ScalarInfo& aInfo)
{
ScalarBase* scalar = nullptr;
switch (aInfo.kind) {
case nsITelemetry::SCALAR_COUNT:
scalar = new ScalarUnsigned();
break;
case nsITelemetry::SCALAR_STRING:
scalar = new ScalarString();
break;
case nsITelemetry::SCALAR_BOOLEAN:
scalar = new ScalarBoolean();
break;
default:
MOZ_ASSERT(false, "Invalid scalar type");
}
return scalar;
}
/**
* Get the scalar enum id from the scalar name.
*
@ -708,6 +946,7 @@ nsresult
internal_GetScalarByEnum(mozilla::Telemetry::ScalarID aId, ScalarBase** aRet)
{
if (!IsValidEnumId(aId)) {
MOZ_ASSERT(false, "Requested a scalar with an invalid id.");
return NS_ERROR_INVALID_ARG;
}
@ -725,7 +964,7 @@ internal_GetScalarByEnum(mozilla::Telemetry::ScalarID aId, ScalarBase** aRet)
return NS_ERROR_NOT_AVAILABLE;
}
scalar = internal_ScalarAllocate(info);
scalar = internal_ScalarAllocate(info.kind);
if (!scalar) {
return NS_ERROR_INVALID_ARG;
}
@ -753,6 +992,102 @@ internal_GetRecordableScalar(mozilla::Telemetry::ScalarID aId)
return nullptr;
}
if (internal_IsKeyedScalar(aId)) {
return nullptr;
}
// Are we allowed to record this scalar?
if (!internal_CanRecordForScalarID(aId)) {
return nullptr;
}
return scalar;
}
} // namespace
////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////
//
// PRIVATE: thread-unsafe helpers for the keyed scalars
namespace {
/**
* Get a keyed scalar object by its enum id. This implicitly allocates the keyed
* scalar object in the storage if it wasn't previously allocated.
*
* @param aId The scalar id.
* @param aRes The output variable that stores scalar object.
* @return
* NS_ERROR_INVALID_ARG if the scalar id is unknown or a this is a keyed string
* scalar.
* NS_ERROR_NOT_AVAILABLE if the scalar is expired.
* NS_OK if the scalar was found. If that's the case, aResult contains a
* valid pointer to a scalar type.
*/
nsresult
internal_GetKeyedScalarByEnum(mozilla::Telemetry::ScalarID aId, KeyedScalar** aRet)
{
if (!IsValidEnumId(aId)) {
MOZ_ASSERT(false, "Requested a keyed scalar with an invalid id.");
return NS_ERROR_INVALID_ARG;
}
const uint32_t id = static_cast<uint32_t>(aId);
KeyedScalar* scalar = nullptr;
if (gKeyedScalarStorageMap.Get(id, &scalar)) {
*aRet = scalar;
return NS_OK;
}
const ScalarInfo &info = gScalars[id];
if (IsExpiredVersion(info.expiration())) {
return NS_ERROR_NOT_AVAILABLE;
}
// We don't currently support keyed string scalars. Disable them.
if (info.kind == nsITelemetry::SCALAR_STRING) {
MOZ_ASSERT(false, "Keyed string scalars are not currently supported.");
return NS_ERROR_INVALID_ARG;
}
scalar = new KeyedScalar(info.kind);
if (!scalar) {
return NS_ERROR_INVALID_ARG;
}
gKeyedScalarStorageMap.Put(id, scalar);
*aRet = scalar;
return NS_OK;
}
/**
* Get a keyed scalar object by its enum id, if we're allowed to record it.
*
* @param aId The scalar id.
* @return The KeyedScalar instance or nullptr if we're not allowed to record
* the scalar.
*/
KeyedScalar*
internal_GetRecordableKeyedScalar(mozilla::Telemetry::ScalarID aId)
{
// Get the scalar by the enum (it also internally checks for aId validity).
KeyedScalar* scalar = nullptr;
nsresult rv = internal_GetKeyedScalarByEnum(aId, &scalar);
if (NS_FAILED(rv)) {
return nullptr;
}
if (!internal_IsKeyedScalar(aId)) {
return nullptr;
}
// Are we allowed to record this scalar?
if (!internal_CanRecordForScalarID(aId)) {
return nullptr;
@ -812,6 +1147,7 @@ TelemetryScalar::DeInitializeGlobalState()
gCanRecordExtended = false;
gScalarNameIDMap.Clear();
gScalarStorageMap.Clear();
gKeyedScalarStorageMap.Clear();
gInitDone = false;
}
@ -834,7 +1170,7 @@ TelemetryScalar::SetCanRecordExtended(bool b) {
* @param aName The scalar name.
* @param aVal The numeric value to add to the scalar.
* @param aCx The JS context.
* @return NS_OK if the value was added or if we're not allow to record to this
* @return NS_OK if the value was added or if we're not allowed to record to this
* dataset. Otherwise, return an error.
*/
nsresult
@ -858,6 +1194,11 @@ TelemetryScalar::Add(const nsACString& aName, JS::HandleValue aVal, JSContext* a
return rv;
}
// We're trying to set a plain scalar, so make sure this is one.
if (internal_IsKeyedScalar(id)) {
return NS_ERROR_ILLEGAL_VALUE;
}
// Are we allowed to record this scalar?
if (!internal_CanRecordForScalarID(id)) {
return NS_OK;
@ -885,6 +1226,70 @@ TelemetryScalar::Add(const nsACString& aName, JS::HandleValue aVal, JSContext* a
return MapToNsResult(sr);
}
/**
* Adds the value to the given scalar.
*
* @param aName The scalar name.
* @param aKey The key name.
* @param aVal The numeric value to add to the scalar.
* @param aCx The JS context.
* @return NS_OK if the value was added or if we're not allow to record to this
* dataset. Otherwise, return an error.
*/
nsresult
TelemetryScalar::Add(const nsACString& aName, const nsAString& aKey, JS::HandleValue aVal,
JSContext* aCx)
{
// Unpack the aVal to nsIVariant. This uses the JS context.
nsCOMPtr<nsIVariant> unpackedVal;
nsresult rv =
nsContentUtils::XPConnect()->JSToVariant(aCx, aVal, getter_AddRefs(unpackedVal));
if (NS_FAILED(rv)) {
return rv;
}
ScalarResult sr;
{
StaticMutexAutoLock locker(gTelemetryScalarsMutex);
mozilla::Telemetry::ScalarID id;
rv = internal_GetEnumByScalarName(aName, &id);
if (NS_FAILED(rv)) {
return rv;
}
// Make sure this is a keyed scalar.
if (!internal_IsKeyedScalar(id)) {
return NS_ERROR_ILLEGAL_VALUE;
}
// Are we allowed to record this scalar?
if (!internal_CanRecordForScalarID(id)) {
return NS_OK;
}
// Finally get the scalar.
KeyedScalar* scalar = nullptr;
rv = internal_GetKeyedScalarByEnum(id, &scalar);
if (NS_FAILED(rv)) {
// Don't throw on expired scalars.
if (rv == NS_ERROR_NOT_AVAILABLE) {
return NS_OK;
}
return rv;
}
sr = scalar->AddValue(aKey, unpackedVal);
}
// Warn the user about the error if we need to.
if (internal_ShouldLogError(sr)) {
internal_LogScalarError(aName, sr);
}
return MapToNsResult(sr);
}
/**
* Adds the value to the given scalar.
*
@ -904,6 +1309,27 @@ TelemetryScalar::Add(mozilla::Telemetry::ScalarID aId, uint32_t aValue)
scalar->AddValue(aValue);
}
/**
* Adds the value to the given keyed scalar.
*
* @param aId The scalar enum id.
* @param aKey The key name.
* @param aVal The numeric value to add to the scalar.
*/
void
TelemetryScalar::Add(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
uint32_t aValue)
{
StaticMutexAutoLock locker(gTelemetryScalarsMutex);
KeyedScalar* scalar = internal_GetRecordableKeyedScalar(aId);
if (!scalar) {
return;
}
scalar->AddValue(aKey, aValue);
}
/**
* Sets the scalar to the given value.
*
@ -934,6 +1360,11 @@ TelemetryScalar::Set(const nsACString& aName, JS::HandleValue aVal, JSContext* a
return rv;
}
// We're trying to set a plain scalar, so make sure this is one.
if (internal_IsKeyedScalar(id)) {
return NS_ERROR_ILLEGAL_VALUE;
}
// Are we allowed to record this scalar?
if (!internal_CanRecordForScalarID(id)) {
return NS_OK;
@ -961,6 +1392,70 @@ TelemetryScalar::Set(const nsACString& aName, JS::HandleValue aVal, JSContext* a
return MapToNsResult(sr);
}
/**
* Sets the keyed scalar to the given value.
*
* @param aName The scalar name.
* @param aKey The key name.
* @param aVal The value to set the scalar to.
* @param aCx The JS context.
* @return NS_OK if the value was added or if we're not allow to record to this
* dataset. Otherwise, return an error.
*/
nsresult
TelemetryScalar::Set(const nsACString& aName, const nsAString& aKey, JS::HandleValue aVal,
JSContext* aCx)
{
// Unpack the aVal to nsIVariant. This uses the JS context.
nsCOMPtr<nsIVariant> unpackedVal;
nsresult rv =
nsContentUtils::XPConnect()->JSToVariant(aCx, aVal, getter_AddRefs(unpackedVal));
if (NS_FAILED(rv)) {
return rv;
}
ScalarResult sr;
{
StaticMutexAutoLock locker(gTelemetryScalarsMutex);
mozilla::Telemetry::ScalarID id;
rv = internal_GetEnumByScalarName(aName, &id);
if (NS_FAILED(rv)) {
return rv;
}
// We're trying to set a keyed scalar. Report an error if this isn't one.
if (!internal_IsKeyedScalar(id)) {
return NS_ERROR_ILLEGAL_VALUE;
}
// Are we allowed to record this scalar?
if (!internal_CanRecordForScalarID(id)) {
return NS_OK;
}
// Finally get the scalar.
KeyedScalar* scalar = nullptr;
rv = internal_GetKeyedScalarByEnum(id, &scalar);
if (NS_FAILED(rv)) {
// Don't throw on expired scalars.
if (rv == NS_ERROR_NOT_AVAILABLE) {
return NS_OK;
}
return rv;
}
sr = scalar->SetValue(aKey, unpackedVal);
}
// Warn the user about the error if we need to.
if (internal_ShouldLogError(sr)) {
internal_LogScalarError(aName, sr);
}
return MapToNsResult(sr);
}
/**
* Sets the scalar to the given numeric value.
*
@ -1018,6 +1513,48 @@ TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId, bool aValue)
scalar->SetValue(aValue);
}
/**
* Sets the keyed scalar to the given numeric value.
*
* @param aId The scalar enum id.
* @param aKey The scalar key.
* @param aValue The numeric, unsigned value to set the scalar to.
*/
void
TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
uint32_t aValue)
{
StaticMutexAutoLock locker(gTelemetryScalarsMutex);
KeyedScalar* scalar = internal_GetRecordableKeyedScalar(aId);
if (!scalar) {
return;
}
scalar->SetValue(aKey, aValue);
}
/**
* Sets the scalar to the given boolean value.
*
* @param aId The scalar enum id.
* @param aKey The scalar key.
* @param aValue The boolean value to set the scalar to.
*/
void
TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
bool aValue)
{
StaticMutexAutoLock locker(gTelemetryScalarsMutex);
KeyedScalar* scalar = internal_GetRecordableKeyedScalar(aId);
if (!scalar) {
return;
}
scalar->SetValue(aKey, aValue);
}
/**
* Sets the scalar to the maximum of the current and the passed value.
*
@ -1048,6 +1585,11 @@ TelemetryScalar::SetMaximum(const nsACString& aName, JS::HandleValue aVal, JSCon
return rv;
}
// Make sure this is not a keyed scalar.
if (internal_IsKeyedScalar(id)) {
return NS_ERROR_ILLEGAL_VALUE;
}
// Are we allowed to record this scalar?
if (!internal_CanRecordForScalarID(id)) {
return NS_OK;
@ -1075,6 +1617,70 @@ TelemetryScalar::SetMaximum(const nsACString& aName, JS::HandleValue aVal, JSCon
return MapToNsResult(sr);
}
/**
* Sets the scalar to the maximum of the current and the passed value.
*
* @param aName The scalar name.
* @param aKey The key name.
* @param aVal The numeric value to set the scalar to.
* @param aCx The JS context.
* @return NS_OK if the value was added or if we're not allow to record to this
* dataset. Otherwise, return an error.
*/
nsresult
TelemetryScalar::SetMaximum(const nsACString& aName, const nsAString& aKey, JS::HandleValue aVal,
JSContext* aCx)
{
// Unpack the aVal to nsIVariant. This uses the JS context.
nsCOMPtr<nsIVariant> unpackedVal;
nsresult rv =
nsContentUtils::XPConnect()->JSToVariant(aCx, aVal, getter_AddRefs(unpackedVal));
if (NS_FAILED(rv)) {
return rv;
}
ScalarResult sr;
{
StaticMutexAutoLock locker(gTelemetryScalarsMutex);
mozilla::Telemetry::ScalarID id;
rv = internal_GetEnumByScalarName(aName, &id);
if (NS_FAILED(rv)) {
return rv;
}
// Make sure this is a keyed scalar.
if (!internal_IsKeyedScalar(id)) {
return NS_ERROR_ILLEGAL_VALUE;
}
// Are we allowed to record this scalar?
if (!internal_CanRecordForScalarID(id)) {
return NS_OK;
}
// Finally get the scalar.
KeyedScalar* scalar = nullptr;
rv = internal_GetKeyedScalarByEnum(id, &scalar);
if (NS_FAILED(rv)) {
// Don't throw on expired scalars.
if (rv == NS_ERROR_NOT_AVAILABLE) {
return NS_OK;
}
return rv;
}
sr = scalar->SetMaximum(aKey, unpackedVal);
}
// Warn the user about the error if we need to.
if (internal_ShouldLogError(sr)) {
internal_LogScalarError(aName, sr);
}
return MapToNsResult(sr);
}
/**
* Sets the scalar to the maximum of the current and the passed value.
*
@ -1094,6 +1700,27 @@ TelemetryScalar::SetMaximum(mozilla::Telemetry::ScalarID aId, uint32_t aValue)
scalar->SetValue(aValue);
}
/**
* Sets the keyed scalar to the maximum of the current and the passed value.
*
* @param aId The scalar enum id.
* @param aKey The key name.
* @param aValue The numeric value to set the scalar to.
*/
void
TelemetryScalar::SetMaximum(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
uint32_t aValue)
{
StaticMutexAutoLock locker(gTelemetryScalarsMutex);
KeyedScalar* scalar = internal_GetRecordableKeyedScalar(aId);
if (!scalar) {
return;
}
scalar->SetValue(aKey, aValue);
}
/**
* Serializes the scalars from the given dataset to a json-style object and resets them.
* The returned structure looks like {"group1.probe":1,"group1.other_probe":false,...}.
@ -1169,6 +1796,99 @@ TelemetryScalar::CreateSnapshots(unsigned int aDataset, bool aClearScalars, JSCo
return NS_OK;
}
/**
* Serializes the scalars from the given dataset to a json-style object and resets them.
* The returned structure looks like:
* { "group1.probe": { "key_1": 2, "key_2": 1, ... }, ... }
*
* @param aDataset DATASET_RELEASE_CHANNEL_OPTOUT or DATASET_RELEASE_CHANNEL_OPTIN.
* @param aClear Whether to clear out the keyed scalars after snapshotting.
*/
nsresult
TelemetryScalar::CreateKeyedSnapshots(unsigned int aDataset, bool aClearScalars, JSContext* aCx,
uint8_t optional_argc, JS::MutableHandle<JS::Value> aResult)
{
// If no arguments were passed in, apply the default value.
if (!optional_argc) {
aClearScalars = false;
}
JS::Rooted<JSObject*> root_obj(aCx, JS_NewPlainObject(aCx));
if (!root_obj) {
return NS_ERROR_FAILURE;
}
aResult.setObject(*root_obj);
// Only lock the mutex while accessing our data, without locking any JS related code.
typedef mozilla::Pair<const char*, nsTArray<KeyedScalar::KeyValuePair>> DataPair;
nsTArray<DataPair> scalarsToReflect;
{
StaticMutexAutoLock locker(gTelemetryScalarsMutex);
// Iterate the scalars in gKeyedScalarStorageMap. The storage may contain empty or yet
// to be initialized scalars.
for (auto iter = gKeyedScalarStorageMap.Iter(); !iter.Done(); iter.Next()) {
KeyedScalar* scalar = static_cast<KeyedScalar*>(iter.Data());
// Get the informations for this scalar.
const ScalarInfo& info = gScalars[iter.Key()];
// Serialize the scalar if it's in the desired dataset.
if (IsInDataset(info.dataset, aDataset)) {
// Get the keys for this scalar.
nsTArray<KeyedScalar::KeyValuePair> scalarKeyedData;
nsresult rv = scalar->GetValue(scalarKeyedData);
if (NS_FAILED(rv)) {
return rv;
}
// Append it to our list.
scalarsToReflect.AppendElement(mozilla::MakePair(info.name(), scalarKeyedData));
}
}
if (aClearScalars) {
// The map already takes care of freeing the allocated memory.
gKeyedScalarStorageMap.Clear();
}
}
// Reflect it to JS.
for (nsTArray<DataPair>::size_type i = 0; i < scalarsToReflect.Length(); i++) {
const DataPair& keyedScalarData = scalarsToReflect[i];
// Go through each keyed scalar and create a keyed scalar object.
// This object will hold the values for all the keyed scalar keys.
JS::RootedObject keyedScalarObj(aCx, JS_NewPlainObject(aCx));
// Define a property for each scalar key, then add it to the keyed scalar
// object.
const nsTArray<KeyedScalar::KeyValuePair>& keyProps = keyedScalarData.second();
for (uint32_t i = 0; i < keyProps.Length(); i++) {
const KeyedScalar::KeyValuePair& keyData = keyProps[i];
// Convert the value for the key to a JSValue.
JS::Rooted<JS::Value> keyJsValue(aCx);
nsresult rv =
nsContentUtils::XPConnect()->VariantToJS(aCx, keyedScalarObj, keyData.second(), &keyJsValue);
if (NS_FAILED(rv)) {
return rv;
}
// Add the key to the scalar representation.
const NS_ConvertUTF8toUTF16 key(keyData.first());
if (!JS_DefineUCProperty(aCx, keyedScalarObj, key.Data(), key.Length(), keyJsValue, JSPROP_ENUMERATE)) {
return NS_ERROR_FAILURE;
}
}
// Add the scalar to the root object.
if (!JS_DefineProperty(aCx, root_obj, keyedScalarData.first(), keyedScalarObj, JSPROP_ENUMERATE)) {
return NS_ERROR_FAILURE;
}
}
return NS_OK;
}
/**
* Resets all the stored scalars. This is intended to be only used in tests.
*/
@ -1177,6 +1897,7 @@ TelemetryScalar::ClearScalars()
{
StaticMutexAutoLock locker(gTelemetryScalarsMutex);
gScalarStorageMap.Clear();
gKeyedScalarStorageMap.Clear();
}
size_t
@ -1191,9 +1912,15 @@ TelemetryScalar::GetScalarSizesOfIncludingThis(mozilla::MallocSizeOf aMallocSize
{
StaticMutexAutoLock locker(gTelemetryScalarsMutex);
size_t n = 0;
// For the plain scalars...
for (auto iter = gScalarStorageMap.Iter(); !iter.Done(); iter.Next()) {
ScalarBase* scalar = static_cast<ScalarBase*>(iter.Data());
n += scalar->SizeOfIncludingThis(aMallocSizeOf);
}
// ...and for the keyed scalars.
for (auto iter = gKeyedScalarStorageMap.Iter(); !iter.Done(); iter.Next()) {
KeyedScalar* scalar = static_cast<KeyedScalar*>(iter.Data());
n += scalar->SizeOfIncludingThis(aMallocSizeOf);
}
return n;
}

View File

@ -29,6 +29,17 @@ nsresult CreateSnapshots(unsigned int aDataset, bool aClearScalars,
JSContext* aCx, uint8_t optional_argc,
JS::MutableHandle<JS::Value> aResult);
// Keyed JS API Endpoints.
nsresult Add(const nsACString& aName, const nsAString& aKey, JS::HandleValue aVal,
JSContext* aCx);
nsresult Set(const nsACString& aName, const nsAString& aKey, JS::HandleValue aVal,
JSContext* aCx);
nsresult SetMaximum(const nsACString& aName, const nsAString& aKey, JS::HandleValue aVal,
JSContext* aCx);
nsresult CreateKeyedSnapshots(unsigned int aDataset, bool aClearScalars,
JSContext* aCx, uint8_t optional_argc,
JS::MutableHandle<JS::Value> aResult);
// C++ API Endpoints.
void Add(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
void Set(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
@ -36,6 +47,12 @@ void Set(mozilla::Telemetry::ScalarID aId, const nsAString& aValue);
void Set(mozilla::Telemetry::ScalarID aId, bool aValue);
void SetMaximum(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
// Keyed C++ API Endpoints.
void Add(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue);
void Set(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue);
void Set(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, bool aValue);
void SetMaximum(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue);
// Only to be used for testing.
void ClearScalars();

View File

@ -971,8 +971,16 @@ var Impl = {
return ret;
},
getScalars: function (subsession, clearSubsession) {
this._log.trace("getScalars - subsession: " + subsession + ", clearSubsession: " + clearSubsession);
/**
* Get a snapshot of the scalars and clear them.
* @param {subsession} If true, then we collect the data for a subsession.
* @param {clearSubsession} If true, we need to clear the subsession.
* @param {keyed} Take a snapshot of keyed or non keyed scalars.
* @return {Object} The scalar data as a Javascript object.
*/
getScalars: function (subsession, clearSubsession, keyed) {
this._log.trace("getScalars - subsession: " + subsession + ", clearSubsession: " +
clearSubsession + ", keyed: " + keyed);
if (!subsession) {
// We only support scalars for subsessions.
@ -980,7 +988,8 @@ var Impl = {
return {};
}
let scalarsSnapshot =
let scalarsSnapshot = keyed ?
Telemetry.snapshotKeyedScalars(this.getDatasetType(), clearSubsession) :
Telemetry.snapshotScalars(this.getDatasetType(), clearSubsession);
// Don't return the test scalars.
@ -1285,6 +1294,7 @@ var Impl = {
payloadObj.processes = {
parent: {
scalars: protect(() => this.getScalars(isSubsession, clearSubsession)),
keyedScalars: protect(() => this.getScalars(isSubsession, clearSubsession, true)),
},
content: {
histograms: histograms[HISTOGRAM_SUFFIXES.CONTENT],

View File

@ -23,9 +23,13 @@ Probes in privileged JavaScript code can use the following functions to manipula
Services.telemetry.scalarSet(aName, aValue);
Services.telemetry.scalarSetMaximum(aName, aValue);
Services.telemetry.keyedScalarAdd(aName, aKey, aValue);
Services.telemetry.keyedScalarSet(aName, aKey, aValue);
Services.telemetry.keyedScalarSetMaximum(aName, aKey, aValue);
These functions can throw if, for example, an operation is performed on a scalar type that doesn't support it
(e.g. calling scalarSetMaximum on a scalar of the string kind). Please look at the code documentation for
additional informations.
(e.g. calling scalarSetMaximum on a scalar of the string kind). Please look at the `code documentation <https://dxr.mozilla.org/mozilla-central/search?q=regexp%3ATelemetryScalar%3A%3A(Set%7CAdd)+file%3ATelemetryScalar.cpp&redirect=false>`_ for
additional information.
C++ API
-------
@ -39,6 +43,11 @@ Probes in native code can use the more convenient helper functions declared in `
void ScalarSet(mozilla::Telemetry::ScalarID aId, bool aValue);
void ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
void ScalarAdd(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue);
void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue);
void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, bool aValue);
void ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue);
The YAML definition file
========================
Scalar probes are required to be registered, both for validation and transparency reasons,
@ -97,12 +106,19 @@ Optional Fields
- ``cpp_guard``: A string that gets inserted as an ``#ifdef`` directive around the automatically generated C++ declaration. This is typically used for platform-specific scalars, e.g. ``ANDROID``.
- ``release_channel_collection``: This can be either ``opt-in`` (default) or ``opt-out``. With the former the scalar is submitted by default on pre-release channels; on the release channel only if the user opted into additional data collection. With the latter the scalar is submitted by default on release and pre-release channels, unless the user opted out.
- ``keyed``: A boolean that determines whether this is a keyed scalar. It defaults to ``False``.
String type restrictions
------------------------
To prevent abuses, the content of a string scalar is limited to 50 characters in length. Trying
to set a longer string will result in an error and no string being set.
Keyed Scalars
-------------
Keyed scalars are collections of one of the available scalar types, indexed by a string key that can contain UTF8 characters and cannot be longer than 70 characters. Keyed scalars can contain up to 100 keys. This scalar type is for example useful when you want to break down certain counts by a name, like how often searches happen with which search engine.
Keyed scalars should only be used if the set of keys are not known beforehand. If the keys are from a known set of strings, other options are preferred if suitable, like categorical histograms or splitting measurements up into separate scalars.
The processor scripts
=====================
The scalar definition file is processed and checked for correctness at compile time. If it

View File

@ -40,11 +40,12 @@ def write_scalar_info(scalar, output, name_index, expiration_index):
if cpp_guard:
print("#if defined(%s)" % cpp_guard, file=output)
print(" {{ {}, {}, {}, {} }},"\
print(" {{ {}, {}, {}, {}, {} }},"\
.format(scalar.nsITelemetry_kind,
name_index,
expiration_index,
scalar.dataset),
scalar.dataset,
"true" if scalar.keyed else "false"),
file=output)
if cpp_guard:

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