Merge fx-team to m-c. a=merge

This commit is contained in:
Ryan VanderMeulen 2015-01-12 15:14:56 -05:00
commit 786f6e25a8
121 changed files with 5696 additions and 323 deletions

View File

@ -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)

View File

@ -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() {

View File

@ -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

View File

@ -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)

View File

@ -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]);

View File

@ -12,7 +12,6 @@ JAR_MANIFESTS += ['jar.mn']
EXTRA_COMPONENTS += [
'BrowserDownloads.manifest',
'DownloadsStartup.js',
'DownloadsUI.js',
]
EXTRA_JS_MODULES += [

View File

@ -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");

View File

@ -180,6 +180,11 @@ var gSearchPane = {
Services.search.currentEngine =
document.getElementById("defaultEngine").selectedItem.engine;
}
},
loadAddEngines: function () {
window.opener.BrowserSearch.loadAddEngines();
window.document.documentElement.acceptDialog();
}
};

View File

@ -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>

View 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);

View File

@ -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>

View 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;
}

View 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']

View 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]

View File

@ -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");
});

View File

@ -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");
});

View File

@ -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");
});

View File

@ -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");
});

View File

@ -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);
});

View File

@ -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");
});

View File

@ -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");
});

View File

@ -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");
}
});

View File

@ -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");
});

View File

@ -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");
});

View File

@ -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");
});

View 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");
});

View File

@ -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>

View 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);
});

View File

@ -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]),

View File

@ -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);
}

View File

@ -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();

View File

@ -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);
},
/**

View File

@ -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)

View File

@ -5,6 +5,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DIRS += [
'animationinspector',
'app-manager',
'canvasdebugger',
'commandline',

View File

@ -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.

View File

@ -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,

View File

@ -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"

View File

@ -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]

View File

@ -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,

View File

@ -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.");
}
});

View File

@ -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);
}
});

View File

@ -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.");
}
});

View File

@ -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);
});

View File

@ -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);
}
});

View File

@ -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);
});

View File

@ -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);
}
});

View File

@ -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==";

View 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: *");
}

View File

@ -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!");
}
}

View File

@ -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

View File

@ -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>

View File

@ -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]

View File

@ -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();
}

View File

@ -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();
}

View 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);

View File

@ -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);

View File

@ -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();

View File

@ -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',

View File

@ -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",

View File

@ -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]

View 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.");
}

View 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.");
}

View 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
});
}

View 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
});
}

View 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;
}

View 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: []
}];

View File

@ -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,

View File

@ -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);

View 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;
}
};

View File

@ -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;

View File

@ -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";

View File

@ -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

View File

@ -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">

View File

@ -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.">

View File

@ -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=&#8734;
# 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

View File

@ -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…">

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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;

View File

@ -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;

View File

@ -16,7 +16,7 @@
width: 9em;
}
.requests-menu-domain {
.requests-menu-security-and-domain {
width: 16vw;
}

View File

@ -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)

View File

@ -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)

View File

@ -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);

View 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);
}

View File

@ -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

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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)

View File

@ -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) {

View File

@ -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;
}

View File

@ -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

View File

@ -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);

View File

@ -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));

View File

@ -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++;

View File

@ -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