author | Wes Kocher <wkocher@mozilla.com> |
Fri, 12 Dec 2014 17:18:42 -0800 | |
changeset 247024 | f14dcd1c8c0bb4868442f4db40d67cb90430036d |
parent 247000 | f798a2ef5212482f830df5f09cf5cd3039900091 (current diff) |
parent 247023 | a1663501b36bdde4d98bceb02ba67a94d21fb2d9 (diff) |
child 247030 | 120465dd70d289d21cf24dddee3f3b3b2de199f9 |
child 247067 | 0717d2d7406317fd68cd8d5f8df3f40bef54345d |
child 247136 | 6866978381b6d8c67d381ab582cea607c21e3fd4 |
push id | 698 |
push user | jlund@mozilla.com |
push date | Mon, 23 Mar 2015 22:08:11 +0000 |
treeherder | mozilla-release@b0c0ae7b02a3 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | merge |
milestone | 37.0a1 |
first release with | nightly linux32
f14dcd1c8c0b
/
37.0a1
/
20141213030202
/
files
nightly linux64
f14dcd1c8c0b
/
37.0a1
/
20141213030202
/
files
nightly mac
f14dcd1c8c0b
/
37.0a1
/
20141213030202
/
files
nightly win32
f14dcd1c8c0b
/
37.0a1
/
20141213030202
/
files
nightly win64
f14dcd1c8c0b
/
37.0a1
/
20141213030202
/
files
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
releases | nightly linux32
37.0a1
/
20141213030202
/
pushlog to previous
nightly linux64
37.0a1
/
20141213030202
/
pushlog to previous
nightly mac
37.0a1
/
20141213030202
/
pushlog to previous
nightly win32
37.0a1
/
20141213030202
/
pushlog to previous
nightly win64
37.0a1
/
20141213030202
/
pushlog to previous
|
--- a/b2g/simulator/lib/main.js +++ b/b2g/simulator/lib/main.js @@ -1,30 +1,67 @@ /* 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/. */ const { Cc, Ci, Cu } = require("chrome"); -const { SimulatorProcess } = require("./simulator-process"); +const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {}); const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {}); +const { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); const { Simulator } = Cu.import("resource://gre/modules/devtools/Simulator.jsm"); -const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {}); +const { SimulatorProcess } = require("./simulator-process"); +const Runtime = require("sdk/system/runtime"); +const URL = require("sdk/url"); + +const ROOT_URI = require("addon").uri; +const PROFILE_URL = ROOT_URI + "profile/"; +const BIN_URL = ROOT_URI + "b2g/"; let process; -function launch({ port }) { - // Close already opened simulation +function launch(options) { + // Close already opened simulation. if (process) { - return close().then(launch.bind(null, { port: port })); + return close().then(launch.bind(null, options)); } - process = SimulatorProcess(); - process.remoteDebuggerPort = port; + // Compute B2G runtime path. + let path; + try { + let pref = "extensions." + require("addon").id + ".customRuntime"; + path = Services.prefs.getComplexValue(pref, Ci.nsIFile); + } catch(e) {} + + if (!path) { + let executables = { + WINNT: "b2g-bin.exe", + Darwin: "B2G.app/Contents/MacOS/b2g-bin", + Linux: "b2g-bin", + }; + path = URL.toFilename(BIN_URL); + path += Runtime.OS == "WINNT" ? "\\" : "/"; + path += executables[Runtime.OS]; + } + options.runtimePath = path; + console.log("simulator path:", options.runtimePath); + + // Compute Gaia profile path. + if (!options.profilePath) { + let gaiaProfile; + try { + let pref = "extensions." + require("addon").id + ".gaiaProfile"; + gaiaProfile = Services.prefs.getComplexValue(pref, Ci.nsIFile).path; + } catch(e) {} + + options.profilePath = gaiaProfile || URL.toFilename(PROFILE_URL); + } + + process = new SimulatorProcess(options); process.run(); return promise.resolve(); } function close() { if (!process) { return promise.resolve();
--- a/b2g/simulator/lib/simulator-process.js +++ b/b2g/simulator/lib/simulator-process.js @@ -4,48 +4,43 @@ */ 'use strict'; const { Cc, Ci, Cu, ChromeWorker } = require("chrome"); Cu.import("resource://gre/modules/Services.jsm"); -const { EventTarget } = require("sdk/event/target"); -const { emit, off } = require("sdk/event/core"); -const { Class } = require("sdk/core/heritage"); const Environment = require("sdk/system/environment").env; const Runtime = require("sdk/system/runtime"); -const URL = require("sdk/url"); const Subprocess = require("sdk/system/child_process/subprocess"); const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {}); +const { EventEmitter } = Cu.import("resource://gre/modules/devtools/event-emitter.js", {}); -const ROOT_URI = require("addon").uri; -const PROFILE_URL = ROOT_URI + "profile/"; -const BIN_URL = ROOT_URI + "b2g/"; // Log subprocess error and debug messages to the console. This logs messages // for all consumers of the API. We trim the messages because they sometimes // have trailing newlines. And note that registerLogHandler actually registers // an error handler, despite its name. Subprocess.registerLogHandler( function(s) console.error("subprocess: " + s.trim()) ); Subprocess.registerDebugHandler( function(s) console.debug("subprocess: " + s.trim()) ); -exports.SimulatorProcess = Class({ - extends: EventTarget, - initialize: function initialize(options) { - EventTarget.prototype.initialize.call(this, options); +function SimulatorProcess(options) { + this.options = options; - this.on("stdout", function onStdout(data) console.log(data.trim())); - this.on("stderr", function onStderr(data) console.error(data.trim())); - }, + EventEmitter.decorate(this); + this.on("stdout", data => { console.log(data.trim()) }); + this.on("stderr", data => { console.error(data.trim()) }); +} + +SimulatorProcess.prototype = { // check if b2g is running get isRunning() !!this.process, /** * Start the process and connect the debugger client. */ run: function() { @@ -72,131 +67,97 @@ exports.SimulatorProcess = Class({ command: "/usr/bin/osascript", arguments: ["-e", 'tell application "' + path + '" to activate'], }); } }); let environment; if (Runtime.OS == "Linux") { - environment = ["TMPDIR=" + Services.dirsvc.get("TmpD",Ci.nsIFile).path]; + environment = ["TMPDIR=" + Services.dirsvc.get("TmpD", Ci.nsIFile).path]; if ("DISPLAY" in Environment) { environment.push("DISPLAY=" + Environment.DISPLAY); } } // spawn a b2g instance this.process = Subprocess.call({ command: b2gExecutable, arguments: this.b2gArguments, environment: environment, // emit stdout event - stdout: (function(data) { - emit(this, "stdout", data); - }).bind(this), + stdout: data => { + this.emit("stdout", data); + }, // emit stderr event - stderr: (function(data) { - emit(this, "stderr", data); - }).bind(this), + stderr: data => { + this.emit("stderr", data); + }, - // on b2g instance exit, reset tracked process, remoteDebuggerPort and + // on b2g instance exit, reset tracked process, remote debugger port and // shuttingDown flag, then finally emit an exit event done: (function(result) { - console.log(this.b2gFilename + " terminated with " + result.exitCode); + console.log("B2G terminated with " + result.exitCode); this.process = null; - emit(this, "exit", result.exitCode); + this.emit("exit", result.exitCode); }).bind(this) }); }, // request a b2g instance kill kill: function() { let deferred = promise.defer(); if (this.process) { this.once("exit", (exitCode) => { this.shuttingDown = false; deferred.resolve(exitCode); }); if (!this.shuttingDown) { this.shuttingDown = true; - emit(this, "kill", null); + this.emit("kill", null); this.process.kill(); } return deferred.promise; } else { return promise.resolve(undefined); } }, - // compute current b2g filename - get b2gFilename() { - return this._executable ? this._executableFilename : "B2G"; - }, - // compute current b2g file handle get b2gExecutable() { if (this._executable) { return this._executable; } - let customRuntime; - try { - let pref = "extensions." + require("addon").id + ".customRuntime"; - customRuntime = Services.prefs.getComplexValue(pref, Ci.nsIFile); - } catch(e) {} - - if (customRuntime) { - this._executable = customRuntime; - this._executableFilename = "Custom runtime"; - return this._executable; - } - - let bin = URL.toFilename(BIN_URL); - let executables = { - WINNT: "b2g-bin.exe", - Darwin: "B2G.app/Contents/MacOS/b2g-bin", - Linux: "b2g-bin", - }; - - let path = bin; - path += Runtime.OS == "WINNT" ? "\\" : "/"; - path += executables[Runtime.OS]; - console.log("simulator path: " + path); let executable = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); - executable.initWithPath(path); + executable.initWithPath(this.options.runtimePath); if (!executable.exists()) { // B2G binaries not found throw Error("b2g-desktop Executable not found."); } this._executable = executable; - this._executableFilename = "b2g-bin"; return executable; }, // compute b2g CLI arguments get b2gArguments() { let args = []; - let gaiaProfile; - try { - let pref = "extensions." + require("addon").id + ".gaiaProfile"; - gaiaProfile = Services.prefs.getComplexValue(pref, Ci.nsIFile).path; - } catch(e) {} - - let profile = gaiaProfile || URL.toFilename(PROFILE_URL); + let profile = this.options.profilePath; args.push("-profile", profile); console.log("profile", profile); // NOTE: push dbgport option on the b2g-desktop commandline - args.push("-start-debugger-server", "" + this.remoteDebuggerPort); + args.push("-start-debugger-server", "" + this.options.port); // Ignore eventual zombie instances of b2g that are left over args.push("-no-remote"); return args; }, -}); +}; +exports.SimulatorProcess = SimulatorProcess;
--- a/browser/base/content/browser-safebrowsing.js +++ b/browser/base/content/browser-safebrowsing.js @@ -2,17 +2,17 @@ # 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/. #ifdef MOZ_SAFE_BROWSING var gSafeBrowsing = { setReportPhishingMenu: function() { // A phishing page will have a specific about:blocked content documentURI - var uri = getBrowser().currentURI; + var uri = gBrowser.currentURI; var isPhishingPage = uri && uri.spec.startsWith("about:blocked?e=phishingBlocked"); // Show/hide the appropriate menu item. document.getElementById("menu_HelpPopup_reportPhishingtoolmenu") .hidden = isPhishingPage; document.getElementById("menu_HelpPopup_reportPhishingErrortoolmenu") .hidden = !isPhishingPage;
--- a/browser/base/content/urlbarBindings.xml +++ b/browser/base/content/urlbarBindings.xml @@ -1053,17 +1053,17 @@ let addEngineList = document.getAnonymousElementByAttribute(this, "anonid", "add-engines"); while (addEngineList.firstChild) addEngineList.firstChild.remove(); const kXULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; - let addEngines = getBrowser().mCurrentBrowser.engines; + let addEngines = gBrowser.mCurrentBrowser.engines; if (addEngines && addEngines.length > 0) { const kBundleURI = "chrome://browser/locale/search.properties"; let bundle = Services.strings.createBundle(kBundleURI); for (let engine of addEngines) { let button = document.createElementNS(kXULNS, "button"); let label = bundle.formatStringFromName("cmd_addFoundEngine", [engine.title], 1); button.setAttribute("class", "addengine-item");
--- a/browser/components/feeds/WebContentConverter.js +++ b/browser/components/feeds/WebContentConverter.js @@ -447,17 +447,17 @@ WebContentConverterRegistrar.prototype = handlerInfo.alwaysAskBeforeHandling = true; var hs = Cc["@mozilla.org/uriloader/handler-service;1"]. getService(Ci.nsIHandlerService); hs.store(handlerInfo); } }; var browserElement = this._getBrowserForContentWindow(browserWindow, aContentWindow); - var notificationBox = browserWindow.getBrowser().getNotificationBox(browserElement); + var notificationBox = browserWindow.gBrowser.getNotificationBox(browserElement); notificationBox.appendNotification(message, notificationValue, notificationIcon, notificationBox.PRIORITY_INFO_LOW, [addButton]); }, /** @@ -476,17 +476,17 @@ WebContentConverterRegistrar.prototype = if (contentType != TYPE_MAYBE_FEED) return; if (aContentWindow) { var uri = this._checkAndGetURI(aURIString, aContentWindow); var browserWindow = this._getBrowserWindowForContentWindow(aContentWindow); var browserElement = this._getBrowserForContentWindow(browserWindow, aContentWindow); - var notificationBox = browserWindow.getBrowser().getNotificationBox(browserElement); + var notificationBox = browserWindow.gBrowser.getNotificationBox(browserElement); this._appendFeedReaderNotification(uri, aTitle, notificationBox); } else this._registerContentHandler(contentType, aURIString, aTitle); }, /** * Returns the browser chrome window in which the content window is in @@ -511,17 +511,17 @@ WebContentConverterRegistrar.prototype = * @param aContentWindow * The content window. It's possible to pass a child content window * (i.e. the content window of a frame/iframe). */ _getBrowserForContentWindow: function WCCR__getBrowserForContentWindow(aBrowserWindow, aContentWindow) { // This depends on pseudo APIs of browser.js and tabbrowser.xml aContentWindow = aContentWindow.top; - var browsers = aBrowserWindow.getBrowser().browsers; + var browsers = aBrowserWindow.gBrowser.browsers; for (var i = 0; i < browsers.length; ++i) { if (browsers[i].contentWindow == aContentWindow) return browsers[i]; } }, /** * Appends a notifcation for the given feed reader details.
--- a/browser/components/loop/LoopRooms.jsm +++ b/browser/components/loop/LoopRooms.jsm @@ -130,17 +130,17 @@ let LoopRoomsInternal = { /** * @var {Number} participantsCount The total amount of participants currently * inside all rooms. */ get participantsCount() { let count = 0; for (let room of this.rooms.values()) { - if (!("participants" in room)) { + if (room.deleted || !("participants" in room)) { continue; } count += room.participants.length; } return count; }, /**
--- a/browser/components/loop/MozLoopService.jsm +++ b/browser/components/loop/MozLoopService.jsm @@ -1068,21 +1068,23 @@ this.MozLoopService = { gFxAEnabled = Services.prefs.getBoolPref("loop.fxa.enabled"); if (!gFxAEnabled) { yield this.logOutFromFxA(); } } // The Loop toolbar button should change icon when the room participant count // changes from 0 to something. - const onRoomsChange = () => { - MozLoopServiceInternal.notifyStatusChanged(); + const onRoomsChange = (e) => { + // Pass the event name as notification reason for better logging. + MozLoopServiceInternal.notifyStatusChanged("room-" + e); }; LoopRooms.on("add", onRoomsChange); LoopRooms.on("update", onRoomsChange); + LoopRooms.on("delete", onRoomsChange); LoopRooms.on("joined", (e, room, participant) => { // Don't alert if we're in the doNotDisturb mode, or the participant // is the owner - the content code deals with the rest of the sounds. if (MozLoopServiceInternal.doNotDisturb || participant.owner) { return; } let window = gWM.getMostRecentWindow("navigator:browser");
--- a/browser/components/loop/content/js/panel.js +++ b/browser/components/loop/content/js/panel.js @@ -314,17 +314,18 @@ loop.panel = (function(_, mozL10n) { SettingsDropdownEntry({label: mozL10n.get("settings_menu_item_settings"), onClick: this.handleClickSettingsEntry, displayed: false, icon: "settings"}), SettingsDropdownEntry({label: mozL10n.get("settings_menu_item_account"), onClick: this.handleClickAccountEntry, icon: "account", displayed: this._isSignedIn()}), - SettingsDropdownEntry({label: mozL10n.get("tour_label"), + SettingsDropdownEntry({icon: "tour", + label: mozL10n.get("tour_label"), onClick: this.openGettingStartedTour}), SettingsDropdownEntry({label: this._isSignedIn() ? mozL10n.get("settings_menu_item_signout") : mozL10n.get("settings_menu_item_signin"), onClick: this.handleClickAuthEntry, displayed: navigator.mozLoop.fxAEnabled, icon: this._isSignedIn() ? "signout" : "signin"}), SettingsDropdownEntry({label: mozL10n.get("help_label"), @@ -690,17 +691,17 @@ loop.panel = (function(_, mozL10n) { ); } }); /** * Room list. */ var RoomList = React.createClass({displayName: 'RoomList', - mixins: [Backbone.Events], + mixins: [Backbone.Events, sharedMixins.WindowCloseMixin], propTypes: { store: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired, dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, userDisplayName: React.PropTypes.string.isRequired // for room creation }, getInitialState: function() { @@ -732,16 +733,18 @@ loop.panel = (function(_, mozL10n) { return mozL10n.get("rooms_list_current_conversations", {num: numRooms}); }, _hasPendingOperation: function() { return this.state.pendingCreation || this.state.pendingInitialRetrieval; }, handleCreateButtonClick: function() { + this.closeWindow(); + this.props.dispatcher.dispatch(new sharedActions.CreateRoom({ nameTemplate: mozL10n.get("rooms_default_room_name_template"), roomOwner: this.props.userDisplayName })); }, render: function() { if (this.state.error) {
--- a/browser/components/loop/content/js/panel.jsx +++ b/browser/components/loop/content/js/panel.jsx @@ -314,17 +314,18 @@ loop.panel = (function(_, mozL10n) { <SettingsDropdownEntry label={mozL10n.get("settings_menu_item_settings")} onClick={this.handleClickSettingsEntry} displayed={false} icon="settings" /> <SettingsDropdownEntry label={mozL10n.get("settings_menu_item_account")} onClick={this.handleClickAccountEntry} icon="account" displayed={this._isSignedIn()} /> - <SettingsDropdownEntry label={mozL10n.get("tour_label")} + <SettingsDropdownEntry icon="tour" + label={mozL10n.get("tour_label")} onClick={this.openGettingStartedTour} /> <SettingsDropdownEntry label={this._isSignedIn() ? mozL10n.get("settings_menu_item_signout") : mozL10n.get("settings_menu_item_signin")} onClick={this.handleClickAuthEntry} displayed={navigator.mozLoop.fxAEnabled} icon={this._isSignedIn() ? "signout" : "signin"} /> <SettingsDropdownEntry label={mozL10n.get("help_label")} @@ -690,17 +691,17 @@ loop.panel = (function(_, mozL10n) { ); } }); /** * Room list. */ var RoomList = React.createClass({ - mixins: [Backbone.Events], + mixins: [Backbone.Events, sharedMixins.WindowCloseMixin], propTypes: { store: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired, dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, userDisplayName: React.PropTypes.string.isRequired // for room creation }, getInitialState: function() { @@ -732,16 +733,18 @@ loop.panel = (function(_, mozL10n) { return mozL10n.get("rooms_list_current_conversations", {num: numRooms}); }, _hasPendingOperation: function() { return this.state.pendingCreation || this.state.pendingInitialRetrieval; }, handleCreateButtonClick: function() { + this.closeWindow(); + this.props.dispatcher.dispatch(new sharedActions.CreateRoom({ nameTemplate: mozL10n.get("rooms_default_room_name_template"), roomOwner: this.props.userDisplayName })); }, render: function() { if (this.state.error) {
--- a/browser/components/loop/content/shared/css/conversation.css +++ b/browser/components/loop/content/shared/css/conversation.css @@ -507,16 +507,46 @@ .local-stream-audio .OT_publisher .OT_video-poster { background-image: url("../img/audio-call-avatar.svg"); background-repeat: no-repeat; background-color: #4BA6E7; background-size: contain; background-position: center; } +/* + * Ensure that the publisher (i.e. local) video is never cropped, so that it's + * not possible for someone to be presented with a picture that displays + * (for example) a person from the neck up, even though the camera is capturing + * and transmitting a picture of that person from the waist up. + * + * The !importants are necessary to override the SDK attempts to avoid + * letterboxing entirely. + * + * If we could easily use test video streams with the SDK (eg if initPublisher + * supported something like a "testMediaToStreamURI" parameter that it would + * use to source the stream rather than the output of gUM, it wouldn't be too + * hard to generate a video with a 1 pixel border at the edges that one could + * at least visually see wasn't being cropped. + * + * Another less ugly possibility would be to work with Ted Mielczarek to use + * the fake camera drivers he has for Linux. + */ +.room-conversation .OT_publisher .OT_video-container { + height: 100% !important; + width: 100% !important; + top: 0 !important; + left: 0 !important; + background-color: transparent; /* avoid visually obvious letterboxing */ +} + +.room-conversation .OT_publisher .OT_video-container video { + background-color: transparent; /* avoid visually obvious letterboxing */ +} + .fx-embedded .media.nested { min-height: 200px; } .fx-embedded-call-identifier { display: inline; width: 100%; padding: 1.2em;
--- a/browser/components/loop/content/shared/css/panel.css +++ b/browser/components/loop/content/shared/css/panel.css @@ -675,16 +675,20 @@ body[dir=rtl] .generate-url-spinner { height: 12px; margin-right: 1em; } .settings-menu .icon-settings { background: transparent url(../img/svg/glyph-settings-16x16.svg) no-repeat center center; } +.settings-menu .icon-tour { + background: transparent url("../img/icons-16x16.svg#tour") no-repeat center center; +} + .settings-menu .icon-account { background: transparent url(../img/svg/glyph-account-16x16.svg) no-repeat center center; } .settings-menu .icon-signin { background: transparent url(../img/svg/glyph-signin-16x16.svg) no-repeat center center; }
--- a/browser/components/loop/content/shared/img/icons-16x16.svg +++ b/browser/components/loop/content/shared/img/icons-16x16.svg @@ -117,16 +117,20 @@ use[id$="-red"] { <rect x="7.75" y="7.542" fill="#FFFFFF" width="0.5" height="4"/> <polyline fill="#FFFFFF" points="9.25,7.542 8.75,7.542 8.75,11.542 9.25,11.542 "/> <rect x="6.75" y="7.542" fill="#FFFFFF" width="0.5" height="4"/> </g> <g id="leave-shape"> <polygon fill="#FFFFFF" points="2.08,11.52 2.08,4 8,4 8,2.24 0.32,2.24 0.32,13.28 8,13.28 8,11.52"/> <polygon fill="#FFFFFF" points="15.66816,7.77344 9.6,2.27456 9.6,5.6 3.68,5.6 3.68,9.92 9.6,9.92 9.6,13.27232"/> </g> + <path id="tour-shape" fill="#5A5A5A" d="M8,0C4.831,0,2.262,2.674,2.262,5.972c0,1.393,1.023,3.398,2.206,5.249l0.571,0.866C6.504,14.245,8,16,8,16 + s1.496-1.755,2.961-3.912l0.571-0.866c1.182-1.852,2.206-3.856,2.206-5.249C13.738,2.674,11.169,0,8,0z M8,7.645 + c-0.603,0-1.146-0.262-1.534-0.681C6.098,6.566,5.87,6.025,5.87,5.428c0-1.224,0.954-2.217,2.13-2.217s2.13,0.992,2.13,2.217 + C10.13,6.653,9.177,7.645,8,7.645z"/> </defs> <use id="audio" xlink:href="#audio-shape"/> <use id="audio-hover" xlink:href="#audio-shape"/> <use id="audio-active" xlink:href="#audio-shape"/> <use id="block" xlink:href="#block-shape"/> <use id="block-red" xlink:href="#block-shape"/> <use id="block-hover" xlink:href="#block-shape"/> <use id="block-active" xlink:href="#block-shape"/> @@ -153,9 +157,10 @@ use[id$="-red"] { <use id="tag-active" xlink:href="#tag-shape"/> <use id="trash" xlink:href="#trash-shape"/> <use id="unblock" xlink:href="#unblock-shape"/> <use id="unblock-hover" xlink:href="#unblock-shape"/> <use id="unblock-active" xlink:href="#unblock-shape"/> <use id="video" xlink:href="#video-shape"/> <use id="video-hover" xlink:href="#video-shape"/> <use id="video-active" xlink:href="#video-shape"/> +<use id="tour" xlink:href="#tour-shape"/> </svg>
--- a/browser/components/loop/test/desktop-local/panel_test.js +++ b/browser/components/loop/test/desktop-local/panel_test.js @@ -892,78 +892,83 @@ describe("loop.panel", function() { expect( roomEntry.getDOMNode().querySelector(".edit-in-place").textContent) .eql("New room name"); }); }); }); describe("loop.panel.RoomList", function() { - var roomStore, dispatcher, fakeEmail; + var roomStore, dispatcher, fakeEmail, dispatch; beforeEach(function() { fakeEmail = "fakeEmail@example.com"; dispatcher = new loop.Dispatcher(); roomStore = new loop.store.RoomStore(dispatcher, { mozLoop: navigator.mozLoop }); roomStore.setStoreState({ pendingCreation: false, pendingInitialRetrieval: false, rooms: [], error: undefined }); + dispatch = sandbox.stub(dispatcher, "dispatch"); }); function createTestComponent() { return TestUtils.renderIntoDocument(loop.panel.RoomList({ store: roomStore, dispatcher: dispatcher, userDisplayName: fakeEmail })); } it("should dispatch a GetAllRooms action on mount", function() { - var dispatch = sandbox.stub(dispatcher, "dispatch"); - createTestComponent(); sinon.assert.calledOnce(dispatch); sinon.assert.calledWithExactly(dispatch, new sharedActions.GetAllRooms()); }); it("should dispatch a CreateRoom action when clicking on the Start a " + "conversation button", function() { navigator.mozLoop.userProfile = {email: fakeEmail}; - var dispatch = sandbox.stub(dispatcher, "dispatch"); var view = createTestComponent(); TestUtils.Simulate.click(view.getDOMNode().querySelector("button")); sinon.assert.calledWith(dispatch, new sharedActions.CreateRoom({ nameTemplate: "fakeText", roomOwner: fakeEmail })); }); + it("should close the panel when 'Start a Conversation' is clicked", + function() { + var view = createTestComponent(); + + TestUtils.Simulate.click(view.getDOMNode().querySelector("button")); + + sinon.assert.calledOnce(fakeWindow.close); + }); + it("should disable the create button when a creation operation is ongoing", function() { - var dispatch = sandbox.stub(dispatcher, "dispatch"); roomStore.setStoreState({pendingCreation: true}); var view = createTestComponent(); var buttonNode = view.getDOMNode().querySelector("button[disabled]"); expect(buttonNode).to.not.equal(null); }); it("should disable the create button when a list retrieval operation is pending", function() { - var dispatch = sandbox.stub(dispatcher, "dispatch"); roomStore.setStoreState({pendingInitialRetrieval: true}); var view = createTestComponent(); var buttonNode = view.getDOMNode().querySelector("button[disabled]"); expect(buttonNode).to.not.equal(null); }); });
--- a/browser/components/preferences/in-content/advanced.js +++ b/browser/components/preferences/in-content/advanced.js @@ -74,16 +74,25 @@ var gAdvancedPane = { gAdvancedPane.updateWritePrefs); setEventListener("showUpdateHistory", "command", gAdvancedPane.showUpdates); #endif setEventListener("viewCertificatesButton", "command", gAdvancedPane.showCertificates); setEventListener("viewSecurityDevicesButton", "command", gAdvancedPane.showSecurityDevices); + +#ifdef MOZ_WIDGET_GTK + // GTK tabbox' allow the scroll wheel to change the selected tab, + // but we don't want this behavior for the in-content preferences. + let tabsElement = document.getElementById("tabsElement"); + tabsElement.addEventListener("DOMMouseScroll", event => { + event.stopPropagation(); + }, true); +#endif }, /** * Stores the identity of the current tab in preferences so that the selected * tab can be persisted between openings of the preferences window. */ tabSelectionChanged: function () {
--- a/browser/components/search/content/search.xml +++ b/browser/components/search/content/search.xml @@ -344,17 +344,17 @@ // indexes as items are removed. var items = popup.childNodes; for (var i = items.length - 1; i >= 0; i--) { if (items[i].classList.contains("addengine-item") || items[i].classList.contains("addengine-separator")) popup.removeChild(items[i]); } - var addengines = getBrowser().mCurrentBrowser.engines; + var addengines = gBrowser.mCurrentBrowser.engines; if (addengines && addengines.length > 0) { const kXULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; // Find the (first) separator in the remaining menu, or the first item // if no separators are present. var insertLocation = popup.firstChild; while (insertLocation.nextSibling &&
--- a/browser/devtools/inspector/test/browser_inspector_delete-selected-node-02.js +++ b/browser/devtools/inspector/test/browser_inspector_delete-selected-node-02.js @@ -37,17 +37,17 @@ add_task(function* () { info("Clicking 'Delete Node' in the context menu."); inspector.panelDoc.getElementById("node-menu-delete").click(); info("Waiting for inspector to update."); yield inspector.once("inspector-updated"); info("Inspector updated, performing checks."); - yield assertNodeSelectedAndPanelsUpdated("#deleteChildren", "ul#deleteChildren"); + yield assertNodeSelectedAndPanelsUpdated("#selectedAfterDelete", "li#selectedAfterDelete"); } function* testAutomaticallyDeleteSelectedNode() { info("Selecting a node, deleting it via javascript and checking that " + "its parent node is selected and breadcrumbs are updated."); let div = yield getNodeFront("#deleteAutomatically", inspector); yield selectNode(div, inspector);
--- a/browser/devtools/inspector/test/doc_inspector_delete-selected-node-02.html +++ b/browser/devtools/inspector/test/doc_inspector_delete-selected-node-02.html @@ -2,13 +2,14 @@ <html lang="en"> <head> <meta charset="utf-8"> <title>node delete - reset selection - test</title> </head> <body> <ul id="deleteChildren"> <li id="deleteManually">Delete me via the inspector</li> + <li id="selectedAfterDelete">This node is selected after manual delete</li> <li id="deleteAutomatically">Delete me via javascript</li> </ul> <iframe id="deleteIframe" src="data:text/html,%3C!DOCTYPE%20html%3E%3Chtml%20lang%3D%22en%22%3E%3Cbody%3E%3Cp%20id%3D%22deleteInIframe%22%3EDelete my container iframe%3C%2Fp%3E%3C%2Fbody%3E%3C%2Fhtml%3E"></iframe> </body> </html>
--- a/browser/devtools/markupview/markup-view.js +++ b/browser/devtools/markupview/markup-view.js @@ -421,18 +421,20 @@ MarkupView.prototype = { if (node.hidden) { this.walker.unhideNode(node).then(() => this.nodeChanged(node)); } else { this.walker.hideNode(node).then(() => this.nodeChanged(node)); } } break; case Ci.nsIDOMKeyEvent.DOM_VK_DELETE: + this.deleteNode(this._selectedContainer.node); + break; case Ci.nsIDOMKeyEvent.DOM_VK_BACK_SPACE: - this.deleteNode(this._selectedContainer.node); + this.deleteNode(this._selectedContainer.node, true); break; case Ci.nsIDOMKeyEvent.DOM_VK_HOME: let rootContainer = this.getContainer(this._rootNode); this.navigate(rootContainer.children.firstChild.container); break; case Ci.nsIDOMKeyEvent.DOM_VK_LEFT: if (this._selectedContainer.expanded) { this.collapseNode(this._selectedContainer.node); @@ -503,34 +505,45 @@ MarkupView.prototype = { aEvent.stopPropagation(); aEvent.preventDefault(); } }, /** * Delete a node from the DOM. * This is an undoable action. + * + * @param {NodeFront} aNode The node to remove. + * @param {boolean} moveBackward If set to true, focus the previous sibling, + * otherwise the next one. */ - deleteNode: function(aNode) { + deleteNode: function(aNode, moveBackward) { if (aNode.isDocumentElement || aNode.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE || aNode.isAnonymous) { return; } let container = this.getContainer(aNode); // Retain the node so we can undo this... this.walker.retainNode(aNode).then(() => { let parent = aNode.parentNode(); let nextSibling = null; this.undo.do(() => { this.walker.removeNode(aNode).then(siblings => { - let focusNode = siblings.previousSibling || parent; nextSibling = siblings.nextSibling; + let focusNode = moveBackward ? siblings.previousSibling : nextSibling; + + // If we can't move as the user wants, we move to the other direction. + // If there is no sibling elements anymore, move to the parent node. + if (!focusNode) { + focusNode = nextSibling || siblings.previousSibling || parent; + } + if (container.selected) { this.navigate(this.getContainer(focusNode)); } }); }, () => { this.walker.insertBefore(aNode, parent, nextSibling); }); }).then(null, console.error);
--- a/browser/devtools/markupview/test/browser_markupview_tag_edit_04.js +++ b/browser/devtools/markupview/test/browser_markupview_tag_edit_04.js @@ -5,23 +5,23 @@ "use strict"; // Tests that a node can be deleted from the markup-view with the delete key. // Also checks that after deletion the correct element is highlighted. // The next sibling is preferred, but the parent is a fallback. const TEST_URL = "data:text/html,<div id='parent'><div id='first'></div><div id='second'></div><div id='third'></div></div>"; -function* checkDeleteAndSelection(inspector, nodeSelector, focusedNodeSelector) { +function* checkDeleteAndSelection(inspector, key, nodeSelector, focusedNodeSelector) { yield selectNode(nodeSelector, inspector); yield clickContainer(nodeSelector, inspector); - info("Deleting the element \"" + nodeSelector + "\" with the keyboard"); + info(`Deleting the element "${nodeSelector}" using the ${key} key`); let mutated = inspector.once("markupmutation"); - EventUtils.sendKey("delete", inspector.panelWin); + EventUtils.sendKey(key, inspector.panelWin); yield Promise.all([mutated, inspector.once("inspector-updated")]); let nodeFront = yield getNodeFront(focusedNodeSelector, inspector); is(inspector.selection.nodeFront, nodeFront, focusedNodeSelector + " should be selected after " + nodeSelector + " node gets deleted."); info("Checking that it's gone, baby gone!"); @@ -31,14 +31,29 @@ function* checkDeleteAndSelection(inspec ok(content.document.querySelector(nodeSelector), "The test node is back!"); } let test = asyncTest(function*() { let {inspector} = yield addTab(TEST_URL).then(openInspector); info("Selecting the test node by clicking on it to make sure it receives focus"); - yield checkDeleteAndSelection(inspector, "#first", "#parent"); - yield checkDeleteAndSelection(inspector, "#second", "#first"); - yield checkDeleteAndSelection(inspector, "#third", "#second"); + yield checkDeleteAndSelection(inspector, "delete", "#first", "#second"); + yield checkDeleteAndSelection(inspector, "delete", "#second", "#third"); + yield checkDeleteAndSelection(inspector, "delete", "#third", "#second"); + + yield checkDeleteAndSelection(inspector, "back_space", "#first", "#second"); + yield checkDeleteAndSelection(inspector, "back_space", "#second", "#first"); + yield checkDeleteAndSelection(inspector, "back_space", "#third", "#second"); + + // Removing the siblings of #first. + let mutated = inspector.once("markupmutation"); + for (let node of content.document.querySelectorAll("#second, #third")) { + node.remove(); + } + yield mutated; + // Testing with an only child. + info("testing with an only child"); + yield checkDeleteAndSelection(inspector, "delete", "#first", "#parent"); + yield checkDeleteAndSelection(inspector, "back_space", "#first", "#parent"); yield inspector.once("inspector-updated"); });
--- a/browser/devtools/performance/performance-controller.js +++ b/browser/devtools/performance/performance-controller.js @@ -50,16 +50,20 @@ const EVENTS = { OVERVIEW_RANGE_CLEARED: "Performance:UI:OverviewRangeCleared", // Emitted by the DetailsView when a subview is selected DETAILS_VIEW_SELECTED: "Performance:UI:DetailsViewSelected", // Emitted by the CallTreeView when a call tree has been rendered CALL_TREE_RENDERED: "Performance:UI:CallTreeRendered", + // When a source is shown in the JavaScript Debugger at a specific location. + SOURCE_SHOWN_IN_JS_DEBUGGER: "Performance:UI:SourceShownInJsDebugger", + SOURCE_NOT_FOUND_IN_JS_DEBUGGER: "Performance:UI:SourceNotFoundInJsDebugger", + // Emitted by the WaterfallView when it has been rendered WATERFALL_RENDERED: "Performance:UI:WaterfallRendered" }; /** * The current target and the profiler connection, set by this tool's host. */ let gToolbox, gTarget, gFront;
--- a/browser/devtools/performance/test/browser.ini +++ b/browser/devtools/performance/test/browser.ini @@ -30,11 +30,13 @@ support-files = [browser_perf-data-samples.js] [browser_perf-data-massaging-01.js] [browser_perf-ui-recording.js] [browser_perf-overview-render-01.js] [browser_perf-overview-render-02.js] [browser_perf-overview-selection.js] [browser_perf-details.js] +[browser_perf-jump-to-debugger-01.js] +[browser_perf-jump-to-debugger-02.js] [browser_perf-details-calltree-render-01.js] [browser_perf-details-calltree-render-02.js] [browser_perf-details-waterfall-render-01.js]
new file mode 100644 --- /dev/null +++ b/browser/devtools/performance/test/browser_perf-jump-to-debugger-01.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the performance tool can jump to the debugger. + */ + +function spawnTest () { + let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL); + let { viewSourceInDebugger } = panel.panelWin; + + yield viewSourceInDebugger(SIMPLE_URL, 14); + + let debuggerPanel = toolbox.getPanel("jsdebugger"); + ok(debuggerPanel, "The debugger panel was opened."); + + let { DebuggerView } = debuggerPanel.panelWin; + let Sources = DebuggerView.Sources; + + is(Sources.selectedValue, getSourceActor(Sources, SIMPLE_URL), + "The correct source is shown in the debugger."); + is(DebuggerView.editor.getCursor().line + 1, 14, + "The correct line is highlighted in the debugger's source editor."); + + yield teardown(panel); + finish(); +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/performance/test/browser_perf-jump-to-debugger-02.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the performance tool can jump to the debugger, when the source was already + * loaded in that tool. + */ + +function spawnTest() { + let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL, "jsdebugger"); + let debuggerWin = panel.panelWin; + let debuggerEvents = debuggerWin.EVENTS; + let { DebuggerView } = debuggerWin; + let Sources = DebuggerView.Sources; + + yield debuggerWin.once(debuggerEvents.SOURCE_SHOWN); + ok("A source was shown in the debugger."); + + is(Sources.selectedValue, getSourceActor(Sources, SIMPLE_URL), + "The correct source is initially shown in the debugger."); + is(DebuggerView.editor.getCursor().line, 0, + "The correct line is initially highlighted in the debugger's source editor."); + + yield toolbox.selectTool("performance"); + let perfPanel = toolbox.getCurrentPanel(); + let perfWin = perfPanel.panelWin; + let { viewSourceInDebugger } = perfWin; + + yield viewSourceInDebugger(SIMPLE_URL, 14); + + panel = toolbox.getPanel("jsdebugger"); + ok(panel, "The debugger panel was reselected."); + + is(DebuggerView.Sources.selectedValue, getSourceActor(Sources, SIMPLE_URL), + "The correct source is still shown in the debugger."); + is(DebuggerView.editor.getCursor().line + 1, 14, + "The correct line is now highlighted in the debugger's source editor."); + + yield teardown(perfPanel); + finish(); +}
--- a/browser/devtools/performance/test/head.js +++ b/browser/devtools/performance/test/head.js @@ -153,27 +153,27 @@ function initBackend(aUrl) { let connection = getPerformanceActorsConnection(target); yield connection.open(); let front = new PerformanceFront(connection); return { target, front }; }); } -function initPerformance(aUrl) { +function initPerformance(aUrl, selectedTool="performance") { info("Initializing a performance pane."); return Task.spawn(function*() { let tab = yield addTab(aUrl); let target = TargetFactory.forTab(tab); yield target.makeRemote(); Services.prefs.setBoolPref("devtools.performance_dev.enabled", true); - let toolbox = yield gDevTools.showToolbox(target, "performance"); + let toolbox = yield gDevTools.showToolbox(target, selectedTool); let panel = toolbox.getCurrentPanel(); return { target, panel, toolbox }; }); } function* teardown(panel) { info("Destroying the performance tool."); @@ -312,8 +312,13 @@ function dragStop(graph, x, y = 1) { graph._onMouseMove({ clientX: x, clientY: y }); graph._onMouseUp({ clientX: x, clientY: y }); } function dropSelection(graph) { graph.dropSelection(); graph.emit("mouseup"); } + +function getSourceActor(aSources, aURL) { + let item = aSources.getItemForAttachment(a => a.source.url === aURL); + return item && item.value; +}
--- a/browser/devtools/performance/views/details-call-tree.js +++ b/browser/devtools/performance/views/details-call-tree.js @@ -9,16 +9,17 @@ let CallTreeView = { /** * Sets up the view with event binding. */ initialize: function () { this.el = $(".call-tree"); this._graphEl = $(".call-tree-cells-container"); this._onRangeChange = this._onRangeChange.bind(this); + this._onLink = this._onLink.bind(this); this._stop = this._stop.bind(this); OverviewView.on(EVENTS.OVERVIEW_RANGE_SELECTED, this._onRangeChange); OverviewView.on(EVENTS.OVERVIEW_RANGE_CLEARED, this._onRangeChange); PerformanceController.on(EVENTS.RECORDING_STOPPED, this._stop); }, /** @@ -53,16 +54,23 @@ let CallTreeView = { */ _onRangeChange: function (_, params) { // When a range is cleared, we'll have no beginAt/endAt data, // so the rebuild will just render all the data again. let { beginAt, endAt } = params || {}; this.render(this._profilerData, beginAt, endAt); }, + _onLink: function (_, treeItem) { + let { url, line } = treeItem.frame.getInfo(); + viewSourceInDebugger(url, line).then( + () => this.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER), + () => this.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER)); + }, + /** * 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 @@ -80,21 +88,51 @@ let CallTreeView = { _populateCallTree: function (frameNode, options={}) { let root = new CallView({ autoExpandDepth: options.inverted ? 0 : undefined, frame: frameNode, hidden: options.inverted, inverted: options.inverted }); + // Bind events + root.on("link", this._onLink); + // Clear out other graphs this._graphEl.innerHTML = ""; root.attachTo(this._graphEl); let contentOnly = !Prefs.showPlatformData; root.toggleCategories(!contentOnly); } }; /** * Convenient way of emitting events from the view. */ EventEmitter.decorate(CallTreeView); + +/** + * Opens/selects the debugger in this toolbox and jumps to the specified + * file name and line number. + * @param string url + * @param number line + */ +let viewSourceInDebugger = Task.async(function *(url, line) { + // If the Debugger was already open, switch to it and try to show the + // source immediately. Otherwise, initialize it and wait for the sources + // to be added first. + let debuggerAlreadyOpen = gToolbox.getPanel("jsdebugger"); + + let { panelWin: dbg } = yield gToolbox.selectTool("jsdebugger"); + + if (!debuggerAlreadyOpen) { + yield new Promise((resolve) => dbg.once(dbg.EVENTS.SOURCES_ADDED, () => resolve(dbg))); + } + + let { DebuggerView } = dbg; + let item = DebuggerView.Sources.getItemForAttachment(a => a.source.url === url); + + if (item) { + return DebuggerView.setEditorLocation(item.attachment.source.actor, line, { noDebug: true }); + } + return Promise.reject(); +});
--- a/browser/devtools/shared/DeveloperToolbar.jsm +++ b/browser/devtools/shared/DeveloperToolbar.jsm @@ -212,17 +212,17 @@ let CommandUtils = { return this.target.tab.ownerDocument.defaultView; }, get chromeDocument() { return this.chromeWindow.document; }, get window() { - return this.chromeWindow.getBrowser().selectedTab.linkedBrowser.contentWindow; + return this.chromeWindow.gBrowser.selectedTab.linkedBrowser.contentWindow; }, get document() { return this.window.document; } }; }, }; @@ -291,17 +291,17 @@ const NOTIFICATIONS = { */ DeveloperToolbar.prototype.NOTIFICATIONS = NOTIFICATIONS; /** * target is dynamic because the selectedTab changes */ Object.defineProperty(DeveloperToolbar.prototype, "target", { get: function() { - return TargetFactory.forTab(this._chromeWindow.getBrowser().selectedTab); + return TargetFactory.forTab(this._chromeWindow.gBrowser.selectedTab); }, enumerable: true }); /** * Is the toolbar open? */ Object.defineProperty(DeveloperToolbar.prototype, 'visible', { @@ -405,17 +405,17 @@ DeveloperToolbar.prototype.show = functi ]; return promise.all(panelPromises).then(panels => { [ this.tooltipPanel, this.outputPanel ] = panels; this._doc.getElementById("Tools:DevToolbar").setAttribute("checked", "true"); return gcli.load().then(() => { this.display = gcli.createDisplay({ - contentDocument: this._chromeWindow.getBrowser().contentDocument, + contentDocument: this._chromeWindow.gBrowser.contentDocument, chromeDocument: this._doc, chromeWindow: this._chromeWindow, hintElement: this.tooltipPanel.hintElement, inputElement: this._input, completeElement: this._doc.querySelector(".gclitoolbar-complete-node"), backgroundElement: this._doc.querySelector(".gclitoolbar-stack-node"), outputDocument: this.outputPanel.document, environment: CommandUtils.createEnvironment(this, "target"), @@ -428,17 +428,17 @@ DeveloperToolbar.prototype.show = functi this.display.focusManager.addMonitoredElement(this._element); this.display.onVisibilityChange.add(this.outputPanel._visibilityChanged, this.outputPanel); this.display.onVisibilityChange.add(this.tooltipPanel._visibilityChanged, this.tooltipPanel); this.display.onOutput.add(this.outputPanel._outputChanged, this.outputPanel); - let tabbrowser = this._chromeWindow.getBrowser(); + let tabbrowser = this._chromeWindow.gBrowser; tabbrowser.tabContainer.addEventListener("TabSelect", this, false); tabbrowser.tabContainer.addEventListener("TabClose", this, false); tabbrowser.addEventListener("load", this, true); tabbrowser.addEventListener("beforeunload", this, true); this._initErrorsCount(tabbrowser.selectedTab); this._devtoolsUnloaded = this._devtoolsUnloaded.bind(this); this._devtoolsLoaded = this._devtoolsLoaded.bind(this); @@ -495,26 +495,26 @@ DeveloperToolbar.prototype.hide = functi return this._hidePromise; }; /** * The devtools-unloaded event handler. * @private */ DeveloperToolbar.prototype._devtoolsUnloaded = function() { - let tabbrowser = this._chromeWindow.getBrowser(); + let tabbrowser = this._chromeWindow.gBrowser; Array.prototype.forEach.call(tabbrowser.tabs, this._stopErrorsCount, this); }; /** * The devtools-loaded event handler. * @private */ DeveloperToolbar.prototype._devtoolsLoaded = function() { - let tabbrowser = this._chromeWindow.getBrowser(); + let tabbrowser = this._chromeWindow.gBrowser; this._initErrorsCount(tabbrowser.selectedTab); }; /** * Initialize the listeners needed for tracking the number of errors for a given * tab. * * @private @@ -570,17 +570,17 @@ DeveloperToolbar.prototype._stopErrorsCo /** * Hide the developer toolbar */ DeveloperToolbar.prototype.destroy = function() { if (this._input == null) { return; // Already destroyed } - let tabbrowser = this._chromeWindow.getBrowser(); + let tabbrowser = this._chromeWindow.gBrowser; tabbrowser.tabContainer.removeEventListener("TabSelect", this, false); tabbrowser.tabContainer.removeEventListener("TabClose", this, false); tabbrowser.removeEventListener("load", this, true); tabbrowser.removeEventListener("beforeunload", this, true); Services.obs.removeObserver(this._devtoolsUnloaded, "devtools-unloaded"); Services.obs.removeObserver(this._devtoolsLoaded, "devtools-loaded"); Array.prototype.forEach.call(tabbrowser.tabs, this._stopErrorsCount, this); @@ -619,17 +619,17 @@ DeveloperToolbar.prototype._notify = fun /** * Update various parts of the UI when the current tab changes */ DeveloperToolbar.prototype.handleEvent = function(ev) { if (ev.type == "TabSelect" || ev.type == "load") { if (this.visible) { this.display.reattach({ - contentDocument: this._chromeWindow.getBrowser().contentDocument + contentDocument: this._chromeWindow.gBrowser.contentDocument }); if (ev.type == "TabSelect") { this._initErrorsCount(ev.target); } } } else if (ev.type == "TabClose") { @@ -671,17 +671,17 @@ DeveloperToolbar.prototype._onPageError * @param nsIDOMEvent ev the beforeunload DOM event. */ DeveloperToolbar.prototype._onPageBeforeUnload = function(ev) { let window = ev.target.defaultView; if (window.top !== window) { return; } - let tabs = this._chromeWindow.getBrowser().tabs; + let tabs = this._chromeWindow.gBrowser.tabs; Array.prototype.some.call(tabs, function(tab) { if (tab.linkedBrowser.contentWindow === window) { let tabId = tab.linkedPanel; if (tabId in this._errorsCount || tabId in this._warningsCount) { this._errorsCount[tabId] = 0; this._warningsCount[tabId] = 0; this._updateErrorsCount(tabId); } @@ -696,17 +696,17 @@ DeveloperToolbar.prototype._onPageBefore * currently selected tab. * * @private * @param string [changedTabId] Optional. The tab ID that had its page errors * count changed. If this is provided and it doesn't match the currently * selected tab, then the button is not updated. */ DeveloperToolbar.prototype._updateErrorsCount = function(changedTabId) { - let tabId = this._chromeWindow.getBrowser().selectedTab.linkedPanel; + let tabId = this._chromeWindow.gBrowser.selectedTab.linkedPanel; if (changedTabId && tabId != changedTabId) { return; } let errors = this._errorsCount[tabId]; let warnings = this._warningsCount[tabId]; let btn = this._errorCounterButton; if (errors) {
--- a/browser/devtools/shared/widgets/Tooltip.js +++ b/browser/devtools/shared/widgets/Tooltip.js @@ -410,17 +410,17 @@ Tooltip.prototype = { _onBaseNodeMouseMove: function(event) { if (event.target !== this._lastHovered) { this.hide(); this._lastHovered = event.target; setNamedTimeout(this.uid, this._showDelay, () => { this.isValidHoverTarget(event.target).then(target => { this.show(target); - }).catch((reason) => { + }, reason => { if (reason === false) { // isValidHoverTarget rejects with false if the tooltip should // not be shown. This can be safely ignored. return; } // Report everything else. Reason might be error that should not be // hidden. console.error("isValidHoverTarget rejected with an unexpected reason:");
--- a/browser/extensions/pdfjs/test/browser_pdfjs_navigation.js +++ b/browser/extensions/pdfjs/test/browser_pdfjs_navigation.js @@ -55,16 +55,70 @@ const TESTS = [ selector: "#thumbnailView a:nth-child(2)", event: "click" }, expectedPage: 2, message: "navigated to 2nd page using thumbnail view" }, { action: { + selector: "#viewer", + event: "keydown", + keyCode: 36 + }, + expectedPage: 1, + message: "navigated to 1st page using 'home' key" + }, + { + action: { + selector: "#viewer", + event: "keydown", + keyCode: 34 + }, + expectedPage: 2, + message: "navigated to 2nd page using 'Page Down' key" + }, + { + action: { + selector: "#viewer", + event: "keydown", + keyCode: 33 + }, + expectedPage: 1, + message: "navigated to 1st page using 'Page Up' key" + }, + { + action: { + selector: "#viewer", + event: "keydown", + keyCode: 39 + }, + expectedPage: 2, + message: "navigated to 2nd page using 'right' key" + }, + { + action: { + selector: "#viewer", + event: "keydown", + keyCode: 37 + }, + expectedPage: 1, + message: "navigated to 1st page using 'left' key" + }, + { + action: { + selector: "#viewer", + event: "keydown", + keyCode: 35 + }, + expectedPage: 5, + message: "navigated to last page using 'home' key" + }, + { + action: { selector: ".outlineItem:nth-child(1) a", event: "click" }, expectedPage: 1, message: "navigated to 1st page using outline view" }, { action: { @@ -125,17 +179,24 @@ function test() { function runTests(document, window, finish) { // Check if PDF is opened with internal viewer ok(document.querySelector('div#viewer'), "document content has viewer UI"); ok('PDFJS' in window.wrappedJSObject, "window content has PDFJS object"); // Wait for outline items, the start the navigation actions waitForOutlineItems(document).then(function () { - runNextTest(document, window, finish); + // The key navigation has to happen in page-fit, otherwise it won't scroll + // trough a complete page + setZoomToPageFit(document).then(function () { + runNextTest(document, window, finish); + }, function () { + ok(false, "Current scale has been ste to 'page-fit'"); + finish(); + }); }, function () { ok(false, "Outline items have ben found"); finish(); }); } /** * As the page changes asynchronously, we have to wait for the event after @@ -165,17 +226,27 @@ function runNextTest(document, window, e var el = document.querySelector(test.action.selector); ok(el, "Element '" + test.action.selector + "' has been found"); // The value option is for input case if (test.action.value) el.value = test.action.value; // Dispatch the event for changing the page - el.dispatchEvent(new Event(test.action.event)); + if (test.action.event == "keydown") { + var ev = document.createEvent("KeyboardEvent"); + ev.initKeyEvent("keydown", true, true, null, false, false, false, false, + test.action.keyCode, 0); + el.dispatchEvent(ev); + } + else { + var ev = new Event(test.action.event); + } + el.dispatchEvent(ev); + // When the promise gets resolved we call the next test if there are any left // or else we call the final callback which will end the test deferred.promise.then(function (pgNumber) { is(pgNumber, test.expectedPage, test.message); if (TESTS.length) runNextTest(document, window, endCallback); @@ -202,8 +273,32 @@ function waitForOutlineItems(document) { clearInterval(interval); clearTimeout(timeout); deferred.resolve(); } }, 500); return deferred.promise; } + +/** + * The key navigation has to happen in page-fit, otherwise it won't scroll + * trough a complete page + * + * @param document + * @returns {deferred.promise|*} + */ +function setZoomToPageFit(document) { + var deferred = Promise.defer(); + document.addEventListener("pagerendered", function onZoom(e) { + document.removeEventListener("pagerendered", onZoom), false; + document.querySelector("#viewer").click(); + deferred.resolve(); + + }, false); + + var select = document.querySelector("select#scaleSelect"); + select.selectedIndex = 2; + select.dispatchEvent(new Event("change")); + + return deferred.promise; +} +
--- a/browser/fuel/fuelApplication.js +++ b/browser/fuel/fuelApplication.js @@ -89,17 +89,17 @@ function Window(aWindow) { } Window.prototype = { get events() { return this._events; }, get _tabbrowser() { - return this._window.getBrowser(); + return this._window.gBrowser; }, /* * Helper used to setup event handlers on the XBL element. Note that the events * are actually dispatched to tabs, so we capture them. */ _watch: function win_watch(aType) { this._tabbrowser.tabContainer.addEventListener(aType, this,
--- a/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd +++ b/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd @@ -148,17 +148,17 @@ <!ENTITY options.stylesheetAutocompletion.label "Autocomplete CSS"> <!ENTITY options.stylesheetAutocompletion.tooltip "Autocomplete CSS properties, values and selectors in Style Editor as you type"> <!-- LOCALIZATION NOTE (options.profiler.label): This is the label for the - heading of the group of JavaScript Profiler preferences in the options - panel. --> <!ENTITY options.profiler.label "JavaScript Profiler"> -<!-- LOCALICATION NOTE (options.commonprefs): This is the label for the heading +<!-- LOCALIZATION NOTE (options.commonprefs): This is the label for the heading of all preferences that affect both the Web Console and the Network Monitor --> <!ENTITY options.commonPrefs.label "Common Preferences"> <!-- LOCALIZATION NOTE (options.enablePersistentLogs.label): This is the - label for the checkbox that toggles persistent logs in the Web Console and - network monitor, i.e. devtools.webconsole.persistlog a boolean preference in - about:config, in the options panel. -->
--- a/browser/locales/en-US/chrome/browser/preferences/search.dtd +++ b/browser/locales/en-US/chrome/browser/preferences/search.dtd @@ -9,9 +9,18 @@ <!ENTITY provideSearchSuggestions.label "Provide search suggestions"> <!ENTITY provideSearchSuggestions.accesskey "s"> <!ENTITY oneClickSearchEngines.label "One-click search engines"> <!ENTITY chooseWhichOneToDisplay.label "The search bar lets you search alternate engines directly. Choose which ones to display."> +<!ENTITY engineNameColumn.label "Search Engine"> +<!ENTITY engineKeywordColumn.label "Keyword"> + +<!ENTITY restoreDefaultSearchEngines.label "Restore Default Search Engines"> +<!ENTITY restoreDefaultSearchEngines.accesskey "d"> + +<!ENTITY removeEngine.label "Remove"> +<!ENTITY removeEngine.accesskey "r"> + <!ENTITY addMoreSearchEngines.label "Add more search engines…">
--- a/browser/themes/shared/incontentprefs/preferences.inc.css +++ b/browser/themes/shared/incontentprefs/preferences.inc.css @@ -136,111 +136,32 @@ treecol { #applicationsContent { padding: 15px 0; } #filter { -moz-margin-start: 0; } -#handlersView { - -moz-appearance: none; - -moz-margin-start: 0; - font-size: 1.25rem; - line-height: 22px; - color: #333333; - border: 1px solid #C1C1C1; - border-radius: 2px; - background-color: #FBFBFB; - overflow-y: auto; - height: 500px; -} - -#handlersView > listheader { - -moz-appearance: none; - border: 0; - padding: 0; -} - -#typeColumn, -#actionColumn { - -moz-appearance: none; - line-height: 20px; - color: #333333; - height: 36px; - padding: 0 10px; - background-color: #FBFBFB; - border: 1px solid #C1C1C1; - -moz-border-top-colors: none; - -moz-border-right-colors: none; - -moz-border-bottom-colors: none; - -moz-border-left-colors: none; -} - -#typeColumn:-moz-locale-dir(ltr), -#actionColumn:-moz-locale-dir(rtl) { - border-top-left-radius: 2px; -} - -#typeColumn:-moz-locale-dir(rtl), -#actionColumn:-moz-locale-dir(ltr) { - border-top-right-radius: 2px; -} - -#typeColumn:hover, -#actionColumn:hover { - border-color: #0095DD; -} - -#typeColumn:hover:active, -#actionColumn:hover:active { - padding: 0 10px; -} - -#typeColumn > .treecol-sortdirection[sortDirection=ascending], -#actionColumn > .treecol-sortdirection[sortDirection=ascending], -#typeColumn > .treecol-sortdirection[sortDirection=descending], -#actionColumn > .treecol-sortdirection[sortDirection=descending] { - -moz-appearance: none; - list-style-image: url("chrome://global/skin/in-content/sorter.png"); -} - -#typeColumn > .treecol-sortdirection[sortDirection=descending], -#actionColumn > .treecol-sortdirection[sortDirection=descending] { - transform: scaleY(-1); -} - -@media (min-resolution: 2dppx) { - #typeColumn > .treecol-sortdirection[sortDirection=ascending], - #actionColumn > .treecol-sortdirection[sortDirection=ascending], - #typeColumn > .treecol-sortdirection[sortDirection=descending], - #actionColumn > .treecol-sortdirection[sortDirection=descending] { - width: 12px; - height: 8px; - list-style-image: url("chrome://global/skin/in-content/sorter@2x.png"); - } -} - #handlersView > richlistitem { - min-height: 40px !important; + min-height: 36px !important; } .typeIcon { -moz-margin-start: 10px !important; -moz-margin-end: 9px !important; } .actionIcon { -moz-margin-start: 11px !important; -moz-margin-end: 8px !important; } .actionsMenu { - height: 40px; - max-height: 40px; + min-height: 36px; } .actionsMenu > menupopup > menuitem { -moz-padding-start: 10px !important; } .actionsMenu > menupopup > menuitem > .menu-iconic-left { -moz-margin-end: 8px !important;
--- a/docshell/base/nsDocShell.cpp +++ b/docshell/base/nsDocShell.cpp @@ -2893,16 +2893,21 @@ nsDocShell::PopProfileTimelineMarkers(JS // this array. nsTArray<TimelineMarker*> keptMarkers; for (uint32_t i = 0; i < mProfileTimelineMarkers.Length(); ++i) { TimelineMarker* startPayload = mProfileTimelineMarkers[i]; const char* startMarkerName = startPayload->GetName(); bool hasSeenPaintedLayer = false; + bool isPaint = strcmp(startMarkerName, "Paint") == 0; + + // If we are processing a Paint marker, we append information from + // all the embedded Layer markers to this array. + mozilla::dom::Sequence<mozilla::dom::ProfileTimelineLayerRect> layerRectangles; if (startPayload->GetMetaData() == TRACING_INTERVAL_START) { bool hasSeenEnd = false; // DOM events can be nested, so we must take care when searching // for the matching end. It doesn't hurt to apply this logic to // all event types. uint32_t markerDepth = 0; @@ -2910,40 +2915,44 @@ nsDocShell::PopProfileTimelineMarkers(JS // The assumption is that the devtools timeline flushes markers frequently // enough for the amount of markers to always be small enough that the // nested for loop isn't going to be a performance problem. for (uint32_t j = i + 1; j < mProfileTimelineMarkers.Length(); ++j) { TimelineMarker* endPayload = mProfileTimelineMarkers[j]; const char* endMarkerName = endPayload->GetName(); // Look for Layer markers to stream out paint markers. - if (strcmp(endMarkerName, "Layer") == 0) { + if (isPaint && strcmp(endMarkerName, "Layer") == 0) { hasSeenPaintedLayer = true; + endPayload->AddLayerRectangles(layerRectangles); } if (!startPayload->Equals(endPayload)) { continue; } - bool isPaint = strcmp(startMarkerName, "Paint") == 0; // Pair start and end markers. if (endPayload->GetMetaData() == TRACING_INTERVAL_START) { ++markerDepth; } else if (endPayload->GetMetaData() == TRACING_INTERVAL_END) { if (markerDepth > 0) { --markerDepth; } else { // But ignore paint start/end if no layer has been painted. if (!isPaint || (isPaint && hasSeenPaintedLayer)) { mozilla::dom::ProfileTimelineMarker marker; marker.mName = NS_ConvertUTF8toUTF16(startPayload->GetName()); marker.mStart = startPayload->GetTime(); marker.mEnd = endPayload->GetTime(); - startPayload->AddDetails(marker); + if (isPaint) { + marker.mRectangles.Construct(layerRectangles); + } else { + startPayload->AddDetails(marker); + } profileTimelineMarkers.AppendElement(marker); } // We want the start to be dropped either way. hasSeenEnd = true; break; }
--- a/docshell/base/nsDocShell.h +++ b/docshell/base/nsDocShell.h @@ -300,16 +300,21 @@ public: } // Add details specific to this marker type to aMarker. The // standard elements have already been set. virtual void AddDetails(mozilla::dom::ProfileTimelineMarker& aMarker) { } + virtual void AddLayerRectangles(mozilla::dom::Sequence<mozilla::dom::ProfileTimelineLayerRect>&) + { + MOZ_ASSERT_UNREACHABLE("can only be called on layer markers"); + } + const char* GetName() const { return mName; } TracingMetadata GetMetaData() const { return mMetaData;
--- a/docshell/test/browser/browser_timelineMarkers-02.js +++ b/docshell/test/browser/browser_timelineMarkers-02.js @@ -2,43 +2,57 @@ http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; // Test that the docShell profile timeline API returns the right markers when // restyles, reflows and paints occur let URL = '<!DOCTYPE html><style>' + + 'body {margin:0; padding: 0;} ' + 'div {width:100px;height:100px;background:red;} ' + '.resize-change-color {width:50px;height:50px;background:blue;} ' + '.change-color {width:50px;height:50px;background:yellow;} ' + '.add-class {}' + '</style><div></div>'; let TESTS = [{ desc: "Changing the width of the test element", setup: function(div) { div.setAttribute("class", "resize-change-color"); }, check: function(markers) { ok(markers.length > 0, "markers were returned"); console.log(markers); + info(JSON.stringify(markers.filter(m => m.name == "Paint"))); ok(markers.some(m => m.name == "Reflow"), "markers includes Reflow"); ok(markers.some(m => m.name == "Paint"), "markers includes Paint"); + for (let marker of markers.filter(m => m.name == "Paint")) { + // This change should generate at least one rectangle. + ok(marker.rectangles.length >= 1, "marker has one rectangle"); + // One of the rectangles should contain the div. + ok(marker.rectangles.some(r => rectangleContains(r, 0, 0, 100, 100))); + } ok(markers.some(m => m.name == "Styles"), "markers includes Restyle"); } }, { desc: "Changing the test element's background color", setup: function(div) { div.setAttribute("class", "change-color"); }, check: function(markers) { ok(markers.length > 0, "markers were returned"); ok(!markers.some(m => m.name == "Reflow"), "markers doesn't include Reflow"); ok(markers.some(m => m.name == "Paint"), "markers includes Paint"); + for (let marker of markers.filter(m => m.name == "Paint")) { + // This change should generate at least one rectangle. + ok(marker.rectangles.length >= 1, "marker has one rectangle"); + // One of the rectangles should contain the div. + ok(marker.rectangles.some(r => rectangleContains(r, 0, 0, 50, 50))); + } ok(markers.some(m => m.name == "Styles"), "markers includes Restyle"); } }, { desc: "Changing the test element's classname", setup: function(div) { div.setAttribute("class", "change-color add-class"); }, check: function(markers) { @@ -140,8 +154,13 @@ function waitForMarkers(docshell) { if (waitIterationCount > maxWaitIterationCount) { clearInterval(interval); resolve([]); } waitIterationCount++; }, 200); }); } + +function rectangleContains(rect, x, y, width, height) { + return rect.x <= x && rect.y <= y && rect.width >= width && + rect.height >= height; +}
--- a/dom/webidl/ProfileTimelineMarker.webidl +++ b/dom/webidl/ProfileTimelineMarker.webidl @@ -1,16 +1,25 @@ /* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ +dictionary ProfileTimelineLayerRect { + long x = 0; + long y = 0; + long width = 0; + long height = 0; +}; + dictionary ProfileTimelineMarker { DOMString name = ""; DOMHighResTimeStamp start = 0; DOMHighResTimeStamp end = 0; /* For ConsoleTime markers. */ DOMString causeName; /* For DOMEvent markers. */ DOMString type; unsigned short eventPhase; + /* For Paint markers. */ + sequence<ProfileTimelineLayerRect> rectangles; };
--- a/layout/base/FrameLayerBuilder.cpp +++ b/layout/base/FrameLayerBuilder.cpp @@ -21,16 +21,17 @@ #include "LayerTreeInvalidation.h" #include "nsSVGIntegrationUtils.h" #include "ImageContainer.h" #include "ActiveLayerTracker.h" #include "gfx2DGlue.h" #include "mozilla/LookAndFeel.h" #include "nsDocShell.h" #include "nsImageFrame.h" +#include "mozilla/dom/ProfileTimelineMarkerBinding.h" #include "GeckoProfiler.h" #include "mozilla/gfx/Tools.h" #include "mozilla/gfx/2D.h" #include "gfxPrefs.h" #include <algorithm> @@ -4414,16 +4415,46 @@ static void DrawForcedBackgroundColor(Dr { if (NS_GET_A(aBackgroundColor) > 0) { nsIntRect r = aLayer->GetVisibleRegion().GetBounds(); ColorPattern color(ToDeviceColor(aBackgroundColor)); aDrawTarget.FillRect(Rect(r.x, r.y, r.width, r.height), color); } } +class LayerTimelineMarker : public nsDocShell::TimelineMarker +{ +public: + LayerTimelineMarker(nsDocShell* aDocShell, const nsIntRegion& aRegion) + : nsDocShell::TimelineMarker(aDocShell, "Layer", TRACING_EVENT) + , mRegion(aRegion) + { + } + + ~LayerTimelineMarker() + { + } + + virtual void AddLayerRectangles(mozilla::dom::Sequence<mozilla::dom::ProfileTimelineLayerRect>& aRectangles) + { + nsIntRegionRectIterator it(mRegion); + while (const nsIntRect* iterRect = it.Next()) { + mozilla::dom::ProfileTimelineLayerRect rect; + rect.mX = iterRect->X(); + rect.mY = iterRect->Y(); + rect.mWidth = iterRect->Width(); + rect.mHeight = iterRect->Height(); + aRectangles.AppendElement(rect); + } + } + +private: + nsIntRegion mRegion; +}; + /* * A note on residual transforms: * * In a transformed subtree we sometimes apply the PaintedLayer's * "residual transform" when drawing content into the PaintedLayer. * This is a translation by components in the range [-0.5,0.5) provided * by the layer system; applying the residual transform followed by the * transforms used by layer compositing ensures that the subpixel alignment @@ -4563,17 +4594,23 @@ FrameLayerBuilder::DrawPaintedLayer(Pain gfxUtils::ClipToRegion(aContext, aRegionToDraw); } } FlashPaint(aContext); } if (presContext && presContext->GetDocShell() && isActiveLayerManager) { nsDocShell* docShell = static_cast<nsDocShell*>(presContext->GetDocShell()); - docShell->AddProfileTimelineMarker("Layer", TRACING_EVENT); + bool isRecording; + docShell->GetRecordProfileTimelineMarkers(&isRecording); + if (isRecording) { + mozilla::UniquePtr<nsDocShell::TimelineMarker> marker = + MakeUnique<LayerTimelineMarker>(docShell, aRegionToDraw); + docShell->AddProfileTimelineMarker(marker); + } } if (!aRegionToInvalidate.IsEmpty()) { aLayer->AddInvalidRect(aRegionToInvalidate.GetBounds()); } } bool
--- a/mobile/android/base/AppConstants.java.in +++ b/mobile/android/base/AppConstants.java.in @@ -230,16 +230,23 @@ public class AppConstants { // https://wiki.mozilla.org/Platform/Channel-specific_build_defines public static final boolean RELEASE_BUILD = //#ifdef RELEASE_BUILD true; //#else false; //#endif + public static final boolean NIGHTLY_BUILD = +//#ifdef NIGHTLY_BUILD + true; +//#else + false; +//#endif + public static final boolean DEBUG_BUILD = //#ifdef MOZ_DEBUG true; //#else false; //#endif public static final boolean MOZ_MEDIA_PLAYER =
--- a/mobile/android/base/locales/en-US/android_strings.dtd +++ b/mobile/android/base/locales/en-US/android_strings.dtd @@ -167,16 +167,18 @@ <!ENTITY pref_header_vendor "&vendorShortName;"> <!ENTITY pref_header_devtools "Developer tools"> <!ENTITY pref_cookies_menu "Cookies"> <!ENTITY pref_cookies_accept_all "Enabled"> <!ENTITY pref_cookies_not_accept_foreign "Enabled, excluding 3rd party"> <!ENTITY pref_cookies_disabled "Disabled"> +<!ENTITY pref_tracking_protection_title "Tracking protection"> +<!ENTITY pref_tracking_protection_summary "&brandShortName; will prevent sites from tracking you"> <!ENTITY pref_donottrack_title "Do not track"> <!ENTITY pref_donottrack_summary "&brandShortName; will tell sites that you do not want to be tracked"> <!ENTITY pref_char_encoding "Character encoding"> <!ENTITY pref_char_encoding_on "Show menu"> <!ENTITY pref_char_encoding_off "Don\'t show menu"> <!ENTITY pref_clear_private_data2 "Clear now"> <!ENTITY pref_clear_private_data_category "Clear private data">
--- a/mobile/android/base/preferences/GeckoPreferences.java +++ b/mobile/android/base/preferences/GeckoPreferences.java @@ -118,16 +118,18 @@ OnSharedPreferenceChangeListener private static final String PREFS_UPDATER_AUTODOWNLOAD = "app.update.autodownload"; private static final String PREFS_GEO_REPORTING = NON_PREF_PREFIX + "app.geo.reportdata"; private static final String PREFS_GEO_LEARN_MORE = NON_PREF_PREFIX + "geo.learn_more"; private static final String PREFS_HEALTHREPORT_LINK = NON_PREF_PREFIX + "healthreport.link"; private static final String PREFS_DEVTOOLS_REMOTE_ENABLED = "devtools.debugger.remote-enabled"; private static final String PREFS_DISPLAY_REFLOW_ON_ZOOM = "browser.zoom.reflowOnZoom"; private static final String PREFS_DISPLAY_TITLEBAR_MODE = "browser.chrome.titlebarMode"; private static final String PREFS_SYNC = NON_PREF_PREFIX + "sync"; + private static final String PREFS_TRACKING_PROTECTION = "privacy.trackingprotection.enabled"; + private static final String PREFS_TRACKING_PROTECTION_LEARN_MORE = NON_PREF_PREFIX + "trackingprotection.learn_more"; private static final String ACTION_STUMBLER_UPLOAD_PREF = AppConstants.ANDROID_PACKAGE_NAME + ".STUMBLER_PREF"; // This isn't a Gecko pref, even if it looks like one. private static final String PREFS_BROWSER_LOCALE = "locale"; public static final String PREFS_RESTORE_SESSION = NON_PREF_PREFIX + "restoreSession3"; public static final String PREFS_SUGGESTED_SITES = NON_PREF_PREFIX + "home_suggested_sites"; @@ -677,16 +679,23 @@ OnSharedPreferenceChangeListener i--; continue; } else if ((AppConstants.RELEASE_BUILD || !HardwareUtils.isTablet()) && PREFS_NEW_TABLET_UI.equals(key)) { // Remove toggle for new tablet UI on release builds and phones. preferences.removePreference(pref); i--; continue; + } else if (!AppConstants.NIGHTLY_BUILD && + (PREFS_TRACKING_PROTECTION.equals(key) || + PREFS_TRACKING_PROTECTION_LEARN_MORE.equals(key))) { + // Remove UI for tracking protection preference on non-Nightly builds. + preferences.removePreference(pref); + i--; + continue; } else if (!AppConstants.MOZ_TELEMETRY_REPORTING && PREFS_TELEMETRY_ENABLED.equals(key)) { preferences.removePreference(pref); i--; continue; } else if (!AppConstants.MOZ_SERVICES_HEALTHREPORT && (PREFS_HEALTHREPORT_UPLOAD_ENABLED.equals(key) || PREFS_HEALTHREPORT_LINK.equals(key))) {
--- a/mobile/android/base/resources/xml/preferences_privacy.xml +++ b/mobile/android/base/resources/xml/preferences_privacy.xml @@ -3,36 +3,50 @@ - 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/. --> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" xmlns:gecko="http://schemas.android.com/apk/res-auto" android:title="@string/pref_category_privacy_short" android:enabled="false"> + <CheckBoxPreference android:key="privacy.trackingprotection.enabled" + android:title="@string/pref_tracking_protection_title" + android:summary="@string/pref_tracking_protection_summary" + android:persistent="false" /> + + <org.mozilla.gecko.preferences.AlignRightLinkPreference + android:key="android.not_a_preference.trackingprotection.learn_more" + android:title="@string/pref_learn_more" + android:persistent="false" + url="https://support.mozilla.org/kb/firefox-android-tracking-protection" /> + <CheckBoxPreference android:key="privacy.donottrackheader.enabled" android:title="@string/pref_donottrack_title" android:summary="@string/pref_donottrack_summary" - android:defaultValue="false" android:persistent="false" /> + <org.mozilla.gecko.preferences.AlignRightLinkPreference + android:key="android.not_a_preference.donottrackheader.learn_more" + android:title="@string/pref_learn_more" + android:persistent="false" + url="https://www.mozilla.org/firefox/dnt/" /> + <ListPreference android:key="network.cookie.cookieBehavior" android:title="@string/pref_cookies_menu" android:entries="@array/pref_cookies_entries" android:entryValues="@array/pref_cookies_values" android:persistent="false" /> <CheckBoxPreference android:key="signon.rememberSignons" android:title="@string/pref_remember_signons" - android:defaultValue="true" android:persistent="false" /> <CheckBoxPreference android:key="privacy.masterpassword.enabled" android:title="@string/pref_use_master_password" - android:defaultValue="false" android:persistent="false" /> <!-- keys prefixed with "android.not_a_preference." are not synced with Gecko --> <PreferenceCategory android:title="@string/pref_clear_private_data_category"> <org.mozilla.gecko.preferences.PrivateDataPreference android:key="android.not_a_preference.privacy.clear" android:title="@string/pref_clear_private_data"
--- a/mobile/android/base/strings.xml.in +++ b/mobile/android/base/strings.xml.in @@ -177,16 +177,18 @@ <string name="pref_remember_signons">&pref_remember_signons;</string> <string name="pref_cookies_menu">&pref_cookies_menu;</string> <string name="pref_cookies_accept_all">&pref_cookies_accept_all;</string> <string name="pref_cookies_not_accept_foreign">&pref_cookies_not_accept_foreign;</string> <string name="pref_cookies_disabled">&pref_cookies_disabled;</string> + <string name="pref_tracking_protection_title">&pref_tracking_protection_title;</string> + <string name="pref_tracking_protection_summary">&pref_tracking_protection_summary;</string> <string name="pref_donottrack_title">&pref_donottrack_title;</string> <string name="pref_donottrack_summary">&pref_donottrack_summary;</string> <string name="pref_char_encoding">&pref_char_encoding;</string> <string name="pref_char_encoding_on">&pref_char_encoding_on;</string> <string name="pref_char_encoding_off">&pref_char_encoding_off;</string> <string name="pref_clear_private_data">&pref_clear_private_data2;</string> <string name="pref_clear_private_data_category">&pref_clear_private_data_category;</string>
--- a/mobile/android/base/tests/BaseRobocopTest.java +++ b/mobile/android/base/tests/BaseRobocopTest.java @@ -50,16 +50,19 @@ public abstract class BaseRobocopTest ex cl = Activity.class; } BROWSER_INTENT_CLASS = cl; } protected Assert mAsserter; protected String mLogFile; + protected String mBaseHostnameUrl; + protected String mBaseIpUrl; + protected Map<String, String> mConfig; protected String mRootPath; /** * The browser is started at the beginning of this test. A single test is a * class inheriting from <code>BaseRobocopTest</code> that contains test * methods. * <p> @@ -107,16 +110,19 @@ public abstract class BaseRobocopTest ex // Initialize the asserter. if (getTestType() == Type.TALOS) { mAsserter = new FennecTalosAssert(); } else { mAsserter = new FennecMochitestAssert(); } mAsserter.setLogFile(mLogFile); mAsserter.setTestName(getClass().getName()); + + mBaseHostnameUrl = mConfig.get("host").replaceAll("(/$)", ""); + mBaseIpUrl = mConfig.get("rawhost").replaceAll("(/$)", ""); } /** * Function to early abort if we can't reach the given HTTP server. Provides local testers * with diagnostic information. Not currently available for TALOS tests, which are rarely run * locally in any case. */ public void throwIfHttpGetFails() {
--- a/mobile/android/base/tests/BaseTest.java +++ b/mobile/android/base/tests/BaseTest.java @@ -72,18 +72,16 @@ abstract class BaseTest extends BaseRobo private static final String URL_HTTP_PREFIX = "http://"; private Activity mActivity; private int mPreferenceRequestID = 0; protected Solo mSolo; protected Driver mDriver; protected Actions mActions; - protected String mBaseUrl; - protected String mRawBaseUrl; protected String mProfile; public Device mDevice; protected DatabaseHelper mDatabaseHelper; protected int mScreenMidWidth; protected int mScreenMidHeight; private final HashSet<Integer> mKnownTabIDs = new HashSet<Integer>(); protected void blockForDelayedStartup() { @@ -108,18 +106,16 @@ abstract class BaseTest extends BaseRobo } } @Override public void setUp() throws Exception { super.setUp(); // Create the intent to be used with all the important arguments. - mBaseUrl = mConfig.get("host").replaceAll("(/$)", ""); - mRawBaseUrl = mConfig.get("rawhost").replaceAll("(/$)", ""); Intent i = new Intent(Intent.ACTION_MAIN); mProfile = mConfig.get("profile"); i.putExtra("args", "-no-remote -profile " + mProfile); String envString = mConfig.get("envvars"); if (envString != "") { String[] envStrings = envString.split(","); for (int iter = 0; iter < envStrings.length; iter++) { i.putExtra("env" + iter, envStrings[iter]); @@ -306,21 +302,21 @@ abstract class BaseTest extends BaseRobo @Override public boolean isSatisfied() { String textValue = mTextView.getText().toString(); return mExpected.equals(textValue); } } protected final String getAbsoluteUrl(String url) { - return mBaseUrl + "/" + url.replaceAll("(^/)", ""); + return mBaseHostnameUrl + "/" + url.replaceAll("(^/)", ""); } protected final String getAbsoluteRawUrl(String url) { - return mRawBaseUrl + "/" + url.replaceAll("(^/)", ""); + return mBaseIpUrl + "/" + url.replaceAll("(^/)", ""); } /* * Wrapper method for mSolo.waitForCondition with additional logging. */ protected final boolean waitForCondition(Condition condition, int timeout) { boolean result = mSolo.waitForCondition(condition, timeout); if (!result) {
--- a/mobile/android/base/tests/StringHelper.java +++ b/mobile/android/base/tests/StringHelper.java @@ -172,17 +172,18 @@ public class StringHelper { public static final String CHARACTER_ENCODING_LABEL = "Character encoding"; public static final String PLUGINS_LABEL = "Plugins"; // Title bar public static final String SHOW_PAGE_TITLE_LABEL = "Show page title"; public static final String SHOW_PAGE_ADDRESS_LABEL = "Show page address"; // Privacy - public static final String TRACKING_LABEL = "Do not track"; + public static final String TRACKING_PROTECTION_LABEL = "Tracking protection"; + public static final String DNT_LABEL = "Do not track"; public static final String COOKIES_LABEL = "Cookies"; public static final String REMEMBER_PASSWORDS_LABEL = "Remember passwords"; public static final String MASTER_PASSWORD_LABEL = "Use master password"; public static final String CLEAR_PRIVATE_DATA_LABEL = "Clear now"; // Mozilla public static final String BRAND_NAME = "(Fennec|Nightly|Aurora|Firefox Beta|Firefox)"; public static final String ABOUT_LABEL = "About " + BRAND_NAME;
--- a/mobile/android/base/tests/UITest.java +++ b/mobile/android/base/tests/UITest.java @@ -40,21 +40,16 @@ abstract class UITest extends BaseRoboco private static final String JUNIT_FAILURE_MSG = "A JUnit method was called. Make sure " + "you are using AssertionHelper to make assertions. Try `fAssert*(...);`"; private Solo mSolo; private Driver mDriver; private Actions mActions; - // Base to build hostname URLs - private String mBaseHostnameUrl; - // Base to build IP URLs - private String mBaseIpUrl; - protected AboutHomeComponent mAboutHome; protected AppMenuComponent mAppMenu; protected GeckoViewComponent mGeckoView; protected ToolbarComponent mToolbar; @Override protected void setUp() throws Exception { super.setUp(); @@ -63,19 +58,16 @@ abstract class UITest extends BaseRoboco final Intent intent = createActivityIntent(mConfig); setActivityIntent(intent); final Activity activity = getActivity(); mSolo = new Solo(getInstrumentation(), activity); mDriver = new FennecNativeDriver(activity, mSolo, mRootPath); mActions = new FennecNativeActions(activity, mSolo, getInstrumentation(), mAsserter); - mBaseHostnameUrl = mConfig.get("host").replaceAll("(/$)", ""); - mBaseIpUrl = mConfig.get("rawhost").replaceAll("(/$)", ""); - // Helpers depend on components so initialize them first. initComponents(); initHelpers(); // Ensure Robocop tests have access to network, and are run with Display powered on. throwIfHttpGetFails(); throwIfScreenNotOn(); }
--- a/mobile/android/base/tests/testSettingsMenuItems.java +++ b/mobile/android/base/tests/testSettingsMenuItems.java @@ -55,17 +55,18 @@ public class testSettingsMenuItems exten { "Advanced" }, { StringHelper.CHARACTER_ENCODING_LABEL, "Don't show menu", "Show menu", "Don't show menu" }, { StringHelper.PLUGINS_LABEL, "Tap to play", "Enabled", "Tap to play", "Disabled" }, }; // Privacy menu items. String[] PATH_PRIVACY = { StringHelper.PRIVACY_SECTION_LABEL }; String[][] OPTIONS_PRIVACY = { - { StringHelper.TRACKING_LABEL }, + { StringHelper.TRACKING_PROTECTION_LABEL }, + { StringHelper.DNT_LABEL }, { StringHelper.COOKIES_LABEL, "Enabled", "Enabled, excluding 3rd party", "Disabled" }, { StringHelper.REMEMBER_PASSWORDS_LABEL }, { StringHelper.MASTER_PASSWORD_LABEL }, { StringHelper.CLEAR_PRIVATE_DATA_LABEL, "", "Browsing history", "Downloads", "Form & search history", "Cookies & active logins", "Saved passwords", "Cache", "Offline website data", "Site settings", "Clear data" }, }; // Mozilla/vendor menu items. String[] PATH_MOZILLA = { StringHelper.MOZILLA_SECTION_LABEL };
--- a/mobile/android/base/toolbar/SiteIdentityPopup.java +++ b/mobile/android/base/toolbar/SiteIdentityPopup.java @@ -34,17 +34,17 @@ import android.widget.TextView; */ public class SiteIdentityPopup extends ArrowPopup { private static final String LOGTAG = "GeckoSiteIdentityPopup"; private static final String MIXED_CONTENT_SUPPORT_URL = "https://support.mozilla.org/kb/how-does-insecure-content-affect-safety-android"; private static final String TRACKING_CONTENT_SUPPORT_URL = - "https://support.mozilla.org/kb/how-does-insecure-content-affect-safety-android"; + "https://support.mozilla.org/kb/firefox-android-tracking-protection"; private SiteIdentity mSiteIdentity; private LinearLayout mIdentity; private LinearLayout mIdentityKnownContainer; private LinearLayout mIdentityUnknownContainer;
--- a/toolkit/components/places/Bookmarks.jsm +++ b/toolkit/components/places/Bookmarks.jsm @@ -451,93 +451,25 @@ let Bookmarks = Object.freeze({ * * Note that roots are preserved, only their children will be removed. * * @return {Promise} resolved when the removal is complete. * @resolves once the removal is complete. */ eraseEverything: Task.async(function* () { let db = yield DBConnPromised; - yield db.executeTransaction(function* () { - let rows = yield db.executeCached( - `WITH RECURSIVE - descendants(did) AS ( - SELECT b.id FROM moz_bookmarks b - JOIN moz_bookmarks p ON b.parent = p.id - WHERE p.guid IN ( :toolbarGuid, :menuGuid, :unfiledGuid ) - UNION ALL - SELECT id FROM moz_bookmarks - JOIN descendants ON parent = did - ) - SELECT b.id AS _id, b.parent AS _parentId, b.position AS 'index', - b.type, url, b.guid, p.guid AS parentGuid, b.dateAdded, - b.lastModified, b.title, p.parent AS _grandParentId, - NULL AS _childCount, NULL AS keyword - FROM moz_bookmarks b - JOIN moz_bookmarks p ON p.id = b.parent - LEFT JOIN moz_places h ON b.fk = h.id - WHERE b.id IN descendants - `, { menuGuid: this.menuGuid, toolbarGuid: this.toolbarGuid, - unfiledGuid: this.unfiledGuid }); - let items = rowsToItemsArray(rows); - - yield db.executeCached( - `WITH RECURSIVE - descendants(did) AS ( - SELECT b.id FROM moz_bookmarks b - JOIN moz_bookmarks p ON b.parent = p.id - WHERE p.guid IN ( :toolbarGuid, :menuGuid, :unfiledGuid ) - UNION ALL - SELECT id FROM moz_bookmarks - JOIN descendants ON parent = did - ) - DELETE FROM moz_bookmarks WHERE id IN descendants - `, { menuGuid: this.menuGuid, toolbarGuid: this.toolbarGuid, - unfiledGuid: this.unfiledGuid }); - - // Clenup orphans. - yield removeOrphanAnnotations(db); - yield removeOrphanKeywords(db); - - // TODO (Bug 1087576): this may leave orphan tags behind. - - // Update roots' lastModified. - yield db.executeCached( - `UPDATE moz_bookmarks SET lastModified = :time - WHERE id IN (SELECT id FROM moz_bookmarks - WHERE guid IN ( :rootGuid, :toolbarGuid, :menuGuid, :unfiledGuid )) - `, { time: toPRTime(new Date()), rootGuid: this.rootGuid, - menuGuid: this.menuGuid, toolbarGuid: this.toolbarGuid, - unfiledGuid: this.unfiledGuid }); - - let urls = [for (item of items) if (item.url) item.url]; - updateFrecency(db, urls).then(null, Cu.reportError); - - // Send onItemRemoved notifications to listeners. - // TODO (Bug 1087580): this should send a single clear bookmarks - // notification rather than notifying for each bookmark. - - // Notify listeners in reverse order to serve children before parents. - let observers = PlacesUtils.bookmarks.getObservers(); - for (let item of items.reverse()) { - let uri = item.hasOwnProperty("url") ? toURI(item.url) : null; - notify(observers, "onItemRemoved", [ item._id, item._parentId, - item.index, item.type, uri, - item.guid, item.parentGuid ]); - - let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId; - if (isUntagging) { - for (let entry of (yield fetchBookmarksByURL(item))) { - notify(observers, "onItemChanged", [ entry._id, "tags", false, "", - toPRTime(entry.lastModified), - entry.type, entry._parentId, - entry.guid, entry.parentGuid ]); - } - } + const folderGuids = [this.toolbarGuid, this.menuGuid, this.unfiledGuid]; + yield removeFoldersContents(db, folderGuids); + const time = toPRTime(new Date()); + for (let folderGuid of folderGuids) { + yield db.executeCached( + `UPDATE moz_bookmarks SET lastModified = :time + WHERE id IN (SELECT id FROM moz_bookmarks WHERE guid = :folderGuid ) + `, { folderGuid, time }); } }.bind(this)); }), /** * Fetches information about a bookmark-item. * * REMARK: any successful call to this method resolves to a single @@ -1021,16 +953,20 @@ function* fetchBookmarksByKeyword(info) // Remove implementation. function* removeBookmark(item) { let db = yield DBConnPromised; let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId; yield db.executeTransaction(function* transaction() { + // If it's a folder, remove its contents first. + if (item.type == Bookmarks.TYPE_FOLDER) + yield removeFoldersContents(db, [item.guid]); + // Remove annotations first. If it's a tag, we can avoid paying that cost. if (!isUntagging) { // We don't go through the annotations service for this cause otherwise // we'd get a pointless onItemChanged notification and it would also // set lastModified to an unexpected value. yield removeAnnotationsForItem(db, item._id); } @@ -1409,8 +1345,88 @@ let setAncestorsLastModified = Task.asyn JOIN ancestors ON id = aid WHERE type = :type ) UPDATE moz_bookmarks SET lastModified = :time WHERE id IN ancestors `, { guid: folderGuid, type: Bookmarks.TYPE_FOLDER, time: toPRTime(time) }); }); + +/** + * Remove all descendants of one or more bookmark folders. + * + * @param db + * the Sqlite.jsm connection handle. + * @param folderGuids + * array of folder guids. + */ +let removeFoldersContents = +Task.async(function* (db, folderGuids) { + let itemsRemoved = []; + for (let folderGuid of folderGuids) { + let rows = yield db.executeCached( + `WITH RECURSIVE + descendants(did) AS ( + SELECT b.id FROM moz_bookmarks b + JOIN moz_bookmarks p ON b.parent = p.id + WHERE p.guid = :folderGuid + UNION ALL + SELECT id FROM moz_bookmarks + JOIN descendants ON parent = did + ) + SELECT b.id AS _id, b.parent AS _parentId, b.position AS 'index', + b.type, url, b.guid, p.guid AS parentGuid, b.dateAdded, + b.lastModified, b.title, p.parent AS _grandParentId, + NULL AS _childCount, NULL AS keyword + FROM moz_bookmarks b + JOIN moz_bookmarks p ON p.id = b.parent + LEFT JOIN moz_places h ON b.fk = h.id + WHERE b.id IN descendants`, { folderGuid }); + + itemsRemoved = itemsRemoved.concat(rowsToItemsArray(rows)); + + yield db.executeCached( + `WITH RECURSIVE + descendants(did) AS ( + SELECT b.id FROM moz_bookmarks b + JOIN moz_bookmarks p ON b.parent = p.id + WHERE p.guid = :folderGuid + UNION ALL + SELECT id FROM moz_bookmarks + JOIN descendants ON parent = did + ) + DELETE FROM moz_bookmarks WHERE id IN descendants`, { folderGuid }); + } + + // Cleanup orphans. + yield removeOrphanAnnotations(db); + yield removeOrphanKeywords(db); + + // TODO (Bug 1087576): this may leave orphan tags behind. + + let urls = [for (item of itemsRemoved) if (item.url) item.url]; + updateFrecency(db, urls).then(null, Cu.reportError); + + // Send onItemRemoved notifications to listeners. + // TODO (Bug 1087580): for the case of eraseEverything, this should send a + // single clear bookmarks notification rather than notifying for each + // bookmark. + + // Notify listeners in reverse order to serve children before parents. + let observers = PlacesUtils.bookmarks.getObservers(); + for (let item of itemsRemoved.reverse()) { + let uri = item.hasOwnProperty("url") ? toURI(item.url) : null; + notify(observers, "onItemRemoved", [ item._id, item._parentId, + item.index, item.type, uri, + item.guid, item.parentGuid ]); + + let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId; + if (isUntagging) { + for (let entry of (yield fetchBookmarksByURL(item))) { + notify(observers, "onItemChanged", [ entry._id, "tags", false, "", + toPRTime(entry.lastModified), + entry.type, entry._parentId, + entry.guid, entry.parentGuid ]); + } + } + } +});
--- a/toolkit/components/places/tests/bookmarks/test_bookmarks_notifications.js +++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_notifications.js @@ -310,16 +310,56 @@ add_task(function* remove_bookmark_tag_n tag.url, tag.guid, tag.parentGuid ] }, { name: "onItemChanged", arguments: [ itemId, "tags", false, "", bm.lastModified, bm.type, parentId, bm.guid, bm.parentGuid ] } ]); }); +add_task(function* remove_folder_notification() { + let folder1 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid }); + let folder1Id = yield PlacesUtils.promiseItemId(folder1.guid); + let folder1ParentId = yield PlacesUtils.promiseItemId(folder1.parentGuid); + + let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: folder1.guid, + url: new URL("http://example.com/") }); + let bmItemId = yield PlacesUtils.promiseItemId(bm.guid); + + let folder2 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: folder1.guid }); + let folder2Id = yield PlacesUtils.promiseItemId(folder2.guid); + + let bm2 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: folder2.guid, + url: new URL("http://example.com/") }); + let bm2ItemId = yield PlacesUtils.promiseItemId(bm2.guid); + + let observer = expectNotifications(); + yield PlacesUtils.bookmarks.remove(folder1.guid); + + observer.check([ { name: "onItemRemoved", + arguments: [ bm2ItemId, folder2Id, bm2.index, bm2.type, + bm2.url, bm2.guid, bm2.parentGuid ] }, + { name: "onItemRemoved", + arguments: [ folder2Id, folder1Id, folder2.index, + folder2.type, null, folder2.guid, + folder2.parentGuid ] }, + { name: "onItemRemoved", + arguments: [ bmItemId, folder1Id, bm.index, bm.type, + bm.url, bm.guid, bm.parentGuid ] }, + { name: "onItemRemoved", + arguments: [ folder1Id, folder1ParentId, folder1.index, + folder1.type, null, folder1.guid, + folder1.parentGuid ] } + ]); +}); + add_task(function* eraseEverything_notification() { // Let's start from a clean situation. yield PlacesUtils.bookmarks.eraseEverything(); let folder1 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER, parentGuid: PlacesUtils.bookmarks.unfiledGuid }); let folder1Id = yield PlacesUtils.promiseItemId(folder1.guid); let folder1ParentId = yield PlacesUtils.promiseItemId(folder1.parentGuid); @@ -343,39 +383,39 @@ add_task(function* eraseEverything_notif let menuBm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, parentGuid: PlacesUtils.bookmarks.menuGuid, url: new URL("http://example.com/") }); let menuBmId = yield PlacesUtils.promiseItemId(menuBm.guid); let menuBmParentId = yield PlacesUtils.promiseItemId(menuBm.parentGuid); let observer = expectNotifications(); - let removed = yield PlacesUtils.bookmarks.eraseEverything(); + yield PlacesUtils.bookmarks.eraseEverything(); observer.check([ { name: "onItemRemoved", - arguments: [ menuBmId, menuBmParentId, - menuBm.index, menuBm.type, - menuBm.url, menuBm.guid, - menuBm.parentGuid ] }, - { name: "onItemRemoved", - arguments: [ toolbarBmId, toolbarBmParentId, - toolbarBm.index, toolbarBm.type, - toolbarBm.url, toolbarBm.guid, - toolbarBm.parentGuid ] }, - { name: "onItemRemoved", arguments: [ folder2Id, folder2ParentId, folder2.index, folder2.type, null, folder2.guid, folder2.parentGuid ] }, { name: "onItemRemoved", arguments: [ itemId, parentId, bm.index, bm.type, bm.url, bm.guid, bm.parentGuid ] }, { name: "onItemRemoved", arguments: [ folder1Id, folder1ParentId, folder1.index, folder1.type, null, folder1.guid, - folder1.parentGuid ] } + folder1.parentGuid ] }, + { name: "onItemRemoved", + arguments: [ menuBmId, menuBmParentId, + menuBm.index, menuBm.type, + menuBm.url, menuBm.guid, + menuBm.parentGuid ] }, + { name: "onItemRemoved", + arguments: [ toolbarBmId, toolbarBmParentId, + toolbarBm.index, toolbarBm.type, + toolbarBm.url, toolbarBm.guid, + toolbarBm.parentGuid ] } ]); }); function expectNotifications() { let notifications = []; let observer = new Proxy(NavBookmarkObserver, { get(target, name) { if (name == "check") {
--- a/toolkit/components/places/tests/bookmarks/test_bookmarks_remove.js +++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_remove.js @@ -142,16 +142,30 @@ add_task(function* remove_folder() { Assert.equal(bm2.index, 0); Assert.deepEqual(bm2.dateAdded, bm2.lastModified); Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_FOLDER); Assert.equal(bm2.title, "a folder"); Assert.ok(!("url" in bm2)); Assert.ok(!("keyword" in bm2)); }); +add_task(function* test_nested_contents_removed() { + let folder1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "a folder" }); + let folder2 = yield PlacesUtils.bookmarks.insert({ parentGuid: folder1.guid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "a folder" }); + let sep = yield PlacesUtils.bookmarks.insert({ parentGuid: folder2.guid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR }); + yield PlacesUtils.bookmarks.remove(folder1); + Assert.strictEqual((yield PlacesUtils.bookmarks.fetch(folder1.guid)), null); + Assert.strictEqual((yield PlacesUtils.bookmarks.fetch(folder2.guid)), null); + Assert.strictEqual((yield PlacesUtils.bookmarks.fetch(sep.guid)), null); +}); add_task(function* remove_folder_empty_title() { let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, type: PlacesUtils.bookmarks.TYPE_FOLDER, title: "" }); checkBookmarkObject(bm1); let bm2 = yield PlacesUtils.bookmarks.remove(bm1.guid); checkBookmarkObject(bm2);
--- a/toolkit/themes/shared/in-content/common.inc.css +++ b/toolkit/themes/shared/in-content/common.inc.css @@ -161,16 +161,32 @@ xul|menulist[open="true"]:not([disabled= html|button:disabled, xul|button[disabled="true"], xul|colorpicker[type="button"][disabled="true"], xul|menulist[disabled="true"] { cursor: not-allowed; opacity: 0.5; } +*|button.primary { + background-color: #0095dd; + border-color: transparent; + color: #fff; +} + +html|button.primary:enabled:hover, +xul|button.primary:not([disabled="true"]):hover { + background-color: #008acb; +} + +html|button.primary:enabled:hover:active, +xul|button.primary:not([disabled="true"]):hover:active { + background-color: #006b9d; +} + xul|colorpicker[type="button"] { padding: 6px; width: 50px; } xul|button > xul|*.button-box, xul|menulist > xul|*.menulist-label-box { padding-right: 10px !important; @@ -556,8 +572,83 @@ xul|filefield + xul|button:-moz-locale-d border-top-right-radius: 0; border-bottom-right-radius: 0; } xul|textbox + xul|button, xul|filefield + xul|button { -moz-border-start: none; } + +/* List boxes */ + +xul|richlistbox, +xul|listbox { + -moz-appearance: none; + border: 1px solid #c1c1c1; +} + +xul|treechildren::-moz-tree-row, +xul|listbox xul|listitem { + padding: 5px; + margin: 0; + border: none; + background-image: none; +} + +xul|treechildren::-moz-tree-row(selected), +xul|listbox xul|listitem[selected="true"] { + background-color: #f1f1f1; +} + +/* Trees */ + +xul|tree { + -moz-appearance: none; + font-size: 1em; + border: 1px solid #c1c1c1; +} + +xul|listheader, +xul|treecols { + -moz-appearance: none; + border: none; + border-bottom: 1px solid #c1c1c1; + padding: 0; +} + +xul|treecol:not([hideheader="true"]), +xul|treecolpicker { + -moz-appearance: none; + border: none; + background-color: #ebebeb; + color: #808080; + padding: 5px 10px; +} + +xul|treecol:not([hideheader="true"]):hover, +xul|treecolpicker:hover { + background-color: #dadada; + color: #333; +} + +xul|treecol:not([hideheader="true"]):not(:first-child), +xul|treecolpicker { + -moz-border-start-width: 1px; + -moz-border-start-style: solid; + border-image: linear-gradient(transparent 0%, transparent 20%, #c1c1c1 20%, #c1c1c1 80%, transparent 80%, transparent 100%) 1 1; +} + +xul|treecol:not([hideheader="true"]) > xul|*.treecol-sortdirection[sortDirection] { + list-style-image: url("chrome://global/skin/in-content/sorter.png"); + width: 12px; + height: 8px; +} + +xul|treecol:not([hideheader="true"]) > xul|*.treecol-sortdirection[sortDirection="descending"] { + transform: scaleY(-1); +} + +@media (min-resolution: 2dppx) { + xul|treecol:not([hideheader="true"]) > xul|*.treecol-sortdirection[sortDirection] { + list-style-image: url("chrome://global/skin/in-content/sorter@2x.png"); + } +} \ No newline at end of file