gecko/browser/components/places/content/controller.js
2008-04-21 02:20:16 -07:00

1421 lines
51 KiB
JavaScript
Executable File

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is the Places Command Controller.
*
* The Initial Developer of the Original Code is Google Inc.
* Portions created by the Initial Developer are Copyright (C) 2005
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Ben Goodger <beng@google.com>
* Myk Melez <myk@mozilla.org>
* Asaf Romano <mano@mozilla.com>
* Marco Bonardo <mak77@supereva.it>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
// XXXmano: we should move most/all of these constants to PlacesUtils
const ORGANIZER_ROOT_BOOKMARKS = "place:folder=BOOKMARKS_MENU&excludeItems=1&queryType=1";
const ORGANIZER_SUBSCRIPTIONS_QUERY = "place:annotation=livemark%2FfeedURI";
// No change to the view, preserve current selection
const RELOAD_ACTION_NOTHING = 0;
// Inserting items new to the view, select the inserted rows
const RELOAD_ACTION_INSERT = 1;
// Removing items from the view, select the first item after the last selected
const RELOAD_ACTION_REMOVE = 2;
// Moving items within a view, don't treat the dropped items as additional
// rows.
const RELOAD_ACTION_MOVE = 3;
// when removing a bunch of pages we split them in chunks to avoid passing
// a too big array to RemovePages
// 300 is the best choice with an history of about 150000 visits
// smaller chunks could cause a Slow Script warning with a huge history
const REMOVE_PAGES_CHUNKLEN = 300;
// if we are removing less than this pages we will remove them one by one
// since it will be reflected faster on the UI
// 10 is a good compromise, since allows the user to delete a little amount of
// urls for privacy reasons, but does not cause heavy disk access
const REMOVE_PAGES_MAX_SINGLEREMOVES = 10;
/**
* Represents an insertion point within a container where we can insert
* items.
* @param aItemId
* The identifier of the parent container
* @param aIndex
* The index within the container where we should insert
* @param aOrientation
* The orientation of the insertion. NOTE: the adjustments to the
* insertion point to accommodate the orientation should be done by
* the person who constructs the IP, not the user. The orientation
* is provided for informational purposes only!
* @param [optional] aIsTag
* Indicates if parent container is a tag
* @constructor
*/
function InsertionPoint(aItemId, aIndex, aOrientation, aIsTag) {
this.itemId = aItemId;
this.index = aIndex;
this.orientation = aOrientation;
this.isTag = aIsTag;
}
InsertionPoint.prototype.toString = function IP_toString() {
return "[object InsertionPoint(folder:" + this.itemId + ",index:" + this.index + ",orientation:" + this.orientation + ",isTag:" + this.isTag + ")]";
};
/**
* Places Controller
*/
function PlacesController(aView) {
this._view = aView;
}
PlacesController.prototype = {
/**
* The places view.
*/
_view: null,
isCommandEnabled: function PC_isCommandEnabled(aCommand) {
switch (aCommand) {
case "cmd_undo":
return PlacesUIUtils.ptm.numberOfUndoItems > 0;
case "cmd_redo":
return PlacesUIUtils.ptm.numberOfRedoItems > 0;
case "cmd_cut":
case "cmd_delete":
return this._hasRemovableSelection(false);
case "placesCmd_moveBookmarks":
return this._hasRemovableSelection(true);
case "cmd_copy":
return this._view.hasSelection;
case "cmd_paste":
return this._canInsert() && this._isClipboardDataPasteable();
case "cmd_selectAll":
if (this._view.selType != "single") {
var result = this._view.getResult();
if (result) {
var container = asContainer(result.root);
if (container.childCount > 0);
return true;
}
}
return false;
case "placesCmd_open":
case "placesCmd_open:window":
case "placesCmd_open:tab":
var selectedNode = this._view.selectedNode;
return selectedNode && PlacesUtils.nodeIsURI(selectedNode);
case "placesCmd_new:folder":
case "placesCmd_new:livemark":
return this._canInsert();
case "placesCmd_new:bookmark":
return this._canInsert();
case "placesCmd_new:separator":
return this._canInsert() &&
!asQuery(this._view.getResult().root).queryOptions.excludeItems &&
this._view.getResult().sortingMode ==
Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
case "placesCmd_show:info":
var selectedNode = this._view.selectedNode;
if (selectedNode) {
if (PlacesUtils.nodeIsFolder(selectedNode) ||
(PlacesUtils.nodeIsBookmark(selectedNode) &&
!PlacesUtils.nodeIsLivemarkItem(selectedNode)))
return true;
}
return false;
case "placesCmd_reloadMicrosummary":
var selectedNode = this._view.selectedNode;
return selectedNode && PlacesUtils.nodeIsBookmark(selectedNode) &&
PlacesUIUtils.microsummaries.hasMicrosummary(selectedNode.itemId);
case "placesCmd_reload":
// Livemark containers
var selectedNode = this._view.selectedNode;
return selectedNode && PlacesUtils.nodeIsLivemarkContainer(selectedNode);
case "placesCmd_sortBy:name":
var selectedNode = this._view.selectedNode;
return selectedNode &&
PlacesUtils.nodeIsFolder(selectedNode) &&
!PlacesUtils.nodeIsReadOnly(selectedNode) &&
this._view.getResult().sortingMode ==
Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
default:
return false;
}
},
supportsCommand: function PC_supportsCommand(aCommand) {
//LOG("supportsCommand: " + command);
// Non-Places specific commands that we also support
switch (aCommand) {
case "cmd_undo":
case "cmd_redo":
case "cmd_cut":
case "cmd_copy":
case "cmd_paste":
case "cmd_delete":
case "cmd_selectAll":
return true;
}
// All other Places Commands are prefixed with "placesCmd_" ... this
// filters out other commands that we do _not_ support (see 329587).
const CMD_PREFIX = "placesCmd_";
return (aCommand.substr(0, CMD_PREFIX.length) == CMD_PREFIX);
},
doCommand: function PC_doCommand(aCommand) {
switch (aCommand) {
case "cmd_undo":
PlacesUIUtils.ptm.undoTransaction();
break;
case "cmd_redo":
PlacesUIUtils.ptm.redoTransaction();
break;
case "cmd_cut":
this.cut();
break;
case "cmd_copy":
this.copy();
break;
case "cmd_paste":
this.paste();
break;
case "cmd_delete":
this.remove("Remove Selection");
break;
case "cmd_selectAll":
this.selectAll();
break;
case "placesCmd_open":
PlacesUIUtils.openNodeIn(this._view.selectedNode, "current");
break;
case "placesCmd_open:window":
PlacesUIUtils.openNodeIn(this._view.selectedNode, "window");
break;
case "placesCmd_open:tab":
PlacesUIUtils.openNodeIn(this._view.selectedNode, "tab");
break;
case "placesCmd_new:folder":
this.newItem("folder");
break;
case "placesCmd_new:bookmark":
this.newItem("bookmark");
break;
case "placesCmd_new:livemark":
this.newItem("livemark");
break;
case "placesCmd_new:separator":
this.newSeparator();
break;
case "placesCmd_show:info":
this.showBookmarkPropertiesForSelection();
break;
case "placesCmd_moveBookmarks":
this.moveSelectedBookmarks();
break;
case "placesCmd_reload":
this.reloadSelectedLivemark();
break;
case "placesCmd_reloadMicrosummary":
this.reloadSelectedMicrosummary();
break;
case "placesCmd_sortBy:name":
this.sortFolderByName();
break;
}
},
onEvent: function PC_onEvent(eventName) { },
/**
* Determine whether or not the selection can be removed, either by the
* delete or cut operations based on whether or not any of its contents
* are non-removable. We don't need to worry about recursion here since it
* is a policy decision that a removable item not be placed inside a non-
* removable item.
* @param aIsMoveCommand
* True if thecommand for which this method is called only moves the
* selected items to another container, false otherwise.
* @returns true if the there's a selection which has no nodes that cannot be removed,
* false otherwise.
*/
_hasRemovableSelection: function PC__hasRemovableSelection(aIsMoveCommand) {
var nodes = this._view.getSelectionNodes();
var root = this._view.getResultNode();
for (var i = 0; i < nodes.length; ++i) {
// Disallow removing the view's root node
if (nodes[i] == root)
return false;
// Disallow removing shortcuts from the left pane
var nodeItemId = nodes[i].itemId;
if (PlacesUtils.annotations
.itemHasAnnotation(nodeItemId, ORGANIZER_QUERY_ANNO))
return false;
// Disallow removing the toolbar, menu and unfiled-bookmarks folders
if (!aIsMoveCommand &&
(nodeItemId == PlacesUtils.toolbarFolderId ||
nodeItemId == PlacesUtils.unfiledBookmarksFolderId ||
nodeItemId == PlacesUtils.bookmarksMenuFolderId))
return false;
// We don't call nodeIsReadOnly here, because nodeIsReadOnly means that
// a node has children that cannot be edited, reordered or removed. Here,
// we don't care if a node's children can't be reordered or edited, just
// that they're removable. All history results have removable children
// (based on the principle that any URL in the history table should be
// removable), but some special bookmark folders may have non-removable
// children, e.g. live bookmark folder children. It doesn't make sense
// to delete a child of a live bookmark folder, since when the folder
// refreshes, the child will return.
var parent = nodes[i].parent || root;
if (PlacesUtils.isReadonlyFolder(parent))
return false;
}
return true;
},
/**
* Determines whether or not nodes can be inserted relative to the selection.
*/
_canInsert: function PC__canInsert() {
var ip = this._view.insertionPoint;
return ip != null && ip.isTag != true;
},
/**
* Determines whether or not the root node for the view is selected
*/
rootNodeIsSelected: function PC_rootNodeIsSelected() {
var nodes = this._view.getSelectionNodes();
var root = this._view.getResultNode();
for (var i = 0; i < nodes.length; ++i) {
if (nodes[i] == root)
return true;
}
return false;
},
/**
* Looks at the data on the clipboard to see if it is paste-able.
* Paste-able data is:
* - in a format that the view can receive
* @returns true if: - clipboard data is of a TYPE_X_MOZ_PLACE_* flavor,
- clipboard data is of type TEXT_UNICODE and
is a valid URI.
*/
_isClipboardDataPasteable: function PC__isClipboardDataPasteable() {
// if the clipboard contains TYPE_X_MOZ_PLACE_* data, it is definitely
// pasteable, with no need to unwrap all the nodes.
var flavors = PlacesUIUtils.placesFlavors;
var clipboard = PlacesUIUtils.clipboard;
var hasPlacesData =
clipboard.hasDataMatchingFlavors(flavors, flavors.length,
Ci.nsIClipboard.kGlobalClipboard);
if (hasPlacesData)
return this._view.insertionPoint != null;
// if the clipboard doesn't have TYPE_X_MOZ_PLACE_* data, we also allow
// pasting of valid "text/unicode" and "text/x-moz-url" data
var xferable = Cc["@mozilla.org/widget/transferable;1"].
createInstance(Ci.nsITransferable);
xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_URL);
xferable.addDataFlavor(PlacesUtils.TYPE_UNICODE);
clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
try {
// getAnyTransferData will throw if no data is available.
var data = { }, type = { };
xferable.getAnyTransferData(type, data, { });
data = data.value.QueryInterface(Ci.nsISupportsString).data;
if (type.value != PlacesUtils.TYPE_X_MOZ_URL &&
type.value != PlacesUtils.TYPE_UNICODE)
return false;
// unwrapNodes() will throw if the data blob is malformed.
var unwrappedNodes = PlacesUtils.unwrapNodes(data, type.value);
return this._view.insertionPoint != null;
}
catch (e) {
// getAnyTransferData or unwrapNodes failed
return false;
}
},
/**
* Gathers information about the selected nodes according to the following
* rules:
* "link" node is a URI
* "bookmark" node is a bookamrk
* "livemarkChild" node is a child of a livemark
* "tagChild" node is a child of a tag
* "folder" node is a folder
* "query" node is a query
* "dynamiccontainer" node is a dynamic container
* "separator" node is a separator line
* "host" node is a host
*
* @returns an array of objects corresponding the selected nodes. Each
* object has each of the properties above set if its corresponding
* node matches the rule. In addition, the annotations names for each
* node are set on its corresponding object as properties.
* Notes:
* 1) This can be slow, so don't call it anywhere performance critical!
* 2) A single-object array corresponding the root node is returned if
* there's no selection.
*/
_buildSelectionMetadata: function PC__buildSelectionMetadata() {
var metadata = [];
var root = this._view.getResult().root;
var nodes = this._view.getSelectionNodes();
if (nodes.length == 0)
nodes.push(root); // See the second note above
for (var i=0; i < nodes.length; i++) {
var nodeData = {};
var node = nodes[i];
var nodeType = node.type;
var uri = null;
// We don't use the nodeIs* methods here to avoid going through the type
// property way too often
switch(nodeType) {
case Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY:
nodeData["query"] = true;
if (node.parent) {
switch (asQuery(node.parent).queryOptions.resultType) {
case Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY:
nodeData["host"] = true;
break;
case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY:
case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY:
nodeData["day"] = true;
break;
}
}
break;
case Ci.nsINavHistoryResultNode.RESULT_TYPE_DYNAMIC_CONTAINER:
nodeData["dynamiccontainer"] = true;
break;
case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER:
case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT:
nodeData["folder"] = true;
break;
case Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR:
nodeData["separator"] = true;
break;
case Ci.nsINavHistoryResultNode.RESULT_TYPE_URI:
case Ci.nsINavHistoryResultNode.RESULT_TYPE_VISIT:
case Ci.nsINavHistoryResultNode.RESULT_TYPE_FULL_VISIT:
nodeData["link"] = true;
uri = PlacesUtils._uri(node.uri);
if (PlacesUtils.nodeIsBookmark(node)) {
nodeData["bookmark"] = true;
PlacesUtils.nodeIsTagQuery(node.parent)
var mss = PlacesUIUtils.microsummaries;
if (mss.hasMicrosummary(node.itemId))
nodeData["microsummary"] = true;
var parentNode = node.parent;
if (parentNode) {
if (PlacesUtils.nodeIsTagQuery(parentNode))
nodeData["tagChild"] = true;
else if (PlacesUtils.nodeIsLivemarkContainer(parentNode))
nodeData["livemarkChild"] = true;
}
}
break;
}
// annotations
if (uri) {
var names = PlacesUtils.annotations.getPageAnnotationNames(uri, {});
for (var j = 0; j < names.length; ++j)
nodeData[names[j]] = true;
}
// For items also include the item-specific annotations
if (node.itemId != -1) {
names = PlacesUtils.annotations
.getItemAnnotationNames(node.itemId, {});
for (j = 0; j < names.length; ++j)
nodeData[names[j]] = true;
}
metadata.push(nodeData);
}
return metadata;
},
/**
* Determines if a context-menu item should be shown
* @param aMenuItem
* the context menu item
* @param aMetaData
* meta data about the selection
* @returns true if the conditions (see buildContextMenu) are satisfied
* and the item can be displayed, false otherwise.
*/
_shouldShowMenuItem: function PC__shouldShowMenuItem(aMenuItem, aMetaData) {
var selectiontype = aMenuItem.getAttribute("selectiontype");
if (selectiontype == "multiple" && aMetaData.length == 1)
return false;
if (selectiontype == "single" && aMetaData.length != 1)
return false;
var forceHideRules = aMenuItem.getAttribute("forcehideselection").split("|");
for (var i = 0; i < aMetaData.length; ++i) {
for (var j=0; j < forceHideRules.length; ++j) {
if (forceHideRules[j] in aMetaData[i])
return false;
}
}
var selectionAttr = aMenuItem.getAttribute("selection");
if (selectionAttr) {
if (selectionAttr == "any")
return true;
var showRules = selectionAttr.split("|");
var anyMatched = false;
function metaDataNodeMatches(metaDataNode, rules) {
for (var i=0; i < rules.length; i++) {
if (rules[i] in metaDataNode)
return true;
}
return false;
}
for (var i = 0; i < aMetaData.length; ++i) {
if (metaDataNodeMatches(aMetaData[i], showRules))
anyMatched = true;
else
return false;
}
return anyMatched;
}
return !aMenuItem.hidden;
},
/**
* Detects information (meta-data rules) about the current selection in the
* view (see _buildSelectionMetadata) and sets the visibility state for each
* of the menu-items in the given popup with the following rules applied:
* 1) The "selectiontype" attribute may be set on a menu-item to "single"
* if the menu-item should be visible only if there is a single node
* selected, or to "multiple" if the menu-item should be visible only if
* multiple nodes are selected. If the attribute is not set or if it is
* set to an invalid value, the menu-item may be visible for both types of
* selection.
* 2) The "selection" attribute may be set on a menu-item to the various
* meta-data rules for which it may be visible. The rules should be
* separated with the | character.
* 3) A menu-item may be visible only if at least one of the rules set in
* its selection attribute apply to each of the selected nodes in the
* view.
* 4) The "forcehideselection" attribute may be set on a menu-item to rules
* for which it should be hidden. This attribute takes priority over the
* selection attribute. A menu-item would be hidden if at least one of the
* given rules apply to one of the selected nodes. The rules should be
* separated with the | character.
* 5) The "hideifnoinsetionpoint" attribute may be set on a menu-item to
* true if it should be hidden when there's no insertion point
* 6) The visibility state of a menu-item is unchanged if none of these
* attribute are set.
* 7) These attributes should not be set on separators for which the
* visibility state is "auto-detected."
* @param aPopup
* The menupopup to build children into.
* @return true if at least one item is visible, false otherwise.
*/
buildContextMenu: function PC_buildContextMenu(aPopup) {
var metadata = this._buildSelectionMetadata();
var ip = this._view.insertionPoint;
var noIp = !ip || ip.isTag;
var separator = null;
var visibleItemsBeforeSep = false;
var anyVisible = false;
for (var i = 0; i < aPopup.childNodes.length; ++i) {
var item = aPopup.childNodes[i];
if (item.localName != "menuseparator") {
item.hidden = (item.getAttribute("hideifnoinsetionpoint") == "true" && noIp) ||
!this._shouldShowMenuItem(item, metadata);
if (!item.hidden) {
visibleItemsBeforeSep = true;
anyVisible = true;
// Show the separator above the menu-item if any
if (separator) {
separator.hidden = false;
separator = null;
}
}
}
else { // menuseparator
// Initially hide it. It will be unhidden if there will be at least one
// visible menu-item above and below it.
item.hidden = true;
// We won't show the separator at all if no items are visible above it
if (visibleItemsBeforeSep)
separator = item;
// New separator, count again:
visibleItemsBeforeSep = false;
}
}
// Set Open Folder/Links In Tabs items enabled state if they're visible
if (anyVisible) {
var openContainerInTabsItem = document.getElementById("placesContext_openContainer:tabs");
if (!openContainerInTabsItem.hidden && this._view.selectedNode &&
PlacesUtils.nodeIsContainer(this._view.selectedNode)) {
openContainerInTabsItem.disabled =
!PlacesUtils.hasChildURIs(this._view.selectedNode);
}
else {
// see selectiontype rule in the overlay
var openLinksInTabsItem = document.getElementById("placesContext_openLinks:tabs");
openLinksInTabsItem.disabled = openLinksInTabsItem.hidden;
}
}
return anyVisible;
},
/**
* Select all links in the current view.
*/
selectAll: function PC_selectAll() {
this._view.selectAll();
},
/**
* Opens the bookmark properties for the selected URI Node.
*/
showBookmarkPropertiesForSelection:
function PC_showBookmarkPropertiesForSelection() {
var node = this._view.selectedNode;
if (!node)
return;
if (PlacesUtils.nodeIsFolder(node))
PlacesUIUtils.showFolderProperties(node.itemId);
else if (PlacesUtils.nodeIsBookmark(node))
PlacesUIUtils.showBookmarkProperties(node.itemId);
},
/**
* This method can be run on a URI parameter to ensure that it didn't
* receive a string instead of an nsIURI object.
*/
_assertURINotString: function PC__assertURINotString(value) {
NS_ASSERT((typeof(value) == "object") && !(value instanceof String),
"This method should be passed a URI as a nsIURI object, not as a string.");
},
/**
* Reloads the selected livemark if any.
*/
reloadSelectedLivemark: function PC_reloadSelectedLivemark() {
var selectedNode = this._view.selectedNode;
if (selectedNode && PlacesUtils.nodeIsLivemarkContainer(selectedNode))
PlacesUtils.livemarks.reloadLivemarkFolder(selectedNode.itemId);
},
/**
* Reload the microsummary associated with the selection
*/
reloadSelectedMicrosummary: function PC_reloadSelectedMicrosummary() {
var selectedNode = this._view.selectedNode;
var mss = PlacesUIUtils.microsummaries;
if (mss.hasMicrosummary(selectedNode.itemId))
mss.refreshMicrosummary(selectedNode.itemId);
},
/**
* Gives the user a chance to cancel loading lots of tabs at once
*/
_confirmOpenTabs: function(numTabsToOpen) {
var pref = Cc["@mozilla.org/preferences-service;1"].
getService(Ci.nsIPrefBranch);
const kWarnOnOpenPref = "browser.tabs.warnOnOpen";
var reallyOpen = true;
if (pref.getBoolPref(kWarnOnOpenPref)) {
if (numTabsToOpen >= pref.getIntPref("browser.tabs.maxOpenBeforeWarn")) {
var promptService = Cc["@mozilla.org/embedcomp/prompt-service;1"].
getService(Ci.nsIPromptService);
// default to true: if it were false, we wouldn't get this far
var warnOnOpen = { value: true };
var messageKey = "tabs.openWarningMultipleBranded";
var openKey = "tabs.openButtonMultiple";
var strings = document.getElementById("placeBundle");
const BRANDING_BUNDLE_URI = "chrome://branding/locale/brand.properties";
var brandShortName = Cc["@mozilla.org/intl/stringbundle;1"].
getService(Ci.nsIStringBundleService).
createBundle(BRANDING_BUNDLE_URI).
GetStringFromName("brandShortName");
var buttonPressed = promptService.confirmEx(window,
PlacesUIUtils.getString("tabs.openWarningTitle"),
PlacesUIUtils.getFormattedString(messageKey,
[numTabsToOpen, brandShortName]),
(promptService.BUTTON_TITLE_IS_STRING * promptService.BUTTON_POS_0)
+ (promptService.BUTTON_TITLE_CANCEL * promptService.BUTTON_POS_1),
PlacesUIUtils.getString(openKey),
null, null,
PlacesUIUtils.getFormattedString("tabs.openWarningPromptMeBranded",
[brandShortName]),
warnOnOpen);
reallyOpen = (buttonPressed == 0);
// don't set the pref unless they press OK and it's false
if (reallyOpen && !warnOnOpen.value)
pref.setBoolPref(kWarnOnOpenPref, false);
}
}
return reallyOpen;
},
/**
* Opens the links in the selected folder, or the selected links in new tabs.
*/
openSelectionInTabs: function PC_openLinksInTabs(aEvent) {
var node = this._view.selectedNode;
if (node && PlacesUtils.nodeIsContainer(node))
PlacesUIUtils.openContainerNodeInTabs(this._view.selectedNode, aEvent);
else
PlacesUIUtils.openURINodesInTabs(this._view.getSelectionNodes(), aEvent);
},
/**
* Shows the Add Bookmark UI for the current insertion point.
*
* @param aType
* the type of the new item (bookmark/livemark/folder)
*/
newItem: function PC_newItem(aType) {
var ip = this._view.insertionPoint;
if (!ip)
throw Cr.NS_ERROR_NOT_AVAILABLE;
var performed = false;
if (aType == "bookmark")
performed = PlacesUIUtils.showAddBookmarkUI(null, null, null, ip);
else if (aType == "livemark")
performed = PlacesUIUtils.showAddLivemarkUI(null, null, null, null, ip);
else // folder
performed = PlacesUIUtils.showAddFolderUI(null, ip);
if (performed) {
// select the new item
var insertedNodeId = PlacesUtils.bookmarks
.getIdForItemAt(ip.itemId, ip.index);
this._view.selectItems([insertedNodeId], ip.itemId);
}
},
/**
* Create a new Bookmark folder somewhere. Prompts the user for the name
* of the folder.
*/
newFolder: function PC_newFolder() {
var ip = this._view.insertionPoint;
if (!ip)
throw Cr.NS_ERROR_NOT_AVAILABLE;
var performed = false;
performed = PlacesUIUtils.showAddFolderUI(null, ip);
if (performed) {
// select the new item
var insertedNodeId = PlacesUtils.bookmarks
.getIdForItemAt(ip.itemId, ip.index);
this._view.selectItems([insertedNodeId]);
}
},
/**
* Create a new Bookmark separator somewhere.
*/
newSeparator: function PC_newSeparator() {
var ip = this._view.insertionPoint;
if (!ip)
throw Cr.NS_ERROR_NOT_AVAILABLE;
var txn = PlacesUIUtils.ptm.createSeparator(ip.itemId, ip.index);
PlacesUIUtils.ptm.doTransaction(txn);
// select the new item
var insertedNodeId = PlacesUtils.bookmarks
.getIdForItemAt(ip.itemId, ip.index);
this._view.selectItems([insertedNodeId]);
},
/**
* Opens a dialog for moving the selected nodes.
*/
moveSelectedBookmarks: function PC_moveBookmarks() {
window.openDialog("chrome://browser/content/places/moveBookmarks.xul",
"", "chrome, modal",
this._view.getSelectionNodes());
},
/**
* Sort the selected folder by name
*/
sortFolderByName: function PC_sortFolderByName() {
var itemId = PlacesUtils.getConcreteItemId(this._view.selectedNode);
var txn = PlacesUIUtils.ptm.sortFolderByName(itemId);
PlacesUIUtils.ptm.doTransaction(txn);
},
/**
* Walk the list of folders we're removing in this delete operation, and
* see if the selected node specified is already implicitly being removed
* because it is a child of that folder.
* @param node
* Node to check for containment.
* @param pastFolders
* List of folders the calling function has already traversed
* @returns true if the node should be skipped, false otherwise.
*/
_shouldSkipNode: function PC_shouldSkipNode(node, pastFolders) {
/**
* Determines if a node is contained by another node within a resultset.
* @param node
* The node to check for containment for
* @param parent
* The parent container to check for containment in
* @returns true if node is a member of parent's children, false otherwise.
*/
function isContainedBy(node, parent) {
var cursor = node.parent;
while (cursor) {
if (cursor == parent)
return true;
cursor = cursor.parent;
}
return false;
}
for (var j = 0; j < pastFolders.length; ++j) {
if (isContainedBy(node, pastFolders[j]))
return true;
}
return false;
},
/**
* Creates a set of transactions for the removal of a range of items.
* A range is an array of adjacent nodes in a view.
* @param [in] range
* An array of nodes to remove. Should all be adjacent.
* @param [out] transactions
* An array of transactions.
*/
_removeRange: function PC__removeRange(range, transactions) {
NS_ASSERT(transactions instanceof Array, "Must pass a transactions array");
var removedFolders = [];
for (var i = 0; i < range.length; ++i) {
var node = range[i];
if (this._shouldSkipNode(node, removedFolders))
continue;
if (PlacesUtils.nodeIsFolder(node))
removedFolders.push(node);
else if (PlacesUtils.nodeIsTagQuery(node.parent)) {
var queries = asQuery(node.parent).getQueries({});
var folders = queries[0].getFolders({});
var uri = PlacesUtils._uri(node.uri);
var tagItemId = folders[0];
transactions.push(PlacesUIUtils.ptm.untagURI(uri, [tagItemId]));
continue;
}
else if (PlacesUtils.nodeIsQuery(node.parent) &&
asQuery(node.parent).queryOptions.queryType ==
Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY &&
node.uri) {
// remove page from history, history deletes are not undoable
var bhist = PlacesUtils.history.QueryInterface(Ci.nsIBrowserHistory);
bhist.removePage(PlacesUtils._uri(node.uri));
continue;
}
transactions.push(PlacesUIUtils.ptm.removeItem(node.itemId));
}
},
/**
* Removes the set of selected ranges from bookmarks.
* @param txnName
* See |remove|.
*/
_removeRowsFromBookmarks: function PC__removeRowsFromBookmarks(txnName) {
var ranges = this._view.getRemovableSelectionRanges();
var transactions = [];
// Delete the selected rows. Do this by walking the selection backward, so
// that when undo is performed they are re-inserted in the correct order.
for (var i = ranges.length - 1; i >= 0 ; --i)
this._removeRange(ranges[i], transactions);
if (transactions.length > 0) {
var txn = PlacesUIUtils.ptm.aggregateTransactions(txnName, transactions);
PlacesUIUtils.ptm.doTransaction(txn);
}
},
/**
* Removes the set of selected ranges from history.
*/
_removeRowsFromHistory: function PC__removeRowsFromHistory() {
// Other containers are history queries, just delete from history
// history deletes are not undoable.
var nodes = this._view.getSelectionNodes();
var URIs = [];
var bhist = PlacesUtils.history.QueryInterface(Ci.nsIBrowserHistory);
var resultView = this._view.getResultView();
var root = this._view.getResultNode();
for (var i = 0; i < nodes.length; ++i) {
var node = nodes[i];
if (PlacesUtils.nodeIsHost(node))
bhist.removePagesFromHost(node.title, true);
else if (PlacesUtils.nodeIsURI(node)) {
var uri = PlacesUtils._uri(node.uri);
// avoid trying to delete the same url twice
if (URIs.indexOf(uri) < 0) {
URIs.push(uri);
}
}
else if (PlacesUtils.nodeIsDay(node)) {
// this is the oldest date
// for the last node endDate is end of epoch
var beginDate = 0;
// this is the newest date
// day nodes have time property set to the last day in the interval
var endDate = node.time;
var nodeIdx = 0;
var cc = root.childCount;
// Find index of current day node
while (nodeIdx < cc && root.getChild(nodeIdx) != node)
++nodeIdx;
// We have an older day
if (nodeIdx+1 < cc)
beginDate = root.getChild(nodeIdx+1).time;
// we want to exclude beginDate from the removal
bhist.removePagesByTimeframe(beginDate+1, endDate);
}
}
// if we have to delete a lot of urls RemovePage will be slow, it's better
// to delete them in bunch and rebuild the full treeView
if (URIs.length > REMOVE_PAGES_MAX_SINGLEREMOVES) {
// do removal in chunks to avoid passing a too big array to removePages
for (var i = 0; i < URIs.length; i += REMOVE_PAGES_CHUNKLEN) {
var URIslice = URIs.slice(i, Math.max(i + REMOVE_PAGES_CHUNKLEN, URIs.length));
// set DoBatchNotify only on the last chunk
bhist.removePages(URIslice, URIslice.length,
(i + REMOVE_PAGES_CHUNKLEN) >= URIs.length);
}
}
else {
// if we have to delete fewer urls, removepage will allow us to avoid
// rebuilding the full treeView
for (var i = 0; i < URIs.length; ++i)
bhist.removePage(URIs[i]);
}
},
/**
* Removes the selection
* @param aTxnName
* A name for the transaction if this is being performed
* as part of another operation.
*/
remove: function PC_remove(aTxnName) {
NS_ASSERT(aTxnName !== undefined, "Must supply Transaction Name");
var root = this._view.getResult().root;
if (PlacesUtils.nodeIsFolder(root))
this._removeRowsFromBookmarks(aTxnName);
else if (PlacesUtils.nodeIsQuery(root)) {
var queryType = asQuery(root).queryOptions.queryType;
if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS)
this._removeRowsFromBookmarks(aTxnName);
else if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY)
this._removeRowsFromHistory();
else
NS_ASSERT(false, "implement support for QUERY_TYPE_UNIFIED");
}
else
NS_ASSERT(false, "unexpected root");
},
/**
* Get a TransferDataSet containing the content of the selection that can be
* dropped elsewhere.
* @param dragAction
* The action to happen when dragging, i.e. copy
* @returns A TransferDataSet object that can be dragged and dropped
* elsewhere.
*/
getTransferData: function PC_getTransferData(dragAction) {
var result = this._view.getResult();
var oldViewer = result.viewer;
try {
result.viewer = null;
var nodes = this._view.getDragableSelection();
if (dragAction == Ci.nsIDragService.DRAGDROP_ACTION_MOVE) {
nodes = nodes.filter(function(node) {
var parent = node.parent;
return parent && !PlacesUtils.nodeIsReadOnly(parent);
});
}
var dataSet = new TransferDataSet();
for (var i = 0; i < nodes.length; ++i) {
var node = nodes[i];
var data = new TransferData();
function addData(type, overrideURI) {
data.addDataForFlavour(type, PlacesUIUtils._wrapString(
PlacesUtils.wrapNode(node, type, overrideURI)));
}
function addURIData(overrideURI) {
addData(PlacesUtils.TYPE_X_MOZ_URL, overrideURI);
addData(PlacesUtils.TYPE_UNICODE, overrideURI);
addData(PlacesUtils.TYPE_HTML, overrideURI);
}
// This order is _important_! It controls how this and other
// applications select data to be inserted based on type.
addData(PlacesUtils.TYPE_X_MOZ_PLACE);
var uri;
// Allow dropping the feed uri of live-bookmark folders
if (PlacesUtils.nodeIsLivemarkContainer(node))
uri = PlacesUtils.livemarks.getFeedURI(node.itemId).spec;
addURIData(uri);
dataSet.push(data);
}
}
finally {
if (oldViewer)
result.viewer = oldViewer;
}
return dataSet;
},
/**
* Copy Bookmarks and Folders to the clipboard
*/
copy: function PC_copy() {
var result = this._view.getResult();
var oldViewer = result.viewer;
try {
result.viewer = null;
var nodes = this._view.getSelectionNodes();
var xferable = Cc["@mozilla.org/widget/transferable;1"].
createInstance(Ci.nsITransferable);
var foundFolder = false, foundLink = false;
var copiedFolders = [];
var placeString = mozURLString = htmlString = unicodeString = "";
for (var i = 0; i < nodes.length; ++i) {
var node = nodes[i];
if (this._shouldSkipNode(node, copiedFolders))
continue;
if (PlacesUtils.nodeIsFolder(node))
copiedFolders.push(node);
function generateChunk(type, overrideURI) {
var suffix = i < (nodes.length - 1) ? NEWLINE : "";
var uri = overrideURI;
if (PlacesUtils.nodeIsLivemarkContainer(node))
uri = PlacesUtils.livemarks.getFeedURI(node.itemId).spec
mozURLString += (PlacesUtils.wrapNode(node, PlacesUtils.TYPE_X_MOZ_URL,
uri) + suffix);
unicodeString += (PlacesUtils.wrapNode(node, PlacesUtils.TYPE_UNICODE,
uri) + suffix);
htmlString += (PlacesUtils.wrapNode(node, PlacesUtils.TYPE_HTML,
uri) + suffix);
var placeSuffix = i < (nodes.length - 1) ? "," : "";
return PlacesUtils.wrapNode(node, type, overrideURI) + placeSuffix;
}
// all items wrapped as TYPE_X_MOZ_PLACE
placeString += generateChunk(PlacesUtils.TYPE_X_MOZ_PLACE);
}
function addData(type, data) {
xferable.addDataFlavor(type);
xferable.setTransferData(type, PlacesUIUtils._wrapString(data), data.length * 2);
}
// This order is _important_! It controls how this and other applications
// select data to be inserted based on type.
if (placeString)
addData(PlacesUtils.TYPE_X_MOZ_PLACE, placeString);
if (mozURLString)
addData(PlacesUtils.TYPE_X_MOZ_URL, mozURLString);
if (unicodeString)
addData(PlacesUtils.TYPE_UNICODE, unicodeString);
if (htmlString)
addData(PlacesUtils.TYPE_HTML, htmlString);
if (placeString || unicodeString || htmlString || mozURLString) {
PlacesUIUtils.clipboard.setData(xferable, null, Ci.nsIClipboard.kGlobalClipboard);
}
}
finally {
if (oldViewer)
result.viewer = oldViewer;
}
},
/**
* Cut Bookmarks and Folders to the clipboard
*/
cut: function PC_cut() {
this.copy();
this.remove("Cut Selection");
},
/**
* Paste Bookmarks and Folders from the clipboard
*/
paste: function PC_paste() {
// Strategy:
//
// There can be data of various types (folder, separator, link) on the
// clipboard. We need to get all of that data and build edit transactions
// for them. This means asking the clipboard once for each type and
// aggregating the results.
/**
* Constructs a transferable that can receive data of specific types.
* @param types
* The types of data the transferable can hold, in order of
* preference.
* @returns The transferable.
*/
function makeXferable(types) {
var xferable =
Cc["@mozilla.org/widget/transferable;1"].
createInstance(Ci.nsITransferable);
for (var i = 0; i < types.length; ++i)
xferable.addDataFlavor(types[i]);
return xferable;
}
var clipboard = PlacesUIUtils.clipboard;
var ip = this._view.insertionPoint;
if (!ip)
throw Cr.NS_ERROR_NOT_AVAILABLE;
/**
* Gets a list of transactions to perform the paste of specific types.
* @param types
* The types of data to form paste transactions for
* @returns An array of transactions that perform the paste.
*/
function getTransactions(types) {
var xferable = makeXferable(types);
clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
var data = { }, type = { };
try {
xferable.getAnyTransferData(type, data, { });
data = data.value.QueryInterface(Ci.nsISupportsString).data;
var items = PlacesUtils.unwrapNodes(data, type.value);
var transactions = [];
var index = ip.index;
for (var i = 0; i < items.length; ++i) {
// adjusted to make sure that items are given the correct index -
// transactions insert differently if index == -1
if (ip.index > -1)
index = ip.index + i;
transactions.push(PlacesUIUtils.makeTransaction(items[i], type.value,
ip.itemId, index,
true));
}
return transactions;
}
catch (e) {
// getAnyTransferData will throw if there is no data of the specified
// type on the clipboard.
// unwrapNodes will throw if the data that is present is malformed in
// some way.
// In either case, don't fail horribly, just return no data.
}
return [];
}
// Get transactions to paste any folders, separators or links that might
// be on the clipboard, aggregate them and execute them.
var transactions = getTransactions([PlacesUtils.TYPE_X_MOZ_PLACE,
PlacesUtils.TYPE_X_MOZ_URL,
PlacesUtils.TYPE_UNICODE]);
var txn = PlacesUIUtils.ptm.aggregateTransactions("Paste", transactions);
PlacesUIUtils.ptm.doTransaction(txn);
// select the pasted items, they should be consecutive
var insertedNodeIds = [];
for (var i = 0; i < transactions.length; ++i)
insertedNodeIds.push(PlacesUtils.bookmarks
.getIdForItemAt(ip.itemId, ip.index + i));
if (insertedNodeIds.length > 0)
this._view.selectItems(insertedNodeIds);
}
};
/**
* Handles drag and drop operations for views. Note that this is view agnostic!
* You should not use PlacesController._view within these methods, since
* the view that the item(s) have been dropped on was not necessarily active.
* Drop functions are passed the view that is being dropped on.
*/
var PlacesControllerDragHelper = {
/**
* Determines if the mouse is currently being dragged over a child node of
* this menu. This is necessary so that the menu doesn't close while the
* mouse is dragging over one of its submenus
* @param node
* The container node
* @returns true if the user is dragging over a node within the hierarchy of
* the container, false otherwise.
*/
draggingOverChildNode: function PCDH_draggingOverChildNode(node) {
var currentNode = this.currentDropTarget;
while (currentNode) {
if (currentNode == node)
return true;
currentNode = currentNode.parentNode;
}
return false;
},
/**
* DOM Element currently being dragged over
*/
currentDropTarget: null,
/**
* @returns The current active drag session. Returns null if there is none.
*/
getSession: function PCDH__getSession() {
var dragService = Cc["@mozilla.org/widget/dragservice;1"].
getService(Ci.nsIDragService);
return dragService.getCurrentSession();
},
/**
* Determines whether or not the data currently being dragged can be dropped
* on a places view.
* @param ip
* The insertion point where the items should be dropped
*/
canDrop: function PCDH_canDrop(ip) {
var session = this.getSession();
if (!session)
return false;
var types = PlacesUIUtils.GENERIC_VIEW_DROP_TYPES;
var foundType = false;
for (var i = 0; i < types.length && !foundType; ++i) {
if (session.isDataFlavorSupported(types[i]))
foundType = true;
}
if (!foundType)
return false;
// Check every dragged item
var xferable = this._initTransferable(session);
var dropCount = session.numDropItems;
for (i = 0; i < dropCount; i++) {
// Get the information of the dragged item
session.getData(xferable, i);
var data = { }, flavor = { };
xferable.getAnyTransferData(flavor, data, { });
data.value.QueryInterface(Ci.nsISupportsString);
var dragged = PlacesUtils.unwrapNodes(data.value.data, flavor.value)[0];
// Only bookmarks and urls can be dropped into tag containers
if (ip.isTag && dragged.type != PlacesUtils.TYPE_X_MOZ_URL &&
(dragged.type != PlacesUtils.TYPE_X_MOZ_PLACE ||
/^place:/.test(dragged.uri)))
return false;
// The following loop disallows the dropping of a folder on itself or
// on any of its descendants.
if (dragged.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER ||
/^place:/.test(dragged.uri)) {
var parentId = ip.itemId;
while (parentId != PlacesUtils.placesRootId) {
if (dragged.concreteId == parentId || dragged.id == parentId)
return false;
parentId = PlacesUtils.bookmarks.getFolderIdForItem(parentId);
}
}
}
return true;
},
/**
* Creates a Transferable object that can be filled with data of types
* supported by a view.
* @param session
* The active drag session
* @returns An object implementing nsITransferable that can receive data
* dropped onto a view.
*/
_initTransferable: function PCDH__initTransferable(session) {
var xferable = Cc["@mozilla.org/widget/transferable;1"].
createInstance(Ci.nsITransferable);
var types = PlacesUIUtils.GENERIC_VIEW_DROP_TYPES;
for (var i = 0; i < types.length; ++i) {
if (session.isDataFlavorSupported(types[i]))
xferable.addDataFlavor(types[i]);
}
return xferable;
},
/**
* Handles the drop of one or more items onto a view.
* @param insertionPoint
* The insertion point where the items should be dropped
*/
onDrop: function PCDH_onDrop(insertionPoint) {
var session = this.getSession();
var copy = session.dragAction & Ci.nsIDragService.DRAGDROP_ACTION_COPY;
var transactions = [];
var xferable = this._initTransferable(session);
var dropCount = session.numDropItems;
var movedCount = 0;
for (var i = 0; i < dropCount; ++i) {
session.getData(xferable, i);
var data = { }, flavor = { };
xferable.getAnyTransferData(flavor, data, { });
data.value.QueryInterface(Ci.nsISupportsString);
// There's only ever one in the D&D case.
var unwrapped = PlacesUtils.unwrapNodes(data.value.data,
flavor.value)[0];
var index = insertionPoint.index;
// Adjust insertion index to prevent reversal of dragged items. When you
// drag multiple elts upward: need to increment index or each successive
// elt will be inserted at the same index, each above the previous.
if ((index != -1) && ((index < unwrapped.index) ||
(unwrapped.folder && (index < unwrapped.folder.index)))) {
index = index + movedCount;
movedCount++;
}
// if dragging over a tag container we should tag the item
if (insertionPoint.isTag) {
var uri = PlacesUtils._uri(unwrapped.uri);
var tagItemId = insertionPoint.itemId;
transactions.push(PlacesUIUtils.ptm.tagURI(uri,[tagItemId]));
}
else {
transactions.push(PlacesUIUtils.makeTransaction(unwrapped,
flavor.value, insertionPoint.itemId,
index, copy));
}
}
var txn = PlacesUIUtils.ptm.aggregateTransactions("DropItems", transactions);
PlacesUIUtils.ptm.doTransaction(txn);
}
};
function goUpdatePlacesCommands() {
var placesController;
try {
// Or any other command...
placesController = top.document.commandDispatcher
.getControllerForCommand("placesCmd_open");
}
catch(ex) { return; }
function updatePlacesCommand(aCommand) {
var enabled = false;
if (placesController)
enabled = placesController.isCommandEnabled(aCommand);
goSetCommandEnabled(aCommand, enabled);
}
updatePlacesCommand("placesCmd_open");
updatePlacesCommand("placesCmd_open:window");
updatePlacesCommand("placesCmd_open:tab");
updatePlacesCommand("placesCmd_new:folder");
updatePlacesCommand("placesCmd_new:bookmark");
updatePlacesCommand("placesCmd_new:livemark");
updatePlacesCommand("placesCmd_new:separator");
updatePlacesCommand("placesCmd_show:info");
updatePlacesCommand("placesCmd_moveBookmarks");
updatePlacesCommand("placesCmd_reload");
updatePlacesCommand("placesCmd_reloadMicrosummary");
updatePlacesCommand("placesCmd_sortBy:name");
}