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