browser/components/tabview/tabitems.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
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 // **********
michael@0 6 // Title: tabitems.js
michael@0 7
michael@0 8 // ##########
michael@0 9 // Class: TabItem
michael@0 10 // An <Item> that represents a tab. Also implements the <Subscribable> interface.
michael@0 11 //
michael@0 12 // Parameters:
michael@0 13 // tab - a xul:tab
michael@0 14 function TabItem(tab, options) {
michael@0 15 Utils.assert(tab, "tab");
michael@0 16
michael@0 17 this.tab = tab;
michael@0 18 // register this as the tab's tabItem
michael@0 19 this.tab._tabViewTabItem = this;
michael@0 20
michael@0 21 if (!options)
michael@0 22 options = {};
michael@0 23
michael@0 24 // ___ set up div
michael@0 25 document.body.appendChild(TabItems.fragment().cloneNode(true));
michael@0 26
michael@0 27 // The document fragment contains just one Node
michael@0 28 // As per DOM3 appendChild: it will then be the last child
michael@0 29 let div = document.body.lastChild;
michael@0 30 let $div = iQ(div);
michael@0 31
michael@0 32 this._showsCachedData = false;
michael@0 33 this.canvasSizeForced = false;
michael@0 34 this.$thumb = iQ('.thumb', $div);
michael@0 35 this.$fav = iQ('.favicon', $div);
michael@0 36 this.$tabTitle = iQ('.tab-title', $div);
michael@0 37 this.$canvas = iQ('.thumb canvas', $div);
michael@0 38 this.$cachedThumb = iQ('img.cached-thumb', $div);
michael@0 39 this.$favImage = iQ('.favicon>img', $div);
michael@0 40 this.$close = iQ('.close', $div);
michael@0 41
michael@0 42 this.tabCanvas = new TabCanvas(this.tab, this.$canvas[0]);
michael@0 43
michael@0 44 this._hidden = false;
michael@0 45 this.isATabItem = true;
michael@0 46 this.keepProportional = true;
michael@0 47 this._hasBeenDrawn = false;
michael@0 48 this._reconnected = false;
michael@0 49 this.isDragging = false;
michael@0 50 this.isStacked = false;
michael@0 51
michael@0 52 // Read off the total vertical and horizontal padding on the tab container
michael@0 53 // and cache this value, as it must be the same for every TabItem.
michael@0 54 if (Utils.isEmptyObject(TabItems.tabItemPadding)) {
michael@0 55 TabItems.tabItemPadding.x = parseInt($div.css('padding-left'))
michael@0 56 + parseInt($div.css('padding-right'));
michael@0 57
michael@0 58 TabItems.tabItemPadding.y = parseInt($div.css('padding-top'))
michael@0 59 + parseInt($div.css('padding-bottom'));
michael@0 60 }
michael@0 61
michael@0 62 this.bounds = new Rect(0,0,1,1);
michael@0 63
michael@0 64 this._lastTabUpdateTime = Date.now();
michael@0 65
michael@0 66 // ___ superclass setup
michael@0 67 this._init(div);
michael@0 68
michael@0 69 // ___ drag/drop
michael@0 70 // override dropOptions with custom tabitem methods
michael@0 71 this.dropOptions.drop = function(e) {
michael@0 72 let groupItem = drag.info.item.parent;
michael@0 73 groupItem.add(drag.info.$el);
michael@0 74 };
michael@0 75
michael@0 76 this.draggable();
michael@0 77
michael@0 78 let self = this;
michael@0 79
michael@0 80 // ___ more div setup
michael@0 81 $div.mousedown(function(e) {
michael@0 82 if (!Utils.isRightClick(e))
michael@0 83 self.lastMouseDownTarget = e.target;
michael@0 84 });
michael@0 85
michael@0 86 $div.mouseup(function(e) {
michael@0 87 var same = (e.target == self.lastMouseDownTarget);
michael@0 88 self.lastMouseDownTarget = null;
michael@0 89 if (!same)
michael@0 90 return;
michael@0 91
michael@0 92 // press close button or middle mouse click
michael@0 93 if (iQ(e.target).hasClass("close") || Utils.isMiddleClick(e)) {
michael@0 94 self.closedManually = true;
michael@0 95 self.close();
michael@0 96 } else {
michael@0 97 if (!Items.item(this).isDragging)
michael@0 98 self.zoomIn();
michael@0 99 }
michael@0 100 });
michael@0 101
michael@0 102 this.droppable(true);
michael@0 103
michael@0 104 this.$close.attr("title", tabbrowserString("tabs.closeTab"));
michael@0 105
michael@0 106 TabItems.register(this);
michael@0 107
michael@0 108 // ___ reconnect to data from Storage
michael@0 109 if (!TabItems.reconnectingPaused())
michael@0 110 this._reconnect(options);
michael@0 111 };
michael@0 112
michael@0 113 TabItem.prototype = Utils.extend(new Item(), new Subscribable(), {
michael@0 114 // ----------
michael@0 115 // Function: toString
michael@0 116 // Prints [TabItem (tab)] for debug use
michael@0 117 toString: function TabItem_toString() {
michael@0 118 return "[TabItem (" + this.tab + ")]";
michael@0 119 },
michael@0 120
michael@0 121 // ----------
michael@0 122 // Function: forceCanvasSize
michael@0 123 // Repaints the thumbnail with the given resolution, and forces it
michael@0 124 // to stay that resolution until unforceCanvasSize is called.
michael@0 125 forceCanvasSize: function TabItem_forceCanvasSize(w, h) {
michael@0 126 this.canvasSizeForced = true;
michael@0 127 this.$canvas[0].width = w;
michael@0 128 this.$canvas[0].height = h;
michael@0 129 this.tabCanvas.paint();
michael@0 130 },
michael@0 131
michael@0 132 // ----------
michael@0 133 // Function: unforceCanvasSize
michael@0 134 // Stops holding the thumbnail resolution; allows it to shift to the
michael@0 135 // size of thumbnail on screen. Note that this call does not nest, unlike
michael@0 136 // <TabItems.resumePainting>; if you call forceCanvasSize multiple
michael@0 137 // times, you just need a single unforce to clear them all.
michael@0 138 unforceCanvasSize: function TabItem_unforceCanvasSize() {
michael@0 139 this.canvasSizeForced = false;
michael@0 140 },
michael@0 141
michael@0 142 // ----------
michael@0 143 // Function: isShowingCachedData
michael@0 144 // Returns a boolean indicates whether the cached data is being displayed or
michael@0 145 // not.
michael@0 146 isShowingCachedData: function TabItem_isShowingCachedData() {
michael@0 147 return this._showsCachedData;
michael@0 148 },
michael@0 149
michael@0 150 // ----------
michael@0 151 // Function: showCachedData
michael@0 152 // Shows the cached data i.e. image and title. Note: this method should only
michael@0 153 // be called at browser startup with the cached data avaliable.
michael@0 154 showCachedData: function TabItem_showCachedData() {
michael@0 155 let {title, url} = this.getTabState();
michael@0 156 let thumbnailURL = gPageThumbnails.getThumbnailURL(url);
michael@0 157
michael@0 158 this.$cachedThumb.attr("src", thumbnailURL).show();
michael@0 159 this.$canvas.css({opacity: 0});
michael@0 160
michael@0 161 let tooltip = (title && title != url ? title + "\n" + url : url);
michael@0 162 this.$tabTitle.text(title).attr("title", tooltip);
michael@0 163 this._showsCachedData = true;
michael@0 164 },
michael@0 165
michael@0 166 // ----------
michael@0 167 // Function: hideCachedData
michael@0 168 // Hides the cached data i.e. image and title and show the canvas.
michael@0 169 hideCachedData: function TabItem_hideCachedData() {
michael@0 170 this.$cachedThumb.attr("src", "").hide();
michael@0 171 this.$canvas.css({opacity: 1.0});
michael@0 172 this._showsCachedData = false;
michael@0 173 },
michael@0 174
michael@0 175 // ----------
michael@0 176 // Function: getStorageData
michael@0 177 // Get data to be used for persistent storage of this object.
michael@0 178 getStorageData: function TabItem_getStorageData() {
michael@0 179 let data = {
michael@0 180 groupID: (this.parent ? this.parent.id : 0)
michael@0 181 };
michael@0 182 if (this.parent && this.parent.getActiveTab() == this)
michael@0 183 data.active = true;
michael@0 184
michael@0 185 return data;
michael@0 186 },
michael@0 187
michael@0 188 // ----------
michael@0 189 // Function: save
michael@0 190 // Store persistent for this object.
michael@0 191 save: function TabItem_save() {
michael@0 192 try {
michael@0 193 if (!this.tab || !Utils.isValidXULTab(this.tab) || !this._reconnected) // too soon/late to save
michael@0 194 return;
michael@0 195
michael@0 196 let data = this.getStorageData();
michael@0 197 if (TabItems.storageSanity(data))
michael@0 198 Storage.saveTab(this.tab, data);
michael@0 199 } catch(e) {
michael@0 200 Utils.log("Error in saving tab value: "+e);
michael@0 201 }
michael@0 202 },
michael@0 203
michael@0 204 // ----------
michael@0 205 // Function: _getCurrentTabStateEntry
michael@0 206 // Returns the current tab state's active history entry.
michael@0 207 _getCurrentTabStateEntry: function TabItem__getCurrentTabStateEntry() {
michael@0 208 let tabState = Storage.getTabState(this.tab);
michael@0 209
michael@0 210 if (tabState) {
michael@0 211 let index = (tabState.index || tabState.entries.length) - 1;
michael@0 212 if (index in tabState.entries)
michael@0 213 return tabState.entries[index];
michael@0 214 }
michael@0 215
michael@0 216 return null;
michael@0 217 },
michael@0 218
michael@0 219 // ----------
michael@0 220 // Function: getTabState
michael@0 221 // Returns the current tab state, i.e. the title and URL of the active
michael@0 222 // history entry.
michael@0 223 getTabState: function TabItem_getTabState() {
michael@0 224 let entry = this._getCurrentTabStateEntry();
michael@0 225 let title = "";
michael@0 226 let url = "";
michael@0 227
michael@0 228 if (entry) {
michael@0 229 if (entry.title)
michael@0 230 title = entry.title;
michael@0 231
michael@0 232 url = entry.url;
michael@0 233 } else {
michael@0 234 url = this.tab.linkedBrowser.currentURI.spec;
michael@0 235 }
michael@0 236
michael@0 237 return {title: title, url: url};
michael@0 238 },
michael@0 239
michael@0 240 // ----------
michael@0 241 // Function: _reconnect
michael@0 242 // Load the reciever's persistent data from storage. If there is none,
michael@0 243 // treats it as a new tab.
michael@0 244 //
michael@0 245 // Parameters:
michael@0 246 // options - an object with additional parameters, see below
michael@0 247 //
michael@0 248 // Possible options:
michael@0 249 // groupItemId - if the tab doesn't have any data associated with it and
michael@0 250 // groupItemId is available, add the tab to that group.
michael@0 251 _reconnect: function TabItem__reconnect(options) {
michael@0 252 Utils.assertThrow(!this._reconnected, "shouldn't already be reconnected");
michael@0 253 Utils.assertThrow(this.tab, "should have a xul:tab");
michael@0 254
michael@0 255 let tabData = Storage.getTabData(this.tab);
michael@0 256 let groupItem;
michael@0 257
michael@0 258 if (tabData && TabItems.storageSanity(tabData)) {
michael@0 259 // Show the cached data while we're waiting for the tabItem to be updated.
michael@0 260 // If the tab isn't restored yet this acts as a placeholder until it is.
michael@0 261 this.showCachedData();
michael@0 262
michael@0 263 if (this.parent)
michael@0 264 this.parent.remove(this, {immediately: true});
michael@0 265
michael@0 266 if (tabData.groupID)
michael@0 267 groupItem = GroupItems.groupItem(tabData.groupID);
michael@0 268 else
michael@0 269 groupItem = new GroupItem([], {immediately: true, bounds: tabData.bounds});
michael@0 270
michael@0 271 if (groupItem) {
michael@0 272 groupItem.add(this, {immediately: true});
michael@0 273
michael@0 274 // restore the active tab for each group between browser sessions
michael@0 275 if (tabData.active)
michael@0 276 groupItem.setActiveTab(this);
michael@0 277
michael@0 278 // if it matches the selected tab or no active tab and the browser
michael@0 279 // tab is hidden, the active group item would be set.
michael@0 280 if (this.tab.selected ||
michael@0 281 (!GroupItems.getActiveGroupItem() && !this.tab.hidden))
michael@0 282 UI.setActive(this.parent);
michael@0 283 }
michael@0 284 } else {
michael@0 285 if (options && options.groupItemId)
michael@0 286 groupItem = GroupItems.groupItem(options.groupItemId);
michael@0 287
michael@0 288 if (groupItem) {
michael@0 289 groupItem.add(this, {immediately: true});
michael@0 290 } else {
michael@0 291 // create tab group by double click is handled in UI_init().
michael@0 292 GroupItems.newTab(this, {immediately: true});
michael@0 293 }
michael@0 294 }
michael@0 295
michael@0 296 this._reconnected = true;
michael@0 297 this.save();
michael@0 298 this._sendToSubscribers("reconnected");
michael@0 299 },
michael@0 300
michael@0 301 // ----------
michael@0 302 // Function: setHidden
michael@0 303 // Hide/unhide this item
michael@0 304 setHidden: function TabItem_setHidden(val) {
michael@0 305 if (val)
michael@0 306 this.addClass("tabHidden");
michael@0 307 else
michael@0 308 this.removeClass("tabHidden");
michael@0 309 this._hidden = val;
michael@0 310 },
michael@0 311
michael@0 312 // ----------
michael@0 313 // Function: getHidden
michael@0 314 // Return hide state of item
michael@0 315 getHidden: function TabItem_getHidden() {
michael@0 316 return this._hidden;
michael@0 317 },
michael@0 318
michael@0 319 // ----------
michael@0 320 // Function: setBounds
michael@0 321 // Moves this item to the specified location and size.
michael@0 322 //
michael@0 323 // Parameters:
michael@0 324 // rect - a <Rect> giving the new bounds
michael@0 325 // immediately - true if it should not animate; default false
michael@0 326 // options - an object with additional parameters, see below
michael@0 327 //
michael@0 328 // Possible options:
michael@0 329 // force - true to always update the DOM even if the bounds haven't changed; default false
michael@0 330 setBounds: function TabItem_setBounds(inRect, immediately, options) {
michael@0 331 Utils.assert(Utils.isRect(inRect), 'TabItem.setBounds: rect is not a real rectangle!');
michael@0 332
michael@0 333 if (!options)
michael@0 334 options = {};
michael@0 335
michael@0 336 // force the input size to be valid
michael@0 337 let validSize = TabItems.calcValidSize(
michael@0 338 new Point(inRect.width, inRect.height),
michael@0 339 {hideTitle: (this.isStacked || options.hideTitle === true)});
michael@0 340 let rect = new Rect(inRect.left, inRect.top,
michael@0 341 validSize.x, validSize.y);
michael@0 342
michael@0 343 var css = {};
michael@0 344
michael@0 345 if (rect.left != this.bounds.left || options.force)
michael@0 346 css.left = rect.left;
michael@0 347
michael@0 348 if (rect.top != this.bounds.top || options.force)
michael@0 349 css.top = rect.top;
michael@0 350
michael@0 351 if (rect.width != this.bounds.width || options.force) {
michael@0 352 css.width = rect.width - TabItems.tabItemPadding.x;
michael@0 353 css.fontSize = TabItems.getFontSizeFromWidth(rect.width);
michael@0 354 css.fontSize += 'px';
michael@0 355 }
michael@0 356
michael@0 357 if (rect.height != this.bounds.height || options.force) {
michael@0 358 css.height = rect.height - TabItems.tabItemPadding.y;
michael@0 359 if (!this.isStacked)
michael@0 360 css.height -= TabItems.fontSizeRange.max;
michael@0 361 }
michael@0 362
michael@0 363 if (Utils.isEmptyObject(css))
michael@0 364 return;
michael@0 365
michael@0 366 this.bounds.copy(rect);
michael@0 367
michael@0 368 // If this is a brand new tab don't animate it in from
michael@0 369 // a random location (i.e., from [0,0]). Instead, just
michael@0 370 // have it appear where it should be.
michael@0 371 if (immediately || (!this._hasBeenDrawn)) {
michael@0 372 this.$container.css(css);
michael@0 373 } else {
michael@0 374 TabItems.pausePainting();
michael@0 375 this.$container.animate(css, {
michael@0 376 duration: 200,
michael@0 377 easing: "tabviewBounce",
michael@0 378 complete: function() {
michael@0 379 TabItems.resumePainting();
michael@0 380 }
michael@0 381 });
michael@0 382 }
michael@0 383
michael@0 384 if (css.fontSize && !(this.parent && this.parent.isStacked())) {
michael@0 385 if (css.fontSize < TabItems.fontSizeRange.min)
michael@0 386 immediately ? this.$tabTitle.hide() : this.$tabTitle.fadeOut();
michael@0 387 else
michael@0 388 immediately ? this.$tabTitle.show() : this.$tabTitle.fadeIn();
michael@0 389 }
michael@0 390
michael@0 391 if (css.width) {
michael@0 392 TabItems.update(this.tab);
michael@0 393
michael@0 394 let widthRange, proportion;
michael@0 395
michael@0 396 if (this.parent && this.parent.isStacked()) {
michael@0 397 if (UI.rtl) {
michael@0 398 this.$fav.css({top:0, right:0});
michael@0 399 } else {
michael@0 400 this.$fav.css({top:0, left:0});
michael@0 401 }
michael@0 402 widthRange = new Range(70, 90);
michael@0 403 proportion = widthRange.proportion(css.width); // between 0 and 1
michael@0 404 } else {
michael@0 405 if (UI.rtl) {
michael@0 406 this.$fav.css({top:4, right:2});
michael@0 407 } else {
michael@0 408 this.$fav.css({top:4, left:4});
michael@0 409 }
michael@0 410 widthRange = new Range(40, 45);
michael@0 411 proportion = widthRange.proportion(css.width); // between 0 and 1
michael@0 412 }
michael@0 413
michael@0 414 if (proportion <= .1)
michael@0 415 this.$close.hide();
michael@0 416 else
michael@0 417 this.$close.show().css({opacity:proportion});
michael@0 418
michael@0 419 var pad = 1 + 5 * proportion;
michael@0 420 var alphaRange = new Range(0.1,0.2);
michael@0 421 this.$fav.css({
michael@0 422 "-moz-padding-start": pad + "px",
michael@0 423 "-moz-padding-end": pad + 2 + "px",
michael@0 424 "padding-top": pad + "px",
michael@0 425 "padding-bottom": pad + "px",
michael@0 426 "border-color": "rgba(0,0,0,"+ alphaRange.scale(proportion) +")",
michael@0 427 });
michael@0 428 }
michael@0 429
michael@0 430 this._hasBeenDrawn = true;
michael@0 431
michael@0 432 UI.clearShouldResizeItems();
michael@0 433
michael@0 434 rect = this.getBounds(); // ensure that it's a <Rect>
michael@0 435
michael@0 436 Utils.assert(Utils.isRect(this.bounds), 'TabItem.setBounds: this.bounds is not a real rectangle!');
michael@0 437
michael@0 438 if (!this.parent && Utils.isValidXULTab(this.tab))
michael@0 439 this.setTrenches(rect);
michael@0 440
michael@0 441 this.save();
michael@0 442 },
michael@0 443
michael@0 444 // ----------
michael@0 445 // Function: setZ
michael@0 446 // Sets the z-index for this item.
michael@0 447 setZ: function TabItem_setZ(value) {
michael@0 448 this.zIndex = value;
michael@0 449 this.$container.css({zIndex: value});
michael@0 450 },
michael@0 451
michael@0 452 // ----------
michael@0 453 // Function: close
michael@0 454 // Closes this item (actually closes the tab associated with it, which automatically
michael@0 455 // closes the item.
michael@0 456 // Parameters:
michael@0 457 // groupClose - true if this method is called by group close action.
michael@0 458 // Returns true if this tab is removed.
michael@0 459 close: function TabItem_close(groupClose) {
michael@0 460 // When the last tab is closed, put a new tab into closing tab's group. If
michael@0 461 // closing tab doesn't belong to a group and no empty group, create a new
michael@0 462 // one for the new tab.
michael@0 463 if (!groupClose && gBrowser.tabs.length == 1) {
michael@0 464 let group = this.tab._tabViewTabItem.parent;
michael@0 465 group.newTab(null, { closedLastTab: true });
michael@0 466 }
michael@0 467
michael@0 468 // when "TabClose" event is fired, the browser tab is about to close and our
michael@0 469 // item "close" is fired before the browser tab actually get closed.
michael@0 470 // Therefore, we need "tabRemoved" event below.
michael@0 471 gBrowser.removeTab(this.tab);
michael@0 472 let tabClosed = !this.tab;
michael@0 473
michael@0 474 if (tabClosed)
michael@0 475 this._sendToSubscribers("tabRemoved");
michael@0 476
michael@0 477 // No need to explicitly delete the tab data, becasue sessionstore data
michael@0 478 // associated with the tab will automatically go away
michael@0 479 return tabClosed;
michael@0 480 },
michael@0 481
michael@0 482 // ----------
michael@0 483 // Function: addClass
michael@0 484 // Adds the specified CSS class to this item's container DOM element.
michael@0 485 addClass: function TabItem_addClass(className) {
michael@0 486 this.$container.addClass(className);
michael@0 487 },
michael@0 488
michael@0 489 // ----------
michael@0 490 // Function: removeClass
michael@0 491 // Removes the specified CSS class from this item's container DOM element.
michael@0 492 removeClass: function TabItem_removeClass(className) {
michael@0 493 this.$container.removeClass(className);
michael@0 494 },
michael@0 495
michael@0 496 // ----------
michael@0 497 // Function: makeActive
michael@0 498 // Updates this item to visually indicate that it's active.
michael@0 499 makeActive: function TabItem_makeActive() {
michael@0 500 this.$container.addClass("focus");
michael@0 501
michael@0 502 if (this.parent)
michael@0 503 this.parent.setActiveTab(this);
michael@0 504 },
michael@0 505
michael@0 506 // ----------
michael@0 507 // Function: makeDeactive
michael@0 508 // Updates this item to visually indicate that it's not active.
michael@0 509 makeDeactive: function TabItem_makeDeactive() {
michael@0 510 this.$container.removeClass("focus");
michael@0 511 },
michael@0 512
michael@0 513 // ----------
michael@0 514 // Function: zoomIn
michael@0 515 // Allows you to select the tab and zoom in on it, thereby bringing you
michael@0 516 // to the tab in Firefox to interact with.
michael@0 517 // Parameters:
michael@0 518 // isNewBlankTab - boolean indicates whether it is a newly opened blank tab.
michael@0 519 zoomIn: function TabItem_zoomIn(isNewBlankTab) {
michael@0 520 // don't allow zoom in if its group is hidden
michael@0 521 if (this.parent && this.parent.hidden)
michael@0 522 return;
michael@0 523
michael@0 524 let self = this;
michael@0 525 let $tabEl = this.$container;
michael@0 526 let $canvas = this.$canvas;
michael@0 527
michael@0 528 Search.hide();
michael@0 529
michael@0 530 UI.setActive(this);
michael@0 531 TabItems._update(this.tab, {force: true});
michael@0 532
michael@0 533 // Zoom in!
michael@0 534 let tab = this.tab;
michael@0 535
michael@0 536 function onZoomDone() {
michael@0 537 $canvas.css({ 'transform': null });
michael@0 538 $tabEl.removeClass("front");
michael@0 539
michael@0 540 UI.goToTab(tab);
michael@0 541
michael@0 542 // tab might not be selected because hideTabView() is invoked after
michael@0 543 // UI.goToTab() so we need to setup everything for the gBrowser.selectedTab
michael@0 544 if (!tab.selected) {
michael@0 545 UI.onTabSelect(gBrowser.selectedTab);
michael@0 546 } else {
michael@0 547 if (isNewBlankTab)
michael@0 548 gWindow.gURLBar.focus();
michael@0 549 }
michael@0 550 if (self.parent && self.parent.expanded)
michael@0 551 self.parent.collapse();
michael@0 552
michael@0 553 self._sendToSubscribers("zoomedIn");
michael@0 554 }
michael@0 555
michael@0 556 let animateZoom = gPrefBranch.getBoolPref("animate_zoom");
michael@0 557 if (animateZoom) {
michael@0 558 let transform = this.getZoomTransform();
michael@0 559 TabItems.pausePainting();
michael@0 560
michael@0 561 if (this.parent && this.parent.expanded)
michael@0 562 $tabEl.removeClass("stack-trayed");
michael@0 563 $tabEl.addClass("front");
michael@0 564 $canvas
michael@0 565 .css({ 'transform-origin': transform.transformOrigin })
michael@0 566 .animate({ 'transform': transform.transform }, {
michael@0 567 duration: 230,
michael@0 568 easing: 'fast',
michael@0 569 complete: function() {
michael@0 570 onZoomDone();
michael@0 571
michael@0 572 setTimeout(function() {
michael@0 573 TabItems.resumePainting();
michael@0 574 }, 0);
michael@0 575 }
michael@0 576 });
michael@0 577 } else {
michael@0 578 setTimeout(onZoomDone, 0);
michael@0 579 }
michael@0 580 },
michael@0 581
michael@0 582 // ----------
michael@0 583 // Function: zoomOut
michael@0 584 // Handles the zoom down animation after returning to TabView.
michael@0 585 // It is expected that this routine will be called from the chrome thread
michael@0 586 //
michael@0 587 // Parameters:
michael@0 588 // complete - a function to call after the zoom down animation
michael@0 589 zoomOut: function TabItem_zoomOut(complete) {
michael@0 590 let $tab = this.$container, $canvas = this.$canvas;
michael@0 591 var self = this;
michael@0 592
michael@0 593 let onZoomDone = function onZoomDone() {
michael@0 594 $tab.removeClass("front");
michael@0 595 $canvas.css("transform", null);
michael@0 596
michael@0 597 if (typeof complete == "function")
michael@0 598 complete();
michael@0 599 };
michael@0 600
michael@0 601 UI.setActive(this);
michael@0 602 TabItems._update(this.tab, {force: true});
michael@0 603
michael@0 604 $tab.addClass("front");
michael@0 605
michael@0 606 let animateZoom = gPrefBranch.getBoolPref("animate_zoom");
michael@0 607 if (animateZoom) {
michael@0 608 // The scaleCheat of 2 here is a clever way to speed up the zoom-out
michael@0 609 // code. See getZoomTransform() below.
michael@0 610 let transform = this.getZoomTransform(2);
michael@0 611 TabItems.pausePainting();
michael@0 612
michael@0 613 $canvas.css({
michael@0 614 'transform': transform.transform,
michael@0 615 'transform-origin': transform.transformOrigin
michael@0 616 });
michael@0 617
michael@0 618 $canvas.animate({ "transform": "scale(1.0)" }, {
michael@0 619 duration: 300,
michael@0 620 easing: 'cubic-bezier', // note that this is legal easing, even without parameters
michael@0 621 complete: function() {
michael@0 622 TabItems.resumePainting();
michael@0 623 onZoomDone();
michael@0 624 }
michael@0 625 });
michael@0 626 } else {
michael@0 627 onZoomDone();
michael@0 628 }
michael@0 629 },
michael@0 630
michael@0 631 // ----------
michael@0 632 // Function: getZoomTransform
michael@0 633 // Returns the transform function which represents the maximum bounds of the
michael@0 634 // tab thumbnail in the zoom animation.
michael@0 635 getZoomTransform: function TabItem_getZoomTransform(scaleCheat) {
michael@0 636 // Taking the bounds of the container (as opposed to the canvas) makes us
michael@0 637 // immune to any transformations applied to the canvas.
michael@0 638 let { left, top, width, height, right, bottom } = this.$container.bounds();
michael@0 639
michael@0 640 let { innerWidth: windowWidth, innerHeight: windowHeight } = window;
michael@0 641
michael@0 642 // The scaleCheat is a clever way to speed up the zoom-in code.
michael@0 643 // Because image scaling is slowest on big images, we cheat and stop
michael@0 644 // the image at scaled-down size and placed accordingly. Because the
michael@0 645 // animation is fast, you can't see the difference but it feels a lot
michael@0 646 // zippier. The only trick is choosing the right animation function so
michael@0 647 // that you don't see a change in percieved animation speed from frame #1
michael@0 648 // (the tab) to frame #2 (the half-size image) to frame #3 (the first frame
michael@0 649 // of real animation). Choosing an animation that starts fast is key.
michael@0 650
michael@0 651 if (!scaleCheat)
michael@0 652 scaleCheat = 1.7;
michael@0 653
michael@0 654 let zoomWidth = width + (window.innerWidth - width) / scaleCheat;
michael@0 655 let zoomScaleFactor = zoomWidth / width;
michael@0 656
michael@0 657 let zoomHeight = height * zoomScaleFactor;
michael@0 658 let zoomTop = top * (1 - 1/scaleCheat);
michael@0 659 let zoomLeft = left * (1 - 1/scaleCheat);
michael@0 660
michael@0 661 let xOrigin = (left - zoomLeft) / ((left - zoomLeft) + (zoomLeft + zoomWidth - right)) * 100;
michael@0 662 let yOrigin = (top - zoomTop) / ((top - zoomTop) + (zoomTop + zoomHeight - bottom)) * 100;
michael@0 663
michael@0 664 return {
michael@0 665 transformOrigin: xOrigin + "% " + yOrigin + "%",
michael@0 666 transform: "scale(" + zoomScaleFactor + ")"
michael@0 667 };
michael@0 668 },
michael@0 669
michael@0 670 // ----------
michael@0 671 // Function: updateCanvas
michael@0 672 // Updates the tabitem's canvas.
michael@0 673 updateCanvas: function TabItem_updateCanvas() {
michael@0 674 // ___ thumbnail
michael@0 675 let $canvas = this.$canvas;
michael@0 676 if (!this.canvasSizeForced) {
michael@0 677 let w = $canvas.width();
michael@0 678 let h = $canvas.height();
michael@0 679 if (w != $canvas[0].width || h != $canvas[0].height) {
michael@0 680 $canvas[0].width = w;
michael@0 681 $canvas[0].height = h;
michael@0 682 }
michael@0 683 }
michael@0 684
michael@0 685 TabItems._lastUpdateTime = Date.now();
michael@0 686 this._lastTabUpdateTime = TabItems._lastUpdateTime;
michael@0 687
michael@0 688 if (this.tabCanvas)
michael@0 689 this.tabCanvas.paint();
michael@0 690
michael@0 691 // ___ cache
michael@0 692 if (this.isShowingCachedData())
michael@0 693 this.hideCachedData();
michael@0 694 }
michael@0 695 });
michael@0 696
michael@0 697 // ##########
michael@0 698 // Class: TabItems
michael@0 699 // Singleton for managing <TabItem>s
michael@0 700 let TabItems = {
michael@0 701 minTabWidth: 40,
michael@0 702 tabWidth: 160,
michael@0 703 tabHeight: 120,
michael@0 704 tabAspect: 0, // set in init
michael@0 705 invTabAspect: 0, // set in init
michael@0 706 fontSize: 9,
michael@0 707 fontSizeRange: new Range(8,15),
michael@0 708 _fragment: null,
michael@0 709 items: [],
michael@0 710 paintingPaused: 0,
michael@0 711 _tabsWaitingForUpdate: null,
michael@0 712 _heartbeat: null, // see explanation at startHeartbeat() below
michael@0 713 _heartbeatTiming: 200, // milliseconds between calls
michael@0 714 _maxTimeForUpdating: 200, // milliseconds that consecutive updates can take
michael@0 715 _lastUpdateTime: Date.now(),
michael@0 716 _eventListeners: [],
michael@0 717 _pauseUpdateForTest: false,
michael@0 718 _reconnectingPaused: false,
michael@0 719 tabItemPadding: {},
michael@0 720 _mozAfterPaintHandler: null,
michael@0 721
michael@0 722 // ----------
michael@0 723 // Function: toString
michael@0 724 // Prints [TabItems count=count] for debug use
michael@0 725 toString: function TabItems_toString() {
michael@0 726 return "[TabItems count=" + this.items.length + "]";
michael@0 727 },
michael@0 728
michael@0 729 // ----------
michael@0 730 // Function: init
michael@0 731 // Set up the necessary tracking to maintain the <TabItems>s.
michael@0 732 init: function TabItems_init() {
michael@0 733 Utils.assert(window.AllTabs, "AllTabs must be initialized first");
michael@0 734 let self = this;
michael@0 735
michael@0 736 // Set up tab priority queue
michael@0 737 this._tabsWaitingForUpdate = new TabPriorityQueue();
michael@0 738 this.minTabHeight = this.minTabWidth * this.tabHeight / this.tabWidth;
michael@0 739 this.tabAspect = this.tabHeight / this.tabWidth;
michael@0 740 this.invTabAspect = 1 / this.tabAspect;
michael@0 741
michael@0 742 let $canvas = iQ("<canvas>")
michael@0 743 .attr('moz-opaque', '');
michael@0 744 $canvas.appendTo(iQ("body"));
michael@0 745 $canvas.hide();
michael@0 746
michael@0 747 let mm = gWindow.messageManager;
michael@0 748 this._mozAfterPaintHandler = this.onMozAfterPaint.bind(this);
michael@0 749 mm.addMessageListener("Panorama:MozAfterPaint", this._mozAfterPaintHandler);
michael@0 750
michael@0 751 // When a tab is opened, create the TabItem
michael@0 752 this._eventListeners.open = function (event) {
michael@0 753 let tab = event.target;
michael@0 754
michael@0 755 if (!tab.pinned)
michael@0 756 self.link(tab);
michael@0 757 }
michael@0 758 // When a tab's content is loaded, show the canvas and hide the cached data
michael@0 759 // if necessary.
michael@0 760 this._eventListeners.attrModified = function (event) {
michael@0 761 let tab = event.target;
michael@0 762
michael@0 763 if (!tab.pinned)
michael@0 764 self.update(tab);
michael@0 765 }
michael@0 766 // When a tab is closed, unlink.
michael@0 767 this._eventListeners.close = function (event) {
michael@0 768 let tab = event.target;
michael@0 769
michael@0 770 // XXX bug #635975 - don't unlink the tab if the dom window is closing.
michael@0 771 if (!tab.pinned && !UI.isDOMWindowClosing)
michael@0 772 self.unlink(tab);
michael@0 773 }
michael@0 774 for (let name in this._eventListeners) {
michael@0 775 AllTabs.register(name, this._eventListeners[name]);
michael@0 776 }
michael@0 777
michael@0 778 let activeGroupItem = GroupItems.getActiveGroupItem();
michael@0 779 let activeGroupItemId = activeGroupItem ? activeGroupItem.id : null;
michael@0 780 // For each tab, create the link.
michael@0 781 AllTabs.tabs.forEach(function (tab) {
michael@0 782 if (tab.pinned)
michael@0 783 return;
michael@0 784
michael@0 785 let options = {immediately: true};
michael@0 786 // if tab is visible in the tabstrip and doesn't have any data stored in
michael@0 787 // the session store (see TabItem__reconnect), it implies that it is a
michael@0 788 // new tab which is created before Panorama is initialized. Therefore,
michael@0 789 // passing the active group id to the link() method for setting it up.
michael@0 790 if (!tab.hidden && activeGroupItemId)
michael@0 791 options.groupItemId = activeGroupItemId;
michael@0 792 self.link(tab, options);
michael@0 793 self.update(tab);
michael@0 794 });
michael@0 795 },
michael@0 796
michael@0 797 // ----------
michael@0 798 // Function: uninit
michael@0 799 uninit: function TabItems_uninit() {
michael@0 800 let mm = gWindow.messageManager;
michael@0 801 mm.removeMessageListener("Panorama:MozAfterPaint", this._mozAfterPaintHandler);
michael@0 802
michael@0 803 for (let name in this._eventListeners) {
michael@0 804 AllTabs.unregister(name, this._eventListeners[name]);
michael@0 805 }
michael@0 806 this.items.forEach(function(tabItem) {
michael@0 807 delete tabItem.tab._tabViewTabItem;
michael@0 808
michael@0 809 for (let x in tabItem) {
michael@0 810 if (typeof tabItem[x] == "object")
michael@0 811 tabItem[x] = null;
michael@0 812 }
michael@0 813 });
michael@0 814
michael@0 815 this.items = null;
michael@0 816 this._eventListeners = null;
michael@0 817 this._lastUpdateTime = null;
michael@0 818 this._tabsWaitingForUpdate.clear();
michael@0 819 },
michael@0 820
michael@0 821 // ----------
michael@0 822 // Function: fragment
michael@0 823 // Return a DocumentFragment which has a single <div> child. This child node
michael@0 824 // will act as a template for all TabItem containers.
michael@0 825 // The first call of this function caches the DocumentFragment in _fragment.
michael@0 826 fragment: function TabItems_fragment() {
michael@0 827 if (this._fragment)
michael@0 828 return this._fragment;
michael@0 829
michael@0 830 let div = document.createElement("div");
michael@0 831 div.classList.add("tab");
michael@0 832 div.innerHTML = "<div class='thumb'>" +
michael@0 833 "<img class='cached-thumb' style='display:none'/><canvas moz-opaque/></div>" +
michael@0 834 "<div class='favicon'><img/></div>" +
michael@0 835 "<span class='tab-title'>&nbsp;</span>" +
michael@0 836 "<div class='close'></div>";
michael@0 837 this._fragment = document.createDocumentFragment();
michael@0 838 this._fragment.appendChild(div);
michael@0 839
michael@0 840 return this._fragment;
michael@0 841 },
michael@0 842
michael@0 843 // Function: _isComplete
michael@0 844 // Checks whether the xul:tab has fully loaded and calls a callback with a
michael@0 845 // boolean indicates whether the tab is loaded or not.
michael@0 846 _isComplete: function TabItems__isComplete(tab, callback) {
michael@0 847 Utils.assertThrow(tab, "tab");
michael@0 848
michael@0 849 // A pending tab can't be complete, yet.
michael@0 850 if (tab.hasAttribute("pending")) {
michael@0 851 setTimeout(() => callback(false));
michael@0 852 return;
michael@0 853 }
michael@0 854
michael@0 855 let mm = tab.linkedBrowser.messageManager;
michael@0 856 let message = "Panorama:isDocumentLoaded";
michael@0 857
michael@0 858 mm.addMessageListener(message, function onMessage(cx) {
michael@0 859 mm.removeMessageListener(cx.name, onMessage);
michael@0 860 callback(cx.json.isLoaded);
michael@0 861 });
michael@0 862 mm.sendAsyncMessage(message);
michael@0 863 },
michael@0 864
michael@0 865 // ----------
michael@0 866 // Function: onMozAfterPaint
michael@0 867 // Called when a web page is painted.
michael@0 868 onMozAfterPaint: function TabItems_onMozAfterPaint(cx) {
michael@0 869 let index = gBrowser.browsers.indexOf(cx.target);
michael@0 870 if (index == -1)
michael@0 871 return;
michael@0 872
michael@0 873 let tab = gBrowser.tabs[index];
michael@0 874 if (!tab.pinned)
michael@0 875 this.update(tab);
michael@0 876 },
michael@0 877
michael@0 878 // ----------
michael@0 879 // Function: update
michael@0 880 // Takes in a xul:tab.
michael@0 881 update: function TabItems_update(tab) {
michael@0 882 try {
michael@0 883 Utils.assertThrow(tab, "tab");
michael@0 884 Utils.assertThrow(!tab.pinned, "shouldn't be an app tab");
michael@0 885 Utils.assertThrow(tab._tabViewTabItem, "should already be linked");
michael@0 886
michael@0 887 let shouldDefer = (
michael@0 888 this.isPaintingPaused() ||
michael@0 889 this._tabsWaitingForUpdate.hasItems() ||
michael@0 890 Date.now() - this._lastUpdateTime < this._heartbeatTiming
michael@0 891 );
michael@0 892
michael@0 893 if (shouldDefer) {
michael@0 894 this._tabsWaitingForUpdate.push(tab);
michael@0 895 this.startHeartbeat();
michael@0 896 } else
michael@0 897 this._update(tab);
michael@0 898 } catch(e) {
michael@0 899 Utils.log(e);
michael@0 900 }
michael@0 901 },
michael@0 902
michael@0 903 // ----------
michael@0 904 // Function: _update
michael@0 905 // Takes in a xul:tab.
michael@0 906 //
michael@0 907 // Parameters:
michael@0 908 // tab - a xul tab to update
michael@0 909 // options - an object with additional parameters, see below
michael@0 910 //
michael@0 911 // Possible options:
michael@0 912 // force - true to always update the tab item even if it's incomplete
michael@0 913 _update: function TabItems__update(tab, options) {
michael@0 914 try {
michael@0 915 if (this._pauseUpdateForTest)
michael@0 916 return;
michael@0 917
michael@0 918 Utils.assertThrow(tab, "tab");
michael@0 919
michael@0 920 // ___ get the TabItem
michael@0 921 Utils.assertThrow(tab._tabViewTabItem, "must already be linked");
michael@0 922 let tabItem = tab._tabViewTabItem;
michael@0 923
michael@0 924 // Even if the page hasn't loaded, display the favicon and title
michael@0 925 // ___ icon
michael@0 926 FavIcons.getFavIconUrlForTab(tab, function TabItems__update_getFavIconUrlCallback(iconUrl) {
michael@0 927 let favImage = tabItem.$favImage[0];
michael@0 928 let fav = tabItem.$fav;
michael@0 929 if (iconUrl) {
michael@0 930 if (favImage.src != iconUrl)
michael@0 931 favImage.src = iconUrl;
michael@0 932 fav.show();
michael@0 933 } else {
michael@0 934 if (favImage.hasAttribute("src"))
michael@0 935 favImage.removeAttribute("src");
michael@0 936 fav.hide();
michael@0 937 }
michael@0 938 tabItem._sendToSubscribers("iconUpdated");
michael@0 939 });
michael@0 940
michael@0 941 // ___ label
michael@0 942 let label = tab.label;
michael@0 943 let $name = tabItem.$tabTitle;
michael@0 944 if ($name.text() != label)
michael@0 945 $name.text(label);
michael@0 946
michael@0 947 // ___ remove from waiting list now that we have no other
michael@0 948 // early returns
michael@0 949 this._tabsWaitingForUpdate.remove(tab);
michael@0 950
michael@0 951 // ___ URL
michael@0 952 let tabUrl = tab.linkedBrowser.currentURI.spec;
michael@0 953 let tooltip = (label == tabUrl ? label : label + "\n" + tabUrl);
michael@0 954 tabItem.$container.attr("title", tooltip);
michael@0 955
michael@0 956 // ___ Make sure the tab is complete and ready for updating.
michael@0 957 if (options && options.force) {
michael@0 958 tabItem.updateCanvas();
michael@0 959 tabItem._sendToSubscribers("updated");
michael@0 960 } else {
michael@0 961 this._isComplete(tab, function TabItems__update_isComplete(isComplete) {
michael@0 962 if (!Utils.isValidXULTab(tab) || tab.pinned)
michael@0 963 return;
michael@0 964
michael@0 965 if (isComplete) {
michael@0 966 tabItem.updateCanvas();
michael@0 967 tabItem._sendToSubscribers("updated");
michael@0 968 } else {
michael@0 969 this._tabsWaitingForUpdate.push(tab);
michael@0 970 }
michael@0 971 }.bind(this));
michael@0 972 }
michael@0 973 } catch(e) {
michael@0 974 Utils.log(e);
michael@0 975 }
michael@0 976 },
michael@0 977
michael@0 978 // ----------
michael@0 979 // Function: link
michael@0 980 // Takes in a xul:tab, creates a TabItem for it and adds it to the scene.
michael@0 981 link: function TabItems_link(tab, options) {
michael@0 982 try {
michael@0 983 Utils.assertThrow(tab, "tab");
michael@0 984 Utils.assertThrow(!tab.pinned, "shouldn't be an app tab");
michael@0 985 Utils.assertThrow(!tab._tabViewTabItem, "shouldn't already be linked");
michael@0 986 new TabItem(tab, options); // sets tab._tabViewTabItem to itself
michael@0 987 } catch(e) {
michael@0 988 Utils.log(e);
michael@0 989 }
michael@0 990 },
michael@0 991
michael@0 992 // ----------
michael@0 993 // Function: unlink
michael@0 994 // Takes in a xul:tab and destroys the TabItem associated with it.
michael@0 995 unlink: function TabItems_unlink(tab) {
michael@0 996 try {
michael@0 997 Utils.assertThrow(tab, "tab");
michael@0 998 Utils.assertThrow(tab._tabViewTabItem, "should already be linked");
michael@0 999 // note that it's ok to unlink an app tab; see .handleTabUnpin
michael@0 1000
michael@0 1001 this.unregister(tab._tabViewTabItem);
michael@0 1002 tab._tabViewTabItem._sendToSubscribers("close");
michael@0 1003 tab._tabViewTabItem.$container.remove();
michael@0 1004 tab._tabViewTabItem.removeTrenches();
michael@0 1005 Items.unsquish(null, tab._tabViewTabItem);
michael@0 1006
michael@0 1007 tab._tabViewTabItem.tab = null;
michael@0 1008 tab._tabViewTabItem.tabCanvas.tab = null;
michael@0 1009 tab._tabViewTabItem.tabCanvas = null;
michael@0 1010 tab._tabViewTabItem = null;
michael@0 1011 Storage.saveTab(tab, null);
michael@0 1012
michael@0 1013 this._tabsWaitingForUpdate.remove(tab);
michael@0 1014 } catch(e) {
michael@0 1015 Utils.log(e);
michael@0 1016 }
michael@0 1017 },
michael@0 1018
michael@0 1019 // ----------
michael@0 1020 // when a tab becomes pinned, destroy its TabItem
michael@0 1021 handleTabPin: function TabItems_handleTabPin(xulTab) {
michael@0 1022 this.unlink(xulTab);
michael@0 1023 },
michael@0 1024
michael@0 1025 // ----------
michael@0 1026 // when a tab becomes unpinned, create a TabItem for it
michael@0 1027 handleTabUnpin: function TabItems_handleTabUnpin(xulTab) {
michael@0 1028 this.link(xulTab);
michael@0 1029 this.update(xulTab);
michael@0 1030 },
michael@0 1031
michael@0 1032 // ----------
michael@0 1033 // Function: startHeartbeat
michael@0 1034 // Start a new heartbeat if there isn't one already started.
michael@0 1035 // The heartbeat is a chain of setTimeout calls that allows us to spread
michael@0 1036 // out update calls over a period of time.
michael@0 1037 // _heartbeat is used to make sure that we don't add multiple
michael@0 1038 // setTimeout chains.
michael@0 1039 startHeartbeat: function TabItems_startHeartbeat() {
michael@0 1040 if (!this._heartbeat) {
michael@0 1041 let self = this;
michael@0 1042 this._heartbeat = setTimeout(function() {
michael@0 1043 self._checkHeartbeat();
michael@0 1044 }, this._heartbeatTiming);
michael@0 1045 }
michael@0 1046 },
michael@0 1047
michael@0 1048 // ----------
michael@0 1049 // Function: _checkHeartbeat
michael@0 1050 // This periodically checks for tabs waiting to be updated, and calls
michael@0 1051 // _update on them.
michael@0 1052 // Should only be called by startHeartbeat and resumePainting.
michael@0 1053 _checkHeartbeat: function TabItems__checkHeartbeat() {
michael@0 1054 this._heartbeat = null;
michael@0 1055
michael@0 1056 if (this.isPaintingPaused())
michael@0 1057 return;
michael@0 1058
michael@0 1059 // restart the heartbeat to update all waiting tabs once the UI becomes idle
michael@0 1060 if (!UI.isIdle()) {
michael@0 1061 this.startHeartbeat();
michael@0 1062 return;
michael@0 1063 }
michael@0 1064
michael@0 1065 let accumTime = 0;
michael@0 1066 let items = this._tabsWaitingForUpdate.getItems();
michael@0 1067 // Do as many updates as we can fit into a "perceived" amount
michael@0 1068 // of time, which is tunable.
michael@0 1069 while (accumTime < this._maxTimeForUpdating && items.length) {
michael@0 1070 let updateBegin = Date.now();
michael@0 1071 this._update(items.pop());
michael@0 1072 let updateEnd = Date.now();
michael@0 1073
michael@0 1074 // Maintain a simple average of time for each tabitem update
michael@0 1075 // We can use this as a base by which to delay things like
michael@0 1076 // tab zooming, so there aren't any hitches.
michael@0 1077 let deltaTime = updateEnd - updateBegin;
michael@0 1078 accumTime += deltaTime;
michael@0 1079 }
michael@0 1080
michael@0 1081 if (this._tabsWaitingForUpdate.hasItems())
michael@0 1082 this.startHeartbeat();
michael@0 1083 },
michael@0 1084
michael@0 1085 // ----------
michael@0 1086 // Function: pausePainting
michael@0 1087 // Tells TabItems to stop updating thumbnails (so you can do
michael@0 1088 // animations without thumbnail paints causing stutters).
michael@0 1089 // pausePainting can be called multiple times, but every call to
michael@0 1090 // pausePainting needs to be mirrored with a call to <resumePainting>.
michael@0 1091 pausePainting: function TabItems_pausePainting() {
michael@0 1092 this.paintingPaused++;
michael@0 1093 if (this._heartbeat) {
michael@0 1094 clearTimeout(this._heartbeat);
michael@0 1095 this._heartbeat = null;
michael@0 1096 }
michael@0 1097 },
michael@0 1098
michael@0 1099 // ----------
michael@0 1100 // Function: resumePainting
michael@0 1101 // Undoes a call to <pausePainting>. For instance, if you called
michael@0 1102 // pausePainting three times in a row, you'll need to call resumePainting
michael@0 1103 // three times before TabItems will start updating thumbnails again.
michael@0 1104 resumePainting: function TabItems_resumePainting() {
michael@0 1105 this.paintingPaused--;
michael@0 1106 Utils.assert(this.paintingPaused > -1, "paintingPaused should not go below zero");
michael@0 1107 if (!this.isPaintingPaused())
michael@0 1108 this.startHeartbeat();
michael@0 1109 },
michael@0 1110
michael@0 1111 // ----------
michael@0 1112 // Function: isPaintingPaused
michael@0 1113 // Returns a boolean indicating whether painting
michael@0 1114 // is paused or not.
michael@0 1115 isPaintingPaused: function TabItems_isPaintingPaused() {
michael@0 1116 return this.paintingPaused > 0;
michael@0 1117 },
michael@0 1118
michael@0 1119 // ----------
michael@0 1120 // Function: pauseReconnecting
michael@0 1121 // Don't reconnect any new tabs until resume is called.
michael@0 1122 pauseReconnecting: function TabItems_pauseReconnecting() {
michael@0 1123 Utils.assertThrow(!this._reconnectingPaused, "shouldn't already be paused");
michael@0 1124
michael@0 1125 this._reconnectingPaused = true;
michael@0 1126 },
michael@0 1127
michael@0 1128 // ----------
michael@0 1129 // Function: resumeReconnecting
michael@0 1130 // Reconnect all of the tabs that were created since we paused.
michael@0 1131 resumeReconnecting: function TabItems_resumeReconnecting() {
michael@0 1132 Utils.assertThrow(this._reconnectingPaused, "should already be paused");
michael@0 1133
michael@0 1134 this._reconnectingPaused = false;
michael@0 1135 this.items.forEach(function(item) {
michael@0 1136 if (!item._reconnected)
michael@0 1137 item._reconnect();
michael@0 1138 });
michael@0 1139 },
michael@0 1140
michael@0 1141 // ----------
michael@0 1142 // Function: reconnectingPaused
michael@0 1143 // Returns true if reconnecting is paused.
michael@0 1144 reconnectingPaused: function TabItems_reconnectingPaused() {
michael@0 1145 return this._reconnectingPaused;
michael@0 1146 },
michael@0 1147
michael@0 1148 // ----------
michael@0 1149 // Function: register
michael@0 1150 // Adds the given <TabItem> to the master list.
michael@0 1151 register: function TabItems_register(item) {
michael@0 1152 Utils.assert(item && item.isAnItem, 'item must be a TabItem');
michael@0 1153 Utils.assert(this.items.indexOf(item) == -1, 'only register once per item');
michael@0 1154 this.items.push(item);
michael@0 1155 },
michael@0 1156
michael@0 1157 // ----------
michael@0 1158 // Function: unregister
michael@0 1159 // Removes the given <TabItem> from the master list.
michael@0 1160 unregister: function TabItems_unregister(item) {
michael@0 1161 var index = this.items.indexOf(item);
michael@0 1162 if (index != -1)
michael@0 1163 this.items.splice(index, 1);
michael@0 1164 },
michael@0 1165
michael@0 1166 // ----------
michael@0 1167 // Function: getItems
michael@0 1168 // Returns a copy of the master array of <TabItem>s.
michael@0 1169 getItems: function TabItems_getItems() {
michael@0 1170 return Utils.copy(this.items);
michael@0 1171 },
michael@0 1172
michael@0 1173 // ----------
michael@0 1174 // Function: saveAll
michael@0 1175 // Saves all open <TabItem>s.
michael@0 1176 saveAll: function TabItems_saveAll() {
michael@0 1177 let tabItems = this.getItems();
michael@0 1178
michael@0 1179 tabItems.forEach(function TabItems_saveAll_forEach(tabItem) {
michael@0 1180 tabItem.save();
michael@0 1181 });
michael@0 1182 },
michael@0 1183
michael@0 1184 // ----------
michael@0 1185 // Function: storageSanity
michael@0 1186 // Checks the specified data (as returned by TabItem.getStorageData or loaded from storage)
michael@0 1187 // and returns true if it looks valid.
michael@0 1188 // TODO: this is a stub, please implement
michael@0 1189 storageSanity: function TabItems_storageSanity(data) {
michael@0 1190 return true;
michael@0 1191 },
michael@0 1192
michael@0 1193 // ----------
michael@0 1194 // Function: getFontSizeFromWidth
michael@0 1195 // Private method that returns the fontsize to use given the tab's width
michael@0 1196 getFontSizeFromWidth: function TabItem_getFontSizeFromWidth(width) {
michael@0 1197 let widthRange = new Range(0, TabItems.tabWidth);
michael@0 1198 let proportion = widthRange.proportion(width - TabItems.tabItemPadding.x, true);
michael@0 1199 // proportion is in [0,1]
michael@0 1200 return TabItems.fontSizeRange.scale(proportion);
michael@0 1201 },
michael@0 1202
michael@0 1203 // ----------
michael@0 1204 // Function: _getWidthForHeight
michael@0 1205 // Private method that returns the tabitem width given a height.
michael@0 1206 _getWidthForHeight: function TabItems__getWidthForHeight(height) {
michael@0 1207 return height * TabItems.invTabAspect;
michael@0 1208 },
michael@0 1209
michael@0 1210 // ----------
michael@0 1211 // Function: _getHeightForWidth
michael@0 1212 // Private method that returns the tabitem height given a width.
michael@0 1213 _getHeightForWidth: function TabItems__getHeightForWidth(width) {
michael@0 1214 return width * TabItems.tabAspect;
michael@0 1215 },
michael@0 1216
michael@0 1217 // ----------
michael@0 1218 // Function: calcValidSize
michael@0 1219 // Pass in a desired size, and receive a size based on proper title
michael@0 1220 // size and aspect ratio.
michael@0 1221 calcValidSize: function TabItems_calcValidSize(size, options) {
michael@0 1222 Utils.assert(Utils.isPoint(size), 'input is a Point');
michael@0 1223
michael@0 1224 let width = Math.max(TabItems.minTabWidth, size.x);
michael@0 1225 let showTitle = !options || !options.hideTitle;
michael@0 1226 let titleSize = showTitle ? TabItems.fontSizeRange.max : 0;
michael@0 1227 let height = Math.max(TabItems.minTabHeight, size.y - titleSize);
michael@0 1228 let retSize = new Point(width, height);
michael@0 1229
michael@0 1230 if (size.x > -1)
michael@0 1231 retSize.y = this._getHeightForWidth(width);
michael@0 1232 if (size.y > -1)
michael@0 1233 retSize.x = this._getWidthForHeight(height);
michael@0 1234
michael@0 1235 if (size.x > -1 && size.y > -1) {
michael@0 1236 if (retSize.x < size.x)
michael@0 1237 retSize.y = this._getHeightForWidth(retSize.x);
michael@0 1238 else
michael@0 1239 retSize.x = this._getWidthForHeight(retSize.y);
michael@0 1240 }
michael@0 1241
michael@0 1242 if (showTitle)
michael@0 1243 retSize.y += titleSize;
michael@0 1244
michael@0 1245 return retSize;
michael@0 1246 }
michael@0 1247 };
michael@0 1248
michael@0 1249 // ##########
michael@0 1250 // Class: TabPriorityQueue
michael@0 1251 // Container that returns tab items in a priority order
michael@0 1252 // Current implementation assigns tab to either a high priority
michael@0 1253 // or low priority queue, and toggles which queue items are popped
michael@0 1254 // from. This guarantees that high priority items which are constantly
michael@0 1255 // being added will not eclipse changes for lower priority items.
michael@0 1256 function TabPriorityQueue() {
michael@0 1257 };
michael@0 1258
michael@0 1259 TabPriorityQueue.prototype = {
michael@0 1260 _low: [], // low priority queue
michael@0 1261 _high: [], // high priority queue
michael@0 1262
michael@0 1263 // ----------
michael@0 1264 // Function: toString
michael@0 1265 // Prints [TabPriorityQueue count=count] for debug use
michael@0 1266 toString: function TabPriorityQueue_toString() {
michael@0 1267 return "[TabPriorityQueue count=" + (this._low.length + this._high.length) + "]";
michael@0 1268 },
michael@0 1269
michael@0 1270 // ----------
michael@0 1271 // Function: clear
michael@0 1272 // Empty the update queue
michael@0 1273 clear: function TabPriorityQueue_clear() {
michael@0 1274 this._low = [];
michael@0 1275 this._high = [];
michael@0 1276 },
michael@0 1277
michael@0 1278 // ----------
michael@0 1279 // Function: hasItems
michael@0 1280 // Return whether pending items exist
michael@0 1281 hasItems: function TabPriorityQueue_hasItems() {
michael@0 1282 return (this._low.length > 0) || (this._high.length > 0);
michael@0 1283 },
michael@0 1284
michael@0 1285 // ----------
michael@0 1286 // Function: getItems
michael@0 1287 // Returns all queued items, ordered from low to high priority
michael@0 1288 getItems: function TabPriorityQueue_getItems() {
michael@0 1289 return this._low.concat(this._high);
michael@0 1290 },
michael@0 1291
michael@0 1292 // ----------
michael@0 1293 // Function: push
michael@0 1294 // Add an item to be prioritized
michael@0 1295 push: function TabPriorityQueue_push(tab) {
michael@0 1296 // Push onto correct priority queue.
michael@0 1297 // It's only low priority if it's in a stack, and isn't the top,
michael@0 1298 // and the stack isn't expanded.
michael@0 1299 // If it already exists in the destination queue,
michael@0 1300 // leave it. If it exists in a different queue, remove it first and push
michael@0 1301 // onto new queue.
michael@0 1302 let item = tab._tabViewTabItem;
michael@0 1303 if (item.parent && (item.parent.isStacked() &&
michael@0 1304 !item.parent.isTopOfStack(item) &&
michael@0 1305 !item.parent.expanded)) {
michael@0 1306 let idx = this._high.indexOf(tab);
michael@0 1307 if (idx != -1) {
michael@0 1308 this._high.splice(idx, 1);
michael@0 1309 this._low.unshift(tab);
michael@0 1310 } else if (this._low.indexOf(tab) == -1)
michael@0 1311 this._low.unshift(tab);
michael@0 1312 } else {
michael@0 1313 let idx = this._low.indexOf(tab);
michael@0 1314 if (idx != -1) {
michael@0 1315 this._low.splice(idx, 1);
michael@0 1316 this._high.unshift(tab);
michael@0 1317 } else if (this._high.indexOf(tab) == -1)
michael@0 1318 this._high.unshift(tab);
michael@0 1319 }
michael@0 1320 },
michael@0 1321
michael@0 1322 // ----------
michael@0 1323 // Function: pop
michael@0 1324 // Remove and return the next item in priority order
michael@0 1325 pop: function TabPriorityQueue_pop() {
michael@0 1326 let ret = null;
michael@0 1327 if (this._high.length)
michael@0 1328 ret = this._high.pop();
michael@0 1329 else if (this._low.length)
michael@0 1330 ret = this._low.pop();
michael@0 1331 return ret;
michael@0 1332 },
michael@0 1333
michael@0 1334 // ----------
michael@0 1335 // Function: peek
michael@0 1336 // Return the next item in priority order, without removing it
michael@0 1337 peek: function TabPriorityQueue_peek() {
michael@0 1338 let ret = null;
michael@0 1339 if (this._high.length)
michael@0 1340 ret = this._high[this._high.length-1];
michael@0 1341 else if (this._low.length)
michael@0 1342 ret = this._low[this._low.length-1];
michael@0 1343 return ret;
michael@0 1344 },
michael@0 1345
michael@0 1346 // ----------
michael@0 1347 // Function: remove
michael@0 1348 // Remove the passed item
michael@0 1349 remove: function TabPriorityQueue_remove(tab) {
michael@0 1350 let index = this._high.indexOf(tab);
michael@0 1351 if (index != -1)
michael@0 1352 this._high.splice(index, 1);
michael@0 1353 else {
michael@0 1354 index = this._low.indexOf(tab);
michael@0 1355 if (index != -1)
michael@0 1356 this._low.splice(index, 1);
michael@0 1357 }
michael@0 1358 }
michael@0 1359 };
michael@0 1360
michael@0 1361 // ##########
michael@0 1362 // Class: TabCanvas
michael@0 1363 // Takes care of the actual canvas for the tab thumbnail
michael@0 1364 // Does not need to be accessed from outside of tabitems.js
michael@0 1365 function TabCanvas(tab, canvas) {
michael@0 1366 this.tab = tab;
michael@0 1367 this.canvas = canvas;
michael@0 1368 };
michael@0 1369
michael@0 1370 TabCanvas.prototype = Utils.extend(new Subscribable(), {
michael@0 1371 // ----------
michael@0 1372 // Function: toString
michael@0 1373 // Prints [TabCanvas (tab)] for debug use
michael@0 1374 toString: function TabCanvas_toString() {
michael@0 1375 return "[TabCanvas (" + this.tab + ")]";
michael@0 1376 },
michael@0 1377
michael@0 1378 // ----------
michael@0 1379 // Function: paint
michael@0 1380 paint: function TabCanvas_paint(evt) {
michael@0 1381 var w = this.canvas.width;
michael@0 1382 var h = this.canvas.height;
michael@0 1383 if (!w || !h)
michael@0 1384 return;
michael@0 1385
michael@0 1386 if (!this.tab.linkedBrowser.contentWindow) {
michael@0 1387 Utils.log('no tab.linkedBrowser.contentWindow in TabCanvas.paint()');
michael@0 1388 return;
michael@0 1389 }
michael@0 1390
michael@0 1391 let win = this.tab.linkedBrowser.contentWindow;
michael@0 1392 gPageThumbnails.captureToCanvas(win, this.canvas);
michael@0 1393
michael@0 1394 this._sendToSubscribers("painted");
michael@0 1395 },
michael@0 1396
michael@0 1397 // ----------
michael@0 1398 // Function: toImageData
michael@0 1399 toImageData: function TabCanvas_toImageData() {
michael@0 1400 return this.canvas.toDataURL("image/png");
michael@0 1401 }
michael@0 1402 });

mercurial