1 /* 2 * Copyright (C) 2008 Apple Inc. All Rights Reserved. 3 * Copyright (C) 2011 Google Inc. All Rights Reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions 7 * are met: 8 * 1. Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * 2. Redistributions in binary form must reproduce the above copyright 11 * notice, this list of conditions and the following disclaimer in the 12 * documentation and/or other materials provided with the distribution. 13 * 14 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY 15 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 17 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR 18 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 19 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 20 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 21 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 22 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 */ 26 27 /** 28 * @constructor 29 * @extends {WebInspector.Object} 30 */ 31 WebInspector.View = function() 32 { 33 this.element = document.createElementWithClass("div", "view"); 34 this.element.__view = this; 35 this._visible = true; 36 this._isRoot = false; 37 this._isShowing = false; 38 this._children = []; 39 this._hideOnDetach = false; 40 this._cssFiles = []; 41 this._notificationDepth = 0; 42 } 43 44 WebInspector.View._cssFileToVisibleViewCount = {}; 45 WebInspector.View._cssFileToStyleElement = {}; 46 WebInspector.View._cssUnloadTimeout = 2000; 47 48 WebInspector.View._buildSourceURL = function(cssFile) 49 { 50 return "\n/*# sourceURL=" + WebInspector.ParsedURL.completeURL(window.location.href, cssFile) + " */"; 51 } 52 53 /** 54 * @param {string} cssFile 55 * @return {!Element} 56 */ 57 WebInspector.View.createStyleElement = function(cssFile) 58 { 59 var styleElement = document.createElement("style"); 60 styleElement.type = "text/css"; 61 styleElement.textContent = loadResource(cssFile) + WebInspector.View._buildSourceURL(cssFile); 62 document.head.insertBefore(styleElement, document.head.firstChild); 63 return styleElement; 64 } 65 66 WebInspector.View.prototype = { 67 markAsRoot: function() 68 { 69 WebInspector.View.__assert(!this.element.parentElement, "Attempt to mark as root attached node"); 70 this._isRoot = true; 71 }, 72 73 /** 74 * @return {?WebInspector.View} 75 */ 76 parentView: function() 77 { 78 return this._parentView; 79 }, 80 81 /** 82 * @return {!Array.<!WebInspector.View>} 83 */ 84 children: function() 85 { 86 return this._children; 87 }, 88 89 /** 90 * @return {boolean} 91 */ 92 isShowing: function() 93 { 94 return this._isShowing; 95 }, 96 97 setHideOnDetach: function() 98 { 99 this._hideOnDetach = true; 100 }, 101 102 /** 103 * @return {boolean} 104 */ 105 _inNotification: function() 106 { 107 return !!this._notificationDepth || (this._parentView && this._parentView._inNotification()); 108 }, 109 110 _parentIsShowing: function() 111 { 112 if (this._isRoot) 113 return true; 114 return this._parentView && this._parentView.isShowing(); 115 }, 116 117 /** 118 * @param {function(this:WebInspector.View)} method 119 */ 120 _callOnVisibleChildren: function(method) 121 { 122 var copy = this._children.slice(); 123 for (var i = 0; i < copy.length; ++i) { 124 if (copy[i]._parentView === this && copy[i]._visible) 125 method.call(copy[i]); 126 } 127 }, 128 129 _processWillShow: function() 130 { 131 this._loadCSSIfNeeded(); 132 this._callOnVisibleChildren(this._processWillShow); 133 this._isShowing = true; 134 }, 135 136 _processWasShown: function() 137 { 138 if (this._inNotification()) 139 return; 140 this.restoreScrollPositions(); 141 this._notify(this.wasShown); 142 this._callOnVisibleChildren(this._processWasShown); 143 }, 144 145 _processWillHide: function() 146 { 147 if (this._inNotification()) 148 return; 149 this.storeScrollPositions(); 150 151 this._callOnVisibleChildren(this._processWillHide); 152 this._notify(this.willHide); 153 this._isShowing = false; 154 }, 155 156 _processWasHidden: function() 157 { 158 this._disableCSSIfNeeded(); 159 this._callOnVisibleChildren(this._processWasHidden); 160 }, 161 162 _processOnResize: function() 163 { 164 if (this._inNotification()) 165 return; 166 if (!this.isShowing()) 167 return; 168 this._notify(this.onResize); 169 this._callOnVisibleChildren(this._processOnResize); 170 }, 171 172 /** 173 * @param {function(this:WebInspector.View)} notification 174 */ 175 _notify: function(notification) 176 { 177 ++this._notificationDepth; 178 try { 179 notification.call(this); 180 } finally { 181 --this._notificationDepth; 182 } 183 }, 184 185 wasShown: function() 186 { 187 }, 188 189 willHide: function() 190 { 191 }, 192 193 onResize: function() 194 { 195 }, 196 197 onLayout: function() 198 { 199 }, 200 201 /** 202 * @param {?Element} parentElement 203 * @param {?Element=} insertBefore 204 */ 205 show: function(parentElement, insertBefore) 206 { 207 WebInspector.View.__assert(parentElement, "Attempt to attach view with no parent element"); 208 209 // Update view hierarchy 210 if (this.element.parentElement !== parentElement) { 211 if (this.element.parentElement) 212 this.detach(); 213 214 var currentParent = parentElement; 215 while (currentParent && !currentParent.__view) 216 currentParent = currentParent.parentElement; 217 218 if (currentParent) { 219 this._parentView = currentParent.__view; 220 this._parentView._children.push(this); 221 this._isRoot = false; 222 } else 223 WebInspector.View.__assert(this._isRoot, "Attempt to attach view to orphan node"); 224 } else if (this._visible) { 225 return; 226 } 227 228 this._visible = true; 229 230 if (this._parentIsShowing()) 231 this._processWillShow(); 232 233 this.element.classList.add("visible"); 234 235 // Reparent 236 if (this.element.parentElement !== parentElement) { 237 WebInspector.View._incrementViewCounter(parentElement, this.element); 238 if (insertBefore) 239 WebInspector.View._originalInsertBefore.call(parentElement, this.element, insertBefore); 240 else 241 WebInspector.View._originalAppendChild.call(parentElement, this.element); 242 } 243 244 if (this._parentIsShowing()) 245 this._processWasShown(); 246 247 if (this._parentView && this._hasNonZeroConstraints()) 248 this._parentView.invalidateConstraints(); 249 else 250 this._processOnResize(); 251 }, 252 253 /** 254 * @param {boolean=} overrideHideOnDetach 255 */ 256 detach: function(overrideHideOnDetach) 257 { 258 var parentElement = this.element.parentElement; 259 if (!parentElement) 260 return; 261 262 if (this._parentIsShowing()) 263 this._processWillHide(); 264 265 if (this._hideOnDetach && !overrideHideOnDetach) { 266 this.element.classList.remove("visible"); 267 this._visible = false; 268 if (this._parentIsShowing()) 269 this._processWasHidden(); 270 if (this._parentView && this._hasNonZeroConstraints()) 271 this._parentView.invalidateConstraints(); 272 return; 273 } 274 275 // Force legal removal 276 WebInspector.View._decrementViewCounter(parentElement, this.element); 277 WebInspector.View._originalRemoveChild.call(parentElement, this.element); 278 279 this._visible = false; 280 if (this._parentIsShowing()) 281 this._processWasHidden(); 282 283 // Update view hierarchy 284 if (this._parentView) { 285 var childIndex = this._parentView._children.indexOf(this); 286 WebInspector.View.__assert(childIndex >= 0, "Attempt to remove non-child view"); 287 this._parentView._children.splice(childIndex, 1); 288 var parent = this._parentView; 289 this._parentView = null; 290 if (this._hasNonZeroConstraints()) 291 parent.invalidateConstraints(); 292 } else 293 WebInspector.View.__assert(this._isRoot, "Removing non-root view from DOM"); 294 }, 295 296 detachChildViews: function() 297 { 298 var children = this._children.slice(); 299 for (var i = 0; i < children.length; ++i) 300 children[i].detach(); 301 }, 302 303 /** 304 * @return {!Array.<!Element>} 305 */ 306 elementsToRestoreScrollPositionsFor: function() 307 { 308 return [this.element]; 309 }, 310 311 storeScrollPositions: function() 312 { 313 var elements = this.elementsToRestoreScrollPositionsFor(); 314 for (var i = 0; i < elements.length; ++i) { 315 var container = elements[i]; 316 container._scrollTop = container.scrollTop; 317 container._scrollLeft = container.scrollLeft; 318 } 319 }, 320 321 restoreScrollPositions: function() 322 { 323 var elements = this.elementsToRestoreScrollPositionsFor(); 324 for (var i = 0; i < elements.length; ++i) { 325 var container = elements[i]; 326 if (container._scrollTop) 327 container.scrollTop = container._scrollTop; 328 if (container._scrollLeft) 329 container.scrollLeft = container._scrollLeft; 330 } 331 }, 332 333 doResize: function() 334 { 335 if (!this.isShowing()) 336 return; 337 // No matter what notification we are in, dispatching onResize is not needed. 338 if (!this._inNotification()) 339 this._callOnVisibleChildren(this._processOnResize); 340 }, 341 342 doLayout: function() 343 { 344 if (!this.isShowing()) 345 return; 346 this._notify(this.onLayout); 347 this.doResize(); 348 }, 349 350 registerRequiredCSS: function(cssFile) 351 { 352 this._cssFiles.push(cssFile); 353 }, 354 355 _loadCSSIfNeeded: function() 356 { 357 for (var i = 0; i < this._cssFiles.length; ++i) { 358 var cssFile = this._cssFiles[i]; 359 360 var viewsWithCSSFile = WebInspector.View._cssFileToVisibleViewCount[cssFile]; 361 WebInspector.View._cssFileToVisibleViewCount[cssFile] = (viewsWithCSSFile || 0) + 1; 362 if (!viewsWithCSSFile) 363 this._doLoadCSS(cssFile); 364 } 365 }, 366 367 _doLoadCSS: function(cssFile) 368 { 369 var styleElement = WebInspector.View._cssFileToStyleElement[cssFile]; 370 if (styleElement) { 371 styleElement.disabled = false; 372 return; 373 } 374 styleElement = WebInspector.View.createStyleElement(cssFile); 375 WebInspector.View._cssFileToStyleElement[cssFile] = styleElement; 376 }, 377 378 _disableCSSIfNeeded: function() 379 { 380 var scheduleUnload = !!WebInspector.View._cssUnloadTimer; 381 382 for (var i = 0; i < this._cssFiles.length; ++i) { 383 var cssFile = this._cssFiles[i]; 384 385 if (!--WebInspector.View._cssFileToVisibleViewCount[cssFile]) 386 scheduleUnload = true; 387 } 388 389 function doUnloadCSS() 390 { 391 delete WebInspector.View._cssUnloadTimer; 392 393 for (cssFile in WebInspector.View._cssFileToVisibleViewCount) { 394 if (WebInspector.View._cssFileToVisibleViewCount.hasOwnProperty(cssFile) && !WebInspector.View._cssFileToVisibleViewCount[cssFile]) 395 WebInspector.View._cssFileToStyleElement[cssFile].disabled = true; 396 } 397 } 398 399 if (scheduleUnload && !WebInspector.View._cssUnloadTimer) 400 WebInspector.View._cssUnloadTimer = setTimeout(doUnloadCSS, WebInspector.View._cssUnloadTimeout); 401 }, 402 403 printViewHierarchy: function() 404 { 405 var lines = []; 406 this._collectViewHierarchy("", lines); 407 console.log(lines.join("\n")); 408 }, 409 410 _collectViewHierarchy: function(prefix, lines) 411 { 412 lines.push(prefix + "[" + this.element.className + "]" + (this._children.length ? " {" : "")); 413 414 for (var i = 0; i < this._children.length; ++i) 415 this._children[i]._collectViewHierarchy(prefix + " ", lines); 416 417 if (this._children.length) 418 lines.push(prefix + "}"); 419 }, 420 421 /** 422 * @return {!Element} 423 */ 424 defaultFocusedElement: function() 425 { 426 return this._defaultFocusedElement || this.element; 427 }, 428 429 /** 430 * @param {!Element} element 431 */ 432 setDefaultFocusedElement: function(element) 433 { 434 this._defaultFocusedElement = element; 435 }, 436 437 focus: function() 438 { 439 var element = this.defaultFocusedElement(); 440 if (!element || element.isAncestor(document.activeElement)) 441 return; 442 443 WebInspector.setCurrentFocusElement(element); 444 }, 445 446 /** 447 * @return {boolean} 448 */ 449 hasFocus: function() 450 { 451 var activeElement = document.activeElement; 452 return activeElement && activeElement.isSelfOrDescendant(this.element); 453 }, 454 455 /** 456 * @return {!Size} 457 */ 458 measurePreferredSize: function() 459 { 460 this._loadCSSIfNeeded(); 461 WebInspector.View._originalAppendChild.call(document.body, this.element); 462 this.element.positionAt(0, 0); 463 var result = new Size(this.element.offsetWidth, this.element.offsetHeight); 464 this.element.positionAt(undefined, undefined); 465 WebInspector.View._originalRemoveChild.call(document.body, this.element); 466 this._disableCSSIfNeeded(); 467 return result; 468 }, 469 470 /** 471 * @return {!Constraints} 472 */ 473 calculateConstraints: function() 474 { 475 return new Constraints(new Size(0, 0)); 476 }, 477 478 /** 479 * @return {!Constraints} 480 */ 481 constraints: function() 482 { 483 if (typeof this._constraints !== "undefined") 484 return this._constraints; 485 if (typeof this._cachedConstraints === "undefined") 486 this._cachedConstraints = this.calculateConstraints(); 487 return this._cachedConstraints; 488 }, 489 490 /** 491 * @param {number} width 492 * @param {number} height 493 * @param {number} preferredWidth 494 * @param {number} preferredHeight 495 */ 496 setMinimumAndPreferredSizes: function(width, height, preferredWidth, preferredHeight) 497 { 498 this._constraints = new Constraints(new Size(width, height), new Size(preferredWidth, preferredHeight)); 499 this.invalidateConstraints(); 500 }, 501 502 /** 503 * @param {number} width 504 * @param {number} height 505 */ 506 setMinimumSize: function(width, height) 507 { 508 this._constraints = new Constraints(new Size(width, height)); 509 this.invalidateConstraints(); 510 }, 511 512 /** 513 * @return {boolean} 514 */ 515 _hasNonZeroConstraints: function() 516 { 517 var constraints = this.constraints(); 518 return !!(constraints.minimum.width || constraints.minimum.height || constraints.preferred.width || constraints.preferred.height); 519 }, 520 521 invalidateConstraints: function() 522 { 523 var cached = this._cachedConstraints; 524 delete this._cachedConstraints; 525 var actual = this.constraints(); 526 if (!actual.isEqual(cached) && this._parentView) 527 this._parentView.invalidateConstraints(); 528 else 529 this.doLayout(); 530 }, 531 532 __proto__: WebInspector.Object.prototype 533 } 534 535 WebInspector.View._originalAppendChild = Element.prototype.appendChild; 536 WebInspector.View._originalInsertBefore = Element.prototype.insertBefore; 537 WebInspector.View._originalRemoveChild = Element.prototype.removeChild; 538 WebInspector.View._originalRemoveChildren = Element.prototype.removeChildren; 539 540 WebInspector.View._incrementViewCounter = function(parentElement, childElement) 541 { 542 var count = (childElement.__viewCounter || 0) + (childElement.__view ? 1 : 0); 543 if (!count) 544 return; 545 546 while (parentElement) { 547 parentElement.__viewCounter = (parentElement.__viewCounter || 0) + count; 548 parentElement = parentElement.parentElement; 549 } 550 } 551 552 WebInspector.View._decrementViewCounter = function(parentElement, childElement) 553 { 554 var count = (childElement.__viewCounter || 0) + (childElement.__view ? 1 : 0); 555 if (!count) 556 return; 557 558 while (parentElement) { 559 parentElement.__viewCounter -= count; 560 parentElement = parentElement.parentElement; 561 } 562 } 563 564 WebInspector.View.__assert = function(condition, message) 565 { 566 if (!condition) { 567 console.trace(); 568 throw new Error(message); 569 } 570 } 571 572 /** 573 * @constructor 574 * @extends {WebInspector.View} 575 */ 576 WebInspector.VBox = function() 577 { 578 WebInspector.View.call(this); 579 this.element.classList.add("vbox"); 580 }; 581 582 WebInspector.VBox.prototype = { 583 /** 584 * @return {!Constraints} 585 */ 586 calculateConstraints: function() 587 { 588 var constraints = new Constraints(new Size(0, 0)); 589 590 /** 591 * @this {!WebInspector.View} 592 * @suppressReceiverCheck 593 */ 594 function updateForChild() 595 { 596 var child = this.constraints(); 597 constraints = constraints.widthToMax(child); 598 constraints = constraints.addHeight(child); 599 } 600 601 this._callOnVisibleChildren(updateForChild); 602 return constraints; 603 }, 604 605 __proto__: WebInspector.View.prototype 606 }; 607 608 /** 609 * @constructor 610 * @extends {WebInspector.View} 611 */ 612 WebInspector.HBox = function() 613 { 614 WebInspector.View.call(this); 615 this.element.classList.add("hbox"); 616 }; 617 618 WebInspector.HBox.prototype = { 619 /** 620 * @return {!Constraints} 621 */ 622 calculateConstraints: function() 623 { 624 var constraints = new Constraints(new Size(0, 0)); 625 626 /** 627 * @this {!WebInspector.View} 628 * @suppressReceiverCheck 629 */ 630 function updateForChild() 631 { 632 var child = this.constraints(); 633 constraints = constraints.addWidth(child); 634 constraints = constraints.heightToMax(child); 635 } 636 637 this._callOnVisibleChildren(updateForChild); 638 return constraints; 639 }, 640 641 __proto__: WebInspector.View.prototype 642 }; 643 644 /** 645 * @constructor 646 * @extends {WebInspector.VBox} 647 * @param {function()} resizeCallback 648 */ 649 WebInspector.VBoxWithResizeCallback = function(resizeCallback) 650 { 651 WebInspector.VBox.call(this); 652 this._resizeCallback = resizeCallback; 653 } 654 655 WebInspector.VBoxWithResizeCallback.prototype = { 656 onResize: function() 657 { 658 this._resizeCallback(); 659 }, 660 661 __proto__: WebInspector.VBox.prototype 662 } 663 664 /** 665 * @param {?Node} child 666 * @return {?Node} 667 * @suppress {duplicate} 668 */ 669 Element.prototype.appendChild = function(child) 670 { 671 WebInspector.View.__assert(!child.__view || child.parentElement === this, "Attempt to add view via regular DOM operation."); 672 return WebInspector.View._originalAppendChild.call(this, child); 673 } 674 675 /** 676 * @param {?Node} child 677 * @param {?Node} anchor 678 * @return {!Node} 679 * @suppress {duplicate} 680 */ 681 Element.prototype.insertBefore = function(child, anchor) 682 { 683 WebInspector.View.__assert(!child.__view || child.parentElement === this, "Attempt to add view via regular DOM operation."); 684 return WebInspector.View._originalInsertBefore.call(this, child, anchor); 685 } 686 687 /** 688 * @param {?Node} child 689 * @return {!Node} 690 * @suppress {duplicate} 691 */ 692 Element.prototype.removeChild = function(child) 693 { 694 WebInspector.View.__assert(!child.__viewCounter && !child.__view, "Attempt to remove element containing view via regular DOM operation"); 695 return WebInspector.View._originalRemoveChild.call(this, child); 696 } 697 698 Element.prototype.removeChildren = function() 699 { 700 WebInspector.View.__assert(!this.__viewCounter, "Attempt to remove element containing view via regular DOM operation"); 701 WebInspector.View._originalRemoveChildren.call(this); 702 } 703