Merge mozilla-central to mozilla-inbound

This commit is contained in:
Carsten "Tomcat" Book 2016-05-31 14:42:43 +02:00
commit 5ec1e29d9a
89 changed files with 2411 additions and 750 deletions

View File

@ -105,6 +105,7 @@ devtools/client/webaudioeditor/**
devtools/client/webconsole/**
!devtools/client/webconsole/panel.js
!devtools/client/webconsole/jsterm.js
!devtools/client/webconsole/console-commands.js
devtools/client/webide/**
devtools/server/**
!devtools/server/actors/webbrowser.js

View File

@ -22,4 +22,4 @@
# changes to stick? As of bug 928195, this shouldn't be necessary! Please
# don't change CLOBBER for WebIDL changes any more.
Bug 1271829 - Compile nss with SSE2 optimizations
Bug 1247047 - Update Android dependencies (Google Play Services)

View File

@ -8,21 +8,19 @@ module.metadata = {
"stability": "experimental"
};
const { Class } = require("./heritage");
const { Observer, subscribe, unsubscribe, observe } = require("./observer");
const { isWeak, WeakReference } = require("./reference");
const { isWeak } = require("./reference");
const SDKWeakSet = require("../lang/weak-set");
const method = require("../../method/core");
const unloadSubject = require('@loader/unload');
const addonUnloadTopic = "sdk:loader:destroy";
const uninstall = method("disposable/uninstall");
exports.uninstall = uninstall;
const shutdown = method("disposable/shutdown");
exports.shutdown = shutdown;
@ -42,28 +40,94 @@ const dispose = method("disposable/dispose");
exports.dispose = dispose;
dispose.define(Object, object => object.dispose());
const setup = method("disposable/setup");
exports.setup = setup;
setup.define(Object, (object, ...args) => object.setup(...args));
// DisposablesUnloadObserver is the class which subscribe the
// Observer Service to be notified when the add-on loader is
// unloading to be able to dispose all the existent disposables.
const DisposablesUnloadObserver = Class({
implements: [Observer],
initialize: function(...args) {
// Set of the non-weak disposables registered to be disposed.
this.disposables = new Set();
// Target of the weak disposables registered to be disposed
// (and tracked on this target using the SDK weak-set module).
this.weakDisposables = {};
},
subscribe(disposable) {
if (isWeak(disposable)) {
SDKWeakSet.add(this.weakDisposables, disposable);
} else {
this.disposables.add(disposable);
}
},
unsubscribe(disposable) {
if (isWeak(disposable)) {
SDKWeakSet.remove(this.weakDisposables, disposable);
} else {
this.disposables.delete(disposable);
}
},
tryUnloadDisposable(disposable) {
try {
if (disposable) {
unload(disposable);
}
} catch(e) {
console.error("Error unloading a",
isWeak(disposable) ? "weak disposable" : "disposable",
disposable, e);
}
},
unloadAll() {
// Remove all the subscribed disposables.
for (let disposable of this.disposables) {
this.tryUnloadDisposable(disposable);
}
this.disposables.clear();
// Remove all the subscribed weak disposables.
for (let disposable of SDKWeakSet.iterator(this.weakDisposables)) {
this.tryUnloadDisposable(disposable);
}
SDKWeakSet.clear(this.weakDisposables);
}
});
const disposablesUnloadObserver = new DisposablesUnloadObserver();
// The DisposablesUnloadObserver instance is the only object which subscribes
// the Observer Service directly, it observes add-on unload notifications in
// order to trigger `unload` on all its subscribed disposables.
observe.define(DisposablesUnloadObserver, (obj, subject, topic, data) => {
const isUnloadTopic = topic === addonUnloadTopic;
const isUnloadSubject = subject.wrappedJSObject === unloadSubject;
if (isUnloadTopic && isUnloadSubject) {
unsubscribe(disposablesUnloadObserver, addonUnloadTopic);
disposablesUnloadObserver.unloadAll();
}
});
subscribe(disposablesUnloadObserver, addonUnloadTopic, false);
// Set's up disposable instance.
const setupDisposable = disposable => {
subscribe(disposable, addonUnloadTopic, isWeak(disposable));
disposablesUnloadObserver.subscribe(disposable);
};
exports.setupDisposable = setupDisposable;
// Tears down disposable instance.
const disposeDisposable = disposable => {
unsubscribe(disposable, addonUnloadTopic);
disposablesUnloadObserver.unsubscribe(disposable);
};
exports.disposeDisposable = disposeDisposable;
// Base type that takes care of disposing it's instances on add-on unload.
// Also makes sure to remove unload listener if it's already being disposed.
const Disposable = Class({
implements: [Observer],
initialize: function(...args) {
// First setup instance before initializing it's disposal. If instance
// fails to initialize then there is no instance to be disposed at the
@ -86,17 +150,6 @@ const Disposable = Class({
});
exports.Disposable = Disposable;
// Disposable instances observe add-on unload notifications in
// order to trigger `unload` on them.
observe.define(Disposable, (disposable, subject, topic, data) => {
const isUnloadTopic = topic === addonUnloadTopic;
const isUnloadSubject = subject.wrappedJSObject === unloadSubject;
if (isUnloadTopic && isUnloadSubject) {
unsubscribe(disposable, topic);
unload(disposable);
}
});
const unloaders = {
destroy: dispose,
uninstall: uninstall,
@ -104,7 +157,8 @@ const unloaders = {
disable: disable,
upgrade: upgrade,
downgrade: downgrade
}
};
const unloaded = new WeakMap();
unload.define(Disposable, (disposable, reason) => {
if (!unloaded.get(disposable)) {
@ -117,9 +171,8 @@ unload.define(Disposable, (disposable, reason) => {
}
});
// If add-on is disabled munally, it's being upgraded, downgraded
// or uniststalled `dispose` is invoked to undo any changes that
// If add-on is disabled manually, it's being upgraded, downgraded
// or uninstalled `dispose` is invoked to undo any changes that
// has being done by it in this session.
disable.define(Disposable, dispose);
downgrade.define(Disposable, dispose);

View File

@ -9,7 +9,36 @@ const { Disposable } = require("sdk/core/disposable");
const { Cc, Ci, Cu } = require("chrome");
const { setTimeout } = require("sdk/timers");
exports["test disposeDisposable"] = assert => {
let loader = Loader(module);
const { Disposable, disposeDisposable } = loader.require("sdk/core/disposable");
const { isWeak, WeakReference } = loader.require("sdk/core/reference");
let disposals = 0;
const Foo = Class({
extends: Disposable,
implements: [WeakReference],
dispose(...params) {
disposeDisposable(this);
disposals = disposals + 1;
}
});
const f1 = new Foo();
assert.equal(isWeak(f1), true, "f1 has WeakReference support");
f1.dispose();
assert.equal(disposals, 1, "disposed on dispose");
loader.unload("uninstall");
assert.equal(disposals, 1, "after disposeDisposable, dispose is not called anymore");
};
exports["test destroy reasons"] = assert => {
let disposals = 0;
const Foo = Class({
extends: Disposable,
dispose: function() {
@ -17,7 +46,6 @@ exports["test destroy reasons"] = assert => {
}
});
let disposals = 0;
const f1 = new Foo();
f1.destroy();
@ -38,9 +66,10 @@ exports["test destroy reasons"] = assert => {
const f3 = new Foo();
f3.destroy("shutdown");
assert.equal(disposals, 0, "shutdown doesn't invoke disposal");
assert.equal(disposals, 0, "shutdown invoke disposal");
f3.destroy("shutdown");
f3.destroy();
assert.equal(disposals, 0, "shutdown already skipped disposal");
assert.equal(disposals, 0, "shutdown disposal happens just once");
disposals = 0;
const f4 = new Foo();
@ -149,7 +178,7 @@ exports["test different unload hooks"] = assert => {
assert.deepEqual(u7.log, ["dispose"], "dispose hook invoked");
};
exports["test disposables are desposed on unload"] = function(assert) {
exports["test disposables are disposed on unload"] = function(assert) {
let loader = Loader(module);
let { Disposable } = loader.require("sdk/core/disposable");

View File

@ -458,7 +458,8 @@ var gPopupBlockerObserver = {
if (!this._reportButton)
this._reportButton = document.getElementById("page-report-button");
if (!gBrowser.selectedBrowser.blockedPopups) {
if (!gBrowser.selectedBrowser.blockedPopups ||
gBrowser.selectedBrowser.blockedPopups.count == 0) {
// Hide the icon in the location bar (if the location bar exists)
this._reportButton.hidden = true;
@ -574,63 +575,66 @@ var gPopupBlockerObserver = {
else
blockedPopupAllowSite.removeAttribute("disabled");
var foundUsablePopupURI = false;
var blockedPopups = browser.blockedPopups;
if (blockedPopups) {
for (let i = 0; i < blockedPopups.length; i++) {
let blockedPopup = blockedPopups[i];
// popupWindowURI will be null if the file picker popup is blocked.
// xxxdz this should make the option say "Show file picker" and do it (Bug 590306)
if (!blockedPopup.popupWindowURI)
continue;
var popupURIspec = blockedPopup.popupWindowURI.spec;
// Sometimes the popup URI that we get back from the blockedPopup
// isn't useful (for instance, netscape.com's popup URI ends up
// being "http://www.netscape.com", which isn't really the URI of
// the popup they're trying to show). This isn't going to be
// useful to the user, so we won't create a menu item for it.
if (popupURIspec == "" || popupURIspec == "about:blank" ||
popupURIspec == uri.spec)
continue;
// Because of the short-circuit above, we may end up in a situation
// in which we don't have any usable popup addresses to show in
// the menu, and therefore we shouldn't show the separator. However,
// since we got past the short-circuit, we must've found at least
// one usable popup URI and thus we'll turn on the separator later.
foundUsablePopupURI = true;
var menuitem = document.createElement("menuitem");
var label = gNavigatorBundle.getFormattedString("popupShowPopupPrefix",
[popupURIspec]);
menuitem.setAttribute("label", label);
menuitem.setAttribute("oncommand", "gPopupBlockerObserver.showBlockedPopup(event);");
menuitem.setAttribute("popupReportIndex", i);
menuitem.popupReportBrowser = browser;
aEvent.target.appendChild(menuitem);
}
}
// Show or hide the separator, depending on whether we added any
// showable popup addresses to the menu.
var blockedPopupsSeparator =
document.getElementById("blockedPopupsSeparator");
if (foundUsablePopupURI)
blockedPopupsSeparator.removeAttribute("hidden");
else
blockedPopupsSeparator.setAttribute("hidden", true);
var blockedPopupDontShowMessage = document.getElementById("blockedPopupDontShowMessage");
var showMessage = gPrefService.getBoolPref("privacy.popups.showBrowserMessage");
let blockedPopupDontShowMessage = document.getElementById("blockedPopupDontShowMessage");
let showMessage = gPrefService.getBoolPref("privacy.popups.showBrowserMessage");
blockedPopupDontShowMessage.setAttribute("checked", !showMessage);
if (aEvent.target.anchorNode.id == "page-report-button") {
aEvent.target.anchorNode.setAttribute("open", "true");
blockedPopupDontShowMessage.setAttribute("label", gNavigatorBundle.getString("popupWarningDontShowFromLocationbar"));
} else
} else {
blockedPopupDontShowMessage.setAttribute("label", gNavigatorBundle.getString("popupWarningDontShowFromMessage"));
}
let blockedPopupsSeparator =
document.getElementById("blockedPopupsSeparator");
blockedPopupsSeparator.setAttribute("hidden", true);
gBrowser.selectedBrowser.retrieveListOfBlockedPopups().then(blockedPopups => {
let foundUsablePopupURI = false;
if (blockedPopups) {
for (let i = 0; i < blockedPopups.length; i++) {
let blockedPopup = blockedPopups[i];
// popupWindowURI will be null if the file picker popup is blocked.
// xxxdz this should make the option say "Show file picker" and do it (Bug 590306)
if (!blockedPopup.popupWindowURIspec)
continue;
var popupURIspec = blockedPopup.popupWindowURIspec;
// Sometimes the popup URI that we get back from the blockedPopup
// isn't useful (for instance, netscape.com's popup URI ends up
// being "http://www.netscape.com", which isn't really the URI of
// the popup they're trying to show). This isn't going to be
// useful to the user, so we won't create a menu item for it.
if (popupURIspec == "" || popupURIspec == "about:blank" ||
popupURIspec == "<self>" ||
popupURIspec == uri.spec)
continue;
// Because of the short-circuit above, we may end up in a situation
// in which we don't have any usable popup addresses to show in
// the menu, and therefore we shouldn't show the separator. However,
// since we got past the short-circuit, we must've found at least
// one usable popup URI and thus we'll turn on the separator later.
foundUsablePopupURI = true;
var menuitem = document.createElement("menuitem");
var label = gNavigatorBundle.getFormattedString("popupShowPopupPrefix",
[popupURIspec]);
menuitem.setAttribute("label", label);
menuitem.setAttribute("oncommand", "gPopupBlockerObserver.showBlockedPopup(event);");
menuitem.setAttribute("popupReportIndex", i);
menuitem.popupReportBrowser = browser;
aEvent.target.appendChild(menuitem);
}
}
// Show the separator if we added any
// showable popup addresses to the menu.
if (foundUsablePopupURI)
blockedPopupsSeparator.removeAttribute("hidden");
}, null);
},
onPopupHiding: function (aEvent) {
@ -655,14 +659,12 @@ var gPopupBlockerObserver = {
showAllBlockedPopups: function (aBrowser)
{
let popups = aBrowser.blockedPopups;
if (!popups)
return;
for (let i = 0; i < popups.length; i++) {
if (popups[i].popupWindowURI)
aBrowser.unblockPopup(i);
}
let popups = aBrowser.retrieveListOfBlockedPopups().then(popups => {
for (let i = 0; i < popups.length; i++) {
if (popups[i].popupWindowURIspec)
aBrowser.unblockPopup(i);
}
}, null);
},
editPopupSettings: function ()

View File

@ -21,11 +21,18 @@ function test() {
// Show the menu.
let popupShown = promiseWaitForEvent(window, "popupshown");
let popupFilled = BrowserTestUtils.waitForMessage(gBrowser.selectedBrowser.messageManager,
"PopupBlocking:ReplyGetBlockedPopupList");
notification.querySelector("button").doCommand();
let popup_event = yield popupShown;
let menu = popup_event.target;
is(menu.id, "blockedPopupOptions", "Blocked popup menu shown");
yield popupFilled;
// The menu is filled on the same message that we waited for, so let's ensure that it
// had a chance of running before this test code.
yield new Promise(resolve => executeSoon(resolve));
// Check the menu contents.
let sep = menu.querySelector("menuseparator");
let popupCount = 0;

View File

@ -74,6 +74,7 @@ function load()
var panelBrowser = getPanelBrowser();
panelBrowser.webProgress.addProgressListener(panelProgressListener,
Ci.nsIWebProgress.NOTIFY_ALL);
panelBrowser.messageManager.loadFrameScript("chrome://browser/content/content.js", true);
var cachedurl = panelBrowser.getAttribute("cachedurl")
if (cachedurl) {
panelBrowser.webNavigation

View File

@ -1,6 +1,7 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html>
<head>
<meta charset="utf8">
<title>Page creating a popup</title>
</head>
<body>

View File

@ -11,6 +11,8 @@
%globalDTD;
<!ENTITY % searchresetDTD SYSTEM "chrome://browser/locale/aboutSearchReset.dtd">
%searchresetDTD;
<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
%brandDTD;
]>
<html xmlns="http://www.w3.org/1999/xhtml"

View File

@ -6,16 +6,18 @@
<!ENTITY searchreset.pageTitle "Restore your search settings?">
<!ENTITY searchreset.pageInfo1 "Your search settings might be out-of-date. Firefox can help you restore the default search settings.">
<!ENTITY searchreset.pageInfo1 "Your search settings might be out-of-date. &brandShortName; can help you restore the default search settings.">
<!-- LOCALIZATION NOTE (searchreset.selector.label): this string is
followed by a dropdown of all the built-in search engines. -->
<!ENTITY searchreset.selector.label "This will set your default search engine to">
<!-- LOCALIZATION NOTE (searchreset.beforelink.pageInfo,
searchreset.afterlink.pageInfo): these two string are used respectively
before and after the the "Settings page" link (searchreset.link.pageInfo).
before and after the "Settings page" link (searchreset.link.pageInfo2).
Localizers can use one of them, or both, to better adapt this sentence to
their language.
-->
their language. -->
<!ENTITY searchreset.beforelink.pageInfo2 "You can change these settings at any time from the ">
<!ENTITY searchreset.afterlink.pageInfo2 ".">

View File

@ -21,6 +21,10 @@ select {
background-size: 16px, 16px;
}
select:-moz-dir(rtl) {
background-position: calc(100% - 8px) center, 4px center;
}
select:-moz-focusring {
color: transparent;
text-shadow: 0 0 0 var(--in-content-text-color);
@ -34,3 +38,7 @@ option {
background-size: 16px;
background-color: var(--in-content-page-background);
}
option:-moz-dir(rtl) {
background-position: calc(100% - 8px) center;
}

View File

@ -12,7 +12,7 @@ const EXTENSION_DIR = "chrome://mochitests/content/extensions/mozscreenshots/bro
let TestRunner;
function* setup() {
requestLongerTimeout(20);
requestLongerTimeout(10);
info("installing extension temporarily");
let chromeURL = Services.io.newURI(EXTENSION_DIR, null, null);

View File

@ -8,6 +8,7 @@ this.EXPORTED_SYMBOLS = ["TestRunner"];
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
const env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
const APPLY_CONFIG_TIMEOUT_MS = 60 * 1000;
const HOME_PAGE = "chrome://mozscreenshots/content/lib/mozscreenshots.html";
Cu.import("resource://gre/modules/FileUtils.jsm");
@ -156,11 +157,14 @@ this.TestRunner = {
function changeConfig(config) {
log.debug("calling " + config.name);
let promise = Promise.resolve(config.applyConfig());
let applyPromise = Promise.resolve(config.applyConfig());
let timeoutPromise = new Promise((resolve, reject) => {
setTimeout(reject, APPLY_CONFIG_TIMEOUT_MS, "Timed out");
});
log.debug("called " + config.name);
// Add a default timeout of 500ms to avoid conflicts when configurations
// try to apply at the same time. e.g WindowSize and TabsInTitlebar
return promise.then(() => {
return Promise.race([applyPromise, timeoutPromise]).then(() => {
return new Promise((resolve) => {
setTimeout(resolve, 500);
});

View File

@ -8,7 +8,9 @@ add_task(function* capture() {
if (!shouldCapture()) {
return;
}
let sets = ["TabsInTitlebar", "Tabs", "WindowSize", "Toolbars", "LightweightThemes"];
requestLongerTimeout(20);
let sets = ["TabsInTitlebar", "Tabs", "WindowSize", "Toolbars", "LightweightThemes"];
yield TestRunner.start(sets, "primaryUI");
});

View File

@ -210,7 +210,7 @@ AC_DEFUN([MOZ_ANDROID_AAR],[
ANDROID_SUPPORT_LIBRARY_VERSION="23.0.1"
AC_SUBST(ANDROID_SUPPORT_LIBRARY_VERSION)
ANDROID_GOOGLE_PLAY_SERVICES_VERSION="8.1.0"
ANDROID_GOOGLE_PLAY_SERVICES_VERSION="8.4.0"
AC_SUBST(ANDROID_GOOGLE_PLAY_SERVICES_VERSION)
AC_DEFUN([MOZ_ANDROID_GOOGLE_PLAY_SERVICES],
@ -234,6 +234,7 @@ if test -n "$MOZ_ANDROID_GCM" ; then
MOZ_ANDROID_AAR(play-services-base, $ANDROID_GOOGLE_PLAY_SERVICES_VERSION, google, com/google/android/gms)
MOZ_ANDROID_AAR(play-services-basement, $ANDROID_GOOGLE_PLAY_SERVICES_VERSION, google, com/google/android/gms)
MOZ_ANDROID_AAR(play-services-gcm, $ANDROID_GOOGLE_PLAY_SERVICES_VERSION, google, com/google/android/gms)
MOZ_ANDROID_AAR(play-services-measurement, $ANDROID_GOOGLE_PLAY_SERVICES_VERSION, google, com/google/android/gms)
fi
])
@ -244,8 +245,6 @@ AC_DEFUN([MOZ_ANDROID_INSTALL_TRACKING],
if test -n "$MOZ_INSTALL_TRACKING"; then
AC_SUBST(MOZ_INSTALL_TRACKING)
MOZ_ANDROID_AAR(play-services-ads, $ANDROID_GOOGLE_PLAY_SERVICES_VERSION, google, com/google/android/gms)
MOZ_ANDROID_AAR(play-services-analytics, $ANDROID_GOOGLE_PLAY_SERVICES_VERSION, google, com/google/android/gms)
MOZ_ANDROID_AAR(play-services-appindexing, $ANDROID_GOOGLE_PLAY_SERVICES_VERSION, google, com/google/android/gms)
MOZ_ANDROID_AAR(play-services-basement, $ANDROID_GOOGLE_PLAY_SERVICES_VERSION, google, com/google/android/gms)
fi

View File

@ -55,12 +55,18 @@ add_task(function* () {
ok(name, "Found the service worker in the list");
let targetElement = name.parentNode.parentNode;
// Check that there is a Debug button but not a Start button.
ok(targetElement.querySelector(".debug-button"), "Found its debug button");
ok(!targetElement.querySelector(".start-button"), "No start button");
// The service worker may already be killed with the low 1s timeout
if (!targetElement.querySelector(".start-button")) {
// Check that there is a Debug button but not a Start button.
ok(targetElement.querySelector(".debug-button"), "Found its debug button");
// Wait for the service worker to be killed due to inactivity.
yield waitForMutation(targetElement, { childList: true });
// Wait for the service worker to be killed due to inactivity.
yield waitForMutation(targetElement, { childList: true });
} else {
// Check that there is no Debug button when the SW is already shut down.
ok(!targetElement.querySelector(".debug-button"), "No debug button when " +
"the worker is already killed");
}
// We should now have a Start button but no Debug button.
let startBtn = targetElement.querySelector(".start-button");

View File

@ -33,7 +33,7 @@ add_task(function* () {
let toolbar = doc.querySelector(".devtools-tabbar");
let toolbarControls = [...toolbar.querySelectorAll(
".devtools-tab, toolbarbutton")].filter(elm =>
".devtools-tab, button")].filter(elm =>
!elm.hidden && doc.defaultView.getComputedStyle(elm).getPropertyValue(
"display") !== "none");

View File

@ -115,7 +115,7 @@ function testToggleToolboxButtons() {
is(matchedCheckboxes[0].nextSibling.textContent, tool.label,
"The label for checkbox matches the tool definition.");
is(matchedButtons[0].getAttribute("tooltiptext"), tool.label,
is(matchedButtons[0].getAttribute("title"), tool.label,
"The tooltip for button matches the tool definition.");
}

View File

@ -30,13 +30,17 @@ add_task(function* () {
is(getTitle(), `Developer Tools - Page title - ${URL}`,
"Devtools title correct after switching to detached window host");
// Verify that the frame list button is visible and populated
// Open frame menu and wait till it's available on the screen.
let btn = toolbox.doc.getElementById("command-button-frames");
let frames = Array.slice(btn.firstChild.querySelectorAll("[data-window-id]"));
let menu = toolbox.showFramesMenu({target: btn});
yield once(menu, "open");
// Verify that the frame list menu is populated
let frames = menu.menuitems;
is(frames.length, 2, "We have both frames in the list");
let topFrameBtn = frames.filter(b => b.getAttribute("label") == URL)[0];
let iframeBtn = frames.filter(b => b.getAttribute("label") == IFRAME_URL)[0];
let topFrameBtn = frames.filter(b => b.label == URL)[0];
let iframeBtn = frames.filter(b => b.label == IFRAME_URL)[0];
ok(topFrameBtn, "Got top level document in the list");
ok(iframeBtn, "Got iframe document in the list");

View File

@ -15,6 +15,7 @@ const MAX_ZOOM = 2;
const OS_HISTOGRAM = "DEVTOOLS_OS_ENUMERATED_PER_USER";
const OS_IS_64_BITS = "DEVTOOLS_OS_IS_64_BITS_PER_USER";
const SCREENSIZE_HISTOGRAM = "DEVTOOLS_SCREEN_RESOLUTION_ENUMERATED_PER_USER";
const HTML_NS = "http://www.w3.org/1999/xhtml";
var {Cc, Ci, Cu} = require("chrome");
var promise = require("promise");
@ -26,6 +27,8 @@ var Telemetry = require("devtools/client/shared/telemetry");
var HUDService = require("devtools/client/webconsole/hudservice");
var viewSource = require("devtools/client/shared/view-source");
var { attachThread, detachThread } = require("./attach-thread");
var Menu = require("devtools/client/framework/menu");
var MenuItem = require("devtools/client/framework/menu-item");
Cu.import("resource://devtools/client/scratchpad/scratchpad-manager.jsm");
Cu.import("resource://devtools/client/shared/DOMHelpers.jsm");
@ -126,11 +129,15 @@ function Toolbox(target, selectedTool, hostType, hostOptions) {
this._initInspector = null;
this._inspector = null;
// Map of frames (id => frame-info) and currently selected frame id.
this.frameMap = new Map();
this.selectedFrameId = null;
this._toolRegistered = this._toolRegistered.bind(this);
this._toolUnregistered = this._toolUnregistered.bind(this);
this._refreshHostTitle = this._refreshHostTitle.bind(this);
this._toggleAutohide = this._toggleAutohide.bind(this);
this.selectFrame = this.selectFrame.bind(this);
this.showFramesMenu = this.showFramesMenu.bind(this);
this._updateFrames = this._updateFrames.bind(this);
this._splitConsoleOnKeypress = this._splitConsoleOnKeypress.bind(this);
this.destroy = this.destroy.bind(this);
@ -405,15 +412,15 @@ Toolbox.prototype = {
let framesPromise = this._listFrames();
this.closeButton = this.doc.getElementById("toolbox-close");
this.closeButton.addEventListener("command", this.destroy, true);
this.closeButton.addEventListener("click", this.destroy, true);
gDevTools.on("pref-changed", this._prefChanged);
let framesMenu = this.doc.getElementById("command-button-frames");
framesMenu.addEventListener("command", this.selectFrame, true);
framesMenu.addEventListener("click", this.showFramesMenu, false);
let noautohideMenu = this.doc.getElementById("command-button-noautohide");
noautohideMenu.addEventListener("command", this._toggleAutohide, true);
noautohideMenu.addEventListener("click", this._toggleAutohide, true);
this.textboxContextMenuPopup =
this.doc.getElementById("toolbox-textbox-context-popup");
@ -839,10 +846,11 @@ Toolbox.prototype = {
// Bottom-type host can be minimized, add a button for this.
if (this.hostType == Toolbox.HostType.BOTTOM) {
let minimizeBtn = this.doc.createElement("toolbarbutton");
let minimizeBtn = this.doc.createElementNS(HTML_NS, "button");
minimizeBtn.id = "toolbox-dock-bottom-minimize";
minimizeBtn.className = "devtools-button";
minimizeBtn.addEventListener("command", this._toggleMinimizeMode);
minimizeBtn.addEventListener("click", this._toggleMinimizeMode);
dockBox.appendChild(minimizeBtn);
// Show the button in its maximized state.
this._onBottomHostMaximized();
@ -872,12 +880,12 @@ Toolbox.prototype = {
continue;
}
let button = this.doc.createElement("toolbarbutton");
let button = this.doc.createElementNS(HTML_NS, "button");
button.id = "toolbox-dock-" + position;
button.className = "toolbox-dock-button";
button.setAttribute("tooltiptext", toolboxStrings("toolboxDockButtons." +
position + ".tooltip"));
button.addEventListener("command", () => {
button.className = "toolbox-dock-button devtools-button";
button.setAttribute("title", toolboxStrings("toolboxDockButtons." +
position + ".tooltip"));
button.addEventListener("click", () => {
this.switchHost(position);
});
@ -895,7 +903,7 @@ Toolbox.prototype = {
let btn = this.doc.querySelector("#toolbox-dock-bottom-minimize");
btn.className = "minimized";
btn.setAttribute("tooltiptext",
btn.setAttribute("title",
toolboxStrings("toolboxDockButtons.bottom.maximize") + " " +
this._getMinimizeButtonShortcutTooltip());
},
@ -904,7 +912,7 @@ Toolbox.prototype = {
let btn = this.doc.querySelector("#toolbox-dock-bottom-minimize");
btn.className = "maximized";
btn.setAttribute("tooltiptext",
btn.setAttribute("title",
toolboxStrings("toolboxDockButtons.bottom.minimize") + " " +
this._getMinimizeButtonShortcutTooltip());
},
@ -1053,17 +1061,17 @@ Toolbox.prototype = {
* since we want it to work for remote targets too
*/
_buildPickerButton: function () {
this._pickerButton = this.doc.createElement("toolbarbutton");
this._pickerButton = this.doc.createElementNS(HTML_NS, "button");
this._pickerButton.id = "command-button-pick";
this._pickerButton.className = "command-button command-button-invertable";
this._pickerButton.setAttribute("tooltiptext", toolboxStrings("pickButton.tooltip"));
this._pickerButton.className = "command-button command-button-invertable devtools-button";
this._pickerButton.setAttribute("title", toolboxStrings("pickButton.tooltip"));
this._pickerButton.setAttribute("hidden", "true");
let container = this.doc.querySelector("#toolbox-picker-container");
container.appendChild(this._pickerButton);
this._togglePicker = this.highlighterUtils.togglePicker.bind(this.highlighterUtils);
this._pickerButton.addEventListener("command", this._togglePicker, false);
this._pickerButton.addEventListener("click", this._togglePicker, false);
},
/**
@ -1122,7 +1130,7 @@ Toolbox.prototype = {
return {
id: options.id,
button: button,
label: button.getAttribute("tooltiptext"),
label: button.getAttribute("title"),
visibilityswitch: "devtools." + options.id + ".enabled",
isTargetSupported: options.isTargetSupported
? options.isTargetSupported
@ -1697,17 +1705,69 @@ Toolbox.prototype = {
});
},
selectFrame: function (event) {
let windowId = event.target.getAttribute("data-window-id");
/**
* Show a drop down menu that allows the user to switch frames.
*/
showFramesMenu: function (event) {
let menu = new Menu();
// Generate list of menu items from the list of frames.
this.frameMap.forEach(frame => {
// A frame is checked if it's the selected one.
let checked = frame.id == this.selectedFrameId;
// Create menu item.
menu.append(new MenuItem({
label: frame.url,
type: "radio",
checked,
click: () => {
this.onSelectFrame(frame.id);
}
}));
});
// Show a drop down menu with frames.
// XXX Missing menu API for specifying target (anchor)
// and relative position to it. See also:
// https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/Method/openPopup
// https://bugzilla.mozilla.org/show_bug.cgi?id=1274551
let rect = event.target.getBoundingClientRect();
let screenX = event.target.ownerDocument.defaultView.mozInnerScreenX;
let screenY = event.target.ownerDocument.defaultView.mozInnerScreenY;
menu.popup(rect.left + screenX, rect.bottom + screenY, this);
return menu;
},
/**
* Select a frame by sending 'switchToFrame' packet to the backend.
*/
onSelectFrame: function (frameId) {
// Send packet to the backend to select specified frame and
// wait for 'frameUpdate' event packet to update the UI.
let packet = {
to: this._target.form.actor,
type: "switchToFrame",
windowId: windowId
windowId: frameId
};
this._target.client.request(packet);
// Wait for frameUpdate event to update the UI
},
/**
* A handler for 'frameUpdate' packets received from the backend.
* Following properties might be set on the packet:
*
* destroyAll {Boolean}: All frames have been destroyed.
* selected {Number}: A frame has been selected
* frames {Array}: list of frames. Every frame can have:
* id {Number}: frame ID
* url {String}: frame URL
* title {String}: frame title
* destroy {Boolean}: Set to true if destroyed
* parentID {Number}: ID of the parent frame (not set
* for top level window)
*/
_updateFrames: function (event, data) {
if (!Services.prefs.getBoolPref("devtools.command-button-frames.enabled")) {
return;
@ -1718,59 +1778,48 @@ Toolbox.prototype = {
return;
}
let menu = this.doc.getElementById("command-button-frames");
// Store (synchronize) data about all existing frames on the backend
if (data.destroyAll) {
let menupopup = menu.firstChild;
while (menupopup.firstChild) {
menupopup.firstChild.remove();
}
return;
this.frameMap.clear();
this.selectedFrameId = null;
} else if (data.selected) {
let item = menu.querySelector("menuitem[data-window-id=\"" + data.selected + "\"]");
if (!item) {
return;
}
// Toggle the toolbarbutton if we selected a non top-level frame
if (item.hasAttribute("data-parent-id")) {
menu.setAttribute("checked", "true");
} else {
menu.removeAttribute("checked");
}
// Uncheck the previously selected frame
let selected = menu.querySelector("menuitem[checked=true]");
if (selected) {
selected.removeAttribute("checked");
}
// Check the new one
item.setAttribute("checked", "true");
this.selectedFrameId = data.selected;
} else if (data.frames) {
data.frames.forEach(win => {
let item = menu.querySelector("menuitem[data-window-id=\"" + win.id + "\"]");
if (win.destroy) {
if (item) {
item.remove();
data.frames.forEach(frame => {
if (frame.destroy) {
this.frameMap.delete(frame.id);
// Reset the currently selected frame if it's destroyed.
if (this.selectedFrameId == frame.id) {
this.selectedFrameId = null;
}
return;
} else {
this.frameMap.set(frame.id, frame);
}
if (!item) {
item = this.doc.createElement("menuitem");
item.setAttribute("type", "radio");
item.setAttribute("data-window-id", win.id);
if (win.parentID) {
item.setAttribute("data-parent-id", win.parentID);
}
// If we register a root docshell and we don't have any selected,
// consider it as the currently targeted one.
if (!win.parentID && !menu.querySelector("menuitem[checked=true]")) {
item.setAttribute("checked", "true");
menu.removeAttribute("checked");
}
menu.firstChild.appendChild(item);
}
item.setAttribute("label", win.url);
});
}
// If there is no selected frame select the first top level
// frame by default. Note that there might be more top level
// frames in case of the BrowserToolbox.
if (!this.selectedFrameId) {
let frames = [...this.frameMap.values()];
let topFrames = frames.filter(frame => !frame.parentID);
this.selectedFrameId = topFrames.length ? topFrames[0].id : null;
}
// Check out whether top frame is currently selected.
// Note that only child frame has parentID.
let frame = this.frameMap.get(this.selectedFrameId);
let topFrameSelected = frame ? !frame.parentID : false;
let button = this.doc.getElementById("command-button-frames");
button.removeAttribute("checked");
// If non-top level frame is selected the toolbar button is
// marked as 'checked' indicating that a child frame is active.
if (!topFrameSelected && this.selectedFrameId) {
button.setAttribute("checked", "true");
}
},
/**
@ -2075,7 +2124,7 @@ Toolbox.prototype = {
this.webconsolePanel.removeEventListener("resize",
this._saveSplitConsoleHeight);
}
this.closeButton.removeEventListener("command", this.destroy, true);
this.closeButton.removeEventListener("click", this.destroy, true);
this.textboxContextMenuPopup.removeEventListener("popupshowing",
this._updateTextboxMenuItems, true);
@ -2108,7 +2157,7 @@ Toolbox.prototype = {
outstanding.push(this.destroyInspector().then(() => {
// Removing buttons
if (this._pickerButton) {
this._pickerButton.removeEventListener("command", this._togglePicker, false);
this._pickerButton.removeEventListener("click", this._togglePicker, false);
this._pickerButton = null;
}
}));

View File

@ -16,7 +16,8 @@
%globalKeysDTD;
]>
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml">
<script type="application/javascript;version=1.8"
src="chrome://devtools/content/shared/theme-switching.js"/>
@ -50,25 +51,22 @@
<hbox id="toolbox-picker-container" />
<hbox id="toolbox-tabs" flex="1" role="tablist" />
<hbox id="toolbox-buttons" pack="end">
<toolbarbutton id="command-button-frames"
class="command-button command-button-invertable devtools-toolbarbutton"
tooltiptext="&toolboxFramesTooltip;"
type="menu"
hidden="true">
<menupopup position="bottomright topright"></menupopup>
</toolbarbutton>
<toolbarbutton id="command-button-noautohide"
class="command-button command-button-invertable"
tooltiptext="&toolboxNoAutoHideTooltip;"
hidden="true" />
<html:button id="command-button-frames"
class="command-button command-button-invertable devtools-button"
title="&toolboxFramesTooltip;"
hidden="true" />
<html:button id="command-button-noautohide"
class="command-button command-button-invertable devtools-button"
title="&toolboxNoAutoHideTooltip;"
hidden="true" />
</hbox>
<vbox id="toolbox-controls-separator" class="devtools-separator"/>
<hbox id="toolbox-option-container"/>
<hbox id="toolbox-controls">
<hbox id="toolbox-dock-buttons"/>
<toolbarbutton id="toolbox-close"
class="devtools-closebutton"
tooltiptext="&toolboxCloseButton.tooltip;"/>
<html:button id="toolbox-close"
class="devtools-button"
title="&toolboxCloseButton.tooltip;"/>
</hbox>
</toolbar>
<vbox flex="1" class="theme-body">

View File

@ -98,6 +98,13 @@ function InspectorPanel(iframeWindow, toolbox) {
this.onPaneToggleButtonClicked = this.onPaneToggleButtonClicked.bind(this);
this._onMarkupFrameLoad = this._onMarkupFrameLoad.bind(this);
let doc = this.panelDoc;
// Handle 'Add Node' toolbar button.
this.addNode = this.addNode.bind(this);
this.addNodeButton = doc.getElementById("inspector-element-add-button");
this.addNodeButton.addEventListener("click", this.addNode);
this._target.on("will-navigate", this._onBeforeNavigate);
EventEmitter.decorate(this);
@ -659,6 +666,8 @@ InspectorPanel.prototype = {
let sidebarDestroyer = this.sidebar.destroy();
this.sidebar = null;
this.addNodeButton.removeEventListener("click", this.addNode);
this.nodemenu.removeEventListener("popupshowing", this._setupNodeMenu, true);
this.nodemenu.removeEventListener("popuphiding", this._resetNodeMenu, true);
this.breadcrumbs.destroy();

View File

@ -149,24 +149,23 @@
<box flex="1" class="devtools-responsive-container theme-body">
<vbox flex="1" class="devtools-main-content">
<toolbar id="inspector-toolbar"
<html:div id="inspector-toolbar"
class="devtools-toolbar"
nowindowdrag="true">
<toolbarbutton id="inspector-element-add-button"
class="devtools-toolbarbutton"
tooltiptext="&inspectorAddNode.label;"
oncommand="inspector.addNode()" />
<spacer flex="1"/>
<box id="inspector-searchlabel" />
<html:button id="inspector-element-add-button"
title="&inspectorAddNode.label;"
class="devtools-button" />
<html:div class="devtools-toolbar-spacer" />
<html:span id="inspector-searchlabel" />
<textbox id="inspector-searchbox"
type="search"
timeout="50"
class="devtools-searchinput"
placeholder="&inspectorSearchHTML.label3;"/>
<toolbarbutton id="inspector-pane-toggle"
class="devtools-toolbarbutton"
<html:button id="inspector-pane-toggle"
class="devtools-button"
tabindex="0" />
</toolbar>
</html:div>
<vbox flex="1" id="markup-box">
</vbox>
<toolbar id="inspector-breadcrumbs-toolbar"

View File

@ -10,6 +10,7 @@ const {Cc, Ci, Cu} = require("chrome");
const promise = require("promise");
const {Rule} = require("devtools/client/inspector/rules/models/rule");
const {promiseWarn} = require("devtools/client/inspector/shared/utils");
const {ELEMENT_STYLE} = require("devtools/server/actors/styles");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@ -229,7 +230,9 @@ ElementStyle.prototype = {
// determine if the property is overridden.
let textProps = [];
for (let rule of this.rules) {
if (rule.pseudoElement === pseudo && !rule.keyframes) {
if ((rule.matchedSelectors.length > 0 ||
rule.domRule.type === ELEMENT_STYLE) &&
rule.pseudoElement === pseudo && !rule.keyframes) {
for (let textProp of rule.textProps.slice(0).reverse()) {
if (textProp.enabled) {
textProps.push(textProp);

View File

@ -47,15 +47,15 @@ support-files =
[browser_rules_add-property_01.js]
[browser_rules_add-property_02.js]
[browser_rules_add-property-svg.js]
[browser_rules_add-rule_01.js]
[browser_rules_add-rule_02.js]
[browser_rules_add-rule_03.js]
[browser_rules_add-rule_04.js]
[browser_rules_add-rule_05.js]
[browser_rules_add-rule_06.js]
[browser_rules_add-rule_pseudo_class.js]
[browser_rules_add-rule_iframes.js]
[browser_rules_add-rule-and-property.js]
[browser_rules_add-rule-button-state.js]
[browser_rules_add-rule-edit-selector.js]
[browser_rules_add-rule-iframes.js]
[browser_rules_add-rule-namespace-elements.js]
[browser_rules_add-rule-pseudo-class.js]
[browser_rules_add-rule-then-property-edit-selector.js]
[browser_rules_add-rule-with-menu.js]
[browser_rules_add-rule.js]
[browser_rules_authored.js]
[browser_rules_authored_color.js]
[browser_rules_authored_override.js]
@ -126,6 +126,7 @@ skip-if = os == "mac" # Bug 1245996 : click on scrollbar not working on OSX
[browser_rules_edit-selector_06.js]
[browser_rules_edit-selector_07.js]
[browser_rules_edit-selector_08.js]
[browser_rules_edit-selector_09.js]
[browser_rules_editable-field-focus_01.js]
[browser_rules_editable-field-focus_02.js]
[browser_rules_eyedropper.js]

View File

@ -0,0 +1,30 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Tests adding a new rule and a new property in this rule.
add_task(function* () {
yield addTab("data:text/html;charset=utf-8,<div id='testid'>Styled Node</div>");
let {inspector, view} = yield openRuleView();
info("Selecting the test node");
yield selectNode("#testid", inspector);
info("Adding a new rule for this node and blurring the new selector field");
yield addNewRuleAndDismissEditor(inspector, view, "#testid", 1);
info("Adding a new property for this rule");
let ruleEditor = getRuleViewRuleEditor(view, 1);
let onRuleViewChanged = view.once("ruleview-changed");
ruleEditor.addProperty("font-weight", "bold", "");
yield onRuleViewChanged;
let textProps = ruleEditor.rule.textProps;
let prop = textProps[textProps.length - 1];
is(prop.name, "font-weight", "The last property name is font-weight");
is(prop.value, "bold", "The last property value is bold");
});

View File

@ -17,48 +17,20 @@ add_task(function* () {
yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
let {inspector, view} = yield openRuleView();
yield selectNode("div", inspector);
yield addNewRule(inspector, view);
yield testNewRule(view, "div", 1);
yield addNewRuleAndDismissEditor(inspector, view, "div", 1);
yield addNewProperty(view, 1, "color", "red");
let innerFrameDiv1 = yield getNodeFrontInFrame("div", "#frame1", inspector);
yield selectNode(innerFrameDiv1, inspector);
yield addNewRule(inspector, view);
yield testNewRule(view, "div", 1);
yield addNewRuleAndDismissEditor(inspector, view, "div", 1);
yield addNewProperty(view, 1, "color", "blue");
let innerFrameDiv2 = yield getNodeFrontInFrame("div", "#frame2", inspector);
yield selectNode(innerFrameDiv2, inspector);
yield addNewRule(inspector, view);
yield testNewRule(view, "div", 1);
yield addNewRuleAndDismissEditor(inspector, view, "div", 1);
yield addNewProperty(view, 1, "color", "green");
});
function* addNewRule(inspector, view) {
info("Adding the new rule using the button");
view.addRuleButton.click();
info("Waiting for rule view to change");
let onRuleViewChanged = once(view, "ruleview-changed");
yield onRuleViewChanged;
}
/**
* Check the newly created rule has the expected selector and submit the
* selector editor.
*/
function* testNewRule(view, expected, index) {
let idRuleEditor = getRuleViewRuleEditor(view, index);
let editor = idRuleEditor.selectorText.ownerDocument.activeElement;
is(editor.value, expected,
"Selector editor value is as expected: " + expected);
info("Entering the escape key");
EventUtils.synthesizeKey("VK_ESCAPE", {});
is(idRuleEditor.selectorText.textContent, expected,
"Selector text value is as expected: " + expected);
}
/**
* Add a new property in the rule at the provided index in the rule view.
*

View File

@ -36,14 +36,6 @@ add_task(function* () {
for (let data of TEST_DATA) {
let {node, expected} = data;
yield selectNode(node, inspector);
yield addNewRule(inspector, view);
yield testNewRule(view, expected, 1);
yield addNewRuleAndDismissEditor(inspector, view, expected, 1);
}
});
function* testNewRule(view, expected, index) {
let idRuleEditor = getRuleViewRuleEditor(view, index);
let editor = idRuleEditor.selectorText.ownerDocument.activeElement;
is(editor.value, expected,
"Selector editor value is as expected: " + expected);
}

View File

@ -31,32 +31,11 @@ add_task(function* () {
function* runTestData(inspector, view, pseudoClasses) {
yield setPseudoLocks(inspector, view, pseudoClasses);
yield addNewRule(inspector, view);
yield testNewRule(view, pseudoClasses, 1);
yield resetPseudoLocks(inspector, view);
}
function* addNewRule(inspector, view) {
info("Adding the new rule using the button");
view.addRuleButton.click();
info("Waiting for rule view to change");
let onRuleViewChanged = once(view, "ruleview-changed");
yield onRuleViewChanged;
}
function* testNewRule(view, pseudoClasses, index) {
let idRuleEditor = getRuleViewRuleEditor(view, index);
let editor = idRuleEditor.selectorText.ownerDocument.activeElement;
let expected = EXPECTED_SELECTOR + pseudoClasses.join("");
yield addNewRuleAndDismissEditor(inspector, view, expected, 1);
is(editor.value, expected,
"Selector editor value is as expected: " + expected);
info("Entering the escape key");
EventUtils.synthesizeKey("VK_ESCAPE", {});
is(idRuleEditor.selectorText.textContent, expected,
"Selector text value is as expected: " + expected);
yield resetPseudoLocks(inspector, view);
}
function* setPseudoLocks(inspector, view, pseudoClasses) {

View File

@ -22,10 +22,10 @@ add_task(function* () {
let {inspector, view} = yield openRuleView();
yield selectNode("#testid", inspector);
yield addNewRule(inspector, view);
yield addNewRuleAndDismissEditor(inspector, view, "#testid", 1);
info("Adding new properties to the new rule");
yield testNewRule(view, "#testid", 1);
info("Adding a new property to the new rule");
yield testAddingProperty(view, 1);
info("Editing existing selector field");
yield testEditSelector(view, "span");
@ -37,21 +37,10 @@ add_task(function* () {
yield checkModifiedElement(view, "span", 1);
});
function* testNewRule(view, expected, index) {
let idRuleEditor = getRuleViewRuleEditor(view, index);
let editor = idRuleEditor.selectorText.ownerDocument.activeElement;
is(editor.value, expected,
"Selector editor value is as expected: " + expected);
info("Entering the escape key");
EventUtils.synthesizeKey("VK_ESCAPE", {});
is(idRuleEditor.selectorText.textContent, expected,
"Selector text value is as expected: " + expected);
info("Adding new properties to new rule: " + expected);
idRuleEditor.addProperty("font-weight", "bold", "");
let textProps = idRuleEditor.rule.textProps;
function* testAddingProperty(view, index) {
let ruleEditor = getRuleViewRuleEditor(view, index);
ruleEditor.addProperty("font-weight", "bold", "");
let textProps = ruleEditor.rule.textProps;
let lastRule = textProps[textProps.length - 1];
is(lastRule.name, "font-weight", "Last rule name is font-weight");
is(lastRule.value, "bold", "Last rule value is bold");

View File

@ -4,8 +4,7 @@
"use strict";
// Tests the behaviour of adding a new rule using the add rule button and the
// various inplace-editor behaviours in the new rule editor.
// Tests adding a new rule using the add rule button.
const TEST_URI = `
<style type="text/css">
@ -38,39 +37,11 @@ const TEST_DATA = [
add_task(function* () {
yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
let {inspector, view, testActor} = yield openRuleView();
let {inspector, view} = yield openRuleView();
for (let data of TEST_DATA) {
let {node, expected} = data;
yield selectNode(node, inspector);
yield addNewRule(inspector, view);
yield testNewRule(view, expected, 1);
info("Resetting page content");
yield testActor.eval(
"content.document.body.innerHTML = `" + TEST_URI + "`;");
yield addNewRuleAndDismissEditor(inspector, view, expected, 1);
}
});
function* testNewRule(view, expected, index) {
let idRuleEditor = getRuleViewRuleEditor(view, index);
let editor = idRuleEditor.selectorText.ownerDocument.activeElement;
is(editor.value, expected,
"Selector editor value is as expected: " + expected);
info("Entering the escape key");
EventUtils.synthesizeKey("VK_ESCAPE", {});
is(idRuleEditor.selectorText.textContent, expected,
"Selector text value is as expected: " + expected);
info("Adding new properties to new rule: " + expected);
let onRuleViewChanged = view.once("ruleview-changed");
idRuleEditor.addProperty("font-weight", "bold", "");
yield onRuleViewChanged;
let textProps = idRuleEditor.rule.textProps;
let lastRule = textProps[textProps.length - 1];
is(lastRule.name, "font-weight", "Last rule name is font-weight");
is(lastRule.value, "bold", "Last rule value is bold");
}

View File

@ -1,72 +0,0 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Tests the behaviour of adding a new rule to the rule view using the context
// menu and the various inplace-editor behaviours in the new rule editor.
const TEST_URI = `
<style type="text/css">
.testclass {
text-align: center;
}
</style>
<div id="testid" class="testclass">Styled Node</div>
<span class="testclass2">This is a span</span>
<span class="class1 class2">Multiple classes</span>
<span class="class3 class4">Multiple classes</span>
<p>Empty<p>
<h1 class="asd@@@@a!!!!:::@asd">Invalid characters in class</h1>
<h2 id="asd@@@a!!2a">Invalid characters in id</h2>
`;
const TEST_DATA = [
{ node: "#testid", expected: "#testid" },
{ node: ".testclass2", expected: ".testclass2" },
{ node: ".class1.class2", expected: ".class1.class2" },
{ node: ".class3.class4", expected: ".class3.class4" },
{ node: "p", expected: "p" },
{ node: "h1", expected: ".asd\\@\\@\\@\\@a\\!\\!\\!\\!\\:\\:\\:\\@asd" },
{ node: "h2", expected: "#asd\\@\\@\\@a\\!\\!2a" }
];
add_task(function* () {
yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
let {inspector, view} = yield openRuleView();
for (let data of TEST_DATA) {
let {node, expected} = data;
yield selectNode(node, inspector);
yield testNewRule(inspector, view, expected);
}
});
function* testNewRule(inspector, view, expected) {
info("Adding a new rule and expecting a ruleview-changed event");
let onRuleViewChanged = view.once("ruleview-changed");
yield addNewRule(inspector, view);
yield onRuleViewChanged;
let ruleEditor = getRuleViewRuleEditor(view, 1);
let editor = ruleEditor.selectorText.ownerDocument.activeElement;
is(editor.value, expected,
"Selector editor value is as expected: " + expected);
info("Entering the escape key");
EventUtils.synthesizeKey("VK_ESCAPE", {});
is(ruleEditor.selectorText.textContent, expected,
"Selector text value is as expected: " + expected);
info("Adding new properties to new rule: " + expected);
onRuleViewChanged = view.once("ruleview-changed");
ruleEditor.addProperty("font-weight", "bold", "");
yield onRuleViewChanged;
let textProps = ruleEditor.rule.textProps;
let lastRule = textProps[textProps.length - 1];
is(lastRule.name, "font-weight", "Last rule name is font-weight");
is(lastRule.value, "bold", "Last rule value is bold");
}

View File

@ -74,5 +74,5 @@ function* testAddProperty(view) {
let textProp = yield addProperty(view, 1, "text-align", "center");
is(textProp.value, "center", "Text prop should have been changed.");
is(textProp.overridden, false, "Property should not be overridden");
ok(!textProp.overridden, "Property should not be overridden");
}

View File

@ -0,0 +1,110 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Tests that editing a selector to an unmatched rule does set up the correct
// property on the rule, and that settings property in said rule does not
// lead to overriding properties from matched rules.
// Test that having a rule with both matched and unmatched selectors does work
// correctly.
const TEST_URI = `
<style type="text/css">
#testid {
color: black;
}
.testclass {
background-color: white;
}
</style>
<div id="testid">Styled Node</div>
<span class="testclass">This is a span</span>
`;
add_task(function* () {
yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
let {inspector, view} = yield openRuleView();
yield selectNode("#testid", inspector);
yield testEditSelector(view, "span");
yield testAddImportantProperty(view);
yield testAddMatchedRule(view, "span, div");
});
function* testEditSelector(view, name) {
info("Test editing existing selector fields");
let ruleEditor = getRuleViewRuleEditor(view, 1);
info("Focusing an existing selector name in the rule-view");
let editor = yield focusEditableField(view, ruleEditor.selectorText);
is(inplaceEditor(ruleEditor.selectorText), editor,
"The selector editor got focused");
info("Entering a new selector name and committing");
editor.input.value = name;
info("Waiting for rule view to update");
let onRuleViewChanged = once(view, "ruleview-changed");
info("Entering the commit key");
EventUtils.synthesizeKey("VK_RETURN", {});
yield onRuleViewChanged;
ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
ok(getRuleViewRuleEditor(view, 1).element.getAttribute("unmatched"),
"Rule with " + name + " does not match the current element.");
// Escape the new property editor after editing the selector
let onBlur = once(view.styleDocument.activeElement, "blur");
EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
yield onBlur;
}
function* testAddImportantProperty(view) {
info("Test creating a new property with !important");
let textProp = yield addProperty(view, 1, "color", "red !important");
is(textProp.value, "red", "Text prop should have been changed.");
is(textProp.priority, "important",
"Text prop has an \"important\" priority.");
ok(!textProp.overridden, "Property should not be overridden");
let ruleEditor = getRuleViewRuleEditor(view, 1);
let prop = ruleEditor.rule.textProps[0];
ok(!prop.overridden,
"Existing property on matched rule should not be overridden");
}
function* testAddMatchedRule(view, name) {
info("Test adding a matching selector");
let ruleEditor = getRuleViewRuleEditor(view, 1);
info("Focusing an existing selector name in the rule-view");
let editor = yield focusEditableField(view, ruleEditor.selectorText);
is(inplaceEditor(ruleEditor.selectorText), editor,
"The selector editor got focused");
info("Entering a new selector name and committing");
editor.input.value = name;
info("Waiting for rule view to update");
let onRuleViewChanged = once(view, "ruleview-changed");
info("Entering the commit key");
EventUtils.synthesizeKey("VK_RETURN", {});
yield onRuleViewChanged;
is(getRuleViewRuleEditor(view, 1).element.getAttribute("unmatched"), "false",
"Rule with " + name + " does match the current element.");
// Escape the new property editor after editing the selector
let onBlur = once(view.styleDocument.activeElement, "blur");
EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
yield onBlur;
}

View File

@ -733,6 +733,7 @@ function* reloadPage(inspector, testActor) {
/**
* Create a new rule by clicking on the "add rule" button.
* This will leave the selector inplace-editor active.
*
* @param {InspectorPanel} inspector
* The instance of InspectorPanel currently loaded in the toolbox
@ -748,6 +749,36 @@ function* addNewRule(inspector, view) {
yield view.once("ruleview-changed");
}
/**
* Create a new rule by clicking on the "add rule" button, dismiss the editor field and
* verify that the selector is correct.
*
* @param {InspectorPanel} inspector
* The instance of InspectorPanel currently loaded in the toolbox
* @param {CssRuleView} view
* The instance of the rule-view panel
* @param {String} expectedSelector
* The value we expect the selector to have
* @param {Number} expectedIndex
* The index we expect the rule to have in the rule-view
* @return a promise that resolves after the rule has been added
*/
function* addNewRuleAndDismissEditor(inspector, view, expectedSelector, expectedIndex) {
yield addNewRule(inspector, view);
info("Getting the new rule at index " + expectedIndex);
let ruleEditor = getRuleViewRuleEditor(view, expectedIndex);
let editor = ruleEditor.selectorText.ownerDocument.activeElement;
is(editor.value, expectedSelector,
"The editor for the new selector has the correct value: " + expectedSelector);
info("Pressing escape to leave the editor");
EventUtils.synthesizeKey("VK_ESCAPE", {});
is(ruleEditor.selectorText.textContent, expectedSelector,
"The new selector has the correct text: " + expectedSelector);
}
/**
* Simulate a sequence of non-character keys (return, escape, tab) and wait for
* a given element to receive the focus.

View File

@ -343,6 +343,8 @@ TextPropertyEditor.prototype = {
onRevert: this._onSwatchRevert
});
span.on("unit-change", this._onSwatchCommit);
let title = CssLogic.l10n("rule.colorSwatch.tooltip");
span.setAttribute("title", title);
}
}
@ -359,6 +361,8 @@ TextPropertyEditor.prototype = {
onCommit: this._onSwatchCommit,
onRevert: this._onSwatchRevert
});
let title = CssLogic.l10n("rule.bezierSwatch.tooltip");
span.setAttribute("title", title);
}
}
@ -374,6 +378,8 @@ TextPropertyEditor.prototype = {
onCommit: this._onSwatchCommit,
onRevert: this._onSwatchRevert
}, outputParser, parserOptions);
let title = CssLogic.l10n("rule.filterSwatch.tooltip");
span.setAttribute("title", title);
}
}
@ -382,6 +388,8 @@ TextPropertyEditor.prototype = {
if (this.ruleEditor.isEditable) {
for (let angleSpan of this.angleSwatchSpans) {
angleSpan.on("unit-change", this._onSwatchCommit);
let title = CssLogic.l10n("rule.angleSwatch.tooltip");
angleSpan.setAttribute("title", title);
}
}

View File

@ -42,13 +42,16 @@ add_task(function* () {
* @return {Promise}
*/
function* switchToFrameContext(frameIndex, toolbox, inspector) {
// Verify that the frame list button is visible and populated
let frameListBtn = toolbox.doc.getElementById("command-button-frames");
let frameBtns = frameListBtn.firstChild.querySelectorAll("[data-window-id]");
// Open frame menu and wait till it's available on the screen.
let btn = toolbox.doc.getElementById("command-button-frames");
let menu = toolbox.showFramesMenu({target: btn});
yield once(menu, "open");
info("Select the iframe in the frame list.");
let newRoot = inspector.once("new-root");
frameBtns[frameIndex].click();
menu.menuitems[frameIndex].click();
yield newRoot;
yield inspector.once("inspector-updated");

View File

@ -24,20 +24,24 @@ add_task(function* () {
assertMarkupViewIsLoaded(inspector);
// Verify that the frame list button is visible and populated
// Verify that the frame map button is empty at the moment.
let btn = toolbox.doc.getElementById("command-button-frames");
ok(!btn.firstChild.getAttribute("hidden"),
"The frame list button is visible");
let frameBtns = Array.slice(
btn.firstChild.querySelectorAll("[data-window-id]"));
is(frameBtns.length, 2, "We have both frames in the list");
frameBtns.sort(function (a, b) {
return a.getAttribute("label").localeCompare(b.getAttribute("label"));
ok(!btn.firstChild, "The frame list button doesn't have any children");
// Open frame menu and wait till it's available on the screen.
let menu = toolbox.showFramesMenu({target: btn});
yield once(menu, "open");
// Verify that the menu is popuplated.
let frames = menu.menuitems.slice();
is(frames.length, 2, "We have both frames in the menu");
frames.sort(function (a, b) {
return a.label.localeCompare(b.label);
});
is(frameBtns[0].getAttribute("label"), FrameURL,
"Got top level document in the list");
is(frameBtns[1].getAttribute("label"), URL,
"Got iframe document in the list");
is(frames[0].label, FrameURL, "Got top level document in the list");
is(frames[1].label, URL, "Got iframe document in the list");
// Listen to will-navigate to check if the view is empty
let willNavigate = toolbox.target.once("will-navigate").then(() => {
@ -50,7 +54,7 @@ add_task(function* () {
let newRoot = inspector.once("new-root");
yield selectNode("#top", inspector);
info("Select the iframe");
frameBtns[0].click();
frames[0].click();
yield willNavigate;
yield newRoot;
@ -59,9 +63,9 @@ add_task(function* () {
// Verify we are on page one
ok(!(yield testActor.hasNode("iframe")),
"We not longer have access to the top frame elements");
"We not longer have access to the top frame elements");
ok((yield testActor.hasNode("#frame")),
"But now have direct access to the iframe elements");
"But now have direct access to the iframe elements");
// On page 2 load, verify we have the right content
assertMarkupViewIsLoaded(inspector);

View File

@ -87,7 +87,6 @@ skip-if = (os == 'linux' && e10s && debug) # Bug 1242204
[browser_net_html-preview.js]
[browser_net_icon-preview.js]
[browser_net_image-tooltip.js]
skip-if = true # Bug 1234341, bug 1252641
[browser_net_json-long.js]
[browser_net_json-malformed.js]
[browser_net_json_custom_mime.js]

View File

@ -1,78 +1,70 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests if image responses show a popup in the requests menu when hovered.
*/
add_task(function* test() {
let [, debuggee, monitor] = yield initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL);
info("Starting test... ");
function test() {
initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL).then(([aTab, aDebuggee, aMonitor]) => {
info("Starting test... ");
let { $, EVENTS, ACTIVITY_TYPE, NetMonitorView, NetMonitorController } =
monitor.panelWin;
let { RequestsMenu } = NetMonitorView;
RequestsMenu.lazyUpdate = true;
let { $, EVENTS, ACTIVITY_TYPE, NetMonitorView, NetMonitorController } = aMonitor.panelWin;
let { RequestsMenu } = NetMonitorView;
let onEvents = waitForNetworkEvents(monitor, 7);
let onThumbnail = waitFor(monitor.panelWin, EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED);
promise.all([
waitForNetworkEvents(aMonitor, 7),
waitFor(aMonitor.panelWin, EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED)
]).then(() => {
info("Checking the image thumbnail after a few requests were made...");
let requestItem = RequestsMenu.items[5];
let requestTooltip = requestItem.attachment.tooltip;
ok(requestTooltip, "There should be a tooltip instance for the image request.");
debuggee.performRequests();
yield onEvents;
yield onThumbnail;
let anchor = $(".requests-menu-file", requestItem.target);
return showTooltipOn(requestTooltip, anchor);
}).then(aTooltip => {
ok(true,
"An tooltip was successfully opened for the image request.");
is(aTooltip.content.querySelector("image").src, TEST_IMAGE_DATA_URI,
"The tooltip's image content is displayed correctly.");
info("Checking the image thumbnail after a few requests were made...");
yield showTooltipAndVerify(RequestsMenu.items[5]);
info("Reloading the debuggee and performing all requests again...");
reloadAndPerformRequests();
// 7 XHRs as before + 1 extra document reload
onEvents = waitForNetworkEvents(monitor, 8);
onThumbnail = waitFor(monitor.panelWin, EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED);
return promise.all([
waitForNetworkEvents(aMonitor, 7), // 6 + 1
waitFor(aMonitor.panelWin, EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED)
]);
}).then(() => {
info("Checking the image thumbnail after a reload.");
let requestItem = RequestsMenu.items[6];
let requestTooltip = requestItem.attachment.tooltip;
ok(requestTooltip, "There should be a tooltip instance for the image request.");
info("Reloading the debuggee and performing all requests again...");
yield NetMonitorController.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED);
debuggee.performRequests();
yield onEvents;
yield onThumbnail;
let anchor = $(".requests-menu-file", requestItem.target);
return showTooltipOn(requestTooltip, anchor);
}).then(aTooltip => {
ok(true,
"An tooltip was successfully opened for the image request.");
is(aTooltip.content.querySelector("image").src, TEST_IMAGE_DATA_URI,
"The tooltip's image content is displayed correctly.");
info("Checking the image thumbnail after a reload.");
yield showTooltipAndVerify(RequestsMenu.items[6]);
teardown(aMonitor).then(finish);
});
yield teardown(monitor);
finish();
function reloadAndPerformRequests() {
NetMonitorController.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED).then(() => {
aDebuggee.performRequests();
});
}
/**
* Show a tooltip on the {requestItem} and verify that it was displayed
* with the expected content.
*/
function* showTooltipAndVerify(requestItem) {
let { tooltip } = requestItem.attachment;
ok(tooltip, "There should be a tooltip instance for the image request.");
/**
* @return a promise that resolves when the tooltip is shown
*/
function showTooltipOn(tooltip, element) {
return Task.spawn(function* () {
let isValidTarget = yield tooltip._toggle.isValidHoverTarget(element);
ok(isValidTarget, "Element is a valid tooltip target");
let onShown = tooltip.once("shown");
tooltip.show();
yield onShown;
return tooltip;
});
}
let anchor = $(".requests-menu-file", requestItem.target);
yield showTooltipOn(tooltip, anchor);
aDebuggee.performRequests();
});
}
info("Tooltip was successfully opened for the image request.");
is(tooltip.content.querySelector("image").src, TEST_IMAGE_DATA_URI,
"The tooltip's image content is displayed correctly.");
}
/**
* Trigger a tooltip over an element by sending mousemove event.
* @return a promise that resolves when the tooltip is shown
*/
function showTooltipOn(tooltip, element) {
let onShown = tooltip.once("shown");
let win = element.ownerDocument.defaultView;
EventUtils.synthesizeMouseAtCenter(element, {type: "mousemove"}, win);
return onShown;
}
});

View File

@ -254,7 +254,8 @@ function waitForNetworkEvents(aMonitor, aGetRequests, aPostRequests = 0) {
let url = networkInfo.request.url;
updateProgressForURL(url, event);
info("> Current state: " + JSON.stringify(progress, null, 2));
// Uncomment this to get a detailed progress logging (when debugging a test)
// info("> Current state: " + JSON.stringify(progress, null, 2));
// There are 15 updates which need to be fired for a request to be
// considered finished. The "requestPostData" packet isn't fired for

View File

@ -595,7 +595,9 @@ var ShadersEditorsView = {
let tooltip = node._markerErrorsTooltip = new Tooltip(document);
tooltip.defaultOffsetX = GUTTER_ERROR_PANEL_OFFSET_X;
tooltip.setTextContent({ messages: messages });
tooltip.startTogglingOnHover(node, () => true, GUTTER_ERROR_PANEL_DELAY);
tooltip.startTogglingOnHover(node, () => true, {
toggleDelay: GUTTER_ERROR_PANEL_DELAY
});
},
/**

View File

@ -30,7 +30,7 @@ define(function (require, exports, module) {
},
getTitle: function () {
return "";
return this.props.object.class || "Object";
},
longPropIterator: function (object) {
@ -146,8 +146,7 @@ define(function (require, exports, module) {
if (this.props.mode == "tiny" || !props.length) {
return (
ObjectBox({className: "object"},
span({className: "objectTitle"}, this.getTitle(object)),
span({className: "objectLeftBrace", role: "presentation"}, "{}")
span({className: "objectTitle"}, this.getTitle(object))
)
);
}
@ -155,7 +154,7 @@ define(function (require, exports, module) {
return (
ObjectBox({className: "object"},
span({className: "objectTitle"}, this.getTitle(object)),
span({className: "objectLeftBrace", role: "presentation"}, "{"),
span({className: "objectLeftBrace", role: "presentation"}, " {"),
props,
span({className: "objectRightBrace"}, "}")
)

View File

@ -30,7 +30,7 @@ define(function (require, exports, module) {
},
getTitle: function () {
return "";
return "Object";
},
longPropIterator: function (object) {
@ -136,9 +136,17 @@ define(function (require, exports, module) {
let object = this.props.object;
let props = this.shortPropIterator(object);
if (this.props.mode == "tiny" || !props.length) {
return (
ObjectBox({className: "object"},
span({className: "objectTitle"}, this.getTitle())
)
);
}
return (
ObjectBox({className: "object"},
span({className: "objectTitle"}, this.getTitle(object)),
span({className: "objectTitle"}, this.getTitle()),
span({className: "objectLeftBrace", role: "presentation"}, "{"),
props,
span({className: "objectRightBrace"}, "}")

View File

@ -76,7 +76,7 @@ var CommandUtils = {
return util.promiseEach(toolbarSpec, typed => {
// Ask GCLI to parse the typed string (doesn't execute it)
return requisition.update(typed).then(() => {
let button = document.createElement("toolbarbutton");
let button = document.createElementNS(NS_XHTML, "button");
// Ignore invalid commands
let command = requisition.commandAssignment.value;
@ -93,13 +93,15 @@ var CommandUtils = {
else {
button.setAttribute("text-as-image", "true");
button.setAttribute("label", command.name);
button.className = "devtools-toolbarbutton";
}
button.classList.add("devtools-button");
if (command.tooltipText != null) {
button.setAttribute("tooltiptext", command.tooltipText);
button.setAttribute("title", command.tooltipText);
}
else if (command.description != null) {
button.setAttribute("tooltiptext", command.description);
button.setAttribute("title", command.description);
}
button.addEventListener("click", () => {

View File

@ -10,6 +10,7 @@ XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
DIRS += [
'components',
'redux',
'shim',
'vendor',
'widgets',
]

View File

@ -0,0 +1,487 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/* globals localStorage, window */
// Some constants from nsIPrefBranch.idl.
const PREF_INVALID = 0;
const PREF_STRING = 32;
const PREF_INT = 64;
const PREF_BOOL = 128;
const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";
/**
* Create a new preference object.
*
* @param {PrefBranch} branch the branch holding this preference
* @param {String} name the base name of this preference
* @param {String} fullName the fully-qualified name of this preference
*/
function Preference(branch, name, fullName) {
this.branch = branch;
this.name = name;
this.fullName = fullName;
this.defaultValue = null;
this.hasUserValue = false;
this.userValue = null;
this.type = null;
}
Preference.prototype = {
/**
* Return this preference's current value.
*
* @return {Any} The current value of this preference. This may
* return a string, a number, or a boolean depending on the
* preference's type.
*/
get: function () {
if (this.hasUserValue) {
return this.userValue;
}
return this.defaultValue;
},
/**
* Set the preference's value. The new value is assumed to be a
* user value. After setting the value, this function emits a
* change notification.
*
* @param {Any} value the new value
*/
set: function (value) {
if (!this.hasUserValue || value !== this.userValue) {
this.userValue = value;
this.hasUserValue = true;
this.saveAndNotify();
}
},
/**
* Set the default value for this preference, and emit a
* notification if this results in a visible change.
*
* @param {Any} value the new default value
*/
setDefault: function (value) {
if (this.defaultValue !== value) {
this.defaultValue = value;
if (!this.hasUserValue) {
this.saveAndNotify();
}
}
},
/**
* If this preference has a user value, clear it. If a change was
* made, emit a change notification.
*/
clearUserValue: function () {
if (this.hasUserValue) {
this.userValue = null;
this.hasUserValue = false;
this.saveAndNotify();
}
},
/**
* Helper function to write the preference's value to local storage
* and then emit a change notification.
*/
saveAndNotify: function () {
let store = {
type: this.type,
defaultValue: this.defaultValue,
hasUserValue: this.hasUserValue,
userValue: this.userValue,
};
localStorage.setItem(this.fullName, JSON.stringify(store));
this.branch._notify(this.name);
},
/**
* Change this preference's value without writing it back to local
* storage. This is used to handle changes to local storage that
* were made externally.
*
* @param {Number} type one of the PREF_* values
* @param {Any} userValue the user value to use if the pref does not exist
* @param {Any} defaultValue the default value to use if the pref
* does not exist
* @param {Boolean} hasUserValue if a new pref is created, whether
* the default value is also a user value
* @param {Object} store the new value of the preference. It should
* be of the form {type, defaultValue, hasUserValue, userValue};
* where |type| is one of the PREF_* type constants; |defaultValue|
* and |userValue| are the default and user values, respectively;
* and |hasUserValue| is a boolean indicating whether the user value
* is valid
*/
storageUpdated: function (type, userValue, hasUserValue, defaultValue) {
this.type = type;
this.defaultValue = defaultValue;
this.hasUserValue = hasUserValue;
this.userValue = userValue;
// There's no need to write this back to local storage, since it
// came from there; and this avoids infinite event loops.
this.branch._notify(this.name);
},
};
/**
* Create a new preference branch. This object conforms largely to
* nsIPrefBranch and nsIPrefService, though it only implements the
* subset needed by devtools.
*
* @param {PrefBranch} parent the parent branch, or null for the root
* branch.
* @param {String} name the base name of this branch
* @param {String} fullName the fully-qualified name of this branch
*/
function PrefBranch(parent, name, fullName) {
this._parent = parent;
this._name = name;
this._fullName = fullName;
this._observers = {};
this._children = {};
if (!parent) {
this._initializeRoot();
}
}
PrefBranch.prototype = {
PREF_INVALID: PREF_INVALID,
PREF_STRING: PREF_STRING,
PREF_INT: PREF_INT,
PREF_BOOL: PREF_BOOL,
/** @see nsIPrefBranch.root. */
get root() {
return this._fullName;
},
/** @see nsIPrefBranch.getPrefType. */
getPrefType: function (prefName) {
return this._findPref(prefName).type;
},
/** @see nsIPrefBranch.getBoolPref. */
getBoolPref: function (prefName) {
let thePref = this._findPref(prefName);
if (thePref.type !== PREF_BOOL) {
throw new Error(`${prefName} does not have bool type`);
}
return thePref.get();
},
/** @see nsIPrefBranch.setBoolPref. */
setBoolPref: function (prefName, value) {
if (typeof value !== "boolean") {
throw new Error("non-bool passed to setBoolPref");
}
let thePref = this._findOrCreatePref(prefName, value, true, value);
if (thePref.type !== PREF_BOOL) {
throw new Error(`${prefName} does not have bool type`);
}
thePref.set(value);
},
/** @see nsIPrefBranch.getCharPref. */
getCharPref: function (prefName) {
let thePref = this._findPref(prefName);
if (thePref.type !== PREF_STRING) {
throw new Error(`${prefName} does not have string type`);
}
return thePref.get();
},
/** @see nsIPrefBranch.setCharPref. */
setCharPref: function (prefName, value) {
if (typeof value !== "string") {
throw new Error("non-string passed to setCharPref");
}
let thePref = this._findOrCreatePref(prefName, value, true, value);
if (thePref.type !== PREF_STRING) {
throw new Error(`${prefName} does not have string type`);
}
thePref.set(value);
},
/** @see nsIPrefBranch.getIntPref. */
getIntPref: function (prefName) {
let thePref = this._findPref(prefName);
if (thePref.type !== PREF_INT) {
throw new Error(`${prefName} does not have int type`);
}
return thePref.get();
},
/** @see nsIPrefBranch.setIntPref. */
setIntPref: function (prefName, value) {
if (typeof value !== "number") {
throw new Error("non-number passed to setIntPref");
}
let thePref = this._findOrCreatePref(prefName, value, true, value);
if (thePref.type !== PREF_INT) {
throw new Error(`${prefName} does not have int type`);
}
thePref.set(value);
},
/** @see nsIPrefBranch.clearUserPref */
clearUserPref: function (prefName) {
let thePref = this._findPref(prefName);
thePref.clearUserValue();
},
/** @see nsIPrefBranch.prefHasUserValue */
prefHasUserValue: function (prefName) {
let thePref = this._findPref(prefName);
return thePref.hasUserValue;
},
/** @see nsIPrefBranch.addObserver */
addObserver: function (domain, observer, holdWeak) {
if (domain !== "" && !domain.endsWith(".")) {
throw new Error("invalid domain to addObserver: " + domain);
}
if (holdWeak) {
throw new Error("shim prefs only supports strong observers");
}
if (!(domain in this._observers)) {
this._observers[domain] = [];
}
this._observers[domain].push(observer);
},
/** @see nsIPrefBranch.removeObserver */
removeObserver: function (domain, observer) {
if (!(domain in this._observers)) {
return;
}
let index = this._observers[domain].indexOf(observer);
if (index >= 0) {
this._observers[domain].splice(index, 1);
}
},
/** @see nsIPrefService.savePrefFile */
savePrefFile: function (file) {
if (file) {
throw new Error("shim prefs only supports null file in savePrefFile");
}
// Nothing to do - this implementation always writes back.
},
/** @see nsIPrefService.getBranch */
getBranch: function (prefRoot) {
if (!prefRoot) {
return this;
}
if (prefRoot.endsWith(".")) {
prefRoot = prefRoot.slice(0, -1);
}
// This is a bit weird since it could erroneously return a pref,
// not a pref branch.
return this._findPref(prefRoot);
},
/**
* Helper function to find either a Preference or PrefBranch object
* given its name. If the name is not found, throws an exception.
*
* @param {String} prefName the fully-qualified preference name
* @return {Object} Either a Preference or PrefBranch object
*/
_findPref: function (prefName) {
let branchNames = prefName.split(".");
let branch = this;
for (let branchName of branchNames) {
branch = branch._children[branchName];
if (!branch) {
throw new Error("could not find pref branch " + prefName);
}
}
return branch;
},
/**
* Helper function to notify any observers when a preference has
* changed. This will also notify the parent branch for further
* reporting.
*
* @param {String} relativeName the name of the updated pref,
* relative to this branch
*/
_notify: function (relativeName) {
for (let domain in this._observers) {
if (relativeName.startsWith(domain)) {
// Allow mutation while walking.
let localList = this._observers[domain].slice();
for (let observer of localList) {
try {
observer.observe(this, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID,
relativeName);
} catch (e) {
console.error(e);
}
}
}
}
if (this._parent) {
this._parent._notify(this._name + "." + relativeName);
}
},
/**
* Helper function to create a branch given an array of branch names
* representing the path of the new branch.
*
* @param {Array} branchList an array of strings, one per component
* of the branch to be created
* @return {PrefBranch} the new branch
*/
_createBranch: function (branchList) {
let parent = this;
for (let branch of branchList) {
if (!parent._children[branch]) {
parent._children[branch] = new PrefBranch(parent, branch,
parent.root + "." + branch);
}
parent = parent._children[branch];
}
return parent;
},
/**
* Create a new preference. The new preference is assumed to be in
* local storage already, and the new value is taken from there.
*
* @param {String} keyName the full-qualified name of the preference.
* This is also the name of the key in local storage.
* @param {Any} userValue the user value to use if the pref does not exist
* @param {Any} defaultValue the default value to use if the pref
* does not exist
* @param {Boolean} hasUserValue if a new pref is created, whether
* the default value is also a user value
*/
_findOrCreatePref: function (keyName, userValue, hasUserValue, defaultValue) {
let branchName = keyName.split(".");
let prefName = branchName.pop();
let branch = this._createBranch(branchName);
if (!(prefName in branch._children)) {
if (hasUserValue && typeof (userValue) !== typeof (defaultValue)) {
throw new Error("inconsistent values when creating " + keyName);
}
let type;
switch (typeof (defaultValue)) {
case "boolean":
type = PREF_BOOL;
break;
case "number":
type = PREF_INT;
break;
case "string":
type = PREF_STRING;
break;
default:
throw new Error("unhandled argument type: " + typeof (defaultValue));
}
let thePref = new Preference(branch, prefName, keyName);
thePref.storageUpdated(type, userValue, hasUserValue, defaultValue);
branch._children[prefName] = thePref;
}
return branch._children[prefName];
},
/**
* Helper function that is called when local storage changes. This
* updates the preferences and notifies pref observers as needed.
*
* @param {StorageEvent} event the event representing the local
* storage change
*/
_onStorageChange: function (event) {
if (event.storageArea !== localStorage) {
return;
}
// Ignore delete events. Not clear what's correct.
if (event.key === null || event.newValue === null) {
return;
}
let {type, userValue, hasUserValue, defaultValue} =
JSON.parse(event.newValue);
if (event.oldValue === null) {
this._findOrCreatePref(event.key, userValue, hasUserValue, defaultValue);
} else {
let thePref = this._findPref(event.key);
thePref.storageUpdated(type, userValue, hasUserValue, defaultValue);
}
},
/**
* Helper function to initialize the root PrefBranch.
*/
_initializeRoot: function () {
if (localStorage.length === 0) {
// FIXME - this is where we'll load devtools.js to install the
// default prefs.
}
// Read the prefs from local storage and create the local
// representations.
for (let i = 0; i < localStorage.length; ++i) {
let keyName = localStorage.key(i);
let {userValue, hasUserValue, defaultValue} =
JSON.parse(localStorage.getItem(keyName));
this._findOrCreatePref(keyName, userValue, hasUserValue, defaultValue);
}
this._onStorageChange = this._onStorageChange.bind(this);
window.addEventListener("storage", this._onStorageChange);
},
};
const Services = {
/**
* An implementation of nsIPrefService that is based on local
* storage. Only the subset of nsIPrefService that is actually used
* by devtools is implemented here.
*/
prefs: new PrefBranch(null, "", ""),
};
/**
* Create a new preference. This is used during startup (see
* devtools/client/preferences/devtools.js) to install the
* default preferences.
*
* @param {String} name the name of the preference
* @param {Any} value the default value of the preference
*/
function pref(name, value) {
let thePref = Services.prefs._findOrCreatePref(name, value, true, value);
thePref.setDefault(value);
}
exports.Services = Services;
// This is exported to silence eslint and, at some point, perhaps to
// provide it when loading devtools.js in order to install the default
// preferences.
exports.pref = pref;

View File

@ -0,0 +1,13 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DevToolsModules(
'Services.js',
)
MOCHITEST_MANIFESTS += [
'test/mochitest.ini',
]

View File

@ -0,0 +1,4 @@
{
// Extend from the shared list of defined globals for mochitests.
"extends": "../../../../.eslintrc.mochitests"
}

View File

@ -0,0 +1,5 @@
[DEFAULT]
support-files =
prefs-wrapper.js
[test_service_prefs.html]

View File

@ -0,0 +1,80 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// A wrapper for Services.prefs that compares our shim content
// implementation with the real service.
// We assume we're loaded in a global where Services was already loaded.
/* globals isDeeply, Services */
"use strict";
function setMethod(methodName, prefName, value) {
let savedException;
let prefThrew = false;
try {
Services.prefs[methodName](prefName, value);
} catch (e) {
prefThrew = true;
savedException = e;
}
let realThrew = false;
try {
SpecialPowers[methodName](prefName, value);
} catch (e) {
realThrew = true;
savedException = e;
}
is(prefThrew, realThrew, methodName + " [throw check]");
if (prefThrew || realThrew) {
throw savedException;
}
}
function getMethod(methodName, prefName) {
let prefThrew = false;
let prefValue = undefined;
let savedException;
try {
prefValue = Services.prefs[methodName](prefName);
} catch (e) {
prefThrew = true;
savedException = e;
}
let realValue = undefined;
let realThrew = false;
try {
realValue = SpecialPowers[methodName](prefName);
} catch (e) {
realThrew = true;
savedException = e;
}
is(prefThrew, realThrew, methodName + " [throw check]");
isDeeply(prefValue, realValue, methodName + " [equality]");
if (prefThrew || realThrew) {
throw savedException;
}
return prefValue;
}
var WrappedPrefs = {};
for (let method of ["getPrefType", "getBoolPref", "getCharPref", "getIntPref",
"clearUserPref"]) {
WrappedPrefs[method] = getMethod.bind(null, method);
}
for (let method of ["setBoolPref", "setCharPref", "setIntPref"]) {
WrappedPrefs[method] = setMethod.bind(null, method);
}
// Silence eslint.
exports.WrappedPrefs = WrappedPrefs;

View File

@ -0,0 +1,235 @@
<!DOCTYPE html>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1265808
-->
<head>
<title>Test for Bug 1265808 - replace Services.prefs</title>
<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css"
href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript;version=1.8">
"use strict";
var exports = {};
// Add some starter prefs.
localStorage.setItem("devtools.branch1.somebool", JSON.stringify({
// bool
type: 128,
defaultValue: false,
hasUserValue: false,
userValue: false
}));
localStorage.setItem("devtools.branch1.somestring", JSON.stringify({
// string
type: 32,
defaultValue: "dinosaurs",
hasUserValue: true,
userValue: "elephants"
}));
localStorage.setItem("devtools.branch2.someint", JSON.stringify({
// string
type: 64,
defaultValue: -16,
hasUserValue: false,
userValue: null
}));
</script>
<script type="application/javascript;version=1.8"
src="prefs-wrapper.js"></script>
<script type="application/javascript;version=1.8"
src="resource://devtools/client/shared/shim/Services.js"></script>
</head>
<body>
<script type="application/javascript;version=1.8">
"use strict";
function do_tests() {
is(Services.prefs.getBoolPref("devtools.branch1.somebool"), false,
"bool pref value");
Services.prefs.setBoolPref("devtools.branch1.somebool", true);
is(Services.prefs.getBoolPref("devtools.branch1.somebool"), true,
"bool pref value after setting");
let threw;
try {
threw = false;
WrappedPrefs.getIntPref("devtools.branch1.somebool");
} catch (e) {
threw = true;
}
ok(threw, "type-checking for bool pref");
try {
threw = false;
Services.prefs.setIntPref("devtools.branch1.somebool", 27);
} catch (e) {
threw = true;
}
ok(threw, "type-checking for setting bool pref");
try {
threw = false;
Services.prefs.setBoolPref("devtools.branch1.somebool", 27);
} catch (e) {
threw = true;
}
ok(threw, "setting bool pref to wrong type");
try {
threw = false;
Services.prefs.getCharPref("devtools.branch2.someint");
} catch (e) {
threw = true;
}
ok(threw, "type-checking for int pref");
try {
threw = false;
Services.prefs.setCharPref("devtools.branch2.someint", "whatever");
} catch (e) {
threw = true;
}
ok(threw, "type-checking for setting int pref");
try {
threw = false;
Services.prefs.setIntPref("devtools.branch2.someint", "whatever");
} catch (e) {
threw = true;
}
ok(threw, "setting int pref to wrong type");
try {
threw = false;
Services.prefs.getBoolPref("devtools.branch1.somestring");
} catch (e) {
threw = true;
}
ok(threw, "type-checking for char pref");
try {
threw = false;
Services.prefs.setBoolPref("devtools.branch1.somestring", true);
} catch (e) {
threw = true;
}
ok(threw, "type-checking for setting char pref");
try {
threw = false;
Services.prefs.setCharPref("devtools.branch1.somestring", true);
} catch (e) {
threw = true;
}
ok(threw, "setting char pref to wrong type");
is(Services.prefs.getPrefType("devtools.branch1.somebool"),
Services.prefs.PREF_BOOL, "type of bool pref");
is(Services.prefs.getPrefType("devtools.branch2.someint"),
Services.prefs.PREF_INT, "type of int pref");
is(Services.prefs.getPrefType("devtools.branch1.somestring"),
Services.prefs.PREF_STRING, "type of string pref");
WrappedPrefs.setBoolPref("devtools.branch1.somebool", true);
ok(WrappedPrefs.getBoolPref("devtools.branch1.somebool"), "set bool pref");
WrappedPrefs.setIntPref("devtools.branch2.someint", -93);
is(WrappedPrefs.getIntPref("devtools.branch2.someint"), -93, "set int pref");
WrappedPrefs.setCharPref("devtools.branch1.somestring", "hello");
ok(WrappedPrefs.getCharPref("devtools.branch1.somestring"), "hello",
"set string pref");
Services.prefs.clearUserPref("devtools.branch1.somestring");
ok(Services.prefs.getCharPref("devtools.branch1.somestring"), "dinosaurs",
"clear string pref");
ok(Services.prefs.prefHasUserValue("devtools.branch1.somebool"),
"bool pref has user value");
ok(!Services.prefs.prefHasUserValue("devtools.branch1.somestring"),
"string pref does not have user value");
Services.prefs.savePrefFile(null);
ok(true, "saved pref file without error");
let branch0 = Services.prefs.getBranch(null);
let branch1 = Services.prefs.getBranch("devtools.branch1.");
branch1.setCharPref("somestring", "octopus");
Services.prefs.setCharPref("devtools.branch1.somestring", "octopus");
is(Services.prefs.getCharPref("devtools.branch1.somestring"), "octopus",
"set correctly via branch");
ok(branch0.getCharPref("devtools.branch1.somestring"), "octopus",
"get via base branch");
ok(branch1.getCharPref("somestring"), "octopus", "get via branch");
let notifications = {};
let clearNotificationList = () => { notifications = {}; }
let observer = {
observe: function (subject, topic, data) {
notifications[data] = true;
}
};
try {
threw = false;
branch0.addObserver("devtools.branch1", null, null);
} catch (e) {
threw = true;
}
ok(threw, "invalid branch name to addObserver");
branch0.addObserver("devtools.branch1.", observer, false);
branch1.addObserver("", observer, false);
Services.prefs.setCharPref("devtools.branch1.somestring", "elf owl");
isDeeply(notifications, {
"devtools.branch1.somestring": true,
"somestring": true
}, "notifications sent to two listeners");
clearNotificationList();
Services.prefs.setIntPref("devtools.branch2.someint", 1729);
isDeeply(notifications, {}, "no notifications sent");
clearNotificationList();
branch0.removeObserver("devtools.branch1.", observer);
Services.prefs.setCharPref("devtools.branch1.somestring", "tapir");
isDeeply(notifications, {
"somestring": true
}, "removeObserver worked");
// Make sure we update if the pref change comes from somewhere else.
clearNotificationList();
pref("devtools.branch1.someotherstring", "lazuli bunting");
isDeeply(notifications, {
"someotherstring": true
}, "pref worked");
// Clean up.
localStorage.clear();
SimpleTest.finish();
}
SimpleTest.waitForExplicitFinish();
SpecialPowers.pushPrefEnv(
{"set": [
["devtools.branch1.somestring", "elephants"],
["devtools.branch1.somebool", false],
["devtools.branch2.someint", "-16"],
]},
do_tests);
</script>
</body>

View File

@ -342,6 +342,15 @@ Tooltip.prototype = {
this.panel = null;
},
/**
* Returns the outer container node (that includes the arrow etc.). Happens
* to be identical to this.panel here, can be different element in other
* Tooltip implementations.
*/
get container() {
return this.panel;
},
/**
* Set the content of this tooltip. Will first empty the tooltip and then
* append the new content element.

View File

@ -8,7 +8,7 @@
const {Task} = require("devtools/shared/task");
const DEFAULT_SHOW_DELAY = 50;
const DEFAULT_TOGGLE_DELAY = 50;
/**
* Tooltip helper designed to show/hide the tooltip when the mouse hovers over
@ -25,6 +25,9 @@ function TooltipToggle(tooltip) {
this._onMouseMove = this._onMouseMove.bind(this);
this._onMouseLeave = this._onMouseLeave.bind(this);
this._onTooltipMouseOver = this._onTooltipMouseOver.bind(this);
this._onTooltipMouseOut = this._onTooltipMouseOut.bind(this);
}
module.exports.TooltipToggle = TooltipToggle;
@ -56,11 +59,18 @@ TooltipToggle.prototype = {
* The function can also return a promise that will resolve to one of
* the values listed above.
* If omitted, the tooltip will be shown everytime.
* @param {Number} showDelay
* An optional delay that will be observed before showing the tooltip.
* Defaults to DEFAULT_SHOW_DELAY.
* @param {Object} options
Set of optional arguments:
* - {Number} toggleDelay
* An optional delay (in ms) that will be observed before showing
* and before hiding the tooltip. Defaults to DEFAULT_TOGGLE_DELAY.
* - {Boolean} interactive
* If enabled, the tooltip is not hidden when mouse leaves the
* target element and enters the tooltip. Allows the tooltip
* content to be interactive.
*/
start: function (baseNode, targetNodeCb, showDelay = DEFAULT_SHOW_DELAY) {
start: function (baseNode, targetNodeCb,
{toggleDelay = DEFAULT_TOGGLE_DELAY, interactive = false} = {}) {
this.stop();
if (!baseNode) {
@ -69,11 +79,17 @@ TooltipToggle.prototype = {
}
this._baseNode = baseNode;
this._showDelay = showDelay;
this._targetNodeCb = targetNodeCb || (() => true);
this._toggleDelay = toggleDelay;
this._interactive = interactive;
baseNode.addEventListener("mousemove", this._onMouseMove, false);
baseNode.addEventListener("mouseleave", this._onMouseLeave, false);
baseNode.addEventListener("mousemove", this._onMouseMove);
baseNode.addEventListener("mouseleave", this._onMouseLeave);
if (this._interactive) {
this.tooltip.container.addEventListener("mouseover", this._onTooltipMouseOver);
this.tooltip.container.addEventListener("mouseout", this._onTooltipMouseOut);
}
},
/**
@ -88,8 +104,13 @@ TooltipToggle.prototype = {
return;
}
this._baseNode.removeEventListener("mousemove", this._onMouseMove, false);
this._baseNode.removeEventListener("mouseleave", this._onMouseLeave, false);
this._baseNode.removeEventListener("mousemove", this._onMouseMove);
this._baseNode.removeEventListener("mouseleave", this._onMouseLeave);
if (this._interactive) {
this.tooltip.container.removeEventListener("mouseover", this._onTooltipMouseOver);
this.tooltip.container.removeEventListener("mouseout", this._onTooltipMouseOut);
}
this._baseNode = null;
this._targetNodeCb = null;
@ -98,11 +119,11 @@ TooltipToggle.prototype = {
_onMouseMove: function (event) {
if (event.target !== this._lastHovered) {
this.tooltip.hide();
this._lastHovered = event.target;
this.win.clearTimeout(this.toggleTimer);
this.toggleTimer = this.win.setTimeout(() => {
this.tooltip.hide();
this.isValidHoverTarget(event.target).then(target => {
if (target === null) {
return;
@ -112,7 +133,7 @@ TooltipToggle.prototype = {
console.error("isValidHoverTarget rejected with unexpected reason:");
console.error(reason);
});
}, this._showDelay);
}, this._toggleDelay);
}
},
@ -132,9 +153,22 @@ TooltipToggle.prototype = {
}),
_onMouseLeave: function () {
this.win.clearTimeout(this.toggleTimer);
this._lastHovered = null;
this.tooltip.hide();
this.win.clearTimeout(this.toggleTimer);
this.toggleTimer = this.win.setTimeout(() => {
this.tooltip.hide();
}, this._toggleDelay);
},
_onTooltipMouseOver() {
this.win.clearTimeout(this.toggleTimer);
},
_onTooltipMouseOut() {
this.win.clearTimeout(this.toggleTimer);
this.toggleTimer = this.win.setTimeout(() => {
this.tooltip.hide();
}, this._toggleDelay);
},
destroy: function () {

View File

@ -14,12 +14,12 @@
/* Remove filters on firebug specific images */
.theme-firebug #toolbox-dock-buttons > toolbarbutton > image,
.theme-firebug .devtools-closebutton > image,
.theme-firebug .devtools-tabbar .devtools-button::before,
.theme-firebug .devtools-option-toolbarbutton > image,
.theme-firebug .command-button-invertable > image,
.theme-firebug .command-button-invertable::before,
.theme-firebug #sources-toolbar image,
.theme-firebug [id$="pane-toggle"] > image,
.theme-firebug [id$="pane-toggle"]::before,
.theme-firebug #global-toolbar .devtools-button::before,
.theme-firebug #element-picker::before,
.theme-firebug #debugger-controls .toolbarbutton-icon,
@ -244,7 +244,6 @@
min-width: 24px;
}
/* Move the Inspector button a bit down (looks better) */
.theme-firebug #command-button-pick > image {
margin-bottom: -4px;
.theme-firebug #command-button-frames {
min-width: 32px;
}

View File

@ -3,10 +3,36 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* Use flex layout for the Inspector toolbar. For now, it's done
specifically for the Inspector toolbar since general rule applied
on .devtools-toolbar breaks breadcrubs and also toolbars in other
panels (e.g. webconsole, debugger), these are not ready for HTML
layout yet. */
#inspector-toolbar.devtools-toolbar {
display: flex;
}
#inspector-toolbar.devtools-toolbar .devtools-toolbar-spacer {
flex-grow: 1;
display: inline-block;
}
#inspector-searchlabel {
overflow: hidden;
}
/* Make sure the text is vertically centered in Inspector's
search box. This can be removed when the search box is
switched to HTML.
See also: https://bugzilla.mozilla.org/show_bug.cgi?id=1265759 */
.theme-dark #inspector-searchbox,
.theme-light #inspector-searchbox {
line-height: 19px;
}
.theme-firebug #inspector-searchbox {
line-height: 17px;
}
#inspector-breadcrumbs-toolbar {
padding: 0px;
border-bottom-width: 0px;
@ -15,12 +41,12 @@
/* Expand/collapse panel toolbar button */
#inspector-pane-toggle {
list-style-image: var(--theme-pane-collapse-image);
#inspector-pane-toggle::before {
background-image: var(--theme-pane-collapse-image);
}
#inspector-pane-toggle[pane-collapsed] {
list-style-image: var(--theme-pane-expand-image);
#inspector-pane-toggle[pane-collapsed]::before {
background-image: var(--theme-pane-expand-image);
}
@media (max-width: 700px) {
@ -31,8 +57,8 @@
/* Add element toolbar button */
#inspector-element-add-button {
list-style-image: url("chrome://devtools/skin/images/add.svg");
#inspector-element-add-button::before {
background-image: url("chrome://devtools/skin/images/add.svg");
}
/* Tooltip: Events */

View File

@ -212,6 +212,10 @@
background: var(--theme-tab-toolbar-background);
}
.ruleview-rule[unmatched=true] {
opacity: 0.5;
}
.ruleview-rule[uneditable=true] :focus {
outline: none;
}
@ -448,7 +452,8 @@
cursor: text;
}
.ruleview-selector-separator, .ruleview-selector-unmatched {
.ruleview-selector-separator,
.ruleview-selector-unmatched {
color: #888;
}

View File

@ -213,14 +213,14 @@
min-width: 32px;
}
#toolbox-buttons .devtools-toolbarbutton[text-as-image] {
padding-inline-start: 5px;
padding-inline-end: 5px;
min-width: inherit;
/* Set flex attribute to Toolbox buttons and Picker container so,
they don't overlapp with the tab bar */
#toolbox-buttons {
display: flex;
}
#toolbox-buttons .devtools-toolbarbutton[type=menu] > .toolbarbutton-menu-dropmarker {
padding: 0 2px;
#toolbox-picker-container {
display: flex;
}
/* Invert toolbox button icons in Firebug theme. */
@ -306,50 +306,42 @@
/* Text-only buttons */
.theme-light .devtools-toolbarbutton[label]:not([text-as-image]):not([type=menu-button]),
.theme-light .devtools-toolbarbutton[data-text-only],
.theme-light #toolbox-buttons .devtools-toolbarbutton[text-as-image] {
.theme-light .devtools-toolbarbutton[data-text-only] {
background-color: var(--toolbar-tab-hover);
}
.theme-dark .devtools-toolbarbutton[label]:not([text-as-image]):not([type=menu-button]),
.theme-dark .devtools-toolbarbutton[data-text-only],
.theme-dark #toolbox-buttons .devtools-toolbarbutton[text-as-image] {
.theme-dark .devtools-toolbarbutton[data-text-only] {
background-color: rgba(0, 0, 0, .2); /* Splitter */
}
/* Text-only button states */
.theme-dark .devtools-button:not(:empty):not([disabled]):hover,
.theme-dark #toolbox-buttons .devtools-toolbarbutton:not([disabled])[text-as-image]:hover,
.theme-dark .devtools-toolbarbutton:not(:-moz-any([checked=true],[disabled],[text-as-image]))[label]:hover {
background: rgba(0, 0, 0, .3); /* Splitters */
}
.theme-light .devtools-button:not(:empty):not([disabled]):hover,
.theme-light #toolbox-buttons .devtools-toolbarbutton:not([disabled])[text-as-image]:hover,
.theme-light .devtools-toolbarbutton:not(:-moz-any([checked=true],[disabled],[text-as-image]))[label]:hover {
background: rgba(170, 170, 170, .3); /* Splitters */
}
.theme-dark .devtools-button:not(:empty):not([disabled]):hover:active,
.theme-dark #toolbox-buttons .devtools-toolbarbutton:not([disabled])[text-as-image]:hover:active,
.theme-dark .devtools-toolbarbutton:not(:-moz-any([checked=true],[disabled],[text-as-image]))[label]:hover:active {
background: rgba(0, 0, 0, .4); /* Splitters */
}
.theme-light .devtools-button:not(:empty):not([disabled]):hover:active,
.theme-light #toolbox-buttons .devtools-toolbarbutton:not([disabled])[text-as-image]:hover:active,
.theme-light .devtools-toolbarbutton:not(:-moz-any([checked=true],[disabled],[text-as-image]))[label]:hover:active {
background: var(--toolbar-tab-hover-active);
}
.theme-dark .devtools-toolbarbutton:not([disabled])[label][checked=true],
.theme-dark .devtools-toolbarbutton:not([disabled])[label][open],
.theme-dark .devtools-button:not(:empty)[checked=true],
.theme-dark #toolbox-buttons .devtools-toolbarbutton[text-as-image][checked=true] {
.theme-dark .devtools-button:not(:empty)[checked=true] {
background: var(--theme-selection-background-semitransparent);
color: var(--theme-selection-color);
}
.theme-light .devtools-toolbarbutton:not([disabled])[label][checked=true],
.theme-light .devtools-toolbarbutton:not([disabled])[label][open],
.theme-light .devtools-button:not(:empty)[checked=true],
.theme-light #toolbox-buttons .devtools-toolbarbutton[text-as-image][checked=true] {
.theme-light .devtools-button:not(:empty)[checked=true] {
background: rgba(76, 158, 217, .3); /* Select highlight blue */
}
@ -539,37 +531,8 @@
/* Close button */
.devtools-closebutton {
-moz-appearance: none;
border: none;
margin: 0 4px;
min-width: 16px;
width: 16px;
opacity: 0.8;
}
.devtools-closebutton > image {
width: 16px;
height: 16px;
-moz-appearance: none;
background-size: cover;
#toolbox-close::before {
background-image: var(--close-button-image);
background-position: center center;
background-repeat: no-repeat;
}
.devtools-closebutton > .toolbarbutton-icon {
/* XXX Buttons have padding in widget/ that we don't want here but can't override with good CSS, so we must
use evil CSS to give the impression of smaller content */
margin: -4px;
}
.devtools-closebutton > .toolbarbutton-text {
display: none;
}
.devtools-closebutton:hover {
opacity: 1;
}
/* In-tools sidebar */
@ -669,8 +632,8 @@
* Rules that apply to the global toolbox like command buttons,
* devtools tabs, docking buttons, etc. */
#toolbox-controls > toolbarbutton,
#toolbox-dock-buttons > toolbarbutton {
#toolbox-controls > button,
#toolbox-dock-buttons > button {
-moz-appearance: none;
-moz-user-focus: normal;
border: none;
@ -679,41 +642,22 @@
width: 16px;
}
#toolbox-controls > toolbarbutton > .toolbarbutton-text,
#toolbox-dock-buttons > toolbarbutton > .toolbarbutton-text,
.command-button > .toolbarbutton-text {
display: none;
}
/* Save space in Firebug theme */
.theme-firebug #toolbox-controls toolbarbutton {
.theme-firebug #toolbox-controls button {
margin-inline-start: 0 !important;
width: 12px;
min-width: 12px;
margin: 0 1px;
}
.theme-firebug #toolbox-controls toolbarbutton:hover {
background: none;
}
#toolbox-dock-buttons > toolbarbutton > image {
-moz-appearance: none;
width: 16px;
height: 16px;
background-size: 16px 16px;
background-position: 0 center;
background-repeat: no-repeat;
}
#toolbox-dock-bottom > image {
#toolbox-dock-bottom::before {
background-image: var(--dock-bottom-image);
}
#toolbox-dock-side > image {
#toolbox-dock-side::before {
background-image: var(--dock-side-image);
}
#toolbox-dock-window > image {
#toolbox-dock-window::before {
background-image: var(--dock-undock-image);
}
@ -723,11 +667,11 @@
display: none;
}
#toolbox-dock-bottom-minimize > image {
#toolbox-dock-bottom-minimize::before {
background-image: url("chrome://devtools/skin/images/dock-bottom-minimize@2x.png");
}
#toolbox-dock-bottom-minimize.minimized > image {
#toolbox-dock-bottom-minimize.minimized::before {
background-image: url("chrome://devtools/skin/images/dock-bottom-maximize@2x.png");
}
@ -764,89 +708,93 @@
/* Command buttons */
.command-button {
-moz-appearance: none;
border: none;
padding: 0 8px;
padding: 0;
margin: 0;
width: 32px;
position: relative;
-moz-user-focus: normal;
}
.command-button::before {
opacity: 0.7;
}
.command-button:hover {
background-color: var(--toolbar-tab-hover);
}
.command-button:hover:active, .command-button[checked=true]:not(:hover) {
.command-button:hover:active,
.command-button[checked=true]:not(:hover) {
background-color: var(--toolbar-tab-hover-active)
}
.command-button > image {
-moz-appearance: none;
width: 16px;
height: 16px;
background-size: cover;
background-position: 0 center;
background-repeat: no-repeat;
opacity: 0.7;
}
.command-button:hover > image {
.command-button:hover::before {
opacity: 0.85;
}
.command-button:hover:active > image,
.command-button[checked=true] > image,
.command-button[open=true] > image {
.command-button:hover:active::before,
.command-button[checked=true]::before,
.command-button[open=true]::before {
opacity: 1;
}
.command-button[checked=true] > image {
filter: url(images/filters.svg#checked-icon-state) !important;
}
/* Toolbox buttons images */
/* Toolbox command buttons */
#command-button-paintflashing > image {
#command-button-paintflashing::before {
background-image: var(--command-paintflashing-image);
}
#command-button-screenshot > image {
#command-button-screenshot::before {
background-image: var(--command-screenshot-image);
}
#command-button-responsive > image {
#command-button-responsive::before {
background-image: var(--command-responsive-image);
}
#command-button-scratchpad > image {
#command-button-scratchpad::before {
background-image: var(--command-scratchpad-image);
}
#command-button-pick > image {
#command-button-pick::before {
background-image: var(--command-pick-image);
}
#command-button-frames > image {
background-image: var(--command-frames-image);
}
#command-button-splitconsole > image {
#command-button-splitconsole::before {
background-image: var(--command-splitconsole-image);
}
#command-button-noautohide > image {
#command-button-noautohide::before {
background-image: var(--command-noautohide-image);
}
#command-button-eyedropper > image {
#command-button-eyedropper::before {
background-image: var(--command-eyedropper-image);
}
#command-button-rulers > image {
#command-button-rulers::before {
background-image: var(--command-rulers-image);
}
#command-button-measure > image {
#command-button-measure::before {
background-image: var(--command-measure-image);
}
#command-button-frames::before {
background-image: var(--command-frames-image);
}
#command-button-frames {
background: url("chrome://devtools/skin/images/dropmarker.svg") no-repeat right;
/* Override background-size from the command-button.
The drop down arrow is smaller */
background-size: 8px 4px !important;
}
#command-button-frames:-moz-dir(rtl) {
background-position: left;
}
/* Tabs */
.devtools-tabbar {
@ -1017,9 +965,6 @@
* inside of the light theme.
*/
.theme-light .devtools-tab[icon-invertable] > image,
.theme-light #toolbox-dock-buttons > toolbarbutton > image,
.theme-light .command-button-invertable > image,
.theme-light .devtools-closebutton > image,
.theme-light .devtools-toolbarbutton > image,
.theme-light .devtools-button::before,
.theme-light #breadcrumb-separator-normal,
@ -1033,6 +978,11 @@
filter: var(--icon-filter);
}
/* Reset the filter defined above */
.theme-light .command-button:not(.command-button-invertable) {
filter: none !important;
}
/* Since selected backgrounds are blue, we want to use the normal
* (light) icons. */
.theme-light .devtools-tab[icon-invertable][selected] > image,

