michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: /** michael@0: * Wraps a list/grid control implementing nsIDOMXULSelectControlElement and michael@0: * fills it with the user's bookmarks. michael@0: * michael@0: * @param aSet Control implementing nsIDOMXULSelectControlElement. michael@0: * @param aRoot Bookmark root to show in the view. michael@0: */ michael@0: function BookmarksView(aSet, aRoot, aFilterUnpinned) { michael@0: View.call(this, aSet); michael@0: michael@0: this._inBatch = false; // batch up grid updates to avoid redundant arrangeItems calls michael@0: michael@0: // View monitors this for maximum tile display counts michael@0: this.tilePrefName = "browser.display.startUI.bookmarks.maxresults"; michael@0: this.showing = this.maxTiles > 0; michael@0: michael@0: this._filterUnpinned = aFilterUnpinned; michael@0: this._bookmarkService = PlacesUtils.bookmarks; michael@0: this._navHistoryService = gHistSvc; michael@0: michael@0: this._changes = new BookmarkChangeListener(this); michael@0: this._pinHelper = new ItemPinHelper("metro.bookmarks.unpinned"); michael@0: this._bookmarkService.addObserver(this._changes, false); michael@0: StartUI.chromeWin.addEventListener('MozAppbarDismissing', this, false); michael@0: StartUI.chromeWin.addEventListener('BookmarksNeedsRefresh', this, false); michael@0: window.addEventListener("TabClose", this, true); michael@0: michael@0: this.root = aRoot; michael@0: } michael@0: michael@0: BookmarksView.prototype = Util.extend(Object.create(View.prototype), { michael@0: _set: null, michael@0: _changes: null, michael@0: _root: null, michael@0: _sort: 0, // Natural bookmark order. michael@0: _toRemove: null, michael@0: michael@0: // For View's showing property michael@0: get vbox() { michael@0: return document.getElementById("start-bookmarks"); michael@0: }, michael@0: michael@0: get sort() { michael@0: return this._sort; michael@0: }, michael@0: michael@0: set sort(aSort) { michael@0: this._sort = aSort; michael@0: this.clearBookmarks(); michael@0: this.getBookmarks(); michael@0: }, michael@0: michael@0: get root() { michael@0: return this._root; michael@0: }, michael@0: michael@0: set root(aRoot) { michael@0: this._root = aRoot; michael@0: }, michael@0: michael@0: destruct: function bv_destruct() { michael@0: this._bookmarkService.removeObserver(this._changes); michael@0: if (StartUI.chromeWin) { michael@0: StartUI.chromeWin.removeEventListener('MozAppbarDismissing', this, false); michael@0: StartUI.chromeWin.removeEventListener('BookmarksNeedsRefresh', this, false); michael@0: } michael@0: View.prototype.destruct.call(this); michael@0: }, michael@0: michael@0: refreshView: function () { michael@0: this.clearBookmarks(); michael@0: this.getBookmarks(); michael@0: }, michael@0: michael@0: handleItemClick: function bv_handleItemClick(aItem) { michael@0: let url = aItem.getAttribute("value"); michael@0: StartUI.goToURI(url); michael@0: }, michael@0: michael@0: _getItemForBookmarkId: function bv__getItemForBookmark(aBookmarkId) { michael@0: return this._set.querySelector("richgriditem[anonid='" + aBookmarkId + "']"); michael@0: }, michael@0: michael@0: _getBookmarkIdForItem: function bv__getBookmarkForItem(aItem) { michael@0: return +aItem.getAttribute("anonid"); michael@0: }, michael@0: michael@0: _updateItemWithAttrs: function dv__updateItemWithAttrs(anItem, aAttrs) { michael@0: for (let name in aAttrs) michael@0: anItem.setAttribute(name, aAttrs[name]); michael@0: }, michael@0: michael@0: getBookmarks: function bv_getBookmarks(aRefresh) { michael@0: let options = this._navHistoryService.getNewQueryOptions(); michael@0: options.queryType = options.QUERY_TYPE_BOOKMARKS; michael@0: options.excludeQueries = true; // Don't include "smart folders" michael@0: options.sortingMode = this._sort; michael@0: michael@0: let limit = this.maxTiles; michael@0: michael@0: let query = this._navHistoryService.getNewQuery(); michael@0: query.setFolders([Bookmarks.metroRoot], 1); michael@0: michael@0: let result = this._navHistoryService.executeQuery(query, options); michael@0: let rootNode = result.root; michael@0: rootNode.containerOpen = true; michael@0: let childCount = rootNode.childCount; michael@0: michael@0: this._inBatch = true; // batch up grid updates to avoid redundant arrangeItems calls michael@0: michael@0: for (let i = 0, addedCount = 0; i < childCount && addedCount < limit; i++) { michael@0: let node = rootNode.getChild(i); michael@0: michael@0: // Ignore folders, separators, undefined item types, etc. michael@0: if (node.type != node.RESULT_TYPE_URI) michael@0: continue; michael@0: michael@0: // If item is marked for deletion, skip it. michael@0: if (this._toRemove && this._toRemove.indexOf(node.itemId) !== -1) michael@0: continue; michael@0: michael@0: let item = this._getItemForBookmarkId(node.itemId); michael@0: michael@0: // Item has been unpinned. michael@0: if (this._filterUnpinned && !this._pinHelper.isPinned(node.itemId)) { michael@0: if (item) michael@0: this.removeBookmark(node.itemId); michael@0: michael@0: continue; michael@0: } michael@0: michael@0: if (!aRefresh || !item) { michael@0: // If we're not refreshing or the item is not in the grid, add it. michael@0: this.addBookmark(node.itemId, addedCount); michael@0: } else if (aRefresh && item) { michael@0: // Update context action in case it changed in another view. michael@0: this._setContextActions(item); michael@0: } michael@0: michael@0: addedCount++; michael@0: } michael@0: michael@0: // Remove extra items in case a refresh added more than the limit. michael@0: // This can happen when undoing a delete. michael@0: if (aRefresh) { michael@0: while (this._set.itemCount > limit) michael@0: this._set.removeItemAt(this._set.itemCount - 1, true); michael@0: } michael@0: this._set.arrangeItems(); michael@0: this._inBatch = false; michael@0: rootNode.containerOpen = false; michael@0: }, michael@0: michael@0: inCurrentView: function bv_inCurrentView(aParentId, aItemId) { michael@0: if (this._root && aParentId != this._root) michael@0: return false; michael@0: michael@0: return !!this._getItemForBookmarkId(aItemId); michael@0: }, michael@0: michael@0: clearBookmarks: function bv_clearBookmarks() { michael@0: if ('clearAll' in this._set) michael@0: this._set.clearAll(); michael@0: }, michael@0: michael@0: addBookmark: function bv_addBookmark(aBookmarkId, aPos) { michael@0: let index = this._bookmarkService.getItemIndex(aBookmarkId); michael@0: let uri = this._bookmarkService.getBookmarkURI(aBookmarkId); michael@0: let title = this._bookmarkService.getItemTitle(aBookmarkId) || uri.spec; michael@0: let item = this._set.insertItemAt(aPos || index, title, uri.spec, this._inBatch); michael@0: item.setAttribute("anonid", aBookmarkId); michael@0: this._setContextActions(item); michael@0: this._updateFavicon(item, uri); michael@0: }, michael@0: michael@0: _setContextActions: function bv__setContextActions(aItem) { michael@0: let itemId = this._getBookmarkIdForItem(aItem); michael@0: aItem.setAttribute("data-contextactions", "delete," + (this._pinHelper.isPinned(itemId) ? "hide" : "pin")); michael@0: if (aItem.refresh) aItem.refresh(); michael@0: }, michael@0: michael@0: _sendNeedsRefresh: function bv__sendNeedsRefresh(){ michael@0: // Event sent when all view instances need to refresh. michael@0: let event = document.createEvent("Events"); michael@0: event.initEvent("BookmarksNeedsRefresh", true, false); michael@0: window.dispatchEvent(event); michael@0: }, michael@0: michael@0: updateBookmark: function bv_updateBookmark(aBookmarkId) { michael@0: let item = this._getItemForBookmarkId(aBookmarkId); michael@0: michael@0: if (!item) michael@0: return; michael@0: michael@0: let oldIndex = this._set.getIndexOfItem(item); michael@0: let index = this._bookmarkService.getItemIndex(aBookmarkId); michael@0: michael@0: if (oldIndex != index) { michael@0: this.removeBookmark(aBookmarkId); michael@0: this.addBookmark(aBookmarkId); michael@0: return; michael@0: } michael@0: michael@0: let uri = this._bookmarkService.getBookmarkURI(aBookmarkId); michael@0: let title = this._bookmarkService.getItemTitle(aBookmarkId) || uri.spec; michael@0: michael@0: item.setAttribute("anonid", aBookmarkId); michael@0: item.setAttribute("value", uri.spec); michael@0: item.setAttribute("label", title); michael@0: michael@0: this._updateFavicon(item, uri); michael@0: }, michael@0: michael@0: removeBookmark: function bv_removeBookmark(aBookmarkId) { michael@0: let item = this._getItemForBookmarkId(aBookmarkId); michael@0: let index = this._set.getIndexOfItem(item); michael@0: this._set.removeItemAt(index, this._inBatch); michael@0: }, michael@0: michael@0: doActionOnSelectedTiles: function bv_doActionOnSelectedTiles(aActionName, aEvent) { michael@0: let tileGroup = this._set; michael@0: let selectedTiles = tileGroup.selectedItems; michael@0: michael@0: switch (aActionName){ michael@0: case "delete": michael@0: Array.forEach(selectedTiles, function(aNode) { michael@0: if (!this._toRemove) { michael@0: this._toRemove = []; michael@0: } michael@0: michael@0: let itemId = this._getBookmarkIdForItem(aNode); michael@0: michael@0: this._toRemove.push(itemId); michael@0: this.removeBookmark(itemId); michael@0: }, this); michael@0: michael@0: // stop the appbar from dismissing michael@0: aEvent.preventDefault(); michael@0: michael@0: // at next tick, re-populate the context appbar. michael@0: setTimeout(function(){ michael@0: // fire a MozContextActionsChange event to update the context appbar michael@0: let event = document.createEvent("Events"); michael@0: // we need the restore button to show (the tile node will go away though) michael@0: event.actions = ["restore"]; michael@0: event.initEvent("MozContextActionsChange", true, false); michael@0: tileGroup.dispatchEvent(event); michael@0: }, 0); michael@0: break; michael@0: michael@0: case "restore": michael@0: // clear toRemove and let _sendNeedsRefresh update the items. michael@0: this._toRemove = null; michael@0: break; michael@0: michael@0: case "unpin": michael@0: Array.forEach(selectedTiles, function(aNode) { michael@0: let itemId = this._getBookmarkIdForItem(aNode); michael@0: michael@0: if (this._filterUnpinned) michael@0: this.removeBookmark(itemId); michael@0: michael@0: this._pinHelper.setUnpinned(itemId); michael@0: }, this); michael@0: break; michael@0: michael@0: case "pin": michael@0: Array.forEach(selectedTiles, function(aNode) { michael@0: let itemId = this._getBookmarkIdForItem(aNode); michael@0: michael@0: this._pinHelper.setPinned(itemId); michael@0: }, this); michael@0: break; michael@0: michael@0: default: michael@0: return; michael@0: } michael@0: michael@0: // Send refresh event so all view are in sync. michael@0: this._sendNeedsRefresh(); michael@0: }, michael@0: michael@0: handleEvent: function bv_handleEvent(aEvent) { michael@0: switch (aEvent.type){ michael@0: case "MozAppbarDismissing": michael@0: // If undo wasn't pressed, time to do definitive actions. michael@0: if (this._toRemove) { michael@0: for (let bookmarkId of this._toRemove) { michael@0: this._bookmarkService.removeItem(bookmarkId); michael@0: } michael@0: this._toRemove = null; michael@0: } michael@0: break; michael@0: michael@0: case "BookmarksNeedsRefresh": michael@0: this.getBookmarks(true); michael@0: break; michael@0: michael@0: case "TabClose": michael@0: // Flush any pending actions - appbar will call us back michael@0: // before this returns with 'MozAppbarDismissing' above. michael@0: StartUI.chromeWin.ContextUI.dismissContextAppbar(); michael@0: break; michael@0: } michael@0: } michael@0: }); michael@0: michael@0: let BookmarksStartView = { michael@0: _view: null, michael@0: get _grid() { return document.getElementById("start-bookmarks-grid"); }, michael@0: michael@0: init: function init() { michael@0: this._view = new BookmarksView(this._grid, Bookmarks.metroRoot, true); michael@0: this._view.getBookmarks(); michael@0: this._grid.removeAttribute("fade"); michael@0: }, michael@0: michael@0: uninit: function uninit() { michael@0: if (this._view) { michael@0: this._view.destruct(); michael@0: } michael@0: }, michael@0: }; michael@0: michael@0: /** michael@0: * Observes bookmark changes and keeps a linked BookmarksView updated. michael@0: * michael@0: * @param aView An instance of BookmarksView. michael@0: */ michael@0: function BookmarkChangeListener(aView) { michael@0: this._view = aView; michael@0: } michael@0: michael@0: BookmarkChangeListener.prototype = { michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// nsINavBookmarkObserver michael@0: onBeginUpdateBatch: function () { }, michael@0: onEndUpdateBatch: function () { }, michael@0: michael@0: onItemAdded: function bCL_onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle, aDateAdded, aGUID, aParentGUID) { michael@0: this._view.getBookmarks(true); michael@0: }, michael@0: michael@0: onItemChanged: function bCL_onItemChanged(aItemId, aProperty, aIsAnnotationProperty, aNewValue, aLastModified, aItemType, aParentId, aGUID, aParentGUID) { michael@0: let itemIndex = PlacesUtils.bookmarks.getItemIndex(aItemId); michael@0: if (!this._view.inCurrentView(aParentId, aItemId)) michael@0: return; michael@0: michael@0: this._view.updateBookmark(aItemId); michael@0: }, michael@0: michael@0: onItemMoved: function bCL_onItemMoved(aItemId, aOldParentId, aOldIndex, aNewParentId, aNewIndex, aItemType, aGUID, aOldParentGUID, aNewParentGUID) { michael@0: let wasInView = this._view.inCurrentView(aOldParentId, aItemId); michael@0: let nowInView = this._view.inCurrentView(aNewParentId, aItemId); michael@0: michael@0: if (!wasInView && nowInView) michael@0: this._view.addBookmark(aItemId); michael@0: michael@0: if (wasInView && !nowInView) michael@0: this._view.removeBookmark(aItemId); michael@0: michael@0: this._view.getBookmarks(true); michael@0: }, michael@0: michael@0: onBeforeItemRemoved: function (aItemId, aItemType, aParentId, aGUID, aParentGUID) { }, michael@0: onItemRemoved: function bCL_onItemRemoved(aItemId, aParentId, aIndex, aItemType, aURI, aGUID, aParentGUID) { michael@0: if (!this._view.inCurrentView(aParentId, aItemId)) michael@0: return; michael@0: michael@0: this._view.removeBookmark(aItemId); michael@0: this._view.getBookmarks(true); michael@0: }, michael@0: michael@0: onItemVisited: function(aItemId, aVisitId, aTime, aTransitionType, aURI, aParentId, aGUID, aParentGUID) { }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// nsISupports michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver]) michael@0: };