merge fx-team to mozilla-central a=merge

This commit is contained in:
Carsten "Tomcat" Book 2014-11-10 14:33:10 +01:00
commit 0921a96787
45 changed files with 1312 additions and 730 deletions

View File

@ -333,22 +333,22 @@ function init() {
wrapper.init(url, urlParams);
});
break;
case "migrateToDevEdition":
if (user == null) {
migrateToDevEdition(user, urlParams);
break;
}
// else, fall through
default:
// No action specified, or migration request when we already have a user.
// No action specified.
if (user) {
show("stage", "manage");
let sb = Services.strings.createBundle("chrome://browser/locale/syncSetup.properties");
document.title = sb.GetStringFromName("manage.pageTitle");
} else {
show("stage", "intro");
// load the remote frame in the background
wrapper.init(fxAccounts.getAccountsSignUpURI(), urlParams);
// Attempt a migration if enabled or show the introductory page
// otherwise.
migrateToDevEdition(urlParams).then(migrated => {
if (!migrated) {
show("stage", "intro");
// load the remote frame in the background
wrapper.init(fxAccounts.getAccountsSignUpURI(), urlParams);
}
});
}
break;
}
@ -382,7 +382,9 @@ function show(id, childId) {
}
// Migrate sync data from the default profile to the dev-edition profile.
function migrateToDevEdition(user, urlParams) {
// Returns a promise of a true value if migration succeeded, or false if it
// failed.
function migrateToDevEdition(urlParams) {
let defaultProfilePath;
try {
defaultProfilePath = window.getDefaultProfilePath();
@ -393,31 +395,34 @@ function migrateToDevEdition(user, urlParams) {
migrateSyncCreds = Services.prefs.getBoolPref("identity.fxaccounts.migrateToDevEdition");
} catch (e) {}
}
if (migrateSyncCreds) {
Cu.import("resource://gre/modules/osfile.jsm");
let fxAccountsStorage = OS.Path.join(defaultProfilePath, fxAccountsCommon.DEFAULT_STORAGE_FILENAME);
return OS.File.read(fxAccountsStorage, { encoding: "utf-8" }).then(text => {
let accountData = JSON.parse(text).accountData;
return fxAccounts.setSignedInUser(accountData);
}).then(() => {
return fxAccounts.promiseAccountsForceSigninURI().then(url => {
show("remote");
wrapper.init(url, urlParams);
});
}).then(null, error => {
log("Failed to migrate FX Account: " + error);
show("stage", "intro");
// load the remote frame in the background
wrapper.init(fxAccounts.getAccountsSignUpURI(), urlParams);
}).then(() => {
// Reset the pref after migration.
Services.prefs.setBoolPref("identity.fxaccounts.migrateToDevEdition", false);
if (!migrateSyncCreds) {
return Promise.resolve(false);
}
Cu.import("resource://gre/modules/osfile.jsm");
let fxAccountsStorage = OS.Path.join(defaultProfilePath, fxAccountsCommon.DEFAULT_STORAGE_FILENAME);
return OS.File.read(fxAccountsStorage, { encoding: "utf-8" }).then(text => {
let accountData = JSON.parse(text).accountData;
return fxAccounts.setSignedInUser(accountData);
}).then(() => {
return fxAccounts.promiseAccountsForceSigninURI().then(url => {
show("remote");
wrapper.init(url, urlParams);
});
} else {
}).then(null, error => {
log("Failed to migrate FX Account: " + error);
show("stage", "intro");
// load the remote frame in the background
wrapper.init(fxAccounts.getAccountsSignUpURI(), urlParams);
}
}).then(() => {
// Reset the pref after migration.
Services.prefs.setBoolPref("identity.fxaccounts.migrateToDevEdition", false);
return true;
}).then(null, err => {
Cu.reportError("Failed to reset the migrateToDevEdition pref: " + err);
return false;
});
}
// Helper function that returns the path of the default profile on disk. Will be

View File

@ -14,7 +14,6 @@ let DevEdition = {
styleSheetLocation: "chrome://browser/skin/devedition.css",
styleSheet: null,
defaultThemeID: "{972ce4c6-7e08-4474-a285-3208198ce6fd}",
init: function () {
this._updateDevtoolsThemeAttribute();
@ -32,7 +31,7 @@ let DevEdition = {
observe: function (subject, topic, data) {
if (topic == "lightweight-theme-styling-update") {
let newTheme = JSON.parse(data);
if (!newTheme || newTheme.id === this.defaultThemeID) {
if (!newTheme) {
// A lightweight theme has been unapplied, so just re-read prefs.
this._updateStyleSheetFromPrefs();
} else {
@ -56,6 +55,7 @@ let DevEdition = {
// to change colors based on the selected devtools theme.
document.documentElement.setAttribute("devtoolstheme",
Services.prefs.getCharPref(this._devtoolsThemePrefName));
ToolbarIconColor.inferFromText();
this._updateStyleSheetFromPrefs();
},
@ -70,13 +70,8 @@ let DevEdition = {
defaultThemeSelected = Services.prefs.getCharPref(this._themePrefName) == "classic/1.0";
} catch(e) {}
let devtoolsIsDark = false;
try {
devtoolsIsDark = Services.prefs.getCharPref(this._devtoolsThemePrefName) == "dark";
} catch(e) {}
let deveditionThemeEnabled = Services.prefs.getBoolPref(this._prefName) &&
!lightweightThemeSelected && defaultThemeSelected && devtoolsIsDark;
!lightweightThemeSelected && defaultThemeSelected;
this._toggleStyleSheet(deveditionThemeEnabled);
},

View File

@ -183,7 +183,7 @@ let gTests = [
},
},
{
desc: "Test action=migrateToDevEdition (success)",
desc: "Test with migrateToDevEdition enabled (success)",
teardown: function* () {
gBrowser.removeCurrentTab();
yield signOut();
@ -225,7 +225,7 @@ let gTests = [
let mm = tab.linkedBrowser.messageManager;
mm.sendAsyncMessage("test:load-with-mocked-profile-path", {
url: "about:accounts?action=migrateToDevEdition",
url: "about:accounts",
profilePath: mockDir.path,
});
@ -244,7 +244,7 @@ let gTests = [
},
},
{
desc: "Test action=migrateToDevEdition (no user to migrate)",
desc: "Test with migrateToDevEdition enabled (no user to migrate)",
teardown: function* () {
gBrowser.removeCurrentTab();
yield signOut();
@ -266,7 +266,7 @@ let gTests = [
let mm = tab.linkedBrowser.messageManager;
mm.sendAsyncMessage("test:load-with-mocked-profile-path", {
url: "about:accounts?action=migrateToDevEdition",
url: "about:accounts",
profilePath: mockDir.path,
});

View File

@ -59,22 +59,12 @@ function testDevtoolsTheme() {
Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "light");
is (document.documentElement.getAttribute("devtoolstheme"), "light",
"The documentElement has an attribute based on devtools theme.");
ok (!DevEdition.styleSheet, "The devedition stylesheet has been removed because of light devtools theme.");
ok (DevEdition.styleSheet, "The devedition stylesheet is still there with the light devtools theme.");
Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "dark");
is (document.documentElement.getAttribute("devtoolstheme"), "dark",
"The documentElement has an attribute based on devtools theme.");
ok (DevEdition.styleSheet, "The devedition stylesheet has been readded because of dark devtools theme.");
Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "light");
is (document.documentElement.getAttribute("devtoolstheme"), "light",
"The documentElement has an attribute based on devtools theme.");
ok (!DevEdition.styleSheet, "The devedition stylesheet has been removed because of light devtools theme.");
Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "dark");
is (document.documentElement.getAttribute("devtoolstheme"), "dark",
"The documentElement has an attribute based on devtools theme.");
ok (DevEdition.styleSheet, "The devedition stylesheet has been readded because of dark devtools theme.");
ok (DevEdition.styleSheet, "The devedition stylesheet is still there with the dark devtools theme.");
}
function dummyLightweightTheme(id) {
@ -115,7 +105,7 @@ function testLightweightThemePreview() {
info ("Turning the pref on, then previewing the default theme, turning it off and resetting the preview");
Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, true);
ok (DevEdition.styleSheet, "The devedition stylesheet is enabled.");
LightweightThemeManager.previewTheme(dummyLightweightTheme("{972ce4c6-7e08-4474-a285-3208198ce6fd}"));
LightweightThemeManager.previewTheme(null);
ok (DevEdition.styleSheet, "The devedition stylesheet is still enabled after the default theme is applied.");
LightweightThemeManager.resetPreview();
ok (DevEdition.styleSheet, "The devedition stylesheet is still enabled after resetting the preview.");

View File

@ -9,6 +9,7 @@
# BrandFullNameInternal is used for some registry and file system values
# instead of BrandFullName and typically should not be modified.
!define BrandFullNameInternal "Firefox Developer Edition"
!define BrandShortName "Firefox Developer Edition"
!define CompanyName "mozilla.org"
!define URLInfoAbout "https://www.mozilla.org"
!define HelpLink "https://support.mozilla.org"

View File

@ -1293,7 +1293,8 @@ CustomizeMode.prototype = {
const RECENT_LWT_COUNT = 5;
function previewTheme(aEvent) {
LightweightThemeManager.previewTheme(aEvent.target.theme);
LightweightThemeManager.previewTheme(aEvent.target.theme.id != DEFAULT_THEME_ID ?
aEvent.target.theme : null);
}
function resetPreview() {

View File

@ -115,9 +115,6 @@ add_task(function() {
is(undoResetButton.hidden, true, "Undo reset button should be hidden at start of test");
Services.prefs.setBoolPref(prefName, !defaultValue);
//XXXgijs this line should be removed once bug 1094509 lands
Services.prefs.setCharPref("devtools.theme", "dark");
yield waitForCondition(() => !restoreDefaultsButton.disabled);
ok(!restoreDefaultsButton.disabled, "Restore defaults button should be enabled when pref changed");
is(deveditionThemeButton.hasAttribute("checked"), !defaultValue, "Devedition theme button should reflect changed pref value");

View File

@ -93,7 +93,7 @@ this.PlacesUIUtils = {
* Makes a URI from a spec, and do fixup
* @param aSpec
* The string spec of the URI
* @returns A URI object for the spec.
* @return A URI object for the spec.
*/
createFixedURI: function PUIU_createFixedURI(aSpec) {
return URIFixup.createFixupURI(aSpec, Ci.nsIURIFixup.FIXUP_FLAG_NONE);
@ -335,8 +335,8 @@ this.PlacesUIUtils = {
* The index within the container the item was dropped or pasted at
* @param copy
* The drag action was copy, so don't move folders or links.
* @returns An object implementing nsITransaction that can perform
* the move/insert.
* @return An object implementing nsITransaction that can perform
* the move/insert.
*/
makeTransaction:
function PUIU_makeTransaction(data, type, container, index, copy)
@ -387,19 +387,19 @@ this.PlacesUIUtils = {
* Constructs a Places Transaction for the drop or paste of a blob of data
* into a container.
*
* @param aData
* The unwrapped data blob of dropped or pasted data.
* @param aType
* The content type of the data.
* @param aNewParentGuid
* GUID of the container the data was dropped or pasted into.
* @param aIndex
* The index within the container the item was dropped or pasted at.
* @param aCopy
* The drag action was copy, so don't move folders or links.
* @param aData
* The unwrapped data blob of dropped or pasted data.
* @param aType
* The content type of the data.
* @param aNewParentGuid
* GUID of the container the data was dropped or pasted into.
* @param aIndex
* The index within the container the item was dropped or pasted at.
* @param aCopy
* The drag action was copy, so don't move folders or links.
*
* @returns a Places Transaction that can be passed to
* PlacesTranactions.transact for performing the move/insert command.
* @return a Places Transaction that can be transacted for performing the
* move/insert command.
*/
getTransactionForData: function(aData, aType, aNewParentGuid, aIndex, aCopy) {
if (this.SUPPORTED_FLAVORS.indexOf(aData.type) == -1)
@ -570,8 +570,8 @@ this.PlacesUIUtils = {
* element.
* @param doc
* A DOM Document to get a description for
* @returns A description string if a META element was discovered with a
* "description" or "httpequiv" attribute, empty string otherwise.
* @return A description string if a META element was discovered with a
* "description" or "httpequiv" attribute, empty string otherwise.
*/
getDescriptionFromDocument: function PUIU_getDescriptionFromDocument(doc) {
var metaElements = doc.getElementsByTagName("META");
@ -588,7 +588,7 @@ this.PlacesUIUtils = {
* Retrieve the description of an item
* @param aItemId
* item identifier
* @returns the description of the given item, or an empty string if it is
* @return the description of the given item, or an empty string if it is
* not set.
*/
getItemDescription: function PUIU_getItemDescription(aItemId) {
@ -1163,7 +1163,7 @@ this.PlacesUIUtils = {
* or an empty string if not.
*
* @param aItemId id of a container
* @returns the name of the query, or empty string if not a left-pane query
* @return the name of the query, or empty string if not a left-pane query
*/
getLeftPaneQueryNameFromId: function PUIU_getLeftPaneQueryNameFromId(aItemId) {
var queryName = "";
@ -1334,7 +1334,7 @@ XPCOMUtils.defineLazyGetter(PlacesUIUtils, "ptm", function() {
* id of the bookmark where to set Load-in-sidebar annotation.
* @param aLoadInSidebar
* boolean value.
* @returns nsITransaction object.
* @return nsITransaction object.
*/
setLoadInSidebar: function(aItemId, aLoadInSidebar)
{
@ -1353,7 +1353,7 @@ XPCOMUtils.defineLazyGetter(PlacesUIUtils, "ptm", function() {
* id of the item to edit.
* @param aDescription
* new description.
* @returns nsITransaction object.
* @return nsITransaction object.
*/
editItemDescription: function(aItemId, aDescription)
{

View File

@ -767,7 +767,7 @@ PlacesController.prototype = {
let txn = PlacesTransactions.NewSeparator({ parentGuid: yield ip.promiseGuid()
, index: ip.index });
let guid = yield PlacesTransactions.transact(txn);
let guid = yield txn.transact();
let itemId = yield PlacesUtils.promiseItemId(guid);
// Select the new item.
this._view.selectItems([itemId], false);
@ -793,7 +793,7 @@ PlacesController.prototype = {
return;
}
let guid = yield PlacesUtils.promiseItemGuid(itemId);
yield PlacesTransactions.transact(PlacesTransactions.SortByName(guid));
yield PlacesTransactions.SortByName(guid).transact();
}),
/**
@ -940,7 +940,7 @@ PlacesController.prototype = {
if (transactions.length > 0) {
if (PlacesUIUtils.useAsyncTransactions) {
yield PlacesTransactions.transact(transactions);
yield PlacesTransactions.batch(transactions);
}
else {
var txn = new PlacesAggregatedTransaction(txnName, transactions);
@ -1304,11 +1304,10 @@ PlacesController.prototype = {
if (ip.isTag) {
let uris = [for (item of items) if ("uri" in item)
NetUtil.newURI(item.uri)];
yield PlacesTransactions.transact(
PlacesTransactions.Tag({ uris: uris, tag: ip.tagName }));
yield PlacesTransactions.Tag({ uris: uris, tag: ip.tagName }).transact();
}
else {
yield PlacesTransactions.transact(function* () {
yield PlacesTransactions.batch(function* () {
let insertionIndex = ip.index;
let parent = yield ip.promiseGuid();
@ -1324,7 +1323,7 @@ PlacesController.prototype = {
doCopy = true;
}
let guid = yield PlacesUIUtils.getTransactionForData(
item, type, parent, insertionIndex, doCopy);
item, type, parent, insertionIndex, doCopy).transact();
itemsToSelect.push(yield PlacesUtils.promiseItemId(guid));
// Adjust index to make sure items are pasted in the correct
@ -1655,7 +1654,7 @@ let PlacesControllerDragHelper = {
}
if (PlacesUIUtils.useAsyncTransactions) {
yield PlacesTransactions.transact(transactions);
yield PlacesTransactions.batch(transactions);
}
else {
let txn = new PlacesAggregatedTransaction("DropItems", transactions);

View File

@ -45,14 +45,14 @@ var gMoveBookmarksDialog = {
return;
}
PlacesTransactions.transact(function* () {
PlacesTransactions.batch(function* () {
let newParentGuid = yield PlacesUtils.promiseItemGuid(selectedFolderId);
for (let node of this._nodes) {
// Nothing to do if the node is already under the selected folder.
if (node.parent.itemId == selectedFolderId)
continue;
yield PlacesTransactions.Move({ guid: node.bookmarkGuid
, newParentGuid: newParentGuid });
, newParentGuid }).transact();
}
}.bind(this)).then(null, Components.utils.reportError);
},

View File

@ -244,7 +244,7 @@ var gMainPane = {
let win = wm.getMostRecentWindow("navigator:browser");
if (win) {
let accountsTab = win.gBrowser.addTab("about:accounts?action=migrateToDevEdition");
let accountsTab = win.gBrowser.addTab("about:accounts");
win.gBrowser.selectedTab = accountsTab;
}
},

View File

@ -135,7 +135,7 @@ var gMainPane = {
let win = wm.getMostRecentWindow("navigator:browser");
if (win) {
let accountsTab = win.gBrowser.addTab("about:accounts?action=migrateToDevEdition");
let accountsTab = win.gBrowser.addTab("about:accounts");
win.gBrowser.selectedTab = accountsTab;
}
},

View File

@ -0,0 +1,20 @@
/* 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/. */
addMessageListener("Eyedropper:RequestContentScreenshot", sendContentScreenshot);
function sendContentScreenshot() {
let canvas = content.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
let width = content.innerWidth;
let height = content.innerHeight;
canvas.width = width;
canvas.height = height;
canvas.mozOpaque = true;
let ctx = canvas.getContext("2d");
ctx.drawWindow(content, content.scrollX, content.scrollY, width, height, "#fff");
sendAsyncMessage("Eyedropper:Screenshot", canvas.toDataURL());
}

View File

@ -5,6 +5,7 @@
const {Cc, Ci, Cu} = require("chrome");
const {rgbToHsl} = require("devtools/css-color").colorUtils;
const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js");
const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
Cu.import("resource://gre/modules/Services.jsm");
@ -112,6 +113,8 @@ function Eyedropper(chromeWindow, opts = { copyOnSelect: true }) {
this._chromeWindow = chromeWindow;
this._chromeDocument = chromeWindow.document;
this._OS = XULRuntime.OS;
this._dragging = true;
this.loaded = false;
@ -126,6 +129,10 @@ function Eyedropper(chromeWindow, opts = { copyOnSelect: true }) {
width: CANVAS_WIDTH, // width of canvas to draw zoomed area onto
height: CANVAS_WIDTH // height of canvas
};
let mm = this._contentTab.linkedBrowser.messageManager;
mm.loadFrameScript("resource:///modules/devtools/eyedropper/eyedropper-child.js", true);
EventEmitter.decorate(this);
}
@ -167,6 +174,30 @@ Eyedropper.prototype = {
return rgb;
},
get _contentTab() {
return this._chromeWindow.gBrowser.selectedTab;
},
/**
* Fetch a screenshot of the content.
*
* @return {promise}
* Promise that resolves with the screenshot as a dataURL
*/
getContentScreenshot: function() {
let deferred = promise.defer();
let mm = this._contentTab.linkedBrowser.messageManager;
function onScreenshot(message) {
mm.removeMessageListener("Eyedropper:Screenshot", onScreenshot);
deferred.resolve(message.data);
}
mm.addMessageListener("Eyedropper:Screenshot", onScreenshot);
mm.sendAsyncMessage("Eyedropper:RequestContentScreenshot");
return deferred.promise;
},
/**
* Start the eyedropper. Add listeners for a mouse move in the window to
* show the eyedropper.
@ -174,15 +205,31 @@ Eyedropper.prototype = {
open: function() {
if (this.isOpen) {
// the eyedropper is aready open, don't create another panel.
return;
return promise.resolve();
}
let deferred = promise.defer();
this.isOpen = true;
this._OS = XULRuntime.OS;
this._chromeDocument.addEventListener("mousemove", this._onFirstMouseMove);
this._showCrosshairs();
// Get screenshot of content so we can inspect colors
this.getContentScreenshot().then((dataURL) => {
this._contentImage = new this._chromeWindow.Image();
this._contentImage.src = dataURL;
// Wait for screenshot to load
this._contentImage.onload = () => {
// Then start showing the eyedropper UI
this._chromeDocument.addEventListener("mousemove", this._onFirstMouseMove);
deferred.resolve();
this.isStarted = true;
this.emit("started");
}
});
return deferred.promise;
},
/**
@ -209,6 +256,25 @@ Eyedropper.prototype = {
this._hideCursor();
},
/**
* Whether the coordinates are over the content or chrome.
*
* @param {number} clientX
* x-coordinate of mouse relative to browser window.
* @param {number} clientY
* y-coordinate of mouse relative to browser window.
*/
_isInContent: function(clientX, clientY) {
let box = this._contentTab.linkedBrowser.getBoundingClientRect();
if (clientX > box.left &&
clientX < box.right &&
clientY > box.top &&
clientY < box.bottom) {
return true;
}
return false;
},
/**
* Set the current coordinates to inspect from where a mousemove originated.
*
@ -216,21 +282,23 @@ Eyedropper.prototype = {
* Event for the mouse move.
*/
_setCoordinates: function(event) {
let inContent = this._isInContent(event.clientX, event.clientY);
let win = this._chromeWindow;
let x, y;
if (this._OS == "Linux") {
// event.clientX is off on Linux, so calculate it by hand
let windowX = win.screenX + (win.outerWidth - win.innerWidth);
x = event.screenX - windowX;
// offset of mouse from browser window
let x = event.clientX;
let y = event.clientY;
let windowY = win.screenY + (win.outerHeight - win.innerHeight);
y = event.screenY - windowY;
}
else {
x = event.clientX;
y = event.clientY;
if (inContent) {
// calculate the offset of the mouse from the content window
let box = this._contentTab.linkedBrowser.getBoundingClientRect();
x = x - box.left;
y = y - box.top;
this._zoomArea.contentWidth = box.width;
this._zoomArea.contentHeight = box.height;
}
this._zoomArea.inContent = inContent;
// don't let it inspect outside the browser window
x = Math.max(0, Math.min(x, win.outerWidth - 1));
@ -548,29 +616,66 @@ Eyedropper.prototype = {
* Draw the inspected area onto the canvas using the zoom level.
*/
_drawWindow: function() {
let { width, height, x, y } = this._zoomArea;
let { width, height, x, y, inContent,
contentWidth, contentHeight } = this._zoomArea;
let zoomedWidth = width / this.zoom;
let zoomedHeight = height / this.zoom;
let drawX = x - (zoomedWidth / 2);
let drawY = y - (zoomedHeight / 2);
let leftX = x - (zoomedWidth / 2);
let topY = y - (zoomedHeight / 2);
// draw the portion of the window we're inspecting
this._ctx.drawWindow(this._chromeWindow, drawX, drawY, zoomedWidth,
zoomedHeight, "white");
if (inContent) {
// draw from content source image "s" to destination rect "d"
let sx = leftX;
let sy = topY;
let sw = zoomedWidth;
let sh = zoomedHeight;
let dx = 0;
let dy = 0;
// we're at the content edge, so we have to crop the drawing
if (leftX < 0) {
sx = 0;
sw = zoomedWidth + leftX;
dx = -leftX;
}
else if (leftX + zoomedWidth > contentWidth) {
sw = contentWidth - leftX;
}
if (topY < 0) {
sy = 0;
sh = zoomedHeight + topY;
dy = -topY;
}
else if (topY + zoomedHeight > contentHeight) {
sh = contentHeight - topY;
}
let dw = sw;
let dh = sh;
// we don't want artifacts when we're inspecting the edges of content
if (leftX < 0 || topY < 0 ||
leftX + zoomedWidth > contentWidth ||
topY + zoomedHeight > contentHeight) {
this._ctx.fillStyle = "white";
this._ctx.fillRect(0, 0, width, height);
}
// draw from the screenshot to the eyedropper canvas
this._ctx.drawImage(this._contentImage, sx, sy, sw,
sh, dx, dy, dw, dh);
}
else {
// the mouse is over the chrome, so draw that instead of the content
this._ctx.drawWindow(this._chromeWindow, leftX, topY, zoomedWidth,
zoomedHeight, "white");
}
// now scale it
let sx = 0;
let sy = 0;
let sw = zoomedWidth;
let sh = zoomedHeight;
let dx = 0;
let dy = 0;
let dw = width;
let dh = height;
this._ctx.drawImage(this._canvas, sx, sy, sw, sh, dx, dy, dw, dh);
this._ctx.drawImage(this._canvas, 0, 0, zoomedWidth, zoomedHeight,
0, 0, width, height);
let rgb = this.centerColor;
this._colorPreview.style.backgroundColor = toColorString(rgb, "rgb");
@ -635,6 +740,7 @@ Eyedropper.prototype = {
this._removePanelListeners();
this._removeListeners();
this.isStarted = false;
this.isOpen = false;
this._isSelecting = false;

View File

@ -6,6 +6,7 @@
EXTRA_JS_MODULES.devtools.eyedropper += [
'commands.js',
'eyedropper-child.js',
'eyedropper.js'
]

View File

@ -1,5 +1,4 @@
[DEFAULT]
skip-if = e10s # Bug ?????? - devtools tests disabled with e10s
subsuite = devtools
support-files =
color-block.html

View File

@ -10,53 +10,69 @@ const DIV_COLOR = "#0000FF";
* - Opening eyedropper and pressing ESC closes the eyedropper
* - Opening eyedropper and clicking copies the center color
*/
function test() {
addTab(TESTCASE_URI).then(testEscape);
}
let test = asyncTest(function*() {
yield addTab(TESTCASE_URI);
function testEscape() {
info("added tab");
yield testEscape();
info("testing selecting a color");
yield testSelect();
});
function* testEscape() {
let dropper = new Eyedropper(window);
dropper.once("destroy", (event) => {
ok(true, "escape closed the eyedropper");
yield inspectPage(dropper, false);
// now test selecting a color
testSelect();
});
let destroyed = dropper.once("destroy");
pressESC();
yield destroyed;
inspectPage(dropper, false).then(pressESC);
ok(true, "escape closed the eyedropper");
}
function testSelect() {
function* testSelect() {
let dropper = new Eyedropper(window);
dropper.once("select", (event, color) => {
is(color, DIV_COLOR, "correct color selected");
});
let selected = dropper.once("select");
let copied = waitForClipboard(() => {}, DIV_COLOR);
// wait for DIV_COLOR to be copied to the clipboard then finish the test.
waitForClipboard(DIV_COLOR, () => {
inspectPage(dropper); // setup: inspect the page
}, finish, finish);
yield inspectPage(dropper);
let color = yield selected;
is(color, DIV_COLOR, "correct color selected");
// wait for DIV_COLOR to be copied to the clipboard
yield copied;
}
/* Helpers */
function inspectPage(dropper, click=true) {
dropper.open();
function* inspectPage(dropper, click=true) {
yield dropper.open();
let target = content.document.getElementById("test");
let win = content.window;
info("dropper opened");
EventUtils.synthesizeMouse(target, 20, 20, { type: "mousemove" }, win);
let target = document.documentElement;
let win = window;
return dropperLoaded(dropper).then(() => {
EventUtils.synthesizeMouse(target, 30, 30, { type: "mousemove" }, win);
// get location of the <div> in the content, offset from browser window
let box = gBrowser.selectedTab.linkedBrowser.getBoundingClientRect();
let x = box.left + 100;
let y = box.top + 100;
if (click) {
EventUtils.synthesizeMouse(target, 30, 30, {}, win);
}
});
EventUtils.synthesizeMouse(target, x, y, { type: "mousemove" }, win);
yield dropperLoaded(dropper);
EventUtils.synthesizeMouse(target, x + 10, y + 10, { type: "mousemove" }, win);
if (click) {
EventUtils.synthesizeMouse(target, x + 10, y + 10, {}, win);
}
}
function pressESC() {

View File

@ -31,26 +31,29 @@ function spawnTest() {
}
function inspectAndWaitForCopy() {
let deferred = promise.defer();
waitForClipboard(DIV_COLOR, () => {
return waitForClipboard(() => {
inspectPage(); // setup: inspect the page
}, deferred.resolve, deferred.reject);
return deferred.promise;
}, DIV_COLOR);
}
function inspectPage() {
let target = content.document.getElementById("test");
let win = content.window;
let target = document.documentElement;
let win = window;
EventUtils.synthesizeMouse(target, 20, 20, { type: "mousemove" }, win);
// get location of the <div> in the content, offset from browser window
let box = gBrowser.selectedTab.linkedBrowser.getBoundingClientRect();
let x = box.left + 100;
let y = box.top + 100;
let dropper = EyedropperManager.getInstance(window);
return dropperLoaded(dropper).then(() => {
EventUtils.synthesizeMouse(target, 30, 30, { type: "mousemove" }, win);
return dropperStarted(dropper).then(() => {
EventUtils.synthesizeMouse(target, x, y, { type: "mousemove" }, win);
EventUtils.synthesizeMouse(target, 30, 30, {}, win);
});
return dropperLoaded(dropper).then(() => {
EventUtils.synthesizeMouse(target, x + 10, y + 10, { type: "mousemove" }, win);
EventUtils.synthesizeMouse(target, x + 10, y + 10, {}, win);
});
})
}

View File

@ -10,8 +10,8 @@
#test {
margin: 100px;
background-color: blue;
width: 200px;
height: 200px;
width: 20px;
height: 20px;
}
</style>
</head>

View File

@ -13,6 +13,13 @@ Services.scriptloader.loadSubScript(testDir + "../../../commandline/test/helpers
waitForExplicitFinish();
/**
* Define an async test based on a generator function
*/
function asyncTest(generator) {
return () => Task.spawn(generator).then(null, ok.bind(null, false)).then(finish);
}
function cleanup()
{
while (gBrowser.tabs.length > 1) {
@ -38,6 +45,19 @@ function addTab(uri) {
return deferred.promise;
}
function waitForClipboard(setup, expected) {
let deferred = promise.defer();
SimpleTest.waitForClipboard(expected, setup, deferred.resolve, deferred.reject);
return deferred.promise;
}
function dropperStarted(dropper) {
if (dropper.isStarted) {
return promise.resolve();
}
return dropper.once("started");
}
function dropperLoaded(dropper) {
if (dropper.loaded) {
return promise.resolve();

View File

@ -116,6 +116,7 @@ function Toolbox(target, selectedTool, hostType, hostOptions) {
}
this._defaultToolId = selectedTool;
this._hostOptions = hostOptions;
this._host = this._createHost(hostType, hostOptions);
EventEmitter.decorate(this);
@ -289,8 +290,12 @@ Toolbox.prototype = {
this._addKeysToWindow();
this._addReloadKeys();
this._addHostListeners();
this._addZoomKeys();
this._loadInitialZoom();
if (this._hostOptions && this._hostOptions.zoom === false) {
this._disableZoomKeys();
} else {
this._addZoomKeys();
this._loadInitialZoom();
}
this.webconsolePanel = this.doc.querySelector("#toolbox-panel-webconsole");
this.webconsolePanel.height =
@ -453,6 +458,20 @@ Toolbox.prototype = {
resetKey.addEventListener("command", this.zoomReset.bind(this), true);
},
_disableZoomKeys: function() {
let inKey = this.doc.getElementById("toolbox-zoom-in-key");
inKey.setAttribute("disabled", "true");
let inKey2 = this.doc.getElementById("toolbox-zoom-in-key2");
inKey2.setAttribute("disabled", "true");
let outKey = this.doc.getElementById("toolbox-zoom-out-key");
outKey.setAttribute("disabled", "true");
let resetKey = this.doc.getElementById("toolbox-zoom-reset-key");
resetKey.setAttribute("disabled", "true");
},
/**
* Set zoom on toolbox to whatever the last setting was.
*/

View File

@ -7,16 +7,24 @@
const PAGE_CONTENT = [
'<style type="text/css">',
' body {',
' background-color: #ff5;',
' padding: 50px',
' background-color: white;',
' padding: 0px',
' }',
' div {',
' width: 100px;',
' height: 100px;',
'',
' #div1 {',
' background-color: #ff5;',
' width: 20px;',
' height: 20px;',
' }',
'',
' #div2 {',
' margin-left: 20px;',
' width: 20px;',
' height: 20px;',
' background-color: #f09;',
' }',
'</style>',
'<body><div></div></body>'
'<body><div id="div1"></div><div id="div2"></div></body>'
].join("\n");
const ORIGINAL_COLOR = "rgb(255, 0, 153)"; // #f09
@ -30,9 +38,9 @@ let test = asyncTest(function*() {
content.document.body.innerHTML = PAGE_CONTENT;
let {toolbox, inspector, view} = yield openRuleView();
yield selectNode("div", inspector);
yield selectNode("#div2", inspector);
let property = getRuleViewProperty(view, "div", "background-color");
let property = getRuleViewProperty(view, "#div2", "background-color");
let swatch = property.valueSpan.querySelector(".ruleview-colorswatch");
ok(swatch, "Color swatch is displayed for the bg-color property");
@ -112,24 +120,38 @@ function openEyedropper(view, swatch) {
}
function inspectPage(dropper, click=true) {
let target = content.document.body;
let win = content.window;
let target = document.documentElement;
let win = window;
EventUtils.synthesizeMouse(target, 10, 10, { type: "mousemove" }, win);
// get location of the content, offset from browser window
let box = gBrowser.selectedTab.linkedBrowser.getBoundingClientRect();
let x = box.left + 1;
let y = box.top + 1;
return dropperLoaded(dropper).then(() => {
EventUtils.synthesizeMouse(target, 20, 20, { type: "mousemove" }, win);
if (click) {
EventUtils.synthesizeMouse(target, 20, 20, {}, win);
}
return dropperStarted(dropper).then(() => {
EventUtils.synthesizeMouse(target, x, y, { type: "mousemove" }, win);
return dropperLoaded(dropper).then(() => {
EventUtils.synthesizeMouse(target, x + 10, y + 10, { type: "mousemove" }, win);
if (click) {
EventUtils.synthesizeMouse(target, x + 10, y + 10, {}, win);
}
});
});
}
function dropperStarted(dropper) {
if (dropper.isStarted) {
return promise.resolve();
}
return dropper.once("started");
}
function dropperLoaded(dropper) {
if (dropper.loaded) {
return promise.resolve();
}
return dropper.once("load");
}

View File

@ -32,17 +32,13 @@ function consoleOpened(aHud) {
function testLocationColumn() {
let messages = hud.outputNode.children;
let expected = ['6:6', '6:38', '7:8', '8:10', '9:8', '10:6'];
let valid = true;
let expected = ['10:6', '10:38', '11:8', '12:10', '13:8', '14:6'];
for(let i = 0, len = messages.length; i < len; i++) {
let msg = messages[i].textContent;
is(!msg.contains(expected[i]), true, 'Found expected line:column of ' + expected[i])
is(msg.contains(expected[i]), true, 'Found expected line:column of ' + expected[i]);
}
is(valid, true, 'column numbers match expected results');
finishTest();
}
}

View File

@ -30,6 +30,9 @@ const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/
const HTML = "http://www.w3.org/1999/xhtml";
const HELP_URL = "https://developer.mozilla.org/docs/Tools/WebIDE/Troubleshooting";
const MAX_ZOOM = 1.4;
const MIN_ZOOM = 0.6;
// download template index early
GetTemplatesJSON(true);
@ -97,6 +100,12 @@ let UI = {
}
this.setupDeck();
this.contentViewer = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell)
.contentViewer;
this.contentViewer.fullZoom = Services.prefs.getCharPref("devtools.webide.zoom");
},
uninit: function() {
@ -738,6 +747,7 @@ let UI = {
}
deck.selectedPanel = panel;
this.updateProjectEditorMenusVisibility();
this.updateToolboxFullscreenState();
},
resetDeck: function() {
@ -896,9 +906,11 @@ let UI = {
splitter.removeAttribute("hidden");
let iframe = document.createElement("iframe");
iframe.id = "toolbox";
document.querySelector("notificationbox").insertBefore(iframe, splitter.nextSibling);
let host = devtools.Toolbox.HostType.CUSTOM;
let options = { customIframe: iframe };
let options = { customIframe: iframe, zoom: false };
this.toolboxIframe = iframe;
let height = Services.prefs.getIntPref("devtools.toolbox.footer.height");
@ -906,9 +918,22 @@ let UI = {
document.querySelector("#action-button-debug").setAttribute("active", "true");
this.updateToolboxFullscreenState();
return gDevTools.showToolbox(target, null, host, options);
},
updateToolboxFullscreenState: function() {
let panel = document.querySelector("#deck").selectedPanel;
let nbox = document.querySelector("#notificationbox");
if (panel.id == "deck-panel-details" &&
AppManager.selectedProject.type != "packaged" &&
this.toolboxIframe) {
nbox.setAttribute("toolboxfullscreen", "true");
} else {
nbox.removeAttribute("toolboxfullscreen");
}
},
closeToolboxUI: function() {
this.resetFocus();
Services.prefs.setIntPref("devtools.toolbox.footer.height", this.toolboxIframe.height);
@ -921,6 +946,7 @@ let UI = {
let splitter = document.querySelector(".devtools-horizontal-splitter");
splitter.setAttribute("hidden", "true");
document.querySelector("#action-button-debug").removeAttribute("active");
this.updateToolboxFullscreenState();
},
};
@ -1258,4 +1284,23 @@ let Cmds = {
showPrefs: function() {
UI.selectDeckPanel("prefs");
},
zoomIn: function() {
if (UI.contentViewer.fullZoom < MAX_ZOOM) {
UI.contentViewer.fullZoom += 0.1;
Services.prefs.setCharPref("devtools.webide.zoom", UI.contentViewer.fullZoom);
}
},
zoomOut: function() {
if (UI.contentViewer.fullZoom > MIN_ZOOM) {
UI.contentViewer.fullZoom -= 0.1;
Services.prefs.setCharPref("devtools.webide.zoom", UI.contentViewer.fullZoom);
}
},
resetZoom: function() {
UI.contentViewer.fullZoom = 1;
Services.prefs.setCharPref("devtools.webide.zoom", 1);
},
};

View File

@ -51,6 +51,9 @@
<command id="cmd_play" oncommand="Cmds.play()"/>
<command id="cmd_stop" oncommand="Cmds.stop()" label="&projectMenu_stop_label;"/>
<command id="cmd_toggleToolbox" oncommand="Cmds.toggleToolbox()"/>
<command id="cmd_zoomin" label="&viewMenu_zoomin_label;" oncommand="Cmds.zoomIn()"/>
<command id="cmd_zoomout" label="&viewMenu_zoomout_label;" oncommand="Cmds.zoomOut()"/>
<command id="cmd_resetzoom" label="&viewMenu_resetzoom_label;" oncommand="Cmds.resetZoom()"/>
</commandset>
</commandset>
@ -88,6 +91,10 @@
<menu id="menu-view" label="&viewMenu_label;" accesskey="&viewMenu_accesskey;">
<menupopup id="menu-ViewPopup">
<menuitem command="cmd_toggleEditor" key="key_toggleEditor" accesskey="&viewMenu_toggleEditor_accesskey;"/>
<menuseparator/>
<menuitem command="cmd_zoomin" key="key_zoomin" accesskey="&viewMenu_zoomin_accesskey;"/>
<menuitem command="cmd_zoomout" key="key_zoomout" accesskey="&viewMenu_zoomout_accesskey;"/>
<menuitem command="cmd_resetzoom" key="key_resetzoom" accesskey="&viewMenu_resetzoom_accesskey;"/>
</menupopup>
</menu>
@ -99,6 +106,10 @@
<key key="&key_play;" id="key_play" command="cmd_play" modifiers="accel"/>
<key key="&key_toggleEditor;" id="key_toggleEditor" command="cmd_toggleEditor" modifiers="accel"/>
<key keycode="&key_toggleToolbox;" id="key_toggleToolbox" command="cmd_toggleToolbox"/>
<key key="&key_zoomin;" id="key_zoomin" command="cmd_zoomin" modifiers="accel"/>
<key key="&key_zoomin2;" id="key_zoomin2" command="cmd_zoomin" modifiers="accel"/>
<key key="&key_zoomout;" id="key_zoomout" command="cmd_zoomout" modifiers="accel"/>
<key key="&key_resetzoom;" id="key_resetzoom" command="cmd_resetzoom" modifiers="accel"/>
</keyset>
<tooltip id="aHTMLTooltip" page="true"/>

View File

@ -39,3 +39,5 @@ support-files =
[test_autoconnect_runtime.html]
[test_telemetry.html]
[test_device_preferences.html]
[test_fullscreenToolbox.html]
[test_zoom.html]

View File

@ -0,0 +1,74 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
<title></title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
<script type="application/javascript;version=1.8" src="head.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
</head>
<body>
<script type="application/javascript;version=1.8">
function connectToLocal(win) {
let deferred = promise.defer();
win.AppManager.connection.once(
win.Connection.Events.CONNECTED,
() => deferred.resolve());
win.document.querySelectorAll(".runtime-panel-item-other")[1].click();
return deferred.promise;
}
window.onload = function() {
SimpleTest.waitForExplicitFinish();
Task.spawn(function* () {
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
win = yield openWebIDE();
win.AppManager.update("runtimelist");
yield connectToLocal(win);
yield waitForUpdate(win, "list-tabs-response");
// Select main process
yield win.Cmds.showProjectPanel();
SimpleTest.executeSoon(() => {
win.document.querySelectorAll("#project-panel-runtimeapps .panel-item")[0].click();
});
yield waitForUpdate(win, "project");
ok(win.UI.toolboxPromise, "Toolbox promise exists");
yield win.UI.toolboxPromise;
ok(win.UI.toolboxIframe, "Toolbox iframe exists");
let nbox = win.document.querySelector("#notificationbox");
ok(nbox.hasAttribute("toolboxfullscreen"), "Toolbox is fullsreen");
win.Cmds.showRuntimeDetails();
ok(!nbox.hasAttribute("toolboxfullscreen"), "Toolbox is not fullscreen");
yield win.Cmds.disconnectRuntime();
yield closeWebIDE(win);
const { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
DebuggerServer.destroy();
SimpleTest.finish();
});
}
</script>
</body>
</html>

View File

@ -0,0 +1,75 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
<title></title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
<script type="application/javascript;version=1.8" src="head.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
</head>
<body>
<script type="application/javascript;version=1.8">
window.onload = function() {
SimpleTest.waitForExplicitFinish();
Task.spawn(function* () {
let win = yield openWebIDE();
let viewer = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell)
.contentViewer;
win.Cmds.zoomOut();
win.Cmds.zoomOut();
win.Cmds.zoomOut();
win.Cmds.zoomOut();
win.Cmds.zoomOut();
win.Cmds.zoomOut();
win.Cmds.zoomOut();
let roundZoom = Math.round(10 * viewer.fullZoom) / 10;
is(roundZoom, 0.6, "Reach min zoom");
win.Cmds.zoomIn();
win.Cmds.zoomIn();
win.Cmds.zoomIn();
win.Cmds.zoomIn();
win.Cmds.zoomIn();
win.Cmds.zoomIn();
win.Cmds.zoomIn();
win.Cmds.zoomIn();
win.Cmds.zoomIn();
win.Cmds.zoomIn();
win.Cmds.zoomIn();
win.Cmds.zoomIn();
roundZoom = Math.round(10 * viewer.fullZoom) / 10;
is(roundZoom, 1.4, "Reach max zoom");
yield closeWebIDE(win);
win = yield openWebIDE();
viewer = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell)
.contentViewer;
roundZoom = Math.round(10 * viewer.fullZoom) / 10;
is(roundZoom, 1.4, "Zoom restored");
win.Cmds.resetZoom();
is(viewer.fullZoom, 1, "Zoom reset");
SimpleTest.finish();
});
}
</script>
</body>
</html>

View File

@ -291,3 +291,14 @@ panel > .panel-arrowcontainer > .panel-arrowcontent {
position: relative;
border-bottom: 1px solid #aaa;
}
/* Toolbox */
#notificationbox[toolboxfullscreen] > .devtools-horizontal-splitter,
#notificationbox[toolboxfullscreen] > #deck {
display: none;
}
#notificationbox[toolboxfullscreen] > #toolbox {
-moz-box-flex: 1;
}

View File

@ -30,3 +30,4 @@ pref("devtools.webide.widget.inNavbarByDefault", true);
pref("devtools.webide.widget.enabled", false);
pref("devtools.webide.widget.inNavbarByDefault", false);
#endif
pref("devtools.webide.zoom", "1");

View File

@ -31,7 +31,9 @@
!define DDEApplication "Firefox"
!define AppRegName "Firefox"
!ifndef DEV_EDITION
!define BrandShortName "@MOZ_APP_DISPLAYNAME@"
!endif
!define BrandFullName "${BrandFullNameInternal}"
!define NO_UNINSTALL_SURVEY

View File

@ -46,6 +46,12 @@
<!ENTITY viewMenu_accesskey "V">
<!ENTITY viewMenu_toggleEditor_label "Toggle Editor">
<!ENTITY viewMenu_toggleEditor_accesskey "E">
<!ENTITY viewMenu_zoomin_label "Zoom In">
<!ENTITY viewMenu_zoomin_accesskey "I">
<!ENTITY viewMenu_zoomout_label "Zoom Out">
<!ENTITY viewMenu_zoomout_accesskey "O">
<!ENTITY viewMenu_resetzoom_label "Reset Zoom">
<!ENTITY viewMenu_resetzoom_accesskey "R">
<!ENTITY projectButton_label "Open App">
<!ENTITY runtimeButton_label "Select Runtime">
@ -61,6 +67,11 @@
<!ENTITY key_toggleToolbox "VK_F12">
<!-- toggle sidebar -->
<!ENTITY key_toggleEditor "B">
<!-- zoom -->
<!ENTITY key_zoomin "+">
<!ENTITY key_zoomin2 "=">
<!ENTITY key_zoomout "-">
<!ENTITY key_resetzoom "0">
<!ENTITY projectPanel_myProjects "My Projects">
<!ENTITY projectPanel_runtimeApps "Runtime Apps">

View File

@ -31,7 +31,7 @@
margin: 0;
border: none;
padding: 2px 6px;
background: #252C33;
background: var(--chrome-nav-buttons-background);
box-shadow: none !important;
}
@ -42,7 +42,7 @@
#back-button:hover:not([disabled="true"]) > .toolbarbutton-icon,
#forward-button:hover:not([disabled="true"]) > .toolbarbutton-icon {
background: #1B2127 !important;
background: var(--chrome-nav-buttons-hover-background) !important;
}
#back-button > .toolbarbutton-icon {
@ -61,3 +61,12 @@
#new-tab-button {
background: var(--chrome-background-color);
}
#new-tab-button:hover > .toolbarbutton-icon {
border-color: transparent !important;
}
/* Prevent double border below tabs toolbar */
#TabsToolbar:not([collapsed="true"]) + #nav-bar {
border-top-width: 0 !important;
}

View File

@ -4,6 +4,19 @@
%include ../shared/devedition.inc.css
/* Use forward-facing magnifying glasses for the search box */
:root[devtoolstheme="dark"] {
--search-button-image: url("chrome://browser/skin/devedition/search.svg#search-icon-mac-inverted");
}
:root[devtoolstheme="light"] {
--search-button-image: url("chrome://browser/skin/devedition/search.svg#search-icon-mac");
}
/* Use only 1px separator between nav toolbox and page content */
#navigator-toolbox::after {
background: linear-gradient(to top, var(--chrome-navigator-toolbox-separator-color), var(--chrome-navigator-toolbox-separator-color) 1px, transparent 1px);
}
/* Include extra space on left/right for dragging since there is no space above
the tabs */
#main-window[tabsintitlebar] #TabsToolbar {
@ -31,12 +44,12 @@
height: 22px !important;
box-shadow: none !important;
border: none !important;
background: #252C33 !important;
background: var(--chrome-nav-buttons-background) !important;
}
#back-button:hover:not([disabled="true"]),
#forward-button:hover:not([disabled="true"]) {
background: #1B2127 !important;
background: var(--chrome-nav-buttons-hover-background) !important;
}
#back-button {
@ -65,11 +78,6 @@
box-shadow: none;
}
/* Use forward-facing magnifying glass for the search box */
.search-go-button {
list-style-image: url("chrome://browser/skin/devedition/search.svg#search-icon-mac-inverted");
}
/* Don't use the default background for tabs toolbar */
#TabsToolbar {
-moz-appearance: none !important;

View File

@ -7,24 +7,35 @@
there are overrides for each platform in their devedition.css files. */
:root {
/* Chrome */
--space-above-tabbar: 1px;
--toolbarbutton-text-shadow: none;
--panel-ui-button-background-size: 1px calc(100% - 1px);
--panel-ui-button-background-position: 1px 0px;
}
:root[devtoolstheme="dark"] {
/* Chrome */
--chrome-background-color: #1C2126;
--chrome-color: #F5F7FA;
--chrome-secondary-background-color: #39424D;
--chrome-navigator-toolbox-separator-color: rgba(0,0,0,.2);
--chrome-nav-bar-separator-color: rgba(0,0,0,.2);
--chrome-nav-buttons-background: #252C33;
--chrome-nav-buttons-hover-background: #1B2127;
--chrome-selection-color: #fff;
--chrome-selection-background-color: #074D75;
/* Tabs */
--tabs-toolbar-color: #F5F7FA;
--tab-background-color: #1C2126;
--tab-color: #ced3d9;
--tab-hover-background-color: hsla(206,37%,4%,.5);
--tab-separator-color: #464C50;
--tab-hover-background-color: #07090a;
--tab-separator-color: #474C50;
--tab-selection-color: #f5f7fa;
--tab-selection-background-color: #1a4666;
--tab-selection-box-shadow: 0 2px 0 #d7f1ff inset,
0 8px 3px -5px #2b82bf inset,
0 -2px 0 rgba(0,0,0,.2) inset;
0 -1px 0 rgba(0,0,0,.2) inset;
/* Toolbar buttons */
--toolbarbutton-hover-background: rgba(25,33, 38,.6) linear-gradient(rgba(25,33,38,.6), rgba(25,33,38,.6)) padding-box;
@ -36,7 +47,6 @@
--toolbarbutton-checkedhover-backgroundcolor: #1D4F73;
--toolbarbutton-combined-boxshadow: none;
--toolbarbutton-combined-backgroundimage: linear-gradient(#5F6670 0, #5F6670 18px);
--toolbarbutton-text-shadow: none;
/* Identity box */
--identity-box-chrome-color: #46afe3;
@ -53,18 +63,59 @@
--urlbar-dropmarker-2x-url: url("chrome://browser/skin/devedition/urlbar-history-dropmarker.svg");
--urlbar-dropmarker-2x-region: rect(0px, 11px, 14px, 0px);
--urlbar-dropmarker-active-2x-region: rect(0px, 22px, 14px, 11px);
--search-button-image: url("chrome://browser/skin/devedition/search.svg#search-icon-inverted");
/* Menu button separator */
--panel-ui-button-background-image: linear-gradient(to bottom, #5E6670, #5E6670);
--panel-ui-button-background-size: 1px calc(100% - 1px);
--panel-ui-button-background-position: 1px 0px;
--panel-ui-button-background-image: linear-gradient(to bottom, transparent, #5F6670 30%, #5F6670 70%, transparent);
}
.searchbar-dropmarker-image {
:root[devtoolstheme="dark"] .searchbar-dropmarker-image {
--searchbar-dropmarker-url: url("chrome://browser/skin/devtools/dropmarker.svg");
--searchbar-dropmarker-2x-url: url("chrome://browser/skin/devtools/dropmarker.svg");
}
:root[devtoolstheme="light"] {
--url-and-searchbar-background-color: #fff;
--chrome-background-color: #E3E4E6;
--chrome-color: #18191a;
--chrome-secondary-background-color: #f0f1f2;
--chrome-navigator-toolbox-separator-color: #cccccc;
--chrome-nav-bar-separator-color: #B6B6B8;
--chrome-nav-buttons-background: #f0f1f2;
--chrome-nav-buttons-hover-background: #DADBDB;
--chrome-selection-color: #f5f7fa;
--chrome-selection-background-color: #4c9ed9;
--tab-color: #18191a;
--tab-background-color: #E3E4E6;
--tab-hover-background-color: rgba(170,170,170,.2);
--tab-color: #18191a;
--tab-separator-color: rgba(170,170,170,.5);
--tab-selection-color: #f5f7fa;
--tab-selection-background-color: #4c9ed9;
--tab-selection-box-shadow: 0 2px 0 #d7f1ff inset,
0 8px 3px -5px #319BDB inset,
0 -1px 0 #2A7CB1 inset;
/* Toolbar buttons */
--toolbarbutton-hover-background: #D7D7D8;
--toolbarbutton-hover-boxshadow: none;
--toolbarbutton-hover-bordercolor: rgba(0,0,0,0.1);
--toolbarbutton-active-background: rgba(76,158,217,.5) linear-gradient(rgba(76,158,217,.5), rgba(76,158,217,.5)) border-box
--toolbarbutton-active-boxshadow: none;
--toolbarbutton-active-bordercolor: rgba(0,0,0,0.3);
--toolbarbutton-checkedhover-backgroundcolor: rgba(0,0,0,0.2);
--toolbarbutton-combined-boxshadow: none;
--toolbarbutton-combined-backgroundimage: linear-gradient(rgba(0,0,0,0.1) 0, rgba(0,0,0,0.1) 18px);
/* Url and search bars */
--search-button-image: url("chrome://browser/skin/devedition/search.svg#search-icon");
/* Menu button separator */
--panel-ui-button-background-image: linear-gradient(to bottom, transparent, rgba(0,0,0,0.1) 30%, rgba(0,0,0,0.1) 70%, transparent);
}
/* Give some space to drag the window around while customizing
(normal space to left and right of tabs doesn't work in this case) */
#main-window[tabsintitlebar][customizing] {
@ -104,9 +155,10 @@
}
/* End override @tabCurveHalfWidth@ and @tabCurveWidth@ */
#navigator-toolbox ::-moz-selection {
background-color: #074D75;
color: #fff;
#urlbar ::-moz-selection,
#navigator-toolbox .searchbar-textbox ::-moz-selection {
background-color: var(--chrome-selection-background-color);
color: var(--chrome-selection-color);
}
/* Change the base colors for the browser chrome */
@ -119,7 +171,7 @@
}
#navigator-toolbox::after {
background: var(--chrome-navigator-toolbox-separator-color)
background: var(--chrome-navigator-toolbox-separator-color);
}
#navigator-toolbox > toolbar:not(#TabsToolbar):not(#toolbar-menubar),
@ -173,7 +225,7 @@ window:not([chromehidden~="toolbar"]) #urlbar-wrapper {
margin-top: 0 !important;
border: none !important;
border-radius: 0 !important;
box-shadow: 0 -1px var(--chrome-navigator-toolbox-separator-color) !important;
box-shadow: 0 1px var(--chrome-nav-bar-separator-color) inset !important;
background-image: none !important;
}
@ -192,7 +244,7 @@ window:not([chromehidden~="toolbar"]) #urlbar-wrapper {
.search-go-button {
/* !important is needed because searchbar.css is loaded after this */
-moz-image-region: auto !important;
list-style-image: url("chrome://browser/skin/devedition/search.svg#search-icon-inverted");
list-style-image: var(--search-button-image);
}
.tab-background {
@ -202,7 +254,7 @@ window:not([chromehidden~="toolbar"]) #urlbar-wrapper {
#tabbrowser-tabs[movingtab] > .tabbrowser-tab[beforeselected]:not([last-visible-tab])::after,
.tabbrowser-tab:not([selected]):not([afterselected-visible]):not([afterhovered]):not([first-visible-tab]):not(:hover)::before,
#tabbrowser-tabs:not([overflow]) > .tabbrowser-tab[last-visible-tab]:not([selected]):not([beforehovered]):not(:hover)::after {
background-image: linear-gradient(to top, #474C50, #474C50);
background-image: linear-gradient(to top, var(--tab-separator-color), var(--tab-separator-color));
background-position: 1px 0;
background-repeat: no-repeat;
background-size: 1px 100%;
@ -242,7 +294,7 @@ window:not([chromehidden~="toolbar"]) #urlbar-wrapper {
}
.tabbrowser-tab[selected] {
color: var(--tab-selection-color);
color: var(--tab-selection-color) !important; /* Override color: inherit */
background-color: var(--tab-selection-background-color);
box-shadow: var(--tab-selection-box-shadow);
}

View File

@ -10,7 +10,7 @@
#back-button > .toolbarbutton-icon,
#forward-button > .toolbarbutton-icon {
background: #252C33 !important;
background: var(--chrome-nav-buttons-background) !important;
border-radius: 0 !important;
width: auto !important;
height: auto !important;
@ -20,6 +20,18 @@
box-shadow: none !important;
}
/* Override a box shadow for disabled back button */
#main-window:not([customizing]) #back-button[disabled] > .toolbarbutton-icon {
box-shadow: none !important;
}
/* Override !important properties for hovered back button */
#main-window #back-button:hover:not([disabled="true"]) > .toolbarbutton-icon,
#main-window #forward-button:hover:not([disabled="true"]) > .toolbarbutton-icon {
background: var(--chrome-nav-buttons-hover-background) !important;
box-shadow: none !important;
}
#back-button > .toolbarbutton-icon {
border-radius: 2px 0 0 2px !important;
}

View File

@ -9007,7 +9007,7 @@ if test "$ACCESSIBILITY" -a "$MOZ_ENABLE_GTK" ; then
AC_DEFINE_UNQUOTED(ATK_REV_VERSION, $ATK_REV_VERSION)
fi
if test "$MOZ_UPDATE_CHANNEL" = "aurora"; then
if test -z "$RELEASE_BUILD" -a -z "$NIGHTLY_BUILD"; then
AC_DEFINE(MOZ_DEV_EDITION)
fi

View File

@ -1549,6 +1549,8 @@ public class BrowserApp extends GeckoApp
BrowserDB.getCount(getContentResolver(), "favicons"));
Telemetry.HistogramAdd("FENNEC_THUMBNAILS_COUNT",
BrowserDB.getCount(getContentResolver(), "thumbnails"));
Telemetry.HistogramAdd("FENNEC_READING_LIST_COUNT",
BrowserDB.getCount(getContentResolver(), "readinglist"));
Telemetry.HistogramAdd("BROWSER_IS_USER_DEFAULT", (isDefaultBrowser(Intent.ACTION_VIEW) ? 1 : 0));
if (Versions.feature16Plus) {
Telemetry.HistogramAdd("BROWSER_IS_ASSIST_DEFAULT", (isDefaultBrowser(Intent.ACTION_ASSIST) ? 1 : 0));

View File

@ -552,6 +552,9 @@ public class LocalBrowserDB {
} else if ("favicons".equals(database)) {
uri = mFaviconsUriWithProfile;
columns = new String[] { Favicons._ID };
} else if ("readinglist".equals(database)) {
uri = mReadingListUriWithProfile;
columns = new String[] { ReadingListItems._ID };
}
if (uri != null) {
final Cursor cursor = cr.query(uri, columns, constraint, null, null);

View File

@ -9,55 +9,62 @@ this.EXPORTED_SYMBOLS = ["PlacesTransactions"];
/**
* Overview
* --------
* This modules serves as the transactions manager for Places, and implements
* all the standard transactions for its UI commands (creating items, editing
* various properties, etc.). It shares most of its semantics with common
* command pattern implementations, the HTML5 Undo Manager in particular.
* However, the asynchronous design of [future] Places APIs, combined with the
* commitment to serialize all UI operations, makes things a little bit
* different. For example, when |undo| is called in order to undo the top undo
* entry, the caller cannot tell for sure what entry would it be because the
* execution of some transaction is either in process, or queued.
* This modules serves as the transactions manager for Places (hereinafter PTM).
* It implements all the elementary transactions for its UI commands: creating
* items, editing their various properties, and so forth.
*
* GUIDs and item-ids
* -------------------
* The Bookmarks API still relies heavily on item-ids, but since those do not
* play nicely with the concept of undo and redo (especially not in an
* asynchronous environment), this API only accepts bookmark GUIDs, both for
* input (e.g. for specifying the parent folder for a new bookmark) and for
* output (when the GUID for such a bookmark is propagated).
* Note that since the effect of invoking a Places command is not limited to the
* window in which it was performed (e.g. a folder created in the Library may be
* the parent of a bookmark created in some browser window), PTM is a singleton.
* It's therefore unnecessary to initialize PTM in any way apart importing this
* module.
*
* GUIDs are readily available when dealing with the "output" of this API and
* when result nodes are used (see nsINavHistoryResultNode::bookmarkGuid).
* If you only have item-ids in hand, use PlacesUtils.promiseItemGuid for
* converting them. Should you need to convert them back into itemIds, use
* PlacesUtils.promiseItemId.
* PTM shares most of its semantics with common command pattern implementations.
* However, the asynchronous design of contemporary and future APIs, combined
* with the commitment to serialize all UI operations, does make things a little
* bit different. For example, when |undo| is called in order to undo the top
* undo entry, the caller cannot tell for sure what entry would it be, because
* the execution of some transactions is either in process, or enqueued to be.
*
* The Standard Transactions
* Also note that unlike the nsITransactionManager, for example, this API is by
* no means generic. That is, it cannot be used to execute anything but the
* elementary transactions implemented here (Please file a bug if you find
* anything uncovered). More-complex transactions (e.g. creating a folder and
* moving a bookmark into it) may be implemented as a batch (see below).
*
* A note about GUIDs and item-ids
* -------------------------------
* There's an ongoing effort (see bug 1071511) to deprecate item-ids in Places
* in favor of GUIDs. Both because new APIs (e.g. Bookmark.jsm) expose them to
* the minimum necessary, and because GUIDs play much better with implementing
* |redo|, this API doesn't support item-ids at all, and only accepts bookmark
* GUIDs, both for input (e.g. for setting the parent folder for a new bookmark)
* and for output (when the GUID for such a bookmark is propagated).
*
* When working in conjugation with older Places API which only expose item ids,
* use PlacesUtils.promiseItemGuid for converting those to GUIDs (note that
* for result nodes, the guid is available through their bookmarkGuid getter).
* Should you need to convert GUIDs to item-ids, use PlacesUtils.promiseItemId.
*
* Constructing transactions
* -------------------------
* At the bottom of this module you will find implementations for all Places UI
* commands (One should almost never fallback to raw Places APIs. Please file
* a bug if you find anything uncovered). The transactions' constructors are
* set on the PlacesTransactions object (e.g. PlacesTransactions.NewFolder).
* The input for this constructors is taken in the form of a single argument
* plain object. Input properties may be either required (e.g. the |keyword|
* property for the EditKeyword transaction) or optional (e.g. the |keyword|
* property for NewBookmark). Once a transaction is created, you may pass it
* to |transact| or use it in the for batching (see next section).
*
* The constructors throw right away when any required input is missing or when
* some input is invalid "on the surface" (e.g. GUID values are validated to be
* 12-characters strings, but are not validated to point to existing item. Such
* an error will reveal when the transaction is executed).
* At the bottom of this module you will find transactions for all Places UI
* commands. They are exposed as constructors set on the PlacesTransactions
* object (e.g. PlacesTransactions.NewFolder). The input for this constructors
* is taken in the form of a single argument, a plain object consisting of the
* properties for the transaction. Input properties may be either required or
* optional (for example, |keyword| is required for the EditKeyword transaction,
* but optional for the NewBookmark transaction).
*
* To make things simple, a given input property has the same basic meaning and
* valid values across all transactions which accept it in the input object.
* Here is a list of all supported input properties along with their expected
* values:
* - uri: an nsIURI object or an uri spec string.
* - feedURI: an nsIURI object, holding the url for a live bookmark.
* - siteURI: an nsIURI object, holding the url for the site with which
* a live bookmark is associated.
* - url: a URL object, an nsIURI object, or a href.
* - urls: an array of urls, as above.
* - feedUrl: an url (as above), holding the url for a live bookmark.
* - siteUrl an url (as above), holding the url for the site with which
* a live bookmark is associated.
* - tag - a string.
* - tags: an array of strings.
* - guid, parentGuid, newParentGuid: a valid places GUID string.
@ -70,32 +77,87 @@ this.EXPORTED_SYMBOLS = ["PlacesTransactions"];
* - excludingAnnotation: a string (annotation name).
* - excludingAnnotations: an array of string (annotation names).
*
* Batching transactions
* ---------------------
* Sometimes it is useful to "batch" or "merge" transactions. For example,
* "Bookmark All Tabs" may be implemented as one NewFolder transaction followed
* by numerous NewBookmark transactions - all to be undone or redone in a single
* command. The |transact| method makes this possible using a generator
* function as an input. These generators have the same semantics as in
* Task.jsm except that when you yield a transaction, it's executed, and the
* resolution (e.g. the new bookmark GUID) is sent to the generator so you can
* use it as the input for another transaction. See |transact| for the details.
* If a required property is missing in the input object (e.g. not specifying
* parentGuid for NewBookmark), or if the value for any of the input properties
* is invalid "on the surface" (e.g. a numeric value for GUID, or a string that
* isn't 12-characters long), the transaction constructor throws right way.
* More complex errors (e.g. passing a non-existent GUID for parentGuid) only
* reveal once the transaction is executed.
*
* "Custom" transactions
* ---------------------
* In the legacy transactions API it was possible to pass-in transactions
* implemented "externally". For various reason this isn't allowed anymore:
* transact throws right away if one attempts to pass a transaction that was not
* created by this module. However, it's almost always possible to achieve the
* same functionality with the batching technique described above.
* Executing Transactions (the |transact| method of transactions)
* --------------------------------------------------------------
* Once a transaction is created, you must call its |transact| method for it to
* be executed and take effect. |transact| is an asynchronous method that takes
* no arguments, and returns a promise that resolves once the transaction is
* executed. Executing one of the transactions for creating items (NewBookmark,
* NewFolder, NewSeparator or NewLivemark) resolve to the new item's GUID.
* There's no resolution value for other transactions.
* If a transaction fails to execute, |transact| rejects and the transactions
* history is not affected.
*
* |transact| throws if it's called more than once (successfully or not) on the
* same transaction object.
*
* Batches
* -------
* Sometimes it is useful to "batch" or "merge" transactions. For example,
* something like "Bookmark All Tabs" may be implemented as one NewFolder
* transaction followed by numerous NewBookmark transactions - all to be undone
* or redone in a single undo or redo command. Use |PlacesTransactions.batch|
* in such cases. It can take either an array of transactions which will be
* executed in the given order and later be treated a a single entry in the
* transactions history, or a generator function that is passed to Task.spawn,
* that is to "contain" the batch: once the generator function is called a batch
* starts, and it lasts until the asynchronous generator iteration is complete
* All transactions executed by |transact| during this time are to be treated as
* a single entry in the transactions history.
*
* In both modes, |PlacesTransactions.batch| returns a promise that is to be
* resolved when the batch ends. In the array-input mode, there's no resolution
* value. In the generator mode, the resolution value is whatever the generator
* function returned (the semantics are the same as in Task.spawn, basically).
*
* The array-input mode of |PlacesTransactions.batch| is useful for implementing
* a batch of mostly-independent transaction (for example, |paste| into a folder
* can be implemented as a batch of multiple NewBookmark transactions).
* The generator mode is useful when the resolution value of executing one
* transaction is the input of one more subsequent transaction.
*
* In the array-input mode, if any transactions fails to execute, the batch
* continues (exceptions are logged). Only transactions that were executed
* successfully are added to the transactions history.
*
* WARNING: "nested" batches are not supported, if you call batch while another
* batch is still running, the new batch is enqueued with all other PTM work
* and thus not run until the running batch ends. The same goes for undo, redo
* and clearTransactionsHistory (note batches cannot be done partially, meaning
* undo and redo calls that during a batch are just enqueued).
*
* *****************************************************************************
* IT"S PARTICULARLY IMPORTANT NOT TO YIELD ANY PROMISE RETURNED BY ANY OF
* THESE METHODS (undo, redo, clearTransactionsHistory) FROM A BATCH FUNCTION.
* UNTIL WE FIND A WAY TO THROW IN THAT CASE (SEE BUG 1091446) DOING SO WILL
* COMPLETELY BREAK PTM UNTIL SHUTDOWN, NOT ALLOWING THE EXECUTION OF ANY
* TRANSACTION!
* *****************************************************************************
*
* Serialization
* -------------
* All |PlacesTransaction| operations are serialized. That is, even though the
* implementation is asynchronous, the order in which PlacesTransactions methods
* is called does guarantee the order in which they are to be invoked.
*
* The only exception to this rule is |transact| calls done during a batch (see
* above). |transact| calls are serialized with each other (and with undo, redo
* and clearTransactionsHistory), but they are, of course, not serialized with
* batches.
*
* The transactions-history structure
* ----------------------------------
* The transactions-history is a two-dimensional stack of transactions: the
* transactions are ordered in reverse to the order they were committed.
* It's two-dimensional because the undo manager allows batching transactions
* together for the purpose of undo or redo (batched transactions can never be
* undone or redone partially).
* It's two-dimensional because PTM allows batching transactions together for
* the purpose of undo or redo (see Batches above).
*
* The undoPosition property is set to the index of the top entry. If there is
* no entry at that index, there is nothing to undo.
@ -124,22 +186,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
XPCOMUtils.defineLazyModuleGetter(this, "console",
"resource://gre/modules/devtools/Console.jsm");
// Updates commands in the undo group of the active window commands.
// Inactive windows commands will be updated on focus.
function updateCommandsOnActiveWindow() {
// Updating "undo" will cause a group update including "redo".
try {
let win = Services.focus.activeWindow;
if (win)
win.updateCommands("undo");
}
catch(ex) { console.error(ex, "Couldn't update undo commands"); }
}
Components.utils.importGlobalProperties(["URL"]);
// The internal object for managing the transactions history.
// The public API is included in PlacesTransactions.
// TODO bug 982099: extending the array "properly" makes it painful to implement
// getters. If/when ES6 gets proper array subclassing we can revise this.
let TransactionsHistory = [];
TransactionsHistory.__proto__ = {
__proto__: Array.prototype,
@ -169,7 +217,9 @@ TransactionsHistory.__proto__ = {
* @see getRawTransaction for retrieving the raw transaction.
*/
proxifyTransaction: function (aRawTransaction) {
let proxy = Object.freeze({});
let proxy = Object.freeze({
transact() TransactionsManager.transact(this)
});
this.proxifiedToRaw.set(proxy, aRawTransaction);
return proxy;
},
@ -191,61 +241,7 @@ TransactionsHistory.__proto__ = {
* @return the transaction proxified by aProxy; |undefined| is returned if
* aProxy is not a proxified transaction.
*/
getRawTransaction: function (aProxy) this.proxifiedToRaw.get(aProxy),
/**
* Undo the top undo entry, if any, and update the undo position accordingly.
*/
undo: function* () {
let entry = this.topUndoEntry;
if (!entry)
return;
for (let transaction of entry) {
try {
yield TransactionsHistory.getRawTransaction(transaction).undo();
}
catch(ex) {
// If one transaction is broken, it's not safe to work with any other
// undo entry. Report the error and clear the undo history.
console.error(ex,
"Couldn't undo a transaction, clearing all undo entries.");
this.clearUndoEntries();
return;
}
}
this._undoPosition++;
updateCommandsOnActiveWindow();
},
/**
* Redo the top redo entry, if any, and update the undo position accordingly.
*/
redo: function* () {
let entry = this.topRedoEntry;
if (!entry)
return;
for (let i = entry.length - 1; i >= 0; i--) {
let transaction = TransactionsHistory.getRawTransaction(entry[i]);
try {
if (transaction.redo)
yield transaction.redo();
else
yield transaction.execute();
}
catch(ex) {
// If one transaction is broken, it's not safe to work with any other
// redo entry. Report the error and clear the undo history.
console.error(ex,
"Couldn't redo a transaction, clearing all redo entries.");
this.clearRedoEntries();
return;
}
}
this._undoPosition--;
updateCommandsOnActiveWindow();
},
getRawTransaction(aProxy) this.proxifiedToRaw.get(aProxy),
/**
* Add a transaction either as a new entry, if forced or if there are no undo
@ -259,7 +255,7 @@ TransactionsHistory.__proto__ = {
* If false, an entry will we created only if there's no undo entry
* to extend.
*/
add: function (aProxifiedTransaction, aForceNewEntry = false) {
add(aProxifiedTransaction, aForceNewEntry = false) {
if (!this.isProxifiedTransactionObject(aProxifiedTransaction))
throw new Error("aProxifiedTransaction is not a proxified transaction");
@ -270,13 +266,12 @@ TransactionsHistory.__proto__ = {
else {
this[this.undoPosition].unshift(aProxifiedTransaction);
}
updateCommandsOnActiveWindow();
},
/**
* Clear all undo entries.
*/
clearUndoEntries: function () {
clearUndoEntries() {
if (this.undoPosition < this.length)
this.splice(this.undoPosition);
},
@ -284,7 +279,7 @@ TransactionsHistory.__proto__ = {
/**
* Clear all redo entries.
*/
clearRedoEntries: function () {
clearRedoEntries() {
if (this.undoPosition > 0) {
this.splice(0, this.undoPosition);
this._undoPosition = 0;
@ -294,7 +289,7 @@ TransactionsHistory.__proto__ = {
/**
* Clear all entries.
*/
clearAllEntries: function () {
clearAllEntries() {
if (this.length > 0) {
this.splice(0);
this._undoPosition = 0;
@ -303,141 +298,36 @@ TransactionsHistory.__proto__ = {
};
// Our transaction manager is asynchronous in the sense that all of its methods
// don't execute synchronously. However, all actions must be serialized.
let currentTask = Promise.resolve();
function Serialize(aTask) {
// Ignore failures.
return currentTask = currentTask.then( () => Task.spawn(aTask) )
.then(null, Components.utils.reportError);
}
// Transactions object should never be recycled (that is, |execute| should
// only be called once, or not at all, after they're constructed.
// This keeps track of all transactions which were executed.
let executedTransactions = new WeakMap(); // TODO: use WeakSet (bug 792439)
executedTransactions.add = k => executedTransactions.set(k, null);
let PlacesTransactions = {
/**
* Asynchronously transact either a single transaction, or a sequence of
* transactions that would be treated as a single entry in the transactions
* history.
*
* @param aToTransact
* Either a transaction object or an array of transaction objects, or a
* generator function (ES6-style only) that yields transaction objects.
*
* Generator mode how-to: when a transaction is yielded, it's executed.
* Then, if it was executed successfully, the resolution of |execute|
* is sent to the generator. If |execute| threw or rejected, the
* exception is propagated to the generator.
* Any other value yielded by a generator function is handled the
* same way as in a Task (see Task.jsm).
*
* @return {Promise}
* @resolves either to the resolution of |execute|, in single-transaction
* mode, or to the return value of the generator, in generator-mode. For an
* array of transactions, there's no resolution value.
*
* @rejects either if |execute| threw, in single-transaction mode, or if
* the generator function threw (or didn't handle) an exception, in generator
* mode.
* @throws if aTransactionOrGeneratorFunction is neither a transaction object
* created by this module, nor an array of such object, nor a generator
* function.
* @note If no transaction was executed successfully, the transactions history
* is not affected.
*
* @note All PlacesTransactions operations are serialized. This means that the
* transactions history state may change by the time the transaction/generator
* is processed. It's guaranteed, however, that a generator function "blocks"
* the queue: that is, it is assured that no other operations are performed
* by or on PlacesTransactions until the generator returns. Keep in mind you
* are not protected from consumers who use the raw places APIs directly.
* @see Batches in the module documentation.
*/
transact: function (aToTransact) {
if (Array.isArray(aToTransact)) {
if (aToTransact.some(
batch(aToBatch) {
let batchFunc;
if (Array.isArray(aToBatch)) {
if (aToBatch.length == 0)
throw new Error("aToBatch must not be an empty array");
if (aToBatch.some(
o => !TransactionsHistory.isProxifiedTransactionObject(o))) {
throw new Error("aToTransact contains non-transaction element");
throw new Error("aToBatch contains non-transaction element");
}
// Proceed as if a generator yielding the transactions was passed in.
return this.transact(function* () {
for (let t of aToTransact) {
yield t;
return TransactionsManager.batch(function* () {
for (let txn of aToBatch) {
try {
yield txn.transact();
}
catch(ex) {
console.error(ex);
}
}
});
}
let isGeneratorObj =
o => Object.prototype.toString.call(o) == "[object Generator]";
let generator = null;
if (typeof(aToTransact) == "function") {
generator = aToTransact();
if (!isGeneratorObj(generator))
throw new Error("aToTransact is not a generator function");
}
else if (!TransactionsHistory.isProxifiedTransactionObject(aToTransact)) {
throw new Error("aToTransact is not a valid transaction object");
}
else if (executedTransactions.has(aToTransact)) {
throw new Error("Transactions objects may not be recycled.");
if (typeof(aToBatch) == "function") {
return TransactionsManager.batch(aToBatch);
}
return Serialize(function* () {
// The entry in the transactions history is created once the first
// transaction is committed. This means that if |transact| is called
// in its "generator mode" and no transactions are committed by the
// generator, the transactions history is left unchanged.
// Bug 982115: Depending on how this API is actually used we may revise
// this decision and make it so |transact| always forces a new entry.
let forceNewEntry = true;
function* transactOneTransaction(aTransaction) {
let retval =
yield TransactionsHistory.getRawTransaction(aTransaction).execute();
executedTransactions.add(aTransaction);
TransactionsHistory.add(aTransaction, forceNewEntry);
forceNewEntry = false;
return retval;
}
function* transactBatch(aGenerator) {
let error = false;
let sendValue = undefined;
while (true) {
let next = error ?
aGenerator.throw(sendValue) : aGenerator.next(sendValue);
sendValue = next.value;
if (isGeneratorObj(sendValue)) {
sendValue = yield transactBatch(sendValue);
}
else if (typeof(sendValue) == "object" && sendValue) {
if (TransactionsHistory.isProxifiedTransactionObject(sendValue)) {
if (executedTransactions.has(sendValue)) {
sendValue = new Error("Transactions may not be recycled.");
error = true;
}
else {
sendValue = yield transactOneTransaction(sendValue);
}
}
else if ("then" in sendValue) {
sendValue = yield sendValue;
}
}
if (next.done)
break;
}
return sendValue;
}
if (generator)
return yield transactBatch(generator);
else
return yield transactOneTransaction(aToTransact);
}.bind(this));
throw new Error("aToBatch must be either a function or a transactions array");
},
/**
@ -449,7 +339,7 @@ let PlacesTransactions = {
* @note All undo manager operations are queued. This means that transactions
* history may change by the time your request is fulfilled.
*/
undo: function () Serialize(() => TransactionsHistory.undo()),
undo() TransactionsManager.undo(),
/**
* Asynchronously redo the transaction immediately before the current undo
@ -460,7 +350,7 @@ let PlacesTransactions = {
* @note All undo manager operations are queued. This means that transactions
* history may change by the time your request is fulfilled.
*/
redo: function () Serialize(() => TransactionsHistory.redo()),
redo() TransactionsManager.redo(),
/**
* Asynchronously clear the undo, redo, or all entries from the transactions
@ -476,19 +366,8 @@ let PlacesTransactions = {
* @note All undo manager operations are queued. This means that transactions
* history may change by the time your request is fulfilled.
*/
clearTransactionsHistory:
function (aUndoEntries = true, aRedoEntries = true) {
return Serialize(function* () {
if (aUndoEntries && aRedoEntries)
TransactionsHistory.clearAllEntries();
else if (aUndoEntries)
TransactionsHistory.clearUndoEntries();
else if (aRedoEntries)
TransactionsHistory.clearRedoEntries();
else
throw new Error("either aUndoEntries or aRedoEntries should be true");
});
},
clearTransactionsHistory(aUndoEntries = true, aRedoEntries = true)
TransactionsManager.clearTransactionsHistory(aUndoEntries, aRedoEntries),
/**
* The numbers of entries in the transactions history.
@ -507,7 +386,7 @@ let PlacesTransactions = {
* @note the returned array is a clone of the history entry and is not
* kept in sync with the original entry if it changes.
*/
entry: function (aIndex) {
entry(aIndex) {
if (!Number.isInteger(aIndex) || aIndex < 0 || aIndex >= this.length)
throw new Error("Invalid index");
@ -533,6 +412,216 @@ let PlacesTransactions = {
get topRedoEntry() TransactionsHistory.topRedoEntry
};
/**
* Helper for serializing the calls to TransactionsManager methods. It allows
* us to guarantee that the order in which TransactionsManager asynchronous
* methods are called also enforces the order in which they're executed, and
* that they are never executed in parallel.
*
* In other words: Enqueuer.enqueue(aFunc1); Enqueuer.enqueue(aFunc2) is roughly
* the same as Task.spawn(aFunc1).then(Task.spawn(aFunc2)).
*/
function Enqueuer() {
this._promise = Promise.resolve();
}
Enqueuer.prototype = {
/**
* Spawn a functions once all previous functions enqueued are done running,
* and all promises passed to alsoWaitFor are no longer pending.
*
* @param aFunc
* @see Task.spawn.
* @return a promise that resolves once aFunc is done running. The promise
* "mirrors" the promise returned by aFunc.
*/
enqueue(aFunc) {
let promise = this._promise.then(Task.async(aFunc));
// Propagate exceptions to the caller, but dismiss them internally.
this._promise = promise.catch(console.error);
return promise;
},
/**
* Same as above, but for a promise returned by a function that already run.
* This is useful, for example, for serializing transact calls with undo calls,
* even though transact has its own Enqueuer.
*
* @param aPromise
* any promise.
*/
alsoWaitFor(aPromise) {
// We don't care if aPromise resolves or rejects, but just that is not
// pending anymore.
let promise = aPromise.catch(console.error);
this._promise = Promise.all([this._promise, promise]);
},
/**
* The promise for this queue.
*/
get promise() this._promise
};
let TransactionsManager = {
// See the documentation at the top of this file. |transact| calls are not
// serialized with |batch| calls.
_mainEnqueuer: new Enqueuer(),
_transactEnqueuer: new Enqueuer(),
// Is a batch in progress? set when we enter a batch function and unset when
// it's execution is done.
_batching: false,
// If a batch started, this indicates if we've already created an entry in the
// transactions history for the batch (i.e. if at least one transaction was
// executed successfully).
_createdBatchEntry: false,
// Transactions object should never be recycled (that is, |execute| should
// only be called once (or not at all) after they're constructed.
// This keeps track of all transactions which were executed.
_executedTransactions: new WeakSet(),
transact(aTxnProxy) {
let rawTxn = TransactionsHistory.getRawTransaction(aTxnProxy);
if (!rawTxn)
throw new Error("|transact| was called with an unexpected object");
if (this._executedTransactions.has(rawTxn))
throw new Error("Transactions objects may not be recycled.");
// Add it in advance so one doesn't accidentally do
// sameTxn.transact(); sameTxn.transact();
this._executedTransactions.add(rawTxn);
let promise = this._transactEnqueuer.enqueue(function* () {
// Don't try to catch exceptions. If execute fails, we better not add the
// transaction to the undo stack.
let retval = yield rawTxn.execute();
let forceNewEntry = !this._batching || !this._createdBatchEntry;
TransactionsHistory.add(aTxnProxy, forceNewEntry);
if (this._batching)
this._createdBatchEntry = true;
this._updateCommandsOnActiveWindow();
return retval;
}.bind(this));
this._mainEnqueuer.alsoWaitFor(promise);
return promise;
},
batch(aTask) {
return this._mainEnqueuer.enqueue(function* () {
this._batching = true;
this._createdBatchEntry = false;
let rv;
try {
// We should return here, but bug 958949 makes that impossible.
rv = (yield Task.spawn(aTask));
}
finally {
this._batching = false;
this._createdBatchEntry = false;
}
return rv;
}.bind(this));
},
/**
* Undo the top undo entry, if any, and update the undo position accordingly.
*/
undo() {
let promise = this._mainEnqueuer.enqueue(function* () {
let entry = TransactionsHistory.topUndoEntry;
if (!entry)
return;
for (let txnProxy of entry) {
try {
yield TransactionsHistory.getRawTransaction(txnProxy).undo();
}
catch(ex) {
// If one transaction is broken, it's not safe to work with any other
// undo entry. Report the error and clear the undo history.
console.error(ex,
"Couldn't undo a transaction, clearing all undo entries.");
TransactionsHistory.clearUndoEntries();
return;
}
}
TransactionsHistory._undoPosition++;
this._updateCommandsOnActiveWindow();
}.bind(this));
this._transactEnqueuer.alsoWaitFor(promise);
return promise;
},
/**
* Redo the top redo entry, if any, and update the undo position accordingly.
*/
redo() {
let promise = this._mainEnqueuer.enqueue(function* () {
let entry = TransactionsHistory.topRedoEntry;
if (!entry)
return;
for (let i = entry.length - 1; i >= 0; i--) {
let transaction = TransactionsHistory.getRawTransaction(entry[i]);
try {
if (transaction.redo)
yield transaction.redo();
else
yield transaction.execute();
}
catch(ex) {
// If one transaction is broken, it's not safe to work with any other
// redo entry. Report the error and clear the undo history.
console.error(ex,
"Couldn't redo a transaction, clearing all redo entries.");
TransactionsHistory.clearRedoEntries();
return;
}
}
TransactionsHistory._undoPosition--;
this._updateCommandsOnActiveWindow();
}.bind(this));
this._transactEnqueuer.alsoWaitFor(promise);
return promise;
},
clearTransactionsHistory(aUndoEntries, aRedoEntries) {
let promise = this._mainEnqueuer.enqueue(function* () {
if (aUndoEntries && aRedoEntries)
TransactionsHistory.clearAllEntries();
else if (aUndoEntries)
TransactionsHistory.clearUndoEntries();
else if (aRedoEntries)
TransactionsHistory.clearRedoEntries();
else
throw new Error("either aUndoEntries or aRedoEntries should be true");
}.bind(this));
this._transactEnqueuer.alsoWaitFor(promise);
return promise;
},
// Updates commands in the undo group of the active window commands.
// Inactive windows commands will be updated on focus.
_updateCommandsOnActiveWindow() {
// Updating "undo" will cause a group update including "redo".
try {
let win = Services.focus.activeWindow;
if (win)
win.updateCommands("undo");
}
catch(ex) { console.error(ex, "Couldn't update undo commands"); }
}
};
/**
* Internal helper for defining the standard transactions and their input.
* It takes the required and optional properties, and generates the public
@ -618,10 +707,13 @@ DefineTransaction.annotationObjectValidate = function (obj) {
throw new Error("Invalid annotation object");
};
DefineTransaction.uriValidate = function(uriOrSpec) {
if (uriOrSpec instanceof Components.interfaces.nsIURI)
return uriOrSpec;
return NetUtil.newURI(uriOrSpec);
DefineTransaction.urlValidate = function(url) {
// When this module is updated to use Bookmarks.jsm, we should actually
// convert nsIURIs/spec to URL objects.
if (url instanceof Components.interfaces.nsIURI)
return url;
let spec = url instanceof URL ? url.href : url;
return NetUtil.newURI(spec);
};
DefineTransaction.inputProps = new Map();
@ -672,8 +764,8 @@ function (aName, aBasePropertyName) {
return [for (e of aValue) baseProp.validateValue(e)];
},
// We allow setting either the array property itself (e.g. uris), or a
// single element of it (uri, in that example), that is then transformed
// We allow setting either the array property itself (e.g. urls), or a
// single element of it (url, in that example), that is then transformed
// into a single-element array.
validateInput: function (aInput, aRequired) {
if (aName in aInput) {
@ -763,8 +855,8 @@ function (aInput, aRequiredProps = [], aOptionalProps = []) {
// Update the documentation at the top of this module if you add or
// remove properties.
DefineTransaction.defineInputProps(["uri", "feedURI", "siteURI"],
DefineTransaction.uriValidate, null);
DefineTransaction.defineInputProps(["url", "feedUrl", "siteUrl"],
DefineTransaction.urlValidate, null);
DefineTransaction.defineInputProps(["guid", "parentGuid", "newParentGuid"],
DefineTransaction.guidValidate);
DefineTransaction.defineInputProps(["title"],
@ -777,7 +869,7 @@ DefineTransaction.defineInputProps(["index", "newIndex"],
PlacesUtils.bookmarks.DEFAULT_INDEX);
DefineTransaction.defineInputProps(["annotation"],
DefineTransaction.annotationObjectValidate);
DefineTransaction.defineArrayInputProp("uris", "uri");
DefineTransaction.defineArrayInputProp("urls", "url");
DefineTransaction.defineArrayInputProp("tags", "tag");
DefineTransaction.defineArrayInputProp("annotations", "annotation");
DefineTransaction.defineArrayInputProp("excludingAnnotations",
@ -955,12 +1047,12 @@ let PT = PlacesTransactions;
/**
* Transaction for creating a bookmark.
*
* Required Input Properties: uri, parentGuid.
* Required Input Properties: url, parentGuid.
* Optional Input Properties: index, title, keyword, annotations, tags.
*
* When this transaction is executed, it's resolved to the new bookmark's GUID.
*/
PT.NewBookmark = DefineTransaction(["parentGuid", "uri"],
PT.NewBookmark = DefineTransaction(["parentGuid", "url"],
["index", "title", "keyword", "postData",
"annotations", "tags"]);
PT.NewBookmark.prototype = Object.seal({
@ -1039,14 +1131,14 @@ PT.NewSeparator.prototype = Object.seal({
* Transaction for creating a live bookmark (see mozIAsyncLivemarks for the
* semantics).
*
* Required Input Properties: feedURI, title, parentGuid.
* Optional Input Properties: siteURI, index, annotations.
* Required Input Properties: feedUrl, title, parentGuid.
* Optional Input Properties: siteUrl, index, annotations.
*
* When this transaction is executed, it's resolved to the new livemark's
* GUID.
*/
PT.NewLivemark = DefineTransaction(["feedURI", "title", "parentGuid"],
["siteURI", "index", "annotations"]);
PT.NewLivemark = DefineTransaction(["feedUrl", "title", "parentGuid"],
["siteUrl", "index", "annotations"]);
PT.NewLivemark.prototype = Object.seal({
execute: function* (aFeedURI, aTitle, aParentGuid, aSiteURI, aIndex, aAnnos) {
let livemarkInfo = { title: aTitle
@ -1132,10 +1224,10 @@ PT.EditTitle.prototype = Object.seal({
/**
* Transaction for setting the URI for an item.
*
* Required Input Properties: guid, uri.
* Required Input Properties: guid, url.
*/
PT.EditURI = DefineTransaction(["guid", "uri"]);
PT.EditURI.prototype = Object.seal({
PT.EditUrl = DefineTransaction(["guid", "url"]);
PT.EditUrl.prototype = Object.seal({
execute: function* (aGuid, aURI) {
let itemId = yield PlacesUtils.promiseItemId(aGuid),
oldURI = PlacesUtils.bookmarks.getBookmarkURI(itemId),
@ -1315,9 +1407,9 @@ PT.Remove.prototype = {
/**
* Transaction for tagging a URI.
*
* Required Input Properties: uris, tags.
* Required Input Properties: urls, tags.
*/
PT.Tag = DefineTransaction(["uris", "tags"]);
PT.Tag = DefineTransaction(["urls", "tags"]);
PT.Tag.prototype = {
execute: function* (aURIs, aTags) {
let onUndo = [], onRedo = [];
@ -1335,7 +1427,7 @@ PT.Tag.prototype = {
if (yield promiseIsBookmarked(currentURI)) {
// Tagging is only allowed for bookmarked URIs (but see 424160).
let createTxn = TransactionsHistory.getRawTransaction(
PT.NewBookmark({ uri: currentURI
PT.NewBookmark({ url: currentURI
, tags: aTags
, parentGuid: PlacesUtils.bookmarks.unfiledGuid }));
yield createTxn.execute();
@ -1370,12 +1462,12 @@ PT.Tag.prototype = {
/**
* Transaction for removing tags from a URI.
*
* Required Input Properties: uris.
* Required Input Properties: urls.
* Optional Input Properties: tags.
*
* If |tags| is not set, all tags set for |uri| are removed.
* If |tags| is not set, all tags set for |url| are removed.
*/
PT.Untag = DefineTransaction(["uris"], ["tags"]);
PT.Untag = DefineTransaction(["urls"], ["tags"]);
PT.Untag.prototype = {
execute: function* (aURIs, aTags) {
let onUndo = [], onRedo = [];

View File

@ -9,6 +9,8 @@ const tagssvc = PlacesUtils.tagging;
const annosvc = PlacesUtils.annotations;
const PT = PlacesTransactions;
Components.utils.importGlobalProperties(["URL"]);
// Create and add bookmarks observer.
let observer = {
__proto__: NavBookmarkObserver.prototype,
@ -48,7 +50,7 @@ let observer = {
, index: aIndex
, itemType: aItemType
, title: aTitle
, uri: aURI });
, url: aURI });
},
onItemRemoved:
@ -174,8 +176,8 @@ function ensureItemsAdded(...items) {
if (propName in item)
Assert.equal(info[propName], item[propName]);
}
if ("uri" in item)
Assert.ok(info.uri.equals(item.uri));
if ("url" in item)
Assert.ok(info.url.equals(item.url));
}
}
@ -204,8 +206,8 @@ function ensureItemsChanged(...items) {
let info = changes.get(item.property);
do_check_eq(info.isAnnoProperty, Boolean(item.isAnnoProperty));
do_check_eq(info.newValue, item.newValue);
if ("uri" in item)
do_check_true(item.uri.equals(info.uri));
if ("url" in item)
do_check_true(item.url.equals(info.url));
}
}
@ -325,21 +327,11 @@ function* ensureNonExistent(...aGuids) {
}
}
add_task(function* test_invalid_transact_calls() {
try {
PT.transact({ execute: () => {}, undo: () => {}, redo: () => {}});
do_throw("transact shouldn't accept 'external' transactions");
PT.transact(null);
do_throw("transact should throw for invalid arguments");
}
catch(ex) { }
});
add_task(function* test_recycled_transactions() {
function ensureTransactThrowsFor(aTransaction) {
let [txns, undoPosition] = getTransactionsHistoryState();
try {
yield PT.transact(aTransaction);
yield aTransaction.transact();
do_throw("Shouldn't be able to use the same transaction twice");
}
catch(ex) { }
@ -347,9 +339,9 @@ add_task(function* test_recycled_transactions() {
}
let txn_a = PT.NewFolder(yield createTestFolderInfo());
ensureTransactThrowsFor(txn_a);
yield PT.transact(txn_a);
yield txn_a.transact();
ensureUndoState([[txn_a]], 0);
yield ensureTransactThrowsFor(txn_a);
yield PT.undo();
ensureUndoState([[txn_a]], 1);
@ -360,14 +352,14 @@ add_task(function* test_recycled_transactions() {
ensureTransactThrowsFor(txn_a);
let txn_b = PT.NewFolder(yield createTestFolderInfo());
yield PT.transact(function* () {
yield PT.batch(function* () {
try {
yield txn_a;
yield txn_a.transact();
do_throw("Shouldn't be able to use the same transaction twice");
}
catch(ex) { }
ensureUndoState();
yield txn_b;
yield txn_b.transact();
});
ensureUndoState([[txn_b]], 0);
@ -381,25 +373,6 @@ add_task(function* test_recycled_transactions() {
observer.reset();
});
add_task(function* test_nested_batches() {
let txn_a = PT.NewFolder(yield createTestFolderInfo()),
txn_b = PT.NewFolder(yield createTestFolderInfo());
yield PT.transact(function* () {
yield txn_a;
yield (function*() {
yield txn_b;
}());
});
ensureUndoState([[txn_b, txn_a]], 0);
yield PT.undo();
ensureUndoState([[txn_b, txn_a]], 1);
yield PT.clearTransactionsHistory();
ensureUndoState();
observer.reset();
});
add_task(function* test_new_folder_with_annotation() {
const ANNO = { name: "TestAnno", value: "TestValue" };
let folder_info = yield createTestFolderInfo();
@ -407,7 +380,7 @@ add_task(function* test_new_folder_with_annotation() {
folder_info.annotations = [ANNO];
ensureUndoState();
let txn = PT.NewFolder(folder_info);
folder_info.guid = yield PT.transact(txn);
folder_info.guid = yield txn.transact();
let ensureDo = function* (aRedo = false) {
ensureUndoState([[txn]], 0);
yield ensureItemsAdded(folder_info);
@ -438,13 +411,13 @@ add_task(function* test_new_folder_with_annotation() {
add_task(function* test_new_bookmark() {
let bm_info = { parentGuid: yield PlacesUtils.promiseItemGuid(root)
, uri: NetUtil.newURI("http://test_create_item.com")
, url: NetUtil.newURI("http://test_create_item.com")
, index: bmStartIndex
, title: "Test creating an item" };
ensureUndoState();
let txn = PT.NewBookmark(bm_info);
bm_info.guid = yield PT.transact(txn);
bm_info.guid = yield txn.transact();
let ensureDo = function* (aRedo = false) {
ensureUndoState([[txn]], 0);
@ -475,16 +448,16 @@ add_task(function* test_new_bookmark() {
add_task(function* test_merge_create_folder_and_item() {
let folder_info = yield createTestFolderInfo();
let bm_info = { uri: NetUtil.newURI("http://test_create_item_to_folder.com")
let bm_info = { url: NetUtil.newURI("http://test_create_item_to_folder.com")
, title: "Test Bookmark"
, index: bmStartIndex };
let { folderTxn, bkmTxn } = yield PT.transact( function* () {
let { folderTxn, bkmTxn } = yield PT.batch(function* () {
let folderTxn = PT.NewFolder(folder_info);
folder_info.guid = bm_info.parentGuid = yield folderTxn;
folder_info.guid = bm_info.parentGuid = yield folderTxn.transact();
let bkmTxn = PT.NewBookmark(bm_info);
bm_info.guid = yield bkmTxn;;
return { folderTxn: folderTxn, bkmTxn: bkmTxn};
bm_info.guid = yield bkmTxn.transact();
return { folderTxn, bkmTxn };
});
let ensureDo = function* () {
@ -513,21 +486,21 @@ add_task(function* test_merge_create_folder_and_item() {
add_task(function* test_move_items_to_folder() {
let folder_a_info = yield createTestFolderInfo("Folder A");
let bkm_a_info = { uri: NetUtil.newURI("http://test_move_items.com")
let bkm_a_info = { url: new URL("http://test_move_items.com")
, title: "Bookmark A" };
let bkm_b_info = { uri: NetUtil.newURI("http://test_move_items.com")
let bkm_b_info = { url: NetUtil.newURI("http://test_move_items.com")
, title: "Bookmark B" };
// Test moving items within the same folder.
let [folder_a_txn, bkm_a_txn, bkm_b_txn] = yield PT.transact(function* () {
let [folder_a_txn, bkm_a_txn, bkm_b_txn] = yield PT.batch(function* () {
let folder_a_txn = PT.NewFolder(folder_a_info);
folder_a_info.guid =
bkm_a_info.parentGuid = bkm_b_info.parentGuid = yield folder_a_txn;
folder_a_info.guid = bkm_a_info.parentGuid = bkm_b_info.parentGuid =
yield folder_a_txn.transact();
let bkm_a_txn = PT.NewBookmark(bkm_a_info);
bkm_a_info.guid = yield bkm_a_txn;
bkm_a_info.guid = yield bkm_a_txn.transact();
let bkm_b_txn = PT.NewBookmark(bkm_b_info);
bkm_b_info.guid = yield bkm_b_txn;
bkm_b_info.guid = yield bkm_b_txn.transact();
return [folder_a_txn, bkm_a_txn, bkm_b_txn];
});
@ -535,7 +508,7 @@ add_task(function* test_move_items_to_folder() {
let moveTxn = PT.Move({ guid: bkm_a_info.guid
, newParentGuid: folder_a_info.guid });
yield PT.transact(moveTxn);
yield moveTxn.transact();
let ensureDo = () => {
ensureUndoState([[moveTxn], [bkm_b_txn, bkm_a_txn, folder_a_txn]], 0);
@ -570,14 +543,14 @@ add_task(function* test_move_items_to_folder() {
// Test moving items between folders.
let folder_b_info = yield createTestFolderInfo("Folder B");
let folder_b_txn = PT.NewFolder(folder_b_info);
folder_b_info.guid = yield PT.transact(folder_b_txn);
folder_b_info.guid = yield folder_b_txn.transact();
ensureUndoState([ [folder_b_txn]
, [bkm_b_txn, bkm_a_txn, folder_a_txn] ], 0);
moveTxn = PT.Move({ guid: bkm_a_info.guid
, newParentGuid: folder_b_info.guid
, newIndex: bmsvc.DEFAULT_INDEX });
yield PT.transact(moveTxn);
yield moveTxn.transact();
ensureDo = () => {
ensureUndoState([ [moveTxn]
@ -625,12 +598,12 @@ add_task(function* test_remove_folder() {
let folder_level_1_info = yield createTestFolderInfo("Folder Level 1");
let folder_level_2_info = { title: "Folder Level 2" };
let [folder_level_1_txn,
folder_level_2_txn] = yield PT.transact(function* () {
folder_level_2_txn] = yield PT.batch(function* () {
let folder_level_1_txn = PT.NewFolder(folder_level_1_info);
folder_level_1_info.guid = yield folder_level_1_txn;
folder_level_1_info.guid = yield folder_level_1_txn.transact();
folder_level_2_info.parentGuid = folder_level_1_info.guid;
let folder_level_2_txn = PT.NewFolder(folder_level_2_info);
folder_level_2_info.guid = yield folder_level_2_txn;
folder_level_2_info.guid = yield folder_level_2_txn.transact();
return [folder_level_1_txn, folder_level_2_txn];
});
@ -639,7 +612,7 @@ add_task(function* test_remove_folder() {
observer.reset();
let remove_folder_2_txn = PT.Remove(folder_level_2_info);
yield PT.transact(remove_folder_2_txn);
yield remove_folder_2_txn.transact();
ensureUndoState([ [remove_folder_2_txn]
, [folder_level_2_txn, folder_level_1_txn] ]);
@ -716,13 +689,13 @@ add_task(function* test_add_and_remove_bookmarks_with_additional_info() {
, ANNO = { name: "TestAnno", value: "TestAnnoValue" };
let folder_info = yield createTestFolderInfo();
folder_info.guid = yield PT.transact(PT.NewFolder(folder_info));
folder_info.guid = yield PT.NewFolder(folder_info).transact();
let ensureTags = ensureTagsForURI.bind(null, testURI);
// Check that the NewBookmark transaction preserves tags.
observer.reset();
let b1_info = { parentGuid: folder_info.guid, uri: testURI, tags: [TAG_1] };
b1_info.guid = yield PT.transact(PT.NewBookmark(b1_info));
let b1_info = { parentGuid: folder_info.guid, url: testURI, tags: [TAG_1] };
b1_info.guid = yield PT.NewBookmark(b1_info).transact();
ensureTags([TAG_1]);
yield PT.undo();
ensureTags([]);
@ -734,7 +707,7 @@ add_task(function* test_add_and_remove_bookmarks_with_additional_info() {
// Check if the Remove transaction removes and restores tags of children
// correctly.
yield PT.transact(PT.Remove(folder_info.guid));
yield PT.Remove(folder_info.guid).transact();
ensureTags([]);
observer.reset();
@ -755,11 +728,11 @@ add_task(function* test_add_and_remove_bookmarks_with_additional_info() {
// * Test the "keyword" property of the NewBookmark transaction.
observer.reset();
let b2_info = { parentGuid: folder_info.guid
, uri: testURI, tags: [TAG_1, TAG_2]
, url: testURI, tags: [TAG_1, TAG_2]
, keyword: KEYWORD
, postData: POST_DATA
, annotations: [ANNO] };
b2_info.guid = yield PT.transact(PT.NewBookmark(b2_info));
b2_info.guid = yield PT.NewBookmark(b2_info).transact();
let b2_post_creation_changes = [
{ guid: b2_info.guid
, isAnnoProperty: true
@ -788,9 +761,9 @@ add_task(function* test_add_and_remove_bookmarks_with_additional_info() {
// Test Remove for multiple items.
observer.reset();
yield PT.transact(PT.Remove(b1_info.guid));
yield PT.transact(PT.Remove(b2_info.guid));
yield PT.transact(PT.Remove(folder_info.guid));
yield PT.Remove(b1_info.guid).transact();
yield PT.Remove(b2_info.guid).transact();
yield PT.Remove(folder_info.guid).transact();
yield ensureItemsRemoved(b1_info, b2_info, folder_info);
ensureTags([]);
@ -835,11 +808,11 @@ add_task(function* test_creating_and_removing_a_separator() {
let undoEntries = [];
observer.reset();
let create_txns = yield PT.transact(function* () {
let create_txns = yield PT.batch(function* () {
let folder_txn = PT.NewFolder(folder_info);
folder_info.guid = separator_info.parentGuid = yield folder_txn;
folder_info.guid = separator_info.parentGuid = yield folder_txn.transact();
let separator_txn = PT.NewSeparator(separator_info);
separator_info.guid = yield separator_txn;
separator_info.guid = yield separator_txn.transact();
return [separator_txn, folder_txn];
});
undoEntries.unshift(create_txns);
@ -858,7 +831,7 @@ add_task(function* test_creating_and_removing_a_separator() {
observer.reset();
let remove_sep_txn = PT.Remove(separator_info);
yield PT.transact(remove_sep_txn);
yield remove_sep_txn.transact();
undoEntries.unshift([remove_sep_txn]);
ensureUndoState(undoEntries, 0);
ensureItemsRemoved(separator_info);
@ -898,16 +871,16 @@ add_task(function* test_creating_and_removing_a_separator() {
add_task(function* test_add_and_remove_livemark() {
let createLivemarkTxn = PT.NewLivemark(
{ feedURI: NetUtil.newURI("http://test.remove.livemark")
{ feedUrl: NetUtil.newURI("http://test.remove.livemark")
, parentGuid: yield PlacesUtils.promiseItemGuid(root)
, title: "Test Remove Livemark" });
let guid = yield PlacesTransactions.transact(createLivemarkTxn);
let guid = yield createLivemarkTxn.transact();
let originalInfo = yield PlacesUtils.promiseBookmarksTree(guid);
Assert.ok(originalInfo);
yield ensureLivemarkCreatedByAddLivemark(guid);
let removeTxn = PT.Remove(guid);
yield PT.transact(removeTxn);
yield removeTxn.transact();
yield ensureNonExistent(guid);
function* undo() {
ensureUndoState([[removeTxn], [createLivemarkTxn]], 0);
@ -941,7 +914,7 @@ add_task(function* test_add_and_remove_livemark() {
add_task(function* test_edit_title() {
let bm_info = { parentGuid: yield PlacesUtils.promiseItemGuid(root)
, uri: NetUtil.newURI("http://test_create_item.com")
, url: NetUtil.newURI("http://test_create_item.com")
, title: "Original Title" };
function ensureTitleChange(aCurrentTitle) {
@ -950,10 +923,10 @@ add_task(function* test_edit_title() {
, newValue: aCurrentTitle});
}
bm_info.guid = yield PT.transact(PT.NewBookmark(bm_info));
bm_info.guid = yield PT.NewBookmark(bm_info).transact();
observer.reset();
yield PT.transact(PT.EditTitle({ guid: bm_info.guid, title: "New Title" }));
yield PT.EditTitle({ guid: bm_info.guid, title: "New Title" }).transact();
ensureTitleChange("New Title");
observer.reset();
@ -979,7 +952,7 @@ add_task(function* test_edit_url() {
let oldURI = NetUtil.newURI("http://old.test_editing_item_uri.com/");
let newURI = NetUtil.newURI("http://new.test_editing_item_uri.com/");
let bm_info = { parentGuid: yield PlacesUtils.promiseItemGuid(root)
, uri: oldURI
, url: oldURI
, tags: ["TestTag"]};
function ensureURIAndTags(aPreChangeURI, aPostChangeURI, aOLdURITagsPreserved) {
@ -990,12 +963,12 @@ add_task(function* test_edit_url() {
ensureTagsForURI(aPreChangeURI, aOLdURITagsPreserved ? bm_info.tags : []);
}
bm_info.guid = yield PT.transact(PT.NewBookmark(bm_info));
bm_info.guid = yield PT.NewBookmark(bm_info).transact();
ensureTagsForURI(oldURI, bm_info.tags);
// When there's a single bookmark for the same url, tags should be moved.
observer.reset();
yield PT.transact(PT.EditURI({ guid: bm_info.guid, uri: newURI }));
yield PT.EditUrl({ guid: bm_info.guid, url: newURI }).transact();
ensureURIAndTags(oldURI, newURI, false);
observer.reset();
@ -1012,13 +985,13 @@ add_task(function* test_edit_url() {
// When there're multiple bookmarks for the same url, tags should be copied.
let bm2_info = Object.create(bm_info);
bm2_info.guid = yield PT.transact(PT.NewBookmark(bm2_info));
bm2_info.guid = yield PT.NewBookmark(bm2_info).transact();
let bm3_info = Object.create(bm_info);
bm3_info.uri = newURI;
bm3_info.guid = yield PT.transact(PT.NewBookmark(bm3_info));
bm3_info.url = newURI;
bm3_info.guid = yield PT.NewBookmark(bm3_info).transact();
observer.reset();
yield PT.transact(PT.EditURI({ guid: bm_info.guid, uri: newURI }));
yield PT.EditUrl({ guid: bm_info.guid, url: newURI }).transact();
ensureURIAndTags(oldURI, newURI, true);
observer.reset();
@ -1044,19 +1017,19 @@ add_task(function* test_edit_url() {
add_task(function* test_edit_keyword() {
let bm_info = { parentGuid: yield PlacesUtils.promiseItemGuid(root)
, uri: NetUtil.newURI("http://test.edit.keyword") };
, url: NetUtil.newURI("http://test.edit.keyword") };
const KEYWORD = "test_keyword";
bm_info.guid = yield PT.transact(PT.NewBookmark(bm_info));
bm_info.guid = yield PT.NewBookmark(bm_info).transact();
function ensureKeywordChange(aCurrentKeyword = "") {
ensureItemsChanged({ guid: bm_info.guid
, property: "keyword"
, newValue: aCurrentKeyword });
}
bm_info.guid = yield PT.transact(PT.NewBookmark(bm_info));
bm_info.guid = yield PT.NewBookmark(bm_info).transact();
observer.reset();
yield PT.transact(PT.EditKeyword({ guid: bm_info.guid, keyword: KEYWORD }));
yield PT.EditKeyword({ guid: bm_info.guid, keyword: KEYWORD }).transact();
ensureKeywordChange(KEYWORD);
observer.reset();
@ -1080,9 +1053,9 @@ add_task(function* test_edit_keyword() {
add_task(function* test_tag_uri() {
// This also tests passing uri specs.
let bm_info_a = { uri: "http://bookmarked.uri"
let bm_info_a = { url: "http://bookmarked.uri"
, parentGuid: yield PlacesUtils.promiseItemGuid(root) };
let bm_info_b = { uri: NetUtil.newURI("http://bookmarked2.uri")
let bm_info_b = { url: NetUtil.newURI("http://bookmarked2.uri")
, parentGuid: yield PlacesUtils.promiseItemGuid(root) };
let unbookmarked_uri = NetUtil.newURI("http://un.bookmarked.uri");
@ -1094,42 +1067,42 @@ add_task(function* test_tag_uri() {
return deferred.promise;
}
yield PT.transact(function* () {
bm_info_a.guid = yield PT.NewBookmark(bm_info_a);
bm_info_b.guid = yield PT.NewBookmark(bm_info_b);
yield PT.batch(function* () {
bm_info_a.guid = yield PT.NewBookmark(bm_info_a).transact();
bm_info_b.guid = yield PT.NewBookmark(bm_info_b).transact();
});
function* doTest(aInfo) {
let uris = "uri" in aInfo ? [aInfo.uri] : aInfo.uris;
let urls = "url" in aInfo ? [aInfo.url] : aInfo.urls;
let tags = "tag" in aInfo ? [aInfo.tag] : aInfo.tags;
let ensureURI = uri => typeof(uri) == "string" ? NetUtil.newURI(uri) : uri;
uris = [for (uri of uris) ensureURI(uri)];
let ensureURI = url => typeof(url) == "string" ? NetUtil.newURI(url) : url;
urls = [for (url of urls) ensureURI(url)];
let tagWillAlsoBookmark = new Set();
for (let uri of uris) {
if (!(yield promiseIsBookmarked(uri))) {
tagWillAlsoBookmark.add(uri);
for (let url of urls) {
if (!(yield promiseIsBookmarked(url))) {
tagWillAlsoBookmark.add(url);
}
}
function* ensureTagsSet() {
for (let uri of uris) {
ensureTagsForURI(uri, tags);
Assert.ok(yield promiseIsBookmarked(uri));
for (let url of urls) {
ensureTagsForURI(url, tags);
Assert.ok(yield promiseIsBookmarked(url));
}
}
function* ensureTagsUnset() {
for (let uri of uris) {
ensureTagsForURI(uri, []);
if (tagWillAlsoBookmark.has(uri))
Assert.ok(!(yield promiseIsBookmarked(uri)));
for (let url of urls) {
ensureTagsForURI(url, []);
if (tagWillAlsoBookmark.has(url))
Assert.ok(!(yield promiseIsBookmarked(url)));
else
Assert.ok(yield promiseIsBookmarked(uri));
Assert.ok(yield promiseIsBookmarked(url));
}
}
yield PT.transact(PT.Tag(aInfo));
yield PT.Tag(aInfo).transact();
yield ensureTagsSet();
yield PT.undo();
yield ensureTagsUnset();
@ -1139,10 +1112,10 @@ add_task(function* test_tag_uri() {
yield ensureTagsUnset();
}
yield doTest({ uri: bm_info_a.uri, tags: ["MyTag"] });
yield doTest({ uris: [bm_info_a.uri], tag: "MyTag" });
yield doTest({ uris: [bm_info_a.uri, bm_info_b.uri], tags: ["A, B"] });
yield doTest({ uris: [bm_info_a.uri, unbookmarked_uri], tag: "C" });
yield doTest({ url: bm_info_a.url, tags: ["MyTag"] });
yield doTest({ urls: [bm_info_a.url], tag: "MyTag" });
yield doTest({ urls: [bm_info_a.url, bm_info_b.url], tags: ["A, B"] });
yield doTest({ urls: [bm_info_a.url, unbookmarked_uri], tag: "C" });
// Cleanup
observer.reset();
@ -1154,55 +1127,55 @@ add_task(function* test_tag_uri() {
});
add_task(function* test_untag_uri() {
let bm_info_a = { uri: NetUtil.newURI("http://bookmarked.uri")
let bm_info_a = { url: NetUtil.newURI("http://bookmarked.uri")
, parentGuid: yield PlacesUtils.promiseItemGuid(root)
, tags: ["A", "B"] };
let bm_info_b = { uri: NetUtil.newURI("http://bookmarked2.uri")
let bm_info_b = { url: NetUtil.newURI("http://bookmarked2.uri")
, parentGuid: yield PlacesUtils.promiseItemGuid(root)
, tag: "B" };
yield PT.transact(function* () {
bm_info_a.guid = yield PT.NewBookmark(bm_info_a);
ensureTagsForURI(bm_info_a.uri, bm_info_a.tags);
bm_info_b.guid = yield PT.NewBookmark(bm_info_b);
ensureTagsForURI(bm_info_b.uri, [bm_info_b.tag]);
yield PT.batch(function* () {
bm_info_a.guid = yield PT.NewBookmark(bm_info_a).transact();
ensureTagsForURI(bm_info_a.url, bm_info_a.tags);
bm_info_b.guid = yield PT.NewBookmark(bm_info_b).transact();
ensureTagsForURI(bm_info_b.url, [bm_info_b.tag]);
});
function* doTest(aInfo) {
let uris, tagsToRemove;
let urls, tagsToRemove;
if (aInfo instanceof Ci.nsIURI) {
uris = [aInfo];
urls = [aInfo];
tagsRemoved = [];
}
else if (Array.isArray(aInfo)) {
uris = aInfo;
urls = aInfo;
tagsRemoved = [];
}
else {
uris = "uri" in aInfo ? [aInfo.uri] : aInfo.uris;
urls = "url" in aInfo ? [aInfo.url] : aInfo.urls;
tagsRemoved = "tag" in aInfo ? [aInfo.tag] : aInfo.tags;
}
let preRemovalTags = new Map();
for (let uri of uris) {
preRemovalTags.set(uri, tagssvc.getTagsForURI(uri));
for (let url of urls) {
preRemovalTags.set(url, tagssvc.getTagsForURI(url));
}
function ensureTagsSet() {
for (let uri of uris) {
ensureTagsForURI(uri, preRemovalTags.get(uri));
for (let url of urls) {
ensureTagsForURI(url, preRemovalTags.get(url));
}
}
function ensureTagsUnset() {
for (let uri of uris) {
for (let url of urls) {
let expectedTags = tagsRemoved.length == 0 ?
[] : [for (tag of preRemovalTags.get(uri))
[] : [for (tag of preRemovalTags.get(url))
if (tagsRemoved.indexOf(tag) == -1) tag];
ensureTagsForURI(uri, expectedTags);
ensureTagsForURI(url, expectedTags);
}
}
yield PT.transact(PT.Untag(aInfo));
yield PT.Untag(aInfo).transact();
yield ensureTagsUnset();
yield PT.undo();
yield ensureTagsSet();
@ -1214,13 +1187,13 @@ add_task(function* test_untag_uri() {
yield doTest(bm_info_a);
yield doTest(bm_info_b);
yield doTest(bm_info_a.uri);
yield doTest(bm_info_b.uri);
yield doTest([bm_info_a.uri, bm_info_b.uri]);
yield doTest({ uris: [bm_info_a.uri, bm_info_b.uri], tags: ["A", "B"] });
yield doTest({ uris: [bm_info_a.uri, bm_info_b.uri], tag: "B" });
yield doTest({ uris: [bm_info_a.uri, bm_info_b.uri], tag: "C" });
yield doTest({ uris: [bm_info_a.uri, bm_info_b.uri], tags: ["C"] });
yield doTest(bm_info_a.url);
yield doTest(bm_info_b.url);
yield doTest([bm_info_a.url, bm_info_b.url]);
yield doTest({ urls: [bm_info_a.url, bm_info_b.url], tags: ["A", "B"] });
yield doTest({ urls: [bm_info_a.url, bm_info_b.url], tag: "B" });
yield doTest({ urls: [bm_info_a.url, bm_info_b.url], tag: "C" });
yield doTest({ urls: [bm_info_a.url, bm_info_b.url], tags: ["C"] });
// Cleanup
observer.reset();
@ -1232,7 +1205,7 @@ add_task(function* test_untag_uri() {
});
add_task(function* test_annotate() {
let bm_info = { uri: NetUtil.newURI("http://test.item.annotation")
let bm_info = { url: NetUtil.newURI("http://test.item.annotation")
, parentGuid: yield PlacesUtils.promiseItemGuid(root) };
let anno_info = { name: "TestAnno", value: "TestValue" };
function ensureAnnoState(aSet) {
@ -1241,10 +1214,10 @@ add_task(function* test_annotate() {
, value: aSet ? anno_info.value : null }]);
}
bm_info.guid = yield PT.transact(PT.NewBookmark(bm_info));
bm_info.guid = yield PT.NewBookmark(bm_info).transact();
observer.reset();
yield PT.transact(PT.Annotate({ guid: bm_info.guid, annotation: anno_info }));
yield PT.Annotate({ guid: bm_info.guid, annotation: anno_info }).transact();
ensureAnnoState(true);
observer.reset();
@ -1257,8 +1230,8 @@ add_task(function* test_annotate() {
// Test removing the annotation by not passing the |value| property.
observer.reset();
yield PT.transact(PT.Annotate({ guid: bm_info.guid,
annotation: { name: anno_info.name }}));
yield PT.Annotate({ guid: bm_info.guid,
annotation: { name: anno_info.name }}).transact();
ensureAnnoState(false);
observer.reset();
@ -1275,7 +1248,7 @@ add_task(function* test_annotate() {
});
add_task(function* test_annotate_multiple() {
let guid = yield PT.transact(PT.NewFolder(yield createTestFolderInfo()));
let guid = yield PT.NewFolder(yield createTestFolderInfo()).transact();
let itemId = yield PlacesUtils.promiseItemId(guid);
function AnnoObj(aName, aValue) {
@ -1298,19 +1271,19 @@ add_task(function* test_annotate_multiple() {
Assert.deepEqual(currentAnnos, expectedAnnos);
}
yield PT.transact(PT.Annotate({ guid: guid, annotations: annos(1, 2) }));
yield PT.Annotate({ guid: guid, annotations: annos(1, 2) }).transact();
verifyAnnoValues(1, 2);
yield PT.undo();
verifyAnnoValues();
yield PT.redo();
verifyAnnoValues(1, 2);
yield PT.transact(PT.Annotate({ guid: guid
, annotation: { name: "A" } }));
yield PT.Annotate({ guid: guid
, annotation: { name: "A" } }).transact();
verifyAnnoValues(null, 2);
yield PT.transact(PT.Annotate({ guid: guid
, annotation: { name: "B", value: 0 } }));
yield PT.Annotate({ guid: guid
, annotation: { name: "B", value: 0 } }).transact();
verifyAnnoValues(null, 0);
yield PT.undo();
verifyAnnoValues(null, 2);
@ -1331,20 +1304,21 @@ add_task(function* test_annotate_multiple() {
add_task(function* test_sort_folder_by_name() {
let folder_info = yield createTestFolderInfo();
let uri = NetUtil.newURI("http://sort.by.name/");
let preSep = [{ title: i, uri: uri } for (i of ["3","2","1"])];
let url = NetUtil.newURI("http://sort.by.name/");
let preSep = [{ title: i, url } for (i of ["3","2","1"])];
let sep = {};
let postSep = [{ title: l, uri: uri } for (l of ["c","b","a"])];
let postSep = [{ title: l, url } for (l of ["c","b","a"])];
let originalOrder = [...preSep, sep, ...postSep];
let sortedOrder = [...preSep.slice(0).reverse(),
sep,
...postSep.slice(0).reverse()];
yield PT.transact(function* () {
folder_info.guid = yield PT.NewFolder(folder_info);
yield PT.batch(function* () {
folder_info.guid = yield PT.NewFolder(folder_info).transact();
for (let info of originalOrder) {
info.parentGuid = folder_info.guid;
info.guid = yield info == sep ?
PT.NewSeparator(info) : PT.NewBookmark(info);
PT.NewSeparator(info).transact() :
PT.NewBookmark(info).transact();
}
});
@ -1357,7 +1331,7 @@ add_task(function* test_sort_folder_by_name() {
}
ensureOrder(originalOrder);
yield PT.transact(PT.SortByName(folder_info.guid));
yield PT.SortByName(folder_info.guid).transact();
ensureOrder(sortedOrder);
yield PT.undo();
ensureOrder(originalOrder);
@ -1374,7 +1348,7 @@ add_task(function* test_sort_folder_by_name() {
add_task(function* test_livemark_txns() {
let livemark_info =
{ feedURI: NetUtil.newURI("http://test.feed.uri")
{ feedUrl: NetUtil.newURI("http://test.feed.uri")
, parentGuid: yield PlacesUtils.promiseItemGuid(root)
, title: "Test Livemark" };
function ensureLivemarkAdded() {
@ -1383,10 +1357,10 @@ add_task(function* test_livemark_txns() {
, parentGuid: livemark_info.parentGuid
, itemType: bmsvc.TYPE_FOLDER });
let annos = [{ name: PlacesUtils.LMANNO_FEEDURI
, value: livemark_info.feedURI.spec }];
if ("siteURI" in livemark_info) {
, value: livemark_info.feedUrl.spec }];
if ("siteUrl" in livemark_info) {
annos.push({ name: PlacesUtils.LMANNO_SITEURI
, value: livemark_info.siteURI.spec });
, value: livemark_info.siteUrl.spec });
}
ensureAnnotationsSet(livemark_info.guid, annos);
}
@ -1397,7 +1371,7 @@ add_task(function* test_livemark_txns() {
function* _testDoUndoRedoUndo() {
observer.reset();
livemark_info.guid = yield PT.transact(PT.NewLivemark(livemark_info));
livemark_info.guid = yield PT.NewLivemark(livemark_info).transact();
ensureLivemarkAdded();
observer.reset();
@ -1413,7 +1387,7 @@ add_task(function* test_livemark_txns() {
}
yield* _testDoUndoRedoUndo()
livemark_info.siteURI = NetUtil.newURI("http://feed.site.uri");
livemark_info.siteUrl = NetUtil.newURI("http://feed.site.uri");
yield* _testDoUndoRedoUndo();
// Cleanup
@ -1425,8 +1399,8 @@ add_task(function* test_copy() {
let rootGuid = yield PlacesUtils.promiseItemGuid(root);
function* duplicate_and_test(aOriginalGuid) {
yield duplicateGuid = yield PT.transact(
PT.Copy({ guid: aOriginalGuid, newParentGuid: rootGuid }));
yield duplicateGuid =
yield PT.Copy({ guid: aOriginalGuid, newParentGuid: rootGuid }).transact();
let originalInfo = yield PlacesUtils.promiseBookmarksTree(aOriginalGuid);
let duplicateInfo = yield PlacesUtils.promiseBookmarksTree(duplicateGuid);
yield ensureEqualBookmarksTrees(originalInfo, duplicateInfo, false);
@ -1456,33 +1430,35 @@ add_task(function* test_copy() {
}
// Test duplicating leafs (bookmark, separator, empty folder)
let bmTxn = PT.NewBookmark({ uri: NetUtil.newURI("http://test.item.duplicate")
let bmTxn = PT.NewBookmark({ url: new URL("http://test.item.duplicate")
, parentGuid: rootGuid
, annos: [{ name: "Anno", value: "AnnoValue"}] });
let sepTxn = PT.NewSeparator({ parentGuid: rootGuid, index: 1 });
let livemarkTxn = PT.NewLivemark(
{ feedURI: NetUtil.newURI("http://test.feed.uri")
{ feedUrl: new URL("http://test.feed.uri")
, parentGuid: yield PlacesUtils.promiseItemGuid(root)
, title: "Test Livemark", index: 1 });
let emptyFolderTxn = PT.NewFolder(yield createTestFolderInfo());
for (let txn of [livemarkTxn, sepTxn, emptyFolderTxn]) {
let guid = yield PT.transact(txn);
let guid = yield txn.transact();
yield duplicate_and_test(guid);
}
// Test duplicating a folder having some contents.
let filledFolderGuid = yield PT.transact(function *() {
let folderGuid = yield PT.NewFolder(yield createTestFolderInfo());
let nestedFolderGuid = yield PT.NewFolder({ parentGuid: folderGuid
, title: "Nested Folder" });
let filledFolderGuid = yield PT.batch(function *() {
let folderGuid =
yield PT.NewFolder(yield createTestFolderInfo()).transact();
let nestedFolderGuid =
yield PT.NewFolder({ parentGuid: folderGuid
, title: "Nested Folder" }).transact();
// Insert a bookmark under the nested folder.
yield PT.NewBookmark({ uri: NetUtil.newURI("http://nested.nested.bookmark")
, parentGuid: nestedFolderGuid });
yield PT.NewBookmark({ url: new URL("http://nested.nested.bookmark")
, parentGuid: nestedFolderGuid }).transact();
// Insert a separator below the nested folder
yield PT.NewSeparator({ parentGuid: folderGuid });
yield PT.NewSeparator({ parentGuid: folderGuid }).transact();
// And another bookmark.
yield PT.NewBookmark({ uri: NetUtil.newURI("http://nested.bookmark")
, parentGuid: folderGuid });
yield PT.NewBookmark({ url: new URL("http://nested.bookmark")
, parentGuid: folderGuid }).transact();
return folderGuid;
});
@ -1492,15 +1468,15 @@ add_task(function* test_copy() {
yield PT.clearTransactionsHistory();
});
add_task(function* test_array_input_for_transact() {
add_task(function* test_array_input_for_batch() {
let rootGuid = yield PlacesUtils.promiseItemGuid(root);
let folderTxn = PT.NewFolder(yield createTestFolderInfo());
let folderGuid = yield PT.transact(folderTxn);
let folderGuid = yield folderTxn.transact();
let sep1_txn = PT.NewSeparator({ parentGuid: folderGuid });
let sep2_txn = PT.NewSeparator({ parentGuid: folderGuid });
yield PT.transact([sep1_txn, sep2_txn]);
yield PT.batch([sep1_txn, sep2_txn]);
ensureUndoState([[sep2_txn, sep1_txn], [folderTxn]], 0);
let ensureChildCount = function* (count) {
@ -1532,7 +1508,7 @@ add_task(function* test_copy_excluding_annotations() {
let folderInfo = yield createTestFolderInfo();
let anno = n => { return { name: n, value: 1 } };
folderInfo.annotations = [anno("a"), anno("b"), anno("c")];
let folderGuid = yield PT.transact(PT.NewFolder(folderInfo));
let folderGuid = yield PT.NewFolder(folderInfo).transact();
let ensureAnnosSet = function* (guid, ...expectedAnnoNames) {
let tree = yield PlacesUtils.promiseBookmarksTree(guid);
@ -1544,15 +1520,15 @@ add_task(function* test_copy_excluding_annotations() {
yield ensureAnnosSet(folderGuid, "a", "b", "c");
let excluding_a_dupeGuid =
yield PT.transact(PT.Copy({ guid: folderGuid
, newParentGuid: rootGuid
, excludingAnnotation: "a" }));
yield PT.Copy({ guid: folderGuid
, newParentGuid: rootGuid
, excludingAnnotation: "a" }).transact();
yield ensureAnnosSet(excluding_a_dupeGuid, "b", "c");
let excluding_ac_dupeGuid =
yield PT.transact(PT.Copy({ guid: folderGuid
, newParentGuid: rootGuid
, excludingAnnotations: ["a", "c"] }));
yield PT.Copy({ guid: folderGuid
, newParentGuid: rootGuid
, excludingAnnotations: ["a", "c"] }).transact();
yield ensureAnnosSet(excluding_ac_dupeGuid, "b");
// Cleanup
@ -1566,12 +1542,12 @@ add_task(function* test_invalid_uri_spec_throws() {
let rootGuid = yield PlacesUtils.promiseItemGuid(root);
Assert.throws(() =>
PT.NewBookmark({ parentGuid: rootGuid
, uri: "invalid uri spec"
, url: "invalid uri spec"
, title: "test bookmark" }));
Assert.throws(() =>
PT.Tag({ tag: "TheTag"
, uris: ["invalid uri spec"] }));
, urls: ["invalid uri spec"] }));
Assert.throws(() =>
PT.Tag({ tag: "TheTag"
, uris: ["about:blank", "invalid uri spec"] }));
, urls: ["about:blank", "invalid uri spec"] }));
});

View File

@ -166,19 +166,19 @@ function* compareToNode(aItem, aNode, aIsRootItem, aExcludedGuids = []) {
let itemsCount = 0;
function* new_bookmark(aInfo) {
let currentItem = ++itemsCount;
if (!("uri" in aInfo))
aInfo.uri = uri("http://test.item." + itemsCount);
if (!("url" in aInfo))
aInfo.url = uri("http://test.item." + itemsCount);
if (!("title" in aInfo))
aInfo.title = "Test Item (bookmark) " + itemsCount;
yield PlacesTransactions.transact(PlacesTransactions.NewBookmark(aInfo));
yield PlacesTransactions.NewBookmark(aInfo).transact();
}
function* new_folder(aInfo) {
if (!("title" in aInfo))
aInfo.title = "Test Item (folder) " + itemsCount;
return yield PlacesTransactions.transact(PlacesTransactions.NewFolder(aInfo));
return yield PlacesTransactions.NewFolder(aInfo).transact();
}
// Walks a result nodes tree and test promiseBookmarksTree for each node.
@ -214,8 +214,8 @@ add_task(function* () {
yield new_folder({ parentGuid: PlacesUtils.bookmarks.menuGuid
, annotations: [{ name: "TestAnnoA", value: "TestVal"
, name: "TestAnnoB", value: 0 }]});
yield PlacesTransactions.transact(
PlacesTransactions.NewSeparator({ parentGuid: PlacesUtils.bookmarks.menuGuid }));
let sepInfo = { parentGuid: PlacesUtils.bookmarks.menuGuid };
yield PlacesTransactions.NewSeparator(sepInfo).transact();
let folderGuid = yield new_folder({ parentGuid: PlacesUtils.bookmarks.menuGuid });
yield new_bookmark({ title: null
, parentGuid: folderGuid
@ -223,7 +223,7 @@ add_task(function* () {
, tags: ["TestTagA", "TestTagB"]
, annotations: [{ name: "TestAnnoA", value: "TestVal2"}]});
let urlWithCharsetAndFavicon = uri("http://charset.and.favicon");
yield new_bookmark({ parentGuid: folderGuid, uri: urlWithCharsetAndFavicon });
yield new_bookmark({ parentGuid: folderGuid, url: urlWithCharsetAndFavicon });
yield PlacesUtils.setCharsetForURI(urlWithCharsetAndFavicon, "UTF-8");
yield promiseSetIconForPage(urlWithCharsetAndFavicon, SMALLPNG_DATA_URI);
// Test the default places root without specifying it.

View File

@ -3177,6 +3177,15 @@
"extended_statistics_ok": true,
"description": "Number of thumbnails stored in the browser DB"
},
"FENNEC_READING_LIST_COUNT": {
"expires_in_version": "40",
"kind": "exponential",
"high": "1000",
"n_buckets": 10,
"cpp_guard": "ANDROID",
"extended_statistics_ok": true,
"description": "Number of reading list items stored in the browser DB"
},
"PLACES_SORTED_BOOKMARKS_PERC": {
"expires_in_version": "never",
"kind": "linear",

View File

@ -16,7 +16,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
// because the native preferences code treats integers as NSPR PRInt32s,
// which are 32-bit signed integers on all platforms.
const MAX_INT = 0x7FFFFFFF; // Math.pow(2, 31) - 1
const MIN_INT = -MAX_INT;
const MIN_INT = -0x80000000;
this.Preferences =
function Preferences(args) {

View File

@ -142,9 +142,6 @@ this.LightweightThemeManager = {
},
previewTheme: function LightweightThemeManager_previewTheme(aData) {
if (!aData)
return;
let cancel = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
cancel.data = false;
Services.obs.notifyObservers(cancel, "lightweight-theme-preview-requested",