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.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