browser/components/places/content/browserPlacesViews.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 6 Components.utils.import("resource://gre/modules/Services.jsm");
michael@0 7
michael@0 8 /**
michael@0 9 * The base view implements everything that's common to the toolbar and
michael@0 10 * menu views.
michael@0 11 */
michael@0 12 function PlacesViewBase(aPlace, aOptions) {
michael@0 13 this.place = aPlace;
michael@0 14 this.options = aOptions;
michael@0 15 this._controller = new PlacesController(this);
michael@0 16 this._viewElt.controllers.appendController(this._controller);
michael@0 17 }
michael@0 18
michael@0 19 PlacesViewBase.prototype = {
michael@0 20 // The xul element that holds the entire view.
michael@0 21 _viewElt: null,
michael@0 22 get viewElt() this._viewElt,
michael@0 23
michael@0 24 get associatedElement() this._viewElt,
michael@0 25
michael@0 26 get controllers() this._viewElt.controllers,
michael@0 27
michael@0 28 // The xul element that represents the root container.
michael@0 29 _rootElt: null,
michael@0 30
michael@0 31 // Set to true for views that are represented by native widgets (i.e.
michael@0 32 // the native mac menu).
michael@0 33 _nativeView: false,
michael@0 34
michael@0 35 QueryInterface: XPCOMUtils.generateQI(
michael@0 36 [Components.interfaces.nsINavHistoryResultObserver,
michael@0 37 Components.interfaces.nsISupportsWeakReference]),
michael@0 38
michael@0 39 _place: "",
michael@0 40 get place() this._place,
michael@0 41 set place(val) {
michael@0 42 this._place = val;
michael@0 43
michael@0 44 let history = PlacesUtils.history;
michael@0 45 let queries = { }, options = { };
michael@0 46 history.queryStringToQueries(val, queries, { }, options);
michael@0 47 if (!queries.value.length)
michael@0 48 queries.value = [history.getNewQuery()];
michael@0 49
michael@0 50 let result = history.executeQueries(queries.value, queries.value.length,
michael@0 51 options.value);
michael@0 52 result.addObserver(this, false);
michael@0 53 return val;
michael@0 54 },
michael@0 55
michael@0 56 _result: null,
michael@0 57 get result() this._result,
michael@0 58 set result(val) {
michael@0 59 if (this._result == val)
michael@0 60 return val;
michael@0 61
michael@0 62 if (this._result) {
michael@0 63 this._result.removeObserver(this);
michael@0 64 this._resultNode.containerOpen = false;
michael@0 65 }
michael@0 66
michael@0 67 if (this._rootElt.localName == "menupopup")
michael@0 68 this._rootElt._built = false;
michael@0 69
michael@0 70 this._result = val;
michael@0 71 if (val) {
michael@0 72 this._resultNode = val.root;
michael@0 73 this._rootElt._placesNode = this._resultNode;
michael@0 74 this._domNodes = new Map();
michael@0 75 this._domNodes.set(this._resultNode, this._rootElt);
michael@0 76
michael@0 77 // This calls _rebuild through invalidateContainer.
michael@0 78 this._resultNode.containerOpen = true;
michael@0 79 }
michael@0 80 else {
michael@0 81 this._resultNode = null;
michael@0 82 delete this._domNodes;
michael@0 83 }
michael@0 84
michael@0 85 return val;
michael@0 86 },
michael@0 87
michael@0 88 _options: null,
michael@0 89 get options() this._options,
michael@0 90 set options(val) {
michael@0 91 if (!val)
michael@0 92 val = {};
michael@0 93
michael@0 94 if (!("extraClasses" in val))
michael@0 95 val.extraClasses = {};
michael@0 96 this._options = val;
michael@0 97
michael@0 98 return val;
michael@0 99 },
michael@0 100
michael@0 101 /**
michael@0 102 * Gets the DOM node used for the given places node.
michael@0 103 *
michael@0 104 * @param aPlacesNode
michael@0 105 * a places result node.
michael@0 106 * @throws if there is no DOM node set for aPlacesNode.
michael@0 107 */
michael@0 108 _getDOMNodeForPlacesNode:
michael@0 109 function PVB__getDOMNodeForPlacesNode(aPlacesNode) {
michael@0 110 let node = this._domNodes.get(aPlacesNode, null);
michael@0 111 if (!node) {
michael@0 112 throw new Error("No DOM node set for aPlacesNode.\nnode.type: " +
michael@0 113 aPlacesNode.type + ". node.parent: " + aPlacesNode);
michael@0 114 }
michael@0 115 return node;
michael@0 116 },
michael@0 117
michael@0 118 get controller() this._controller,
michael@0 119
michael@0 120 get selType() "single",
michael@0 121 selectItems: function() { },
michael@0 122 selectAll: function() { },
michael@0 123
michael@0 124 get selectedNode() {
michael@0 125 if (this._contextMenuShown) {
michael@0 126 let popup = document.popupNode;
michael@0 127 return popup._placesNode || popup.parentNode._placesNode || null;
michael@0 128 }
michael@0 129 return null;
michael@0 130 },
michael@0 131
michael@0 132 get hasSelection() this.selectedNode != null,
michael@0 133
michael@0 134 get selectedNodes() {
michael@0 135 let selectedNode = this.selectedNode;
michael@0 136 return selectedNode ? [selectedNode] : [];
michael@0 137 },
michael@0 138
michael@0 139 get removableSelectionRanges() {
michael@0 140 // On static content the current selectedNode would be the selection's
michael@0 141 // parent node. We don't want to allow removing a node when the
michael@0 142 // selection is not explicit.
michael@0 143 if (document.popupNode &&
michael@0 144 (document.popupNode == "menupopup" || !document.popupNode._placesNode))
michael@0 145 return [];
michael@0 146
michael@0 147 return [this.selectedNodes];
michael@0 148 },
michael@0 149
michael@0 150 get draggableSelection() [this._draggedElt],
michael@0 151
michael@0 152 get insertionPoint() {
michael@0 153 // There is no insertion point for history queries, so bail out now and
michael@0 154 // save a lot of work when updating commands.
michael@0 155 let resultNode = this._resultNode;
michael@0 156 if (PlacesUtils.nodeIsQuery(resultNode) &&
michael@0 157 PlacesUtils.asQuery(resultNode).queryOptions.queryType ==
michael@0 158 Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY)
michael@0 159 return null;
michael@0 160
michael@0 161 // By default, the insertion point is at the top level, at the end.
michael@0 162 let index = PlacesUtils.bookmarks.DEFAULT_INDEX;
michael@0 163 let container = this._resultNode;
michael@0 164 let orientation = Ci.nsITreeView.DROP_BEFORE;
michael@0 165 let isTag = false;
michael@0 166
michael@0 167 let selectedNode = this.selectedNode;
michael@0 168 if (selectedNode) {
michael@0 169 let popup = document.popupNode;
michael@0 170 if (!popup._placesNode || popup._placesNode == this._resultNode ||
michael@0 171 popup._placesNode.itemId == -1) {
michael@0 172 // If a static menuitem is selected, or if the root node is selected,
michael@0 173 // the insertion point is inside the folder, at the end.
michael@0 174 container = selectedNode;
michael@0 175 orientation = Ci.nsITreeView.DROP_ON;
michael@0 176 }
michael@0 177 else {
michael@0 178 // In all other cases the insertion point is before that node.
michael@0 179 container = selectedNode.parent;
michael@0 180 index = container.getChildIndex(selectedNode);
michael@0 181 isTag = PlacesUtils.nodeIsTagQuery(container);
michael@0 182 }
michael@0 183 }
michael@0 184
michael@0 185 if (PlacesControllerDragHelper.disallowInsertion(container))
michael@0 186 return null;
michael@0 187
michael@0 188 return new InsertionPoint(PlacesUtils.getConcreteItemId(container),
michael@0 189 index, orientation, isTag);
michael@0 190 },
michael@0 191
michael@0 192 buildContextMenu: function PVB_buildContextMenu(aPopup) {
michael@0 193 this._contextMenuShown = true;
michael@0 194 window.updateCommands("places");
michael@0 195 return this.controller.buildContextMenu(aPopup);
michael@0 196 },
michael@0 197
michael@0 198 destroyContextMenu: function PVB_destroyContextMenu(aPopup) {
michael@0 199 this._contextMenuShown = false;
michael@0 200 },
michael@0 201
michael@0 202 _cleanPopup: function PVB_cleanPopup(aPopup, aDelay) {
michael@0 203 // Remove Places nodes from the popup.
michael@0 204 let child = aPopup._startMarker;
michael@0 205 while (child.nextSibling != aPopup._endMarker) {
michael@0 206 let sibling = child.nextSibling;
michael@0 207 if (sibling._placesNode && !aDelay) {
michael@0 208 aPopup.removeChild(sibling);
michael@0 209 }
michael@0 210 else if (sibling._placesNode && aDelay) {
michael@0 211 // HACK (bug 733419): the popups originating from the OS X native
michael@0 212 // menubar don't live-update while open, thus we don't clean it
michael@0 213 // until the next popupshowing, to avoid zombie menuitems.
michael@0 214 if (!aPopup._delayedRemovals)
michael@0 215 aPopup._delayedRemovals = [];
michael@0 216 aPopup._delayedRemovals.push(sibling);
michael@0 217 child = child.nextSibling;
michael@0 218 }
michael@0 219 else {
michael@0 220 child = child.nextSibling;
michael@0 221 }
michael@0 222 }
michael@0 223 },
michael@0 224
michael@0 225 _rebuildPopup: function PVB__rebuildPopup(aPopup) {
michael@0 226 let resultNode = aPopup._placesNode;
michael@0 227 if (!resultNode.containerOpen)
michael@0 228 return;
michael@0 229
michael@0 230 if (this.controller.hasCachedLivemarkInfo(resultNode)) {
michael@0 231 this._setEmptyPopupStatus(aPopup, false);
michael@0 232 aPopup._built = true;
michael@0 233 this._populateLivemarkPopup(aPopup);
michael@0 234 return;
michael@0 235 }
michael@0 236
michael@0 237 this._cleanPopup(aPopup);
michael@0 238
michael@0 239 let cc = resultNode.childCount;
michael@0 240 if (cc > 0) {
michael@0 241 this._setEmptyPopupStatus(aPopup, false);
michael@0 242
michael@0 243 for (let i = 0; i < cc; ++i) {
michael@0 244 let child = resultNode.getChild(i);
michael@0 245 this._insertNewItemToPopup(child, aPopup, null);
michael@0 246 }
michael@0 247 }
michael@0 248 else {
michael@0 249 this._setEmptyPopupStatus(aPopup, true);
michael@0 250 }
michael@0 251 aPopup._built = true;
michael@0 252 },
michael@0 253
michael@0 254 _removeChild: function PVB__removeChild(aChild) {
michael@0 255 // If document.popupNode pointed to this child, null it out,
michael@0 256 // otherwise controller's command-updating may rely on the removed
michael@0 257 // item still being "selected".
michael@0 258 if (document.popupNode == aChild)
michael@0 259 document.popupNode = null;
michael@0 260
michael@0 261 aChild.parentNode.removeChild(aChild);
michael@0 262 },
michael@0 263
michael@0 264 _setEmptyPopupStatus:
michael@0 265 function PVB__setEmptyPopupStatus(aPopup, aEmpty) {
michael@0 266 if (!aPopup._emptyMenuitem) {
michael@0 267 let label = PlacesUIUtils.getString("bookmarksMenuEmptyFolder");
michael@0 268 aPopup._emptyMenuitem = document.createElement("menuitem");
michael@0 269 aPopup._emptyMenuitem.setAttribute("label", label);
michael@0 270 aPopup._emptyMenuitem.setAttribute("disabled", true);
michael@0 271 aPopup._emptyMenuitem.className = "bookmark-item";
michael@0 272 if (typeof this.options.extraClasses.entry == "string")
michael@0 273 aPopup._emptyMenuitem.classList.add(this.options.extraClasses.entry);
michael@0 274 }
michael@0 275
michael@0 276 if (aEmpty) {
michael@0 277 aPopup.setAttribute("emptyplacesresult", "true");
michael@0 278 // Don't add the menuitem if there is static content.
michael@0 279 if (!aPopup._startMarker.previousSibling &&
michael@0 280 !aPopup._endMarker.nextSibling)
michael@0 281 aPopup.insertBefore(aPopup._emptyMenuitem, aPopup._endMarker);
michael@0 282 }
michael@0 283 else {
michael@0 284 aPopup.removeAttribute("emptyplacesresult");
michael@0 285 try {
michael@0 286 aPopup.removeChild(aPopup._emptyMenuitem);
michael@0 287 } catch (ex) {}
michael@0 288 }
michael@0 289 },
michael@0 290
michael@0 291 _createMenuItemForPlacesNode:
michael@0 292 function PVB__createMenuItemForPlacesNode(aPlacesNode) {
michael@0 293 this._domNodes.delete(aPlacesNode);
michael@0 294
michael@0 295 let element;
michael@0 296 let type = aPlacesNode.type;
michael@0 297 if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
michael@0 298 element = document.createElement("menuseparator");
michael@0 299 element.setAttribute("class", "small-separator");
michael@0 300 }
michael@0 301 else {
michael@0 302 let itemId = aPlacesNode.itemId;
michael@0 303 if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_URI) {
michael@0 304 element = document.createElement("menuitem");
michael@0 305 element.className = "menuitem-iconic bookmark-item menuitem-with-favicon";
michael@0 306 element.setAttribute("scheme",
michael@0 307 PlacesUIUtils.guessUrlSchemeForUI(aPlacesNode.uri));
michael@0 308 }
michael@0 309 else if (PlacesUtils.containerTypes.indexOf(type) != -1) {
michael@0 310 element = document.createElement("menu");
michael@0 311 element.setAttribute("container", "true");
michael@0 312
michael@0 313 if (aPlacesNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY) {
michael@0 314 element.setAttribute("query", "true");
michael@0 315 if (PlacesUtils.nodeIsTagQuery(aPlacesNode))
michael@0 316 element.setAttribute("tagContainer", "true");
michael@0 317 else if (PlacesUtils.nodeIsDay(aPlacesNode))
michael@0 318 element.setAttribute("dayContainer", "true");
michael@0 319 else if (PlacesUtils.nodeIsHost(aPlacesNode))
michael@0 320 element.setAttribute("hostContainer", "true");
michael@0 321 }
michael@0 322 else if (itemId != -1) {
michael@0 323 PlacesUtils.livemarks.getLivemark({ id: itemId })
michael@0 324 .then(aLivemark => {
michael@0 325 element.setAttribute("livemark", "true");
michael@0 326 #ifdef XP_MACOSX
michael@0 327 // OS X native menubar doesn't track list-style-images since
michael@0 328 // it doesn't have a frame (bug 733415). Thus enforce updating.
michael@0 329 element.setAttribute("image", "");
michael@0 330 element.removeAttribute("image");
michael@0 331 #endif
michael@0 332 this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
michael@0 333 }, () => undefined);
michael@0 334 }
michael@0 335
michael@0 336 let popup = document.createElement("menupopup");
michael@0 337 popup._placesNode = PlacesUtils.asContainer(aPlacesNode);
michael@0 338
michael@0 339 if (!this._nativeView) {
michael@0 340 popup.setAttribute("placespopup", "true");
michael@0 341 }
michael@0 342
michael@0 343 element.appendChild(popup);
michael@0 344 element.className = "menu-iconic bookmark-item";
michael@0 345 if (typeof this.options.extraClasses.entry == "string") {
michael@0 346 element.classList.add(this.options.extraClasses.entry);
michael@0 347 }
michael@0 348
michael@0 349 this._domNodes.set(aPlacesNode, popup);
michael@0 350 }
michael@0 351 else
michael@0 352 throw "Unexpected node";
michael@0 353
michael@0 354 element.setAttribute("label", PlacesUIUtils.getBestTitle(aPlacesNode));
michael@0 355
michael@0 356 let icon = aPlacesNode.icon;
michael@0 357 if (icon)
michael@0 358 element.setAttribute("image", icon);
michael@0 359 }
michael@0 360
michael@0 361 element._placesNode = aPlacesNode;
michael@0 362 if (!this._domNodes.has(aPlacesNode))
michael@0 363 this._domNodes.set(aPlacesNode, element);
michael@0 364
michael@0 365 return element;
michael@0 366 },
michael@0 367
michael@0 368 _insertNewItemToPopup:
michael@0 369 function PVB__insertNewItemToPopup(aNewChild, aPopup, aBefore) {
michael@0 370 let element = this._createMenuItemForPlacesNode(aNewChild);
michael@0 371 let before = aBefore || aPopup._endMarker;
michael@0 372
michael@0 373 if (element.localName == "menuitem" || element.localName == "menu") {
michael@0 374 if (typeof this.options.extraClasses.entry == "string")
michael@0 375 element.classList.add(this.options.extraClasses.entry);
michael@0 376 }
michael@0 377
michael@0 378 aPopup.insertBefore(element, before);
michael@0 379 return element;
michael@0 380 },
michael@0 381
michael@0 382 _setLivemarkSiteURIMenuItem:
michael@0 383 function PVB__setLivemarkSiteURIMenuItem(aPopup) {
michael@0 384 let livemarkInfo = this.controller.getCachedLivemarkInfo(aPopup._placesNode);
michael@0 385 let siteUrl = livemarkInfo && livemarkInfo.siteURI ?
michael@0 386 livemarkInfo.siteURI.spec : null;
michael@0 387 if (!siteUrl && aPopup._siteURIMenuitem) {
michael@0 388 aPopup.removeChild(aPopup._siteURIMenuitem);
michael@0 389 aPopup._siteURIMenuitem = null;
michael@0 390 aPopup.removeChild(aPopup._siteURIMenuseparator);
michael@0 391 aPopup._siteURIMenuseparator = null;
michael@0 392 }
michael@0 393 else if (siteUrl && !aPopup._siteURIMenuitem) {
michael@0 394 // Add "Open (Feed Name)" menuitem.
michael@0 395 aPopup._siteURIMenuitem = document.createElement("menuitem");
michael@0 396 aPopup._siteURIMenuitem.className = "openlivemarksite-menuitem";
michael@0 397 if (typeof this.options.extraClasses.entry == "string") {
michael@0 398 aPopup._siteURIMenuitem.classList.add(this.options.extraClasses.entry);
michael@0 399 }
michael@0 400 aPopup._siteURIMenuitem.setAttribute("targetURI", siteUrl);
michael@0 401 aPopup._siteURIMenuitem.setAttribute("oncommand",
michael@0 402 "openUILink(this.getAttribute('targetURI'), event);");
michael@0 403
michael@0 404 // If a user middle-clicks this item we serve the oncommand event.
michael@0 405 // We are using checkForMiddleClick because of Bug 246720.
michael@0 406 // Note: stopPropagation is needed to avoid serving middle-click
michael@0 407 // with BT_onClick that would open all items in tabs.
michael@0 408 aPopup._siteURIMenuitem.setAttribute("onclick",
michael@0 409 "checkForMiddleClick(this, event); event.stopPropagation();");
michael@0 410 let label =
michael@0 411 PlacesUIUtils.getFormattedString("menuOpenLivemarkOrigin.label",
michael@0 412 [aPopup.parentNode.getAttribute("label")])
michael@0 413 aPopup._siteURIMenuitem.setAttribute("label", label);
michael@0 414 aPopup.insertBefore(aPopup._siteURIMenuitem, aPopup._startMarker);
michael@0 415
michael@0 416 aPopup._siteURIMenuseparator = document.createElement("menuseparator");
michael@0 417 aPopup.insertBefore(aPopup._siteURIMenuseparator, aPopup._startMarker);
michael@0 418 }
michael@0 419 },
michael@0 420
michael@0 421 /**
michael@0 422 * Add, update or remove the livemark status menuitem.
michael@0 423 * @param aPopup
michael@0 424 * The livemark container popup
michael@0 425 * @param aStatus
michael@0 426 * The livemark status
michael@0 427 */
michael@0 428 _setLivemarkStatusMenuItem:
michael@0 429 function PVB_setLivemarkStatusMenuItem(aPopup, aStatus) {
michael@0 430 let statusMenuitem = aPopup._statusMenuitem;
michael@0 431 if (!statusMenuitem) {
michael@0 432 // Create the status menuitem and cache it in the popup object.
michael@0 433 statusMenuitem = document.createElement("menuitem");
michael@0 434 statusMenuitem.className = "livemarkstatus-menuitem";
michael@0 435 if (typeof this.options.extraClasses.entry == "string") {
michael@0 436 statusMenuitem.classList.add(this.options.extraClasses.entry);
michael@0 437 }
michael@0 438 statusMenuitem.setAttribute("disabled", true);
michael@0 439 aPopup._statusMenuitem = statusMenuitem;
michael@0 440 }
michael@0 441
michael@0 442 if (aStatus == Ci.mozILivemark.STATUS_LOADING ||
michael@0 443 aStatus == Ci.mozILivemark.STATUS_FAILED) {
michael@0 444 // Status has changed, update the cached status menuitem.
michael@0 445 let stringId = aStatus == Ci.mozILivemark.STATUS_LOADING ?
michael@0 446 "bookmarksLivemarkLoading" : "bookmarksLivemarkFailed";
michael@0 447 statusMenuitem.setAttribute("label", PlacesUIUtils.getString(stringId));
michael@0 448 if (aPopup._startMarker.nextSibling != statusMenuitem)
michael@0 449 aPopup.insertBefore(statusMenuitem, aPopup._startMarker.nextSibling);
michael@0 450 }
michael@0 451 else {
michael@0 452 // The livemark has finished loading.
michael@0 453 if (aPopup._statusMenuitem.parentNode == aPopup)
michael@0 454 aPopup.removeChild(aPopup._statusMenuitem);
michael@0 455 }
michael@0 456 },
michael@0 457
michael@0 458 toggleCutNode: function PVB_toggleCutNode(aPlacesNode, aValue) {
michael@0 459 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
michael@0 460
michael@0 461 // We may get the popup for menus, but we need the menu itself.
michael@0 462 if (elt.localName == "menupopup")
michael@0 463 elt = elt.parentNode;
michael@0 464 if (aValue)
michael@0 465 elt.setAttribute("cutting", "true");
michael@0 466 else
michael@0 467 elt.removeAttribute("cutting");
michael@0 468 },
michael@0 469
michael@0 470 nodeURIChanged: function PVB_nodeURIChanged(aPlacesNode, aURIString) {
michael@0 471 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
michael@0 472
michael@0 473 // Here we need the <menu>.
michael@0 474 if (elt.localName == "menupopup")
michael@0 475 elt = elt.parentNode;
michael@0 476
michael@0 477 elt.setAttribute("scheme", PlacesUIUtils.guessUrlSchemeForUI(aURIString));
michael@0 478 },
michael@0 479
michael@0 480 nodeIconChanged: function PVB_nodeIconChanged(aPlacesNode) {
michael@0 481 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
michael@0 482
michael@0 483 // There's no UI representation for the root node, thus there's nothing to
michael@0 484 // be done when the icon changes.
michael@0 485 if (elt == this._rootElt)
michael@0 486 return;
michael@0 487
michael@0 488 // Here we need the <menu>.
michael@0 489 if (elt.localName == "menupopup")
michael@0 490 elt = elt.parentNode;
michael@0 491
michael@0 492 let icon = aPlacesNode.icon;
michael@0 493 if (!icon)
michael@0 494 elt.removeAttribute("image");
michael@0 495 else if (icon != elt.getAttribute("image"))
michael@0 496 elt.setAttribute("image", icon);
michael@0 497 },
michael@0 498
michael@0 499 nodeAnnotationChanged:
michael@0 500 function PVB_nodeAnnotationChanged(aPlacesNode, aAnno) {
michael@0 501 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
michael@0 502
michael@0 503 // All livemarks have a feedURI, so use it as our indicator of a livemark
michael@0 504 // being modified.
michael@0 505 if (aAnno == PlacesUtils.LMANNO_FEEDURI) {
michael@0 506 let menu = elt.parentNode;
michael@0 507 if (!menu.hasAttribute("livemark")) {
michael@0 508 menu.setAttribute("livemark", "true");
michael@0 509 #ifdef XP_MACOSX
michael@0 510 // OS X native menubar doesn't track list-style-images since
michael@0 511 // it doesn't have a frame (bug 733415). Thus enforce updating.
michael@0 512 menu.setAttribute("image", "");
michael@0 513 menu.removeAttribute("image");
michael@0 514 #endif
michael@0 515 }
michael@0 516
michael@0 517 PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId })
michael@0 518 .then(aLivemark => {
michael@0 519 // Controller will use this to build the meta data for the node.
michael@0 520 this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
michael@0 521 this.invalidateContainer(aPlacesNode);
michael@0 522 }, () => undefined);
michael@0 523 }
michael@0 524 },
michael@0 525
michael@0 526 nodeTitleChanged:
michael@0 527 function PVB_nodeTitleChanged(aPlacesNode, aNewTitle) {
michael@0 528 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
michael@0 529
michael@0 530 // There's no UI representation for the root node, thus there's
michael@0 531 // nothing to be done when the title changes.
michael@0 532 if (elt == this._rootElt)
michael@0 533 return;
michael@0 534
michael@0 535 // Here we need the <menu>.
michael@0 536 if (elt.localName == "menupopup")
michael@0 537 elt = elt.parentNode;
michael@0 538
michael@0 539 if (!aNewTitle && elt.localName != "toolbarbutton") {
michael@0 540 // Many users consider toolbars as shortcuts containers, so explicitly
michael@0 541 // allow empty labels on toolbarbuttons. For any other element try to be
michael@0 542 // smarter, guessing a title from the uri.
michael@0 543 elt.setAttribute("label", PlacesUIUtils.getBestTitle(aPlacesNode));
michael@0 544 }
michael@0 545 else {
michael@0 546 elt.setAttribute("label", aNewTitle);
michael@0 547 }
michael@0 548 },
michael@0 549
michael@0 550 nodeRemoved:
michael@0 551 function PVB_nodeRemoved(aParentPlacesNode, aPlacesNode, aIndex) {
michael@0 552 let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
michael@0 553 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
michael@0 554
michael@0 555 // Here we need the <menu>.
michael@0 556 if (elt.localName == "menupopup")
michael@0 557 elt = elt.parentNode;
michael@0 558
michael@0 559 if (parentElt._built) {
michael@0 560 parentElt.removeChild(elt);
michael@0 561
michael@0 562 // Figure out if we need to show the "<Empty>" menu-item.
michael@0 563 // TODO Bug 517701: This doesn't seem to handle the case of an empty
michael@0 564 // root.
michael@0 565 if (parentElt._startMarker.nextSibling == parentElt._endMarker)
michael@0 566 this._setEmptyPopupStatus(parentElt, true);
michael@0 567 }
michael@0 568 },
michael@0 569
michael@0 570 nodeHistoryDetailsChanged:
michael@0 571 function PVB_nodeHistoryDetailsChanged(aPlacesNode, aTime, aCount) {
michael@0 572 if (aPlacesNode.parent &&
michael@0 573 this.controller.hasCachedLivemarkInfo(aPlacesNode.parent)) {
michael@0 574 // Find the node in the parent.
michael@0 575 let popup = this._getDOMNodeForPlacesNode(aPlacesNode.parent);
michael@0 576 for (let child = popup._startMarker.nextSibling;
michael@0 577 child != popup._endMarker;
michael@0 578 child = child.nextSibling) {
michael@0 579 if (child._placesNode && child._placesNode.uri == aPlacesNode.uri) {
michael@0 580 if (aCount)
michael@0 581 child.setAttribute("visited", "true");
michael@0 582 else
michael@0 583 child.removeAttribute("visited");
michael@0 584 break;
michael@0 585 }
michael@0 586 }
michael@0 587 }
michael@0 588 },
michael@0 589
michael@0 590 nodeTagsChanged: function() { },
michael@0 591 nodeDateAddedChanged: function() { },
michael@0 592 nodeLastModifiedChanged: function() { },
michael@0 593 nodeKeywordChanged: function() { },
michael@0 594 sortingChanged: function() { },
michael@0 595 batching: function() { },
michael@0 596
michael@0 597 nodeInserted:
michael@0 598 function PVB_nodeInserted(aParentPlacesNode, aPlacesNode, aIndex) {
michael@0 599 let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
michael@0 600 if (!parentElt._built)
michael@0 601 return;
michael@0 602
michael@0 603 let index = Array.indexOf(parentElt.childNodes, parentElt._startMarker) +
michael@0 604 aIndex + 1;
michael@0 605 this._insertNewItemToPopup(aPlacesNode, parentElt,
michael@0 606 parentElt.childNodes[index]);
michael@0 607 this._setEmptyPopupStatus(parentElt, false);
michael@0 608 },
michael@0 609
michael@0 610 nodeMoved:
michael@0 611 function PBV_nodeMoved(aPlacesNode,
michael@0 612 aOldParentPlacesNode, aOldIndex,
michael@0 613 aNewParentPlacesNode, aNewIndex) {
michael@0 614 // Note: the current implementation of moveItem does not actually
michael@0 615 // use this notification when the item in question is moved from one
michael@0 616 // folder to another. Instead, it calls nodeRemoved and nodeInserted
michael@0 617 // for the two folders. Thus, we can assume old-parent == new-parent.
michael@0 618 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
michael@0 619
michael@0 620 // Here we need the <menu>.
michael@0 621 if (elt.localName == "menupopup")
michael@0 622 elt = elt.parentNode;
michael@0 623
michael@0 624 // If our root node is a folder, it might be moved. There's nothing
michael@0 625 // we need to do in that case.
michael@0 626 if (elt == this._rootElt)
michael@0 627 return;
michael@0 628
michael@0 629 let parentElt = this._getDOMNodeForPlacesNode(aNewParentPlacesNode);
michael@0 630 if (parentElt._built) {
michael@0 631 // Move the node.
michael@0 632 parentElt.removeChild(elt);
michael@0 633 let index = Array.indexOf(parentElt.childNodes, parentElt._startMarker) +
michael@0 634 aNewIndex + 1;
michael@0 635 parentElt.insertBefore(elt, parentElt.childNodes[index]);
michael@0 636 }
michael@0 637 },
michael@0 638
michael@0 639 containerStateChanged:
michael@0 640 function PVB_containerStateChanged(aPlacesNode, aOldState, aNewState) {
michael@0 641 if (aNewState == Ci.nsINavHistoryContainerResultNode.STATE_OPENED ||
michael@0 642 aNewState == Ci.nsINavHistoryContainerResultNode.STATE_CLOSED) {
michael@0 643 this.invalidateContainer(aPlacesNode);
michael@0 644
michael@0 645 if (PlacesUtils.nodeIsFolder(aPlacesNode)) {
michael@0 646 let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions;
michael@0 647 if (queryOptions.excludeItems) {
michael@0 648 return;
michael@0 649 }
michael@0 650
michael@0 651 PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId })
michael@0 652 .then(aLivemark => {
michael@0 653 let shouldInvalidate =
michael@0 654 !this.controller.hasCachedLivemarkInfo(aPlacesNode);
michael@0 655 this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
michael@0 656 if (aNewState == Ci.nsINavHistoryContainerResultNode.STATE_OPENED) {
michael@0 657 aLivemark.registerForUpdates(aPlacesNode, this);
michael@0 658 // Prioritize the current livemark.
michael@0 659 aLivemark.reload();
michael@0 660 PlacesUtils.livemarks.reloadLivemarks();
michael@0 661 if (shouldInvalidate)
michael@0 662 this.invalidateContainer(aPlacesNode);
michael@0 663 }
michael@0 664 else {
michael@0 665 aLivemark.unregisterForUpdates(aPlacesNode);
michael@0 666 }
michael@0 667 }, () => undefined);
michael@0 668 }
michael@0 669 }
michael@0 670 },
michael@0 671
michael@0 672 _populateLivemarkPopup: function PVB__populateLivemarkPopup(aPopup)
michael@0 673 {
michael@0 674 this._setLivemarkSiteURIMenuItem(aPopup);
michael@0 675 // Show the loading status only if there are no entries yet.
michael@0 676 if (aPopup._startMarker.nextSibling == aPopup._endMarker)
michael@0 677 this._setLivemarkStatusMenuItem(aPopup, Ci.mozILivemark.STATUS_LOADING);
michael@0 678
michael@0 679 PlacesUtils.livemarks.getLivemark({ id: aPopup._placesNode.itemId })
michael@0 680 .then(aLivemark => {
michael@0 681 let placesNode = aPopup._placesNode;
michael@0 682 if (!placesNode.containerOpen)
michael@0 683 return;
michael@0 684
michael@0 685 if (aLivemark.status != Ci.mozILivemark.STATUS_LOADING)
michael@0 686 this._setLivemarkStatusMenuItem(aPopup, aLivemark.status);
michael@0 687 this._cleanPopup(aPopup,
michael@0 688 this._nativeView && aPopup.parentNode.hasAttribute("open"));
michael@0 689
michael@0 690 let children = aLivemark.getNodesForContainer(placesNode);
michael@0 691 for (let i = 0; i < children.length; i++) {
michael@0 692 let child = children[i];
michael@0 693 this.nodeInserted(placesNode, child, i);
michael@0 694 if (child.accessCount)
michael@0 695 this._getDOMNodeForPlacesNode(child).setAttribute("visited", true);
michael@0 696 else
michael@0 697 this._getDOMNodeForPlacesNode(child).removeAttribute("visited");
michael@0 698 }
michael@0 699 }, Components.utils.reportError);
michael@0 700 },
michael@0 701
michael@0 702 invalidateContainer: function PVB_invalidateContainer(aPlacesNode) {
michael@0 703 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
michael@0 704 elt._built = false;
michael@0 705
michael@0 706 // If the menupopup is open we should live-update it.
michael@0 707 if (elt.parentNode.open)
michael@0 708 this._rebuildPopup(elt);
michael@0 709 },
michael@0 710
michael@0 711 uninit: function PVB_uninit() {
michael@0 712 if (this._result) {
michael@0 713 this._result.removeObserver(this);
michael@0 714 this._resultNode.containerOpen = false;
michael@0 715 this._resultNode = null;
michael@0 716 this._result = null;
michael@0 717 }
michael@0 718
michael@0 719 if (this._controller) {
michael@0 720 this._controller.terminate();
michael@0 721 // Removing the controller will fail if it is already no longer there.
michael@0 722 // This can happen if the view element was removed/reinserted without
michael@0 723 // our knowledge. There is no way to check for that having happened
michael@0 724 // without the possibility of an exception. :-(
michael@0 725 try {
michael@0 726 this._viewElt.controllers.removeController(this._controller);
michael@0 727 } catch (ex) {
michael@0 728 } finally {
michael@0 729 this._controller = null;
michael@0 730 }
michael@0 731 }
michael@0 732
michael@0 733 delete this._viewElt._placesView;
michael@0 734 },
michael@0 735
michael@0 736 get isRTL() {
michael@0 737 if ("_isRTL" in this)
michael@0 738 return this._isRTL;
michael@0 739
michael@0 740 return this._isRTL = document.defaultView
michael@0 741 .getComputedStyle(this.viewElt, "")
michael@0 742 .direction == "rtl";
michael@0 743 },
michael@0 744
michael@0 745 get ownerWindow() window,
michael@0 746
michael@0 747 /**
michael@0 748 * Adds an "Open All in Tabs" menuitem to the bottom of the popup.
michael@0 749 * @param aPopup
michael@0 750 * a Places popup.
michael@0 751 */
michael@0 752 _mayAddCommandsItems: function PVB__mayAddCommandsItems(aPopup) {
michael@0 753 // The command items are never added to the root popup.
michael@0 754 if (aPopup == this._rootElt)
michael@0 755 return;
michael@0 756
michael@0 757 let hasMultipleURIs = false;
michael@0 758
michael@0 759 // Check if the popup contains at least 2 menuitems with places nodes.
michael@0 760 // We don't currently support opening multiple uri nodes when they are not
michael@0 761 // populated by the result.
michael@0 762 if (aPopup._placesNode.childCount > 0) {
michael@0 763 let currentChild = aPopup.firstChild;
michael@0 764 let numURINodes = 0;
michael@0 765 while (currentChild) {
michael@0 766 if (currentChild.localName == "menuitem" && currentChild._placesNode) {
michael@0 767 if (++numURINodes == 2)
michael@0 768 break;
michael@0 769 }
michael@0 770 currentChild = currentChild.nextSibling;
michael@0 771 }
michael@0 772 hasMultipleURIs = numURINodes > 1;
michael@0 773 }
michael@0 774
michael@0 775 if (!hasMultipleURIs) {
michael@0 776 aPopup.setAttribute("singleitempopup", "true");
michael@0 777 } else {
michael@0 778 aPopup.removeAttribute("singleitempopup");
michael@0 779 }
michael@0 780
michael@0 781 if (!hasMultipleURIs) {
michael@0 782 // We don't have to show any option.
michael@0 783 if (aPopup._endOptOpenAllInTabs) {
michael@0 784 aPopup.removeChild(aPopup._endOptOpenAllInTabs);
michael@0 785 aPopup._endOptOpenAllInTabs = null;
michael@0 786
michael@0 787 aPopup.removeChild(aPopup._endOptSeparator);
michael@0 788 aPopup._endOptSeparator = null;
michael@0 789 }
michael@0 790 }
michael@0 791 else if (!aPopup._endOptOpenAllInTabs) {
michael@0 792 // Create a separator before options.
michael@0 793 aPopup._endOptSeparator = document.createElement("menuseparator");
michael@0 794 aPopup._endOptSeparator.className = "bookmarks-actions-menuseparator";
michael@0 795 aPopup.appendChild(aPopup._endOptSeparator);
michael@0 796
michael@0 797 // Add the "Open All in Tabs" menuitem.
michael@0 798 aPopup._endOptOpenAllInTabs = document.createElement("menuitem");
michael@0 799 aPopup._endOptOpenAllInTabs.className = "openintabs-menuitem";
michael@0 800
michael@0 801 if (typeof this.options.extraClasses.entry == "string")
michael@0 802 aPopup._endOptOpenAllInTabs.classList.add(this.options.extraClasses.entry);
michael@0 803 if (typeof this.options.extraClasses.footer == "string")
michael@0 804 aPopup._endOptOpenAllInTabs.classList.add(this.options.extraClasses.footer);
michael@0 805
michael@0 806 aPopup._endOptOpenAllInTabs.setAttribute("oncommand",
michael@0 807 "PlacesUIUtils.openContainerNodeInTabs(this.parentNode._placesNode, event, " +
michael@0 808 "PlacesUIUtils.getViewForNode(this));");
michael@0 809 aPopup._endOptOpenAllInTabs.setAttribute("onclick",
michael@0 810 "checkForMiddleClick(this, event); event.stopPropagation();");
michael@0 811 aPopup._endOptOpenAllInTabs.setAttribute("label",
michael@0 812 gNavigatorBundle.getString("menuOpenAllInTabs.label"));
michael@0 813 aPopup.appendChild(aPopup._endOptOpenAllInTabs);
michael@0 814 }
michael@0 815 },
michael@0 816
michael@0 817 _ensureMarkers: function PVB__ensureMarkers(aPopup) {
michael@0 818 if (aPopup._startMarker)
michael@0 819 return;
michael@0 820
michael@0 821 // _startMarker is an hidden menuseparator that lives before places nodes.
michael@0 822 aPopup._startMarker = document.createElement("menuseparator");
michael@0 823 aPopup._startMarker.hidden = true;
michael@0 824 aPopup.insertBefore(aPopup._startMarker, aPopup.firstChild);
michael@0 825
michael@0 826 // _endMarker is a DOM node that lives after places nodes, specified with
michael@0 827 // the 'insertionPoint' option or will be a hidden menuseparator.
michael@0 828 let node = ("insertionPoint" in this.options) ?
michael@0 829 aPopup.querySelector(this.options.insertionPoint) : null;
michael@0 830 if (node) {
michael@0 831 aPopup._endMarker = node;
michael@0 832 } else {
michael@0 833 aPopup._endMarker = document.createElement("menuseparator");
michael@0 834 aPopup._endMarker.hidden = true;
michael@0 835 }
michael@0 836 aPopup.appendChild(aPopup._endMarker);
michael@0 837
michael@0 838 // Move the markers to the right position.
michael@0 839 let firstNonStaticNodeFound = false;
michael@0 840 for (let i = 0; i < aPopup.childNodes.length; i++) {
michael@0 841 let child = aPopup.childNodes[i];
michael@0 842 // Menus that have static content at the end, but are initially empty,
michael@0 843 // use a special "builder" attribute to figure out where to start
michael@0 844 // inserting places nodes.
michael@0 845 if (child.getAttribute("builder") == "end") {
michael@0 846 aPopup.insertBefore(aPopup._endMarker, child);
michael@0 847 break;
michael@0 848 }
michael@0 849
michael@0 850 if (child._placesNode && !firstNonStaticNodeFound) {
michael@0 851 firstNonStaticNodeFound = true;
michael@0 852 aPopup.insertBefore(aPopup._startMarker, child);
michael@0 853 }
michael@0 854 }
michael@0 855 if (!firstNonStaticNodeFound) {
michael@0 856 aPopup.insertBefore(aPopup._startMarker, aPopup._endMarker);
michael@0 857 }
michael@0 858 },
michael@0 859
michael@0 860 _onPopupShowing: function PVB__onPopupShowing(aEvent) {
michael@0 861 // Avoid handling popupshowing of inner views.
michael@0 862 let popup = aEvent.originalTarget;
michael@0 863
michael@0 864 this._ensureMarkers(popup);
michael@0 865
michael@0 866 // Remove any delayed element, see _cleanPopup for details.
michael@0 867 if ("_delayedRemovals" in popup) {
michael@0 868 while (popup._delayedRemovals.length > 0) {
michael@0 869 popup.removeChild(popup._delayedRemovals.shift());
michael@0 870 }
michael@0 871 }
michael@0 872
michael@0 873 if (popup._placesNode && PlacesUIUtils.getViewForNode(popup) == this) {
michael@0 874 if (!popup._placesNode.containerOpen)
michael@0 875 popup._placesNode.containerOpen = true;
michael@0 876 if (!popup._built)
michael@0 877 this._rebuildPopup(popup);
michael@0 878
michael@0 879 this._mayAddCommandsItems(popup);
michael@0 880 }
michael@0 881 },
michael@0 882
michael@0 883 _addEventListeners:
michael@0 884 function PVB__addEventListeners(aObject, aEventNames, aCapturing) {
michael@0 885 for (let i = 0; i < aEventNames.length; i++) {
michael@0 886 aObject.addEventListener(aEventNames[i], this, aCapturing);
michael@0 887 }
michael@0 888 },
michael@0 889
michael@0 890 _removeEventListeners:
michael@0 891 function PVB__removeEventListeners(aObject, aEventNames, aCapturing) {
michael@0 892 for (let i = 0; i < aEventNames.length; i++) {
michael@0 893 aObject.removeEventListener(aEventNames[i], this, aCapturing);
michael@0 894 }
michael@0 895 },
michael@0 896 };
michael@0 897
michael@0 898 function PlacesToolbar(aPlace) {
michael@0 899 let startTime = Date.now();
michael@0 900 // Add some smart getters for our elements.
michael@0 901 let thisView = this;
michael@0 902 [
michael@0 903 ["_viewElt", "PlacesToolbar"],
michael@0 904 ["_rootElt", "PlacesToolbarItems"],
michael@0 905 ["_dropIndicator", "PlacesToolbarDropIndicator"],
michael@0 906 ["_chevron", "PlacesChevron"],
michael@0 907 ["_chevronPopup", "PlacesChevronPopup"]
michael@0 908 ].forEach(function (elementGlobal) {
michael@0 909 let [name, id] = elementGlobal;
michael@0 910 thisView.__defineGetter__(name, function () {
michael@0 911 let element = document.getElementById(id);
michael@0 912 if (!element)
michael@0 913 return null;
michael@0 914
michael@0 915 delete thisView[name];
michael@0 916 return thisView[name] = element;
michael@0 917 });
michael@0 918 });
michael@0 919
michael@0 920 this._viewElt._placesView = this;
michael@0 921
michael@0 922 this._addEventListeners(this._viewElt, this._cbEvents, false);
michael@0 923 this._addEventListeners(this._rootElt, ["popupshowing", "popuphidden"], true);
michael@0 924 this._addEventListeners(this._rootElt, ["overflow", "underflow"], true);
michael@0 925 this._addEventListeners(window, ["resize", "unload"], false);
michael@0 926
michael@0 927 // If personal-bookmarks has been dragged to the tabs toolbar,
michael@0 928 // we have to track addition and removals of tabs, to properly
michael@0 929 // recalculate the available space for bookmarks.
michael@0 930 // TODO (bug 734730): Use a performant mutation listener when available.
michael@0 931 if (this._viewElt.parentNode.parentNode == document.getElementById("TabsToolbar")) {
michael@0 932 this._addEventListeners(gBrowser.tabContainer, ["TabOpen", "TabClose"], false);
michael@0 933 }
michael@0 934
michael@0 935 PlacesViewBase.call(this, aPlace);
michael@0 936
michael@0 937 Services.telemetry.getHistogramById("FX_BOOKMARKS_TOOLBAR_INIT_MS")
michael@0 938 .add(Date.now() - startTime);
michael@0 939 }
michael@0 940
michael@0 941 PlacesToolbar.prototype = {
michael@0 942 __proto__: PlacesViewBase.prototype,
michael@0 943
michael@0 944 _cbEvents: ["dragstart", "dragover", "dragexit", "dragend", "drop",
michael@0 945 "mousemove", "mouseover", "mouseout"],
michael@0 946
michael@0 947 QueryInterface: function PT_QueryInterface(aIID) {
michael@0 948 if (aIID.equals(Ci.nsIDOMEventListener) ||
michael@0 949 aIID.equals(Ci.nsITimerCallback))
michael@0 950 return this;
michael@0 951
michael@0 952 return PlacesViewBase.prototype.QueryInterface.apply(this, arguments);
michael@0 953 },
michael@0 954
michael@0 955 uninit: function PT_uninit() {
michael@0 956 this._removeEventListeners(this._viewElt, this._cbEvents, false);
michael@0 957 this._removeEventListeners(this._rootElt, ["popupshowing", "popuphidden"],
michael@0 958 true);
michael@0 959 this._removeEventListeners(this._rootElt, ["overflow", "underflow"], true);
michael@0 960 this._removeEventListeners(window, ["resize", "unload"], false);
michael@0 961 this._removeEventListeners(gBrowser.tabContainer, ["TabOpen", "TabClose"], false);
michael@0 962
michael@0 963 if (this._chevron._placesView) {
michael@0 964 this._chevron._placesView.uninit();
michael@0 965 }
michael@0 966
michael@0 967 PlacesViewBase.prototype.uninit.apply(this, arguments);
michael@0 968 },
michael@0 969
michael@0 970 _openedMenuButton: null,
michael@0 971 _allowPopupShowing: true,
michael@0 972
michael@0 973 _rebuild: function PT__rebuild() {
michael@0 974 // Clear out references to existing nodes, since they will be removed
michael@0 975 // and re-added.
michael@0 976 if (this._overFolder.elt)
michael@0 977 this._clearOverFolder();
michael@0 978
michael@0 979 this._openedMenuButton = null;
michael@0 980 while (this._rootElt.hasChildNodes()) {
michael@0 981 this._rootElt.removeChild(this._rootElt.firstChild);
michael@0 982 }
michael@0 983
michael@0 984 let cc = this._resultNode.childCount;
michael@0 985 for (let i = 0; i < cc; ++i) {
michael@0 986 this._insertNewItem(this._resultNode.getChild(i), null);
michael@0 987 }
michael@0 988
michael@0 989 if (this._chevronPopup.hasAttribute("type")) {
michael@0 990 // Chevron has already been initialized, but since we are forcing
michael@0 991 // a rebuild of the toolbar, it has to be rebuilt.
michael@0 992 // Otherwise, it will be initialized when the toolbar overflows.
michael@0 993 this._chevronPopup.place = this.place;
michael@0 994 }
michael@0 995 },
michael@0 996
michael@0 997 _insertNewItem:
michael@0 998 function PT__insertNewItem(aChild, aBefore) {
michael@0 999 this._domNodes.delete(aChild);
michael@0 1000
michael@0 1001 let type = aChild.type;
michael@0 1002 let button;
michael@0 1003 if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
michael@0 1004 button = document.createElement("toolbarseparator");
michael@0 1005 }
michael@0 1006 else {
michael@0 1007 button = document.createElement("toolbarbutton");
michael@0 1008 button.className = "bookmark-item";
michael@0 1009 button.setAttribute("label", aChild.title);
michael@0 1010 let icon = aChild.icon;
michael@0 1011 if (icon)
michael@0 1012 button.setAttribute("image", icon);
michael@0 1013
michael@0 1014 if (PlacesUtils.containerTypes.indexOf(type) != -1) {
michael@0 1015 button.setAttribute("type", "menu");
michael@0 1016 button.setAttribute("container", "true");
michael@0 1017
michael@0 1018 if (PlacesUtils.nodeIsQuery(aChild)) {
michael@0 1019 button.setAttribute("query", "true");
michael@0 1020 if (PlacesUtils.nodeIsTagQuery(aChild))
michael@0 1021 button.setAttribute("tagContainer", "true");
michael@0 1022 }
michael@0 1023 else if (PlacesUtils.nodeIsFolder(aChild)) {
michael@0 1024 PlacesUtils.livemarks.getLivemark({ id: aChild.itemId })
michael@0 1025 .then(aLivemark => {
michael@0 1026 button.setAttribute("livemark", "true");
michael@0 1027 this.controller.cacheLivemarkInfo(aChild, aLivemark);
michael@0 1028 }, () => undefined);
michael@0 1029 }
michael@0 1030
michael@0 1031 let popup = document.createElement("menupopup");
michael@0 1032 popup.setAttribute("placespopup", "true");
michael@0 1033 button.appendChild(popup);
michael@0 1034 popup._placesNode = PlacesUtils.asContainer(aChild);
michael@0 1035 popup.setAttribute("context", "placesContext");
michael@0 1036
michael@0 1037 this._domNodes.set(aChild, popup);
michael@0 1038 }
michael@0 1039 else if (PlacesUtils.nodeIsURI(aChild)) {
michael@0 1040 button.setAttribute("scheme",
michael@0 1041 PlacesUIUtils.guessUrlSchemeForUI(aChild.uri));
michael@0 1042 }
michael@0 1043 }
michael@0 1044
michael@0 1045 button._placesNode = aChild;
michael@0 1046 if (!this._domNodes.has(aChild))
michael@0 1047 this._domNodes.set(aChild, button);
michael@0 1048
michael@0 1049 if (aBefore)
michael@0 1050 this._rootElt.insertBefore(button, aBefore);
michael@0 1051 else
michael@0 1052 this._rootElt.appendChild(button);
michael@0 1053 },
michael@0 1054
michael@0 1055 _updateChevronPopupNodesVisibility:
michael@0 1056 function PT__updateChevronPopupNodesVisibility() {
michael@0 1057 for (let i = 0, node = this._chevronPopup._startMarker.nextSibling;
michael@0 1058 node != this._chevronPopup._endMarker;
michael@0 1059 i++, node = node.nextSibling) {
michael@0 1060 node.hidden = this._rootElt.childNodes[i].style.visibility != "hidden";
michael@0 1061 }
michael@0 1062 },
michael@0 1063
michael@0 1064 _onChevronPopupShowing:
michael@0 1065 function PT__onChevronPopupShowing(aEvent) {
michael@0 1066 // Handle popupshowing only for the chevron popup, not for nested ones.
michael@0 1067 if (aEvent.target != this._chevronPopup)
michael@0 1068 return;
michael@0 1069
michael@0 1070 if (!this._chevron._placesView)
michael@0 1071 this._chevron._placesView = new PlacesMenu(aEvent, this.place);
michael@0 1072
michael@0 1073 this._updateChevronPopupNodesVisibility();
michael@0 1074 },
michael@0 1075
michael@0 1076 handleEvent: function PT_handleEvent(aEvent) {
michael@0 1077 switch (aEvent.type) {
michael@0 1078 case "unload":
michael@0 1079 this.uninit();
michael@0 1080 break;
michael@0 1081 case "resize":
michael@0 1082 // This handler updates nodes visibility in both the toolbar
michael@0 1083 // and the chevron popup when a window resize does not change
michael@0 1084 // the overflow status of the toolbar.
michael@0 1085 this.updateChevron();
michael@0 1086 break;
michael@0 1087 case "overflow":
michael@0 1088 if (!this._isOverflowStateEventRelevant(aEvent))
michael@0 1089 return;
michael@0 1090 this._onOverflow();
michael@0 1091 break;
michael@0 1092 case "underflow":
michael@0 1093 if (!this._isOverflowStateEventRelevant(aEvent))
michael@0 1094 return;
michael@0 1095 this._onUnderflow();
michael@0 1096 break;
michael@0 1097 case "TabOpen":
michael@0 1098 case "TabClose":
michael@0 1099 this.updateChevron();
michael@0 1100 break;
michael@0 1101 case "dragstart":
michael@0 1102 this._onDragStart(aEvent);
michael@0 1103 break;
michael@0 1104 case "dragover":
michael@0 1105 this._onDragOver(aEvent);
michael@0 1106 break;
michael@0 1107 case "dragexit":
michael@0 1108 this._onDragExit(aEvent);
michael@0 1109 break;
michael@0 1110 case "dragend":
michael@0 1111 this._onDragEnd(aEvent);
michael@0 1112 break;
michael@0 1113 case "drop":
michael@0 1114 this._onDrop(aEvent);
michael@0 1115 break;
michael@0 1116 case "mouseover":
michael@0 1117 this._onMouseOver(aEvent);
michael@0 1118 break;
michael@0 1119 case "mousemove":
michael@0 1120 this._onMouseMove(aEvent);
michael@0 1121 break;
michael@0 1122 case "mouseout":
michael@0 1123 this._onMouseOut(aEvent);
michael@0 1124 break;
michael@0 1125 case "popupshowing":
michael@0 1126 this._onPopupShowing(aEvent);
michael@0 1127 break;
michael@0 1128 case "popuphidden":
michael@0 1129 this._onPopupHidden(aEvent);
michael@0 1130 break;
michael@0 1131 default:
michael@0 1132 throw "Trying to handle unexpected event.";
michael@0 1133 }
michael@0 1134 },
michael@0 1135
michael@0 1136 updateOverflowStatus: function() {
michael@0 1137 if (this._rootElt.scrollLeftMax > 0) {
michael@0 1138 this._onOverflow();
michael@0 1139 } else {
michael@0 1140 this._onUnderflow();
michael@0 1141 }
michael@0 1142 },
michael@0 1143
michael@0 1144 _isOverflowStateEventRelevant: function PT_isOverflowStateEventRelevant(aEvent) {
michael@0 1145 // Ignore events not aimed at ourselves, as well as purely vertical ones:
michael@0 1146 return aEvent.target == aEvent.currentTarget && aEvent.detail > 0;
michael@0 1147 },
michael@0 1148
michael@0 1149 _onOverflow: function PT_onOverflow() {
michael@0 1150 // Attach the popup binding to the chevron popup if it has not yet
michael@0 1151 // been initialized.
michael@0 1152 if (!this._chevronPopup.hasAttribute("type")) {
michael@0 1153 this._chevronPopup.setAttribute("place", this.place);
michael@0 1154 this._chevronPopup.setAttribute("type", "places");
michael@0 1155 }
michael@0 1156 this._chevron.collapsed = false;
michael@0 1157 this.updateChevron();
michael@0 1158 },
michael@0 1159
michael@0 1160 _onUnderflow: function PT_onUnderflow() {
michael@0 1161 this.updateChevron();
michael@0 1162 this._chevron.collapsed = true;
michael@0 1163 },
michael@0 1164
michael@0 1165 updateChevron: function PT_updateChevron() {
michael@0 1166 // If the chevron is collapsed there's nothing to update.
michael@0 1167 if (this._chevron.collapsed)
michael@0 1168 return;
michael@0 1169
michael@0 1170 // Update the chevron on a timer. This will avoid repeated work when
michael@0 1171 // lot of changes happen in a small timeframe.
michael@0 1172 if (this._updateChevronTimer)
michael@0 1173 this._updateChevronTimer.cancel();
michael@0 1174
michael@0 1175 this._updateChevronTimer = this._setTimer(100);
michael@0 1176 },
michael@0 1177
michael@0 1178 _updateChevronTimerCallback: function PT__updateChevronTimerCallback() {
michael@0 1179 let scrollRect = this._rootElt.getBoundingClientRect();
michael@0 1180 let childOverflowed = false;
michael@0 1181 for (let i = 0; i < this._rootElt.childNodes.length; i++) {
michael@0 1182 let child = this._rootElt.childNodes[i];
michael@0 1183 // Once a child overflows, all the next ones will.
michael@0 1184 if (!childOverflowed) {
michael@0 1185 let childRect = child.getBoundingClientRect();
michael@0 1186 childOverflowed = this.isRTL ? (childRect.left < scrollRect.left)
michael@0 1187 : (childRect.right > scrollRect.right);
michael@0 1188
michael@0 1189 }
michael@0 1190 child.style.visibility = childOverflowed ? "hidden" : "visible";
michael@0 1191 }
michael@0 1192
michael@0 1193 // We rebuild the chevron on popupShowing, so if it is open
michael@0 1194 // we must update it.
michael@0 1195 if (this._chevron.open)
michael@0 1196 this._updateChevronPopupNodesVisibility();
michael@0 1197 },
michael@0 1198
michael@0 1199 nodeInserted:
michael@0 1200 function PT_nodeInserted(aParentPlacesNode, aPlacesNode, aIndex) {
michael@0 1201 let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
michael@0 1202 if (parentElt == this._rootElt) {
michael@0 1203 let children = this._rootElt.childNodes;
michael@0 1204 this._insertNewItem(aPlacesNode,
michael@0 1205 aIndex < children.length ? children[aIndex] : null);
michael@0 1206 this.updateChevron();
michael@0 1207 return;
michael@0 1208 }
michael@0 1209
michael@0 1210 PlacesViewBase.prototype.nodeInserted.apply(this, arguments);
michael@0 1211 },
michael@0 1212
michael@0 1213 nodeRemoved:
michael@0 1214 function PT_nodeRemoved(aParentPlacesNode, aPlacesNode, aIndex) {
michael@0 1215 let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
michael@0 1216 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
michael@0 1217
michael@0 1218 // Here we need the <menu>.
michael@0 1219 if (elt.localName == "menupopup")
michael@0 1220 elt = elt.parentNode;
michael@0 1221
michael@0 1222 if (parentElt == this._rootElt) {
michael@0 1223 this._removeChild(elt);
michael@0 1224 this.updateChevron();
michael@0 1225 return;
michael@0 1226 }
michael@0 1227
michael@0 1228 PlacesViewBase.prototype.nodeRemoved.apply(this, arguments);
michael@0 1229 },
michael@0 1230
michael@0 1231 nodeMoved:
michael@0 1232 function PT_nodeMoved(aPlacesNode,
michael@0 1233 aOldParentPlacesNode, aOldIndex,
michael@0 1234 aNewParentPlacesNode, aNewIndex) {
michael@0 1235 let parentElt = this._getDOMNodeForPlacesNode(aNewParentPlacesNode);
michael@0 1236 if (parentElt == this._rootElt) {
michael@0 1237 // Container is on the toolbar.
michael@0 1238
michael@0 1239 // Move the element.
michael@0 1240 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
michael@0 1241
michael@0 1242 // Here we need the <menu>.
michael@0 1243 if (elt.localName == "menupopup")
michael@0 1244 elt = elt.parentNode;
michael@0 1245
michael@0 1246 this._removeChild(elt);
michael@0 1247 this._rootElt.insertBefore(elt, this._rootElt.childNodes[aNewIndex]);
michael@0 1248
michael@0 1249 // The chevron view may get nodeMoved after the toolbar. In such a case,
michael@0 1250 // we should ensure (by manually swapping menuitems) that the actual nodes
michael@0 1251 // are in the final position before updateChevron tries to updates their
michael@0 1252 // visibility, or the chevron may go out of sync.
michael@0 1253 // Luckily updateChevron runs on a timer, so, by the time it updates
michael@0 1254 // nodes, the menu has already handled the notification.
michael@0 1255
michael@0 1256 this.updateChevron();
michael@0 1257 return;
michael@0 1258 }
michael@0 1259
michael@0 1260 PlacesViewBase.prototype.nodeMoved.apply(this, arguments);
michael@0 1261 },
michael@0 1262
michael@0 1263 nodeAnnotationChanged:
michael@0 1264 function PT_nodeAnnotationChanged(aPlacesNode, aAnno) {
michael@0 1265 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
michael@0 1266 if (elt == this._rootElt)
michael@0 1267 return;
michael@0 1268
michael@0 1269 // We're notified for the menupopup, not the containing toolbarbutton.
michael@0 1270 if (elt.localName == "menupopup")
michael@0 1271 elt = elt.parentNode;
michael@0 1272
michael@0 1273 if (elt.parentNode == this._rootElt) {
michael@0 1274 // Node is on the toolbar.
michael@0 1275
michael@0 1276 // All livemarks have a feedURI, so use it as our indicator.
michael@0 1277 if (aAnno == PlacesUtils.LMANNO_FEEDURI) {
michael@0 1278 elt.setAttribute("livemark", true);
michael@0 1279
michael@0 1280 PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId })
michael@0 1281 .then(aLivemark => {
michael@0 1282 this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
michael@0 1283 this.invalidateContainer(aPlacesNode);
michael@0 1284 }, Components.utils.reportError);
michael@0 1285 }
michael@0 1286 }
michael@0 1287 else {
michael@0 1288 // Node is in a submenu.
michael@0 1289 PlacesViewBase.prototype.nodeAnnotationChanged.apply(this, arguments);
michael@0 1290 }
michael@0 1291 },
michael@0 1292
michael@0 1293 nodeTitleChanged: function PT_nodeTitleChanged(aPlacesNode, aNewTitle) {
michael@0 1294 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
michael@0 1295
michael@0 1296 // There's no UI representation for the root node, thus there's
michael@0 1297 // nothing to be done when the title changes.
michael@0 1298 if (elt == this._rootElt)
michael@0 1299 return;
michael@0 1300
michael@0 1301 PlacesViewBase.prototype.nodeTitleChanged.apply(this, arguments);
michael@0 1302
michael@0 1303 // Here we need the <menu>.
michael@0 1304 if (elt.localName == "menupopup")
michael@0 1305 elt = elt.parentNode;
michael@0 1306
michael@0 1307 if (elt.parentNode == this._rootElt) {
michael@0 1308 // Node is on the toolbar
michael@0 1309 this.updateChevron();
michael@0 1310 }
michael@0 1311 },
michael@0 1312
michael@0 1313 invalidateContainer: function PT_invalidateContainer(aPlacesNode) {
michael@0 1314 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
michael@0 1315 if (elt == this._rootElt) {
michael@0 1316 // Container is the toolbar itself.
michael@0 1317 this._rebuild();
michael@0 1318 return;
michael@0 1319 }
michael@0 1320
michael@0 1321 PlacesViewBase.prototype.invalidateContainer.apply(this, arguments);
michael@0 1322 },
michael@0 1323
michael@0 1324 _overFolder: { elt: null,
michael@0 1325 openTimer: null,
michael@0 1326 hoverTime: 350,
michael@0 1327 closeTimer: null },
michael@0 1328
michael@0 1329 _clearOverFolder: function PT__clearOverFolder() {
michael@0 1330 // The mouse is no longer dragging over the stored menubutton.
michael@0 1331 // Close the menubutton, clear out drag styles, and clear all
michael@0 1332 // timers for opening/closing it.
michael@0 1333 if (this._overFolder.elt && this._overFolder.elt.lastChild) {
michael@0 1334 if (!this._overFolder.elt.lastChild.hasAttribute("dragover")) {
michael@0 1335 this._overFolder.elt.lastChild.hidePopup();
michael@0 1336 }
michael@0 1337 this._overFolder.elt.removeAttribute("dragover");
michael@0 1338 this._overFolder.elt = null;
michael@0 1339 }
michael@0 1340 if (this._overFolder.openTimer) {
michael@0 1341 this._overFolder.openTimer.cancel();
michael@0 1342 this._overFolder.openTimer = null;
michael@0 1343 }
michael@0 1344 if (this._overFolder.closeTimer) {
michael@0 1345 this._overFolder.closeTimer.cancel();
michael@0 1346 this._overFolder.closeTimer = null;
michael@0 1347 }
michael@0 1348 },
michael@0 1349
michael@0 1350 /**
michael@0 1351 * This function returns information about where to drop when dragging over
michael@0 1352 * the toolbar. The returned object has the following properties:
michael@0 1353 * - ip: the insertion point for the bookmarks service.
michael@0 1354 * - beforeIndex: child index to drop before, for the drop indicator.
michael@0 1355 * - folderElt: the folder to drop into, if applicable.
michael@0 1356 */
michael@0 1357 _getDropPoint: function PT__getDropPoint(aEvent) {
michael@0 1358 let result = this.result;
michael@0 1359 if (!PlacesUtils.nodeIsFolder(this._resultNode))
michael@0 1360 return null;
michael@0 1361
michael@0 1362 let dropPoint = { ip: null, beforeIndex: null, folderElt: null };
michael@0 1363 let elt = aEvent.target;
michael@0 1364 if (elt._placesNode && elt != this._rootElt &&
michael@0 1365 elt.localName != "menupopup") {
michael@0 1366 let eltRect = elt.getBoundingClientRect();
michael@0 1367 let eltIndex = Array.indexOf(this._rootElt.childNodes, elt);
michael@0 1368 if (PlacesUtils.nodeIsFolder(elt._placesNode) &&
michael@0 1369 !PlacesUtils.nodeIsReadOnly(elt._placesNode)) {
michael@0 1370 // This is a folder.
michael@0 1371 // If we are in the middle of it, drop inside it.
michael@0 1372 // Otherwise, drop before it, with regards to RTL mode.
michael@0 1373 let threshold = eltRect.width * 0.25;
michael@0 1374 if (this.isRTL ? (aEvent.clientX > eltRect.right - threshold)
michael@0 1375 : (aEvent.clientX < eltRect.left + threshold)) {
michael@0 1376 // Drop before this folder.
michael@0 1377 dropPoint.ip =
michael@0 1378 new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode),
michael@0 1379 eltIndex, Ci.nsITreeView.DROP_BEFORE);
michael@0 1380 dropPoint.beforeIndex = eltIndex;
michael@0 1381 }
michael@0 1382 else if (this.isRTL ? (aEvent.clientX > eltRect.left + threshold)
michael@0 1383 : (aEvent.clientX < eltRect.right - threshold)) {
michael@0 1384 // Drop inside this folder.
michael@0 1385 dropPoint.ip =
michael@0 1386 new InsertionPoint(PlacesUtils.getConcreteItemId(elt._placesNode),
michael@0 1387 -1, Ci.nsITreeView.DROP_ON,
michael@0 1388 PlacesUtils.nodeIsTagQuery(elt._placesNode));
michael@0 1389 dropPoint.beforeIndex = eltIndex;
michael@0 1390 dropPoint.folderElt = elt;
michael@0 1391 }
michael@0 1392 else {
michael@0 1393 // Drop after this folder.
michael@0 1394 let beforeIndex =
michael@0 1395 (eltIndex == this._rootElt.childNodes.length - 1) ?
michael@0 1396 -1 : eltIndex + 1;
michael@0 1397
michael@0 1398 dropPoint.ip =
michael@0 1399 new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode),
michael@0 1400 beforeIndex, Ci.nsITreeView.DROP_BEFORE);
michael@0 1401 dropPoint.beforeIndex = beforeIndex;
michael@0 1402 }
michael@0 1403 }
michael@0 1404 else {
michael@0 1405 // This is a non-folder node or a read-only folder.
michael@0 1406 // Drop before it with regards to RTL mode.
michael@0 1407 let threshold = eltRect.width * 0.5;
michael@0 1408 if (this.isRTL ? (aEvent.clientX > eltRect.left + threshold)
michael@0 1409 : (aEvent.clientX < eltRect.left + threshold)) {
michael@0 1410 // Drop before this bookmark.
michael@0 1411 dropPoint.ip =
michael@0 1412 new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode),
michael@0 1413 eltIndex, Ci.nsITreeView.DROP_BEFORE);
michael@0 1414 dropPoint.beforeIndex = eltIndex;
michael@0 1415 }
michael@0 1416 else {
michael@0 1417 // Drop after this bookmark.
michael@0 1418 let beforeIndex =
michael@0 1419 eltIndex == this._rootElt.childNodes.length - 1 ?
michael@0 1420 -1 : eltIndex + 1;
michael@0 1421 dropPoint.ip =
michael@0 1422 new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode),
michael@0 1423 beforeIndex, Ci.nsITreeView.DROP_BEFORE);
michael@0 1424 dropPoint.beforeIndex = beforeIndex;
michael@0 1425 }
michael@0 1426 }
michael@0 1427 }
michael@0 1428 else {
michael@0 1429 // We are most likely dragging on the empty area of the
michael@0 1430 // toolbar, we should drop after the last node.
michael@0 1431 dropPoint.ip =
michael@0 1432 new InsertionPoint(PlacesUtils.getConcreteItemId(this._resultNode),
michael@0 1433 -1, Ci.nsITreeView.DROP_BEFORE);
michael@0 1434 dropPoint.beforeIndex = -1;
michael@0 1435 }
michael@0 1436
michael@0 1437 return dropPoint;
michael@0 1438 },
michael@0 1439
michael@0 1440 _setTimer: function PT_setTimer(aTime) {
michael@0 1441 let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
michael@0 1442 timer.initWithCallback(this, aTime, timer.TYPE_ONE_SHOT);
michael@0 1443 return timer;
michael@0 1444 },
michael@0 1445
michael@0 1446 notify: function PT_notify(aTimer) {
michael@0 1447 if (aTimer == this._updateChevronTimer) {
michael@0 1448 this._updateChevronTimer = null;
michael@0 1449 this._updateChevronTimerCallback();
michael@0 1450 }
michael@0 1451
michael@0 1452 // * Timer to turn off indicator bar.
michael@0 1453 else if (aTimer == this._ibTimer) {
michael@0 1454 this._dropIndicator.collapsed = true;
michael@0 1455 this._ibTimer = null;
michael@0 1456 }
michael@0 1457
michael@0 1458 // * Timer to open a menubutton that's being dragged over.
michael@0 1459 else if (aTimer == this._overFolder.openTimer) {
michael@0 1460 // Set the autoopen attribute on the folder's menupopup so that
michael@0 1461 // the menu will automatically close when the mouse drags off of it.
michael@0 1462 this._overFolder.elt.lastChild.setAttribute("autoopened", "true");
michael@0 1463 this._overFolder.elt.open = true;
michael@0 1464 this._overFolder.openTimer = null;
michael@0 1465 }
michael@0 1466
michael@0 1467 // * Timer to close a menubutton that's been dragged off of.
michael@0 1468 else if (aTimer == this._overFolder.closeTimer) {
michael@0 1469 // Close the menubutton if we are not dragging over it or one of
michael@0 1470 // its children. The autoopened attribute will let the menu know to
michael@0 1471 // close later if the menu is still being dragged over.
michael@0 1472 let currentPlacesNode = PlacesControllerDragHelper.currentDropTarget;
michael@0 1473 let inHierarchy = false;
michael@0 1474 while (currentPlacesNode) {
michael@0 1475 if (currentPlacesNode == this._rootElt) {
michael@0 1476 inHierarchy = true;
michael@0 1477 break;
michael@0 1478 }
michael@0 1479 currentPlacesNode = currentPlacesNode.parentNode;
michael@0 1480 }
michael@0 1481 // The _clearOverFolder() function will close the menu for
michael@0 1482 // _overFolder.elt. So null it out if we don't want to close it.
michael@0 1483 if (inHierarchy)
michael@0 1484 this._overFolder.elt = null;
michael@0 1485
michael@0 1486 // Clear out the folder and all associated timers.
michael@0 1487 this._clearOverFolder();
michael@0 1488 }
michael@0 1489 },
michael@0 1490
michael@0 1491 _onMouseOver: function PT__onMouseOver(aEvent) {
michael@0 1492 let button = aEvent.target;
michael@0 1493 if (button.parentNode == this._rootElt && button._placesNode &&
michael@0 1494 PlacesUtils.nodeIsURI(button._placesNode))
michael@0 1495 window.XULBrowserWindow.setOverLink(aEvent.target._placesNode.uri, null);
michael@0 1496 },
michael@0 1497
michael@0 1498 _onMouseOut: function PT__onMouseOut(aEvent) {
michael@0 1499 window.XULBrowserWindow.setOverLink("", null);
michael@0 1500 },
michael@0 1501
michael@0 1502 _cleanupDragDetails: function PT__cleanupDragDetails() {
michael@0 1503 // Called on dragend and drop.
michael@0 1504 PlacesControllerDragHelper.currentDropTarget = null;
michael@0 1505 this._draggedElt = null;
michael@0 1506 if (this._ibTimer)
michael@0 1507 this._ibTimer.cancel();
michael@0 1508
michael@0 1509 this._dropIndicator.collapsed = true;
michael@0 1510 },
michael@0 1511
michael@0 1512 _onDragStart: function PT__onDragStart(aEvent) {
michael@0 1513 // Sub menus have their own d&d handlers.
michael@0 1514 let draggedElt = aEvent.target;
michael@0 1515 if (draggedElt.parentNode != this._rootElt || !draggedElt._placesNode)
michael@0 1516 return;
michael@0 1517
michael@0 1518 if (draggedElt.localName == "toolbarbutton" &&
michael@0 1519 draggedElt.getAttribute("type") == "menu") {
michael@0 1520 // If the drag gesture on a container is toward down we open instead
michael@0 1521 // of dragging.
michael@0 1522 let translateY = this._cachedMouseMoveEvent.clientY - aEvent.clientY;
michael@0 1523 let translateX = this._cachedMouseMoveEvent.clientX - aEvent.clientX;
michael@0 1524 if ((translateY) >= Math.abs(translateX/2)) {
michael@0 1525 // Don't start the drag.
michael@0 1526 aEvent.preventDefault();
michael@0 1527 // Open the menu.
michael@0 1528 draggedElt.open = true;
michael@0 1529 return;
michael@0 1530 }
michael@0 1531
michael@0 1532 // If the menu is open, close it.
michael@0 1533 if (draggedElt.open) {
michael@0 1534 draggedElt.lastChild.hidePopup();
michael@0 1535 draggedElt.open = false;
michael@0 1536 }
michael@0 1537 }
michael@0 1538
michael@0 1539 // Activate the view and cache the dragged element.
michael@0 1540 this._draggedElt = draggedElt._placesNode;
michael@0 1541 this._rootElt.focus();
michael@0 1542
michael@0 1543 this._controller.setDataTransfer(aEvent);
michael@0 1544 aEvent.stopPropagation();
michael@0 1545 },
michael@0 1546
michael@0 1547 _onDragOver: function PT__onDragOver(aEvent) {
michael@0 1548 // Cache the dataTransfer
michael@0 1549 PlacesControllerDragHelper.currentDropTarget = aEvent.target;
michael@0 1550 let dt = aEvent.dataTransfer;
michael@0 1551
michael@0 1552 let dropPoint = this._getDropPoint(aEvent);
michael@0 1553 if (!dropPoint || !dropPoint.ip ||
michael@0 1554 !PlacesControllerDragHelper.canDrop(dropPoint.ip, dt)) {
michael@0 1555 this._dropIndicator.collapsed = true;
michael@0 1556 aEvent.stopPropagation();
michael@0 1557 return;
michael@0 1558 }
michael@0 1559
michael@0 1560 if (this._ibTimer) {
michael@0 1561 this._ibTimer.cancel();
michael@0 1562 this._ibTimer = null;
michael@0 1563 }
michael@0 1564
michael@0 1565 if (dropPoint.folderElt || aEvent.originalTarget == this._chevron) {
michael@0 1566 // Dropping over a menubutton or chevron button.
michael@0 1567 // Set styles and timer to open relative menupopup.
michael@0 1568 let overElt = dropPoint.folderElt || this._chevron;
michael@0 1569 if (this._overFolder.elt != overElt) {
michael@0 1570 this._clearOverFolder();
michael@0 1571 this._overFolder.elt = overElt;
michael@0 1572 this._overFolder.openTimer = this._setTimer(this._overFolder.hoverTime);
michael@0 1573 }
michael@0 1574 if (!this._overFolder.elt.hasAttribute("dragover"))
michael@0 1575 this._overFolder.elt.setAttribute("dragover", "true");
michael@0 1576
michael@0 1577 this._dropIndicator.collapsed = true;
michael@0 1578 }
michael@0 1579 else {
michael@0 1580 // Dragging over a normal toolbarbutton,
michael@0 1581 // show indicator bar and move it to the appropriate drop point.
michael@0 1582 let ind = this._dropIndicator;
michael@0 1583 ind.parentNode.collapsed = false;
michael@0 1584 let halfInd = ind.clientWidth / 2;
michael@0 1585 let translateX;
michael@0 1586 if (this.isRTL) {
michael@0 1587 halfInd = Math.ceil(halfInd);
michael@0 1588 translateX = 0 - this._rootElt.getBoundingClientRect().right - halfInd;
michael@0 1589 if (this._rootElt.firstChild) {
michael@0 1590 if (dropPoint.beforeIndex == -1)
michael@0 1591 translateX += this._rootElt.lastChild.getBoundingClientRect().left;
michael@0 1592 else {
michael@0 1593 translateX += this._rootElt.childNodes[dropPoint.beforeIndex]
michael@0 1594 .getBoundingClientRect().right;
michael@0 1595 }
michael@0 1596 }
michael@0 1597 }
michael@0 1598 else {
michael@0 1599 halfInd = Math.floor(halfInd);
michael@0 1600 translateX = 0 - this._rootElt.getBoundingClientRect().left +
michael@0 1601 halfInd;
michael@0 1602 if (this._rootElt.firstChild) {
michael@0 1603 if (dropPoint.beforeIndex == -1)
michael@0 1604 translateX += this._rootElt.lastChild.getBoundingClientRect().right;
michael@0 1605 else {
michael@0 1606 translateX += this._rootElt.childNodes[dropPoint.beforeIndex]
michael@0 1607 .getBoundingClientRect().left;
michael@0 1608 }
michael@0 1609 }
michael@0 1610 }
michael@0 1611
michael@0 1612 ind.style.transform = "translate(" + Math.round(translateX) + "px)";
michael@0 1613 ind.style.MozMarginStart = (-ind.clientWidth) + "px";
michael@0 1614 ind.collapsed = false;
michael@0 1615
michael@0 1616 // Clear out old folder information.
michael@0 1617 this._clearOverFolder();
michael@0 1618 }
michael@0 1619
michael@0 1620 aEvent.preventDefault();
michael@0 1621 aEvent.stopPropagation();
michael@0 1622 },
michael@0 1623
michael@0 1624 _onDrop: function PT__onDrop(aEvent) {
michael@0 1625 PlacesControllerDragHelper.currentDropTarget = aEvent.target;
michael@0 1626
michael@0 1627 let dropPoint = this._getDropPoint(aEvent);
michael@0 1628 if (dropPoint && dropPoint.ip) {
michael@0 1629 PlacesControllerDragHelper.onDrop(dropPoint.ip, aEvent.dataTransfer)
michael@0 1630 aEvent.preventDefault();
michael@0 1631 }
michael@0 1632
michael@0 1633 this._cleanupDragDetails();
michael@0 1634 aEvent.stopPropagation();
michael@0 1635 },
michael@0 1636
michael@0 1637 _onDragExit: function PT__onDragExit(aEvent) {
michael@0 1638 PlacesControllerDragHelper.currentDropTarget = null;
michael@0 1639
michael@0 1640 // Set timer to turn off indicator bar (if we turn it off
michael@0 1641 // here, dragenter might be called immediately after, creating
michael@0 1642 // flicker).
michael@0 1643 if (this._ibTimer)
michael@0 1644 this._ibTimer.cancel();
michael@0 1645 this._ibTimer = this._setTimer(10);
michael@0 1646
michael@0 1647 // If we hovered over a folder, close it now.
michael@0 1648 if (this._overFolder.elt)
michael@0 1649 this._overFolder.closeTimer = this._setTimer(this._overFolder.hoverTime);
michael@0 1650 },
michael@0 1651
michael@0 1652 _onDragEnd: function PT_onDragEnd(aEvent) {
michael@0 1653 this._cleanupDragDetails();
michael@0 1654 },
michael@0 1655
michael@0 1656 _onPopupShowing: function PT__onPopupShowing(aEvent) {
michael@0 1657 if (!this._allowPopupShowing) {
michael@0 1658 this._allowPopupShowing = true;
michael@0 1659 aEvent.preventDefault();
michael@0 1660 return;
michael@0 1661 }
michael@0 1662
michael@0 1663 let parent = aEvent.target.parentNode;
michael@0 1664 if (parent.localName == "toolbarbutton")
michael@0 1665 this._openedMenuButton = parent;
michael@0 1666
michael@0 1667 PlacesViewBase.prototype._onPopupShowing.apply(this, arguments);
michael@0 1668 },
michael@0 1669
michael@0 1670 _onPopupHidden: function PT__onPopupHidden(aEvent) {
michael@0 1671 let popup = aEvent.target;
michael@0 1672 let placesNode = popup._placesNode;
michael@0 1673 // Avoid handling popuphidden of inner views
michael@0 1674 if (placesNode && PlacesUIUtils.getViewForNode(popup) == this) {
michael@0 1675 // UI performance: folder queries are cheap, keep the resultnode open
michael@0 1676 // so we don't rebuild its contents whenever the popup is reopened.
michael@0 1677 // Though, we want to always close feed containers so their expiration
michael@0 1678 // status will be checked at next opening.
michael@0 1679 if (!PlacesUtils.nodeIsFolder(placesNode) ||
michael@0 1680 this.controller.hasCachedLivemarkInfo(placesNode)) {
michael@0 1681 placesNode.containerOpen = false;
michael@0 1682 }
michael@0 1683 }
michael@0 1684
michael@0 1685 let parent = popup.parentNode;
michael@0 1686 if (parent.localName == "toolbarbutton") {
michael@0 1687 this._openedMenuButton = null;
michael@0 1688 // Clear the dragover attribute if present, if we are dragging into a
michael@0 1689 // folder in the hierachy of current opened popup we don't clear
michael@0 1690 // this attribute on clearOverFolder. See Notify for closeTimer.
michael@0 1691 if (parent.hasAttribute("dragover"))
michael@0 1692 parent.removeAttribute("dragover");
michael@0 1693 }
michael@0 1694 },
michael@0 1695
michael@0 1696 _onMouseMove: function PT__onMouseMove(aEvent) {
michael@0 1697 // Used in dragStart to prevent dragging folders when dragging down.
michael@0 1698 this._cachedMouseMoveEvent = aEvent;
michael@0 1699
michael@0 1700 if (this._openedMenuButton == null ||
michael@0 1701 PlacesControllerDragHelper.getSession())
michael@0 1702 return;
michael@0 1703
michael@0 1704 let target = aEvent.originalTarget;
michael@0 1705 if (this._openedMenuButton != target &&
michael@0 1706 target.localName == "toolbarbutton" &&
michael@0 1707 target.type == "menu") {
michael@0 1708 this._openedMenuButton.open = false;
michael@0 1709 target.open = true;
michael@0 1710 }
michael@0 1711 }
michael@0 1712 };
michael@0 1713
michael@0 1714 /**
michael@0 1715 * View for Places menus. This object should be created during the first
michael@0 1716 * popupshowing that's dispatched on the menu.
michael@0 1717 */
michael@0 1718 function PlacesMenu(aPopupShowingEvent, aPlace, aOptions) {
michael@0 1719 this._rootElt = aPopupShowingEvent.target; // <menupopup>
michael@0 1720 this._viewElt = this._rootElt.parentNode; // <menu>
michael@0 1721 this._viewElt._placesView = this;
michael@0 1722 this._addEventListeners(this._rootElt, ["popupshowing", "popuphidden"], true);
michael@0 1723 this._addEventListeners(window, ["unload"], false);
michael@0 1724
michael@0 1725 #ifdef XP_MACOSX
michael@0 1726 // Must walk up to support views in sub-menus, like Bookmarks Toolbar menu.
michael@0 1727 for (let elt = this._viewElt.parentNode; elt; elt = elt.parentNode) {
michael@0 1728 if (elt.localName == "menubar") {
michael@0 1729 this._nativeView = true;
michael@0 1730 break;
michael@0 1731 }
michael@0 1732 }
michael@0 1733 #endif
michael@0 1734
michael@0 1735 PlacesViewBase.call(this, aPlace, aOptions);
michael@0 1736 this._onPopupShowing(aPopupShowingEvent);
michael@0 1737 }
michael@0 1738
michael@0 1739 PlacesMenu.prototype = {
michael@0 1740 __proto__: PlacesViewBase.prototype,
michael@0 1741
michael@0 1742 QueryInterface: function PM_QueryInterface(aIID) {
michael@0 1743 if (aIID.equals(Ci.nsIDOMEventListener))
michael@0 1744 return this;
michael@0 1745
michael@0 1746 return PlacesViewBase.prototype.QueryInterface.apply(this, arguments);
michael@0 1747 },
michael@0 1748
michael@0 1749 _removeChild: function PM_removeChild(aChild) {
michael@0 1750 PlacesViewBase.prototype._removeChild.apply(this, arguments);
michael@0 1751 },
michael@0 1752
michael@0 1753 uninit: function PM_uninit() {
michael@0 1754 this._removeEventListeners(this._rootElt, ["popupshowing", "popuphidden"],
michael@0 1755 true);
michael@0 1756 this._removeEventListeners(window, ["unload"], false);
michael@0 1757
michael@0 1758 PlacesViewBase.prototype.uninit.apply(this, arguments);
michael@0 1759 },
michael@0 1760
michael@0 1761 handleEvent: function PM_handleEvent(aEvent) {
michael@0 1762 switch (aEvent.type) {
michael@0 1763 case "unload":
michael@0 1764 this.uninit();
michael@0 1765 break;
michael@0 1766 case "popupshowing":
michael@0 1767 this._onPopupShowing(aEvent);
michael@0 1768 break;
michael@0 1769 case "popuphidden":
michael@0 1770 this._onPopupHidden(aEvent);
michael@0 1771 break;
michael@0 1772 }
michael@0 1773 },
michael@0 1774
michael@0 1775 _onPopupHidden: function PM__onPopupHidden(aEvent) {
michael@0 1776 // Avoid handling popuphidden of inner views.
michael@0 1777 let popup = aEvent.originalTarget;
michael@0 1778 let placesNode = popup._placesNode;
michael@0 1779 if (!placesNode || PlacesUIUtils.getViewForNode(popup) != this)
michael@0 1780 return;
michael@0 1781
michael@0 1782 // UI performance: folder queries are cheap, keep the resultnode open
michael@0 1783 // so we don't rebuild its contents whenever the popup is reopened.
michael@0 1784 // Though, we want to always close feed containers so their expiration
michael@0 1785 // status will be checked at next opening.
michael@0 1786 if (!PlacesUtils.nodeIsFolder(placesNode) ||
michael@0 1787 this.controller.hasCachedLivemarkInfo(placesNode))
michael@0 1788 placesNode.containerOpen = false;
michael@0 1789
michael@0 1790 // The autoopened attribute is set for folders which have been
michael@0 1791 // automatically opened when dragged over. Turn off this attribute
michael@0 1792 // when the folder closes because it is no longer applicable.
michael@0 1793 popup.removeAttribute("autoopened");
michael@0 1794 popup.removeAttribute("dragstart");
michael@0 1795 }
michael@0 1796 };
michael@0 1797
michael@0 1798 function PlacesPanelMenuView(aPlace, aViewId, aRootId, aOptions) {
michael@0 1799 this._viewElt = document.getElementById(aViewId);
michael@0 1800 this._rootElt = document.getElementById(aRootId);
michael@0 1801 this._viewElt._placesView = this;
michael@0 1802 this.options = aOptions;
michael@0 1803
michael@0 1804 PlacesViewBase.call(this, aPlace, aOptions);
michael@0 1805 }
michael@0 1806
michael@0 1807 PlacesPanelMenuView.prototype = {
michael@0 1808 __proto__: PlacesViewBase.prototype,
michael@0 1809
michael@0 1810 QueryInterface: function PAMV_QueryInterface(aIID) {
michael@0 1811 return PlacesViewBase.prototype.QueryInterface.apply(this, arguments);
michael@0 1812 },
michael@0 1813
michael@0 1814 uninit: function PAMV_uninit() {
michael@0 1815 PlacesViewBase.prototype.uninit.apply(this, arguments);
michael@0 1816 },
michael@0 1817
michael@0 1818 _insertNewItem:
michael@0 1819 function PAMV__insertNewItem(aChild, aBefore) {
michael@0 1820 this._domNodes.delete(aChild);
michael@0 1821
michael@0 1822 let type = aChild.type;
michael@0 1823 let button;
michael@0 1824 if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
michael@0 1825 button = document.createElement("toolbarseparator");
michael@0 1826 button.setAttribute("class", "small-separator");
michael@0 1827 }
michael@0 1828 else {
michael@0 1829 button = document.createElement("toolbarbutton");
michael@0 1830 button.className = "bookmark-item";
michael@0 1831 if (typeof this.options.extraClasses.entry == "string")
michael@0 1832 button.classList.add(this.options.extraClasses.entry);
michael@0 1833 button.setAttribute("label", aChild.title);
michael@0 1834 let icon = aChild.icon;
michael@0 1835 if (icon)
michael@0 1836 button.setAttribute("image", icon);
michael@0 1837
michael@0 1838 if (PlacesUtils.containerTypes.indexOf(type) != -1) {
michael@0 1839 button.setAttribute("container", "true");
michael@0 1840
michael@0 1841 if (PlacesUtils.nodeIsQuery(aChild)) {
michael@0 1842 button.setAttribute("query", "true");
michael@0 1843 if (PlacesUtils.nodeIsTagQuery(aChild))
michael@0 1844 button.setAttribute("tagContainer", "true");
michael@0 1845 }
michael@0 1846 else if (PlacesUtils.nodeIsFolder(aChild)) {
michael@0 1847 PlacesUtils.livemarks.getLivemark({ id: aChild.itemId })
michael@0 1848 .then(aLivemark => {
michael@0 1849 button.setAttribute("livemark", "true");
michael@0 1850 this.controller.cacheLivemarkInfo(aChild, aLivemark);
michael@0 1851 }, () => undefined);
michael@0 1852 }
michael@0 1853 }
michael@0 1854 else if (PlacesUtils.nodeIsURI(aChild)) {
michael@0 1855 button.setAttribute("scheme",
michael@0 1856 PlacesUIUtils.guessUrlSchemeForUI(aChild.uri));
michael@0 1857 }
michael@0 1858 }
michael@0 1859
michael@0 1860 button._placesNode = aChild;
michael@0 1861 if (!this._domNodes.has(aChild))
michael@0 1862 this._domNodes.set(aChild, button);
michael@0 1863
michael@0 1864 this._rootElt.insertBefore(button, aBefore);
michael@0 1865 },
michael@0 1866
michael@0 1867 nodeInserted:
michael@0 1868 function PAMV_nodeInserted(aParentPlacesNode, aPlacesNode, aIndex) {
michael@0 1869 let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
michael@0 1870 if (parentElt != this._rootElt)
michael@0 1871 return;
michael@0 1872
michael@0 1873 let children = this._rootElt.childNodes;
michael@0 1874 this._insertNewItem(aPlacesNode,
michael@0 1875 aIndex < children.length ? children[aIndex] : null);
michael@0 1876 },
michael@0 1877
michael@0 1878 nodeRemoved:
michael@0 1879 function PAMV_nodeRemoved(aParentPlacesNode, aPlacesNode, aIndex) {
michael@0 1880 let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
michael@0 1881 if (parentElt != this._rootElt)
michael@0 1882 return;
michael@0 1883
michael@0 1884 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
michael@0 1885 this._removeChild(elt);
michael@0 1886 },
michael@0 1887
michael@0 1888 nodeMoved:
michael@0 1889 function PAMV_nodeMoved(aPlacesNode,
michael@0 1890 aOldParentPlacesNode, aOldIndex,
michael@0 1891 aNewParentPlacesNode, aNewIndex) {
michael@0 1892 let parentElt = this._getDOMNodeForPlacesNode(aNewParentPlacesNode);
michael@0 1893 if (parentElt != this._rootElt)
michael@0 1894 return;
michael@0 1895
michael@0 1896 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
michael@0 1897 this._removeChild(elt);
michael@0 1898 this._rootElt.insertBefore(elt, this._rootElt.childNodes[aNewIndex]);
michael@0 1899 },
michael@0 1900
michael@0 1901 nodeAnnotationChanged:
michael@0 1902 function PAMV_nodeAnnotationChanged(aPlacesNode, aAnno) {
michael@0 1903 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
michael@0 1904 // There's no UI representation for the root node.
michael@0 1905 if (elt == this._rootElt)
michael@0 1906 return;
michael@0 1907
michael@0 1908 if (elt.parentNode != this._rootElt)
michael@0 1909 return;
michael@0 1910
michael@0 1911 // All livemarks have a feedURI, so use it as our indicator.
michael@0 1912 if (aAnno == PlacesUtils.LMANNO_FEEDURI) {
michael@0 1913 elt.setAttribute("livemark", true);
michael@0 1914
michael@0 1915 PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId })
michael@0 1916 .then(aLivemark => {
michael@0 1917 this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
michael@0 1918 this.invalidateContainer(aPlacesNode);
michael@0 1919 }, Components.utils.reportError);
michael@0 1920 }
michael@0 1921 },
michael@0 1922
michael@0 1923 nodeTitleChanged: function PAMV_nodeTitleChanged(aPlacesNode, aNewTitle) {
michael@0 1924 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
michael@0 1925
michael@0 1926 // There's no UI representation for the root node.
michael@0 1927 if (elt == this._rootElt)
michael@0 1928 return;
michael@0 1929
michael@0 1930 PlacesViewBase.prototype.nodeTitleChanged.apply(this, arguments);
michael@0 1931 },
michael@0 1932
michael@0 1933 invalidateContainer: function PAMV_invalidateContainer(aPlacesNode) {
michael@0 1934 let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
michael@0 1935 if (elt != this._rootElt)
michael@0 1936 return;
michael@0 1937
michael@0 1938 // Container is the toolbar itself.
michael@0 1939 while (this._rootElt.hasChildNodes()) {
michael@0 1940 this._rootElt.removeChild(this._rootElt.firstChild);
michael@0 1941 }
michael@0 1942
michael@0 1943 for (let i = 0; i < this._resultNode.childCount; ++i) {
michael@0 1944 this._insertNewItem(this._resultNode.getChild(i), null);
michael@0 1945 }
michael@0 1946 }
michael@0 1947 };

mercurial