mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-17 15:25:52 +00:00
Merge fx-team to m-c. a=merge
This commit is contained in:
commit
786f6e25a8
@ -3799,8 +3799,11 @@ function toJavaScriptConsole()
|
||||
|
||||
function BrowserDownloadsUI()
|
||||
{
|
||||
Cc["@mozilla.org/download-manager-ui;1"].
|
||||
getService(Ci.nsIDownloadManagerUI).show(window);
|
||||
if (PrivateBrowsingUtils.isWindowPrivate(window)) {
|
||||
openUILinkIn("about:downloads", "tab");
|
||||
} else {
|
||||
PlacesCommandHook.showPlacesOrganizer("Downloads");
|
||||
}
|
||||
}
|
||||
|
||||
function toOpenWindowByType(inType, uri, features)
|
||||
|
@ -988,13 +988,18 @@
|
||||
</xul:treecols>
|
||||
<xul:treechildren class="autocomplete-treebody"/>
|
||||
</xul:tree>
|
||||
<xul:hbox anonid="search-panel-one-offs-header"
|
||||
<xul:deck anonid="search-panel-one-offs-header"
|
||||
selectedIndex="0"
|
||||
class="search-panel-header search-panel-current-input"
|
||||
xbl:inherits="hidden=showonlysettings">
|
||||
<xul:label anonid="searchbar-oneoffheader-before" value="&searchFor.label;"/>
|
||||
<xul:label anonid="searchbar-oneoffheader-searchtext" flex="1" crop="end" class="search-panel-input-value"/>
|
||||
<xul:label anonid="searchbar-oneoffheader-after" flex="10000" value="&searchWith.label;"/>
|
||||
</xul:hbox>
|
||||
<xul:label anonid="searchbar-oneoffheader-search" value="&searchWithHeader.label;"/>
|
||||
<xul:hbox anonid="search-panel-searchforwith"
|
||||
class="search-panel-current-input">
|
||||
<xul:label anonid="searchbar-oneoffheader-before" value="&searchFor.label;"/>
|
||||
<xul:label anonid="searchbar-oneoffheader-searchtext" flex="1" crop="end" class="search-panel-input-value"/>
|
||||
<xul:label anonid="searchbar-oneoffheader-after" flex="10000" value="&searchWith.label;"/>
|
||||
</xul:hbox>
|
||||
</xul:deck>
|
||||
<xul:description anonid="search-panel-one-offs"
|
||||
class="search-panel-one-offs"
|
||||
xbl:inherits="hidden=showonlysettings"/>
|
||||
@ -1059,12 +1064,20 @@
|
||||
let headerSearchText =
|
||||
document.getAnonymousElementByAttribute(this, "anonid",
|
||||
"searchbar-oneoffheader-searchtext");
|
||||
let headerPanel =
|
||||
document.getAnonymousElementByAttribute(this, "anonid",
|
||||
"search-panel-one-offs-header");
|
||||
let textbox = searchbar.textbox;
|
||||
let self = this;
|
||||
let inputHandler = function() {
|
||||
headerSearchText.setAttribute("value", textbox.value);
|
||||
if (textbox.value)
|
||||
if (textbox.value) {
|
||||
self.removeAttribute("showonlysettings");
|
||||
headerPanel.selectedIndex = 1;
|
||||
}
|
||||
else {
|
||||
headerPanel.selectedIndex = 0;
|
||||
}
|
||||
};
|
||||
textbox.addEventListener("input", inputHandler);
|
||||
this.addEventListener("popuphiding", function hiding() {
|
||||
|
@ -1,4 +1,3 @@
|
||||
component {49507fe5-2cee-4824-b6a3-e999150ce9b8} DownloadsStartup.js
|
||||
contract @mozilla.org/browser/downloadsstartup;1 {49507fe5-2cee-4824-b6a3-e999150ce9b8}
|
||||
category profile-after-change DownloadsStartup @mozilla.org/browser/downloadsstartup;1
|
||||
component {4d99321e-d156-455b-81f7-e7aa2308134f} DownloadsUI.js
|
||||
|
@ -22,12 +22,6 @@ const Cr = Components.results;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
/**
|
||||
* CID and Contract ID of our implementation of nsIDownloadManagerUI.
|
||||
*/
|
||||
const kDownloadsUICid = Components.ID("{4d99321e-d156-455b-81f7-e7aa2308134f}");
|
||||
const kDownloadsUIContractId = "@mozilla.org/download-manager-ui;1";
|
||||
|
||||
/**
|
||||
* CID and Contract ID of the JavaScript implementation of nsITransfer.
|
||||
*/
|
||||
@ -54,18 +48,6 @@ DownloadsStartup.prototype = {
|
||||
|
||||
observe: function DS_observe(aSubject, aTopic, aData)
|
||||
{
|
||||
if (aTopic != "profile-after-change") {
|
||||
Cu.reportError("Unexpected observer notification.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Override Toolkit's nsIDownloadManagerUI implementation with our own.
|
||||
// This must be done at application startup and not in the manifest to
|
||||
// ensure that our implementation overrides the original one.
|
||||
Components.manager.QueryInterface(Ci.nsIComponentRegistrar)
|
||||
.registerFactory(kDownloadsUICid, "",
|
||||
kDownloadsUIContractId, null);
|
||||
|
||||
// Override Toolkit's nsITransfer implementation with the one from the
|
||||
// JavaScript API for downloads.
|
||||
Components.manager.QueryInterface(Ci.nsIComponentRegistrar)
|
||||
|
@ -1,128 +0,0 @@
|
||||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* This component implements the nsIDownloadManagerUI interface and opens the
|
||||
* Downloads view for the most recent browser window when requested.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Globals
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cu = Components.utils;
|
||||
const Cr = Components.results;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon",
|
||||
"resource:///modules/DownloadsCommon.jsm");
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "gBrowserGlue",
|
||||
"@mozilla.org/browser/browserglue;1",
|
||||
"nsIBrowserGlue");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
|
||||
"resource:///modules/RecentWindow.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
|
||||
"resource://gre/modules/PrivateBrowsingUtils.jsm");
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// DownloadsUI
|
||||
|
||||
function DownloadsUI()
|
||||
{
|
||||
}
|
||||
|
||||
DownloadsUI.prototype = {
|
||||
classID: Components.ID("{4d99321e-d156-455b-81f7-e7aa2308134f}"),
|
||||
|
||||
_xpcom_factory: XPCOMUtils.generateSingletonFactory(DownloadsUI),
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//// nsISupports
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIDownloadManagerUI]),
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//// nsIDownloadManagerUI
|
||||
|
||||
show: function DUI_show(aWindowContext, aDownload, aReason, aUsePrivateUI)
|
||||
{
|
||||
if (!aReason) {
|
||||
aReason = Ci.nsIDownloadManagerUI.REASON_USER_INTERACTED;
|
||||
}
|
||||
|
||||
if (aReason == Ci.nsIDownloadManagerUI.REASON_NEW_DOWNLOAD) {
|
||||
const kMinimized = Ci.nsIDOMChromeWindow.STATE_MINIMIZED;
|
||||
let browserWin = gBrowserGlue.getMostRecentBrowserWindow();
|
||||
|
||||
if (!browserWin || browserWin.windowState == kMinimized) {
|
||||
this._showDownloadManagerUI(aWindowContext, aUsePrivateUI);
|
||||
}
|
||||
else {
|
||||
// If the indicator is visible, then new download notifications are
|
||||
// already handled by the panel service.
|
||||
browserWin.DownloadsButton.checkIsVisible(function(isVisible) {
|
||||
if (!isVisible) {
|
||||
this._showDownloadManagerUI(aWindowContext, aUsePrivateUI);
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
} else {
|
||||
this._showDownloadManagerUI(aWindowContext, aUsePrivateUI);
|
||||
}
|
||||
},
|
||||
|
||||
get visible() true,
|
||||
|
||||
getAttention: function () {},
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//// Private
|
||||
|
||||
/**
|
||||
* Helper function that opens the download manager UI.
|
||||
*/
|
||||
_showDownloadManagerUI: function (aWindowContext, aUsePrivateUI)
|
||||
{
|
||||
// If we weren't given a window context, try to find a browser window
|
||||
// to use as our parent - and if that doesn't work, error out and give up.
|
||||
let parentWindow = aWindowContext;
|
||||
if (!parentWindow) {
|
||||
parentWindow = RecentWindow.getMostRecentBrowserWindow({ private: !!aUsePrivateUI });
|
||||
if (!parentWindow) {
|
||||
Components.utils.reportError(
|
||||
"Couldn't find a browser window to open the Places Downloads View " +
|
||||
"from.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If window is private then show it in a tab.
|
||||
if (PrivateBrowsingUtils.isWindowPrivate(parentWindow)) {
|
||||
parentWindow.openUILinkIn("about:downloads", "tab");
|
||||
return;
|
||||
} else {
|
||||
let organizer = Services.wm.getMostRecentWindow("Places:Organizer");
|
||||
if (!organizer) {
|
||||
parentWindow.openDialog("chrome://browser/content/places/places.xul",
|
||||
"", "chrome,toolbar=yes,dialog=no,resizable",
|
||||
"Downloads");
|
||||
} else {
|
||||
organizer.PlacesOrganizer.selectLeftPaneQuery("Downloads");
|
||||
organizer.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Module
|
||||
|
||||
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DownloadsUI]);
|
@ -12,7 +12,6 @@ JAR_MANIFESTS += ['jar.mn']
|
||||
EXTRA_COMPONENTS += [
|
||||
'BrowserDownloads.manifest',
|
||||
'DownloadsStartup.js',
|
||||
'DownloadsUI.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES += [
|
||||
|
@ -526,7 +526,7 @@ var gCookiesWindow = {
|
||||
},
|
||||
|
||||
onCookieSelected: function () {
|
||||
var properties, item;
|
||||
var item;
|
||||
var seln = this._tree.view.selection;
|
||||
if (!this._view._filtered)
|
||||
item = this._view._getItemAtIndex(seln.currentIndex);
|
||||
@ -543,15 +543,12 @@ var gCookiesWindow = {
|
||||
for (var j = min.value; j <= max.value; ++j) {
|
||||
item = this._view._getItemAtIndex(j);
|
||||
if (!item) continue;
|
||||
if (item.container && !item.open)
|
||||
if (item.container)
|
||||
selectedCookieCount += item.cookies.length;
|
||||
else if (!item.container)
|
||||
++selectedCookieCount;
|
||||
}
|
||||
}
|
||||
var item = this._view._getItemAtIndex(seln.currentIndex);
|
||||
if (item && seln.count == 1 && item.container && item.open)
|
||||
selectedCookieCount += 2;
|
||||
|
||||
let buttonLabel = this._bundle.getString("removeSelectedCookies");
|
||||
let removeSelectedCookies = document.getElementById("removeSelectedCookies");
|
||||
|
@ -180,6 +180,11 @@ var gSearchPane = {
|
||||
Services.search.currentEngine =
|
||||
document.getElementById("defaultEngine").selectedItem.engine;
|
||||
}
|
||||
},
|
||||
|
||||
loadAddEngines: function () {
|
||||
window.opener.BrowserSearch.loadAddEngines();
|
||||
window.document.documentElement.acceptDialog();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -82,7 +82,7 @@
|
||||
|
||||
<hbox pack="start">
|
||||
<label id="addEngines" class="text-link" value="&addMoreSearchEngines.label;"
|
||||
onclick="if (event.button == 0) { Services.wm.getMostRecentWindow('navigator:browser').BrowserSearch.loadAddEngines(); }"/>
|
||||
onclick="if (event.button == 0) { gSearchPane.loadAddEngines(); }"/>
|
||||
</hbox>
|
||||
</groupbox>
|
||||
|
||||
|
223
browser/devtools/animationinspector/animation-controller.js
Normal file
223
browser/devtools/animationinspector/animation-controller.js
Normal file
@ -0,0 +1,223 @@
|
||||
/* -*- indent-tabs-mode: nil; js-indent-level: 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 { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://gre/modules/devtools/Loader.jsm");
|
||||
Cu.import("resource://gre/modules/devtools/Console.jsm");
|
||||
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
|
||||
|
||||
devtools.lazyRequireGetter(this, "promise");
|
||||
devtools.lazyRequireGetter(this, "EventEmitter",
|
||||
"devtools/toolkit/event-emitter");
|
||||
devtools.lazyRequireGetter(this, "AnimationsFront",
|
||||
"devtools/server/actors/animation", true);
|
||||
|
||||
const require = devtools.require;
|
||||
|
||||
const STRINGS_URI = "chrome://browser/locale/devtools/animationinspector.properties";
|
||||
const L10N = new ViewHelpers.L10N(STRINGS_URI);
|
||||
|
||||
// Global toolbox/inspector, set when startup is called.
|
||||
let gToolbox, gInspector;
|
||||
|
||||
/**
|
||||
* Startup the animationinspector controller and view, called by the sidebar
|
||||
* widget when loading/unloading the iframe into the tab.
|
||||
*/
|
||||
let startup = Task.async(function*(inspector) {
|
||||
gInspector = inspector;
|
||||
gToolbox = inspector.toolbox;
|
||||
|
||||
// Don't assume that AnimationsPanel is defined here, it's in another file.
|
||||
if (!typeof AnimationsPanel === "undefined") {
|
||||
throw new Error("AnimationsPanel was not loaded in the animationinspector window");
|
||||
}
|
||||
|
||||
yield promise.all([
|
||||
AnimationsController.initialize(),
|
||||
AnimationsPanel.initialize()
|
||||
]).then(null, Cu.reportError);
|
||||
});
|
||||
|
||||
/**
|
||||
* Shutdown the animationinspector controller and view, called by the sidebar
|
||||
* widget when loading/unloading the iframe into the tab.
|
||||
*/
|
||||
let shutdown = Task.async(function*() {
|
||||
yield promise.all([
|
||||
AnimationsController.destroy(),
|
||||
// Don't assume that AnimationsPanel is defined here, it's in another file.
|
||||
typeof AnimationsPanel !== "undefined"
|
||||
? AnimationsPanel.destroy()
|
||||
: promise.resolve()
|
||||
]).then(() => {
|
||||
gToolbox = gInspector = null;
|
||||
}, Cu.reportError);
|
||||
});
|
||||
|
||||
// This is what makes the sidebar widget able to load/unload the panel.
|
||||
function setPanel(panel) {
|
||||
return startup(panel);
|
||||
}
|
||||
function destroy() {
|
||||
return shutdown();
|
||||
}
|
||||
|
||||
/**
|
||||
* The animationinspector controller's job is to retrieve AnimationPlayerFronts
|
||||
* from the server. It is also responsible for keeping the list of players up to
|
||||
* date when the node selection changes in the inspector, as well as making sure
|
||||
* no updates are done when the animationinspector sidebar panel is not visible.
|
||||
*
|
||||
* AnimationPlayerFronts are available in AnimationsController.animationPlayers.
|
||||
*
|
||||
* Note also that all AnimationPlayerFronts handled by the controller are set to
|
||||
* auto-refresh (except when the sidebar panel is not visible).
|
||||
*
|
||||
* Usage example:
|
||||
*
|
||||
* AnimationsController.on(AnimationsController.PLAYERS_UPDATED_EVENT, onPlayers);
|
||||
* function onPlayers() {
|
||||
* for (let player of AnimationsController.animationPlayers) {
|
||||
* // do something with player
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
let AnimationsController = {
|
||||
PLAYERS_UPDATED_EVENT: "players-updated",
|
||||
|
||||
initialize: Task.async(function*() {
|
||||
if (this.initialized) {
|
||||
return this.initialized.promise;
|
||||
}
|
||||
this.initialized = promise.defer();
|
||||
|
||||
let target = gToolbox.target;
|
||||
this.animationsFront = new AnimationsFront(target.client, target.form);
|
||||
|
||||
this.onPanelVisibilityChange = this.onPanelVisibilityChange.bind(this);
|
||||
this.onNewNodeFront = this.onNewNodeFront.bind(this);
|
||||
|
||||
this.startListeners();
|
||||
|
||||
yield this.onNewNodeFront();
|
||||
|
||||
this.initialized.resolve();
|
||||
}),
|
||||
|
||||
destroy: Task.async(function*() {
|
||||
if (!this.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.destroyed) {
|
||||
return this.destroyed.promise;
|
||||
}
|
||||
this.destroyed = promise.defer();
|
||||
|
||||
this.stopListeners();
|
||||
yield this.destroyAnimationPlayers();
|
||||
this.nodeFront = null;
|
||||
|
||||
if (this.animationsFront) {
|
||||
this.animationsFront.destroy();
|
||||
this.animationsFront = null;
|
||||
}
|
||||
|
||||
this.destroyed.resolve();
|
||||
}),
|
||||
|
||||
startListeners: function() {
|
||||
// Re-create the list of players when a new node is selected, except if the
|
||||
// sidebar isn't visible. And set the players to auto-refresh when needed.
|
||||
gInspector.selection.on("new-node-front", this.onNewNodeFront);
|
||||
gInspector.sidebar.on("select", this.onPanelVisibilityChange);
|
||||
gToolbox.on("select", this.onPanelVisibilityChange);
|
||||
},
|
||||
|
||||
stopListeners: function() {
|
||||
gInspector.selection.off("new-node-front", this.onNewNodeFront);
|
||||
gInspector.sidebar.off("select", this.onPanelVisibilityChange);
|
||||
gToolbox.off("select", this.onPanelVisibilityChange);
|
||||
},
|
||||
|
||||
isPanelVisible: function() {
|
||||
return gToolbox.currentToolId === "inspector" &&
|
||||
gInspector.sidebar &&
|
||||
gInspector.sidebar.getCurrentTabID() == "animationinspector";
|
||||
},
|
||||
|
||||
onPanelVisibilityChange: Task.async(function*(e, id) {
|
||||
if (this.isPanelVisible()) {
|
||||
this.onNewNodeFront();
|
||||
this.startAllAutoRefresh();
|
||||
} else {
|
||||
this.stopAllAutoRefresh();
|
||||
}
|
||||
}),
|
||||
|
||||
onNewNodeFront: Task.async(function*() {
|
||||
// Ignore if the panel isn't visible or the node selection hasn't changed.
|
||||
if (!this.isPanelVisible() || this.nodeFront === gInspector.selection.nodeFront) {
|
||||
return;
|
||||
}
|
||||
|
||||
let done = gInspector.updating("animationscontroller");
|
||||
|
||||
if(!gInspector.selection.isConnected() ||
|
||||
!gInspector.selection.isElementNode()) {
|
||||
yield this.destroyAnimationPlayers();
|
||||
this.emit(this.PLAYERS_UPDATED_EVENT);
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
this.nodeFront = gInspector.selection.nodeFront;
|
||||
yield this.refreshAnimationPlayers(this.nodeFront);
|
||||
this.emit(this.PLAYERS_UPDATED_EVENT, this.animationPlayers);
|
||||
|
||||
done();
|
||||
}),
|
||||
|
||||
// AnimationPlayerFront objects are managed by this controller. They are
|
||||
// retrieved when refreshAnimationPlayers is called, stored in the
|
||||
// animationPlayers array, and destroyed when refreshAnimationPlayers is
|
||||
// called again.
|
||||
animationPlayers: [],
|
||||
|
||||
refreshAnimationPlayers: Task.async(function*(nodeFront) {
|
||||
yield this.destroyAnimationPlayers();
|
||||
|
||||
this.animationPlayers = yield this.animationsFront.getAnimationPlayersForNode(nodeFront);
|
||||
this.startAllAutoRefresh();
|
||||
}),
|
||||
|
||||
startAllAutoRefresh: function() {
|
||||
for (let front of this.animationPlayers) {
|
||||
front.startAutoRefresh();
|
||||
}
|
||||
},
|
||||
|
||||
stopAllAutoRefresh: function() {
|
||||
for (let front of this.animationPlayers) {
|
||||
front.stopAutoRefresh();
|
||||
}
|
||||
},
|
||||
|
||||
destroyAnimationPlayers: Task.async(function*() {
|
||||
this.stopAllAutoRefresh();
|
||||
for (let front of this.animationPlayers) {
|
||||
yield front.release();
|
||||
}
|
||||
this.animationPlayers = [];
|
||||
})
|
||||
};
|
||||
|
||||
EventEmitter.decorate(AnimationsController);
|
@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- 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/. -->
|
||||
<!DOCTYPE html [
|
||||
<!ENTITY % animationinspectorDTD SYSTEM "chrome://browser/locale/devtools/animationinspector.dtd" >
|
||||
%animationinspectorDTD;
|
||||
]>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<title>&title;</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
<link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/>
|
||||
<link rel="stylesheet" href="chrome://browser/skin/devtools/animationinspector.css" type="text/css"/>
|
||||
<script type="application/javascript;version=1.8" src="chrome://browser/content/devtools/theme-switching.js"/>
|
||||
</head>
|
||||
<body class="theme-sidebar devtools-monospace" role="application">
|
||||
<div id="players" class="theme-toolbar"></div>
|
||||
<div id="error-message">
|
||||
<p>&invalidElement;</p>
|
||||
<p>&selectElement;</p>
|
||||
<button id="element-picker" standalone="true" class="devtools-button"></button>
|
||||
</div>
|
||||
<script type="application/javascript;version=1.8" src="animation-controller.js"></script>
|
||||
<script type="application/javascript;version=1.8" src="animation-panel.js"></script>
|
||||
</body>
|
||||
</html>
|
432
browser/devtools/animationinspector/animation-panel.js
Normal file
432
browser/devtools/animationinspector/animation-panel.js
Normal file
@ -0,0 +1,432 @@
|
||||
/* -*- indent-tabs-mode: nil; js-indent-level: 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";
|
||||
|
||||
/**
|
||||
* The main animations panel UI.
|
||||
*/
|
||||
let AnimationsPanel = {
|
||||
UI_UPDATED_EVENT: "ui-updated",
|
||||
|
||||
initialize: Task.async(function*() {
|
||||
if (this.initialized) {
|
||||
return this.initialized.promise;
|
||||
}
|
||||
this.initialized = promise.defer();
|
||||
|
||||
this.playersEl = document.querySelector("#players");
|
||||
this.errorMessageEl = document.querySelector("#error-message");
|
||||
this.pickerButtonEl = document.querySelector("#element-picker");
|
||||
|
||||
let hUtils = gToolbox.highlighterUtils;
|
||||
this.togglePicker = hUtils.togglePicker.bind(hUtils);
|
||||
this.onPickerStarted = this.onPickerStarted.bind(this);
|
||||
this.onPickerStopped = this.onPickerStopped.bind(this);
|
||||
this.createPlayerWidgets = this.createPlayerWidgets.bind(this);
|
||||
|
||||
this.startListeners();
|
||||
|
||||
this.initialized.resolve();
|
||||
}),
|
||||
|
||||
destroy: Task.async(function*() {
|
||||
if (!this.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.destroyed) {
|
||||
return this.destroyed.promise;
|
||||
}
|
||||
this.destroyed = promise.defer();
|
||||
|
||||
this.stopListeners();
|
||||
yield this.destroyPlayerWidgets();
|
||||
|
||||
this.playersEl = this.errorMessageEl = null;
|
||||
|
||||
this.destroyed.resolve();
|
||||
}),
|
||||
|
||||
startListeners: function() {
|
||||
AnimationsController.on(AnimationsController.PLAYERS_UPDATED_EVENT,
|
||||
this.createPlayerWidgets);
|
||||
this.pickerButtonEl.addEventListener("click", this.togglePicker, false);
|
||||
gToolbox.on("picker-started", this.onPickerStarted);
|
||||
gToolbox.on("picker-stopped", this.onPickerStopped);
|
||||
},
|
||||
|
||||
stopListeners: function() {
|
||||
AnimationsController.off(AnimationsController.PLAYERS_UPDATED_EVENT,
|
||||
this.createPlayerWidgets);
|
||||
this.pickerButtonEl.removeEventListener("click", this.togglePicker, false);
|
||||
gToolbox.off("picker-started", this.onPickerStarted);
|
||||
gToolbox.off("picker-stopped", this.onPickerStopped);
|
||||
},
|
||||
|
||||
displayErrorMessage: function() {
|
||||
this.errorMessageEl.style.display = "block";
|
||||
},
|
||||
|
||||
hideErrorMessage: function() {
|
||||
this.errorMessageEl.style.display = "none";
|
||||
},
|
||||
|
||||
onPickerStarted: function() {
|
||||
this.pickerButtonEl.setAttribute("checked", "true");
|
||||
},
|
||||
|
||||
onPickerStopped: function() {
|
||||
this.pickerButtonEl.removeAttribute("checked");
|
||||
},
|
||||
|
||||
createPlayerWidgets: Task.async(function*() {
|
||||
let done = gInspector.updating("animationspanel");
|
||||
|
||||
// Empty the whole panel first.
|
||||
this.hideErrorMessage();
|
||||
yield this.destroyPlayerWidgets();
|
||||
|
||||
// If there are no players to show, show the error message instead and return.
|
||||
if (!AnimationsController.animationPlayers.length) {
|
||||
this.displayErrorMessage();
|
||||
this.emit(this.UI_UPDATED_EVENT);
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, create player widgets.
|
||||
this.playerWidgets = [];
|
||||
let initPromises = [];
|
||||
|
||||
for (let player of AnimationsController.animationPlayers) {
|
||||
let widget = new PlayerWidget(player, this.playersEl);
|
||||
initPromises.push(widget.initialize());
|
||||
this.playerWidgets.push(widget);
|
||||
}
|
||||
|
||||
yield initPromises;
|
||||
this.emit(this.UI_UPDATED_EVENT);
|
||||
done();
|
||||
}),
|
||||
|
||||
destroyPlayerWidgets: Task.async(function*() {
|
||||
if (!this.playerWidgets) {
|
||||
return;
|
||||
}
|
||||
|
||||
let destroyers = this.playerWidgets.map(widget => widget.destroy());
|
||||
yield promise.all(destroyers);
|
||||
this.playerWidgets = null;
|
||||
this.playersEl.innerHTML = "";
|
||||
})
|
||||
};
|
||||
|
||||
EventEmitter.decorate(AnimationsPanel);
|
||||
|
||||
/**
|
||||
* An AnimationPlayer UI widget
|
||||
*/
|
||||
function PlayerWidget(player, containerEl) {
|
||||
EventEmitter.decorate(this);
|
||||
|
||||
this.player = player;
|
||||
this.containerEl = containerEl;
|
||||
|
||||
this.onStateChanged = this.onStateChanged.bind(this);
|
||||
this.onPlayPauseBtnClick = this.onPlayPauseBtnClick.bind(this);
|
||||
}
|
||||
|
||||
PlayerWidget.prototype = {
|
||||
initialize: Task.async(function*() {
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
this.initialized = true;
|
||||
|
||||
this.createMarkup();
|
||||
this.startListeners();
|
||||
}),
|
||||
|
||||
destroy: Task.async(function*() {
|
||||
if (this.destroyed) {
|
||||
return;
|
||||
}
|
||||
this.destroyed = true;
|
||||
|
||||
this.stopTimelineAnimation();
|
||||
this.stopListeners();
|
||||
|
||||
this.el.remove();
|
||||
this.playPauseBtnEl = this.currentTimeEl = this.timeDisplayEl = null;
|
||||
this.containerEl = this.el = this.player = null;
|
||||
}),
|
||||
|
||||
startListeners: function() {
|
||||
this.player.on(this.player.AUTO_REFRESH_EVENT, this.onStateChanged);
|
||||
this.playPauseBtnEl.addEventListener("click", this.onPlayPauseBtnClick);
|
||||
},
|
||||
|
||||
stopListeners: function() {
|
||||
this.player.off(this.player.AUTO_REFRESH_EVENT, this.onStateChanged);
|
||||
this.playPauseBtnEl.removeEventListener("click", this.onPlayPauseBtnClick);
|
||||
},
|
||||
|
||||
createMarkup: function() {
|
||||
let state = this.player.state;
|
||||
|
||||
this.el = createNode({
|
||||
attributes: {
|
||||
"class": "player-widget " + state.playState
|
||||
}
|
||||
});
|
||||
|
||||
// Animation header
|
||||
let titleEl = createNode({
|
||||
parent: this.el,
|
||||
attributes: {
|
||||
"class": "animation-title"
|
||||
}
|
||||
});
|
||||
let titleHTML = "";
|
||||
|
||||
// Name
|
||||
if (state.name) {
|
||||
// Css animations have names
|
||||
titleHTML += L10N.getStr("player.animationNameLabel");
|
||||
titleHTML += "<strong>" + state.name + "</strong>";
|
||||
} else {
|
||||
// Css transitions don't
|
||||
titleHTML += L10N.getStr("player.transitionNameLabel");
|
||||
}
|
||||
|
||||
// Duration and iteration count
|
||||
titleHTML += "<span class='meta-data'>";
|
||||
titleHTML += L10N.getStr("player.animationDurationLabel");
|
||||
titleHTML += "<strong>" + L10N.getFormatStr("player.timeLabel",
|
||||
this.getFormattedTime(state.duration)) + "</strong>";
|
||||
titleHTML += L10N.getStr("player.animationIterationCountLabel");
|
||||
let count = state.iterationCount || L10N.getStr("player.infiniteIterationCount");
|
||||
titleHTML += "<strong>" + count + "</strong>";
|
||||
titleHTML += "</span>"
|
||||
|
||||
titleEl.innerHTML = titleHTML;
|
||||
|
||||
// Timeline widget
|
||||
let timelineEl = createNode({
|
||||
parent: this.el,
|
||||
attributes: {
|
||||
"class": "timeline"
|
||||
}
|
||||
});
|
||||
|
||||
// Playback control buttons container
|
||||
let playbackControlsEl = createNode({
|
||||
parent: timelineEl,
|
||||
attributes: {
|
||||
"class": "playback-controls"
|
||||
}
|
||||
});
|
||||
|
||||
// Control buttons (when currentTime becomes settable, rewind and
|
||||
// fast-forward can be added here).
|
||||
this.playPauseBtnEl = createNode({
|
||||
parent: playbackControlsEl,
|
||||
nodeType: "button",
|
||||
attributes: {
|
||||
"class": "toggle devtools-button"
|
||||
}
|
||||
});
|
||||
|
||||
// Sliders container
|
||||
let slidersContainerEl = createNode({
|
||||
parent: timelineEl,
|
||||
attributes: {
|
||||
"class": "sliders-container",
|
||||
}
|
||||
});
|
||||
|
||||
let max = state.duration; // Infinite iterations
|
||||
if (state.iterationCount) {
|
||||
// Finite iterations
|
||||
max = state.iterationCount * state.duration;
|
||||
}
|
||||
|
||||
// For now, keyframes aren't exposed by the actor. So the only range <input>
|
||||
// displayed in the container is the currentTime. When keyframes are
|
||||
// available, one input per keyframe can be added here.
|
||||
this.currentTimeEl = createNode({
|
||||
nodeType: "input",
|
||||
parent: slidersContainerEl,
|
||||
attributes: {
|
||||
"type": "range",
|
||||
"class": "current-time",
|
||||
"min": "0",
|
||||
"max": max,
|
||||
"step": "10",
|
||||
// The currentTime isn't settable yet, so disable the timeline slider
|
||||
"disabled": "true"
|
||||
}
|
||||
});
|
||||
|
||||
// Time display
|
||||
this.timeDisplayEl = createNode({
|
||||
parent: timelineEl,
|
||||
attributes: {
|
||||
"class": "time-display"
|
||||
}
|
||||
});
|
||||
this.timeDisplayEl.textContent = L10N.getFormatStr("player.timeLabel",
|
||||
this.getFormattedTime());
|
||||
|
||||
this.containerEl.appendChild(this.el);
|
||||
},
|
||||
|
||||
/**
|
||||
* Format time as a string.
|
||||
* @param {Number} time Defaults to the player's currentTime.
|
||||
* @return {String} The formatted time, e.g. "10.55"
|
||||
*/
|
||||
getFormattedTime: function(time=this.player.state.currentTime) {
|
||||
let str = time/1000 + "";
|
||||
str = str.split(".");
|
||||
if (str.length === 1) {
|
||||
return str[0] + ".00";
|
||||
} else {
|
||||
return str[0] + "." + str[1].substring(0, 2);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Executed when the playPause button is clicked.
|
||||
* Note that tests may want to call this callback directly rather than
|
||||
* simulating a click on the button since it returns the promise returned by
|
||||
* play and paused.
|
||||
* @return {Promise}
|
||||
*/
|
||||
onPlayPauseBtnClick: function() {
|
||||
if (this.player.state.playState === "running") {
|
||||
return this.pause();
|
||||
} else {
|
||||
return this.play();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Whenever a player state update is received.
|
||||
*/
|
||||
onStateChanged: function() {
|
||||
let state = this.player.state;
|
||||
this.updatePlayPauseButton(state.playState);
|
||||
|
||||
switch (state.playState) {
|
||||
case "finished":
|
||||
this.destroy();
|
||||
break;
|
||||
case "running":
|
||||
this.startTimelineAnimation();
|
||||
break;
|
||||
case "paused":
|
||||
this.stopTimelineAnimation();
|
||||
this.displayTime(this.player.state.currentTime);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Pause the animation player via this widget.
|
||||
* @return {Promise} Resolves when the player is paused, the button is
|
||||
* switched to the right state, and the timeline animation is stopped.
|
||||
*/
|
||||
pause: function() {
|
||||
// Switch to the right className on the element right away to avoid waiting
|
||||
// for the next state update to change the playPause icon.
|
||||
this.updatePlayPauseButton("paused");
|
||||
return this.player.pause().then(() => {
|
||||
this.stopTimelineAnimation();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Play the animation player via this widget.
|
||||
* @return {Promise} Resolves when the player is playing, the button is
|
||||
* switched to the right state, and the timeline animation is started.
|
||||
*/
|
||||
play: function() {
|
||||
// Switch to the right className on the element right away to avoid waiting
|
||||
// for the next state update to change the playPause icon.
|
||||
this.updatePlayPauseButton("running");
|
||||
this.startTimelineAnimation();
|
||||
return this.player.play();
|
||||
},
|
||||
|
||||
updatePlayPauseButton: function(playState) {
|
||||
this.el.className = "player-widget " + playState;
|
||||
},
|
||||
|
||||
/**
|
||||
* Make the timeline progress smoothly, even though the currentTime is only
|
||||
* updated at some intervals. This uses a local animation loop.
|
||||
*/
|
||||
startTimelineAnimation: function() {
|
||||
this.stopTimelineAnimation();
|
||||
|
||||
let start = performance.now();
|
||||
let loop = () => {
|
||||
this.rafID = requestAnimationFrame(loop);
|
||||
let now = this.player.state.currentTime + performance.now() - start;
|
||||
this.displayTime(now);
|
||||
};
|
||||
|
||||
loop();
|
||||
},
|
||||
|
||||
/**
|
||||
* Display the time in the timeDisplayEl and in the currentTimeEl slider.
|
||||
*/
|
||||
displayTime: function(time) {
|
||||
let state = this.player.state;
|
||||
|
||||
this.timeDisplayEl.textContent = L10N.getFormatStr("player.timeLabel",
|
||||
this.getFormattedTime(time));
|
||||
if (!state.iterationCount && time !== state.duration) {
|
||||
this.currentTimeEl.value = time % state.duration;
|
||||
} else {
|
||||
this.currentTimeEl.value = time;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Stop the animation loop that makes the timeline progress.
|
||||
*/
|
||||
stopTimelineAnimation: function() {
|
||||
if (this.rafID) {
|
||||
cancelAnimationFrame(this.rafID);
|
||||
this.rafID = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* DOM node creation helper function.
|
||||
* @param {Object} Options to customize the node to be created.
|
||||
* @return {DOMNode} The newly created node.
|
||||
*/
|
||||
function createNode(options) {
|
||||
let type = options.nodeType || "div";
|
||||
let node = document.createElement(type);
|
||||
|
||||
for (let name in options.attributes || {}) {
|
||||
let value = options.attributes[name];
|
||||
node.setAttribute(name, value);
|
||||
}
|
||||
|
||||
if (options.parent) {
|
||||
options.parent.appendChild(node);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
7
browser/devtools/animationinspector/moz.build
Normal file
7
browser/devtools/animationinspector/moz.build
Normal file
@ -0,0 +1,7 @@
|
||||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
|
18
browser/devtools/animationinspector/test/browser.ini
Normal file
18
browser/devtools/animationinspector/test/browser.ini
Normal file
@ -0,0 +1,18 @@
|
||||
[DEFAULT]
|
||||
subsuite = devtools
|
||||
support-files =
|
||||
doc_frame_script.js
|
||||
doc_simple_animation.html
|
||||
head.js
|
||||
|
||||
[browser_animation_empty_on_invalid_nodes.js]
|
||||
[browser_animation_panel_exists.js]
|
||||
[browser_animation_participate_in_inspector_update.js]
|
||||
[browser_animation_play_pause_button.js]
|
||||
[browser_animation_playerFronts_are_refreshed.js]
|
||||
[browser_animation_playerWidgets_destroy.js]
|
||||
[browser_animation_refresh_when_active.js]
|
||||
[browser_animation_same_nb_of_playerWidgets_and_playerFronts.js]
|
||||
[browser_animation_shows_player_on_valid_node.js]
|
||||
[browser_animation_timeline_animates.js]
|
||||
[browser_animation_ui_updates_when_animation_changes.js]
|
@ -0,0 +1,24 @@
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that the panel shows no animation data for invalid or not animated nodes
|
||||
|
||||
add_task(function*() {
|
||||
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
|
||||
let {inspector, panel} = yield openAnimationInspector();
|
||||
|
||||
info("Select node .still and check that the panel is empty");
|
||||
let stillNode = yield getNodeFront(".still", inspector);
|
||||
yield selectNode(stillNode, inspector);
|
||||
ok(!panel.playerWidgets || !panel.playerWidgets.length,
|
||||
"No player widgets displayed for a still node");
|
||||
|
||||
info("Select the comment text node and check that the panel is empty");
|
||||
let commentNode = yield inspector.walker.previousSibling(stillNode);
|
||||
yield selectNode(commentNode, inspector);
|
||||
ok(!panel.playerWidgets || !panel.playerWidgets.length,
|
||||
"No player widgets displayed for a text node");
|
||||
});
|
@ -0,0 +1,18 @@
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that the animation panel sidebar exists
|
||||
|
||||
add_task(function*() {
|
||||
yield addTab("data:text/html;charset=utf-8,welcome to the animation panel");
|
||||
let {panel, controller} = yield openAnimationInspector();
|
||||
|
||||
ok(controller, "The animation controller exists");
|
||||
ok(controller.animationsFront, "The animation controller has been initialized");
|
||||
|
||||
ok(panel, "The animation panel exists");
|
||||
ok(panel.playersEl, "The animation panel has been initialized");
|
||||
});
|
@ -0,0 +1,39 @@
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that the update of the animation panel participate in the
|
||||
// inspector-updated event. This means that the test verifies that the
|
||||
// inspector-updated event is emitted *after* the animation panel is ready.
|
||||
|
||||
add_task(function*() {
|
||||
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
|
||||
let {inspector, panel, controller} = yield openAnimationInspector();
|
||||
|
||||
info("Listen for the players-updated, ui-updated and inspector-updated events");
|
||||
let receivedEvents = [];
|
||||
controller.once(controller.PLAYERS_UPDATED_EVENT, () => {
|
||||
receivedEvents.push(controller.PLAYERS_UPDATED_EVENT);
|
||||
});
|
||||
panel.once(panel.UI_UPDATED_EVENT, () => {
|
||||
receivedEvents.push(panel.UI_UPDATED_EVENT);
|
||||
})
|
||||
inspector.once("inspector-updated", () => {
|
||||
receivedEvents.push("inspector-updated");
|
||||
});
|
||||
|
||||
info("Selecting an animated node");
|
||||
let node = yield getNodeFront(".animated", inspector);
|
||||
yield selectNode(node, inspector);
|
||||
|
||||
info("Check that all events were received, and in the right order");
|
||||
is(receivedEvents.length, 3, "3 events were received");
|
||||
is(receivedEvents[0], controller.PLAYERS_UPDATED_EVENT,
|
||||
"The first event received was the players-updated event");
|
||||
is(receivedEvents[1], panel.UI_UPDATED_EVENT,
|
||||
"The second event received was the ui-updated event");
|
||||
is(receivedEvents[2], "inspector-updated",
|
||||
"The third event received was the inspector-updated event");
|
||||
});
|
@ -0,0 +1,32 @@
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Check that the play/pause button actually plays and pauses the player.
|
||||
|
||||
add_task(function*() {
|
||||
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
|
||||
let {inspector, panel, controller} = yield openAnimationInspector();
|
||||
|
||||
info("Selecting an animated node");
|
||||
yield selectNode(".animated", inspector);
|
||||
|
||||
let player = controller.animationPlayers[0];
|
||||
let widget = panel.playerWidgets[0];
|
||||
|
||||
info("Click the pause button");
|
||||
yield togglePlayPauseButton(widget);
|
||||
|
||||
is(player.state.playState, "paused", "The AnimationPlayerFront is paused");
|
||||
ok(widget.el.classList.contains("paused"), "The button's state has changed");
|
||||
ok(!widget.rafID, "The smooth timeline animation has been stopped");
|
||||
|
||||
info("Click on the play button");
|
||||
yield togglePlayPauseButton(widget);
|
||||
|
||||
is(player.state.playState, "running", "The AnimationPlayerFront is running");
|
||||
ok(widget.el.classList.contains("running"), "The button's state has changed");
|
||||
ok(widget.rafID, "The smooth timeline animation has been started");
|
||||
});
|
@ -0,0 +1,58 @@
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Check that the AnimationPlayerFront objects lifecycle is managed by the
|
||||
// AnimationController.
|
||||
|
||||
add_task(function*() {
|
||||
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
|
||||
let {controller, inspector} = yield openAnimationInspector();
|
||||
|
||||
is(controller.animationPlayers.length, 0,
|
||||
"There are no AnimationPlayerFront objects at first");
|
||||
|
||||
info("Selecting an animated node");
|
||||
// selectNode waits for the inspector-updated event before resolving, which
|
||||
// means the controller.PLAYERS_UPDATED_EVENT event has been emitted before
|
||||
// and players are ready.
|
||||
yield selectNode(".animated", inspector);
|
||||
|
||||
is(controller.animationPlayers.length, 1,
|
||||
"One AnimationPlayerFront has been created");
|
||||
ok(controller.animationPlayers[0].autoRefreshTimer,
|
||||
"The AnimationPlayerFront has been set to auto-refresh");
|
||||
|
||||
info("Selecting a node with mutliple animations");
|
||||
yield selectNode(".multi", inspector);
|
||||
|
||||
is(controller.animationPlayers.length, 2,
|
||||
"2 AnimationPlayerFronts have been created");
|
||||
ok(controller.animationPlayers[0].autoRefreshTimer &&
|
||||
controller.animationPlayers[1].autoRefreshTimer,
|
||||
"The AnimationPlayerFronts have been set to auto-refresh");
|
||||
|
||||
// Hold on to one of the AnimationPlayerFront objects and mock its release
|
||||
// method to test that it is released correctly and that its auto-refresh is
|
||||
// stopped.
|
||||
let retainedFront = controller.animationPlayers[0];
|
||||
let oldRelease = retainedFront.release;
|
||||
let releaseCalled = false;
|
||||
retainedFront.release = () => {
|
||||
releaseCalled = true;
|
||||
};
|
||||
|
||||
info("Selecting a node with no animations");
|
||||
yield selectNode(".still", inspector);
|
||||
|
||||
is(controller.animationPlayers.length, 0,
|
||||
"There are no more AnimationPlayerFront objects");
|
||||
|
||||
info("Checking the destroyed AnimationPlayerFront object");
|
||||
ok(releaseCalled, "The AnimationPlayerFront has been released");
|
||||
ok(!retainedFront.autoRefreshTimer,
|
||||
"The released AnimationPlayerFront's auto-refresh mode has been turned off");
|
||||
yield oldRelease.call(retainedFront);
|
||||
});
|
@ -0,0 +1,23 @@
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that player widgets are destroyed correctly when needed.
|
||||
|
||||
add_task(function*() {
|
||||
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
|
||||
let {inspector, panel} = yield openAnimationInspector();
|
||||
|
||||
info("Select an animated node");
|
||||
yield selectNode(".multi", inspector);
|
||||
|
||||
info("Hold on to one of the player widget instances to test it after destroy");
|
||||
let widget = panel.playerWidgets[0];
|
||||
|
||||
info("Select another node to get the previous widgets destroyed");
|
||||
yield selectNode(".animated", inspector);
|
||||
|
||||
ok(widget.destroyed, "The widget's destroyed flag is true");
|
||||
});
|
@ -0,0 +1,47 @@
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that the panel only refreshes when it is visible in the sidebar.
|
||||
|
||||
add_task(function*() {
|
||||
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
|
||||
let {toolbox, inspector, panel} = yield openAnimationInspector();
|
||||
|
||||
info("Select a non animated node");
|
||||
yield selectNode(".still", inspector);
|
||||
|
||||
info("Switch to the rule-view panel");
|
||||
inspector.sidebar.select("ruleview");
|
||||
|
||||
info("Select the animated node now");
|
||||
yield selectNode(".animated", inspector);
|
||||
|
||||
ok(!panel.playerWidgets || !panel.playerWidgets.length,
|
||||
"The panel doesn't show the animation data while inactive");
|
||||
|
||||
info("Switch to the animation panel");
|
||||
inspector.sidebar.select("animationinspector");
|
||||
yield panel.once(panel.UI_UPDATED_EVENT);
|
||||
|
||||
is(panel.playerWidgets.length, 1,
|
||||
"The panel shows the animation data after selecting it");
|
||||
|
||||
info("Switch again to the rule-view");
|
||||
inspector.sidebar.select("ruleview");
|
||||
|
||||
info("Select the non animated node again");
|
||||
yield selectNode(".still", inspector);
|
||||
|
||||
is(panel.playerWidgets.length, 1,
|
||||
"The panel still shows the previous animation data since it is inactive");
|
||||
|
||||
info("Switch to the animation panel again");
|
||||
inspector.sidebar.select("animationinspector");
|
||||
yield panel.once(panel.UI_UPDATED_EVENT);
|
||||
|
||||
ok(!panel.playerWidgets || !panel.playerWidgets.length,
|
||||
"The panel is now empty after refreshing");
|
||||
});
|
@ -0,0 +1,25 @@
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Check that when playerFronts are updated, the same number of playerWidgets
|
||||
// are created in the panel.
|
||||
|
||||
add_task(function*() {
|
||||
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
|
||||
let {inspector, panel, controller} = yield openAnimationInspector();
|
||||
|
||||
info("Selecting the test animated node");
|
||||
yield selectNode(".multi", inspector);
|
||||
|
||||
is(controller.animationPlayers.length, panel.playerWidgets.length,
|
||||
"As many playerWidgets were created as there are playerFronts");
|
||||
|
||||
for (let widget of panel.playerWidgets) {
|
||||
ok(widget.initialized, "The player widget is initialized");
|
||||
is(widget.el.parentNode, panel.playersEl,
|
||||
"The player widget has been appended to the panel");
|
||||
}
|
||||
});
|
@ -0,0 +1,20 @@
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that the panel shows an animation player when an animated node is
|
||||
// selected.
|
||||
|
||||
add_task(function*() {
|
||||
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
|
||||
let {inspector, panel} = yield openAnimationInspector();
|
||||
|
||||
info("Select node .animated and check that the panel is not empty");
|
||||
let node = yield getNodeFront(".animated", inspector);
|
||||
yield selectNode(node, inspector);
|
||||
|
||||
is(panel.playerWidgets.length, 1,
|
||||
"Exactly 1 player widget is shown for animated node");
|
||||
});
|
@ -0,0 +1,28 @@
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that the currentTime timeline widget actually progresses with the
|
||||
// animation itself.
|
||||
|
||||
add_task(function*() {
|
||||
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
|
||||
let {inspector, panel} = yield openAnimationInspector();
|
||||
|
||||
info("Select the animated node");
|
||||
yield selectNode(".animated", inspector);
|
||||
|
||||
info("Get the player widget's timeline element and its current position");
|
||||
let widget = panel.playerWidgets[0];
|
||||
let timeline = widget.currentTimeEl;
|
||||
|
||||
yield widget.player.once(widget.player.AUTO_REFRESH_EVENT);
|
||||
ok(widget.rafID, "The widget is updating the timeline with a rAF loop");
|
||||
|
||||
info("Pause the animation");
|
||||
yield togglePlayPauseButton(widget);
|
||||
|
||||
ok(!widget.rafID, "The rAF loop has been stopped after the animation was paused");
|
||||
});
|
@ -0,0 +1,49 @@
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Verify that if the animation object changes in content, then the widget
|
||||
// reflects that change.
|
||||
|
||||
add_task(function*() {
|
||||
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
|
||||
let {panel, inspector} = yield openAnimationInspector();
|
||||
|
||||
info("Select the test node");
|
||||
yield selectNode(".animated", inspector);
|
||||
|
||||
info("Get the player widget");
|
||||
let widget = panel.playerWidgets[0];
|
||||
|
||||
info("Pause the animation via the content DOM");
|
||||
yield executeInContent("Test:ToggleAnimationPlayer", {
|
||||
animationIndex: 0,
|
||||
pause: true
|
||||
}, {
|
||||
node: getNode(".animated")
|
||||
});
|
||||
|
||||
info("Wait for the next state update");
|
||||
yield widget.player.once(widget.player.AUTO_REFRESH_EVENT);
|
||||
|
||||
is(widget.player.state.playState, "paused", "The AnimationPlayerFront is paused");
|
||||
ok(widget.el.classList.contains("paused"), "The button's state has changed");
|
||||
ok(!widget.rafID, "The smooth timeline animation has been stopped");
|
||||
|
||||
info("Play the animation via the content DOM");
|
||||
yield executeInContent("Test:ToggleAnimationPlayer", {
|
||||
animationIndex: 0,
|
||||
pause: false
|
||||
}, {
|
||||
node: getNode(".animated")
|
||||
});
|
||||
|
||||
info("Wait for the next state update");
|
||||
yield widget.player.once(widget.player.AUTO_REFRESH_EVENT);
|
||||
|
||||
is(widget.player.state.playState, "running", "The AnimationPlayerFront is running");
|
||||
ok(widget.el.classList.contains("running"), "The button's state has changed");
|
||||
ok(widget.rafID, "The smooth timeline animation has been started");
|
||||
});
|
28
browser/devtools/animationinspector/test/doc_frame_script.js
Normal file
28
browser/devtools/animationinspector/test/doc_frame_script.js
Normal file
@ -0,0 +1,28 @@
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// A helper frame-script for brower/devtools/animationinspector tests.
|
||||
|
||||
/**
|
||||
* Toggle (play or pause) one of the animation players of a given node.
|
||||
* @param {Object} data
|
||||
* - {Number} animationIndex The index of the node's animationPlayers to play or pause
|
||||
* @param {Object} objects
|
||||
* - {DOMNode} node The node to use
|
||||
*/
|
||||
addMessageListener("Test:ToggleAnimationPlayer", function(msg) {
|
||||
let {animationIndex, pause} = msg.data;
|
||||
let {node} = msg.objects;
|
||||
|
||||
let player = node.getAnimationPlayers()[animationIndex];
|
||||
if (pause) {
|
||||
player.pause();
|
||||
} else {
|
||||
player.play();
|
||||
}
|
||||
|
||||
sendAsyncMessage("Test:ToggleAnimationPlayer");
|
||||
});
|
@ -0,0 +1,54 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
.ball {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
background: #f06;
|
||||
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.still {
|
||||
top: 50px;
|
||||
left: 50px;
|
||||
}
|
||||
|
||||
.animated {
|
||||
top: 200px;
|
||||
left: 200px;
|
||||
|
||||
animation: simple-animation 2s infinite alternate;
|
||||
}
|
||||
|
||||
.multi {
|
||||
top: 100px;
|
||||
left: 400px;
|
||||
|
||||
animation: simple-animation 2s infinite alternate,
|
||||
other-animation 5s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes simple-animation {
|
||||
100% {
|
||||
transform: translateX(300px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes other-animation {
|
||||
100% {
|
||||
background: blue;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Comment node -->
|
||||
<div class="ball still"></div>
|
||||
<div class="ball animated"></div>
|
||||
<div class="ball multi"></div>
|
||||
</body>
|
||||
</html>
|
274
browser/devtools/animationinspector/test/head.js
Normal file
274
browser/devtools/animationinspector/test/head.js
Normal file
@ -0,0 +1,274 @@
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
const Cu = Components.utils;
|
||||
let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
|
||||
let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
|
||||
let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
|
||||
let TargetFactory = devtools.TargetFactory;
|
||||
let {console} = Components.utils.import("resource://gre/modules/devtools/Console.jsm", {});
|
||||
|
||||
// All tests are asynchronous
|
||||
waitForExplicitFinish();
|
||||
|
||||
const TEST_URL_ROOT = "http://example.com/browser/browser/devtools/animationinspector/test/";
|
||||
const ROOT_TEST_DIR = getRootDirectory(gTestPath);
|
||||
const FRAME_SCRIPT_URL = ROOT_TEST_DIR + "doc_frame_script.js";
|
||||
|
||||
// Auto clean-up when a test ends
|
||||
registerCleanupFunction(function*() {
|
||||
let target = TargetFactory.forTab(gBrowser.selectedTab);
|
||||
yield gDevTools.closeToolbox(target);
|
||||
|
||||
while (gBrowser.tabs.length > 1) {
|
||||
gBrowser.removeCurrentTab();
|
||||
}
|
||||
});
|
||||
|
||||
// Uncomment this pref to dump all devtools emitted events to the console.
|
||||
// Services.prefs.setBoolPref("devtools.dump.emit", true);
|
||||
|
||||
// Uncomment this pref to dump all devtools protocol traffic
|
||||
// Services.prefs.setBoolPref("devtools.debugger.log", true);
|
||||
|
||||
// Set the testing flag on gDevTools and reset it when the test ends
|
||||
gDevTools.testing = true;
|
||||
registerCleanupFunction(() => gDevTools.testing = false);
|
||||
|
||||
// Clean-up all prefs that might have been changed during a test run
|
||||
// (safer here because if the test fails, then the pref is never reverted)
|
||||
registerCleanupFunction(() => {
|
||||
Services.prefs.clearUserPref("devtools.dump.emit");
|
||||
Services.prefs.clearUserPref("devtools.debugger.log");
|
||||
});
|
||||
|
||||
/**
|
||||
* Add a new test tab in the browser and load the given url.
|
||||
* @param {String} url The url to be loaded in the new tab
|
||||
* @return a promise that resolves to the tab object when the url is loaded
|
||||
*/
|
||||
function addTab(url) {
|
||||
info("Adding a new tab with URL: '" + url + "'");
|
||||
let def = promise.defer();
|
||||
|
||||
window.focus();
|
||||
|
||||
let tab = window.gBrowser.selectedTab = window.gBrowser.addTab(url);
|
||||
let browser = tab.linkedBrowser;
|
||||
|
||||
info("Loading the helper frame script " + FRAME_SCRIPT_URL);
|
||||
browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
|
||||
|
||||
browser.addEventListener("load", function onload() {
|
||||
browser.removeEventListener("load", onload, true);
|
||||
info("URL '" + url + "' loading complete");
|
||||
|
||||
def.resolve(tab);
|
||||
}, true);
|
||||
|
||||
return def.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple DOM node accesor function that takes either a node or a string css
|
||||
* selector as argument and returns the corresponding node
|
||||
* @param {String|DOMNode} nodeOrSelector
|
||||
* @return {DOMNode|CPOW} Note that in e10s mode a CPOW object is returned which
|
||||
* doesn't implement *all* of the DOMNode's properties
|
||||
*/
|
||||
function getNode(nodeOrSelector) {
|
||||
info("Getting the node for '" + nodeOrSelector + "'");
|
||||
return typeof nodeOrSelector === "string" ?
|
||||
content.document.querySelector(nodeOrSelector) :
|
||||
nodeOrSelector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the NodeFront for a given css selector, via the protocol
|
||||
* @param {String} selector
|
||||
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
|
||||
* loaded in the toolbox
|
||||
* @return {Promise} Resolves to the NodeFront instance
|
||||
*/
|
||||
function getNodeFront(selector, {walker}) {
|
||||
return walker.querySelector(walker.rootNode, selector);
|
||||
}
|
||||
|
||||
/*
|
||||
* Set the inspector's current selection to a node or to the first match of the
|
||||
* given css selector.
|
||||
* @param {String|NodeFront}
|
||||
* data The node to select
|
||||
* @param {InspectorPanel} inspector
|
||||
* The instance of InspectorPanel currently
|
||||
* loaded in the toolbox
|
||||
* @param {String} reason
|
||||
* Defaults to "test" which instructs the inspector not
|
||||
* to highlight the node upon selection
|
||||
* @return {Promise} Resolves when the inspector is updated with the new node
|
||||
*/
|
||||
let selectNode = Task.async(function*(data, inspector, reason="test") {
|
||||
info("Selecting the node for '" + data + "'");
|
||||
let nodeFront = data;
|
||||
if (!data._form) {
|
||||
nodeFront = yield getNodeFront(data, inspector);
|
||||
}
|
||||
let updated = inspector.once("inspector-updated");
|
||||
inspector.selection.setNodeFront(nodeFront, reason);
|
||||
yield updated;
|
||||
});
|
||||
|
||||
/**
|
||||
* Open the toolbox, with the inspector tool visible and the animationinspector
|
||||
* sidebar selected.
|
||||
* @return a promise that resolves when the inspector is ready
|
||||
*/
|
||||
let openAnimationInspector = Task.async(function*() {
|
||||
let target = TargetFactory.forTab(gBrowser.selectedTab);
|
||||
|
||||
info("Opening the toolbox with the inspector selected");
|
||||
let toolbox = yield gDevTools.showToolbox(target, "inspector");
|
||||
yield waitForToolboxFrameFocus(toolbox);
|
||||
|
||||
info("Switching to the animationinspector");
|
||||
let inspector = toolbox.getPanel("inspector");
|
||||
let initPromises = [
|
||||
inspector.once("inspector-updated"),
|
||||
inspector.sidebar.once("animationinspector-ready")
|
||||
];
|
||||
inspector.sidebar.select("animationinspector");
|
||||
|
||||
info("Waiting for the inspector and sidebar to be ready");
|
||||
yield promise.all(initPromises);
|
||||
|
||||
let win = inspector.sidebar.getWindowForTab("animationinspector");
|
||||
let {AnimationsController, AnimationsPanel} = win;
|
||||
|
||||
yield promise.all([
|
||||
AnimationsController.initialized,
|
||||
AnimationsPanel.initialized
|
||||
]);
|
||||
|
||||
return {
|
||||
toolbox: toolbox,
|
||||
inspector: inspector,
|
||||
controller: AnimationsController,
|
||||
panel: AnimationsPanel,
|
||||
window: win
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Wait for the toolbox frame to receive focus after it loads
|
||||
* @param {Toolbox} toolbox
|
||||
* @return a promise that resolves when focus has been received
|
||||
*/
|
||||
function waitForToolboxFrameFocus(toolbox) {
|
||||
info("Making sure that the toolbox's frame is focused");
|
||||
let def = promise.defer();
|
||||
let win = toolbox.frame.contentWindow;
|
||||
waitForFocus(def.resolve, win);
|
||||
return def.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the inspector's sidebar corresponding to the given id already
|
||||
* exists
|
||||
* @param {InspectorPanel}
|
||||
* @param {String}
|
||||
* @return {Boolean}
|
||||
*/
|
||||
function hasSideBarTab(inspector, id) {
|
||||
return !!inspector.sidebar.getWindowForTab(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for eventName on target.
|
||||
* @param {Object} target An observable object that either supports on/off or
|
||||
* addEventListener/removeEventListener
|
||||
* @param {String} eventName
|
||||
* @param {Boolean} useCapture Optional, for addEventListener/removeEventListener
|
||||
* @return A promise that resolves when the event has been handled
|
||||
*/
|
||||
function once(target, eventName, useCapture=false) {
|
||||
info("Waiting for event: '" + eventName + "' on " + target + ".");
|
||||
|
||||
let deferred = promise.defer();
|
||||
|
||||
for (let [add, remove] of [
|
||||
["addEventListener", "removeEventListener"],
|
||||
["addListener", "removeListener"],
|
||||
["on", "off"]
|
||||
]) {
|
||||
if ((add in target) && (remove in target)) {
|
||||
target[add](eventName, function onEvent(...aArgs) {
|
||||
target[remove](eventName, onEvent, useCapture);
|
||||
deferred.resolve.apply(deferred, aArgs);
|
||||
}, useCapture);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a content -> chrome message on the message manager (the window
|
||||
* messagemanager is used).
|
||||
* @param {String} name The message name
|
||||
* @return {Promise} A promise that resolves to the response data when the
|
||||
* message has been received
|
||||
*/
|
||||
function waitForContentMessage(name) {
|
||||
info("Expecting message " + name + " from content");
|
||||
|
||||
let mm = gBrowser.selectedBrowser.messageManager;
|
||||
|
||||
let def = promise.defer();
|
||||
mm.addMessageListener(name, function onMessage(msg) {
|
||||
mm.removeMessageListener(name, onMessage);
|
||||
def.resolve(msg.data);
|
||||
});
|
||||
return def.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an async message to the frame script (chrome -> content) and wait for a
|
||||
* response message with the same name (content -> chrome).
|
||||
* @param {String} name The message name. Should be one of the messages defined
|
||||
* in doc_frame_script.js
|
||||
* @param {Object} data Optional data to send along
|
||||
* @param {Object} objects Optional CPOW objects to send along
|
||||
* @param {Boolean} expectResponse If set to false, don't wait for a response
|
||||
* with the same name from the content script. Defaults to true.
|
||||
* @return {Promise} Resolves to the response data if a response is expected,
|
||||
* immediately resolves otherwise
|
||||
*/
|
||||
function executeInContent(name, data={}, objects={}, expectResponse=true) {
|
||||
info("Sending message " + name + " to content");
|
||||
let mm = gBrowser.selectedBrowser.messageManager;
|
||||
|
||||
mm.sendAsyncMessage(name, data, objects);
|
||||
if (expectResponse) {
|
||||
return waitForContentMessage(name);
|
||||
} else {
|
||||
return promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate a click on the playPause button of a playerWidget.
|
||||
*/
|
||||
let togglePlayPauseButton = Task.async(function*(widget) {
|
||||
// Note that instead of simulating a real event here, the callback is just
|
||||
// called. This is better because the callback returns a promise, so we know
|
||||
// when the player is paused, and we don't really care to test that simulating
|
||||
// a DOM event actually works.
|
||||
yield widget.onPlayPauseBtnClick();
|
||||
|
||||
// Wait for the next sate change event to make sure the state is updated
|
||||
yield widget.player.once(widget.player.AUTO_REFRESH_EVENT);
|
||||
});
|
@ -23,6 +23,18 @@ devtoolsCommandlineHandler.prototype = {
|
||||
if (debuggerFlag) {
|
||||
this.handleDebuggerFlag(cmdLine);
|
||||
}
|
||||
let debuggerServerFlag;
|
||||
try {
|
||||
debuggerServerFlag =
|
||||
cmdLine.handleFlagWithParam("start-debugger-server", false);
|
||||
} catch(e) {
|
||||
// We get an error if the option is given but not followed by a value.
|
||||
// By catching and trying again, the value is effectively optional.
|
||||
debuggerServerFlag = cmdLine.handleFlag("start-debugger-server", false);
|
||||
}
|
||||
if (debuggerServerFlag) {
|
||||
this.handleDebuggerServerFlag(cmdLine, debuggerServerFlag);
|
||||
}
|
||||
},
|
||||
|
||||
handleConsoleFlag: function(cmdLine) {
|
||||
@ -43,24 +55,69 @@ devtoolsCommandlineHandler.prototype = {
|
||||
}
|
||||
},
|
||||
|
||||
handleDebuggerFlag: function(cmdLine) {
|
||||
_isRemoteDebuggingEnabled() {
|
||||
let remoteDebuggingEnabled = false;
|
||||
try {
|
||||
remoteDebuggingEnabled = kDebuggerPrefs.every((pref) => Services.prefs.getBoolPref(pref));
|
||||
} catch (ex) {
|
||||
Cu.reportError(ex);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if (remoteDebuggingEnabled) {
|
||||
Cu.import("resource:///modules/devtools/ToolboxProcess.jsm");
|
||||
BrowserToolboxProcess.init();
|
||||
} else {
|
||||
if (!remoteDebuggingEnabled) {
|
||||
let errorMsg = "Could not run chrome debugger! You need the following prefs " +
|
||||
"to be set to true: " + kDebuggerPrefs.join(", ");
|
||||
Cu.reportError(errorMsg);
|
||||
// Dump as well, as we're doing this from a commandline, make sure people don't miss it:
|
||||
// Dump as well, as we're doing this from a commandline, make sure people
|
||||
// don't miss it:
|
||||
dump(errorMsg + "\n");
|
||||
}
|
||||
return remoteDebuggingEnabled;
|
||||
},
|
||||
|
||||
handleDebuggerFlag: function(cmdLine) {
|
||||
if (!this._isRemoteDebuggingEnabled()) {
|
||||
return;
|
||||
}
|
||||
Cu.import("resource:///modules/devtools/ToolboxProcess.jsm");
|
||||
BrowserToolboxProcess.init();
|
||||
|
||||
if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) {
|
||||
cmdLine.preventDefault = true;
|
||||
}
|
||||
},
|
||||
|
||||
handleDebuggerServerFlag: function(cmdLine, portOrPath) {
|
||||
if (!this._isRemoteDebuggingEnabled()) {
|
||||
return;
|
||||
}
|
||||
if (portOrPath === true) {
|
||||
// Default to TCP port 6000 if no value given
|
||||
portOrPath = 6000;
|
||||
}
|
||||
let { DevToolsLoader } =
|
||||
Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
|
||||
|
||||
try {
|
||||
// Create a separate loader instance, so that we can be sure to receive
|
||||
// a separate instance of the DebuggingServer from the rest of the
|
||||
// devtools. This allows us to safely use the tools against even the
|
||||
// actors and DebuggingServer itself, especially since we can mark
|
||||
// serverLoader as invisible to the debugger (unlike the usual loader
|
||||
// settings).
|
||||
let serverLoader = new DevToolsLoader();
|
||||
serverLoader.invisibleToDebugger = true;
|
||||
serverLoader.main("devtools/server/main");
|
||||
let debuggerServer = serverLoader.DebuggerServer;
|
||||
debuggerServer.init();
|
||||
debuggerServer.addBrowserActors();
|
||||
|
||||
let listener = debuggerServer.createListener();
|
||||
listener.portOrPath = portOrPath;
|
||||
listener.open();
|
||||
dump("Started debugger server on " + portOrPath + "\n");
|
||||
} catch(e) {
|
||||
dump("Unable to start debugger server on " + portOrPath + ": " + e);
|
||||
}
|
||||
|
||||
if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) {
|
||||
cmdLine.preventDefault = true;
|
||||
@ -68,7 +125,10 @@ devtoolsCommandlineHandler.prototype = {
|
||||
},
|
||||
|
||||
helpInfo : " --jsconsole Open the Browser Console.\n" +
|
||||
" --jsdebugger Open the Browser Toolbox.\n",
|
||||
" --jsdebugger Open the Browser Toolbox.\n" +
|
||||
" --start-debugger-server [port|path] " +
|
||||
"Start the debugger server on a TCP port or " +
|
||||
"Unix domain socket path. Defaults to TCP port 6000.\n",
|
||||
|
||||
classID: Components.ID("{9e9a9283-0ce9-4e4a-8f1c-ba129a032c32}"),
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsICommandLineHandler]),
|
||||
|
@ -257,7 +257,10 @@ ToolSidebar.prototype = {
|
||||
|
||||
this._tabbox.tabpanels.removeEventListener("select", this, true);
|
||||
|
||||
while (this._tabbox.tabpanels.hasChildNodes()) {
|
||||
// Note that we check for the existence of this._tabbox.tabpanels at each
|
||||
// step as the container window may have been closed by the time one of the
|
||||
// panel's destroy promise resolves.
|
||||
while (this._tabbox.tabpanels && this._tabbox.tabpanels.hasChildNodes()) {
|
||||
let panel = this._tabbox.tabpanels.firstChild;
|
||||
let win = panel.firstChild.contentWindow;
|
||||
if ("destroy" in win) {
|
||||
@ -266,7 +269,7 @@ ToolSidebar.prototype = {
|
||||
panel.remove();
|
||||
}
|
||||
|
||||
while (this._tabbox.tabs.hasChildNodes()) {
|
||||
while (this._tabbox.tabs && this._tabbox.tabs.hasChildNodes()) {
|
||||
this._tabbox.tabs.removeChild(this._tabbox.tabs.firstChild);
|
||||
}
|
||||
|
||||
|
@ -341,6 +341,12 @@ InspectorPanel.prototype = {
|
||||
"chrome://browser/content/devtools/layoutview/view.xhtml",
|
||||
"layoutview" == defaultTab);
|
||||
|
||||
if (this.target.form.animationsActor) {
|
||||
this.sidebar.addTab("animationinspector",
|
||||
"chrome://browser/content/devtools/animationinspector/animation-inspector.xhtml",
|
||||
"animationinspector" == defaultTab);
|
||||
}
|
||||
|
||||
let ruleViewTab = this.sidebar.getTab("ruleview");
|
||||
|
||||
this.sidebar.show();
|
||||
|
@ -4,6 +4,8 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
const { Cu } = require("chrome");
|
||||
|
||||
const promise = require("resource://gre/modules/Promise.jsm").Promise;
|
||||
loader.lazyGetter(this, "EventEmitter", () => require("devtools/toolkit/event-emitter"));
|
||||
loader.lazyGetter(this, "AutocompletePopup", () => require("devtools/shared/autocomplete-popup").AutocompletePopup);
|
||||
@ -265,7 +267,7 @@ SelectorSearch.prototype = {
|
||||
}
|
||||
this.searchBox.classList.add("devtools-no-search-result");
|
||||
return this.showSuggestions();
|
||||
}).then(() => this.emit("processing-done"));
|
||||
}).then(() => this.emit("processing-done"), Cu.reportError);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -36,6 +36,9 @@ browser.jar:
|
||||
content/browser/devtools/fontinspector/font-inspector.js (fontinspector/font-inspector.js)
|
||||
content/browser/devtools/fontinspector/font-inspector.xhtml (fontinspector/font-inspector.xhtml)
|
||||
content/browser/devtools/fontinspector/font-inspector.css (fontinspector/font-inspector.css)
|
||||
content/browser/devtools/animationinspector/animation-controller.js (animationinspector/animation-controller.js)
|
||||
content/browser/devtools/animationinspector/animation-panel.js (animationinspector/animation-panel.js)
|
||||
content/browser/devtools/animationinspector/animation-inspector.xhtml (animationinspector/animation-inspector.xhtml)
|
||||
content/browser/devtools/codemirror/codemirror.js (sourceeditor/codemirror/codemirror.js)
|
||||
content/browser/devtools/codemirror/codemirror.css (sourceeditor/codemirror/codemirror.css)
|
||||
content/browser/devtools/codemirror/javascript.js (sourceeditor/codemirror/mode/javascript.js)
|
||||
@ -94,6 +97,7 @@ browser.jar:
|
||||
content/browser/devtools/performance/views/details.js (performance/views/details.js)
|
||||
content/browser/devtools/performance/views/details-call-tree.js (performance/views/details-call-tree.js)
|
||||
content/browser/devtools/performance/views/details-waterfall.js (performance/views/details-waterfall.js)
|
||||
content/browser/devtools/performance/views/details-flamegraph.js (performance/views/details-flamegraph.js)
|
||||
#endif
|
||||
content/browser/devtools/responsivedesign/resize-commands.js (responsivedesign/resize-commands.js)
|
||||
content/browser/devtools/commandline.css (commandline/commandline.css)
|
||||
|
@ -5,6 +5,7 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DIRS += [
|
||||
'animationinspector',
|
||||
'app-manager',
|
||||
'canvasdebugger',
|
||||
'commandline',
|
||||
|
@ -8,6 +8,7 @@
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
||||
|
||||
const NET_STRINGS_URI = "chrome://browser/locale/devtools/netmonitor.properties";
|
||||
const PKI_STRINGS_URI = "chrome://pippki/locale/pippki.properties";
|
||||
const LISTENERS = [ "NetworkActivity" ];
|
||||
const NET_PREFS = { "NetworkMonitor.saveRequestAndResponseBodies": true };
|
||||
|
||||
@ -34,6 +35,10 @@ const EVENTS = {
|
||||
UPDATING_REQUEST_POST_DATA: "NetMonitor:NetworkEventUpdating:RequestPostData",
|
||||
RECEIVED_REQUEST_POST_DATA: "NetMonitor:NetworkEventUpdated:RequestPostData",
|
||||
|
||||
// When security information begins and finishes receiving.
|
||||
UPDATING_SECURITY_INFO: "NetMonitor::NetworkEventUpdating:SecurityInfo",
|
||||
RECEIVED_SECURITY_INFO: "NetMonitor::NetworkEventUpdated:SecurityInfo",
|
||||
|
||||
// When response headers begin and finish receiving.
|
||||
UPDATING_RESPONSE_HEADERS: "NetMonitor:NetworkEventUpdating:ResponseHeaders",
|
||||
RECEIVED_RESPONSE_HEADERS: "NetMonitor:NetworkEventUpdated:ResponseHeaders",
|
||||
@ -136,6 +141,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "DevToolsUtils",
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
|
||||
"@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "DOMParser",
|
||||
"@mozilla.org/xmlextras/domparser;1", "nsIDOMParser");
|
||||
|
||||
Object.defineProperty(this, "NetworkHelper", {
|
||||
get: function() {
|
||||
return require("devtools/toolkit/webconsole/network-helper");
|
||||
@ -570,6 +578,13 @@ NetworkEventsHandler.prototype = {
|
||||
this.webConsoleClient.getRequestPostData(actor, this._onRequestPostData);
|
||||
window.emit(EVENTS.UPDATING_REQUEST_POST_DATA, actor);
|
||||
break;
|
||||
case "securityInfo":
|
||||
NetMonitorView.RequestsMenu.updateRequest(aPacket.from, {
|
||||
securityState: aPacket.state,
|
||||
});
|
||||
this.webConsoleClient.getSecurityInfo(actor, this._onSecurityInfo);
|
||||
window.emit(EVENTS.UPDATING_SECURITY_INFO, actor);
|
||||
break;
|
||||
case "responseHeaders":
|
||||
this.webConsoleClient.getResponseHeaders(actor, this._onResponseHeaders);
|
||||
window.emit(EVENTS.UPDATING_RESPONSE_HEADERS, actor);
|
||||
@ -644,6 +659,20 @@ NetworkEventsHandler.prototype = {
|
||||
window.emit(EVENTS.RECEIVED_REQUEST_POST_DATA, aResponse.from);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles additional information received for a "securityInfo" packet.
|
||||
*
|
||||
* @param object aResponse
|
||||
* The message received from the server.
|
||||
*/
|
||||
_onSecurityInfo: function(aResponse) {
|
||||
NetMonitorView.RequestsMenu.updateRequest(aResponse.from, {
|
||||
securityInfo: aResponse.securityInfo
|
||||
});
|
||||
|
||||
window.emit(EVENTS.RECEIVED_SECURITY_INFO, aResponse.from);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles additional information received for a "responseHeaders" packet.
|
||||
*
|
||||
@ -738,6 +767,7 @@ NetworkEventsHandler.prototype = {
|
||||
* Localization convenience methods.
|
||||
*/
|
||||
let L10N = new ViewHelpers.L10N(NET_STRINGS_URI);
|
||||
let PKI_L10N = new ViewHelpers.L10N(PKI_STRINGS_URI);
|
||||
|
||||
/**
|
||||
* Shortcuts for accessing various network monitor preferences.
|
||||
|
@ -336,6 +336,7 @@ function RequestsMenuView() {
|
||||
this._byFile = this._byFile.bind(this);
|
||||
this._byDomain = this._byDomain.bind(this);
|
||||
this._byType = this._byType.bind(this);
|
||||
this._onSecurityIconClick = this._onSecurityIconClick.bind(this);
|
||||
}
|
||||
|
||||
RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
|
||||
@ -1055,6 +1056,17 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
|
||||
tooltip.defaultPosition = REQUESTS_TOOLTIP_POSITION;
|
||||
},
|
||||
|
||||
/**
|
||||
* Attaches security icon click listener for the given request menu item.
|
||||
*
|
||||
* @param object item
|
||||
* The network request item to attach the listener to.
|
||||
*/
|
||||
attachSecurityIconClickListener: function ({ target }) {
|
||||
let icon = $(".requests-security-state-icon", target);
|
||||
icon.addEventListener("click", this._onSecurityIconClick);
|
||||
},
|
||||
|
||||
/**
|
||||
* Schedules adding additional information to a network request.
|
||||
*
|
||||
@ -1133,6 +1145,13 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
|
||||
requestItem.attachment.requestPostData = value;
|
||||
requestItem.attachment.requestHeadersFromUploadStream = currentStore;
|
||||
break;
|
||||
case "securityState":
|
||||
requestItem.attachment.securityState = value;
|
||||
this.updateMenuView(requestItem, key, value);
|
||||
break;
|
||||
case "securityInfo":
|
||||
requestItem.attachment.securityInfo = value;
|
||||
break;
|
||||
case "responseHeaders":
|
||||
requestItem.attachment.responseHeaders = value;
|
||||
break;
|
||||
@ -1286,6 +1305,15 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
|
||||
domain.setAttribute("tooltiptext", hostPort);
|
||||
break;
|
||||
}
|
||||
case "securityState": {
|
||||
let tooltip = L10N.getStr("netmonitor.security.state." + aValue);
|
||||
let icon = $(".requests-security-state-icon", target);
|
||||
icon.classList.add("security-state-" + aValue);
|
||||
icon.setAttribute("tooltiptext", tooltip);
|
||||
|
||||
this.attachSecurityIconClickListener(aItem);
|
||||
break;
|
||||
}
|
||||
case "status": {
|
||||
let node = $(".requests-menu-status", target);
|
||||
let codeNode = $(".requests-menu-status-code", target);
|
||||
@ -1575,6 +1603,11 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
|
||||
// in this container, so it's necessary to refresh the Tooltip instances.
|
||||
this.refreshTooltip(firstItem);
|
||||
this.refreshTooltip(secondItem);
|
||||
|
||||
// Reattach click listener to the security icons
|
||||
this.attachSecurityIconClickListener(firstItem);
|
||||
this.attachSecurityIconClickListener(secondItem);
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
@ -1609,6 +1642,18 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* A handler that opens the security tab in the details view if secure or
|
||||
* broken security indicator is clicked.
|
||||
*/
|
||||
_onSecurityIconClick: function(e) {
|
||||
let state = this.selectedItem.attachment.securityState;
|
||||
if (state === "broken" || state === "secure") {
|
||||
// Choose the security tab.
|
||||
NetMonitorView.NetworkDetails.widget.selectedIndex = 5;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* The resize listener for this container's window.
|
||||
*/
|
||||
@ -2048,9 +2093,20 @@ NetworkDetailsView.prototype = {
|
||||
$("#preview-tab").hidden = !isHtml;
|
||||
$("#preview-tabpanel").hidden = !isHtml;
|
||||
|
||||
// Show the "Security" tab only for requests that
|
||||
// 1) are https (state != insecure)
|
||||
// 2) come from a target that provides security information.
|
||||
let hasSecurityInfo = aData.securityState &&
|
||||
aData.securityState !== "insecure";
|
||||
|
||||
$("#security-tab").hidden = !hasSecurityInfo;
|
||||
|
||||
// Switch to the "Headers" tabpanel if the "Preview" previously selected
|
||||
// and this is not an HTML response.
|
||||
if (!isHtml && this.widget.selectedIndex == 5) {
|
||||
// and this is not an HTML response or "Security" was selected but this
|
||||
// request has no security information.
|
||||
|
||||
if (!isHtml && this.widget.selectedPanel === $("#preview-tabpanel") ||
|
||||
!hasSecurityInfo && this.widget.selectedPanel === $("#security-tabpanel")) {
|
||||
this.widget.selectedIndex = 0;
|
||||
}
|
||||
|
||||
@ -2117,7 +2173,10 @@ NetworkDetailsView.prototype = {
|
||||
case 4: // "Timings"
|
||||
yield view._setTimingsInformation(src.eventTimings);
|
||||
break;
|
||||
case 5: // "Preview"
|
||||
case 5: // "Security"
|
||||
yield view._setSecurityInfo(src.securityInfo, src.url);
|
||||
break;
|
||||
case 6: // "Preview"
|
||||
yield view._setHtmlPreview(src.responseContent);
|
||||
break;
|
||||
}
|
||||
@ -2622,6 +2681,98 @@ NetworkDetailsView.prototype = {
|
||||
window.emit(EVENTS.RESPONSE_HTML_PREVIEW_DISPLAYED);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Sets the security information shown in this view.
|
||||
*
|
||||
* @param object securityInfo
|
||||
* The data received from server
|
||||
* @param string url
|
||||
* The URL of this request
|
||||
* @return object
|
||||
* A promise that is resolved when the security info is rendered.
|
||||
*/
|
||||
_setSecurityInfo: Task.async(function* (securityInfo, url) {
|
||||
if (!securityInfo) {
|
||||
// We don't have security info. This could mean one of two things:
|
||||
// 1) This connection is not secure and this tab is not visible and thus
|
||||
// we shouldn't be here.
|
||||
// 2) We have already received securityState and the tab is visible BUT
|
||||
// the rest of the information is still on its way. Once it arrives
|
||||
// this method is called again.
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper that sets label text to specified value.
|
||||
*
|
||||
* @param string selector
|
||||
* A selector for the label.
|
||||
* @param string value
|
||||
* The value label should have. If this evaluates to false a
|
||||
* placeholder string <Not Available> is used instead.
|
||||
*/
|
||||
function setLabel(selector, value) {
|
||||
let label = $(selector);
|
||||
if (!value) {
|
||||
label.value = L10N.getStr("netmonitor.security.notAvailable");
|
||||
label.setAttribute("tooltiptext", label.value);
|
||||
} else {
|
||||
label.value = value;
|
||||
label.setAttribute("tooltiptext", value);
|
||||
}
|
||||
}
|
||||
|
||||
let errorbox = $("#security-error");
|
||||
let infobox = $("#security-information");
|
||||
|
||||
if (securityInfo.state === "secure") {
|
||||
infobox.hidden = false;
|
||||
errorbox.hidden = true;
|
||||
|
||||
let enabledLabel = L10N.getStr("netmonitor.security.enabled");
|
||||
let disabledLabel = L10N.getStr("netmonitor.security.disabled");
|
||||
|
||||
// Connection parameters
|
||||
setLabel("#security-protocol-version-value", securityInfo.protocolVersion);
|
||||
setLabel("#security-ciphersuite-value", securityInfo.cipherSuite);
|
||||
|
||||
// Host header
|
||||
let domain = NetMonitorView.RequestsMenu._getUriHostPort(url);
|
||||
let hostHeader = L10N.getFormatStr("netmonitor.security.hostHeader", domain);
|
||||
setLabel("#security-info-host-header", hostHeader);
|
||||
|
||||
// Parameters related to the domain
|
||||
setLabel("#security-http-strict-transport-security-value",
|
||||
securityInfo.hsts ? enabledLabel : disabledLabel);
|
||||
|
||||
setLabel("#security-public-key-pinning-value",
|
||||
securityInfo.hpkp ? enabledLabel : disabledLabel);
|
||||
|
||||
// Certificate parameters
|
||||
let cert = securityInfo.cert;
|
||||
setLabel("#security-cert-subject-cn", cert.subject.commonName);
|
||||
setLabel("#security-cert-subject-o", cert.subject.organization);
|
||||
setLabel("#security-cert-subject-ou", cert.subject.organizationalUnit);
|
||||
|
||||
setLabel("#security-cert-issuer-cn", cert.issuer.commonName);
|
||||
setLabel("#security-cert-issuer-o", cert.issuer.organization);
|
||||
setLabel("#security-cert-issuer-ou", cert.issuer.organizationalUnit);
|
||||
|
||||
setLabel("#security-cert-validity-begins", cert.validity.start);
|
||||
setLabel("#security-cert-validity-expires", cert.validity.end);
|
||||
|
||||
setLabel("#security-cert-sha1-fingerprint", cert.fingerprint.sha1);
|
||||
setLabel("#security-cert-sha256-fingerprint", cert.fingerprint.sha256);
|
||||
} else {
|
||||
infobox.hidden = true;
|
||||
errorbox.hidden = false;
|
||||
|
||||
// Strip any HTML from the message.
|
||||
let plain = DOMParser.parseFromString(securityInfo.errorMessage, "text/html");
|
||||
$("#security-error-message").textContent = plain.body.textContent;
|
||||
}
|
||||
}),
|
||||
|
||||
_dataSrc: null,
|
||||
_headers: null,
|
||||
_cookies: null,
|
||||
|
@ -11,6 +11,8 @@
|
||||
<!DOCTYPE window [
|
||||
<!ENTITY % netmonitorDTD SYSTEM "chrome://browser/locale/devtools/netmonitor.dtd">
|
||||
%netmonitorDTD;
|
||||
<!ENTITY % certManagerDTD SYSTEM "chrome://pippki/locale/certManager.dtd">
|
||||
%certManagerDTD;
|
||||
]>
|
||||
|
||||
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
@ -82,10 +84,10 @@
|
||||
</button>
|
||||
</hbox>
|
||||
<hbox id="requests-menu-domain-header-box"
|
||||
class="requests-menu-header requests-menu-domain"
|
||||
class="requests-menu-header requests-menu-security-and-domain"
|
||||
align="center">
|
||||
<button id="requests-menu-domain-button"
|
||||
class="requests-menu-header-button requests-menu-domain"
|
||||
class="requests-menu-header-button requests-menu-security-and-domain"
|
||||
data-key="domain"
|
||||
label="&netmonitorUI.toolbar.domain;"
|
||||
flex="1">
|
||||
@ -170,8 +172,13 @@
|
||||
crop="end"
|
||||
flex="1"/>
|
||||
</hbox>
|
||||
<label class="plain requests-menu-subitem requests-menu-domain"
|
||||
crop="end"/>
|
||||
<hbox class="requests-menu-subitem requests-menu-security-and-domain"
|
||||
align="center">
|
||||
<image class="requests-security-state-icon" />
|
||||
<label class="plain requests-menu-domain"
|
||||
crop="end"
|
||||
flex="1"/>
|
||||
</hbox>
|
||||
<label class="plain requests-menu-subitem requests-menu-type"
|
||||
crop="end"/>
|
||||
<label class="plain requests-menu-subitem requests-menu-size"
|
||||
@ -266,6 +273,8 @@
|
||||
label="&netmonitorUI.tab.response;"/>
|
||||
<tab id="timings-tab"
|
||||
label="&netmonitorUI.tab.timings;"/>
|
||||
<tab id="security-tab"
|
||||
label="&netmonitorUI.tab.security;"/>
|
||||
<tab id="preview-tab"
|
||||
label="&netmonitorUI.tab.preview;"/>
|
||||
</tabs>
|
||||
@ -460,6 +469,194 @@
|
||||
</hbox>
|
||||
</vbox>
|
||||
</tabpanel>
|
||||
<tabpanel id="security-tabpanel"
|
||||
class="tabpanel-content">
|
||||
<vbox id="security-error"
|
||||
class="tabpanel-summary-container"
|
||||
flex="1">
|
||||
<label class="plain tabpanel-summary-label"
|
||||
value="&netmonitorUI.security.error;"/>
|
||||
<description id="security-error-message" flex="1"/>
|
||||
</vbox>
|
||||
<vbox id="security-information"
|
||||
flex="1">
|
||||
<vbox id="security-info-connection"
|
||||
class="tabpanel-summary-container">
|
||||
<label class="plain tabpanel-summary-label"
|
||||
value="&netmonitorUI.security.connection;"/>
|
||||
<vbox class="security-info-section">
|
||||
<hbox id="security-protocol-version"
|
||||
class="tabpanel-summary-container"
|
||||
align="center">
|
||||
<label class="plain tabpanel-summary-label"
|
||||
value="&netmonitorUI.security.protocolVersion;"/>
|
||||
<label id="security-protocol-version-value"
|
||||
class="plain tabpanel-summary-value devtools-monospace"
|
||||
crop="end"
|
||||
flex="1"/>
|
||||
</hbox>
|
||||
<hbox id="security-ciphersuite"
|
||||
class="tabpanel-summary-container"
|
||||
align="center">
|
||||
<label class="plain tabpanel-summary-label"
|
||||
value="&netmonitorUI.security.cipherSuite;"/>
|
||||
<label id="security-ciphersuite-value"
|
||||
class="plain tabpanel-summary-value devtools-monospace"
|
||||
crop="end"
|
||||
flex="1"/>
|
||||
</hbox>
|
||||
</vbox>
|
||||
</vbox>
|
||||
<vbox id="security-info-domain"
|
||||
class="tabpanel-summary-container">
|
||||
<label class="plain tabpanel-summary-label"
|
||||
id="security-info-host-header"/>
|
||||
<vbox class="security-info-section">
|
||||
<hbox id="security-http-strict-transport-security"
|
||||
class="tabpanel-summary-container"
|
||||
align="center">
|
||||
<label class="plain tabpanel-summary-label"
|
||||
value="&netmonitorUI.security.hsts;"/>
|
||||
<label id="security-http-strict-transport-security-value"
|
||||
class="plain tabpanel-summary-value devtools-monospace"
|
||||
crop="end"
|
||||
flex="1"/>
|
||||
</hbox>
|
||||
<hbox id="security-public-key-pinning"
|
||||
class="tabpanel-summary-container"
|
||||
align="center">
|
||||
<label class="plain tabpanel-summary-label"
|
||||
value="&netmonitorUI.security.hpkp;"/>
|
||||
<label id="security-public-key-pinning-value"
|
||||
class="plain tabpanel-summary-value devtools-monospace"
|
||||
crop="end"
|
||||
flex="1"/>
|
||||
</hbox>
|
||||
</vbox>
|
||||
</vbox>
|
||||
<vbox id="security-info-certificate"
|
||||
class="tabpanel-summary-container">
|
||||
<label class="plain tabpanel-summary-label"
|
||||
value="&netmonitorUI.security.certificate;"/>
|
||||
<vbox class="security-info-section">
|
||||
<vbox class="tabpanel-summary-container">
|
||||
<label class="plain tabpanel-summary-label"
|
||||
value="&certmgr.subjectinfo.label;" flex="1"/>
|
||||
</vbox>
|
||||
<vbox class="security-info-section">
|
||||
<hbox class="tabpanel-summary-container"
|
||||
align="center">
|
||||
<label class="plain tabpanel-summary-label"
|
||||
value="&certmgr.certdetail.cn;:"/>
|
||||
<label id="security-cert-subject-cn"
|
||||
class="plain tabpanel-summary-value devtools-monospace"
|
||||
crop="end"
|
||||
flex="1"/>
|
||||
</hbox>
|
||||
<hbox class="tabpanel-summary-container"
|
||||
align="center">
|
||||
<label class="plain tabpanel-summary-label"
|
||||
value="&certmgr.certdetail.o;:"/>
|
||||
<label id="security-cert-subject-o"
|
||||
class="plain tabpanel-summary-value devtools-monospace"
|
||||
crop="end"
|
||||
flex="1"/>
|
||||
</hbox>
|
||||
<hbox class="tabpanel-summary-container"
|
||||
align="center">
|
||||
<label class="plain tabpanel-summary-label"
|
||||
value="&certmgr.certdetail.ou;:"/>
|
||||
<label id="security-cert-subject-ou"
|
||||
class="plain tabpanel-summary-value devtools-monospace"
|
||||
crop="end"
|
||||
flex="1"/>
|
||||
</hbox>
|
||||
</vbox>
|
||||
<vbox class="tabpanel-summary-container">
|
||||
<label class="plain tabpanel-summary-label"
|
||||
value="&certmgr.issuerinfo.label;" flex="1"/>
|
||||
</vbox>
|
||||
<vbox class="security-info-section">
|
||||
<hbox class="tabpanel-summary-container"
|
||||
align="center">
|
||||
<label class="plain tabpanel-summary-label"
|
||||
value="&certmgr.certdetail.cn;:"/>
|
||||
<label id="security-cert-issuer-cn"
|
||||
class="plain tabpanel-summary-value devtools-monospace"
|
||||
crop="end"
|
||||
flex="1"/>
|
||||
</hbox>
|
||||
<hbox class="tabpanel-summary-container"
|
||||
align="center">
|
||||
<label class="plain tabpanel-summary-label"
|
||||
value="&certmgr.certdetail.o;:"/>
|
||||
<label id="security-cert-issuer-o"
|
||||
class="plain tabpanel-summary-value devtools-monospace"
|
||||
crop="end"
|
||||
flex="1"/>
|
||||
</hbox>
|
||||
<hbox class="tabpanel-summary-container"
|
||||
align="center">
|
||||
<label class="plain tabpanel-summary-label"
|
||||
value="&certmgr.certdetail.ou;:"/>
|
||||
<label id="security-cert-issuer-ou"
|
||||
class="plain tabpanel-summary-value devtools-monospace"
|
||||
crop="end"
|
||||
flex="1"/>
|
||||
</hbox>
|
||||
</vbox>
|
||||
<vbox class="tabpanel-summary-container">
|
||||
<label class="plain tabpanel-summary-label"
|
||||
value="&certmgr.periodofvalidity.label;" flex="1"/>
|
||||
</vbox>
|
||||
<vbox class="security-info-section">
|
||||
<hbox class="tabpanel-summary-container"
|
||||
align="center">
|
||||
<label class="plain tabpanel-summary-label"
|
||||
value="&certmgr.begins;:"/>
|
||||
<label id="security-cert-validity-begins"
|
||||
class="plain tabpanel-summary-value devtools-monospace"
|
||||
crop="end"
|
||||
flex="1"/>
|
||||
</hbox>
|
||||
<hbox class="tabpanel-summary-container"
|
||||
align="center">
|
||||
<label class="plain tabpanel-summary-label"
|
||||
value="&certmgr.expires;:"/>
|
||||
<label id="security-cert-validity-expires"
|
||||
class="plain tabpanel-summary-value devtools-monospace"
|
||||
crop="end"
|
||||
flex="1"/>
|
||||
</hbox>
|
||||
</vbox>
|
||||
<vbox class="tabpanel-summary-container">
|
||||
<label class="plain tabpanel-summary-label"
|
||||
value="&certmgr.fingerprints.label;" flex="1"/>
|
||||
</vbox>
|
||||
<vbox class="security-info-section">
|
||||
<hbox class="tabpanel-summary-container"
|
||||
align="center">
|
||||
<label class="plain tabpanel-summary-label"
|
||||
value="&certmgr.certdetail.sha256fingerprint;:"/>
|
||||
<label id="security-cert-sha256-fingerprint"
|
||||
class="plain tabpanel-summary-value devtools-monospace"
|
||||
crop="end"
|
||||
flex="1"/>
|
||||
</hbox>
|
||||
<hbox class="tabpanel-summary-container"
|
||||
align="center">
|
||||
<label class="plain tabpanel-summary-label"
|
||||
value="&certmgr.certdetail.sha1fingerprint;:"/>
|
||||
<label id="security-cert-sha1-fingerprint"
|
||||
class="plain tabpanel-summary-value devtools-monospace"
|
||||
crop="end"
|
||||
flex="1"/>
|
||||
</hbox>
|
||||
</vbox>
|
||||
</vbox>
|
||||
</vbox>
|
||||
</vbox>
|
||||
</tabpanel>
|
||||
<tabpanel id="preview-tabpanel"
|
||||
class="tabpanel-content">
|
||||
<html:iframe id="response-preview"
|
||||
|
@ -26,6 +26,8 @@ support-files =
|
||||
html_copy-as-curl.html
|
||||
html_curl-utils.html
|
||||
sjs_content-type-test-server.sjs
|
||||
sjs_cors-test-server.sjs
|
||||
sjs_https-redirect-test-server.sjs
|
||||
sjs_simple-test-server.sjs
|
||||
sjs_sorting-test-server.sjs
|
||||
sjs_status-codes-test-server.sjs
|
||||
@ -83,6 +85,13 @@ skip-if = e10s # Bug 1091603
|
||||
[browser_net_req-resp-bodies.js]
|
||||
[browser_net_resend.js]
|
||||
skip-if = e10s # Bug 1091612
|
||||
[browser_net_security-details.js]
|
||||
[browser_net_security-error.js]
|
||||
[browser_net_security-icon-click.js]
|
||||
[browser_net_security-redirect.js]
|
||||
[browser_net_security-state.js]
|
||||
[browser_net_security-tab-deselect.js]
|
||||
[browser_net_security-tab-visibility.js]
|
||||
[browser_net_simple-init.js]
|
||||
[browser_net_simple-request-data.js]
|
||||
[browser_net_simple-request-details.js]
|
||||
|
@ -26,10 +26,10 @@ function test() {
|
||||
"The preview tabpanel should be hidden for non html responses.");
|
||||
|
||||
RequestsMenu.selectedIndex = 4;
|
||||
NetMonitorView.toggleDetailsPane({ visible: true, animated: false }, 5);
|
||||
NetMonitorView.toggleDetailsPane({ visible: true, animated: false }, 6);
|
||||
|
||||
is($("#event-details-pane").selectedIndex, 5,
|
||||
"The fifth tab in the details pane should be selected.");
|
||||
is($("#event-details-pane").selectedIndex, 6,
|
||||
"The sixth tab in the details pane should be selected.");
|
||||
is($("#preview-tab").hidden, false,
|
||||
"The preview tab should be visible now.");
|
||||
is($("#preview-tabpanel").hidden, false,
|
||||
|
@ -0,0 +1,90 @@
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Test that Security details tab contains the expected data.
|
||||
*/
|
||||
|
||||
add_task(function* () {
|
||||
let [tab, debuggee, monitor] = yield initNetMonitor(CUSTOM_GET_URL);
|
||||
let { $, EVENTS, NetMonitorView } = monitor.panelWin;
|
||||
let { RequestsMenu, NetworkDetails } = NetMonitorView;
|
||||
RequestsMenu.lazyUpdate = false;
|
||||
|
||||
info("Performing a secure request.");
|
||||
debuggee.performRequests(1, "https://example.com" + CORS_SJS_PATH);
|
||||
|
||||
yield waitForNetworkEvents(monitor, 1);
|
||||
|
||||
info("Selecting the request.");
|
||||
RequestsMenu.selectedIndex = 0;
|
||||
|
||||
info("Waiting for details pane to be updated.");
|
||||
yield monitor.panelWin.once(EVENTS.TAB_UPDATED);
|
||||
|
||||
info("Selecting security tab.");
|
||||
NetworkDetails.widget.selectedIndex = 5;
|
||||
|
||||
info("Waiting for security tab to be updated.");
|
||||
yield monitor.panelWin.once(EVENTS.TAB_UPDATED);
|
||||
|
||||
let errorbox = $("#security-error");
|
||||
let infobox = $("#security-information");
|
||||
|
||||
is(errorbox.hidden, true, "Error box is hidden.");
|
||||
is(infobox.hidden, false, "Information box visible.");
|
||||
|
||||
// Connection
|
||||
checkLabel("#security-protocol-version-value", "TLSv1");
|
||||
checkLabel("#security-ciphersuite-value", "TLS_RSA_WITH_AES_128_CBC_SHA");
|
||||
|
||||
// Host
|
||||
checkLabel("#security-info-host-header", "Host example.com:");
|
||||
checkLabel("#security-http-strict-transport-security-value", "Disabled");
|
||||
checkLabel("#security-public-key-pinning-value", "Disabled");
|
||||
|
||||
// Cert
|
||||
checkLabel("#security-cert-subject-cn", "example.com");
|
||||
checkLabel("#security-cert-subject-o", "<Not Available>");
|
||||
checkLabel("#security-cert-subject-ou", "<Not Available>");
|
||||
|
||||
checkLabel("#security-cert-issuer-cn", "Temporary Certificate Authority");
|
||||
checkLabel("#security-cert-issuer-o", "Mozilla Testing");
|
||||
checkLabel("#security-cert-issuer-ou", "<Not Available>");
|
||||
|
||||
// Locale sensitive and varies between timezones. Cant't compare equality or
|
||||
// the test fails depending on which part of the world the test is executed.
|
||||
checkLabelNotEmpty("#security-cert-validity-begins");
|
||||
checkLabelNotEmpty("#security-cert-validity-expires");
|
||||
|
||||
checkLabelNotEmpty("#security-cert-sha1-fingerprint");
|
||||
checkLabelNotEmpty("#security-cert-sha256-fingerprint");
|
||||
yield teardown(monitor);
|
||||
|
||||
/**
|
||||
* A helper that compares value attribute of a label with given selector to the
|
||||
* expected value.
|
||||
*/
|
||||
function checkLabel(selector, expected) {
|
||||
info("Checking label " + selector);
|
||||
|
||||
let element = $(selector);
|
||||
|
||||
ok(element, "Selector matched an element.");
|
||||
is(element.value, expected, "Label has the expected value.");
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper that checks the label with given selector is not an empty string.
|
||||
*/
|
||||
function checkLabelNotEmpty(selector) {
|
||||
info("Checking that label " + selector + " is non-empty.");
|
||||
|
||||
let element = $(selector);
|
||||
|
||||
ok(element, "Selector matched an element.");
|
||||
isnot(element.value, "", "Label was not empty.");
|
||||
}
|
||||
});
|
@ -0,0 +1,67 @@
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Test that Security details tab shows an error message with broken connections.
|
||||
*/
|
||||
|
||||
add_task(function* () {
|
||||
let [tab, debuggee, monitor] = yield initNetMonitor(CUSTOM_GET_URL);
|
||||
let { $, EVENTS, NetMonitorView } = monitor.panelWin;
|
||||
let { RequestsMenu, NetworkDetails } = NetMonitorView;
|
||||
RequestsMenu.lazyUpdate = false;
|
||||
|
||||
info("Requesting a resource that has a certificate problem.");
|
||||
debuggee.performRequests(1, "https://nocert.example.com");
|
||||
|
||||
yield waitForSecurityBrokenNetworkEvent();
|
||||
|
||||
info("Selecting the request.");
|
||||
RequestsMenu.selectedIndex = 0;
|
||||
|
||||
info("Waiting for details pane to be updated.");
|
||||
yield monitor.panelWin.once(EVENTS.TAB_UPDATED);
|
||||
|
||||
info("Selecting security tab.");
|
||||
NetworkDetails.widget.selectedIndex = 5;
|
||||
|
||||
info("Waiting for security tab to be updated.");
|
||||
yield monitor.panelWin.once(EVENTS.TAB_UPDATED);
|
||||
|
||||
let errorbox = $("#security-error");
|
||||
let errormsg = $("#security-error-message");
|
||||
let infobox = $("#security-information");
|
||||
|
||||
is(errorbox.hidden, false, "Error box is visble.");
|
||||
is(infobox.hidden, true, "Information box is hidden.");
|
||||
|
||||
isnot(errormsg.textContent, "", "Error message is not empty.");
|
||||
|
||||
yield teardown(monitor);
|
||||
|
||||
/**
|
||||
* Returns a promise that's resolved once a request with security issues is
|
||||
* completed.
|
||||
*/
|
||||
function waitForSecurityBrokenNetworkEvent() {
|
||||
let awaitedEvents = [
|
||||
"UPDATING_REQUEST_HEADERS",
|
||||
"RECEIVED_REQUEST_HEADERS",
|
||||
"UPDATING_REQUEST_COOKIES",
|
||||
"RECEIVED_REQUEST_COOKIES",
|
||||
"STARTED_RECEIVING_RESPONSE",
|
||||
"UPDATING_RESPONSE_CONTENT",
|
||||
"RECEIVED_RESPONSE_CONTENT",
|
||||
"UPDATING_EVENT_TIMINGS",
|
||||
"RECEIVED_EVENT_TIMINGS",
|
||||
];
|
||||
|
||||
let promises = awaitedEvents.map((event) => {
|
||||
return monitor.panelWin.once(EVENTS[event]);
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
});
|
@ -0,0 +1,53 @@
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Test that clicking on the security indicator opens the security details tab.
|
||||
*/
|
||||
|
||||
add_task(function* () {
|
||||
let [tab, debuggee, monitor] = yield initNetMonitor(CUSTOM_GET_URL);
|
||||
let { $, EVENTS, NetMonitorView } = monitor.panelWin;
|
||||
let { RequestsMenu, NetworkDetails } = NetMonitorView;
|
||||
RequestsMenu.lazyUpdate = false;
|
||||
|
||||
info("Requesting a resource over HTTPS.");
|
||||
debuggee.performRequests(1, "https://example.com" + CORS_SJS_PATH + "?request_2");
|
||||
yield waitForNetworkEvents(monitor, 1);
|
||||
|
||||
debuggee.performRequests(1, "https://example.com" + CORS_SJS_PATH + "?request_1");
|
||||
yield waitForNetworkEvents(monitor, 1);
|
||||
|
||||
is(RequestsMenu.itemCount, 2, "Two events event logged.");
|
||||
|
||||
yield clickAndTestSecurityIcon();
|
||||
|
||||
info("Selecting headers panel again.");
|
||||
NetworkDetails.widget.selectedIndex = 0;
|
||||
yield monitor.panelWin.once(EVENTS.TAB_UPDATED);
|
||||
|
||||
info("Sorting the items by filename.");
|
||||
EventUtils.sendMouseEvent({ type: "click" }, $("#requests-menu-file-button"));
|
||||
|
||||
info("Testing that security icon can be clicked after the items were sorted.");
|
||||
yield clickAndTestSecurityIcon();
|
||||
|
||||
yield teardown(monitor);
|
||||
|
||||
function* clickAndTestSecurityIcon() {
|
||||
let item = RequestsMenu.items[0];
|
||||
let icon = $(".requests-security-state-icon", item.target);
|
||||
|
||||
info("Clicking security icon of the first request and waiting for the " +
|
||||
"panel to update.");
|
||||
|
||||
icon.click();
|
||||
yield monitor.panelWin.once(EVENTS.TAB_UPDATED);
|
||||
|
||||
is(NetworkDetails.widget.selectedPanel, $("#security-tabpanel"),
|
||||
"Security tab is selected.");
|
||||
}
|
||||
|
||||
});
|
@ -0,0 +1,35 @@
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Test a http -> https redirect shows secure icon only for redirected https
|
||||
* request.
|
||||
*/
|
||||
|
||||
add_task(function* () {
|
||||
let [tab, debuggee, monitor] = yield initNetMonitor(CUSTOM_GET_URL);
|
||||
let { $, NetMonitorView } = monitor.panelWin;
|
||||
let { RequestsMenu } = NetMonitorView;
|
||||
RequestsMenu.lazyUpdate = false;
|
||||
|
||||
debuggee.performRequests(1, HTTPS_REDIRECT_SJS);
|
||||
yield waitForNetworkEvents(monitor, 2);
|
||||
|
||||
is(RequestsMenu.itemCount, 2, "There were two requests due to redirect.");
|
||||
|
||||
let initial = RequestsMenu.items[0];
|
||||
let redirect = RequestsMenu.items[1];
|
||||
|
||||
let initialSecurityIcon = $(".requests-security-state-icon", initial.target);
|
||||
let redirectSecurityIcon = $(".requests-security-state-icon", redirect.target);
|
||||
|
||||
ok(initialSecurityIcon.classList.contains("security-state-insecure"),
|
||||
"Initial request was marked insecure.");
|
||||
|
||||
ok(redirectSecurityIcon.classList.contains("security-state-secure"),
|
||||
"Redirected request was marked secure.");
|
||||
|
||||
yield teardown(monitor);
|
||||
});
|
@ -0,0 +1,99 @@
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Test that correct security state indicator appears depending on the security
|
||||
* state.
|
||||
*/
|
||||
|
||||
add_task(function* () {
|
||||
const EXPECTED_SECURITY_STATES = {
|
||||
"test1.example.com": "security-state-insecure",
|
||||
"example.com": "security-state-secure",
|
||||
"nocert.example.com": "security-state-broken",
|
||||
};
|
||||
|
||||
let [tab, debuggee, monitor] = yield initNetMonitor(CUSTOM_GET_URL);
|
||||
let { $, EVENTS, NetMonitorView } = monitor.panelWin;
|
||||
let { RequestsMenu } = NetMonitorView;
|
||||
RequestsMenu.lazyUpdate = false;
|
||||
|
||||
yield performRequests();
|
||||
|
||||
for (let item of RequestsMenu.items) {
|
||||
let domain = $(".requests-menu-domain", item.target).value;
|
||||
|
||||
info("Found a request to " + domain);
|
||||
ok(domain in EXPECTED_SECURITY_STATES, "Domain " + domain + " was expected.");
|
||||
|
||||
let classes = $(".requests-security-state-icon", item.target).classList;
|
||||
let expectedClass = EXPECTED_SECURITY_STATES[domain];
|
||||
|
||||
info("Classes of security state icon are: " + classes);
|
||||
info("Security state icon is expected to contain class: " + expectedClass);
|
||||
ok(classes.contains(expectedClass), "Icon contained the correct class name.");
|
||||
}
|
||||
|
||||
yield teardown(monitor);
|
||||
|
||||
/**
|
||||
* A helper that performs requests to
|
||||
* - https://nocert.example.com (broken)
|
||||
* - https://example.com (secure)
|
||||
* - http://test1.example.com (insecure)
|
||||
* and waits until NetworkMonitor has handled all packets sent by the server.
|
||||
*/
|
||||
function* performRequests() {
|
||||
// waitForNetworkEvents does not work for requests with security errors as
|
||||
// those only emit 9/13 events of a successful request.
|
||||
let done = waitForSecurityBrokenNetworkEvent();
|
||||
|
||||
info("Requesting a resource that has a certificate problem.");
|
||||
debuggee.performRequests(1, "https://nocert.example.com");
|
||||
|
||||
// Wait for the request to complete before firing another request. Otherwise
|
||||
// the request with security issues interfere with waitForNetworkEvents.
|
||||
info("Waiting for request to complete.");
|
||||
yield done;
|
||||
|
||||
// Next perform a request over HTTP. If done the other way around the latter
|
||||
// occasionally hangs waiting for event timings that don't seem to appear...
|
||||
done = waitForNetworkEvents(monitor, 1);
|
||||
info("Requesting a resource over HTTP.");
|
||||
debuggee.performRequests(1, "http://test1.example.com" + CORS_SJS_PATH);
|
||||
yield done;
|
||||
|
||||
done = waitForNetworkEvents(monitor, 1);
|
||||
info("Requesting a resource over HTTPS.");
|
||||
debuggee.performRequests(1, "https://example.com" + CORS_SJS_PATH);
|
||||
yield done;
|
||||
|
||||
is(RequestsMenu.itemCount, 3, "Three events logged.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that's resolved once a request with security issues is
|
||||
* completed.
|
||||
*/
|
||||
function waitForSecurityBrokenNetworkEvent() {
|
||||
let awaitedEvents = [
|
||||
"UPDATING_REQUEST_HEADERS",
|
||||
"RECEIVED_REQUEST_HEADERS",
|
||||
"UPDATING_REQUEST_COOKIES",
|
||||
"RECEIVED_REQUEST_COOKIES",
|
||||
"STARTED_RECEIVING_RESPONSE",
|
||||
"UPDATING_RESPONSE_CONTENT",
|
||||
"RECEIVED_RESPONSE_CONTENT",
|
||||
"UPDATING_EVENT_TIMINGS",
|
||||
"RECEIVED_EVENT_TIMINGS",
|
||||
];
|
||||
|
||||
let promises = awaitedEvents.map((event) => {
|
||||
return monitor.panelWin.once(EVENTS[event]);
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
});
|
@ -0,0 +1,38 @@
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Test that security details tab is no longer selected if an insecure request
|
||||
* is selected.
|
||||
*/
|
||||
|
||||
add_task(function* () {
|
||||
let [tab, debuggee, monitor] = yield initNetMonitor(CUSTOM_GET_URL);
|
||||
let { $, EVENTS, NetMonitorView } = monitor.panelWin;
|
||||
let { RequestsMenu, NetworkDetails } = NetMonitorView;
|
||||
RequestsMenu.lazyUpdate = false;
|
||||
|
||||
info("Performing requests.");
|
||||
debuggee.performRequests(1, "https://example.com" + CORS_SJS_PATH);
|
||||
debuggee.performRequests(1, "http://example.com" + CORS_SJS_PATH);
|
||||
yield waitForNetworkEvents(monitor, 2);
|
||||
|
||||
info("Selecting secure request.");
|
||||
RequestsMenu.selectedIndex = 0;
|
||||
|
||||
info("Selecting security tab.");
|
||||
NetworkDetails.widget.selectedIndex = 5;
|
||||
|
||||
info("Selecting insecure request.");
|
||||
RequestsMenu.selectedIndex = 1;
|
||||
|
||||
info("Waiting for security tab to be updated.");
|
||||
yield monitor.panelWin.once(EVENTS.NETWORKDETAILSVIEW_POPULATED);
|
||||
|
||||
is(NetworkDetails.widget.selectedIndex, 0,
|
||||
"Selected tab was reset when selected security tab was hidden.");
|
||||
|
||||
yield teardown(monitor);
|
||||
});
|
@ -0,0 +1,111 @@
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Test that security details tab is visible only when it should.
|
||||
*/
|
||||
|
||||
add_task(function* () {
|
||||
const TEST_DATA = [
|
||||
{
|
||||
desc: "http request",
|
||||
uri: "http://example.com" + CORS_SJS_PATH,
|
||||
visibleOnNewEvent: false,
|
||||
visibleOnSecurityInfo: false,
|
||||
visibleOnceComplete: false,
|
||||
}, {
|
||||
desc: "working https request",
|
||||
uri: "https://example.com" + CORS_SJS_PATH,
|
||||
visibleOnNewEvent: false,
|
||||
visibleOnSecurityInfo: true,
|
||||
visibleOnceComplete: true,
|
||||
}, {
|
||||
desc: "broken https request",
|
||||
uri: "https://nocert.example.com",
|
||||
isBroken: true,
|
||||
visibleOnNewEvent: false,
|
||||
visibleOnSecurityInfo: true,
|
||||
visibleOnceComplete: true,
|
||||
}
|
||||
];
|
||||
|
||||
let [tab, debuggee, monitor] = yield initNetMonitor(CUSTOM_GET_URL);
|
||||
let { $, EVENTS, NetMonitorView } = monitor.panelWin;
|
||||
let { RequestsMenu } = NetMonitorView;
|
||||
RequestsMenu.lazyUpdate = false;
|
||||
|
||||
for (let testcase of TEST_DATA) {
|
||||
info("Testing Security tab visibility for " + testcase.desc);
|
||||
let onNewItem = monitor.panelWin.once(EVENTS.NETWORK_EVENT);
|
||||
let onSecurityInfo = monitor.panelWin.once(EVENTS.RECEIVED_SECURITY_INFO);
|
||||
let onComplete = testcase.isBroken ?
|
||||
waitForSecurityBrokenNetworkEvent() :
|
||||
waitForNetworkEvents(monitor, 1);
|
||||
|
||||
let tab = $("#security-tab");
|
||||
|
||||
info("Performing a request to " + testcase.uri);
|
||||
debuggee.performRequests(1, testcase.uri);
|
||||
|
||||
info("Waiting for new network event.");
|
||||
yield onNewItem;
|
||||
|
||||
info("Selecting the request.");
|
||||
RequestsMenu.selectedIndex = 0;
|
||||
|
||||
is(RequestsMenu.selectedItem.attachment.securityState, undefined,
|
||||
"Security state has not yet arrived.");
|
||||
is(tab.hidden, !testcase.visibleOnNewEvent,
|
||||
"Security tab is " +
|
||||
(testcase.visibleOnNewEvent ? "visible" : "hidden") +
|
||||
" after new request was added to the menu.");
|
||||
|
||||
info("Waiting for security information to arrive.");
|
||||
yield onSecurityInfo;
|
||||
|
||||
ok(RequestsMenu.selectedItem.attachment.securityState,
|
||||
"Security state arrived.");
|
||||
is(tab.hidden, !testcase.visibleOnSecurityInfo,
|
||||
"Security tab is " +
|
||||
(testcase.visibleOnSecurityInfo ? "visible" : "hidden") +
|
||||
" after security information arrived.");
|
||||
|
||||
info("Waiting for request to complete.");
|
||||
yield onComplete;
|
||||
is(tab.hidden, !testcase.visibleOnceComplete,
|
||||
"Security tab is " +
|
||||
(testcase.visibleOnceComplete ? "visible" : "hidden") +
|
||||
" after request has been completed.");
|
||||
|
||||
info("Clearing requests.");
|
||||
RequestsMenu.clear();
|
||||
}
|
||||
|
||||
yield teardown(monitor);
|
||||
|
||||
/**
|
||||
* Returns a promise that's resolved once a request with security issues is
|
||||
* completed.
|
||||
*/
|
||||
function waitForSecurityBrokenNetworkEvent() {
|
||||
let awaitedEvents = [
|
||||
"UPDATING_REQUEST_HEADERS",
|
||||
"RECEIVED_REQUEST_HEADERS",
|
||||
"UPDATING_REQUEST_COOKIES",
|
||||
"RECEIVED_REQUEST_COOKIES",
|
||||
"STARTED_RECEIVING_RESPONSE",
|
||||
"UPDATING_RESPONSE_CONTENT",
|
||||
"RECEIVED_RESPONSE_CONTENT",
|
||||
"UPDATING_EVENT_TIMINGS",
|
||||
"RECEIVED_EVENT_TIMINGS",
|
||||
];
|
||||
|
||||
let promises = awaitedEvents.map((event) => {
|
||||
return monitor.panelWin.once(EVENTS[event]);
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
});
|
@ -43,6 +43,8 @@ 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 HTTPS_REDIRECT_SJS = EXAMPLE_URL + "sjs_https-redirect-test-server.sjs";
|
||||
const CORS_SJS_PATH = "/browser/browser/devtools/netmonitor/test/sjs_cors-test-server.sjs";
|
||||
|
||||
const TEST_IMAGE = EXAMPLE_URL + "test-image.png";
|
||||
const TEST_IMAGE_DATA_URI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==";
|
||||
|
16
browser/devtools/netmonitor/test/sjs_cors-test-server.sjs
Normal file
16
browser/devtools/netmonitor/test/sjs_cors-test-server.sjs
Normal file
@ -0,0 +1,16 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
function handleRequest(request, response) {
|
||||
response.setStatusLine(request.httpVersion, 200, "Och Aye");
|
||||
|
||||
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
response.setHeader("Pragma", "no-cache");
|
||||
response.setHeader("Expires", "0");
|
||||
|
||||
response.setHeader("Access-Control-Allow-Origin", "*", false);
|
||||
|
||||
response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
|
||||
|
||||
response.write("Access-Control-Allow-Origin: *");
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
function handleRequest(request, response) {
|
||||
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
response.setHeader("Pragma", "no-cache");
|
||||
response.setHeader("Expires", "0");
|
||||
|
||||
response.setHeader("Access-Control-Allow-Origin", "*", false);
|
||||
|
||||
if (request.scheme === "http") {
|
||||
response.setStatusLine(request.httpVersion, 302, "Found");
|
||||
response.setHeader("Location", "https://" + request.host + request.path);
|
||||
} else {
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.write("Page was accessed over HTTPS!");
|
||||
}
|
||||
|
||||
}
|
@ -17,6 +17,8 @@ devtools.lazyRequireGetter(this, "EventEmitter",
|
||||
devtools.lazyRequireGetter(this, "DevToolsUtils",
|
||||
"devtools/toolkit/DevToolsUtils");
|
||||
|
||||
devtools.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
|
||||
"devtools/timeline/global", true);
|
||||
devtools.lazyRequireGetter(this, "L10N",
|
||||
"devtools/profiler/global", true);
|
||||
devtools.lazyRequireGetter(this, "PerformanceIO",
|
||||
@ -33,14 +35,18 @@ devtools.lazyRequireGetter(this, "CallView",
|
||||
"devtools/profiler/tree-view", true);
|
||||
devtools.lazyRequireGetter(this, "ThreadNode",
|
||||
"devtools/profiler/tree-model", true);
|
||||
devtools.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
|
||||
"devtools/timeline/global", true);
|
||||
|
||||
|
||||
devtools.lazyImporter(this, "CanvasGraphUtils",
|
||||
"resource:///modules/devtools/Graphs.jsm");
|
||||
devtools.lazyImporter(this, "LineGraphWidget",
|
||||
"resource:///modules/devtools/Graphs.jsm");
|
||||
|
||||
devtools.lazyImporter(this, "FlameGraphUtils",
|
||||
"resource:///modules/devtools/FlameGraph.jsm");
|
||||
devtools.lazyImporter(this, "FlameGraph",
|
||||
"resource:///modules/devtools/FlameGraph.jsm");
|
||||
|
||||
// Events emitted by various objects in the panel.
|
||||
const EVENTS = {
|
||||
// Emitted by the PerformanceView on record button click
|
||||
@ -84,7 +90,10 @@ const EVENTS = {
|
||||
SOURCE_NOT_FOUND_IN_JS_DEBUGGER: "Performance:UI:SourceNotFoundInJsDebugger",
|
||||
|
||||
// Emitted by the WaterfallView when it has been rendered
|
||||
WATERFALL_RENDERED: "Performance:UI:WaterfallRendered"
|
||||
WATERFALL_RENDERED: "Performance:UI:WaterfallRendered",
|
||||
|
||||
// Emitted by the FlameGraphView when it has been rendered
|
||||
FLAMEGRAPH_RENDERED: "Performance:UI:FlameGraphRendered"
|
||||
};
|
||||
|
||||
// Constant defining the end time for a recording that hasn't finished
|
||||
|
@ -20,6 +20,7 @@
|
||||
<script type="application/javascript" src="performance/views/details.js"/>
|
||||
<script type="application/javascript" src="performance/views/details-call-tree.js"/>
|
||||
<script type="application/javascript" src="performance/views/details-waterfall.js"/>
|
||||
<script type="application/javascript" src="performance/views/details-flamegraph.js"/>
|
||||
|
||||
<vbox class="theme-body" flex="1">
|
||||
<toolbar id="performance-toolbar" class="devtools-toolbar">
|
||||
@ -43,26 +44,27 @@
|
||||
</toolbar>
|
||||
|
||||
<vbox id="overview-pane">
|
||||
<hbox id="time-framerate"/>
|
||||
<hbox id="markers-overview"/>
|
||||
<hbox id="memory-overview"/>
|
||||
<hbox id="time-framerate"/>
|
||||
</vbox>
|
||||
|
||||
<toolbar id="details-toolbar" class="devtools-toolbar">
|
||||
<hbox class="devtools-toolbarbutton-group">
|
||||
<toolbarbutton id="select-waterfall-view"
|
||||
class="devtools-toolbarbutton"
|
||||
tooltiptext="waterfall"
|
||||
data-view="waterfall" />
|
||||
<toolbarbutton id="select-calltree-view"
|
||||
class="devtools-toolbarbutton"
|
||||
tooltiptext="calltree"
|
||||
data-view="calltree" />
|
||||
<toolbarbutton id="select-flamegraph-view"
|
||||
class="devtools-toolbarbutton"
|
||||
data-view="flamegraph" />
|
||||
</hbox>
|
||||
</toolbar>
|
||||
|
||||
<deck id="details-pane" flex="1">
|
||||
<hbox id="waterfall-view">
|
||||
<hbox id="waterfall-view" flex="1">
|
||||
<vbox id="waterfall-graph" flex="1" />
|
||||
<splitter class="devtools-side-splitter"/>
|
||||
<vbox id="waterfall-details"
|
||||
@ -71,7 +73,7 @@
|
||||
height="150"/>
|
||||
</hbox>
|
||||
|
||||
<vbox id="calltree-view" class="call-tree" flex="1">
|
||||
<vbox id="calltree-view" flex="1">
|
||||
<hbox class="call-tree-headers-container">
|
||||
<label class="plain call-tree-header"
|
||||
type="duration"
|
||||
@ -100,6 +102,9 @@
|
||||
</hbox>
|
||||
<vbox class="call-tree-cells-container" flex="1"/>
|
||||
</vbox>
|
||||
|
||||
<hbox id="flamegraph-view" flex="1">
|
||||
</hbox>
|
||||
</deck>
|
||||
</vbox>
|
||||
</window>
|
||||
|
@ -13,6 +13,7 @@ support-files =
|
||||
[browser_perf-data-samples.js]
|
||||
[browser_perf-details-calltree-render-01.js]
|
||||
[browser_perf-details-calltree-render-02.js]
|
||||
[browser_perf-details-flamegraph-render-01.js]
|
||||
[browser_perf-details-waterfall-render-01.js]
|
||||
[browser_perf-details.js]
|
||||
[browser_perf-front-basic-profiler-01.js]
|
||||
|
@ -0,0 +1,22 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Tests that the flamegraph view renders content after recording.
|
||||
*/
|
||||
function spawnTest () {
|
||||
let { panel } = yield initPerformance(SIMPLE_URL);
|
||||
let { EVENTS, PerformanceController, FlameGraphView } = panel.panelWin;
|
||||
|
||||
yield startRecording(panel);
|
||||
yield waitUntil(() => PerformanceController.getMarkers().length);
|
||||
|
||||
let rendered = once(FlameGraphView, EVENTS.FLAMEGRAPH_RENDERED);
|
||||
yield stopRecording(panel);
|
||||
yield rendered;
|
||||
|
||||
ok(true, "FlameGraphView rendered after recording is stopped.");
|
||||
|
||||
yield teardown(panel);
|
||||
finish();
|
||||
}
|
@ -25,6 +25,13 @@ function spawnTest () {
|
||||
is(viewName, "waterfall", "DETAILS_VIEW_SELECTED fired with view name");
|
||||
checkViews(DetailsView, doc, "waterfall");
|
||||
|
||||
// Select flamegraph view
|
||||
viewChanged = onceSpread(DetailsView, EVENTS.DETAILS_VIEW_SELECTED);
|
||||
command($("toolbarbutton[data-view='flamegraph']"));
|
||||
[_, viewName] = yield viewChanged;
|
||||
is(viewName, "flamegraph", "DETAILS_VIEW_SELECTED fired with view name");
|
||||
checkViews(DetailsView, doc, "flamegraph");
|
||||
|
||||
yield teardown(panel);
|
||||
finish();
|
||||
}
|
||||
|
69
browser/devtools/performance/views/details-flamegraph.js
Normal file
69
browser/devtools/performance/views/details-flamegraph.js
Normal file
@ -0,0 +1,69 @@
|
||||
/* 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";
|
||||
|
||||
/**
|
||||
* FlameGraph view containing a pyramid-like visualization of a profile,
|
||||
* controlled by DetailsView.
|
||||
*/
|
||||
let FlameGraphView = {
|
||||
/**
|
||||
* Sets up the view with event binding.
|
||||
*/
|
||||
initialize: Task.async(function* () {
|
||||
this._onRecordingStopped = this._onRecordingStopped.bind(this);
|
||||
this._onRangeChange = this._onRangeChange.bind(this);
|
||||
|
||||
this.graph = new FlameGraph($("#flamegraph-view"));
|
||||
this.graph.timelineTickUnits = L10N.getStr("graphs.ms");
|
||||
yield this.graph.ready();
|
||||
|
||||
PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
|
||||
OverviewView.on(EVENTS.OVERVIEW_RANGE_SELECTED, this._onRangeChange);
|
||||
OverviewView.on(EVENTS.OVERVIEW_RANGE_CLEARED, this._onRangeChange);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Unbinds events.
|
||||
*/
|
||||
destroy: function () {
|
||||
PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
|
||||
OverviewView.off(EVENTS.OVERVIEW_RANGE_SELECTED, this._onRangeChange);
|
||||
OverviewView.off(EVENTS.OVERVIEW_RANGE_CLEARED, this._onRangeChange);
|
||||
},
|
||||
|
||||
/**
|
||||
* Method for handling all the set up for rendering a new flamegraph.
|
||||
*/
|
||||
render: function (profilerData) {
|
||||
// Empty recordings might yield no profiler data.
|
||||
if (profilerData.profile == null) {
|
||||
return;
|
||||
}
|
||||
let samples = profilerData.profile.threads[0].samples;
|
||||
let dataSrc = FlameGraphUtils.createFlameGraphDataFromSamples(samples);
|
||||
this.graph.setData(dataSrc);
|
||||
this.emit(EVENTS.FLAMEGRAPH_RENDERED);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when recording is stopped.
|
||||
*/
|
||||
_onRecordingStopped: function () {
|
||||
let profilerData = PerformanceController.getProfilerData();
|
||||
this.render(profilerData);
|
||||
},
|
||||
|
||||
/**
|
||||
* Fired when a range is selected or cleared in the OverviewView.
|
||||
*/
|
||||
_onRangeChange: function (_, params) {
|
||||
// TODO bug 1105014
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Convenient way of emitting events from the view.
|
||||
*/
|
||||
EventEmitter.decorate(FlameGraphView);
|
@ -15,7 +15,8 @@ let DetailsView = {
|
||||
*/
|
||||
viewIndexes: {
|
||||
waterfall: 0,
|
||||
calltree: 1
|
||||
calltree: 1,
|
||||
flamegraph: 2
|
||||
},
|
||||
|
||||
/**
|
||||
@ -32,6 +33,7 @@ let DetailsView = {
|
||||
|
||||
yield CallTreeView.initialize();
|
||||
yield WaterfallView.initialize();
|
||||
yield FlameGraphView.initialize();
|
||||
|
||||
this.selectView(DEFAULT_DETAILS_SUBVIEW);
|
||||
}),
|
||||
@ -46,6 +48,7 @@ let DetailsView = {
|
||||
|
||||
yield CallTreeView.destroy();
|
||||
yield WaterfallView.destroy();
|
||||
yield FlameGraphView.destroy();
|
||||
}),
|
||||
|
||||
/**
|
||||
@ -59,10 +62,11 @@ let DetailsView = {
|
||||
this.el.selectedIndex = this.viewIndexes[selectedView];
|
||||
|
||||
for (let button of $$("toolbarbutton[data-view]", $("#details-toolbar"))) {
|
||||
if (button.getAttribute("data-view") === selectedView)
|
||||
if (button.getAttribute("data-view") === selectedView) {
|
||||
button.setAttribute("checked", true);
|
||||
else
|
||||
} else {
|
||||
button.removeAttribute("checked");
|
||||
}
|
||||
}
|
||||
|
||||
this.emit(EVENTS.DETAILS_VIEW_SELECTED, selectedView);
|
||||
|
@ -12,9 +12,9 @@ const FRAMERATE_GRAPH_LOW_RES_INTERVAL = 100; // ms
|
||||
const FRAMERATE_GRAPH_HIGH_RES_INTERVAL = 16; // ms
|
||||
|
||||
const FRAMERATE_GRAPH_HEIGHT = 45; // px
|
||||
const MARKERS_GRAPH_HEADER_HEIGHT = 12; // px
|
||||
const MARKERS_GRAPH_BODY_HEIGHT = 45; // 9px * 5 groups
|
||||
const MARKERS_GROUP_VERTICAL_PADDING = 3.5; // px
|
||||
const MARKERS_GRAPH_HEADER_HEIGHT = 14; // px
|
||||
const MARKERS_GRAPH_ROW_HEIGHT = 11; // px
|
||||
const MARKERS_GROUP_VERTICAL_PADDING = 5; // px
|
||||
const MEMORY_GRAPH_HEIGHT = 30; // px
|
||||
|
||||
const GRAPH_SCROLL_EVENTS_DRAIN = 50; // ms
|
||||
@ -82,7 +82,7 @@ let OverviewView = {
|
||||
_showMarkersGraph: Task.async(function *() {
|
||||
this.markersOverview = new MarkersOverview($("#markers-overview"), TIMELINE_BLUEPRINT);
|
||||
this.markersOverview.headerHeight = MARKERS_GRAPH_HEADER_HEIGHT;
|
||||
this.markersOverview.bodyHeight = MARKERS_GRAPH_BODY_HEIGHT;
|
||||
this.markersOverview.rowHeight = MARKERS_GRAPH_ROW_HEIGHT;
|
||||
this.markersOverview.groupPadding = MARKERS_GROUP_VERTICAL_PADDING;
|
||||
yield this.markersOverview.ready();
|
||||
|
||||
|
@ -22,6 +22,7 @@ EXTRA_JS_MODULES.devtools += [
|
||||
'widgets/AbstractTreeItem.jsm',
|
||||
'widgets/BreadcrumbsWidget.jsm',
|
||||
'widgets/Chart.jsm',
|
||||
'widgets/FlameGraph.jsm',
|
||||
'widgets/Graphs.jsm',
|
||||
'widgets/GraphsWorker.js',
|
||||
'widgets/SideMenuWidget.jsm',
|
||||
|
@ -105,6 +105,11 @@ Telemetry.prototype = {
|
||||
userHistogram: "DEVTOOLS_FONTINSPECTOR_OPENED_PER_USER_FLAG",
|
||||
timerHistogram: "DEVTOOLS_FONTINSPECTOR_TIME_ACTIVE_SECONDS"
|
||||
},
|
||||
animationinspector: {
|
||||
histogram: "DEVTOOLS_ANIMATIONINSPECTOR_OPENED_BOOLEAN",
|
||||
userHistogram: "DEVTOOLS_ANIMATIONINSPECTOR_OPENED_PER_USER_FLAG",
|
||||
timerHistogram: "DEVTOOLS_ANIMATIONINSPECTOR_TIME_ACTIVE_SECONDS"
|
||||
},
|
||||
jsdebugger: {
|
||||
histogram: "DEVTOOLS_JSDEBUGGER_OPENED_BOOLEAN",
|
||||
userHistogram: "DEVTOOLS_JSDEBUGGER_OPENED_PER_USER_FLAG",
|
||||
|
@ -15,6 +15,12 @@ support-files =
|
||||
[browser_cubic-bezier-01.js]
|
||||
[browser_cubic-bezier-02.js]
|
||||
[browser_cubic-bezier-03.js]
|
||||
[browser_flame-graph-01.js]
|
||||
[browser_flame-graph-02.js]
|
||||
[browser_flame-graph-03a.js]
|
||||
[browser_flame-graph-03b.js]
|
||||
[browser_flame-graph-04.js]
|
||||
[browser_flame-graph-utils.js]
|
||||
[browser_graphs-01.js]
|
||||
[browser_graphs-02.js]
|
||||
[browser_graphs-03.js]
|
||||
|
60
browser/devtools/shared/test/browser_flame-graph-01.js
Normal file
60
browser/devtools/shared/test/browser_flame-graph-01.js
Normal file
@ -0,0 +1,60 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Tests that flame graph widget works properly.
|
||||
|
||||
let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
|
||||
let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
|
||||
let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
|
||||
let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
|
||||
|
||||
let test = Task.async(function*() {
|
||||
yield promiseTab("about:blank");
|
||||
yield performTest();
|
||||
gBrowser.removeCurrentTab();
|
||||
finish();
|
||||
});
|
||||
|
||||
function* performTest() {
|
||||
let [host, win, doc] = yield createHost();
|
||||
doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
|
||||
|
||||
let graph = new FlameGraph(doc.body);
|
||||
|
||||
let readyEventEmitted;
|
||||
graph.once("ready", () => readyEventEmitted = true);
|
||||
|
||||
yield graph.ready();
|
||||
ok(readyEventEmitted, "The 'ready' event should have been emitted");
|
||||
|
||||
testGraph(host, graph);
|
||||
|
||||
graph.destroy();
|
||||
host.destroy();
|
||||
}
|
||||
|
||||
function testGraph(host, graph) {
|
||||
ok(graph._container.classList.contains("flame-graph-widget-container"),
|
||||
"The correct graph container was created.");
|
||||
ok(graph._canvas.classList.contains("flame-graph-widget-canvas"),
|
||||
"The correct graph container was created.");
|
||||
|
||||
let bounds = host.frame.getBoundingClientRect();
|
||||
|
||||
is(graph.width, bounds.width * window.devicePixelRatio,
|
||||
"The graph has the correct width.");
|
||||
is(graph.height, bounds.height * window.devicePixelRatio,
|
||||
"The graph has the correct height.");
|
||||
|
||||
ok(graph._selection.start === null,
|
||||
"The graph's selection start value is initially null.");
|
||||
ok(graph._selection.end === null,
|
||||
"The graph's selection end value is initially null.");
|
||||
|
||||
ok(graph._selectionDragger.origin === null,
|
||||
"The graph's dragger origin value is initially null.");
|
||||
ok(graph._selectionDragger.anchor.start === null,
|
||||
"The graph's dragger anchor start value is initially null.");
|
||||
ok(graph._selectionDragger.anchor.end === null,
|
||||
"The graph's dragger anchor end value is initially null.");
|
||||
}
|
45
browser/devtools/shared/test/browser_flame-graph-02.js
Normal file
45
browser/devtools/shared/test/browser_flame-graph-02.js
Normal file
@ -0,0 +1,45 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Tests that flame graph widgets may have a fixed width or height.
|
||||
|
||||
let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
|
||||
let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
|
||||
let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
|
||||
let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
|
||||
|
||||
let test = Task.async(function*() {
|
||||
yield promiseTab("about:blank");
|
||||
yield performTest();
|
||||
gBrowser.removeCurrentTab();
|
||||
finish();
|
||||
});
|
||||
|
||||
function* performTest() {
|
||||
let [host, win, doc] = yield createHost();
|
||||
doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
|
||||
|
||||
let graph = new FlameGraph(doc.body);
|
||||
graph.fixedWidth = 200;
|
||||
graph.fixedHeight = 100;
|
||||
|
||||
yield graph.ready();
|
||||
testGraph(host, graph);
|
||||
|
||||
graph.destroy();
|
||||
host.destroy();
|
||||
}
|
||||
|
||||
function testGraph(host, graph) {
|
||||
let bounds = host.frame.getBoundingClientRect();
|
||||
|
||||
isnot(graph.width, bounds.width * window.devicePixelRatio,
|
||||
"The graph should not span all the parent node's width.");
|
||||
isnot(graph.height, bounds.height * window.devicePixelRatio,
|
||||
"The graph should not span all the parent node's height.");
|
||||
|
||||
is(graph.width, graph.fixedWidth * window.devicePixelRatio,
|
||||
"The graph has the correct width.");
|
||||
is(graph.height, graph.fixedHeight * window.devicePixelRatio,
|
||||
"The graph has the correct height.");
|
||||
}
|
122
browser/devtools/shared/test/browser_flame-graph-03a.js
Normal file
122
browser/devtools/shared/test/browser_flame-graph-03a.js
Normal file
@ -0,0 +1,122 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Tests that selections in the flame graph widget work properly.
|
||||
|
||||
let TEST_DATA = [{ color: "#f00", blocks: [{ x: 0, y: 0, width: 50, height: 20, text: "FOO" }, { x: 50, y: 0, width: 100, height: 20, text: "BAR" }] }, { color: "#00f", blocks: [{ x: 0, y: 30, width: 30, height: 20, text: "BAZ" }] }];
|
||||
let TEST_WIDTH = 200;
|
||||
let TEST_HEIGHT = 100;
|
||||
|
||||
let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
|
||||
let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
|
||||
let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
|
||||
let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
|
||||
|
||||
let test = Task.async(function*() {
|
||||
yield promiseTab("about:blank");
|
||||
yield performTest();
|
||||
gBrowser.removeCurrentTab();
|
||||
finish();
|
||||
});
|
||||
|
||||
function* performTest() {
|
||||
let [host, win, doc] = yield createHost();
|
||||
doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
|
||||
|
||||
let graph = new FlameGraph(doc.body, 1);
|
||||
graph.fixedWidth = TEST_WIDTH;
|
||||
graph.fixedHeight = TEST_HEIGHT;
|
||||
|
||||
yield graph.ready();
|
||||
|
||||
testGraph(graph);
|
||||
|
||||
graph.destroy();
|
||||
host.destroy();
|
||||
}
|
||||
|
||||
function testGraph(graph) {
|
||||
graph.setData(TEST_DATA);
|
||||
|
||||
is(graph.getDataWindowStart(), 0,
|
||||
"The selection start boundary is correct (1).");
|
||||
is(graph.getDataWindowEnd(), TEST_WIDTH,
|
||||
"The selection end boundary is correct (1).");
|
||||
|
||||
scroll(graph, 200, HORIZONTAL_AXIS, 10);
|
||||
is(graph.getDataWindowStart() | 0, 100,
|
||||
"The selection start boundary is correct (2).");
|
||||
is(graph.getDataWindowEnd() | 0, 200,
|
||||
"The selection end boundary is correct (2).");
|
||||
|
||||
scroll(graph, -200, HORIZONTAL_AXIS, 10);
|
||||
is(graph.getDataWindowStart() | 0, 50,
|
||||
"The selection start boundary is correct (3).");
|
||||
is(graph.getDataWindowEnd() | 0, 150,
|
||||
"The selection end boundary is correct (3).");
|
||||
|
||||
scroll(graph, 200, VERTICAL_AXIS, TEST_WIDTH / 2);
|
||||
is(graph.getDataWindowStart() | 0, 46,
|
||||
"The selection start boundary is correct (4).");
|
||||
is(graph.getDataWindowEnd() | 0, 153,
|
||||
"The selection end boundary is correct (4).");
|
||||
|
||||
scroll(graph, -200, VERTICAL_AXIS, TEST_WIDTH / 2);
|
||||
is(graph.getDataWindowStart() | 0, 50,
|
||||
"The selection start boundary is correct (5).");
|
||||
is(graph.getDataWindowEnd() | 0, 149,
|
||||
"The selection end boundary is correct (5).");
|
||||
|
||||
dragStart(graph, TEST_WIDTH / 2);
|
||||
is(graph.getDataWindowStart() | 0, 50,
|
||||
"The selection start boundary is correct (6).");
|
||||
is(graph.getDataWindowEnd() | 0, 149,
|
||||
"The selection end boundary is correct (6).");
|
||||
|
||||
hover(graph, TEST_WIDTH / 2 - 10);
|
||||
is(graph.getDataWindowStart() | 0, 55,
|
||||
"The selection start boundary is correct (7).");
|
||||
is(graph.getDataWindowEnd() | 0, 154,
|
||||
"The selection end boundary is correct (7).");
|
||||
|
||||
dragStop(graph, 10);
|
||||
is(graph.getDataWindowStart() | 0, 95,
|
||||
"The selection start boundary is correct (8).");
|
||||
is(graph.getDataWindowEnd() | 0, 194,
|
||||
"The selection end boundary is correct (8).");
|
||||
}
|
||||
|
||||
// EventUtils just doesn't work!
|
||||
|
||||
function hover(graph, x, y = 1) {
|
||||
x /= window.devicePixelRatio;
|
||||
y /= window.devicePixelRatio;
|
||||
graph._onMouseMove({ clientX: x, clientY: y });
|
||||
}
|
||||
|
||||
function dragStart(graph, x, y = 1) {
|
||||
x /= window.devicePixelRatio;
|
||||
y /= window.devicePixelRatio;
|
||||
graph._onMouseMove({ clientX: x, clientY: y });
|
||||
graph._onMouseDown({ clientX: x, clientY: y });
|
||||
}
|
||||
|
||||
function dragStop(graph, x, y = 1) {
|
||||
x /= window.devicePixelRatio;
|
||||
y /= window.devicePixelRatio;
|
||||
graph._onMouseMove({ clientX: x, clientY: y });
|
||||
graph._onMouseUp({ clientX: x, clientY: y });
|
||||
}
|
||||
|
||||
let HORIZONTAL_AXIS = 1;
|
||||
let VERTICAL_AXIS = 2;
|
||||
|
||||
function scroll(graph, wheel, axis, x, y = 1) {
|
||||
x /= window.devicePixelRatio;
|
||||
y /= window.devicePixelRatio;
|
||||
graph._onMouseMove({ clientX: x, clientY: y });
|
||||
graph._onMouseWheel({ clientX: x, clientY: y, axis, detail: wheel, axis,
|
||||
HORIZONTAL_AXIS,
|
||||
VERTICAL_AXIS
|
||||
});
|
||||
}
|
68
browser/devtools/shared/test/browser_flame-graph-03b.js
Normal file
68
browser/devtools/shared/test/browser_flame-graph-03b.js
Normal file
@ -0,0 +1,68 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Tests that selections in the flame graph widget work properly on HiDPI.
|
||||
|
||||
let TEST_DATA = [{ color: "#f00", blocks: [{ x: 0, y: 0, width: 50, height: 20, text: "FOO" }, { x: 50, y: 0, width: 100, height: 20, text: "BAR" }] }, { color: "#00f", blocks: [{ x: 0, y: 30, width: 30, height: 20, text: "BAZ" }] }];
|
||||
let TEST_WIDTH = 200;
|
||||
let TEST_HEIGHT = 100;
|
||||
let TEST_DPI_DENSITIY = 2;
|
||||
|
||||
let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
|
||||
let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
|
||||
let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
|
||||
let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
|
||||
|
||||
let test = Task.async(function*() {
|
||||
yield promiseTab("about:blank");
|
||||
yield performTest();
|
||||
gBrowser.removeCurrentTab();
|
||||
finish();
|
||||
});
|
||||
|
||||
function* performTest() {
|
||||
let [host, win, doc] = yield createHost();
|
||||
doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
|
||||
|
||||
let graph = new FlameGraph(doc.body, TEST_DPI_DENSITIY);
|
||||
graph.fixedWidth = TEST_WIDTH;
|
||||
graph.fixedHeight = TEST_HEIGHT;
|
||||
|
||||
yield graph.ready();
|
||||
|
||||
testGraph(graph);
|
||||
|
||||
graph.destroy();
|
||||
host.destroy();
|
||||
}
|
||||
|
||||
function testGraph(graph) {
|
||||
graph.setData(TEST_DATA);
|
||||
|
||||
is(graph.getDataWindowStart(), 0,
|
||||
"The selection start boundary is correct on HiDPI (1).");
|
||||
is(graph.getDataWindowEnd(), TEST_WIDTH * TEST_DPI_DENSITIY,
|
||||
"The selection end boundary is correct on HiDPI (1).");
|
||||
|
||||
scroll(graph, 10000, HORIZONTAL_AXIS, 1);
|
||||
|
||||
is(graph.getDataWindowStart(), 380,
|
||||
"The selection start boundary is correct on HiDPI (2).");
|
||||
is(graph.getDataWindowEnd(), TEST_WIDTH * TEST_DPI_DENSITIY,
|
||||
"The selection end boundary is correct on HiDPI (2).");
|
||||
}
|
||||
|
||||
// EventUtils just doesn't work!
|
||||
|
||||
let HORIZONTAL_AXIS = 1;
|
||||
let VERTICAL_AXIS = 2;
|
||||
|
||||
function scroll(graph, wheel, axis, x, y = 1) {
|
||||
x /= window.devicePixelRatio;
|
||||
y /= window.devicePixelRatio;
|
||||
graph._onMouseMove({ clientX: x, clientY: y });
|
||||
graph._onMouseWheel({ clientX: x, clientY: y, axis, detail: wheel, axis,
|
||||
HORIZONTAL_AXIS,
|
||||
VERTICAL_AXIS
|
||||
});
|
||||
}
|
88
browser/devtools/shared/test/browser_flame-graph-04.js
Normal file
88
browser/devtools/shared/test/browser_flame-graph-04.js
Normal file
@ -0,0 +1,88 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Tests that text metrics in the flame graph widget work properly.
|
||||
|
||||
let HTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
let FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE = 9; // px
|
||||
let FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY = "sans-serif";
|
||||
let {ViewHelpers} = Cu.import("resource:///modules/devtools/ViewHelpers.jsm", {});
|
||||
let {FlameGraph} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
|
||||
let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
|
||||
let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
|
||||
let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
|
||||
|
||||
let L10N = new ViewHelpers.L10N();
|
||||
|
||||
let test = Task.async(function*() {
|
||||
yield promiseTab("about:blank");
|
||||
yield performTest();
|
||||
gBrowser.removeCurrentTab();
|
||||
finish();
|
||||
});
|
||||
|
||||
function* performTest() {
|
||||
let [host, win, doc] = yield createHost();
|
||||
let graph = new FlameGraph(doc.body, 1);
|
||||
yield graph.ready();
|
||||
|
||||
testGraph(graph);
|
||||
|
||||
graph.destroy();
|
||||
host.destroy();
|
||||
}
|
||||
|
||||
function testGraph(graph) {
|
||||
is(graph._averageCharWidth, getAverageCharWidth(),
|
||||
"The average char width was calculated correctly.");
|
||||
is(graph._overflowCharWidth, getCharWidth(L10N.ellipsis),
|
||||
"The ellipsis char width was calculated correctly.");
|
||||
|
||||
is(graph._getTextWidthApprox("This text is maybe overflowing"),
|
||||
getAverageCharWidth() * 30,
|
||||
"The approximate width was calculated correctly.");
|
||||
|
||||
is(graph._getFittedText("This text is maybe overflowing", 1000),
|
||||
"This text is maybe overflowing",
|
||||
"The fitted text for 1000px width is correct.");
|
||||
|
||||
isnot(graph._getFittedText("This text is maybe overflowing", 100),
|
||||
"This text is maybe overflowing",
|
||||
"The fitted text for 100px width is correct (1).");
|
||||
|
||||
ok(graph._getFittedText("This text is maybe overflowing", 100)
|
||||
.contains(L10N.ellipsis),
|
||||
"The fitted text for 100px width is correct (2).");
|
||||
|
||||
is(graph._getFittedText("This text is maybe overflowing", 10),
|
||||
L10N.ellipsis,
|
||||
"The fitted text for 10px width is correct.");
|
||||
|
||||
is(graph._getFittedText("This text is maybe overflowing", 1),
|
||||
"",
|
||||
"The fitted text for 1px width is correct.");
|
||||
}
|
||||
|
||||
function getAverageCharWidth() {
|
||||
let letterWidthsSum = 0;
|
||||
let start = 32; // space
|
||||
let end = 123; // "z"
|
||||
|
||||
for (let i = start; i < end; i++) {
|
||||
let char = String.fromCharCode(i);
|
||||
letterWidthsSum += getCharWidth(char);
|
||||
}
|
||||
|
||||
return letterWidthsSum / (end - start);
|
||||
}
|
||||
|
||||
function getCharWidth(char) {
|
||||
let canvas = document.createElementNS(HTML_NS, "canvas");
|
||||
let ctx = canvas.getContext("2d");
|
||||
|
||||
let fontSize = FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE;
|
||||
let fontFamily = FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY;
|
||||
ctx.font = fontSize + "px " + fontFamily;
|
||||
|
||||
return ctx.measureText(char).width;
|
||||
}
|
261
browser/devtools/shared/test/browser_flame-graph-utils.js
Normal file
261
browser/devtools/shared/test/browser_flame-graph-utils.js
Normal file
@ -0,0 +1,261 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Tests that text metrics in the flame graph widget work properly.
|
||||
|
||||
let {FlameGraphUtils} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
|
||||
|
||||
let test = Task.async(function*() {
|
||||
yield promiseTab("about:blank");
|
||||
yield performTest();
|
||||
gBrowser.removeCurrentTab();
|
||||
finish();
|
||||
});
|
||||
|
||||
function* performTest() {
|
||||
let out = FlameGraphUtils.createFlameGraphDataFromSamples(TEST_DATA);
|
||||
|
||||
ok(out, "Some data was outputted properly");
|
||||
is(out.length, 10, "The outputted length is correct.");
|
||||
|
||||
info("Got flame graph data:\n" + out.toSource() + "\n");
|
||||
|
||||
for (let i = 0; i < out.length; i++) {
|
||||
let found = out[i];
|
||||
let expected = EXPECTED_OUTPUT[i];
|
||||
|
||||
is(found.blocks.length, expected.blocks.length,
|
||||
"The correct number of blocks were found in this bucket.");
|
||||
|
||||
for (let j = 0; j < found.blocks.length; j++) {
|
||||
is(found.blocks[j].x, expected.blocks[j].x,
|
||||
"The expected block X position is correct for this frame.");
|
||||
is(found.blocks[j].y, expected.blocks[j].y,
|
||||
"The expected block Y position is correct for this frame.");
|
||||
is(found.blocks[j].width, expected.blocks[j].width,
|
||||
"The expected block width is correct for this frame.");
|
||||
is(found.blocks[j].height, expected.blocks[j].height,
|
||||
"The expected block height is correct for this frame.");
|
||||
is(found.blocks[j].text, expected.blocks[j].text,
|
||||
"The expected block text is correct for this frame.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let TEST_DATA = [{
|
||||
frames: [{
|
||||
location: "M"
|
||||
}, {
|
||||
location: "N",
|
||||
}, {
|
||||
location: "P"
|
||||
}],
|
||||
time: 50,
|
||||
}, {
|
||||
frames: [{
|
||||
location: "A"
|
||||
}, {
|
||||
location: "B",
|
||||
}, {
|
||||
location: "C"
|
||||
}],
|
||||
time: 100,
|
||||
}, {
|
||||
frames: [{
|
||||
location: "A"
|
||||
}, {
|
||||
location: "B",
|
||||
}, {
|
||||
location: "D"
|
||||
}],
|
||||
time: 210,
|
||||
}, {
|
||||
frames: [{
|
||||
location: "A"
|
||||
}, {
|
||||
location: "E",
|
||||
}, {
|
||||
location: "F"
|
||||
}],
|
||||
time: 330,
|
||||
}, {
|
||||
frames: [{
|
||||
location: "A"
|
||||
}, {
|
||||
location: "B",
|
||||
}, {
|
||||
location: "C"
|
||||
}],
|
||||
time: 460,
|
||||
}, {
|
||||
frames: [{
|
||||
location: "X"
|
||||
}, {
|
||||
location: "Y",
|
||||
}, {
|
||||
location: "Z"
|
||||
}],
|
||||
time: 500
|
||||
}];
|
||||
|
||||
let EXPECTED_OUTPUT = [{
|
||||
blocks: []
|
||||
}, {
|
||||
blocks: []
|
||||
}, {
|
||||
blocks: [{
|
||||
srcData: {
|
||||
startTime: 50,
|
||||
rawLocation: "A"
|
||||
},
|
||||
x: 50,
|
||||
y: 0,
|
||||
width: 410,
|
||||
height: 12,
|
||||
text: "A"
|
||||
}]
|
||||
}, {
|
||||
blocks: [{
|
||||
srcData: {
|
||||
startTime: 50,
|
||||
rawLocation: "B"
|
||||
},
|
||||
x: 50,
|
||||
y: 12,
|
||||
width: 160,
|
||||
height: 12,
|
||||
text: "B"
|
||||
}, {
|
||||
srcData: {
|
||||
startTime: 330,
|
||||
rawLocation: "B"
|
||||
},
|
||||
x: 330,
|
||||
y: 12,
|
||||
width: 130,
|
||||
height: 12,
|
||||
text: "B"
|
||||
}]
|
||||
}, {
|
||||
blocks: [{
|
||||
srcData: {
|
||||
startTime: 0,
|
||||
rawLocation: "M"
|
||||
},
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 50,
|
||||
height: 12,
|
||||
text: "M"
|
||||
}, {
|
||||
srcData: {
|
||||
startTime: 50,
|
||||
rawLocation: "C"
|
||||
},
|
||||
x: 50,
|
||||
y: 24,
|
||||
width: 50,
|
||||
height: 12,
|
||||
text: "C"
|
||||
}, {
|
||||
srcData: {
|
||||
startTime: 330,
|
||||
rawLocation: "C"
|
||||
},
|
||||
x: 330,
|
||||
y: 24,
|
||||
width: 130,
|
||||
height: 12,
|
||||
text: "C"
|
||||
}]
|
||||
}, {
|
||||
blocks: [{
|
||||
srcData: {
|
||||
startTime: 0,
|
||||
rawLocation: "N"
|
||||
},
|
||||
x: 0,
|
||||
y: 12,
|
||||
width: 50,
|
||||
height: 12,
|
||||
text: "N"
|
||||
}, {
|
||||
srcData: {
|
||||
startTime: 100,
|
||||
rawLocation: "D"
|
||||
},
|
||||
x: 100,
|
||||
y: 24,
|
||||
width: 110,
|
||||
height: 12,
|
||||
text: "D"
|
||||
}, {
|
||||
srcData: {
|
||||
startTime: 460,
|
||||
rawLocation: "X"
|
||||
},
|
||||
x: 460,
|
||||
y: 0,
|
||||
width: 40,
|
||||
height: 12,
|
||||
text: "X"
|
||||
}]
|
||||
}, {
|
||||
blocks: [{
|
||||
srcData: {
|
||||
startTime: 210,
|
||||
rawLocation: "E"
|
||||
},
|
||||
x: 210,
|
||||
y: 12,
|
||||
width: 120,
|
||||
height: 12,
|
||||
text: "E"
|
||||
}, {
|
||||
srcData: {
|
||||
startTime: 460,
|
||||
rawLocation: "Y"
|
||||
},
|
||||
x: 460,
|
||||
y: 12,
|
||||
width: 40,
|
||||
height: 12,
|
||||
text: "Y"
|
||||
}]
|
||||
}, {
|
||||
blocks: [{
|
||||
srcData: {
|
||||
startTime: 0,
|
||||
rawLocation: "P"
|
||||
},
|
||||
x: 0,
|
||||
y: 24,
|
||||
width: 50,
|
||||
height: 12,
|
||||
text: "P"
|
||||
}, {
|
||||
srcData: {
|
||||
startTime: 210,
|
||||
rawLocation: "F"
|
||||
},
|
||||
x: 210,
|
||||
y: 24,
|
||||
width: 120,
|
||||
height: 12,
|
||||
text: "F"
|
||||
}, {
|
||||
srcData: {
|
||||
startTime: 460,
|
||||
rawLocation: "Z"
|
||||
},
|
||||
x: 460,
|
||||
y: 24,
|
||||
width: 40,
|
||||
height: 12,
|
||||
text: "Z"
|
||||
}]
|
||||
}, {
|
||||
blocks: []
|
||||
}, {
|
||||
blocks: []
|
||||
}];
|
@ -31,8 +31,8 @@ function* performTest() {
|
||||
function* testGraph(graph) {
|
||||
yield graph.setDataWhenReady(TEST_DATA);
|
||||
|
||||
is(graph._gutter.hidden, true,
|
||||
"The gutter should be hidden because the tooltips don't have arrows.");
|
||||
is(graph._gutter.hidden, false,
|
||||
"The gutter should be visible even if the tooltips don't have arrows.");
|
||||
is(graph._maxTooltip.hidden, false,
|
||||
"The max tooltip should not be hidden.");
|
||||
is(graph._avgTooltip.hidden, false,
|
||||
|
@ -40,7 +40,8 @@ function testSidebar() {
|
||||
|
||||
gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
|
||||
let inspector = toolbox.getCurrentPanel();
|
||||
let sidebarTools = ["ruleview", "computedview", "fontinspector", "layoutview"];
|
||||
let sidebarTools = ["ruleview", "computedview", "fontinspector",
|
||||
"layoutview", "animationinspector"];
|
||||
|
||||
// Concatenate the array with itself so that we can open each tool twice.
|
||||
sidebarTools.push.apply(sidebarTools, sidebarTools);
|
||||
|
916
browser/devtools/shared/widgets/FlameGraph.jsm
Normal file
916
browser/devtools/shared/widgets/FlameGraph.jsm
Normal file
@ -0,0 +1,916 @@
|
||||
/* 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 = Components.utils;
|
||||
|
||||
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
|
||||
Cu.import("resource:///modules/devtools/Graphs.jsm");
|
||||
const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
|
||||
const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
|
||||
const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
|
||||
|
||||
this.EXPORTED_SYMBOLS = [
|
||||
"FlameGraph",
|
||||
"FlameGraphUtils"
|
||||
];
|
||||
|
||||
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
const GRAPH_SRC = "chrome://browser/content/devtools/graphs-frame.xhtml";
|
||||
const L10N = new ViewHelpers.L10N();
|
||||
|
||||
const GRAPH_RESIZE_EVENTS_DRAIN = 100; // ms
|
||||
|
||||
const GRAPH_WHEEL_ZOOM_SENSITIVITY = 0.00035;
|
||||
const GRAPH_WHEEL_SCROLL_SENSITIVITY = 0.5;
|
||||
const GRAPH_MIN_SELECTION_WIDTH = 10; // ms
|
||||
|
||||
const TIMELINE_TICKS_MULTIPLE = 5; // ms
|
||||
const TIMELINE_TICKS_SPACING_MIN = 75; // px
|
||||
|
||||
const OVERVIEW_HEADER_HEIGHT = 18; // px
|
||||
const OVERVIEW_HEADER_SAFE_BOUNDS = 50; // px
|
||||
const OVERVIEW_HEADER_TEXT_COLOR = "#18191a";
|
||||
const OVERVIEW_HEADER_TEXT_FONT_SIZE = 9; // px
|
||||
const OVERVIEW_HEADER_TEXT_FONT_FAMILY = "sans-serif";
|
||||
const OVERVIEW_HEADER_TEXT_PADDING_LEFT = 6; // px
|
||||
const OVERVIEW_HEADER_TEXT_PADDING_TOP = 5; // px
|
||||
const OVERVIEW_TIMELINE_STROKES = "#ddd";
|
||||
|
||||
const FLAME_GRAPH_BLOCK_BORDER = 1; // px
|
||||
const FLAME_GRAPH_BLOCK_TEXT_COLOR = "#000";
|
||||
const FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE = 9; // px
|
||||
const FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY = "sans-serif";
|
||||
const FLAME_GRAPH_BLOCK_TEXT_PADDING_TOP = 1; // px
|
||||
const FLAME_GRAPH_BLOCK_TEXT_PADDING_LEFT = 3; // px
|
||||
const FLAME_GRAPH_BLOCK_TEXT_PADDING_RIGHT = 3; // px
|
||||
|
||||
/**
|
||||
* A flamegraph visualization. This implementation is responsable only with
|
||||
* drawing the graph, using a data source consisting of rectangles and
|
||||
* their corresponding widths.
|
||||
*
|
||||
* Example usage:
|
||||
* let graph = new FlameGraph(node);
|
||||
* let src = FlameGraphUtils.createFlameGraphDataFromSamples(samples);
|
||||
* graph.once("ready", () => {
|
||||
* graph.setData(src);
|
||||
* });
|
||||
*
|
||||
* Data source format:
|
||||
* [
|
||||
* {
|
||||
* color: "string",
|
||||
* blocks: [
|
||||
* {
|
||||
* x: number,
|
||||
* y: number,
|
||||
* width: number,
|
||||
* height: number,
|
||||
* text: "string"
|
||||
* },
|
||||
* ...
|
||||
* ]
|
||||
* },
|
||||
* {
|
||||
* color: "string",
|
||||
* blocks: [...]
|
||||
* },
|
||||
* ...
|
||||
* {
|
||||
* color: "string",
|
||||
* blocks: [...]
|
||||
* }
|
||||
* ]
|
||||
*
|
||||
* Use `FlameGraphUtils` to convert profiler data (or any other data source)
|
||||
* into a drawable format.
|
||||
*
|
||||
* @param nsIDOMNode parent
|
||||
* The parent node holding the graph.
|
||||
* @param number sharpness [optional]
|
||||
* Defaults to the current device pixel ratio.
|
||||
*/
|
||||
function FlameGraph(parent, sharpness) {
|
||||
EventEmitter.decorate(this);
|
||||
|
||||
this._parent = parent;
|
||||
this._ready = promise.defer();
|
||||
|
||||
AbstractCanvasGraph.createIframe(GRAPH_SRC, parent, iframe => {
|
||||
this._iframe = iframe;
|
||||
this._window = iframe.contentWindow;
|
||||
this._document = iframe.contentDocument;
|
||||
this._pixelRatio = sharpness || this._window.devicePixelRatio;
|
||||
|
||||
let container = this._container = this._document.getElementById("graph-container");
|
||||
container.className = "flame-graph-widget-container graph-widget-container";
|
||||
|
||||
let canvas = this._canvas = this._document.getElementById("graph-canvas");
|
||||
canvas.className = "flame-graph-widget-canvas graph-widget-canvas";
|
||||
|
||||
let bounds = parent.getBoundingClientRect();
|
||||
bounds.width = this.fixedWidth || bounds.width;
|
||||
bounds.height = this.fixedHeight || bounds.height;
|
||||
iframe.setAttribute("width", bounds.width);
|
||||
iframe.setAttribute("height", bounds.height);
|
||||
|
||||
this._width = canvas.width = bounds.width * this._pixelRatio;
|
||||
this._height = canvas.height = bounds.height * this._pixelRatio;
|
||||
this._ctx = canvas.getContext("2d");
|
||||
|
||||
this._selection = new GraphSelection();
|
||||
this._selectionDragger = new GraphSelectionDragger();
|
||||
|
||||
// Calculating text widths is necessary to trim the text inside the blocks
|
||||
// while the scaling changes (e.g. via scrolling). This is very expensive,
|
||||
// so maintain a cache of string contents to text widths.
|
||||
this._textWidthsCache = {};
|
||||
|
||||
let fontSize = FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE * this._pixelRatio;
|
||||
let fontFamily = FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY;
|
||||
this._ctx.font = fontSize + "px " + fontFamily;
|
||||
this._averageCharWidth = this._calcAverageCharWidth();
|
||||
this._overflowCharWidth = this._getTextWidth(this.overflowChar);
|
||||
|
||||
this._onAnimationFrame = this._onAnimationFrame.bind(this);
|
||||
this._onMouseMove = this._onMouseMove.bind(this);
|
||||
this._onMouseDown = this._onMouseDown.bind(this);
|
||||
this._onMouseUp = this._onMouseUp.bind(this);
|
||||
this._onMouseWheel = this._onMouseWheel.bind(this);
|
||||
this._onResize = this._onResize.bind(this);
|
||||
this.refresh = this.refresh.bind(this);
|
||||
|
||||
container.addEventListener("mousemove", this._onMouseMove);
|
||||
container.addEventListener("mousedown", this._onMouseDown);
|
||||
container.addEventListener("mouseup", this._onMouseUp);
|
||||
container.addEventListener("MozMousePixelScroll", this._onMouseWheel);
|
||||
|
||||
let ownerWindow = this._parent.ownerDocument.defaultView;
|
||||
ownerWindow.addEventListener("resize", this._onResize);
|
||||
|
||||
this._animationId = this._window.requestAnimationFrame(this._onAnimationFrame);
|
||||
|
||||
this._ready.resolve(this);
|
||||
this.emit("ready", this);
|
||||
});
|
||||
}
|
||||
|
||||
FlameGraph.prototype = {
|
||||
/**
|
||||
* Read-only width and height of the canvas.
|
||||
* @return number
|
||||
*/
|
||||
get width() {
|
||||
return this._width;
|
||||
},
|
||||
get height() {
|
||||
return this._height;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a promise resolved once this graph is ready to receive data.
|
||||
*/
|
||||
ready: function() {
|
||||
return this._ready.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Destroys this graph.
|
||||
*/
|
||||
destroy: function() {
|
||||
let container = this._container;
|
||||
container.removeEventListener("mousemove", this._onMouseMove);
|
||||
container.removeEventListener("mousedown", this._onMouseDown);
|
||||
container.removeEventListener("mouseup", this._onMouseUp);
|
||||
container.removeEventListener("MozMousePixelScroll", this._onMouseWheel);
|
||||
|
||||
let ownerWindow = this._parent.ownerDocument.defaultView;
|
||||
ownerWindow.removeEventListener("resize", this._onResize);
|
||||
|
||||
this._window.cancelAnimationFrame(this._animationId);
|
||||
this._iframe.remove();
|
||||
|
||||
this._selection = null;
|
||||
this._selectionDragger = null;
|
||||
|
||||
this._data = null;
|
||||
|
||||
this.emit("destroyed");
|
||||
},
|
||||
|
||||
/**
|
||||
* Rendering options. Subclasses should override these.
|
||||
*/
|
||||
overviewHeaderTextColor: OVERVIEW_HEADER_TEXT_COLOR,
|
||||
overviewTimelineStrokes: OVERVIEW_TIMELINE_STROKES,
|
||||
blockTextColor: FLAME_GRAPH_BLOCK_TEXT_COLOR,
|
||||
|
||||
/**
|
||||
* Makes sure the canvas graph is of the specified width or height, and
|
||||
* doesn't flex to fit all the available space.
|
||||
*/
|
||||
fixedWidth: null,
|
||||
fixedHeight: null,
|
||||
|
||||
/**
|
||||
* The units used in the overhead ticks. Could be "ms", for example.
|
||||
* Overwrite this with your own localized format.
|
||||
*/
|
||||
timelineTickUnits: "",
|
||||
|
||||
/**
|
||||
* Character used when a block's text is overflowing.
|
||||
* Defaults to an ellipsis.
|
||||
*/
|
||||
overflowChar: L10N.ellipsis,
|
||||
|
||||
/**
|
||||
* Sets the data source for this graph.
|
||||
*
|
||||
* @param object data
|
||||
* The data source. See the constructor for more information.
|
||||
*/
|
||||
setData: function(data) {
|
||||
this._data = data;
|
||||
this._selection = { start: 0, end: this._width };
|
||||
this._shouldRedraw = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Same as `setData`, but waits for this graph to finish initializing first.
|
||||
*
|
||||
* @param object data
|
||||
* The data source. See the constructor for more information.
|
||||
* @return promise
|
||||
* A promise resolved once the data is set.
|
||||
*/
|
||||
setDataWhenReady: Task.async(function*(data) {
|
||||
yield this.ready();
|
||||
this.setData(data);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Gets whether or not this graph has a data source.
|
||||
* @return boolean
|
||||
*/
|
||||
hasData: function() {
|
||||
return !!this._data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the start or end of this graph's selection, i.e. the 'data window'.
|
||||
* @return number
|
||||
*/
|
||||
getDataWindowStart: function() {
|
||||
return this._selection.start;
|
||||
},
|
||||
getDataWindowEnd: function() {
|
||||
return this._selection.end;
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates this graph to reflect the new dimensions of the parent node.
|
||||
*/
|
||||
refresh: function() {
|
||||
let bounds = this._parent.getBoundingClientRect();
|
||||
let newWidth = this.fixedWidth || bounds.width;
|
||||
let newHeight = this.fixedHeight || bounds.height;
|
||||
|
||||
// Prevent redrawing everything if the graph's width & height won't change.
|
||||
if (this._width == newWidth * this._pixelRatio &&
|
||||
this._height == newHeight * this._pixelRatio) {
|
||||
this.emit("refresh-cancelled");
|
||||
return;
|
||||
}
|
||||
|
||||
bounds.width = newWidth;
|
||||
bounds.height = newHeight;
|
||||
this._iframe.setAttribute("width", bounds.width);
|
||||
this._iframe.setAttribute("height", bounds.height);
|
||||
this._width = this._canvas.width = bounds.width * this._pixelRatio;
|
||||
this._height = this._canvas.height = bounds.height * this._pixelRatio;
|
||||
|
||||
this._shouldRedraw = true;
|
||||
this.emit("refresh");
|
||||
},
|
||||
|
||||
/**
|
||||
* The contents of this graph are redrawn only when something changed,
|
||||
* like the data source, or the selection bounds etc. This flag tracks
|
||||
* if the rendering is "dirty" and needs to be refreshed.
|
||||
*/
|
||||
_shouldRedraw: false,
|
||||
|
||||
/**
|
||||
* Animation frame callback, invoked on each tick of the refresh driver.
|
||||
*/
|
||||
_onAnimationFrame: function() {
|
||||
this._animationId = this._window.requestAnimationFrame(this._onAnimationFrame);
|
||||
this._drawWidget();
|
||||
},
|
||||
|
||||
/**
|
||||
* Redraws the widget when necessary. The actual graph is not refreshed
|
||||
* every time this function is called, only the cliphead, selection etc.
|
||||
*/
|
||||
_drawWidget: function() {
|
||||
if (!this._shouldRedraw) {
|
||||
return;
|
||||
}
|
||||
let ctx = this._ctx;
|
||||
let canvasWidth = this._width;
|
||||
let canvasHeight = this._height;
|
||||
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
|
||||
|
||||
let selection = this._selection;
|
||||
let selectionWidth = selection.end - selection.start;
|
||||
let selectionScale = canvasWidth / selectionWidth;
|
||||
this._drawTicks(selection.start, selectionScale);
|
||||
this._drawPyramid(this._data, selection.start, selectionScale);
|
||||
|
||||
this._shouldRedraw = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Draws the overhead ticks in this graph.
|
||||
*
|
||||
* @param number dataOffset, dataScale
|
||||
* Offsets and scales the data source by the specified amount.
|
||||
* This is used for scrolling the visualization.
|
||||
*/
|
||||
_drawTicks: function(dataOffset, dataScale) {
|
||||
let ctx = this._ctx;
|
||||
let canvasWidth = this._width;
|
||||
let canvasHeight = this._height;
|
||||
let scaledOffset = dataOffset * dataScale;
|
||||
|
||||
let safeBounds = OVERVIEW_HEADER_SAFE_BOUNDS * this._pixelRatio;
|
||||
let availableWidth = canvasWidth - safeBounds;
|
||||
|
||||
let fontSize = OVERVIEW_HEADER_TEXT_FONT_SIZE * this._pixelRatio;
|
||||
let fontFamily = OVERVIEW_HEADER_TEXT_FONT_FAMILY;
|
||||
let textPaddingLeft = OVERVIEW_HEADER_TEXT_PADDING_LEFT * this._pixelRatio;
|
||||
let textPaddingTop = OVERVIEW_HEADER_TEXT_PADDING_TOP * this._pixelRatio;
|
||||
let tickInterval = this._findOptimalTickInterval(dataScale);
|
||||
|
||||
ctx.textBaseline = "top";
|
||||
ctx.font = fontSize + "px " + fontFamily;
|
||||
ctx.fillStyle = this.overviewHeaderTextColor;
|
||||
ctx.strokeStyle = this.overviewTimelineStrokes;
|
||||
ctx.beginPath();
|
||||
|
||||
for (let x = 0; x < availableWidth + scaledOffset; x += tickInterval) {
|
||||
let lineLeft = x - scaledOffset;
|
||||
let textLeft = lineLeft + textPaddingLeft;
|
||||
let time = Math.round(x / dataScale / this._pixelRatio);
|
||||
let label = time + " " + this.timelineTickUnits;
|
||||
ctx.fillText(label, textLeft, textPaddingTop);
|
||||
ctx.moveTo(lineLeft, 0);
|
||||
ctx.lineTo(lineLeft, canvasHeight);
|
||||
}
|
||||
|
||||
ctx.stroke();
|
||||
},
|
||||
|
||||
/**
|
||||
* Draws the blocks and text in this graph.
|
||||
*
|
||||
* @param object dataSource
|
||||
* The data source. See the constructor for more information.
|
||||
* @param number dataOffset, dataScale
|
||||
* Offsets and scales the data source by the specified amount.
|
||||
* This is used for scrolling the visualization.
|
||||
*/
|
||||
_drawPyramid: function(dataSource, dataOffset, dataScale) {
|
||||
let ctx = this._ctx;
|
||||
|
||||
let fontSize = FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE * this._pixelRatio;
|
||||
let fontFamily = FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY;
|
||||
let visibleBlocks = this._drawPyramidFill(dataSource, dataOffset, dataScale);
|
||||
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.font = fontSize + "px " + fontFamily;
|
||||
ctx.fillStyle = this.blockTextColor;
|
||||
|
||||
this._drawPyramidText(visibleBlocks, dataOffset, dataScale);
|
||||
},
|
||||
|
||||
/**
|
||||
* Fills all block inside this graph's pyramid.
|
||||
* @see FlameGraph.prototype._drawPyramid
|
||||
*/
|
||||
_drawPyramidFill: function(dataSource, dataOffset, dataScale) {
|
||||
let visibleBlocksStore = [];
|
||||
let minVisibleBlockWidth = this._overflowCharWidth;
|
||||
|
||||
for (let { color, blocks } of dataSource) {
|
||||
this._drawBlocksFill(
|
||||
color, blocks, dataOffset, dataScale,
|
||||
visibleBlocksStore, minVisibleBlockWidth);
|
||||
}
|
||||
|
||||
return visibleBlocksStore;
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds the text for all block inside this graph's pyramid.
|
||||
* @see FlameGraph.prototype._drawPyramid
|
||||
*/
|
||||
_drawPyramidText: function(blocks, dataOffset, dataScale) {
|
||||
for (let block of blocks) {
|
||||
this._drawBlockText(block, dataOffset, dataScale);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Fills a group of blocks sharing the same style.
|
||||
*
|
||||
* @param string color
|
||||
* The color used as the block's background.
|
||||
* @param array blocks
|
||||
* A list of { x, y, width, height } objects visually representing
|
||||
* all the blocks sharing this particular style.
|
||||
* @param number dataOffset, dataScale
|
||||
* Offsets and scales the data source by the specified amount.
|
||||
* This is used for scrolling the visualization.
|
||||
* @param array visibleBlocksStore
|
||||
* An array to store all the visible blocks into, after drawing them.
|
||||
* The provided array will be populated.
|
||||
* @param number minVisibleBlockWidth
|
||||
* The minimum width of the blocks that will be added into
|
||||
* the `visibleBlocksStore`.
|
||||
*/
|
||||
_drawBlocksFill: function(
|
||||
color, blocks, dataOffset, dataScale,
|
||||
visibleBlocksStore, minVisibleBlockWidth)
|
||||
{
|
||||
let ctx = this._ctx;
|
||||
let canvasWidth = this._width;
|
||||
let canvasHeight = this._height;
|
||||
let scaledOffset = dataOffset * dataScale;
|
||||
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
|
||||
for (let block of blocks) {
|
||||
let { x, y, width, height } = block;
|
||||
let rectLeft = x * this._pixelRatio * dataScale - scaledOffset;
|
||||
let rectTop = (y + OVERVIEW_HEADER_HEIGHT) * this._pixelRatio;
|
||||
let rectWidth = width * this._pixelRatio * dataScale;
|
||||
let rectHeight = height * this._pixelRatio;
|
||||
|
||||
if (rectLeft > canvasWidth || // Too far right.
|
||||
rectLeft < -rectWidth || // Too far left.
|
||||
rectTop > canvasHeight) { // Too far bottom.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Clamp the blocks position to start at 0. Avoid negative X coords,
|
||||
// to properly place the text inside the blocks.
|
||||
if (rectLeft < 0) {
|
||||
rectWidth += rectLeft;
|
||||
rectLeft = 0;
|
||||
}
|
||||
|
||||
// Avoid drawing blocks that are too narrow.
|
||||
if (rectWidth <= FLAME_GRAPH_BLOCK_BORDER ||
|
||||
rectHeight <= FLAME_GRAPH_BLOCK_BORDER) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ctx.rect(
|
||||
rectLeft, rectTop,
|
||||
rectWidth - FLAME_GRAPH_BLOCK_BORDER,
|
||||
rectHeight - FLAME_GRAPH_BLOCK_BORDER);
|
||||
|
||||
// Populate the visible blocks store with this block if the width
|
||||
// is longer than a given threshold.
|
||||
if (rectWidth > minVisibleBlockWidth) {
|
||||
visibleBlocksStore.push(block);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.fill();
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds text for a single block.
|
||||
*
|
||||
* @param object block
|
||||
* A single { x, y, width, height, text } object visually representing
|
||||
* the block containing the text.
|
||||
* @param number dataOffset, dataScale
|
||||
* Offsets and scales the data source by the specified amount.
|
||||
* This is used for scrolling the visualization.
|
||||
*/
|
||||
_drawBlockText: function(block, dataOffset, dataScale) {
|
||||
let ctx = this._ctx;
|
||||
let scaledOffset = dataOffset * dataScale;
|
||||
|
||||
let { x, y, width, height, text } = block;
|
||||
|
||||
let paddingTop = FLAME_GRAPH_BLOCK_TEXT_PADDING_TOP * this._pixelRatio;
|
||||
let paddingLeft = FLAME_GRAPH_BLOCK_TEXT_PADDING_LEFT * this._pixelRatio;
|
||||
let paddingRight = FLAME_GRAPH_BLOCK_TEXT_PADDING_RIGHT * this._pixelRatio;
|
||||
let totalHorizontalPadding = paddingLeft + paddingRight;
|
||||
|
||||
let rectLeft = x * this._pixelRatio * dataScale - scaledOffset;
|
||||
let rectWidth = width * this._pixelRatio * dataScale;
|
||||
|
||||
// Clamp the blocks position to start at 0. Avoid negative X coords,
|
||||
// to properly place the text inside the blocks.
|
||||
if (rectLeft < 0) {
|
||||
rectWidth += rectLeft;
|
||||
rectLeft = 0;
|
||||
}
|
||||
|
||||
let textLeft = rectLeft + paddingLeft;
|
||||
let textTop = (y + height / 2 + OVERVIEW_HEADER_HEIGHT) * this._pixelRatio + paddingTop;
|
||||
let textAvailableWidth = rectWidth - totalHorizontalPadding;
|
||||
|
||||
// Massage the text to fit inside a given width. This clamps the string
|
||||
// at the end to avoid overflowing.
|
||||
let fittedText = this._getFittedText(text, textAvailableWidth);
|
||||
if (fittedText.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.fillText(fittedText, textLeft, textTop);
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculating text widths is necessary to trim the text inside the blocks
|
||||
* while the scaling changes (e.g. via scrolling). This is very expensive,
|
||||
* so maintain a cache of string contents to text widths.
|
||||
*/
|
||||
_textWidthsCache: null,
|
||||
_overflowCharWidth: null,
|
||||
_averageCharWidth: null,
|
||||
|
||||
/**
|
||||
* Gets the width of the specified text, for the current context state
|
||||
* (font size, family etc.).
|
||||
*
|
||||
* @param string text
|
||||
* The text to analyze.
|
||||
* @return number
|
||||
* The text width.
|
||||
*/
|
||||
_getTextWidth: function(text) {
|
||||
let cachedWidth = this._textWidthsCache[text];
|
||||
if (cachedWidth) {
|
||||
return cachedWidth;
|
||||
}
|
||||
let metrics = this._ctx.measureText(text);
|
||||
return (this._textWidthsCache[text] = metrics.width);
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets an approximate width of the specified text. This is much faster
|
||||
* than `_getTextWidth`, but inexact.
|
||||
*
|
||||
* @param string text
|
||||
* The text to analyze.
|
||||
* @return number
|
||||
* The approximate text width.
|
||||
*/
|
||||
_getTextWidthApprox: function(text) {
|
||||
return text.length * this._averageCharWidth;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the average letter width in the English alphabet, for the current
|
||||
* context state (font size, family etc.). This provides a close enough
|
||||
* value to use in `_getTextWidthApprox`.
|
||||
*
|
||||
* @return number
|
||||
* The average letter width.
|
||||
*/
|
||||
_calcAverageCharWidth: function() {
|
||||
let letterWidthsSum = 0;
|
||||
let start = 32; // space
|
||||
let end = 123; // "z"
|
||||
|
||||
for (let i = start; i < end; i++) {
|
||||
let char = String.fromCharCode(i);
|
||||
letterWidthsSum += this._getTextWidth(char);
|
||||
}
|
||||
|
||||
return letterWidthsSum / (end - start);
|
||||
},
|
||||
|
||||
/**
|
||||
* Massage a text to fit inside a given width. This clamps the string
|
||||
* at the end to avoid overflowing.
|
||||
*
|
||||
* @param string text
|
||||
* The text to fit inside the given width.
|
||||
* @param number maxWidth
|
||||
* The available width for the given text.
|
||||
* @return string
|
||||
* The fitted text.
|
||||
*/
|
||||
_getFittedText: function(text, maxWidth) {
|
||||
let textWidth = this._getTextWidth(text);
|
||||
if (textWidth < maxWidth) {
|
||||
return text;
|
||||
}
|
||||
if (this._overflowCharWidth > maxWidth) {
|
||||
return "";
|
||||
}
|
||||
for (let i = 1, len = text.length; i <= len; i++) {
|
||||
let trimmedText = text.substring(0, len - i);
|
||||
let trimmedWidth = this._getTextWidthApprox(trimmedText) + this._overflowCharWidth;
|
||||
if (trimmedWidth < maxWidth) {
|
||||
return trimmedText + this.overflowChar;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
},
|
||||
|
||||
/**
|
||||
* Listener for the "mousemove" event on the graph's container.
|
||||
*/
|
||||
_onMouseMove: function(e) {
|
||||
let offset = this._getContainerOffset();
|
||||
let mouseX = (e.clientX - offset.left) * this._pixelRatio;
|
||||
|
||||
let canvasWidth = this._width;
|
||||
let canvasHeight = this._height;
|
||||
|
||||
let selection = this._selection;
|
||||
let selectionWidth = selection.end - selection.start;
|
||||
let selectionScale = canvasWidth / selectionWidth;
|
||||
|
||||
let dragger = this._selectionDragger;
|
||||
if (dragger.origin != null) {
|
||||
selection.start = dragger.anchor.start + (dragger.origin - mouseX) / selectionScale;
|
||||
selection.end = dragger.anchor.end + (dragger.origin - mouseX) / selectionScale;
|
||||
this._normalizeSelectionBounds();
|
||||
this._shouldRedraw = true;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Listener for the "mousedown" event on the graph's container.
|
||||
*/
|
||||
_onMouseDown: function(e) {
|
||||
let offset = this._getContainerOffset();
|
||||
let mouseX = (e.clientX - offset.left) * this._pixelRatio;
|
||||
|
||||
this._selectionDragger.origin = mouseX;
|
||||
this._selectionDragger.anchor.start = this._selection.start;
|
||||
this._selectionDragger.anchor.end = this._selection.end;
|
||||
this._canvas.setAttribute("input", "adjusting-selection-boundary");
|
||||
},
|
||||
|
||||
/**
|
||||
* Listener for the "mouseup" event on the graph's container.
|
||||
*/
|
||||
_onMouseUp: function() {
|
||||
this._selectionDragger.origin = null;
|
||||
this._canvas.removeAttribute("input");
|
||||
},
|
||||
|
||||
/**
|
||||
* Listener for the "wheel" event on the graph's container.
|
||||
*/
|
||||
_onMouseWheel: function(e) {
|
||||
let offset = this._getContainerOffset();
|
||||
let mouseX = (e.clientX - offset.left) * this._pixelRatio;
|
||||
|
||||
let canvasWidth = this._width;
|
||||
let canvasHeight = this._height;
|
||||
|
||||
let selection = this._selection;
|
||||
let selectionWidth = selection.end - selection.start;
|
||||
let selectionScale = canvasWidth / selectionWidth;
|
||||
|
||||
switch (e.axis) {
|
||||
case e.VERTICAL_AXIS: {
|
||||
let distFromStart = mouseX;
|
||||
let distFromEnd = canvasWidth - mouseX;
|
||||
let vector = e.detail * GRAPH_WHEEL_ZOOM_SENSITIVITY / selectionScale;
|
||||
selection.start -= distFromStart * vector;
|
||||
selection.end += distFromEnd * vector;
|
||||
break;
|
||||
}
|
||||
case e.HORIZONTAL_AXIS: {
|
||||
let vector = e.detail * GRAPH_WHEEL_SCROLL_SENSITIVITY / selectionScale;
|
||||
selection.start += vector;
|
||||
selection.end += vector;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this._normalizeSelectionBounds();
|
||||
this._shouldRedraw = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Makes sure the start and end points of the current selection
|
||||
* are withing the graph's visible bounds, and that they form a selection
|
||||
* wider than the allowed minimum width.
|
||||
*/
|
||||
_normalizeSelectionBounds: function() {
|
||||
let canvasWidth = this._width;
|
||||
let canvasHeight = this._height;
|
||||
|
||||
let { start, end } = this._selection;
|
||||
let minSelectionWidth = GRAPH_MIN_SELECTION_WIDTH * this._pixelRatio;
|
||||
|
||||
if (start < 0) {
|
||||
start = 0;
|
||||
}
|
||||
if (end < 0) {
|
||||
start = 0;
|
||||
end = minSelectionWidth;
|
||||
}
|
||||
if (end > canvasWidth) {
|
||||
end = canvasWidth;
|
||||
}
|
||||
if (start > canvasWidth) {
|
||||
end = canvasWidth;
|
||||
start = canvasWidth - minSelectionWidth;
|
||||
}
|
||||
if (end - start < minSelectionWidth) {
|
||||
let midPoint = (start + end) / 2;
|
||||
start = midPoint - minSelectionWidth / 2;
|
||||
end = midPoint + minSelectionWidth / 2;
|
||||
}
|
||||
|
||||
this._selection.start = start;
|
||||
this._selection.end = end;
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* Finds the optimal tick interval between time markers in this graph.
|
||||
*
|
||||
* @param number dataScale
|
||||
* @return number
|
||||
*/
|
||||
_findOptimalTickInterval: function(dataScale) {
|
||||
let timingStep = TIMELINE_TICKS_MULTIPLE;
|
||||
let spacingMin = TIMELINE_TICKS_SPACING_MIN * this._pixelRatio;
|
||||
|
||||
if (dataScale > spacingMin) {
|
||||
return dataScale;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
let scaledStep = dataScale * timingStep;
|
||||
if (scaledStep < spacingMin) {
|
||||
timingStep <<= 1;
|
||||
continue;
|
||||
}
|
||||
return scaledStep;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the offset of this graph's container relative to the owner window.
|
||||
*
|
||||
* @return object
|
||||
* The { left, top } offset.
|
||||
*/
|
||||
_getContainerOffset: function() {
|
||||
let node = this._canvas;
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
|
||||
while ((node = node.offsetParent)) {
|
||||
x += node.offsetLeft;
|
||||
y += node.offsetTop;
|
||||
}
|
||||
|
||||
return { left: x, top: y };
|
||||
},
|
||||
|
||||
/**
|
||||
* Listener for the "resize" event on the graph's parent node.
|
||||
*/
|
||||
_onResize: function() {
|
||||
if (this.hasData()) {
|
||||
setNamedTimeout(this._uid, GRAPH_RESIZE_EVENTS_DRAIN, this.refresh);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const FLAME_GRAPH_BLOCK_HEIGHT = 12; // px
|
||||
|
||||
const PALLETTE_SIZE = 10;
|
||||
const PALLETTE_HUE_OFFSET = Math.random() * 90;
|
||||
const PALLETTE_HUE_RANGE = 270;
|
||||
const PALLETTE_SATURATION = 60;
|
||||
const PALLETTE_BRIGHTNESS = 75;
|
||||
const PALLETTE_OPACITY = 0.7;
|
||||
|
||||
const COLOR_PALLETTE = Array.from(Array(PALLETTE_SIZE)).map((_, i) => "hsla" +
|
||||
"(" + ((PALLETTE_HUE_OFFSET + (i / PALLETTE_SIZE * PALLETTE_HUE_RANGE))|0 % 360) +
|
||||
"," + PALLETTE_SATURATION + "%" +
|
||||
"," + PALLETTE_BRIGHTNESS + "%" +
|
||||
"," + PALLETTE_OPACITY +
|
||||
")"
|
||||
);
|
||||
|
||||
/**
|
||||
* A collection of utility functions converting various data sources
|
||||
* into a format drawable by the FlameGraph.
|
||||
*/
|
||||
let FlameGraphUtils = {
|
||||
/**
|
||||
* Converts a list of samples from the profiler data to something that's
|
||||
* drawable by a FlameGraph widget.
|
||||
*
|
||||
* @param array samples
|
||||
* A list of { time, frames: [{ location }] } objects.
|
||||
* @param array out [optional]
|
||||
* An output storage to reuse for storing the flame graph data.
|
||||
* @return array
|
||||
* The flame graph data.
|
||||
*/
|
||||
createFlameGraphDataFromSamples: function(samples, out = []) {
|
||||
// 1. Create a map of colors to arrays, representing buckets of
|
||||
// blocks inside the flame graph pyramid sharing the same style.
|
||||
|
||||
let buckets = new Map();
|
||||
|
||||
for (let color of COLOR_PALLETTE) {
|
||||
buckets.set(color, []);
|
||||
}
|
||||
|
||||
// 2. Populate the buckets by iterating over every frame in every sample.
|
||||
|
||||
let prevTime = 0;
|
||||
let prevFrames = [];
|
||||
|
||||
for (let { frames, time } of samples) {
|
||||
let frameIndex = 0;
|
||||
|
||||
for (let { location } of frames) {
|
||||
let prevFrame = prevFrames[frameIndex];
|
||||
|
||||
// Frames at the same location and the same depth will be reused.
|
||||
// If there is a block already created, change its width.
|
||||
if (prevFrame && prevFrame.srcData.rawLocation == location) {
|
||||
prevFrame.width = (time - prevFrame.srcData.startTime);
|
||||
}
|
||||
// Otherwise, create a new block for this frame at this depth,
|
||||
// using a simple location based salt for picking a color.
|
||||
else {
|
||||
let hash = this._getStringHash(location);
|
||||
let color = COLOR_PALLETTE[hash % PALLETTE_SIZE];
|
||||
let bucket = buckets.get(color);
|
||||
|
||||
bucket.push(prevFrames[frameIndex] = {
|
||||
srcData: { startTime: prevTime, rawLocation: location },
|
||||
x: prevTime,
|
||||
y: frameIndex * FLAME_GRAPH_BLOCK_HEIGHT,
|
||||
width: time - prevTime,
|
||||
height: FLAME_GRAPH_BLOCK_HEIGHT,
|
||||
text: location
|
||||
});
|
||||
}
|
||||
|
||||
frameIndex++;
|
||||
}
|
||||
|
||||
// Previous frames at stack depths greater than the current sample's
|
||||
// maximum need to be nullified. It's nonsensical to reuse them.
|
||||
prevFrames.length = frameIndex;
|
||||
prevTime = time;
|
||||
}
|
||||
|
||||
// 3. Convert the buckets into a data source usable by the FlameGraph.
|
||||
// This is a simple conversion from a Map to an Array.
|
||||
|
||||
for (let [color, blocks] of buckets) {
|
||||
out.push({ color, blocks });
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
/**
|
||||
* Very dumb hashing of a string. Used to pick colors from a pallette.
|
||||
*
|
||||
* @param string input
|
||||
* @return number
|
||||
*/
|
||||
_getStringHash: function(input) {
|
||||
const STRING_HASH_PRIME1 = 7;
|
||||
const STRING_HASH_PRIME2 = 31;
|
||||
|
||||
let hash = STRING_HASH_PRIME1;
|
||||
|
||||
for (let i = 0, len = input.length; i < len; i++) {
|
||||
hash *= STRING_HASH_PRIME2;
|
||||
hash += input.charCodeAt(i);
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
};
|
@ -7,10 +7,14 @@ const Cu = Components.utils;
|
||||
|
||||
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
|
||||
const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
|
||||
const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
|
||||
const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
|
||||
const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
|
||||
|
||||
this.EXPORTED_SYMBOLS = [
|
||||
"GraphCursor",
|
||||
"GraphSelection",
|
||||
"GraphSelectionDragger",
|
||||
"GraphSelectionResizer",
|
||||
"AbstractCanvasGraph",
|
||||
"LineGraphWidget",
|
||||
"BarGraphWidget",
|
||||
@ -93,28 +97,23 @@ const BAR_GRAPH_LEGEND_MOUSEOVER_DEBOUNCE = 50; // ms
|
||||
/**
|
||||
* Small data primitives for all graphs.
|
||||
*/
|
||||
this.GraphCursor = function() {};
|
||||
this.GraphSelection = function() {};
|
||||
this.GraphSelectionDragger = function() {};
|
||||
this.GraphSelectionResizer = function() {};
|
||||
|
||||
GraphCursor.prototype = {
|
||||
x: null,
|
||||
y: null
|
||||
this.GraphCursor = function() {
|
||||
this.x = null;
|
||||
this.y = null;
|
||||
};
|
||||
|
||||
GraphSelection.prototype = {
|
||||
start: null,
|
||||
end: null
|
||||
this.GraphSelection = function() {
|
||||
this.start = null;
|
||||
this.end = null;
|
||||
};
|
||||
|
||||
GraphSelectionDragger.prototype = {
|
||||
origin: null,
|
||||
anchor: new GraphSelection()
|
||||
this.GraphSelectionDragger = function() {
|
||||
this.origin = null;
|
||||
this.anchor = new GraphSelection();
|
||||
};
|
||||
|
||||
GraphSelectionResizer.prototype = {
|
||||
margin: null
|
||||
this.GraphSelectionResizer = function() {
|
||||
this.margin = null;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -245,6 +244,11 @@ AbstractCanvasGraph.prototype = {
|
||||
this._window.cancelAnimationFrame(this._animationId);
|
||||
this._iframe.remove();
|
||||
|
||||
this._cursor = null;
|
||||
this._selection = null;
|
||||
this._selectionDragger = null;
|
||||
this._selectionResizer = null;
|
||||
|
||||
this._data = null;
|
||||
this._mask = null;
|
||||
this._maskArgs = null;
|
||||
@ -892,6 +896,9 @@ AbstractCanvasGraph.prototype = {
|
||||
|
||||
/**
|
||||
* Gets the offset of this graph's container relative to the owner window.
|
||||
*
|
||||
* @return object
|
||||
* The { left, top } offset.
|
||||
*/
|
||||
_getContainerOffset: function() {
|
||||
let node = this._canvas;
|
||||
@ -1449,7 +1456,7 @@ LineGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
|
||||
this._maxTooltip.hidden = this._showMax === false || !totalTicks || distanceMinMax < LINE_GRAPH_MIN_MAX_TOOLTIP_DISTANCE;
|
||||
this._avgTooltip.hidden = this._showAvg === false || !totalTicks;
|
||||
this._minTooltip.hidden = this._showMin === false || !totalTicks;
|
||||
this._gutter.hidden = (this._showMin === false && this._showMax === false) || !totalTicks || !this.withTooltipArrows;
|
||||
this._gutter.hidden = (this._showMin === false && this._showAvg === false && this._showMax === false) || !totalTicks;
|
||||
|
||||
this._maxGutterLine.hidden = this._showMax === false;
|
||||
this._avgGutterLine.hidden = this._showAvg === false;
|
||||
|
@ -21,9 +21,8 @@ loader.lazyRequireGetter(this, "L10N",
|
||||
"devtools/timeline/global", true);
|
||||
|
||||
const OVERVIEW_HEADER_HEIGHT = 14; // px
|
||||
const OVERVIEW_ROW_HEIGHT = 11; // row height
|
||||
const OVERVIEW_ROW_HEIGHT = 11; // px
|
||||
|
||||
const OVERVIEW_BODY_HEIGHT = 55; // 11px * 5 groups
|
||||
const OVERVIEW_SELECTION_LINE_COLOR = "#666";
|
||||
const OVERVIEW_CLIPHEAD_LINE_COLOR = "#555";
|
||||
|
||||
|
@ -377,7 +377,6 @@
|
||||
@RESPATH@/browser/components/nsSetDefaultBrowser.js
|
||||
@RESPATH@/browser/components/BrowserDownloads.manifest
|
||||
@RESPATH@/browser/components/DownloadsStartup.js
|
||||
@RESPATH@/browser/components/DownloadsUI.js
|
||||
@RESPATH@/browser/components/BrowserPlaces.manifest
|
||||
@RESPATH@/browser/components/devtools-clhandler.manifest
|
||||
@RESPATH@/browser/components/devtools-clhandler.js
|
||||
@ -413,8 +412,6 @@
|
||||
#endif
|
||||
@RESPATH@/components/nsHelperAppDlg.manifest
|
||||
@RESPATH@/components/nsHelperAppDlg.js
|
||||
@RESPATH@/components/nsDownloadManagerUI.manifest
|
||||
@RESPATH@/components/nsDownloadManagerUI.js
|
||||
@RESPATH@/components/NetworkGeolocationProvider.manifest
|
||||
@RESPATH@/components/NetworkGeolocationProvider.js
|
||||
@RESPATH@/browser/components/nsSidebar.manifest
|
||||
|
@ -422,6 +422,11 @@ These should match what Safari and other Apple applications use on OS X Lion. --
|
||||
search providers: "Search for <used typed keywords> with:" -->
|
||||
<!ENTITY searchFor.label "Search for ">
|
||||
<!ENTITY searchWith.label " with:">
|
||||
<!-- LOCALIZATION NOTE (searchWithHeader.label):
|
||||
The wording of this string should be as close as possible to
|
||||
searchFor.label and searchWith.label. This string will be used instead of
|
||||
them when the user has not typed any keyword. -->
|
||||
<!ENTITY searchWithHeader.label "Search with:">
|
||||
<!ENTITY changeSearchSettings.button "Change Search Settings">
|
||||
|
||||
<!ENTITY tabView.commandkey "e">
|
||||
|
@ -0,0 +1,24 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<!-- LOCALIZATION NOTE : FILE This file contains the Animations panel strings.
|
||||
- The Animations panel is part of the Inspector sidebar -->
|
||||
|
||||
<!-- LOCALIZATION NOTE : FILE The correct localization of this file might be to
|
||||
- keep it in English, or another language commonly spoken among web developers.
|
||||
- You want to make that choice consistent across the developer tools.
|
||||
- A good criteria is the language in which you'd find the best
|
||||
- documentation on web development on the web. -->
|
||||
|
||||
<!-- LOCALIZATION NOTE (title): This is the label shown in the sidebar tab -->
|
||||
<!ENTITY title "Animations">
|
||||
|
||||
<!-- LOCALIZATION NOTE (invalidElement): This is the label shown in the panel
|
||||
- when an invalid node is currently selected in the inspector. -->
|
||||
<!ENTITY invalidElement "No animations were found for the current element.">
|
||||
|
||||
<!-- LOCALIZATION NOTE (selectElement): This is the label shown in the panel
|
||||
- when an invalid node is currently selected in the inspector, to invite the
|
||||
- user to select a new node by clicking on the element-picker icon. -->
|
||||
<!ENTITY selectElement "Pick another element from the page.">
|
@ -0,0 +1,43 @@
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
# LOCALIZATION NOTE These strings are used inside the Animation inspector
|
||||
# which is available as a sidebar panel in the Inspector.
|
||||
# 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 (player.animationNameLabel):
|
||||
# This string is displayed in each animation player widget. It is the label
|
||||
# displayed before the animation name.
|
||||
player.animationNameLabel=Animation:
|
||||
|
||||
# LOCALIZATION NOTE (player.transitionNameLabel):
|
||||
# This string is displayed in each animation player widget. It is the label
|
||||
# displayed in the header, when the element is animated by mean of a css
|
||||
# transition
|
||||
player.transitionNameLabel=Transition
|
||||
|
||||
# LOCALIZATION NOTE (player.animationDurationLabel):
|
||||
# This string is displayed in each animation player widget. It is the label
|
||||
# displayed before the animation duration.
|
||||
player.animationDurationLabel=Duration:
|
||||
|
||||
# LOCALIZATION NOTE (player.animationIterationCountLabel):
|
||||
# This string is displayed in each animation player widget. It is the label
|
||||
# displayed before the number of times the animation is set to repeat.
|
||||
player.animationIterationCountLabel=Repeats:
|
||||
|
||||
# LOCALIZATION NOTE (player.infiniteIterationCount):
|
||||
# In case the animation repeats infinitely, this string is displayed next to the
|
||||
# player.animationIterationCountLabel string, instead of a number.
|
||||
player.infiniteIterationCount=∞
|
||||
|
||||
# LOCALIZATION NOTE (player.timeLabel):
|
||||
# This string is displayed in each animation player widget, to indicate either
|
||||
# how long (in seconds) the animation lasts, or what is the animation's current
|
||||
# time (in seconds too);
|
||||
player.timeLabel=%Ss
|
@ -74,6 +74,10 @@
|
||||
- in the network details pane identifying the preview tab. -->
|
||||
<!ENTITY netmonitorUI.tab.preview "Preview">
|
||||
|
||||
<!-- LOCALIZATION NOTE (netmonitorUI.tab.security): This is the label displayed
|
||||
- in the network details pane identifying the security tab. -->
|
||||
<!ENTITY netmonitorUI.tab.security "Security">
|
||||
|
||||
<!-- LOCALIZATION NOTE (debuggerUI.footer.filterAll): This is the label displayed
|
||||
- in the network details footer for the "All" filtering button. -->
|
||||
<!ENTITY netmonitorUI.footer.filterAll "All">
|
||||
@ -192,6 +196,35 @@
|
||||
- in a "receive" state. -->
|
||||
<!ENTITY netmonitorUI.timings.receive "Receiving:">
|
||||
|
||||
<!-- LOCALIZATION NOTE (netmonitorUI.security.error): This is the label displayed
|
||||
- in the security tab if a security error prevented the connection. -->
|
||||
<!ENTITY netmonitorUI.security.error "An error occured:">
|
||||
|
||||
<!-- LOCALIZATION NOTE (netmonitorUI.security.protocolVersion): This is the label displayed
|
||||
- in the security tab describing TLS/SSL protocol version. -->
|
||||
<!ENTITY netmonitorUI.security.protocolVersion "Protocol version:">
|
||||
|
||||
<!-- LOCALIZATION NOTE (netmonitorUI.security.cipherSuite): This is the label displayed
|
||||
- in the security tab describing the cipher suite used to secure this connection. -->
|
||||
<!ENTITY netmonitorUI.security.cipherSuite "Cipher suite:">
|
||||
|
||||
<!-- LOCALIZATION NOTE (netmonitorUI.security.hsts): This is the label displayed
|
||||
- in the security tab describing the usage of HTTP Strict Transport Security. -->
|
||||
<!ENTITY netmonitorUI.security.hsts "HTTP Strict Transport Security:">
|
||||
|
||||
<!-- LOCALIZATION NOTE (netmonitorUI.security.hpkp): This is the label displayed
|
||||
- in the security tab describing the usage of Public Key Pinning. -->
|
||||
<!ENTITY netmonitorUI.security.hpkp "Public Key Pinning:">
|
||||
|
||||
<!-- LOCALIZATION NOTE (netmonitorUI.security.connection): This is the label displayed
|
||||
- in the security tab describing the section containing information related to
|
||||
- the secure connection. -->
|
||||
<!ENTITY netmonitorUI.security.connection "Connection:">
|
||||
|
||||
<!-- LOCALIZATION NOTE (netmonitorUI.security.certificate): This is the label displayed
|
||||
- in the security tab describing the server certificate section. -->
|
||||
<!ENTITY netmonitorUI.security.certificate "Certificate:">
|
||||
|
||||
<!-- 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…">
|
||||
|
@ -29,6 +29,46 @@ netmonitor.accesskey=N
|
||||
# displayed inside the developer tools window.
|
||||
netmonitor.tooltip=Network Monitor
|
||||
|
||||
# LOCALIZATION NOTE (netmonitor.security.state.secure)
|
||||
# This string is used as an tooltip for request that was performed over secure
|
||||
# channel i.e. the connection was encrypted.
|
||||
netmonitor.security.state.secure=The connection used to fetch this resource was secure.
|
||||
|
||||
# LOCALIZATION NOTE (netmonitor.security.state.insecure)
|
||||
# This string is used as an tooltip for request that was performed over insecure
|
||||
# channel i.e. the connection was not encrypted.
|
||||
netmonitor.security.state.insecure=The connection used to fetch this resource was not encrypted.
|
||||
|
||||
# LOCALIZATION NOTE (netmonitor.security.state.broken)
|
||||
# This string is used as an tooltip for request that failed due to security
|
||||
# issues.
|
||||
netmonitor.security.state.broken=A security error prevented the resource from being loaded.
|
||||
|
||||
# LOCALIZATION NOTE (netmonitor.security.enabled):
|
||||
# This string is used to indicate that a specific security feature is used by
|
||||
# a connection in the security details tab.
|
||||
# For example: "HTTP Strict Transport Security: Enabled"
|
||||
netmonitor.security.enabled=Enabled
|
||||
|
||||
# LOCALIZATION NOTE (netmonitor.security.disabled):
|
||||
# This string is used to indicate that a specific security feature is not used by
|
||||
# a connection in the security details tab.
|
||||
# For example: "HTTP Strict Transport Security: Disabled"
|
||||
netmonitor.security.disabled=Disabled
|
||||
|
||||
# LOCALIZATION NOTE (netmonitor.security.hostHeader):
|
||||
# This string is used as a header for section containing security information
|
||||
# related to the remote host. %S is replaced with the domain name of the remote
|
||||
# host. For example: Host example.com
|
||||
netmonitor.security.hostHeader=Host %S:
|
||||
|
||||
# LOCALIZATION NOTE (netmonitor.security.notAvailable):
|
||||
# This string is used to indicate that a certain piece of information is not
|
||||
# available to be displayd. For example a certificate that has no organization
|
||||
# defined:
|
||||
# Organization: <Not Available>
|
||||
netmonitor.security.notAvailable=<Not Available>
|
||||
|
||||
# LOCALIZATION NOTE (collapseDetailsPane): This is the tooltip for the button
|
||||
# that collapses the network details pane in the UI.
|
||||
collapseDetailsPane=Hide request details
|
||||
|
@ -64,6 +64,12 @@ profile.tab=%1$S ms → %2$S ms
|
||||
# AS SHORT AS POSSIBLE so it doesn't obstruct important parts of the graph.
|
||||
graphs.fps=fps
|
||||
|
||||
# LOCALIZATION NOTE (graphs.ms):
|
||||
# This string is displayed in the flamegraph of the Profiler,
|
||||
# as the unit used to measure time (in milliseconds). This label should be kept
|
||||
# AS SHORT AS POSSIBLE so it doesn't obstruct important parts of the graph.
|
||||
graphs.ms=ms
|
||||
|
||||
# LOCALIZATION NOTE (category.*):
|
||||
# These strings are displayed in the categories graph of the Profiler,
|
||||
# as the legend for each block in every bar. These labels should be kept
|
||||
@ -102,3 +108,4 @@ recordingsList.saveDialogJSONFilter=JSON Files
|
||||
# LOCALIZATION NOTE (recordingsList.saveDialogAllFilter):
|
||||
# This string is displayed as a filter for saving a recording to disk.
|
||||
recordingsList.saveDialogAllFilter=All Files
|
||||
|
||||
|
@ -27,6 +27,8 @@
|
||||
locale/browser/baseMenuOverlay.dtd (%chrome/browser/baseMenuOverlay.dtd)
|
||||
locale/browser/browser.properties (%chrome/browser/browser.properties)
|
||||
locale/browser/customizableui/customizableWidgets.properties (%chrome/browser/customizableui/customizableWidgets.properties)
|
||||
locale/browser/devtools/animationinspector.dtd (%chrome/browser/devtools/animationinspector.dtd)
|
||||
locale/browser/devtools/animationinspector.properties (%chrome/browser/devtools/animationinspector.properties)
|
||||
locale/browser/devtools/appcacheutils.properties (%chrome/browser/devtools/appcacheutils.properties)
|
||||
locale/browser/devtools/debugger.dtd (%chrome/browser/devtools/debugger.dtd)
|
||||
locale/browser/devtools/debugger.properties (%chrome/browser/devtools/debugger.properties)
|
||||
|
@ -556,7 +556,7 @@ this.BrowserUITelemetry = {
|
||||
result.hiddenTabs = hiddenTabs;
|
||||
|
||||
if (Components.isSuccessCode(searchResult)) {
|
||||
result.currentSearchEngine = Services.search.currentEngine;
|
||||
result.currentSearchEngine = Services.search.currentEngine.name;
|
||||
}
|
||||
|
||||
return result;
|
||||
|
@ -125,6 +125,10 @@ toolbarbutton.bookmark-item:not(.subviewbutton),
|
||||
padding: 2px 3px;
|
||||
}
|
||||
|
||||
toolbarbutton.bookmark-item:not(.subviewbutton):not(:hover):not(:active):not([open]) {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
toolbarbutton.bookmark-item:not(.subviewbutton):hover:active,
|
||||
toolbarbutton.bookmark-item[open="true"] {
|
||||
padding-top: 3px;
|
||||
|
@ -16,7 +16,7 @@
|
||||
width: 9em;
|
||||
}
|
||||
|
||||
.requests-menu-domain {
|
||||
.requests-menu-security-and-domain {
|
||||
width: 16vw;
|
||||
}
|
||||
|
||||
|
@ -268,6 +268,7 @@ browser.jar:
|
||||
skin/classic/browser/devtools/breadcrumbs-divider@2x.png (../shared/devtools/images/breadcrumbs-divider@2x.png)
|
||||
skin/classic/browser/devtools/breadcrumbs-scrollbutton.png (../shared/devtools/images/breadcrumbs-scrollbutton.png)
|
||||
skin/classic/browser/devtools/breadcrumbs-scrollbutton@2x.png (../shared/devtools/images/breadcrumbs-scrollbutton@2x.png)
|
||||
skin/classic/browser/devtools/animationinspector.css (../shared/devtools/animationinspector.css)
|
||||
* skin/classic/browser/devtools/canvasdebugger.css (devtools/canvasdebugger.css)
|
||||
* skin/classic/browser/devtools/debugger.css (devtools/debugger.css)
|
||||
skin/classic/browser/devtools/eyedropper.css (../shared/devtools/eyedropper.css)
|
||||
|
@ -399,6 +399,7 @@ browser.jar:
|
||||
skin/classic/browser/devtools/breadcrumbs-divider@2x.png (../shared/devtools/images/breadcrumbs-divider@2x.png)
|
||||
skin/classic/browser/devtools/breadcrumbs-scrollbutton.png (../shared/devtools/images/breadcrumbs-scrollbutton.png)
|
||||
skin/classic/browser/devtools/breadcrumbs-scrollbutton@2x.png (../shared/devtools/images/breadcrumbs-scrollbutton@2x.png)
|
||||
skin/classic/browser/devtools/animationinspector.css (../shared/devtools/animationinspector.css)
|
||||
* skin/classic/browser/devtools/canvasdebugger.css (devtools/canvasdebugger.css)
|
||||
* skin/classic/browser/devtools/debugger.css (devtools/debugger.css)
|
||||
skin/classic/browser/devtools/eyedropper.css (../shared/devtools/eyedropper.css)
|
||||
|
@ -250,7 +250,7 @@ window:not([chromehidden~="toolbar"]) #urlbar-wrapper {
|
||||
-moz-image-region: rect(0, 54px, 18px, 36px);
|
||||
}
|
||||
|
||||
.search-go-button {
|
||||
searchbar:not([oneoffui]) .search-go-button {
|
||||
/* !important is needed because searchbar.css is loaded after this */
|
||||
-moz-image-region: auto !important;
|
||||
list-style-image: var(--search-button-image);
|
||||
|
149
browser/themes/shared/devtools/animationinspector.css
Normal file
149
browser/themes/shared/devtools/animationinspector.css
Normal file
@ -0,0 +1,149 @@
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* The error message, shown when an invalid/unanimated element is selected */
|
||||
|
||||
#error-message {
|
||||
margin-top: 10%;
|
||||
text-align: center;
|
||||
|
||||
/* The error message is hidden by default */
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Element picker button */
|
||||
|
||||
#element-picker {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#element-picker::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
margin: -8px 0 0 -8px;
|
||||
background-image: url("chrome://browser/skin/devtools/command-pick.png");
|
||||
}
|
||||
|
||||
#element-picker[checked]::before {
|
||||
background-position: -48px 0;
|
||||
filter: none; /* Icon is blue when checked, don't invert for light theme */
|
||||
}
|
||||
|
||||
@media (min-resolution: 2dppx) {
|
||||
#element-picker::before {
|
||||
background-image: url("chrome://browser/skin/devtools/command-pick@2x.png");
|
||||
background-size: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation title gutter, contains the name, duration, iteration */
|
||||
|
||||
.animation-title {
|
||||
background-color: var(--theme-toolbar-background);
|
||||
color: var(--theme-content-color3);
|
||||
border-bottom: 1px solid var(--theme-splitter-color);
|
||||
padding: 1px 4px;
|
||||
word-wrap: break-word;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.animation-title .meta-data {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.animation-title strong {
|
||||
margin: 0 .5em;
|
||||
}
|
||||
|
||||
/* Timeline wiget */
|
||||
|
||||
.timeline {
|
||||
height: 20px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-bottom: 1px solid var(--theme-splitter-color);
|
||||
}
|
||||
|
||||
.timeline .playback-controls {
|
||||
width: 50px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
/* Playback control buttons */
|
||||
|
||||
.timeline .playback-controls button {
|
||||
flex-grow: 1;
|
||||
border-width: 0 1px 0 0;
|
||||
}
|
||||
|
||||
/* Play/pause button */
|
||||
|
||||
.timeline .toggle::before {
|
||||
background-image: url(debugger-pause.png);
|
||||
}
|
||||
|
||||
.paused .timeline .toggle::before {
|
||||
background-image: url(debugger-play.png);
|
||||
}
|
||||
|
||||
@media (min-resolution: 2dppx) {
|
||||
.timeline .toggle::before {
|
||||
background-image: url(debugger-pause@2x.png);
|
||||
}
|
||||
|
||||
.paused .timeline .toggle::before {
|
||||
background-image: url(debugger-play@2x.png);
|
||||
}
|
||||
}
|
||||
|
||||
/* Slider (input type range) container */
|
||||
|
||||
.timeline .sliders-container {
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
border-width: 1px 0;
|
||||
}
|
||||
|
||||
.timeline .sliders-container .current-time {
|
||||
position: absolute;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.timeline .sliders-container .current-time::-moz-range-thumb {
|
||||
height: 100%;
|
||||
width: 4px;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
background: var(--theme-highlight-blue);
|
||||
}
|
||||
|
||||
.timeline .sliders-container .current-time::-moz-range-track {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Current time label */
|
||||
|
||||
.timeline .time-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 50px;
|
||||
border-left: 1px solid var(--theme-splitter-color);
|
||||
background: var(--theme-toolbar-background);
|
||||
}
|
@ -7,35 +7,36 @@
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<g id="call-tree">
|
||||
<rect x="1px" y="3.5px" width="14px" height="2px" rx="1" ry="1"/>
|
||||
<rect x="1px" y="7.5px" width="7px" height="2px" rx="1" ry="1"/>
|
||||
<rect x="11px" y="7.5px" width="4px" height="2px" rx="1" ry="1"/>
|
||||
<rect x="4px" y="11.5px" width="4px" height="2px" rx="1" ry="1"/>
|
||||
<g id="overview-markers">
|
||||
<rect x="0px" y="3px" width="5px" height="2.5px" rx="1" ry="1"/>
|
||||
<rect x="7px" y="3px" width="9px" height="2.5px" rx="1" ry="1"/>
|
||||
<rect x="0px" y="7px" width="9px" height="2.5px" rx="1" ry="1"/>
|
||||
<rect x="10px" y="7px" width="6px" height="2.5px" rx="1" ry="1"/>
|
||||
<rect x="4px" y="11px" width="5px" height="2.5px" rx="1" ry="1"/>
|
||||
<rect x="12px" y="11px" width="4px" height="2.5px" rx="1" ry="1"/>
|
||||
</g>
|
||||
<g id="flamechart">
|
||||
<rect x="1px" y="3px" width="14px" height="4px" style="shape-rendering: crispEdges"/>
|
||||
<rect x="1px" y="5px" width="3px" height="5px" rx="1" ry="1"/>
|
||||
<rect x="4px" y="5px" width="3px" height="10px" rx="1" ry="1"/>
|
||||
<rect x="7px" y="5px" width="5px" height="3px" rx="1" ry="1"/>
|
||||
<rect x="12px" y="5px" width="3px" height="7px" rx="1" ry="1"/>
|
||||
<g id="overview-frames">
|
||||
<rect x="1px" y="4px" width="2px" height="12px" rx="1" ry="1"/>
|
||||
<rect x="5px" y="12px" width="2px" height="4px" rx="1" ry="1"/>
|
||||
<rect x="9px" y="9px" width="2px" height="7px" rx="1" ry="1"/>
|
||||
<rect x="13px" y="7px" width="2px" height="9px" rx="1" ry="1"/>
|
||||
</g>
|
||||
<g id="frame">
|
||||
<rect x="1px" y="4px" width="2px" height="13px" rx="1" ry="1"/>
|
||||
<rect x="5px" y="12px" width="2px" height="5px" rx="1" ry="1"/>
|
||||
<rect x="9px" y="9px" width="2px" height="8px" rx="1" ry="1"/>
|
||||
<rect x="13px" y="7px" width="2px" height="10px" rx="1" ry="1"/>
|
||||
</g>
|
||||
<g id="markers">
|
||||
<path d="m2.1,2.1h9.6c.6,0 1.1,.5 1.1,1.1 0,.6-.5,1.1-1.1,1.1h-9.6c-.6,0-1.1-.5-1.1-1.1 .1-.6 .5-1.1 1.1-1.1z"/>
|
||||
<path d="m7.4,5.3h7.4c.6,0 1.1,.5 1.1,1.1 0,.6-.5,1.1-1.1,1.1h-7.4c-.5-.1-1-.5-1-1.1 0-.6 .5-1.1 1-1.1z"/>
|
||||
<path d="m5.3,8.5h3.2c.6,0 1.1,.5 1.1,1.1 0,.6-.5,1.1-1.1,1.1h-3.2c-.6,0-1.1-.5-1.1-1.1 .1-.6 .5-1.1 1.1-1.1z"/>
|
||||
<path d="m4.3,11.7h2.1c.6,0 1.1,.5 1.1,1.1 0,.6-.5,1.1-1.1,1.1h-2.1c-.6,0-1.1-.5-1.1-1.1 0-.6 .5-1.1 1.1-1.1z"/>
|
||||
<path d="m4.3,11.7h2.1c.6,0 1.1,.5 1.1,1.1 0,.6-.5,1.1-1.1,1.1h-2.1c-.6,0-1.1-.5-1.1-1.1 0-.6 .5-1.1 1.1-1.1z" style="transform: translateX(7px)"/>
|
||||
</g>
|
||||
<g id="waterfall">
|
||||
<rect x="1px" y="3px" width="8px" height="2.5px" rx="1" ry="1"/>
|
||||
<g id="details-waterfall">
|
||||
<rect x="0px" y="3px" width="9px" height="2.5px" rx="1" ry="1"/>
|
||||
<rect x="5px" y="7px" width="8px" height="2.5px" rx="1" ry="1"/>
|
||||
<rect x="7px" y="11.5px" width="8px" height="2.5px" rx="1" ry="1"/>
|
||||
<rect x="7px" y="11px" width="9px" height="2.5px" rx="1" ry="1"/>
|
||||
</g>
|
||||
</svg>
|
||||
<g id="details-call-tree">
|
||||
<rect x="0px" y="3px" width="16px" height="2px"/>
|
||||
<rect x="3px" y="6px" width="7px" height="2px"/>
|
||||
<rect x="6px" y="9px" width="6px" height="2px"/>
|
||||
<rect x="9px" y="12px" width="5px" height="2px"/>
|
||||
</g>
|
||||
<g id="details-flamegraph">
|
||||
<rect x="0px" y="3px" width="16px" height="2px"/>
|
||||
<rect x="0px" y="6px" width="8px" height="2px"/>
|
||||
<rect x="10px" y="6px" width="6px" height="2px"/>
|
||||
<rect x="2px" y="9px" width="6px" height="2px"/>
|
||||
<rect x="5px" y="12px" width="3px" height="2px"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.7 KiB |
@ -151,11 +151,38 @@
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.requests-menu-domain {
|
||||
.requests-menu-security-and-domain {
|
||||
width: 14vw;
|
||||
min-width: 10em;
|
||||
}
|
||||
|
||||
.requests-security-state-icon {
|
||||
-moz-margin-end: 4px;
|
||||
-moz-image-region:rect(0px, 16px, 16px, 0px);
|
||||
}
|
||||
|
||||
.requests-security-state-icon:hover {
|
||||
-moz-image-region: rect(0px, 32px, 16px, 16px);
|
||||
}
|
||||
|
||||
.requests-security-state-icon:active {
|
||||
-moz-image-region: rect(0px, 48px, 16px, 32px);
|
||||
}
|
||||
|
||||
.security-state-insecure {
|
||||
list-style-image: url(chrome://browser/skin/identity-icons-generic.png);
|
||||
}
|
||||
|
||||
.security-state-secure {
|
||||
cursor: pointer;
|
||||
list-style-image: url(chrome://browser/skin/identity-icons-https.png);
|
||||
}
|
||||
|
||||
.security-state-broken {
|
||||
cursor: pointer;
|
||||
list-style-image: url(chrome://browser/skin/identity-icons-https-mixed-active.png);
|
||||
}
|
||||
|
||||
.requests-menu-type {
|
||||
text-align: center;
|
||||
width: 4em;
|
||||
@ -533,6 +560,19 @@ label.requests-menu-status-code {
|
||||
transition: transform 0.2s ease-out;
|
||||
}
|
||||
|
||||
/* Security tabpanel */
|
||||
.security-info-section {
|
||||
-moz-padding-start: 1em;
|
||||
}
|
||||
|
||||
#security-tabpanel {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#security-error-message {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* Custom request form */
|
||||
|
||||
#custom-pane {
|
||||
@ -778,7 +818,7 @@ label.requests-menu-status-code {
|
||||
width: 30vw;
|
||||
}
|
||||
|
||||
.requests-menu-domain {
|
||||
.requests-menu-security-and-domain {
|
||||
width: 30vw;
|
||||
}
|
||||
|
||||
|
@ -42,13 +42,16 @@
|
||||
/* Details Panel */
|
||||
|
||||
#select-waterfall-view {
|
||||
list-style-image: url(performance-icons.svg#waterfall);
|
||||
list-style-image: url(performance-icons.svg#details-waterfall);
|
||||
}
|
||||
|
||||
#select-calltree-view {
|
||||
list-style-image: url(performance-icons.svg#call-tree);
|
||||
list-style-image: url(performance-icons.svg#details-call-tree);
|
||||
}
|
||||
|
||||
#select-flamegraph-view {
|
||||
list-style-image: url(performance-icons.svg#details-flamegraph);
|
||||
}
|
||||
|
||||
/* Profile call tree */
|
||||
|
||||
@ -331,21 +334,18 @@
|
||||
#waterfall-details {
|
||||
-moz-padding-start: 8px;
|
||||
-moz-padding-end: 8px;
|
||||
padding-top: 8vh;
|
||||
padding-top: 2vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.marker-details-bullet {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin: 0 8px;
|
||||
border: 1px solid;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.marker-details-start,
|
||||
.marker-details-end,
|
||||
.marker-details-duration {
|
||||
#waterfall-details > * {
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
|
@ -217,7 +217,7 @@
|
||||
#timeline-waterfall-details {
|
||||
-moz-padding-start: 8px;
|
||||
-moz-padding-end: 8px;
|
||||
padding-top: 8vh;
|
||||
padding-top: 2vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@ -228,9 +228,7 @@
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.marker-details-start,
|
||||
.marker-details-end,
|
||||
.marker-details-duration {
|
||||
#timeline-waterfall-details > * {
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
|
@ -244,6 +244,67 @@
|
||||
-moz-margin-start: 1px;
|
||||
}
|
||||
|
||||
/* HTML buttons, similar to toolbar buttons, but work in HTML documents */
|
||||
|
||||
.devtools-button {
|
||||
border: 0 solid var(--theme-splitter-color);
|
||||
background: var(--theme-toolbar-background);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-width: 32px;
|
||||
min-height: 18px;
|
||||
/* The icon is absolutely positioned in the button using ::before */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.devtools-button[standalone] {
|
||||
min-height: 32px;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
/* Button States */
|
||||
.theme-dark .devtools-button:not([disabled]):hover {
|
||||
background: rgba(0, 0, 0, .3); /* Splitters */
|
||||
}
|
||||
.theme-light .devtools-button:not([disabled]):hover {
|
||||
background: rgba(170, 170, 170, .3); /* Splitters */
|
||||
}
|
||||
|
||||
.theme-dark .devtools-button:not([disabled]):hover:active {
|
||||
background: rgba(0, 0, 0, .4); /* Splitters */
|
||||
}
|
||||
.theme-light .devtools-button:not([disabled]):hover:active {
|
||||
background: rgba(170, 170, 170, .4); /* Splitters */
|
||||
}
|
||||
|
||||
/* Menu type buttons and checked states */
|
||||
.theme-dark .devtools-button[checked] {
|
||||
background: rgba(29, 79, 115, .7) !important; /* Select highlight blue */
|
||||
color: var(--theme-selection-color);
|
||||
}
|
||||
|
||||
.theme-light .devtools-button[checked] {
|
||||
background: rgba(76, 158, 217, .2) !important; /* Select highlight blue */
|
||||
}
|
||||
|
||||
.devtools-button::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
margin: -8px 0 0 -8px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
@media (min-resolution: 2dppx) {
|
||||
.devtools-button::before {
|
||||
background-size: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Text input */
|
||||
|
||||
.devtools-textinput,
|
||||
@ -822,7 +883,8 @@
|
||||
.theme-light #canvas-debugging-empty-notice-button .button-icon,
|
||||
.theme-light #requests-menu-perf-notice-button .button-icon,
|
||||
.theme-light #requests-menu-network-summary-button .button-icon,
|
||||
.theme-light .event-tooltip-debugger-icon {
|
||||
.theme-light .event-tooltip-debugger-icon,
|
||||
.theme-light .devtools-button::before {
|
||||
filter: url(filters.svg#invert);
|
||||
}
|
||||
|
||||
|
@ -935,26 +935,14 @@
|
||||
}
|
||||
|
||||
.line-graph-widget-tooltip[type=maximum] {
|
||||
left: -1px;
|
||||
left: 14px;
|
||||
}
|
||||
|
||||
.line-graph-widget-tooltip[type=minimum] {
|
||||
left: -1px;
|
||||
left: 14px;
|
||||
}
|
||||
|
||||
.line-graph-widget-tooltip[type=average] {
|
||||
right: -1px;
|
||||
}
|
||||
|
||||
.line-graph-widget-tooltip[type=maximum][with-arrows=true] {
|
||||
left: 14px;
|
||||
}
|
||||
|
||||
.line-graph-widget-tooltip[type=minimum][with-arrows=true] {
|
||||
left: 14px;
|
||||
}
|
||||
|
||||
.line-graph-widget-tooltip[type=average][with-arrows=true] {
|
||||
right: 4px;
|
||||
}
|
||||
|
||||
|
@ -306,6 +306,7 @@ browser.jar:
|
||||
skin/classic/browser/devtools/breadcrumbs-divider@2x.png (../shared/devtools/images/breadcrumbs-divider@2x.png)
|
||||
skin/classic/browser/devtools/breadcrumbs-scrollbutton.png (../shared/devtools/images/breadcrumbs-scrollbutton.png)
|
||||
skin/classic/browser/devtools/breadcrumbs-scrollbutton@2x.png (../shared/devtools/images/breadcrumbs-scrollbutton@2x.png)
|
||||
skin/classic/browser/devtools/animationinspector.css (../shared/devtools/animationinspector.css)
|
||||
skin/classic/browser/devtools/eyedropper.css (../shared/devtools/eyedropper.css)
|
||||
* skin/classic/browser/devtools/canvasdebugger.css (devtools/canvasdebugger.css)
|
||||
* skin/classic/browser/devtools/debugger.css (devtools/debugger.css)
|
||||
@ -767,6 +768,7 @@ browser.jar:
|
||||
skin/classic/aero/browser/devtools/breadcrumbs-divider@2x.png (../shared/devtools/images/breadcrumbs-divider@2x.png)
|
||||
skin/classic/aero/browser/devtools/breadcrumbs-scrollbutton.png (../shared/devtools/images/breadcrumbs-scrollbutton.png)
|
||||
skin/classic/aero/browser/devtools/breadcrumbs-scrollbutton@2x.png (../shared/devtools/images/breadcrumbs-scrollbutton@2x.png)
|
||||
skin/classic/aero/browser/devtools/animationinspector.css (../shared/devtools/animationinspector.css)
|
||||
* skin/classic/aero/browser/devtools/canvasdebugger.css (devtools/canvasdebugger.css)
|
||||
* skin/classic/aero/browser/devtools/debugger.css (devtools/debugger.css)
|
||||
skin/classic/aero/browser/devtools/eyedropper.css (../shared/devtools/eyedropper.css)
|
||||
|
@ -10748,7 +10748,9 @@ FactoryOp::CheckPermission(ContentParent* aContentParent,
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
MOZ_ASSERT(mState == State_Initial || mState == State_PermissionRetry);
|
||||
|
||||
if (NS_WARN_IF(!Preferences::GetBool(kPrefIndexedDBEnabled, false))) {
|
||||
const PrincipalInfo& principalInfo = mCommonParams.principalInfo();
|
||||
if (principalInfo.type() != PrincipalInfo::TSystemPrincipalInfo &&
|
||||
NS_WARN_IF(!Preferences::GetBool(kPrefIndexedDBEnabled, false))) {
|
||||
if (aContentParent) {
|
||||
// The DOM in the other process should have kept us from receiving any
|
||||
// indexedDB messages so assume that the child is misbehaving.
|
||||
@ -10764,7 +10766,6 @@ FactoryOp::CheckPermission(ContentParent* aContentParent,
|
||||
|
||||
PersistenceType persistenceType = mCommonParams.metadata().persistenceType();
|
||||
|
||||
const PrincipalInfo& principalInfo = mCommonParams.principalInfo();
|
||||
MOZ_ASSERT(principalInfo.type() != PrincipalInfo::TNullPrincipalInfo);
|
||||
|
||||
if (principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) {
|
||||
|
@ -129,13 +129,15 @@ IDBFactory::CreateForWindow(nsPIDOMWindow* aWindow,
|
||||
MOZ_ASSERT(aWindow->IsInnerWindow());
|
||||
MOZ_ASSERT(aFactory);
|
||||
|
||||
if (NS_WARN_IF(!Preferences::GetBool(kPrefIndexedDBEnabled, false))) {
|
||||
nsCOMPtr<nsIPrincipal> principal;
|
||||
nsresult rv = AllowedForWindowInternal(aWindow, getter_AddRefs(principal));
|
||||
|
||||
if (!(NS_SUCCEEDED(rv) && nsContentUtils::IsSystemPrincipal(principal)) &&
|
||||
NS_WARN_IF(!Preferences::GetBool(kPrefIndexedDBEnabled, false))) {
|
||||
*aFactory = nullptr;
|
||||
return NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR;
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIPrincipal> principal;
|
||||
nsresult rv = AllowedForWindowInternal(aWindow, getter_AddRefs(principal));
|
||||
if (rv == NS_ERROR_DOM_NOT_SUPPORTED_ERR) {
|
||||
NS_WARNING("IndexedDB is not permitted in a third-party window.");
|
||||
*aFactory = nullptr;
|
||||
@ -261,8 +263,10 @@ IDBFactory::CreateForMainThreadJSInternal(
|
||||
IDBFactory** aFactory)
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
MOZ_ASSERT(aPrincipalInfo);
|
||||
|
||||
if (NS_WARN_IF(!Preferences::GetBool(kPrefIndexedDBEnabled, false))) {
|
||||
if (aPrincipalInfo->type() != PrincipalInfo::TSystemPrincipalInfo &&
|
||||
NS_WARN_IF(!Preferences::GetBool(kPrefIndexedDBEnabled, false))) {
|
||||
*aFactory = nullptr;
|
||||
return NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR;
|
||||
}
|
||||
|
@ -4,6 +4,8 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#include "nsPluginStreamListenerPeer.h"
|
||||
#include "nsIContentPolicy.h"
|
||||
#include "nsContentPolicyUtils.h"
|
||||
#include "nsIDOMElement.h"
|
||||
#include "nsIStreamConverterService.h"
|
||||
#include "nsIHttpChannel.h"
|
||||
@ -12,6 +14,7 @@
|
||||
#include "nsMimeTypes.h"
|
||||
#include "nsISupportsPrimitives.h"
|
||||
#include "nsNetCID.h"
|
||||
#include "nsPluginInstanceOwner.h"
|
||||
#include "nsPluginLogging.h"
|
||||
#include "nsIURI.h"
|
||||
#include "nsIURL.h"
|
||||
@ -477,6 +480,37 @@ nsPluginStreamListenerPeer::OnStartRequest(nsIRequest *request,
|
||||
}
|
||||
}
|
||||
|
||||
nsAutoCString contentType;
|
||||
rv = channel->GetContentType(contentType);
|
||||
if (NS_FAILED(rv))
|
||||
return rv;
|
||||
|
||||
// Check ShouldProcess with content policy
|
||||
nsRefPtr<nsPluginInstanceOwner> owner;
|
||||
if (mPluginInstance) {
|
||||
owner = mPluginInstance->GetOwner();
|
||||
}
|
||||
nsCOMPtr<nsIDOMElement> element;
|
||||
nsCOMPtr<nsIDocument> doc;
|
||||
if (owner) {
|
||||
owner->GetDOMElement(getter_AddRefs(element));
|
||||
owner->GetDocument(getter_AddRefs(doc));
|
||||
}
|
||||
nsCOMPtr<nsIPrincipal> principal = doc ? doc->NodePrincipal() : nullptr;
|
||||
|
||||
int16_t shouldLoad = nsIContentPolicy::ACCEPT;
|
||||
rv = NS_CheckContentProcessPolicy(nsIContentPolicy::TYPE_OBJECT_SUBREQUEST,
|
||||
mURL,
|
||||
principal,
|
||||
element,
|
||||
contentType,
|
||||
nullptr,
|
||||
&shouldLoad);
|
||||
if (NS_FAILED(rv) || NS_CP_REJECTED(shouldLoad)) {
|
||||
mRequestFailed = true;
|
||||
return NS_ERROR_CONTENT_BLOCKED;
|
||||
}
|
||||
|
||||
// Get the notification callbacks from the channel and save it as
|
||||
// week ref we'll use it in nsPluginStreamInfo::RequestRead() when
|
||||
// we'll create channel for byte range request.
|
||||
@ -509,11 +543,6 @@ nsPluginStreamListenerPeer::OnStartRequest(nsIRequest *request,
|
||||
mLength = uint32_t(length);
|
||||
}
|
||||
|
||||
nsAutoCString aContentType; // XXX but we already got the type above!
|
||||
rv = channel->GetContentType(aContentType);
|
||||
if (NS_FAILED(rv))
|
||||
return rv;
|
||||
|
||||
nsCOMPtr<nsIURI> aURL;
|
||||
rv = channel->GetURI(getter_AddRefs(aURL));
|
||||
if (NS_FAILED(rv))
|
||||
@ -521,13 +550,13 @@ nsPluginStreamListenerPeer::OnStartRequest(nsIRequest *request,
|
||||
|
||||
aURL->GetSpec(mURLSpec);
|
||||
|
||||
if (!aContentType.IsEmpty())
|
||||
mContentType = aContentType;
|
||||
if (!contentType.IsEmpty())
|
||||
mContentType = contentType;
|
||||
|
||||
#ifdef PLUGIN_LOGGING
|
||||
PR_LOG(nsPluginLogging::gPluginLog, PLUGIN_LOG_NOISY,
|
||||
("nsPluginStreamListenerPeer::OnStartRequest this=%p request=%p mime=%s, url=%s\n",
|
||||
this, request, aContentType.get(), mURLSpec.get()));
|
||||
this, request, contentType.get(), mURLSpec.get()));
|
||||
|
||||
PR_LogFlush();
|
||||
#endif
|
||||
|
@ -664,9 +664,18 @@ pref("ui.scrolling.negate_wheel_scrollY", true);
|
||||
pref("ui.scrolling.gamepad_dead_zone", 115);
|
||||
|
||||
// Prefs for fling acceleration
|
||||
pref("ui.scrolling.fling_accel_interval", 500);
|
||||
pref("ui.scrolling.fling_accel_base_multiplier", "1.0");
|
||||
pref("ui.scrolling.fling_accel_supplemental_multiplier", "1.0");
|
||||
pref("ui.scrolling.fling_accel_interval", -1);
|
||||
pref("ui.scrolling.fling_accel_base_multiplier", -1);
|
||||
pref("ui.scrolling.fling_accel_supplemental_multiplier", -1);
|
||||
|
||||
// Prefs for fling curving
|
||||
pref("ui.scrolling.fling_curve_function_x1", -1);
|
||||
pref("ui.scrolling.fling_curve_function_y1", -1);
|
||||
pref("ui.scrolling.fling_curve_function_x2", -1);
|
||||
pref("ui.scrolling.fling_curve_function_y2", -1);
|
||||
pref("ui.scrolling.fling_curve_threshold_velocity", -1);
|
||||
pref("ui.scrolling.fling_curve_max_velocity", -1);
|
||||
pref("ui.scrolling.fling_curve_newton_iterations", -1);
|
||||
|
||||
// Enable accessibility mode if platform accessibility is enabled.
|
||||
pref("accessibility.accessfu.activate", 2);
|
||||
|
@ -96,6 +96,9 @@ public class FindInPageBar extends LinearLayout implements TextWatcher, View.OnC
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
// Always clear the Find string, primarily for privacy.
|
||||
mFindText.setText("");
|
||||
|
||||
setVisibility(GONE);
|
||||
getInputMethodManager(mFindText).hideSoftInputFromWindow(mFindText.getWindowToken(), 0);
|
||||
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("FindInPage:Closed", null));
|
||||
|
@ -34,6 +34,13 @@ abstract class Axis {
|
||||
private static final String PREF_FLING_ACCEL_INTERVAL = "ui.scrolling.fling_accel_interval";
|
||||
private static final String PREF_FLING_ACCEL_BASE_MULTIPLIER = "ui.scrolling.fling_accel_base_multiplier";
|
||||
private static final String PREF_FLING_ACCEL_SUPPLEMENTAL_MULTIPLIER = "ui.scrolling.fling_accel_supplemental_multiplier";
|
||||
private static final String PREF_FLING_CURVE_FUNCTION_X1 = "ui.scrolling.fling_curve_function_x1";
|
||||
private static final String PREF_FLING_CURVE_FUNCTION_Y1 = "ui.scrolling.fling_curve_function_y1";
|
||||
private static final String PREF_FLING_CURVE_FUNCTION_X2 = "ui.scrolling.fling_curve_function_x2";
|
||||
private static final String PREF_FLING_CURVE_FUNCTION_Y2 = "ui.scrolling.fling_curve_function_y2";
|
||||
private static final String PREF_FLING_CURVE_THRESHOLD_VELOCITY = "ui.scrolling.fling_curve_threshold_velocity";
|
||||
private static final String PREF_FLING_CURVE_MAXIMUM_VELOCITY = "ui.scrolling.fling_curve_max_velocity";
|
||||
private static final String PREF_FLING_CURVE_NEWTON_ITERATIONS = "ui.scrolling.fling_curve_newton_iterations";
|
||||
|
||||
// This fraction of velocity remains after every animation frame when the velocity is low.
|
||||
private static float FRICTION_SLOW;
|
||||
@ -64,6 +71,27 @@ abstract class Axis {
|
||||
// The multiplication constant of the supplemental velocity in case of accelerated scrolling.
|
||||
private static float FLING_ACCEL_SUPPLEMENTAL_MULTIPLIER;
|
||||
|
||||
// x co-ordinate of the second bezier control point
|
||||
private static float FLING_CURVE_FUNCTION_X1;
|
||||
|
||||
// y co-ordinate of the second bezier control point
|
||||
private static float FLING_CURVE_FUNCTION_Y1;
|
||||
|
||||
// x co-ordinate of the third bezier control point
|
||||
private static float FLING_CURVE_FUNCTION_X2;
|
||||
|
||||
// y co-ordinate of the third bezier control point
|
||||
private static float FLING_CURVE_FUNCTION_Y2;
|
||||
|
||||
// Minimum velocity for curve to be implemented i.e fling curving
|
||||
private static float FLING_CURVE_THRESHOLD_VELOCITY;
|
||||
|
||||
// Maximum permitted velocity
|
||||
private static float FLING_CURVE_MAXIMUM_VELOCITY;
|
||||
|
||||
// Number of iterations in the Newton-Raphson method
|
||||
private static int FLING_CURVE_NEWTON_ITERATIONS;
|
||||
|
||||
private static float getFloatPref(Map<String, Integer> prefs, String prefName, int defaultValue) {
|
||||
Integer value = (prefs == null ? null : prefs.get(prefName));
|
||||
return (value == null || value < 0 ? defaultValue : value) / 1000f;
|
||||
@ -83,7 +111,14 @@ abstract class Axis {
|
||||
PREF_SCROLLING_MIN_SCROLLABLE_DISTANCE,
|
||||
PREF_FLING_ACCEL_INTERVAL,
|
||||
PREF_FLING_ACCEL_BASE_MULTIPLIER,
|
||||
PREF_FLING_ACCEL_SUPPLEMENTAL_MULTIPLIER };
|
||||
PREF_FLING_ACCEL_SUPPLEMENTAL_MULTIPLIER,
|
||||
PREF_FLING_CURVE_FUNCTION_X1,
|
||||
PREF_FLING_CURVE_FUNCTION_Y1,
|
||||
PREF_FLING_CURVE_FUNCTION_X2,
|
||||
PREF_FLING_CURVE_FUNCTION_Y2,
|
||||
PREF_FLING_CURVE_THRESHOLD_VELOCITY,
|
||||
PREF_FLING_CURVE_MAXIMUM_VELOCITY,
|
||||
PREF_FLING_CURVE_NEWTON_ITERATIONS };
|
||||
|
||||
PrefsHelper.getPrefs(prefs, new PrefsHelper.PrefHandlerBase() {
|
||||
Map<String, Integer> mPrefs = new HashMap<String, Integer>();
|
||||
@ -120,6 +155,14 @@ abstract class Axis {
|
||||
FLING_ACCEL_INTERVAL = getIntPref(prefs, PREF_FLING_ACCEL_INTERVAL, 500);
|
||||
FLING_ACCEL_BASE_MULTIPLIER = getFloatPref(prefs, PREF_FLING_ACCEL_BASE_MULTIPLIER, 1000);
|
||||
FLING_ACCEL_SUPPLEMENTAL_MULTIPLIER = getFloatPref(prefs, PREF_FLING_ACCEL_SUPPLEMENTAL_MULTIPLIER, 1000);
|
||||
FLING_CURVE_FUNCTION_X1 = getFloatPref(prefs, PREF_FLING_CURVE_FUNCTION_X1, 410);
|
||||
FLING_CURVE_FUNCTION_Y1 = getFloatPref(prefs, PREF_FLING_CURVE_FUNCTION_Y1, 0);
|
||||
FLING_CURVE_FUNCTION_X2 = getFloatPref(prefs, PREF_FLING_CURVE_FUNCTION_X2, 800);
|
||||
FLING_CURVE_FUNCTION_Y2 = getFloatPref(prefs, PREF_FLING_CURVE_FUNCTION_Y2, 1000);
|
||||
FLING_CURVE_THRESHOLD_VELOCITY = getFloatPref(prefs, PREF_FLING_CURVE_THRESHOLD_VELOCITY, 30);
|
||||
FLING_CURVE_MAXIMUM_VELOCITY = getFloatPref(prefs, PREF_FLING_CURVE_MAXIMUM_VELOCITY, 70);
|
||||
FLING_CURVE_NEWTON_ITERATIONS = getIntPref(prefs, PREF_FLING_CURVE_NEWTON_ITERATIONS, 5);
|
||||
|
||||
Log.i(LOGTAG, "Prefs: " + FRICTION_SLOW + "," + FRICTION_FAST + "," + VELOCITY_THRESHOLD + ","
|
||||
+ MAX_EVENT_ACCELERATION + "," + OVERSCROLL_DECEL_RATE + "," + SNAP_LIMIT + "," + MIN_SCROLLABLE_DISTANCE);
|
||||
}
|
||||
@ -212,9 +255,58 @@ abstract class Axis {
|
||||
mLastTouchPos = mTouchPos;
|
||||
}
|
||||
|
||||
// Calculates and return the slope of the curve at given parameter t
|
||||
float getSlope(float t) {
|
||||
float y1 = FLING_CURVE_FUNCTION_Y1;
|
||||
float y2 = FLING_CURVE_FUNCTION_Y2;
|
||||
|
||||
return (3 * y1)
|
||||
+ t * (6 * y2 - 12 * y1)
|
||||
+ t * t * (9 * y1 - 9 * y2 + 3);
|
||||
}
|
||||
|
||||
// Calculates and returns the value of the bezier curve with the given parameter t and control points p1 and p2
|
||||
float cubicBezier(float p1, float p2, float t) {
|
||||
return (3 * t * (1-t) * (1-t) * p1)
|
||||
+ (3 * t * t * (1-t) * p2)
|
||||
+ (t * t * t);
|
||||
}
|
||||
|
||||
// Responsible for mapping the physical velocity to a the velocity obtained after applying bezier curve (with control points (X1,Y1) and (X2,Y2))
|
||||
float flingCurve(float By) {
|
||||
int ni = FLING_CURVE_NEWTON_ITERATIONS;
|
||||
float[] guess = new float[ni];
|
||||
float y1 = FLING_CURVE_FUNCTION_Y1;
|
||||
float y2 = FLING_CURVE_FUNCTION_Y2;
|
||||
guess[0] = By;
|
||||
|
||||
for (int i = 1; i < ni; i++) {
|
||||
guess[i] = guess[i-1] - (cubicBezier(y1, y2, guess[i-1]) - By) / getSlope(guess[i-1]);
|
||||
}
|
||||
// guess[4] is the final approximate root the cubic equation.
|
||||
float t = guess[4];
|
||||
|
||||
float x1 = FLING_CURVE_FUNCTION_X1;
|
||||
float x2 = FLING_CURVE_FUNCTION_X2;
|
||||
return cubicBezier(x1, x2, t);
|
||||
}
|
||||
|
||||
void updateWithTouchAt(float pos, float timeDelta) {
|
||||
float curveVelocityThreshold = FLING_CURVE_THRESHOLD_VELOCITY * GeckoAppShell.getDpi() * MS_PER_FRAME;
|
||||
float maxVelocity = FLING_CURVE_MAXIMUM_VELOCITY * GeckoAppShell.getDpi() * MS_PER_FRAME;
|
||||
|
||||
float newVelocity = (mTouchPos - pos) / timeDelta * MS_PER_FRAME;
|
||||
|
||||
if (Math.abs(newVelocity) > curveVelocityThreshold && Math.abs(newVelocity) < maxVelocity) {
|
||||
float sign = Math.signum(newVelocity);
|
||||
newVelocity = newVelocity * sign;
|
||||
float scale = maxVelocity - curveVelocityThreshold;
|
||||
float functInp = (newVelocity - curveVelocityThreshold) / scale;
|
||||
float functOut = flingCurve(functInp);
|
||||
newVelocity = functOut * scale + curveVelocityThreshold;
|
||||
newVelocity = newVelocity * sign;
|
||||
}
|
||||
|
||||
mRecentVelocities[mRecentVelocityCount % FLING_VELOCITY_POINTS] = newVelocity;
|
||||
mRecentVelocityCount++;
|
||||
|
||||
|
@ -233,12 +233,12 @@ public class ImmutableViewportMetrics {
|
||||
public ImmutableViewportMetrics offsetViewportByAndClamp(float dx, float dy) {
|
||||
if (isRTL) {
|
||||
return setViewportOrigin(
|
||||
Math.min(pageRectRight - getWidthWithoutMargins(), Math.max(viewportRectLeft + dx, pageRectLeft)),
|
||||
Math.max(pageRectTop, Math.min(viewportRectTop + dy, pageRectBottom - getHeightWithoutMargins())));
|
||||
Math.min(pageRectRight - getWidth(), Math.max(viewportRectLeft + dx, pageRectLeft)),
|
||||
Math.max(pageRectTop, Math.min(viewportRectTop + dy, pageRectBottom - getHeight())));
|
||||
}
|
||||
return setViewportOrigin(
|
||||
Math.max(pageRectLeft, Math.min(viewportRectLeft + dx, pageRectRight - getWidthWithoutMargins())),
|
||||
Math.max(pageRectTop, Math.min(viewportRectTop + dy, pageRectBottom - getHeightWithoutMargins())));
|
||||
Math.max(pageRectLeft, Math.min(viewportRectLeft + dx, pageRectRight - getWidth())),
|
||||
Math.max(pageRectTop, Math.min(viewportRectTop + dy, pageRectBottom - getHeight())));
|
||||
}
|
||||
|
||||
public ImmutableViewportMetrics setPageRect(RectF pageRect, RectF cssPageRect) {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user