Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / third_party / WebKit / Source / devtools / front_end / ui / View.js
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         if (window.flattenImports)
354             cssFile = cssFile.split("/").reverse()[0];
355         this._cssFiles.push(cssFile);
356     },
357
358     _loadCSSIfNeeded: function()
359     {
360         for (var i = 0; i < this._cssFiles.length; ++i) {
361             var cssFile = this._cssFiles[i];
362
363             var viewsWithCSSFile = WebInspector.View._cssFileToVisibleViewCount[cssFile];
364             WebInspector.View._cssFileToVisibleViewCount[cssFile] = (viewsWithCSSFile || 0) + 1;
365             if (!viewsWithCSSFile)
366                 this._doLoadCSS(cssFile);
367         }
368     },
369
370     _doLoadCSS: function(cssFile)
371     {
372         var styleElement = WebInspector.View._cssFileToStyleElement[cssFile];
373         if (styleElement) {
374             styleElement.disabled = false;
375             return;
376         }
377         styleElement = WebInspector.View.createStyleElement(cssFile);
378         WebInspector.View._cssFileToStyleElement[cssFile] = styleElement;
379     },
380
381     _disableCSSIfNeeded: function()
382     {
383         var scheduleUnload = !!WebInspector.View._cssUnloadTimer;
384
385         for (var i = 0; i < this._cssFiles.length; ++i) {
386             var cssFile = this._cssFiles[i];
387
388             if (!--WebInspector.View._cssFileToVisibleViewCount[cssFile])
389                 scheduleUnload = true;
390         }
391
392         function doUnloadCSS()
393         {
394             delete WebInspector.View._cssUnloadTimer;
395
396             for (cssFile in WebInspector.View._cssFileToVisibleViewCount) {
397                 if (WebInspector.View._cssFileToVisibleViewCount.hasOwnProperty(cssFile) && !WebInspector.View._cssFileToVisibleViewCount[cssFile])
398                     WebInspector.View._cssFileToStyleElement[cssFile].disabled = true;
399             }
400         }
401
402         if (scheduleUnload && !WebInspector.View._cssUnloadTimer)
403             WebInspector.View._cssUnloadTimer = setTimeout(doUnloadCSS, WebInspector.View._cssUnloadTimeout);
404     },
405
406     printViewHierarchy: function()
407     {
408         var lines = [];
409         this._collectViewHierarchy("", lines);
410         console.log(lines.join("\n"));
411     },
412
413     _collectViewHierarchy: function(prefix, lines)
414     {
415         lines.push(prefix + "[" + this.element.className + "]" + (this._children.length ? " {" : ""));
416
417         for (var i = 0; i < this._children.length; ++i)
418             this._children[i]._collectViewHierarchy(prefix + "    ", lines);
419
420         if (this._children.length)
421             lines.push(prefix + "}");
422     },
423
424     /**
425      * @return {!Element}
426      */
427     defaultFocusedElement: function()
428     {
429         return this._defaultFocusedElement || this.element;
430     },
431
432     /**
433      * @param {!Element} element
434      */
435     setDefaultFocusedElement: function(element)
436     {
437         this._defaultFocusedElement = element;
438     },
439
440     focus: function()
441     {
442         var element = this.defaultFocusedElement();
443         if (!element || element.isAncestor(document.activeElement))
444             return;
445
446         WebInspector.setCurrentFocusElement(element);
447     },
448
449     /**
450      * @return {boolean}
451      */
452     hasFocus: function()
453     {
454         var activeElement = document.activeElement;
455         return activeElement && activeElement.isSelfOrDescendant(this.element);
456     },
457
458     /**
459      * @return {!Size}
460      */
461     measurePreferredSize: function()
462     {
463         this._loadCSSIfNeeded();
464         WebInspector.View._originalAppendChild.call(document.body, this.element);
465         this.element.positionAt(0, 0);
466         var result = new Size(this.element.offsetWidth, this.element.offsetHeight);
467         this.element.positionAt(undefined, undefined);
468         WebInspector.View._originalRemoveChild.call(document.body, this.element);
469         this._disableCSSIfNeeded();
470         return result;
471     },
472
473     /**
474      * @return {!Constraints}
475      */
476     calculateConstraints: function()
477     {
478         return new Constraints(new Size(0, 0));
479     },
480
481     /**
482      * @return {!Constraints}
483      */
484     constraints: function()
485     {
486         if (typeof this._constraints !== "undefined")
487             return this._constraints;
488         if (typeof this._cachedConstraints === "undefined")
489             this._cachedConstraints = this.calculateConstraints();
490         return this._cachedConstraints;
491     },
492
493     /**
494      * @param {number} width
495      * @param {number} height
496      * @param {number} preferredWidth
497      * @param {number} preferredHeight
498      */
499     setMinimumAndPreferredSizes: function(width, height, preferredWidth, preferredHeight)
500     {
501         this._constraints = new Constraints(new Size(width, height), new Size(preferredWidth, preferredHeight));
502         this.invalidateConstraints();
503     },
504
505     /**
506      * @param {number} width
507      * @param {number} height
508      */
509     setMinimumSize: function(width, height)
510     {
511         this._constraints = new Constraints(new Size(width, height));
512         this.invalidateConstraints();
513     },
514
515     /**
516      * @return {boolean}
517      */
518     _hasNonZeroConstraints: function()
519     {
520         var constraints = this.constraints();
521         return !!(constraints.minimum.width || constraints.minimum.height || constraints.preferred.width || constraints.preferred.height);
522     },
523
524     invalidateConstraints: function()
525     {
526         var cached = this._cachedConstraints;
527         delete this._cachedConstraints;
528         var actual = this.constraints();
529         if (!actual.isEqual(cached) && this._parentView)
530             this._parentView.invalidateConstraints();
531         else
532             this.doLayout();
533     },
534
535     __proto__: WebInspector.Object.prototype
536 }
537
538 WebInspector.View._originalAppendChild = Element.prototype.appendChild;
539 WebInspector.View._originalInsertBefore = Element.prototype.insertBefore;
540 WebInspector.View._originalRemoveChild = Element.prototype.removeChild;
541 WebInspector.View._originalRemoveChildren = Element.prototype.removeChildren;
542
543 WebInspector.View._incrementViewCounter = function(parentElement, childElement)
544 {
545     var count = (childElement.__viewCounter || 0) + (childElement.__view ? 1 : 0);
546     if (!count)
547         return;
548
549     while (parentElement) {
550         parentElement.__viewCounter = (parentElement.__viewCounter || 0) + count;
551         parentElement = parentElement.parentElement;
552     }
553 }
554
555 WebInspector.View._decrementViewCounter = function(parentElement, childElement)
556 {
557     var count = (childElement.__viewCounter || 0) + (childElement.__view ? 1 : 0);
558     if (!count)
559         return;
560
561     while (parentElement) {
562         parentElement.__viewCounter -= count;
563         parentElement = parentElement.parentElement;
564     }
565 }
566
567 WebInspector.View._assert = function(condition, message)
568 {
569     if (!condition) {
570         console.trace();
571         throw new Error(message);
572     }
573 }
574
575 /**
576  * @constructor
577  * @extends {WebInspector.View}
578  */
579 WebInspector.VBox = function()
580 {
581     WebInspector.View.call(this);
582     this.element.classList.add("vbox");
583 };
584
585 WebInspector.VBox.prototype = {
586     /**
587      * @return {!Constraints}
588      */
589     calculateConstraints: function()
590     {
591         var constraints = new Constraints(new Size(0, 0));
592
593         /**
594          * @this {!WebInspector.View}
595          * @suppressReceiverCheck
596          */
597         function updateForChild()
598         {
599             var child = this.constraints();
600             constraints = constraints.widthToMax(child);
601             constraints = constraints.addHeight(child);
602         }
603
604         this._callOnVisibleChildren(updateForChild);
605         return constraints;
606     },
607
608     __proto__: WebInspector.View.prototype
609 };
610
611 /**
612  * @constructor
613  * @extends {WebInspector.View}
614  */
615 WebInspector.HBox = function()
616 {
617     WebInspector.View.call(this);
618     this.element.classList.add("hbox");
619 };
620
621 WebInspector.HBox.prototype = {
622     /**
623      * @return {!Constraints}
624      */
625     calculateConstraints: function()
626     {
627         var constraints = new Constraints(new Size(0, 0));
628
629         /**
630          * @this {!WebInspector.View}
631          * @suppressReceiverCheck
632          */
633         function updateForChild()
634         {
635             var child = this.constraints();
636             constraints = constraints.addWidth(child);
637             constraints = constraints.heightToMax(child);
638         }
639
640         this._callOnVisibleChildren(updateForChild);
641         return constraints;
642     },
643
644     __proto__: WebInspector.View.prototype
645 };
646
647 /**
648  * @constructor
649  * @extends {WebInspector.VBox}
650  * @param {function()} resizeCallback
651  */
652 WebInspector.VBoxWithResizeCallback = function(resizeCallback)
653 {
654     WebInspector.VBox.call(this);
655     this._resizeCallback = resizeCallback;
656 }
657
658 WebInspector.VBoxWithResizeCallback.prototype = {
659     onResize: function()
660     {
661         this._resizeCallback();
662     },
663
664     __proto__: WebInspector.VBox.prototype
665 }
666
667 Element.prototype.appendChild = function(child)
668 {
669     WebInspector.View._assert(!child.__view || child.parentElement === this, "Attempt to add view via regular DOM operation.");
670     return WebInspector.View._originalAppendChild.call(this, child);
671 }
672
673 Element.prototype.insertBefore = function(child, anchor)
674 {
675     WebInspector.View._assert(!child.__view || child.parentElement === this, "Attempt to add view via regular DOM operation.");
676     return WebInspector.View._originalInsertBefore.call(this, child, anchor);
677 }
678
679
680 Element.prototype.removeChild = function(child)
681 {
682     WebInspector.View._assert(!child.__viewCounter && !child.__view, "Attempt to remove element containing view via regular DOM operation");
683     return WebInspector.View._originalRemoveChild.call(this, child);
684 }
685
686 Element.prototype.removeChildren = function()
687 {
688     WebInspector.View._assert(!this.__viewCounter, "Attempt to remove element containing view via regular DOM operation");
689     WebInspector.View._originalRemoveChildren.call(this);
690 }