|
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/. */ |
|
5 |
|
6 XPCOMUtils.defineLazyModuleGetter(this, "ForgetAboutSite", |
|
7 "resource://gre/modules/ForgetAboutSite.jsm"); |
|
8 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", |
|
9 "resource://gre/modules/NetUtil.jsm"); |
|
10 |
|
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"; |
|
13 |
|
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; |
|
23 |
|
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; |
|
27 |
|
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 } |
|
54 |
|
55 InsertionPoint.prototype = { |
|
56 set index(val) { |
|
57 return this._index = val; |
|
58 }, |
|
59 |
|
60 promiseGUID: function () PlacesUtils.promiseItemGUID(this.itemId), |
|
61 |
|
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 }; |
|
72 |
|
73 /** |
|
74 * Places Controller |
|
75 */ |
|
76 |
|
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 }); |
|
85 |
|
86 this._cachedLivemarkInfoObjects = new Map(); |
|
87 } |
|
88 |
|
89 PlacesController.prototype = { |
|
90 /** |
|
91 * The places view. |
|
92 */ |
|
93 _view: null, |
|
94 |
|
95 QueryInterface: XPCOMUtils.generateQI([ |
|
96 Ci.nsIClipboardOwner |
|
97 ]), |
|
98 |
|
99 // nsIClipboardOwner |
|
100 LosingOwnership: function PC_LosingOwnership (aXferable) { |
|
101 this.cutNodes = []; |
|
102 }, |
|
103 |
|
104 terminate: function PC_terminate() { |
|
105 this._releaseClipboardOwnership(); |
|
106 }, |
|
107 |
|
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 } |
|
120 |
|
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 }, |
|
126 |
|
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 } |
|
144 |
|
145 switch (aCommand) { |
|
146 case "cmd_undo": |
|
147 if (!PlacesUIUtils.useAsyncTransactions) |
|
148 return PlacesUtils.transactionManager.numberOfUndoItems > 0; |
|
149 |
|
150 return PlacesTransactions.topUndoEntry != null; |
|
151 case "cmd_redo": |
|
152 if (!PlacesUIUtils.useAsyncTransactions) |
|
153 return PlacesUtils.transactionManager.numberOfRedoItems > 0; |
|
154 |
|
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 }, |
|
219 |
|
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 }, |
|
309 |
|
310 onEvent: function PC_onEvent(eventName) { }, |
|
311 |
|
312 |
|
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; |
|
329 |
|
330 var root = this._view.result.root; |
|
331 |
|
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; |
|
338 |
|
339 if (PlacesUtils.nodeIsFolder(nodes[i]) && |
|
340 !PlacesControllerDragHelper.canMoveNode(nodes[i])) |
|
341 return false; |
|
342 |
|
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 } |
|
357 |
|
358 return true; |
|
359 }, |
|
360 |
|
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 }, |
|
368 |
|
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 } |
|
379 |
|
380 return false; |
|
381 }, |
|
382 |
|
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. |
|
394 |
|
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; |
|
402 |
|
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); |
|
408 |
|
409 xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_URL); |
|
410 xferable.addDataFlavor(PlacesUtils.TYPE_UNICODE); |
|
411 clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard); |
|
412 |
|
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; |
|
421 |
|
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 }, |
|
431 |
|
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 |
|
459 |
|
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; |
|
465 |
|
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) |
|
496 |
|
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 } |
|
507 |
|
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 } |
|
514 |
|
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 } |
|
524 |
|
525 return metadata; |
|
526 }, |
|
527 |
|
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; |
|
543 |
|
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 } |
|
554 |
|
555 var selectionAttr = aMenuItem.getAttribute("selection"); |
|
556 if (!selectionAttr) { |
|
557 return !aMenuItem.hidden; |
|
558 } |
|
559 |
|
560 if (selectionAttr == "any") |
|
561 return true; |
|
562 |
|
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 } |
|
572 |
|
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 }, |
|
581 |
|
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; |
|
619 |
|
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); |
|
630 |
|
631 if (!item.hidden) { |
|
632 visibleItemsBeforeSep = true; |
|
633 anyVisible = true; |
|
634 |
|
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; |
|
646 |
|
647 // We won't show the separator at all if no items are visible above it |
|
648 if (visibleItemsBeforeSep) |
|
649 separator = item; |
|
650 |
|
651 // New separator, count again: |
|
652 visibleItemsBeforeSep = false; |
|
653 } |
|
654 } |
|
655 |
|
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 } |
|
670 |
|
671 return anyVisible; |
|
672 }, |
|
673 |
|
674 /** |
|
675 * Select all links in the current view. |
|
676 */ |
|
677 selectAll: function PC_selectAll() { |
|
678 this._view.selectAll(); |
|
679 }, |
|
680 |
|
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; |
|
689 |
|
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 } |
|
700 |
|
701 PlacesUIUtils.showBookmarkDialog({ action: "edit" |
|
702 , type: itemType |
|
703 , itemId: itemId |
|
704 , readOnly: isRootItem |
|
705 , hiddenRows: [ "folderPicker" ] |
|
706 }, window.top); |
|
707 }, |
|
708 |
|
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 }, |
|
717 |
|
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 }, |
|
731 |
|
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 }, |
|
742 |
|
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; |
|
753 |
|
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 }, |
|
767 |
|
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; |
|
775 |
|
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 } |
|
785 |
|
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 }), |
|
793 |
|
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 }, |
|
802 |
|
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 }), |
|
816 |
|
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 } |
|
845 |
|
846 for (var j = 0; j < pastFolders.length; ++j) { |
|
847 if (isContainedBy(node, pastFolders[j])) |
|
848 return true; |
|
849 } |
|
850 return false; |
|
851 }, |
|
852 |
|
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 = []; |
|
867 |
|
868 for (var i = 0; i < range.length; ++i) { |
|
869 var node = range[i]; |
|
870 if (this._shouldSkipNode(node, removedFolders)) |
|
871 continue; |
|
872 |
|
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 }, |
|
926 |
|
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 = []; |
|
936 |
|
937 for (var i = 0; i < ranges.length; i++) |
|
938 this._removeRange(ranges[i], transactions, removedFolders); |
|
939 |
|
940 if (transactions.length > 0) { |
|
941 var txn = new PlacesAggregatedTransaction(txnName, transactions); |
|
942 PlacesUtils.transactionManager.doTransaction(txn); |
|
943 } |
|
944 }, |
|
945 |
|
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 } |
|
969 |
|
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 }, |
|
986 |
|
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 }, |
|
1013 |
|
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; |
|
1023 |
|
1024 NS_ASSERT(aTxnName !== undefined, "Must supply Transaction Name"); |
|
1025 |
|
1026 var root = this._view.result.root; |
|
1027 |
|
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 }, |
|
1042 |
|
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; |
|
1051 |
|
1052 let result = this._view.result; |
|
1053 let didSuppressNotifications = result.suppressNotifications; |
|
1054 if (!didSuppressNotifications) |
|
1055 result.suppressNotifications = true; |
|
1056 |
|
1057 function addData(type, index, overrideURI) { |
|
1058 let wrapNode = PlacesUtils.wrapNode(node, type, overrideURI); |
|
1059 dt.mozSetDataAt(type, wrapNode, index); |
|
1060 } |
|
1061 |
|
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 } |
|
1067 |
|
1068 try { |
|
1069 let nodes = this._view.draggableSelection; |
|
1070 for (let i = 0; i < nodes.length; ++i) { |
|
1071 var node = nodes[i]; |
|
1072 |
|
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); |
|
1076 |
|
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 }, |
|
1092 |
|
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"; |
|
1115 |
|
1116 return action; |
|
1117 }, |
|
1118 |
|
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 }, |
|
1125 |
|
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 }, |
|
1136 |
|
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 ]; |
|
1146 |
|
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); |
|
1155 |
|
1156 let livemarkInfo = this.getCachedLivemarkInfo(node); |
|
1157 let overrideURI = livemarkInfo ? livemarkInfo.feedURI.spec : null; |
|
1158 |
|
1159 contents.forEach(function (content) { |
|
1160 content.entries.push( |
|
1161 PlacesUtils.wrapNode(node, content.type, overrideURI) |
|
1162 ); |
|
1163 }); |
|
1164 }, this); |
|
1165 |
|
1166 function addData(type, data) { |
|
1167 xferable.addDataFlavor(type); |
|
1168 xferable.setTransferData(type, PlacesUtils.toISupportsString(data), |
|
1169 data.length * 2); |
|
1170 } |
|
1171 |
|
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 }); |
|
1186 |
|
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); |
|
1192 |
|
1193 if (hasData) { |
|
1194 this.clipboard.setData(xferable, |
|
1195 this.cutNodes.length > 0 ? this : null, |
|
1196 Ci.nsIClipboard.kGlobalClipboard); |
|
1197 } |
|
1198 }, |
|
1199 |
|
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 } |
|
1209 |
|
1210 updateCutNodes(false); |
|
1211 this._cutNodes = aNodes; |
|
1212 updateCutNodes(true); |
|
1213 return aNodes; |
|
1214 }, |
|
1215 |
|
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 }, |
|
1232 |
|
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 }, |
|
1250 |
|
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; |
|
1259 |
|
1260 let action = this.clipboardAction; |
|
1261 |
|
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)); |
|
1271 |
|
1272 this.clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard); |
|
1273 |
|
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 } |
|
1285 |
|
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 } |
|
1297 |
|
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; |
|
1302 |
|
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 } |
|
1315 |
|
1316 let aggregatedTxn = new PlacesAggregatedTransaction("Paste", transactions); |
|
1317 PlacesUtils.transactionManager.doTransaction(aggregatedTxn); |
|
1318 |
|
1319 // Cut/past operations are not repeatable, so clear the clipboard. |
|
1320 if (action == "cut") { |
|
1321 this._clearClipboard(); |
|
1322 } |
|
1323 |
|
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 }, |
|
1334 |
|
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 }, |
|
1346 |
|
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), |
|
1356 |
|
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 }; |
|
1367 |
|
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, |
|
1379 |
|
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 }, |
|
1398 |
|
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 }, |
|
1405 |
|
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 } |
|
1416 |
|
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 } |
|
1423 |
|
1424 return null; |
|
1425 }, |
|
1426 |
|
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; |
|
1435 |
|
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; |
|
1441 |
|
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; |
|
1454 |
|
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 } |
|
1463 |
|
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; |
|
1470 |
|
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 }, |
|
1485 |
|
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 }, |
|
1501 |
|
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; |
|
1514 |
|
1515 let parentId = PlacesUtils.getConcreteItemId(aNode.parent); |
|
1516 let concreteId = PlacesUtils.getConcreteItemId(aNode); |
|
1517 |
|
1518 // Can't move children of tag containers. |
|
1519 if (PlacesUtils.nodeIsTagQuery(aNode.parent)) |
|
1520 return false; |
|
1521 |
|
1522 // Can't move children of read-only containers. |
|
1523 if (PlacesUtils.nodeIsReadOnly(aNode.parent)) |
|
1524 return false; |
|
1525 |
|
1526 // Check for special folders, etc. |
|
1527 if (PlacesUtils.nodeIsContainer(aNode) && |
|
1528 !this.canMoveContainer(aNode.itemId, parentId)) |
|
1529 return false; |
|
1530 |
|
1531 return true; |
|
1532 }, |
|
1533 |
|
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; |
|
1547 |
|
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; |
|
1554 |
|
1555 // Get parent id if necessary. |
|
1556 if (aParentId == null || aParentId == -1) |
|
1557 aParentId = PlacesUtils.bookmarks.getFolderIdForItem(aId); |
|
1558 |
|
1559 if (PlacesUtils.bookmarks.getFolderReadonly(aParentId)) |
|
1560 return false; |
|
1561 |
|
1562 return true; |
|
1563 }, |
|
1564 |
|
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; |
|
1572 |
|
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; |
|
1580 |
|
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") |
|
1598 |
|
1599 let index = insertionPoint.index; |
|
1600 |
|
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++; |
|
1608 |
|
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 } |
|
1630 |
|
1631 let txn = new PlacesAggregatedTransaction("DropItems", transactions); |
|
1632 PlacesUtils.transactionManager.doTransaction(txn); |
|
1633 }, |
|
1634 |
|
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 }, |
|
1649 |
|
1650 placesFlavors: [PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER, |
|
1651 PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR, |
|
1652 PlacesUtils.TYPE_X_MOZ_PLACE], |
|
1653 |
|
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 }; |
|
1662 |
|
1663 |
|
1664 XPCOMUtils.defineLazyServiceGetter(PlacesControllerDragHelper, "dragService", |
|
1665 "@mozilla.org/widget/dragservice;1", |
|
1666 "nsIDragService"); |
|
1667 |
|
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 } |
|
1675 |
|
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 } |
|
1691 |
|
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 } |
|
1708 |
|
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; |
|
1715 |
|
1716 return null; |
|
1717 } |
|
1718 |
|
1719 function goDoPlacesCommand(aCommand) |
|
1720 { |
|
1721 let controller = doGetPlacesControllerForCommand(aCommand); |
|
1722 if (controller && controller.isCommandEnabled(aCommand)) |
|
1723 controller.doCommand(aCommand); |
|
1724 } |
|
1725 |