[Qt] Unreviewed gardening.
[WebKit-https.git] / Source / WebCore / inspector / front-end / 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.__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
47 WebInspector.View.prototype = {
48     /**
49      * @return {?Element}
50      */
51     statusBarText: function()
52     {
53         return null;
54     },
55
56     markAsRoot: function()
57     {
58         WebInspector.View._assert(!this.element.parentElement, "Attempt to mark as root attached node");
59         this._isRoot = true;
60     },
61
62     isShowing: function()
63     {
64         return this._isShowing;
65     },
66
67     setHideOnDetach: function()
68     {
69         this._hideOnDetach = true;
70     },
71
72     /**
73      * @return {boolean} 
74      */
75     _inNotification: function()
76     {
77         return !!this._notificationDepth || (this._parentView && this._parentView._inNotification());
78     },
79
80     _parentIsShowing: function()
81     {
82         if (this._isRoot)
83             return true;
84         return this._parentView && this._parentView.isShowing();
85     },
86
87     /**
88      * @param {function(this:WebInspector.View)} method
89      */
90     _callOnVisibleChildren: function(method)
91     {
92         var copy = this._children.slice();
93         for (var i = 0; i < copy.length; ++i) {
94             if (copy[i]._parentView === this && copy[i]._visible)
95                 method.call(copy[i]);
96         }
97     },
98
99     _processWillShow: function()
100     {
101         this._loadCSSIfNeeded();
102         this._callOnVisibleChildren(this._processWillShow);
103     },
104
105     _processWasShown: function()
106     {
107         if (this._inNotification())
108             return;
109         this._isShowing = true;
110         this.restoreScrollPositions();
111         this._notify(this.wasShown);
112         this._notify(this.onResize);
113         this._callOnVisibleChildren(this._processWasShown);
114     },
115
116     _processWillHide: function()
117     {
118         if (this._inNotification())
119             return;
120         this.storeScrollPositions();
121
122         this._callOnVisibleChildren(this._processWillHide);
123         this._notify(this.willHide);
124         this._isShowing = false;
125     },
126
127     _processWasHidden: function()
128     {
129         this._disableCSSIfNeeded();
130         this._callOnVisibleChildren(this._processWasHidden);
131     },
132
133     _processOnResize: function()
134     {
135         if (this._inNotification())
136             return;
137         if (!this.isShowing())
138             return;
139         this._notify(this.onResize);
140         this._callOnVisibleChildren(this._processOnResize);
141     },
142
143     /**
144      * @param {function(this:WebInspector.View)} notification
145      */
146     _notify: function(notification)
147     {
148         ++this._notificationDepth;
149         try {
150             notification.call(this);
151         } finally {
152             --this._notificationDepth;
153         }
154     },
155
156     wasShown: function()
157     {
158     },
159
160     willHide: function()
161     {
162     },
163
164     onResize: function()
165     {
166     },
167
168     /**
169      * @param {Element} parentElement
170      * @param {Element=} insertBefore
171      */
172     show: function(parentElement, insertBefore)
173     {
174         WebInspector.View._assert(parentElement, "Attempt to attach view with no parent element");
175
176         // Update view hierarchy
177         if (this.element.parentElement !== parentElement) {
178             if (this.element.parentElement)
179                 this.detach();
180
181             var currentParent = parentElement;
182             while (currentParent && !currentParent.__view)
183                 currentParent = currentParent.parentElement;
184
185             if (currentParent) {
186                 this._parentView = currentParent.__view;
187                 this._parentView._children.push(this);
188                 this._isRoot = false;
189             } else
190                 WebInspector.View._assert(this._isRoot, "Attempt to attach view to orphan node");
191         } else if (this._visible)
192             return;
193
194         this._visible = true;
195
196         if (this._parentIsShowing())
197             this._processWillShow();
198
199         this.element.addStyleClass("visible");
200
201         // Reparent
202         if (this.element.parentElement !== parentElement) {
203             WebInspector.View._incrementViewCounter(parentElement, this.element);
204             if (insertBefore)
205                 WebInspector.View._originalInsertBefore.call(parentElement, this.element, insertBefore);
206             else
207                 WebInspector.View._originalAppendChild.call(parentElement, this.element);
208         }
209
210         if (this._parentIsShowing())
211             this._processWasShown();
212     },
213
214     /**
215      * @param {boolean=} overrideHideOnDetach
216      */
217     detach: function(overrideHideOnDetach)
218     {
219         var parentElement = this.element.parentElement;
220         if (!parentElement)
221             return;
222
223         if (this._parentIsShowing())
224             this._processWillHide();
225
226         if (this._hideOnDetach && !overrideHideOnDetach) {
227             this.element.removeStyleClass("visible");
228             this._visible = false;
229             if (this._parentIsShowing())
230                 this._processWasHidden();
231             return;
232         }
233
234         // Force legal removal
235         WebInspector.View._decrementViewCounter(parentElement, this.element);
236         WebInspector.View._originalRemoveChild.call(parentElement, this.element);
237
238         this._visible = false;
239         if (this._parentIsShowing())
240             this._processWasHidden();
241
242         // Update view hierarchy
243         if (this._parentView) {
244             var childIndex = this._parentView._children.indexOf(this);
245             WebInspector.View._assert(childIndex >= 0, "Attempt to remove non-child view");
246             this._parentView._children.splice(childIndex, 1);
247             this._parentView = null;
248         } else
249             WebInspector.View._assert(this._isRoot, "Removing non-root view from DOM");
250     },
251
252     detachChildViews: function()
253     {
254         var children = this._children.slice();
255         for (var i = 0; i < children.length; ++i)
256             children[i].detach();
257     },
258
259     elementsToRestoreScrollPositionsFor: function()
260     {
261         return [this.element];
262     },
263
264     storeScrollPositions: function()
265     {
266         var elements = this.elementsToRestoreScrollPositionsFor();
267         for (var i = 0; i < elements.length; ++i) {
268             var container = elements[i];
269             container._scrollTop = container.scrollTop;
270             container._scrollLeft = container.scrollLeft;
271         }
272     },
273
274     restoreScrollPositions: function()
275     {
276         var elements = this.elementsToRestoreScrollPositionsFor();
277         for (var i = 0; i < elements.length; ++i) {
278             var container = elements[i];
279             if (container._scrollTop)
280                 container.scrollTop = container._scrollTop;
281             if (container._scrollLeft)
282                 container.scrollLeft = container._scrollLeft;
283         }
284     },
285
286     canHighlightLine: function()
287     {
288         return false;
289     },
290
291     highlightLine: function(line)
292     {
293     },
294
295     doResize: function()
296     {
297         this._processOnResize();
298     },
299
300     registerRequiredCSS: function(cssFile)
301     {
302         this._cssFiles.push(cssFile);
303     },
304
305     _loadCSSIfNeeded: function()
306     {
307         for (var i = 0; i < this._cssFiles.length; ++i) {
308             var cssFile = this._cssFiles[i];
309
310             var viewsWithCSSFile = WebInspector.View._cssFileToVisibleViewCount[cssFile];
311             WebInspector.View._cssFileToVisibleViewCount[cssFile] = (viewsWithCSSFile || 0) + 1;
312             if (!viewsWithCSSFile)
313                 this._doLoadCSS(cssFile);
314         }
315     },
316
317     _doLoadCSS: function(cssFile)
318     {
319         var styleElement = WebInspector.View._cssFileToStyleElement[cssFile];
320         if (styleElement) {
321             styleElement.disabled = false;
322             return;
323         }
324
325         if (window.debugCSS) { /* debugging support */
326             styleElement = document.createElement("link");
327             styleElement.rel = "stylesheet";
328             styleElement.type = "text/css";
329             styleElement.href = cssFile;
330         } else {
331             var xhr = new XMLHttpRequest();
332             xhr.open("GET", cssFile, false);
333             xhr.send(null);
334
335             styleElement = document.createElement("style");
336             styleElement.type = "text/css";
337             styleElement.textContent = xhr.responseText;
338         }
339         document.head.insertBefore(styleElement, document.head.firstChild);
340
341         WebInspector.View._cssFileToStyleElement[cssFile] = styleElement;
342     },
343
344     _disableCSSIfNeeded: function()
345     {
346         for (var i = 0; i < this._cssFiles.length; ++i) {
347             var cssFile = this._cssFiles[i];
348
349             var viewsWithCSSFile = WebInspector.View._cssFileToVisibleViewCount[cssFile];
350             viewsWithCSSFile--;
351             WebInspector.View._cssFileToVisibleViewCount[cssFile] = viewsWithCSSFile;
352
353             if (!viewsWithCSSFile)
354                 this._doUnloadCSS(cssFile);
355         }
356     },
357
358     _doUnloadCSS: function(cssFile)
359     {
360         var styleElement = WebInspector.View._cssFileToStyleElement[cssFile];
361         styleElement.disabled = true;
362     },
363
364     printViewHierarchy: function()
365     {
366         var lines = [];
367         this._collectViewHierarchy("", lines);
368         console.log(lines.join("\n"));
369     },
370
371     _collectViewHierarchy: function(prefix, lines)
372     {
373         lines.push(prefix + "[" + this.element.className + "]" + (this._children.length ? " {" : ""));
374
375         for (var i = 0; i < this._children.length; ++i)
376             this._children[i]._collectViewHierarchy(prefix + "    ", lines);
377
378         if (this._children.length)
379             lines.push(prefix + "}");
380     },
381
382     /**
383      * @return {Element}
384      */
385     defaultFocusedElement: function()
386     {
387         return this._defaultFocusedElement || this.element;
388     },
389
390     /**
391      * @param {Element} element
392      */
393     setDefaultFocusedElement: function(element)
394     {
395         this._defaultFocusedElement = element;
396     },
397
398     focus: function()
399     {
400         var element = this.defaultFocusedElement();
401         if (!element || element.isAncestor(document.activeElement))
402             return;
403
404         WebInspector.setCurrentFocusElement(element);
405     },
406
407     /**
408      * @return {Size}
409      */
410     measurePreferredSize: function()
411     {
412         this._loadCSSIfNeeded();
413         WebInspector.View._originalAppendChild.call(document.body, this.element);
414         this.element.positionAt(0, 0);
415         var result = new Size(this.element.offsetWidth, this.element.offsetHeight);
416         this.element.positionAt(undefined, undefined);
417         WebInspector.View._originalRemoveChild.call(document.body, this.element);
418         this._disableCSSIfNeeded();
419         return result;
420     },
421
422     __proto__: WebInspector.Object.prototype
423 }
424
425 WebInspector.View._originalAppendChild = Element.prototype.appendChild;
426 WebInspector.View._originalInsertBefore = Element.prototype.insertBefore;
427 WebInspector.View._originalRemoveChild = Element.prototype.removeChild;
428 WebInspector.View._originalRemoveChildren = Element.prototype.removeChildren;
429
430 WebInspector.View._incrementViewCounter = function(parentElement, childElement)
431 {
432     var count = (childElement.__viewCounter || 0) + (childElement.__view ? 1 : 0);
433     if (!count)
434         return;
435
436     while (parentElement) {
437         parentElement.__viewCounter = (parentElement.__viewCounter || 0) + count;
438         parentElement = parentElement.parentElement;
439     }
440 }
441
442 WebInspector.View._decrementViewCounter = function(parentElement, childElement)
443 {
444     var count = (childElement.__viewCounter || 0) + (childElement.__view ? 1 : 0);
445     if (!count)
446         return;
447
448     while (parentElement) {
449         parentElement.__viewCounter -= count;
450         parentElement = parentElement.parentElement;
451     }
452 }
453
454 WebInspector.View._assert = function(condition, message)
455 {
456     if (!condition) {
457         console.trace();
458         throw new Error(message);
459     }
460 }
461
462 Element.prototype.appendChild = function(child)
463 {
464     WebInspector.View._assert(!child.__view, "Attempt to add view via regular DOM operation.");
465     return WebInspector.View._originalAppendChild.call(this, child);
466 }
467
468 Element.prototype.insertBefore = function(child, anchor)
469 {
470     WebInspector.View._assert(!child.__view, "Attempt to add view via regular DOM operation.");
471     return WebInspector.View._originalInsertBefore.call(this, child, anchor);
472 }
473
474
475 Element.prototype.removeChild = function(child)
476 {
477     WebInspector.View._assert(!child.__viewCounter && !child.__view, "Attempt to remove element containing view via regular DOM operation");
478     return WebInspector.View._originalRemoveChild.call(this, child);
479 }
480
481 Element.prototype.removeChildren = function()
482 {
483     WebInspector.View._assert(!this.__viewCounter, "Attempt to remove element containing view via regular DOM operation");
484     WebInspector.View._originalRemoveChildren.call(this);
485 }