controller.js 66 KB

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