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 */
31WebInspector.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
44WebInspector.View._cssFileToVisibleViewCount = {};
45WebInspector.View._cssFileToStyleElement = {};
46WebInspector.View._cssUnloadTimeout = 2000;
47
48WebInspector.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 */
57WebInspector.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
66WebInspector.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
535WebInspector.View._originalAppendChild = Element.prototype.appendChild;
536WebInspector.View._originalInsertBefore = Element.prototype.insertBefore;
537WebInspector.View._originalRemoveChild = Element.prototype.removeChild;
538WebInspector.View._originalRemoveChildren = Element.prototype.removeChildren;
539
540WebInspector.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
552WebInspector.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
564WebInspector.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 */
576WebInspector.VBox = function()
577{
578    WebInspector.View.call(this);
579    this.element.classList.add("vbox");
580};
581
582WebInspector.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 */
612WebInspector.HBox = function()
613{
614    WebInspector.View.call(this);
615    this.element.classList.add("hbox");
616};
617
618WebInspector.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 */
649WebInspector.VBoxWithResizeCallback = function(resizeCallback)
650{
651    WebInspector.VBox.call(this);
652    this._resizeCallback = resizeCallback;
653}
654
655WebInspector.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 */
669Element.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 */
681Element.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 */
692Element.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
698Element.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