View File

@ -26,12 +26,17 @@ add_task(function* () {
"The tool's content should initially be hidden.");
let btn = toolbox.doc.getElementById("command-button-frames");
ok(!btn.firstChild.getAttribute("hidden"), "The frame list button is visible");
let frameBtns = btn.firstChild.querySelectorAll("[data-window-id]");
is(frameBtns.length, 2, "We have both frames in the list");
ok(!btn.firstChild, "The frame list button has no children");
// Open frame menu and wait till it's available on the screen.
let menu = toolbox.showFramesMenu({target: btn});
yield once(menu, "open");
let frames = menu.menuitems;
is(frames.length, 2, "We have both frames in the list");
// Select the iframe
frameBtns[1].click();
frames[1].click();
let navigating = once(target, "will-navigate");

View File

@ -46,12 +46,11 @@ exports.items = [
let toolbox = gDevTools.getToolbox(target);
if (!toolbox) {
return gDevTools.showToolbox(target, "inspector").then((toolbox) => {
toolbox.toggleSplitConsole();
return gDevTools.showToolbox(target, "inspector").then((newToolbox) => {
newToolbox.toggleSplitConsole();
});
} else {
toolbox.toggleSplitConsole();
}
return toolbox.toggleSplitConsole();
}
},
{
@ -67,12 +66,12 @@ exports.items = [
exec: function (args, context) {
let toolbox = gDevTools.getToolbox(context.environment.target);
if (toolbox == null) {
return;
return null;
}
let panel = toolbox.getPanel("webconsole");
if (panel == null) {
return;
return null;
}
let onceMessagesCleared = panel.hud.jsterm.once("messages-cleared");
@ -86,8 +85,8 @@ exports.items = [
name: "console close",
description: l10n.lookup("consolecloseDesc"),
exec: function (args, context) {
return gDevTools.closeToolbox(context.environment.target)
.then(() => {}); // Don't return a value to GCLI
// Don't return a value to GCLI
return gDevTools.closeToolbox(context.environment.target).then(() => {});
}
},
{
@ -97,8 +96,8 @@ exports.items = [
description: l10n.lookup("consoleopenDesc"),
exec: function (args, context) {
const target = context.environment.target;
return gDevTools.showToolbox(target, "webconsole")
.then(() => {}); // Don't return a value to GCLI
// Don't return a value to GCLI
return gDevTools.showToolbox(target, "webconsole").then(() => {});
}
}
];

View File

@ -1467,13 +1467,15 @@ var StyleRuleActor = protocol.ActorClassWithSpec(styleRuleSpec, {
if (newCssRule) {
let ruleEntry = this.pageStyle.findEntryMatchingRule(node, newCssRule);
if (ruleEntry.length === 1) {
isMatching = true;
ruleProps =
this.pageStyle.getAppliedProps(node, ruleEntry,
{ matchedSelectors: true });
} else {
ruleProps = this.pageStyle.getNewAppliedProps(node, newCssRule);
}
isMatching = ruleProps.entries.some((ruleProp) =>
ruleProp.matchedSelectors.length > 0);
}
return { ruleProps, isMatching };

View File

@ -1479,6 +1479,8 @@ WorkerClient.prototype = {
aOnResponse(connectReponse, this.thread);
return [connectResponse, this.thread];
});
}, error => {
aOnResponse(error, null);
});
},

View File

@ -77,6 +77,22 @@ rule.empty=No element selected.
# tooltip when the mouse is over a selector highlighter icon in the rule view.
rule.selectorHighlighter.tooltip=Highlight all elements matching this selector
# LOCALIZATION NOTE (rule.colorSwatch.tooltip): Text displayed in a tooltip
# when the mouse is over a color swatch in the rule view.
rule.colorSwatch.tooltip=Click to open the color picker, shift+click to change the color format
# LOCALIZATION NOTE (rule.bezierSwatch.tooltip): Text displayed in a tooltip
# when the mouse is over a cubic-bezier swatch in the rule view.
rule.bezierSwatch.tooltip=Click to open the timing-function editor
# LOCALIZATION NOTE (rule.filterSwatch.tooltip): Text displayed in a tooltip
# when the mouse is over a filter swatch in the rule view.
rule.filterSwatch.tooltip=Click to open the filter editor
# LOCALIZATION NOTE (rule.angleSwatch.tooltip): Text displayed in a tooltip
# when the mouse is over a angle swatch in the rule view.
rule.angleSwatch.tooltip=Shift+click to change the angle format
# LOCALIZATION NOTE (styleinspector.contextmenu.copyColor): Text displayed in the rule
# and computed view context menu when a color value was clicked.
styleinspector.contextmenu.copyColor=Copy Color

View File

@ -95,11 +95,23 @@ function _attachConsole(aListeners, aCallback, aAttachToTab, aAttachToWorker)
let tab = aResponse.tabs[aResponse.selected];
aState.dbgClient.attachTab(tab.actor, function (response, tabClient) {
if (aAttachToWorker) {
var worker = new Worker("console-test-worker.js");
let workerName = "console-test-worker.js#" + new Date().getTime();
var worker = new Worker(workerName);
worker.addEventListener("message", function listener() {
worker.removeEventListener("message", listener);
tabClient.listWorkers(function (response) {
tabClient.attachWorker(response.workers[0].actor, function (response, workerClient) {
let worker = response.workers.filter(w => w.url == workerName)[0];
if (!worker) {
console.error("listWorkers failed. Unable to find the " +
"worker actor\n");
return;
}
tabClient.attachWorker(worker.actor, function (response, workerClient) {
if (!workerClient || response.error) {
console.error("attachWorker failed. No worker client or " +
" error: " + response.error);
return;
}
workerClient.attachThread({}, function (aResponse) {
aState.actor = workerClient.consoleActor;
aState.dbgClient.attachConsole(workerClient.consoleActor, aListeners,

View File

@ -186,8 +186,6 @@ dependencies {
if (mozconfig.substs.MOZ_INSTALL_TRACKING) {
compile "com.google.android.gms:play-services-ads:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
compile "com.google.android.gms:play-services-analytics:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
compile "com.google.android.gms:play-services-appindexing:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
compile "com.google.android.gms:play-services-basement:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
}
@ -195,6 +193,7 @@ dependencies {
compile "com.google.android.gms:play-services-basement:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
compile "com.google.android.gms:play-services-base:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
compile "com.google.android.gms:play-services-gcm:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
compile "com.google.android.gms:play-services-measurement:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
}
// Gradle based builds include LeakCanary. Gradle based tests include the no-op version. Mach

View File

@ -84,14 +84,13 @@ ifdef MOZ_ANDROID_GCM
$(ANDROID_PLAY_SERVICES_BASE_AAR_LIB) \
$(ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB) \
$(ANDROID_PLAY_SERVICES_GCM_AAR_LIB) \
$(ANDROID_PLAY_SERVICES_MEASUREMENT_AAR_LIB) \
$(NULL)
endif
ifdef MOZ_INSTALL_TRACKING
JAVA_CLASSPATH += \
$(ANDROID_PLAY_SERVICES_ADS_AAR_LIB) \
$(ANDROID_PLAY_SERVICES_ANALYTICS_AAR_LIB) \
$(ANDROID_PLAY_SERVICES_APPINDEXING_AAR_LIB) \
$(ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB) \
$(NULL)
endif
@ -124,14 +123,13 @@ ifdef MOZ_ANDROID_GCM
$(ANDROID_PLAY_SERVICES_BASE_AAR_LIB) \
$(ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB) \
$(ANDROID_PLAY_SERVICES_GCM_AAR_LIB) \
$(ANDROID_PLAY_SERVICES_MEASUREMENT_AAR_LIB) \
$(NULL)
endif
ifdef MOZ_INSTALL_TRACKING
java_bundled_libs += \
$(ANDROID_PLAY_SERVICES_ADS_AAR_LIB) \
$(ANDROID_PLAY_SERVICES_ANALYTICS_AAR_LIB) \
$(ANDROID_PLAY_SERVICES_APPINDEXING_AAR_LIB) \
$(ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB) \
$(NULL)
endif
@ -426,11 +424,10 @@ generated/android/support/v7/mediarouter/R.java: .aapt.deps ;
generated/android/support/v7/recyclerview/R.java: .aapt.deps ;
generated/com/google/android/gms/R.java: .aapt.deps ;
generated/com/google/android/gms/ads/R.java: .aapt.deps ;
generated/com/google/android/gms/analytics/R.java: .aapt.deps ;
generated/com/google/android/gms/appindexing/R.java: .aapt.deps ;
generated/com/google/android/gms/base/R.java: .aapt.deps ;
generated/com/google/android/gms/cast/R.java: .aapt.deps ;
generated/com/google/android/gms/gcm/R.java: .aapt.deps ;
generated/com/google/android/gms/measurement/R.java: .aapt.deps ;
gecko.ap_: .aapt.deps ;
R.txt: .aapt.deps ;

View File

@ -49,6 +49,7 @@ import org.mozilla.gecko.util.PrefUtils;
import org.mozilla.gecko.util.ThreadUtils;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ContentResolver;
@ -76,6 +77,7 @@ import android.os.Process;
import android.os.StrictMode;
import android.provider.ContactsContract;
import android.provider.MediaStore.Images.Media;
import android.support.annotation.WorkerThread;
import android.support.design.widget.Snackbar;
import android.text.TextUtils;
import android.util.AttributeSet;
@ -200,6 +202,8 @@ public abstract class GeckoApp
protected boolean mLastSessionCrashed;
protected boolean mShouldRestore;
private boolean mSessionRestoreParsingFinished = false;
protected boolean mInitialized;
protected boolean mWindowFocusInitialized;
private Telemetry.Timer mJavaUiStartupTimer;
@ -1277,6 +1281,43 @@ public abstract class GeckoApp
mPrivateBrowsingSession = savedInstanceState.getString(SAVED_STATE_PRIVATE_SESSION);
}
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
// If we are doing a restore, read the session data so we can send it to Gecko later.
String restoreMessage = null;
if (!mIsRestoringActivity && mShouldRestore) {
try {
// restoreSessionTabs() will create simple tab stubs with the
// URL and title for each page, but we also need to restore
// session history. restoreSessionTabs() will inject the IDs
// of the tab stubs into the JSON data (which holds the session
// history). This JSON data is then sent to Gecko so session
// history can be restored for each tab.
final SafeIntent intent = new SafeIntent(getIntent());
restoreMessage = restoreSessionTabs(invokedWithExternalURL(getIntentURI(intent)));
} catch (SessionRestoreException e) {
// If restore failed, do a normal startup
Log.e(LOGTAG, "An error occurred during restore", e);
mShouldRestore = false;
}
}
synchronized (this) {
mSessionRestoreParsingFinished = true;
notifyAll();
}
// If we are doing a restore, send the parsed session data to Gecko.
if (!mIsRestoringActivity) {
GeckoAppShell.notifyObservers("Session:Restore", restoreMessage);
}
// Make sure sessionstore.bak is either updated or deleted as necessary.
getProfile().updateSessionFile(mShouldRestore);
}
});
// Perform background initialization.
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
@ -1453,6 +1494,22 @@ public abstract class GeckoApp
return null;
}
private String getIntentURI(SafeIntent intent) {
final String passedUri;
final String uri = getURIFromIntent(intent);
if (!TextUtils.isEmpty(uri)) {
passedUri = uri;
} else {
passedUri = null;
}
return passedUri;
}
private boolean invokedWithExternalURL(String uri) {
return uri != null && !AboutPages.isAboutHome(uri);
}
private void initialize() {
mInitialized = true;
@ -1462,16 +1519,9 @@ public abstract class GeckoApp
final SafeIntent intent = new SafeIntent(getIntent());
final String action = intent.getAction();
final String uri = getURIFromIntent(intent);
final String passedUri = getIntentURI(intent);
final String passedUri;
if (!TextUtils.isEmpty(uri)) {
passedUri = uri;
} else {
passedUri = null;
}
final boolean isExternalURL = passedUri != null && !AboutPages.isAboutHome(passedUri);
final boolean isExternalURL = invokedWithExternalURL(passedUri);
// Start migrating as early as possible, can do this in
// parallel with Gecko load.
@ -1481,26 +1531,17 @@ public abstract class GeckoApp
initializeChrome();
// If we are doing a restore, read the session data and send it to Gecko
if (!mIsRestoringActivity) {
String restoreMessage = null;
if (mShouldRestore) {
// We need to wait here because mShouldRestore can revert back to
// false if a parsing error occurs and the startup tab we load
// depends on whether we restore tabs or not.
synchronized (this) {
while (!mSessionRestoreParsingFinished) {
try {
// restoreSessionTabs() will create simple tab stubs with the
// URL and title for each page, but we also need to restore
// session history. restoreSessionTabs() will inject the IDs
// of the tab stubs into the JSON data (which holds the session
// history). This JSON data is then sent to Gecko so session
// history can be restored for each tab.
restoreMessage = restoreSessionTabs(isExternalURL);
} catch (SessionRestoreException e) {
// If restore failed, do a normal startup
Log.e(LOGTAG, "An error occurred during restore", e);
mShouldRestore = false;
wait();
} catch (final InterruptedException e) {
// Ignore and wait again.
}
}
GeckoAppShell.notifyObservers("Session:Restore", restoreMessage);
}
// External URLs should always be loaded regardless of whether Gecko is
@ -1532,9 +1573,6 @@ public abstract class GeckoApp
processTabQueue();
}
// Make sure sessionstore.bak is either updated or deleted as necessary.
getProfile().updateSessionFile(mShouldRestore);
recordStartupActionTelemetry(passedUri, action);
// Check if launched from data reporting notification.
@ -1603,6 +1641,7 @@ public abstract class GeckoApp
}
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
@Override
public void onGlobalLayout() {
if (Versions.preJB) {
@ -1642,6 +1681,7 @@ public abstract class GeckoApp
});
}
@WorkerThread
private String restoreSessionTabs(final boolean isExternalURL) throws SessionRestoreException {
try {
String sessionString = getProfile().readSessionFile(false);
@ -1657,7 +1697,7 @@ public abstract class GeckoApp
final JSONObject windowObject = new JSONObject();
SessionParser parser = new SessionParser() {
@Override
public void onTabRead(SessionTab sessionTab) {
public void onTabRead(final SessionTab sessionTab) {
JSONObject tabObject = sessionTab.getTabObject();
int flags = Tabs.LOADURL_NEW_TAB;
@ -1665,8 +1705,13 @@ public abstract class GeckoApp
flags |= (tabObject.optBoolean("desktopMode") ? Tabs.LOADURL_DESKTOP : 0);
flags |= (tabObject.optBoolean("isPrivate") ? Tabs.LOADURL_PRIVATE : 0);
Tab tab = Tabs.getInstance().loadUrl(sessionTab.getUrl(), flags);
tab.updateTitle(sessionTab.getTitle());
final Tab tab = Tabs.getInstance().loadUrl(sessionTab.getUrl(), flags);
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
tab.updateTitle(sessionTab.getTitle());
}
});
try {
tabObject.put("tabId", tab.getId());
@ -2331,6 +2376,7 @@ public abstract class GeckoApp
private void doShutdown() {
// Shut down GeckoApp activity.
runOnUiThread(new Runnable() {
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
@Override public void run() {
if (!isFinishing() && (Versions.preJBMR1 || !isDestroyed())) {
finish();

View File

@ -77,6 +77,8 @@ public final class GeckoProfile {
private static final String SESSION_FILE_BACKUP = "sessionstore.bak";
private static final long MAX_BACKUP_FILE_AGE = 1000 * 3600 * 24; // 24 hours
private boolean mOldSessionDataProcessed = false;
private static final HashMap<String, GeckoProfile> sProfileCache = new HashMap<String, GeckoProfile>();
private static String sDefaultProfileName;
@ -601,6 +603,22 @@ public final class GeckoProfile {
sessionFileBackup.delete();
}
}
synchronized (this) {
mOldSessionDataProcessed = true;
notifyAll();
}
}
public void waitForOldSessionDataProcessing() {
synchronized (this) {
while (!mOldSessionDataProcessed) {
try {
wait();
} catch (final InterruptedException e) {
// Ignore and wait again.
}
}
}
}
/**

View File

@ -329,6 +329,9 @@ public class RecentTabsPanel extends HomeFragment
}
}
// We need to ensure that the session restore code has updated sessionstore.bak as necessary.
GeckoProfile.get(context).waitForOldSessionDataProcessing();
final String jsonString = GeckoProfile.get(context).readSessionFile(true);
if (jsonString == null) {
// No previous session data

View File

@ -775,6 +775,7 @@ if CONFIG['MOZ_ANDROID_GCM']:
CONFIG['ANDROID_PLAY_SERVICES_BASE_AAR_LIB'],
CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB'],
CONFIG['ANDROID_PLAY_SERVICES_GCM_AAR_LIB'],
CONFIG['ANDROID_PLAY_SERVICES_MEASUREMENT_AAR_LIB'],
]
if CONFIG['ANDROID_PLAY_SERVICES_BASE_AAR']:
@ -792,11 +793,14 @@ if CONFIG['MOZ_ANDROID_GCM']:
ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_GCM_AAR_RES']]
resjar.generated_sources += ['com/google/android/gms/gcm/R.java']
if CONFIG['ANDROID_PLAY_SERVICES_MEASUREMENT_AAR']:
ANDROID_EXTRA_PACKAGES += ['com.google.android.gms.measurement']
ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_MEASUREMENT_AAR_RES']]
resjar.generated_sources += ['com/google/android/gms/measurement/R.java']
if CONFIG['MOZ_INSTALL_TRACKING']:
gbjar.extra_jars += [
CONFIG['ANDROID_PLAY_SERVICES_ADS_AAR_LIB'],
CONFIG['ANDROID_PLAY_SERVICES_ANALYTICS_AAR_LIB'],
CONFIG['ANDROID_PLAY_SERVICES_APPINDEXING_AAR_LIB'],
CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB'],
]
@ -805,17 +809,6 @@ if CONFIG['MOZ_INSTALL_TRACKING']:
ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_ADS_AAR_RES']]
resjar.generated_sources += ['com/google/android/gms/ads/R.java']
if CONFIG['ANDROID_PLAY_SERVICES_ANALYTICS_AAR']:
ANDROID_EXTRA_PACKAGES += ['com.google.android.gms.analytics']
ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_ANALYTICS_AAR_RES']]
resjar.generated_sources += ['com/google/android/gms/analytics/R.java']
if CONFIG['ANDROID_PLAY_SERVICES_APPINDEXING_AAR']:
ANDROID_EXTRA_PACKAGES += ['com.google.android.gms.appindexing']
ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_APPINDEXING_AAR_RES']]
resjar.generated_sources += ['com/google/android/gms/appindexing/R.java']
if CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR']:
ANDROID_EXTRA_PACKAGES += ['com.google.android.gms']
ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR_RES']]

View File

@ -4468,6 +4468,7 @@ Tab.prototype = {
// browser.contentDocument is changed to the new document we're loading
this.contentDocumentIsDisplayed = false;
this.hasTouchListener = false;
Services.obs.notifyObservers(this.browser, "Session:NotifyLocationChange", null);
} else {
setTimeout(function() {
this.sendViewportUpdate();
@ -6034,7 +6035,7 @@ var PopupBlockerObserver = {
let pageReport = BrowserApp.selectedBrowser.pageReport;
if (pageReport) {
for (let i = 0; i < pageReport.length; ++i) {
let popupURIspec = pageReport[i].popupWindowURI.spec;
let popupURIspec = pageReport[i].popupWindowURIspec;
// Sometimes the popup URI that we get back from the pageReport
// isn't useful (for instance, netscape.com's popup URI ends up

View File

@ -15,6 +15,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm
XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FormData", "resource://gre/modules/FormData.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition", "resource://gre/modules/ScrollPosition.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch", "resource://gre/modules/TelemetryStopwatch.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Log", "resource://gre/modules/AndroidLog.jsm", "AndroidLog");
XPCOMUtils.defineLazyModuleGetter(this, "SharedPreferences", "resource://gre/modules/SharedPreferences.jsm");
@ -61,6 +62,7 @@ SessionStore.prototype = {
_interval: 10000,
_maxTabsUndo: 5,
_pendingWrite: 0,
_scrollSavePending: null,
// The index where the most recently closed tab was in the tabs array
// when it was closed.
@ -110,6 +112,7 @@ SessionStore.prototype = {
observerService.addObserver(this, "domwindowclosed", true);
observerService.addObserver(this, "browser:purge-session-history", true);
observerService.addObserver(this, "Session:Restore", true);
observerService.addObserver(this, "Session:NotifyLocationChange", true);
observerService.addObserver(this, "application-background", true);
observerService.addObserver(this, "ClosedTabs:StartNotifications", true);
observerService.addObserver(this, "ClosedTabs:StopNotifications", true);
@ -186,6 +189,14 @@ SessionStore.prototype = {
}
break;
}
case "Session:NotifyLocationChange": {
let browser = aSubject;
if (browser.__SS_restoreDataOnLocationChange) {
delete browser.__SS_restoreDataOnLocationChange;
this._restoreZoom(browser.__SS_data.scrolldata, browser);
}
break;
}
case "Tabs:OpenMultiple": {
let data = JSON.parse(aData);
@ -275,16 +286,46 @@ SessionStore.prototype = {
break;
}
case "load": {
// Handle restoring the text data into the content and frames. We wait
// until the main content and all frames are loaded before trying to
// restore the text data.
let browser = aEvent.currentTarget;
// Skip subframe loads.
if (browser.contentDocument !== aEvent.originalTarget) {
return;
}
// Handle restoring the text data into the content and frames.
// We wait until the main content and all frames are loaded
// before trying to restore this data.
log("load for tab " + window.BrowserApp.getTabForBrowser(browser).id);
if (browser.__SS_restore_text_data) {
if (browser.__SS_restoreDataOnLoad) {
delete browser.__SS_restoreDataOnLoad;
this._restoreTextData(browser.__SS_data.formdata, browser);
}
break;
}
case "pageshow": {
let browser = aEvent.currentTarget;
// Skip subframe pageshows.
if (browser.contentDocument !== aEvent.originalTarget) {
return;
}
// Restoring the scroll position needs to happen after the zoom level has been
// restored, which is done by the MobileViewportManager either on first paint
// or on load, whichever comes first.
// In the latter case, our load handler runs before the MVM's one, which is the
// wrong way around, so we have to use a later event instead.
log("pageshow for tab " + window.BrowserApp.getTabForBrowser(browser).id);
if (browser.__SS_restoreDataOnPageshow) {
delete browser.__SS_restoreDataOnPageshow;
this._restoreScrollPosition(browser.__SS_data.scrolldata, browser);
} else {
// We're not restoring, capture the initial scroll position on pageshow.
this.onTabScroll(window, browser);
}
break;
}
case "change":
case "input":
case "DOMAutoComplete": {
@ -293,6 +334,22 @@ SessionStore.prototype = {
this.onTabInput(window, browser);
break;
}
case "resize":
case "scroll": {
let browser = aEvent.currentTarget;
// Duplicated logging check to avoid calling getTabForBrowser on each scroll event.
if (loggingEnabled) {
log(aEvent.type + " for tab " + window.BrowserApp.getTabForBrowser(browser).id);
}
if (!this._scrollSavePending) {
this._scrollSavePending =
window.setTimeout(() => {
this._scrollSavePending = null;
this.onTabScroll(window, browser);
}, 500);
}
break;
}
}
},
@ -369,11 +426,19 @@ SessionStore.prototype = {
// Use load to restore text data
aBrowser.addEventListener("load", this, true);
// Gecko might set the initial zoom level after the JS "load" event,
// so we have to restore zoom and scroll position after that.
aBrowser.addEventListener("pageshow", this, true);
// Use a combination of events to watch for text data changes
aBrowser.addEventListener("change", this, true);
aBrowser.addEventListener("input", this, true);
aBrowser.addEventListener("DOMAutoComplete", this, true);
// Record the current scroll position and zoom level.
aBrowser.addEventListener("scroll", this, true);
aBrowser.addEventListener("resize", this, true);
log("onTabAdd() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id +
", aNoNotification = " + aNoNotification);
if (!aNoNotification) {
@ -386,9 +451,12 @@ SessionStore.prototype = {
// Cleanup event listeners
aBrowser.removeEventListener("DOMTitleChanged", this, true);
aBrowser.removeEventListener("load", this, true);
aBrowser.removeEventListener("pageshow", this, true);
aBrowser.removeEventListener("change", this, true);
aBrowser.removeEventListener("input", this, true);
aBrowser.removeEventListener("DOMAutoComplete", this, true);
aBrowser.removeEventListener("scroll", this, true);
aBrowser.removeEventListener("resize", this, true);
let tabId = aWindow.BrowserApp.getTabForBrowser(aBrowser).id;
@ -467,17 +535,20 @@ SessionStore.prototype = {
let data = { entries: entries, index: index };
let formdata;
let scrolldata;
if (aBrowser.__SS_data) {
formdata = aBrowser.__SS_data.formdata;
scrolldata = aBrowser.__SS_data.scrolldata;
}
delete aBrowser.__SS_data;
this._collectTabData(aWindow, aBrowser, data);
if (aBrowser.__SS_restore_text_data) {
// If the tab has been freshly restored and the "load" event
// hasn't yet fired, we need to restore any form data that
// might have been present.
if (aBrowser.__SS_restoreDataOnLoad || aBrowser.__SS_restoreDataOnPageshow) {
// If the tab has been freshly restored and the "load" or "pageshow"
// events haven't yet fired, we need to preserve any form data and
// scroll positions that might have been present.
aBrowser.__SS_data.formdata = formdata;
aBrowser.__SS_data.scrolldata = scrolldata;
} else {
// When navigating via the forward/back buttons, Gecko restores
// the form data all by itself and doesn't invoke any input events.
@ -579,6 +650,95 @@ SessionStore.prototype = {
}
},
onTabScroll: function ss_onTabScroll(aWindow, aBrowser) {
// If we've been called directly, cancel any pending timeouts.
if (this._scrollSavePending) {
aWindow.clearTimeout(this._scrollSavePending);
this._scrollSavePending = null;
log("onTabScroll() clearing pending timeout");
}
// If this browser is being restored, skip any session save activity.
if (aBrowser.__SS_restore) {
return;
}
// Don't bother trying to save scroll positions if we don't have history yet.
let data = aBrowser.__SS_data;
if (!data || data.entries.length == 0) {
return;
}
// Neither bother if we're yet to restore the previous scroll position.
if (aBrowser.__SS_restoreDataOnLoad || aBrowser.__SS_restoreDataOnPageshow) {
return;
}
// Start with storing the main content.
let content = aBrowser.contentWindow;
// Store the main content.
let scrolldata = ScrollPosition.collect(content) || {};
// Loop over direct child frames, and store the scroll positions.
let children = [];
for (let i = 0; i < content.frames.length; i++) {
let frame = content.frames[i];
let result = ScrollPosition.collect(frame);
if (result && Object.keys(result).length) {
children[i] = result;
}
}
// If any frame had scroll positions, add them to the main scroll data.
if (children.length) {
scrolldata.children = children;
}
// Save the current document resolution.
let zoom = { value: 1 };
content.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(
Ci.nsIDOMWindowUtils).getResolution(zoom);
scrolldata.zoom = {};
scrolldata.zoom.resolution = zoom.value;
log("onTabScroll() zoom level: " + zoom.value);
// Save some data that'll help in adjusting the zoom level
// when restoring in a different screen orientation.
let viewportInfo = this._getViewportInfo(aWindow.outerWidth, aWindow.outerHeight, content);
scrolldata.zoom.autoSize = viewportInfo.autoSize;
log("onTabScroll() autoSize: " + scrolldata.zoom.autoSize);
scrolldata.zoom.windowWidth = aWindow.outerWidth;
log("onTabScroll() windowWidth: " + scrolldata.zoom.windowWidth);
// Save zoom and scroll data.
data.scrolldata = scrolldata;
log("onTabScroll() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id);
let evt = new Event("SSTabScrollCaptured", {"bubbles":true, "cancelable":false});
aBrowser.dispatchEvent(evt);
this.saveStateDelayed();
},
_getViewportInfo: function ss_getViewportInfo(aDisplayWidth, aDisplayHeight, aWindow) {
let viewportInfo = {};
let defaultZoom = {}, allowZoom = {}, minZoom = {}, maxZoom ={},
width = {}, height = {}, autoSize = {};
aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(
Ci.nsIDOMWindowUtils).getViewportInfo(aDisplayWidth, aDisplayHeight,
defaultZoom, allowZoom, minZoom, maxZoom, width, height, autoSize);
viewportInfo.defaultZoom = defaultZoom.value;
viewportInfo.allowZoom = allowZoom.value;
viewportInfo.minZoom = maxZoom.value;
viewportInfo.maxZoom = maxZoom.value;
viewportInfo.width = width.value;
viewportInfo.height = height.value;
viewportInfo.autoSize = autoSize.value;
return viewportInfo;
},
saveStateDelayed: function ss_saveStateDelayed() {
if (!this._saveTimer) {
// Interval until the next disk operation is allowed
@ -1120,9 +1280,17 @@ SessionStore.prototype = {
}
this._restoreHistory(aTabData, aBrowser.sessionHistory);
// Restoring the text data requires waiting for the content to load. So
// we set a flag and delay this until the "load" event.
aBrowser.__SS_restore_text_data = true;
// Various bits of state can only be restored if page loading has progressed far enough:
// The MobileViewportManager needs to be told as early as possible about
// our desired zoom level so it can take it into account during the
// initial document resolution calculation.
aBrowser.__SS_restoreDataOnLocationChange = true;
// Restoring saved form data requires the input fields to be available,
// so we have to wait for the content to load.
aBrowser.__SS_restoreDataOnLoad = true;
// Restoring the scroll position depends on the document resolution having been set,
// which is only guaranteed to have happened *after* we receive the load event.
aBrowser.__SS_restoreDataOnPageshow = true;
},
/**
@ -1167,7 +1335,51 @@ SessionStore.prototype = {
log("_restoreTextData()");
FormData.restoreTree(aBrowser.contentWindow, aFormData);
}
delete aBrowser.__SS_restore_text_data;
},
/**
* Restores the zoom level of the window. This needs to be called before
* first paint/load (whichever comes first) to take any effect.
*/
_restoreZoom: function ss_restoreZoom(aScrollData, aBrowser) {
if (aScrollData && aScrollData.zoom) {
let recalculatedZoom = this._recalculateZoom(aScrollData.zoom);
log("_restoreZoom(), resolution: " + recalculatedZoom);
let utils = aBrowser.contentWindow.QueryInterface(
Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
// Restore zoom level.
utils.setRestoreResolution(recalculatedZoom);
}
},
/**
* Recalculates the zoom level to account for a changed display width,
* e.g. because the device was rotated.
*/
_recalculateZoom: function ss_recalculateZoom(aZoomData) {
let browserWin = Services.wm.getMostRecentWindow("navigator:browser");
// Pages with "width=device-width" won't need any zoom level scaling.
if (!aZoomData.autoSize) {
let oldWidth = aZoomData.windowWidth;
let newWidth = browserWin.outerWidth;
if (oldWidth != newWidth && oldWidth > 0 && newWidth > 0) {
log("_recalculateZoom(), old resolution: " + aZoomData.resolution);
return newWidth / oldWidth * aZoomData.resolution;
}
}
return aZoomData.resolution;
},
/**
* Takes serialized scroll positions and restores them into the given browser.
*/
_restoreScrollPosition: function ss_restoreScrollPosition(aScrollData, aBrowser) {
if (aScrollData) {
log("_restoreScrollPosition()");
ScrollPosition.restoreTree(aBrowser.contentWindow, aScrollData);
}
},
getBrowserState: function ss_getBrowserState() {

View File

@ -3,13 +3,13 @@
"versions": [
"Android SDK 6.0 / API 23",
"Android tools r24.4",
"Android build tools 23.0.1",
"Android build tools 23.0.3",
"Android Support Repository (Support Library 23.0.1)",
"Google Support Repository (Google Play Services 8.1.0)"
],
"size": 510613380,
"size": 573952124,
"visibility": "internal",
"digest": "cdc5d661e5879ba9095c506090e1ed372a810ba073cf4e3e2c680b8d6a35016161d00c5f4b4859fe97a0decab816e9b882255271ed385b311304ad8cd600ac16",
"digest": "1d495d7a7386af3f27b14982e0ff7b0963fd1a63a08040b9b1db0e94c9681fa3704c195ba8be23b5f73e15101b2b767293bc8f96e0584e17867ef13b074e5038",
"algorithm": "sha512",
"filename": "android-sdk-linux.tar.xz",
"unpack": true

View File

@ -3,13 +3,13 @@
"versions": [
"Android SDK 6.0 / API 23",
"Android tools r24.4",
"Android build tools 23.0.1",
"Android build tools 23.0.3",
"Android Support Repository (Support Library 23.0.1)",
"Google Support Repository (Google Play Services 8.1.0)"
],
"size": 510613380,
"size": 573952124,
"visibility": "internal",
"digest": "cdc5d661e5879ba9095c506090e1ed372a810ba073cf4e3e2c680b8d6a35016161d00c5f4b4859fe97a0decab816e9b882255271ed385b311304ad8cd600ac16",
"digest": "1d495d7a7386af3f27b14982e0ff7b0963fd1a63a08040b9b1db0e94c9681fa3704c195ba8be23b5f73e15101b2b767293bc8f96e0584e17867ef13b074e5038",
"algorithm": "sha512",
"filename": "android-sdk-linux.tar.xz",
"unpack": true

View File

@ -12,13 +12,13 @@
"versions": [
"Android SDK 6.0 / API 23",
"Android tools r24.4",
"Android build tools 23.0.1",
"Android build tools 23.0.3",
"Android Support Repository (Support Library 23.0.1)",
"Google Support Repository (Google Play Services 8.1.0)"
],
"size": 510613380,
"size": 573952124,
"visibility": "internal",
"digest": "cdc5d661e5879ba9095c506090e1ed372a810ba073cf4e3e2c680b8d6a35016161d00c5f4b4859fe97a0decab816e9b882255271ed385b311304ad8cd600ac16",
"digest": "1d495d7a7386af3f27b14982e0ff7b0963fd1a63a08040b9b1db0e94c9681fa3704c195ba8be23b5f73e15101b2b767293bc8f96e0584e17867ef13b074e5038",
"algorithm": "sha512",
"filename": "android-sdk-linux.tar.xz",
"unpack": true

View File

@ -12,13 +12,13 @@
"versions": [
"Android SDK 6.0 / API 23",
"Android tools r24.4",
"Android build tools 23.0.1",
"Android build tools 23.0.3",
"Android Support Repository (Support Library 23.0.1)",
"Google Support Repository (Google Play Services 8.1.0)"
],
"size": 510613380,
"size": 573952124,
"visibility": "internal",
"digest": "cdc5d661e5879ba9095c506090e1ed372a810ba073cf4e3e2c680b8d6a35016161d00c5f4b4859fe97a0decab816e9b882255271ed385b311304ad8cd600ac16",
"digest": "1d495d7a7386af3f27b14982e0ff7b0963fd1a63a08040b9b1db0e94c9681fa3704c195ba8be23b5f73e15101b2b767293bc8f96e0584e17867ef13b074e5038",
"algorithm": "sha512",
"filename": "android-sdk-linux.tar.xz",
"unpack": true

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<title>Article title</title>
<meta name="description" content="This is the article description." />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<header>Site header</header>
<div>
<h1>Article title</h1>
<h2 class="author">by Jane Doe</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.</p>
<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.</p>
<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
</div>
</body>
</html>

View File

@ -2,6 +2,7 @@
skip-if = os != 'android'
support-files =
basic_article.html
basic_article_mobile.html
desktopmode_user_agent.sjs
devicesearch.xml
head.js
@ -40,6 +41,7 @@ skip-if = true # Bug 1241478
[test_select_disabled.html]
[test_selectoraddtab.html]
[test_session_form_data.html]
[test_session_scroll_position.html]
[test_session_zombification.html]
[test_shared_preferences.html]
[test_simple_discovery.html]

View File

@ -1,6 +1,10 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
function fuzzyEquals(a, b) {
return (Math.abs(a - b) < 1e-6);
}
function promiseBrowserEvent(browser, eventType) {
return new Promise((resolve) => {
function handle(event) {

View File

@ -0,0 +1,183 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=810981
-->
<head>
<meta charset="utf-8">
<title>Test for Bug 810981</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
<script type="application/javascript" src="head.js"></script>
<script type="application/javascript;version=1.7">
/** Test for Bug 810981 **/
"use strict";
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Messaging.jsm");
Cu.import("resource://gre/modules/Task.jsm");
// The chrome window.
let chromeWin;
// Track the tabs where the tests are happening.
let tabScroll;
// Use something with enough content to allow for scrolling.
const URL = "http://example.org/chrome/mobile/android/tests/browser/chrome/basic_article_mobile.html";
function dispatchUIEvent(browser, type) {
let event = browser.contentDocument.createEvent("UIEvents");
event.initUIEvent(type, true, false, browser.contentDocument.defaultView, 0);
browser.dispatchEvent(event);
}
function setScrollPosition(browser, x, y) {
browser.contentWindow.scrollTo(x, y);
dispatchUIEvent(browser, "scroll");
}
function setZoomLevel(browser, zoom) {
browser.contentWindow.QueryInterface(
Ci.nsIInterfaceRequestor).getInterface(
Ci.nsIDOMWindowUtils).setResolutionAndScaleTo(zoom);
}
let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
add_task(function* test_sessionStoreScrollPosition() {
const SCROLL_X = 0;
const SCROLL_Y = 38;
chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
let BrowserApp = chromeWin.BrowserApp;
// Creates a tab, sets a scroll position and closes the tab.
function createAndRemoveTab() {
return Task.spawn(function () {
// Create a new tab.
tabScroll = BrowserApp.addTab(URL);
let browser = tabScroll.browser;
yield promiseBrowserEvent(browser, "pageshow");
// Modify scroll position.
setScrollPosition(browser, SCROLL_X, SCROLL_Y);
yield promiseTabEvent(browser, "SSTabScrollCaptured");
// Check that we've actually scrolled.
let ifreq = browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor);
let utils = ifreq.getInterface(Ci.nsIDOMWindowUtils);
let scrollX = {}, scrollY = {};
utils.getScrollXY(false, scrollX, scrollY);
is(scrollX.value, SCROLL_X, "scrollX set correctly");
is(scrollY.value, SCROLL_Y, "scrollY set correctly");
// Remove the tab.
BrowserApp.closeTab(tabScroll);
yield promiseTabEvent(browser, "SSTabCloseProcessed");
});
}
yield createAndRemoveTab();
let state = ss.getClosedTabs(chromeWin);
let [{scrolldata}] = state;
is(scrolldata.scroll, SCROLL_X + "," + SCROLL_Y, "stored scroll position is correct");
// Restore the closed tab.
let closedTabData = ss.getClosedTabs(chromeWin)[0];
let browser = ss.undoCloseTab(chromeWin, closedTabData);
yield promiseBrowserEvent(browser, "pageshow");
// Check the scroll position.
let ifreq = browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor);
let utils = ifreq.getInterface(Ci.nsIDOMWindowUtils);
let scrollX = {}, scrollY = {};
utils.getScrollXY(false, scrollX, scrollY);
is(scrollX.value, SCROLL_X, "scrollX restored correctly");
is(scrollY.value, SCROLL_Y, "scrollY restored correctly");
// Remove the tab.
BrowserApp.closeTab(BrowserApp.getTabForBrowser(browser));
});
add_task(function* test_sessionStoreZoomLevel() {
const ZOOM = 4.2;
const SCROLL_X = 42;
const SCROLL_Y = 42;
chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
let BrowserApp = chromeWin.BrowserApp;
// Creates a tab, sets a scroll position and zoom level and closes the tab.
function createAndRemoveTab() {
return Task.spawn(function () {
// Create a new tab.
tabScroll = BrowserApp.addTab(URL);
let browser = tabScroll.browser;
yield promiseBrowserEvent(browser, "pageshow");
// Modify scroll position and zoom level.
setZoomLevel(browser, ZOOM);
setScrollPosition(browser, SCROLL_X, SCROLL_Y);
yield promiseTabEvent(browser, "SSTabScrollCaptured");
// Check that we've actually scrolled and zoomed.
let ifreq = browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor);
let utils = ifreq.getInterface(Ci.nsIDOMWindowUtils);
let scrollX = {}, scrollY = {}, zoom = {};
utils.getResolution(zoom);
utils.getScrollXY(false, scrollX, scrollY);
ok(fuzzyEquals(zoom.value, ZOOM), "zoom set correctly");
is(scrollX.value, SCROLL_X, "scrollX set correctly");
is(scrollY.value, SCROLL_Y, "scrollY set correctly");
// Remove the tab.
BrowserApp.closeTab(tabScroll);
yield promiseTabEvent(browser, "SSTabCloseProcessed");
});
}
yield createAndRemoveTab();
let state = ss.getClosedTabs(chromeWin);
let [{scrolldata}] = state;
is(scrolldata.scroll, SCROLL_X + "," + SCROLL_Y, "stored scroll position is correct");
ok(fuzzyEquals(scrolldata.zoom.resolution, ZOOM), "stored zoom level is correct");
// Restore the closed tab.
let closedTabData = ss.getClosedTabs(chromeWin)[0];
let browser = ss.undoCloseTab(chromeWin, closedTabData);
yield promiseBrowserEvent(browser, "pageshow");
// Check the scroll position and zoom level.
let ifreq = browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor);
let utils = ifreq.getInterface(Ci.nsIDOMWindowUtils);
let scrollX = {}, scrollY = {}, zoom = {};
utils.getResolution(zoom);
utils.getScrollXY(false, scrollX, scrollY);
ok(fuzzyEquals(zoom.value, ZOOM), "zoom restored correctly");
is(scrollX.value, SCROLL_X, "scrollX restored correctly");
is(scrollY.value, SCROLL_Y, "scrollY restored correctly");
// Remove the tab.
BrowserApp.closeTab(BrowserApp.getTabForBrowser(browser));
});
</script>
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=810981">Mozilla Bug 810981</a>
<p id="display"></p>
<div id="content" style="display: none">
</div>
<pre id="test">
</pre>
</body>
</html>

View File

@ -129,7 +129,9 @@
/* Only used on desktop */
.dropdown-popup > hr,
.dropdown-arrow,
#font-type-buttons > button > .name {
#font-type-buttons > button > .name,
#content-width-buttons,
#line-height-buttons {
display: none;
}

View File

@ -2892,7 +2892,7 @@ dnl ========================================================
if test -z "$gonkdir" ; then
case "$MOZ_BUILD_APP" in
mobile/android)
MOZ_ANDROID_SDK(23, 23.0.1)
MOZ_ANDROID_SDK(23, 23.0.3)
;;
esac
fi

View File

@ -15,7 +15,7 @@ import sys
# mobile/android, respectively. Try to keep these in synch with the
# build system and Mozilla's automation.
ANDROID_TARGET_SDK = '23'
ANDROID_BUILD_TOOLS_VERSION = '23.0.1'
ANDROID_BUILD_TOOLS_VERSION = '23.0.3'
# These are the "Android packages" needed for building Firefox for Android.
# Use |android list sdk --extended| to see these identifiers.

View File

@ -27,7 +27,7 @@ task:
tar xvfz eslint.tar.gz &&
rm eslint.tar.gz &&
cd ../.. &&
testing/eslint/node_modules/.bin/eslint --plugin html --ext [.js,.jsm,.jsx,.xml,.html] -f tools/lint/eslint-formatter .
testing/eslint/node_modules/.bin/eslint --quiet --plugin html --ext [.js,.jsm,.jsx,.xml,.html] -f tools/lint/eslint-formatter .
extra:
locations:

View File

@ -116,6 +116,59 @@ var multipleResponsesCompletionSet = [
}
];
function buildCompletionRequest(aCompletionSet) {
let prefixes = [];
let prefixSet = new Set();
aCompletionSet.forEach(s => {
let prefix = s.hash.substring(0, 4);
if (prefixSet.has(prefix)) {
return;
}
prefixSet.add(prefix);
prefixes.push(prefix);
});
return 4 + ":" + (4 * prefixes.length) + "\n" + prefixes.join("");
}
function parseCompletionRequest(aRequest) {
// Format: [partial_length]:[num_of_prefix * partial_length]\n[prefixes_data]
let tokens = /(\d):(\d+)/.exec(aRequest);
if (tokens.length < 3) {
dump("Request format error.");
return null;
}
let partialLength = parseInt(tokens[1]);
let payloadLength = parseInt(tokens[2]);
let payloadStart = tokens[1].length + // partial length
1 + // ':'
tokens[2].length + // payload length
1; // '\n'
let prefixSet = [];
for (let i = payloadStart; i < aRequest.length; i += partialLength) {
let prefix = aRequest.substr(i, partialLength);
if (prefix.length !== partialLength) {
dump("Header info not correct: " + aRequest.substr(0, payloadStart));
return null;
}
prefixSet.push(prefix);
}
prefixSet.sort();
return prefixSet;
}
// Compare the requests in string format.
function compareCompletionRequest(aRequest1, aRequest2) {
let prefixSet1 = parseCompletionRequest(aRequest1);
let prefixSet2 = parseCompletionRequest(aRequest2);
return equal(JSON.stringify(prefixSet1), JSON.stringify(prefixSet2));
}
// The fifth completion set is added at runtime by getRandomCompletionSet.
// Each completion in the set only has one response and its purpose is to
// provide an easy way to test the HashCompleter handling an arbitrarily large
@ -256,6 +309,10 @@ function hashCompleterServer(aRequest, aResponse) {
let len = stream.available();
let data = wrapperStream.readBytes(len);
// Check if we got the expected completion request.
let expectedRequest = buildCompletionRequest(completionSets[currentCompletionSet]);
compareCompletionRequest(data, expectedRequest);
// To avoid a response with duplicate hash completions, we keep track of all
// completed hash prefixes so far.
let completedHashes = [];

View File

@ -263,6 +263,7 @@ var PopupBlocking = {
addEventListener("pagehide", this, true);
addMessageListener("PopupBlocking:UnblockPopup", this);
addMessageListener("PopupBlocking:GetBlockedPopupList", this);
},
receiveMessage: function(msg) {
@ -277,11 +278,36 @@ var PopupBlocking = {
// If we have a requesting window and the requesting document is
// still the current document, open the popup.
if (dwi && dwi.document == internals.requestingDocument) {
dwi.open(data.popupWindowURI, data.popupWindowName, data.popupWindowFeatures);
dwi.open(data.popupWindowURIspec, data.popupWindowName, data.popupWindowFeatures);
}
}
break;
}
case "PopupBlocking:GetBlockedPopupList": {
let popupData = [];
let length = this.popupData ? this.popupData.length : 0;
// Limit 15 popup URLs to be reported through the UI
length = Math.min(length, 15);
for (let i = 0; i < length; i++) {
let popupWindowURIspec = this.popupData[i].popupWindowURIspec;
if (popupWindowURIspec == global.content.location.href) {
popupWindowURIspec = "<self>";
} else {
// Limit 500 chars to be sent because the URI will be cropped
// by the UI anyway, and data: URIs can be significantly larger.
popupWindowURIspec = popupWindowURIspec.substring(0, 500)
}
popupData.push({popupWindowURIspec});
}
sendAsyncMessage("PopupBlocking:ReplyGetBlockedPopupList", {popupData});
break;
}
}
},
@ -304,7 +330,7 @@ var PopupBlocking = {
}
let obj = {
popupWindowURI: ev.popupWindowURI ? ev.popupWindowURI.spec : "about:blank",
popupWindowURIspec: ev.popupWindowURI ? ev.popupWindowURI.spec : "about:blank",
popupWindowFeatures: ev.popupWindowFeatures,
popupWindowName: ev.popupWindowName
};
@ -351,7 +377,10 @@ var PopupBlocking = {
updateBlockedPopups: function(freshPopup) {
sendAsyncMessage("PopupBlocking:UpdateBlockedPopups",
{blockedPopups: this.popupData, freshPopup: freshPopup});
{
count: this.popupData ? this.popupData.length : 0,
freshPopup
});
},
};
PopupBlocking.init();

View File

@ -674,6 +674,24 @@
</body>
</method>
<method name="retrieveListOfBlockedPopups">
<body>
<![CDATA[
this.messageManager.sendAsyncMessage("PopupBlocking:GetBlockedPopupList", null);
return new Promise(resolve => {
let self = this;
this.messageManager.addMessageListener("PopupBlocking:ReplyGetBlockedPopupList",
function replyReceived(msg) {
self.messageManager.removeMessageListener("PopupBlocking:ReplyGetBlockedPopupList",
replyReceived);
resolve(msg.data.popupData);
}
);
});
]]>
</body>
</method>
<method name="unblockPopup">
<parameter name="aPopupIndex"/>
<body><![CDATA[
@ -986,17 +1004,11 @@
let data = aMessage.data;
switch (aMessage.name) {
case "PopupBlocking:UpdateBlockedPopups": {
this.blockedPopups = data.blockedPopups;
if (this.blockedPopups) {
for (let i = 0; i < this.blockedPopups.length; i++) {
let scope = Components.utils.import("resource://gre/modules/BrowserUtils.jsm", {});
let uri = scope.BrowserUtils.makeURI(this.blockedPopups[i].popupWindowURI);
this.blockedPopups[i].popupWindowURI = uri;
}
if (data.freshPopup) {
this.blockedPopups.reported = false;
}
}
this.blockedPopups = {
length: data.count,
reported: !data.freshPopup,
};
this.updateBlockedPopups();
break;
}