Merge fx-team to m-c a=merge

This commit is contained in:
Wes Kocher 2014-11-21 15:58:51 -08:00
commit 8f93791278
37 changed files with 1200 additions and 255 deletions

View File

@ -11,12 +11,17 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
this.EXPORTED_SYMBOLS = ["LoopCalls"];
const EMAIL_OR_PHONE_RE = /^(:?\S+@\S+|\+\d+)$/;
XPCOMUtils.defineLazyModuleGetter(this, "MozLoopService",
"resource:///modules/loop/MozLoopService.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LOOP_SESSION_TYPE",
"resource:///modules/loop/MozLoopService.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LoopContacts",
"resource:///modules/loop/LoopContacts.jsm");
/**
* Attempts to open a websocket.
*
@ -269,7 +274,35 @@ let LoopCallsInternal = {
* "outgoing".
*/
_startCall: function(callData) {
this.conversationInProgress.id = MozLoopService.openChatWindow(callData);
const openChat = () => {
this.conversationInProgress.id = MozLoopService.openChatWindow(callData);
};
if (callData.type == "incoming" && ("callerId" in callData) &&
EMAIL_OR_PHONE_RE.test(callData.callerId)) {
LoopContacts.search({
q: callData.callerId,
field: callData.callerId.contains("@") ? "email" : "tel"
}, (err, contacts) => {
if (err) {
// Database error, helas!
openChat();
return;
}
for (let contact of contacts) {
if (contact.blocked) {
// Blocked! Send a busy signal back to the caller.
this._returnBusy(callData);
return;
}
}
openChat();
})
} else {
openChat();
}
},
/**

View File

@ -184,16 +184,10 @@ let LoopRoomsInternal = {
delete room.currSize;
}
this.rooms.set(room.roomToken, room);
// When a version is specified, all the data is already provided by this
// request.
if (version) {
eventEmitter.emit("update", room);
eventEmitter.emit("update" + ":" + room.roomToken, room);
} else {
// Next, request the detailed information for each room. If the request
// fails the room data will not be added to the map.
yield LoopRooms.promise("get", room.roomToken);
}
let eventName = orig ? "update" : "add";
eventEmitter.emit(eventName, room);
eventEmitter.emit(eventName + ":" + room.roomToken, room);
}
// If there's no rooms in the list, remove the guest created room flag, so that

View File

@ -89,7 +89,15 @@ const cloneValueInto = function(value, targetWindow) {
return cloneErrorObject(value, targetWindow);
}
return Cu.cloneInto(value, targetWindow);
let clone;
try {
clone = Cu.cloneInto(value, targetWindow);
} catch (ex) {
MozLoopService.log.debug("Failed to clone value:", value);
throw ex;
}
return clone;
};
/**
@ -106,11 +114,18 @@ const injectObjectAPI = function(api, targetWindow) {
Object.keys(api).forEach(func => {
injectedAPI[func] = function(...params) {
let lastParam = params.pop();
let callbackIsFunction = (typeof lastParam == "function");
// If the last parameter is a function, assume its a callback
// and wrap it differently.
if (lastParam && typeof lastParam === "function") {
if (callbackIsFunction) {
api[func](...params, function(...results) {
// When the function was garbage collected due to async events, like
// closing a window, we want to circumvent a JS error.
if (callbackIsFunction && typeof lastParam != "function") {
MozLoopService.log.debug(func + ": callback function was lost.");
return;
}
lastParam(...[cloneValueInto(r, targetWindow) for (r of results)]);
});
} else {
@ -231,7 +246,7 @@ function injectLoopAPI(targetWindow) {
enumerable: true,
writable: true,
value: function(conversationWindowId) {
return Cu.cloneInto(MozLoopService.getConversationWindowData(conversationWindowId),
return cloneValueInto(MozLoopService.getConversationWindowData(conversationWindowId),
targetWindow);
}
},
@ -495,9 +510,16 @@ function injectLoopAPI(targetWindow) {
writable: true,
value: function(sessionType, path, method, payloadObj, callback) {
// XXX Should really return a DOM promise here.
let callbackIsFunction = (typeof callback == "function");
MozLoopService.hawkRequest(sessionType, path, method, payloadObj).then((response) => {
callback(null, response.body);
}, hawkError => {
// When the function was garbage collected due to async events, like
// closing a window, we want to circumvent a JS error.
if (callbackIsFunction && typeof callback != "function") {
MozLoopService.log.debug("hawkRequest: callback function was lost.");
return;
}
// The hawkError.error property, while usually a string representing
// an HTTP response status message, may also incorrectly be a native
// error object that will cause the cloning function to fail.

View File

@ -18,8 +18,6 @@ const LOOP_SESSION_TYPE = {
// See LOG_LEVELS in Console.jsm. Common examples: "All", "Info", "Warn", & "Error".
const PREF_LOG_LEVEL = "loop.debug.loglevel";
const EMAIL_OR_PHONE_RE = /^(:?\S+@\S+|\+\d+)$/;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Promise.jsm");

View File

@ -132,7 +132,7 @@
background-position: center;
background-size: 16px 16px;
background-repeat: no-repeat;
background-color: fff;
background-color: #fff;
}
.contact > .details > .email {

View File

@ -47,6 +47,7 @@ support-files =
[browser_inspector_highlighter-rect_02.js]
[browser_inspector_highlighter-selector_01.js]
[browser_inspector_highlighter-selector_02.js]
[browser_inspector_highlighter-zoom.js]
[browser_inspector_iframe-navigation.js]
[browser_inspector_infobar_01.js]
[browser_inspector_initialization.js]

View File

@ -10,6 +10,7 @@ const TEST_URL = "data:text/html;charset=utf-8,<div>test</div>";
// IDs of all highlighter elements that we expect to find in the canvasFrame.
const ELEMENTS = ["box-model-root",
"box-model-elements",
"box-model-margin",
"box-model-border",
"box-model-padding",

View File

@ -33,7 +33,7 @@ add_task(function*() {
function* isHiddenByDefault(highlighterFront, inspector) {
info("Checking that the highlighter is hidden by default");
let hidden = yield getAttribute("css-transform-root", "hidden", highlighterFront);
let hidden = yield getAttribute("css-transform-elements", "hidden", highlighterFront);
ok(hidden, "The highlighter is hidden by default");
}
@ -58,7 +58,7 @@ function* isNotShownForUntransformed(highlighterFront, inspector) {
let node = yield getNodeFront("#untransformed", inspector);
yield highlighterFront.show(node);
let hidden = yield getAttribute("css-transform-root", "hidden", highlighterFront);
let hidden = yield getAttribute("css-transform-elements", "hidden", highlighterFront);
ok(hidden, "The highlighter is still hidden");
}
@ -68,7 +68,7 @@ function* isNotShownForInline(highlighterFront, inspector) {
let node = yield getNodeFront("#inline", inspector);
yield highlighterFront.show(node);
let hidden = yield getAttribute("css-transform-root", "hidden", highlighterFront);
let hidden = yield getAttribute("css-transform-elements", "hidden", highlighterFront);
ok(hidden, "The highlighter is still hidden");
}
@ -78,13 +78,13 @@ function* isVisibleWhenShown(highlighterFront, inspector) {
let node = yield getNodeFront("#transformed", inspector);
yield highlighterFront.show(node);
let hidden = yield getAttribute("css-transform-root", "hidden", highlighterFront);
let hidden = yield getAttribute("css-transform-elements", "hidden", highlighterFront);
ok(!hidden, "The highlighter is visible");
info("Hiding the highlighter");
yield highlighterFront.hide();
hidden = yield getAttribute("css-transform-root", "hidden", highlighterFront);
hidden = yield getAttribute("css-transform-elements", "hidden", highlighterFront);
ok(hidden, "The highlighter is hidden");
}

View File

@ -24,7 +24,7 @@ const TEST_DATA = [
let hidden = yield getAttribute("box-model-nodeinfobar-container", "hidden", toolbox);
ok(!hidden, "Node infobar is visible");
hidden = yield getAttribute("box-model-root", "hidden", toolbox);
hidden = yield getAttribute("box-model-elements", "hidden", toolbox);
ok(!hidden, "SVG container is visible");
for (let side of ["top", "right", "bottom", "left"]) {

View File

@ -0,0 +1,74 @@
/* 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";
// Test that the highlighter stays correctly positioned and has the right aspect
// ratio even when the page is zoomed in or out.
const TEST_URL = "data:text/html;charset=utf-8,<div>zoom me</div>";
// TEST_LEVELS entries should contain the following properties:
// - level: the zoom level to test
// - expected: the style attribute value to check for on the root highlighter
// element.
const TEST_LEVELS = [{
level: 2,
expected: "position:absolute;transform-origin:top left;transform:scale(0.5);width:200%;height:200%;"
}, {
level: 1,
expected: "position:absolute;width:100%;height:100%;"
}, {
level: .5,
expected: "position:absolute;transform-origin:top left;transform:scale(2);width:50%;height:50%;"
}];
add_task(function*() {
let {inspector, toolbox} = yield openInspectorForURL(TEST_URL);
info("Highlighting the test node");
yield hoverElement("div", inspector);
let isVisible = yield isHighlighting(toolbox);
ok(isVisible, "The highlighter is visible");
for (let {level, expected} of TEST_LEVELS) {
info("Zoom to level " + level + " and check that the highlighter is correct");
yield zoomPageTo(level, getHighlighterActorID(toolbox));
isVisible = yield isHighlighting(toolbox);
ok(isVisible, "The highlighter is still visible at zoom level " + level);
yield isNodeCorrectlyHighlighted(getNode("div"), toolbox);
info("Check that the highlighter root wrapper node was scaled down");
let style = yield getRootNodeStyle(toolbox);
is(style, expected, "The style attribute of the root element is correct");
}
gBrowser.removeCurrentTab();
});
function* hoverElement(selector, inspector) {
info("Hovering node " + selector + " in the markup view");
let container = yield getContainerForSelector(selector, inspector);
yield hoverContainer(container, inspector);
}
function* hoverContainer(container, inspector) {
let onHighlight = inspector.toolbox.once("node-highlight");
EventUtils.synthesizeMouse(container.tagLine, 2, 2, {type: "mousemove"},
inspector.markup.doc.defaultView);
yield onHighlight;
}
function* getRootNodeStyle(toolbox) {
let {data: value} = yield executeInContent("Test:GetHighlighterAttribute", {
nodeID: "box-model-root",
name: "style",
actorID: getHighlighterActorID(toolbox)
});
return value;
}

View File

@ -131,6 +131,35 @@ addMessageListener("Test:ChangeHighlightedNodeWaitForUpdate", function(msg) {
h.currentNode.setAttribute(name, value);
});
/**
* Change the zoom level of the page.
* Optionally subscribe to the box-model highlighter's update event and waiting
* for it to refresh before responding.
* @param {Object} msg The msg.data part expects the following properties
* - {Number} level The new zoom level
* - {String} actorID Optional. The highlighter actor ID
*/
addMessageListener("Test:ChangeZoomLevel", function(msg) {
let {level, actorID} = msg.data;
dumpn("Zooming page to " + level);
if (actorID) {
let {_highlighter: h} = getHighlighterActor(actorID);
h.once("updated", () => {
sendAsyncMessage("Test:ChangeZoomLevel");
});
}
let docShell = content.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell);
docShell.contentViewer.fullZoom = level;
if (!actorID) {
sendAsyncMessage("Test:ChangeZoomLevel");
}
});
/**
* Get the element at the given x/y coordinates.
* @param {Object} msg The msg.data part expects the following properties

View File

@ -395,7 +395,7 @@ let isRegionHidden = Task.async(function*(region, toolbox) {
*/
let isHighlighting = Task.async(function*(toolbox) {
let {data: value} = yield executeInContent("Test:GetHighlighterAttribute", {
nodeID: "box-model-root",
nodeID: "box-model-elements",
name: "hidden",
actorID: getHighlighterActorID(toolbox)
});
@ -555,6 +555,19 @@ let clickContainer = Task.async(function*(selector, inspector) {
return updated;
});
/**
* Zoom the current page to a given level.
* @param {Number} level The new zoom level.
* @param {String} actorID Optional highlighter actor ID. If provided, the
* returned promise will only resolve when the highlighter has updated to the
* new zoom level.
* @return {Promise}
*/
let zoomPageTo = Task.async(function*(level, actorID) {
yield executeInContent("Test:ChangeZoomLevel",
{level, actorID});
});
/**
* Simulate the mouse leaving the markup-view area
* @param {InspectorPanel} inspector The instance of InspectorPanel currently loaded in the toolbox

View File

@ -91,6 +91,8 @@ browser.jar:
content/browser/devtools/performance/controller.js (performance/controller.js)
content/browser/devtools/performance/views/main.js (performance/views/main.js)
content/browser/devtools/performance/views/overview.js (performance/views/overview.js)
content/browser/devtools/performance/views/details.js (performance/views/details.js)
content/browser/devtools/performance/views/call-tree.js (performance/views/call-tree.js)
#endif
content/browser/devtools/responsivedesign/resize-commands.js (responsivedesign/resize-commands.js)
content/browser/devtools/commandline.css (commandline/commandline.css)

View File

@ -22,6 +22,10 @@ devtools.lazyRequireGetter(this, "L10N",
"devtools/profiler/global", true);
devtools.lazyImporter(this, "LineGraphWidget",
"resource:///modules/devtools/Graphs.jsm");
devtools.lazyRequireGetter(this, "CallView",
"devtools/profiler/tree-view", true);
devtools.lazyRequireGetter(this, "ThreadNode",
"devtools/profiler/tree-model", true);
// Events emitted by the `PerformanceController`
const EVENTS = {
@ -36,7 +40,10 @@ const EVENTS = {
UI_STOP_RECORDING: "Performance:UI:StopRecording",
// Emitted by the OverviewView when more data has been rendered
OVERVIEW_RENDERED: "Performance:UI:OverviewRendered"
OVERVIEW_RENDERED: "Performance:UI:OverviewRendered",
// Emitted by the CallTreeView when a call tree has been rendered
CALL_TREE_RENDERED: "Performance:UI:CallTreeRendered"
};
/**
@ -51,8 +58,7 @@ let startupPerformance = Task.async(function*() {
yield promise.all([
PrefObserver.register(),
PerformanceController.initialize(),
PerformanceView.initialize(),
OverviewView.initialize()
PerformanceView.initialize()
]);
});
@ -63,8 +69,7 @@ let shutdownPerformance = Task.async(function*() {
yield promise.all([
PrefObserver.unregister(),
PerformanceController.destroy(),
PerformanceView.destroy(),
OverviewView.destroy()
PerformanceView.destroy()
]);
});
@ -147,6 +152,7 @@ EventEmitter.decorate(PerformanceController);
* Shortcuts for accessing various profiler preferences.
*/
const Prefs = new ViewHelpers.Prefs("devtools.profiler", {
showPlatformData: ["Bool", "ui.show-platform-data"]
});
/**

View File

@ -17,6 +17,8 @@
<script type="application/javascript" src="performance/controller.js"/>
<script type="application/javascript" src="performance/views/main.js"/>
<script type="application/javascript" src="performance/views/overview.js"/>
<script type="application/javascript" src="performance/views/details.js"/>
<script type="application/javascript" src="performance/views/call-tree.js"/>
<vbox class="theme-body" flex="1">
<toolbar id="performance-toolbar" class="devtools-toolbar">
@ -45,6 +47,35 @@
<box id="details-pane"
class="devtools-responsive-container"
flex="1">
<vbox class="call-tree" flex="1">
<hbox class="call-tree-headers-container">
<label class="plain call-tree-header"
type="duration"
crop="end"
value="&profilerUI.table.totalDuration;"/>
<label class="plain call-tree-header"
type="percentage"
crop="end"
value="&profilerUI.table.totalPercentage;"/>
<label class="plain call-tree-header"
type="self-duration"
crop="end"
value="&profilerUI.table.selfDuration;"/>
<label class="plain call-tree-header"
type="self-percentage"
crop="end"
value="&profilerUI.table.selfPercentage;"/>
<label class="plain call-tree-header"
type="samples"
crop="end"
value="&profilerUI.table.samples;"/>
<label class="plain call-tree-header"
type="function"
crop="end"
value="&profilerUI.table.function;"/>
</hbox>
<vbox class="call-tree-cells-container" flex="1"/>
</vbox>
</box>
</vbox>
</window>

View File

@ -31,3 +31,4 @@ support-files =
[browser_perf-ui-recording.js]
[browser_perf-overview-render-01.js]
[browser_perf-overview-render-02.js]
[browser_perf-details-calltree-render-01.js]

View File

@ -0,0 +1,34 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that the call tree view renders after recording.
*/
function spawnTest () {
let { panel } = yield initPerformance(SIMPLE_URL);
let { EVENTS, CallTreeView } = panel.panelWin;
let updated = 0;
CallTreeView.on(EVENTS.CALL_TREE_RENDERED, () => updated++);
yield startRecording(panel);
yield busyWait(100);
let rendered = once(CallTreeView, EVENTS.CALL_TREE_RENDERED);
yield stopRecording(panel);
yield rendered;
ok(true, "CallTreeView rendered on recording completed.");
yield startRecording(panel);
yield busyWait(100);
rendered = once(CallTreeView, EVENTS.CALL_TREE_RENDERED);
yield stopRecording(panel);
yield rendered;
ok(true, "CallTreeView rendered again after recording completed a second time.");
yield teardown(panel);
finish();
}

View File

@ -0,0 +1,73 @@
/* 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";
/**
* CallTree view containing profiler call tree, controlled by DetailsView.
*/
let CallTreeView = {
/**
* Sets up the view with event binding.
*/
initialize: function () {
this.el = $(".call-tree");
this._graphEl = $(".call-tree-cells-container");
this._stop = this._stop.bind(this);
PerformanceController.on(EVENTS.RECORDING_STOPPED, this._stop);
},
/**
* Unbinds events.
*/
destroy: function () {
PerformanceController.off(EVENTS.RECORDING_STOPPED, this._stop);
},
_stop: function (_, { profilerData }) {
this._prepareCallTree(profilerData);
},
/**
* Called when the recording is stopped and prepares data to
* populate the call tree.
*/
_prepareCallTree: function (profilerData, beginAt, endAt, options={}) {
let threadSamples = profilerData.profile.threads[0].samples;
let contentOnly = !Prefs.showPlatformData;
// TODO handle inverted tree bug 1102347
let invertTree = false;
let threadNode = new ThreadNode(threadSamples, contentOnly, beginAt, endAt, invertTree);
options.inverted = invertTree && threadNode.samples > 0;
this._populateCallTree(threadNode, options);
},
/**
* Renders the call tree.
*/
_populateCallTree: function (frameNode, options={}) {
let root = new CallView({
autoExpandDepth: options.inverted ? 0 : undefined,
frame: frameNode,
hidden: options.inverted,
inverted: options.inverted
});
// Clear out other graphs
this._graphEl.innerHTML = "";
root.attachTo(this._graphEl);
let contentOnly = !Prefs.showPlatformData;
root.toggleCategories(!contentOnly);
this.emit(EVENTS.CALL_TREE_RENDERED);
}
};
/**
* Convenient way of emitting events from the view.
*/
EventEmitter.decorate(CallTreeView);

View File

@ -0,0 +1,39 @@
/* 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";
/**
* Details view containing profiler call tree. Manages
* subviews and toggles visibility between them.
*/
let DetailsView = {
/**
* Sets up the view with event binding, initializes
* subviews.
*/
initialize: function () {
this.views = {
callTree: CallTreeView
};
// Initialize subviews
return promise.all([
CallTreeView.initialize()
]);
},
/**
* Unbinds events, destroys subviews.
*/
destroy: function () {
return promise.all([
CallTreeView.destroy()
]);
}
};
/**
* Convenient way of emitting events from the view.
*/
EventEmitter.decorate(DetailsView);

View File

@ -8,7 +8,7 @@
*/
let PerformanceView = {
/**
* Sets up the view with event binding.
* Sets up the view with event binding and main subviews.
*/
initialize: function () {
this._recordButton = $("#record-button");
@ -21,15 +21,25 @@ let PerformanceView = {
// Bind to controller events to unlock the record button
PerformanceController.on(EVENTS.RECORDING_STARTED, this._unlockRecordButton);
PerformanceController.on(EVENTS.RECORDING_STOPPED, this._unlockRecordButton);
return promise.all([
OverviewView.initialize(),
DetailsView.initialize()
]);
},
/**
* Unbinds events.
* Unbinds events and destroys subviews.
*/
destroy: function () {
this._recordButton.removeEventListener("click", this._onRecordButtonClick);
PerformanceController.off(EVENTS.RECORDING_STARTED, this._unlockRecordButton);
PerformanceController.off(EVENTS.RECORDING_STOPPED, this._unlockRecordButton);
return promise.all([
OverviewView.destroy(),
DetailsView.destroy()
]);
},
/**

View File

@ -52,15 +52,11 @@ XPCOMUtils.defineLazyGetter(this, "_stringBundle", function() {
.createBundle("chrome://browser/locale/taskbar.properties");
});
XPCOMUtils.defineLazyGetter(this, "PlacesUtils", function() {
Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
return PlacesUtils;
});
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "NetUtil", function() {
Components.utils.import("resource://gre/modules/NetUtil.jsm");
return NetUtil;
});
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "_idle",
"@mozilla.org/widget/idleservice;1",
@ -77,6 +73,16 @@ XPCOMUtils.defineLazyServiceGetter(this, "_winShellService",
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "gHistoryObserver", function() {
return Object.freeze({
onClearHistory() {
WinTaskbarJumpList.update();
},
QueryInterface: XPCOMUtils.generateQI(Ci.nsINavHistoryObserver),
__noSuchMethod__: () => {}, // Catch all of the other notifications.
});
});
/**
* Global functions
*/
@ -146,7 +152,7 @@ this.WinTaskbarJumpList =
/**
* Startup, shutdown, and update
*/
*/
startup: function WTBJL_startup() {
// exit if this isn't win7 or higher.
@ -155,7 +161,7 @@ this.WinTaskbarJumpList =
// Win shell shortcut maintenance. If we've gone through an update,
// this will update any pinned taskbar shortcuts. Not specific to
// jump lists, but this was a convienent place to call it.
// jump lists, but this was a convienent place to call it.
try {
// dev builds may not have helper.exe, ignore failures.
this._shortcutMaintenance();
@ -186,14 +192,6 @@ this.WinTaskbarJumpList =
_shutdown: function WTBJL__shutdown() {
this._shuttingDown = true;
// Correctly handle a clear history on shutdown. If there are no
// entries be sure to empty all history lists. Luckily Places caches
// this value, so it's a pretty fast call.
if (!PlacesUtils.history.hasHistoryEntries) {
this.update();
}
this._free();
},
@ -253,13 +251,13 @@ this.WinTaskbarJumpList =
/**
* Taskbar api wrappers
*/
*/
_startBuild: function WTBJL__startBuild() {
var removedItems = Cc["@mozilla.org/array;1"].
createInstance(Ci.nsIMutableArray);
this._builder.abortListBuild();
if (this._builder.initListBuild(removedItems)) {
if (this._builder.initListBuild(removedItems)) {
// Prior to building, delete removed items from history.
this._clearHistory(removedItems);
return true;
@ -283,7 +281,7 @@ this.WinTaskbarJumpList =
task.args, task.iconIndex, null);
items.appendElement(item, false);
}, this);
if (items.length > 0)
this._builder.addListToBuild(this._builder.JUMPLIST_CATEGORY_TASKS, items);
},
@ -294,11 +292,6 @@ this.WinTaskbarJumpList =
},
_buildFrequent: function WTBJL__buildFrequent() {
// If history is empty, just bail out.
if (!PlacesUtils.history.hasHistoryEntries) {
return;
}
// Windows supports default frequent and recent lists,
// but those depend on internal windows visit tracking
// which we don't populate. So we build our own custom
@ -324,7 +317,7 @@ this.WinTaskbarJumpList =
let title = aResult.title || aResult.uri;
let faviconPageUri = Services.io.newURI(aResult.uri, null, null);
let shortcut = this._getHandlerAppItem(title, title, aResult.uri, 1,
let shortcut = this._getHandlerAppItem(title, title, aResult.uri, 1,
faviconPageUri);
items.appendElement(shortcut, false);
this._frequentHashList.push(aResult.uri);
@ -334,11 +327,6 @@ this.WinTaskbarJumpList =
},
_buildRecent: function WTBJL__buildRecent() {
// If history is empty, just bail out.
if (!PlacesUtils.history.hasHistoryEntries) {
return;
}
var items = Cc["@mozilla.org/array;1"].
createInstance(Ci.nsIMutableArray);
// Frequent items will be skipped, so we select a double amount of
@ -386,8 +374,8 @@ this.WinTaskbarJumpList =
* Jump list item creation helpers
*/
_getHandlerAppItem: function WTBJL__getHandlerAppItem(name, description,
args, iconIndex,
_getHandlerAppItem: function WTBJL__getHandlerAppItem(name, description,
args, iconIndex,
faviconPageUri) {
var file = Services.dirsvc.get("XREExeF", Ci.nsILocalFile);
@ -469,7 +457,7 @@ this.WinTaskbarJumpList =
/**
* Prefs utilities
*/
*/
_refreshPrefs: function WTBJL__refreshPrefs() {
this._enabled = _prefs.getBoolPref(PREF_TASKBAR_ENABLED);
@ -481,7 +469,7 @@ this.WinTaskbarJumpList =
/**
* Init and shutdown utilities
*/
*/
_initTaskbar: function WTBJL__initTaskbar() {
this._builder = _taskbarService.createJumpListBuilder();
@ -498,12 +486,14 @@ this.WinTaskbarJumpList =
Services.obs.addObserver(this, "profile-before-change", false);
Services.obs.addObserver(this, "browser:purge-session-history", false);
_prefs.addObserver("", this, false);
PlacesUtils.history.addObserver(gHistoryObserver, false);
},
_freeObs: function WTBJL__freeObs() {
Services.obs.removeObserver(this, "profile-before-change");
Services.obs.removeObserver(this, "browser:purge-session-history");
_prefs.removeObserver("", this);
PlacesUtils.history.removeObserver(gHistoryObserver);
},
_updateTimer: function WTBJL__updateTimer() {

View File

@ -32,3 +32,226 @@
#record-button[locked] {
pointer-events: none;
}
/* Profile call tree */
.theme-dark .call-tree-headers-container {
border-top: 1px solid #000;
}
.theme-light .call-tree-headers-container {
border-top: 1px solid #aaa;
}
.call-tree-cells-container {
/* Hack: force hardware acceleration */
transform: translateZ(1px);
overflow: auto;
}
.call-tree-cells-container[categories-hidden] .call-tree-category {
display: none;
}
.call-tree-header[type="duration"],
.call-tree-cell[type="duration"],
.call-tree-header[type="self-duration"],
.call-tree-cell[type="self-duration"] {
width: 9em;
}
.call-tree-header[type="percentage"],
.call-tree-cell[type="percentage"],
.call-tree-header[type="self-percentage"],
.call-tree-cell[type="self-percentage"] {
width: 6em;
}
.call-tree-header[type="samples"],
.call-tree-cell[type="samples"] {
width: 5em;
}
.call-tree-header[type="function"],
.call-tree-cell[type="function"] {
-moz-box-flex: 1;
}
.call-tree-header,
.call-tree-cell {
-moz-box-align: center;
overflow: hidden;
padding: 1px 4px;
}
.call-tree-header:not(:last-child),
.call-tree-cell:not(:last-child) {
-moz-border-end: 1px solid;
}
.theme-dark .call-tree-header,
.theme-dark .call-tree-cell {
-moz-border-end-color: rgba(255,255,255,0.15);
color: #8fa1b2; /* Body Text */
}
.theme-light .call-tree-header,
.theme-light .call-tree-cell {
-moz-border-end-color: rgba(0,0,0,0.15);
color: #18191a; /* Body Text */
}
.call-tree-header:not(:last-child) {
text-align: center;
}
.call-tree-cell:not(:last-child) {
text-align: end;
}
.theme-dark .call-tree-header {
background-color: #252c33; /* Tab Toolbar */
}
.theme-light .call-tree-header {
background-color: #ebeced; /* Tab Toolbar */
}
.theme-dark .call-tree-item:last-child:not(:focus) {
border-bottom: 1px solid rgba(255,255,255,0.15);
}
.theme-light .call-tree-item:last-child:not(:focus) {
border-bottom: 1px solid rgba(0,0,0,0.15);
}
.theme-dark .call-tree-item:nth-child(2n) {
background-color: rgba(29,79,115,0.15);
}
.theme-light .call-tree-item:nth-child(2n) {
background-color: rgba(76,158,217,0.1);
}
.theme-dark .call-tree-item:hover {
background-color: rgba(29,79,115,0.25);
}
.theme-light .call-tree-item:hover {
background-color: rgba(76,158,217,0.2);
}
.theme-dark .call-tree-item:focus {
background-color: #1d4f73; /* Select Highlight Blue */
}
.theme-light .call-tree-item:focus {
background-color: #4c9ed9; /* Select Highlight Blue */
}
.call-tree-item:focus label {
color: #f5f7fa !important; /* Light foreground text */
}
.theme-dark .call-tree-item:focus .call-tree-cell {
-moz-border-end-color: rgba(0,0,0,0.3);
}
.theme-light .call-tree-item:focus .call-tree-cell {
-moz-border-end-color: rgba(255,255,255,0.5);
}
.call-tree-item:not([origin="content"]) .call-tree-name,
.call-tree-item:not([origin="content"]) .call-tree-url,
.call-tree-item:not([origin="content"]) .call-tree-line {
/* Style chrome and non-JS nodes differently. */
opacity: 0.6;
}
.call-tree-url {
-moz-margin-start: 4px !important;
cursor: pointer;
}
.call-tree-url:hover {
text-decoration: underline;
}
.theme-dark .call-tree-url {
color: #46afe3;
}
.theme-light .call-tree-url {
color: #0088cc;
}
.theme-dark .call-tree-line {
color: #d96629;
}
.theme-light .call-tree-line {
color: #f13c00;
}
.call-tree-host {
-moz-margin-start: 8px !important;
font-size: 90%;
}
.theme-dark .call-tree-host {
color: #8fa1b2;
}
.theme-light .call-tree-host {
color: #8fa1b2;
}
.call-tree-url[value=""],
.call-tree-line[value=""],
.call-tree-host[value=""] {
display: none;
}
.call-tree-zoom {
-moz-appearance: none;
background-color: transparent;
background-position: center;
background-repeat: no-repeat;
background-size: 11px;
min-width: 11px;
-moz-margin-start: 8px !important;
cursor: zoom-in;
opacity: 0;
}
.theme-dark .call-tree-zoom {
background-image: url(magnifying-glass.png);
}
.theme-light .call-tree-zoom {
background-image: url(magnifying-glass-light.png);
}
@media (min-resolution: 2dppx) {
.theme-dark .call-tree-zoom {
background-image: url(magnifying-glass@2x.png);
}
.theme-light .call-tree-zoom {
background-image: url(magnifying-glass-light@2x.png);
}
}
.call-tree-item:hover .call-tree-zoom {
transition: opacity 0.3s ease-in;
opacity: 1;
}
.call-tree-item:hover .call-tree-zoom:hover {
opacity: 0;
}
.call-tree-category {
transform: scale(0.75);
transform-origin: center right;
}

View File

@ -91,7 +91,7 @@
background-color: ThreeDShadow;
}
#navigator-toolbox > toolbar:not(:-moz-lwtheme) {
#navigator-toolbox > toolbar {
-moz-appearance: none;
border-style: none;
}

View File

@ -1953,12 +1953,12 @@ public class BrowserApp extends GeckoApp
}
/**
* Enters editing mode with the specified URL. This method will
* always open the HISTORY page on about:home.
* Enters editing mode with the specified URL. If a null
* url is given, the empty String will be used instead.
*/
private void enterEditingMode(String url) {
if (url == null) {
throw new IllegalArgumentException("Cannot handle null URLs in enterEditingMode");
url = "";
}
if (mBrowserToolbar.isEditing() || mBrowserToolbar.isAnimating()) {

View File

@ -241,9 +241,10 @@ public class TabsPanel extends LinearLayout
Resources resources = tabsContainer.getContext().getResources();
int screenHeight = resources.getDisplayMetrics().heightPixels;
int actionBarHeight = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height);
if(NewTabletUI.isEnabled(tabsContainer.getContext())){
return screenHeight;
return screenHeight - actionBarHeight;
}
PanelView panelView = tabsContainer.getCurrentPanelView();
@ -251,8 +252,6 @@ public class TabsPanel extends LinearLayout
return resources.getDimensionPixelSize(R.dimen.tabs_layout_horizontal_height);
}
int actionBarHeight = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height);
Rect windowRect = new Rect();
tabsContainer.getWindowVisibleDisplayFrame(windowRect);
int windowHeight = windowRect.bottom - windowRect.top;

View File

@ -0,0 +1,8 @@
function handleRequest(request, response)
{
let page = "<!DOCTYPE html><html><head><script>window.opener = null; location.replace('" + request.queryString + "')</script></head><body><p>Redirecting...</p></body></html>";
response.setStatusLine("1.0", 200, "OK");
response.setHeader("Content-Type", "text/html; charset=utf-8", false);
response.write(page);
}

View File

@ -104,6 +104,7 @@ skip-if = android_version == "10"
[testDebuggerServer]
[testDeviceSearchEngine]
[testFilePicker]
[testHistoryService]
[testJNI]
# [testMozPay] # see bug 945675
[testNetworkManager]

View File

@ -0,0 +1,5 @@
function handleRequest(request, response)
{
response.setStatusLine(request.httpVersion, 301, "Moved Permanently");
response.setHeader("Location", request.queryString, false);
}

View File

@ -0,0 +1,12 @@
/* 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/. */
package org.mozilla.gecko.tests;
public class testHistoryService extends JavascriptTest {
public testHistoryService() {
super("testHistoryService.js");
}
}

View File

@ -0,0 +1,130 @@
// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
/* 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 } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
function ok(passed, text) {
do_report_result(passed, text, Components.stack.caller, false);
}
// Make the timer global so it doesn't get GC'd
let gTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
function sleep(wait) {
return new Promise((resolve, reject) => {
do_print("sleep start");
gTimer.initWithCallback({
notify: function () {
do_print("sleep end");
resolve();
},
}, wait, gTimer.TYPE_ONE_SHOT);
});
}
function promiseLoadEvent(browser, url, eventType="load") {
return new Promise((resolve, reject) => {
do_print("Wait browser event: " + eventType);
function handle(event) {
// Since we'll be redirecting, don't make assumptions about the given URL and the loaded URL
if (event.target != browser.contentDocument || event.target.location.href == "about:blank") {
do_print("Skipping spurious '" + eventType + "' event" + " for " + event.target.location.href);
return;
}
browser.removeEventListener(eventType, handle, true);
do_print("Browser event received: " + eventType);
resolve(event);
}
browser.addEventListener(eventType, handle, true);
if (url) {
browser.loadURI(url);
}
});
}
// Wait 4 seconds for the pending visits to flush (which should happen in 3 seconds)
const PENDING_VISIT_WAIT = 4000;
// Manage the saved history visits so we can compare in the tests
let gVisitURLs = [];
function visitObserver(subject, topic, data) {
let uri = subject.QueryInterface(Ci.nsIURI);
do_print("Observer: " + uri.spec);
gVisitURLs.push(uri.spec);
};
// Track the <browser> where the tests are happening
let gBrowser;
add_test(function setup_browser() {
let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
let BrowserApp = chromeWin.BrowserApp;
do_register_cleanup(function cleanup() {
Services.obs.removeObserver(visitObserver, "link-visited");
BrowserApp.closeTab(BrowserApp.getTabForBrowser(browser));
});
Services.obs.addObserver(visitObserver, "link-visited", false);
// Load a blank page
let url = "about:blank";
gBrowser = BrowserApp.addTab(url, { selected: true, parentId: BrowserApp.selectedTab.id }).browser;
gBrowser.addEventListener("load", function startTests(event) {
gBrowser.removeEventListener("load", startTests, true);
Services.tm.mainThread.dispatch(run_next_test, Ci.nsIThread.DISPATCH_NORMAL);
}, true);
});
add_task(function* () {
// Wait for any initial page loads to be saved to history
yield sleep(PENDING_VISIT_WAIT);
// Load a simple HTML page with no redirects
gVisitURLs = [];
yield promiseLoadEvent(gBrowser, "http://example.org/tests/robocop/robocop_blank_01.html");
yield sleep(PENDING_VISIT_WAIT);
do_print("visit counts: " + gVisitURLs.length);
ok(gVisitURLs.length == 1, "Simple visit makes 1 history item");
do_print("visit URL: " + gVisitURLs[0]);
ok(gVisitURLs[0] == "http://example.org/tests/robocop/robocop_blank_01.html", "Simple visit makes final history item");
// Load a simple HTML page via a 301 temporary redirect
gVisitURLs = [];
yield promiseLoadEvent(gBrowser, "http://example.org/tests/robocop/simple_redirect.sjs?http://example.org/tests/robocop/robocop_blank_02.html");
yield sleep(PENDING_VISIT_WAIT);
do_print("visit counts: " + gVisitURLs.length);
ok(gVisitURLs.length == 1, "Simple 301 redirect makes 1 history item");
do_print("visit URL: " + gVisitURLs[0]);
ok(gVisitURLs[0] == "http://example.org/tests/robocop/robocop_blank_02.html", "Simple 301 redirect makes final history item");
// Load a simple HTML page via a JavaScript redirect
gVisitURLs = [];
yield promiseLoadEvent(gBrowser, "http://example.org/tests/robocop/javascript_redirect.sjs?http://example.org/tests/robocop/robocop_blank_03.html");
yield sleep(PENDING_VISIT_WAIT);
do_print("visit counts: " + gVisitURLs.length);
ok(gVisitURLs.length == 2, "JavaScript redirect makes 2 history items");
do_print("visit URL 1: " + gVisitURLs[0]);
ok(gVisitURLs[0] == "http://example.org/tests/robocop/javascript_redirect.sjs?http://example.org/tests/robocop/robocop_blank_03.html", "JavaScript redirect makes intermediate history item");
do_print("visit URL 2: " + gVisitURLs[1]);
ok(gVisitURLs[1] == "http://example.org/tests/robocop/robocop_blank_03.html", "JavaScript redirect makes final history item");
});
run_next_test();

View File

@ -15,7 +15,7 @@ function ok(passed, text) {
do_report_result(passed, text, Components.stack.caller, false);
}
function promiseLoadEvent(browser, url, eventType="load") {
function promiseLoadEvent(browser, url, eventType="load", runBeforeLoad) {
return new Promise((resolve, reject) => {
do_print("Wait browser event: " + eventType);
@ -30,7 +30,11 @@ function promiseLoadEvent(browser, url, eventType="load") {
resolve(event);
}
browser.addEventListener(eventType, handle, true, true);
browser.addEventListener(eventType, handle, true);
if (runBeforeLoad) {
runBeforeLoad();
}
if (url) {
browser.loadURI(url);
}
@ -122,6 +126,19 @@ add_task(function* () {
yield promiseLoadEvent(browser, "http://tracking.example.org/tests/robocop/tracking_bad.html");
Messaging.sendRequest({ type: "Test:Expected", expected: "tracking_content_blocked" });
// Simulate a click on the "Disable protection" button in the site identity popup.
// We need to wait for a "load" event because "Session:Reload" will cause a full page reload.
yield promiseLoadEvent(browser, undefined, undefined, () => {
Services.obs.notifyObservers(null, "Session:Reload", "{\"allowContent\":true,\"contentType\":\"tracking\"}");
});
Messaging.sendRequest({ type: "Test:Expected", expected: "tracking_content_loaded" });
// Simulate a click on the "Enable protection" button in the site identity popup.
yield promiseLoadEvent(browser, undefined, undefined, () => {
Services.obs.notifyObservers(null, "Session:Reload", "{\"allowContent\":false,\"contentType\":\"tracking\"}");
});
Messaging.sendRequest({ type: "Test:Expected", expected: "tracking_content_blocked" });
// Disable Tracking Protection
Services.prefs.setBoolPref(PREF, false);

View File

@ -287,8 +287,8 @@ public class SiteIdentityPopup extends ArrowPopup {
public void onButtonClick(DoorHanger dh, String tag) {
try {
JSONObject data = new JSONObject();
String allowType = (dh == mMixedContentNotification ? "allowMixedContent" : "allowTrackingContent");
data.put(allowType, tag.equals("disable"));
data.put("allowContent", tag.equals("disable"));
data.put("contentType", (dh == mMixedContentNotification ? "mixed" : "tracking"));
GeckoEvent e = GeckoEvent.createBroadcastEvent("Session:Reload", data.toString());
GeckoAppShell.sendEventToGecko(e);

View File

@ -1548,14 +1548,34 @@ var BrowserApp = {
// Check to see if this is a message to enable/disable mixed content blocking.
if (aData) {
let allowMixedContent = JSON.parse(aData).allowMixedContent;
if (allowMixedContent) {
// Set a flag to disable mixed content blocking.
flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT;
} else {
// Set mixedContentChannel to null to re-enable mixed content blocking.
let docShell = browser.webNavigation.QueryInterface(Ci.nsIDocShell);
docShell.mixedContentChannel = null;
let data = JSON.parse(aData);
if (data.contentType === "mixed") {
if (data.allowContent) {
// Set a flag to disable mixed content blocking.
flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT;
} else {
// Set mixedContentChannel to null to re-enable mixed content blocking.
let docShell = browser.webNavigation.QueryInterface(Ci.nsIDocShell);
docShell.mixedContentChannel = null;
}
} else if (data.contentType === "tracking") {
if (data.allowContent) {
// Convert document URI into the format used by
// nsChannelClassifier::ShouldEnableTrackingProtection
// (any scheme turned into https is correct)
let normalizedUrl = Services.io.newURI("https://" + browser.currentURI.hostPort, null, null);
// Add the current host in the 'trackingprotection' consumer of
// the permission manager using a normalized URI. This effectively
// places this host on the tracking protection white list.
Services.perms.add(normalizedUrl, "trackingprotection", Services.perms.ALLOW_ACTION);
Telemetry.addData("TRACKING_PROTECTION_EVENTS", 1);
} else {
// Remove the current host from the 'trackingprotection' consumer
// of the permission manager. This effectively removes this host
// from the tracking protection white list (any list actually).
Services.perms.remove(browser.currentURI.host, "trackingprotection");
Telemetry.addData("TRACKING_PROTECTION_EVENTS", 2);
}
}
}
@ -5162,7 +5182,7 @@ var BrowserEventHandler = {
let win = BrowserApp.selectedBrowser.contentWindow;
try {
let cwu = win.top.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
cwu.sendMouseEventToWindow(aName, aX, aY, 0, 1, 0, true, 0, Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH);
cwu.sendMouseEventToWindow(aName, aX, aY, 0, 1, 0, true, 0, Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH, false);
} catch(e) {
Cu.reportError(e);
}

View File

@ -228,13 +228,6 @@ nsAndroidHistory::VisitURI(nsIURI *aURI, nsIURI *aLastVisitedURI, uint32_t aFlag
return NS_OK;
}
if (aFlags & VisitFlags::REDIRECT_SOURCE || aFlags & VisitFlags::REDIRECT_PERMANENT || aFlags & VisitFlags::REDIRECT_TEMPORARY) {
// aLastVisitedURI redirected to aURI. We want to ignore aLastVisitedURI,
// so remove the pending visit. We want to give aURI a chance to be saved,
// so don't return early.
RemovePendingVisitURI(aLastVisitedURI);
}
// Silently return if URI is something we shouldn't add to DB.
bool canAdd;
nsresult rv = CanAddURI(aURI, &canAdd);
@ -244,6 +237,15 @@ nsAndroidHistory::VisitURI(nsIURI *aURI, nsIURI *aLastVisitedURI, uint32_t aFlag
}
if (aLastVisitedURI) {
if (aFlags & VisitFlags::REDIRECT_SOURCE ||
aFlags & VisitFlags::REDIRECT_PERMANENT ||
aFlags & VisitFlags::REDIRECT_TEMPORARY) {
// aLastVisitedURI redirected to aURI. We want to ignore aLastVisitedURI,
// so remove the pending visit. We want to give aURI a chance to be saved,
// so don't return early.
RemovePendingVisitURI(aLastVisitedURI);
}
bool same;
rv = aURI->Equals(aLastVisitedURI, &same);
NS_ENSURE_SUCCESS(rv, rv);

View File

@ -50,7 +50,7 @@ LayoutHelpers.prototype = {
}
let [xOffset, yOffset] = this.getFrameOffsets(node);
let scale = this.calculateScale(node);
let scale = LayoutHelpers.getCurrentZoom(node);
return {
p1: {
@ -90,19 +90,6 @@ LayoutHelpers.prototype = {
};
},
/**
* Get the current zoom factor applied to the container window of a given node
* @param {DOMNode}
* The node for which the zoom factor should be calculated
* @return {Number}
*/
calculateScale: function(node) {
let win = node.ownerDocument.defaultView;
let winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
return winUtils.fullZoom;
},
/**
* Compute the absolute position and the dimensions of a node, relativalely
* to the root window.
@ -411,7 +398,7 @@ LayoutHelpers.prototype = {
let xOffset = 0;
let yOffset = 0;
let frameWin = node.ownerDocument.defaultView;
let scale = this.calculateScale(node);
let scale = LayoutHelpers.getCurrentZoom(node);
while (true) {
// Are we in the top-level window?
@ -454,7 +441,7 @@ LayoutHelpers.prototype = {
return;
}
let scale = this.calculateScale(node);
let scale = LayoutHelpers.getCurrentZoom(node);
// Find out the offset of the node in its current frame
let offsetLeft = 0;
@ -607,3 +594,25 @@ LayoutHelpers.isShadowAnonymous = function(node) {
// is not native anonymous
return parent.shadowRoot && parent.shadowRoot.contains(node);
};
/**
* Get the current zoom factor applied to the container window of a given node.
* Container windows are used as a weakmap key to store the corresponding
* nsIDOMWindowUtils instance to avoid querying it every time.
*
* @param {DOMNode} The node for which the zoom factor should be calculated
* @return {Number}
*/
let windowUtils = new WeakMap;
LayoutHelpers.getCurrentZoom = function(node, map = z=>z) {
let win = node.ownerDocument.defaultView;
let utils = windowUtils.get(win);
if (utils) {
return utils.fullZoom;
}
utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
windowUtils.set(win, utils);
return utils.fullZoom;
};

View File

@ -26,7 +26,7 @@
/* Box model highlighter */
:-moz-native-anonymous .box-model-container {
:-moz-native-anonymous .box-model-regions {
opacity: 0.4;
}

View File

@ -498,6 +498,40 @@ CanvasFrameAnonymousContentHelper.prototype = {
return null;
}
return this._content;
},
/**
* The canvasFrame anonymous content container gets zoomed in/out with the
* page. If this is unwanted, i.e. if you want the inserted element to remain
* unzoomed, then this method can be used.
*
* Consumers of the CanvasFrameAnonymousContentHelper should call this method,
* it isn't executed automatically. Typically, AutoRefreshHighlighter can call
* it when _update is executed.
*
* The matching element will be scaled down or up by 1/zoomLevel (using css
* transform) to cancel the current zoom. The element's width and height
* styles will also be set according to the scale. Finally, the element's
* position will be set as absolute.
*
* Note that if the matching element already has an inline style attribute, it
* *won't* be preserved.
*
* @param {DOMNode} node This node is used to determine which container window
* should be used to read the current zoom value.
* @param {String} id The ID of the root element inserted with this API.
*/
scaleRootElement: function(node, id) {
let zoom = LayoutHelpers.getCurrentZoom(node);
let value = "position:absolute;width:100%;height:100%;";
if (zoom !== 1) {
value = "position:absolute;";
value += "transform-origin:top left;transform:scale(" + (1/zoom) + ");";
value += "width:" + (100*zoom) + "%;height:" + (100*zoom) + "%;";
}
this.setAttributeForElement(id, "style", value);
}
};
@ -699,35 +733,36 @@ AutoRefreshHighlighter.prototype = {
*
* Structure:
* <div class="highlighter-container">
* <svg class="box-model-root" hidden="true">
* <g class="box-model-container">
* <polygon class="box-model-margin" points="317,122 747,36 747,181 317,267" />
* <polygon class="box-model-border" points="317,128 747,42 747,161 317,247" />
* <polygon class="box-model-padding" points="323,127 747,42 747,161 323,246" />
* <polygon class="box-model-content" points="335,137 735,57 735,152 335,232" />
* </g>
* <line class="box-model-guide-top" x1="0" y1="592" x2="99999" y2="592" />
* <line class="box-model-guide-right" x1="735" y1="0" x2="735" y2="99999" />
* <line class="box-model-guide-bottom" x1="0" y1="612" x2="99999" y2="612" />
* <line class="box-model-guide-left" x1="334" y1="0" x2="334" y2="99999" />
* </svg>
* <div class="highlighter-nodeinfobar-container">
* <div class="highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-top" />
* <div class="highlighter-nodeinfobar">
* <div class="highlighter-nodeinfobar-text" align="center" flex="1">
* <span class="highlighter-nodeinfobar-tagname">Node name</span>
* <span class="highlighter-nodeinfobar-id">Node id</span>
* <span class="highlighter-nodeinfobar-classes">.someClass</span>
* <span class="highlighter-nodeinfobar-pseudo-classes">:hover</span>
* <div class="box-model-root">
* <svg class="box-model-elements" hidden="true">
* <g class="box-model-regions">
* <polygon class="box-model-margin" points="..." />
* <polygon class="box-model-border" points="..." />
* <polygon class="box-model-padding" points="..." />
* <polygon class="box-model-content" points="..." />
* </g>
* <line class="box-model-guide-top" x1="..." y1="..." x2="..." y2="..." />
* <line class="box-model-guide-right" x1="..." y1="..." x2="..." y2="..." />
* <line class="box-model-guide-bottom" x1="..." y1="..." x2="..." y2="..." />
* <line class="box-model-guide-left" x1="..." y1="..." x2="..." y2="..." />
* </svg>
* <div class="box-model-nodeinfobar-container">
* <div class="box-model-nodeinfobar-arrow highlighter-nodeinfobar-arrow-top" />
* <div class="box-model-nodeinfobar">
* <div class="box-model-nodeinfobar-text" align="center">
* <span class="box-model-nodeinfobar-tagname">Node name</span>
* <span class="box-model-nodeinfobar-id">Node id</span>
* <span class="box-model-nodeinfobar-classes">.someClass</span>
* <span class="box-model-nodeinfobar-pseudo-classes">:hover</span>
* </div>
* </div>
* <div class="box-model-nodeinfobar-arrow box-model-nodeinfobar-arrow-bottom"/>
* </div>
* <div class="highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-bottom"/>
* </div>
* </div>
*/
function BoxModelHighlighter(tabActor) {
AutoRefreshHighlighter.call(this, tabActor);
EventEmitter.decorate(this);
this.markup = new CanvasFrameAnonymousContentHelper(this.tabActor,
this._buildMarkup.bind(this));
@ -764,102 +799,142 @@ BoxModelHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.prototype
let highlighterContainer = doc.createElement("div");
highlighterContainer.className = "highlighter-container";
// Building the SVG element with its polygons and lines
let svgRoot = this._createSVGNode("svg", highlighterContainer, {
"id": "root",
"class": "root",
"width": "100%",
"height": "100%",
"style": "width:100%;height:100%;",
"hidden": "true"
// Build the root wrapper, used to adapt to the page zoom.
let rootWrapper = createNode(this.win, {
parent: highlighterContainer,
attributes: {
"id": "root",
"class": "root"
},
prefix: this.ID_CLASS_PREFIX
});
let boxModelContainer = this._createSVGNode("g", svgRoot, {
"class": "container"
// Building the SVG element with its polygons and lines
let svg = createSVGNode(this.win, {
nodeType: "svg",
parent: rootWrapper,
attributes: {
"id": "elements",
"width": "100%",
"height": "100%",
"style": "width:100%;height:100%;",
"hidden": "true"
},
prefix: this.ID_CLASS_PREFIX
});
let regions = createSVGNode(this.win, {
nodeType: "g",
parent: svg,
attributes: {
"class": "regions"
},
prefix: this.ID_CLASS_PREFIX
});
for (let region of BOX_MODEL_REGIONS) {
this._createSVGNode("polygon", boxModelContainer, {
"class": region,
"id": region
createSVGNode(this.win, {
nodeType: "polygon",
parent: regions,
attributes: {
"class": region,
"id": region
},
prefix: this.ID_CLASS_PREFIX
});
}
for (let side of BOX_MODEL_SIDES) {
this._createSVGNode("line", svgRoot, {
"class": "guide-" + side,
"id": "guide-" + side,
"stroke-width": GUIDE_STROKE_WIDTH
createSVGNode(this.win, {
nodeType: "line",
parent: svg,
attributes: {
"class": "guide-" + side,
"id": "guide-" + side,
"stroke-width": GUIDE_STROKE_WIDTH
},
prefix: this.ID_CLASS_PREFIX
});
}
highlighterContainer.appendChild(svgRoot);
// Building the nodeinfo bar markup
let infobarContainer = this._createNode("div", highlighterContainer, {
"class": "nodeinfobar-container",
"id": "nodeinfobar-container",
"position": "top",
"hidden": "true"
let infobarContainer = createNode(this.win, {
parent: rootWrapper,
attributes: {
"class": "nodeinfobar-container",
"id": "nodeinfobar-container",
"position": "top",
"hidden": "true"
},
prefix: this.ID_CLASS_PREFIX
});
let nodeInfobar = this._createNode("div", infobarContainer, {
"class": "nodeinfobar"
let nodeInfobar = createNode(this.win, {
parent: infobarContainer,
attributes: {
"class": "nodeinfobar"
},
prefix: this.ID_CLASS_PREFIX
});
let texthbox = this._createNode("div", nodeInfobar, {
"class": "nodeinfobar-text"
let texthbox = createNode(this.win, {
parent: nodeInfobar,
attributes: {
"class": "nodeinfobar-text"
},
prefix: this.ID_CLASS_PREFIX
});
this._createNode("span", texthbox, {
"class": "nodeinfobar-tagname",
"id": "nodeinfobar-tagname"
createNode(this.win, {
nodeType: "span",
parent: texthbox,
attributes: {
"class": "nodeinfobar-tagname",
"id": "nodeinfobar-tagname"
},
prefix: this.ID_CLASS_PREFIX
});
this._createNode("span", texthbox, {
"class": "nodeinfobar-id",
"id": "nodeinfobar-id"
createNode(this.win, {
nodeType: "span",
parent: texthbox,
attributes: {
"class": "nodeinfobar-id",
"id": "nodeinfobar-id"
},
prefix: this.ID_CLASS_PREFIX
});
this._createNode("span", texthbox, {
"class": "nodeinfobar-classes",
"id": "nodeinfobar-classes"
createNode(this.win, {
nodeType: "span",
parent: texthbox,
attributes: {
"class": "nodeinfobar-classes",
"id": "nodeinfobar-classes"
},
prefix: this.ID_CLASS_PREFIX
});
this._createNode("span", texthbox, {
"class": "nodeinfobar-pseudo-classes",
"id": "nodeinfobar-pseudo-classes"
createNode(this.win, {
nodeType: "span",
parent: texthbox,
attributes: {
"class": "nodeinfobar-pseudo-classes",
"id": "nodeinfobar-pseudo-classes"
},
prefix: this.ID_CLASS_PREFIX
});
this._createNode("span", texthbox, {
"class": "nodeinfobar-dimensions",
"id": "nodeinfobar-dimensions"
createNode(this.win, {
nodeType: "span",
parent: texthbox,
attributes: {
"class": "nodeinfobar-dimensions",
"id": "nodeinfobar-dimensions"
},
prefix: this.ID_CLASS_PREFIX
});
return highlighterContainer;
},
_createSVGNode: function(nodeType, parent, attributes={}) {
return this._createNode(nodeType, parent, attributes, SVG_NS);
},
_createNode: function(nodeType, parent, attributes={}, namespace=null) {
let node;
if (namespace) {
node = this.win.document.createElementNS(namespace, nodeType);
} else {
node = this.win.document.createElement(nodeType);
}
for (let name in attributes) {
let value = attributes[name];
if (name === "class" || name === "id") {
value = this.ID_CLASS_PREFIX + value
}
node.setAttribute(name, value);
}
parent.appendChild(node);
return node;
},
/**
* Destroy the nodes. Remove listeners.
*/
@ -952,15 +1027,15 @@ BoxModelHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.prototype
* Hide the box model
*/
_hideBoxModel: function() {
this.markup.setAttributeForElement(this.ID_CLASS_PREFIX + "root", "hidden",
"true");
this.markup.setAttributeForElement(this.ID_CLASS_PREFIX + "elements",
"hidden", "true");
},
/**
* Show the box model
*/
_showBoxModel: function() {
this.markup.removeAttributeForElement(this.ID_CLASS_PREFIX + "root",
this.markup.removeAttributeForElement(this.ID_CLASS_PREFIX + "elements",
"hidden");
},
@ -1002,6 +1077,10 @@ BoxModelHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.prototype
}
}
// Un-zoom the root wrapper if the page was zoomed.
let rootId = this.ID_CLASS_PREFIX + "root";
this.markup.scaleRootElement(this.currentNode, rootId);
return true;
}
@ -1255,76 +1334,103 @@ CssTransformHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.proto
_buildMarkup: function() {
let doc = this.win.document;
let container = doc.createElement("div");
container.className = "highlighter-container";
let container = createNode(this.win, {
attributes: {
"class": "highlighter-container"
}
});
let svgRoot = this._createSVGNode("svg", container, {
"class": "root",
"id": "root",
"hidden": "true",
"width": "100%",
"height": "100%"
// The root wrapper is used to unzoom the highlighter when needed.
let rootWrapper = createNode(this.win, {
parent: container,
attributes: {
"id": "root",
"class": "root"
},
prefix: this.ID_CLASS_PREFIX
});
let svg = createSVGNode(this.win, {
nodeType: "svg",
parent: rootWrapper,
attributes: {
"id": "elements",
"hidden": "true",
"width": "100%",
"height": "100%"
},
prefix: this.ID_CLASS_PREFIX
});
// Add a marker tag to the svg root for the arrow tip
this.markerId = "arrow-marker-" + MARKER_COUNTER;
MARKER_COUNTER ++;
let marker = this._createSVGNode("marker", svgRoot, {
"id": this.markerId,
"markerWidth": "10",
"markerHeight": "5",
"orient": "auto",
"markerUnits": "strokeWidth",
"refX": "10",
"refY": "5",
"viewBox": "0 0 10 10",
let marker = createSVGNode(this.win, {
nodeType: "marker",
parent: svg,
attributes: {
"id": this.markerId,
"markerWidth": "10",
"markerHeight": "5",
"orient": "auto",
"markerUnits": "strokeWidth",
"refX": "10",
"refY": "5",
"viewBox": "0 0 10 10"
},
prefix: this.ID_CLASS_PREFIX
});
this._createSVGNode("path", marker, {
"d": "M 0 0 L 10 5 L 0 10 z",
"fill": "#08C"
createSVGNode(this.win, {
nodeType: "path",
parent: marker,
attributes: {
"d": "M 0 0 L 10 5 L 0 10 z",
"fill": "#08C"
}
});
let shapesGroup = this._createSVGNode("g", svgRoot);
let shapesGroup = createSVGNode(this.win, {
nodeType: "g",
parent: svg
});
// Create the 2 polygons (transformed and untransformed)
this._createSVGNode("polygon", shapesGroup, {
"id": "untransformed",
"class": "untransformed"
createSVGNode(this.win, {
nodeType: "polygon",
parent: shapesGroup,
attributes: {
"id": "untransformed",
"class": "untransformed"
},
prefix: this.ID_CLASS_PREFIX
});
this._createSVGNode("polygon", shapesGroup, {
"id": "transformed",
"class": "transformed"
createSVGNode(this.win, {
nodeType: "polygon",
parent: shapesGroup,
attributes: {
"id": "transformed",
"class": "transformed"
},
prefix: this.ID_CLASS_PREFIX
});
// Create the arrows
for (let nb of ["1", "2", "3", "4"]) {
this._createSVGNode("line", shapesGroup, {
"id": "line" + nb,
"class": "line",
"marker-end": "url(#" + this.markerId + ")"
createSVGNode(this.win, {
nodeType: "line",
parent: shapesGroup,
attributes: {
"id": "line" + nb,
"class": "line",
"marker-end": "url(#" + this.markerId + ")"
},
prefix: this.ID_CLASS_PREFIX
});
}
container.appendChild(svgRoot);
return container;
},
_createSVGNode: function(nodeType, parent, attributes={}) {
let node = this.win.document.createElementNS(SVG_NS, nodeType);
for (let name in attributes) {
let value = attributes[name];
if (name === "class" || name === "id") {
value = this.ID_CLASS_PREFIX + value
}
node.setAttribute(name, value);
}
parent.appendChild(node);
return node;
},
/**
* Destroy the nodes. Remove listeners.
*/
@ -1401,6 +1507,9 @@ CssTransformHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.proto
this._setLinePoints(untransformedQuad["p" + nb], quad["p" + nb], "line" + nb);
}
// Adapt to the current zoom
this.markup.scaleRootElement(this.currentNode, this.ID_CLASS_PREFIX + "root");
this._showShapes();
},
@ -1412,11 +1521,13 @@ CssTransformHighlighter.prototype = Heritage.extend(AutoRefreshHighlighter.proto
},
_hideShapes: function() {
this.markup.setAttributeForElement(this.ID_CLASS_PREFIX + "root", "hidden", "true");
this.markup.setAttributeForElement(this.ID_CLASS_PREFIX + "elements",
"hidden", "true");
},
_showShapes: function() {
this.markup.removeAttributeForElement(this.ID_CLASS_PREFIX + "root", "hidden");
this.markup.removeAttributeForElement(this.ID_CLASS_PREFIX + "elements",
"hidden");
}
});
@ -1656,6 +1767,63 @@ function isXUL(tabActor) {
return tabActor.window.document.documentElement.namespaceURI === XUL_NS;
}
/**
* Helper function that creates SVG DOM nodes.
* @param {Window} This window's document will be used to create the element
* @param {Object} Options for the node include:
* - nodeType: the type of node, defaults to "box".
* - attributes: a {name:value} object to be used as attributes for the node.
* - prefix: a string that will be used to prefix the values of the id and class
* attributes.
* - parent: if provided, the newly created element will be appended to this
* node.
*/
function createSVGNode(win, options) {
if (!options.nodeType) {
options.nodeType = "box";
}
options.namespace = SVG_NS;
return createNode(win, options);
}
/**
* Helper function that creates DOM nodes.
* @param {Window} This window's document will be used to create the element
* @param {Object} Options for the node include:
* - nodeType: the type of node, defaults to "div".
* - namespace: if passed, doc.createElementNS will be used instead of
* doc.creatElement.
* - attributes: a {name:value} object to be used as attributes for the node.
* - prefix: a string that will be used to prefix the values of the id and class
* attributes.
* - parent: if provided, the newly created element will be appended to this
* node.
*/
function createNode(win, options) {
let type = options.nodeType || "div";
let node;
if (options.namespace) {
node = win.document.createElementNS(options.namespace, type);
} else {
node = win.document.createElement(type);
}
for (let name in options.attributes || {}) {
let value = options.attributes[name];
if (options.prefix && (name === "class" || name === "id")) {
value = options.prefix + value
}
node.setAttribute(name, value);
}
if (options.parent) {
options.parent.appendChild(node);
}
return node;
}
XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () {
return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils)
});