forked from mirrors/gecko-dev
This patch adds the ability to run SingletonEventManager handlers in different modes: sync, async, raw (no exception handling, arg cloning, or asynchrony), or asyncWithoutClone. When you call the handler, you're required to specify which variant you want. Existing uses of SingletonEventManager are all converted to async calls. Note that some of them were previously synchronous, but it didn't appear to be necessary. Also added a callOnClose for SingletonEventManager when the last listener is removed. MozReview-Commit-ID: ATHO97dWf3X --HG-- extra : rebase_source : bf02d79e3fbab84892be8a7e52ea7a1caf2e003d
374 lines
10 KiB
JavaScript
374 lines
10 KiB
JavaScript
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
|
/* vim: set sts=2 sw=2 et tw=80: */
|
|
"use strict";
|
|
|
|
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
|
|
|
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
|
const {
|
|
SingletonEventManager,
|
|
} = ExtensionUtils;
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
|
|
"resource://devtools/shared/event-emitter.js");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
|
|
"resource://gre/modules/PlacesUtils.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
|
"resource://gre/modules/Task.jsm");
|
|
|
|
let listenerCount = 0;
|
|
|
|
function getTree(rootGuid, onlyChildren) {
|
|
function convert(node, parent) {
|
|
let treenode = {
|
|
id: node.guid,
|
|
title: node.title || "",
|
|
index: node.index,
|
|
dateAdded: node.dateAdded / 1000,
|
|
};
|
|
|
|
if (parent && node.guid != PlacesUtils.bookmarks.rootGuid) {
|
|
treenode.parentId = parent.guid;
|
|
}
|
|
|
|
if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE) {
|
|
// This isn't quite correct. Recently Bookmarked ends up here ...
|
|
treenode.url = node.uri;
|
|
} else {
|
|
treenode.dateGroupModified = node.lastModified / 1000;
|
|
|
|
if (node.children && !onlyChildren) {
|
|
treenode.children = node.children.map(child => convert(child, node));
|
|
}
|
|
}
|
|
|
|
return treenode;
|
|
}
|
|
|
|
return PlacesUtils.promiseBookmarksTree(rootGuid, {
|
|
excludeItemsCallback: item => {
|
|
if (item.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) {
|
|
return true;
|
|
}
|
|
return item.annos &&
|
|
item.annos.find(a => a.name == PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO);
|
|
},
|
|
}).then(root => {
|
|
if (onlyChildren) {
|
|
let children = root.children || [];
|
|
return children.map(child => convert(child, root));
|
|
}
|
|
// It seems like the array always just contains the root node.
|
|
return [convert(root, null)];
|
|
}).catch(e => Promise.reject({message: e.message}));
|
|
}
|
|
|
|
function convert(result) {
|
|
let node = {
|
|
id: result.guid,
|
|
title: result.title || "",
|
|
index: result.index,
|
|
dateAdded: result.dateAdded.getTime(),
|
|
};
|
|
|
|
if (result.guid != PlacesUtils.bookmarks.rootGuid) {
|
|
node.parentId = result.parentGuid;
|
|
}
|
|
|
|
if (result.type == PlacesUtils.bookmarks.TYPE_BOOKMARK) {
|
|
node.url = result.url.href; // Output is always URL object.
|
|
} else {
|
|
node.dateGroupModified = result.lastModified.getTime();
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
let observer = {
|
|
skipTags: true,
|
|
skipDescendantsOnItemRemoval: true,
|
|
|
|
onBeginUpdateBatch() {},
|
|
onEndUpdateBatch() {},
|
|
|
|
onItemAdded(id, parentId, index, itemType, uri, title, dateAdded, guid, parentGuid, source) {
|
|
if (itemType == PlacesUtils.bookmarks.TYPE_SEPARATOR) {
|
|
return;
|
|
}
|
|
|
|
let bookmark = {
|
|
id: guid,
|
|
parentId: parentGuid,
|
|
index,
|
|
title,
|
|
dateAdded: dateAdded / 1000,
|
|
};
|
|
|
|
if (itemType == PlacesUtils.bookmarks.TYPE_BOOKMARK) {
|
|
bookmark.url = uri.spec;
|
|
} else {
|
|
bookmark.dateGroupModified = bookmark.dateAdded;
|
|
}
|
|
|
|
this.emit("created", bookmark);
|
|
},
|
|
|
|
onItemVisited() {},
|
|
|
|
onItemMoved(id, oldParentId, oldIndex, newParentId, newIndex, itemType, guid, oldParentGuid, newParentGuid, source) {
|
|
if (itemType == PlacesUtils.bookmarks.TYPE_SEPARATOR) {
|
|
return;
|
|
}
|
|
|
|
let info = {
|
|
parentId: newParentGuid,
|
|
index: newIndex,
|
|
oldParentId: oldParentGuid,
|
|
oldIndex,
|
|
};
|
|
this.emit("moved", {guid, info});
|
|
},
|
|
|
|
onItemRemoved(id, parentId, index, itemType, uri, guid, parentGuid, source) {
|
|
if (itemType == PlacesUtils.bookmarks.TYPE_SEPARATOR) {
|
|
return;
|
|
}
|
|
|
|
let node = {
|
|
id: guid,
|
|
parentId: parentGuid,
|
|
index,
|
|
};
|
|
|
|
if (itemType == PlacesUtils.bookmarks.TYPE_BOOKMARK) {
|
|
node.url = uri.spec;
|
|
}
|
|
|
|
this.emit("removed", {guid, info: {parentId: parentGuid, index, node}});
|
|
},
|
|
|
|
onItemChanged(id, prop, isAnno, val, lastMod, itemType, parentId, guid, parentGuid, oldVal, source) {
|
|
if (itemType == PlacesUtils.bookmarks.TYPE_SEPARATOR) {
|
|
return;
|
|
}
|
|
|
|
let info = {};
|
|
if (prop == "title") {
|
|
info.title = val;
|
|
} else if (prop == "uri") {
|
|
info.url = val;
|
|
} else {
|
|
// Not defined yet.
|
|
return;
|
|
}
|
|
|
|
this.emit("changed", {guid, info});
|
|
},
|
|
};
|
|
EventEmitter.decorate(observer);
|
|
|
|
function decrementListeners() {
|
|
listenerCount -= 1;
|
|
if (!listenerCount) {
|
|
PlacesUtils.bookmarks.removeObserver(observer);
|
|
}
|
|
}
|
|
|
|
function incrementListeners() {
|
|
listenerCount++;
|
|
if (listenerCount == 1) {
|
|
PlacesUtils.bookmarks.addObserver(observer, false);
|
|
}
|
|
}
|
|
|
|
extensions.registerSchemaAPI("bookmarks", "addon_parent", context => {
|
|
return {
|
|
bookmarks: {
|
|
get: function(idOrIdList) {
|
|
let list = Array.isArray(idOrIdList) ? idOrIdList : [idOrIdList];
|
|
|
|
return Task.spawn(function* () {
|
|
let bookmarks = [];
|
|
for (let id of list) {
|
|
let bookmark = yield PlacesUtils.bookmarks.fetch({guid: id});
|
|
if (!bookmark) {
|
|
throw new Error("Bookmark not found");
|
|
}
|
|
bookmarks.push(convert(bookmark));
|
|
}
|
|
return bookmarks;
|
|
}).catch(error => Promise.reject({message: error.message}));
|
|
},
|
|
|
|
getChildren: function(id) {
|
|
// TODO: We should optimize this.
|
|
return getTree(id, true);
|
|
},
|
|
|
|
getTree: function() {
|
|
return getTree(PlacesUtils.bookmarks.rootGuid, false);
|
|
},
|
|
|
|
getSubTree: function(id) {
|
|
return getTree(id, false);
|
|
},
|
|
|
|
search: function(query) {
|
|
return PlacesUtils.bookmarks.search(query).then(result => result.map(convert));
|
|
},
|
|
|
|
getRecent: function(numberOfItems) {
|
|
return PlacesUtils.bookmarks.getRecent(numberOfItems).then(result => result.map(convert));
|
|
},
|
|
|
|
create: function(bookmark) {
|
|
let info = {
|
|
title: bookmark.title || "",
|
|
};
|
|
|
|
// If url is NULL or missing, it will be a folder.
|
|
if (bookmark.url !== null) {
|
|
info.type = PlacesUtils.bookmarks.TYPE_BOOKMARK;
|
|
info.url = bookmark.url || "";
|
|
} else {
|
|
info.type = PlacesUtils.bookmarks.TYPE_FOLDER;
|
|
}
|
|
|
|
if (bookmark.index !== null) {
|
|
info.index = bookmark.index;
|
|
}
|
|
|
|
if (bookmark.parentId !== null) {
|
|
info.parentGuid = bookmark.parentId;
|
|
} else {
|
|
info.parentGuid = PlacesUtils.bookmarks.unfiledGuid;
|
|
}
|
|
|
|
try {
|
|
return PlacesUtils.bookmarks.insert(info).then(convert)
|
|
.catch(error => Promise.reject({message: error.message}));
|
|
} catch (e) {
|
|
return Promise.reject({message: `Invalid bookmark: ${JSON.stringify(info)}`});
|
|
}
|
|
},
|
|
|
|
move: function(id, destination) {
|
|
let info = {
|
|
guid: id,
|
|
};
|
|
|
|
if (destination.parentId !== null) {
|
|
info.parentGuid = destination.parentId;
|
|
}
|
|
info.index = (destination.index === null) ?
|
|
PlacesUtils.bookmarks.DEFAULT_INDEX : destination.index;
|
|
|
|
try {
|
|
return PlacesUtils.bookmarks.update(info).then(convert)
|
|
.catch(error => Promise.reject({message: error.message}));
|
|
} catch (e) {
|
|
return Promise.reject({message: `Invalid bookmark: ${JSON.stringify(info)}`});
|
|
}
|
|
},
|
|
|
|
update: function(id, changes) {
|
|
let info = {
|
|
guid: id,
|
|
};
|
|
|
|
if (changes.title !== null) {
|
|
info.title = changes.title;
|
|
}
|
|
if (changes.url !== null) {
|
|
info.url = changes.url;
|
|
}
|
|
|
|
try {
|
|
return PlacesUtils.bookmarks.update(info).then(convert)
|
|
.catch(error => Promise.reject({message: error.message}));
|
|
} catch (e) {
|
|
return Promise.reject({message: `Invalid bookmark: ${JSON.stringify(info)}`});
|
|
}
|
|
},
|
|
|
|
remove: function(id) {
|
|
let info = {
|
|
guid: id,
|
|
};
|
|
|
|
// The API doesn't give you the old bookmark at the moment
|
|
try {
|
|
return PlacesUtils.bookmarks.remove(info, {preventRemovalOfNonEmptyFolders: true}).then(result => {})
|
|
.catch(error => Promise.reject({message: error.message}));
|
|
} catch (e) {
|
|
return Promise.reject({message: `Invalid bookmark: ${JSON.stringify(info)}`});
|
|
}
|
|
},
|
|
|
|
removeTree: function(id) {
|
|
let info = {
|
|
guid: id,
|
|
};
|
|
|
|
try {
|
|
return PlacesUtils.bookmarks.remove(info).then(result => {})
|
|
.catch(error => Promise.reject({message: error.message}));
|
|
} catch (e) {
|
|
return Promise.reject({message: `Invalid bookmark: ${JSON.stringify(info)}`});
|
|
}
|
|
},
|
|
|
|
onCreated: new SingletonEventManager(context, "bookmarks.onCreated", fire => {
|
|
let listener = (event, bookmark) => {
|
|
fire.sync(bookmark.id, bookmark);
|
|
};
|
|
|
|
observer.on("created", listener);
|
|
incrementListeners();
|
|
return () => {
|
|
observer.off("created", listener);
|
|
decrementListeners();
|
|
};
|
|
}).api(),
|
|
|
|
onRemoved: new SingletonEventManager(context, "bookmarks.onRemoved", fire => {
|
|
let listener = (event, data) => {
|
|
fire.sync(data.guid, data.info);
|
|
};
|
|
|
|
observer.on("removed", listener);
|
|
incrementListeners();
|
|
return () => {
|
|
observer.off("removed", listener);
|
|
decrementListeners();
|
|
};
|
|
}).api(),
|
|
|
|
onChanged: new SingletonEventManager(context, "bookmarks.onChanged", fire => {
|
|
let listener = (event, data) => {
|
|
fire.sync(data.guid, data.info);
|
|
};
|
|
|
|
observer.on("changed", listener);
|
|
incrementListeners();
|
|
return () => {
|
|
observer.off("changed", listener);
|
|
decrementListeners();
|
|
};
|
|
}).api(),
|
|
|
|
onMoved: new SingletonEventManager(context, "bookmarks.onMoved", fire => {
|
|
let listener = (event, data) => {
|
|
fire.sync(data.guid, data.info);
|
|
};
|
|
|
|
observer.on("moved", listener);
|
|
incrementListeners();
|
|
return () => {
|
|
observer.off("moved", listener);
|
|
decrementListeners();
|
|
};
|
|
}).api(),
|
|
},
|
|
};
|
|
});
|