diff --git a/.eslintignore b/.eslintignore index 727765d6c4c8..6be8317af999 100644 --- a/.eslintignore +++ b/.eslintignore @@ -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/** diff --git a/browser/base/content/test/webrtc/get_user_media_content_script.js b/browser/base/content/test/webrtc/get_user_media_content_script.js index 7786fe00cb32..37345b090ede 100644 --- a/browser/base/content/test/webrtc/get_user_media_content_script.js +++ b/browser/base/content/test/webrtc/get_user_media_content_script.js @@ -84,3 +84,9 @@ addMessageListener("Test:WaitForObserverCall", ({data}) => { } }, topic, false); }); + +addMessageListener("Test:WaitForMessage", () => { + content.addEventListener("message", ({data}) => { + sendAsyncMessage("Test:MessageReceived", data); + }, {once: true}); +}); diff --git a/browser/base/content/test/webrtc/head.js b/browser/base/content/test/webrtc/head.js index 327820160680..9909aec75b95 100644 --- a/browser/base/content/test/webrtc/head.js +++ b/browser/base/content/test/webrtc/head.js @@ -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) { diff --git a/browser/modules/webrtcUI.jsm b/browser/modules/webrtcUI.jsm index 707a8d7cb97f..c05bf6f54026 100644 --- a/browser/modules/webrtcUI.jsm +++ b/browser/modules/webrtcUI.jsm @@ -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) { diff --git a/devtools/client/aboutdebugging/components/target-list.js b/devtools/client/aboutdebugging/components/target-list.js index 2568a417aa25..b52bde588323 100644 --- a/devtools/client/aboutdebugging/components/target-list.js +++ b/devtools/client/aboutdebugging/components/target-list.js @@ -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 = ""; diff --git a/devtools/client/animationinspector/test/browser_animation_spacebar_toggles_node_animations.js b/devtools/client/animationinspector/test/browser_animation_spacebar_toggles_node_animations.js index d407070e0176..422e09821ac2 100644 --- a/devtools/client/animationinspector/test/browser_animation_spacebar_toggles_node_animations.js +++ b/devtools/client/animationinspector/test/browser_animation_spacebar_toggles_node_animations.js @@ -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; diff --git a/devtools/client/debugger/debugger.xul b/devtools/client/debugger/debugger.xul index 79ff4eeb3a78..19c1534f13e5 100644 --- a/devtools/client/debugger/debugger.xul +++ b/devtools/client/debugger/debugger.xul @@ -189,32 +189,16 @@ - - - - { - 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; diff --git a/devtools/client/debugger/panel.js b/devtools/client/debugger/panel.js index 4e430d29186c..352d7b284657 100644 --- a/devtools/client/debugger/panel.js +++ b/devtools/client/debugger/panel.js @@ -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() { diff --git a/devtools/client/debugger/test/mochitest/browser2.ini b/devtools/client/debugger/test/mochitest/browser2.ini index 58cf30c08d89..53a0e3ca274a 100644 --- a/devtools/client/debugger/test/mochitest/browser2.ini +++ b/devtools/client/debugger/test/mochitest/browser2.ini @@ -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] diff --git a/devtools/client/debugger/views/toolbar-view.js b/devtools/client/debugger/views/toolbar-view.js index cbcff6b4c983..5d795f66559a 100644 --- a/devtools/client/debugger/views/toolbar-view.js +++ b/devtools/client/debugger/views/toolbar-view.js @@ -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. diff --git a/devtools/client/definitions.js b/devtools/client/definitions.js index beb2305e9db1..22f85a3367d4 100644 --- a/devtools/client/definitions.js +++ b/devtools/client/definitions.js @@ -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() { diff --git a/devtools/client/framework/test/browser_toolbox_split_console.js b/devtools/client/framework/test/browser_toolbox_split_console.js index 02d934b89591..8e1fecd15283 100644 --- a/devtools/client/framework/test/browser_toolbox_split_console.js +++ b/devtools/client/framework/test/browser_toolbox_split_console.js @@ -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"); } diff --git a/devtools/client/framework/test/browser_toolbox_textbox_context_menu.js b/devtools/client/framework/test/browser_toolbox_textbox_context_menu.js index b3f88cf72ca0..86adffcb09d0 100644 --- a/devtools/client/framework/test/browser_toolbox_textbox_context_menu.js +++ b/devtools/client/framework/test/browser_toolbox_textbox_context_menu.js @@ -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); }); diff --git a/devtools/client/framework/test/browser_toolbox_window_reload_target.js b/devtools/client/framework/test/browser_toolbox_window_reload_target.js index d0a450a3a3a1..ea9e2d600ff1 100644 --- a/devtools/client/framework/test/browser_toolbox_window_reload_target.js +++ b/devtools/client/framework/test/browser_toolbox_window_reload_target.js @@ -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++; } diff --git a/devtools/client/framework/test/shared-head.js b/devtools/client/framework/test/shared-head.js index b404500cf069..acf35ae2d8a6 100644 --- a/devtools/client/framework/test/shared-head.js +++ b/devtools/client/framework/test/shared-head.js @@ -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"; +} diff --git a/devtools/client/framework/toolbox.js b/devtools/client/framework/toolbox.js index 3b32312b4cdb..443cbc5462e9 100644 --- a/devtools/client/framework/toolbox.js +++ b/devtools/client/framework/toolbox.js @@ -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 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 diff --git a/devtools/client/framework/toolbox.xul b/devtools/client/framework/toolbox.xul index 638cbfcfb85c..94aaecebdd8a 100644 --- a/devtools/client/framework/toolbox.xul +++ b/devtools/client/framework/toolbox.xul @@ -31,7 +31,6 @@ - diff --git a/devtools/client/inspector/components/inspector-tab-panel.js b/devtools/client/inspector/components/inspector-tab-panel.js index 4ea3d8ef7db9..68db7781eb4f 100644 --- a/devtools/client/inspector/components/inspector-tab-panel.js +++ b/devtools/client/inspector/components/inspector-tab-panel.js @@ -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); diff --git a/devtools/client/inspector/computed/test/browser_computed_search-filter_context-menu.js b/devtools/client/inspector/computed/test/browser_computed_search-filter_context-menu.js index 34d8fb2a2dbb..2a63fc8c1bda 100644 --- a/devtools/client/inspector/computed/test/browser_computed_search-filter_context-menu.js +++ b/devtools/client/inspector/computed/test/browser_computed_search-filter_context-menu.js @@ -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"); diff --git a/devtools/client/inspector/inspector-panel.js b/devtools/client/inspector/inspector-panel.js index f8f35580405f..33ccf246bbd4 100644 --- a/devtools/client/inspector/inspector-panel.js +++ b/devtools/client/inspector/inspector-panel.js @@ -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 diff --git a/devtools/client/inspector/inspector.xul b/devtools/client/inspector/inspector.xhtml similarity index 66% rename from devtools/client/inspector/inspector.xul rename to devtools/client/inspector/inspector.xhtml index f0ebb4f61294..61f07bae83e8 100644 --- a/devtools/client/inspector/inspector.xul +++ b/devtools/client/inspector/inspector.xhtml @@ -16,73 +16,77 @@ + - %inspectorDTD; - %styleinspectorDTD; - %fontinspectorDTD; - %layoutviewDTD; -]> + - + + + + + + - - - - + + + + + data-localization="placeholder=inspectorSearchHTML.label3"/> -
+ - - + + role="group" data-localization="aria-label=inspector.breadcrumbs.label" tabindex="0" /> - - - - -