author | Phil Ringnalda <philringnalda@gmail.com> |
Sun, 02 Feb 2014 09:10:57 -0800 | |
changeset 166469 | 2918a9e625b4afb867d4afc860eee18932514232 |
parent 166446 | 3e40f7389d1b9a6d81427e0bcae6666a9963bddc (current diff) |
parent 166468 | 463bae14bef347a349f6026a843a298371f478c0 (diff) |
child 166539 | 5f88d54c28e03d8cab01968ff2d54d85ab521ef1 |
push id | 26127 |
push user | philringnalda@gmail.com |
push date | Sun, 02 Feb 2014 17:11:12 +0000 |
treeherder | mozilla-central@2918a9e625b4 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
milestone | 29.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
browser/themes/linux/devtools/vview-delete.png | file | annotate | diff | comparison | revisions | |
browser/themes/linux/devtools/vview-edit.png | file | annotate | diff | comparison | revisions | |
browser/themes/osx/devtools/vview-delete.png | file | annotate | diff | comparison | revisions | |
browser/themes/osx/devtools/vview-edit.png | file | annotate | diff | comparison | revisions | |
browser/themes/windows/devtools/vview-delete.png | file | annotate | diff | comparison | revisions | |
browser/themes/windows/devtools/vview-edit.png | file | annotate | diff | comparison | revisions |
--- a/addon-sdk/source/app-extension/install.rdf +++ b/addon-sdk/source/app-extension/install.rdf @@ -13,17 +13,17 @@ <em:bootstrap>true</em:bootstrap> <em:unpack>false</em:unpack> <!-- Firefox --> <em:targetApplication> <Description> <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> <em:minVersion>21.0</em:minVersion> - <em:maxVersion>25.0a1</em:maxVersion> + <em:maxVersion>29.0a1</em:maxVersion> </Description> </em:targetApplication> <!-- Front End MetaData --> <em:name>Test App</em:name> <em:description>Harness for tests.</em:description> <em:creator>Mozilla Corporation</em:creator> <em:homepageURL></em:homepageURL>
--- a/addon-sdk/source/lib/sdk/event/utils.js +++ b/addon-sdk/source/lib/sdk/event/utils.js @@ -261,15 +261,15 @@ exports.Reactor = Reactor; * used to be called `require('sdk/event/core').setListeners` on. * This strips all keys that would trigger a listener to be set. * * @params {Object} object * @return {Object} */ function stripListeners (object) { - return Object.keys(object).reduce((agg, key) => { + return Object.keys(object || {}).reduce((agg, key) => { if (!EVENT_TYPE_PATTERN.test(key)) agg[key] = object[key]; return agg; }, {}); } exports.stripListeners = stripListeners;
--- a/addon-sdk/source/lib/sdk/l10n/prefs.js +++ b/addon-sdk/source/lib/sdk/l10n/prefs.js @@ -4,17 +4,17 @@ "use strict"; const { on } = require("../system/events"); const core = require("./core"); const { id: jetpackId} = require('../self'); const OPTIONS_DISPLAYED = "addon-options-displayed"; -function onOptionsDisplayed({ subjec: document, data: addonId }) { +function onOptionsDisplayed({ subject: document, data: addonId }) { if (addonId !== jetpackId) return; let query = 'setting[data-jetpack-id="' + jetpackId + '"][pref-name], ' + 'button[data-jetpack-id="' + jetpackId + '"][pref-name]'; let nodes = document.querySelectorAll(query); for (let node of nodes) { let name = node.getAttribute("pref-name"); if (node.tagName == "setting") {
--- a/addon-sdk/source/lib/toolkit/loader.js +++ b/addon-sdk/source/lib/toolkit/loader.js @@ -688,17 +688,19 @@ exports.unload = unload; // If `resolve` does not returns `uri` string exception will be thrown by // an associated `require` call. const Loader = iced(function Loader(options) { let { modules, globals, resolve, paths, rootURI, manifest, requireMap, isNative } = override({ paths: {}, modules: {}, - globals: {}, + globals: { + console: console + }, resolve: options.isNative ? exports.nodeResolve : exports.resolve, }, options); // We create an identity object that will be dispatched on an unload // event as subject. This way unload listeners will be able to assert // which loader is unloaded. Please note that we intentionally don't
new file mode 100644 --- /dev/null +++ b/addon-sdk/source/test/addons/simple-prefs-l10n/locale/en.properties @@ -0,0 +1,5 @@ +# 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/. + +somePreference_title=A
new file mode 100644 --- /dev/null +++ b/addon-sdk/source/test/addons/simple-prefs-l10n/main.js @@ -0,0 +1,57 @@ +/* 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 { Cu } = require('chrome'); +const sp = require('sdk/simple-prefs'); +const app = require('sdk/system/xul-app'); +const self = require('sdk/self'); +const tabs = require('sdk/tabs'); +const { preferencesBranch } = require('sdk/self'); + +const { AddonManager } = Cu.import('resource://gre/modules/AddonManager.jsm', {}); + +exports.testAOMLocalization = function(assert, done) { + tabs.open({ + url: 'about:addons', + onReady: function(tab) { + tab.attach({ + contentScriptWhen: 'end', + contentScript: 'function onLoad() {\n' + + 'unsafeWindow.removeEventListener("load", onLoad, false);\n' + + 'AddonManager.getAddonByID("' + self.id + '", function(aAddon) {\n' + + 'unsafeWindow.gViewController.viewObjects.detail.node.addEventListener("ViewChanged", function whenViewChanges() {\n' + + 'unsafeWindow.gViewController.viewObjects.detail.node.removeEventListener("ViewChanged", whenViewChanges, false);\n' + + 'setTimeout(function() {\n' + // TODO: figure out why this is necessary.. + 'self.postMessage({\n' + + 'somePreference: getAttributes(unsafeWindow.document.querySelector("setting[data-jetpack-id=\'' + self.id + '\']"))\n' + + '});\n' + + '}, 250);\n' + + '}, false);\n' + + 'unsafeWindow.gViewController.commands.cmd_showItemDetails.doCommand(aAddon, true);\n' + + '});\n' + + 'function getAttributes(ele) {\n' + + 'if (!ele) return {};\n' + + 'return {\n' + + 'title: ele.getAttribute("title")\n' + + '}\n' + + '}\n' + + '}\n' + + // Wait for the load event ? + 'if (document.readyState == "complete") {\n' + + 'onLoad()\n' + + '} else {\n' + + 'unsafeWindow.addEventListener("load", onLoad, false);\n' + + '}\n', + onMessage: function(msg) { + // test somePreference + assert.equal(msg.somePreference.title, 'A', 'somePreference title is correct'); + tab.close(done); + } + }); + } + }); +} + +require('sdk/test/runner').runTestsFromModule(module);
new file mode 100644 --- /dev/null +++ b/addon-sdk/source/test/addons/simple-prefs-l10n/package.json @@ -0,0 +1,10 @@ +{ + "id": "test-simple-prefs-l10n", + "preferences": [{ + "name": "somePreference", + "title": "some-title", + "description": "Some short description for the preference", + "type": "string", + "value": "TEST" + }] +} \ No newline at end of file
new file mode 100644 --- /dev/null +++ b/addon-sdk/source/test/fixtures/loader/globals/main.js @@ -0,0 +1,6 @@ +/* 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'; +exports.console = console;
--- a/addon-sdk/source/test/test-loader.js +++ b/addon-sdk/source/test/test-loader.js @@ -322,9 +322,25 @@ exports['test invisibleToDebugger: true' try { dbg.addDebuggee(sandbox); assert.fail('debugger added invisible value'); } catch(e) { assert.ok(true, 'debugger did not add invisible value'); } }; +exports['test console global by default'] = function (assert) { + let uri = root + '/fixtures/loader/globals/'; + let loader = Loader({ paths: { '': uri }}); + let program = main(loader, 'main'); + + assert.ok(typeof program.console === 'object', 'global `console` exists'); + assert.ok(typeof program.console.log === 'function', 'global `console.log` exists'); + + let loader2 = Loader({ paths: { '': uri }, globals: { console: fakeConsole }}); + let program2 = main(loader2, 'main'); + + assert.equal(program2.console, fakeConsole, + 'global console can be overridden with Loader options'); + function fakeConsole () {}; +}; + require('test').run(exports);
--- a/addon-sdk/source/test/test-panel.js +++ b/addon-sdk/source/test/test-panel.js @@ -960,16 +960,23 @@ exports['test emits on url changes'] = f panel.port.emit('hi', 'hi') panel.port.on('bye', function(uri) { assert.equal(uri, uriB, 'message was delivered to new uri'); loader.unload(); done(); }); }; +exports['test panel can be constructed without any arguments'] = function (assert) { + const { Panel } = require('sdk/panel'); + + let panel = Panel(); + assert.ok(true, "Creating a panel with no arguments does not throw"); +}; + if (isWindowPBSupported) { exports.testGetWindow = function(assert, done) { let activeWindow = getMostRecentBrowserWindow(); open(null, { features: { toolbar: true, chrome: true, private: true } }).then(function(window) {
--- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1136,16 +1136,17 @@ pref("devtools.profiler.enabled", true); pref("devtools.profiler.ui.show-platform-data", false); // Enable the Network Monitor pref("devtools.netmonitor.enabled", true); // The default Network Monitor UI settings pref("devtools.netmonitor.panes-network-details-width", 450); pref("devtools.netmonitor.panes-network-details-height", 450); +pref("devtools.netmonitor.statistics", true); // Enable the Tilt inspector pref("devtools.tilt.enabled", true); pref("devtools.tilt.intro_transition", true); pref("devtools.tilt.outro_transition", true); // Scratchpad settings // - recentFileMax: The maximum number of recently-opened files
--- a/browser/base/content/browser.xul +++ b/browser/base/content/browser.xul @@ -994,17 +994,17 @@ class="toolbarbutton-1 chromeclass-toolbar-additional" label="&syncToolbarButton.label;" oncommand="gSyncUI.handleToolbarButton()"/> #endif <toolbarbutton id="tabview-button" class="toolbarbutton-1 chromeclass-toolbar-additional" label="&tabGroupsButton.label;" command="Browser:ToggleTabView" - tooltiptext="&tabGroupsButton.tooltip;" + tooltip="dynamic-shortcut-tooltip" observes="tabviewGroupsNumber"/> </toolbarpalette> </toolbox> <hbox id="fullscr-toggler" collapsed="true"/> <deck id="content-deck" flex="1"> <hbox flex="1" id="browser">
--- a/browser/components/customizableui/src/CustomizableUI.jsm +++ b/browser/components/customizableui/src/CustomizableUI.jsm @@ -1309,23 +1309,39 @@ let CustomizableUIInternal = { } let isInteractive = this._isOnInteractiveElement(aEvent); LOG("maybeAutoHidePanel: interactive ? " + isInteractive); if (isInteractive) { return; } } - if (aEvent.originalTarget.getAttribute("closemenu") == "none" || - aEvent.originalTarget.getAttribute("widget-type") == "view") { + // We can't use event.target because we might have passed a panelview + // anonymous content boundary as well, and so target points to the + // panelmultiview in that case. Unfortunately, this means we get + // anonymous child nodes instead of the real ones, so looking for the + // 'stoooop, don't close me' attributes is more involved. + let target = aEvent.originalTarget; + let closemenu = "auto"; + let widgetType = "button"; + while (target.localName != "panel") { + closemenu = target.getAttribute("closemenu"); + widgetType = target.getAttribute("widget-type"); + if (closemenu == "none" || closemenu == "single" || + widgetType == "view") { + break; + } + target = target.parentNode; + } + if (closemenu == "none" || widgetType == "view") { return; } - if (aEvent.originalTarget.getAttribute("closemenu") == "single") { - let panel = this._getPanelForNode(aEvent.originalTarget); + if (closemenu == "single") { + let panel = this._getPanelForNode(target); let multiview = panel.querySelector("panelmultiview"); if (multiview.showingSubView) { multiview.showMainView(); return; } } // If we get here, we can actually hide the popup:
--- a/browser/components/customizableui/test/browser.ini +++ b/browser/components/customizableui/test/browser.ini @@ -20,20 +20,24 @@ skip-if = os == "mac" [browser_887438_currentset_shim.js] [browser_888817_currentset_updating.js] [browser_889120_customize_tab_merging.js] [browser_890140_orphaned_placeholders.js] [browser_890262_destroyWidget_after_add_to_panel.js] [browser_892955_isWidgetRemovable_for_removed_widgets.js] [browser_892956_destroyWidget_defaultPlacements.js] [browser_909779_overflow_toolbars_new_window.js] +skip-if = os == "linux" + [browser_901207_searchbar_in_panel.js] [browser_913972_currentset_overflow.js] +skip-if = os == "linux" [browser_914138_widget_API_overflowable_toolbar.js] +skip-if = os == "linux" [browser_914863_disabled_help_quit_buttons.js] [browser_918049_skipintoolbarset_dnd.js] [browser_923857_customize_mode_event_wrapping_during_reset.js] [browser_927717_customize_drag_empty_toolbar.js] [browser_932928_show_notice_when_palette_empty.js] [browser_934113_menubar_removable.js]
--- a/browser/devtools/debugger/debugger-panes.js +++ b/browser/devtools/debugger/debugger-panes.js @@ -1725,49 +1725,54 @@ let SourceUtils = { /** * Functions handling the variables bubble UI. */ function VariableBubbleView() { dumpn("VariableBubbleView was instantiated"); this._onMouseMove = this._onMouseMove.bind(this); this._onMouseLeave = this._onMouseLeave.bind(this); - this._onMouseScroll = this._onMouseScroll.bind(this); this._onPopupHiding = this._onPopupHiding.bind(this); } VariableBubbleView.prototype = { /** * Initialization function, called when the debugger is started. */ initialize: function() { dumpn("Initializing the VariableBubbleView"); - this._tooltip = new Tooltip(document); this._editorContainer = document.getElementById("editor"); - + this._editorContainer.addEventListener("mousemove", this._onMouseMove, false); + this._editorContainer.addEventListener("mouseleave", this._onMouseLeave, false); + + this._tooltip = new Tooltip(document, { + closeOnEvents: [{ + emitter: DebuggerController._toolbox, + event: "select" + }, { + emitter: this._editorContainer, + event: "scroll", + useCapture: true + }] + }); this._tooltip.defaultPosition = EDITOR_VARIABLE_POPUP_POSITION; this._tooltip.defaultShowDelay = EDITOR_VARIABLE_HOVER_DELAY; - this._tooltip.panel.addEventListener("popuphiding", this._onPopupHiding); - this._editorContainer.addEventListener("mousemove", this._onMouseMove, false); - this._editorContainer.addEventListener("mouseleave", this._onMouseLeave, false); - this._editorContainer.addEventListener("scroll", this._onMouseScroll, true); }, /** * Destruction function, called when the debugger is closed. */ destroy: function() { dumpn("Destroying the VariableBubbleView"); this._tooltip.panel.removeEventListener("popuphiding", this._onPopupHiding); this._editorContainer.removeEventListener("mousemove", this._onMouseMove, false); this._editorContainer.removeEventListener("mouseleave", this._onMouseLeave, false); - this._editorContainer.removeEventListener("scroll", this._onMouseScroll, true); }, /** * Searches for an identifier underneath the specified position in the * source editor, and if found, opens a VariablesView inspection popup. * * @param number x, y * The left/top coordinates where to look for an identifier. @@ -1903,17 +1908,17 @@ VariableBubbleView.prototype = { } }, [{ label: L10N.getStr("addWatchExpressionButton"), className: "dbg-expression-button", command: () => { DebuggerView.VariableBubble.hideContents(); DebuggerView.WatchExpressions.addExpression(evalPrefix, true); } - }]); + }], DebuggerController._toolbox); } this._tooltip.show(this._markedText.anchor); }, /** * Hides the inspection popup. */ @@ -1970,23 +1975,16 @@ VariableBubbleView.prototype = { /** * The mouseleave listener for the source editor container node. */ _onMouseLeave: function() { clearNamedTimeout("editor-mouse-move"); }, /** - * The mousescroll listener for the source editor container node. - */ - _onMouseScroll: function() { - this.hideContents(); - }, - - /** * Listener handling the popup hiding event. */ _onPopupHiding: function({ target }) { if (this._tooltip.panel != target) { return; } if (this._markedText) { this._markedText.clear();
--- a/browser/devtools/debugger/debugger-view.js +++ b/browser/devtools/debugger/debugger-view.js @@ -164,16 +164,20 @@ let DebuggerView = { searchEnabled: Prefs.variablesSearchboxVisible, eval: (variable, value) => { let string = variable.evaluationMacro(variable, value); DebuggerController.StackFrames.evaluate(string); }, lazyEmpty: true }); + // Attach the current toolbox to the VView so it can link DOMNodes to + // the inspector/highlighter + this.Variables.toolbox = DebuggerController._toolbox; + // Attach a controller that handles interfacing with the debugger protocol. VariablesViewController.attach(this.Variables, { getEnvironmentClient: aObject => gThreadClient.environment(aObject), getObjectClient: aObject => { return aObject instanceof DebuggerController.Tracer.WrappedObject ? DebuggerController.Tracer.syncGripClient(aObject.object) : gThreadClient.pauseGrip(aObject) }
--- a/browser/devtools/debugger/panel.js +++ b/browser/devtools/debugger/panel.js @@ -15,16 +15,17 @@ function DebuggerPanel(iframeWindow, too this.panelWin = iframeWindow; this._toolbox = toolbox; this._destroyer = null; this._view = this.panelWin.DebuggerView; this._controller = this.panelWin.DebuggerController; this._view._hostType = this._toolbox.hostType; this._controller._target = this.target; + this._controller._toolbox = this._toolbox; this.handleHostChanged = this.handleHostChanged.bind(this); this.highlightWhenPaused = this.highlightWhenPaused.bind(this); this.unhighlightWhenResumed = this.unhighlightWhenResumed.bind(this); EventEmitter.decorate(this); };
--- a/browser/devtools/debugger/test/browser.ini +++ b/browser/devtools/debugger/test/browser.ini @@ -31,16 +31,17 @@ support-files = doc_auto-pretty-print-01.html doc_auto-pretty-print-02.html doc_binary_search.html doc_blackboxing.html doc_closures.html doc_cmd-break.html doc_cmd-dbg.html doc_conditional-breakpoints.html + doc_domnode-variables.html doc_editor-mode.html doc_empty-tab-01.html doc_empty-tab-02.html doc_event-listeners.html doc_event-listeners-02.html doc_frame-parameters.html doc_function-display-name.html doc_function-search.html @@ -242,16 +243,17 @@ support-files = [browser_dbg_variables-view-popup-05.js] [browser_dbg_variables-view-popup-06.js] [browser_dbg_variables-view-popup-07.js] [browser_dbg_variables-view-popup-08.js] [browser_dbg_variables-view-popup-09.js] [browser_dbg_variables-view-popup-10.js] [browser_dbg_variables-view-popup-11.js] [browser_dbg_variables-view-popup-12.js] +[browser_dbg_variables-view-popup-13.js] [browser_dbg_variables-view-reexpand-01.js] [browser_dbg_variables-view-reexpand-02.js] [browser_dbg_variables-view-webidl.js] [browser_dbg_watch-expressions-01.js] [browser_dbg_watch-expressions-02.js] [browser_dbg_chrome-create.js] skip-if = os == "linux" # Bug 847558 [browser_dbg_on-pause-raise.js]
--- a/browser/devtools/debugger/test/browser_dbg_variables-view-frame-parameters-01.js +++ b/browser/devtools/debugger/test/browser_dbg_variables-view-frame-parameters-01.js @@ -95,17 +95,17 @@ function testExpandVariables() { ok(thisVar.get("window").target.querySelector(".value").className.contains("token-other"), "Should have the right token class for 'window'."); is(thisVar.get("document").target.querySelector(".name").getAttribute("value"), "document", "Should have the right property name for 'document'."); is(thisVar.get("document").target.querySelector(".value").getAttribute("value"), "HTMLDocument \u2192 doc_frame-parameters.html", "Should have the right property value for 'document'."); - ok(thisVar.get("document").target.querySelector(".value").className.contains("token-other"), + ok(thisVar.get("document").target.querySelector(".value").className.contains("token-domnode"), "Should have the right token class for 'document'."); let argsProps = argsVar.target.querySelectorAll(".variables-view-property"); is(argsProps.length, 8, "The 'arguments' variable should contain 5 enumerable and 3 non-enumerable properties"); is(argsProps[0].querySelector(".name").getAttribute("value"), "0", "Should have the right property name for '0'.");
new file mode 100644 --- /dev/null +++ b/browser/devtools/debugger/test/browser_dbg_variables-view-popup-13.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the variable inspection popup has inspector links for DOMNode + * properties and that the popup closes when the link is clicked + */ + +const TAB_URL = EXAMPLE_URL + "doc_domnode-variables.html"; + +function test() { + Task.spawn(function() { + let [tab, debuggee, panel] = yield initDebugger(TAB_URL); + let win = panel.panelWin; + let bubble = win.DebuggerView.VariableBubble; + let tooltip = bubble._tooltip.panel; + let toolbox = gDevTools.getToolbox(panel.target); + + function getDomNodeInTooltip(propertyName) { + let domNodeProperties = tooltip.querySelectorAll(".token-domnode"); + for (let prop of domNodeProperties) { + let propName = prop.parentNode.querySelector(".name"); + if (propName.getAttribute("value") === propertyName) { + ok(true, "DOMNode " + propertyName + " was found in the tooltip"); + return prop; + } + } + ok(false, "DOMNode " + propertyName + " wasn't found in the tooltip"); + } + + // Allow this generator function to yield first. + executeSoon(() => debuggee.start()); + yield waitForSourceAndCaretAndScopes(panel, ".html", 19); + + // Inspect the div DOM variable. + yield openVarPopup(panel, { line: 17, ch: 38 }, true); + let property = getDomNodeInTooltip("firstElementChild"); + + // Simulate mouseover on the property value + let highlighted = once(toolbox, "node-highlight"); + EventUtils.sendMouseEvent({ type: "mouseover" }, property, + property.ownerDocument.defaultView); + yield highlighted; + ok(true, "The node-highlight event was fired on hover of the DOMNode"); + + // Simulate a click on the "select in inspector" button + let button = property.parentNode.querySelector(".variables-view-open-inspector"); + ok(button, "The select-in-inspector button is present"); + let inspectorSelected = once(toolbox, "inspector-selected"); + EventUtils.sendMouseEvent({ type: "mousedown" }, button, + button.ownerDocument.defaultView); + yield inspectorSelected; + ok(true, "The inspector got selected when clicked on the select-in-inspector"); + + // Make sure the inspector's initialization is finalized before ending the test + // Listening to the event *after* triggering the switch to the inspector isn't + // a problem as the inspector is asynchronously loaded. + yield once(toolbox.getPanel("inspector"), "inspector-updated"); + + yield resumeDebuggerThenCloseAndFinish(panel); + }); +}
--- a/browser/devtools/debugger/test/browser_dbg_variables-view-webidl.js +++ b/browser/devtools/debugger/test/browser_dbg_variables-view-webidl.js @@ -59,32 +59,32 @@ function performTest() { let buttonVar = globalScope.get("button"); let buttonAsProtoVar = globalScope.get("buttonAsProto"); let documentVar = globalScope.get("document"); is(buttonVar.target.querySelector(".name").getAttribute("value"), "button", "Should have the right property name for 'button'."); is(buttonVar.target.querySelector(".value").getAttribute("value"), "<button>", "Should have the right property value for 'button'."); - ok(buttonVar.target.querySelector(".value").className.contains("token-other"), + ok(buttonVar.target.querySelector(".value").className.contains("token-domnode"), "Should have the right token class for 'button'."); is(buttonAsProtoVar.target.querySelector(".name").getAttribute("value"), "buttonAsProto", "Should have the right property name for 'buttonAsProto'."); is(buttonAsProtoVar.target.querySelector(".value").getAttribute("value"), "Object", "Should have the right property value for 'buttonAsProto'."); ok(buttonAsProtoVar.target.querySelector(".value").className.contains("token-other"), "Should have the right token class for 'buttonAsProto'."); is(documentVar.target.querySelector(".name").getAttribute("value"), "document", "Should have the right property name for 'document'."); is(documentVar.target.querySelector(".value").getAttribute("value"), "HTMLDocument \u2192 doc_frame-parameters.html", "Should have the right property value for 'document'."); - ok(documentVar.target.querySelector(".value").className.contains("token-other"), + ok(documentVar.target.querySelector(".value").className.contains("token-domnode"), "Should have the right token class for 'document'."); is(buttonVar.expanded, false, "The buttonVar should not be expanded at this point."); is(buttonAsProtoVar.expanded, false, "The buttonAsProtoVar should not be expanded at this point."); is(documentVar.expanded, false, "The documentVar should not be expanded at this point."); @@ -142,17 +142,17 @@ function performTest() { "Should have the right property value for '__proto__'."); ok(buttonProtoVar.target.querySelector(".value").className.contains("token-other"), "Should have the right token class for '__proto__'."); is(buttonAsProtoProtoVar.target.querySelector(".name").getAttribute("value"), "__proto__", "Should have the right property name for '__proto__'."); is(buttonAsProtoProtoVar.target.querySelector(".value").getAttribute("value"), "<button>", "Should have the right property value for '__proto__'."); - ok(buttonAsProtoProtoVar.target.querySelector(".value").className.contains("token-other"), + ok(buttonAsProtoProtoVar.target.querySelector(".value").className.contains("token-domnode"), "Should have the right token class for '__proto__'."); is(documentProtoVar.target.querySelector(".name").getAttribute("value"), "__proto__", "Should have the right property name for '__proto__'."); is(documentProtoVar.target.querySelector(".value").getAttribute("value"), "HTMLDocumentPrototype", "Should have the right property value for '__proto__'."); ok(documentProtoVar.target.querySelector(".value").className.contains("token-other"), "Should have the right token class for '__proto__'.");
new file mode 100644 --- /dev/null +++ b/browser/devtools/debugger/test/doc_domnode-variables.html @@ -0,0 +1,24 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Debugger test page</title> + </head> + + <body> + <div>Look at this DIV! Just look at it!</div> + + <script type="text/javascript"> + function start() { + var theDiv = document.querySelector("div"); + var theBody = document.body; + var manyDomNodes = [theDiv, theBody]; + debugger; + } + </script> + </body> + +</html>
--- a/browser/devtools/framework/test/browser_keybindings.js +++ b/browser/devtools/framework/test/browser_keybindings.js @@ -59,32 +59,16 @@ function test() gDevTools.once("toolbox-ready", (e, toolbox) => { inspectorShouldBeOpenAndHighlighting(toolbox.getCurrentPanel(), toolbox) }); keysetMap.inspector.synthesizeKey(); } - function moveMouseOver(aElement, aInspector, cb) - { - EventUtils.synthesizeMouse(aElement, 2, 2, {type: "mousemove"}, - aElement.ownerDocument.defaultView); - aInspector.toolbox.once("picker-node-hovered", () => { - executeSoon(cb); - }); - } - - function isHighlighting() - { - let outline = gBrowser.selectedBrowser.parentNode - .querySelector(".highlighter-container .highlighter-outline"); - return outline && !outline.hasAttribute("hidden"); - } - function inspectorShouldBeOpenAndHighlighting(aInspector, aToolbox) { is (aToolbox.currentToolId, "inspector", "Correct tool has been loaded"); aToolbox.once("picker-started", () => { ok(true, "picker-started event received, highlighter started"); keysetMap.inspector.synthesizeKey();
--- a/browser/devtools/framework/toolbox.js +++ b/browser/devtools/framework/toolbox.js @@ -68,17 +68,17 @@ function Toolbox(target, selectedTool, h this._toolPanels = new Map(); this._telemetry = new Telemetry(); this._toolRegistered = this._toolRegistered.bind(this); this._toolUnregistered = this._toolUnregistered.bind(this); this._refreshHostTitle = this._refreshHostTitle.bind(this); this._splitConsoleOnKeypress = this._splitConsoleOnKeypress.bind(this) this.destroy = this.destroy.bind(this); - this.stopPicker = this.stopPicker.bind(this); + this.highlighterUtils = new ToolboxHighlighterUtils(this); this._target.on("close", this.destroy); if (!hostType) { hostType = Services.prefs.getCharPref(this._prefs.LAST_HOST); } if (!selectedTool) { selectedTool = Services.prefs.getCharPref(this._prefs.LAST_TOOL); @@ -187,17 +187,17 @@ Toolbox.prototype = { return parseFloat(Services.prefs.getCharPref(ZOOM_PREF)); }, /** * Get the toolbox highlighter front. Note that it may not always have been * initialized first. Use `initInspector()` if needed. */ get highlighter() { - if (this.isRemoteHighlightable) { + if (this.highlighterUtils.isRemoteHighlightable) { return this._highlighter; } else { return null; } }, /** * Get the toolbox's inspector front. Note that it may not always have been @@ -552,18 +552,18 @@ Toolbox.prototype = { this._pickerButton = this.doc.createElement("toolbarbutton"); this._pickerButton.id = "command-button-pick"; this._pickerButton.className = "command-button"; this._pickerButton.setAttribute("tooltiptext", toolboxStrings("pickButton.tooltip")); let container = this.doc.querySelector("#toolbox-buttons"); container.appendChild(this._pickerButton); - this.togglePicker = this.togglePicker.bind(this); - this._pickerButton.addEventListener("command", this.togglePicker, false); + this._togglePicker = this.highlighterUtils.togglePicker.bind(this.highlighterUtils); + this._pickerButton.addEventListener("command", this._togglePicker, false); }, /** * Build a tab for one tool definition and add to the toolbox * * @param {string} toolDefinition * Tool definition of the tool to build a tab for. */ @@ -1031,17 +1031,17 @@ Toolbox.prototype = { initInspector: function() { let deferred = promise.defer(); if (!this._inspector) { this._inspector = InspectorFront(this._target.client, this._target.form); this._inspector.getWalker().then(walker => { this._walker = walker; this._selection = new Selection(this._walker); - if (this.isRemoteHighlightable) { + if (this.highlighterUtils.isRemoteHighlightable) { this._inspector.getHighlighter().then(highlighter => { this._highlighter = highlighter; deferred.resolve(); }); } else { deferred.resolve(); } }); @@ -1081,108 +1081,16 @@ Toolbox.prototype = { } else { deferred.resolve(); } return deferred.promise; }, /** - * Start/stop the element picker on the debuggee target. - */ - togglePicker: function() { - if (this._isPicking) { - return this.stopPicker(); - } else { - return this.startPicker(); - } - }, - - get isRemoteHighlightable() { - return this._target.client.traits.highlightable; - }, - - /** - * Start the element picker on the debuggee target. - * This will request the inspector actor to start listening for mouse/touch - * events on the target to highlight the hovered/picked element. - * Depending on the server-side capabilities, this may fire events when nodes - * are hovered. - * @return A promise that resolves when the picker has started - */ - startPicker: function() { - let deferred = promise.defer(); - - let done = () => { - this.emit("picker-started"); - this.on("select", this.stopPicker); - deferred.resolve(); - }; - - promise.all([ - this.initInspector(), - this.selectTool("inspector") - ]).then(() => { - this._isPicking = true; - this._pickerButton.setAttribute("checked", "true"); - - if (this.isRemoteHighlightable) { - this.highlighter.pick().then(done); - - this._onPickerNodeHovered = res => { - this.emit("picker-node-hovered", res.node); - }; - this.walker.on("picker-node-hovered", this._onPickerNodeHovered); - - this._onPickerNodePicked = res => { - this.selection.setNodeFront(res.node, "picker-node-picked"); - this.stopPicker(); - }; - this.walker.on("picker-node-picked", this._onPickerNodePicked); - } else { - this.walker.pick().then(node => { - this.selection.setNodeFront(node, "picker-node-picked"); - this.stopPicker(); - }); - done(); - } - }); - - return deferred.promise; - }, - - /** - * Stop the element picker - * @return A promise that resolves when the picker has stopped - */ - stopPicker: function() { - let deferred = promise.defer(); - - let done = () => { - this.emit("picker-stopped"); - this.off("select", this.stopPicker); - deferred.resolve(); - }; - - this.initInspector().then(() => { - this._isPicking = false; - this._pickerButton.removeAttribute("checked"); - if (this.isRemoteHighlightable) { - this.highlighter.cancelPick().then(done); - this.walker.off("picker-node-hovered", this._onPickerNodeHovered); - this.walker.off("picker-node-picked", this._onPickerNodePicked); - } else { - this.walker.cancelPick().then(done); - } - }); - - return deferred.promise; - }, - - /** * Get the toolbox's notification box * * @return The notification box element. */ getNotificationBox: function() { return this.doc.getElementById("toolbox-notificationbox"); }, @@ -1223,17 +1131,17 @@ Toolbox.prototype = { console.error("Panel " + id + ":", e); } } // Destroying the walker and inspector fronts outstanding.push(this.destroyInspector()); // Removing buttons - this._pickerButton.removeEventListener("command", this.togglePicker, false); + this._pickerButton.removeEventListener("command", this._togglePicker, false); this._pickerButton = null; let container = this.doc.getElementById("toolbox-buttons"); while (container.firstChild) { container.removeChild(container.firstChild); } outstanding.push(this.destroyHost()); @@ -1255,8 +1163,189 @@ Toolbox.prototype = { this.emit("destroyed"); // Free _host after the call to destroyed in order to let a chance // to destroyed listeners to still query toolbox attributes this._host = null; this._toolPanels.clear(); }).then(null, console.error); } }; + +/** + * The ToolboxHighlighterUtils is what you should use for anything related to + * node highlighting and picking. + * It encapsulates the logic to connecting to the HighlighterActor. + */ +function ToolboxHighlighterUtils(toolbox) { + this.toolbox = toolbox; + this._onPickerNodeHovered = this._onPickerNodeHovered.bind(this); + this._onPickerNodePicked = this._onPickerNodePicked.bind(this); + this.stopPicker = this.stopPicker.bind(this); +} + +ToolboxHighlighterUtils.prototype = { + /** + * Indicates whether the highlighter actor exists on the server. + */ + get isRemoteHighlightable() { + return this.toolbox._target.client.traits.highlightable; + }, + + /** + * Start/stop the element picker on the debuggee target. + */ + togglePicker: function() { + if (this._isPicking) { + return this.stopPicker(); + } else { + return this.startPicker(); + } + }, + + _onPickerNodeHovered: function(res) { + this.toolbox.emit("picker-node-hovered", res.node); + }, + + _onPickerNodePicked: function(res) { + this.toolbox.selection.setNodeFront(res.node, "picker-node-picked"); + this.stopPicker(); + }, + + /** + * Start the element picker on the debuggee target. + * This will request the inspector actor to start listening for mouse/touch + * events on the target to highlight the hovered/picked element. + * Depending on the server-side capabilities, this may fire events when nodes + * are hovered. + * @return A promise that resolves when the picker has started + */ + startPicker: function() { + let deferred = promise.defer(); + + let done = () => { + this.toolbox.emit("picker-started"); + this.toolbox.on("select", this.stopPicker); + deferred.resolve(); + }; + + promise.all([ + this.toolbox.initInspector(), + this.toolbox.selectTool("inspector") + ]).then(() => { + this._isPicking = true; + this.toolbox._pickerButton.setAttribute("checked", "true"); + + if (this.isRemoteHighlightable) { + this.toolbox.highlighter.pick().then(done); + + this.toolbox.walker.on("picker-node-hovered", this._onPickerNodeHovered); + this.toolbox.walker.on("picker-node-picked", this._onPickerNodePicked); + } else { + this.toolbox.walker.pick().then(node => { + this.toolbox.selection.setNodeFront(node, "picker-node-picked"); + this.stopPicker(); + }); + done(); + } + }); + + return deferred.promise; + }, + + /** + * Stop the element picker + * @return A promise that resolves when the picker has stopped + */ + stopPicker: function() { + let deferred = promise.defer(); + + let done = () => { + this.toolbox.emit("picker-stopped"); + this.toolbox.off("select", this.stopPicker); + deferred.resolve(); + }; + + this.toolbox.initInspector().then(() => { + this._isPicking = false; + this.toolbox._pickerButton.removeAttribute("checked"); + if (this.isRemoteHighlightable) { + this.toolbox.highlighter.cancelPick().then(done); + this.toolbox.walker.off("picker-node-hovered", this._onPickerNodeHovered); + this.toolbox.walker.off("picker-node-picked", this._onPickerNodePicked); + } else { + this.toolbox.walker.cancelPick().then(done); + } + }); + + return deferred.promise; + }, + + /** + * Show the box model highlighter on a node, given its NodeFront (this type + * of front is normally returned by the WalkerActor). + * @return a promise that resolves to the nodeFront when the node has been + * highlit + */ + highlightNodeFront: function(nodeFront, options={}) { + let deferred = promise.defer(); + + // If the remote highlighter exists on the target, use it + if (this.isRemoteHighlightable) { + this.toolbox.initInspector().then(() => { + this.toolbox.highlighter.showBoxModel(nodeFront, options).then(() => { + this.toolbox.emit("node-highlight", nodeFront); + deferred.resolve(nodeFront); + }); + }); + } + // Else, revert to the "older" version of the highlighter in the walker + // actor + else { + this.toolbox.walker.highlight(nodeFront).then(() => { + this.toolbox.emit("node-highlight", nodeFront); + deferred.resolve(nodeFront); + }); + } + + return deferred.promise; + }, + + /** + * This is a convenience method in case you don't have a nodeFront but a + * valueGrip. This is often the case with VariablesView properties. + * This method will simply translate the grip into a nodeFront and call + * highlightNodeFront + * @return a promise that resolves to the nodeFront when the node has been + * highlit + */ + highlightDomValueGrip: function(valueGrip, options={}) { + return this._translateGripToNodeFront(valueGrip).then(nodeFront => { + if (nodeFront) { + return this.highlightNodeFront(nodeFront, options); + } else { + return promise.reject(); + } + }); + }, + + _translateGripToNodeFront: function(grip) { + return this.toolbox.initInspector().then(() => { + return this.toolbox.walker.getNodeActorFromObjectActor(grip.actor); + }); + }, + + /** + * Hide the highlighter. + * @return a promise that resolves when the highlighter is hidden + */ + unhighlight: function() { + if (this.isRemoteHighlightable) { + // If the remote highlighter exists on the target, use it + return this.toolbox.initInspector().then(() => { + return this.toolbox.highlighter.hideBoxModel(); + }); + } else { + // If not, no need to unhighlight as the older highlight method uses a + // setTimeout to hide itself + return promise.resolve(); + } + } +};
--- a/browser/devtools/inspector/test/browser_inspector_basic_highlighter.js +++ b/browser/devtools/inspector/test/browser_inspector_basic_highlighter.js @@ -42,17 +42,17 @@ function test() { } function hoverH1InMarkupView() { let deferred = promise.defer(); let container = getContainerForRawNode(inspector.markup, doc.querySelector("h1")); EventUtils.synthesizeMouse(container.tagLine, 2, 2, {type: "mousemove"}, inspector.markup.doc.defaultView); - inspector.markup.once("node-highlight", deferred.resolve); + inspector.toolbox.once("node-highlight", deferred.resolve); return deferred.promise; } function assertH1Highlighted() { ok(isHighlighting(), "The highlighter is shown on a markup container hover"); is(getHighlitNode(), doc.querySelector("h1"), "The highlighter highlights the right node"); return promise.resolve();
--- a/browser/devtools/inspector/test/browser_inspector_bug_674871.js +++ b/browser/devtools/inspector/test/browser_inspector_bug_674871.js @@ -53,17 +53,17 @@ function test() getHighlighterOutline().setAttribute("disable-transitions", "true"); runTests(); }); }); } function runTests() { - inspector.toolbox.startPicker().then(() => { + inspector.toolbox.highlighterUtils.startPicker().then(() => { moveMouseOver(iframeNode, 1, 1, isTheIframeHighlighted); }); } function isTheIframeHighlighted() { let outlineRect = getHighlighterOutlineRect(); let iframeRect = iframeNode.getBoundingClientRect(); @@ -81,17 +81,17 @@ function test() function isTheIframeContentHighlighted() { is(getHighlitNode(), iframeBodyNode, "highlighter shows the right node"); // 184 == 200 + 11(border) + 13(padding) - 40(scroll) let outlineRect = getHighlighterOutlineRect(); is(outlineRect.height, 184, "highlighter height"); - inspector.toolbox.stopPicker().then(() => { + inspector.toolbox.highlighterUtils.stopPicker().then(() => { let target = TargetFactory.forTab(gBrowser.selectedTab); gDevTools.closeToolbox(target); finishUp(); }); } function finishUp() {
--- a/browser/devtools/inspector/test/browser_inspector_bug_699308_iframe_navigation.js +++ b/browser/devtools/inspector/test/browser_inspector_bug_699308_iframe_navigation.js @@ -10,17 +10,17 @@ function test() { function startTest() { openInspector(aInspector => { inspector = aInspector; runInspectorTests(); }); } function showHighlighter(cb) { - inspector.toolbox.startPicker().then(() => { + inspector.toolbox.highlighterUtils.startPicker().then(() => { EventUtils.synthesizeMouse(content.document.body, 1, 1, {type: "mousemove"}, content); inspector.toolbox.once("picker-node-hovered", () => { executeSoon(() => { getHighlighterOutline().setAttribute("disable-transitions", "true"); cb(); }); }); @@ -58,17 +58,17 @@ function test() { finishTest(); } function finishTest() { is(iframeLoads, 2, "iframe loads"); ok(checksAfterLoads, "the Inspector tests got the chance to run after iframe reloads"); - inspector.toolbox.stopPicker().then(() => { + inspector.toolbox.highlighterUtils.stopPicker().then(() => { iframe = null; gBrowser.removeCurrentTab(); executeSoon(finish); }); } waitForExplicitFinish();
--- a/browser/devtools/inspector/test/browser_inspector_bug_958169_switch_to_inspector_on_pick.js +++ b/browser/devtools/inspector/test/browser_inspector_bug_958169_switch_to_inspector_on_pick.js @@ -19,17 +19,17 @@ function test() { content.location = "data:text/html,<p>Switch to inspector on pick</p>"; } function startTests() { Task.spawn(function() { yield openToolbox(); yield startPickerAndAssertSwitchToInspector(); - yield toolbox.stopPicker(); + yield toolbox.highlighterUtils.stopPicker(); finishTests(); }).then(null, Cu.reportError); } function openToolbox() { let target = TargetFactory.forTab(gBrowser.selectedTab); return gDevTools.showToolbox(target, "webconsole").then(aToolbox => {
--- a/browser/devtools/inspector/test/browser_inspector_bug_958456_highlight_comments.js +++ b/browser/devtools/inspector/test/browser_inspector_bug_958456_highlight_comments.js @@ -82,17 +82,17 @@ function prepareHighlighter() { }); return deferred.promise; } function hoverContainer(container) { let deferred = promise.defer(); EventUtils.synthesizeMouse(container.tagLine, 2, 2, {type: "mousemove"}, markupView.doc.defaultView); - inspector.markup.once("node-highlight", deferred.resolve); + inspector.toolbox.once("node-highlight", deferred.resolve); return deferred.promise; } function hoverElement(selector) { info("Hovering node " + selector + " in the markup view"); let container = getContainerForRawNode(markupView, doc.querySelector(selector)); return hoverContainer(container); }
--- a/browser/devtools/inspector/test/browser_inspector_bug_961771_picker_stops_on_tool_select.js +++ b/browser/devtools/inspector/test/browser_inspector_bug_961771_picker_stops_on_tool_select.js @@ -28,17 +28,17 @@ function test() { inspector = aInspector; toolbox.once("picker-stopped", () => { ok(true, "picker-stopped event fired after switch tools, so picker is closed"); finishUp(); }); Task.spawn(function() { - yield toolbox.startPicker(); + yield toolbox.highlighterUtils.startPicker(); yield toolbox.selectNextTool(); }).then(null, Cu.reportError); }); } function finishUp() { inspector = doc = toolbox = null; gBrowser.removeCurrentTab();
--- a/browser/devtools/inspector/test/browser_inspector_highlighter.js +++ b/browser/devtools/inspector/test/browser_inspector_highlighter.js @@ -47,17 +47,17 @@ function createDocument() { doc.body.appendChild(div2); doc.body.appendChild(div3); openInspector(aInspector => { inspector = aInspector; inspector.selection.setNode(div, null); inspector.once("inspector-updated", () => { getHighlighterOutline().setAttribute("disable-transitions", "true"); - inspector.toolbox.startPicker().then(testMouseOverH1Highlights); + inspector.toolbox.highlighterUtils.startPicker().then(testMouseOverH1Highlights); }); }); } function testMouseOverH1Highlights() { inspector.toolbox.once("picker-node-hovered", () => { ok(isHighlighting(), "Highlighter is shown"); is(getHighlitNode(), h1, "Highlighter's outline correspond to the selected node"); @@ -104,17 +104,17 @@ function testOutlineDimensions() { is(outlineWidth, h1Width, "outline width matches dimensions of element (zoomed)"); is(outlineHeight, h1Height, "outline height matches dimensions of element (zoomed)"); executeSoon(finishUp); }, 500); } function finishUp() { - inspector.toolbox.stopPicker().then(() => { + inspector.toolbox.highlighterUtils.stopPicker().then(() => { doc = h1 = inspector = null; let target = TargetFactory.forTab(gBrowser.selectedTab); gDevTools.closeToolbox(target); gBrowser.removeCurrentTab(); finish(); }); }
--- a/browser/devtools/inspector/test/browser_inspector_iframeTest.js +++ b/browser/devtools/inspector/test/browser_inspector_iframeTest.js @@ -29,17 +29,17 @@ function createDocument() { iframe2.removeEventListener("load", arguments.callee, false); div2 = iframe2.contentDocument.createElement('div'); div2.textContent = 'nested div'; iframe2.contentDocument.body.appendChild(div2); // Open the inspector, start the picker mode, and start the tests openInspector(aInspector => { inspector = aInspector; - inspector.toolbox.startPicker().then(runTests); + inspector.toolbox.highlighterUtils.startPicker().then(runTests); }); }, false); iframe2.src = 'data:text/html,nested iframe'; iframe1.contentDocument.body.appendChild(iframe2); }, false); iframe1.src = 'data:text/html,little iframe'; @@ -70,33 +70,33 @@ function testDiv2Highlighter() { moveMouseOver(div2, () => { is(getHighlitNode(), div2, "highlighter matches selection"); selectRoot(); }); } function selectRoot() { // Select the root document element to clear the breadcrumbs. - inspector.selection.setNode(doc.documentElement); + inspector.selection.setNode(doc.documentElement, null); inspector.once("inspector-updated", selectIframe); } function selectIframe() { // Directly select an element in an iframe (without navigating to it // with mousemoves). - inspector.selection.setNode(div2); + inspector.selection.setNode(div2, null); inspector.once("inspector-updated", () => { let breadcrumbs = inspector.breadcrumbs; is(breadcrumbs.nodeHierarchy.length, 9, "Should have 9 items"); finishUp(); }); } function finishUp() { - inspector.toolbox.stopPicker().then(() => { + inspector.toolbox.highlighterUtils.stopPicker().then(() => { doc = div1 = div2 = iframe1 = iframe2 = inspector = null; let target = TargetFactory.forTab(gBrowser.selectedTab); gDevTools.closeToolbox(target); gBrowser.removeCurrentTab(); finish(); }); }
--- a/browser/devtools/main.js +++ b/browser/devtools/main.js @@ -104,17 +104,17 @@ Tools.inspector = { icon: "chrome://browser/skin/devtools/tool-inspector.svg", url: "chrome://browser/content/devtools/inspector/inspector.xul", label: l10n("inspector.label", inspectorStrings), tooltip: l10n("inspector.tooltip", inspectorStrings), inMenu: true, preventClosingOnKey: true, onkey: function(panel) { - panel.toolbox.togglePicker(); + panel.toolbox.highlighterUtils.togglePicker(); }, isTargetSupported: function(target) { return true; }, build: function(iframeWindow, toolbox) { let panel = new InspectorPanel(iframeWindow, toolbox);
--- a/browser/devtools/markupview/markup-view.js +++ b/browser/devtools/markupview/markup-view.js @@ -49,20 +49,16 @@ loader.lazyGetter(this, "AutocompletePop /** * The markup tree. Manages the mapping of nodes to MarkupContainers, * updating based on mutations, and the undo/redo bindings. * * @param Inspector aInspector * The inspector we're watching. * @param iframe aFrame * An iframe in which the caller has kindly loaded markup-view.xhtml. - * - * Fires the following events: - * - node-highlight: When a node in the markup-view is hovered and the - * corresponding node in the content gets highlighted */ function MarkupView(aInspector, aFrame, aControllerWindow) { this._inspector = aInspector; this.walker = this._inspector.walker; this._frame = aFrame; this.doc = this._frame.contentDocument; this._elt = this.doc.querySelector("#root"); this.htmlEditor = new HTMLEditor(this.doc); @@ -169,51 +165,21 @@ MarkupView.prototype = { } }, _onMouseLeave: function() { this._hideBoxModel(); }, _showBoxModel: function(nodeFront, options={}) { - let toolbox = this._inspector.toolbox; - - // If the remote highlighter exists on the target, use it - if (toolbox.isRemoteHighlightable) { - toolbox.initInspector().then(() => { - toolbox.highlighter.showBoxModel(nodeFront, options).then(() => { - this.emit("node-highlight", nodeFront); - }); - }); - } - // Else, revert to the "older" version of the highlighter in the walker - // actor - else { - this.walker.highlight(nodeFront).then(() => { - this.emit("node-highlight", nodeFront); - }); - } + this._inspector.toolbox.highlighterUtils.highlightNodeFront(nodeFront, options); }, _hideBoxModel: function() { - let deferred = promise.defer(); - let toolbox = this._inspector.toolbox; - - // If the remote highlighter exists on the target, use it - if (toolbox.isRemoteHighlightable) { - toolbox.initInspector().then(() => { - toolbox.highlighter.hideBoxModel().then(deferred.resolve); - }); - } else { - deferred.resolve(); - } - // If not, no need to unhighlight as the older highlight method uses a - // setTimeout to hide itself - - return deferred.promise; + this._inspector.toolbox.highlighterUtils.unhighlight(); }, _briefBoxModelTimer: null, _brieflyShowBoxModel: function(nodeFront, options) { let win = this._frame.contentWindow; if (this._briefBoxModelTimer) { win.clearTimeout(this._briefBoxModelTimer);
--- a/browser/devtools/netmonitor/netmonitor-controller.js +++ b/browser/devtools/netmonitor/netmonitor-controller.js @@ -52,45 +52,74 @@ const EVENTS = { RECEIVED_RESPONSE_CONTENT: "NetMonitor:NetworkEventUpdated:ResponseContent", // When the request post params are displayed in the UI. REQUEST_POST_PARAMS_DISPLAYED: "NetMonitor:RequestPostParamsAvailable", // When the response body is displayed in the UI. RESPONSE_BODY_DISPLAYED: "NetMonitor:ResponseBodyAvailable", - // When `onTabSelect` is fired and subsequently rendered + // When `onTabSelect` is fired and subsequently rendered. TAB_UPDATED: "NetMonitor:TabUpdated", - // Fired when Sidebar is finished being populated + // Fired when Sidebar has finished being populated. SIDEBAR_POPULATED: "NetMonitor:SidebarPopulated", - // Fired when NetworkDetailsView is finished being populated + // Fired when NetworkDetailsView has finished being populated. NETWORKDETAILSVIEW_POPULATED: "NetMonitor:NetworkDetailsViewPopulated", - // Fired when NetworkDetailsView is finished being populated - CUSTOMREQUESTVIEW_POPULATED: "NetMonitor:CustomRequestViewPopulated" + // Fired when CustomRequestView has finished being populated. + CUSTOMREQUESTVIEW_POPULATED: "NetMonitor:CustomRequestViewPopulated", + + // Fired when charts have been displayed in the PerformanceStatisticsView. + PLACEHOLDER_CHARTS_DISPLAYED: "NetMonitor:PlaceholderChartsDisplayed", + PRIMED_CACHE_CHART_DISPLAYED: "NetMonitor:PrimedChartsDisplayed", + EMPTY_CACHE_CHART_DISPLAYED: "NetMonitor:EmptyChartsDisplayed" +}; + +// Descriptions for what this frontend is currently doing. +const ACTIVITY_TYPE = { + // Standing by and handling requests normally. + NONE: 0, + + // Forcing the target to reload with cache enabled or disabled. + RELOAD: { + WITH_CACHE_ENABLED: 1, + WITH_CACHE_DISABLED: 2 + }, + + // Enabling or disabling the cache without triggering a reload. + ENABLE_CACHE: 3, + DISABLE_CACHE: 4 }; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Task.jsm"); -Cu.import("resource:///modules/devtools/shared/event-emitter.js"); Cu.import("resource:///modules/devtools/SideMenuWidget.jsm"); Cu.import("resource:///modules/devtools/VariablesView.jsm"); Cu.import("resource:///modules/devtools/VariablesViewController.jsm"); Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); -const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {}); const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require; +const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise; +const EventEmitter = require("devtools/shared/event-emitter"); const Editor = require("devtools/sourceeditor/editor"); +XPCOMUtils.defineLazyModuleGetter(this, "Chart", + "resource:///modules/devtools/Chart.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); + XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "DevToolsUtils", + "resource://gre/modules/devtools/DevToolsUtils.jsm"); + XPCOMUtils.defineLazyModuleGetter(this, "devtools", "resource://gre/modules/devtools/Loader.jsm"); Object.defineProperty(this, "NetworkHelper", { get: function() { return devtools.require("devtools/toolkit/webconsole/network-helper"); }, configurable: true, @@ -250,19 +279,91 @@ let NetMonitorController = { if (aCallback) { aCallback(); } }); }); }, + /** + * Gets the activity currently performed by the frontend. + * @return number + */ + getCurrentActivity: function() { + return this._currentActivity || ACTIVITY_TYPE.NONE; + }, + + /** + * Triggers a specific "activity" to be performed by the frontend. This can be, + * for example, triggering reloads or enabling/disabling cache. + * + * @param number aType + * The activity type. See the ACTIVITY_TYPE const. + * @return object + * A promise resolved once the activity finishes and the frontend + * is back into "standby" mode. + */ + triggerActivity: function(aType) { + // Puts the frontend into "standby" (when there's no particular activity). + let standBy = () => { + this._currentActivity = ACTIVITY_TYPE.NONE; + }; + + // Waits for a series of "navigation start" and "navigation stop" events. + let waitForNavigation = () => { + let deferred = promise.defer(); + this._target.once("will-navigate", () => { + this._target.once("navigate", () => { + deferred.resolve(); + }); + }); + return deferred.promise; + }; + + // Reconfigures the tab, optionally triggering a reload. + let reconfigureTab = aOptions => { + let deferred = promise.defer(); + this._target.activeTab.reconfigure(aOptions, deferred.resolve); + return deferred.promise; + }; + + // Reconfigures the tab and waits for the target to finish navigating. + let reconfigureTabAndWaitForNavigation = aOptions => { + aOptions.performReload = true; + let navigationFinished = waitForNavigation(); + return reconfigureTab(aOptions).then(() => navigationFinished); + } + + if (aType == ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED) { + this._currentActivity = ACTIVITY_TYPE.ENABLE_CACHE; + this._target.once("will-navigate", () => this._currentActivity = aType); + return reconfigureTabAndWaitForNavigation({ cacheEnabled: true }).then(standBy); + } + if (aType == ACTIVITY_TYPE.RELOAD.WITH_CACHE_DISABLED) { + this._currentActivity = ACTIVITY_TYPE.DISABLE_CACHE; + this._target.once("will-navigate", () => this._currentActivity = aType); + return reconfigureTabAndWaitForNavigation({ cacheEnabled: false }).then(standBy); + } + if (aType == ACTIVITY_TYPE.ENABLE_CACHE) { + this._currentActivity = aType; + return reconfigureTab({ cacheEnabled: true, performReload: false }).then(standBy); + } + if (aType == ACTIVITY_TYPE.DISABLE_CACHE) { + this._currentActivity = aType; + return reconfigureTab({ cacheEnabled: false, performReload: false }).then(standBy); + } + this._currentActivity = ACTIVITY_TYPE.NONE; + return promise.reject(new Error("Invalid activity type")); + }, + _startup: null, _shutdown: null, _connection: null, + _currentActivity: null, client: null, tabClient: null, webConsoleClient: null }; /** * Functions handling target-related lifetime events. */ @@ -309,16 +410,21 @@ TargetEventsHandler.prototype = { _onTabNavigated: function(aType, aPacket) { switch (aType) { case "will-navigate": { // Reset UI. NetMonitorView.RequestsMenu.reset(); NetMonitorView.Sidebar.reset(); NetMonitorView.NetworkDetails.reset(); + // Switch to the default network traffic inspector view. + if (NetMonitorController.getCurrentActivity() == ACTIVITY_TYPE.NONE) { + NetMonitorView.showNetworkInspectorView(); + } + window.emit(EVENTS.TARGET_WILL_NAVIGATE); break; } case "navigate": { window.emit(EVENTS.TARGET_DID_NAVIGATE); break; } } @@ -378,17 +484,16 @@ NetworkEventsHandler.prototype = { * @param string aType * Message type. * @param object aPacket * The message received from the server. */ _onNetworkEvent: function(aType, aPacket) { let { actor, startedDateTime, method, url, isXHR } = aPacket.eventActor; NetMonitorView.RequestsMenu.addRequest(actor, startedDateTime, method, url, isXHR); - window.emit(EVENTS.NETWORK_EVENT); }, /** * The "networkEventUpdate" message type handler. * * @param string aType * Message type. @@ -580,17 +685,18 @@ NetworkEventsHandler.prototype = { */ let L10N = new ViewHelpers.L10N(NET_STRINGS_URI); /** * Shortcuts for accessing various network monitor preferences. */ let Prefs = new ViewHelpers.Prefs("devtools.netmonitor", { networkDetailsWidth: ["Int", "panes-network-details-width"], - networkDetailsHeight: ["Int", "panes-network-details-height"] + networkDetailsHeight: ["Int", "panes-network-details-height"], + statistics: ["Bool", "statistics"] }); /** * Returns true if this is document is in RTL mode. * @return boolean */ XPCOMUtils.defineLazyGetter(window, "isRTL", function() { return window.getComputedStyle(document.documentElement, null).direction == "rtl"; @@ -612,16 +718,49 @@ NetMonitorController.NetworkEventsHandle */ Object.defineProperties(window, { "gNetwork": { get: function() NetMonitorController.NetworkEventsHandler } }); /** + * Makes sure certain properties are available on all objects in a data store. + * + * @param array aDataStore + * A list of objects for which to check the availability of properties. + * @param array aMandatoryFields + * A list of strings representing properties of objects in aDataStore. + * @return object + * A promise resolved when all objects in aDataStore contain the + * properties defined in aMandatoryFields. + */ +function whenDataAvailable(aDataStore, aMandatoryFields) { + let deferred = promise.defer(); + + let interval = setInterval(() => { + if (aDataStore.every(item => aMandatoryFields.every(field => field in item))) { + clearInterval(interval); + clearTimeout(timer); + deferred.resolve(); + } + }, WDA_DEFAULT_VERIFY_INTERVAL); + + let timer = setTimeout(() => { + clearInterval(interval); + deferred.reject(new Error("Timed out while waiting for data")); + }, WDA_DEFAULT_GIVE_UP_TIMEOUT); + + return deferred.promise; +}; + +const WDA_DEFAULT_VERIFY_INTERVAL = 50; // ms +const WDA_DEFAULT_GIVE_UP_TIMEOUT = 2000; // ms + +/** * Helper method for debugging. * @param string */ function dumpn(str) { if (wantLogging) { dump("NET-FRONTEND: " + str + "\n"); } }
--- a/browser/devtools/netmonitor/netmonitor-view.js +++ b/browser/devtools/netmonitor/netmonitor-view.js @@ -16,16 +16,17 @@ const REQUESTS_WATERFALL_HEADER_TICKS_MU const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60; // px const REQUESTS_WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5; // ms const REQUESTS_WATERFALL_BACKGROUND_TICKS_SCALES = 3; const REQUESTS_WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10; // px const REQUESTS_WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144]; const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32; // byte const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32; // byte const DEFAULT_HTTP_VERSION = "HTTP/1.1"; +const REQUEST_TIME_DECIMALS = 2; const HEADERS_SIZE_DECIMALS = 3; const CONTENT_SIZE_DECIMALS = 2; const CONTENT_MIME_TYPE_ABBREVIATIONS = { "ecmascript": "js", "javascript": "js", "x-javascript": "js" }; const CONTENT_MIME_TYPE_MAPPINGS = { @@ -52,16 +53,17 @@ const GENERIC_VARIABLES_VIEW_SETTINGS = searchEnabled: true, editableValueTooltip: "", editableNameTooltip: "", preventDisableOnChange: true, preventDescriptorModifiers: true, eval: () => {}, switch: () => {} }; +const NETWORK_ANALYSIS_PIE_CHART_DIAMETER = 200; // px /** * Object defining the network monitor view components. */ let NetMonitorView = { /** * Initializes the network monitor view. */ @@ -97,16 +99,24 @@ let NetMonitorView = { this._detailsPaneToggleButton = $("#details-pane-toggle"); this._collapsePaneString = L10N.getStr("collapseDetailsPane"); this._expandPaneString = L10N.getStr("expandDetailsPane"); this._detailsPane.setAttribute("width", Prefs.networkDetailsWidth); this._detailsPane.setAttribute("height", Prefs.networkDetailsHeight); this.toggleDetailsPane({ visible: false }); + + // Disable the performance statistics mode. + if (!Prefs.statistics) { + $("#request-menu-context-perf").hidden = true; + $("#notice-perf-message").hidden = true; + $("#requests-menu-network-summary-button").hidden = true; + $("#requests-menu-network-summary-label").hidden = true; + } }, /** * Destroys the UI for all the displayed panes. */ _destroyPanes: function() { dumpn("Destroying the NetMonitorView panes"); @@ -116,18 +126,19 @@ let NetMonitorView = { this._detailsPane = null; this._detailsPaneToggleButton = null; }, /** * Gets the visibility state of the network details pane. * @return boolean */ - get detailsPaneHidden() - this._detailsPane.hasAttribute("pane-collapsed"), + get detailsPaneHidden() { + return this._detailsPane.hasAttribute("pane-collapsed"); + }, /** * Sets the network details pane hidden or visible. * * @param object aFlags * An object containing some of the following properties: * - visible: true if the pane should be shown, false to hide * - animated: true to display an animation on toggle @@ -153,16 +164,76 @@ let NetMonitorView = { } if (aTabIndex !== undefined) { $("#event-details-pane").selectedIndex = aTabIndex; } }, /** + * Gets the current mode for this tool. + * @return string (e.g, "network-inspector-view" or "network-statistics-view") + */ + get currentFrontendMode() { + return this._body.selectedPanel.id; + }, + + /** + * Toggles between the frontend view modes ("Inspector" vs. "Statistics"). + */ + toggleFrontendMode: function() { + if (this.currentFrontendMode != "network-inspector-view") { + this.showNetworkInspectorView(); + } else { + this.showNetworkStatisticsView(); + } + }, + + /** + * Switches to the "Inspector" frontend view mode. + */ + showNetworkInspectorView: function() { + this._body.selectedPanel = $("#network-inspector-view"); + this.RequestsMenu._flushWaterfallViews(true); + }, + + /** + * Switches to the "Statistics" frontend view mode. + */ + showNetworkStatisticsView: function() { + this._body.selectedPanel = $("#network-statistics-view"); + + let controller = NetMonitorController; + let requestsView = this.RequestsMenu; + let statisticsView = this.PerformanceStatistics; + + Task.spawn(function() { + statisticsView.displayPlaceholderCharts(); + yield controller.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED); + + try { + // • The response headers and status code are required for determining + // whether a response is "fresh" (cacheable). + // • The response content size and request total time are necessary for + // populating the statistics view. + // • The response mime type is used for categorization. + yield whenDataAvailable(requestsView.attachments, [ + "responseHeaders", "status", "contentSize", "mimeType", "totalTime" + ]); + } catch (ex) { + // Timed out while waiting for data. Continue with what we have. + DevToolsUtils.reportException("showNetworkStatisticsView", ex); + } + + statisticsView.createPrimedCacheChart(requestsView.items); + statisticsView.createEmptyCacheChart(requestsView.items); + }); + }, + + /** * Lazily initializes and returns a promise for a Editor instance. * * @param string aId * The id of the editor placeholder node. * @return object * A promise that is resolved when the editor is available. */ editor: function(aId) { @@ -258,46 +329,55 @@ function RequestsMenuView() { RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { /** * Initialization function, called when the network monitor is started. */ initialize: function() { dumpn("Initializing the RequestsMenuView"); this.widget = new SideMenuWidget($("#requests-menu-contents")); - this._splitter = $('#splitter'); - this._summary = $("#request-menu-network-summary"); + this._splitter = $("#network-inspector-view-splitter"); + this._summary = $("#requests-menu-network-summary-label"); + this._summary.setAttribute("value", L10N.getStr("networkMenu.empty")); this.allowFocusOnRightClick = true; this.widget.maintainSelectionVisible = false; this.widget.autoscrollWithAppendedItems = true; this.widget.addEventListener("select", this._onSelect, false); this._splitter.addEventListener("mousemove", this._onResize, false); window.addEventListener("resize", this._onResize, false); this.requestsMenuSortEvent = getKeyWithEvent(this.sortBy.bind(this)); this.requestsMenuFilterEvent = getKeyWithEvent(this.filterOn.bind(this)); - this.clearEvent = this.clear.bind(this); + this.reqeustsMenuClearEvent = this.clear.bind(this); this._onContextShowing = this._onContextShowing.bind(this); this._onContextNewTabCommand = this.openRequestInTab.bind(this); this._onContextCopyUrlCommand = this.copyUrl.bind(this); + this._onContextCopyImageAsDataUriCommand = this.copyImageAsDataUri.bind(this); this._onContextResendCommand = this.cloneSelectedRequest.bind(this); + this._onContextPerfCommand = () => NetMonitorView.toggleFrontendMode(); this.sendCustomRequestEvent = this.sendCustomRequest.bind(this); this.closeCustomRequestEvent = this.closeCustomRequest.bind(this); this.cloneSelectedRequestEvent = this.cloneSelectedRequest.bind(this); $("#toolbar-labels").addEventListener("click", this.requestsMenuSortEvent, false); $("#requests-menu-footer").addEventListener("click", this.requestsMenuFilterEvent, false); - $("#requests-menu-clear-button").addEventListener("click", this.clearEvent, false); + $("#requests-menu-clear-button").addEventListener("click", this.reqeustsMenuClearEvent, false); $("#network-request-popup").addEventListener("popupshowing", this._onContextShowing, false); $("#request-menu-context-newtab").addEventListener("command", this._onContextNewTabCommand, false); $("#request-menu-context-copy-url").addEventListener("command", this._onContextCopyUrlCommand, false); + $("#request-menu-context-copy-image-as-data-uri").addEventListener("command", this._onContextCopyImageAsDataUriCommand, false); $("#request-menu-context-resend").addEventListener("command", this._onContextResendCommand, false); + $("#request-menu-context-perf").addEventListener("command", this._onContextPerfCommand, false); + + $("#requests-menu-perf-notice-button").addEventListener("command", this._onContextPerfCommand, false); + $("#requests-menu-network-summary-button").addEventListener("command", this._onContextPerfCommand, false); + $("#requests-menu-network-summary-label").addEventListener("click", this._onContextPerfCommand, false); $("#custom-request-send-button").addEventListener("click", this.sendCustomRequestEvent, false); $("#custom-request-close-button").addEventListener("click", this.closeCustomRequestEvent, false); $("#headers-summary-resend").addEventListener("click", this.cloneSelectedRequestEvent, false); }, /** * Destruction function, called when the network monitor is closed. @@ -306,32 +386,39 @@ RequestsMenuView.prototype = Heritage.ex dumpn("Destroying the SourcesView"); this.widget.removeEventListener("select", this._onSelect, false); this._splitter.removeEventListener("mousemove", this._onResize, false); window.removeEventListener("resize", this._onResize, false); $("#toolbar-labels").removeEventListener("click", this.requestsMenuSortEvent, false); $("#requests-menu-footer").removeEventListener("click", this.requestsMenuFilterEvent, false); - $("#requests-menu-clear-button").removeEventListener("click", this.clearEvent, false); + $("#requests-menu-clear-button").removeEventListener("click", this.reqeustsMenuClearEvent, false); $("#network-request-popup").removeEventListener("popupshowing", this._onContextShowing, false); $("#request-menu-context-newtab").removeEventListener("command", this._onContextNewTabCommand, false); $("#request-menu-context-copy-url").removeEventListener("command", this._onContextCopyUrlCommand, false); + $("#request-menu-context-copy-image-as-data-uri").removeEventListener("command", this._onContextCopyImageAsDataUriCommand, false); $("#request-menu-context-resend").removeEventListener("command", this._onContextResendCommand, false); + $("#request-menu-context-perf").removeEventListener("command", this._onContextPerfCommand, false); + + $("#requests-menu-perf-notice-button").removeEventListener("command", this._onContextPerfCommand, false); + $("#requests-menu-network-summary-button").removeEventListener("command", this._onContextPerfCommand, false); + $("#requests-menu-network-summary-label").removeEventListener("click", this._onContextPerfCommand, false); $("#custom-request-send-button").removeEventListener("click", this.sendCustomRequestEvent, false); $("#custom-request-close-button").removeEventListener("click", this.closeCustomRequestEvent, false); $("#headers-summary-resend").removeEventListener("click", this.cloneSelectedRequestEvent, false); }, /** * Resets this container (removes all the networking information). */ reset: function() { this.empty(); + this.filterOn("all"); this._firstRequestStartedMillis = -1; this._lastRequestEndedMillis = -1; }, /** * Specifies if this view may be updated lazily. */ lazyUpdate: true, @@ -389,16 +476,17 @@ RequestsMenuView.prototype = Heritage.ex * the currently selected request. */ cloneSelectedRequest: function() { let selected = this.selectedItem.attachment; // Create the element node for the network request item. let menuView = this._createMenuView(selected.method, selected.url); + // Append a network request item to this container. let newItem = this.push([menuView], { attachment: Object.create(selected, { isCustom: { value: true } }) }); // Immediately switch to new request pane. this.selectedItem = newItem; @@ -417,16 +505,28 @@ RequestsMenuView.prototype = Heritage.ex * Copy the request url from the currently selected item. */ copyUrl: function() { let selected = this.selectedItem.attachment; clipboardHelper.copyString(selected.url, document); }, /** + * Copy image as data uri. + */ + copyImageAsDataUri: function() { + let selected = this.selectedItem.attachment; + let { mimeType, text, encoding } = selected.responseContent.content; + gNetwork.getString(text).then(aString => { + let data = "data:" + mimeType + ";" + encoding + "," + aString; + clipboardHelper.copyString(data, document); + }); + }, + + /** * Send a new HTTP request using the data in the custom request form. */ sendCustomRequest: function() { let selected = this.selectedItem.attachment; let data = Object.create(selected, { headers: { value: selected.requestHeaders.headers } }); @@ -452,17 +552,17 @@ RequestsMenuView.prototype = Heritage.ex NetMonitorView.Sidebar.toggle(false); }, /** * Filters all network requests in this container by a specified type. * * @param string aType * Either "all", "html", "css", "js", "xhr", "fonts", "images", "media" - * or "flash". + * "flash" or "other". */ filterOn: function(aType = "all") { let target = $("#requests-menu-filter-" + aType + "-button"); let buttons = document.querySelectorAll(".requests-menu-footer-button"); for (let button of buttons) { if (button != target) { button.removeAttribute("checked"); @@ -472,38 +572,41 @@ RequestsMenuView.prototype = Heritage.ex } // Filter on whatever was requested. switch (aType) { case "all": this.filterContents(() => true); break; case "html": - this.filterContents(this._onHtml); + this.filterContents(e => this.isHtml(e)); break; case "css": - this.filterContents(this._onCss); + this.filterContents(e => this.isCss(e)); break; case "js": - this.filterContents(this._onJs); + this.filterContents(e => this.isJs(e)); break; case "xhr": - this.filterContents(this._onXhr); + this.filterContents(e => this.isXHR(e)); break; case "fonts": - this.filterContents(this._onFonts); + this.filterContents(e => this.isFont(e)); break; case "images": - this.filterContents(this._onImages); + this.filterContents(e => this.isImage(e)); break; case "media": - this.filterContents(this._onMedia); + this.filterContents(e => this.isMedia(e)); break; case "flash": - this.filterContents(this._onFlash); + this.filterContents(e => this.isFlash(e)); + break; + case "other": + this.filterContents(e => this.isOther(e)); break; } this.refreshSummary(); this.refreshZebra(); }, /** @@ -606,56 +709,60 @@ RequestsMenuView.prototype = Heritage.ex /** * Predicates used when filtering items. * * @param object aItem * The filtered item. * @return boolean * True if the item should be visible, false otherwise. */ - _onHtml: function({ attachment: { mimeType } }) + isHtml: function({ attachment: { mimeType } }) mimeType && mimeType.contains("/html"), - _onCss: function({ attachment: { mimeType } }) + isCss: function({ attachment: { mimeType } }) mimeType && mimeType.contains("/css"), - _onJs: function({ attachment: { mimeType } }) + isJs: function({ attachment: { mimeType } }) mimeType && ( mimeType.contains("/ecmascript") || mimeType.contains("/javascript") || mimeType.contains("/x-javascript")), - _onXhr: function({ attachment: { isXHR } }) + isXHR: function({ attachment: { isXHR } }) isXHR, - _onFonts: function({ attachment: { url, mimeType } }) // Fonts are a mess. + isFont: function({ attachment: { url, mimeType } }) // Fonts are a mess. (mimeType && ( mimeType.contains("font/") || mimeType.contains("/font"))) || url.contains(".eot") || url.contains(".ttf") || url.contains(".otf") || url.contains(".woff"), - _onImages: function({ attachment: { mimeType } }) + isImage: function({ attachment: { mimeType } }) mimeType && mimeType.contains("image/"), - _onMedia: function({ attachment: { mimeType } }) // Not including images. + isMedia: function({ attachment: { mimeType } }) // Not including images. mimeType && ( mimeType.contains("audio/") || mimeType.contains("video/") || mimeType.contains("model/")), - _onFlash: function({ attachment: { url, mimeType } }) // Flash is a mess. + isFlash: function({ attachment: { url, mimeType } }) // Flash is a mess. (mimeType && ( mimeType.contains("/x-flv") || mimeType.contains("/x-shockwave-flash"))) || url.contains(".swf") || url.contains(".flv"), + isOther: function(e) + !this.isHtml(e) && !this.isCss(e) && !this.isJs(e) && !this.isXHR(e) && + !this.isFont(e) && !this.isImage(e) && !this.isMedia(e) && !this.isFlash(e), + /** * Predicates used when sorting items. * * @param object aFirst * The first item used in the comparison. * @param object aSecond * The second item used in the comparison. * @return number @@ -719,18 +826,18 @@ RequestsMenuView.prototype = Heritage.ex let totalMillis = this._getNewestRequest(visibleItems).attachment.endedMillis - this._getOldestRequest(visibleItems).attachment.startedMillis; // https://developer.mozilla.org/en-US/docs/Localization_and_Plurals let str = PluralForm.get(visibleRequestsCount, L10N.getStr("networkMenu.summary")); this._summary.setAttribute("value", str .replace("#1", visibleRequestsCount) - .replace("#2", L10N.numberWithDecimals((totalBytes || 0) / 1024, 2)) - .replace("#3", L10N.numberWithDecimals((totalMillis || 0) / 1000, 2)) + .replace("#2", L10N.numberWithDecimals((totalBytes || 0) / 1024, CONTENT_SIZE_DECIMALS)) + .replace("#3", L10N.numberWithDecimals((totalMillis || 0) / 1000, REQUEST_TIME_DECIMALS)) ); }, /** * Adds odd/even attributes to all the visible items in this container. */ refreshZebra: function() { let visibleItems = this.visibleItems; @@ -833,16 +940,22 @@ RequestsMenuView.prototype = Heritage.ex this.updateMenuView(requestItem, key, value); break; case "mimeType": requestItem.attachment.mimeType = value; this.updateMenuView(requestItem, key, value); break; case "responseContent": requestItem.attachment.responseContent = value; + // If there's no mime type available when the response content + // is received, assume text/plain as a fallback. + if (!requestItem.attachment.mimeType) { + requestItem.attachment.mimeType = "text/plain"; + this.updateMenuView(requestItem, "mimeType", "text/plain"); + } break; case "totalTime": requestItem.attachment.totalTime = value; requestItem.attachment.endedMillis = requestItem.attachment.startedMillis + value; this.updateMenuView(requestItem, key, value); this._registerLastRequestEnd(requestItem.attachment.endedMillis); break; case "eventTimings": @@ -1016,16 +1129,21 @@ RequestsMenuView.prototype = Heritage.ex } } // Since at least one timing box should've been rendered, unhide the // start and end timing cap nodes. startCapNode.hidden = false; endCapNode.hidden = false; + // Don't paint things while the waterfall view isn't even visible. + if (NetMonitorView.currentFrontendMode != "network-inspector-view") { + return; + } + // Rescale all the waterfalls so that everything is visible at once. this._flushWaterfallViews(); }, /** * Rescales and redraws all the waterfall views in this container. * * @param boolean aReset @@ -1129,17 +1247,17 @@ RequestsMenuView.prototype = Heritage.ex normalizedTime /= 1000; divisionScale = "second"; } // Showing too many decimals is bad UX. if (divisionScale == "millisecond") { normalizedTime |= 0; } else { - normalizedTime = L10N.numberWithDecimals(normalizedTime, 2); + normalizedTime = L10N.numberWithDecimals(normalizedTime, REQUEST_TIME_DECIMALS); } let node = document.createElement("label"); let text = L10N.getFormatStr("networkMenu." + divisionScale, normalizedTime); node.className = "plain requests-menu-timings-division"; node.setAttribute("division-scale", divisionScale); node.style.transform = translateX; @@ -1258,33 +1376,45 @@ RequestsMenuView.prototype = Heritage.ex NetMonitorView.Sidebar.toggle(false); } }, /** * The resize listener for this container's window. */ _onResize: function(e) { + // Don't paint things while the waterfall view isn't even visible. + if (NetMonitorView.currentFrontendMode != "network-inspector-view") { + return; + } + // Allow requests to settle down first. setNamedTimeout( "resize-events", RESIZE_REFRESH_RATE, () => this._flushWaterfallViews(true)); }, /** * Handle the context menu opening. Hide items if no request is selected. */ _onContextShowing: function() { + let selectedItem = this.selectedItem; + let resendElement = $("#request-menu-context-resend"); - resendElement.hidden = !this.selectedItem || this.selectedItem.attachment.isCustom; + resendElement.hidden = !selectedItem || selectedItem.attachment.isCustom; let copyUrlElement = $("#request-menu-context-copy-url"); - copyUrlElement.hidden = !this.selectedItem; + copyUrlElement.hidden = !selectedItem; + + let copyImageAsDataUriElement = $("#request-menu-context-copy-image-as-data-uri"); + copyImageAsDataUriElement.hidden = !selectedItem || + !selectedItem.attachment.responseContent || + !selectedItem.attachment.responseContent.content.mimeType.contains("image/"); let newTabElement = $("#request-menu-context-newtab"); - newTabElement.hidden = !this.selectedItem; + newTabElement.hidden = !selectedItem; }, /** * Checks if the specified unix time is the first one to be known of, * and saves it if so. * * @param number aUnixTime * The milliseconds to check and save. @@ -1448,17 +1578,17 @@ SidebarView.prototype = { populate: function(aData) { let isCustom = aData.isCustom; let view = isCustom ? NetMonitorView.CustomRequest : NetMonitorView.NetworkDetails; return view.populate(aData).then(() => { $("#details-pane").selectedIndex = isCustom ? 0 : 1 - window.emit(EVENTS.SIDEBAR_POPULATED) + window.emit(EVENTS.SIDEBAR_POPULATED); }); }, /** * Hides this container. */ reset: function() { this.toggle(false); @@ -1475,17 +1605,16 @@ function CustomRequestView() { CustomRequestView.prototype = { /** * Initialization function, called when the network monitor is started. */ initialize: function() { dumpn("Initializing the CustomRequestView"); this.updateCustomRequestEvent = getKeyWithEvent(this.onUpdate.bind(this)); - $("#custom-pane").addEventListener("input", this.updateCustomRequestEvent, false); }, /** * Destruction function, called when the network monitor is closed. */ destroy: function() { dumpn("Destroying the CustomRequestView"); @@ -1550,28 +1679,22 @@ CustomRequestView.prototype = { let query = $("#custom-query-value").value; this.updateCustomUrl(query); field = 'url'; value = $("#custom-url-value").value selectedItem.attachment.url = value; break; case 'body': value = $("#custom-postdata-value").value; - selectedItem.attachment.requestPostData = { - postData: { - text: value - } - }; + selectedItem.attachment.requestPostData = { postData: { text: value } }; break; case 'headers': let headersText = $("#custom-headers-value").value; value = parseHeaderText(headersText); - selectedItem.attachment.requestHeaders = { - headers: value - }; + selectedItem.attachment.requestHeaders = { headers: value }; break; } NetMonitorView.RequestsMenu.updateMenuView(selectedItem, field, value); }, /** * Update the query string field based on the url. @@ -2168,16 +2291,180 @@ NetworkDetailsView.prototype = { _paramsPostPayload: "", _requestHeaders: "", _responseHeaders: "", _requestCookies: "", _responseCookies: "" }; /** + * Functions handling the performance statistics view. + */ +function PerformanceStatisticsView() { +} + +PerformanceStatisticsView.prototype = { + /** + * Initializes and displays empty charts in this container. + */ + displayPlaceholderCharts: function() { + this._createChart({ + id: "#primed-cache-chart", + title: "charts.cacheEnabled" + }); + this._createChart({ + id: "#empty-cache-chart", + title: "charts.cacheDisabled" + }); + window.emit(EVENTS.PLACEHOLDER_CHARTS_DISPLAYED); + }, + + /** + * Populates and displays the primed cache chart in this container. + * + * @param array aItems + * @see this._sanitizeChartDataSource + */ + createPrimedCacheChart: function(aItems) { + this._createChart({ + id: "#primed-cache-chart", + title: "charts.cacheEnabled", + data: this._sanitizeChartDataSource(aItems), + sorted: true, + totals: { + size: L10N.getStr("charts.totalSize"), + time: L10N.getStr("charts.totalTime"), + cached: L10N.getStr("charts.totalCached"), + count: L10N.getStr("charts.totalCount") + } + }); + window.emit(EVENTS.PRIMED_CACHE_CHART_DISPLAYED); + }, + + /** + * Populates and displays the empty cache chart in this container. + * + * @param array aItems + * @see this._sanitizeChartDataSource + */ + createEmptyCacheChart: function(aItems) { + this._createChart({ + id: "#empty-cache-chart", + title: "charts.cacheDisabled", + data: this._sanitizeChartDataSource(aItems, true), + sorted: true, + totals: { + size: L10N.getStr("charts.totalSize"), + time: L10N.getStr("charts.totalTime"), + cached: L10N.getStr("charts.totalCached"), + count: L10N.getStr("charts.totalCount") + } + }); + window.emit(EVENTS.EMPTY_CACHE_CHART_DISPLAYED); + }, + + /** + * Adds a specific chart to this container. + * + * @param object + * An object containing all or some the following properties: + * - id: either "#primed-cache-chart" or "#empty-cache-chart" + * - title/data/sorted/totals: @see Chart.jsm for details + */ + _createChart: function({ id, title, data, sorted, totals }) { + let container = $(id); + + // Nuke all existing charts of the specified type. + while (container.hasChildNodes()) { + container.firstChild.remove(); + } + + // Create a new chart. + let chart = Chart.PieTable(document, { + diameter: NETWORK_ANALYSIS_PIE_CHART_DIAMETER, + title: L10N.getStr(title), + data: data, + sorted: sorted, + totals: totals + }); + + chart.on("click", (_, item) => { + NetMonitorView.RequestsMenu.filterOn(item.label); + NetMonitorView.showNetworkInspectorView(); + }); + + container.appendChild(chart.node); + }, + + /** + * Sanitizes the data source used for creating charts, to follow the + * data format spec defined in Chart.jsm. + * + * @param array aItems + * A collection of request items used as the data source for the chart. + * @param boolean aEmptyCache + * True if the cache is considered enabled, false for disabled. + */ + _sanitizeChartDataSource: function(aItems, aEmptyCache) { + let data = [ + "html", "css", "js", "xhr", "fonts", "images", "media", "flash", "other" + ].map(e => ({ + cached: 0, + count: 0, + label: e, + size: 0, + time: 0 + })); + + for (let requestItem of aItems) { + let details = requestItem.attachment; + let type; + + if (RequestsMenuView.prototype.isHtml(requestItem)) { + type = 0; // "html" + } else if (RequestsMenuView.prototype.isCss(requestItem)) { + type = 1; // "css" + } else if (RequestsMenuView.prototype.isJs(requestItem)) { + type = 2; // "js" + } else if (RequestsMenuView.prototype.isFont(requestItem)) { + type = 4; // "fonts" + } else if (RequestsMenuView.prototype.isImage(requestItem)) { + type = 5; // "images" + } else if (RequestsMenuView.prototype.isMedia(requestItem)) { + type = 6; // "media" + } else if (RequestsMenuView.prototype.isFlash(requestItem)) { + type = 7; // "flash" + } else if (RequestsMenuView.prototype.isXHR(requestItem)) { + // Verify XHR last, to categorize other mime types in their own blobs. + type = 3; // "xhr" + } else { + type = 8; // "other" + } + + if (aEmptyCache || !responseIsFresh(details)) { + data[type].time += details.totalTime || 0; + data[type].size += details.contentSize || 0; + } else { + data[type].cached++; + } + data[type].count++; + } + + for (let chartItem of data) { + let size = L10N.numberWithDecimals(chartItem.size / 1024, CONTENT_SIZE_DECIMALS); + let time = L10N.numberWithDecimals(chartItem.time / 1000, REQUEST_TIME_DECIMALS); + chartItem.size = L10N.getFormatStr("charts.sizeKB", size); + chartItem.time = L10N.getFormatStr("charts.totalMS", time); + } + + return data.filter(e => e.count > 0); + }, +}; + +/** * DOM query helper. */ function $(aSelector, aTarget = document) aTarget.querySelector(aSelector); function $all(aSelector, aTarget = document) aTarget.querySelectorAll(aSelector); /** * Helper for getting an nsIURL instance out of a string. */ @@ -2189,18 +2476,18 @@ function nsIURL(aUrl, aStore = nsIURL.st aStore.set(aUrl, uri); return uri; } nsIURL.store = new Map(); /** * Parse a url's query string into its components * - * @param string aQueryString - * The query part of a url + * @param string aQueryString + * The query part of a url * @return array * Array of query params {name, value} */ function parseQueryString(aQueryString) { // Make sure there's at least one param available. if (!aQueryString || !aQueryString.contains("=")) { return; } @@ -2211,43 +2498,43 @@ function parseQueryString(aQueryString) value: NetworkHelper.convertToUnicode(unescape(param[1])) }); return paramsArray; } /** * Parse text representation of HTTP headers. * - * @param string aText - * Text of headers + * @param string aText + * Text of headers * @return array * Array of headers info {name, value} */ function parseHeaderText(aText) { return parseRequestText(aText, ":"); } /** * Parse readable text list of a query string. * - * @param string aText - * Text of query string represetation + * @param string aText + * Text of query string represetation * @return array * Array of query params {name, value} */ function parseQueryText(aText) { return parseRequestText(aText, "="); } /** * Parse a text representation of a name:value list with * the given name:value divider character. * - * @param string aText - * Text of list + * @param string aText + * Text of list * @return array * Array of headers info {name, value} */ function parseRequestText(aText, aDivider) { let regex = new RegExp("(.+?)\\" + aDivider + "\\s*(.+)"); let pairs = []; for (let line of aText.split("\n")) { let matches; @@ -2291,16 +2578,54 @@ function writeQueryText(aParams) { * @return string * Query string that can be appended to a url. */ function writeQueryString(aParams) { return [(name + "=" + value) for ({name, value} of aParams)].join("&"); } /** + * Checks if the "Expiration Calculations" defined in section 13.2.4 of the + * "HTTP/1.1: Caching in HTTP" spec holds true for a collection of headers. + * + * @param object + * An object containing the { responseHeaders, status } properties. + * @return boolean + * True if the response is fresh and loaded from cache. + */ +function responseIsFresh({ responseHeaders, status }) { + // Check for a "304 Not Modified" status and response headers availability. + if (status != 304 || !responseHeaders) { + return false; + } + + let list = responseHeaders.headers; + let cacheControl = list.filter(e => e.name.toLowerCase() == "cache-control")[0]; + let expires = list.filter(e => e.name.toLowerCase() == "expires")[0]; + + // Check the "Cache-Control" header for a maximum age value. + if (cacheControl) { + let maxAgeMatch = + cacheControl.value.match(/s-maxage\s*=\s*(\d+)/) || + cacheControl.value.match(/max-age\s*=\s*(\d+)/); + + if (maxAgeMatch && maxAgeMatch.pop() > 0) { + return true; + } + } + + // Check the "Expires" header for a valid date. + if (expires && Date.parse(expires.value)) { + return true; + } + + return false; +} + +/** * Helper method to get a wrapped function which can be bound to as an event listener directly and is executed only when data-key is present in event.target. * * @param function callback * Function to execute execute when data-key is present in event.target. * @return function * Wrapped function with the target data-key as the first argument. */ function getKeyWithEvent(callback) { @@ -2315,8 +2640,9 @@ function getKeyWithEvent(callback) { /** * Preliminary setup for the NetMonitorView object. */ NetMonitorView.Toolbar = new ToolbarView(); NetMonitorView.RequestsMenu = new RequestsMenuView(); NetMonitorView.Sidebar = new SidebarView(); NetMonitorView.CustomRequest = new CustomRequestView(); NetMonitorView.NetworkDetails = new NetworkDetailsView(); +NetMonitorView.PerformanceStatistics = new PerformanceStatisticsView();
--- a/browser/devtools/netmonitor/netmonitor.css +++ b/browser/devtools/netmonitor/netmonitor.css @@ -7,51 +7,51 @@ overflow: hidden; } #details-pane-toggle[disabled] { /* Don't use display: none; to avoid collapsing #requests-menu-toolbar */ visibility: hidden; } -#response-content-image-box { +#custom-pane { overflow: auto; } -#custom-pane { +#response-content-image-box { overflow: auto; } #timings-summary-blocked { display: none; /* This doesn't work yet. */ } +#network-statistics-charts { + overflow: auto; +} + /* Responsive sidebar */ @media (max-width: 700px) { #toolbar-spacer, #details-pane-toggle, #details-pane[pane-collapsed], .requests-menu-waterfall, .requests-menu-footer-label { display: none; } } -@media (min-width: 701px) and (max-width: 1024px) { - #body:not([pane-collapsed]) .requests-menu-footer-button, +@media (min-width: 701px) and (max-width: 1280px) { + #body:not([pane-collapsed]) .requests-menu-filter-button, #body:not([pane-collapsed]) .requests-menu-footer-spacer { display: none; } } @media (min-width: 701px) { - #requests-menu-spacer-start { - display: none; - } - #network-table[waterfall-overflows] .requests-menu-waterfall { display: none; } #network-table[size-overflows] .requests-menu-size { display: none; }
--- a/browser/devtools/netmonitor/netmonitor.xul +++ b/browser/devtools/netmonitor/netmonitor.xul @@ -23,25 +23,32 @@ <popupset id="networkPopupSet"> <menupopup id="network-request-popup"> <menuitem id="request-menu-context-newtab" label="&netmonitorUI.context.newTab;" accesskey="&netmonitorUI.context.newTab.accesskey;"/> <menuitem id="request-menu-context-copy-url" label="&netmonitorUI.context.copyUrl;" accesskey="&netmonitorUI.context.copyUrl.accesskey;"/> + <menuitem id="request-menu-context-copy-image-as-data-uri" + label="&netmonitorUI.context.copyImageAsDataUri;" + accesskey="&netmonitorUI.context.copyImageAsDataUri.accesskey;"/> <menuitem id="request-menu-context-resend" label="&netmonitorUI.summary.editAndResend;" accesskey="&netmonitorUI.summary.editAndResend.accesskey;"/> + <menuseparator/> + <menuitem id="request-menu-context-perf" + label="&netmonitorUI.context.perfTools;" + accesskey="&netmonitorUI.context.perfTools.accesskey;"/> </menupopup> </popupset> - <box id="body" - class="devtools-responsive-container theme-sidebar" - flex="1"> + <deck id="body" class="theme-sidebar" flex="1"> + + <box id="network-inspector-view" class="devtools-responsive-container"> <vbox id="network-table" flex="1"> <toolbar id="requests-menu-toolbar" class="devtools-toolbar" align="center"> <hbox id="toolbar-labels" flex="1"> <hbox id="requests-menu-status-and-method-header-box" class="requests-menu-header requests-menu-status-and-method" align="center"> @@ -113,19 +120,30 @@ </hbox> </hbox> <toolbarbutton id="details-pane-toggle" class="devtools-toolbarbutton" tooltiptext="&netmonitorUI.panesButton.tooltip;" disabled="true" tabindex="0"/> </toolbar> - <label id="requests-menu-empty-notice" - class="side-menu-widget-empty-text" - value="&netmonitorUI.emptyNotice2;"/> + + <vbox id="requests-menu-empty-notice" + class="side-menu-widget-empty-text"> + <hbox id="notice-perf-message" align="center"> + <label value="&netmonitorUI.perfNotice1;"/> + <button id="requests-menu-perf-notice-button" + class="devtools-toolbarbutton"/> + <label value="&netmonitorUI.perfNotice2;"/> + </hbox> + <hbox id="notice-reload-message" align="center"> + <label value="&netmonitorUI.emptyNotice3;"/> + </hbox> + </vbox> + <vbox id="requests-menu-contents" flex="1" context="network-request-popup"> <hbox id="requests-menu-item-template" hidden="true"> <hbox class="requests-menu-subitem requests-menu-status-and-method" align="center"> <box class="requests-menu-status"/> <label class="plain requests-menu-method" crop="end" flex="1"/> @@ -146,80 +164,80 @@ <hbox class="start requests-menu-timings-cap" hidden="true"/> <hbox class="end requests-menu-timings-cap" hidden="true"/> <label class="plain requests-menu-timings-total"/> </hbox> </hbox> </hbox> </vbox> <hbox id="requests-menu-footer"> - <spacer id="requests-menu-spacer-start" - class="requests-menu-footer-spacer" - flex="100"/> <button id="requests-menu-filter-all-button" - class="requests-menu-footer-button" + class="requests-menu-filter-button requests-menu-footer-button" checked="true" data-key="all" label="&netmonitorUI.footer.filterAll;"> </button> <button id="requests-menu-filter-html-button" - class="requests-menu-footer-button" + class="requests-menu-filter-button requests-menu-footer-button" data-key="html" label="&netmonitorUI.footer.filterHTML;"> </button> <button id="requests-menu-filter-css-button" - class="requests-menu-footer-button" + class="requests-menu-filter-button requests-menu-footer-button" data-key="css" label="&netmonitorUI.footer.filterCSS;"> </button> <button id="requests-menu-filter-js-button" - class="requests-menu-footer-button" + class="requests-menu-filter-button requests-menu-footer-button" data-key="js" label="&netmonitorUI.footer.filterJS;"> </button> <button id="requests-menu-filter-xhr-button" - class="requests-menu-footer-button" + class="requests-menu-filter-button requests-menu-footer-button" data-key="xhr" label="&netmonitorUI.footer.filterXHR;"> </button> <button id="requests-menu-filter-fonts-button" - class="requests-menu-footer-button" + class="requests-menu-filter-button requests-menu-footer-button" data-key="fonts" label="&netmonitorUI.footer.filterFonts;"> </button> <button id="requests-menu-filter-images-button" - class="requests-menu-footer-button" + class="requests-menu-filter-button requests-menu-footer-button" data-key="images" label="&netmonitorUI.footer.filterImages;"> </button> <button id="requests-menu-filter-media-button" - class="requests-menu-footer-button" + class="requests-menu-filter-button requests-menu-footer-button" data-key="media" label="&netmonitorUI.footer.filterMedia;"> </button> <button id="requests-menu-filter-flash-button" - class="requests-menu-footer-button" + class="requests-menu-filter-button requests-menu-footer-button" data-key="flash" label="&netmonitorUI.footer.filterFlash;"> </button> - <spacer id="requests-menu-spacer-end" + <spacer id="requests-menu-spacer" class="requests-menu-footer-spacer" flex="100"/> - <label id="request-menu-network-summary" + <button id="requests-menu-network-summary-button" + class="requests-menu-footer-button" + tooltiptext="&netmonitorUI.footer.perf;"/> + <label id="requests-menu-network-summary-label" class="plain requests-menu-footer-label" - flex="1" - crop="end"/> + crop="end" + tooltiptext="&netmonitorUI.footer.perf;"/> <button id="requests-menu-clear-button" - class="requests-menu-footer-button" - label="&netmonitorUI.footer.clear;"> - </button> + class="requests-menu-footer-button" + label="&netmonitorUI.footer.clear;"/> </hbox> </vbox> - <splitter id="splitter" class="devtools-side-splitter"/> + <splitter id="network-inspector-view-splitter" + class="devtools-side-splitter"/> <deck id="details-pane" hidden="true"> <vbox id="custom-pane" class="tabpanel-content"> <hbox align="baseline"> <label value="&netmonitorUI.custom.newRequest;" class="plain tabpanel-summary-label @@ -455,9 +473,29 @@ </hbox> </vbox> </tabpanel> </tabpanels> </tabbox> </deck> </box> + <box id="network-statistics-view"> + <toolbar id="network-statistics-toolbar" + class="devtools-toolbar"> + <button id="network-statistics-back-button" + class="devtools-toolbarbutton" + onclick="NetMonitorView.toggleFrontendMode()" + label="&netmonitorUI.backButton;"/> + </toolbar> + <box id="network-statistics-charts" + class="devtools-responsive-container" + flex="1"> + <vbox id="primed-cache-chart" pack="center" flex="1"/> + <splitter id="network-statistics-view-splitter" + class="devtools-side-splitter"/> + <vbox id="empty-cache-chart" pack="center" flex="1"/> + </box> + </box> + + </deck> + </window>
--- a/browser/devtools/netmonitor/test/browser.ini +++ b/browser/devtools/netmonitor/test/browser.ini @@ -1,39 +1,47 @@ [DEFAULT] support-files = head.js html_content-type-test-page.html + html_content-type-without-cache-test-page.html html_custom-get-page.html html_cyrillic-test-page.html html_filter-test-page.html html_infinite-get-page.html html_json-custom-mime-test-page.html html_json-long-test-page.html html_json-malformed-test-page.html html_jsonp-test-page.html html_navigate-test-page.html html_post-data-test-page.html html_post-raw-test-page.html html_simple-test-page.html html_sorting-test-page.html + html_statistics-test-page.html html_status-codes-test-page.html sjs_content-type-test-server.sjs sjs_simple-test-server.sjs sjs_sorting-test-server.sjs sjs_status-codes-test-server.sjs test-image.png [browser_net_aaa_leaktest.js] [browser_net_accessibility-01.js] [browser_net_accessibility-02.js] [browser_net_autoscroll.js] +[browser_net_charts-01.js] +[browser_net_charts-02.js] +[browser_net_charts-03.js] +[browser_net_charts-04.js] +[browser_net_charts-05.js] [browser_net_clear.js] [browser_net_content-type.js] [browser_net_copy_url.js] +[browser_net_copy_image_as_data_uri.js] [browser_net_cyrillic-01.js] [browser_net_cyrillic-02.js] [browser_net_filter-01.js] [browser_net_filter-02.js] [browser_net_filter-03.js] [browser_net_footer-summary.js] [browser_net_json-long.js] [browser_net_json-malformed.js] @@ -52,11 +60,13 @@ support-files = [browser_net_resend.js] [browser_net_simple-init.js] [browser_net_simple-request-data.js] [browser_net_simple-request-details.js] [browser_net_simple-request.js] [browser_net_sort-01.js] [browser_net_sort-02.js] [browser_net_sort-03.js] +[browser_net_statistics-01.js] +[browser_net_statistics-02.js] [browser_net_status-codes.js] [browser_net_timeline_ticks.js] [browser_net_timing-division.js]
new file mode 100644 --- /dev/null +++ b/browser/devtools/netmonitor/test/browser_net_charts-01.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Makes sure Pie Charts have the right internal structure. + */ + +function test() { + initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => { + info("Starting test... "); + + let { document, Chart } = aMonitor.panelWin; + let container = document.createElement("box"); + + let pie = Chart.Pie(document, { + width: 100, + height: 100, + data: [{ + size: 1, + label: "foo" + }, { + size: 2, + label: "bar" + }, { + size: 3, + label: "baz" + }] + }); + + let node = pie.node; + let slices = node.querySelectorAll(".pie-chart-slice.chart-colored-blob"); + let labels = node.querySelectorAll(".pie-chart-label"); + + ok(node.classList.contains("pie-chart-container") && + node.classList.contains("generic-chart-container"), + "A pie chart container was created successfully."); + + is(slices.length, 3, + "There should be 3 pie chart slices created."); + ok(slices[0].getAttribute("d").match(/\s*M 50,50 L 49\.\d+,97\.\d+ A 47\.5,47\.5 0 0 1 49\.\d+,2\.5\d* Z/), + "The first slice has the correct data."); + ok(slices[1].getAttribute("d").match(/\s*M 50,50 L 91\.\d+,26\.\d+ A 47\.5,47\.5 0 0 1 49\.\d+,97\.\d+ Z/), + "The second slice has the correct data."); + ok(slices[2].getAttribute("d").match(/\s*M 50,50 L 50\.\d+,2\.5\d* A 47\.5,47\.5 0 0 1 91\.\d+,26\.\d+ Z/), + "The third slice has the correct data."); + + ok(slices[0].hasAttribute("largest"), + "The first slice should be the largest one."); + ok(slices[2].hasAttribute("smallest"), + "The third slice should be the smallest one."); + + ok(slices[0].getAttribute("name"), "baz", + "The first slice's name is correct."); + ok(slices[1].getAttribute("name"), "bar", + "The first slice's name is correct."); + ok(slices[2].getAttribute("name"), "foo", + "The first slice's name is correct."); + + is(labels.length, 3, + "There should be 3 pie chart labels created."); + is(labels[0].textContent, "baz", + "The first label's text is correct."); + is(labels[1].textContent, "bar", + "The first label's text is correct."); + is(labels[2].textContent, "foo", + "The first label's text is correct."); + + teardown(aMonitor).then(finish); + }); +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/netmonitor/test/browser_net_charts-02.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Makes sure Pie Charts have the right internal structure when + * initialized with empty data. + */ + +function test() { + initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => { + info("Starting test... "); + + let { document, L10N, Chart } = aMonitor.panelWin; + let container = document.createElement("box"); + + let pie = Chart.Pie(document, { + data: null, + width: 100, + height: 100 + }); + + let node = pie.node; + let slices = node.querySelectorAll(".pie-chart-slice.chart-colored-blob"); + let labels = node.querySelectorAll(".pie-chart-label"); + + ok(node.classList.contains("pie-chart-container") && + node.classList.contains("generic-chart-container"), + "A pie chart container was created successfully."); + + is(slices.length, 1, + "There should be 1 pie chart slice created."); + ok(slices[0].getAttribute("d").match(/\s*M 50,50 L 50\.\d+,2\.5\d* A 47\.5,47\.5 0 1 1 49\.\d+,2\.5\d* Z/), + "The first slice has the correct data."); + + ok(slices[0].hasAttribute("largest"), + "The first slice should be the largest one."); + ok(slices[0].hasAttribute("smallest"), + "The first slice should also be the smallest one."); + ok(slices[0].getAttribute("name"), L10N.getStr("pieChart.empty"), + "The first slice's name is correct."); + + is(labels.length, 1, + "There should be 1 pie chart label created."); + is(labels[0].textContent, "Loading", + "The first label's text is correct."); + + teardown(aMonitor).then(finish); + }); +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/netmonitor/test/browser_net_charts-03.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Makes sure Table Charts have the right internal structure. + */ + +function test() { + initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => { + info("Starting test... "); + + let { document, Chart } = aMonitor.panelWin; + let container = document.createElement("box"); + + let table = Chart.Table(document, { + title: "Table title", + data: [{ + label1: 1, + label2: "11.1foo" + }, { + label1: 2, + label2: "12.2bar" + }, { + label1: 3, + label2: "13.3baz" + }], + totals: { + label1: "Hello %S", + label2: "World %S" + } + }); + + let node = table.node; + let title = node.querySelector(".table-chart-title"); + let grid = node.querySelector(".table-chart-grid"); + let totals = node.querySelector(".table-chart-totals"); + let rows = grid.querySelectorAll(".table-chart-row"); + let sums = node.querySelectorAll(".table-chart-summary-label"); + + ok(node.classList.contains("table-chart-container") && + node.classList.contains("generic-chart-container"), + "A table chart container was created successfully."); + + ok(title, + "A title node was created successfully."); + is(title.getAttribute("value"), "Table title", + "The title node displays the correct text."); + + is(rows.length, 3, + "There should be 3 table chart rows created."); + + ok(rows[0].querySelector(".table-chart-row-box.chart-colored-blob"), + "A colored blob exists for the firt row."); + is(rows[0].querySelectorAll("label")[0].getAttribute("name"), "label1", + "The first column of the first row exists."); + is(rows[0].querySelectorAll("label")[1].getAttribute("name"), "label2", + "The second column of the first row exists."); + is(rows[0].querySelectorAll("label")[0].getAttribute("value"), "1", + "The first column of the first row displays the correct text."); + is(rows[0].querySelectorAll("label")[1].getAttribute("value"), "11.1foo", + "The second column of the first row displays the correct text."); + + ok(rows[1].querySelector(".table-chart-row-box.chart-colored-blob"), + "A colored blob exists for the second row."); + is(rows[1].querySelectorAll("label")[0].getAttribute("name"), "label1", + "The first column of the second row exists."); + is(rows[1].querySelectorAll("label")[1].getAttribute("name"), "label2", + "The second column of the second row exists."); + is(rows[1].querySelectorAll("label")[0].getAttribute("value"), "2", + "The first column of the second row displays the correct text."); + is(rows[1].querySelectorAll("label")[1].getAttribute("value"), "12.2bar", + "The second column of the first row displays the correct text."); + + ok(rows[2].querySelector(".table-chart-row-box.chart-colored-blob"), + "A colored blob exists for the third row."); + is(rows[2].querySelectorAll("label")[0].getAttribute("name"), "label1", + "The first column of the third row exists."); + is(rows[2].querySelectorAll("label")[1].getAttribute("name"), "label2", + "The second column of the third row exists."); + is(rows[2].querySelectorAll("label")[0].getAttribute("value"), "3", + "The first column of the third row displays the correct text."); + is(rows[2].querySelectorAll("label")[1].getAttribute("value"), "13.3baz", + "The second column of the third row displays the correct text."); + + is(sums.length, 2, + "There should be 2 total summaries created."); + + is(totals.querySelectorAll(".table-chart-summary-label")[0].getAttribute("name"), "label1", + "The first sum's type is correct."); + is(totals.querySelectorAll(".table-chart-summary-label")[0].getAttribute("value"), "Hello 6", + "The first sum's value is correct."); + + is(totals.querySelectorAll(".table-chart-summary-label")[1].getAttribute("name"), "label2", + "The second sum's type is correct."); + is(totals.querySelectorAll(".table-chart-summary-label")[1].getAttribute("value"), "World 36.60", + "The second sum's value is correct."); + + teardown(aMonitor).then(finish); + }); +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/netmonitor/test/browser_net_charts-04.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Makes sure Pie Charts have the right internal structure when + * initialized with empty data. + */ + +function test() { + initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => { + info("Starting test... "); + + let { document, L10N, Chart } = aMonitor.panelWin; + let container = document.createElement("box"); + + let table = Chart.Table(document, { + title: "Table title", + data: null, + totals: { + label1: "Hello %S", + label2: "World %S" + } + }); + + let node = table.node; + let title = node.querySelector(".table-chart-title"); + let grid = node.querySelector(".table-chart-grid"); + let totals = node.querySelector(".table-chart-totals"); + let rows = grid.querySelectorAll(".table-chart-row"); + let sums = node.querySelectorAll(".table-chart-summary-label"); + + ok(node.classList.contains("table-chart-container") && + node.classList.contains("generic-chart-container"), + "A table chart container was created successfully."); + + ok(title, + "A title node was created successfully."); + is(title.getAttribute("value"), "Table title", + "The title node displays the correct text."); + + is(rows.length, 1, + "There should be 1 table chart row created."); + + ok(rows[0].querySelector(".table-chart-row-box.chart-colored-blob"), + "A colored blob exists for the firt row."); + is(rows[0].querySelectorAll("label")[0].getAttribute("name"), "size", + "The first column of the first row exists."); + is(rows[0].querySelectorAll("label")[1].getAttribute("name"), "label", + "The second column of the first row exists."); + is(rows[0].querySelectorAll("label")[0].getAttribute("value"), "", + "The first column of the first row displays the correct text."); + is(rows[0].querySelectorAll("label")[1].getAttribute("value"), L10N.getStr("tableChart.empty"), + "The second column of the first row displays the correct text."); + + is(sums.length, 2, + "There should be 2 total summaries created."); + + is(totals.querySelectorAll(".table-chart-summary-label")[0].getAttribute("name"), "label1", + "The first sum's type is correct."); + is(totals.querySelectorAll(".table-chart-summary-label")[0].getAttribute("value"), "Hello 0", + "The first sum's value is correct."); + + is(totals.querySelectorAll(".table-chart-summary-label")[1].getAttribute("name"), "label2", + "The second sum's type is correct."); + is(totals.querySelectorAll(".table-chart-summary-label")[1].getAttribute("value"), "World 0", + "The second sum's value is correct."); + + teardown(aMonitor).then(finish); + }); +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/netmonitor/test/browser_net_charts-05.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Makes sure Pie+Table Charts have the right internal structure. + */ + +function test() { + initNetMonitor(SIMPLE_URL).then(([aTab, aDebuggee, aMonitor]) => { + info("Starting test... "); + + let { document, Chart } = aMonitor.panelWin; + let container = document.createElement("box"); + + let chart = Chart.PieTable(document, { + title: "Table title", + data: [{ + size: 1, + label: "11.1foo" + }, { + size: 2, + label: "12.2bar" + }, { + size: 3, + label: "13.3baz" + }], + totals: { + size: "Hello %S", + label: "World %S" + } + }); + + ok(chart.pie, "The pie chart proxy is accessible."); + ok(chart.table, "The table chart proxy is accessible."); + + let node = chart.node; + let slices = node.querySelectorAll(".pie-chart-slice"); + let rows = node.querySelectorAll(".table-chart-row"); + let sums = node.querySelectorAll(".table-chart-summary-label"); + + ok(node.classList.contains("pie-table-chart-container"), + "A pie+table chart container was created successfully."); + + ok(node.querySelector(".table-chart-title"), + "A title node was created successfully."); + ok(node.querySelector(".pie-chart-container"), + "A pie chart was created successfully."); + ok(node.querySelector(".table-chart-container"), + "A table chart was created successfully."); + + is(rows.length, 3, + "There should be 3 pie chart slices created."); + is(rows.length, 3, + "There should be 3 table chart rows created."); + is(sums.length, 2, + "There should be 2 total summaries created."); + + teardown(aMonitor).then(finish); + }); +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/netmonitor/test/browser_net_copy_image_as_data_uri.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if copying an image as data uri works. + */ + +function test() { + initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL).then(([aTab, aDebuggee, aMonitor]) => { + info("Starting test... "); + + let { NetMonitorView } = aMonitor.panelWin; + let { RequestsMenu } = NetMonitorView; + RequestsMenu.lazyUpdate = false; + let imageDataUri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="; + + waitForNetworkEvents(aMonitor, 6).then(() => { + let requestItem = RequestsMenu.getItemAtIndex(5); + RequestsMenu.selectedItem = requestItem; + + waitForClipboard(imageDataUri, function setup() { + RequestsMenu.copyImageAsDataUri(); + }, function onSuccess() { + ok(true, "Clipboard contains the currently selected image as data uri."); + cleanUp(); + }, function onFailure() { + ok(false, "Copying the currently selected image as data uri was unsuccessful."); + cleanUp(); + }); + }); + + aDebuggee.performRequests(); + + function cleanUp(){ + teardown(aMonitor).then(finish); + } + }); +}
--- a/browser/devtools/netmonitor/test/browser_net_footer-summary.js +++ b/browser/devtools/netmonitor/test/browser_net_footer-summary.js @@ -75,32 +75,26 @@ function test() { EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-filter-flash-button")); testStatus(); teardown(aMonitor).then(finish); }) function testStatus() { - let summary = $("#request-menu-network-summary"); + let summary = $("#requests-menu-network-summary-label"); let value = summary.getAttribute("value"); info("Current summary: " + value); let visibleItems = RequestsMenu.visibleItems; let visibleRequestsCount = visibleItems.length; let totalRequestsCount = RequestsMenu.itemCount; info("Current requests: " + visibleRequestsCount + " of " + totalRequestsCount + "."); - if (!totalRequestsCount) { - is(value, "", - "The current summary text is incorrect, expected an empty string."); - return; - } - - if (!visibleRequestsCount) { + if (!totalRequestsCount || !visibleRequestsCount) { is(value, L10N.getStr("networkMenu.empty"), "The current summary text is incorrect, expected an 'empty' label."); return; } let totalBytes = RequestsMenu._getTotalBytesOfRequests(visibleItems); let totalMillis = RequestsMenu._getNewestRequest(visibleItems).attachment.endedMillis -
new file mode 100644 --- /dev/null +++ b/browser/devtools/netmonitor/test/browser_net_statistics-01.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the statistics view is populated correctly. + */ + +function test() { + initNetMonitor(STATISTICS_URL).then(([aTab, aDebuggee, aMonitor]) => { + info("Starting test... "); + + let panel = aMonitor.panelWin; + let { document, $, $all, EVENTS, NetMonitorView } = panel; + + is(NetMonitorView.currentFrontendMode, "network-inspector-view", + "The initial frontend mode is correct."); + + is($("#primed-cache-chart").childNodes.length, 0, + "There should be no primed cache chart created yet."); + is($("#empty-cache-chart").childNodes.length, 0, + "There should be no empty cache chart created yet."); + + waitFor(panel, EVENTS.PLACEHOLDER_CHARTS_DISPLAYED).then(() => { + is($("#primed-cache-chart").childNodes.length, 1, + "There should be a placeholder primed cache chart created now."); + is($("#empty-cache-chart").childNodes.length, 1, + "There should be a placeholder empty cache chart created now."); + + is($all(".pie-chart-container[placeholder=true]").length, 2, + "Two placeholder pie chart appear to be rendered correctly."); + is($all(".table-chart-container[placeholder=true]").length, 2, + "Two placeholder table chart appear to be rendered correctly."); + + promise.all([ + waitFor(panel, EVENTS.PRIMED_CACHE_CHART_DISPLAYED), + waitFor(panel, EVENTS.EMPTY_CACHE_CHART_DISPLAYED) + ]).then(() => { + is($("#primed-cache-chart").childNodes.length, 1, + "There should be a real primed cache chart created now."); + is($("#empty-cache-chart").childNodes.length, 1, + "There should be a real empty cache chart created now."); + + is($all(".pie-chart-container:not([placeholder=true])").length, 2, + "Two real pie chart appear to be rendered correctly."); + is($all(".table-chart-container:not([placeholder=true])").length, 2, + "Two real table chart appear to be rendered correctly."); + + teardown(aMonitor).then(finish); + }); + }); + + NetMonitorView.toggleFrontendMode(); + + is(NetMonitorView.currentFrontendMode, "network-statistics-view", + "The current frontend mode is correct."); + }); +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/netmonitor/test/browser_net_statistics-02.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the network inspector view is shown when the target navigates + * away while in the statistics view. + */ + +function test() { + initNetMonitor(STATISTICS_URL).then(([aTab, aDebuggee, aMonitor]) => { + info("Starting test... "); + + let panel = aMonitor.panelWin; + let { document, EVENTS, NetMonitorView } = panel; + + is(NetMonitorView.currentFrontendMode, "network-inspector-view", + "The initial frontend mode is correct."); + + promise.all([ + waitFor(panel, EVENTS.PRIMED_CACHE_CHART_DISPLAYED), + waitFor(panel, EVENTS.EMPTY_CACHE_CHART_DISPLAYED) + ]).then(() => { + is(NetMonitorView.currentFrontendMode, "network-statistics-view", + "The frontend mode is currently in the statistics view."); + + waitFor(panel, EVENTS.TARGET_WILL_NAVIGATE).then(() => { + is(NetMonitorView.currentFrontendMode, "network-inspector-view", + "The frontend mode switched back to the inspector view."); + + waitFor(panel, EVENTS.TARGET_DID_NAVIGATE).then(() => { + is(NetMonitorView.currentFrontendMode, "network-inspector-view", + "The frontend mode is still in the inspector view."); + + teardown(aMonitor).then(finish); + }); + }); + + aDebuggee.location.reload(); + }); + + NetMonitorView.toggleFrontendMode(); + }); +}
--- a/browser/devtools/netmonitor/test/head.js +++ b/browser/devtools/netmonitor/test/head.js @@ -12,28 +12,30 @@ let { devtools } = Cu.import("resource:/ let TargetFactory = devtools.TargetFactory; let Toolbox = devtools.Toolbox; const EXAMPLE_URL = "http://example.com/browser/browser/devtools/netmonitor/test/"; const SIMPLE_URL = EXAMPLE_URL + "html_simple-test-page.html"; const NAVIGATE_URL = EXAMPLE_URL + "html_navigate-test-page.html"; const CONTENT_TYPE_URL = EXAMPLE_URL + "html_content-type-test-page.html"; +const CONTENT_TYPE_WITHOUT_CACHE_URL = EXAMPLE_URL + "html_content-type-without-cache-test-page.html"; const CYRILLIC_URL = EXAMPLE_URL + "html_cyrillic-test-page.html"; const STATUS_CODES_URL = EXAMPLE_URL + "html_status-codes-test-page.html"; const POST_DATA_URL = EXAMPLE_URL + "html_post-data-test-page.html"; const POST_RAW_URL = EXAMPLE_URL + "html_post-raw-test-page.html"; const JSONP_URL = EXAMPLE_URL + "html_jsonp-test-page.html"; const JSON_LONG_URL = EXAMPLE_URL + "html_json-long-test-page.html"; const JSON_MALFORMED_URL = EXAMPLE_URL + "html_json-malformed-test-page.html"; const JSON_CUSTOM_MIME_URL = EXAMPLE_URL + "html_json-custom-mime-test-page.html"; const SORTING_URL = EXAMPLE_URL + "html_sorting-test-page.html"; const FILTERING_URL = EXAMPLE_URL + "html_filter-test-page.html"; const INFINITE_GET_URL = EXAMPLE_URL + "html_infinite-get-page.html"; const CUSTOM_GET_URL = EXAMPLE_URL + "html_custom-get-page.html"; +const STATISTICS_URL = EXAMPLE_URL + "html_statistics-test-page.html"; const SIMPLE_SJS = EXAMPLE_URL + "sjs_simple-test-server.sjs"; const CONTENT_TYPE_SJS = EXAMPLE_URL + "sjs_content-type-test-server.sjs"; const STATUS_CODES_SJS = EXAMPLE_URL + "sjs_status-codes-test-server.sjs"; const SORTING_SJS = EXAMPLE_URL + "sjs_sorting-test-server.sjs"; const TEST_IMAGE = EXAMPLE_URL + "test-image.png";
new file mode 100644 --- /dev/null +++ b/browser/devtools/netmonitor/test/html_content-type-without-cache-test-page.html @@ -0,0 +1,45 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Network Monitor test page</title> + </head> + + <body> + <p>Content type test</p> + + <script type="text/javascript"> + function get(aAddress, aCallback) { + var xhr = new XMLHttpRequest(); + xhr.open("GET", aAddress, true); + + xhr.onreadystatechange = function() { + if (this.readyState == this.DONE) { + aCallback(); + } + }; + xhr.send(null); + } + + function performRequests() { + get("sjs_content-type-test-server.sjs?fmt=xml", function() { + get("sjs_content-type-test-server.sjs?fmt=css", function() { + get("sjs_content-type-test-server.sjs?fmt=js", function() { + get("sjs_content-type-test-server.sjs?fmt=json", function() { + get("sjs_content-type-test-server.sjs?fmt=bogus", function() { + get("test-image.png?v=" + Math.random(), function() { + // Done. + }); + }); + }); + }); + }); + }); + } + </script> + </body> + +</html>
new file mode 100644 --- /dev/null +++ b/browser/devtools/netmonitor/test/html_statistics-test-page.html @@ -0,0 +1,37 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Network Monitor test page</title> + </head> + + <body> + <p>Statistics test</p> + + <script type="text/javascript"> + function get(aAddress) { + var xhr = new XMLHttpRequest(); + xhr.open("GET", aAddress, true); + xhr.send(null); + } + + get("sjs_content-type-test-server.sjs?sts=304&fmt=txt"); + get("sjs_content-type-test-server.sjs?sts=304&fmt=xml"); + get("sjs_content-type-test-server.sjs?sts=304&fmt=html"); + get("sjs_content-type-test-server.sjs?sts=304&fmt=css"); + get("sjs_content-type-test-server.sjs?sts=304&fmt=js"); + get("sjs_content-type-test-server.sjs?sts=304&fmt=json"); + get("sjs_content-type-test-server.sjs?sts=304&fmt=jsonp"); + get("sjs_content-type-test-server.sjs?sts=304&fmt=font"); + get("sjs_content-type-test-server.sjs?sts=304&fmt=image"); + get("sjs_content-type-test-server.sjs?sts=304&fmt=audio"); + get("sjs_content-type-test-server.sjs?sts=304&fmt=video"); + get("sjs_content-type-test-server.sjs?sts=304&fmt=flash"); + get("test-image.png"); + </script> + </body> + +</html>
--- a/browser/devtools/netmonitor/test/sjs_content-type-test-server.sjs +++ b/browser/devtools/netmonitor/test/sjs_content-type-test-server.sjs @@ -2,128 +2,161 @@ http://creativecommons.org/publicdomain/zero/1.0/ */ const { classes: Cc, interfaces: Ci } = Components; function handleRequest(request, response) { response.processAsync(); let params = request.queryString.split("&"); - let format = params.filter((s) => s.contains("fmt="))[0].split("=")[1]; + let format = (params.filter((s) => s.contains("fmt="))[0] || "").split("=")[1]; + let status = (params.filter((s) => s.contains("sts="))[0] || "").split("=")[1] || 200; + + let cachedCount = 0; + let cacheExpire = 60; // seconds + + function maybeMakeCached() { + if (status != 304) { + return; + } + // Spice things up a little! + if (cachedCount % 2) { + response.setHeader("Cache-Control", "max-age=" + cacheExpire, false); + } else { + response.setHeader("Expires", Date(Date.now() + cacheExpire * 1000), false); + } + cachedCount++; + } Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer).initWithCallback(() => { switch (format) { case "txt": { - response.setStatusLine(request.httpVersion, 200, "DA DA DA"); + response.setStatusLine(request.httpVersion, status, "DA DA DA"); response.setHeader("Content-Type", "text/plain", false); + maybeMakeCached(); response.write("Братан, ты вообще качаешься?"); response.finish(); break; } case "xml": { - response.setStatusLine(request.httpVersion, 200, "OK"); + response.setStatusLine(request.httpVersion, status, "OK"); response.setHeader("Content-Type", "text/xml; charset=utf-8", false); + maybeMakeCached(); response.write("<label value='greeting'>Hello XML!</label>"); response.finish(); break; } case "html": { let content = params.filter((s) => s.contains("res="))[0].split("=")[1]; - response.setStatusLine(request.httpVersion, 200, "OK"); + response.setStatusLine(request.httpVersion, status, "OK"); response.setHeader("Content-Type", "text/html; charset=utf-8", false); + maybeMakeCached(); response.write(content || "<p>Hello HTML!</p>"); response.finish(); break; } case "html-long": { let str = new Array(102400 /* 100 KB in bytes */).join("."); - response.setStatusLine(request.httpVersion, 200, "OK"); + response.setStatusLine(request.httpVersion, status, "OK"); response.setHeader("Content-Type", "text/html; charset=utf-8", false); + maybeMakeCached(); response.write("<p>" + str + "</p>"); response.finish(); break; } case "css": { - response.setStatusLine(request.httpVersion, 200, "OK"); + response.setStatusLine(request.httpVersion, status, "OK"); response.setHeader("Content-Type", "text/css; charset=utf-8", false); + maybeMakeCached(); response.write("body:pre { content: 'Hello CSS!' }"); response.finish(); break; } case "js": { - response.setStatusLine(request.httpVersion, 200, "OK"); + response.setStatusLine(request.httpVersion, status, "OK"); response.setHeader("Content-Type", "application/javascript; charset=utf-8", false); + maybeMakeCached(); response.write("function() { return 'Hello JS!'; }"); response.finish(); break; } case "json": { - response.setStatusLine(request.httpVersion, 200, "OK"); + response.setStatusLine(request.httpVersion, status, "OK"); response.setHeader("Content-Type", "application/json; charset=utf-8", false); + maybeMakeCached(); response.write("{ \"greeting\": \"Hello JSON!\" }"); response.finish(); break; } case "jsonp": { let fun = params.filter((s) => s.contains("jsonp="))[0].split("=")[1]; - response.setStatusLine(request.httpVersion, 200, "OK"); + response.setStatusLine(request.httpVersion, status, "OK"); response.setHeader("Content-Type", "text/json; charset=utf-8", false); + maybeMakeCached(); response.write(fun + "({ \"greeting\": \"Hello JSONP!\" })"); response.finish(); break; } case "json-long": { let str = "{ \"greeting\": \"Hello long string JSON!\" },"; - response.setStatusLine(request.httpVersion, 200, "OK"); + response.setStatusLine(request.httpVersion, status, "OK"); response.setHeader("Content-Type", "text/json; charset=utf-8", false); + maybeMakeCached(); response.write("[" + new Array(2048).join(str).slice(0, -1) + "]"); response.finish(); break; } case "json-malformed": { - response.setStatusLine(request.httpVersion, 200, "OK"); + response.setStatusLine(request.httpVersion, status, "OK"); response.setHeader("Content-Type", "text/json; charset=utf-8", false); + maybeMakeCached(); response.write("{ \"greeting\": \"Hello malformed JSON!\" },"); response.finish(); break; } case "json-custom-mime": { - response.setStatusLine(request.httpVersion, 200, "OK"); + response.setStatusLine(request.httpVersion, status, "OK"); response.setHeader("Content-Type", "text/x-bigcorp-json; charset=utf-8", false); + maybeMakeCached(); response.write("{ \"greeting\": \"Hello oddly-named JSON!\" }"); response.finish(); break; } case "font": { - response.setStatusLine(request.httpVersion, 200, "OK"); + response.setStatusLine(request.httpVersion, status, "OK"); response.setHeader("Content-Type", "font/woff", false); + maybeMakeCached(); response.finish(); break; } case "image": { - response.setStatusLine(request.httpVersion, 200, "OK"); + response.setStatusLine(request.httpVersion, status, "OK"); response.setHeader("Content-Type", "image/png", false); + maybeMakeCached(); response.finish(); break; } case "audio": { - response.setStatusLine(request.httpVersion, 200, "OK"); + response.setStatusLine(request.httpVersion, status, "OK"); response.setHeader("Content-Type", "audio/ogg", false); + maybeMakeCached(); response.finish(); break; } case "video": { - response.setStatusLine(request.httpVersion, 200, "OK"); + response.setStatusLine(request.httpVersion, status, "OK"); response.setHeader("Content-Type", "video/webm", false); + maybeMakeCached(); response.finish(); break; } case "flash": { - response.setStatusLine(request.httpVersion, 200, "OK"); + response.setStatusLine(request.httpVersion, status, "OK"); response.setHeader("Content-Type", "application/x-shockwave-flash", false); + maybeMakeCached(); response.finish(); break; } default: { response.setStatusLine(request.httpVersion, 404, "Not Found"); response.setHeader("Content-Type", "text/html; charset=utf-8", false); response.write("<blink>Not Found</blink>"); response.finish();
--- a/browser/devtools/shared/autocomplete-popup.js +++ b/browser/devtools/shared/autocomplete-popup.js @@ -449,16 +449,27 @@ AutocompletePopup.prototype = { * Getter for the number of items in the popup. * @type number */ get itemCount() { return this._list.childNodes.length; }, /** + * Getter for the height of each item in the list. + * + * @private + * + * @type number + */ + get _itemHeight() { + return this._list.selectedItem.clientHeight; + }, + + /** * Select the next item in the list. * * @return object * The newly selected item object. */ selectNextItem: function AP_selectNextItem() { if (this.selectedIndex < (this.itemCount - 1)) { @@ -470,31 +481,64 @@ AutocompletePopup.prototype = { return this.selectedItem; }, /** * Select the previous item in the list. * * @return object - * The newly selected item object. + * The newly-selected item object. */ selectPreviousItem: function AP_selectPreviousItem() { if (this.selectedIndex > 0) { this.selectedIndex--; } else { this.selectedIndex = this.itemCount - 1; } return this.selectedItem; }, /** + * Select the top-most item in the next page of items or + * the last item in the list. + * + * @return object + * The newly-selected item object. + */ + selectNextPageItem: function AP_selectNextPageItem() + { + let itemsPerPane = Math.floor(this._list.scrollHeight / this._itemHeight); + let nextPageIndex = this.selectedIndex + itemsPerPane + 1; + this.selectedIndex = nextPageIndex > this.itemCount - 1 ? + this.itemCount - 1 : nextPageIndex; + + return this.selectedItem; + }, + + /** + * Select the bottom-most item in the previous page of items, + * or the first item in the list. + * + * @return object + * The newly-selected item object. + */ + selectPreviousPageItem: function AP_selectPreviousPageItem() + { + let itemsPerPane = Math.floor(this._list.scrollHeight / this._itemHeight); + let prevPageIndex = this.selectedIndex - itemsPerPane - 1; + this.selectedIndex = prevPageIndex < 0 ? 0 : prevPageIndex; + + return this.selectedItem; + }, + + /** * Focuses the richlistbox. */ focus: function AP_focus() { this._list.focus(); }, /**
new file mode 100644 --- /dev/null +++ b/browser/devtools/shared/widgets/Chart.jsm @@ -0,0 +1,422 @@ +/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* 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 Ci = Components.interfaces; +const Cu = Components.utils; + +const NET_STRINGS_URI = "chrome://browser/locale/devtools/netmonitor.properties"; +const SVG_NS = "http://www.w3.org/2000/svg"; +const PI = Math.PI; +const TAU = PI * 2; +const EPSILON = 0.0000001; +const NAMED_SLICE_MIN_ANGLE = TAU / 8; +const NAMED_SLICE_TEXT_DISTANCE_RATIO = 1.9; +const HOVERED_SLICE_TRANSLATE_DISTANCE_RATIO = 20; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); +Cu.import("resource:///modules/devtools/shared/event-emitter.js"); + +this.EXPORTED_SYMBOLS = ["Chart"]; + +/** + * Localization convenience methods. + */ +let L10N = new ViewHelpers.L10N(NET_STRINGS_URI); + +/** + * A factory for creating charts. + * Example usage: let myChart = Chart.Pie(document, { ... }); + */ +let Chart = { + Pie: createPieChart, + Table: createTableChart, + PieTable: createPieTableChart +}; + +/** + * A simple pie chart proxy for the underlying view. + * Each item in the `slices` property represents a [data, node] pair containing + * the data used to create the slice and the nsIDOMNode displaying it. + * + * @param nsIDOMNode node + * The node representing the view for this chart. + */ +function PieChart(node) { + this.node = node; + this.slices = new WeakMap(); + EventEmitter.decorate(this); +} + +/** + * A simple table chart proxy for the underlying view. + * Each item in the `rows` property represents a [data, node] pair containing + * the data used to create the row and the nsIDOMNode displaying it. + * + * @param nsIDOMNode node + * The node representing the view for this chart. + */ +function TableChart(node) { + this.node = node; + this.rows = new WeakMap(); + EventEmitter.decorate(this); +} + +/** + * A simple pie+table chart proxy for the underlying view. + * + * @param nsIDOMNode node + * The node representing the view for this chart. + * @param PieChart pie + * The pie chart proxy. + * @param TableChart table + * The table chart proxy. + */ +function PieTableChart(node, pie, table) { + this.node = node; + this.pie = pie; + this.table = table; + EventEmitter.decorate(this); +} + +/** + * Creates the DOM for a pie+table chart. + * + * @param nsIDocument document + * The document responsible with creating the DOM. + * @param object + * An object containing all or some of the following properties: + * - title: a string displayed as the table chart's (description)/local + * - diameter: the diameter of the pie chart, in pixels + * - data: an array of items used to display each slice in the pie + * and each row in the table; + * @see `createPieChart` and `createTableChart` for details. + * - sorted: a flag specifying if the `data` should be sorted + * ascending by `size`. + * - totals: @see `createTableChart` for details. + * @return PieTableChart + * A pie+table chart proxy instance, which emits the following events: + * - "mouseenter", when the mouse enters a slice or a row + * - "mouseleave", when the mouse leaves a slice or a row + * - "click", when the mouse enters a slice or a row + */ +function createPieTableChart(document, { sorted, title, diameter, data, totals }) { + if (sorted) { + data = data.slice().sort((a, b) => +(parseFloat(a.size) < parseFloat(b.size))); + } + + let pie = Chart.Pie(document, { + width: diameter, + data: data + }); + + let table = Chart.Table(document, { + title: title, + data: data, + totals: totals + }); + + let container = document.createElement("hbox"); + container.className = "pie-table-chart-container"; + container.appendChild(pie.node); + container.appendChild(table.node); + + let proxy = new PieTableChart(container, pie, table); + + pie.on("click", (event, item) => { + proxy.emit(event, item) + }); + + table.on("click", (event, item) => { + proxy.emit(event, item) + }); + + pie.on("mouseenter", (event, item) => { + proxy.emit(event, item); + if (table.rows.has(item)) { + table.rows.get(item).setAttribute("focused", ""); + } + }); + + pie.on("mouseleave", (event, item) => { + proxy.emit(event, item); + if (table.rows.has(item)) { + table.rows.get(item).removeAttribute("focused"); + } + }); + + table.on("mouseenter", (event, item) => { + proxy.emit(event, item); + if (pie.slices.has(item)) { + pie.slices.get(item).setAttribute("focused", ""); + } + }); + + table.on("mouseleave", (event, item) => { + proxy.emit(event, item); + if (pie.slices.has(item)) { + pie.slices.get(item).removeAttribute("focused"); + } + }); + + return proxy; +} + +/** + * Creates the DOM for a pie chart based on the specified properties. + * + * @param nsIDocument document + * The document responsible with creating the DOM. + * @param object + * An object containing all or some of the following properties: + * - data: an array of items used to display each slice; all the items + * should be objects containing a `size` and a `label` property. + * e.g: [{ + * size: 1, + * label: "foo" + * }, { + * size: 2, + * label: "bar" + * }]; + * - width: the width of the chart, in pixels + * - height: optional, the height of the chart, in pixels. + * - centerX: optional, the X-axis center of the chart, in pixels. + * - centerY: optional, the Y-axis center of the chart, in pixels. + * - radius: optional, the radius of the chart, in pixels. + * @return PieChart + * A pie chart proxy instance, which emits the following events: + * - "mouseenter", when the mouse enters a slice + * - "mouseleave", when the mouse leaves a slice + * - "click", when the mouse clicks a slice + */ +function createPieChart(document, { data, width, height, centerX, centerY, radius }) { + height = height || width; + centerX = centerX || width / 2; + centerY = centerY || height / 2; + radius = radius || (width + height) / 4; + let isPlaceholder = false; + + // Filter out very small sizes, as they'll just render invisible slices. + data = data ? data.filter(e => parseFloat(e.size) > EPSILON) : null; + + // If there's no data available, display an empty placeholder. + if (!data || !data.length) { + data = emptyPieChartData; + isPlaceholder = true; + } + + let container = document.createElementNS(SVG_NS, "svg"); + container.setAttribute("class", "generic-chart-container pie-chart-container"); + container.setAttribute("pack", "center"); + container.setAttribute("flex", "1"); + container.setAttribute("width", width); + container.setAttribute("height", height); + container.setAttribute("viewBox", "0 0 " + width + " " + height); + container.setAttribute("slices", data.length); + container.setAttribute("placeholder", isPlaceholder); + + let proxy = new PieChart(container); + + let total = data.reduce((acc, e) => acc + parseFloat(e.size), 0); + let angles = data.map(e => parseFloat(e.size) / total * (TAU - EPSILON)); + let largest = data.reduce((a, b) => parseFloat(a.size) > parseFloat(b.size) ? a : b); + let smallest = data.reduce((a, b) => parseFloat(a.size) < parseFloat(b.size) ? a : b); + + let textDistance = radius / NAMED_SLICE_TEXT_DISTANCE_RATIO; + let translateDistance = radius / HOVERED_SLICE_TRANSLATE_DISTANCE_RATIO; + let startAngle = TAU; + let endAngle = 0; + let midAngle = 0; + radius -= translateDistance; + + for (let i = data.length - 1; i >= 0; i--) { + let sliceInfo = data[i]; + let sliceAngle = angles[i]; + if (!sliceInfo.size || sliceAngle < EPSILON) { + continue; + } + + endAngle = startAngle - sliceAngle; + midAngle = (startAngle + endAngle) / 2; + + let x1 = centerX + radius * Math.sin(startAngle); + let y1 = centerY - radius * Math.cos(startAngle); + let x2 = centerX + radius * Math.sin(endAngle); + let y2 = centerY - radius * Math.cos(endAngle); + let largeArcFlag = Math.abs(startAngle - endAngle) > PI ? 1 : 0; + + let pathNode = document.createElementNS(SVG_NS, "path"); + pathNode.setAttribute("class", "pie-chart-slice chart-colored-blob"); + pathNode.setAttribute("name", sliceInfo.label); + pathNode.setAttribute("d", + " M " + centerX + "," + centerY + + " L " + x2 + "," + y2 + + " A " + radius + "," + radius + + " 0 " + largeArcFlag + + " 1 " + x1 + "," + y1 + + " Z"); + + if (sliceInfo == largest) { + pathNode.setAttribute("largest", ""); + } + if (sliceInfo == smallest) { + pathNode.setAttribute("smallest", ""); + } + + let hoverX = translateDistance * Math.sin(midAngle); + let hoverY = -translateDistance * Math.cos(midAngle); + let hoverTransform = "transform: translate(" + hoverX + "px, " + hoverY + "px)"; + pathNode.setAttribute("style", hoverTransform); + + proxy.slices.set(sliceInfo, pathNode); + delegate(proxy, ["click", "mouseenter", "mouseleave"], pathNode, sliceInfo); + container.appendChild(pathNode); + + if (sliceInfo.label && sliceAngle > NAMED_SLICE_MIN_ANGLE) { + let textX = centerX + textDistance * Math.sin(midAngle); + let textY = centerY - textDistance * Math.cos(midAngle); + let label = document.createElementNS(SVG_NS, "text"); + label.appendChild(document.createTextNode(sliceInfo.label)); + label.setAttribute("class", "pie-chart-label"); + label.setAttribute("style", data.length > 1 ? hoverTransform : ""); + label.setAttribute("x", data.length > 1 ? textX : centerX); + label.setAttribute("y", data.length > 1 ? textY : centerY); + container.appendChild(label); + } + + startAngle = endAngle; + } + + return proxy; +} + +/** + * Creates the DOM for a table chart based on the specified properties. + * + * @param nsIDocument document + * The document responsible with creating the DOM. + * @param object + * An object containing all or some of the following properties: + * - title: a string displayed as the chart's (description)/local + * - data: an array of items used to display each row; all the items + * should be objects representing columns, for which the + * properties' values will be displayed in each cell of a row. + * e.g: [{ + * size: 1, + * label2: "1foo", + * label3: "2yolo" + * }, { + * size: 2, + * label2: "3bar", + * label3: "4swag" + * }]; + * - totals: an object specifying for which rows in the `data` array + * the sum of their cells is to be displayed in the chart; + * e.g: { + * label1: "Total size: %S", + * label3: "Total lolz: %S" + * } + * @return TableChart + * A table chart proxy instance, which emits the following events: + * - "mouseenter", when the mouse enters a row + * - "mouseleave", when the mouse leaves a row + * - "click", when the mouse clicks a row + */ +function createTableChart(document, { data, totals, title }) { + let isPlaceholder = false; + + // If there's no data available, display an empty placeholder. + if (!data || !data.length) { + data = emptyTableChartData; + isPlaceholder = true; + } + + let container = document.createElement("vbox"); + container.className = "generic-chart-container table-chart-container"; + container.setAttribute("pack", "center"); + container.setAttribute("flex", "1"); + container.setAttribute("rows", data.length); + container.setAttribute("placeholder", isPlaceholder); + + let proxy = new TableChart(container); + + let titleNode = document.createElement("label"); + titleNode.className = "plain table-chart-title"; + titleNode.setAttribute("value", title); + container.appendChild(titleNode); + + let tableNode = document.createElement("vbox"); + tableNode.className = "plain table-chart-grid"; + container.appendChild(tableNode); + + for (let rowInfo of data) { + let rowNode = document.createElement("hbox"); + rowNode.className = "table-chart-row"; + rowNode.setAttribute("align", "center"); + + let boxNode = document.createElement("hbox"); + boxNode.className = "table-chart-row-box chart-colored-blob"; + boxNode.setAttribute("name", rowInfo.label); + rowNode.appendChild(boxNode); + + for (let [key, value] in Iterator(rowInfo)) { + let labelNode = document.createElement("label"); + labelNode.className = "plain table-chart-row-label"; + labelNode.setAttribute("name", key); + labelNode.setAttribute("value", value); + rowNode.appendChild(labelNode); + } + + proxy.rows.set(rowInfo, rowNode); + delegate(proxy, ["click", "mouseenter", "mouseleave"], rowNode, rowInfo); + tableNode.appendChild(rowNode); + } + + let totalsNode = document.createElement("vbox"); + totalsNode.className = "table-chart-totals"; + + for (let [key, value] in Iterator(totals || {})) { + let total = data.reduce((acc, e) => acc + parseFloat(e[key]), 0); + let formatted = !isNaN(total) ? L10N.numberWithDecimals(total, 2) : 0; + let labelNode = document.createElement("label"); + labelNode.className = "plain table-chart-summary-label"; + labelNode.setAttribute("name", key); + labelNode.setAttribute("value", value.replace("%S", formatted)); + totalsNode.appendChild(labelNode); + } + + container.appendChild(totalsNode); + + return proxy; +} + +XPCOMUtils.defineLazyGetter(this, "emptyPieChartData", () => { + return [{ size: 1, label: L10N.getStr("pieChart.empty") }]; +}); + +XPCOMUtils.defineLazyGetter(this, "emptyTableChartData", () => { + return [{ size: "", label: L10N.getStr("tableChart.empty") }]; +}); + +/** + * Delegates DOM events emitted by an nsIDOMNode to an EventEmitter proxy. + * + * @param EventEmitter emitter + * The event emitter proxy instance. + * @param array events + * An array of events, e.g. ["mouseenter", "mouseleave"]. + * @param nsIDOMNode node + * The element firing the DOM events. + * @param any args + * The arguments passed when emitting events through the proxy. + */ +function delegate(emitter, events, node, args) { + for (let event of events) { + node.addEventListener(event, emitter.emit.bind(emitter, event, args)); + } +}
--- a/browser/devtools/shared/widgets/Tooltip.js +++ b/browser/devtools/shared/widgets/Tooltip.js @@ -29,16 +29,17 @@ XPCOMUtils.defineLazyModuleGetter(this, const GRADIENT_RE = /\b(repeating-)?(linear|radial)-gradient\(((rgb|hsl)a?\(.+?\)|[^\)])+\)/gi; const BORDERCOLOR_RE = /^border-[-a-z]*color$/ig; const BORDER_RE = /^border(-(top|bottom|left|right))?$/ig; const BACKGROUND_IMAGE_RE = /url\([\'\"]?(.*?)[\'\"]?\)/; const XHTML_NS = "http://www.w3.org/1999/xhtml"; const SPECTRUM_FRAME = "chrome://browser/content/devtools/spectrum-frame.xhtml"; const ESCAPE_KEYCODE = Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE; const ENTER_KEYCODE = Ci.nsIDOMKeyEvent.DOM_VK_RETURN; +const POPUP_EVENTS = ["shown", "hidden", "showing", "hiding"]; /** * Tooltip widget. * * This widget is intended at any tool that may need to show rich content in the * form of floating panels. * A common use case is image previewing in the CSS rule view, but more complex * use cases may include color pickers, object inspection, etc... @@ -131,48 +132,62 @@ let PanelFactory = { * return true; * } * }); * t.destroy(); * * @param {XULDocument} doc * The XUL document hosting this tooltip * @param {Object} options - * Optional options that give options to consumers + * Optional options that give options to consumers: * - consumeOutsideClick {Boolean} Wether the first click outside of the * tooltip should close the tooltip and be consumed or not. - * Defaults to false + * Defaults to false. * - closeOnKeys {Array} An array of key codes that should close the - * tooltip. Defaults to [27] (escape key) + * tooltip. Defaults to [27] (escape key). + * - closeOnEvents [{emitter: {Object}, event: {String}, useCapture: {Boolean}}] + * Provide an optional list of emitter objects and event names here to + * trigger the closing of the tooltip when these events are fired by the + * emitters. The emitter objects should either implement on/off(event, cb) + * or addEventListener/removeEventListener(event, cb). Defaults to []. + * For instance, the following would close the tooltip whenever the + * toolbox selects a new tool and when a DOM node gets scrolled: + * new Tooltip(doc, { + * closeOnEvents: [ + * {emitter: toolbox, event: "select"}, + * {emitter: myContainer, event: "scroll", useCapture: true} + * ] + * }); * - noAutoFocus {Boolean} Should the focus automatically go to the panel - * when it opens. Defaults to true + * when it opens. Defaults to true. * * Fires these events: * - showing : just before the tooltip shows * - shown : when the tooltip is shown * - hiding : just before the tooltip closes * - hidden : when the tooltip gets hidden * - keypress : when any key gets pressed, with keyCode */ function Tooltip(doc, options) { EventEmitter.decorate(this); this.doc = doc; this.options = new OptionsStore({ consumeOutsideClick: false, closeOnKeys: [ESCAPE_KEYCODE], - noAutoFocus: true + noAutoFocus: true, + closeOnEvents: [] }, options); this.panel = PanelFactory.get(doc, this.options); // Used for namedTimeouts in the mouseover handling this.uid = "tooltip-" + Date.now(); // Emit show/hide events - for (let event of ["shown", "hidden", "showing", "hiding"]) { + for (let event of POPUP_EVENTS) { this["_onPopup" + event] = ((e) => { return () => this.emit(e); })(event); this.panel.addEventListener("popup" + event, this["_onPopup" + event], false); } // Listen to keypress events to close the tooltip if configured to do so @@ -182,16 +197,28 @@ function Tooltip(doc, options) { if (this.options.get("closeOnKeys").indexOf(event.keyCode) !== -1) { if (!this.panel.hidden) { event.stopPropagation(); } this.hide(); } }; win.addEventListener("keypress", this._onKeyPress, false); + + // Listen to custom emitters' events to close the tooltip + this.hide = this.hide.bind(this); + let closeOnEvents = this.options.get("closeOnEvents"); + for (let {emitter, event, useCapture} of closeOnEvents) { + for (let add of ["addEventListener", "on"]) { + if (add in emitter) { + emitter[add](event, this.hide, useCapture); + break; + } + } + } } module.exports.Tooltip = Tooltip; Tooltip.prototype = { defaultPosition: "before_start", defaultOffsetX: 0, // px defaultOffsetY: 0, // px @@ -259,24 +286,34 @@ Tooltip.prototype = { }, /** * Get rid of references and event listeners */ destroy: function () { this.hide(); - for (let event of ["shown", "hidden", "showing", "hiding"]) { + for (let event of POPUP_EVENTS) { this.panel.removeEventListener("popup" + event, this["_onPopup" + event], false); } let win = this.doc.querySelector("window"); win.removeEventListener("keypress", this._onKeyPress, false); + let closeOnEvents = this.options.get("closeOnEvents"); + for (let {emitter, event, useCapture} of closeOnEvents) { + for (let remove of ["removeEventListener", "off"]) { + if (remove in emitter) { + emitter[remove](event, this.hide, useCapture); + break; + } + } + } + this.content = null; if (this._basedNode) { this.stopTogglingOnHover(); } this.doc = null; @@ -471,23 +508,27 @@ Tooltip.prototype = { * Options for the variables view controller. * @param {object} relayEvents [optional] * A collection of events to listen on the variables view widget. * For example, { fetched: () => ... } * @param {boolean} reuseCachedWidget [optional] * Pass false to instantiate a brand new widget for this variable. * Otherwise, if a variable was previously inspected, its widget * will be reused. + * @param {Toolbox} toolbox [optional] + * Pass the instance of the current toolbox if you want the variables + * view widget to allow highlighting and selection of DOM nodes */ setVariableContent: function( objectActor, viewOptions = {}, controllerOptions = {}, relayEvents = {}, - extraButtons = []) { + extraButtons = [], + toolbox = null) { let vbox = this.doc.createElement("vbox"); vbox.className = "devtools-tooltip-variables-view-box"; vbox.setAttribute("flex", "1"); let innerbox = this.doc.createElement("vbox"); innerbox.className = "devtools-tooltip-variables-view-innerbox"; innerbox.setAttribute("flex", "1"); @@ -498,16 +539,21 @@ Tooltip.prototype = { button.className = className; button.setAttribute("label", label); button.addEventListener("command", command); vbox.appendChild(button); } let widget = new VariablesView(innerbox, viewOptions); + // If a toolbox was provided, link it to the vview + if (toolbox) { + widget.toolbox = toolbox; + } + // Analyzing state history isn't useful with transient object inspectors. widget.commitHierarchy = () => {}; for (let e in relayEvents) widget.on(e, relayEvents[e]); VariablesViewController.attach(widget, controllerOptions); // Some of the view options are allowed to change between uses. widget.searchPlaceholder = viewOptions.searchPlaceholder;
--- a/browser/devtools/shared/widgets/VariablesView.jsm +++ b/browser/devtools/shared/widgets/VariablesView.jsm @@ -18,16 +18,18 @@ const PAGE_SIZE_MAX_JUMPS = 30; const SEARCH_ACTION_MAX_DELAY = 300; // ms const ITEM_FLASH_DURATION = 300 // ms Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); Cu.import("resource:///modules/devtools/shared/event-emitter.js"); Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js").Promise; XPCOMUtils.defineLazyModuleGetter(this, "devtools", "resource://gre/modules/devtools/Loader.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm"); XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper", @@ -206,16 +208,22 @@ VariablesView.prototype = { if (!this._store.length) { this._appendEmptyNotice(); this._toggleSearchVisibility(false); } }, aTimeout); }, /** + * Optional DevTools toolbox containing this VariablesView. Used to + * communicate with the inspector and highlighter. + */ + toolbox: null, + + /** * The controller for this VariablesView, if it has one. */ controller: null, /** * The amount of time (in milliseconds) it takes to empty this view lazily. */ lazyEmptyDelay: LAZY_EMPTY_DELAY, @@ -320,16 +328,25 @@ VariablesView.prototype = { * in order to change the variable or property to a plain value. * * This flag is applied recursively onto each scope in this view and * affects only the child nodes when they're created. */ editButtonTooltip: STR.GetStringFromName("variablesEditButtonTooltip"), /** + * The tooltip text shown on a variable or property's value if that value is + * a DOMNode that can be highlighted and selected in the inspector. + * + * This flag is applied recursively onto each scope in this view and + * affects only the child nodes when they're created. + */ + domNodeValueTooltip: STR.GetStringFromName("variablesDomNodeValueTooltip"), + + /** * The tooltip text shown on a variable or property's delete button if a * |delete| function is provided, in order to delete the variable or property. * * This flag is applied recursively onto each scope in this view and * affects only the child nodes when they're created. */ deleteButtonTooltip: STR.GetStringFromName("variablesCloseButtonTooltip"), @@ -1204,16 +1221,17 @@ function Scope(aView, aName, aFlags = {} this.delete = aView.delete; this.new = aView.new; this.preventDisableOnChange = aView.preventDisableOnChange; this.preventDescriptorModifiers = aView.preventDescriptorModifiers; this.editableNameTooltip = aView.editableNameTooltip; this.editableValueTooltip = aView.editableValueTooltip; this.editButtonTooltip = aView.editButtonTooltip; this.deleteButtonTooltip = aView.deleteButtonTooltip; + this.domNodeValueTooltip = aView.domNodeValueTooltip; this.contextMenuId = aView.contextMenuId; this.separatorStr = aView.separatorStr; this._init(aName.trim(), aFlags); } Scope.prototype = { /** @@ -2064,16 +2082,17 @@ Scope.prototype = { delete: null, new: null, preventDisableOnChange: false, preventDescriptorModifiers: false, editableNameTooltip: "", editableValueTooltip: "", editButtonTooltip: "", deleteButtonTooltip: "", + domNodeValueTooltip: "", contextMenuId: "", separatorStr: "", _store: null, _enumItems: null, _nonEnumItems: null, _fetched: false, _committed: false, @@ -2114,16 +2133,19 @@ XPCOMUtils.defineLazyGetter(Scope, "elli * The variable's name. * @param object aDescriptor * The variable's descriptor. */ function Variable(aScope, aName, aDescriptor) { this._setTooltips = this._setTooltips.bind(this); this._activateNameInput = this._activateNameInput.bind(this); this._activateValueInput = this._activateValueInput.bind(this); + this.openNodeInInspector = this.openNodeInInspector.bind(this); + this.highlightDomNode = this.highlightDomNode.bind(this); + this.unhighlightDomNode = this.unhighlightDomNode.bind(this); // Treat safe getter descriptors as descriptors with a value. if ("getterValue" in aDescriptor) { aDescriptor.value = aDescriptor.getterValue; delete aDescriptor.get; delete aDescriptor.set; } @@ -2166,16 +2188,23 @@ Variable.prototype = Heritage.extend(Sco _createChild: function(aName, aDescriptor) { return new Property(this, aName, aDescriptor); }, /** * Remove this Variable from its parent and remove all children recursively. */ remove: function() { + if (this._linkedToInspector) { + this.unhighlightDomNode(); + this._valueLabel.removeEventListener("mouseover", this.highlightDomNode, false); + this._valueLabel.removeEventListener("mouseout", this.unhighlightDomNode, false); + this._openInspectorNode.removeEventListener("mousedown", this.openNodeInInspector, false); + } + this.ownerView._store.delete(this._nameString); this._variablesView._itemsByElement.delete(this._target); this._variablesView._currHierarchy.delete(this._absoluteName); this._target.remove(); for (let property of this._store.values()) { property.remove(); @@ -2380,16 +2409,21 @@ Variable.prototype = Heritage.extend(Sco concise: true, noEllipsis: true, }); this._valueClassName = VariablesView.getClass(aGrip); this._valueLabel.classList.add(this._valueClassName); this._valueLabel.setAttribute("value", this._valueString); this._separatorLabel.hidden = false; + + // DOMNodes get special treatment since they can be linked to the inspector + if (this._valueGrip.preview && this._valueGrip.preview.kind === "DOMNode") { + this._linkToInspector(); + } }, /** * Marks this variable as overridden. * * @param boolean aFlag * Whether this variable is overridden or not. */ @@ -2550,17 +2584,17 @@ Variable.prototype = Heritage.extend(Sco } if (ownerView.preventDescriptorModifiers) { return; } if (!descriptor.writable && !ownerView.getter && !ownerView.setter) { let nonWritableIcon = this.document.createElement("hbox"); - nonWritableIcon.className = "variable-or-property-non-writable-icon"; + nonWritableIcon.className = "plain variable-or-property-non-writable-icon"; nonWritableIcon.setAttribute("optional-visibility", ""); this._title.appendChild(nonWritableIcon); } if (descriptor.value && typeof descriptor.value == "object") { if (descriptor.value.frozen) { let frozenLabel = this.document.createElement("label"); frozenLabel.className = "plain variable-or-property-frozen-label"; frozenLabel.setAttribute("optional-visibility", ""); @@ -2618,28 +2652,135 @@ Variable.prototype = Heritage.extend(Sco } this._target.appendChild(tooltip); this._target.setAttribute("tooltip", tooltip.id); if (this._editNode && ownerView.eval) { this._editNode.setAttribute("tooltiptext", ownerView.editButtonTooltip); } + if (this._openInspectorNode && this._linkedToInspector) { + this._openInspectorNode.setAttribute("tooltiptext", this.ownerView.domNodeValueTooltip); + } if (this._valueLabel && ownerView.eval) { this._valueLabel.setAttribute("tooltiptext", ownerView.editableValueTooltip); } if (this._name && ownerView.switch) { this._name.setAttribute("tooltiptext", ownerView.editableNameTooltip); } if (this._deleteNode && ownerView.delete) { this._deleteNode.setAttribute("tooltiptext", ownerView.deleteButtonTooltip); } }, /** + * Get the parent variablesview toolbox, if any. + */ + get toolbox() { + return this._variablesView.toolbox; + }, + + /** + * Checks if this variable is a DOMNode and is part of a variablesview that + * has been linked to the toolbox, so that highlighting and jumping to the + * inspector can be done. + */ + _isLinkableToInspector: function() { + let isDomNode = this._valueGrip && this._valueGrip.preview.kind === "DOMNode"; + let hasBeenLinked = this._linkedToInspector; + let hasToolbox = !!this.toolbox; + + return isDomNode && !hasBeenLinked && hasToolbox; + }, + + /** + * If the variable is a DOMNode, and if a toolbox is set, then link it to the + * inspector (highlight on hover, and jump to markup-view on click) + */ + _linkToInspector: function() { + if (!this._isLinkableToInspector()) { + return; + } + + // Listen to value mouseover/click events to highlight and jump + this._valueLabel.addEventListener("mouseover", this.highlightDomNode, false); + this._valueLabel.addEventListener("mouseout", this.unhighlightDomNode, false); + + // Add a button to open the node in the inspector + this._openInspectorNode = this.document.createElement("toolbarbutton"); + this._openInspectorNode.className = "plain variables-view-open-inspector"; + this._openInspectorNode.addEventListener("mousedown", this.openNodeInInspector, false); + this._title.insertBefore(this._openInspectorNode, this._title.querySelector("toolbarbutton")); + + this._linkedToInspector = true; + }, + + /** + * In case this variable is a DOMNode and part of a variablesview that has been + * linked to the toolbox's inspector, then select the corresponding node in + * the inspector, and switch the inspector tool in the toolbox + * @return a promise that resolves when the node is selected and the inspector + * has been switched to and is ready + */ + openNodeInInspector: function(event) { + if (!this.toolbox) { + return promise.reject(new Error("Toolbox not available")); + } + + event && event.stopPropagation(); + + return Task.spawn(function*() { + yield this.toolbox.initInspector(); + + let nodeFront = this._nodeFront; + if (!nodeFront) { + nodeFront = yield this.toolbox.walker.getNodeActorFromObjectActor(this._valueGrip.actor); + } + + if (nodeFront) { + yield this.toolbox.selectTool("inspector"); + + let inspectorReady = promise.defer(); + this.toolbox.getPanel("inspector").once("inspector-updated", inspectorReady.resolve); + yield this.toolbox.selection.setNodeFront(nodeFront, "variables-view"); + yield inspectorReady.promise; + } + }.bind(this)); + }, + + /** + * In case this variable is a DOMNode and part of a variablesview that has been + * linked to the toolbox's inspector, then highlight the corresponding node + */ + highlightDomNode: function() { + if (this.toolbox) { + if (this._nodeFront) { + // If the nodeFront has been retrieved before, no need to ask the server + // again for it + this.toolbox.highlighterUtils.highlightNodeFront(this._nodeFront); + return; + } + + this.toolbox.highlighterUtils.highlightDomValueGrip(this._valueGrip).then(front => { + this._nodeFront = front; + }); + } + }, + + /** + * Unhighlight a previously highlit node + * @see highlightDomNode + */ + unhighlightDomNode: function() { + if (this.toolbox) { + this.toolbox.highlighterUtils.unhighlight(); + } + }, + + /** * Sets a variable's configurable, enumerable and writable attributes, * and specifies if it's a 'this', '<exception>', '<return>' or '__proto__' * reference. */ _setAttributes: function() { let ownerView = this.ownerView; if (ownerView.preventDescriptorModifiers) { return; @@ -2735,16 +2876,19 @@ Variable.prototype = Heritage.extend(Sco }, /** * Makes this variable's value editable. */ _activateValueInput: function(e) { EditableValue.create(this, { onSave: aString => { + if (this._linkedToInspector) { + this.unhighlightDomNode(); + } if (!this._variablesView.preventDisableOnChange) { this._disable(); } this.ownerView.eval(this, aString); } }, e); }, @@ -3568,16 +3712,23 @@ VariablesView.stringifiers._getNMoreStri * * @param any aGrip * @see Variable.setGrip * @return string * The custom class style. */ VariablesView.getClass = function(aGrip) { if (aGrip && typeof aGrip == "object") { + if (aGrip.preview) { + switch (aGrip.preview.kind) { + case "DOMNode": + return "token-domnode"; + } + } + switch (aGrip.type) { case "undefined": return "token-undefined"; case "null": return "token-null"; case "Infinity": case "-Infinity": case "NaN":
--- a/browser/devtools/shared/widgets/widgets.css +++ b/browser/devtools/shared/widgets/widgets.css @@ -74,30 +74,33 @@ .variable-or-property:not([non-extensible]) > tooltip > label.extensible, .variable-or-property:not([frozen]) > tooltip > label.frozen, .variable-or-property:not([sealed]) > tooltip > label.sealed { display: none; } .variable-or-property[pseudo-item] > tooltip, .variable-or-property[pseudo-item] > .title > .variables-view-edit, +.variable-or-property[pseudo-item] > .title > .variables-view-open-inspector, .variable-or-property[pseudo-item] > .title > .variables-view-delete, .variable-or-property[pseudo-item] > .title > .variables-view-add-property, .variable-or-property[pseudo-item] > .title > .variable-or-property-frozen-label, .variable-or-property[pseudo-item] > .title > .variable-or-property-sealed-label, .variable-or-property[pseudo-item] > .title > .variable-or-property-non-extensible-label, .variable-or-property[pseudo-item] > .title > .variable-or-property-non-writable-icon { display: none; } *:not(:hover) .variables-view-delete, +*:not(:hover) .variables-view-open-inspector, *:not(:hover) .variables-view-add-property { visibility: hidden; } .variables-view-delete > .toolbarbutton-text, +.variables-view-open-inspector > .toolbarbutton-text, .variables-view-add-property > .toolbarbutton-text { display: none; } .variables-view-container[aligned-values] [optional-visibility] { display: none; }
--- a/browser/devtools/styleeditor/StyleEditorUI.jsm +++ b/browser/devtools/styleeditor/StyleEditorUI.jsm @@ -9,16 +9,17 @@ this.EXPORTED_SYMBOLS = ["StyleEditorUI" const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/PluralForm.jsm"); Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://gre/modules/osfile.jsm"); let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js").Promise; Cu.import("resource:///modules/devtools/shared/event-emitter.js"); Cu.import("resource:///modules/devtools/gDevTools.jsm"); Cu.import("resource:///modules/devtools/StyleEditorUtil.jsm"); Cu.import("resource:///modules/devtools/SplitView.jsm"); Cu.import("resource:///modules/devtools/StyleSheetEditor.jsm"); const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require; @@ -222,16 +223,17 @@ StyleEditorUI.prototype = { } styleSheet.getOriginalSources().then((sources) => { if (sources && sources.length) { this._removeStyleSheetEditor(editor); sources.forEach((source) => { // set so the first sheet will be selected, even if it's a source source.styleSheetIndex = styleSheet.styleSheetIndex; + source.relatedStyleSheet = styleSheet; this._addStyleSheetEditor(source); }); } }); }, /** @@ -245,16 +247,18 @@ StyleEditorUI.prototype = { * Optional if stylesheet is a new sheet created by user */ _addStyleSheetEditor: function(styleSheet, file, isNew) { let editor = new StyleSheetEditor(styleSheet, this._window, file, isNew, this._walker); editor.on("property-change", this._summaryChange.bind(this, editor)); editor.on("style-applied", this._summaryChange.bind(this, editor)); + editor.on("linked-css-file", this._summaryChange.bind(this, editor)); + editor.on("linked-css-file-error", this._summaryChange.bind(this, editor)); editor.on("error", this._onError); this.editors.push(editor); editor.fetchSource(this._sourceLoaded.bind(this, editor)); return editor; }, @@ -552,37 +556,48 @@ StyleEditorUI.prototype = { * Optional item's summary element to update. If none, item corresponding * to passed editor is used. */ _updateSummaryForEditor: function(editor, summary) { summary = summary || editor.summary; if (!summary) { return; } - let ruleCount = "-"; - if (editor.styleSheet.ruleCount !== undefined) { - ruleCount = editor.styleSheet.ruleCount; + + let ruleCount = editor.styleSheet.ruleCount; + if (editor.styleSheet.relatedStyleSheet) { + ruleCount = editor.styleSheet.relatedStyleSheet.ruleCount; + } + if (ruleCount === undefined) { + ruleCount = "-"; } var flags = []; if (editor.styleSheet.disabled) { flags.push("disabled"); } if (editor.unsaved) { flags.push("unsaved"); } + if (editor.linkedCSSFileError) { + flags.push("linked-file-error"); + } this._view.setItemClassName(summary, flags.join(" ")); let label = summary.querySelector(".stylesheet-name > label"); label.setAttribute("value", editor.friendlyName); + let linkedCSSFile = ""; + if (editor.linkedCSSFile) { + linkedCSSFile = OS.Path.basename(editor.linkedCSSFile); + } + text(summary, ".stylesheet-linked-file", linkedCSSFile); text(summary, ".stylesheet-title", editor.styleSheet.title || ""); text(summary, ".stylesheet-rule-count", PluralForm.get(ruleCount, _("ruleCount.label")).replace("#1", ruleCount)); - text(summary, ".stylesheet-error-message", editor.errorMessage); }, destroy: function() { this._clearStyleSheetEditors(); this._prefObserver.off(PREF_ORIG_SOURCES, this._onNewDocument); this._prefObserver.destroy(); }
--- a/browser/devtools/styleeditor/StyleSheetEditor.jsm +++ b/browser/devtools/styleeditor/StyleSheetEditor.jsm @@ -29,16 +29,23 @@ const SAVE_ERROR = "error-save"; // max update frequency in ms (avoid potential typing lag and/or flicker) // @see StyleEditor.updateStylesheet const UPDATE_STYLESHEET_THROTTLE_DELAY = 500; // Pref which decides if CSS autocompletion is enabled in Style Editor or not. const AUTOCOMPLETION_PREF = "devtools.styleeditor.autocompletion-enabled"; +// How long to wait to update linked CSS file after original source was saved +// to disk. Time in ms. +const CHECK_LINKED_SHEET_DELAY=500; + +// How many times to check for linked file changes +const MAX_CHECK_COUNT=10; + /** * StyleSheetEditor controls the editor linked to a particular StyleSheet * object. * * Emits events: * 'property-change': A property on the underlying stylesheet has changed * 'source-editor-load': The source editor for this editor has been loaded * 'error': An error has occured @@ -54,53 +61,52 @@ const AUTOCOMPLETION_PREF = "devtools.st * @param {Walker} walker * Optional walker used for selectors autocompletion */ function StyleSheetEditor(styleSheet, win, file, isNew, walker) { EventEmitter.decorate(this); this.styleSheet = styleSheet; this._inputElement = null; - this._sourceEditor = null; + this.sourceEditor = null; this._window = win; this._isNew = isNew; - this.savedFile = file; this.walker = walker; - this.errorMessage = null; - - let readOnly = false; - if (styleSheet.isOriginalSource) { - // live-preview won't work with sources that need compilation - readOnly = true; - } - this._state = { // state to use when inputElement attaches text: "", selection: { start: {line: 0, ch: 0}, end: {line: 0, ch: 0} }, - readOnly: readOnly, - topIndex: 0, // the first visible line + topIndex: 0 // the first visible line }; this._styleSheetFilePath = null; if (styleSheet.href && Services.io.extractScheme(this.styleSheet.href) == "file") { this._styleSheetFilePath = this.styleSheet.href; } this._onPropertyChange = this._onPropertyChange.bind(this); this._onError = this._onError.bind(this); + this.checkLinkedFileForChanges = this.checkLinkedFileForChanges.bind(this); + this.markLinkedFileBroken = this.markLinkedFileBroken.bind(this); this._focusOnSourceEditorReady = false; + let relatedSheet = this.styleSheet.relatedStyleSheet; + if (relatedSheet) { + relatedSheet.on("property-change", this._onPropertyChange); + } this.styleSheet.on("property-change", this._onPropertyChange); this.styleSheet.on("error", this._onError); + + this.savedFile = file; + this.linkCSSFile(); } StyleSheetEditor.prototype = { /** * Whether there are unsaved changes in the editor */ get unsaved() { return this.sourceEditor && !this.sourceEditor.isClean(); @@ -109,16 +115,26 @@ StyleSheetEditor.prototype = { /** * Whether the editor is for a stylesheet created by the user * through the style editor UI. */ get isNew() { return this._isNew; }, + get savedFile() { + return this._savedFile; + }, + + set savedFile(name) { + this._savedFile = name; + + this.linkCSSFile(); + }, + /** * Get a user-friendly name for the style sheet. * * @return string */ get friendlyName() { if (this.savedFile) { return this.savedFile.leafName; @@ -141,16 +157,58 @@ StyleSheetEditor.prototype = { this._friendlyName = decodeURI(this._friendlyName); } catch (ex) { } } return this._friendlyName; }, /** + * If this is an original source, get the path of the CSS file it generated. + */ + linkCSSFile: function() { + if (!this.styleSheet.isOriginalSource) { + return; + } + + let relatedSheet = this.styleSheet.relatedStyleSheet; + + let path; + var uri = NetUtil.newURI(relatedSheet.href); + + if (uri.scheme == "file") { + var file = uri.QueryInterface(Ci.nsIFileURL).file; + path = file.path; + } + else if (this.savedFile) { + let origUri = NetUtil.newURI(this.styleSheet.href); + path = findLinkedFilePath(uri, origUri, this.savedFile); + } + else { + // we can't determine path to generated file on disk + return; + } + + if (this.linkedCSSFile == path) { + return; + } + + this.linkedCSSFile = path; + + this.linkedCSSFileError = null; + + // save last file change time so we can compare when we check for changes. + OS.File.stat(path).then((info) => { + this._fileModDate = info.lastModificationDate.getTime(); + }, this.markLinkedFileBroken); + + this.emit("linked-css-file"); + }, + + /** * Start fetching the full text source for this editor's sheet. */ fetchSource: function(callback) { this.styleSheet.getText().then((longStr) => { longStr.string().then((source) => { this._state.text = prettifyCSS(source); this.sourceLoaded = true; @@ -191,35 +249,37 @@ StyleSheetEditor.prototype = { */ load: function(inputElement) { this._inputElement = inputElement; let config = { value: this._state.text, lineNumbers: true, mode: Editor.modes.css, - readOnly: this._state.readOnly, + readOnly: false, autoCloseBrackets: "{}()[]", extraKeys: this._getKeyBindings(), contextMenu: "sourceEditorContextMenu" }; let sourceEditor = new Editor(config); sourceEditor.appendTo(inputElement).then(() => { if (Services.prefs.getBoolPref(AUTOCOMPLETION_PREF)) { sourceEditor.extend(AutoCompleter); sourceEditor.setupAutoCompletion(this.walker); } sourceEditor.on("save", () => { this.saveToFile(); }); - sourceEditor.on("change", () => { - this.updateStyleSheet(); - }); + if (this.styleSheet.update) { + sourceEditor.on("change", () => { + this.updateStyleSheet(); + }); + } this.sourceEditor = sourceEditor; if (this._focusOnSourceEditorReady) { this._focusOnSourceEditorReady = false; sourceEditor.focus(); } @@ -250,29 +310,29 @@ StyleSheetEditor.prototype = { }); return deferred.promise; }, /** * Focus the Style Editor input. */ focus: function() { - if (this._sourceEditor) { - this._sourceEditor.focus(); + if (this.sourceEditor) { + this.sourceEditor.focus(); } else { this._focusOnSourceEditorReady = true; } }, /** * Event handler for when the editor is shown. */ onShow: function() { - if (this._sourceEditor) { - this._sourceEditor.setFirstVisibleLine(this._state.topIndex); + if (this.sourceEditor) { + this.sourceEditor.setFirstVisibleLine(this._state.topIndex); } this.focus(); }, /** * Toggled the disabled state of the underlying stylesheet. */ toggleDisabled: function() { @@ -338,18 +398,18 @@ StyleSheetEditor.prototype = { let onFile = (returnFile) => { if (!returnFile) { if (callback) { callback(null); } return; } - if (this._sourceEditor) { - this._state.text = this._sourceEditor.getText(); + if (this.sourceEditor) { + this._state.text = this.sourceEditor.getText(); } let ostream = FileUtils.openSafeFileOutputStream(returnFile); let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] .createInstance(Ci.nsIScriptableUnicodeConverter); converter.charset = "UTF-8"; let istream = converter.convertToInputStream(this._state.text); @@ -357,36 +417,106 @@ StyleSheetEditor.prototype = { if (!Components.isSuccessCode(status)) { if (callback) { callback(null); } this.emit("error", SAVE_ERROR); return; } FileUtils.closeSafeFileOutputStream(ostream); - // remember filename for next save if any - this._friendlyName = null; - this.savedFile = returnFile; + + this.onFileSaved(returnFile); if (callback) { callback(returnFile); } - this.sourceEditor.setClean(); - - this.emit("property-change"); }.bind(this)); }; let defaultName; if (this._friendlyName) { defaultName = OS.Path.basename(this._friendlyName); } showFilePicker(file || this._styleSheetFilePath, true, this._window, onFile, defaultName); - }, + }, + + /** + * Called when this source has been successfully saved to disk. + */ + onFileSaved: function(returnFile) { + this._friendlyName = null; + this.savedFile = returnFile; + + this.sourceEditor.setClean(); + + this.emit("property-change"); + + // TODO: replace with file watching + this._modCheckCount = 0; + this._window.clearTimeout(this._timeout); + + if (this.linkedCSSFile && !this.linkedCSSFileError) { + this._timeout = this._window.setTimeout(this.checkLinkedFileForChanges, + CHECK_LINKED_SHEET_DELAY); + } + }, + + /** + * Check to see if our linked CSS file has changed on disk, and + * if so, update the live style sheet. + */ + checkLinkedFileForChanges: function() { + OS.File.stat(this.linkedCSSFile).then((info) => { + let lastChange = info.lastModificationDate.getTime(); + + if (this._fileModDate && lastChange != this._fileModDate) { + this._fileModDate = lastChange; + this._modCheckCount = 0; + + this.updateLinkedStyleSheet(); + return; + } + + if (++this._modCheckCount > MAX_CHECK_COUNT) { + this.updateLinkedStyleSheet(); + return; + } + + // try again in a bit + this._timeout = this._window.setTimeout(this.checkLinkedFileForChanges, + CHECK_LINKED_SHEET_DELAY); + }, this.markLinkedFileBroken); + }, + + /** + * Notify that the linked CSS file (if this is an original source) + * doesn't exist on disk in the place we think it does. + * + * @param string error + * The error we got when trying to access the file. + */ + markLinkedFileBroken: function(error) { + this.linkedCSSFileError = error || true; + this.emit("linked-css-file-error"); + }, + + /** + * For original sources (e.g. Sass files). Fetch contents of linked CSS + * file from disk and live update the stylesheet object with the contents. + */ + updateLinkedStyleSheet: function() { + OS.File.read(this.linkedCSSFile).then((array) => { + let decoder = new TextDecoder(); + let text = decoder.decode(array); + + let relatedSheet = this.styleSheet.relatedStyleSheet; + relatedSheet.update(text, true); + }, this.markLinkedFileBroken); + }, /** * Retrieve custom key bindings objects as expected by Editor. * Editor action names are not displayed to the user. * * @return {array} key binding objects for the source editor */ _getKeyBindings: function() { @@ -477,8 +607,78 @@ function prettifyCSS(text) if (c == "{") { indent = TAB_CHARS.repeat(++indentLevel); } } return parts.join(LINE_SEPARATOR); } +/** + * Find a path on disk for a file given it's hosted uri, the uri of the + * original resource that generated it (e.g. Sass file), and the location of the + * local file for that source. + */ +function findLinkedFilePath(uri, origUri, file) { + let project = findProjectPath(origUri, file); + let branch = findUnsharedBranch(origUri, uri); + + let parts = project.concat(branch); + let path = OS.Path.join.apply(this, parts); + + return path; +} + +/** + * Find the path of a project given a file in the project and the uri + * of that resource. e.g.: + * "http://localhost/src/a.css" and "/Users/moz/proj/src/a.css" + * would yeild ["Users", "moz", "proj"] + * + * @param {nsIURI} uri + * uri of hosted resource + * @param {nsIFile} file + * file for that resource on disk + * @return {array} + * array of path parts + */ +function findProjectPath(uri, file) { + let uri = OS.Path.split(uri.path).components; + let path = OS.Path.split(file.path).components; + + // don't care about differing leaf names + uri.pop(); + path.pop(); + + let dir = path.pop(); + while(dir) { + let serverDir = uri.pop(); + if (serverDir != dir) { + return path.concat([dir]); + } + dir = path.pop(); + } + return []; +} + +/** + * Find the part of a uri past the root it shares with another uri. e.g: + * "http://localhost/built/a.scss" and "http://localhost/src/a.css" + * would yeild ["built", "a.scss"]; + * + * @param {nsIURI} origUri + * uri to find unshared branch of + * @param {nsIURI} origUri + * uri to compare against to get a shared root + * @return {array} + * array of path parts for branch + */ +function findUnsharedBranch(origUri, uri) { + origUri = OS.Path.split(origUri.path).components; + uri = OS.Path.split(uri.path).components; + + for (var i = 0; i < uri.length - 1; i++) { + if (uri[i] != origUri[i]) { + return uri.slice(i); + } + } + return uri; +}
--- a/browser/devtools/styleeditor/styleeditor-panel.js +++ b/browser/devtools/styleeditor/styleeditor-panel.js @@ -131,17 +131,16 @@ StyleEditorPanel.prototype = { if (!this._destroyed) { this._destroyed = true; this._target.off("close", this.destroy); this._target = null; this._toolbox = null; this._panelDoc = null; - this._debuggee.destroy(); this.UI.destroy(); } return promise.resolve(null); }, } XPCOMUtils.defineLazyGetter(StyleEditorPanel.prototype, "strings",
--- a/browser/devtools/styleeditor/styleeditor.css +++ b/browser/devtools/styleeditor/styleeditor.css @@ -43,16 +43,32 @@ li.error > .stylesheet-info > .styleshee .stylesheet-name { white-space: nowrap; } li.unsaved > hgroup > h1 > .stylesheet-name:before { content: "*"; } +li.linked-file-error .stylesheet-linked-file { + text-decoration: line-through; +} + +li.linked-file-error .stylesheet-linked-file:after { + content: " ✘"; +} + +li.linked-file-error .stylesheet-rule-count { + visibility: hidden; +} + +.stylesheet-linked-file:not(:empty):before { + content: " ↳ "; +} + .stylesheet-enabled { display: -moz-box; cursor: pointer; } .stylesheet-saveButton { display: none; margin-top: 0px;
--- a/browser/devtools/styleeditor/styleeditor.xul +++ b/browser/devtools/styleeditor/styleeditor.xul @@ -106,18 +106,18 @@ <li id="splitview-tpl-summary-stylesheet" tabindex="0"> <xul:label class="stylesheet-enabled" tabindex="0" tooltiptext="&visibilityToggle.tooltip;" accesskey="&saveButton.accesskey;"></xul:label> <hgroup class="stylesheet-info"> <h1><a class="stylesheet-name" tabindex="0"><xul:label crop="start"/></a></h1> <div class="stylesheet-more"> <h3 class="stylesheet-title"></h3> + <h3 class="stylesheet-linked-file"></h3> <h3 class="stylesheet-rule-count"></h3> - <h3 class="stylesheet-error-message"></h3> <xul:spacer/> <h3><xul:label class="stylesheet-saveButton" tooltiptext="&saveButton.tooltip;" accesskey="&saveButton.accesskey;">&saveButton.label;</xul:label></h3> </div> </hgroup> </li>
--- a/browser/devtools/styleeditor/test/browser.ini +++ b/browser/devtools/styleeditor/test/browser.ini @@ -44,8 +44,9 @@ support-files = # Disabled because of intermittent failures - See Bug 942473 skip-if = true [browser_styleeditor_private_perwindowpb.js] [browser_styleeditor_reload.js] [browser_styleeditor_sv_keynav.js] [browser_styleeditor_sv_resize.js] [browser_styleeditor_selectstylesheet.js] [browser_styleeditor_sourcemaps.js] +[browser_styleeditor_sourcemap_watching.js]
new file mode 100644 --- /dev/null +++ b/browser/devtools/styleeditor/test/browser_styleeditor_sourcemap_watching.js @@ -0,0 +1,182 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +Components.utils.import("resource://gre/modules/Task.jsm"); +let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); +let promise = devtools.require("sdk/core/promise"); + +const TESTCASE_URI_HTML = TEST_BASE + "sourcemaps.html"; +const TESTCASE_URI_CSS = TEST_BASE + "sourcemaps.css"; +const TESTCASE_URI_REG_CSS = TEST_BASE + "simple.css"; +const TESTCASE_URI_SCSS = TEST_BASE + "sourcemaps.scss"; +const TESTCASE_URI_MAP = TEST_BASE + "sourcemaps.css.map"; + +const PREF = "devtools.styleeditor.source-maps-enabled"; + +const CSS_TEXT = "* { color: blue }"; + +const Cc = Components.classes; +const Ci = Components.interfaces; + +let tempScope = {}; +Components.utils.import("resource://gre/modules/FileUtils.jsm", tempScope); +Components.utils.import("resource://gre/modules/NetUtil.jsm", tempScope); +let FileUtils = tempScope.FileUtils; +let NetUtil = tempScope.NetUtil; + +function test() +{ + waitForExplicitFinish(); + + Services.prefs.setBoolPref(PREF, true); + + Task.spawn(function() { + // copy all our files over so we don't screw them up for other tests + let HTMLFile = yield copy(TESTCASE_URI_HTML, "sourcemaps.html"); + let CSSFile = yield copy(TESTCASE_URI_CSS, "sourcemaps.css"); + yield copy(TESTCASE_URI_SCSS, "sourcemaps.scss"); + yield copy(TESTCASE_URI_MAP, "sourcemaps.css.map"); + yield copy(TESTCASE_URI_REG_CSS, "simple.css"); + + let uri = Services.io.newFileURI(HTMLFile); + let testcaseURI = uri.resolve(""); + + let editor = yield openEditor(testcaseURI); + + let element = content.document.querySelector("div"); + let style = content.getComputedStyle(element, null); + + is(style.color, "rgb(255, 0, 102)", "div is red before saving file"); + + editor.styleSheet.relatedStyleSheet.once("style-applied", function() { + is(style.color, "rgb(0, 0, 255)", "div is blue after saving file"); + finishUp(); + }); + + yield pauseForTimeChange(); + + // Edit and save Sass in the editor. This will start off a file-watching + // process waiting for the CSS file to change. + yield editSCSS(editor); + + // We can't run Sass or another compiler, so we fake it by just + // directly changing the CSS file. + yield editCSSFile(CSSFile); + + info("wrote to CSS file"); + }) +} + +function openEditor(testcaseURI) { + let deferred = promise.defer(); + + addTabAndOpenStyleEditor((panel) => { + info("style editor panel opened"); + + let UI = panel.UI; + let count = 0; + + UI.on("editor-added", (event, editor) => { + if (++count == 3) { + // wait for 3 editors - 1 for first style sheet, 1 for the + // generated style sheet, and 1 for original source after it + // loads and replaces the generated style sheet. + let editor = UI.editors[1]; + + let link = getStylesheetNameLinkFor(editor); + link.click(); + + editor.getSourceEditor().then(deferred.resolve); + } + }); + }) + content.location = testcaseURI; + + return deferred.promise; +} + +function editSCSS(editor) { + let deferred = promise.defer(); + + let pos = {line: 0, ch: 0}; + editor.sourceEditor.replaceText(CSS_TEXT, pos, pos); + + editor.saveToFile(null, function (file) { + ok(file, "Scss file should be saved"); + deferred.resolve(); + }); + + return deferred.promise; +} + +function editCSSFile(CSSFile) { + return write(CSS_TEXT, CSSFile); +} + +function pauseForTimeChange() { + let deferred = promise.defer(); + + // We have to wait for the system time to turn over > 1000 ms so that + // our file's last change time will show a change. This reflects what + // would happen in real life with a user manually saving the file. + setTimeout(deferred.resolve, 2000); + + return deferred.promise; +} + +function finishUp() { + Services.prefs.clearUserPref(PREF); + finish(); +} + +/* Helpers */ + +function getStylesheetNameLinkFor(editor) { + return editor.summary.querySelector(".stylesheet-name"); +} + +function copy(aSrcChromeURL, aDestFileName) +{ + let destFile = FileUtils.getFile("ProfD", [aDestFileName]); + return write(read(aSrcChromeURL), destFile); +} + +function read(aSrcChromeURL) +{ + let scriptableStream = Cc["@mozilla.org/scriptableinputstream;1"] + .getService(Ci.nsIScriptableInputStream); + + let channel = Services.io.newChannel(aSrcChromeURL, null, null); + let input = channel.open(); + scriptableStream.init(input); + + let data = scriptableStream.read(input.available()); + scriptableStream.close(); + input.close(); + + return data; +} + +function write(aData, aFile) +{ + let deferred = promise.defer(); + + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Ci.nsIScriptableUnicodeConverter); + + converter.charset = "UTF-8"; + + let istream = converter.convertToInputStream(aData); + let ostream = FileUtils.openSafeFileOutputStream(aFile); + + NetUtil.asyncCopy(istream, ostream, function(status) { + if (!Components.isSuccessCode(status)) { + info("Coudln't write to " + aFile.path); + return; + } + deferred.resolve(aFile); + }); + + return deferred.promise; +}
--- a/browser/devtools/webconsole/test/browser.ini +++ b/browser/devtools/webconsole/test/browser.ini @@ -99,16 +99,17 @@ support-files = test_bug_770099_violation.html test_bug_770099_violation.html^headers^ test-autocomplete-in-stackframe.html testscript.js test-bug_923281_console_log_filter.html test-bug_923281_test1.js test-bug_923281_test2.js test-bug_939783_console_trace_duplicates.html + test-bug-952277-highlight-nodes-in-vview.html [browser_bug664688_sandbox_update_after_navigation.js] [browser_bug_638949_copy_link_location.js] [browser_bug_862916_console_dir_and_filter_off.js] [browser_bug_865288_repeat_different_objects.js] [browser_bug_865871_variables_view_close_on_esc_key.js] [browser_bug_869003_inspect_cross_domain_object.js] [browser_bug_871156_ctrlw_close_tab.js] @@ -253,9 +254,10 @@ run-if = os == "mac" [browser_webconsole_expandable_timestamps.js] [browser_webconsole_autocomplete_in_debugger_stackframe.js] [browser_webconsole_autocomplete_popup_close_on_tab_switch.js] [browser_webconsole_output_01.js] [browser_webconsole_output_02.js] [browser_webconsole_output_03.js] [browser_webconsole_output_04.js] [browser_webconsole_output_events.js] +[browser_console_variables_view_highlighter.js] [browser_webconsole_console_trace_duplicates.js]
--- a/browser/devtools/webconsole/test/browser_console_click_focus.js +++ b/browser/devtools/webconsole/test/browser_console_click_focus.js @@ -11,29 +11,50 @@ function test() { addTab(TEST_URI); browser.addEventListener("DOMContentLoaded", testInputFocus, false); } function testInputFocus() { browser.removeEventListener("DOMContentLoaded", testInputFocus, false); openConsole().then((hud) => { - let inputNode = hud.jsterm.inputNode; - ok(inputNode.getAttribute("focused"), "input node is focused"); + waitForMessages({ + webconsole: hud, + messages: [{ + text: "Dolske Digs Bacon", + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }).then(([result]) => { + let msg = [...result.matched][0]; + let outputItem = msg.querySelector(".body"); + ok(outputItem, "found a logged message"); + let inputNode = hud.jsterm.inputNode; + ok(inputNode.getAttribute("focused"), "input node is focused, first"); - let lostFocus = () => { - inputNode.removeEventListener("blur", lostFocus); - info("input node lost focus"); - } - - inputNode.addEventListener("blur", lostFocus); + let lostFocus = () => { + inputNode.removeEventListener("blur", lostFocus); + info("input node lost focus"); + } - browser.ownerDocument.getElementById("urlbar").click(); + inputNode.addEventListener("blur", lostFocus); + + browser.ownerDocument.getElementById("urlbar").click(); - ok(!inputNode.getAttribute("focused"), "input node is not focused"); + ok(!inputNode.getAttribute("focused"), "input node is not focused"); + + EventUtils.sendMouseEvent({type: "click"}, hud.outputNode); + + ok(inputNode.getAttribute("focused"), "input node is focused, second time") - hud.outputNode.click(); + // test click-drags are not focusing the input element. + EventUtils.sendMouseEvent({type: "mousedown", clientX: 3, clientY: 4}, + outputItem); + EventUtils.sendMouseEvent({type: "click", clientX: 15, clientY: 5}, + outputItem); - ok(inputNode.getAttribute("focused"), "input node is focused"); - - finishTest(); + executeSoon(() => { + todo(!inputNode.getAttribute("focused"), "input node is not focused after drag"); + finishTest(); + }); + }); }); }
--- a/browser/devtools/webconsole/test/browser_console_keyboard_accessibility.js +++ b/browser/devtools/webconsole/test/browser_console_keyboard_accessibility.js @@ -17,29 +17,39 @@ function test() openConsole(null, consoleOpened); }, true); function consoleOpened(aHud) { hud = aHud; ok(hud, "Web Console opened"); - content.console.log("foobarz1"); + info("dump some spew into the console for scrolling"); + for (let i = 0; i < 100; i++) + content.console.log("foobarz" + i); waitForMessages({ webconsole: hud, messages: [{ - text: "foobarz1", + text: "foobarz99", category: CATEGORY_WEBDEV, severity: SEVERITY_LOG, }], }).then(onConsoleMessage); } function onConsoleMessage() { + let currentPosition = hud.outputNode.parentNode.scrollTop; + EventUtils.synthesizeKey("VK_PAGE_UP", {}); + isnot(hud.outputNode.parentNode.scrollTop, currentPosition, "scroll position changed after page up"); + + currentPosition = hud.outputNode.parentNode.scrollTop; + EventUtils.synthesizeKey("VK_PAGE_DOWN", {}); + ok(hud.outputNode.parentNode.scrollTop > currentPosition, "scroll position now at bottom"); + hud.jsterm.once("messages-cleared", onClear); info("try ctrl-l to clear output"); EventUtils.synthesizeKey("l", { ctrlKey: true }); } function onClear() { is(hud.outputNode.textContent.indexOf("foobarz1"), -1, "output cleared");
new file mode 100644 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_console_variables_view_highlighter.js @@ -0,0 +1,91 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Check that variables view is linked to the inspector for highlighting and +// selecting DOM nodes + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-952277-highlight-nodes-in-vview.html"; + +let gWebConsole, gJSTerm, gVariablesView, gToolbox; + +function test() +{ + addTab(TEST_URI); + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + openConsole(null, consoleOpened); + }, true); +} + +function consoleOpened(hud) +{ + gWebConsole = hud; + gJSTerm = hud.jsterm; + gToolbox = gDevTools.getToolbox(hud.target); + gJSTerm.execute("document.querySelectorAll('p')", onQSAexecuted); +} + +function onQSAexecuted(msg) +{ + ok(msg, "output message found"); + let anchor = msg.querySelector("a"); + ok(anchor, "object link found"); + + gJSTerm.once("variablesview-fetched", onNodeListVviewFetched); + + executeSoon(() => + EventUtils.synthesizeMouse(anchor, 2, 2, {}, gWebConsole.iframeWindow) + ); +} + +function onNodeListVviewFetched(aEvent, aVar) +{ + gVariablesView = aVar._variablesView; + ok(gVariablesView, "variables view object"); + + // Transform the vview into an array we can filter properties from + let props = [[id, prop] for([id, prop] of aVar)]; + // These properties are the DOM nodes ones + props = props.filter(v => v[0].match(/[0-9]+/)); + + function hoverOverDomNodeVariableAndAssertHighlighter(index) { + if (props[index]) { + let prop = props[index][1]; + let valueEl = prop._valueLabel; + + gToolbox.once("node-highlight", () => { + ok(true, "The highlighter was shown on hover of the DOMNode"); + gToolbox.highlighterUtils.unhighlight().then(() => { + clickOnDomNodeVariableAndAssertInspectorSelected(index); + }); + }); + + // Rather than trying to emulate a mouseenter event, let's call the + // variable's highlightDomNode and see if it has the desired effect + prop.highlightDomNode(); + } else { + finishTest(); + } + } + + function clickOnDomNodeVariableAndAssertInspectorSelected(index) { + let prop = props[index][1]; + + // Make sure the inspector is initialized so we can listen to its events + gToolbox.initInspector().then(() => { + // Rather than trying to click on the value here, let's just call the + // variable's openNodeInInspector function and see if it has the + // desired effect + prop.openNodeInInspector().then(() => { + is(gToolbox.currentToolId, "inspector", "The toolbox switched over the inspector on DOMNode click"); + gToolbox.selectTool("webconsole").then(() => { + hoverOverDomNodeVariableAndAssertHighlighter(index + 1); + }); + }); + }); + } + + hoverOverDomNodeVariableAndAssertHighlighter(0); +}
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js @@ -86,16 +86,28 @@ function consoleOpened(aHud) { EventUtils.synthesizeKey("VK_UP", {}); is(popup.selectedIndex, 0, "index 0 is selected"); is(popup.selectedItem.label, "watch", "watch is selected"); is(completeNode.value, prefix + "watch", "completeNode.value holds watch"); + let currentSelectionIndex = popup.selectedIndex; + + EventUtils.synthesizeKey("VK_PAGE_DOWN", {}); + + ok(popup.selectedIndex > currentSelectionIndex, + "Index is greater after PGDN"); + + currentSelectionIndex = popup.selectedIndex; + EventUtils.synthesizeKey("VK_PAGE_UP", {}); + + ok(popup.selectedIndex < currentSelectionIndex, "Index is less after Page UP"); + info("press Tab and wait for popup to hide"); popup._panel.addEventListener("popuphidden", popupHideAfterTab, false); EventUtils.synthesizeKey("VK_TAB", {}); }, false); info("wait for completion: window.foobarBug585991."); jsterm.setInputValue("window.foobarBug585991"); EventUtils.synthesizeKey(".", {});
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js @@ -44,17 +44,17 @@ function createDocument() { function setupHighlighterTests() { ok(h1, "we have the header node"); openInspector(runSelectionTests); } function runSelectionTests(aInspector) { inspector = aInspector; - inspector.toolbox.startPicker(); + inspector.toolbox.highlighterUtils.startPicker(); inspector.toolbox.once("picker-started", () => { info("Picker mode started, now clicking on H1 to select that node"); executeSoon(() => { EventUtils.synthesizeMouseAtCenter(h1, {}, content); inspector.toolbox.once("picker-stopped", () => { info("Picker mode stopped, H1 selected, now switching to the console"); openConsole(gBrowser.selectedTab).then(performWebConsoleTests); });
new file mode 100644 --- /dev/null +++ b/browser/devtools/webconsole/test/test-bug-952277-highlight-nodes-in-vview.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Web Console test for bug 952277 - Highlighting and selecting nodes from the variablesview</title> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> + </head> + <body> + <p>Web Console test for bug 952277 - Highlighting and selecting nodes from the variablesview</p> + <p>Web Console test for bug 952277 - Highlighting and selecting nodes from the variablesview</p> + <p>Web Console test for bug 952277 - Highlighting and selecting nodes from the variablesview</p> + </body> +</html> +
--- a/browser/devtools/webconsole/webconsole.js +++ b/browser/devtools/webconsole/webconsole.js @@ -566,25 +566,23 @@ WebConsoleFrame.prototype = { let toolbox = gDevTools.getToolbox(this.owner.target); if (toolbox) { toolbox.on("webconsole-selected", this._onPanelSelected); } /* * Focus input line whenever the output area is clicked. - * Only focus when the target node (or parent, as in source links) is - * not an anchor. + * Reusing _addMEssageLinkCallback since it correctly filters + * drag and select events. */ - this.outputNode.addEventListener("click", (e) => { - if ((e.button == 0) && - (e.target.nodeName.toLowerCase() != "a") && - (e.target.parentNode.nodeName.toLowerCase() != "a")) { + this._addFocusCallback(this.outputNode, (evt) => { + if ((evt.target.nodeName.toLowerCase() != "a") && + (evt.target.parentNode.nodeName.toLowerCase() != "a")) this.jsterm.inputNode.focus(); - } }); // Toggle the timestamp on preference change gDevTools.on("pref-changed", this._onToolboxPrefChanged); this._onToolboxPrefChanged("pref-changed", { pref: PREF_MESSAGE_TIMESTAMP, newValue: Services.prefs.getBoolPref(PREF_MESSAGE_TIMESTAMP), }); @@ -2639,40 +2637,80 @@ WebConsoleFrame.prototype = { * @private * @param nsIDOMNode aNode * The node for which you want to add the event handlers. * @param function aCallback * The function you want to invoke on click. */ _addMessageLinkCallback: function WCF__addMessageLinkCallback(aNode, aCallback) { - aNode.addEventListener("mousedown", function(aEvent) { + aNode.addEventListener("mousedown", (aEvent) => { this._mousedown = true; this._startX = aEvent.clientX; this._startY = aEvent.clientY; }, false); - aNode.addEventListener("click", function(aEvent) { + aNode.addEventListener("click", (aEvent) => { let mousedown = this._mousedown; this._mousedown = false; // Do not allow middle/right-click or 2+ clicks. if (aEvent.detail != 1 || aEvent.button != 0) { return; } aEvent.preventDefault(); // If this event started with a mousedown event and it ends at a different // location, we consider this text selection. - if (mousedown && this._startX != aEvent.clientX && - this._startY != aEvent.clientY) { + if (mousedown && + (this._startX != aEvent.clientX) && + (this._startY != aEvent.clientY)) + { + this._startX = this._startY = undefined; return; } + this._startX = this._startY = undefined; + + aCallback.call(this, aEvent); + }, false); + }, + + _addFocusCallback: function WCF__addFocusCallback(aNode, aCallback) + { + aNode.addEventListener("mousedown", (aEvent) => { + this._mousedown = true; + this._startX = aEvent.clientX; + this._startY = aEvent.clientY; + }, false); + + aNode.addEventListener("click", (aEvent) => { + let mousedown = this._mousedown; + this._mousedown = false; + + // Do not allow middle/right-click or 2+ clicks. + if (aEvent.detail != 1 || aEvent.button != 0) { + return; + } + + // If this event started with a mousedown event and it ends at a different + // location, we consider this text selection. + // Add a fuzz modifier of two pixels in any direction to account for sloppy + // clicking. + if (mousedown && + (Math.abs(aEvent.clientX - this._startX) >= 2) && + (Math.abs(aEvent.clientY - this._startY) >= 1)) + { + this._startX = this._startY = undefined; + return; + } + + this._startX = this._startY = undefined; + aCallback.call(this, aEvent); }, false); }, /** * Handler for the pref-changed event coming from the toolbox. * Currently this function only handles the timestamps preferences. * @@ -3018,16 +3056,18 @@ JSTerm.prototype = { * Getter for the debugger WebConsoleClient. * @type object */ get webConsoleClient() this.hud.webConsoleClient, COMPLETE_FORWARD: 0, COMPLETE_BACKWARD: 1, COMPLETE_HINT_ONLY: 2, + COMPLETE_PAGEUP: 3, + COMPLETE_PAGEDOWN: 4, /** * Initialize the JSTerminal UI. */ init: function JST_init() { let autocompleteOptions = { onSelect: this.onAutocompleteSelect.bind(this), @@ -3415,16 +3455,17 @@ JSTerm.prototype = { * - hideFilterInput: boolean, if true the variables filter input is * hidden. * @return object * The new Variables View instance. */ _createVariablesView: function JST__createVariablesView(aOptions) { let view = new VariablesView(aOptions.container); + view.toolbox = gDevTools.getToolbox(this.hud.owner.target); view.searchPlaceholder = l10n.getStr("propertiesFilterPlaceholder"); view.emptyText = l10n.getStr("emptyPropertiesList"); view.searchEnabled = !aOptions.hideFilterInput; view.lazyEmpty = this._lazyVariablesView; VariablesViewController.attach(view, { getEnvironmentClient: aGrip => { return new EnvironmentClient(this.hud.proxy.client, aGrip); @@ -3878,16 +3919,50 @@ JSTerm.prototype = { else if (this.canCaretGoNext()) { inputUpdated = this.historyPeruse(HISTORY_FORWARD); } if (inputUpdated) { aEvent.preventDefault(); } break; + case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP: + if (this.autocompletePopup.isOpen) { + inputUpdated = this.complete(this.COMPLETE_PAGEUP); + if (inputUpdated) { + this._autocompletePopupNavigated = true; + } + } + else { + this.hud.outputNode.parentNode.scrollTop = + Math.max(0, + this.hud.outputNode.parentNode.scrollTop - + this.hud.outputNode.parentNode.clientHeight + ); + } + aEvent.preventDefault(); + break; + + case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN: + if (this.autocompletePopup.isOpen) { + inputUpdated = this.complete(this.COMPLETE_PAGEDOWN); + if (inputUpdated) { + this._autocompletePopupNavigated = true; + } + } + else { + this.hud.outputNode.parentNode.scrollTop = + Math.min(this.hud.outputNode.parentNode.scrollHeight, + this.hud.outputNode.parentNode.scrollTop + + this.hud.outputNode.parentNode.clientHeight + ); + } + aEvent.preventDefault(); + break; + case Ci.nsIDOMKeyEvent.DOM_VK_HOME: case Ci.nsIDOMKeyEvent.DOM_VK_END: case Ci.nsIDOMKeyEvent.DOM_VK_LEFT: if (this.autocompletePopup.isOpen || this.lastCompletion.value) { this.clearCompletion(); } break; @@ -4049,16 +4124,20 @@ JSTerm.prototype = { * completions is used. If the value changed, then the first possible * completion is used and the selection is set from the current * cursor position to the end of the completed text. * If there is only one possible completion, then this completion * value is used and the cursor is put at the end of the completion. * - this.COMPLETE_BACKWARD: Same as this.COMPLETE_FORWARD but if the * value stayed the same as the last time the function was called, * then the previous completion of all possible completions is used. + * - this.COMPLETE_PAGEUP: Scroll up one page if available or select the first + * item. + * - this.COMPLETE_PAGEDOWN: Scroll down one page if available or select the + * last item. * - this.COMPLETE_HINT_ONLY: If there is more than one possible * completion and the input value stayed the same compared to the * last time this function was called, then the same completion is * used again. If there is only one possible completion, then * the inputNode.value is set to this value and the selection is set * from the current cursor position to the end of the completed text. * @param function aCallback * Optional function invoked when the autocomplete properties are @@ -4102,16 +4181,22 @@ JSTerm.prototype = { accepted = true; } else if (aType == this.COMPLETE_BACKWARD) { popup.selectPreviousItem(); } else if (aType == this.COMPLETE_FORWARD) { popup.selectNextItem(); } + else if (aType == this.COMPLETE_PAGEUP) { + popup.selectPreviousPageItem(); + } + else if (aType == this.COMPLETE_PAGEDOWN) { + popup.selectNextPageItem(); + } aCallback && aCallback(this); this.emit("autocomplete-updated"); return accepted || popup.itemCount > 0; }, /** * Update the completion result. This operation is performed asynchronously by
--- a/browser/locales/en-US/chrome/browser/browser.dtd +++ b/browser/locales/en-US/chrome/browser/browser.dtd @@ -170,17 +170,16 @@ These should match what Safari and other <!ENTITY locationItem.title "Location"> <!ENTITY searchItem.title "Search"> <!-- Toolbar items --> <!ENTITY homeButton.label "Home"> <!ENTITY tabGroupsButton.label "Tab Groups"> -<!ENTITY tabGroupsButton.tooltip "Group your tabs"> <!ENTITY bookmarksButton.label "Bookmarks"> <!ENTITY bookmarksCmd.commandkey "b"> <!ENTITY bookmarksMenuButton.label "Bookmarks"> <!ENTITY bookmarksMenuButton.unsorted.label "Unsorted Bookmarks"> <!ENTITY viewBookmarksSidebar2.label "View Bookmarks Sidebar"> <!ENTITY viewBookmarksToolbar.label "View Bookmarks Toolbar">
--- a/browser/locales/en-US/chrome/browser/devtools/debugger.dtd +++ b/browser/locales/en-US/chrome/browser/devtools/debugger.dtd @@ -59,130 +59,130 @@ <!ENTITY debuggerUI.clearButton "Clear"> <!-- LOCALIZATION NOTE (debuggerUI.clearButton.tooltip): This is the tooltip for - the button that clears the collected tracing data in the tracing tab. --> <!ENTITY debuggerUI.clearButton.tooltip "Clear the collected traces"> <!-- LOCALIZATION NOTE (debuggerUI.pauseExceptions): This is the label for the - checkbox that toggles pausing on exceptions. --> -<!ENTITY debuggerUI.pauseExceptions "Pause on exceptions"> +<!ENTITY debuggerUI.pauseExceptions "Pause on Exceptions"> <!ENTITY debuggerUI.pauseExceptions.accesskey "E"> <!-- LOCALIZATION NOTE (debuggerUI.ignoreCaughtExceptions): This is the label for the - checkbox that toggles ignoring caught exceptions. --> -<!ENTITY debuggerUI.ignoreCaughtExceptions "Ignore caught exceptions"> +<!ENTITY debuggerUI.ignoreCaughtExceptions "Ignore Caught Exceptions"> <!ENTITY debuggerUI.ignoreCaughtExceptions.accesskey "C"> <!-- LOCALIZATION NOTE (debuggerUI.showPanesOnInit): This is the label for the - checkbox that toggles visibility of panes when opening the debugger. --> -<!ENTITY debuggerUI.showPanesOnInit "Show panes on startup"> +<!ENTITY debuggerUI.showPanesOnInit "Show Panes on Startup"> <!ENTITY debuggerUI.showPanesOnInit.accesskey "S"> <!-- LOCALIZATION NOTE (debuggerUI.showVarsFilter): This is the label for the - checkbox that toggles visibility of a designated variables filter box. --> -<!ENTITY debuggerUI.showVarsFilter "Show variables filter box"> +<!ENTITY debuggerUI.showVarsFilter "Show Variables Filter Box"> <!ENTITY debuggerUI.showVarsFilter.accesskey "V"> <!-- LOCALIZATION NOTE (debuggerUI.showOnlyEnum): This is the label for the - checkbox that toggles visibility of hidden (non-enumerable) variables and - properties in stack views. The "enumerable" flag is a state of a property - defined in JavaScript. When in doubt, leave untranslated. --> -<!ENTITY debuggerUI.showOnlyEnum "Show only enumerable properties"> +<!ENTITY debuggerUI.showOnlyEnum "Show Only Enumerable Properties"> <!ENTITY debuggerUI.showOnlyEnum.accesskey "P"> <!-- LOCALIZATION NOTE (debuggerUI.showOriginalSource): This is the label for - the checkbox that toggles the display of original or sourcemap-derived - sources. --> -<!ENTITY debuggerUI.showOriginalSource "Show original sources"> +<!ENTITY debuggerUI.showOriginalSource "Show Original Sources"> <!ENTITY debuggerUI.showOriginalSource.accesskey "O"> <!-- LOCALIZATION NOTE (debuggerUI.searchPanelOperators): This is the text that - appears in the filter panel popup as a header for the operators part. --> <!ENTITY debuggerUI.searchPanelOperators "Operators:"> <!-- LOCALIZATION NOTE (debuggerUI.searchFile): This is the text that appears - in the source editor's context menu for the scripts search operation. --> -<!ENTITY debuggerUI.searchFile "Filter scripts"> +<!ENTITY debuggerUI.searchFile "Filter Scripts"> <!ENTITY debuggerUI.searchFile.key "P"> <!ENTITY debuggerUI.searchFile.altkey "O"> <!ENTITY debuggerUI.searchFile.accesskey "P"> <!-- LOCALIZATION NOTE (debuggerUI.searchGlobal): This is the text that appears - in the source editor's context menu for the global search operation. --> -<!ENTITY debuggerUI.searchGlobal "Search in all files"> +<!ENTITY debuggerUI.searchGlobal "Search in All Files"> <!ENTITY debuggerUI.searchGlobal.key "F"> <!ENTITY debuggerUI.searchGlobal.accesskey "F"> <!-- LOCALIZATION NOTE (debuggerUI.searchFunction): This is the text that appears - in the source editor's context menu for the function search operation. --> -<!ENTITY debuggerUI.searchFunction "Search for function definition"> +<!ENTITY debuggerUI.searchFunction "Search for Function Definition"> <!ENTITY debuggerUI.searchFunction.key "D"> <!ENTITY debuggerUI.searchFunction.accesskey "D"> <!-- LOCALIZATION NOTE (debuggerUI.searchToken): This is the text that appears - in the source editor's context menu for the token search operation. --> <!ENTITY debuggerUI.searchToken "Find"> <!ENTITY debuggerUI.searchToken.key "F"> <!ENTITY debuggerUI.searchToken.accesskey "F"> <!-- LOCALIZATION NOTE (debuggerUI.searchLine): This is the text that appears - in the source editor's context menu for the line search operation. --> -<!ENTITY debuggerUI.searchGoToLine "Go to line…"> +<!ENTITY debuggerUI.searchGoToLine "Go to Line…"> <!ENTITY debuggerUI.searchGoToLine.key "L"> <!ENTITY debuggerUI.searchGoToLine.accesskey "L"> <!-- LOCALIZATION NOTE (debuggerUI.searchVariable): This is the text that appears - in the source editor's context menu for the variables search operation. --> -<!ENTITY debuggerUI.searchVariable "Filter variables"> +<!ENTITY debuggerUI.searchVariable "Filter Variables"> <!ENTITY debuggerUI.searchVariable.key "V"> <!ENTITY debuggerUI.searchVariable.accesskey "V"> <!-- LOCALIZATION NOTE (debuggerUI.focusVariables): This is the text that appears - in the source editor's context menu for the variables focus operation. --> -<!ENTITY debuggerUI.focusVariables "Focus variables tree"> +<!ENTITY debuggerUI.focusVariables "Focus Variables Tree"> <!ENTITY debuggerUI.focusVariables.key "V"> <!ENTITY debuggerUI.focusVariables.accesskey "V"> <!-- LOCALIZATION NOTE (debuggerUI.condBreakPanelTitle): This is the text that - appears in the conditional breakpoint panel popup as a description. --> <!ENTITY debuggerUI.condBreakPanelTitle "This breakpoint will stop execution only if the following expression is true"> <!-- LOCALIZATION NOTE (debuggerUI.seMenuBreak): This is the text that - appears in the source editor context menu for adding a breakpoint. --> -<!ENTITY debuggerUI.seMenuBreak "Add breakpoint"> +<!ENTITY debuggerUI.seMenuBreak "Add Breakpoint"> <!ENTITY debuggerUI.seMenuBreak.key "B"> <!-- LOCALIZATION NOTE (debuggerUI.seMenuCondBreak): This is the text that - appears in the source editor context menu for adding a conditional - breakpoint. --> -<!ENTITY debuggerUI.seMenuCondBreak "Add conditional breakpoint"> +<!ENTITY debuggerUI.seMenuCondBreak "Add Conditional Breakpoint"> <!ENTITY debuggerUI.seMenuCondBreak.key "B"> <!-- LOCALIZATION NOTE (debuggerUI.tabs.*): This is the text that - appears in the debugger's side pane tabs. --> <!ENTITY debuggerUI.tabs.sources "Sources"> <!ENTITY debuggerUI.tabs.traces "Traces"> <!ENTITY debuggerUI.tabs.callstack "Call Stack"> <!ENTITY debuggerUI.tabs.variables "Variables"> <!ENTITY debuggerUI.tabs.events "Events"> <!-- LOCALIZATION NOTE (debuggerUI.seMenuAddWatch): This is the text that - appears in the source editor context menu for adding an expression. --> -<!ENTITY debuggerUI.seMenuAddWatch "Selection to watch expression"> +<!ENTITY debuggerUI.seMenuAddWatch "Selection to Watch Expression"> <!ENTITY debuggerUI.seMenuAddWatch.key "E"> <!-- LOCALIZATION NOTE (debuggerUI.addWatch): This is the text that - appears in the watch expressions context menu for adding an expression. --> -<!ENTITY debuggerUI.addWatch "Add watch expression"> +<!ENTITY debuggerUI.addWatch "Add Watch Expression"> <!ENTITY debuggerUI.addWatch.accesskey "E"> <!-- LOCALIZATION NOTE (debuggerUI.removeWatch): This is the text that - appears in the watch expressions context menu for removing all expressions. --> -<!ENTITY debuggerUI.removeAllWatch "Remove all watch expressions"> +<!ENTITY debuggerUI.removeAllWatch "Remove All Watch Expressions"> <!ENTITY debuggerUI.removeAllWatch.key "E"> <!ENTITY debuggerUI.removeAllWatch.accesskey "E"> <!-- LOCALIZATION NOTE (debuggerUI.stepping): These are the keycodes that - 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">
--- a/browser/locales/en-US/chrome/browser/devtools/debugger.properties +++ b/browser/locales/en-US/chrome/browser/devtools/debugger.properties @@ -261,16 +261,21 @@ variablesEditableValueTooltip=Click to c # LOCALIZATION NOTE (variablesCloseButtonTooltip): The text that is displayed # in the variables list on an item which can be removed. variablesCloseButtonTooltip=Click to remove # LOCALIZATION NOTE (variablesEditButtonTooltip): The text that is displayed # in the variables list on a getter or setter which can be edited. variablesEditButtonTooltip=Click to set value +# LOCALIZATION NOTE (variablesEditableValueTooltip): The text that is displayed +# in a tooltip on the "open in inspector" button in the the variables list for a +# DOMNode item. +variablesDomNodeValueTooltip=Click to select the node in the inspector + # LOCALIZATION NOTE (configurable|...|Tooltip): The text that is displayed # in the variables list on certain variables or properties as tooltips. # Expanations of what these represent can be found at the following links: # https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty # https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/isExtensible # https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/isFrozen # https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/isSealed # It's probably best to keep these in English.
--- a/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd +++ b/browser/locales/en-US/chrome/browser/devtools/netmonitor.dtd @@ -6,19 +6,24 @@ <!-- LOCALIZATION NOTE : FILE Do not translate commandkey --> <!-- 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 (netmonitorUI.emptyNotice2): This is the label displayed +<!-- LOCALIZATION NOTE (netmonitorUI.perfNotice1/2): These are the labels displayed + - in the network table when empty to start performance analysis. --> +<!ENTITY netmonitorUI.perfNotice1 "• Click on the"> +<!ENTITY netmonitorUI.perfNotice2 "button to start performance analysis."> + +<!-- LOCALIZATION NOTE (netmonitorUI.emptyNotice3): This is the label displayed - in the network table when empty. --> -<!ENTITY netmonitorUI.emptyNotice2 "Perform a request or reload the page to see detailed information about network activity."> +<!ENTITY netmonitorUI.emptyNotice3 "• Perform a request or reload the page to see detailed information about network activity."> <!-- LOCALIZATION NOTE (netmonitorUI.toolbar.status2): This is the label displayed - in the network table toolbar, above the "status" column. --> <!ENTITY netmonitorUI.toolbar.status2 "✓"> <!-- LOCALIZATION NOTE (netmonitorUI.toolbar.method): This is the label displayed - in the network table toolbar, above the "method" column. --> <!ENTITY netmonitorUI.toolbar.method "Method"> @@ -94,20 +99,28 @@ <!-- LOCALIZATION NOTE (debuggerUI.footer.filterMedia): This is the label displayed - in the network details footer for the "Media" filtering button. --> <!ENTITY netmonitorUI.footer.filterMedia "Media"> <!-- LOCALIZATION NOTE (debuggerUI.footer.filterFlash): This is the label displayed - in the network details footer for the "Flash" filtering button. --> <!ENTITY netmonitorUI.footer.filterFlash "Flash"> +<!-- LOCALIZATION NOTE (debuggerUI.footer.filterOther): This is the label displayed + - in the network details footer for the "Other" filtering button. --> +<!ENTITY netmonitorUI.footer.filterOther "Other"> + <!-- LOCALIZATION NOTE (debuggerUI.footer.clear): This is the label displayed - in the network details footer for the "Clear" button. --> <!ENTITY netmonitorUI.footer.clear "Clear"> +<!-- LOCALIZATION NOTE (debuggerUI.footer.clear): This is the label displayed + - in the network details footer for the performance analysis button. --> +<!ENTITY netmonitorUI.footer.perf "Toggle performance analysis..."> + <!-- LOCALIZATION NOTE (debuggerUI.panesButton.tooltip): This is the tooltip for - the button that toggles the panes visible or hidden in the netmonitor UI. --> <!ENTITY netmonitorUI.panesButton.tooltip "Toggle network info"> <!-- LOCALIZATION NOTE (debuggerUI.summary.url): This is the label displayed - in the network details headers tab identifying the URL. --> <!ENTITY netmonitorUI.summary.url "Request URL:"> @@ -168,42 +181,58 @@ - in a "wait" state. --> <!ENTITY netmonitorUI.timings.wait "Waiting:"> <!-- LOCALIZATION NOTE (debuggerUI.timings.receive): This is the label displayed - in the network details timings tab identifying the amount of time spent - in a "receive" state. --> <!ENTITY netmonitorUI.timings.receive "Receiving:"> +<!-- LOCALIZATION NOTE (netmonitorUI.context.perfTools): This is the label displayed + - on the context menu that shows the performance analysis tools --> +<!ENTITY netmonitorUI.context.perfTools "Start Performance Analysis..."> + +<!-- LOCALIZATION NOTE (netmonitorUI.context.perfTools.accesskey): This is the access key + - for the performance analysis menu item displayed in the context menu for a request --> +<!ENTITY netmonitorUI.context.perfTools.accesskey "S"> + <!-- LOCALIZATION NOTE (netmonitorUI.context.copyUrl): This is the label displayed - on the context menu that copies the selected request's url --> -<!ENTITY netmonitorUI.context.copyUrl "Copy URL"> +<!ENTITY netmonitorUI.context.copyUrl "Copy URL"> <!-- LOCALIZATION NOTE (netmonitorUI.context.copyUrl.accesskey): This is the access key - for the Copy URL menu item displayed in the context menu for a request --> -<!ENTITY netmonitorUI.context.copyUrl.accesskey "C"> +<!ENTITY netmonitorUI.context.copyUrl.accesskey "C"> + +<!-- LOCALIZATION NOTE (netmonitorUI.context.copyImageAsDataUri): This is the label displayed + - on the context menu that copies the selected image as data uri --> +<!ENTITY netmonitorUI.context.copyImageAsDataUri "Copy Image as Data URI"> + +<!-- LOCALIZATION NOTE (netmonitorUI.context.copyImageAsDataUri.accesskey): This is the access key + - for the Copy Image As Data URI menu item displayed in the context menu for a request --> +<!ENTITY netmonitorUI.context.copyImageAsDataUri.accesskey "I"> <!-- LOCALIZATION NOTE (debuggerUI.summary.editAndResend): This is the label displayed - on the button in the headers tab that opens a form to edit and resend the currently displayed request --> -<!ENTITY netmonitorUI.summary.editAndResend "Edit and Resend"> +<!ENTITY netmonitorUI.summary.editAndResend "Edit and Resend"> <!-- LOCALIZATION NOTE (debuggerUI.summary.editAndResend.accesskey): This is the access key - for the "Edit and Resend" menu item displayed in the context menu for a request --> -<!ENTITY netmonitorUI.summary.editAndResend.accesskey "R"> +<!ENTITY netmonitorUI.summary.editAndResend.accesskey "R"> <!-- LOCALIZATION NOTE (netmonitorUI.context.newTab): This is the label - for the Open in New Tab menu item displayed in the context menu of the - network container --> <!ENTITY netmonitorUI.context.newTab "Open in New Tab"> <!-- LOCALIZATION NOTE (netmonitorUI.context.newTab.accesskey): This is the access key - for the Open in New Tab menu item displayed in the context menu of the - network container --> -<!ENTITY netmonitorUI.context.newTab.accesskey "O"> +<!ENTITY netmonitorUI.context.newTab.accesskey "O"> <!-- LOCALIZATION NOTE (debuggerUI.custom.newRequest): This is the label displayed - as the title of the new custom request form --> <!ENTITY netmonitorUI.custom.newRequest "New Request"> <!-- LOCALIZATION NOTE (debuggerUI.custom.query): This is the label displayed - above the query string entry in the custom request form --> <!ENTITY netmonitorUI.custom.query "Query String:"> @@ -218,8 +247,12 @@ <!-- LOCALIZATION NOTE (debuggerUI.custom.send): This is the label displayed - on the button which sends the custom request --> <!ENTITY netmonitorUI.custom.send "Send"> <!-- LOCALIZATION NOTE (debuggerUI.custom.cancel): This is the label displayed - on the button which cancels and closes the custom request form --> <!ENTITY netmonitorUI.custom.cancel "Cancel"> + +<!-- LOCALIZATION NOTE (debuggerUI.backButton): This is the label displayed + - on the button which exists the performance statistics view --> +<!ENTITY netmonitorUI.backButton "Back">
--- a/browser/locales/en-US/chrome/browser/devtools/netmonitor.properties +++ b/browser/locales/en-US/chrome/browser/devtools/netmonitor.properties @@ -130,8 +130,54 @@ networkMenu.millisecond=%S ms # LOCALIZATION NOTE (networkMenu.second): This is the label displayed # in the network menu specifying timing interval divisions (in seconds). networkMenu.second=%S s # LOCALIZATION NOTE (networkMenu.minute): This is the label displayed # in the network menu specifying timing interval divisions (in minutes). networkMenu.minute=%S min + +# LOCALIZATION NOTE (networkMenu.minute): This is the label displayed +# in the network menu specifying timing interval divisions (in minutes). +networkMenu.minute=%S min + +# LOCALIZATION NOTE (pieChart.empty): This is the label displayed +# for pie charts (e.g., in the performance analysis view) when there is +# no data available yet. +pieChart.empty=Loading + +# LOCALIZATION NOTE (tableChart.empty): This is the label displayed +# for table charts (e.g., in the performance analysis view) when there is +# no data available yet. +tableChart.empty=Please wait… + +# LOCALIZATION NOTE (charts.sizeKB): This is the label displayed +# in pie or table charts specifying the size of a request (in kilobytes). +charts.sizeKB=%S KB + +# LOCALIZATION NOTE (charts.totalMS): This is the label displayed +# in pie or table charts specifying the time for a request to finish (in milliseconds). +charts.totalMS=%S ms + +# LOCALIZATION NOTE (charts.cacheEnabled): This is the label displayed +# in the performance analysis view for "cache enabled" charts. +charts.cacheEnabled=Primed cache + +# LOCALIZATION NOTE (charts.cacheDisabled): This is the label displayed +# in the performance analysis view for "cache disabled" charts. +charts.cacheDisabled=Empty cache + +# LOCALIZATION NOTE (charts.totalSize): This is the label displayed +# in the performance analysis view for total requests size, in kilobytes. +charts.totalSize=Size: %S KB + +# LOCALIZATION NOTE (charts.totalTime): This is the label displayed +# in the performance analysis view for total requests time, in milliseconds. +charts.totalTime=Time: %S ms + +# LOCALIZATION NOTE (charts.totalCached): This is the label displayed +# in the performance analysis view for total cached responses. +charts.totalCached=Cached responses: %S + +# LOCALIZATION NOTE (charts.totalCount): This is the label displayed +# in the performance analysis view for total requests. +charts.totalCount=Total requests: %S
--- a/browser/metro/base/content/browser-ui.js +++ b/browser/metro/base/content/browser-ui.js @@ -1120,18 +1120,19 @@ var BrowserUI = { case "cmd_savePage": this.savePage(); break; } }, confirmSanitizeDialog: function () { let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties"); - let title = bundle.GetStringFromName("clearPrivateData.title"); - let message = bundle.GetStringFromName("clearPrivateData.message"); + let title = bundle.GetStringFromName("clearPrivateData.title2"); + let options = bundle.GetStringFromName("optionsCharm"); + let message = bundle.GetStringFromName("clearPrivateData.message2").replace("#1", options); let clearbutton = bundle.GetStringFromName("clearPrivateData.clearButton"); let prefsClearButton = document.getElementById("prefs-clear-data"); prefsClearButton.disabled = true; let buttonPressed = Services.prompt.confirmEx( null, title,
--- a/browser/metro/locales/en-US/chrome/browser.properties +++ b/browser/metro/locales/en-US/chrome/browser.properties @@ -33,18 +33,19 @@ contextAppbar2.delete=Delete # Button with this label only appears immediately after a deletion. contextAppbar2.restore=Undo delete # LOCALIZATION NOTE (contextAppbar2.clear): Unselects pages without modification. contextAppbar2.clear=Clear selection # Clear private data clearPrivateData.clearButton=Clear -clearPrivateData.title=Clear Private Data -clearPrivateData.message=Clear your private data? +clearPrivateData.title2=Clear private data +# LOCALIZATION NOTE (clearPrivateData.message2): #1 is optionsCharm +clearPrivateData.message2=This will permanently delete the private data you have selected in #1 # Settings Charms aboutCharm1=About optionsCharm=Options searchCharm=Search helpOnlineCharm=Help (online) # General
deleted file mode 100644 index 0945af275e8860ed810900b6d1f36d1707622eae..0000000000000000000000000000000000000000 GIT binary patch literal 0 Hc$@<O00001
deleted file mode 100644 index af42a28df9c4a32ecdc6cbe670abbe5c7f4fe316..0000000000000000000000000000000000000000 GIT binary patch literal 0 Hc$@<O00001
--- a/browser/themes/linux/jar.mn +++ b/browser/themes/linux/jar.mn @@ -219,38 +219,40 @@ browser.jar: skin/classic/browser/devtools/responsive-vertical-resizer.png (devtools/responsive-vertical-resizer.png) skin/classic/browser/devtools/responsive-horizontal-resizer.png (devtools/responsive-horizontal-resizer.png) skin/classic/browser/devtools/responsive-background.png (devtools/responsive-background.png) skin/classic/browser/devtools/toggle-tools.png (devtools/toggle-tools.png) skin/classic/browser/devtools/dock-bottom@2x.png (../shared/devtools/images/dock-bottom@2x.png) skin/classic/browser/devtools/dock-side@2x.png (../shared/devtools/images/dock-side@2x.png) skin/classic/browser/devtools/floating-scrollbars.css (devtools/floating-scrollbars.css) skin/classic/browser/devtools/floating-scrollbars-light.css (devtools/floating-scrollbars-light.css) - skin/classic/browser/devtools/inspector.css (devtools/inspector.css) - skin/classic/browser/devtools/profiler-stopwatch.png (../shared/devtools/images/profiler-stopwatch.png) + skin/classic/browser/devtools/inspector.css (devtools/inspector.css) + skin/classic/browser/devtools/profiler-stopwatch.png (../shared/devtools/images/profiler-stopwatch.png) skin/classic/browser/devtools/tool-options.svg (../shared/devtools/images/tool-options.svg) skin/classic/browser/devtools/tool-webconsole.svg (../shared/devtools/images/tool-webconsole.svg) skin/classic/browser/devtools/tool-debugger.svg (../shared/devtools/images/tool-debugger.svg) skin/classic/browser/devtools/tool-debugger-paused.svg (../shared/devtools/images/tool-debugger-paused.svg) skin/classic/browser/devtools/tool-inspector.svg (../shared/devtools/images/tool-inspector.svg) skin/classic/browser/devtools/tool-inspector.svg (../shared/devtools/images/tool-inspector.svg) skin/classic/browser/devtools/tool-styleeditor.svg (../shared/devtools/images/tool-styleeditor.svg) skin/classic/browser/devtools/tool-profiler.svg (../shared/devtools/images/tool-profiler.svg) skin/classic/browser/devtools/tool-network.svg (../shared/devtools/images/tool-network.svg) skin/classic/browser/devtools/tool-scratchpad.svg (../shared/devtools/images/tool-scratchpad.svg) - skin/classic/browser/devtools/close.png (../shared/devtools/images/close.png) - skin/classic/browser/devtools/close@2x.png (../shared/devtools/images/close@2x.png) - skin/classic/browser/devtools/vview-delete.png (devtools/vview-delete.png) - skin/classic/browser/devtools/vview-edit.png (devtools/vview-edit.png) - skin/classic/browser/devtools/undock@2x.png (../shared/devtools/images/undock@2x.png) - skin/classic/browser/devtools/font-inspector.css (devtools/font-inspector.css) - skin/classic/browser/devtools/computedview.css (devtools/computedview.css) - skin/classic/browser/devtools/arrow-e.png (devtools/arrow-e.png) - skin/classic/browser/devtools/responsiveui-rotate.png (../shared/devtools/responsiveui-rotate.png) - skin/classic/browser/devtools/responsiveui-touch.png (../shared/devtools/responsiveui-touch.png) + skin/classic/browser/devtools/close.png (../shared/devtools/images/close.png) + skin/classic/browser/devtools/close@2x.png (../shared/devtools/images/close@2x.png) + skin/classic/browser/devtools/vview-delete.png (../shared/devtools/images/vview-delete.png) + skin/classic/browser/devtools/vview-lock.png (../shared/devtools/images/vview-lock.png) + skin/classic/browser/devtools/vview-edit.png (../shared/devtools/images/vview-edit.png) + skin/classic/browser/devtools/vview-open-inspector.png (../shared/devtools/images/vview-open-inspector.png) + skin/classic/browser/devtools/undock@2x.png (../shared/devtools/images/undock@2x.png) + skin/classic/browser/devtools/font-inspector.css (devtools/font-inspector.css) + skin/classic/browser/devtools/computedview.css (devtools/computedview.css) + skin/classic/browser/devtools/arrow-e.png (devtools/arrow-e.png) + skin/classic/browser/devtools/responsiveui-rotate.png (../shared/devtools/responsiveui-rotate.png) + skin/classic/browser/devtools/responsiveui-touch.png (../shared/devtools/responsiveui-touch.png) skin/classic/browser/devtools/responsiveui-screenshot.png (../shared/devtools/responsiveui-screenshot.png) skin/classic/browser/devtools/app-manager/connection-footer.css (../shared/devtools/app-manager/connection-footer.css) skin/classic/browser/devtools/app-manager/index.css (../shared/devtools/app-manager/index.css) skin/classic/browser/devtools/app-manager/device.css (../shared/devtools/app-manager/device.css) skin/classic/browser/devtools/app-manager/projects.css (../shared/devtools/app-manager/projects.css) skin/classic/browser/devtools/app-manager/help.css (../shared/devtools/app-manager/help.css) skin/classic/browser/devtools/app-manager/warning.svg (../shared/devtools/app-manager/images/warning.svg) skin/classic/browser/devtools/app-manager/error.svg (../shared/devtools/app-manager/images/error.svg)
deleted file mode 100644 index 0945af275e8860ed810900b6d1f36d1707622eae..0000000000000000000000000000000000000000 GIT binary patch literal 0 Hc$@<O00001
deleted file mode 100644 index af42a28df9c4a32ecdc6cbe670abbe5c7f4fe316..0000000000000000000000000000000000000000 GIT binary patch literal 0 Hc$@<O00001
--- a/browser/themes/osx/jar.mn +++ b/browser/themes/osx/jar.mn @@ -340,18 +340,20 @@ browser.jar: skin/classic/browser/devtools/tool-inspector.svg (../shared/devtools/images/tool-inspector.svg) skin/classic/browser/devtools/tool-inspector.svg (../shared/devtools/images/tool-inspector.svg) skin/classic/browser/devtools/tool-styleeditor.svg (../shared/devtools/images/tool-styleeditor.svg) skin/classic/browser/devtools/tool-profiler.svg (../shared/devtools/images/tool-profiler.svg) skin/classic/browser/devtools/tool-network.svg (../shared/devtools/images/tool-network.svg) skin/classic/browser/devtools/tool-scratchpad.svg (../shared/devtools/images/tool-scratchpad.svg) skin/classic/browser/devtools/close.png (../shared/devtools/images/close.png) skin/classic/browser/devtools/close@2x.png (../shared/devtools/images/close@2x.png) - skin/classic/browser/devtools/vview-delete.png (devtools/vview-delete.png) - skin/classic/browser/devtools/vview-edit.png (devtools/vview-edit.png) + skin/classic/browser/devtools/vview-delete.png (../shared/devtools/images/vview-delete.png) + skin/classic/browser/devtools/vview-lock.png (../shared/devtools/images/vview-lock.png) + skin/classic/browser/devtools/vview-edit.png (../shared/devtools/images/vview-edit.png) + skin/classic/browser/devtools/vview-open-inspector.png (../shared/devtools/images/vview-open-inspector.png) skin/classic/browser/devtools/undock@2x.png (../shared/devtools/images/undock@2x.png) skin/classic/browser/devtools/font-inspector.css (devtools/font-inspector.css) skin/classic/browser/devtools/computedview.css (devtools/computedview.css) skin/classic/browser/devtools/arrow-e.png (devtools/arrow-e.png) skin/classic/browser/devtools/responsiveui-rotate.png (../shared/devtools/responsiveui-rotate.png) skin/classic/browser/devtools/responsiveui-touch.png (../shared/devtools/responsiveui-touch.png) skin/classic/browser/devtools/responsiveui-screenshot.png (../shared/devtools/responsiveui-screenshot.png) skin/classic/browser/devtools/app-manager/connection-footer.css (../shared/devtools/app-manager/connection-footer.css)
--- a/browser/themes/shared/devtools/dark-theme.css +++ b/browser/themes/shared/devtools/dark-theme.css @@ -143,20 +143,25 @@ color: #b26b47; } .theme-fg-color7, .cm-s-mozilla .cm-atom, .cm-s-mozilla .cm-quote, .cm-s-mozilla .cm-error, .variable-or-property .token-boolean, +.variable-or-property .token-domnode, .variable-or-property[exception] > .title > .name { /* Red */ color: #bf5656; } +.variable-or-property .token-domnode { + font-weight: bold; +} + .theme-toolbar, .devtools-toolbar, .devtools-sidebar-tabs > tabs { /* General toolbar styling */ color: #b6babf; background-color: #343c45; border-color: hsla(210,8%,5%,.6); }
--- a/browser/themes/shared/devtools/debugger.inc.css +++ b/browser/themes/shared/devtools/debugger.inc.css @@ -473,16 +473,24 @@ transition: transform 0.3s ease-in-out; } .dbg-results-line-contents-string[match=true][focused] { transition-duration: 0.1s; transform: scale(1.75, 1.75); } +.theme-dark .dbg-source-results:not(.selected):hover { + background-color: #181d20; /* Sidebar background */ +} + +.theme-light .dbg-source-results:not(.selected):hover { + background-color: #f7f7f7; /* Sidebar background */ +} + .theme-dark .dbg-results-header { background-color: #252c33; /* Tab toolbar */ color: #b8c8d9; /* Light content text */ } .theme-light .dbg-results-header { background-color: #ebeced; /* Tab toolbar */ color: #667380; /* Dark grey content text */
new file mode 100644 index 0000000000000000000000000000000000000000..db4b0620c4118ace6695af18f663d594170f3b20 GIT binary patch literal 3229 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 P00000NkvXXu0mjfDrh$J
new file mode 100644 index 0000000000000000000000000000000000000000..ae6fbb21c31c46e94fa7739d3be5a5b64f1ae6b7 GIT binary patch literal 3542 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 QGXMYp07*qoM6N<$f~r{2#sB~S
new file mode 100644 index 0000000000000000000000000000000000000000..f575032008eb5a312879954586423c41ecc7a412 GIT binary patch literal 3329 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 LNkvXXu0mjfST$*#
new file mode 100644 index 0000000000000000000000000000000000000000..a34c8ede6f8bb66511270fb55caa379d7d5e7944 GIT binary patch literal 4062 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 QX#fBK07*qoM6N<$g0O<+g8%>k
new file mode 100644 index 0000000000000000000000000000000000000000..b68cc5e7dc7b54060db5f48dc879bc16ff3928c4 GIT binary patch literal 3269 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 D8JSYg
new file mode 100644 index 0000000000000000000000000000000000000000..5d7640bbe2f04c932eec767ab8d08255af88a9d2 GIT binary patch literal 3839 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 Bf4Kkv
new file mode 100644 index 0000000000000000000000000000000000000000..ee1d7a59e4b2abe40bda3a5ac25071c0d860f9b1 GIT binary patch literal 2942 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
new file mode 100644 index 0000000000000000000000000000000000000000..2b09f01a566a624abcf0aa6b904c61fe0d5f9ce8 GIT binary patch literal 3095 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
--- a/browser/themes/shared/devtools/light-theme.css +++ b/browser/themes/shared/devtools/light-theme.css @@ -142,20 +142,25 @@ color: hsl(24,85%,39%); } .theme-fg-color7, .cm-s-mozilla .cm-atom, .cm-s-mozilla .cm-quote, .cm-s-mozilla .cm-error, .variable-or-property .token-boolean, +.variable-or-property .token-domnode, .variable-or-property[exception] > .title > .name { /* Red */ color: #bf5656; } +.variable-or-property .token-domnode { + font-weight: bold; +} + .theme-fg-contrast { /* To be used for text on theme-bg-contrast */ color: black; } .theme-toolbar, .devtools-toolbar, .devtools-sidebar-tabs > tabs { /* General toolbar styling */ color: #585959;
--- a/browser/themes/shared/devtools/netmonitor.inc.css +++ b/browser/themes/shared/devtools/netmonitor.inc.css @@ -1,27 +1,39 @@ /* 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/. */ #requests-menu-empty-notice { margin: 0; padding: 12px; - font-size: 110%; + font-size: 120%; } .theme-dark #requests-menu-empty-notice { color: #f5f7fa; /* Light foreground text */ } .theme-light #requests-menu-empty-notice { color: #585959; /* Grey foreground text */ } +#requests-menu-perf-notice-button { + min-width: 30px; + min-height: 28px; + margin: 0; + list-style-image: url(profiler-stopwatch.png); + -moz-image-region: rect(0px,16px,16px,0px); +} + +#requests-menu-perf-notice-button .button-text { + display: none; +} + %filter substitution %define table_itemDarkStartBorder rgba(0,0,0,0.2) %define table_itemDarkEndBorder rgba(128,128,128,0.15) %define table_itemLightStartBorder rgba(128,128,128,0.25) %define table_itemLightEndBorder transparent /* Network requests table */ @@ -470,22 +482,21 @@ box.requests-menu-status { #details-pane-toggle:active { -moz-image-region: rect(0px,32px,16px,16px); } /* Network request details tabpanels */ .theme-dark .tabpanel-content { + background: url(background-noise-toolbar.png), #343c45; /* Toolbars */ color: #f5f7fa; /* Light foreground text */ } -.theme-dark .tabpanel-summary-label { - color: #f5f7fa; /* Dark foreground text */ -} +/* Summary tabpanel */ .tabpanel-summary-container { padding: 1px; } .tabpanel-summary-label { -moz-padding-start: 4px; -moz-padding-end: 3px; @@ -510,21 +521,28 @@ box.requests-menu-status { #headers-summary-resend { margin-top: -10px; -moz-margin-end: 6px; } /* Response tabpanel */ #response-content-info-header { - background: linear-gradient(hsl(0,60%,40%), hsl(0,60%,30%)) repeat-x top left; - box-shadow: inset 0 1px 0 hsla(210,40%,80%,.15), - inset 0 -1px 0 hsla(210,40%,80%,.05); margin: 0; - padding: 5px 8px; + padding: 3px 8px; +} + +.theme-dark #response-content-info-header { + background: url(background-noise-toolbar.png), #eb5368; /* Red highlight */ + color: #f5f7fa; /* Light foreground text */ +} + +.theme-light #response-content-info-header { + background: url(background-noise-toolbar.png), #ed2655; /* Red highlight */ + color: #f5f7fa; /* Light foreground text */ } #response-content-image-box { padding-top: 10px; padding-bottom: 10px; } #response-content-image { @@ -573,41 +591,41 @@ box.requests-menu-status { border-top: solid 1px hsla(210,5%,5%,.3); } .requests-menu-footer-button, .requests-menu-footer-label { min-width: 1em; margin: 0; border: none; - padding: 2px 1.5vw; + padding: 2px 0.75vw; } .theme-dark .requests-menu-footer-button, .theme-dark .requests-menu-footer-label { color: #f5f7fa; /* Light foreground text */ } .theme-light .requests-menu-footer-button, .theme-light .requests-menu-footer-label { color: #18191a; /* Dark foreground text */ } .requests-menu-footer-spacer { min-width: 2px; } -.theme-dark .requests-menu-footer-spacer:not(:first-of-type), -.theme-dark .requests-menu-footer-button:not(:first-of-type) { +.theme-dark .requests-menu-footer-spacer:not(:first-child), +.theme-dark .requests-menu-footer-button:not(:first-child) { -moz-border-start: 1px solid @table_itemDarkStartBorder@; box-shadow: -1px 0 0 @table_itemDarkEndBorder@; } -.theme-light .requests-menu-footer-spacer:not(:first-of-type), -.theme-light .requests-menu-footer-button:not(:first-of-type) { +.theme-light .requests-menu-footer-spacer:not(:first-child), +.theme-light .requests-menu-footer-button:not(:first-child) { -moz-border-start: 1px solid @table_itemLightStartBorder@; box-shadow: -1px 0 0 @table_itemLightEndBorder@; } .requests-menu-footer-button { -moz-appearance: none; background: rgba(0,0,0,0.025); } @@ -623,33 +641,202 @@ box.requests-menu-status { .requests-menu-footer-button:not(:active)[checked] { background-color: rgba(0,0,0,0.25); background-image: radial-gradient(farthest-side at center top, hsla(200,100%,70%,.7), hsla(200,100%,70%,0.3)); background-size: 100% 1px; background-repeat: no-repeat; } .requests-menu-footer-label { - padding-top: 2px; + padding-top: 3px; font-weight: 600; } +/* Performance analysis buttons */ + +#requests-menu-network-summary-button { + background: none; + box-shadow: none; + border-color: transparent; + list-style-image: url(profiler-stopwatch.png); + -moz-image-region: rect(0px,16px,16px,0px); + -moz-padding-end: 0; + cursor: pointer; +} + +#requests-menu-network-summary-label { + -moz-padding-start: 0; + cursor: pointer; +} + +#requests-menu-network-summary-label:hover { + text-decoration: underline; +} + +/* Performance analysis view */ + +#network-statistics-toolbar { + border: none; + margin: 0; + padding: 0; +} + +#network-statistics-back-button { + min-width: 4em; + min-height: 100vh; + margin: 0; + padding: 0; + border-radius: 0; + border-top: none; + border-bottom: none; + -moz-border-start: none; +} + +#network-statistics-view-splitter { + border-color: rgba(0,0,0,0.2); + cursor: default; + pointer-events: none; +} + +#network-statistics-charts { + min-height: 1px; +} + +.theme-dark #network-statistics-charts { + background: url(background-noise-toolbar.png), #343c45; /* Toolbars */ +} + +.theme-light #network-statistics-charts { + background: url(background-noise-toolbar.png), #f0f1f2; /* Toolbars */ +} + +#network-statistics-charts .pie-chart-container { + -moz-margin-start: 3vw; + -moz-margin-end: 1vw; +} + +#network-statistics-charts .table-chart-container { + -moz-margin-start: 1vw; + -moz-margin-end: 3vw; +} + +.theme-dark .chart-colored-blob[name=html] { + fill: #5e88b0; /* Blue-Grey highlight */ + background: #5e88b0; +} + +.theme-light .chart-colored-blob[name=html] { + fill: #5f88b0; /* Blue-Grey highlight */ + background: #5f88b0; +} + +.theme-dark .chart-colored-blob[name=css] { + fill: #46afe3; /* Blue highlight */ + background: #46afe3; +} + +.theme-light .chart-colored-blob[name=css] { + fill: #0088cc; /* Blue highlight */ + background: #0088cc; +} + +.theme-dark .chart-colored-blob[name=js] { + fill: #d99b28; /* Light Orange highlight */ + background: #d99b28; +} + +.theme-light .chart-colored-blob[name=js] { + fill: #d97e00; /* Light Orange highlight */ + background: #d97e00; +} + +.theme-dark .chart-colored-blob[name=xhr] { + fill: #d96629; /* Orange highlight */ + background: #d96629; +} + +.theme-light .chart-colored-blob[name=xhr] { + fill: #f13c00; /* Orange highlight */ + background: #f13c00; +} + +.theme-dark .chart-colored-blob[name=fonts] { + fill: #6b7abb; /* Purple highlight */ + background: #6b7abb; +} + +.theme-light .chart-colored-blob[name=fonts] { + fill: #5b5fff; /* Purple highlight */ + background: #5b5fff; +} + +.theme-dark .chart-colored-blob[name=images] { + fill: #df80ff; /* Pink highlight */ + background: #df80ff; +} + +.theme-light .chart-colored-blob[name=images] { + fill: #b82ee5; /* Pink highlight */ + background: #b82ee5; +} + +.theme-dark .chart-colored-blob[name=media] { + fill: #70bf53; /* Green highlight */ + background: #70bf53; +} + +.theme-light .chart-colored-blob[name=media] { + fill: #2cbb0f; /* Green highlight */ + background: #2cbb0f; +} + +.theme-dark .chart-colored-blob[name=flash] { + fill: #eb5368; /* Red highlight */ + background: #eb5368; +} + +.theme-light .chart-colored-blob[name=flash] { + fill: #ed2655; /* Red highlight */ + background: #ed2655; +} + +.table-chart-row-label[name=cached] { + display: none; +} + +.table-chart-row-label[name=count] { + width: 3em; + text-align: end; +} + +.table-chart-row-label[name=label] { + width: 7em; +} + +.table-chart-row-label[name=size] { + width: 7em; +} + +.table-chart-row-label[name=time] { + width: 7em; +} + /* Responsive sidebar */ @media (max-width: 700px) { #requests-menu-toolbar { height: 22px; } .requests-menu-header-button { min-height: 20px; } .requests-menu-footer-button, .requests-menu-footer-label { - padding: 2px 2vw; + padding: 2px 1vw; } #details-pane { max-width: none; margin: 0 !important; /* To prevent all the margin hacks to hide the sidebar. */ }
--- a/browser/themes/shared/devtools/shadereditor.inc.css +++ b/browser/themes/shared/devtools/shadereditor.inc.css @@ -88,17 +88,17 @@ .editor-label { padding: 1px 12px; border-top: 1px solid; } .theme-dark .editor-label { background: #343c45; /* Dark toolbars */ - border-color: #222426; /* Match the splitter color. */ + border-color: #000; /* Match the splitter color. */ color: #f5f7fa; /* Light foreground text */ } .theme-light .editor-label { background: #f0f1f2; /* Light toolbars */ border-color: #aaa; /* Match the splitter color. */ color: #585959; /* Grey foreground text */ }
--- a/browser/themes/shared/devtools/styleeditor.css +++ b/browser/themes/shared/devtools/styleeditor.css @@ -13,38 +13,41 @@ } .theme-dark .stylesheet-title, .theme-dark .stylesheet-name { color: #f5f7fa; } .theme-dark .stylesheet-rule-count, +.theme-dark .stylesheet-linked-file, .theme-dark .stylesheet-saveButton { color: #b6babf; } .theme-light .stylesheet-title, .theme-light .stylesheet-name { color: #585959; } .theme-light .stylesheet-rule-count, +.theme-light .stylesheet-linked-file, .theme-light .stylesheet-saveButton { color: #18191a; } .stylesheet-saveButton { text-decoration: underline; cursor: pointer; } .splitview-active .stylesheet-title, .splitview-active .stylesheet-name, .theme-light .splitview-active .stylesheet-rule-count, +.theme-light .splitview-active .stylesheet-linked-file, .theme-light .splitview-active .stylesheet-saveButton { color: #f5f7fa; } .splitview-nav:focus { outline: 0; /* focus ring is on the stylesheet name */ } @@ -80,18 +83,26 @@ background-position: -24px 8px; } .splitview-nav > li > .stylesheet-enabled:focus, .splitview-nav > li:hover > .stylesheet-enabled { outline: 0; } -.stylesheet-error-message { - color: red; +.stylesheet-linked-file:not(:empty){ + -moz-margin-end: 0.4em; +} + +.stylesheet-linked-file:not(:empty):before { + -moz-margin-start: 0.4em; +} + +li.linked-file-error .stylesheet-linked-file:after { + font-size: 110%; } .stylesheet-more > h3 { font-size: 11px; -moz-margin-end: 2px; } .devtools-searchinput {
--- a/browser/themes/shared/devtools/toolbars.inc.css +++ b/browser/themes/shared/devtools/toolbars.inc.css @@ -718,17 +718,19 @@ .theme-light .command-button > image, .theme-light .command-button:active > image, .theme-light .devtools-closebutton > image, .theme-light .devtools-toolbarbutton > image, .theme-light .devtools-option-toolbarbutton > image, .theme-light #breadcrumb-separator-normal, .theme-light .scrollbutton-up > .toolbarbutton-icon, .theme-light .scrollbutton-down > .toolbarbutton-icon, -.theme-light #black-boxed-message-button .button-icon { +.theme-light #black-boxed-message-button .button-icon, +.theme-light #requests-menu-perf-notice-button .button-icon, +.theme-light #requests-menu-network-summary-button .button-icon { filter: url(filters.svg#invert); } /* Since selected backgrounds are blue, we want to use the normal * (light) icons. */ .theme-light .command-button[checked=true]:not(:active) > image, .theme-light .devtools-tab[selected] > image, .theme-light .devtools-tab[highlighted] > image,
--- a/browser/themes/shared/devtools/widgets.inc.css +++ b/browser/themes/shared/devtools/widgets.inc.css @@ -216,34 +216,40 @@ .breadcrumbs-widget-item[checked] .breadcrumbs-widget-item-id, .breadcrumbs-widget-item[checked] .breadcrumbs-widget-item-tag, .breadcrumbs-widget-item[checked] .breadcrumbs-widget-item-pseudo-classes, .breadcrumbs-widget-item[checked] .breadcrumbs-widget-item-classes { color: #f5f7fa; /* Foreground (Text) - Light */ } -.theme-dark .breadcrumbs-widget-item, -.theme-dark .breadcrumbs-widget-item-classes { +.theme-dark .breadcrumbs-widget-item { color: #f5f7fa; /* Foreground (Text) - Light */ } -.theme-light .breadcrumbs-widget-item, -.theme-light .breadcrumbs-widget-item-classes { +.theme-light .breadcrumbs-widget-item { color: #18191a; /* Foreground (Text) - Dark */ } .theme-dark .breadcrumbs-widget-item-id { color: #b6babf; /* Foreground (Text) - Grey */ } .theme-light .breadcrumbs-widget-item-id { color: #585959; /* Foreground (Text) - Grey */ } +.theme-dark .breadcrumbs-widget-item-classes { + color: #b8c8d9; /* Content (Text) - Light */ +} + +.theme-light .breadcrumbs-widget-item-classes { + color: #667380; /* Content (Text) - Dark Grey */ +} + .theme-dark .breadcrumbs-widget-item-pseudo-classes { color: #d99b28; /* Light Orange */ } .theme-light .breadcrumbs-widget-item-pseudo-classes { color: #d97e00; /* Light Orange */ } @@ -252,36 +258,37 @@ } .theme-light .breadcrumbs-widget-item:not([checked]):hover label { color: black; } /* SimpleListWidget */ -%filter substitution -%define slw_selectionGradient linear-gradient(hsl(206,59%,39%), hsl(206,59%,29%)) -%define slw_selectionTextColor #fff - .simple-list-widget-container { /* Hack: force hardware acceleration */ transform: translateZ(1px); } -.simple-list-widget-item.selected { - background: @slw_selectionGradient@; - color: @slw_selectionTextColor@; +.theme-dark .simple-list-widget-item.selected { + background-color: #1d4f73; /* Select Highlight Blue */ + color: #f5f7fa; /* Light foreground text */ +} + +.theme-light .simple-list-widget-item.selected { + background-color: #4c9ed9; /* Select Highlight Blue */ + color: #f5f7fa; /* Light foreground text */ } .theme-dark .simple-list-widget-item:not(.selected):hover { - background-color: #181d20; /* Sidebar background */ + background-color: rgba(255,255,255,.05); } .theme-light .simple-list-widget-item:not(.selected):hover { - background-color: #f7f7f7; /* Sidebar background */ + background-color: rgba(0,0,0,.05); } .simple-list-widget-empty-text, .simple-list-widget-perma-text { padding: 4px 8px; } .theme-dark .simple-list-widget-empty-text, @@ -311,18 +318,17 @@ .theme-light .fast-list-widget-empty-text { color: #585959; /* Grey foreground text */ } /* SideMenuWidget */ %filter substitution -%define smw_selectionTextColor #f5f7fa -%define smw_marginDark #222426 +%define smw_marginDark #000 %define smw_marginLight #aaa %define smw_itemDarkTopBorder rgba(0,0,0,0.2) %define smw_itemDarkBottomBorder rgba(128,128,128,0.15) %define smw_itemLightTopBorder rgba(128,128,128,0.15) %define smw_itemLightBottomBorder transparent .side-menu-widget-container { /* Hack: force hardware acceleration */ @@ -408,22 +414,22 @@ } .theme-light .side-menu-widget-item:last-of-type { box-shadow: inset 0 -1px 0 @smw_itemLightTopBorder@; } .theme-dark .side-menu-widget-item.selected { background-color: #1d4f73; /* Select Highlight Blue */ - color: @smw_selectionTextColor@; + color: #f5f7fa; /* Light foreground text */ } .theme-light .side-menu-widget-item.selected { background-color: #4c9ed9; /* Select Highlight Blue */ - color: @smw_selectionTextColor@; + color: #f5f7fa; /* Light foreground text */ } .side-menu-widget-item-arrow { -moz-margin-start: -7px; width: 7px; /* The image's width is 7 pixels */ /* Cover the border of the side-menu-widget-item */ margin-top: -1px; margin-bottom: -1px; @@ -497,22 +503,22 @@ .theme-light .side-menu-widget-item.selected .side-menu-widget-item-other { background-color: rgba(255,255,255,.8); /* Lighten the selection by 20% */ color: #18191a; /* Dark foreground text */ } .theme-dark .side-menu-widget-item.selected .side-menu-widget-item-other.selected { background-color: transparent; - color: @smw_selectionTextColor@; + color: #f5f7fa; /* Light foreground text */ } .theme-light .side-menu-widget-item.selected .side-menu-widget-item-other.selected { background-color: transparent; - color: @smw_selectionTextColor@; + color: #f5f7fa; /* Light foreground text */ } /* SideMenuWidget checkboxes */ .side-menu-widget-group-checkbox { margin: 0; -moz-margin-end: 4px; } @@ -638,29 +644,28 @@ border-bottom: 1px dashed #f99; } .variable-or-property[safe-getter]:not([pseudo-item]) > .title > .name { border-bottom: 1px dashed #8b0; } .variable-or-property-non-writable-icon { - background: url("chrome://browser/skin/identity-icons-https.png") no-repeat; + background: url("chrome://browser/skin/devtools/vview-lock.png") no-repeat; width: 16px; height: 16px; - opacity: 0.5; } -@media (min-resolution: 2dppx) { +/*@media (min-resolution: 2dppx) { .variable-or-property-non-writable-icon { background-image: url("chrome://browser/skin/identity-icons-https@2x.png"); background-size: 32px; } } - +*/ .variable-or-property-frozen-label, .variable-or-property-sealed-label, .variable-or-property-non-extensible-label { -moz-padding-end: 4px; } .variable-or-property:not(:focus) > .title > .variable-or-property-frozen-label, .variable-or-property:not(:focus) > .title > .variable-or-property-sealed-label, @@ -681,16 +686,17 @@ .variables-view-container[aligned-values] .title > .element-value-input { width: calc(70vw - 10px); } /* Actions first */ .variables-view-container[actions-first] .variables-view-delete, +.variables-view-container[actions-first] .variables-view-open-inspector, .variables-view-container[actions-first] .variables-view-add-property { -moz-box-ordinal-group: 0; } .variables-view-container[actions-first] [invisible] { visibility: hidden; } @@ -722,28 +728,62 @@ /* Variables and properties editing */ .variables-view-delete { list-style-image: url("chrome://browser/skin/devtools/vview-delete.png"); -moz-image-region: rect(0,16px,16px,0); } .variables-view-delete:hover { - -moz-image-region: rect(0,32px,16px,16px); + -moz-image-region: rect(0,48px,16px,32px); } .variables-view-delete:active { - -moz-image-region: rect(0,48px,16px,32px); + -moz-image-region: rect(0,32px,16px,16px); +} + +.variable-or-property:focus .variables-view-delete { + -moz-image-region: rect(0,16px,16px,0); } .variables-view-edit { - background: url("chrome://browser/skin/devtools/vview-edit.png") center no-repeat; - width: 20px; - height: 16px; + list-style-image: url("chrome://browser/skin/devtools/vview-edit.png"); + -moz-image-region: rect(0,16px,16px,0); cursor: pointer; + padding-left: 2px; +} + +.variables-view-edit:hover { + -moz-image-region: rect(0,48px,16px,32px); +} + +.variables-view-edit:active { + -moz-image-region: rect(0,32px,16px,16px); +} + +.variable-or-property:focus .variables-view-edit { + -moz-image-region: rect(0,16px,16px,0); +} + +.variables-view-open-inspector { + list-style-image: url("chrome://browser/skin/devtools/vview-open-inspector.png"); + -moz-image-region: rect(0,16px,16px,0); + cursor: pointer; +} + +.variables-view-open-inspector:hover { + -moz-image-region: rect(0,48px,16px,32px); +} + +.variables-view-open-inspector:active { + -moz-image-region: rect(0,32px,16px,16px); +} + +.variable-or-property:focus .variables-view-open-inspector { + -moz-image-region: rect(0,16px,16px,0); } .variables-view-throbber { background: url("chrome://global/skin/icons/loading_16.png") center no-repeat; width: 16px; height: 16px; } @@ -787,9 +827,150 @@ .arrow[open] { -moz-appearance: treetwistyopen; } .arrow[invisible] { visibility: hidden; } +/* Charts */ + +.generic-chart-container { + /* Hack: force hardware acceleration */ + transform: translateZ(1px); +} + +.theme-dark .generic-chart-container { + color: #f5f7fa; /* Light foreground text */ +} + +.theme-light .generic-chart-container { + color: #585959; /* Grey foreground text */ +} + +.theme-dark .chart-colored-blob { + fill: #b8c8d9; /* Light content text */ + background: #b8c8d9; +} + +.theme-light .chart-colored-blob { + fill: #8fa1b2; /* Grey content text */ + background: #8fa1b2; +} + +/* Charts: Pie */ + +.pie-chart-slice { + stroke-width: 1px; + cursor: pointer; +} + +.theme-dark .pie-chart-slice { + stroke: rgba(0,0,0,0.2); +} + +.theme-light .pie-chart-slice { + stroke: rgba(255,255,255,0.8); +} + +.theme-dark .pie-chart-slice[largest] { + stroke-width: 2px; + stroke: #fff; +} + +.theme-light .pie-chart-slice[largest] { + stroke: #000; +} + +.pie-chart-label { + text-anchor: middle; + dominant-baseline: middle; + pointer-events: none; +} + +.theme-dark .pie-chart-label { + fill: #000; +} + +.theme-light .pie-chart-label { + fill: #fff; +} + +.pie-chart-container[slices="1"] > .pie-chart-slice { + stroke-width: 0px; +} + +.pie-chart-slice, +.pie-chart-label { + transition: all 0.1s ease-out; +} + +.pie-chart-slice:not(:hover):not([focused]), +.pie-chart-slice:not(:hover):not([focused]) + .pie-chart-label { + transform: none !important; +} + +/* Charts: Table */ + +.table-chart-title { + padding-bottom: 10px; + font-size: 120%; + font-weight: 600; +} + +.table-chart-row { + margin-top: 1px; + cursor: pointer; +} + +.table-chart-grid:hover > .table-chart-row { + transition: opacity 0.1s ease-in-out; +} + +.table-chart-grid:not(:hover) > .table-chart-row { + transition: opacity 0.2s ease-in-out; +} + +.generic-chart-container:hover > .table-chart-grid:hover > .table-chart-row:not(:hover), +.generic-chart-container:hover ~ .table-chart-container > .table-chart-grid > .table-chart-row:not([focused]) { + opacity: 0.4; +} + +.table-chart-row-box { + width: 8px; + height: 1.5em; + -moz-margin-end: 10px; +} + +.table-chart-row-label { + width: 8em; + -moz-padding-end: 6px; + cursor: inherit; +} + +.table-chart-totals { + margin-top: 8px; + padding-top: 6px; +} + +.theme-dark .table-chart-totals { + border-top: 1px solid #b6babf; /* Grey foreground text */ +} + +.theme-light .table-chart-totals { + border-top: 1px solid #585959; /* Grey foreground text */ +} + +.table-chart-summary-label { + font-weight: 600; + padding: 1px 0px; +} + +.theme-dark .table-chart-summary-label { + color: #f5f7fa; /* Light foreground text */ +} + +.theme-light .table-chart-summary-label { + color: #18191a; /* Dark foreground text */ +} + %include ../../shared/devtools/app-manager/manifest-editor.inc.css
deleted file mode 100644 index 9604653c0d4280d4bc4901b85786181f4f3d7929..0000000000000000000000000000000000000000 GIT binary patch literal 0 Hc$@<O00001
deleted file mode 100644 index af42a28df9c4a32ecdc6cbe670abbe5c7f4fe316..0000000000000000000000000000000000000000 GIT binary patch literal 0 Hc$@<O00001
--- a/browser/themes/windows/jar.mn +++ b/browser/themes/windows/jar.mn @@ -261,18 +261,20 @@ browser.jar: skin/classic/browser/devtools/tool-debugger-paused.svg (../shared/devtools/images/tool-debugger-paused.svg) skin/classic/browser/devtools/tool-inspector.svg (../shared/devtools/images/tool-inspector.svg) skin/classic/browser/devtools/tool-styleeditor.svg (../shared/devtools/images/tool-styleeditor.svg) skin/classic/browser/devtools/tool-profiler.svg (../shared/devtools/images/tool-profiler.svg) skin/classic/browser/devtools/tool-network.svg (../shared/devtools/images/tool-network.svg) skin/classic/browser/devtools/tool-scratchpad.svg (../shared/devtools/images/tool-scratchpad.svg) skin/classic/browser/devtools/close.png (../shared/devtools/images/close.png) skin/classic/browser/devtools/close@2x.png (../shared/devtools/images/close@2x.png) - skin/classic/browser/devtools/vview-delete.png (devtools/vview-delete.png) - skin/classic/browser/devtools/vview-edit.png (devtools/vview-edit.png) + skin/classic/browser/devtools/vview-delete.png (../shared/devtools/images/vview-delete.png) + skin/classic/browser/devtools/vview-lock.png (../shared/devtools/images/vview-lock.png) + skin/classic/browser/devtools/vview-edit.png (../shared/devtools/images/vview-edit.png) + skin/classic/browser/devtools/vview-open-inspector.png (../shared/devtools/images/vview-open-inspector.png) skin/classic/browser/devtools/undock@2x.png (../shared/devtools/images/undock@2x.png) skin/classic/browser/devtools/font-inspector.css (devtools/font-inspector.css) skin/classic/browser/devtools/computedview.css (devtools/computedview.css) skin/classic/browser/devtools/arrow-e.png (devtools/arrow-e.png) skin/classic/browser/devtools/responsiveui-rotate.png (../shared/devtools/responsiveui-rotate.png) skin/classic/browser/devtools/responsiveui-touch.png (../shared/devtools/responsiveui-touch.png) skin/classic/browser/devtools/responsiveui-screenshot.png (../shared/devtools/responsiveui-screenshot.png) skin/classic/browser/devtools/app-manager/connection-footer.css (../shared/devtools/app-manager/connection-footer.css) @@ -567,18 +569,20 @@ browser.jar: skin/classic/aero/browser/devtools/tool-debugger-paused.svg (../shared/devtools/images/tool-debugger-paused.svg) skin/classic/aero/browser/devtools/tool-inspector.svg (../shared/devtools/images/tool-inspector.svg) skin/classic/aero/browser/devtools/tool-styleeditor.svg (../shared/devtools/images/tool-styleeditor.svg) skin/classic/aero/browser/devtools/tool-profiler.svg (../shared/devtools/images/tool-profiler.svg) skin/classic/aero/browser/devtools/tool-network.svg (../shared/devtools/images/tool-network.svg) skin/classic/aero/browser/devtools/tool-scratchpad.svg (../shared/devtools/images/tool-scratchpad.svg) skin/classic/aero/browser/devtools/close.png (../shared/devtools/images/close.png) skin/classic/aero/browser/devtools/close@2x.png (../shared/devtools/images/close@2x.png) - skin/classic/aero/browser/devtools/vview-delete.png (devtools/vview-delete.png) - skin/classic/aero/browser/devtools/vview-edit.png (devtools/vview-edit.png) + skin/classic/aero/browser/devtools/vview-delete.png (../shared/devtools/images/vview-delete.png) + skin/classic/aero/browser/devtools/vview-lock.png (../shared/devtools/images/vview-lock.png) + skin/classic/aero/browser/devtools/vview-edit.png (../shared/devtools/images/vview-edit.png) + skin/classic/aero/browser/devtools/vview-open-inspector.png (../shared/devtools/images/vview-open-inspector.png) skin/classic/aero/browser/devtools/undock@2x.png (../shared/devtools/images/undock@2x.png) skin/classic/aero/browser/devtools/font-inspector.css (devtools/font-inspector.css) skin/classic/aero/browser/devtools/computedview.css (devtools/computedview.css) skin/classic/aero/browser/devtools/arrow-e.png (devtools/arrow-e.png) skin/classic/aero/browser/devtools/responsiveui-rotate.png (../shared/devtools/responsiveui-rotate.png) skin/classic/aero/browser/devtools/responsiveui-touch.png (../shared/devtools/responsiveui-touch.png) skin/classic/aero/browser/devtools/responsiveui-screenshot.png (../shared/devtools/responsiveui-screenshot.png) skin/classic/aero/browser/devtools/app-manager/connection-footer.css (../shared/devtools/app-manager/connection-footer.css)
--- a/mobile/android/base/tests/BaseTest.java +++ b/mobile/android/base/tests/BaseTest.java @@ -85,16 +85,26 @@ abstract class BaseTest extends Activity geckoReadyExpector.blockForEvent(); } geckoReadyExpector.unregisterListener(); } catch (Exception e) { mAsserter.dumpLog("Exception in blockForGeckoReady", e); } } + protected void blockForGeckoDelayedStartup() { + try { + Actions.EventExpecter geckoReadyExpector = mActions.expectGeckoEvent("Gecko:DelayedStartup"); + geckoReadyExpector.blockForEvent(); + geckoReadyExpector.unregisterListener(); + } catch (Exception e) { + mAsserter.dumpLog("Exception in blockForGeckoDelayedStartup", e); + } + } + static { try { mLauncherActivityClass = (Class<Activity>)Class.forName(LAUNCH_ACTIVITY_FULL_CLASSNAME); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } }
--- a/mobile/android/base/tests/testFormHistory.java +++ b/mobile/android/base/tests/testFormHistory.java @@ -24,17 +24,17 @@ public class testFormHistory extends Bas } public void testFormHistory() { Context context = (Context)getActivity(); ContentResolver cr = context.getContentResolver(); ContentValues[] cvs = new ContentValues[1]; cvs[0] = new ContentValues(); - blockForGeckoReady(); + blockForGeckoDelayedStartup(); Uri formHistoryUri; Uri insertUri; Uri expectedUri; int numUpdated; int numDeleted; cvs[0].put("fieldname", "fieldname");
--- a/mobile/android/base/tests/testPasswordProvider.java +++ b/mobile/android/base/tests/testPasswordProvider.java @@ -25,17 +25,17 @@ public class testPasswordProvider extend } public void testPasswordProvider() { Context context = (Context)getActivity(); ContentResolver cr = context.getContentResolver(); ContentValues[] cvs = new ContentValues[1]; cvs[0] = new ContentValues(); - blockForGeckoReady(); + blockForGeckoDelayedStartup(); cvs[0].put("hostname", "http://www.example.com"); cvs[0].put("httpRealm", "http://www.example.com"); cvs[0].put("formSubmitURL", "http://www.example.com"); cvs[0].put("usernameField", "usernameField"); cvs[0].put("passwordField", "passwordField"); cvs[0].put("encryptedUsername", "username"); cvs[0].put("encryptedPassword", "password");
--- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -282,16 +282,24 @@ var BrowserApp = { deck: null, startup: function startup() { window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = new nsBrowserAccess(); dump("zerdatime " + Date.now() + " - browser chrome startup finished."); this.deck = document.getElementById("browsers"); + this.deck.addEventListener("DOMContentLoaded", function BrowserApp_delayedStartup() { + try { + BrowserApp.deck.removeEventListener("DOMContentLoaded", BrowserApp_delayedStartup, false); + Services.obs.notifyObservers(window, "browser-delayed-startup-finished", ""); + sendMessageToJava({ type: "Gecko:DelayedStartup" }); + } catch(ex) { console.log(ex); } + }, false); + BrowserEventHandler.init(); ViewportHandler.init(); Services.androidBridge.browserApp = this; Services.obs.addObserver(this, "Locale:Changed", false); Services.obs.addObserver(this, "Tab:Load", false); Services.obs.addObserver(this, "Tab:Selected", false); @@ -410,16 +418,18 @@ var BrowserApp = { // XXX maybe we don't do this if the launch was kicked off from external Services.io.offline = false; // Broadcast a UIReady message so add-ons know we are finished with startup let event = document.createEvent("Events"); event.initEvent("UIReady", true, false); window.dispatchEvent(event); + Services.obs.addObserver(this, "browser-delayed-startup-finished", false); + if (this._startupStatus) this.onAppUpdated(); // Store the low-precision buffer pref this.gUseLowPrecision = Services.prefs.getBoolPref("layers.low-precision-buffer"); // notify java that gecko has loaded sendMessageToJava({ type: "Gecko:Ready" }); @@ -681,20 +691,16 @@ var BrowserApp = { // Skipped trying to pull MIME type out of cache for now ContentAreaUtils.internalSave(url, null, null, null, null, false, filePickerTitleKey, null, aTarget.ownerDocument.documentURIObject, aTarget.ownerDocument, true, null); }); }, onAppUpdated: function() { - // initialize the form history and passwords databases on upgrades - Services.obs.notifyObservers(null, "FormHistory:Init", ""); - Services.obs.notifyObservers(null, "Passwords:Init", ""); - // Migrate user-set "plugins.click_to_play" pref. See bug 884694. // Because the default value is true, a user-set pref means that the pref was set to false. if (Services.prefs.prefHasUserValue("plugins.click_to_play")) { Services.prefs.setIntPref("plugin.default.state", Ci.nsIPluginTag.STATE_ENABLED); Services.prefs.clearUserPref("plugins.click_to_play"); } }, @@ -1601,23 +1607,35 @@ var BrowserApp = { console.log("Locale:Changed: " + aData); // TODO: do we need to be more nuanced here -- e.g., checking for the // OS locale -- or should it always be false on Fennec? Services.prefs.setBoolPref("intl.locale.matchOS", false); Services.prefs.setCharPref("general.useragent.locale", aData); break; + case "browser-delayed-startup-finished": + this._delayedStartup(); + break; + default: dump('BrowserApp.observe: unexpected topic "' + aTopic + '"\n'); break; } }, + _delayedStartup: function() { + // initialize the form history and passwords databases on upgrades + if (this._startupStatus) { + Services.obs.notifyObservers(null, "FormHistory:Init", ""); + Services.obs.notifyObservers(null, "Passwords:Init", ""); + } + }, + get defaultBrowserWidth() { delete this.defaultBrowserWidth; let width = Services.prefs.getIntPref("browser.viewport.desktopWidth"); return this.defaultBrowserWidth = width; }, // nsIAndroidBrowserApp getBrowserTab: function(tabId) {
--- a/mobile/android/components/Snippets.js +++ b/mobile/android/components/Snippets.js @@ -317,16 +317,20 @@ function Snippets() {} Snippets.prototype = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsITimerCallback]), classID: Components.ID("{a78d7e59-b558-4321-a3d6-dffe2f1e76dd}"), observe: function(subject, topic, data) { switch(topic) { case "profile-after-change": + Services.obs.addObserver(this, "browser-delayed-startup-finished", false); + break; + case "browser-delayed-startup-finished": + Services.obs.removeObserver(this, "browser-delayed-startup-finished", false); if (Services.prefs.getBoolPref("browser.snippets.syncPromo.enabled")) { loadSyncPromoBanner(); } if (Services.prefs.getBoolPref("browser.snippets.enabled")) { loadSnippetsFromCache(); } break;
--- a/toolkit/devtools/server/actors/inspector.js +++ b/toolkit/devtools/server/actors/inspector.js @@ -2081,17 +2081,41 @@ var WalkerActor = protocol.ActorClass({ added: [], removed: [] }); } // Need to force a release of this node, because those nodes can't // be accessed anymore. this.releaseNode(documentActor, { force: true }); - } + }, + + /** + * Given an ObjectActor (identified by its ID), commonly used in the debugger, + * webconsole and variablesView, return the corresponding inspector's NodeActor + */ + getNodeActorFromObjectActor: method(function(objectActorID) { + let debuggerObject = this.conn.poolFor(objectActorID).get(objectActorID).obj; + let rawNode = debuggerObject.unsafeDereference(); + + // This is a special case for the document object whereby it is considered + // as document.documentElement (the <html> node) + if (rawNode.defaultView && rawNode === rawNode.defaultView.document) { + rawNode = rawNode.documentElement; + } + + return this.attachElement(rawNode); + }, { + request: { + objectActorID: Arg(0, "string") + }, + response: { + nodeFront: RetVal("disconnectedNode") + } + }), }); /** * Client side of the DOM walker. */ var WalkerFront = exports.WalkerFront = protocol.FrontClass(WalkerActor, { // Set to true if cleanup should be requested after every mutation list. autoCleanup: true, @@ -2215,16 +2239,24 @@ var WalkerFront = exports.WalkerFront = querySelector: protocol.custom(function(queryNode, selector) { return this._querySelector(queryNode, selector).then(response => { return response.node; }); }, { impl: "_querySelector" }), + getNodeActorFromObjectActor: protocol.custom(function(objectActorID) { + return this._getNodeActorFromObjectActor(objectActorID).then(response => { + return response.node; + }); + }, { + impl: "_getNodeActorFromObjectActor" + }), + _releaseFront: function(node, force) { if (node.retained && !force) { node.reparent(null); this._retainedOrphans.add(node); return; } if (node.retained) {
--- a/toolkit/devtools/server/actors/stylesheets.js +++ b/toolkit/devtools/server/actors/stylesheets.js @@ -542,17 +542,17 @@ let StyleSheetActor = protocol.ActorClas this._originalSources = null; }, /** * Sets the source map's sourceRoot to be relative to the source map url. */ _setSourceMapRoot: function(aSourceMap, aAbsSourceMapURL, aScriptURL) { const base = dirname( - aAbsSourceMapURL.indexOf("data:") === 0 + aAbsSourceMapURL.startsWith("data:") ? aScriptURL : aAbsSourceMapURL); aSourceMap.sourceRoot = aSourceMap.sourceRoot ? normalize(aSourceMap.sourceRoot, base) : base; }, /** @@ -695,36 +695,36 @@ let StyleSheetActor = protocol.ActorClas // Set up clean up and commit after transition duration (+10% buffer) // @see _onTransitionEnd this.window.setTimeout(this._onTransitionEnd.bind(this), Math.floor(TRANSITION_DURATION_MS * 1.1)); }, /** - * This cleans up class and rule added for transition effect and then - * notifies that the style has been applied. - */ + * This cleans up class and rule added for transition effect and then + * notifies that the style has been applied. + */ _onTransitionEnd: function() { if (--this._transitionRefCount == 0) { this.document.documentElement.classList.remove(TRANSITION_CLASS); this.rawSheet.deleteRule(this.rawSheet.cssRules.length - 1); } events.emit(this, "style-applied"); } }) /** * StyleSheetFront is the client-side counterpart to a StyleSheetActor. */ var StyleSheetFront = protocol.FrontClass(StyleSheetActor, { - initialize: function(conn, form, ctx, detail) { - protocol.Front.prototype.initialize.call(this, conn, form, ctx, detail); + initialize: function(conn, form) { + protocol.Front.prototype.initialize.call(this, conn, form); this._onPropertyChange = this._onPropertyChange.bind(this); events.on(this, "property-change", this._onPropertyChange); }, destroy: function() { events.off(this, "property-change", this._onPropertyChange); @@ -770,17 +770,17 @@ let OriginalSourceActor = protocol.Actor this.text = null; }, form: function() { return { actor: this.actorID, // actorID is set when it's added to a pool url: this.url, - parentSource: this.parentActor.actorID + relatedStyleSheet: this.parentActor.form() }; }, _getText: function() { if (this.text) { return promise.resolve(this.text); } return fetch(this.url, { window: this.window }).then(({content}) => {
--- a/toolkit/devtools/server/actors/webbrowser.js +++ b/toolkit/devtools/server/actors/webbrowser.js @@ -776,17 +776,22 @@ BrowserTabActor.prototype = { reload = true; } if (typeof options.cacheEnabled !== "undefined" && options.cacheEnabled !== this._getCacheEnabled()) { this._setCacheEnabled(options.cacheEnabled); reload = true; } - if (reload) { + // Reload if: + // - there's an explicit `performReload` flag and it's true + // - there's no `performReload` flag, but it makes sense to do so + let hasExplicitReloadFlag = "performReload" in options; + if ((hasExplicitReloadFlag && options.performReload) || + (!hasExplicitReloadFlag && reload)) { this.onReload(); } }, /** * Disable or enable the cache via docShell. */ _setCacheEnabled: function(allow) {