• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.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
45WebInspector.View._cssFileToVisibleViewCount = {};
46WebInspector.View._cssFileToStyleElement = {};
47WebInspector.View._cssUnloadTimeout = 2000;
48
49WebInspector.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 */
58WebInspector.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
67WebInspector.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
536WebInspector.View._originalAppendChild = Element.prototype.appendChild;
537WebInspector.View._originalInsertBefore = Element.prototype.insertBefore;
538WebInspector.View._originalRemoveChild = Element.prototype.removeChild;
539WebInspector.View._originalRemoveChildren = Element.prototype.removeChildren;
540
541WebInspector.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
553WebInspector.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
565WebInspector.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 */
577WebInspector.VBox = function()
578{
579    WebInspector.View.call(this);
580    this.element.classList.add("vbox");
581};
582
583WebInspector.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 */
613WebInspector.HBox = function()
614{
615    WebInspector.View.call(this);
616    this.element.classList.add("hbox");
617};
618
619WebInspector.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 */
650WebInspector.VBoxWithResizeCallback = function(resizeCallback)
651{
652    WebInspector.VBox.call(this);
653    this._resizeCallback = resizeCallback;
654}
655
656WebInspector.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 */
670Element.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 */
682Element.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 */
693Element.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
699Element.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