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

This commit is contained in:
Ryan VanderMeulen 2015-03-24 11:55:30 -04:00
commit 9172674269
74 changed files with 2382 additions and 1144 deletions

View File

@ -1298,7 +1298,7 @@ pref("services.sync.prefs.sync.extensions.update.enabled", true);
pref("services.sync.prefs.sync.intl.accept_languages", true);
pref("services.sync.prefs.sync.javascript.enabled", true);
pref("services.sync.prefs.sync.layout.spellcheckDefault", true);
pref("services.sync.prefs.sync.lightweightThemes.isThemeSelected", true);
pref("services.sync.prefs.sync.lightweightThemes.selectedThemeID", true);
pref("services.sync.prefs.sync.lightweightThemes.usedThemes", true);
pref("services.sync.prefs.sync.network.cookie.cookieBehavior", true);
pref("services.sync.prefs.sync.network.cookie.lifetimePolicy", true);
@ -1884,3 +1884,4 @@ pref("browser.readinglist.enabled", true);
pref("browser.readinglist.sidebarEverOpened", false);
// Enable the readinglist engine by default.
pref("readinglist.scheduler.enabled", true);
pref("readinglist.server", "https://readinglist.services.mozilla.com/v1");

View File

@ -9,7 +9,7 @@
let DevEdition = {
_prefName: "browser.devedition.theme.enabled",
_themePrefName: "general.skins.selectedSkin",
_lwThemePrefName: "lightweightThemes.isThemeSelected",
_lwThemePrefName: "lightweightThemes.selectedThemeID",
_devtoolsThemePrefName: "devtools.theme",
styleSheetLocation: "chrome://browser/skin/devedition.css",
@ -76,7 +76,7 @@ let DevEdition = {
_updateStyleSheetFromPrefs: function() {
let lightweightThemeSelected = false;
try {
lightweightThemeSelected = Services.prefs.getBoolPref(this._lwThemePrefName);
lightweightThemeSelected = !!Services.prefs.getCharPref(this._lwThemePrefName);
} catch(e) {}
let defaultThemeSelected = false;

View File

@ -457,7 +457,7 @@ let gSyncUI = {
let lastSync, threshold, prolonged;
try {
lastSync = new Date(Services.prefs.getCharPref("readinglist.scheduler.lastSync"));
threshold = new Date(Date.now() - Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout"));
threshold = new Date(Date.now() - Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") * 1000);
prolonged = lastSync <= threshold;
} catch (ex) {
// no pref, assume not prolonged.

View File

@ -6,14 +6,18 @@
*/
const PREF_DEVEDITION_THEME = "browser.devedition.theme.enabled";
const PREF_LWTHEME = "lightweightThemes.isThemeSelected";
const PREF_LWTHEME = "lightweightThemes.selectedThemeID";
const PREF_LWTHEME_USED_THEMES = "lightweightThemes.usedThemes";
const PREF_DEVTOOLS_THEME = "devtools.theme";
const {LightweightThemeManager} = Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", {});
registerCleanupFunction(() => {
// Set preferences back to their original values
LightweightThemeManager.currentTheme = null;
Services.prefs.clearUserPref(PREF_DEVEDITION_THEME);
Services.prefs.clearUserPref(PREF_LWTHEME);
Services.prefs.clearUserPref(PREF_DEVTOOLS_THEME);
Services.prefs.clearUserPref(PREF_LWTHEME_USED_THEMES);
});
add_task(function* startTests() {
@ -28,12 +32,12 @@ add_task(function* startTests() {
ok (DevEdition.styleSheet, "There is a devedition stylesheet when no themes are applied and pref is set.");
info ("Adding a lightweight theme.");
Services.prefs.setBoolPref(PREF_LWTHEME, true);
LightweightThemeManager.currentTheme = dummyLightweightTheme("preview0");
ok (!DevEdition.styleSheet, "The devedition stylesheet has been removed when a lightweight theme is applied.");
info ("Removing a lightweight theme.");
let onAttributeAdded = waitForBrightTitlebarAttribute();
Services.prefs.setBoolPref(PREF_LWTHEME, false);
LightweightThemeManager.currentTheme = null;
ok (DevEdition.styleSheet, "The devedition stylesheet has been added when a lightweight theme is removed.");
yield onAttributeAdded;
@ -85,16 +89,14 @@ function dummyLightweightTheme(id) {
return {
id: id,
name: id,
headerURL: "http://lwttest.invalid/a.png",
footerURL: "http://lwttest.invalid/b.png",
headerURL: "resource:///chrome/browser/content/browser/defaultthemes/1.header.jpg",
iconURL: "resource:///chrome/browser/content/browser/defaultthemes/1.icon.jpg",
textcolor: "red",
accentcolor: "blue"
};
}
add_task(function* testLightweightThemePreview() {
let {LightweightThemeManager} = Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", {});
info ("Turning the pref on, then previewing lightweight themes");
Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, true);
ok (DevEdition.styleSheet, "The devedition stylesheet is enabled.");

View File

@ -49,7 +49,7 @@ add_task(function () {
let defaultTheme = header.nextSibling;
defaultTheme.doCommand();
is(Services.prefs.getBoolPref("lightweightThemes.isThemeSelected"), false, "No lwtheme should be selected");
is(Services.prefs.prefHasUserValue("lightweightThemes.selectedThemeID"), false, "No lwtheme should be selected");
});
add_task(function asyncCleanup() {

View File

@ -639,6 +639,13 @@ function injectLoopAPI(targetWindow) {
}
},
SHARING_STATE_CHANGE: {
enumerable: true,
get: function() {
return Cu.cloneInto(SHARING_STATE_CHANGE, targetWindow);
}
},
fxAEnabled: {
enumerable: true,
get: function() {

View File

@ -29,6 +29,19 @@ const TWO_WAY_MEDIA_CONN_LENGTH = {
MORE_THAN_5M: "MORE_THAN_5M",
};
/**
* Buckets that we segment sharing state change telemetry probes into.
*
* @type {{WINDOW_ENABLED: String, WINDOW_DISABLED: String,
* BROWSER_ENABLED: String, BROWSER_DISABLED: String}}
*/
const SHARING_STATE_CHANGE = {
WINDOW_ENABLED: "WINDOW_ENABLED",
WINDOW_DISABLED: "WINDOW_DISABLED",
BROWSER_ENABLED: "BROWSER_ENABLED",
BROWSER_DISABLED: "BROWSER_DISABLED"
};
// See LOG_LEVELS in Console.jsm. Common examples: "All", "Info", "Warn", & "Error".
const PREF_LOG_LEVEL = "loop.debug.loglevel";
@ -42,7 +55,8 @@ Cu.import("resource://gre/modules/FxAccountsOAuthClient.jsm");
Cu.importGlobalProperties(["URL"]);
this.EXPORTED_SYMBOLS = ["MozLoopService", "LOOP_SESSION_TYPE", "TWO_WAY_MEDIA_CONN_LENGTH"];
this.EXPORTED_SYMBOLS = ["MozLoopService", "LOOP_SESSION_TYPE",
"TWO_WAY_MEDIA_CONN_LENGTH", "SHARING_STATE_CHANGE"];
XPCOMUtils.defineLazyModuleGetter(this, "injectLoopAPI",
"resource:///modules/loop/MozLoopAPI.jsm");

View File

@ -361,7 +361,9 @@ loop.shared.actions = (function() {
* XXX: should move to some roomActions module - refs bug 1079284
*/
RoomFailure: Action.define("roomFailure", {
error: Object
error: Object,
// True when the failures occurs in the join room request to the loop-server.
failedJoinRequest: Boolean
}),
/**

View File

@ -105,7 +105,7 @@ loop.store.ActiveRoomStore = (function() {
});
this._leaveRoom(actionData.error.errno === REST_ERRNOS.ROOM_FULL ?
ROOM_STATES.FULL : ROOM_STATES.FAILED);
ROOM_STATES.FULL : ROOM_STATES.FAILED, actionData.failedJoinRequest);
},
/**
@ -161,7 +161,10 @@ loop.store.ActiveRoomStore = (function() {
this._mozLoop.rooms.get(actionData.roomToken,
function(error, roomData) {
if (error) {
this.dispatchAction(new sharedActions.RoomFailure({error: error}));
this.dispatchAction(new sharedActions.RoomFailure({
error: error,
failedJoinRequest: false
}));
return;
}
@ -293,7 +296,15 @@ loop.store.ActiveRoomStore = (function() {
this._mozLoop.rooms.join(this._storeState.roomToken,
function(error, responseData) {
if (error) {
this.dispatchAction(new sharedActions.RoomFailure({error: error}));
this.dispatchAction(new sharedActions.RoomFailure({
error: error,
// This is an explicit flag to avoid the leave happening if join
// fails. We can't track it on ROOM_STATES.JOINING as the user
// might choose to leave the room whilst the XHR is in progress
// which would then mean we'd run the race condition of not
// notifying the server of a leave.
failedJoinRequest: true
}));
return;
}
@ -555,7 +566,10 @@ loop.store.ActiveRoomStore = (function() {
this._storeState.sessionToken,
function(error, responseData) {
if (error) {
this.dispatchAction(new sharedActions.RoomFailure({error: error}));
this.dispatchAction(new sharedActions.RoomFailure({
error: error,
failedJoinRequest: false
}));
return;
}
@ -568,8 +582,11 @@ loop.store.ActiveRoomStore = (function() {
* signals to the server the leave of the room.
*
* @param {ROOM_STATES} nextState The next state to switch to.
* @param {Boolean} failedJoinRequest Optional. Set to true if the join
* request to loop-server failed. It
* will skip the leave message.
*/
_leaveRoom: function(nextState) {
_leaveRoom: function(nextState, failedJoinRequest) {
if (loop.standaloneMedia) {
loop.standaloneMedia.multiplexGum.reset();
}
@ -592,10 +609,11 @@ loop.store.ActiveRoomStore = (function() {
delete this._timeout;
}
if (this._storeState.roomState === ROOM_STATES.JOINING ||
if (!failedJoinRequest &&
(this._storeState.roomState === ROOM_STATES.JOINING ||
this._storeState.roomState === ROOM_STATES.JOINED ||
this._storeState.roomState === ROOM_STATES.SESSION_CONNECTED ||
this._storeState.roomState === ROOM_STATES.HAS_PARTICIPANTS) {
this._storeState.roomState === ROOM_STATES.HAS_PARTICIPANTS)) {
this._mozLoop.rooms.leave(this._storeState.roomToken,
this._storeState.sessionToken);
}

View File

@ -165,6 +165,8 @@ loop.OTSdkDriver = (function() {
config);
this.screenshare.on("accessAllowed", this._onScreenShareGranted.bind(this));
this.screenshare.on("accessDenied", this._onScreenShareDenied.bind(this));
this._noteSharingState(options.videoSource, true);
},
/**
@ -196,6 +198,7 @@ loop.OTSdkDriver = (function() {
this.screenshare.off("accessAllowed accessDenied");
this.screenshare.destroy();
delete this.screenshare;
this._noteSharingState(this._windowId ? "browser" : "window", false);
delete this._windowId;
return true;
},
@ -648,15 +651,15 @@ loop.OTSdkDriver = (function() {
* @private
*/
_noteConnectionLength: function(callLengthSeconds) {
var buckets = this.mozLoop.TWO_WAY_MEDIA_CONN_LENGTH;
var bucket = this.mozLoop.TWO_WAY_MEDIA_CONN_LENGTH.SHORTER_THAN_10S;
var bucket = buckets.SHORTER_THAN_10S;
if (callLengthSeconds >= 10 && callLengthSeconds <= 30) {
bucket = this.mozLoop.TWO_WAY_MEDIA_CONN_LENGTH.BETWEEN_10S_AND_30S;
bucket = buckets.BETWEEN_10S_AND_30S;
} else if (callLengthSeconds > 30 && callLengthSeconds <= 300) {
bucket = this.mozLoop.TWO_WAY_MEDIA_CONN_LENGTH.BETWEEN_30S_AND_5M;
bucket = buckets.BETWEEN_30S_AND_5M;
} else if (callLengthSeconds > 300) {
bucket = this.mozLoop.TWO_WAY_MEDIA_CONN_LENGTH.MORE_THAN_5M;
bucket = buckets.MORE_THAN_5M;
}
this.mozLoop.telemetryAddKeyedValue("LOOP_TWO_WAY_MEDIA_CONN_LENGTH",
@ -705,7 +708,32 @@ loop.OTSdkDriver = (function() {
* If set to true, make it easy to test/verify 2-way media connection
* telemetry code operation by viewing the logs.
*/
_debugTwoWayMediaTelemetry: false
_debugTwoWayMediaTelemetry: false,
/**
* Note the sharing state. If this.mozLoop is not defined, we're assumed to
* be running in the standalone client and return immediately.
*
* @param {String} type Type of sharing that was flipped. May be 'window'
* or 'tab'.
* @param {Boolean} enabled Flag that tells us if the feature was flipped on
* or off.
* @private
*/
_noteSharingState: function(type, enabled) {
if (!this.mozLoop) {
return;
}
var bucket = this.mozLoop.SHARING_STATE_CHANGE[type.toUpperCase() + "_" +
(enabled ? "ENABLED" : "DISABLED")];
if (!bucket) {
console.error("No sharing state bucket found for '" + type + "'");
return;
}
this.mozLoop.telemetryAddKeyedValue("LOOP_SHARING_STATE_CHANGE", bucket);
}
};
return OTSdkDriver;

View File

@ -47,3 +47,29 @@ add_task(function* test_mozLoop_telemetryAdd_buckets() {
is(snapshot["BETWEEN_30S_AND_5M"].sum, 3, "TWO_WAY_MEDIA_CONN_LENGTH.BETWEEN_30S_AND_5M");
is(snapshot["MORE_THAN_5M"].sum, 4, "TWO_WAY_MEDIA_CONN_LENGTH.MORE_THAN_5M");
});
add_task(function* test_mozLoop_telemetryAdd_sharing_buckets() {
let histogramId = "LOOP_SHARING_STATE_CHANGE";
let histogram = Services.telemetry.getKeyedHistogramById(histogramId);
const SHARING_STATES = gMozLoopAPI.SHARING_STATE_CHANGE;
histogram.clear();
for (let value of [SHARING_STATES.WINDOW_ENABLED,
SHARING_STATES.WINDOW_DISABLED,
SHARING_STATES.WINDOW_DISABLED,
SHARING_STATES.BROWSER_ENABLED,
SHARING_STATES.BROWSER_ENABLED,
SHARING_STATES.BROWSER_ENABLED,
SHARING_STATES.BROWSER_DISABLED,
SHARING_STATES.BROWSER_DISABLED,
SHARING_STATES.BROWSER_DISABLED,
SHARING_STATES.BROWSER_DISABLED]) {
gMozLoopAPI.telemetryAddKeyedValue(histogramId, value);
}
let snapshot = histogram.snapshot();
Assert.strictEqual(snapshot["WINDOW_ENABLED"].sum, 1, "SHARING_STATE_CHANGE.WINDOW_ENABLED");
Assert.strictEqual(snapshot["WINDOW_DISABLED"].sum, 2, "SHARING_STATE_CHANGE.WINDOW_DISABLED");
Assert.strictEqual(snapshot["BROWSER_ENABLED"].sum, 3, "SHARING_STATE_CHANGE.BROWSER_ENABLED");
Assert.strictEqual(snapshot["BROWSER_DISABLED"].sum, 4, "SHARING_STATE_CHANGE.BROWSER_DISABLED");
});

View File

@ -95,7 +95,10 @@ describe("loop.store.ActiveRoomStore", function () {
});
it("should log the error", function() {
store.roomFailure({error: fakeError});
store.roomFailure(new sharedActions.RoomFailure({
error: fakeError,
failedJoinRequest: false
}));
sinon.assert.calledOnce(console.error);
sinon.assert.calledWith(console.error,
@ -105,13 +108,19 @@ describe("loop.store.ActiveRoomStore", function () {
it("should set the state to `FULL` on server error room full", function() {
fakeError.errno = REST_ERRNOS.ROOM_FULL;
store.roomFailure({error: fakeError});
store.roomFailure(new sharedActions.RoomFailure({
error: fakeError,
failedJoinRequest: false
}));
expect(store._storeState.roomState).eql(ROOM_STATES.FULL);
});
it("should set the state to `FAILED` on generic error", function() {
store.roomFailure({error: fakeError});
store.roomFailure(new sharedActions.RoomFailure({
error: fakeError,
failedJoinRequest: false
}));
expect(store._storeState.roomState).eql(ROOM_STATES.FAILED);
expect(store._storeState.failureReason).eql(FAILURE_DETAILS.UNKNOWN);
@ -121,7 +130,10 @@ describe("loop.store.ActiveRoomStore", function () {
"invalid token", function() {
fakeError.errno = REST_ERRNOS.INVALID_TOKEN;
store.roomFailure({error: fakeError});
store.roomFailure(new sharedActions.RoomFailure({
error: fakeError,
failedJoinRequest: false
}));
expect(store._storeState.roomState).eql(ROOM_STATES.FAILED);
expect(store._storeState.failureReason).eql(FAILURE_DETAILS.EXPIRED_OR_INVALID);
@ -131,14 +143,20 @@ describe("loop.store.ActiveRoomStore", function () {
"expired", function() {
fakeError.errno = REST_ERRNOS.EXPIRED;
store.roomFailure({error: fakeError});
store.roomFailure(new sharedActions.RoomFailure({
error: fakeError,
failedJoinRequest: false
}));
expect(store._storeState.roomState).eql(ROOM_STATES.FAILED);
expect(store._storeState.failureReason).eql(FAILURE_DETAILS.EXPIRED_OR_INVALID);
});
it("should reset the multiplexGum", function() {
store.roomFailure({error: fakeError});
store.roomFailure(new sharedActions.RoomFailure({
error: fakeError,
failedJoinRequest: false
}));
sinon.assert.calledOnce(fakeMultiplexGum.reset);
});
@ -146,14 +164,20 @@ describe("loop.store.ActiveRoomStore", function () {
it("should set screen sharing inactive", function() {
store.setStoreState({windowId: "1234"});
store.roomFailure({error: fakeError});
store.roomFailure(new sharedActions.RoomFailure({
error: fakeError,
failedJoinRequest: false
}));
sinon.assert.calledOnce(fakeMozLoop.setScreenShareState);
sinon.assert.calledWithExactly(fakeMozLoop.setScreenShareState, "1234", false);
});
it("should disconnect from the servers via the sdk", function() {
store.roomFailure({error: fakeError});
store.roomFailure(new sharedActions.RoomFailure({
error: fakeError,
failedJoinRequest: false
}));
sinon.assert.calledOnce(fakeSdkDriver.disconnectSession);
});
@ -162,7 +186,10 @@ describe("loop.store.ActiveRoomStore", function () {
sandbox.stub(window, "clearTimeout");
store._timeout = {};
store.roomFailure({error: fakeError});
store.roomFailure(new sharedActions.RoomFailure({
error: fakeError,
failedJoinRequest: false
}));
sinon.assert.calledOnce(clearTimeout);
});
@ -174,18 +201,33 @@ describe("loop.store.ActiveRoomStore", function () {
}));
// Now simulate room failure.
store.roomFailure({error: fakeError});
store.roomFailure(new sharedActions.RoomFailure({
error: fakeError,
failedJoinRequest: false
}));
sinon.assert.calledOnce(fakeMozLoop.removeBrowserSharingListener);
});
it("should call mozLoop.rooms.leave", function() {
store.roomFailure({error: fakeError});
store.roomFailure(new sharedActions.RoomFailure({
error: fakeError,
failedJoinRequest: false
}));
sinon.assert.calledOnce(fakeMozLoop.rooms.leave);
sinon.assert.calledWithExactly(fakeMozLoop.rooms.leave,
"fakeToken", "1627384950");
});
it("should not call mozLoop.rooms.leave if failedJoinRequest is true", function() {
store.roomFailure(new sharedActions.RoomFailure({
error: fakeError,
failedJoinRequest: true
}));
sinon.assert.notCalled(fakeMozLoop.rooms.leave);
});
});
describe("#setupWindowData", function() {
@ -271,7 +313,8 @@ describe("loop.store.ActiveRoomStore", function () {
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.RoomFailure({
error: fakeError
error: fakeError,
failedJoinRequest: false
}));
});
});
@ -449,7 +492,10 @@ describe("loop.store.ActiveRoomStore", function () {
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWith(dispatcher.dispatch,
new sharedActions.RoomFailure({error: fakeError}));
new sharedActions.RoomFailure({
error: fakeError,
failedJoinRequest: true
}));
});
});
@ -582,7 +628,8 @@ describe("loop.store.ActiveRoomStore", function () {
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWith(dispatcher.dispatch,
new sharedActions.RoomFailure({
error: fakeError
error: fakeError,
failedJoinRequest: false
}));
});
});

View File

@ -71,6 +71,12 @@ describe("loop.OTSdkDriver", function () {
BETWEEN_10S_AND_30S: "BETWEEN_10S_AND_30S",
BETWEEN_30S_AND_5M: "BETWEEN_30S_AND_5M",
MORE_THAN_5M: "MORE_THAN_5M"
},
SHARING_STATE_CHANGE: {
WINDOW_ENABLED: "WINDOW_ENABLED",
WINDOW_DISABLED: "WINDOW_DISABLED",
BROWSER_ENABLED: "BROWSER_ENABLED",
BROWSER_DISABLED: "BROWSER_DISABLED"
}
};
@ -189,6 +195,7 @@ describe("loop.OTSdkDriver", function () {
beforeEach(function() {
sandbox.stub(dispatcher, "dispatch");
sandbox.stub(driver, "_noteSharingState");
fakeElement = {
className: "fakeVideo"
@ -214,6 +221,19 @@ describe("loop.OTSdkDriver", function () {
sinon.assert.calledOnce(sdk.initPublisher);
sinon.assert.calledWithMatch(sdk.initPublisher, fakeElement, options);
});
it("should log a telemetry action", function() {
var options = {
videoSource: "browser",
constraints: {
browserWindow: 42,
scrollWithPage: true
}
};
driver.startScreenShare(options);
sinon.assert.calledWithExactly(driver._noteSharingState, "browser", true);
});
});
describe("#switchAcquiredWindow", function() {
@ -251,26 +271,70 @@ describe("loop.OTSdkDriver", function () {
beforeEach(function() {
driver.getScreenShareElementFunc = function() {};
driver.startScreenShare({
videoSource: "window"
});
sandbox.stub(dispatcher, "dispatch");
driver.session = session;
sandbox.stub(driver, "_noteSharingState");
});
it("should unpublish the share", function() {
driver.startScreenShare({
videoSource: "window"
});
driver.session = session;
driver.endScreenShare(new sharedActions.EndScreenShare());
sinon.assert.calledOnce(session.unpublish);
});
it("should log a telemetry action", function() {
driver.startScreenShare({
videoSource: "window"
});
driver.session = session;
driver.endScreenShare(new sharedActions.EndScreenShare());
sinon.assert.calledWithExactly(driver._noteSharingState, "window", false);
});
it("should destroy the share", function() {
driver.startScreenShare({
videoSource: "window"
});
driver.session = session;
expect(driver.endScreenShare()).to.equal(true);
sinon.assert.calledOnce(publisher.destroy);
});
it("should unpublish the share too when type is 'browser'", function() {
driver.startScreenShare({
videoSource: "browser",
constraints: {
browserWindow: 42
}
});
driver.session = session;
driver.endScreenShare(new sharedActions.EndScreenShare());
sinon.assert.calledOnce(session.unpublish);
});
it("should log a telemetry action too when type is 'browser'", function() {
driver.startScreenShare({
videoSource: "browser",
constraints: {
browserWindow: 42
}
});
driver.session = session;
driver.endScreenShare(new sharedActions.EndScreenShare());
sinon.assert.calledWithExactly(driver._noteSharingState, "browser", false);
});
});
describe("#connectSession", function() {
@ -431,6 +495,44 @@ describe("loop.OTSdkDriver", function () {
});
});
describe("#_noteSharingState", function() {
it("should record enabled sharing states for window", function() {
driver._noteSharingState("window", true);
sinon.assert.calledOnce(mozLoop.telemetryAddKeyedValue);
sinon.assert.calledWithExactly(mozLoop.telemetryAddKeyedValue,
"LOOP_SHARING_STATE_CHANGE",
mozLoop.SHARING_STATE_CHANGE.WINDOW_ENABLED);
});
it("should record enabled sharing states for browser", function() {
driver._noteSharingState("browser", true);
sinon.assert.calledOnce(mozLoop.telemetryAddKeyedValue);
sinon.assert.calledWithExactly(mozLoop.telemetryAddKeyedValue,
"LOOP_SHARING_STATE_CHANGE",
mozLoop.SHARING_STATE_CHANGE.BROWSER_ENABLED);
});
it("should record disabled sharing states for window", function() {
driver._noteSharingState("window", false);
sinon.assert.calledOnce(mozLoop.telemetryAddKeyedValue);
sinon.assert.calledWithExactly(mozLoop.telemetryAddKeyedValue,
"LOOP_SHARING_STATE_CHANGE",
mozLoop.SHARING_STATE_CHANGE.WINDOW_DISABLED);
});
it("should record disabled sharing states for browser", function() {
driver._noteSharingState("browser", false);
sinon.assert.calledOnce(mozLoop.telemetryAddKeyedValue);
sinon.assert.calledWithExactly(mozLoop.telemetryAddKeyedValue,
"LOOP_SHARING_STATE_CHANGE",
mozLoop.SHARING_STATE_CHANGE.BROWSER_DISABLED);
});
});
describe("#forceDisconnectAll", function() {
it("should not disconnect anything when not connected", function() {
driver.session = session;

View File

@ -58,6 +58,17 @@ const ITEM_RECORD_PROPERTIES = `
readPosition
`.trim().split(/\s+/);
// Article objects that are passed to ReadingList.addItem may contain
// some properties that are known but are not currently stored in the
// ReadingList records. This is the list of properties that are knowingly
// disregarded before the item is normalized.
const ITEM_DISREGARDED_PROPERTIES = `
byline
dir
content
length
`.trim().split(/\s+/);
/**
* A reading list contains ReadingListItems.
*
@ -853,6 +864,9 @@ ReadingListItemIterator.prototype = {
function normalizeRecord(nonNormalizedRecord) {
let record = {};
for (let prop in nonNormalizedRecord) {
if (ITEM_DISREGARDED_PROPERTIES.includes(prop)) {
continue;
}
if (!ITEM_RECORD_PROPERTIES.includes(prop)) {
throw new Error("Unrecognized item property: " + prop);
}

View File

@ -0,0 +1,166 @@
/* 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/. */
// The client used to access the ReadingList server.
"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/Log.jsm");
Cu.import("resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "RESTRequest", "resource://services-common/rest.js");
XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils", "resource://services-common/utils.js");
XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts", "resource://gre/modules/FxAccounts.jsm");
let log = Log.repository.getLogger("readinglist.serverclient");
const OAUTH_SCOPE = "readinglist"; // The "scope" on the oauth token we request.
this.EXPORTED_SYMBOLS = [
"ServerClient",
];
// utf-8 joy. rest.js, which we use for the underlying requests, does *not*
// encode the request as utf-8 even though it wants to know the encoding.
// It does, however, explicitly decode the response. This seems insane, but is
// what it is.
// The end result being we need to utf-8 the request and let the response take
// care of itself.
function objectToUTF8Json(obj) {
// FTR, unescape(encodeURIComponent(JSON.stringify(obj))) also works ;)
return CommonUtils.encodeUTF8(JSON.stringify(obj));
}
function ServerClient(fxa = fxAccounts) {
this.fxa = fxa;
}
ServerClient.prototype = {
request(options) {
return this._request(options.path, options.method, options.body, options.headers);
},
get serverURL() {
return Services.prefs.getCharPref("readinglist.server");
},
_getURL(path) {
let result = this.serverURL;
// we expect the path to have a leading slash, so remove any trailing
// slashes on the pref.
if (result.endsWith("/")) {
result = result.slice(0, -1);
}
return result + path;
},
// Hook points for testing.
_getToken() {
// Assume token-caching is in place - if it's not we should avoid doing
// this each request.
return this.fxa.getOAuthToken({scope: OAUTH_SCOPE});
},
_removeToken(token) {
// XXX - remove this check once tokencaching landsin FxA.
if (!this.fxa.removeCachedOAuthToken) {
dump("XXX - token caching support is yet to land - can't remove token!");
return;
}
return this.fxa.removeCachedOAuthToken({token});
},
// Converts an error from the RESTRequest object to an error we export.
_convertRestError(error) {
return error; // XXX - errors?
},
// Converts an error from a try/catch handler to an error we export.
_convertJSError(error) {
return error; // XXX - errors?
},
/*
* Perform a request - handles authentication
*/
_request: Task.async(function* (path, method, body, headers) {
let token = yield this._getToken();
let response = yield this._rawRequest(path, method, body, headers, token);
log.debug("initial request got status ${status}", response);
if (response.status == 401) {
// an auth error - assume our token has expired or similar.
this._removeToken(token);
token = yield this._getToken();
response = yield this._rawRequest(path, method, body, headers, token);
log.debug("retry of request got status ${status}", response);
}
return response;
}),
/*
* Perform a request *without* abstractions such as auth etc
*
* On success (which *includes* non-200 responses) returns an object like:
* {
* status: 200, # http status code
* headers: {}, # header values keyed by header name.
* body: {}, # parsed json
}
*/
_rawRequest(path, method, body, headers, oauthToken) {
return new Promise((resolve, reject) => {
let url = this._getURL(path);
log.debug("dispatching request to", url);
let request = new RESTRequest(url);
method = method.toUpperCase();
request.setHeader("Accept", "application/json");
request.setHeader("Content-Type", "application/json; charset=utf-8");
request.setHeader("Authorization", "Bearer " + oauthToken);
// and additional header specified for this request.
if (headers) {
for (let [headerName, headerValue] in Iterator(headers)) {
log.trace("Caller specified header: ${headerName}=${headerValue}", {headerName, headerValue});
request.setHeader(headerName, headerValue);
}
}
request.onComplete = error => {
if (error) {
return reject(this._convertRestError(error));
}
let response = request.response;
log.debug("received response status: ${status} ${statusText}", response);
// Handle response status codes we know about
let result = {
status: response.status,
headers: response.headers
};
try {
if (response.body) {
result.body = JSON.parse(response.body);
}
} catch (e) {
log.info("Failed to parse JSON body |${body}|: ${e}",
{body: response.body, e});
// We don't reject due to this (and don't even make a huge amount of
// log noise - eg, a 50X error from a load balancer etc may not write
// JSON.
}
resolve(result);
}
// We are assuming the body has already been decoded and thus contains
// unicode, but the server expects utf-8. encodeURIComponent does that.
request.dispatch(method, objectToUTF8Json(body));
});
},
};

View File

@ -6,6 +6,8 @@ JAR_MANIFESTS += ['jar.mn']
EXTRA_JS_MODULES.readinglist += [
'ReadingList.jsm',
'Scheduler.jsm',
'ServerClient.jsm',
'SQLiteStore.jsm',
]
@ -17,9 +19,5 @@ BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell/xpcshell.ini']
EXTRA_JS_MODULES.readinglist += [
'Scheduler.jsm',
]
with Files('**'):
BUG_COMPONENT = ('Firefox', 'Reading List')

View File

@ -161,6 +161,7 @@ let RLSidebar = {
} else {
thumb.style.removeProperty("background-image");
}
thumb.classList.toggle("preview-available", !!item.preview);
},
/**
@ -200,7 +201,7 @@ let RLSidebar = {
return;
}
log.debug(`Setting activeItem: ${node ? node.id : null}`);
log.trace(`Setting activeItem: ${node ? node.id : null}`);
if (node && node.classList.contains("active")) {
return;
@ -233,7 +234,7 @@ let RLSidebar = {
return;
}
log.debug(`Setting activeItem: ${node ? node.id : null}`);
log.trace(`Setting selectedItem: ${node ? node.id : null}`);
let prevItem = document.querySelector("#list > .item.selected");
if (prevItem) {
@ -270,7 +271,7 @@ let RLSidebar = {
},
set selectedIndex(index) {
log.debug(`Setting selectedIndex: ${index}`);
log.trace(`Setting selectedIndex: ${index}`);
if (index == -1) {
this.selectedItem = null;

View File

@ -5,3 +5,52 @@ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
do_get_profile(); // fxa needs a profile directory for storage.
Cu.import("resource://gre/modules/FxAccounts.jsm");
Cu.import("resource://gre/modules/FxAccountsClient.jsm");
// Create a mocked FxAccounts object with a signed-in, verified user.
function* createMockFxA() {
function MockFxAccountsClient() {
this._email = "nobody@example.com";
this._verified = false;
this.accountStatus = function(uid) {
let deferred = Promise.defer();
deferred.resolve(!!uid && (!this._deletedOnServer));
return deferred.promise;
};
this.signOut = function() { return Promise.resolve(); };
FxAccountsClient.apply(this);
}
MockFxAccountsClient.prototype = {
__proto__: FxAccountsClient.prototype
}
function MockFxAccounts() {
return new FxAccounts({
fxAccountsClient: new MockFxAccountsClient(),
getAssertion: () => Promise.resolve("assertion"),
});
}
let fxa = new MockFxAccounts();
let credentials = {
email: "foo@example.com",
uid: "1234@lcip.org",
assertion: "foobar",
sessionToken: "dead",
kA: "beef",
kB: "cafe",
verified: true
};
yield fxa.setSignedInUser(credentials);
return fxa;
}

View File

@ -0,0 +1,209 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
Cu.import("resource://testing-common/httpd.js");
Cu.import("resource:///modules/readinglist/ServerClient.jsm");
Cu.import("resource://gre/modules/Log.jsm");
let appender = new Log.DumpAppender();
for (let logName of ["FirefoxAccounts", "readinglist.serverclient"]) {
Log.repository.getLogger(logName).addAppender(appender);
}
// Some test servers we use.
let Server = function(handlers) {
this._server = null;
this._handlers = handlers;
}
Server.prototype = {
start() {
this._server = new HttpServer();
for (let [path, handler] in Iterator(this._handlers)) {
// httpd.js seems to swallow exceptions
let thisHandler = handler;
let wrapper = (request, response) => {
try {
thisHandler(request, response);
} catch (ex) {
print("**** Handler for", path, "failed:", ex, ex.stack);
throw ex;
}
}
this._server.registerPathHandler(path, wrapper);
}
this._server.start(-1);
},
stop() {
return new Promise(resolve => {
this._server.stop(resolve);
this._server = null;
});
},
get host() {
return "http://localhost:" + this._server.identity.primaryPort;
},
};
// An OAuth server that hands out tokens.
function OAuthTokenServer() {
let server;
let handlers = {
"/v1/authorization": (request, response) => {
response.setStatusLine("1.1", 200, "OK");
let token = "token" + server.numTokenFetches;
print("Test OAuth server handing out token", token);
server.numTokenFetches += 1;
server.activeTokens.add(token);
response.write(JSON.stringify({access_token: token}));
},
"/v1/destroy": (request, response) => {
// Getting the body seems harder than it should be!
let sis = Cc["@mozilla.org/scriptableinputstream;1"]
.createInstance(Ci.nsIScriptableInputStream);
sis.init(request.bodyInputStream);
let body = JSON.parse(sis.read(sis.available()));
sis.close();
let token = body.token;
ok(server.activeTokens.delete(token));
print("after destroy have", server.activeTokens.size, "tokens left.")
response.setStatusLine("1.1", 200, "OK");
response.write('{}');
},
}
server = new Server(handlers);
server.numTokenFetches = 0;
server.activeTokens = new Set();
return server;
}
// The tests.
function run_test() {
run_next_test();
}
// Arrange for the first token we hand out to be rejected - the client should
// notice the 401 and silently get a new token and retry the request.
add_task(function testAuthRetry() {
let handlers = {
"/v1/batch": (request, response) => {
// We know the first token we will get is "token0", so we simulate that
// "expiring" by only accepting "token1". Then we just echo the response
// back.
let authHeader;
try {
authHeader = request.getHeader("Authorization");
} catch (ex) {}
if (authHeader != "Bearer token1") {
response.setStatusLine("1.1", 401, "Unauthorized");
response.write("wrong token");
return;
}
response.setStatusLine("1.1", 200, "OK");
response.write(JSON.stringify({ok: true}));
}
};
let rlserver = new Server(handlers);
rlserver.start();
let authServer = OAuthTokenServer();
authServer.start();
try {
Services.prefs.setCharPref("readinglist.server", rlserver.host + "/v1");
Services.prefs.setCharPref("identity.fxaccounts.remote.oauth.uri", authServer.host + "/v1");
let fxa = yield createMockFxA();
let sc = new ServerClient(fxa);
let response = yield sc.request({
path: "/batch",
method: "post",
body: {foo: "bar"},
});
equal(response.status, 200, "got the 200 we expected");
equal(authServer.numTokenFetches, 2, "took 2 tokens to get the 200")
deepEqual(response.body, {ok: true});
} finally {
yield authServer.stop();
yield rlserver.stop();
}
});
// Check that specified headers are seen by the server, and that server headers
// in the response are seen by the client.
add_task(function testHeaders() {
let handlers = {
"/v1/batch": (request, response) => {
ok(request.hasHeader("x-foo"), "got our foo header");
equal(request.getHeader("x-foo"), "bar", "foo header has the correct value");
response.setHeader("Server-Sent-Header", "hello");
response.setStatusLine("1.1", 200, "OK");
response.write("{}");
}
};
let rlserver = new Server(handlers);
rlserver.start();
try {
Services.prefs.setCharPref("readinglist.server", rlserver.host + "/v1");
let fxa = yield createMockFxA();
let sc = new ServerClient(fxa);
sc._getToken = () => Promise.resolve();
let response = yield sc.request({
path: "/batch",
method: "post",
headers: {"X-Foo": "bar"},
body: {foo: "bar"}});
equal(response.status, 200, "got the 200 we expected");
equal(response.headers["server-sent-header"], "hello", "got the server header");
} finally {
yield rlserver.stop();
}
});
// Check that unicode ends up as utf-8 in requests, and vice-versa in responses.
// (Note the ServerClient assumes all strings in and out are UCS, and thus have
// already been encoded/decoded (ie, it never expects to receive stuff already
// utf-8 encoded, and never returns utf-8 encoded responses.)
add_task(function testUTF8() {
let handlers = {
"/v1/hello": (request, response) => {
// Get the body as bytes.
let sis = Cc["@mozilla.org/scriptableinputstream;1"]
.createInstance(Ci.nsIScriptableInputStream);
sis.init(request.bodyInputStream);
let body = sis.read(sis.available());
sis.close();
// The client sent "{"copyright: "\xa9"} where \xa9 is the copyright symbol.
// It should have been encoded as utf-8 which is \xc2\xa9
equal(body, '{"copyright":"\xc2\xa9"}', "server saw utf-8 encoded data");
// and just write it back unchanged.
response.setStatusLine("1.1", 200, "OK");
response.write(body);
}
};
let rlserver = new Server(handlers);
rlserver.start();
try {
Services.prefs.setCharPref("readinglist.server", rlserver.host + "/v1");
let fxa = yield createMockFxA();
let sc = new ServerClient(fxa);
sc._getToken = () => Promise.resolve();
let body = {copyright: "\xa9"}; // see above - \xa9 is the copyright symbol
let response = yield sc.request({
path: "/hello",
method: "post",
body: body
});
equal(response.status, 200, "got the 200 we expected");
deepEqual(response.body, body);
} finally {
yield rlserver.stop();
}
});

View File

@ -3,5 +3,6 @@ head = head.js
firefox-appdir = browser
[test_ReadingList.js]
[test_ServerClient.js]
[test_scheduler.js]
[test_SQLiteStore.js]

View File

@ -45,7 +45,7 @@ const WINDOW_HIDEABLE_FEATURES = [
];
// Messages that will be received via the Frame Message Manager.
const FMM_MESSAGES = [
const MESSAGES = [
// The content script gives us a reference to an object that performs
// synchronous collection of session data.
"SessionStore:setupSyncHandler",
@ -70,11 +70,15 @@ const FMM_MESSAGES = [
// A tab that is being restored was reloaded. We call restoreTabContent to
// finish restoring it right away.
"SessionStore:reloadPendingTab",
// A crashed tab was revived by navigating to a different page. Remove its
// browser from the list of crashed browsers to stop ignoring its messages.
"SessionStore:crashedTabRevived",
];
// The list of messages we accept from <xul:browser>s that have no tab
// assigned. Those are for example the ones that preload about:newtab pages.
const FMM_NOTAB_MESSAGES = new Set([
const NOTAB_MESSAGES = new Set([
// For a description see above.
"SessionStore:setupSyncHandler",
@ -82,15 +86,13 @@ const FMM_NOTAB_MESSAGES = new Set([
"SessionStore:update",
]);
// Messages that will be received via the Parent Process Message Manager.
const PPMM_MESSAGES = [
// A tab is being revived from the crashed state. The sender of this
// message should actually be running in the parent process, since this
// will be the crashed tab interface. We use the Child and Parent Process
// Message Managers because the message is sent during framescript unload
// when the Frame Message Manager is not available.
"SessionStore:RemoteTabRevived",
];
// The list of messages we want to receive even during the short period after a
// frame has been removed from the DOM and before its frame script has finished
// unloading.
const CLOSED_MESSAGES = new Set([
// For a description see above.
"SessionStore:crashedTabRevived",
]);
// These are tab events that we listen to.
const TAB_EVENTS = [
@ -423,8 +425,6 @@ let SessionStoreInternal = {
Services.obs.addObserver(this, aTopic, true);
}, this);
PPMM_MESSAGES.forEach(msg => ppmm.addMessageListener(msg, this));
this._initPrefs();
this._initialized = true;
},
@ -554,8 +554,6 @@ let SessionStoreInternal = {
// Make sure to cancel pending saves.
SessionSaver.cancel();
PPMM_MESSAGES.forEach(msg => ppmm.removeMessageListener(msg, this));
},
/**
@ -602,12 +600,6 @@ let SessionStoreInternal = {
* and thus enables communication with OOP tabs.
*/
receiveMessage(aMessage) {
// We'll deal with any Parent Process Message Manager messages first...
if (aMessage.name == "SessionStore:RemoteTabRevived") {
this._crashedBrowsers.delete(aMessage.objects.browser.permanentKey);
return;
}
// If we got here, that means we're dealing with a frame message
// manager message, so the target will be a <xul:browser>.
var browser = aMessage.target;
@ -616,7 +608,7 @@ let SessionStoreInternal = {
// Ensure we receive only specific messages from <xul:browser>s that
// have no tab assigned, e.g. the ones that preload about:newtab pages.
if (!tab && !FMM_NOTAB_MESSAGES.has(aMessage.name)) {
if (!tab && !NOTAB_MESSAGES.has(aMessage.name)) {
throw new Error(`received unexpected message '${aMessage.name}' ` +
`from a browser that has no tab`);
}
@ -709,6 +701,9 @@ let SessionStoreInternal = {
}
}
break;
case "SessionStore:crashedTabRevived":
this._crashedBrowsers.delete(browser.permanentKey);
break;
default:
throw new Error(`received unknown message '${aMessage.name}'`);
break;
@ -799,7 +794,10 @@ let SessionStoreInternal = {
aWindow.__SSi = this._generateWindowID();
let mm = aWindow.getGroupMessageManager("browsers");
FMM_MESSAGES.forEach(msg => mm.addMessageListener(msg, this));
MESSAGES.forEach(msg => {
let listenWhenClosed = CLOSED_MESSAGES.has(msg);
mm.addMessageListener(msg, this, listenWhenClosed);
});
// Load the frame script after registering listeners.
mm.loadFrameScript("chrome://browser/content/content-sessionStore.js", true);
@ -1128,7 +1126,7 @@ let SessionStoreInternal = {
DyingWindowCache.set(aWindow, winData);
let mm = aWindow.getGroupMessageManager("browsers");
FMM_MESSAGES.forEach(msg => mm.removeMessageListener(msg, this));
MESSAGES.forEach(msg => mm.removeMessageListener(msg, this));
delete aWindow.__SSi;
},

View File

@ -749,12 +749,8 @@ function handleRevivedTab() {
removeEventListener("pagehide", handleRevivedTab);
// We can't send a message using the frame message manager because by
// the time we reach the unload event handler, it's "too late", and messages
// won't be sent or received. The child-process message manager works though,
// despite the fact that we're really running in the parent process.
let browser = docShell.chromeEventHandler;
cpmm.sendAsyncMessage("SessionStore:RemoteTabRevived", null, {browser: browser});
// Notify the parent.
sendAsyncMessage("SessionStore:crashedTabRevived");
}
}

View File

@ -31,8 +31,8 @@ const Telemetry = devtools.require("devtools/shared/telemetry");
const TABS_OPEN_PEAK_HISTOGRAM = "DEVTOOLS_TABS_OPEN_PEAK_LINEAR";
const TABS_OPEN_AVG_HISTOGRAM = "DEVTOOLS_TABS_OPEN_AVERAGE_LINEAR";
const TABS_PINNED_PEAK_HISTOGRAM = "DEVTOOLS_TABS_PINNED_PEAK_EXPONENTIAL";
const TABS_PINNED_AVG_HISTOGRAM = "DEVTOOLS_TABS_PINNED_AVERAGE_EXPONENTIAL";
const TABS_PINNED_PEAK_HISTOGRAM = "DEVTOOLS_TABS_PINNED_PEAK_LINEAR";
const TABS_PINNED_AVG_HISTOGRAM = "DEVTOOLS_TABS_PINNED_AVERAGE_LINEAR";
const FORBIDDEN_IDS = new Set(["toolbox", ""]);
const MAX_ORDINAL = 99;
@ -47,6 +47,7 @@ this.DevTools = function DevTools() {
this._tools = new Map(); // Map<toolId, tool>
this._themes = new Map(); // Map<themeId, theme>
this._toolboxes = new Map(); // Map<target, toolbox>
this._telemetry = new Telemetry();
// destroy() is an observer's handler so we need to preserve context.
this.destroy = this.destroy.bind(this);

View File

@ -5,599 +5,70 @@
"use strict";
const { Ci, Cc } = require("chrome");
const { getJSON } = require("devtools/shared/getjson");
const { Services } = require("resource://gre/modules/Services.jsm");
const promise = require("promise");
const DEVICES_URL = "devtools.devices.url";
const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/device.properties");
/* `Devices` is a catalog of existing devices and their properties, intended
* for (mobile) device emulation tools and features.
/* This is a catalog of common web-enabled devices and their properties,
* intended for (mobile) device emulation.
*
* The properties of a device are:
* - name: Device brand and model(s).
* - width: Viewport width.
* - height: Viewport height.
* - pixelRatio: Screen pixel ratio to viewport.
* - userAgent: Device UserAgent string.
* - touch: Whether the screen is touch-enabled.
* - name: brand and model(s).
* - width: viewport width.
* - height: viewport height.
* - pixelRatio: ratio from viewport to physical screen pixels.
* - userAgent: UA string of the device's browser.
* - touch: whether it has a touch screen.
* - firefoxOS: whether Firefox OS is supported.
*
* To add more devices to this catalog, either patch this file, or push new
* device descriptions from your own code (e.g. an addon) like so:
* The device types are:
* ["phones", "tablets", "laptops", "televisions", "consoles", "watches"].
*
* You can easily add more devices to this catalog from your own code (e.g. an
* addon) like so:
*
* var myPhone = { name: "My Phone", ... };
* require("devtools/shared/devices").Devices.Others.phones.push(myPhone);
* require("devtools/shared/devices").AddDevice(myPhone, "phones");
*/
let Devices = {
Types: ["phones", "tablets", "notebooks", "televisions", "watches"],
// Local devices catalog that addons can add to.
let localDevices = {};
// Get the localized string of a device type.
GetString(deviceType) {
// Add a device to the local catalog.
function AddDevice(device, type = "phones") {
let list = localDevices[type];
if (!list) {
list = localDevices[type] = [];
}
list.push(device);
}
exports.AddDevice = AddDevice;
// Get the complete devices catalog.
function GetDevices(bypassCache = false) {
let deferred = promise.defer();
// Fetch common devices from Mozilla's CDN.
getJSON(DEVICES_URL, bypassCache).then(devices => {
for (let type in localDevices) {
if (!devices[type]) {
devices.TYPES.push(type);
devices[type] = [];
}
devices[type] = localDevices[type].concat(devices[type]);
}
deferred.resolve(devices);
});
return deferred.promise;
}
exports.GetDevices = GetDevices;
// Get the localized string for a device type.
function GetDeviceString(deviceType) {
return Strings.GetStringFromName("device." + deviceType);
},
};
exports.Devices = Devices;
// The `Devices.FirefoxOS` list was put together from various sources online.
Devices.FirefoxOS = {
phones: [
{
name: "Firefox OS Flame",
width: 320,
height: 570,
pixelRatio: 1.5,
userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
touch: true,
},
{
name: "Alcatel One Touch Fire, Fire C",
width: 320,
height: 480,
pixelRatio: 1,
userAgent: "Mozilla/5.0 (Mobile; ALCATELOneTouch4012X; rv:28.0) Gecko/28.0 Firefox/28.0",
touch: true,
},
{
name: "Alcatel Fire E",
width: 320,
height: 480,
pixelRatio: 2,
userAgent: "Mozilla/5.0 (Mobile; ALCATELOneTouch4012X; rv:28.0) Gecko/28.0 Firefox/28.0",
touch: true,
},
{
name: "Geeksphone Keon",
width: 320,
height: 480,
pixelRatio: 1,
userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
touch: true,
},
{
name: "Geeksphone Peak, Revolution",
width: 360,
height: 640,
pixelRatio: 1.5,
userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
touch: true,
},
{
name: "Intex Cloud Fx",
width: 320,
height: 480,
pixelRatio: 1,
userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
touch: true,
},
{
name: "LG Fireweb",
width: 320,
height: 480,
pixelRatio: 1,
userAgent: "Mozilla/5.0 (Mobile; LG-D300; rv:28.0) Gecko/28.0 Firefox/28.0",
touch: true,
},
{
name: "Spice Fire One Mi-FX1",
width: 320,
height: 480,
pixelRatio: 1,
userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
touch: true,
},
{
name: "Symphony GoFox F15",
width: 320,
height: 480,
pixelRatio: 1,
userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
touch: true,
},
{
name: "Zen Fire 105",
width: 320,
height: 480,
pixelRatio: 1,
userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
touch: true,
},
{
name: "ZTE Open",
width: 320,
height: 480,
pixelRatio: 1,
userAgent: "Mozilla/5.0 (Mobile; ZTEOPEN; rv:28.0) Gecko/28.0 Firefox/28.0",
touch: true,
},
{
name: "ZTE Open C",
width: 320,
height: 450,
pixelRatio: 1.5,
userAgent: "Mozilla/5.0 (Mobile; OPENC; rv:28.0) Gecko/28.0 Firefox/28.0",
touch: true,
},
],
tablets: [
{
name: "Foxconn InFocus",
width: 1280,
height: 800,
pixelRatio: 1,
userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
touch: true,
},
{
name: "VIA Vixen",
width: 1024,
height: 600,
pixelRatio: 1,
userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
touch: true,
},
],
notebooks: [
],
televisions: [
{
name: "720p HD Television",
width: 1280,
height: 720,
pixelRatio: 1,
userAgent: "",
touch: false,
},
{
name: "1080p Full HD Television",
width: 1920,
height: 1080,
pixelRatio: 1,
userAgent: "",
touch: false,
},
{
name: "4K Ultra HD Television",
width: 3840,
height: 2160,
pixelRatio: 1,
userAgent: "",
touch: false,
},
],
watches: [
{
name: "LG G Watch",
width: 280,
height: 280,
pixelRatio: 1,
userAgent: "",
touch: true,
},
{
name: "LG G Watch R",
width: 320,
height: 320,
pixelRatio: 1,
userAgent: "",
touch: true,
},
{
name: "Moto 360",
width: 320,
height: 290,
pixelRatio: 1,
userAgent: "",
touch: true,
},
{
name: "Samsung Gear Live",
width: 320,
height: 320,
pixelRatio: 1,
userAgent: "",
touch: true,
},
],
};
// `Devices.Others` was derived from the Chromium source code:
// - chromium/src/third_party/WebKit/Source/devtools/front_end/toolbox/OverridesUI.js
Devices.Others = {
phones: [
{
name: "Apple iPhone 3GS",
width: 320,
height: 480,
pixelRatio: 1,
userAgent: "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5",
touch: true,
},
{
name: "Apple iPhone 4",
width: 320,
height: 480,
pixelRatio: 2,
userAgent: "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5",
touch: true,
},
{
name: "Apple iPhone 5",
width: 320,
height: 568,
pixelRatio: 2,
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53",
touch: true,
},
{
name: "Apple iPhone 6",
width: 375,
height: 667,
pixelRatio: 2,
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
touch: true,
},
{
name: "Apple iPhone 6 Plus",
width: 414,
height: 736,
pixelRatio: 3,
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
touch: true,
},
{
name: "BlackBerry Z10",
width: 384,
height: 640,
pixelRatio: 2,
userAgent: "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+",
touch: true,
},
{
name: "BlackBerry Z30",
width: 360,
height: 640,
pixelRatio: 2,
userAgent: "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+",
touch: true,
},
{
name: "Google Nexus 4",
width: 384,
height: 640,
pixelRatio: 2,
userAgent: "Mozilla/5.0 (Linux; Android 4.2.1; en-us; Nexus 4 Build/JOP40D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19",
touch: true,
},
{
name: "Google Nexus 5",
width: 360,
height: 640,
pixelRatio: 3,
userAgent: "Mozilla/5.0 (Linux; Android 4.2.1; en-us; Nexus 5 Build/JOP40D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19",
touch: true,
},
{
name: "Google Nexus S",
width: 320,
height: 533,
pixelRatio: 1.5,
userAgent: "Mozilla/5.0 (Linux; U; Android 2.3.4; en-us; Nexus S Build/GRJ22) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
touch: true,
},
{
name: "HTC Evo, Touch HD, Desire HD, Desire",
width: 320,
height: 533,
pixelRatio: 1.5,
userAgent: "Mozilla/5.0 (Linux; U; Android 2.2; en-us; Sprint APA9292KT Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
touch: true,
},
{
name: "HTC One X, EVO LTE",
width: 360,
height: 640,
pixelRatio: 2,
userAgent: "Mozilla/5.0 (Linux; Android 4.0.3; HTC One X Build/IML74K) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19",
touch: true,
},
{
name: "HTC Sensation, Evo 3D",
width: 360,
height: 640,
pixelRatio: 1.5,
userAgent: "Mozilla/5.0 (Linux; U; Android 4.0.3; en-us; HTC Sensation Build/IML74K) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
touch: true,
},
{
name: "LG Optimus 2X, Optimus 3D, Optimus Black",
width: 320,
height: 533,
pixelRatio: 1.5,
userAgent: "Mozilla/5.0 (Linux; U; Android 2.2; en-us; LG-P990/V08c Build/FRG83) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 MMS/LG-Android-MMS-V1.0/1.2",
touch: true,
},
{
name: "LG Optimus G",
width: 384,
height: 640,
pixelRatio: 2,
userAgent: "Mozilla/5.0 (Linux; Android 4.0; LG-E975 Build/IMM76L) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19",
touch: true,
},
{
name: "LG Optimus LTE, Optimus 4X HD",
width: 424,
height: 753,
pixelRatio: 1.7,
userAgent: "Mozilla/5.0 (Linux; U; Android 2.3; en-us; LG-P930 Build/GRJ90) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
touch: true,
},
{
name: "LG Optimus One",
width: 213,
height: 320,
pixelRatio: 1.5,
userAgent: "Mozilla/5.0 (Linux; U; Android 2.2.1; en-us; LG-MS690 Build/FRG83) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
touch: true,
},
{
name: "Motorola Defy, Droid, Droid X, Milestone",
width: 320,
height: 569,
pixelRatio: 1.5,
userAgent: "Mozilla/5.0 (Linux; U; Android 2.0; en-us; Milestone Build/ SHOLS_U2_01.03.1) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17",
touch: true,
},
{
name: "Motorola Droid 3, Droid 4, Droid Razr, Atrix 4G, Atrix 2",
width: 540,
height: 960,
pixelRatio: 1,
userAgent: "Mozilla/5.0 (Linux; U; Android 2.2; en-us; Droid Build/FRG22D) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
touch: true,
},
{
name: "Motorola Droid Razr HD",
width: 720,
height: 1280,
pixelRatio: 1,
userAgent: "Mozilla/5.0 (Linux; U; Android 2.3; en-us; DROID RAZR 4G Build/6.5.1-73_DHD-11_M1-29) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
touch: true,
},
{
name: "Nokia C5, C6, C7, N97, N8, X7",
width: 360,
height: 640,
pixelRatio: 1,
userAgent: "NokiaN97/21.1.107 (SymbianOS/9.4; Series60/5.0 Mozilla/5.0; Profile/MIDP-2.1 Configuration/CLDC-1.1) AppleWebkit/525 (KHTML, like Gecko) BrowserNG/7.1.4",
touch: true,
},
{
name: "Nokia Lumia 7X0, Lumia 8XX, Lumia 900, N800, N810, N900",
width: 320,
height: 533,
pixelRatio: 1.5,
userAgent: "Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 820)",
touch: true,
},
{
name: "Samsung Galaxy Note 3",
width: 360,
height: 640,
pixelRatio: 3,
userAgent: "Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
touch: true,
},
{
name: "Samsung Galaxy Note II",
width: 360,
height: 640,
pixelRatio: 2,
userAgent: "Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
touch: true,
},
{
name: "Samsung Galaxy Note",
width: 400,
height: 640,
pixelRatio: 2,
userAgent: "Mozilla/5.0 (Linux; U; Android 2.3; en-us; SAMSUNG-SGH-I717 Build/GINGERBREAD) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
touch: true,
},
{
name: "Samsung Galaxy S III, Galaxy Nexus",
width: 360,
height: 640,
pixelRatio: 2,
userAgent: "Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
touch: true,
},
{
name: "Samsung Galaxy S, S II, W",
width: 320,
height: 533,
pixelRatio: 1.5,
userAgent: "Mozilla/5.0 (Linux; U; Android 2.1; en-us; GT-I9000 Build/ECLAIR) AppleWebKit/525.10+ (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2",
touch: true,
},
{
name: "Samsung Galaxy S4",
width: 360,
height: 640,
pixelRatio: 3,
userAgent: "Mozilla/5.0 (Linux; Android 4.2.2; GT-I9505 Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.59 Mobile Safari/537.36",
touch: true,
},
{
name: "Sony Xperia S, Ion",
width: 360,
height: 640,
pixelRatio: 2,
userAgent: "Mozilla/5.0 (Linux; U; Android 4.0; en-us; LT28at Build/6.1.C.1.111) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
touch: true,
},
{
name: "Sony Xperia Sola, U",
width: 480,
height: 854,
pixelRatio: 1,
userAgent: "Mozilla/5.0 (Linux; U; Android 2.3; en-us; SonyEricssonST25i Build/6.0.B.1.564) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
touch: true,
},
{
name: "Sony Xperia Z, Z1",
width: 360,
height: 640,
pixelRatio: 3,
userAgent: "Mozilla/5.0 (Linux; U; Android 4.2; en-us; SonyC6903 Build/14.1.G.1.518) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
touch: true,
},
],
tablets: [
{
name: "Amazon Kindle Fire HDX 7″",
width: 1920,
height: 1200,
pixelRatio: 2,
userAgent: "Mozilla/5.0 (Linux; U; en-us; KFTHWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true",
touch: true,
},
{
name: "Amazon Kindle Fire HDX 8.9″",
width: 2560,
height: 1600,
pixelRatio: 2,
userAgent: "Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true",
touch: true,
},
{
name: "Amazon Kindle Fire (First Generation)",
width: 1024,
height: 600,
pixelRatio: 1,
userAgent: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-us; Silk/1.0.141.16-Gen4_11004310) AppleWebkit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16 Silk-Accelerated=true",
touch: true,
},
{
name: "Apple iPad 1 / 2 / iPad Mini",
width: 1024,
height: 768,
pixelRatio: 1,
userAgent: "Mozilla/5.0 (iPad; CPU OS 4_3_5 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8L1 Safari/6533.18.5",
touch: true,
},
{
name: "Apple iPad 3 / 4",
width: 1024,
height: 768,
pixelRatio: 2,
userAgent: "Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53",
touch: true,
},
{
name: "BlackBerry PlayBook",
width: 1024,
height: 600,
pixelRatio: 1,
userAgent: "Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+",
touch: true,
},
{
name: "Google Nexus 10",
width: 1280,
height: 800,
pixelRatio: 2,
userAgent: "Mozilla/5.0 (Linux; Android 4.3; Nexus 10 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.72 Safari/537.36",
touch: true,
},
{
name: "Google Nexus 7 2",
width: 960,
height: 600,
pixelRatio: 2,
userAgent: "Mozilla/5.0 (Linux; Android 4.3; Nexus 7 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.72 Safari/537.36",
touch: true,
},
{
name: "Google Nexus 7",
width: 966,
height: 604,
pixelRatio: 1.325,
userAgent: "Mozilla/5.0 (Linux; Android 4.3; Nexus 7 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.72 Safari/537.36",
touch: true,
},
{
name: "Motorola Xoom, Xyboard",
width: 1280,
height: 800,
pixelRatio: 1,
userAgent: "Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) AppleWebKit/525.10 (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2",
touch: true,
},
{
name: "Samsung Galaxy Tab 7.7, 8.9, 10.1",
width: 1280,
height: 800,
pixelRatio: 1,
userAgent: "Mozilla/5.0 (Linux; U; Android 2.2; en-us; SCH-I800 Build/FROYO) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
touch: true,
},
{
name: "Samsung Galaxy Tab",
width: 1024,
height: 600,
pixelRatio: 1,
userAgent: "Mozilla/5.0 (Linux; U; Android 2.2; en-us; SCH-I800 Build/FROYO) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
touch: true,
},
],
notebooks: [
{
name: "Notebook with touch",
width: 1280,
height: 950,
pixelRatio: 1,
userAgent: "",
touch: true,
},
{
name: "Notebook with HiDPI screen",
width: 1440,
height: 900,
pixelRatio: 2,
userAgent: "",
touch: false,
},
{
name: "Generic notebook",
width: 1280,
height: 800,
pixelRatio: 1,
userAgent: "",
touch: false,
},
],
televisions: [
],
watches: [
],
};
}
exports.GetDeviceString = GetDeviceString;

View File

@ -3,23 +3,22 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const {Cu, CC} = require("chrome");
const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
const promise = require("promise");
const {Services} = Cu.import("resource://gre/modules/Services.jsm");
const XMLHttpRequest = CC("@mozilla.org/xmlextras/xmlhttprequest;1");
function getJSON(bypassCache, pref) {
// Downloads and caches a JSON file from a URL given by the pref.
exports.getJSON = function (prefName, bypassCache) {
if (!bypassCache) {
try {
let str = Services.prefs.getCharPref(pref + "_cache");
let str = Services.prefs.getCharPref(prefName + "_cache");
let json = JSON.parse(str);
return promise.resolve(json);
} catch(e) {/* no pref or invalid json. Let's continue */}
}
let deferred = promise.defer();
let xhr = new XMLHttpRequest();
xhr.onload = () => {
@ -27,9 +26,9 @@ function getJSON(bypassCache, pref) {
try {
json = JSON.parse(xhr.responseText);
} catch(e) {
return deferred.reject("Not valid JSON");
return deferred.reject("Invalid JSON");
}
Services.prefs.setCharPref(pref + "_cache", xhr.responseText);
Services.prefs.setCharPref(prefName + "_cache", xhr.responseText);
deferred.resolve(json);
}
@ -37,18 +36,8 @@ function getJSON(bypassCache, pref) {
deferred.reject("Network error");
}
xhr.open("get", Services.prefs.getCharPref(pref));
xhr.open("get", Services.prefs.getCharPref(prefName));
xhr.send();
return deferred.promise;
}
exports.GetTemplatesJSON = function(bypassCache) {
return getJSON(bypassCache, "devtools.webide.templatesURL");
}
exports.GetAddonsJSON = function(bypassCache) {
return getJSON(bypassCache, "devtools.webide.addonsURL");
}

View File

@ -51,6 +51,7 @@ EXTRA_JS_MODULES.devtools.shared += [
'devices.js',
'doorhanger.js',
'frame-script-utils.js',
'getjson.js',
'inplace-editor.js',
'observable-object.js',
'options-view.js',
@ -61,6 +62,7 @@ EXTRA_JS_MODULES.devtools.shared += [
]
EXTRA_JS_MODULES.devtools.shared.widgets += [
'widgets/CubicBezierPresets.js',
'widgets/CubicBezierWidget.js',
'widgets/FastListWidget.js',
'widgets/Spectrum.js',

View File

@ -7,6 +7,7 @@ support-files =
browser_templater_basic.html
browser_toolbar_basic.html
browser_toolbar_webconsole_errors_count.html
browser_devices.json
doc_options-view.xul
head.js
leakhunt.js
@ -15,6 +16,9 @@ support-files =
[browser_cubic-bezier-01.js]
[browser_cubic-bezier-02.js]
[browser_cubic-bezier-03.js]
[browser_cubic-bezier-04.js]
[browser_cubic-bezier-05.js]
[browser_cubic-bezier-06.js]
[browser_flame-graph-01.js]
[browser_flame-graph-02.js]
[browser_flame-graph-03a.js]
@ -98,3 +102,4 @@ skip-if = buildapp == 'mulet' || e10s # The developertoolbar error count isn't c
[browser_treeWidget_basic.js]
[browser_treeWidget_keyboard_interaction.js]
[browser_treeWidget_mouse_interaction.js]
[browser_devices.js]

View File

@ -7,16 +7,20 @@
// Tests that the CubicBezierWidget generates content in a given parent node
const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
const {CubicBezierWidget} = devtools.require("devtools/shared/widgets/CubicBezierWidget");
const {CubicBezierWidget} =
devtools.require("devtools/shared/widgets/CubicBezierWidget");
add_task(function*() {
yield promiseTab("about:blank");
let [host, win, doc] = yield createHost("bottom", TEST_URI);
info("Checking that the markup is created in the parent");
info("Checking that the graph markup is created in the parent");
let container = doc.querySelector("#container");
let w = new CubicBezierWidget(container);
ok(container.querySelector(".display-wrap"),
"The display has been added");
ok(container.querySelector(".coordinate-plane"),
"The coordinate plane has been added");
let buttons = container.querySelectorAll("button");

View File

@ -7,26 +7,37 @@
// Tests the CubicBezierWidget events
const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
const {CubicBezierWidget, PREDEFINED} =
const {CubicBezierWidget} =
devtools.require("devtools/shared/widgets/CubicBezierWidget");
const {PREDEFINED} = require("devtools/shared/widgets/CubicBezierPresets");
add_task(function*() {
yield promiseTab("about:blank");
let [host, win, doc] = yield createHost("bottom", TEST_URI);
// Required or widget will be clipped inside of 'bottom'
// host by -14. Setting `fixed` zeroes this which is needed for
// calculating offsets. Occurs in test env only.
doc.body.setAttribute("style", "position: fixed");
let container = doc.querySelector("#container");
let w = new CubicBezierWidget(container, PREDEFINED.linear);
yield pointsCanBeDragged(w, win, doc);
yield curveCanBeClicked(w, win, doc);
yield pointsCanBeMovedWithKeyboard(w, win, doc);
let rect = w.curve.getBoundingClientRect();
rect.graphTop = rect.height * w.bezierCanvas.padding[0];
rect.graphBottom = rect.height - rect.graphTop;
rect.graphHeight = rect.graphBottom - rect.graphTop;
yield pointsCanBeDragged(w, win, doc, rect);
yield curveCanBeClicked(w, win, doc, rect);
yield pointsCanBeMovedWithKeyboard(w, win, doc, rect);
w.destroy();
host.destroy();
gBrowser.removeCurrentTab();
});
function* pointsCanBeDragged(widget, win, doc) {
function* pointsCanBeDragged(widget, win, doc, offsets) {
info("Checking that the control points can be dragged with the mouse");
info("Listening for the update event");
@ -34,7 +45,7 @@ function* pointsCanBeDragged(widget, win, doc) {
info("Generating a mousedown/move/up on P1");
widget._onPointMouseDown({target: widget.p1});
doc.onmousemove({pageX: 0, pageY: 100});
doc.onmousemove({pageX: offsets.left, pageY: offsets.graphTop});
doc.onmouseup();
let bezier = yield onUpdated;
@ -48,7 +59,7 @@ function* pointsCanBeDragged(widget, win, doc) {
info("Generating a mousedown/move/up on P2");
widget._onPointMouseDown({target: widget.p2});
doc.onmousemove({pageX: 200, pageY: 300});
doc.onmousemove({pageX: offsets.right, pageY: offsets.graphBottom});
doc.onmouseup();
bezier = yield onUpdated;
@ -56,14 +67,16 @@ function* pointsCanBeDragged(widget, win, doc) {
is(bezier.P2[1], 0, "The new P2 progress coordinate is correct");
}
function* curveCanBeClicked(widget, win, doc) {
function* curveCanBeClicked(widget, win, doc, offsets) {
info("Checking that clicking on the curve moves the closest control point");
info("Listening for the update event");
let onUpdated = widget.once("updated");
info("Click close to P1");
widget._onCurveClick({pageX: 50, pageY: 150});
let x = offsets.left + (offsets.width / 4.0);
let y = offsets.graphTop + (offsets.graphHeight / 4.0);
widget._onCurveClick({pageX: x, pageY: y});
let bezier = yield onUpdated;
ok(true, "The widget fired the updated event");
@ -76,7 +89,9 @@ function* curveCanBeClicked(widget, win, doc) {
onUpdated = widget.once("updated");
info("Click close to P2");
widget._onCurveClick({pageX: 150, pageY: 250});
x = offsets.right - (offsets.width / 4);
y = offsets.graphBottom - (offsets.graphHeight / 4);
widget._onCurveClick({pageX: x, pageY: y});
bezier = yield onUpdated;
is(bezier.P2[0], 0.75, "The new P2 time coordinate is correct");
@ -85,57 +100,89 @@ function* curveCanBeClicked(widget, win, doc) {
is(bezier.P1[1], 0.75, "P1 progress coordinate remained unchanged");
}
function* pointsCanBeMovedWithKeyboard(widget, win, doc) {
function* pointsCanBeMovedWithKeyboard(widget, win, doc, offsets) {
info("Checking that points respond to keyboard events");
let singleStep = 3;
let shiftStep = 30;
info("Moving P1 to the left");
let newOffset = parseInt(widget.p1.style.left) - singleStep;
let x = widget.bezierCanvas.
offsetsToCoordinates({style: {left: newOffset}})[0];
let onUpdated = widget.once("updated");
widget._onPointKeyDown(getKeyEvent(widget.p1, 37));
let bezier = yield onUpdated;
is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
is(bezier.P1[0], x, "The new P1 time coordinate is correct");
is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
info("Moving P1 to the left, fast");
newOffset = parseInt(widget.p1.style.left) - shiftStep;
x = widget.bezierCanvas.
offsetsToCoordinates({style: {left: newOffset}})[0];
onUpdated = widget.once("updated");
widget._onPointKeyDown(getKeyEvent(widget.p1, 37, true));
bezier = yield onUpdated;
is(bezier.P1[0], 0.085, "The new P1 time coordinate is correct");
is(bezier.P1[0], x, "The new P1 time coordinate is correct");
is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
info("Moving P1 to the right, fast");
newOffset = parseInt(widget.p1.style.left) + shiftStep;
x = widget.bezierCanvas.
offsetsToCoordinates({style: {left: newOffset}})[0];
onUpdated = widget.once("updated");
widget._onPointKeyDown(getKeyEvent(widget.p1, 39, true));
bezier = yield onUpdated;
is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
is(bezier.P1[0], x, "The new P1 time coordinate is correct");
is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
info("Moving P1 to the bottom");
newOffset = parseInt(widget.p1.style.top) + singleStep;
let y = widget.bezierCanvas.
offsetsToCoordinates({style: {top: newOffset}})[1];
onUpdated = widget.once("updated");
widget._onPointKeyDown(getKeyEvent(widget.p1, 40));
bezier = yield onUpdated;
is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
is(bezier.P1[1], 0.735, "The new P1 progress coordinate is correct");
is(bezier.P1[0], x, "The new P1 time coordinate is correct");
is(bezier.P1[1], y, "The new P1 progress coordinate is correct");
info("Moving P1 to the bottom, fast");
newOffset = parseInt(widget.p1.style.top) + shiftStep;
y = widget.bezierCanvas.
offsetsToCoordinates({style: {top: newOffset}})[1];
onUpdated = widget.once("updated");
widget._onPointKeyDown(getKeyEvent(widget.p1, 40, true));
bezier = yield onUpdated;
is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
is(bezier.P1[1], 0.585, "The new P1 progress coordinate is correct");
is(bezier.P1[0], x, "The new P1 time coordinate is correct");
is(bezier.P1[1], y, "The new P1 progress coordinate is correct");
info("Moving P1 to the top, fast");
newOffset = parseInt(widget.p1.style.top) - shiftStep;
y = widget.bezierCanvas.
offsetsToCoordinates({style: {top: newOffset}})[1];
onUpdated = widget.once("updated");
widget._onPointKeyDown(getKeyEvent(widget.p1, 38, true));
bezier = yield onUpdated;
is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
is(bezier.P1[1], 0.735, "The new P1 progress coordinate is correct");
is(bezier.P1[0], x, "The new P1 time coordinate is correct");
is(bezier.P1[1], y, "The new P1 progress coordinate is correct");
info("Checking that keyboard events also work with P2");
info("Moving P2 to the left");
newOffset = parseInt(widget.p2.style.left) - singleStep;
x = widget.bezierCanvas.
offsetsToCoordinates({style: {left: newOffset}})[0];
onUpdated = widget.once("updated");
widget._onPointKeyDown(getKeyEvent(widget.p2, 37));
bezier = yield onUpdated;
is(bezier.P2[0], 0.735, "The new P2 time coordinate is correct");
is(bezier.P2[0], x, "The new P2 time coordinate is correct");
is(bezier.P2[1], 0.25, "The new P2 progress coordinate is correct");
}

View File

@ -7,8 +7,9 @@
// Tests that coordinates can be changed programatically in the CubicBezierWidget
const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
const {CubicBezierWidget, PREDEFINED} =
const {CubicBezierWidget} =
devtools.require("devtools/shared/widgets/CubicBezierWidget");
const {PREDEFINED} = require("devtools/shared/widgets/CubicBezierPresets");
add_task(function*() {
yield promiseTab("about:blank");

View File

@ -0,0 +1,51 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Tests that the CubicBezierPresetWidget generates markup.
const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
const {CubicBezierPresetWidget} =
devtools.require("devtools/shared/widgets/CubicBezierWidget");
const {PRESETS} = require("devtools/shared/widgets/CubicBezierPresets");
add_task(function*() {
yield promiseTab("about:blank");
let [host, win, doc] = yield createHost("bottom", TEST_URI);
let container = doc.querySelector("#container");
let w = new CubicBezierPresetWidget(container);
info("Checking that the presets are created in the parent");
ok(container.querySelector(".preset-pane"),
"The preset pane has been added");
ok(container.querySelector("#preset-categories"),
"The preset categories have been added");
let categories = container.querySelectorAll(".category");
is(categories.length, Object.keys(PRESETS).length,
"The preset categories have been added");
Object.keys(PRESETS).forEach(category => {
ok(container.querySelector("#" + category), `${category} has been added`);
ok(container.querySelector("#preset-category-" + category),
`The preset list for ${category} has been added.`);
});
info("Checking that each of the presets and its preview have been added");
Object.keys(PRESETS).forEach(category => {
Object.keys(PRESETS[category]).forEach(presetLabel => {
let preset = container.querySelector("#" + presetLabel);
ok(preset, `${presetLabel} has been added`);
ok(preset.querySelector("canvas"),
`${presetLabel}'s canvas preview has been added`);
ok(preset.querySelector("p"),
`${presetLabel}'s label has been added`);
});
});
w.destroy();
host.destroy();
gBrowser.removeCurrentTab();
});

View File

@ -0,0 +1,49 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Tests that the CubicBezierPresetWidget cycles menus
const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
const {CubicBezierPresetWidget} =
devtools.require("devtools/shared/widgets/CubicBezierWidget");
const {PREDEFINED, PRESETS, DEFAULT_PRESET_CATEGORY} =
require("devtools/shared/widgets/CubicBezierPresets");
add_task(function*() {
yield promiseTab("about:blank");
let [host, win, doc] = yield createHost("bottom", TEST_URI);
let container = doc.querySelector("#container");
let w = new CubicBezierPresetWidget(container);
info("Checking that preset is selected if coordinates are known");
w.refreshMenu([0, 0, 0, 0]);
is(w.activeCategory, container.querySelector(`#${DEFAULT_PRESET_CATEGORY}`),
"The default category is selected");
is(w._activePreset, null, "There is no selected category");
w.refreshMenu(PREDEFINED["linear"]);
is(w.activeCategory, container.querySelector("#ease-in-out"),
"The ease-in-out category is active");
is(w._activePreset, container.querySelector("#ease-in-out-linear"),
"The ease-in-out-linear preset is active");
w.refreshMenu(PRESETS["ease-out"]["ease-out-sine"]);
is(w.activeCategory, container.querySelector("#ease-out"),
"The ease-out category is active");
is(w._activePreset, container.querySelector("#ease-out-sine"),
"The ease-out-sine preset is active");
w.refreshMenu([0, 0, 0, 0]);
is(w.activeCategory, container.querySelector("#ease-out"),
"The ease-out category is still active");
is(w._activePreset, null, "No preset is active");
w.destroy();
host.destroy();
gBrowser.removeCurrentTab();
});

View File

@ -0,0 +1,80 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Tests the integration between CubicBezierWidget and CubicBezierPresets
const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
const {CubicBezierWidget} =
devtools.require("devtools/shared/widgets/CubicBezierWidget");
const {PRESETS} = require("devtools/shared/widgets/CubicBezierPresets");
add_task(function*() {
yield promiseTab("about:blank");
let [host, win, doc] = yield createHost("bottom", TEST_URI);
let container = doc.querySelector("#container");
let w = new CubicBezierWidget(container,
PRESETS["ease-in"]["ease-in-sine"]);
w.presets.refreshMenu(PRESETS["ease-in"]["ease-in-sine"]);
let rect = w.curve.getBoundingClientRect();
rect.graphTop = rect.height * w.bezierCanvas.padding[0];
yield adjustingBezierUpdatesPreset(w, win, doc, rect);
yield selectingPresetUpdatesBezier(w, win, doc, rect);
w.destroy();
host.destroy();
gBrowser.removeCurrentTab();
});
function* adjustingBezierUpdatesPreset(widget, win, doc, rect) {
info("Checking that changing the bezier refreshes the preset menu");
is(widget.presets.activeCategory,
doc.querySelector("#ease-in"),
"The selected category is ease-in");
is(widget.presets._activePreset,
doc.querySelector("#ease-in-sine"),
"The selected preset is ease-in-sine");
info("Generating custom bezier curve by dragging");
widget._onPointMouseDown({target: widget.p1});
doc.onmousemove({pageX: rect.left, pageY: rect.graphTop});
doc.onmouseup();
is(widget.presets.activeCategory,
doc.querySelector("#ease-in"),
"The selected category is still ease-in");
is(widget.presets._activePreset, null,
"There is no active preset");
}
function* selectingPresetUpdatesBezier(widget, win, doc, rect) {
info("Checking that selecting a preset updates bezier curve");
info("Listening for the new coordinates event");
let onNewCoordinates = widget.presets.once("new-coordinates");
let onUpdated = widget.once("updated");
info("Click a preset");
let preset = doc.querySelector("#ease-in-sine");
widget.presets._onPresetClick({currentTarget: preset});
yield onNewCoordinates;
ok(true, "The preset widget fired the new-coordinates event");
let bezier = yield onUpdated;
ok(true, "The bezier canvas fired the updated event");
is(bezier.P1[0], preset.coordinates[0], "The new P1 time coordinate is correct");
is(bezier.P1[1], preset.coordinates[1], "The new P1 progress coordinate is correct");
is(bezier.P2[0], preset.coordinates[2], "P2 time coordinate is correct ");
is(bezier.P2[1], preset.coordinates[3], "P2 progress coordinate is correct");
}

View File

@ -0,0 +1,50 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
let { GetDevices, GetDeviceString, AddDevice } = devtools.require("devtools/shared/devices");
add_task(function*() {
Services.prefs.setCharPref("devtools.devices.url", TEST_URI_ROOT + "browser_devices.json");
let devices = yield GetDevices();
is(devices.TYPES.length, 1, "Found 1 device type.");
let type1 = devices.TYPES[0];
is(devices[type1].length, 2, "Found 2 devices of type #1.");
let string = GetDeviceString(type1);
ok(typeof string === "string" && string.length > 0, "Able to localize type #1.");
let device1 = {
name: "SquarePhone",
width: 320,
height: 320,
pixelRatio: 2,
userAgent: "Mozilla/5.0 (Mobile; rv:42.0)",
touch: true,
firefoxOS: true
};
AddDevice(device1, type1);
devices = yield GetDevices();
is(devices[type1].length, 3, "Added new device of type #1.");
ok(devices[type1].filter(d => d.name === device1.name), "Found the new device.");
let type2 = "appliances";
let device2 = {
name: "Mr Freezer",
width: 800,
height: 600,
pixelRatio: 5,
userAgent: "Mozilla/5.0 (Appliance; rv:42.0)",
touch: true,
firefoxOS: true
};
AddDevice(device2, type2);
devices = yield GetDevices();
is(devices.TYPES.length, 2, "Added device type #2.");
is(devices[type2].length, 1, "Added new device of type #2.");
});

View File

@ -0,0 +1,23 @@
{
"TYPES": [ "phones" ],
"phones": [
{
"name": "Small Phone",
"width": 320,
"height": 480,
"pixelRatio": 1,
"userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
"touch": true,
"firefoxOS": true
},
{
"name": "Big Phone",
"width": 360,
"height": 640,
"pixelRatio": 3,
"userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
"touch": true,
"firefoxOS": true
}
]
}

View File

@ -17,14 +17,18 @@ function* performTest() {
let [host, win, doc] = yield createHost();
let graph = new LineGraphWidget(doc.body, "fps");
yield graph.once("ready");
testGraph(graph);
testGraph(graph, normalDragStop);
yield graph.destroy();
let graph2 = new LineGraphWidget(doc.body, "fps");
yield graph2.once("ready");
testGraph(graph2, buggyDragStop);
yield graph2.destroy();
host.destroy();
}
function testGraph(graph) {
function testGraph(graph, dragStop) {
graph.setData(TEST_DATA);
info("Making a selection.");
@ -186,13 +190,24 @@ function dragStart(graph, x, y = 1) {
graph._onMouseDown({ clientX: x, clientY: y });
}
function dragStop(graph, x, y = 1) {
function normalDragStop(graph, x, y = 1) {
x /= window.devicePixelRatio;
y /= window.devicePixelRatio;
graph._onMouseMove({ clientX: x, clientY: y });
graph._onMouseUp({ clientX: x, clientY: y });
}
function buggyDragStop(graph, x, y = 1) {
x /= window.devicePixelRatio;
y /= window.devicePixelRatio;
// Only fire a mousemove instead of a mouseup.
// This happens when the mouseup happens outside of the toolbox,
// see Bug 1066504.
graph._onMouseMove({ clientX: x, clientY: y });
graph._onMouseMove({ clientX: x, clientY: y, buttons: 0 });
}
function scroll(graph, wheel, x, y = 1) {
x /= window.devicePixelRatio;
y /= window.devicePixelRatio;

View File

@ -104,7 +104,10 @@ function getCanvasMock(w=200, h=400) {
stroke: () => {},
arc: () => {},
fill: () => {},
bezierCurveTo: () => {}
bezierCurveTo: () => {},
save: () => {},
restore: () => {},
setTransform: () => {}
};
},
width: w,

View File

@ -19,6 +19,7 @@ function run_test() {
coordinatesToStringOutputsAString();
pointGettersReturnPointCoordinatesArrays();
toStringOutputsCubicBezierValue();
toStringOutputsCssPresetValues();
}
function throwsWhenMissingCoordinates() {
@ -84,8 +85,27 @@ function pointGettersReturnPointCoordinatesArrays() {
function toStringOutputsCubicBezierValue() {
do_print("toString() outputs the cubic-bezier() value");
let c = new CubicBezier([0, 1, 1, 0]);
do_check_eq(c.toString(), "cubic-bezier(0,1,1,0)");
}
function toStringOutputsCssPresetValues() {
do_print("toString() outputs the css predefined values");
let c = new CubicBezier([0, 0, 1, 1]);
do_check_eq(c.toString(), "cubic-bezier(0,0,1,1)");
do_check_eq(c.toString(), "linear");
c = new CubicBezier([0.25, 0.1, 0.25, 1]);
do_check_eq(c.toString(), "ease");
c = new CubicBezier([0.42, 0, 1, 1]);
do_check_eq(c.toString(), "ease-in");
c = new CubicBezier([0, 0, 0.58, 1]);
do_check_eq(c.toString(), "ease-out");
c = new CubicBezier([0.42, 0, 0.58, 1]);
do_check_eq(c.toString(), "ease-in-out");
}
function do_check_throws(cb, info) {

View File

@ -0,0 +1,64 @@
/**
* 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/.
*/
// Set of preset definitions for use with CubicBezierWidget
// Credit: http://easings.net
"use strict";
const PREDEFINED = {
"ease": [0.25, 0.1, 0.25, 1],
"linear": [0, 0, 1, 1],
"ease-in": [0.42, 0, 1, 1],
"ease-out": [0, 0, 0.58, 1],
"ease-in-out": [0.42, 0, 0.58, 1]
};
const PRESETS = {
"ease-in": {
"ease-in-linear": [0, 0, 1, 1],
"ease-in-ease-in": [0.42, 0, 1, 1],
"ease-in-sine": [0.47, 0, 0.74, 0.71],
"ease-in-quadratic": [0.55, 0.09, 0.68, 0.53],
"ease-in-cubic": [0.55, 0.06, 0.68, 0.19],
"ease-in-quartic": [0.9, 0.03, 0.69, 0.22],
"ease-in-quintic": [0.76, 0.05, 0.86, 0.06],
"ease-in-exponential": [0.95, 0.05, 0.8, 0.04],
"ease-in-circular": [0.6, 0.04, 0.98, 0.34],
"ease-in-backward": [0.6, -0.28, 0.74, 0.05]
},
"ease-out": {
"ease-out-linear": [0, 0, 1, 1],
"ease-out-ease-out": [0, 0, 0.58, 1],
"ease-out-sine": [0.39, 0.58, 0.57, 1],
"ease-out-quadratic": [0.25, 0.46, 0.45, 0.94],
"ease-out-cubic": [0.22, 0.61, 0.36, 1],
"ease-out-quartic": [0.17, 0.84, 0.44, 1],
"ease-out-quintic": [0.23, 1, 0.32, 1],
"ease-out-exponential": [0.19, 1, 0.22, 1],
"ease-out-circular": [0.08, 0.82, 0.17, 1],
"ease-out-backward": [0.18, 0.89, 0.32, 1.28]
},
"ease-in-out": {
"ease-in-out-linear": [0, 0, 1, 1],
"ease-in-out-ease": [0.25, 0.1, 0.25, 1],
"ease-in-out-ease-in-out": [0.42, 0, 0.58, 1],
"ease-in-out-sine": [0.45, 0.05, 0.55, 0.95],
"ease-in-out-quadratic": [0.46, 0.03, 0.52, 0.96],
"ease-in-out-cubic": [0.65, 0.05, 0.36, 1],
"ease-in-out-quartic": [0.77, 0, 0.18, 1],
"ease-in-out-quintic": [0.86, 0, 0.07, 1],
"ease-in-out-exponential": [1, 0, 0, 1],
"ease-in-out-circular": [0.79, 0.14, 0.15, 0.86],
"ease-in-out-backward": [0.68, -0.55, 0.27, 1.55]
}
};
const DEFAULT_PRESET_CATEGORY = Object.keys(PRESETS)[0];
exports.PRESETS = PRESETS;
exports.PREDEFINED = PREDEFINED;
exports.DEFAULT_PRESET_CATEGORY = DEFAULT_PRESET_CATEGORY;

View File

@ -27,14 +27,7 @@
const EventEmitter = require("devtools/toolkit/event-emitter");
const {setTimeout, clearTimeout} = require("sdk/timers");
const PREDEFINED = exports.PREDEFINED = {
"ease": [.25, .1, .25, 1],
"linear": [0, 0, 1, 1],
"ease-in": [.42, 0, 1, 1],
"ease-out": [0, 0, .58, 1],
"ease-in-out": [.42, 0, .58, 1]
};
const {PREDEFINED, PRESETS, DEFAULT_PRESET_CATEGORY} = require("devtools/shared/widgets/CubicBezierPresets");
/**
* CubicBezier data structure helper
@ -59,7 +52,7 @@ function CubicBezier(coordinates) {
return this.map(n => {
return (Math.round(n * 100)/100 + '').replace(/^0\./, '.');
}) + "";
}
};
}
exports.CubicBezier = CubicBezier;
@ -74,7 +67,11 @@ CubicBezier.prototype = {
},
toString: function() {
return 'cubic-bezier(' + this.coordinates + ')';
// Check first if current coords are one of css predefined functions
let predefName = Object.keys(PREDEFINED)
.find(key => coordsAreEqual(PREDEFINED[key], this.coordinates));
return predefName || 'cubic-bezier(' + this.coordinates + ')';
}
};
@ -97,7 +94,7 @@ function BezierCanvas(canvas, bezier, padding) {
-canvas.height * (1 - p[0] - p[2]));
this.ctx.translate(p[3] / (1 - p[1] - p[3]),
-1 - p[0] / (1 - p[0] - p[2]));
};
}
exports.BezierCanvas = BezierCanvas;
@ -115,7 +112,7 @@ BezierCanvas.prototype = {
}, {
left: w * (this.bezier.coordinates[2] * (1 - p[3] - p[1]) - p[3]) + 'px',
top: h * (1 - this.bezier.coordinates[3] * (1 - p[0] - p[2]) - p[0]) + 'px'
}]
}];
},
/**
@ -128,8 +125,8 @@ BezierCanvas.prototype = {
p = p.map(function(a, i) { return a * (i % 2? w : h)});
return [
(parseInt(element.style.left) - p[3]) / (w + p[1] + p[3]),
(h - parseInt(element.style.top) - p[2]) / (h - p[0] - p[2])
(parseFloat(element.style.left) - p[3]) / (w + p[1] + p[3]),
(h - parseFloat(element.style.top) - p[2]) / (h - p[0] - p[2])
];
},
@ -143,15 +140,22 @@ BezierCanvas.prototype = {
handleColor: '#666',
handleThickness: .008,
bezierColor: '#4C9ED9',
bezierThickness: .015
bezierThickness: .015,
drawHandles: true
};
for (let setting in settings) {
defaultSettings[setting] = settings[setting];
}
this.ctx.clearRect(-.5,-.5, 2, 2);
// Clear the canvas making sure to clear the
// whole area by resetting the transform first.
this.ctx.save();
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.restore();
if (defaultSettings.drawHandles) {
// Draw control handles
this.ctx.beginPath();
this.ctx.fillStyle = defaultSettings.handleColor;
@ -166,16 +170,17 @@ BezierCanvas.prototype = {
this.ctx.stroke();
this.ctx.closePath();
function circle(ctx, cx, cy, r) {
var circle = function(ctx, cx, cy, r) {
return ctx.beginPath();
ctx.arc(cx, cy, r, 0, 2*Math.PI, !1);
ctx.closePath();
}
};
circle(this.ctx, xy[0], xy[1], 1.5 * defaultSettings.handleThickness);
this.ctx.fill();
circle(this.ctx, xy[2], xy[3], 1.5 * defaultSettings.handleThickness);
this.ctx.fill();
}
// Draw bezier curve
this.ctx.beginPath();
@ -197,18 +202,20 @@ BezierCanvas.prototype = {
* Emits "updated" events whenever the curve is changed. Along with the event is
* sent a CubicBezier object
*/
function CubicBezierWidget(parent, coordinates=PREDEFINED["ease-in-out"]) {
function CubicBezierWidget(parent, coordinates=PRESETS["ease-in"]["ease-in-sine"]) {
EventEmitter.decorate(this);
this.parent = parent;
let {curve, p1, p2} = this._initMarkup();
this.curve = curve;
this.curveBoundingBox = curve.getBoundingClientRect();
this.curve = curve;
this.p1 = p1;
this.p2 = p2;
// Create and plot the bezier curve
this.bezierCanvas = new BezierCanvas(this.curve,
new CubicBezier(coordinates), [.25, 0]);
new CubicBezier(coordinates), [0.30, 0]);
this.bezierCanvas.plot();
// Place the control points
@ -221,12 +228,15 @@ function CubicBezierWidget(parent, coordinates=PREDEFINED["ease-in-out"]) {
this._onPointMouseDown = this._onPointMouseDown.bind(this);
this._onPointKeyDown = this._onPointKeyDown.bind(this);
this._onCurveClick = this._onCurveClick.bind(this);
this._initEvents();
this._onNewCoordinates = this._onNewCoordinates.bind(this);
// Add preset preview menu
this.presets = new CubicBezierPresetWidget(parent);
// Add the timing function previewer
this.timingPreview = new TimingFunctionPreviewWidget(parent);
EventEmitter.decorate(this);
this._initEvents();
}
exports.CubicBezierWidget = CubicBezierWidget;
@ -235,6 +245,9 @@ CubicBezierWidget.prototype = {
_initMarkup: function() {
let doc = this.parent.ownerDocument;
let wrap = doc.createElement("div");
wrap.className = "display-wrap";
let plane = doc.createElement("div");
plane.className = "coordinate-plane";
@ -249,22 +262,24 @@ CubicBezierWidget.prototype = {
plane.appendChild(p2);
let curve = doc.createElement("canvas");
curve.setAttribute("height", "400");
curve.setAttribute("width", "200");
curve.setAttribute("width", 150);
curve.setAttribute("height", 370);
curve.id = "curve";
plane.appendChild(curve);
this.parent.appendChild(plane);
plane.appendChild(curve);
wrap.appendChild(plane);
this.parent.appendChild(wrap);
return {
p1: p1,
p2: p2,
curve: curve
}
};
},
_removeMarkup: function() {
this.parent.ownerDocument.querySelector(".coordinate-plane").remove();
this.parent.ownerDocument.querySelector(".display-wrap").remove();
},
_initEvents: function() {
@ -275,6 +290,8 @@ CubicBezierWidget.prototype = {
this.p2.addEventListener("keydown", this._onPointKeyDown);
this.curve.addEventListener("click", this._onCurveClick);
this.presets.on("new-coordinates", this._onNewCoordinates);
},
_removeEvents: function() {
@ -285,6 +302,8 @@ CubicBezierWidget.prototype = {
this.p2.removeEventListener("keydown", this._onPointKeyDown);
this.curve.removeEventListener("click", this._onCurveClick);
this.presets.off("new-coordinates", this._onNewCoordinates);
},
_onPointMouseDown: function(event) {
@ -317,7 +336,7 @@ CubicBezierWidget.prototype = {
doc.onmouseup = function () {
point.focus();
doc.onmousemove = doc.onmouseup = null;
}
};
},
_onPointKeyDown: function(event) {
@ -344,6 +363,8 @@ CubicBezierWidget.prototype = {
},
_onCurveClick: function(event) {
this.curveBoundingBox = this.curve.getBoundingClientRect();
let left = this.curveBoundingBox.left;
let top = this.curveBoundingBox.top;
let x = event.pageX - left;
@ -362,14 +383,19 @@ CubicBezierWidget.prototype = {
this._updateFromPoints();
},
_onNewCoordinates: function(event, coordinates) {
this.coordinates = coordinates;
},
/**
* Get the current point coordinates and redraw the curve to match
*/
_updateFromPoints: function() {
// Get the new coordinates from the point's offsets
let coordinates = this.bezierCanvas.offsetsToCoordinates(this.p1)
let coordinates = this.bezierCanvas.offsetsToCoordinates(this.p1);
coordinates = coordinates.concat(this.bezierCanvas.offsetsToCoordinates(this.p2));
this.presets.refreshMenu(coordinates);
this._redraw(coordinates);
},
@ -391,7 +417,7 @@ CubicBezierWidget.prototype = {
* @param {Array} coordinates
*/
set coordinates(coordinates) {
this._redraw(coordinates)
this._redraw(coordinates);
// Move the points
let offsets = this.bezierCanvas.offsets;
@ -420,6 +446,7 @@ CubicBezierWidget.prototype = {
coordinates = value.replace(/cubic-bezier|\(|\)/g, "").split(",").map(parseFloat);
}
this.presets.refreshMenu(coordinates);
this.coordinates = coordinates;
},
@ -428,11 +455,262 @@ CubicBezierWidget.prototype = {
this._removeMarkup();
this.timingPreview.destroy();
this.presets.destroy();
this.curve = this.p1 = this.p2 = null;
}
};
/**
* CubicBezierPreset widget.
* Builds a menu of presets from CubicBezierPresets
* @param {DOMNode} parent The container where the preset panel should be created
*
* Emits "new-coordinate" event along with the coordinates
* whenever a preset is selected.
*/
function CubicBezierPresetWidget(parent) {
this.parent = parent;
let {presetPane, presets, categories} = this._initMarkup();
this.presetPane = presetPane;
this.presets = presets;
this.categories = categories;
this._activeCategory = null;
this._activePresetList = null;
this._activePreset = null;
this._onCategoryClick = this._onCategoryClick.bind(this);
this._onPresetClick = this._onPresetClick.bind(this);
EventEmitter.decorate(this);
this._initEvents();
}
exports.CubicBezierPresetWidget = CubicBezierPresetWidget;
CubicBezierPresetWidget.prototype = {
/*
* Constructs a list of all preset categories and a list
* of presets for each category.
*
* High level markup:
* div .preset-pane
* div .preset-categories
* div .category
* div .category
* ...
* div .preset-container
* div .presetList
* div .preset
* ...
* div .presetList
* div .preset
* ...
*/
_initMarkup: function() {
let doc = this.parent.ownerDocument;
let presetPane = doc.createElement("div");
presetPane.className = "preset-pane";
let categoryList = doc.createElement("div");
categoryList.id = "preset-categories";
let presetContainer = doc.createElement("div");
presetContainer.id = "preset-container";
Object.keys(PRESETS).forEach(categoryLabel => {
let category = this._createCategory(categoryLabel);
categoryList.appendChild(category);
let presetList = this._createPresetList(categoryLabel);
presetContainer.appendChild(presetList);
});
presetPane.appendChild(categoryList);
presetPane.appendChild(presetContainer);
this.parent.appendChild(presetPane);
let allCategories = presetPane.querySelectorAll(".category");
let allPresets = presetPane.querySelectorAll(".preset");
return {
presetPane: presetPane,
presets: allPresets,
categories: allCategories
};
},
_createCategory: function(categoryLabel) {
let doc = this.parent.ownerDocument;
let category = doc.createElement("div");
category.id = categoryLabel;
category.classList.add("category");
let categoryDisplayLabel = this._normalizeCategoryLabel(categoryLabel);
category.textContent = categoryDisplayLabel;
return category;
},
_normalizeCategoryLabel: function(categoryLabel) {
return categoryLabel.replace("/-/g", " ");
},
_createPresetList: function(categoryLabel) {
let doc = this.parent.ownerDocument;
let presetList = doc.createElement("div");
presetList.id = "preset-category-" + categoryLabel;
presetList.classList.add("preset-list");
Object.keys(PRESETS[categoryLabel]).forEach(presetLabel => {
let preset = this._createPreset(categoryLabel, presetLabel);
presetList.appendChild(preset);
});
return presetList;
},
_createPreset: function(categoryLabel, presetLabel) {
let doc = this.parent.ownerDocument;
let preset = doc.createElement("div");
preset.classList.add("preset");
preset.id = presetLabel;
preset.coordinates = PRESETS[categoryLabel][presetLabel];
// Create preset preview
let curve = doc.createElement("canvas");
let bezier = new CubicBezier(preset.coordinates);
curve.setAttribute("height", 55);
curve.setAttribute("width", 55);
preset.bezierCanvas = new BezierCanvas(curve, bezier, [0.15, 0]);
preset.bezierCanvas.plot({
drawHandles: false,
bezierThickness: 0.025
});
preset.appendChild(curve);
// Create preset label
let presetLabelElem = doc.createElement("p");
let presetDisplayLabel = this._normalizePresetLabel(categoryLabel, presetLabel);
presetLabelElem.textContent = presetDisplayLabel;
preset.appendChild(presetLabelElem);
return preset;
},
_normalizePresetLabel: function(categoryLabel, presetLabel) {
return presetLabel.replace(categoryLabel + "-", "").replace("/-/g", " ");
},
_initEvents: function() {
for (let category of this.categories) {
category.addEventListener("click", this._onCategoryClick);
}
for (let preset of this.presets) {
preset.addEventListener("click", this._onPresetClick);
}
},
_removeEvents: function() {
for (let category of this.categories) {
category.removeEventListener("click", this._onCategoryClick);
}
for (let preset of this.presets) {
preset.removeEventListener("click", this._onPresetClick);
}
},
_onPresetClick: function(event) {
this.emit("new-coordinates", event.currentTarget.coordinates);
this.activePreset = event.currentTarget;
},
_onCategoryClick: function(event) {
this.activeCategory = event.target;
},
_setActivePresetList: function(presetListId) {
let presetList = this.presetPane.querySelector("#" + presetListId);
swapClassName("active-preset-list", this._activePresetList, presetList);
this._activePresetList = presetList;
},
set activeCategory(category) {
swapClassName("active-category", this._activeCategory, category);
this._activeCategory = category;
this._setActivePresetList("preset-category-" + category.id);
},
get activeCategory() {
return this._activeCategory;
},
set activePreset(preset) {
swapClassName("active-preset", this._activePreset, preset);
this._activePreset = preset;
},
get activePreset() {
return this._activePreset;
},
/**
* Called by CubicBezierWidget onload and when
* the curve is modified via the canvas.
* Attempts to match the new user setting with an
* existing preset.
* @param {Array} coordinates new coords [i, j, k, l]
*/
refreshMenu: function(coordinates) {
// If we cannot find a matching preset, keep
// menu on last known preset category.
let category = this._activeCategory;
// If we cannot find a matching preset
// deselect any selected preset.
let preset = null;
// If a category has never been viewed before
// show the default category.
if (!category) {
category = this.parent.querySelector("#" + DEFAULT_PRESET_CATEGORY);
}
// If the new coordinates do match a preset,
// set its category and preset button as active.
Object.keys(PRESETS).forEach(categoryLabel => {
Object.keys(PRESETS[categoryLabel]).forEach(presetLabel => {
if (coordsAreEqual(PRESETS[categoryLabel][presetLabel], coordinates)) {
category = this.parent.querySelector("#" + categoryLabel);
preset = this.parent.querySelector("#" + presetLabel);
}
});
});
this.activeCategory = category;
this.activePreset = preset;
},
destroy: function() {
this._removeEvents();
this.parent.querySelector(".preset-pane").remove();
}
};
/**
* The TimingFunctionPreviewWidget animates a dot on a scale with a given
* timing-function
@ -554,3 +832,29 @@ function isValidTimingFunction(value) {
return false;
}
/**
* Removes a class from a node and adds it to another.
* @param {String} className the class to swap
* @param {DOMNode} from the node to remove the class from
* @param {DOMNode} to the node to add the class to
*/
function swapClassName(className, from, to) {
if (from !== null) {
from.classList.remove(className);
}
if (to !== null) {
to.classList.add(className);
}
}
/**
* Compares two arrays of coordinates [i, j, k, l]
* @param {Array} c1 first coordinate array to compare
* @param {Array} c2 second coordinate array to compare
* @return {Boolean}
*/
function coordsAreEqual(c1, c2) {
return c1.reduce((prev, curr, index) => prev && (curr === c2[index]), true);
}

View File

@ -181,6 +181,7 @@ this.AbstractCanvasGraph = function(parent, name, sharpness) {
this._selection = new GraphArea();
this._selectionDragger = new GraphAreaDragger();
this._selectionResizer = new GraphAreaResizer();
this._isMouseActive = false;
this._onAnimationFrame = this._onAnimationFrame.bind(this);
this._onMouseMove = this._onMouseMove.bind(this);
@ -952,13 +953,23 @@ AbstractCanvasGraph.prototype = {
* Listener for the "mousemove" event on the graph's container.
*/
_onMouseMove: function(e) {
let resizer = this._selectionResizer;
let dragger = this._selectionDragger;
// If a mouseup happened outside the toolbox and the current operation
// is causing the selection changed, then end it.
if (e.buttons == 0 && (this.hasSelectionInProgress() ||
resizer.margin != null ||
dragger.origin != null)) {
return this._onMouseUp(e);
}
let offset = this._getContainerOffset();
let mouseX = (e.clientX - offset.left) * this._pixelRatio;
let mouseY = (e.clientY - offset.top) * this._pixelRatio;
this._cursor.x = mouseX;
this._cursor.y = mouseY;
let resizer = this._selectionResizer;
if (resizer.margin != null) {
this._selection[resizer.margin] = mouseX;
this._shouldRedraw = true;
@ -966,7 +977,6 @@ AbstractCanvasGraph.prototype = {
return;
}
let dragger = this._selectionDragger;
if (dragger.origin != null) {
this._selection.start = dragger.anchor.start - dragger.origin + mouseX;
this._selection.end = dragger.anchor.end - dragger.origin + mouseX;
@ -1013,6 +1023,7 @@ AbstractCanvasGraph.prototype = {
* Listener for the "mousedown" event on the graph's container.
*/
_onMouseDown: function(e) {
this._isMouseActive = true;
let offset = this._getContainerOffset();
let mouseX = (e.clientX - offset.left) * this._pixelRatio;
@ -1051,6 +1062,7 @@ AbstractCanvasGraph.prototype = {
* Listener for the "mouseup" event on the graph's container.
*/
_onMouseUp: function(e) {
this._isMouseActive = false;
let offset = this._getContainerOffset();
let mouseX = (e.clientX - offset.left) * this._pixelRatio;
@ -1163,19 +1175,15 @@ AbstractCanvasGraph.prototype = {
/**
* Listener for the "mouseout" event on the graph's container.
* Clear any active cursors if a drag isn't happening.
*/
_onMouseOut: function() {
if (this.hasSelectionInProgress()) {
this.dropSelection();
}
_onMouseOut: function(e) {
if (!this._isMouseActive) {
this._cursor.x = null;
this._cursor.y = null;
this._selectionResizer.margin = null;
this._selectionDragger.origin = null;
this._canvas.removeAttribute("input");
this._shouldRedraw = true;
}
},
/**

View File

@ -795,8 +795,8 @@ Tooltip.prototype = {
// Create an iframe to host the cubic-bezier widget
let iframe = this.doc.createElementNS(XHTML_NS, "iframe");
iframe.setAttribute("transparent", true);
iframe.setAttribute("width", "200");
iframe.setAttribute("height", "415");
iframe.setAttribute("width", "410");
iframe.setAttribute("height", "360");
iframe.setAttribute("flex", "1");
iframe.setAttribute("class", "devtools-tooltip-iframe");

View File

@ -8,14 +8,15 @@
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/>
<link rel="stylesheet" href="chrome://browser/content/devtools/cubic-bezier.css" ype="text/css"/>
<link rel="stylesheet" href="chrome://browser/content/devtools/cubic-bezier.css" type="text/css"/>
<script type="application/javascript;version=1.8" src="theme-switching.js"/>
<style>
body {
html, body {
margin: 0;
padding: 0;
width: 200px;
height: 415px;
overflow: hidden;
width: 410px;
height: 370px;
}
</style>
</head>

View File

@ -5,32 +5,28 @@
/* Based on Lea Verou www.cubic-bezier.com
See https://github.com/LeaVerou/cubic-bezier */
#container {
display: flex;
width: 410px;
height: 370px;
flex-direction: row-reverse;
overflow: hidden;
}
.display-wrap {
width: 50%;
height: 100%;
text-align: center;
overflow: hidden;
}
/* Coordinate Plane */
.coordinate-plane {
position: absolute;
line-height: 0;
height: 400px;
width: 200px;
}
.coordinate-plane:before,
.coordinate-plane:after {
position: absolute;
bottom: 25%;
left: 0;
width: 100%;
}
.coordinate-plane:before {
content: "";
border-bottom: 2px solid;
transform: rotate(-90deg) translateY(2px);
transform-origin: bottom left;
}
.coordinate-plane:after {
content: "";
border-top: 2px solid;
margin-bottom: -2px;
width: 150px;
height: 370px;
margin: 0 auto;
position: relative;
}
.theme-dark .coordinate-plane:before,
@ -50,45 +46,41 @@
outline: none;
border-radius: 5px;
padding: 0;
cursor: pointer;
}
#P1x, #P1y {
color: #f08;
}
#P2x, #P2y {
color: #0ab;
}
canvas#curve {
background:
linear-gradient(-45deg, transparent 49.7%, rgba(0,0,0,.2) 49.7%, rgba(0,0,0,.2) 50.3%, transparent 50.3%) center no-repeat,
repeating-linear-gradient(transparent, #eee 0, #eee .5%, transparent .5%, transparent 10%) no-repeat,
repeating-linear-gradient(-90deg, transparent, #eee 0, #eee .5%, transparent .5%, transparent 10%) no-repeat;
background-size: 100% 50%, 100% 50%, 100% 50%;
background-position: 25%, 0, 0;
.display-wrap {
background: repeating-linear-gradient(0deg, transparent, rgba(0, 0, 0, 0.05) 0, rgba(0, 0, 0, 0.05) 1px, transparent 1px, transparent 15px) no-repeat, repeating-linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.05) 0, rgba(0, 0, 0, 0.05) 1px, transparent 1px, transparent 15px) no-repeat;
background-size: 100% 100%, 100% 100%;
background-position: -2px 5px, -2px 5px;
-moz-user-select: none;
}
.theme-dark canvas#curve {
background:
linear-gradient(-45deg, transparent 49.7%, #eee 49.7%, #eee 50.3%, transparent 50.3%) center no-repeat,
repeating-linear-gradient(transparent, rgba(0,0,0,.2) 0, rgba(0,0,0,.2) .5%, transparent .5%, transparent 10%) no-repeat,
repeating-linear-gradient(-90deg, transparent, rgba(0,0,0,.2) 0, rgba(0,0,0,.2) .5%, transparent .5%, transparent 10%) no-repeat;
.theme-dark .display-wrap {
background: repeating-linear-gradient(0deg, transparent, rgba(0, 0, 0, 0.2) 0, rgba(0, 0, 0, 0.2) 1px, transparent 1px, transparent 15px) no-repeat, repeating-linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.2) 0, rgba(0, 0, 0, 0.2) 1px, transparent 1px, transparent 15px) no-repeat;
background-size: 100% 100%, 100% 100%;
background-position: -2px 5px, -2px 5px;
background-size: 100% 50%, 100% 50%, 100% 50%;
background-position: 25%, 0, 0;
-moz-user-select: none;
}
canvas#curve {
background: linear-gradient(-45deg, transparent 49.7%, rgba(0,0,0,.2) 49.7%, rgba(0,0,0,.2) 50.3%, transparent 50.3%) center no-repeat;
background-size: 100% 100%;
background-position: 0 0;
}
/* Timing function preview widget */
.theme-dark canvas#curve {
background: linear-gradient(-45deg, transparent 49.7%, #eee 49.7%, #eee 50.3%, transparent 50.3%) center no-repeat;
}
/* Timing Function Preview Widget */
.timing-function-preview {
position: absolute;
top: 400px;
bottom: 20px;
right: 27px;
width: 150px;
}
.timing-function-preview .scale {
@ -97,7 +89,7 @@ canvas#curve {
left: 0;
z-index: 1;
width: 200px;
width: 150px;
height: 1px;
background: #ccc;
@ -128,10 +120,10 @@ canvas#curve {
left: -7px;
}
33% {
left: 193px;
left: 143px;
}
50% {
left: 193px;
left: 143px;
}
83% {
left: -7px;
@ -140,3 +132,109 @@ canvas#curve {
left: -7px;
}
}
/* Preset Widget */
.preset-pane {
width:50%;
height: 100%;
border-right: 1px solid var(--theme-splitter-color);
}
#preset-categories {
display: flex;
width: 94%;
border: 1px solid var(--theme-splitter-color);
border-radius: 2px;
background-color: var(--theme-toolbar-background);
margin-left: 4px;
margin-top: 3px;
}
#preset-categories .category:last-child {
border-right: none;
}
.category {
flex: 1 1 auto;
padding: 5px;
width: 33.33%;
text-align: center;
text-transform: capitalize;
border-right: 1px solid var(--theme-splitter-color);
cursor: default;
color: var(--theme-body-color);
}
.category:hover {
background-color: var(--theme-tab-toolbar-background);
}
.active-category {
background-color: var(--theme-selection-background);
color: var(--theme-selection-color);
}
.active-category:hover {
background-color: var(--theme-selection-background);
}
#preset-container {
padding: 0px;
width: 100%;
height: 331px;
overflow-y: scroll;
}
.preset-list {
display: none;
}
.active-preset-list {
display: flex;
flex-wrap: wrap;
justify-content: left;
padding-left: 4px;
padding-top: 3px;
}
.preset {
cursor: pointer;
width: 55px;
margin: 5px 11px 0px 0px;
text-transform: capitalize;
text-align: center;
}
.preset canvas {
display: block;
border: 1px solid #ccc;
border-radius: 3px;
background-color: var(--theme-body-background);
}
.theme-dark .preset canvas {
border-color: #444e58;
}
.preset p {
text-align: center;
font-size: 0.9em;
line-height: 0px;
margin: 2px 0px 0px 0p;
color: var(--theme-body-color-alt);
}
.active-preset p, .active-preset:hover p {
color: var(--theme-body-color);
}
.preset:hover canvas {
border-color: var(--theme-selection-background);
}
.active-preset canvas, .active-preset:hover canvas,
.theme-dark .active-preset canvas, .theme-dark .preset:hover canvas {
background-color: var(--theme-selection-background-semitransparent);
border-color: var(--theme-selection-background);
}

View File

@ -15,9 +15,10 @@ XPCOMUtils.defineLazyModuleGetter(this, "Downloads", "resource://gre/modules/Dow
const {require} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm");
const {AppProjects} = require("devtools/app-manager/app-projects");
const APP_CREATOR_LIST = "devtools.webide.templatesURL";
const {AppManager} = require("devtools/webide/app-manager");
const {GetTemplatesJSON} = require("devtools/webide/remote-resources");
const {getJSON} = require("devtools/shared/getjson");
const TEMPLATES_URL = "devtools.webide.templatesURL";
let gTemplateList = null;
@ -30,11 +31,11 @@ window.addEventListener("load", function onLoad() {
window.removeEventListener("load", onLoad);
let projectNameNode = document.querySelector("#project-name");
projectNameNode.addEventListener("input", canValidate, true);
getJSON();
getTemplatesJSON();
}, true);
function getJSON() {
GetTemplatesJSON().then(list => {
function getTemplatesJSON() {
getJSON(TEMPLATES_URL).then(list => {
if (!Array.isArray(list)) {
throw new Error("JSON response not an array");
}

View File

@ -19,7 +19,7 @@ const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
const ProjectEditor = require("projecteditor/projecteditor");
const {Devices} = Cu.import("resource://gre/modules/devtools/Devices.jsm");
const {GetAvailableAddons} = require("devtools/webide/addons");
const {GetTemplatesJSON, GetAddonsJSON} = require("devtools/webide/remote-resources");
const {getJSON} = require("devtools/shared/getjson");
const utils = require("devtools/webide/utils");
const Telemetry = require("devtools/shared/telemetry");
const {RuntimeScanners, WiFiScanner} = require("devtools/webide/runtimes");
@ -34,8 +34,9 @@ const HELP_URL = "https://developer.mozilla.org/docs/Tools/WebIDE/Troubleshootin
const MAX_ZOOM = 1.4;
const MIN_ZOOM = 0.6;
// download template index early
GetTemplatesJSON(true);
// Download remote resources early
getJSON("devtools.webide.addonsURL", true);
getJSON("devtools.webide.templatesURL", true);
// See bug 989619
console.log = console.log.bind(console);

View File

@ -5,9 +5,11 @@
const {Cu} = require("chrome");
const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm");
const {AddonManager} = Cu.import("resource://gre/modules/AddonManager.jsm");
const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js");
const {Services} = Cu.import("resource://gre/modules/Services.jsm");
const {GetAddonsJSON} = require("devtools/webide/remote-resources");
const {getJSON} = require("devtools/shared/getjson");
const EventEmitter = require("devtools/toolkit/event-emitter");
const ADDONS_URL = "devtools.webide.addonsURL";
let SIMULATOR_LINK = Services.prefs.getCharPref("devtools.webide.simulatorAddonsURL");
let ADB_LINK = Services.prefs.getCharPref("devtools.webide.adbAddonURL");
@ -54,7 +56,7 @@ let GetAvailableAddons = exports.GetAvailableAddons = function() {
simulators: [],
adb: null
}
GetAddonsJSON(true).then(json => {
getJSON(ADDONS_URL, true).then(json => {
for (let stability in json) {
for (let version of json[stability]) {
addons.simulators.push(new SimulatorAddon(stability, version));

View File

@ -25,7 +25,6 @@ EXTRA_JS_MODULES.devtools.webide += [
'modules/build.js',
'modules/config-view.js',
'modules/project-list.js',
'modules/remote-resources.js',
'modules/runtimes.js',
'modules/simulator-process.js',
'modules/simulators.js',

View File

@ -14,6 +14,7 @@
# to simulate (e.g. "ZTE Open C", "VIA Vixen", "720p HD Television", etc).
device.phones=Phones
device.tablets=Tablets
device.notebooks=Notebooks
device.laptops=Laptops
device.televisions=TVs
device.consoles=Gaming consoles
device.watches=Watches

View File

@ -544,7 +544,7 @@ pref("services.sync.registerEngines", "Tab,Bookmarks,Form,History,Password,Prefs
// prefs to sync by default
pref("services.sync.prefs.sync.browser.tabs.warnOnClose", true);
pref("services.sync.prefs.sync.devtools.errorconsole.enabled", true);
pref("services.sync.prefs.sync.lightweightThemes.isThemeSelected", true);
pref("services.sync.prefs.sync.lightweightThemes.selectedThemeID", true);
pref("services.sync.prefs.sync.lightweightThemes.usedThemes", true);
pref("services.sync.prefs.sync.privacy.donottrackheader.enabled", true);
pref("services.sync.prefs.sync.privacy.donottrackheader.value", true);

View File

@ -13,6 +13,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils","resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ReadingList", "resource:///modules/readinglist/ReadingList.jsm");
@ -56,7 +57,18 @@ let ReaderParent = {
break;
case "Reader:FaviconRequest": {
// XXX: To implement.
if (message.target.messageManager) {
let faviconUrl = PlacesUtils.promiseFaviconLinkUrl(message.data.url);
faviconUrl.then(function onResolution(favicon) {
message.target.messageManager.sendAsyncMessage("Reader:FaviconReturn", {
url: message.data.url,
faviconUrl: favicon.path.replace(/^favicon:/, "")
})
},
function onRejection(reason) {
Cu.reportError("Error requesting favicon URL for about:reader content: " + reason);
}).catch(Cu.reportError);
}
break;
}
case "Reader:ListStatusRequest":

View File

@ -50,13 +50,18 @@ body {
border: 1px solid white;
box-shadow: 0px 1px 2px rgba(0,0,0,.35);
margin: 5px;
background-color: #fff;
background-size: cover;
background-color: #ebebeb;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
background-image: url("chrome://branding/content/silhouette-40.svg");
}
.item-thumb-container.preview-available {
background-color: #fff;
background-size: cover;
}
.item-summary-container {
display: flex;
flex-flow: column;
@ -89,7 +94,7 @@ body {
}
.item:not(:hover):not(.selected) .remove-button {
display: none;
visibility: hidden;
}
.remove-button {

View File

@ -43,7 +43,7 @@ class TextSelection extends Layer implements GeckoEventListener {
private final DrawListener mDrawListener;
private boolean mDraggingHandles;
private int selectionID; // Unique ID provided for each selection action.
private String selectionID; // Unique ID provided for each selection action.
private float mViewLeft;
private float mViewTop;
private float mViewZoom;
@ -132,7 +132,7 @@ class TextSelection extends Layer implements GeckoEventListener {
public void run() {
try {
if (event.equals("TextSelection:ShowHandles")) {
selectionID = message.getInt("selectionID");
selectionID = message.getString("selectionID");
final JSONArray handles = message.getJSONArray("handles");
for (int i=0; i < handles.length(); i++) {
String handle = handles.getString(i);

View File

@ -13,7 +13,7 @@ var InputWidgetHelper = {
handleClick: function(aTarget) {
// if we're busy looking at a InputWidget we want to eat any clicks that
// come to us, but not to process them
if (this._uiBusy || !this.hasInputWidget(aTarget))
if (this._uiBusy || !this.hasInputWidget(aTarget) || this._isDisabledElement(aTarget))
return;
this._uiBusy = true;
@ -81,5 +81,16 @@ var InputWidgetHelper = {
setTimeout(function() {
aElement.dispatchEvent(evt);
}, 0);
},
_isDisabledElement : function(aElement) {
let currentElement = aElement;
while (currentElement) {
if (currentElement.disabled)
return true;
currentElement = currentElement.parentElement;
}
return false;
}
};

View File

@ -45,7 +45,7 @@ var SelectionHandler = {
_activeType: 0, // TYPE_NONE
_selectionPrivate: null, // private selection reference
_selectionID: 0, // Unique Selection ID
_selectionID: null, // Unique Selection ID
_draggingHandles: false, // True while user drags text selection handles
_dragStartAnchorOffset: null, // Editables need initial pos during HandleMove events
@ -84,6 +84,13 @@ var SelectionHandler = {
getInterface(Ci.nsIDOMWindowUtils);
},
// Provides UUID service for selection ID's.
get _idService() {
delete this._idService;
return this._idService = Cc["@mozilla.org/uuid-generator;1"].
getService(Ci.nsIUUIDGenerator);
},
_addObservers: function sh_addObservers() {
Services.obs.addObserver(this, "Gesture:SingleTap", false);
Services.obs.addObserver(this, "Tab:Selected", false);
@ -828,7 +835,7 @@ var SelectionHandler = {
aElement.focus();
}
this._selectionID++;
this._selectionID = this._idService.generateUUID().toString();
this._stopDraggingHandles();
this._contentWindow = aElement.ownerDocument.defaultView;
this._targetIsRTL = (this._contentWindow.getComputedStyle(aElement, "").direction == "rtl");

View File

@ -7748,7 +7748,7 @@ var Distribution = {
}
// Apply a lightweight theme if necessary
if (prefs && prefs["lightweightThemes.isThemeSelected"]) {
if (prefs && prefs["lightweightThemes.selectedThemeID"]) {
Services.obs.notifyObservers(null, "lightweight-theme-apply", "");
}

View File

@ -837,6 +837,9 @@ pref("devtools.remote.wifi.visible", false);
// Client must complete TLS handshake within this window (ms)
pref("devtools.remote.tls-handshake-timeout", 10000);
// URL of the remote JSON catalog used for device simulation
pref("devtools.devices.url", "https://code.cdn.mozilla.net/devices/devices.json");
// view source
pref("view_source.syntax_highlight", true);
pref("view_source.wrap_long_lines", false);
@ -4525,7 +4528,7 @@ pref("browser.addon-watch.interval", 15000);
#else
pref("browser.addon-watch.interval", -1);
#endif
pref("browser.addon-watch.ignore", "[\"mochikit@mozilla.org\",\"special-powers@mozilla.org\"]");
pref("browser.addon-watch.ignore", "[\"mochikit@mozilla.org\",\"special-powers@mozilla.org\",\"fxdevtools-adapters@mozilla.org\",\"fx-devtools\"]");
// the percentage of time addons are allowed to use without being labeled slow
pref("browser.addon-watch.percentage-limit", 5);

View File

@ -110,9 +110,8 @@ PrefStore.prototype = {
},
_setAllPrefs: function PrefStore__setAllPrefs(values) {
let enabledPref = "lightweightThemes.isThemeSelected";
let enabledBefore = this._prefs.get(enabledPref, false);
let prevTheme = LightweightThemeManager.currentTheme;
let selectedThemeIDPref = "lightweightThemes.selectedThemeID";
let selectedThemeIDBefore = this._prefs.get(selectedThemeIDPref, null);
for (let [pref, value] in Iterator(values)) {
if (!this._isSynced(pref))
@ -131,13 +130,14 @@ PrefStore.prototype = {
}
}
// Notify the lightweight theme manager of all the new values
let enabledNow = this._prefs.get(enabledPref, false);
if (enabledBefore && !enabledNow) {
// Notify the lightweight theme manager if the selected theme has changed.
let selectedThemeIDAfter = this._prefs.get(selectedThemeIDPref, null);
if (selectedThemeIDBefore != selectedThemeIDAfter) {
// The currentTheme getter will reflect the theme with the new
// selectedThemeID (if there is one). Just reset it to itself
let currentTheme = LightweightThemeManager.currentTheme;
LightweightThemeManager.currentTheme = null;
} else if (enabledNow && LightweightThemeManager.usedThemes[0] != prevTheme) {
LightweightThemeManager.currentTheme = null;
LightweightThemeManager.currentTheme = LightweightThemeManager.usedThemes[0];
LightweightThemeManager.currentTheme = currentTheme;
}
},

View File

@ -95,28 +95,28 @@ function run_test() {
// Ensure we don't go to the network to fetch personas and end up leaking
// stuff.
Services.io.offline = true;
do_check_false(!!prefs.get("lightweightThemes.isThemeSelected"));
do_check_false(!!prefs.get("lightweightThemes.selectedThemeID"));
do_check_eq(LightweightThemeManager.currentTheme, null);
let persona1 = makePersona();
let persona2 = makePersona();
let usedThemes = JSON.stringify([persona1, persona2]);
record.value = {
"lightweightThemes.isThemeSelected": true,
"lightweightThemes.selectedThemeID": persona1.id,
"lightweightThemes.usedThemes": usedThemes
};
store.update(record);
do_check_true(prefs.get("lightweightThemes.isThemeSelected"));
do_check_eq(prefs.get("lightweightThemes.selectedThemeID"), persona1.id);
do_check_true(Utils.deepEquals(LightweightThemeManager.currentTheme,
persona1));
_("Disable persona");
record.value = {
"lightweightThemes.isThemeSelected": false,
"lightweightThemes.selectedThemeID": null,
"lightweightThemes.usedThemes": usedThemes
};
store.update(record);
do_check_false(prefs.get("lightweightThemes.isThemeSelected"));
do_check_false(!!prefs.get("lightweightThemes.selectedThemeID"));
do_check_eq(LightweightThemeManager.currentTheme, null);
_("Only the current app's preferences are applied.");

View File

@ -16,6 +16,8 @@ class mozIStorageAsyncStatement;
namespace mozilla {
namespace storage {
class AsyncStatement;
/*
* Since mozIStorageStatementParams is just a tagging interface we do not have
* an async variant.

View File

@ -8,6 +8,7 @@
#define MOZSTORAGESTATEMENTJSHELPER_H
#include "nsIXPCScriptable.h"
#include "nsIXPConnect.h"
class Statement;

View File

@ -14,6 +14,8 @@
namespace mozilla {
namespace storage {
class Statement;
class StatementRow final : public mozIStorageStatementRow
, public nsIXPCScriptable
{

View File

@ -732,6 +732,18 @@ nsAutoCompleteController::GetSearchString(nsAString &aSearchString)
return NS_OK;
}
void
nsAutoCompleteController::HandleSearchResult(nsIAutoCompleteSearch *aSearch,
nsIAutoCompleteResult *aResult)
{
// Look up the index of the search which is returning.
for (uint32_t i = 0; i < mSearches.Length(); ++i) {
if (mSearches[i] == aSearch) {
ProcessResult(i, aResult);
}
}
}
////////////////////////////////////////////////////////////////////////
//// nsIAutoCompleteObserver
@ -739,18 +751,41 @@ nsAutoCompleteController::GetSearchString(nsAString &aSearchString)
NS_IMETHODIMP
nsAutoCompleteController::OnUpdateSearchResult(nsIAutoCompleteSearch *aSearch, nsIAutoCompleteResult* aResult)
{
MOZ_ASSERT(mSearches.Contains(aSearch));
ClearResults();
return OnSearchResult(aSearch, aResult);
HandleSearchResult(aSearch, aResult);
return NS_OK;
}
NS_IMETHODIMP
nsAutoCompleteController::OnSearchResult(nsIAutoCompleteSearch *aSearch, nsIAutoCompleteResult* aResult)
{
// look up the index of the search which is returning
for (uint32_t i = 0; i < mSearches.Length(); ++i) {
if (mSearches[i] == aSearch) {
ProcessResult(i, aResult);
MOZ_ASSERT(mSearchesOngoing > 0 && mSearches.Contains(aSearch));
// If this is the first search result we are processing
// we should clear out the previously cached results.
if (mFirstSearchResult) {
ClearResults();
mFirstSearchResult = false;
}
uint16_t result = 0;
if (aResult) {
aResult->GetSearchResult(&result);
}
// If our results are incremental, the search is still ongoing.
if (result != nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING &&
result != nsIAutoCompleteResult::RESULT_NOMATCH_ONGOING) {
--mSearchesOngoing;
}
HandleSearchResult(aSearch, aResult);
if (mSearchesOngoing == 0) {
// If this is the last search to return, cleanup.
PostSearchCleanup();
}
return NS_OK;
@ -1075,8 +1110,13 @@ nsAutoCompleteController::StartSearch(uint16_t aSearchType)
NS_ENSURE_STATE(mInput);
nsCOMPtr<nsIAutoCompleteInput> input = mInput;
for (uint32_t i = 0; i < mSearches.Length(); ++i) {
nsCOMPtr<nsIAutoCompleteSearch> search = mSearches[i];
// Iterate a copy of |mSearches| so that we don't run into trouble if the
// array is mutated while we're still in the loop. An nsIAutoCompleteSearch
// implementation could synchronously start a new search when StartSearch()
// is called and that would lead to assertions down the way.
nsCOMArray<nsIAutoCompleteSearch> searchesCopy(mSearches);
for (uint32_t i = 0; i < searchesCopy.Length(); ++i) {
nsCOMPtr<nsIAutoCompleteSearch> search = searchesCopy[i];
// Filter on search type. Not all the searches implement this interface,
// in such a case just consider them delayed.
@ -1107,6 +1147,7 @@ nsAutoCompleteController::StartSearch(uint16_t aSearchType)
rv = search->StartSearch(mSearchString, searchParam, result, static_cast<nsIAutoCompleteObserver *>(this));
if (NS_FAILED(rv)) {
++mSearchesFailed;
MOZ_ASSERT(mSearchesOngoing > 0);
--mSearchesOngoing;
}
// Because of the joy of nested event loops (which can easily happen when some
@ -1429,23 +1470,10 @@ nsAutoCompleteController::ProcessResult(int32_t aSearchIndex, nsIAutoCompleteRes
NS_ENSURE_STATE(mInput);
nsCOMPtr<nsIAutoCompleteInput> input(mInput);
// If this is the first search result we are processing
// we should clear out the previously cached results
if (mFirstSearchResult) {
ClearResults();
mFirstSearchResult = false;
}
uint16_t result = 0;
if (aResult)
aResult->GetSearchResult(&result);
// if our results are incremental, the search is still ongoing
if (result != nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING &&
result != nsIAutoCompleteResult::RESULT_NOMATCH_ONGOING) {
--mSearchesOngoing;
}
uint32_t oldMatchCount = 0;
uint32_t matchCount = 0;
if (aResult)
@ -1505,7 +1533,7 @@ nsAutoCompleteController::ProcessResult(int32_t aSearchIndex, nsIAutoCompleteRes
// get results in the future to avoid unnecessarily canceling searches.
if (mRowCount || !minResults) {
OpenPopup();
} else if (result != nsIAutoCompleteResult::RESULT_NOMATCH_ONGOING) {
} else if (mSearchesOngoing == 0) {
ClosePopup();
}
}
@ -1516,11 +1544,6 @@ nsAutoCompleteController::ProcessResult(int32_t aSearchIndex, nsIAutoCompleteRes
CompleteDefaultIndex(resultIndex);
}
if (mSearchesOngoing == 0) {
// If this is the last search to return, cleanup.
PostSearchCleanup();
}
return NS_OK;
}

View File

@ -50,6 +50,8 @@ protected:
nsresult ClearSearchTimer();
void MaybeCompletePlaceholder();
void HandleSearchResult(nsIAutoCompleteSearch *aSearch,
nsIAutoCompleteResult *aResult);
nsresult ProcessResult(int32_t aSearchIndex, nsIAutoCompleteResult *aResult);
nsresult PostSearchCleanup();

View File

@ -43,7 +43,6 @@ EXTRA_JS_MODULES += [
'InsecurePasswordUtils.jsm',
'LoginHelper.jsm',
'LoginManagerContent.jsm',
'LoginManagerParent.jsm',
'LoginRecipes.jsm',
]

View File

@ -14,12 +14,10 @@ Cu.import("resource://gre/modules/SharedPromptUtils.jsm");
* Mirrored in mobile/android/components/LoginManagerPrompter.js */
const PROMPT_DISPLAYED = 0;
const PROMPT_ADD = 1;
const PROMPT_ADD_OR_UPDATE = 1;
const PROMPT_NOTNOW = 2;
const PROMPT_NEVER = 3;
const PROMPT_UPDATE = 1;
/*
* LoginManagerPromptFactory
*
@ -742,8 +740,6 @@ LoginManagerPrompter.prototype = {
*/
promptToSavePassword : function (aLogin) {
var notifyObj = this._getPopupNote() || this._getNotifyBox();
Services.telemetry.getHistogramById("PWMGR_PROMPT_REMEMBER_ACTION").add(PROMPT_DISPLAYED);
if (notifyObj)
this._showSaveLoginNotification(notifyObj, aLogin);
else
@ -783,6 +779,100 @@ LoginManagerPrompter.prototype = {
}
},
/**
* Displays the PopupNotifications.jsm doorhanger for password save or change.
*
* @param {nsILoginInfo} login
* Login to save or change. For changes, this login should contain the
* new password.
* @param {string} type
* This is "password-save" or "password-change" depending on the
* original notification type. This is used for telemetry and tests.
*/
_showLoginCaptureDoorhanger(login, type) {
let { browser } = this._getNotifyWindow();
let msgNames = type == "password-save" ? {
prompt: "rememberPasswordMsgNoUsername",
buttonLabel: "notifyBarRememberPasswordButtonText",
buttonAccessKey: "notifyBarRememberPasswordButtonAccessKey",
} : {
// We reuse the existing message, even if it expects a username, until we
// switch to the final terminology in bug 1144856.
prompt: "updatePasswordMsg",
buttonLabel: "notifyBarUpdateButtonText",
buttonAccessKey: "notifyBarUpdateButtonAccessKey",
};
let histogramName = type == "password-save" ? "PWMGR_PROMPT_REMEMBER_ACTION"
: "PWMGR_PROMPT_UPDATE_ACTION";
let histogram = Services.telemetry.getHistogramById(histogramName);
histogram.add(PROMPT_DISPLAYED);
// The main action is the "Remember" or "Update" button.
let mainAction = {
label: this._getLocalizedString(msgNames.buttonLabel),
accessKey: this._getLocalizedString(msgNames.buttonAccessKey),
callback: () => {
histogram.add(PROMPT_ADD_OR_UPDATE);
let foundLogins = Services.logins.findLogins({}, login.hostname,
login.formSubmitURL,
login.httpRealm);
let logins = foundLogins.filter(l => l.username == login.username);
if (logins.length == 0) {
Services.logins.addLogin(login);
} else if (logins.length == 1) {
this._updateLogin(logins[0], login.password);
} else {
Cu.reportError("Unexpected match of multiple logins.");
}
browser.focus();
}
};
// Include a "Never for this site" button when saving a new password.
let secondaryActions = type == "password-save" ? [{
label: this._getLocalizedString("notifyBarNeverRememberButtonText"),
accessKey: this._getLocalizedString("notifyBarNeverRememberButtonAccessKey"),
callback: () => {
histogram.add(PROMPT_NEVER);
Services.logins.setLoginSavingEnabled(login.hostname, false);
browser.focus();
}
}] : null;
let usernamePlaceholder = this._getLocalizedString("noUsernamePlaceholder");
let displayHost = this._getShortDisplayHost(login.hostname);
this._getPopupNote().show(
browser,
"password",
this._getLocalizedString(msgNames.prompt, [displayHost]),
"password-notification-icon",
mainAction,
secondaryActions,
{
timeout: Date.now() + 10000,
persistWhileVisible: true,
passwordNotificationType: type,
eventCallback: function (topic) {
if (topic != "showing") {
return false;
}
let chromeDoc = this.browser.ownerDocument;
chromeDoc.getElementById("password-notification-username")
.setAttribute("placeholder", usernamePlaceholder);
chromeDoc.getElementById("password-notification-username")
.setAttribute("value", login.username);
chromeDoc.getElementById("password-notification-password")
.setAttribute("value", login.password);
},
}
);
},
/*
* _showSaveLoginNotification
*
@ -806,8 +896,6 @@ LoginManagerPrompter.prototype = {
this._getLocalizedString("notifyBarRememberPasswordButtonText");
var rememberButtonAccessKey =
this._getLocalizedString("notifyBarRememberPasswordButtonAccessKey");
var usernamePlaceholder =
this._getLocalizedString("noUsernamePlaceholder");
var displayHost = this._getShortDisplayHost(aLogin.hostname);
var notificationText = this._getLocalizedString(
@ -818,58 +906,10 @@ LoginManagerPrompter.prototype = {
// in scope here; set one to |this._pwmgr| so we can get back to pwmgr
// without a getService() call.
var pwmgr = this._pwmgr;
let promptHistogram = Services.telemetry.getHistogramById("PWMGR_PROMPT_REMEMBER_ACTION");
// Notification is a PopupNotification
if (aNotifyObj == this._getPopupNote()) {
// "Remember" button
var mainAction = {
label: rememberButtonText,
accessKey: rememberButtonAccessKey,
callback: function(aNotifyObj, aButton) {
promptHistogram.add(PROMPT_ADD);
pwmgr.addLogin(aLogin);
browser.focus();
}
};
var secondaryActions = [
// "Never for this site" button
{
label: neverButtonText,
accessKey: neverButtonAccessKey,
callback: function(aNotifyObj, aButton) {
promptHistogram.add(PROMPT_NEVER);
pwmgr.setLoginSavingEnabled(aLogin.hostname, false);
browser.focus();
}
}
];
var { browser } = this._getNotifyWindow();
let eventCallback = function (topic) {
if (topic != "showing") {
return false;
}
let chromeDoc = this.browser.ownerDocument;
chromeDoc.getElementById("password-notification-username")
.setAttribute("placeholder", usernamePlaceholder);
chromeDoc.getElementById("password-notification-username")
.setAttribute("value", aLogin.username);
chromeDoc.getElementById("password-notification-password")
.setAttribute("value", aLogin.password);
};
aNotifyObj.show(browser, "password", notificationText,
"password-notification-icon", mainAction,
secondaryActions,
{ timeout: Date.now() + 10000,
persistWhileVisible: true,
passwordNotificationType: "password-save",
eventCallback });
this._showLoginCaptureDoorhanger(aLogin, "password-save");
} else {
var notNowButtonText =
this._getLocalizedString("notifyBarNotNowButtonText");
@ -1030,8 +1070,6 @@ LoginManagerPrompter.prototype = {
this._getLocalizedString("notifyBarUpdateButtonText");
var changeButtonAccessKey =
this._getLocalizedString("notifyBarUpdateButtonAccessKey");
var usernamePlaceholder =
this._getLocalizedString("noUsernamePlaceholder");
// We reuse the existing message, even if it expects a username, until we
// switch to the final terminology in bug 1144856.
@ -1044,44 +1082,10 @@ LoginManagerPrompter.prototype = {
// without a getService() call.
var self = this;
let promptHistogram = Services.telemetry.getHistogramById("PWMGR_PROMPT_UPDATE_ACTION");
// Notification is a PopupNotification
if (aNotifyObj == this._getPopupNote()) {
// "Yes" button
var mainAction = {
label: changeButtonText,
accessKey: changeButtonAccessKey,
popup: null,
callback: function(aNotifyObj, aButton) {
self._updateLogin(aOldLogin, aNewPassword);
promptHistogram.add(PROMPT_UPDATE);
}
};
var { browser } = this._getNotifyWindow();
let eventCallback = function (topic) {
if (topic != "showing") {
return false;
}
let chromeDoc = this.browser.ownerDocument;
chromeDoc.getElementById("password-notification-username")
.setAttribute("placeholder", usernamePlaceholder);
chromeDoc.getElementById("password-notification-username")
.setAttribute("value", aOldLogin.username);
chromeDoc.getElementById("password-notification-password")
.setAttribute("value", aNewPassword);
};
Services.telemetry.getHistogramById("PWMGR_PROMPT_UPDATE_ACTION").add(PROMPT_DISPLAYED);
aNotifyObj.show(browser, "password", notificationText,
"password-notification-icon", mainAction,
null, { timeout: Date.now() + 10000,
persistWhileVisible: true,
passwordNotificationType: "password-change",
eventCallback });
aOldLogin.password = aNewPassword;
this._showLoginCaptureDoorhanger(aOldLogin, "password-change");
} else {
var dontChangeButtonText =
this._getLocalizedString("notifyBarDontChangeButtonText");

View File

@ -1562,7 +1562,7 @@ this.PlacesUtils = {
uri = PlacesUtils.favicons.getFaviconLinkForIcon(uri);
deferred.resolve(uri);
} else {
deferred.reject();
deferred.reject("favicon not found for uri");
}
});
return deferred.promise;

View File

@ -103,7 +103,7 @@ Readability.prototype = {
byline: /byline|author|dateline|writtenby/i,
replaceFonts: /<(\/?)font[^>]*>/gi,
normalize: /\s{2,}/g,
videos: /https?:\/\/(www\.)?(youtube|vimeo)\.com/i,
videos: /https?:\/\/(www\.)?(youtube|youtube-nocookie|player\.vimeo)\.com/i,
nextLink: /(next|weiter|continue|>([^\|]|$)|»([^\|]|$))/i,
prevLink: /(prev|earl|old|new|<|«)/i,
whitespace: /^\s*$/,
@ -125,6 +125,36 @@ Readability.prototype = {
this._fixRelativeUris(articleContent);
},
/**
* Iterate over a NodeList, which doesn't natively fully implement the Array
* interface.
*
* For convenience, the current object context is applied to the provided
* iterate function.
*
* @param NodeList nodeList The NodeList.
* @param Function fn The iterate function.
* @return void
*/
_forEachNode: function(nodeList, fn) {
return Array.prototype.forEach.call(nodeList, fn, this);
},
/**
* Iterate over a NodeList, return true if any of the provided iterate
* function calls returns true, false otherwise.
*
* For convenience, the current object context is applied to the
* provided iterate function.
*
* @param NodeList nodeList The NodeList.
* @param Function fn The iterate function.
* @return Boolean
*/
_someNode: function(nodeList, fn) {
return Array.prototype.some.call(nodeList, fn, this);
},
/**
* Converts each <a> and <img> uri in the given element to an absolute URI.
*
@ -149,6 +179,10 @@ Readability.prototype = {
if (uri[0] == "/")
return prePath + uri;
// Dotslash relative URI.
if (uri.indexOf("./") === 0)
return pathBase + uri.slice(2);
// Standard relative URI; add entire path. pathBase already includes a
// trailing "/".
return pathBase + uri;
@ -156,19 +190,18 @@ Readability.prototype = {
function convertRelativeURIs(tagName, propName) {
var elems = articleContent.getElementsByTagName(tagName);
for (var i = elems.length; --i >= 0;) {
var elem = elems[i];
this._forEachNode(elems, function(elem) {
var relativeURI = elem.getAttribute(propName);
if (relativeURI != null)
elems[i].setAttribute(propName, toAbsoluteURI(relativeURI));
}
elem.setAttribute(propName, toAbsoluteURI(relativeURI));
});
}
// Fix links.
convertRelativeURIs("a", "href");
convertRelativeURIs.call(this, "a", "href");
// Fix images.
convertRelativeURIs("img", "src");
convertRelativeURIs.call(this, "img", "src");
},
/**
@ -224,19 +257,17 @@ Readability.prototype = {
var doc = this._doc;
// Remove all style tags in head
var styleTags = doc.getElementsByTagName("style");
for (var st = styleTags.length - 1; st >= 0; st -= 1) {
styleTags[st].parentNode.removeChild(styleTags[st]);
}
this._forEachNode(doc.getElementsByTagName("style"), function(styleNode) {
styleNode.parentNode.removeChild(styleNode);
});
if (doc.body) {
this._replaceBrs(doc.body);
}
var fonts = doc.getElementsByTagName("FONT");
for (var i = fonts.length; --i >=0;) {
this._setNodeTag(fonts[i], "SPAN");
}
this._forEachNode(doc.getElementsByTagName("font"), function(fontNode) {
this._setNodeTag(fontNode, "SPAN");
});
},
/**
@ -262,9 +293,7 @@ Readability.prototype = {
* <div>foo<br>bar<p>abc</p></div>
*/
_replaceBrs: function (elem) {
var brs = elem.getElementsByTagName("br");
for (var i = 0; i < brs.length; i++) {
var br = brs[i];
this._forEachNode(elem.getElementsByTagName("br"), function(br) {
var next = br.nextSibling;
// Whether 2 or more <br> elements have been found and replaced with a
@ -303,7 +332,7 @@ Readability.prototype = {
next = sibling;
}
}
}
});
},
_setNodeTag: function (node, tag) {
@ -326,6 +355,7 @@ Readability.prototype = {
// Clean out junk from the article content
this._cleanConditionally(articleContent, "form");
this._clean(articleContent, "object");
this._clean(articleContent, "embed");
this._clean(articleContent, "h1");
// If there is only one h2, they are probably using it as a header
@ -343,26 +373,23 @@ Readability.prototype = {
this._cleanConditionally(articleContent, "div");
// Remove extra paragraphs
var articleParagraphs = articleContent.getElementsByTagName('p');
for (var i = articleParagraphs.length - 1; i >= 0; i -= 1) {
var imgCount = articleParagraphs[i].getElementsByTagName('img').length;
var embedCount = articleParagraphs[i].getElementsByTagName('embed').length;
var objectCount = articleParagraphs[i].getElementsByTagName('object').length;
this._forEachNode(articleContent.getElementsByTagName('p'), function(paragraph) {
var imgCount = paragraph.getElementsByTagName('img').length;
var embedCount = paragraph.getElementsByTagName('embed').length;
var objectCount = paragraph.getElementsByTagName('object').length;
// At this point, nasty iframes have been removed, only remain embedded video ones.
var iframeCount = paragraph.getElementsByTagName('iframe').length;
var totalCount = imgCount + embedCount + objectCount + iframeCount;
if (imgCount === 0 &&
embedCount === 0 &&
objectCount === 0 &&
this._getInnerText(articleParagraphs[i], false) === '')
articleParagraphs[i].parentNode.removeChild(articleParagraphs[i]);
}
if (totalCount === 0 && !this._getInnerText(paragraph, false))
paragraph.parentNode.removeChild(paragraph);
});
var brs = articleContent.getElementsByTagName("BR");
for (var i = brs.length; --i >= 0;) {
var br = brs[i];
this._forEachNode(articleContent.getElementsByTagName("br"), function(br) {
var next = this._nextElement(br.nextSibling);
if (next && next.tagName == "P")
br.parentNode.removeChild(br);
}
});
},
/**
@ -529,8 +556,7 @@ Readability.prototype = {
elementsToScore.push(node);
} else {
// EXPERIMENTAL
for (var i = 0, il = node.childNodes.length; i < il; i += 1) {
var childNode = node.childNodes[i];
this._forEachNode(node.childNodes, function(childNode) {
if (childNode.nodeType === Node.TEXT_NODE) {
var p = doc.createElement('p');
p.textContent = childNode.textContent;
@ -538,7 +564,7 @@ Readability.prototype = {
p.className = 'readability-styled';
node.replaceChild(p, childNode);
}
}
});
}
}
node = this._getNextNode(node);
@ -551,17 +577,17 @@ Readability.prototype = {
* A score is determined by things like number of commas, class names, etc. Maybe eventually link density.
**/
var candidates = [];
for (var pt = 0; pt < elementsToScore.length; pt += 1) {
var parentNode = elementsToScore[pt].parentNode;
this._forEachNode(elementsToScore, function(elementToScore) {
var parentNode = elementToScore.parentNode;
var grandParentNode = parentNode ? parentNode.parentNode : null;
var innerText = this._getInnerText(elementsToScore[pt]);
var innerText = this._getInnerText(elementToScore);
if (!parentNode || typeof(parentNode.tagName) === 'undefined')
continue;
return;
// If this paragraph is less than 25 characters, don't even count it.
if (innerText.length < 25)
continue;
return;
// Initialize readability data for the parent.
if (typeof parentNode.readability === 'undefined') {
@ -593,7 +619,7 @@ Readability.prototype = {
if (grandParentNode)
grandParentNode.readability.contentScore += contentScore / 2;
}
});
// After we've calculated scores, loop through all of the possible
// candidate nodes we found and find the one with the highest score.
@ -650,9 +676,9 @@ Readability.prototype = {
// below does some of that - but only if we've looked high enough up the DOM
// tree.
var parentOfTopCandidate = topCandidate.parentNode;
var lastScore = topCandidate.readability.contentScore;
// The scores shouldn't get too low.
var scoreThreshold = topCandidate.readability.contentScore / 3;
var lastScore = parentOfTopCandidate.readability.contentScore;
var scoreThreshold = lastScore / 3;
while (parentOfTopCandidate && parentOfTopCandidate.readability) {
var parentScore = parentOfTopCandidate.readability.contentScore;
if (parentScore < scoreThreshold)
@ -662,6 +688,7 @@ Readability.prototype = {
topCandidate = parentOfTopCandidate;
break;
}
lastScore = parentOfTopCandidate.readability.contentScore;
parentOfTopCandidate = parentOfTopCandidate.parentNode;
}
}
@ -820,14 +847,13 @@ Readability.prototype = {
var propertyPattern = /^\s*og\s*:\s*description\s*$/gi;
// Find description tags.
for (var i = 0; i < metaElements.length; i++) {
var element = metaElements[i];
this._forEachNode(metaElements, function(element) {
var elementName = element.getAttribute("name");
var elementProperty = element.getAttribute("property");
if (elementName === "author") {
metadata.byline = element.getAttribute("content");
continue;
return;
}
var name = null;
@ -846,7 +872,7 @@ Readability.prototype = {
values[name] = content.trim();
}
}
}
});
if ("description" in values) {
metadata.excerpt = values["description"];
@ -867,14 +893,13 @@ Readability.prototype = {
* @param Element
**/
_removeScripts: function(doc) {
var scripts = doc.getElementsByTagName('script');
for (var i = scripts.length - 1; i >= 0; i -= 1) {
scripts[i].nodeValue="";
scripts[i].removeAttribute('src');
this._forEachNode(doc.getElementsByTagName('script'), function(scriptNode) {
scriptNode.nodeValue = "";
scriptNode.removeAttribute('src');
if (scripts[i].parentNode)
scripts[i].parentNode.removeChild(scripts[i]);
}
if (scriptNode.parentNode)
scriptNode.parentNode.removeChild(scriptNode);
});
},
/**
@ -884,22 +909,17 @@ Readability.prototype = {
*
* @param Element
**/
_hasSinglePInsideElement: function(e) {
_hasSinglePInsideElement: function(element) {
// There should be exactly 1 element child which is a P:
if (e.children.length != 1 || e.firstElementChild.tagName !== "P") {
if (element.children.length != 1 || element.firstElementChild.tagName !== "P") {
return false;
}
// And there should be no text nodes with real content
var childNodes = e.childNodes;
for (var i = childNodes.length; --i >= 0;) {
var node = childNodes[i];
if (node.nodeType == Node.TEXT_NODE &&
this.REGEXPS.hasContent.test(node.textContent)) {
return false;
}
}
return true;
// And there should be no text nodes with real content
return !this._someNode(element.childNodes, function(node) {
return node.nodeType === Node.TEXT_NODE &&
this.REGEXPS.hasContent.test(node.textContent);
});
},
/**
@ -907,14 +927,11 @@ Readability.prototype = {
*
* @param Element
*/
_hasChildBlockElement: function (e) {
var length = e.children.length;
for (var i = 0; i < length; i++) {
var child = e.children[i];
if (this.DIV_TO_P_ELEMS.indexOf(child.tagName) !== -1 || this._hasChildBlockElement(child))
return true;
}
return false;
_hasChildBlockElement: function (element) {
return this._someNode(element.childNodes, function(node) {
return this.DIV_TO_P_ELEMS.indexOf(node.tagName) !== -1 ||
this._hasChildBlockElement(node);
});
},
/**
@ -922,11 +939,12 @@ Readability.prototype = {
* This also strips out any excess whitespace to be found.
*
* @param Element
* @param Boolean normalizeSpaces (default: true)
* @return string
**/
_getInnerText: function(e, normalizeSpaces) {
var textContent = e.textContent.trim();
normalizeSpaces = (typeof normalizeSpaces === 'undefined') ? true : normalizeSpaces;
var textContent = e.textContent.trim();
if (normalizeSpaces) {
return textContent.replace(this.REGEXPS.normalize, " ");
@ -985,14 +1003,17 @@ Readability.prototype = {
* @param Element
* @return number (float)
**/
_getLinkDensity: function(e) {
var links = e.getElementsByTagName("a");
var textLength = this._getInnerText(e).length;
_getLinkDensity: function(element) {
var textLength = this._getInnerText(element).length;
if (textLength === 0)
return;
var linkLength = 0;
for (var i = 0, il = links.length; i < il; i += 1) {
linkLength += this._getInnerText(links[i]).length;
}
// XXX implement _reduceNodeList?
this._forEachNode(element.getElementsByTagName("a"), function(linkNode) {
linkLength += this._getInnerText(linkNode).length;
});
return linkLength / textLength;
},
@ -1405,28 +1426,26 @@ Readability.prototype = {
* @return void
**/
_clean: function(e, tag) {
var targetList = e.getElementsByTagName(tag);
var isEmbed = (tag === 'object' || tag === 'embed');
var isEmbed = ["object", "embed", "iframe"].indexOf(tag) !== -1;
for (var y = targetList.length - 1; y >= 0; y -= 1) {
this._forEachNode(e.getElementsByTagName(tag), function(element) {
// Allow youtube and vimeo videos through as people usually want to see those.
if (isEmbed) {
var attributeValues = "";
for (var i = 0, il = targetList[y].attributes.length; i < il; i += 1) {
attributeValues += targetList[y].attributes[i].value + '|';
}
var attributeValues = [].map.call(element.attributes, function(attr) {
return attr.value;
}).join("|");
// First, check the elements attributes to see if any of them contain youtube or vimeo
if (this.REGEXPS.videos.test(attributeValues))
continue;
return;
// Then check the elements inside this element for the same.
if (this.REGEXPS.videos.test(targetList[y].innerHTML))
continue;
if (this.REGEXPS.videos.test(element.innerHTML))
return;
}
targetList[y].parentNode.removeChild(targetList[y]);
}
element.parentNode.removeChild(element);
});
},
/**
@ -1578,7 +1597,7 @@ Readability.prototype = {
if (!metadata.excerpt) {
var paragraphs = articleContent.getElementsByTagName("p");
if (paragraphs.length > 0) {
metadata.excerpt = paragraphs[0].textContent;
metadata.excerpt = paragraphs[0].textContent.trim();
}
}

View File

@ -274,14 +274,20 @@ FormAutoComplete.prototype = {
this.log("getAutocompleteValues failed: " + aError.message);
},
handleCompletion: aReason => {
// Check that the current query is still the one we created. Our
// query might have been canceled shortly before completing, in
// that case we don't want to call the callback anymore.
if (query == this._pendingQuery) {
this._pendingQuery = null;
if (!aReason) {
callback(results);
}
}
}
};
this._pendingQuery = FormHistory.getAutoCompleteResults(searchString, params, processResults);
let query = FormHistory.getAutoCompleteResults(searchString, params, processResults);
this._pendingQuery = query;
},
/*
@ -329,6 +335,7 @@ FormAutoCompleteChild.prototype = {
_debug: false,
_enabled: true,
_pendingSearch: null,
/*
* init
@ -361,7 +368,9 @@ FormAutoCompleteChild.prototype = {
autoCompleteSearchAsync : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener) {
this.log("autoCompleteSearchAsync");
this._pendingListener = aListener;
if (this._pendingSearch) {
this.stopAutoCompleteSearch();
}
let rect = BrowserUtils.getElementBoundingScreenRect(aField);
@ -383,12 +392,20 @@ FormAutoCompleteChild.prototype = {
height: rect.height
});
mm.addMessageListener("FormAutoComplete:AutoCompleteSearchAsyncResult",
function searchFinished(message) {
let search = this._pendingSearch = {};
let searchFinished = message => {
mm.removeMessageListener("FormAutoComplete:AutoCompleteSearchAsyncResult", searchFinished);
// Check whether stopAutoCompleteSearch() was called, i.e. the search
// was cancelled, while waiting for a result.
if (search != this._pendingSearch) {
return;
}
this._pendingSearch = null;
let result = new FormAutoCompleteResult(
null,
[{text: res} for (res of message.data.results)],
[for (res of message.data.results) {text: res}],
null,
null
);
@ -396,13 +413,14 @@ FormAutoCompleteChild.prototype = {
aListener.onSearchCompletion(result);
}
}
);
mm.addMessageListener("FormAutoComplete:AutoCompleteSearchAsyncResult", searchFinished);
this.log("autoCompleteSearchAsync message was sent");
},
stopAutoCompleteSearch : function () {
this.log("stopAutoCompleteSearch");
this._pendingSearch = null;
},
}; // end of FormAutoCompleteChild implementation

View File

@ -6571,16 +6571,16 @@
"n_buckets": 100,
"description": "The peak number of open tabs in all windows for a session for devtools users."
},
"DEVTOOLS_TABS_OPEN_AVERAGE_EXPONENTIAL": {
"DEVTOOLS_TABS_OPEN_AVERAGE_LINEAR": {
"expires_in_version": "never",
"kind": "exponential",
"kind": "linear",
"high": "101",
"n_buckets": "100",
"description": "The mean number of open tabs in all windows for a session for devtools users."
},
"DEVTOOLS_TABS_PINNED_PEAK_EXPONENTIAL": {
"DEVTOOLS_TABS_PINNED_PEAK_LINEAR": {
"expires_in_version": "never",
"kind": "exponential",
"kind": "linear",
"high": "101",
"n_buckets": "100",
"description": "The peak number of pinned tabs (app tabs) in all windows for a session for devtools users."
@ -7287,6 +7287,14 @@
"releaseChannelCollection": "opt-out",
"description": "Connection length for bi-directionally connected media"
},
"LOOP_SHARING_STATE_CHANGE": {
"alert_emails": ["firefox-dev@mozilla.org", "mdeboer@mozilla.com"],
"expires_in_version": "43",
"kind": "count",
"keyed": true,
"releaseChannelCollection": "opt-in",
"description": "Number of times the sharing feature has been enabled and disabled"
},
"E10S_AUTOSTART": {
"expires_in_version": "never",
"kind": "boolean",

View File

@ -69,25 +69,58 @@ this.__defineSetter__("_maxUsedThemes", function maxUsedThemesSetter(aVal) {
var _themeIDBeingEnabled = null;
var _themeIDBeingDisabled = null;
// Convert from the old storage format (in which the order of usedThemes
// was combined with isThemeSelected to determine which theme was selected)
// to the new one (where a selectedThemeID determines which theme is selected).
(function migrateToNewStorageFormat() {
let wasThemeSelected = false;
try {
wasThemeSelected = _prefs.getBoolPref("isThemeSelected");
} catch(e) { }
if (wasThemeSelected) {
_prefs.clearUserPref("isThemeSelected");
let themes = [];
try {
themes = JSON.parse(_prefs.getComplexValue("usedThemes",
Ci.nsISupportsString).data);
} catch (e) { }
if (Array.isArray(themes) && themes[0]) {
_prefs.setCharPref("selectedThemeID", themes[0].id);
}
}
})();
this.LightweightThemeManager = {
get name() "LightweightThemeManager",
// Themes that can be added for an application. They can't be removed, and
// will always show up at the top of the list.
_builtInThemes: new Map(),
get usedThemes () {
let themes = [];
try {
return JSON.parse(_prefs.getComplexValue("usedThemes",
themes = JSON.parse(_prefs.getComplexValue("usedThemes",
Ci.nsISupportsString).data);
} catch (e) {
return [];
}
} catch (e) { }
themes.push(...this._builtInThemes.values());
return themes;
},
get currentTheme () {
let selectedThemeID = null;
try {
if (_prefs.getBoolPref("isThemeSelected"))
var data = this.usedThemes[0];
selectedThemeID = _prefs.getCharPref("selectedThemeID");
} catch (e) {}
return data || null;
let data = null;
if (selectedThemeID) {
data = this.getUsedTheme(selectedThemeID);
}
return data;
},
get currentThemeForDisplay () {
@ -125,7 +158,7 @@ this.LightweightThemeManager = {
forgetUsedTheme: function LightweightThemeManager_forgetUsedTheme(aId) {
let theme = this.getUsedTheme(aId);
if (!theme)
if (!theme || LightweightThemeManager._builtInThemes.has(theme.id))
return;
let wrapper = new AddonWrapper(theme);
@ -141,6 +174,30 @@ this.LightweightThemeManager = {
AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper);
},
addBuiltInTheme: function LightweightThemeManager_addBuiltInTheme(theme) {
if (!theme || !theme.id || this.usedThemes.some(t => t.id == theme.id)) {
throw new Error("Trying to add invalid builtIn theme");
}
this._builtInThemes.set(theme.id, theme);
},
forgetBuiltInTheme: function LightweightThemeManager_forgetBuiltInTheme(id) {
if (!this._builtInThemes.has(id)) {
let currentTheme = this.currentTheme;
if (currentTheme && currentTheme.id == id) {
this.currentTheme = null;
}
}
return this._builtInThemes.delete(id);
},
clearBuiltInThemes: function LightweightThemeManager_clearBuiltInThemes() {
for (let id of this._builtInThemes.keys()) {
this.forgetBuiltInTheme(id);
}
},
previewTheme: function LightweightThemeManager_previewTheme(aData) {
let cancel = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
cancel.data = false;
@ -242,7 +299,11 @@ this.LightweightThemeManager = {
}
}
_prefs.setBoolPref("isThemeSelected", aData != null);
if (aData)
_prefs.setCharPref("selectedThemeID", aData.id);
else
_prefs.clearUserPref("selectedThemeID");
_notifyWindows(aData);
Services.obs.notifyObservers(null, "lightweight-theme-changed", null);
},
@ -462,7 +523,11 @@ function AddonWrapper(aTheme) {
});
this.__defineGetter__("permissions", function AddonWrapper_permissionsGetter() {
let permissions = AddonManager.PERM_CAN_UNINSTALL;
let permissions = 0;
// Do not allow uninstall of builtIn themes.
if (!LightweightThemeManager._builtInThemes.has(aTheme.id))
permissions = AddonManager.PERM_CAN_UNINSTALL;
if (this.userDisabled)
permissions |= AddonManager.PERM_CAN_ENABLE;
else
@ -679,6 +744,9 @@ function _makeURI(aURL, aBaseURI)
Services.io.newURI(aURL, null, aBaseURI);
function _updateUsedThemes(aList) {
// Remove app-specific themes before saving them to the usedThemes pref.
aList = aList.filter(theme => !LightweightThemeManager._builtInThemes.has(theme.id));
// Send uninstall events for all themes that need to be removed.
while (aList.length > _maxUsedThemes) {
let wrapper = new AddonWrapper(aList[aList.length - 1]);

View File

@ -19,18 +19,20 @@ function dummy(id) {
};
}
function hasPermission(aAddon, aPerm) {
var perm = AddonManager["PERM_CAN_" + aPerm.toUpperCase()];
return !!(aAddon.permissions & perm);
}
function run_test() {
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
startupManager();
Services.prefs.setIntPref("lightweightThemes.maxUsedThemes", 8);
var temp = {};
Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", temp);
do_check_eq(typeof temp.LightweightThemeManager, "object");
var ltm = temp.LightweightThemeManager;
let {LightweightThemeManager: ltm} = Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", {});
do_check_eq(typeof ltm, "object");
do_check_eq(typeof ltm.usedThemes, "object");
do_check_eq(ltm.usedThemes.length, 0);
do_check_eq(ltm.currentTheme, null);
@ -511,4 +513,86 @@ function run_test() {
Services.prefs.clearUserPref("lightweightThemes.maxUsedThemes");
do_check_eq(ltm.usedThemes.length, 30);
let usedThemes = ltm.usedThemes;
for (let theme of usedThemes) {
ltm.forgetUsedTheme(theme.id);
}
// Check builtInTheme functionality for Bug 1094821
do_check_eq(ltm._builtInThemes.toString(), "[object Map]");
do_check_eq([...ltm._builtInThemes.entries()].length, 0);
do_check_eq(ltm.usedThemes.length, 0);
ltm.addBuiltInTheme(dummy("builtInTheme0"));
do_check_eq([...ltm._builtInThemes].length, 1);
do_check_eq(ltm.usedThemes.length, 1);
do_check_eq(ltm.usedThemes[0].id, "builtInTheme0");
ltm.addBuiltInTheme(dummy("builtInTheme1"));
do_check_eq([...ltm._builtInThemes].length, 2);
do_check_eq(ltm.usedThemes.length, 2);
do_check_eq(ltm.usedThemes[1].id, "builtInTheme1");
// Clear all and then re-add
ltm.clearBuiltInThemes();
do_check_eq([...ltm._builtInThemes].length, 0);
do_check_eq(ltm.usedThemes.length, 0);
ltm.addBuiltInTheme(dummy("builtInTheme0"));
ltm.addBuiltInTheme(dummy("builtInTheme1"));
do_check_eq([...ltm._builtInThemes].length, 2);
do_check_eq(ltm.usedThemes.length, 2);
do_test_pending();
AddonManager.getAddonByID("builtInTheme0@personas.mozilla.org", aAddon => {
// App specific theme can't be uninstalled or disabled,
// but can be enabled (since it isn't already applied).
do_check_eq(hasPermission(aAddon, "uninstall"), false);
do_check_eq(hasPermission(aAddon, "disable"), false);
do_check_eq(hasPermission(aAddon, "enable"), true);
ltm.currentTheme = dummy("x0");
do_check_eq([...ltm._builtInThemes].length, 2);
do_check_eq(ltm.usedThemes.length, 3);
do_check_eq(ltm.usedThemes[0].id, "x0");
do_check_eq(ltm.currentTheme.id, "x0");
do_check_eq(ltm.usedThemes[1].id, "builtInTheme0");
do_check_eq(ltm.usedThemes[2].id, "builtInTheme1");
Assert.throws(() => { ltm.addBuiltInTheme(dummy("builtInTheme0")) },
"Exception is thrown adding a duplicate theme");
Assert.throws(() => { ltm.addBuiltInTheme("not a theme object") },
"Exception is thrown adding an invalid theme");
AddonManager.getAddonByID("x0@personas.mozilla.org", aAddon => {
// Currently applied (non-app-specific) can be uninstalled or disabled,
// but can't be enabled (since it's already applied).
do_check_eq(hasPermission(aAddon, "uninstall"), true);
do_check_eq(hasPermission(aAddon, "disable"), true);
do_check_eq(hasPermission(aAddon, "enable"), false);
ltm.forgetUsedTheme("x0");
do_check_eq(ltm.currentTheme, null);
// Removing the currently applied app specific theme should unapply it
ltm.currentTheme = ltm.getUsedTheme("builtInTheme0");
do_check_eq(ltm.currentTheme.id, "builtInTheme0");
do_check_true(ltm.forgetBuiltInTheme("builtInTheme0"));
do_check_eq(ltm.currentTheme, null);
do_check_eq([...ltm._builtInThemes].length, 1);
do_check_eq(ltm.usedThemes.length, 1);
do_check_true(ltm.forgetBuiltInTheme("builtInTheme1"));
do_check_false(ltm.forgetBuiltInTheme("not-an-existing-theme-id"));
do_check_eq([...ltm._builtInThemes].length, 0);
do_check_eq(ltm.usedThemes.length, 0);
do_check_eq(ltm.currentTheme, null);
do_test_finished();
});
});
}

View File

@ -118,7 +118,7 @@ function run_test() {
previewURL: "http://localhost/data/preview.png",
iconURL: "http://localhost/data/icon.png"
}]));
Services.prefs.setBoolPref("lightweightThemes.isThemeSelected", true);
Services.prefs.setCharPref("lightweightThemes.selectedThemeID", "1");
let stagedXPIs = profileDir.clone();
stagedXPIs.append("staged-xpis");

View File

@ -257,6 +257,7 @@ body {
border-left: 0;
border-right: 0;
border-bottom: 1px solid #c1c1c1;
padding: 0;
}
.button[hidden] {