123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896 |
- /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
- /* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
- Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
- Components.utils.import("resource://gre/modules/ForgetAboutSite.jsm");
- XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
- "resource://gre/modules/NetUtil.jsm");
- XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
- "resource://gre/modules/PrivateBrowsingUtils.jsm");
- // XXXmano: we should move most/all of these constants to PlacesUtils
- const ORGANIZER_ROOT_BOOKMARKS = "place:folder=BOOKMARKS_MENU&excludeItems=1&queryType=1";
- // No change to the view, preserve current selection
- const RELOAD_ACTION_NOTHING = 0;
- // Inserting items new to the view, select the inserted rows
- const RELOAD_ACTION_INSERT = 1;
- // Removing items from the view, select the first item after the last selected
- const RELOAD_ACTION_REMOVE = 2;
- // Moving items within a view, don't treat the dropped items as additional
- // rows.
- const RELOAD_ACTION_MOVE = 3;
- // When removing a bunch of pages we split them in chunks to give some breath
- // to the main-thread.
- const REMOVE_PAGES_CHUNKLEN = 300;
- /**
- * Represents an insertion point within a container where we can insert
- * items.
- * @param aItemId
- * The identifier of the parent container
- * @param aIndex
- * The index within the container where we should insert
- * @param aOrientation
- * The orientation of the insertion. NOTE: the adjustments to the
- * insertion point to accommodate the orientation should be done by
- * the person who constructs the IP, not the user. The orientation
- * is provided for informational purposes only!
- * @param [optional] aIsTag
- * Indicates if parent container is a tag
- * @param [optional] aDropNearItemId
- * When defined we will calculate index based on this itemId
- * @constructor
- */
- function InsertionPoint(aItemId, aIndex, aOrientation, aIsTag,
- aDropNearItemId) {
- this.itemId = aItemId;
- this._index = aIndex;
- this.orientation = aOrientation;
- this.isTag = aIsTag;
- this.dropNearItemId = aDropNearItemId;
- }
- InsertionPoint.prototype = {
- set index(val) {
- return this._index = val;
- },
- get index() {
- if (this.dropNearItemId > 0) {
- // If dropNearItemId is set up we must calculate the real index of
- // the item near which we will drop.
- var index = PlacesUtils.bookmarks.getItemIndex(this.dropNearItemId);
- return this.orientation == Ci.nsITreeView.DROP_BEFORE ? index : index + 1;
- }
- return this._index;
- }
- };
- /**
- * Places Controller
- */
- function PlacesController(aView) {
- this._view = aView;
- XPCOMUtils.defineLazyServiceGetter(this, "clipboard",
- "@mozilla.org/widget/clipboard;1",
- "nsIClipboard");
- XPCOMUtils.defineLazyGetter(this, "profileName", function() {
- return Services.dirsvc.get("ProfD", Ci.nsIFile).leafName;
- });
- this._cachedLivemarkInfoObjects = new Map();
- }
- PlacesController.prototype = {
- /**
- * The places view.
- */
- _view: null,
- QueryInterface: XPCOMUtils.generateQI([
- Ci.nsIClipboardOwner
- ]),
- // nsIClipboardOwner
- LosingOwnership: function(aXferable) {
- this.cutNodes = [];
- },
- terminate: function() {
- this._releaseClipboardOwnership();
- },
- supportsCommand: function(aCommand) {
- // Non-Places specific commands that we also support
- switch (aCommand) {
- case "cmd_undo":
- case "cmd_redo":
- case "cmd_cut":
- case "cmd_copy":
- case "cmd_paste":
- case "cmd_delete":
- case "cmd_selectAll":
- return true;
- }
- // All other Places Commands are prefixed with "placesCmd_" ... this
- // filters out other commands that we do _not_ support (see 329587).
- const CMD_PREFIX = "placesCmd_";
- return (aCommand.substr(0, CMD_PREFIX.length) == CMD_PREFIX);
- },
- isCommandEnabled: function(aCommand) {
- switch (aCommand) {
- case "cmd_undo":
- return PlacesUtils.transactionManager.numberOfUndoItems > 0;
- case "cmd_redo":
- return PlacesUtils.transactionManager.numberOfRedoItems > 0;
- case "cmd_cut":
- case "placesCmd_cut":
- case "placesCmd_moveBookmarks":
- for (let node of this._view.selectedNodes) {
- // If selection includes history nodes or tags-as-bookmark, disallow
- // cutting.
- if (node.itemId == -1 ||
- (node.parent && PlacesUtils.nodeIsTagQuery(node.parent))) {
- return false;
- }
- }
- // Otherwise fall through to the cmd_delete check.
- case "cmd_delete":
- case "placesCmd_delete":
- case "placesCmd_deleteDataHost":
- return this._hasRemovableSelection();
- case "cmd_copy":
- case "placesCmd_copy":
- return this._view.hasSelection;
- case "cmd_paste":
- case "placesCmd_paste":
- return this._canInsert(true) && this._isClipboardDataPasteable();
- case "cmd_selectAll":
- if (this._view.selType != "single") {
- let rootNode = this._view.result.root;
- if (rootNode.containerOpen && rootNode.childCount > 0)
- return true;
- }
- return false;
- case "placesCmd_open":
- case "placesCmd_open:window":
- case "placesCmd_open:privatewindow":
- case "placesCmd_open:tab":
- var selectedNode = this._view.selectedNode;
- return selectedNode && PlacesUtils.nodeIsURI(selectedNode);
- case "placesCmd_new:folder":
- case "placesCmd_new:livemark":
- return this._canInsert();
- case "placesCmd_new:bookmark":
- return this._canInsert();
- case "placesCmd_new:separator":
- return this._canInsert() &&
- !PlacesUtils.asQuery(this._view.result.root).queryOptions.excludeItems &&
- this._view.result.sortingMode ==
- Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
- case "placesCmd_show:info":
- var selectedNode = this._view.selectedNode;
- return selectedNode && PlacesUtils.getConcreteItemId(selectedNode) != -1
- case "placesCmd_reload":
- // Livemark containers
- var selectedNode = this._view.selectedNode;
- return selectedNode && this.hasCachedLivemarkInfo(selectedNode);
- case "placesCmd_sortBy:name":
- var selectedNode = this._view.selectedNode;
- return selectedNode &&
- PlacesUtils.nodeIsFolder(selectedNode) &&
- !PlacesUIUtils.isContentsReadOnly(selectedNode) &&
- this._view.result.sortingMode ==
- Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
- case "placesCmd_createBookmark":
- var node = this._view.selectedNode;
- return node && PlacesUtils.nodeIsURI(node) && node.itemId == -1;
- case "placesCmd_openParentFolder":
- return true;
- default:
- return false;
- }
- },
- doCommand: function(aCommand) {
- switch (aCommand) {
- case "cmd_undo":
- PlacesUtils.transactionManager.undoTransaction();
- break;
- case "cmd_redo":
- PlacesUtils.transactionManager.redoTransaction();
- break;
- case "cmd_cut":
- case "placesCmd_cut":
- this.cut();
- break;
- case "cmd_copy":
- case "placesCmd_copy":
- this.copy();
- break;
- case "cmd_paste":
- case "placesCmd_paste":
- this.paste();
- break;
- case "cmd_delete":
- case "placesCmd_delete":
- this.remove("Remove Selection");
- break;
- case "placesCmd_deleteDataHost":
- var host;
- if (PlacesUtils.nodeIsHost(this._view.selectedNode)) {
- var queries = this._view.selectedNode.getQueries();
- host = queries[0].domain;
- }
- else
- host = NetUtil.newURI(this._view.selectedNode.uri).host;
- ForgetAboutSite.removeDataFromDomain(host)
- .catch(Components.utils.reportError);
- break;
- case "cmd_selectAll":
- this.selectAll();
- break;
- case "placesCmd_open":
- PlacesUIUtils.openNodeIn(this._view.selectedNode, "current", this._view);
- break;
- case "placesCmd_open:window":
- PlacesUIUtils.openNodeIn(this._view.selectedNode, "window", this._view);
- break;
- case "placesCmd_open:privatewindow":
- PlacesUIUtils.openNodeIn(this._view.selectedNode, "window", this._view, true);
- break;
- case "placesCmd_open:tab":
- PlacesUIUtils.openNodeIn(this._view.selectedNode, "tab", this._view);
- break;
- case "placesCmd_new:folder":
- this.newItem("folder");
- break;
- case "placesCmd_new:bookmark":
- this.newItem("bookmark");
- break;
- case "placesCmd_new:livemark":
- this.newItem("livemark");
- break;
- case "placesCmd_new:separator":
- this.newSeparator();
- break;
- case "placesCmd_show:info":
- this.showBookmarkPropertiesForSelection();
- break;
- case "placesCmd_moveBookmarks":
- this.moveSelectedBookmarks();
- break;
- case "placesCmd_reload":
- this.reloadSelectedLivemark();
- break;
- case "placesCmd_sortBy:name":
- this.sortFolderByName();
- break;
- case "placesCmd_createBookmark":
- let node = this._view.selectedNode;
- PlacesUIUtils.showBookmarkDialog({ action: "add"
- , type: "bookmark"
- , hiddenRows: [ "description"
- , "keyword"
- , "location"
- , "loadInSidebar" ]
- , uri: NetUtil.newURI(node.uri)
- , title: node.title
- }, window.top);
- break;
- case "placesCmd_openParentFolder":
- this.openParentFolder();
- break;
- }
- },
- onEvent: function(eventName) { },
- /**
- * Determine whether or not the selection can be removed, either by the
- * delete or cut operations based on whether or not any of its contents
- * are non-removable. We don't need to worry about recursion here since it
- * is a policy decision that a removable item not be placed inside a non-
- * removable item.
- * @returns true if all nodes in the selection can be removed,
- * false otherwise.
- */
- _hasRemovableSelection() {
- var ranges = this._view.removableSelectionRanges;
- if (!ranges.length)
- return false;
- var root = this._view.result.root;
- for (var j = 0; j < ranges.length; j++) {
- var nodes = ranges[j];
- for (var i = 0; i < nodes.length; ++i) {
- // Disallow removing the view's root node
- if (nodes[i] == root)
- return false;
- if (!PlacesUIUtils.canUserRemove(nodes[i]))
- return false;
- }
- }
- return true;
- },
- /**
- * Determines whether or not nodes can be inserted relative to the selection.
- */
- _canInsert: function(isPaste) {
- var ip = this._view.insertionPoint;
- return ip != null && (isPaste || ip.isTag != true);
- },
- /**
- * Looks at the data on the clipboard to see if it is paste-able.
- * Paste-able data is:
- * - in a format that the view can receive
- * @returns true if: - clipboard data is of a TYPE_X_MOZ_PLACE_* flavor,
- - clipboard data is of type TEXT_UNICODE and
- is a valid URI.
- */
- _isClipboardDataPasteable: function() {
- // if the clipboard contains TYPE_X_MOZ_PLACE_* data, it is definitely
- // pasteable, with no need to unwrap all the nodes.
- var flavors = PlacesControllerDragHelper.placesFlavors;
- var clipboard = this.clipboard;
- var hasPlacesData =
- clipboard.hasDataMatchingFlavors(flavors, flavors.length,
- Ci.nsIClipboard.kGlobalClipboard);
- if (hasPlacesData)
- return this._view.insertionPoint != null;
- // if the clipboard doesn't have TYPE_X_MOZ_PLACE_* data, we also allow
- // pasting of valid "text/unicode" and "text/x-moz-url" data
- var xferable = Cc["@mozilla.org/widget/transferable;1"].
- createInstance(Ci.nsITransferable);
- xferable.init(null);
- xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_URL);
- xferable.addDataFlavor(PlacesUtils.TYPE_UNICODE);
- clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
- try {
- // getAnyTransferData will throw if no data is available.
- var data = { }, type = { };
- xferable.getAnyTransferData(type, data, { });
- data = data.value.QueryInterface(Ci.nsISupportsString).data;
- if (type.value != PlacesUtils.TYPE_X_MOZ_URL &&
- type.value != PlacesUtils.TYPE_UNICODE)
- return false;
- // unwrapNodes() will throw if the data blob is malformed.
- var unwrappedNodes = PlacesUtils.unwrapNodes(data, type.value);
- return this._view.insertionPoint != null;
- }
- catch (e) {
- // getAnyTransferData or unwrapNodes failed
- return false;
- }
- },
- /**
- * Gathers information about the selected nodes according to the following
- * rules:
- * "link" node is a URI
- * "bookmark" node is a bookmark
- * "livemarkChild" node is a child of a livemark
- * "tagChild" node is a child of a tag
- * "folder" node is a folder
- * "query" node is a query
- * "separator" node is a separator line
- * "host" node is a host
- *
- * @returns an array of objects corresponding the selected nodes. Each
- * object has each of the properties above set if its corresponding
- * node matches the rule. In addition, the annotations names for each
- * node are set on its corresponding object as properties.
- * Notes:
- * 1) This can be slow, so don't call it anywhere performance critical!
- */
- _buildSelectionMetadata: function() {
- var metadata = [];
- var nodes = this._view.selectedNodes;
- for (var i = 0; i < nodes.length; i++) {
- var nodeData = {};
- var node = nodes[i];
- var nodeType = node.type;
- var uri = null;
- // We don't use the nodeIs* methods here to avoid going through the type
- // property way too often
- switch (nodeType) {
- case Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY:
- nodeData["query"] = true;
- if (node.parent) {
- switch (PlacesUtils.asQuery(node.parent).queryOptions.resultType) {
- case Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY:
- nodeData["host"] = true;
- break;
- case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY:
- case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY:
- nodeData["day"] = true;
- break;
- }
- }
- break;
- case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER:
- case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT:
- nodeData["folder"] = true;
- break;
- case Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR:
- nodeData["separator"] = true;
- break;
- case Ci.nsINavHistoryResultNode.RESULT_TYPE_URI:
- nodeData["link"] = true;
- uri = NetUtil.newURI(node.uri);
- if (PlacesUtils.nodeIsBookmark(node)) {
- nodeData["bookmark"] = true;
- var parentNode = node.parent;
- if (parentNode) {
- if (PlacesUtils.nodeIsTagQuery(parentNode))
- nodeData["tagChild"] = true;
- }
- } else {
- var parentNode = node.parent;
- if (parentNode) {
- if (this.hasCachedLivemarkInfo(parentNode))
- nodeData["livemarkChild"] = true;
- }
- }
- break;
- }
- // annotations
- if (uri) {
- let names = PlacesUtils.annotations.getPageAnnotationNames(uri);
- for (let j = 0; j < names.length; ++j)
- nodeData[names[j]] = true;
- }
- // For items also include the item-specific annotations
- if (node.itemId != -1) {
- let names = PlacesUtils.annotations
- .getItemAnnotationNames(node.itemId);
- for (let j = 0; j < names.length; ++j)
- nodeData[names[j]] = true;
- }
- metadata.push(nodeData);
- }
- return metadata;
- },
- /**
- * Determines if a context-menu item should be shown
- * @param aMenuItem
- * the context menu item
- * @param aMetaData
- * meta data about the selection
- * @returns true if the conditions (see buildContextMenu) are satisfied
- * and the item can be displayed, false otherwise.
- */
- _shouldShowMenuItem: function(aMenuItem, aMetaData) {
- var selectiontype = aMenuItem.getAttribute("selectiontype");
- if (!selectiontype) {
- selectiontype = "single|multiple";
- }
- var selectionTypes = selectiontype.split("|");
- if (selectionTypes.indexOf("any") != -1) {
- return true;
- }
- var count = aMetaData.length;
- if (count > 1 && selectionTypes.indexOf("multiple") == -1)
- return false;
- if (count == 1 && selectionTypes.indexOf("single") == -1)
- return false;
- // NB: if there is no selection, we show the item if (and only if)
- // the selectiontype includes 'none' - the metadata list will be
- // empty so none of the other criteria will apply anyway.
- if (count == 0)
- return selectionTypes.indexOf("none") != -1;
- var forceHideAttr = aMenuItem.getAttribute("forcehideselection");
- if (forceHideAttr) {
- var forceHideRules = forceHideAttr.split("|");
- for (let i = 0; i < aMetaData.length; ++i) {
- for (let j = 0; j < forceHideRules.length; ++j) {
- if (forceHideRules[j] in aMetaData[i])
- return false;
- }
- }
- }
- var selectionAttr = aMenuItem.getAttribute("selection");
- if (!selectionAttr) {
- return !aMenuItem.hidden;
- }
- if (selectionAttr == "any")
- return true;
- var showRules = selectionAttr.split("|");
- var anyMatched = false;
- function metaDataNodeMatches(metaDataNode, rules) {
- for (var i = 0; i < rules.length; i++) {
- if (rules[i] in metaDataNode)
- return true;
- }
- return false;
- }
- for (var i = 0; i < aMetaData.length; ++i) {
- if (metaDataNodeMatches(aMetaData[i], showRules))
- anyMatched = true;
- else
- return false;
- }
- return anyMatched;
- },
- /**
- * Detects information (meta-data rules) about the current selection in the
- * view (see _buildSelectionMetadata) and sets the visibility state for each
- * of the menu-items in the given popup with the following rules applied:
- * 1) The "selectiontype" attribute may be set on a menu-item to "single"
- * if the menu-item should be visible only if there is a single node
- * selected, or to "multiple" if the menu-item should be visible only if
- * multiple nodes are selected, or to "none" if the menuitems should be
- * visible for if there are no selected nodes, or to a |-separated
- * combination of these.
- * If the attribute is not set or set to an invalid value, the menu-item
- * may be visible irrespective of the selection.
- * 2) The "selection" attribute may be set on a menu-item to the various
- * meta-data rules for which it may be visible. The rules should be
- * separated with the | character.
- * 3) A menu-item may be visible only if at least one of the rules set in
- * its selection attribute apply to each of the selected nodes in the
- * view.
- * 4) The "forcehideselection" attribute may be set on a menu-item to rules
- * for which it should be hidden. This attribute takes priority over the
- * selection attribute. A menu-item would be hidden if at least one of the
- * given rules apply to one of the selected nodes. The rules should be
- * separated with the | character.
- * 5) The "hideifnoinsertionpoint" attribute may be set on a menu-item to
- * true if it should be hidden when there's no insertion point
- * 6) The visibility state of a menu-item is unchanged if none of these
- * attribute are set.
- * 7) These attributes should not be set on separators for which the
- * visibility state is "auto-detected."
- * 8) The "hideifprivatebrowsing" attribute may be set on a menu-item to
- * true if it should be hidden inside the private browsing mode
- * @param aPopup
- * The menupopup to build children into.
- * @return true if at least one item is visible, false otherwise.
- */
- buildContextMenu: function(aPopup) {
- var metadata = this._buildSelectionMetadata();
- var ip = this._view.insertionPoint;
- var noIp = !ip || ip.isTag;
- var separator = null;
- var visibleItemsBeforeSep = false;
- var usableItemCount = 0;
- for (var i = 0; i < aPopup.childNodes.length; ++i) {
- var item = aPopup.childNodes[i];
- if (item.localName != "menuseparator") {
- // We allow pasting into tag containers, so special case that.
- var hideIfNoIP = item.getAttribute("hideifnoinsertionpoint") == "true" &&
- noIp && !(ip && ip.isTag && item.id == "placesContext_paste");
- // Show the "Open Containing Folder" menu-item only when the context is
- // in the Library or in the Sidebar, and only when there's no insertion
- // point.
- var hideParentFolderItem = item.id == "placesContext_openParentFolder" &&
- (!/tree/i.test(this._view.localName) || ip);
- var hideIfPrivate = item.getAttribute("hideifprivatebrowsing") == "true" &&
- PrivateBrowsingUtils.isWindowPrivate(window);
- var shouldHideItem = hideIfNoIP || hideIfPrivate || hideParentFolderItem ||
- !this._shouldShowMenuItem(item, metadata);
- item.hidden = item.disabled = shouldHideItem;
- if (!item.hidden) {
- visibleItemsBeforeSep = true;
- usableItemCount++;
- // Show the separator above the menu-item if any
- if (separator) {
- separator.hidden = false;
- separator = null;
- }
- }
- }
- else { // menuseparator
- // Initially hide it. It will be unhidden if there will be at least one
- // visible menu-item above and below it.
- item.hidden = true;
- // We won't show the separator at all if no items are visible above it
- if (visibleItemsBeforeSep)
- separator = item;
- // New separator, count again:
- visibleItemsBeforeSep = false;
- }
- }
- // Set Open Folder/Links In Tabs items enabled state if they're visible
- if (usableItemCount > 0) {
- var openContainerInTabsItem = document.getElementById("placesContext_openContainer:tabs");
- if (!openContainerInTabsItem.hidden) {
- var containerToUse = this._view.selectedNode || this._view.result.root;
- if (PlacesUtils.nodeIsContainer(containerToUse)) {
- if (!PlacesUtils.hasChildURIs(containerToUse)) {
- openContainerInTabsItem.disabled = true;
- // Ensure that we don't display the menu if nothing is enabled:
- usableItemCount--;
- }
- }
- }
- }
- return usableItemCount > 0;
- },
- /**
- * Select all links in the current view.
- */
- selectAll: function() {
- this._view.selectAll();
- },
- /**
- * Opens the bookmark properties for the selected URI Node.
- */
- showBookmarkPropertiesForSelection:
- function() {
- var node = this._view.selectedNode;
- if (!node)
- return;
- var itemType = PlacesUtils.nodeIsFolder(node) ||
- PlacesUtils.nodeIsTagQuery(node) ? "folder" : "bookmark";
- var concreteId = PlacesUtils.getConcreteItemId(node);
- var isRootItem = PlacesUtils.isRootItem(concreteId);
- var itemId = node.itemId;
- if (isRootItem || PlacesUtils.nodeIsTagQuery(node)) {
- // If this is a root or the Tags query we use the concrete itemId to catch
- // the correct title for the node.
- itemId = concreteId;
- }
- PlacesUIUtils.showBookmarkDialog({ action: "edit"
- , type: itemType
- , itemId: itemId
- , readOnly: isRootItem
- , hiddenRows: [ "folderPicker" ]
- }, window.top);
- },
- /**
- * This method can be run on a URI parameter to ensure that it didn't
- * receive a string instead of an nsIURI object.
- */
- _assertURINotString: function(value) {
- NS_ASSERT((typeof(value) == "object") && !(value instanceof String),
- "This method should be passed a URI as a nsIURI object, not as a string.");
- },
- /**
- * Reloads the selected livemark if any.
- */
- reloadSelectedLivemark: function() {
- var selectedNode = this._view.selectedNode;
- if (selectedNode) {
- let itemId = selectedNode.itemId;
- PlacesUtils.livemarks.getLivemark({ id: itemId })
- .then(aLivemark => {
- aLivemark.reload(true);
- }, Components.utils.reportError);
- }
- },
- /**
- * Opens the links in the selected folder, or the selected links in new tabs.
- */
- openSelectionInTabs: function(aEvent) {
- var node = this._view.selectedNode;
- var nodes = this._view.selectedNodes;
- // In the case of no selection, open the root node:
- if (!node && !nodes.length) {
- node = this._view.result.root;
- }
- if (node && PlacesUtils.nodeIsContainer(node))
- PlacesUIUtils.openContainerNodeInTabs(node, aEvent, this._view);
- else
- PlacesUIUtils.openURINodesInTabs(nodes, aEvent, this._view);
- },
- /**
- * Shows the Add Bookmark UI for the current insertion point.
- *
- * @param aType
- * the type of the new item (bookmark/livemark/folder)
- */
- newItem: function(aType) {
- let ip = this._view.insertionPoint;
- if (!ip)
- throw Cr.NS_ERROR_NOT_AVAILABLE;
- let performed =
- PlacesUIUtils.showBookmarkDialog({ action: "add"
- , type: aType
- , defaultInsertionPoint: ip
- , hiddenRows: [ "folderPicker" ]
- }, window.top);
- if (performed) {
- // Select the new item.
- let insertedNodeId = PlacesUtils.bookmarks
- .getIdForItemAt(ip.itemId, ip.index);
- this._view.selectItems([insertedNodeId], false);
- }
- },
- /**
- * Create a new Bookmark separator somewhere.
- */
- newSeparator: function() {
- var ip = this._view.insertionPoint;
- if (!ip)
- throw Cr.NS_ERROR_NOT_AVAILABLE;
- var txn = new PlacesCreateSeparatorTransaction(ip.itemId, ip.index);
- PlacesUtils.transactionManager.doTransaction(txn);
- // select the new item
- var insertedNodeId = PlacesUtils.bookmarks
- .getIdForItemAt(ip.itemId, ip.index);
- this._view.selectItems([insertedNodeId], false);
- },
- /**
- * Opens a dialog for moving the selected nodes.
- */
- moveSelectedBookmarks: function() {
- window.openDialog("chrome://browser/content/places/moveBookmarks.xul",
- "", "chrome, modal",
- this._view.selectedNodes);
- },
- /**
- * Sort the selected folder by name.
- */
- sortFolderByName: function() {
- var itemId = PlacesUtils.getConcreteItemId(this._view.selectedNode);
- var txn = new PlacesSortFolderByNameTransaction(itemId);
- PlacesUtils.transactionManager.doTransaction(txn);
- },
- /**
- * Open the parent folder for the selected bookmarks search result.
- */
- openParentFolder: function() {
- var view;
- if (!document.popupNode) {
- view = document.commandDispatcher.focusedElement;
- } else {
- view = PlacesUIUtils.getViewForNode(document.popupNode); // XULElement
- }
- if (!view || view.getAttribute("type") != "places")
- return;
- var node = view.selectedNode; // nsINavHistoryResultNode
- var aItemId = node.itemId;
- var aFolderItemId = this.getParentFolderByItemId(aItemId);
- if (aFolderItemId)
- this.selectFolderByItemId(view, aFolderItemId, aItemId);
- },
- getParentFolderByItemId: function(aItemId) {
- var bmsvc = Components.classes["@mozilla.org/browser/nav-bookmarks-service;1"].
- getService(Components.interfaces.nsINavBookmarksService);
- var parentFolderId = bmsvc.getFolderIdForItem(aItemId);
- return parentFolderId;
- },
- selectItems2: function(view, aIDs) {
- var ids = aIDs; // Don't manipulate the caller's array.
- // Array of nodes found by findNodes which are to be selected
- var nodes = [];
- // Array of nodes found by findNodes which should be opened
- var nodesToOpen = [];
- // A set of URIs of container-nodes that were previously searched,
- // and thus shouldn't be searched again. This is empty at the initial
- // start of the recursion and gets filled in as the recursion
- // progresses.
- var nodesURIChecked = [];
- /**
- * Recursively search through a node's children for items
- * with the given IDs. When a matching item is found, remove its ID
- * from the IDs array, and add the found node to the nodes dictionary.
- *
- * NOTE: This method will leave open any node that had matching items
- * in its subtree.
- */
- function findNodes(node) {
- var foundOne = false;
- // See if node matches an ID we wanted; add to results.
- // For simple folder queries, check both itemId and the concrete
- // item id.
- var index = ids.indexOf(node.itemId);
- if (index == -1 &&
- node.type == Components.interfaces.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT) {
- index = ids.indexOf(PlacesUtils.asQuery(node).folderItemId); //xxx Bug 556739 3.7a5pre
- }
- if (index != -1) {
- nodes.push(node);
- foundOne = true;
- ids.splice(index, 1);
- }
- if (ids.length == 0 || !PlacesUtils.nodeIsContainer(node) ||
- nodesURIChecked.indexOf(node.uri) != -1)
- return foundOne;
- nodesURIChecked.push(node.uri);
- PlacesUtils.asContainer(node); // xxx Bug 556739 3.7a6pre
- // Remember the beginning state so that we can re-close
- // this node if we don't find any additional results here.
- var previousOpenness = node.containerOpen;
- node.containerOpen = true;
- for (var child = 0; child < node.childCount && ids.length > 0;
- child++) {
- var childNode = node.getChild(child);
- var found = findNodes(childNode);
- if (!foundOne)
- foundOne = found;
- }
- // If we didn't find any additional matches in this node's
- // subtree, revert the node to its previous openness.
- if (foundOne)
- nodesToOpen.unshift(node);
- node.containerOpen = previousOpenness;
- return foundOne;
- } // findNodes
- // Disable notifications while looking for nodes.
- let result = view.result;
- let didSuppressNotifications = result.suppressNotifications;
- if (!didSuppressNotifications)
- result.suppressNotifications = true
- try {
- findNodes(view.result.root);
- }
- finally {
- if (!didSuppressNotifications)
- result.suppressNotifications = false;
- }
- // For all the nodes we've found, highlight the corresponding
- // index in the tree.
- var resultview = view.view;
- var selection = resultview.selection;
- selection.selectEventsSuppressed = true;
- selection.clearSelection();
- // Open nodes containing found items.
- for (var i = 0; i < nodesToOpen.length; i++) {
- nodesToOpen[i].containerOpen = true;
- }
- for (var i = 0; i < nodes.length; i++) {
- if (PlacesUtils.nodeIsContainer(nodes[i]))
- continue;
- var index = resultview.treeIndexForNode(nodes[i]);
- selection.rangedSelect(index, index, true);
- }
- selection.selectEventsSuppressed = false;
- },
- selectFolderByItemId: function(view, aFolderItemId, aItemId) {
- // Library
- if (view.getAttribute("id") == "placeContent") {
- view = document.getElementById("placesList");
- // Select a folder node in folder pane.
- this.selectItems2(view, [aFolderItemId]);
- view.selectItems([aFolderItemId]);
- if (view.currentIndex)
- view.treeBoxObject.ensureRowIsVisible(view.currentIndex);
- // Reselect child node.
- setTimeout(function(aItemId, view) {
- var aView = view.ownerDocument.getElementById("placeContent");
- aView.selectItems([aItemId]);
- if (aView.currentIndex)
- aView.treeBoxObject.ensureRowIsVisible(aView.currentIndex);
- }, 0, aItemId, view);
- return;
- }
- // Bookmarks Sidebar
- if (!view)
- return;
- view.place = view.place;
- if ('FlatBookmarksOverlay' in window) {
- var sidebarwin = view.ownerDocument.defaultView;
- var searchBox = sidebarwin.document.getElementById("search-box");
- searchBox.value = "";
- searchBox.doCommand();
- sidebarwin.FlatBookmarks._setTreePlace(sidebarwin.FlatBookmarks._makePlaceForFolder(aFolderItemId));
- view.selectItems([aItemId]);
- var tbo = view.treeBoxObject;
- tbo.ensureRowIsVisible(view.currentIndex);
- view.focus();
- return;
- }
- view.findNode = function flatChildNodes(node, aIDs) {
- var ids = aIDs; // Don't manipulate the caller's array.
- // Array of nodes found by findNodes which are to be selected
- var nodes = [];
- // Array of nodes found by findNodes which should be opened
- var nodesToOpen = [];
- // A set of URIs of container-nodes that were previously searched,
- // and thus shouldn't be searched again. This is empty at the initial
- // start of the recursion and gets filled in as the recursion
- // progresses.
- var nodesURIChecked = [];
- /**
- * Recursively search through a node's children for items
- * with the given IDs. When a matching item is found, remove its ID
- * from the IDs array, and add the found node to the nodes dictionary.
- *
- * NOTE: This method will leave open any node that had matching items
- * in its subtree.
- */
- function findNodes(node) {
- var foundOne = false;
- // See if node matches an ID we wanted; add to results.
- // For simple folder queries, check both itemId and the concrete
- // item id.
- var index = ids.indexOf(node.itemId);
- if (index == -1 &&
- node.type == Components.interfaces.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT) {
- index = ids.indexOf(PlacesUtils.asQuery(node).folderItemId); // xxx Bug 556739 3.7a5pre
- }
- if (index != -1) {
- nodes.push(node);
- foundOne = true;
- ids.splice(index, 1);
- }
- if (ids.length == 0 || !PlacesUtils.nodeIsContainer(node) ||
- nodesURIChecked.indexOf(node.uri) != -1)
- return foundOne;
- nodesURIChecked.push(node.uri);
- PlacesUtils.asContainer(node); // xxx Bug 556739 3.7a6pre
- // Remember the beginning state so that we can re-close
- // this node if we don't find any additional results here.
- var previousOpenness = node.containerOpen;
- node.containerOpen = true;
- for (var child = 0; child < node.childCount && ids.length > 0;
- child++) {
- var childNode = node.getChild(child);
- if (PlacesUtils.nodeIsQuery(childNode))
- continue;
- var found = findNodes(childNode);
- if (!foundOne)
- foundOne = found;
- }
- // If we didn't find any additional matches in this node's
- // subtree, revert the node to its previous openness.
- if (foundOne)
- nodesToOpen.unshift(node);
- node.containerOpen = previousOpenness;
- return foundOne;
- } // findNodes
- // Disable notifications while looking for nodes.
- let result = this.result;
- let didSuppressNotifications = result.suppressNotifications;
- if (!didSuppressNotifications)
- result.suppressNotifications = true
- try {
- findNodes(this.result.root);
- }
- finally {
- if (!didSuppressNotifications)
- result.suppressNotifications = false;
- }
- // Open nodes containing found items.
- for (var i = 0; i < nodesToOpen.length; i++) {
- nodesToOpen[i].containerOpen = true;
- }
- return nodes;
- }; // findNode
- // For all the nodes we've found, highlight the corresponding
- // index in the tree.
- var resultview = view.view;
- var selection = view.view.selection;
- selection.selectEventsSuppressed = true;
- selection.clearSelection();
- var nodes = view.findNode(view.result.root, [aFolderItemId]);
- if (nodes.length > 0) {
- var index = resultview.treeIndexForNode(nodes[0]);
- nodes = view.findNode(nodes[0], [aItemId]);
- if (nodes.length > 0) {
- index = resultview.treeIndexForNode(nodes[0]);
- selection.rangedSelect(index, index, true);
- }
- }
- selection.selectEventsSuppressed = false;
- var tbo = view.treeBoxObject;
- tbo.ensureRowIsVisible(view.currentIndex);
- view.focus();
- return;
- },
- /**
- * Walk the list of folders we're removing in this delete operation, and
- * see if the selected node specified is already implicitly being removed
- * because it is a child of that folder.
- * @param node
- * Node to check for containment.
- * @param pastFolders
- * List of folders the calling function has already traversed
- * @returns true if the node should be skipped, false otherwise.
- */
- _shouldSkipNode: function(node, pastFolders) {
- /**
- * Determines if a node is contained by another node within a resultset.
- * @param node
- * The node to check for containment for
- * @param parent
- * The parent container to check for containment in
- * @returns true if node is a member of parent's children, false otherwise.
- */
- function isContainedBy(node, parent) {
- var cursor = node.parent;
- while (cursor) {
- if (cursor == parent)
- return true;
- cursor = cursor.parent;
- }
- return false;
- }
- for (var j = 0; j < pastFolders.length; ++j) {
- if (isContainedBy(node, pastFolders[j]))
- return true;
- }
- return false;
- },
- /**
- * Creates a set of transactions for the removal of a range of items.
- * A range is an array of adjacent nodes in a view.
- * @param [in] range
- * An array of nodes to remove. Should all be adjacent.
- * @param [out] transactions
- * An array of transactions.
- * @param [optional] removedFolders
- * An array of folder nodes that have already been removed.
- */
- _removeRange: function(range, transactions, removedFolders) {
- NS_ASSERT(transactions instanceof Array, "Must pass a transactions array");
- if (!removedFolders)
- removedFolders = [];
- for (var i = 0; i < range.length; ++i) {
- var node = range[i];
- if (this._shouldSkipNode(node, removedFolders))
- continue;
- if (PlacesUtils.nodeIsTagQuery(node.parent)) {
- // This is a uri node inside a tag container. It needs a special
- // untag transaction.
- var tagItemId = PlacesUtils.getConcreteItemId(node.parent);
- var uri = NetUtil.newURI(node.uri);
- let txn = new PlacesUntagURITransaction(uri, [tagItemId]);
- transactions.push(txn);
- }
- else if (PlacesUtils.nodeIsTagQuery(node) && node.parent &&
- PlacesUtils.nodeIsQuery(node.parent) &&
- PlacesUtils.asQuery(node.parent).queryOptions.resultType ==
- Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY) {
- // This is a tag container.
- // Untag all URIs tagged with this tag only if the tag container is
- // child of the "Tags" query in the library, in all other places we
- // must only remove the query node.
- var tag = node.title;
- var URIs = PlacesUtils.tagging.getURIsForTag(tag);
- for (var j = 0; j < URIs.length; j++) {
- let txn = new PlacesUntagURITransaction(URIs[j], [tag]);
- transactions.push(txn);
- }
- }
- else if (PlacesUtils.nodeIsURI(node) &&
- PlacesUtils.nodeIsQuery(node.parent) &&
- PlacesUtils.asQuery(node.parent).queryOptions.queryType ==
- Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
- // This is a uri node inside an history query.
- PlacesUtils.bhistory.removePage(NetUtil.newURI(node.uri));
- // History deletes are not undoable, so we don't have a transaction.
- }
- else if (node.itemId == -1 &&
- PlacesUtils.nodeIsQuery(node) &&
- PlacesUtils.asQuery(node).queryOptions.queryType ==
- Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
- // This is a dynamically generated history query, like queries
- // grouped by site, time or both. Dynamically generated queries don't
- // have an itemId even if they are descendants of a bookmark.
- this._removeHistoryContainer(node);
- // History deletes are not undoable, so we don't have a transaction.
- }
- else {
- // This is a common bookmark item.
- if (PlacesUtils.nodeIsFolder(node)) {
- // If this is a folder we add it to our array of folders, used
- // to skip nodes that are children of an already removed folder.
- removedFolders.push(node);
- }
- let txn = new PlacesRemoveItemTransaction(node.itemId);
- transactions.push(txn);
- }
- }
- },
- /**
- * Removes the set of selected ranges from bookmarks.
- * @param txnName
- * See |remove|.
- */
- _removeRowsFromBookmarks: function(txnName) {
- var ranges = this._view.removableSelectionRanges;
- var transactions = [];
- var removedFolders = [];
- for (var i = 0; i < ranges.length; i++)
- this._removeRange(ranges[i], transactions, removedFolders);
- if (transactions.length > 0) {
- var txn = new PlacesAggregatedTransaction(txnName, transactions);
- PlacesUtils.transactionManager.doTransaction(txn);
- }
- },
- /**
- * Removes the set of selected ranges from history.
- *
- * @note history deletes are not undoable.
- */
- _removeRowsFromHistory: function() {
- let nodes = this._view.selectedNodes;
- let URIs = [];
- for (let i = 0; i < nodes.length; ++i) {
- let node = nodes[i];
- if (PlacesUtils.nodeIsURI(node)) {
- let uri = NetUtil.newURI(node.uri);
- // Avoid duplicates.
- if (URIs.indexOf(uri) < 0) {
- URIs.push(uri);
- }
- }
- else if (PlacesUtils.nodeIsQuery(node) &&
- PlacesUtils.asQuery(node).queryOptions.queryType ==
- Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
- this._removeHistoryContainer(node);
- }
- }
- // Do removal in chunks to give some breath to main-thread.
- function pagesChunkGenerator(aURIs) {
- while (aURIs.length) {
- let URIslice = aURIs.splice(0, REMOVE_PAGES_CHUNKLEN);
- PlacesUtils.bhistory.removePages(URIslice, URIslice.length);
- Services.tm.mainThread.dispatch(function() {
- try {
- gen.next();
- } catch (ex if ex instanceof StopIteration) {}
- }, Ci.nsIThread.DISPATCH_NORMAL);
- yield;
- }
- }
- let gen = pagesChunkGenerator(URIs);
- gen.next();
- },
- /**
- * Removes history visits for an history container node.
- * @param [in] aContainerNode
- * The container node to remove.
- *
- * @note history deletes are not undoable.
- */
- _removeHistoryContainer: function(aContainerNode) {
- if (PlacesUtils.nodeIsHost(aContainerNode)) {
- // Site container.
- PlacesUtils.bhistory.removePagesFromHost(aContainerNode.title, true);
- }
- else if (PlacesUtils.nodeIsDay(aContainerNode)) {
- // Day container.
- let query = aContainerNode.getQueries()[0];
- let beginTime = query.beginTime;
- let endTime = query.endTime;
- NS_ASSERT(query && beginTime && endTime,
- "A valid date container query should exist!");
- // We want to exclude beginTime from the removal because
- // removePagesByTimeframe includes both extremes, while date containers
- // exclude the lower extreme. So, if we would not exclude it, we would
- // end up removing more history than requested.
- PlacesUtils.bhistory.removePagesByTimeframe(beginTime + 1, endTime);
- }
- },
- /**
- * Removes the selection
- * @param aTxnName
- * A name for the transaction if this is being performed
- * as part of another operation.
- */
- remove: function(aTxnName) {
- if (!this._hasRemovableSelection())
- return;
- NS_ASSERT(aTxnName !== undefined, "Must supply Transaction Name");
- var root = this._view.result.root;
- if (PlacesUtils.nodeIsFolder(root))
- this._removeRowsFromBookmarks(aTxnName);
- else if (PlacesUtils.nodeIsQuery(root)) {
- var queryType = PlacesUtils.asQuery(root).queryOptions.queryType;
- if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS)
- this._removeRowsFromBookmarks(aTxnName);
- else if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY)
- this._removeRowsFromHistory();
- else
- NS_ASSERT(false, "implement support for QUERY_TYPE_UNIFIED");
- }
- else
- NS_ASSERT(false, "unexpected root");
- },
- /**
- * Fills a DataTransfer object with the content of the selection that can be
- * dropped elsewhere.
- * @param aEvent
- * The dragstart event.
- */
- setDataTransfer: function(aEvent) {
- let dt = aEvent.dataTransfer;
- let doCopy = ["copyLink", "copy", "link"].indexOf(dt.effectAllowed) != -1;
- let result = this._view.result;
- let didSuppressNotifications = result.suppressNotifications;
- if (!didSuppressNotifications)
- result.suppressNotifications = true;
- function addData(type, index, feedURI) {
- let wrapNode = PlacesUtils.wrapNode(node, type, feedURI);
- dt.mozSetDataAt(type, wrapNode, index);
- }
- function addURIData(index, feedURI) {
- addData(PlacesUtils.TYPE_X_MOZ_URL, index, feedURI);
- addData(PlacesUtils.TYPE_UNICODE, index, feedURI);
- addData(PlacesUtils.TYPE_HTML, index, feedURI);
- }
- try {
- let nodes = this._view.draggableSelection;
- for (let i = 0; i < nodes.length; ++i) {
- var node = nodes[i];
- // This order is _important_! It controls how this and other
- // applications select data to be inserted based on type.
- addData(PlacesUtils.TYPE_X_MOZ_PLACE, i);
- // Drop the feed uri for livemark containers
- let livemarkInfo = this.getCachedLivemarkInfo(node);
- if (livemarkInfo) {
- addURIData(i, livemarkInfo.feedURI.spec);
- }
- else if (node.uri) {
- addURIData(i);
- }
- }
- }
- finally {
- if (!didSuppressNotifications)
- result.suppressNotifications = false;
- }
- },
- get clipboardAction () {
- let action = {};
- let actionOwner;
- try {
- let xferable = Cc["@mozilla.org/widget/transferable;1"].
- createInstance(Ci.nsITransferable);
- xferable.init(null);
- xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION)
- this.clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
- xferable.getTransferData(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION, action, {});
- [action, actionOwner] =
- action.value.QueryInterface(Ci.nsISupportsString).data.split(",");
- } catch(ex) {
- // Paste from external sources don't have any associated action, just
- // fallback to a copy action.
- return "copy";
- }
- // For cuts also check who inited the action, since cuts across different
- // instances should instead be handled as copies (The sources are not
- // available for this instance).
- if (action == "cut" && actionOwner != this.profileName)
- action = "copy";
- return action;
- },
- _releaseClipboardOwnership: function() {
- if (this.cutNodes.length > 0) {
- // This clears the logical clipboard, doesn't remove data.
- this.clipboard.emptyClipboard(Ci.nsIClipboard.kGlobalClipboard);
- }
- },
- _clearClipboard: function() {
- let xferable = Cc["@mozilla.org/widget/transferable;1"].
- createInstance(Ci.nsITransferable);
- xferable.init(null);
- // Empty transferables may cause crashes, so just add an unknown type.
- const TYPE = "text/x-moz-place-empty";
- xferable.addDataFlavor(TYPE);
- xferable.setTransferData(TYPE, PlacesUtils.toISupportsString(""), 0);
- this.clipboard.setData(xferable, null, Ci.nsIClipboard.kGlobalClipboard);
- },
- _populateClipboard: function(aNodes, aAction) {
- // This order is _important_! It controls how this and other applications
- // select data to be inserted based on type.
- let contents = [
- { type: PlacesUtils.TYPE_X_MOZ_PLACE, entries: [] },
- { type: PlacesUtils.TYPE_X_MOZ_URL, entries: [] },
- { type: PlacesUtils.TYPE_HTML, entries: [] },
- { type: PlacesUtils.TYPE_UNICODE, entries: [] },
- ];
- // Avoid handling descendants of a copied node, the transactions take care
- // of them automatically.
- let copiedFolders = [];
- aNodes.forEach(function(node) {
- if (this._shouldSkipNode(node, copiedFolders))
- return;
- if (PlacesUtils.nodeIsFolder(node))
- copiedFolders.push(node);
- let livemarkInfo = this.getCachedLivemarkInfo(node);
- let feedURI = livemarkInfo && livemarkInfo.feedURI.spec;
- contents.forEach(function(content) {
- content.entries.push(
- PlacesUtils.wrapNode(node, content.type, feedURI)
- );
- });
- }, this);
- function addData(type, data) {
- xferable.addDataFlavor(type);
- xferable.setTransferData(type, PlacesUtils.toISupportsString(data),
- data.length * 2);
- }
- let xferable = Cc["@mozilla.org/widget/transferable;1"].
- createInstance(Ci.nsITransferable);
- xferable.init(null);
- let hasData = false;
- // This order matters here! It controls how this and other applications
- // select data to be inserted based on type.
- contents.forEach(function(content) {
- if (content.entries.length > 0) {
- hasData = true;
- let glue =
- content.type == PlacesUtils.TYPE_X_MOZ_PLACE ? "," : PlacesUtils.endl;
- addData(content.type, content.entries.join(glue));
- }
- });
- // Track the exected action in the xferable. This must be the last flavor
- // since it's the least preferred one.
- // Enqueue a unique instance identifier to distinguish operations across
- // concurrent instances of the application.
- addData(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION, aAction + "," + this.profileName);
- if (hasData) {
- this.clipboard.setData(xferable,
- this.cutNodes.length > 0 ? this : null,
- Ci.nsIClipboard.kGlobalClipboard);
- }
- },
- _cutNodes: [],
- get cutNodes() this._cutNodes,
- set cutNodes(aNodes) {
- let self = this;
- function updateCutNodes(aValue) {
- self._cutNodes.forEach(function(aNode) {
- self._view.toggleCutNode(aNode, aValue);
- });
- }
- updateCutNodes(false);
- this._cutNodes = aNodes;
- updateCutNodes(true);
- return aNodes;
- },
- /**
- * Copy Bookmarks and Folders to the clipboard
- */
- copy: function() {
- let result = this._view.result;
- let didSuppressNotifications = result.suppressNotifications;
- if (!didSuppressNotifications)
- result.suppressNotifications = true;
- try {
- this._populateClipboard(this._view.selectedNodes, "copy");
- }
- finally {
- if (!didSuppressNotifications)
- result.suppressNotifications = false;
- }
- },
- /**
- * Cut Bookmarks and Folders to the clipboard
- */
- cut: function() {
- let result = this._view.result;
- let didSuppressNotifications = result.suppressNotifications;
- if (!didSuppressNotifications)
- result.suppressNotifications = true;
- try {
- this._populateClipboard(this._view.selectedNodes, "cut");
- this.cutNodes = this._view.selectedNodes;
- }
- finally {
- if (!didSuppressNotifications)
- result.suppressNotifications = false;
- }
- },
- /**
- * Paste Bookmarks and Folders from the clipboard
- */
- paste: function() {
- // No reason to proceed if there isn't a valid insertion point.
- let ip = this._view.insertionPoint;
- if (!ip)
- throw Cr.NS_ERROR_NOT_AVAILABLE;
- let action = this.clipboardAction;
- let xferable = Cc["@mozilla.org/widget/transferable;1"].
- createInstance(Ci.nsITransferable);
- xferable.init(null);
- // This order matters here! It controls the preferred flavors for this
- // paste operation.
- [ PlacesUtils.TYPE_X_MOZ_PLACE,
- PlacesUtils.TYPE_X_MOZ_URL,
- PlacesUtils.TYPE_UNICODE,
- ].forEach(function(type) xferable.addDataFlavor(type));
- this.clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
- // Now get the clipboard contents, in the best available flavor.
- let data = {}, type = {}, items = [];
- try {
- xferable.getAnyTransferData(type, data, {});
- data = data.value.QueryInterface(Ci.nsISupportsString).data;
- type = type.value;
- items = PlacesUtils.unwrapNodes(data, type);
- } catch(ex) {
- // No supported data exists or nodes unwrap failed, just bail out.
- return;
- }
- let transactions = [];
- let insertionIndex = ip.index;
- for (let i = 0; i < items.length; ++i) {
- if (ip.isTag) {
- // Pasting into a tag container means tagging the item, regardless of
- // the requested action.
- let tagTxn = new PlacesTagURITransaction(NetUtil.newURI(items[i].uri),
- [ip.itemId]);
- transactions.push(tagTxn);
- continue;
- }
- // Adjust index to make sure items are pasted in the correct position.
- // If index is DEFAULT_INDEX, items are just appended.
- if (ip.index != PlacesUtils.bookmarks.DEFAULT_INDEX)
- insertionIndex = ip.index + i;
- transactions.push(
- PlacesUIUtils.makeTransaction(items[i], type, ip.itemId,
- insertionIndex, action == "copy")
- );
- }
- let aggregatedTxn = new PlacesAggregatedTransaction("Paste", transactions);
- PlacesUtils.transactionManager.doTransaction(aggregatedTxn);
- // Cut/past operations are not repeatable, so clear the clipboard.
- if (action == "cut") {
- this._clearClipboard();
- }
- // Select the pasted items, they should be consecutive.
- let insertedNodeIds = [];
- for (let i = 0; i < transactions.length; ++i) {
- insertedNodeIds.push(
- PlacesUtils.bookmarks.getIdForItemAt(ip.itemId, ip.index + i)
- );
- }
- if (insertedNodeIds.length > 0)
- this._view.selectItems(insertedNodeIds, false);
- },
- /**
- * Cache the livemark info for a node. This allows the controller and the
- * views to treat the given node as a livemark.
- * @param aNode
- * a places result node.
- * @param aLivemarkInfo
- * a mozILivemarkInfo object.
- */
- cacheLivemarkInfo: function(aNode, aLivemarkInfo) {
- this._cachedLivemarkInfoObjects.set(aNode, aLivemarkInfo);
- },
- /**
- * Returns whether or not there's cached mozILivemarkInfo object for a node.
- * @param aNode
- * a places result node.
- * @return true if there's a cached mozILivemarkInfo object for
- * aNode, false otherwise.
- */
- hasCachedLivemarkInfo: function(aNode)
- this._cachedLivemarkInfoObjects.has(aNode),
- /**
- * Returns the cached livemark info for a node, if set by cacheLivemarkInfo,
- * null otherwise.
- * @param aNode
- * a places result node.
- * @return the mozILivemarkInfo object for aNode, if set, null otherwise.
- */
- getCachedLivemarkInfo: function(aNode)
- this._cachedLivemarkInfoObjects.get(aNode, null)
- };
- /**
- * Handles drag and drop operations for views. Note that this is view agnostic!
- * You should not use PlacesController._view within these methods, since
- * the view that the item(s) have been dropped on was not necessarily active.
- * Drop functions are passed the view that is being dropped on.
- */
- var PlacesControllerDragHelper = {
- /**
- * DOM Element currently being dragged over
- */
- currentDropTarget: null,
- /**
- * Determines if the mouse is currently being dragged over a child node of
- * this menu. This is necessary so that the menu doesn't close while the
- * mouse is dragging over one of its submenus
- * @param node
- * The container node
- * @returns true if the user is dragging over a node within the hierarchy of
- * the container, false otherwise.
- */
- draggingOverChildNode: function(node) {
- let currentNode = this.currentDropTarget;
- while (currentNode) {
- if (currentNode == node)
- return true;
- currentNode = currentNode.parentNode;
- }
- return false;
- },
- /**
- * @returns The current active drag session. Returns null if there is none.
- */
- getSession: function() {
- return this.dragService.getCurrentSession();
- },
- /**
- * Extract the first accepted flavor from a list of flavors.
- * @param aFlavors
- * The flavors list of type nsIDOMDOMStringList.
- */
- getFirstValidFlavor: function(aFlavors) {
- for (let i = 0; i < aFlavors.length; i++) {
- if (this.GENERIC_VIEW_DROP_TYPES.indexOf(aFlavors[i]) != -1)
- return aFlavors[i];
- }
- // If no supported flavor is found, check if data includes text/plain
- // contents. If so, request them as text/unicode, a conversion will happen
- // automatically.
- if (aFlavors.contains("text/plain")) {
- return PlacesUtils.TYPE_UNICODE;
- }
- return null;
- },
- /**
- * Determines whether or not the data currently being dragged can be dropped
- * on a places view.
- * @param ip
- * The insertion point where the items should be dropped.
- */
- canDrop: function(ip, dt) {
- let dropCount = dt.mozItemCount;
- // Check every dragged item.
- for (let i = 0; i < dropCount; i++) {
- let flavor = this.getFirstValidFlavor(dt.mozTypesAt(i));
- if (!flavor)
- return false;
- // Urls can be dropped on any insertionpoint.
- // XXXmano: remember that this method is called for each dragover event!
- // Thus we shouldn't use unwrapNodes here at all if possible.
- // I think it would be OK to accept bogus data here (e.g. text which was
- // somehow wrapped as TAB_DROP_TYPE, this is not in our control, and
- // will just case the actual drop to be a no-op), and only rule out valid
- // expected cases, which are either unsupported flavors, or items which
- // cannot be dropped in the current insertionpoint. The last case will
- // likely force us to use unwrapNodes for the private data types of
- // places.
- if (flavor == TAB_DROP_TYPE)
- continue;
- let data = dt.mozGetDataAt(flavor, i);
- let dragged;
- try {
- dragged = PlacesUtils.unwrapNodes(data, flavor)[0];
- }
- catch (e) {
- return false;
- }
- // Only bookmarks and urls can be dropped into tag containers.
- if (ip.isTag && ip.orientation == Ci.nsITreeView.DROP_ON &&
- dragged.type != PlacesUtils.TYPE_X_MOZ_URL &&
- (dragged.type != PlacesUtils.TYPE_X_MOZ_PLACE ||
- (dragged.uri && dragged.uri.startsWith("place:")) ))
- return false;
- // The following loop disallows the dropping of a folder on itself or
- // on any of its descendants.
- if (dragged.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER ||
- (dragged.uri && dragged.uri.startsWith("place:")) ) {
- let parentId = ip.itemId;
- while (parentId != PlacesUtils.placesRootId) {
- if (dragged.concreteId == parentId || dragged.id == parentId)
- return false;
- parentId = PlacesUtils.bookmarks.getFolderIdForItem(parentId);
- }
- }
- }
- return true;
- },
- /**
- * Determines if a node can be moved.
- *
- * @param aNode
- * A nsINavHistoryResultNode node.
- * @returns True if the node can be moved, false otherwise.
- */
- canMoveNode:
- function(aNode) {
- // Only bookmark items are movable.
- if (aNode.itemId == -1)
- return false;
- // Once tags and bookmarked are divorced, the tag-query check should be
- // removed.
- let parentNode = aNode.parent;
- return parentNode != null &&
- !(PlacesUtils.nodeIsFolder(parentNode) &&
- PlacesUIUtils.isContentsReadOnly(parentNode)) &&
- !PlacesUtils.nodeIsTagQuery(parentNode);
- },
- /**
- * Handles the drop of one or more items onto a view.
- * @param insertionPoint
- * The insertion point where the items should be dropped
- */
- onDrop: function(insertionPoint, dt) {
- let doCopy = ["copy", "link"].indexOf(dt.dropEffect) != -1;
- let transactions = [];
- let dropCount = dt.mozItemCount;
- let movedCount = 0;
- for (let i = 0; i < dropCount; ++i) {
- let flavor = this.getFirstValidFlavor(dt.mozTypesAt(i));
- if (!flavor)
- return;
- let data = dt.mozGetDataAt(flavor, i);
- let unwrapped;
- if (flavor != TAB_DROP_TYPE) {
- // There's only ever one in the D&D case.
- unwrapped = PlacesUtils.unwrapNodes(data, flavor)[0];
- }
- else if (data instanceof XULElement && data.localName == "tab" &&
- data.ownerDocument.defaultView instanceof ChromeWindow) {
- let uri = data.linkedBrowser.currentURI;
- let spec = uri ? uri.spec : "about:blank";
- let title = data.label;
- unwrapped = { uri: spec,
- title: data.label,
- type: PlacesUtils.TYPE_X_MOZ_URL};
- }
- else
- throw("bogus data was passed as a tab");
- let index = insertionPoint.index;
- // Adjust insertion index to prevent reversal of dragged items. When you
- // drag multiple elts upward: need to increment index or each successive
- // elt will be inserted at the same index, each above the previous.
- let dragginUp = insertionPoint.itemId == unwrapped.parent &&
- index < PlacesUtils.bookmarks.getItemIndex(unwrapped.id);
- if (index != -1 && dragginUp)
- index += movedCount++;
- // If dragging over a tag container we should tag the item.
- if (insertionPoint.isTag &&
- insertionPoint.orientation == Ci.nsITreeView.DROP_ON) {
- let uri = NetUtil.newURI(unwrapped.uri);
- let tagItemId = insertionPoint.itemId;
- let tagTxn = new PlacesTagURITransaction(uri, [tagItemId]);
- transactions.push(tagTxn);
- }
- else {
- transactions.push(PlacesUIUtils.makeTransaction(unwrapped,
- flavor, insertionPoint.itemId,
- index, doCopy));
- }
- }
- let txn = new PlacesAggregatedTransaction("DropItems", transactions);
- PlacesUtils.transactionManager.doTransaction(txn);
- },
- /**
- * Checks if we can insert into a container.
- * @param aContainer
- * The container were we are want to drop
- */
- disallowInsertion: function(aContainer) {
- NS_ASSERT(aContainer, "empty container");
- // Allow dropping into Tag containers and editable folders.
- return !PlacesUtils.nodeIsTagQuery(aContainer) &&
- (!PlacesUtils.nodeIsFolder(aContainer) ||
- PlacesUIUtils.isContentsReadOnly(aContainer));
- },
- placesFlavors: [PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER,
- PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR,
- PlacesUtils.TYPE_X_MOZ_PLACE],
- // The order matters.
- GENERIC_VIEW_DROP_TYPES: [PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER,
- PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR,
- PlacesUtils.TYPE_X_MOZ_PLACE,
- PlacesUtils.TYPE_X_MOZ_URL,
- TAB_DROP_TYPE,
- PlacesUtils.TYPE_UNICODE],
- };
- XPCOMUtils.defineLazyServiceGetter(PlacesControllerDragHelper, "dragService",
- "@mozilla.org/widget/dragservice;1",
- "nsIDragService");
- function goUpdatePlacesCommands() {
- // Get the controller for one of the places commands.
- var placesController = doGetPlacesControllerForCommand("placesCmd_open");
- function updatePlacesCommand(aCommand) {
- goSetCommandEnabled(aCommand, placesController &&
- placesController.isCommandEnabled(aCommand));
- }
- updatePlacesCommand("placesCmd_open");
- updatePlacesCommand("placesCmd_open:window");
- updatePlacesCommand("placesCmd_open:privatewindow");
- updatePlacesCommand("placesCmd_open:tab");
- updatePlacesCommand("placesCmd_new:folder");
- updatePlacesCommand("placesCmd_new:bookmark");
- updatePlacesCommand("placesCmd_new:livemark");
- updatePlacesCommand("placesCmd_new:separator");
- updatePlacesCommand("placesCmd_show:info");
- updatePlacesCommand("placesCmd_moveBookmarks");
- updatePlacesCommand("placesCmd_reload");
- updatePlacesCommand("placesCmd_sortBy:name");
- updatePlacesCommand("placesCmd_openParentFolder");
- updatePlacesCommand("placesCmd_cut");
- updatePlacesCommand("placesCmd_copy");
- updatePlacesCommand("placesCmd_paste");
- updatePlacesCommand("placesCmd_delete");
- }
- function doGetPlacesControllerForCommand(aCommand)
- {
- // A context menu may be built for non-focusable views. Thus, we first try
- // to look for a view associated with document.popupNode
- let popupNode;
- try {
- popupNode = document.popupNode;
- } catch (e) {
- // The document went away (bug 797307).
- return null;
- }
- if (popupNode) {
- let view = PlacesUIUtils.getViewForNode(popupNode);
- if (view && view._contextMenuShown)
- return view.controllers.getControllerForCommand(aCommand);
- }
- // When we're not building a context menu, only focusable views
- // are possible. Thus, we can safely use the command dispatcher.
- let controller = top.document.commandDispatcher
- .getControllerForCommand(aCommand);
- if (controller)
- return controller;
- return null;
- }
- function goDoPlacesCommand(aCommand)
- {
- let controller = doGetPlacesControllerForCommand(aCommand);
- if (controller && controller.isCommandEnabled(aCommand))
- controller.doCommand(aCommand);
- }
|