Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 XPCOMUtils.defineLazyModuleGetter(this, "ForgetAboutSite",
7 "resource://gre/modules/ForgetAboutSite.jsm");
8 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
9 "resource://gre/modules/NetUtil.jsm");
11 // XXXmano: we should move most/all of these constants to PlacesUtils
12 const ORGANIZER_ROOT_BOOKMARKS = "place:folder=BOOKMARKS_MENU&excludeItems=1&queryType=1";
14 // No change to the view, preserve current selection
15 const RELOAD_ACTION_NOTHING = 0;
16 // Inserting items new to the view, select the inserted rows
17 const RELOAD_ACTION_INSERT = 1;
18 // Removing items from the view, select the first item after the last selected
19 const RELOAD_ACTION_REMOVE = 2;
20 // Moving items within a view, don't treat the dropped items as additional
21 // rows.
22 const RELOAD_ACTION_MOVE = 3;
24 // When removing a bunch of pages we split them in chunks to give some breath
25 // to the main-thread.
26 const REMOVE_PAGES_CHUNKLEN = 300;
28 /**
29 * Represents an insertion point within a container where we can insert
30 * items.
31 * @param aItemId
32 * The identifier of the parent container
33 * @param aIndex
34 * The index within the container where we should insert
35 * @param aOrientation
36 * The orientation of the insertion. NOTE: the adjustments to the
37 * insertion point to accommodate the orientation should be done by
38 * the person who constructs the IP, not the user. The orientation
39 * is provided for informational purposes only!
40 * @param [optional] aIsTag
41 * Indicates if parent container is a tag
42 * @param [optional] aDropNearItemId
43 * When defined we will calculate index based on this itemId
44 * @constructor
45 */
46 function InsertionPoint(aItemId, aIndex, aOrientation, aIsTag,
47 aDropNearItemId) {
48 this.itemId = aItemId;
49 this._index = aIndex;
50 this.orientation = aOrientation;
51 this.isTag = aIsTag;
52 this.dropNearItemId = aDropNearItemId;
53 }
55 InsertionPoint.prototype = {
56 set index(val) {
57 return this._index = val;
58 },
60 promiseGUID: function () PlacesUtils.promiseItemGUID(this.itemId),
62 get index() {
63 if (this.dropNearItemId > 0) {
64 // If dropNearItemId is set up we must calculate the real index of
65 // the item near which we will drop.
66 var index = PlacesUtils.bookmarks.getItemIndex(this.dropNearItemId);
67 return this.orientation == Ci.nsITreeView.DROP_BEFORE ? index : index + 1;
68 }
69 return this._index;
70 }
71 };
73 /**
74 * Places Controller
75 */
77 function PlacesController(aView) {
78 this._view = aView;
79 XPCOMUtils.defineLazyServiceGetter(this, "clipboard",
80 "@mozilla.org/widget/clipboard;1",
81 "nsIClipboard");
82 XPCOMUtils.defineLazyGetter(this, "profileName", function () {
83 return Services.dirsvc.get("ProfD", Ci.nsIFile).leafName;
84 });
86 this._cachedLivemarkInfoObjects = new Map();
87 }
89 PlacesController.prototype = {
90 /**
91 * The places view.
92 */
93 _view: null,
95 QueryInterface: XPCOMUtils.generateQI([
96 Ci.nsIClipboardOwner
97 ]),
99 // nsIClipboardOwner
100 LosingOwnership: function PC_LosingOwnership (aXferable) {
101 this.cutNodes = [];
102 },
104 terminate: function PC_terminate() {
105 this._releaseClipboardOwnership();
106 },
108 supportsCommand: function PC_supportsCommand(aCommand) {
109 // Non-Places specific commands that we also support
110 switch (aCommand) {
111 case "cmd_undo":
112 case "cmd_redo":
113 case "cmd_cut":
114 case "cmd_copy":
115 case "cmd_paste":
116 case "cmd_delete":
117 case "cmd_selectAll":
118 return true;
119 }
121 // All other Places Commands are prefixed with "placesCmd_" ... this
122 // filters out other commands that we do _not_ support (see 329587).
123 const CMD_PREFIX = "placesCmd_";
124 return (aCommand.substr(0, CMD_PREFIX.length) == CMD_PREFIX);
125 },
127 isCommandEnabled: function PC_isCommandEnabled(aCommand) {
128 if (PlacesUIUtils.useAsyncTransactions) {
129 switch (aCommand) {
130 case "cmd_cut":
131 case "placesCmd_cut":
132 case "cmd_copy":
133 case "cmd_paste":
134 case "cmd_delete":
135 case "placesCmd_delete":
136 case "cmd_paste":
137 case "placesCmd_paste":
138 case "placesCmd_new:folder":
139 case "placesCmd_new:bookmark":
140 case "placesCmd_createBookmark":
141 return false;
142 }
143 }
145 switch (aCommand) {
146 case "cmd_undo":
147 if (!PlacesUIUtils.useAsyncTransactions)
148 return PlacesUtils.transactionManager.numberOfUndoItems > 0;
150 return PlacesTransactions.topUndoEntry != null;
151 case "cmd_redo":
152 if (!PlacesUIUtils.useAsyncTransactions)
153 return PlacesUtils.transactionManager.numberOfRedoItems > 0;
155 return PlacesTransactions.topRedoEntry != null;
156 case "cmd_cut":
157 case "placesCmd_cut":
158 var nodes = this._view.selectedNodes;
159 // If selection includes history nodes there's no reason to allow cut.
160 for (var i = 0; i < nodes.length; i++) {
161 if (nodes[i].itemId == -1)
162 return false;
163 }
164 // Otherwise fallback to cmd_delete check.
165 case "cmd_delete":
166 case "placesCmd_delete":
167 case "placesCmd_deleteDataHost":
168 return this._hasRemovableSelection(false);
169 case "placesCmd_moveBookmarks":
170 return this._hasRemovableSelection(true);
171 case "cmd_copy":
172 case "placesCmd_copy":
173 return this._view.hasSelection;
174 case "cmd_paste":
175 case "placesCmd_paste":
176 return this._canInsert(true) && this._isClipboardDataPasteable();
177 case "cmd_selectAll":
178 if (this._view.selType != "single") {
179 let rootNode = this._view.result.root;
180 if (rootNode.containerOpen && rootNode.childCount > 0)
181 return true;
182 }
183 return false;
184 case "placesCmd_open":
185 case "placesCmd_open:window":
186 case "placesCmd_open:tab":
187 var selectedNode = this._view.selectedNode;
188 return selectedNode && PlacesUtils.nodeIsURI(selectedNode);
189 case "placesCmd_new:folder":
190 return this._canInsert();
191 case "placesCmd_new:bookmark":
192 return this._canInsert();
193 case "placesCmd_new:separator":
194 return this._canInsert() &&
195 !PlacesUtils.asQuery(this._view.result.root).queryOptions.excludeItems &&
196 this._view.result.sortingMode ==
197 Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
198 case "placesCmd_show:info":
199 var selectedNode = this._view.selectedNode;
200 return selectedNode && PlacesUtils.getConcreteItemId(selectedNode) != -1
201 case "placesCmd_reload":
202 // Livemark containers
203 var selectedNode = this._view.selectedNode;
204 return selectedNode && this.hasCachedLivemarkInfo(selectedNode);
205 case "placesCmd_sortBy:name":
206 var selectedNode = this._view.selectedNode;
207 return selectedNode &&
208 PlacesUtils.nodeIsFolder(selectedNode) &&
209 !PlacesUtils.nodeIsReadOnly(selectedNode) &&
210 this._view.result.sortingMode ==
211 Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
212 case "placesCmd_createBookmark":
213 var node = this._view.selectedNode;
214 return node && PlacesUtils.nodeIsURI(node) && node.itemId == -1;
215 default:
216 return false;
217 }
218 },
220 doCommand: function PC_doCommand(aCommand) {
221 switch (aCommand) {
222 case "cmd_undo":
223 if (!PlacesUIUtils.useAsyncTransactions) {
224 PlacesUtils.transactionManager.undoTransaction();
225 return;
226 }
227 PlacesTransactions.undo().then(null, Components.utils.reportError);
228 break;
229 case "cmd_redo":
230 if (!PlacesUIUtils.useAsyncTransactions) {
231 PlacesUtils.transactionManager.redoTransaction();
232 return;
233 }
234 PlacesTransactions.redo().then(null, Components.utils.reportError);
235 break;
236 case "cmd_cut":
237 case "placesCmd_cut":
238 this.cut();
239 break;
240 case "cmd_copy":
241 case "placesCmd_copy":
242 this.copy();
243 break;
244 case "cmd_paste":
245 case "placesCmd_paste":
246 this.paste();
247 break;
248 case "cmd_delete":
249 case "placesCmd_delete":
250 this.remove("Remove Selection");
251 break;
252 case "placesCmd_deleteDataHost":
253 var host;
254 if (PlacesUtils.nodeIsHost(this._view.selectedNode)) {
255 var queries = this._view.selectedNode.getQueries();
256 host = queries[0].domain;
257 }
258 else
259 host = NetUtil.newURI(this._view.selectedNode.uri).host;
260 ForgetAboutSite.removeDataFromDomain(host);
261 break;
262 case "cmd_selectAll":
263 this.selectAll();
264 break;
265 case "placesCmd_open":
266 PlacesUIUtils.openNodeIn(this._view.selectedNode, "current", this._view);
267 break;
268 case "placesCmd_open:window":
269 PlacesUIUtils.openNodeIn(this._view.selectedNode, "window", this._view);
270 break;
271 case "placesCmd_open:tab":
272 PlacesUIUtils.openNodeIn(this._view.selectedNode, "tab", this._view);
273 break;
274 case "placesCmd_new:folder":
275 this.newItem("folder");
276 break;
277 case "placesCmd_new:bookmark":
278 this.newItem("bookmark");
279 break;
280 case "placesCmd_new:separator":
281 this.newSeparator().then(null, Components.utils.reportError);
282 break;
283 case "placesCmd_show:info":
284 this.showBookmarkPropertiesForSelection();
285 break;
286 case "placesCmd_moveBookmarks":
287 this.moveSelectedBookmarks();
288 break;
289 case "placesCmd_reload":
290 this.reloadSelectedLivemark();
291 break;
292 case "placesCmd_sortBy:name":
293 this.sortFolderByName().then(null, Components.utils.reportError);
294 break;
295 case "placesCmd_createBookmark":
296 let node = this._view.selectedNode;
297 PlacesUIUtils.showBookmarkDialog({ action: "add"
298 , type: "bookmark"
299 , hiddenRows: [ "description"
300 , "keyword"
301 , "location"
302 , "loadInSidebar" ]
303 , uri: NetUtil.newURI(node.uri)
304 , title: node.title
305 }, window.top);
306 break;
307 }
308 },
310 onEvent: function PC_onEvent(eventName) { },
313 /**
314 * Determine whether or not the selection can be removed, either by the
315 * delete or cut operations based on whether or not any of its contents
316 * are non-removable. We don't need to worry about recursion here since it
317 * is a policy decision that a removable item not be placed inside a non-
318 * removable item.
319 * @param aIsMoveCommand
320 * True if the command for which this method is called only moves the
321 * selected items to another container, false otherwise.
322 * @return true if all nodes in the selection can be removed,
323 * false otherwise.
324 */
325 _hasRemovableSelection: function PC__hasRemovableSelection(aIsMoveCommand) {
326 var ranges = this._view.removableSelectionRanges;
327 if (!ranges.length)
328 return false;
330 var root = this._view.result.root;
332 for (var j = 0; j < ranges.length; j++) {
333 var nodes = ranges[j];
334 for (var i = 0; i < nodes.length; ++i) {
335 // Disallow removing the view's root node
336 if (nodes[i] == root)
337 return false;
339 if (PlacesUtils.nodeIsFolder(nodes[i]) &&
340 !PlacesControllerDragHelper.canMoveNode(nodes[i]))
341 return false;
343 // We don't call nodeIsReadOnly here, because nodeIsReadOnly means that
344 // a node has children that cannot be edited, reordered or removed. Here,
345 // we don't care if a node's children can't be reordered or edited, just
346 // that they're removable. All history results have removable children
347 // (based on the principle that any URL in the history table should be
348 // removable), but some special bookmark folders may have non-removable
349 // children, e.g. live bookmark folder children. It doesn't make sense
350 // to delete a child of a live bookmark folder, since when the folder
351 // refreshes, the child will return.
352 var parent = nodes[i].parent || root;
353 if (PlacesUtils.isReadonlyFolder(parent))
354 return false;
355 }
356 }
358 return true;
359 },
361 /**
362 * Determines whether or not nodes can be inserted relative to the selection.
363 */
364 _canInsert: function PC__canInsert(isPaste) {
365 var ip = this._view.insertionPoint;
366 return ip != null && (isPaste || ip.isTag != true);
367 },
369 /**
370 * Determines whether or not the root node for the view is selected
371 */
372 rootNodeIsSelected: function PC_rootNodeIsSelected() {
373 var nodes = this._view.selectedNodes;
374 var root = this._view.result.root;
375 for (var i = 0; i < nodes.length; ++i) {
376 if (nodes[i] == root)
377 return true;
378 }
380 return false;
381 },
383 /**
384 * Looks at the data on the clipboard to see if it is paste-able.
385 * Paste-able data is:
386 * - in a format that the view can receive
387 * @return true if: - clipboard data is of a TYPE_X_MOZ_PLACE_* flavor,
388 * - clipboard data is of type TEXT_UNICODE and
389 * is a valid URI.
390 */
391 _isClipboardDataPasteable: function PC__isClipboardDataPasteable() {
392 // if the clipboard contains TYPE_X_MOZ_PLACE_* data, it is definitely
393 // pasteable, with no need to unwrap all the nodes.
395 var flavors = PlacesControllerDragHelper.placesFlavors;
396 var clipboard = this.clipboard;
397 var hasPlacesData =
398 clipboard.hasDataMatchingFlavors(flavors, flavors.length,
399 Ci.nsIClipboard.kGlobalClipboard);
400 if (hasPlacesData)
401 return this._view.insertionPoint != null;
403 // if the clipboard doesn't have TYPE_X_MOZ_PLACE_* data, we also allow
404 // pasting of valid "text/unicode" and "text/x-moz-url" data
405 var xferable = Cc["@mozilla.org/widget/transferable;1"].
406 createInstance(Ci.nsITransferable);
407 xferable.init(null);
409 xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_URL);
410 xferable.addDataFlavor(PlacesUtils.TYPE_UNICODE);
411 clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
413 try {
414 // getAnyTransferData will throw if no data is available.
415 var data = { }, type = { };
416 xferable.getAnyTransferData(type, data, { });
417 data = data.value.QueryInterface(Ci.nsISupportsString).data;
418 if (type.value != PlacesUtils.TYPE_X_MOZ_URL &&
419 type.value != PlacesUtils.TYPE_UNICODE)
420 return false;
422 // unwrapNodes() will throw if the data blob is malformed.
423 var unwrappedNodes = PlacesUtils.unwrapNodes(data, type.value);
424 return this._view.insertionPoint != null;
425 }
426 catch (e) {
427 // getAnyTransferData or unwrapNodes failed
428 return false;
429 }
430 },
432 /**
433 * Gathers information about the selected nodes according to the following
434 * rules:
435 * "link" node is a URI
436 * "bookmark" node is a bookamrk
437 * "livemarkChild" node is a child of a livemark
438 * "tagChild" node is a child of a tag
439 * "folder" node is a folder
440 * "query" node is a query
441 * "separator" node is a separator line
442 * "host" node is a host
443 *
444 * @return an array of objects corresponding the selected nodes. Each
445 * object has each of the properties above set if its corresponding
446 * node matches the rule. In addition, the annotations names for each
447 * node are set on its corresponding object as properties.
448 * Notes:
449 * 1) This can be slow, so don't call it anywhere performance critical!
450 * 2) A single-object array corresponding the root node is returned if
451 * there's no selection.
452 */
453 _buildSelectionMetadata: function PC__buildSelectionMetadata() {
454 var metadata = [];
455 var root = this._view.result.root;
456 var nodes = this._view.selectedNodes;
457 if (nodes.length == 0)
458 nodes.push(root); // See the second note above
460 for (var i = 0; i < nodes.length; i++) {
461 var nodeData = {};
462 var node = nodes[i];
463 var nodeType = node.type;
464 var uri = null;
466 // We don't use the nodeIs* methods here to avoid going through the type
467 // property way too often
468 switch (nodeType) {
469 case Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY:
470 nodeData["query"] = true;
471 if (node.parent) {
472 switch (PlacesUtils.asQuery(node.parent).queryOptions.resultType) {
473 case Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY:
474 nodeData["host"] = true;
475 break;
476 case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY:
477 case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY:
478 nodeData["day"] = true;
479 break;
480 }
481 }
482 break;
483 case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER:
484 case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT:
485 nodeData["folder"] = true;
486 break;
487 case Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR:
488 nodeData["separator"] = true;
489 break;
490 case Ci.nsINavHistoryResultNode.RESULT_TYPE_URI:
491 nodeData["link"] = true;
492 uri = NetUtil.newURI(node.uri);
493 if (PlacesUtils.nodeIsBookmark(node)) {
494 nodeData["bookmark"] = true;
495 PlacesUtils.nodeIsTagQuery(node.parent)
497 var parentNode = node.parent;
498 if (parentNode) {
499 if (PlacesUtils.nodeIsTagQuery(parentNode))
500 nodeData["tagChild"] = true;
501 else if (this.hasCachedLivemarkInfo(parentNode))
502 nodeData["livemarkChild"] = true;
503 }
504 }
505 break;
506 }
508 // annotations
509 if (uri) {
510 let names = PlacesUtils.annotations.getPageAnnotationNames(uri);
511 for (let j = 0; j < names.length; ++j)
512 nodeData[names[j]] = true;
513 }
515 // For items also include the item-specific annotations
516 if (node.itemId != -1) {
517 let names = PlacesUtils.annotations
518 .getItemAnnotationNames(node.itemId);
519 for (let j = 0; j < names.length; ++j)
520 nodeData[names[j]] = true;
521 }
522 metadata.push(nodeData);
523 }
525 return metadata;
526 },
528 /**
529 * Determines if a context-menu item should be shown
530 * @param aMenuItem
531 * the context menu item
532 * @param aMetaData
533 * meta data about the selection
534 * @return true if the conditions (see buildContextMenu) are satisfied
535 * and the item can be displayed, false otherwise.
536 */
537 _shouldShowMenuItem: function PC__shouldShowMenuItem(aMenuItem, aMetaData) {
538 var selectiontype = aMenuItem.getAttribute("selectiontype");
539 if (selectiontype == "multiple" && aMetaData.length == 1)
540 return false;
541 if (selectiontype == "single" && aMetaData.length != 1)
542 return false;
544 var forceHideAttr = aMenuItem.getAttribute("forcehideselection");
545 if (forceHideAttr) {
546 var forceHideRules = forceHideAttr.split("|");
547 for (let i = 0; i < aMetaData.length; ++i) {
548 for (let j = 0; j < forceHideRules.length; ++j) {
549 if (forceHideRules[j] in aMetaData[i])
550 return false;
551 }
552 }
553 }
555 var selectionAttr = aMenuItem.getAttribute("selection");
556 if (!selectionAttr) {
557 return !aMenuItem.hidden;
558 }
560 if (selectionAttr == "any")
561 return true;
563 var showRules = selectionAttr.split("|");
564 var anyMatched = false;
565 function metaDataNodeMatches(metaDataNode, rules) {
566 for (var i = 0; i < rules.length; i++) {
567 if (rules[i] in metaDataNode)
568 return true;
569 }
570 return false;
571 }
573 for (var i = 0; i < aMetaData.length; ++i) {
574 if (metaDataNodeMatches(aMetaData[i], showRules))
575 anyMatched = true;
576 else
577 return false;
578 }
579 return anyMatched;
580 },
582 /**
583 * Detects information (meta-data rules) about the current selection in the
584 * view (see _buildSelectionMetadata) and sets the visibility state for each
585 * of the menu-items in the given popup with the following rules applied:
586 * 1) The "selectiontype" attribute may be set on a menu-item to "single"
587 * if the menu-item should be visible only if there is a single node
588 * selected, or to "multiple" if the menu-item should be visible only if
589 * multiple nodes are selected. If the attribute is not set or if it is
590 * set to an invalid value, the menu-item may be visible for both types of
591 * selection.
592 * 2) The "selection" attribute may be set on a menu-item to the various
593 * meta-data rules for which it may be visible. The rules should be
594 * separated with the | character.
595 * 3) A menu-item may be visible only if at least one of the rules set in
596 * its selection attribute apply to each of the selected nodes in the
597 * view.
598 * 4) The "forcehideselection" attribute may be set on a menu-item to rules
599 * for which it should be hidden. This attribute takes priority over the
600 * selection attribute. A menu-item would be hidden if at least one of the
601 * given rules apply to one of the selected nodes. The rules should be
602 * separated with the | character.
603 * 5) The "hideifnoinsertionpoint" attribute may be set on a menu-item to
604 * true if it should be hidden when there's no insertion point
605 * 6) The visibility state of a menu-item is unchanged if none of these
606 * attribute are set.
607 * 7) These attributes should not be set on separators for which the
608 * visibility state is "auto-detected."
609 * 8) The "hideifprivatebrowsing" attribute may be set on a menu-item to
610 * true if it should be hidden inside the private browsing mode
611 * @param aPopup
612 * The menupopup to build children into.
613 * @return true if at least one item is visible, false otherwise.
614 */
615 buildContextMenu: function PC_buildContextMenu(aPopup) {
616 var metadata = this._buildSelectionMetadata();
617 var ip = this._view.insertionPoint;
618 var noIp = !ip || ip.isTag;
620 var separator = null;
621 var visibleItemsBeforeSep = false;
622 var anyVisible = false;
623 for (var i = 0; i < aPopup.childNodes.length; ++i) {
624 var item = aPopup.childNodes[i];
625 if (item.localName != "menuseparator") {
626 // We allow pasting into tag containers, so special case that.
627 var hideIfNoIP = item.getAttribute("hideifnoinsertionpoint") == "true" &&
628 noIp && !(ip && ip.isTag && item.id == "placesContext_paste");
629 item.hidden = hideIfNoIP || !this._shouldShowMenuItem(item, metadata);
631 if (!item.hidden) {
632 visibleItemsBeforeSep = true;
633 anyVisible = true;
635 // Show the separator above the menu-item if any
636 if (separator) {
637 separator.hidden = false;
638 separator = null;
639 }
640 }
641 }
642 else { // menuseparator
643 // Initially hide it. It will be unhidden if there will be at least one
644 // visible menu-item above and below it.
645 item.hidden = true;
647 // We won't show the separator at all if no items are visible above it
648 if (visibleItemsBeforeSep)
649 separator = item;
651 // New separator, count again:
652 visibleItemsBeforeSep = false;
653 }
654 }
656 // Set Open Folder/Links In Tabs items enabled state if they're visible
657 if (anyVisible) {
658 var openContainerInTabsItem = document.getElementById("placesContext_openContainer:tabs");
659 if (!openContainerInTabsItem.hidden && this._view.selectedNode &&
660 PlacesUtils.nodeIsContainer(this._view.selectedNode)) {
661 openContainerInTabsItem.disabled =
662 !PlacesUtils.hasChildURIs(this._view.selectedNode);
663 }
664 else {
665 // see selectiontype rule in the overlay
666 var openLinksInTabsItem = document.getElementById("placesContext_openLinks:tabs");
667 openLinksInTabsItem.disabled = openLinksInTabsItem.hidden;
668 }
669 }
671 return anyVisible;
672 },
674 /**
675 * Select all links in the current view.
676 */
677 selectAll: function PC_selectAll() {
678 this._view.selectAll();
679 },
681 /**
682 * Opens the bookmark properties for the selected URI Node.
683 */
684 showBookmarkPropertiesForSelection:
685 function PC_showBookmarkPropertiesForSelection() {
686 var node = this._view.selectedNode;
687 if (!node)
688 return;
690 var itemType = PlacesUtils.nodeIsFolder(node) ||
691 PlacesUtils.nodeIsTagQuery(node) ? "folder" : "bookmark";
692 var concreteId = PlacesUtils.getConcreteItemId(node);
693 var isRootItem = PlacesUtils.isRootItem(concreteId);
694 var itemId = node.itemId;
695 if (isRootItem || PlacesUtils.nodeIsTagQuery(node)) {
696 // If this is a root or the Tags query we use the concrete itemId to catch
697 // the correct title for the node.
698 itemId = concreteId;
699 }
701 PlacesUIUtils.showBookmarkDialog({ action: "edit"
702 , type: itemType
703 , itemId: itemId
704 , readOnly: isRootItem
705 , hiddenRows: [ "folderPicker" ]
706 }, window.top);
707 },
709 /**
710 * This method can be run on a URI parameter to ensure that it didn't
711 * receive a string instead of an nsIURI object.
712 */
713 _assertURINotString: function PC__assertURINotString(value) {
714 NS_ASSERT((typeof(value) == "object") && !(value instanceof String),
715 "This method should be passed a URI as a nsIURI object, not as a string.");
716 },
718 /**
719 * Reloads the selected livemark if any.
720 */
721 reloadSelectedLivemark: function PC_reloadSelectedLivemark() {
722 var selectedNode = this._view.selectedNode;
723 if (selectedNode) {
724 let itemId = selectedNode.itemId;
725 PlacesUtils.livemarks.getLivemark({ id: itemId })
726 .then(aLivemark => {
727 aLivemark.reload(true);
728 }, Components.utils.reportError);
729 }
730 },
732 /**
733 * Opens the links in the selected folder, or the selected links in new tabs.
734 */
735 openSelectionInTabs: function PC_openLinksInTabs(aEvent) {
736 var node = this._view.selectedNode;
737 if (node && PlacesUtils.nodeIsContainer(node))
738 PlacesUIUtils.openContainerNodeInTabs(this._view.selectedNode, aEvent, this._view);
739 else
740 PlacesUIUtils.openURINodesInTabs(this._view.selectedNodes, aEvent, this._view);
741 },
743 /**
744 * Shows the Add Bookmark UI for the current insertion point.
745 *
746 * @param aType
747 * the type of the new item (bookmark/livemark/folder)
748 */
749 newItem: function PC_newItem(aType) {
750 let ip = this._view.insertionPoint;
751 if (!ip)
752 throw Cr.NS_ERROR_NOT_AVAILABLE;
754 let performed =
755 PlacesUIUtils.showBookmarkDialog({ action: "add"
756 , type: aType
757 , defaultInsertionPoint: ip
758 , hiddenRows: [ "folderPicker" ]
759 }, window.top);
760 if (performed) {
761 // Select the new item.
762 let insertedNodeId = PlacesUtils.bookmarks
763 .getIdForItemAt(ip.itemId, ip.index);
764 this._view.selectItems([insertedNodeId], false);
765 }
766 },
768 /**
769 * Create a new Bookmark separator somewhere.
770 */
771 newSeparator: Task.async(function* () {
772 var ip = this._view.insertionPoint;
773 if (!ip)
774 throw Cr.NS_ERROR_NOT_AVAILABLE;
776 if (!PlacesUIUtils.useAsyncTransactions) {
777 let txn = new PlacesCreateSeparatorTransaction(ip.itemId, ip.index);
778 PlacesUtils.transactionManager.doTransaction(txn);
779 // Select the new item.
780 let insertedNodeId = PlacesUtils.bookmarks
781 .getIdForItemAt(ip.itemId, ip.index);
782 this._view.selectItems([insertedNodeId], false);
783 return;
784 }
786 let txn = PlacesTransactions.NewSeparator({ parentGUID: yield ip.promiseGUID()
787 , index: ip.index });
788 let guid = yield PlacesTransactions.transact(txn);
789 let itemId = yield PlacesUtils.promiseItemId(guid);
790 // Select the new item.
791 this._view.selectItems([itemId], false);
792 }),
794 /**
795 * Opens a dialog for moving the selected nodes.
796 */
797 moveSelectedBookmarks: function PC_moveBookmarks() {
798 window.openDialog("chrome://browser/content/places/moveBookmarks.xul",
799 "", "chrome, modal",
800 this._view.selectedNodes);
801 },
803 /**
804 * Sort the selected folder by name
805 */
806 sortFolderByName: Task.async(function* () {
807 let itemId = PlacesUtils.getConcreteItemId(this._view.selectedNode);
808 if (!PlacesUIUtils.useAsyncTransactions) {
809 var txn = new PlacesSortFolderByNameTransaction(itemId);
810 PlacesUtils.transactionManager.doTransaction(txn);
811 return;
812 }
813 let guid = yield PlacesUtils.promiseItemGUID(itemId);
814 yield PlacesTransactions.transact(PlacesTransactions.SortByName(guid));
815 }),
817 /**
818 * Walk the list of folders we're removing in this delete operation, and
819 * see if the selected node specified is already implicitly being removed
820 * because it is a child of that folder.
821 * @param node
822 * Node to check for containment.
823 * @param pastFolders
824 * List of folders the calling function has already traversed
825 * @return true if the node should be skipped, false otherwise.
826 */
827 _shouldSkipNode: function PC_shouldSkipNode(node, pastFolders) {
828 /**
829 * Determines if a node is contained by another node within a resultset.
830 * @param node
831 * The node to check for containment for
832 * @param parent
833 * The parent container to check for containment in
834 * @return true if node is a member of parent's children, false otherwise.
835 */
836 function isContainedBy(node, parent) {
837 var cursor = node.parent;
838 while (cursor) {
839 if (cursor == parent)
840 return true;
841 cursor = cursor.parent;
842 }
843 return false;
844 }
846 for (var j = 0; j < pastFolders.length; ++j) {
847 if (isContainedBy(node, pastFolders[j]))
848 return true;
849 }
850 return false;
851 },
853 /**
854 * Creates a set of transactions for the removal of a range of items.
855 * A range is an array of adjacent nodes in a view.
856 * @param [in] range
857 * An array of nodes to remove. Should all be adjacent.
858 * @param [out] transactions
859 * An array of transactions.
860 * @param [optional] removedFolders
861 * An array of folder nodes that have already been removed.
862 */
863 _removeRange: function PC__removeRange(range, transactions, removedFolders) {
864 NS_ASSERT(transactions instanceof Array, "Must pass a transactions array");
865 if (!removedFolders)
866 removedFolders = [];
868 for (var i = 0; i < range.length; ++i) {
869 var node = range[i];
870 if (this._shouldSkipNode(node, removedFolders))
871 continue;
873 if (PlacesUtils.nodeIsTagQuery(node.parent)) {
874 // This is a uri node inside a tag container. It needs a special
875 // untag transaction.
876 var tagItemId = PlacesUtils.getConcreteItemId(node.parent);
877 var uri = NetUtil.newURI(node.uri);
878 let txn = new PlacesUntagURITransaction(uri, [tagItemId]);
879 transactions.push(txn);
880 }
881 else if (PlacesUtils.nodeIsTagQuery(node) && node.parent &&
882 PlacesUtils.nodeIsQuery(node.parent) &&
883 PlacesUtils.asQuery(node.parent).queryOptions.resultType ==
884 Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY) {
885 // This is a tag container.
886 // Untag all URIs tagged with this tag only if the tag container is
887 // child of the "Tags" query in the library, in all other places we
888 // must only remove the query node.
889 var tag = node.title;
890 var URIs = PlacesUtils.tagging.getURIsForTag(tag);
891 for (var j = 0; j < URIs.length; j++) {
892 let txn = new PlacesUntagURITransaction(URIs[j], [tag]);
893 transactions.push(txn);
894 }
895 }
896 else if (PlacesUtils.nodeIsURI(node) &&
897 PlacesUtils.nodeIsQuery(node.parent) &&
898 PlacesUtils.asQuery(node.parent).queryOptions.queryType ==
899 Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
900 // This is a uri node inside an history query.
901 PlacesUtils.bhistory.removePage(NetUtil.newURI(node.uri));
902 // History deletes are not undoable, so we don't have a transaction.
903 }
904 else if (node.itemId == -1 &&
905 PlacesUtils.nodeIsQuery(node) &&
906 PlacesUtils.asQuery(node).queryOptions.queryType ==
907 Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
908 // This is a dynamically generated history query, like queries
909 // grouped by site, time or both. Dynamically generated queries don't
910 // have an itemId even if they are descendants of a bookmark.
911 this._removeHistoryContainer(node);
912 // History deletes are not undoable, so we don't have a transaction.
913 }
914 else {
915 // This is a common bookmark item.
916 if (PlacesUtils.nodeIsFolder(node)) {
917 // If this is a folder we add it to our array of folders, used
918 // to skip nodes that are children of an already removed folder.
919 removedFolders.push(node);
920 }
921 let txn = new PlacesRemoveItemTransaction(node.itemId);
922 transactions.push(txn);
923 }
924 }
925 },
927 /**
928 * Removes the set of selected ranges from bookmarks.
929 * @param txnName
930 * See |remove|.
931 */
932 _removeRowsFromBookmarks: function PC__removeRowsFromBookmarks(txnName) {
933 var ranges = this._view.removableSelectionRanges;
934 var transactions = [];
935 var removedFolders = [];
937 for (var i = 0; i < ranges.length; i++)
938 this._removeRange(ranges[i], transactions, removedFolders);
940 if (transactions.length > 0) {
941 var txn = new PlacesAggregatedTransaction(txnName, transactions);
942 PlacesUtils.transactionManager.doTransaction(txn);
943 }
944 },
946 /**
947 * Removes the set of selected ranges from history.
948 *
949 * @note history deletes are not undoable.
950 */
951 _removeRowsFromHistory: function PC__removeRowsFromHistory() {
952 let nodes = this._view.selectedNodes;
953 let URIs = [];
954 for (let i = 0; i < nodes.length; ++i) {
955 let node = nodes[i];
956 if (PlacesUtils.nodeIsURI(node)) {
957 let uri = NetUtil.newURI(node.uri);
958 // Avoid duplicates.
959 if (URIs.indexOf(uri) < 0) {
960 URIs.push(uri);
961 }
962 }
963 else if (PlacesUtils.nodeIsQuery(node) &&
964 PlacesUtils.asQuery(node).queryOptions.queryType ==
965 Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
966 this._removeHistoryContainer(node);
967 }
968 }
970 // Do removal in chunks to give some breath to main-thread.
971 function pagesChunkGenerator(aURIs) {
972 while (aURIs.length) {
973 let URIslice = aURIs.splice(0, REMOVE_PAGES_CHUNKLEN);
974 PlacesUtils.bhistory.removePages(URIslice, URIslice.length);
975 Services.tm.mainThread.dispatch(function() {
976 try {
977 gen.next();
978 } catch (ex if ex instanceof StopIteration) {}
979 }, Ci.nsIThread.DISPATCH_NORMAL);
980 yield undefined;
981 }
982 }
983 let gen = pagesChunkGenerator(URIs);
984 gen.next();
985 },
987 /**
988 * Removes history visits for an history container node.
989 * @param [in] aContainerNode
990 * The container node to remove.
991 *
992 * @note history deletes are not undoable.
993 */
994 _removeHistoryContainer: function PC__removeHistoryContainer(aContainerNode) {
995 if (PlacesUtils.nodeIsHost(aContainerNode)) {
996 // Site container.
997 PlacesUtils.bhistory.removePagesFromHost(aContainerNode.title, true);
998 }
999 else if (PlacesUtils.nodeIsDay(aContainerNode)) {
1000 // Day container.
1001 let query = aContainerNode.getQueries()[0];
1002 let beginTime = query.beginTime;
1003 let endTime = query.endTime;
1004 NS_ASSERT(query && beginTime && endTime,
1005 "A valid date container query should exist!");
1006 // We want to exclude beginTime from the removal because
1007 // removePagesByTimeframe includes both extremes, while date containers
1008 // exclude the lower extreme. So, if we would not exclude it, we would
1009 // end up removing more history than requested.
1010 PlacesUtils.bhistory.removePagesByTimeframe(beginTime + 1, endTime);
1011 }
1012 },
1014 /**
1015 * Removes the selection
1016 * @param aTxnName
1017 * A name for the transaction if this is being performed
1018 * as part of another operation.
1019 */
1020 remove: function PC_remove(aTxnName) {
1021 if (!this._hasRemovableSelection(false))
1022 return;
1024 NS_ASSERT(aTxnName !== undefined, "Must supply Transaction Name");
1026 var root = this._view.result.root;
1028 if (PlacesUtils.nodeIsFolder(root))
1029 this._removeRowsFromBookmarks(aTxnName);
1030 else if (PlacesUtils.nodeIsQuery(root)) {
1031 var queryType = PlacesUtils.asQuery(root).queryOptions.queryType;
1032 if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS)
1033 this._removeRowsFromBookmarks(aTxnName);
1034 else if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY)
1035 this._removeRowsFromHistory();
1036 else
1037 NS_ASSERT(false, "implement support for QUERY_TYPE_UNIFIED");
1038 }
1039 else
1040 NS_ASSERT(false, "unexpected root");
1041 },
1043 /**
1044 * Fills a DataTransfer object with the content of the selection that can be
1045 * dropped elsewhere.
1046 * @param aEvent
1047 * The dragstart event.
1048 */
1049 setDataTransfer: function PC_setDataTransfer(aEvent) {
1050 let dt = aEvent.dataTransfer;
1052 let result = this._view.result;
1053 let didSuppressNotifications = result.suppressNotifications;
1054 if (!didSuppressNotifications)
1055 result.suppressNotifications = true;
1057 function addData(type, index, overrideURI) {
1058 let wrapNode = PlacesUtils.wrapNode(node, type, overrideURI);
1059 dt.mozSetDataAt(type, wrapNode, index);
1060 }
1062 function addURIData(index, overrideURI) {
1063 addData(PlacesUtils.TYPE_X_MOZ_URL, index, overrideURI);
1064 addData(PlacesUtils.TYPE_UNICODE, index, overrideURI);
1065 addData(PlacesUtils.TYPE_HTML, index, overrideURI);
1066 }
1068 try {
1069 let nodes = this._view.draggableSelection;
1070 for (let i = 0; i < nodes.length; ++i) {
1071 var node = nodes[i];
1073 // This order is _important_! It controls how this and other
1074 // applications select data to be inserted based on type.
1075 addData(PlacesUtils.TYPE_X_MOZ_PLACE, i);
1077 // Drop the feed uri for livemark containers
1078 let livemarkInfo = this.getCachedLivemarkInfo(node);
1079 if (livemarkInfo) {
1080 addURIData(i, livemarkInfo.feedURI.spec);
1081 }
1082 else if (node.uri) {
1083 addURIData(i);
1084 }
1085 }
1086 }
1087 finally {
1088 if (!didSuppressNotifications)
1089 result.suppressNotifications = false;
1090 }
1091 },
1093 get clipboardAction () {
1094 let action = {};
1095 let actionOwner;
1096 try {
1097 let xferable = Cc["@mozilla.org/widget/transferable;1"].
1098 createInstance(Ci.nsITransferable);
1099 xferable.init(null);
1100 xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION)
1101 this.clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
1102 xferable.getTransferData(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION, action, {});
1103 [action, actionOwner] =
1104 action.value.QueryInterface(Ci.nsISupportsString).data.split(",");
1105 } catch(ex) {
1106 // Paste from external sources don't have any associated action, just
1107 // fallback to a copy action.
1108 return "copy";
1109 }
1110 // For cuts also check who inited the action, since cuts across different
1111 // instances should instead be handled as copies (The sources are not
1112 // available for this instance).
1113 if (action == "cut" && actionOwner != this.profileName)
1114 action = "copy";
1116 return action;
1117 },
1119 _releaseClipboardOwnership: function PC__releaseClipboardOwnership() {
1120 if (this.cutNodes.length > 0) {
1121 // This clears the logical clipboard, doesn't remove data.
1122 this.clipboard.emptyClipboard(Ci.nsIClipboard.kGlobalClipboard);
1123 }
1124 },
1126 _clearClipboard: function PC__clearClipboard() {
1127 let xferable = Cc["@mozilla.org/widget/transferable;1"].
1128 createInstance(Ci.nsITransferable);
1129 xferable.init(null);
1130 // Empty transferables may cause crashes, so just add an unknown type.
1131 const TYPE = "text/x-moz-place-empty";
1132 xferable.addDataFlavor(TYPE);
1133 xferable.setTransferData(TYPE, PlacesUtils.toISupportsString(""), 0);
1134 this.clipboard.setData(xferable, null, Ci.nsIClipboard.kGlobalClipboard);
1135 },
1137 _populateClipboard: function PC__populateClipboard(aNodes, aAction) {
1138 // This order is _important_! It controls how this and other applications
1139 // select data to be inserted based on type.
1140 let contents = [
1141 { type: PlacesUtils.TYPE_X_MOZ_PLACE, entries: [] },
1142 { type: PlacesUtils.TYPE_X_MOZ_URL, entries: [] },
1143 { type: PlacesUtils.TYPE_HTML, entries: [] },
1144 { type: PlacesUtils.TYPE_UNICODE, entries: [] },
1145 ];
1147 // Avoid handling descendants of a copied node, the transactions take care
1148 // of them automatically.
1149 let copiedFolders = [];
1150 aNodes.forEach(function (node) {
1151 if (this._shouldSkipNode(node, copiedFolders))
1152 return;
1153 if (PlacesUtils.nodeIsFolder(node))
1154 copiedFolders.push(node);
1156 let livemarkInfo = this.getCachedLivemarkInfo(node);
1157 let overrideURI = livemarkInfo ? livemarkInfo.feedURI.spec : null;
1159 contents.forEach(function (content) {
1160 content.entries.push(
1161 PlacesUtils.wrapNode(node, content.type, overrideURI)
1162 );
1163 });
1164 }, this);
1166 function addData(type, data) {
1167 xferable.addDataFlavor(type);
1168 xferable.setTransferData(type, PlacesUtils.toISupportsString(data),
1169 data.length * 2);
1170 }
1172 let xferable = Cc["@mozilla.org/widget/transferable;1"].
1173 createInstance(Ci.nsITransferable);
1174 xferable.init(null);
1175 let hasData = false;
1176 // This order matters here! It controls how this and other applications
1177 // select data to be inserted based on type.
1178 contents.forEach(function (content) {
1179 if (content.entries.length > 0) {
1180 hasData = true;
1181 let glue =
1182 content.type == PlacesUtils.TYPE_X_MOZ_PLACE ? "," : PlacesUtils.endl;
1183 addData(content.type, content.entries.join(glue));
1184 }
1185 });
1187 // Track the exected action in the xferable. This must be the last flavor
1188 // since it's the least preferred one.
1189 // Enqueue a unique instance identifier to distinguish operations across
1190 // concurrent instances of the application.
1191 addData(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION, aAction + "," + this.profileName);
1193 if (hasData) {
1194 this.clipboard.setData(xferable,
1195 this.cutNodes.length > 0 ? this : null,
1196 Ci.nsIClipboard.kGlobalClipboard);
1197 }
1198 },
1200 _cutNodes: [],
1201 get cutNodes() this._cutNodes,
1202 set cutNodes(aNodes) {
1203 let self = this;
1204 function updateCutNodes(aValue) {
1205 self._cutNodes.forEach(function (aNode) {
1206 self._view.toggleCutNode(aNode, aValue);
1207 });
1208 }
1210 updateCutNodes(false);
1211 this._cutNodes = aNodes;
1212 updateCutNodes(true);
1213 return aNodes;
1214 },
1216 /**
1217 * Copy Bookmarks and Folders to the clipboard
1218 */
1219 copy: function PC_copy() {
1220 let result = this._view.result;
1221 let didSuppressNotifications = result.suppressNotifications;
1222 if (!didSuppressNotifications)
1223 result.suppressNotifications = true;
1224 try {
1225 this._populateClipboard(this._view.selectedNodes, "copy");
1226 }
1227 finally {
1228 if (!didSuppressNotifications)
1229 result.suppressNotifications = false;
1230 }
1231 },
1233 /**
1234 * Cut Bookmarks and Folders to the clipboard
1235 */
1236 cut: function PC_cut() {
1237 let result = this._view.result;
1238 let didSuppressNotifications = result.suppressNotifications;
1239 if (!didSuppressNotifications)
1240 result.suppressNotifications = true;
1241 try {
1242 this._populateClipboard(this._view.selectedNodes, "cut");
1243 this.cutNodes = this._view.selectedNodes;
1244 }
1245 finally {
1246 if (!didSuppressNotifications)
1247 result.suppressNotifications = false;
1248 }
1249 },
1251 /**
1252 * Paste Bookmarks and Folders from the clipboard
1253 */
1254 paste: function PC_paste() {
1255 // No reason to proceed if there isn't a valid insertion point.
1256 let ip = this._view.insertionPoint;
1257 if (!ip)
1258 throw Cr.NS_ERROR_NOT_AVAILABLE;
1260 let action = this.clipboardAction;
1262 let xferable = Cc["@mozilla.org/widget/transferable;1"].
1263 createInstance(Ci.nsITransferable);
1264 xferable.init(null);
1265 // This order matters here! It controls the preferred flavors for this
1266 // paste operation.
1267 [ PlacesUtils.TYPE_X_MOZ_PLACE,
1268 PlacesUtils.TYPE_X_MOZ_URL,
1269 PlacesUtils.TYPE_UNICODE,
1270 ].forEach(function (type) xferable.addDataFlavor(type));
1272 this.clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
1274 // Now get the clipboard contents, in the best available flavor.
1275 let data = {}, type = {}, items = [];
1276 try {
1277 xferable.getAnyTransferData(type, data, {});
1278 data = data.value.QueryInterface(Ci.nsISupportsString).data;
1279 type = type.value;
1280 items = PlacesUtils.unwrapNodes(data, type);
1281 } catch(ex) {
1282 // No supported data exists or nodes unwrap failed, just bail out.
1283 return;
1284 }
1286 let transactions = [];
1287 let insertionIndex = ip.index;
1288 for (let i = 0; i < items.length; ++i) {
1289 if (ip.isTag) {
1290 // Pasting into a tag container means tagging the item, regardless of
1291 // the requested action.
1292 let tagTxn = new PlacesTagURITransaction(NetUtil.newURI(items[i].uri),
1293 [ip.itemId]);
1294 transactions.push(tagTxn);
1295 continue;
1296 }
1298 // Adjust index to make sure items are pasted in the correct position.
1299 // If index is DEFAULT_INDEX, items are just appended.
1300 if (ip.index != PlacesUtils.bookmarks.DEFAULT_INDEX)
1301 insertionIndex = ip.index + i;
1303 // If this is not a copy, check for safety that we can move the source,
1304 // otherwise report an error and fallback to a copy.
1305 if (action != "copy" && !PlacesControllerDragHelper.canMoveUnwrappedNode(items[i])) {
1306 Components.utils.reportError("Tried to move an unmovable Places node, " +
1307 "reverting to a copy operation.");
1308 action = "copy";
1309 }
1310 transactions.push(
1311 PlacesUIUtils.makeTransaction(items[i], type, ip.itemId,
1312 insertionIndex, action == "copy")
1313 );
1314 }
1316 let aggregatedTxn = new PlacesAggregatedTransaction("Paste", transactions);
1317 PlacesUtils.transactionManager.doTransaction(aggregatedTxn);
1319 // Cut/past operations are not repeatable, so clear the clipboard.
1320 if (action == "cut") {
1321 this._clearClipboard();
1322 }
1324 // Select the pasted items, they should be consecutive.
1325 let insertedNodeIds = [];
1326 for (let i = 0; i < transactions.length; ++i) {
1327 insertedNodeIds.push(
1328 PlacesUtils.bookmarks.getIdForItemAt(ip.itemId, ip.index + i)
1329 );
1330 }
1331 if (insertedNodeIds.length > 0)
1332 this._view.selectItems(insertedNodeIds, false);
1333 },
1335 /**
1336 * Cache the livemark info for a node. This allows the controller and the
1337 * views to treat the given node as a livemark.
1338 * @param aNode
1339 * a places result node.
1340 * @param aLivemarkInfo
1341 * a mozILivemarkInfo object.
1342 */
1343 cacheLivemarkInfo: function PC_cacheLivemarkInfo(aNode, aLivemarkInfo) {
1344 this._cachedLivemarkInfoObjects.set(aNode, aLivemarkInfo);
1345 },
1347 /**
1348 * Returns whether or not there's cached mozILivemarkInfo object for a node.
1349 * @param aNode
1350 * a places result node.
1351 * @return true if there's a cached mozILivemarkInfo object for
1352 * aNode, false otherwise.
1353 */
1354 hasCachedLivemarkInfo: function PC_hasCachedLivemarkInfo(aNode)
1355 this._cachedLivemarkInfoObjects.has(aNode),
1357 /**
1358 * Returns the cached livemark info for a node, if set by cacheLivemarkInfo,
1359 * null otherwise.
1360 * @param aNode
1361 * a places result node.
1362 * @return the mozILivemarkInfo object for aNode, if set, null otherwise.
1363 */
1364 getCachedLivemarkInfo: function PC_getCachedLivemarkInfo(aNode)
1365 this._cachedLivemarkInfoObjects.get(aNode, null)
1366 };
1368 /**
1369 * Handles drag and drop operations for views. Note that this is view agnostic!
1370 * You should not use PlacesController._view within these methods, since
1371 * the view that the item(s) have been dropped on was not necessarily active.
1372 * Drop functions are passed the view that is being dropped on.
1373 */
1374 let PlacesControllerDragHelper = {
1375 /**
1376 * DOM Element currently being dragged over
1377 */
1378 currentDropTarget: null,
1380 /**
1381 * Determines if the mouse is currently being dragged over a child node of
1382 * this menu. This is necessary so that the menu doesn't close while the
1383 * mouse is dragging over one of its submenus
1384 * @param node
1385 * The container node
1386 * @return true if the user is dragging over a node within the hierarchy of
1387 * the container, false otherwise.
1388 */
1389 draggingOverChildNode: function PCDH_draggingOverChildNode(node) {
1390 let currentNode = this.currentDropTarget;
1391 while (currentNode) {
1392 if (currentNode == node)
1393 return true;
1394 currentNode = currentNode.parentNode;
1395 }
1396 return false;
1397 },
1399 /**
1400 * @return The current active drag session. Returns null if there is none.
1401 */
1402 getSession: function PCDH__getSession() {
1403 return this.dragService.getCurrentSession();
1404 },
1406 /**
1407 * Extract the first accepted flavor from a list of flavors.
1408 * @param aFlavors
1409 * The flavors list of type DOMStringList.
1410 */
1411 getFirstValidFlavor: function PCDH_getFirstValidFlavor(aFlavors) {
1412 for (let i = 0; i < aFlavors.length; i++) {
1413 if (this.GENERIC_VIEW_DROP_TYPES.indexOf(aFlavors[i]) != -1)
1414 return aFlavors[i];
1415 }
1417 // If no supported flavor is found, check if data includes text/plain
1418 // contents. If so, request them as text/unicode, a conversion will happen
1419 // automatically.
1420 if (aFlavors.contains("text/plain")) {
1421 return PlacesUtils.TYPE_UNICODE;
1422 }
1424 return null;
1425 },
1427 /**
1428 * Determines whether or not the data currently being dragged can be dropped
1429 * on a places view.
1430 * @param ip
1431 * The insertion point where the items should be dropped.
1432 */
1433 canDrop: function PCDH_canDrop(ip, dt) {
1434 let dropCount = dt.mozItemCount;
1436 // Check every dragged item.
1437 for (let i = 0; i < dropCount; i++) {
1438 let flavor = this.getFirstValidFlavor(dt.mozTypesAt(i));
1439 if (!flavor)
1440 return false;
1442 // Urls can be dropped on any insertionpoint.
1443 // XXXmano: remember that this method is called for each dragover event!
1444 // Thus we shouldn't use unwrapNodes here at all if possible.
1445 // I think it would be OK to accept bogus data here (e.g. text which was
1446 // somehow wrapped as TAB_DROP_TYPE, this is not in our control, and
1447 // will just case the actual drop to be a no-op), and only rule out valid
1448 // expected cases, which are either unsupported flavors, or items which
1449 // cannot be dropped in the current insertionpoint. The last case will
1450 // likely force us to use unwrapNodes for the private data types of
1451 // places.
1452 if (flavor == TAB_DROP_TYPE)
1453 continue;
1455 let data = dt.mozGetDataAt(flavor, i);
1456 let dragged;
1457 try {
1458 dragged = PlacesUtils.unwrapNodes(data, flavor)[0];
1459 }
1460 catch (e) {
1461 return false;
1462 }
1464 // Only bookmarks and urls can be dropped into tag containers.
1465 if (ip.isTag && ip.orientation == Ci.nsITreeView.DROP_ON &&
1466 dragged.type != PlacesUtils.TYPE_X_MOZ_URL &&
1467 (dragged.type != PlacesUtils.TYPE_X_MOZ_PLACE ||
1468 (dragged.uri && dragged.uri.startsWith("place:")) ))
1469 return false;
1471 // The following loop disallows the dropping of a folder on itself or
1472 // on any of its descendants.
1473 if (dragged.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER ||
1474 (dragged.uri && dragged.uri.startsWith("place:")) ) {
1475 let parentId = ip.itemId;
1476 while (parentId != PlacesUtils.placesRootId) {
1477 if (dragged.concreteId == parentId || dragged.id == parentId)
1478 return false;
1479 parentId = PlacesUtils.bookmarks.getFolderIdForItem(parentId);
1480 }
1481 }
1482 }
1483 return true;
1484 },
1486 /**
1487 * Determines if an unwrapped node can be moved.
1488 *
1489 * @param aUnwrappedNode
1490 * A node unwrapped by PlacesUtils.unwrapNodes().
1491 * @return True if the node can be moved, false otherwise.
1492 */
1493 canMoveUnwrappedNode: function (aUnwrappedNode) {
1494 return aUnwrappedNode.id > 0 &&
1495 !PlacesUtils.isRootItem(aUnwrappedNode.id) &&
1496 aUnwrappedNode.parent != PlacesUtils.placesRootId &&
1497 aUnwrappedNode.parent != PlacesUtils.tagsFolderId &&
1498 aUnwrappedNode.grandParentId != PlacesUtils.tagsFolderId &&
1499 !aUnwrappedNode.parentReadOnly;
1500 },
1502 /**
1503 * Determines if a node can be moved.
1504 *
1505 * @param aNode
1506 * A nsINavHistoryResultNode node.
1507 * @return True if the node can be moved, false otherwise.
1508 */
1509 canMoveNode:
1510 function PCDH_canMoveNode(aNode) {
1511 // Can't move query root.
1512 if (!aNode.parent)
1513 return false;
1515 let parentId = PlacesUtils.getConcreteItemId(aNode.parent);
1516 let concreteId = PlacesUtils.getConcreteItemId(aNode);
1518 // Can't move children of tag containers.
1519 if (PlacesUtils.nodeIsTagQuery(aNode.parent))
1520 return false;
1522 // Can't move children of read-only containers.
1523 if (PlacesUtils.nodeIsReadOnly(aNode.parent))
1524 return false;
1526 // Check for special folders, etc.
1527 if (PlacesUtils.nodeIsContainer(aNode) &&
1528 !this.canMoveContainer(aNode.itemId, parentId))
1529 return false;
1531 return true;
1532 },
1534 /**
1535 * Determines if a container node can be moved.
1536 *
1537 * @param aId
1538 * A bookmark folder id.
1539 * @param [optional] aParentId
1540 * The parent id of the folder.
1541 * @return True if the container can be moved to the target.
1542 */
1543 canMoveContainer:
1544 function PCDH_canMoveContainer(aId, aParentId) {
1545 if (aId == -1)
1546 return false;
1548 // Disallow moving of roots and special folders.
1549 const ROOTS = [PlacesUtils.placesRootId, PlacesUtils.bookmarksMenuFolderId,
1550 PlacesUtils.tagsFolderId, PlacesUtils.unfiledBookmarksFolderId,
1551 PlacesUtils.toolbarFolderId];
1552 if (ROOTS.indexOf(aId) != -1)
1553 return false;
1555 // Get parent id if necessary.
1556 if (aParentId == null || aParentId == -1)
1557 aParentId = PlacesUtils.bookmarks.getFolderIdForItem(aId);
1559 if (PlacesUtils.bookmarks.getFolderReadonly(aParentId))
1560 return false;
1562 return true;
1563 },
1565 /**
1566 * Handles the drop of one or more items onto a view.
1567 * @param insertionPoint
1568 * The insertion point where the items should be dropped
1569 */
1570 onDrop: function PCDH_onDrop(insertionPoint, dt) {
1571 let doCopy = ["copy", "link"].indexOf(dt.dropEffect) != -1;
1573 let transactions = [];
1574 let dropCount = dt.mozItemCount;
1575 let movedCount = 0;
1576 for (let i = 0; i < dropCount; ++i) {
1577 let flavor = this.getFirstValidFlavor(dt.mozTypesAt(i));
1578 if (!flavor)
1579 return;
1581 let data = dt.mozGetDataAt(flavor, i);
1582 let unwrapped;
1583 if (flavor != TAB_DROP_TYPE) {
1584 // There's only ever one in the D&D case.
1585 unwrapped = PlacesUtils.unwrapNodes(data, flavor)[0];
1586 }
1587 else if (data instanceof XULElement && data.localName == "tab" &&
1588 data.ownerDocument.defaultView instanceof ChromeWindow) {
1589 let uri = data.linkedBrowser.currentURI;
1590 let spec = uri ? uri.spec : "about:blank";
1591 let title = data.label;
1592 unwrapped = { uri: spec,
1593 title: data.label,
1594 type: PlacesUtils.TYPE_X_MOZ_URL};
1595 }
1596 else
1597 throw("bogus data was passed as a tab")
1599 let index = insertionPoint.index;
1601 // Adjust insertion index to prevent reversal of dragged items. When you
1602 // drag multiple elts upward: need to increment index or each successive
1603 // elt will be inserted at the same index, each above the previous.
1604 let dragginUp = insertionPoint.itemId == unwrapped.parent &&
1605 index < PlacesUtils.bookmarks.getItemIndex(unwrapped.id);
1606 if (index != -1 && dragginUp)
1607 index+= movedCount++;
1609 // If dragging over a tag container we should tag the item.
1610 if (insertionPoint.isTag &&
1611 insertionPoint.orientation == Ci.nsITreeView.DROP_ON) {
1612 let uri = NetUtil.newURI(unwrapped.uri);
1613 let tagItemId = insertionPoint.itemId;
1614 let tagTxn = new PlacesTagURITransaction(uri, [tagItemId]);
1615 transactions.push(tagTxn);
1616 }
1617 else {
1618 // If this is not a copy, check for safety that we can move the source,
1619 // otherwise report an error and fallback to a copy.
1620 if (!doCopy && !PlacesControllerDragHelper.canMoveUnwrappedNode(unwrapped)) {
1621 Components.utils.reportError("Tried to move an unmovable Places node, " +
1622 "reverting to a copy operation.");
1623 doCopy = true;
1624 }
1625 transactions.push(PlacesUIUtils.makeTransaction(unwrapped,
1626 flavor, insertionPoint.itemId,
1627 index, doCopy));
1628 }
1629 }
1631 let txn = new PlacesAggregatedTransaction("DropItems", transactions);
1632 PlacesUtils.transactionManager.doTransaction(txn);
1633 },
1635 /**
1636 * Checks if we can insert into a container.
1637 * @param aContainer
1638 * The container were we are want to drop
1639 */
1640 disallowInsertion: function(aContainer) {
1641 NS_ASSERT(aContainer, "empty container");
1642 // Allow dropping into Tag containers.
1643 if (PlacesUtils.nodeIsTagQuery(aContainer))
1644 return false;
1645 // Disallow insertion of items under readonly folders.
1646 return (!PlacesUtils.nodeIsFolder(aContainer) ||
1647 PlacesUtils.nodeIsReadOnly(aContainer));
1648 },
1650 placesFlavors: [PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER,
1651 PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR,
1652 PlacesUtils.TYPE_X_MOZ_PLACE],
1654 // The order matters.
1655 GENERIC_VIEW_DROP_TYPES: [PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER,
1656 PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR,
1657 PlacesUtils.TYPE_X_MOZ_PLACE,
1658 PlacesUtils.TYPE_X_MOZ_URL,
1659 TAB_DROP_TYPE,
1660 PlacesUtils.TYPE_UNICODE],
1661 };
1664 XPCOMUtils.defineLazyServiceGetter(PlacesControllerDragHelper, "dragService",
1665 "@mozilla.org/widget/dragservice;1",
1666 "nsIDragService");
1668 function goUpdatePlacesCommands() {
1669 // Get the controller for one of the places commands.
1670 var placesController = doGetPlacesControllerForCommand("placesCmd_open");
1671 function updatePlacesCommand(aCommand) {
1672 goSetCommandEnabled(aCommand, placesController &&
1673 placesController.isCommandEnabled(aCommand));
1674 }
1676 updatePlacesCommand("placesCmd_open");
1677 updatePlacesCommand("placesCmd_open:window");
1678 updatePlacesCommand("placesCmd_open:tab");
1679 updatePlacesCommand("placesCmd_new:folder");
1680 updatePlacesCommand("placesCmd_new:bookmark");
1681 updatePlacesCommand("placesCmd_new:separator");
1682 updatePlacesCommand("placesCmd_show:info");
1683 updatePlacesCommand("placesCmd_moveBookmarks");
1684 updatePlacesCommand("placesCmd_reload");
1685 updatePlacesCommand("placesCmd_sortBy:name");
1686 updatePlacesCommand("placesCmd_cut");
1687 updatePlacesCommand("placesCmd_copy");
1688 updatePlacesCommand("placesCmd_paste");
1689 updatePlacesCommand("placesCmd_delete");
1690 }
1692 function doGetPlacesControllerForCommand(aCommand)
1693 {
1694 // A context menu may be built for non-focusable views. Thus, we first try
1695 // to look for a view associated with document.popupNode
1696 let popupNode;
1697 try {
1698 popupNode = document.popupNode;
1699 } catch (e) {
1700 // The document went away (bug 797307).
1701 return null;
1702 }
1703 if (popupNode) {
1704 let view = PlacesUIUtils.getViewForNode(popupNode);
1705 if (view && view._contextMenuShown)
1706 return view.controllers.getControllerForCommand(aCommand);
1707 }
1709 // When we're not building a context menu, only focusable views
1710 // are possible. Thus, we can safely use the command dispatcher.
1711 let controller = top.document.commandDispatcher
1712 .getControllerForCommand(aCommand);
1713 if (controller)
1714 return controller;
1716 return null;
1717 }
1719 function goDoPlacesCommand(aCommand)
1720 {
1721 let controller = doGetPlacesControllerForCommand(aCommand);
1722 if (controller && controller.isCommandEnabled(aCommand))
1723 controller.doCommand(aCommand);
1724 }