Web Inspector: add runtime flag to determine if inspector's source files were flattened.
[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         if (window.flattenImports)
303             cssFile = cssFile.split("/").reverse()[0];
304         this._cssFiles.push(cssFile);
305     },
306
307     _loadCSSIfNeeded: function()
308     {
309         for (var i = 0; i < this._cssFiles.length; ++i) {
310             var cssFile = this._cssFiles[i];
311
312             var viewsWithCSSFile = WebInspector.View._cssFileToVisibleViewCount[cssFile];
313             WebInspector.View._cssFileToVisibleViewCount[cssFile] = (viewsWithCSSFile || 0) + 1;
314             if (!viewsWithCSSFile)
315                 this._doLoadCSS(cssFile);
316         }
317     },
318
319     _doLoadCSS: function(cssFile)
320     {
321         var styleElement = WebInspector.View._cssFileToStyleElement[cssFile];
322         if (styleElement) {
323             styleElement.disabled = false;
324             return;
325         }
326
327         if (window.debugCSS) { /* debugging support */
328             styleElement = document.createElement("link");
329             styleElement.rel = "stylesheet";
330             styleElement.type = "text/css";
331             styleElement.href = cssFile;
332         } else {
333             var xhr = new XMLHttpRequest();
334             xhr.open("GET", cssFile, false);
335             xhr.send(null);
336
337             styleElement = document.createElement("style");
338             styleElement.type = "text/css";
339             styleElement.textContent = xhr.responseText;
340         }
341         document.head.insertBefore(styleElement, document.head.firstChild);
342
343         WebInspector.View._cssFileToStyleElement[cssFile] = styleElement;
344     },
345
346     _disableCSSIfNeeded: function()
347     {
348         for (var i = 0; i < this._cssFiles.length; ++i) {
349             var cssFile = this._cssFiles[i];
350
351             var viewsWithCSSFile = WebInspector.View._cssFileToVisibleViewCount[cssFile];
352             viewsWithCSSFile--;
353             WebInspector.View._cssFileToVisibleViewCount[cssFile] = viewsWithCSSFile;
354
355             if (!viewsWithCSSFile)
356                 this._doUnloadCSS(cssFile);
357         }
358     },
359
360     _doUnloadCSS: function(cssFile)
361     {
362         var styleElement = WebInspector.View._cssFileToStyleElement[cssFile];
363         styleElement.disabled = true;
364     },
365
366     printViewHierarchy: function()
367     {
368         var lines = [];
369         this._collectViewHierarchy("", lines);
370         console.log(lines.join("\n"));
371     },
372
373     _collectViewHierarchy: function(prefix, lines)
374     {
375         lines.push(prefix + "[" + this.element.className + "]" + (this._children.length ? " {" : ""));
376
377         for (var i = 0; i < this._children.length; ++i)
378             this._children[i]._collectViewHierarchy(prefix + "    ", lines);
379
380         if (this._children.length)
381             lines.push(prefix + "}");
382     },
383
384     /**
385      * @return {Element}
386      */
387     defaultFocusedElement: function()
388     {
389         return this._defaultFocusedElement || this.element;
390     },
391
392     /**
393      * @param {Element} element
394      */
395     setDefaultFocusedElement: function(element)
396     {
397         this._defaultFocusedElement = element;
398     },
399
400     focus: function()
401     {
402         var element = this.defaultFocusedElement();
403         if (!element || element.isAncestor(document.activeElement))
404             return;
405
406         WebInspector.setCurrentFocusElement(element);
407     },
408
409     /**
410      * @return {Size}
411      */
412     measurePreferredSize: function()
413     {
414         this._loadCSSIfNeeded();
415         WebInspector.View._originalAppendChild.call(document.body, this.element);
416         this.element.positionAt(0, 0);
417         var result = new Size(this.element.offsetWidth, this.element.offsetHeight);
418         this.element.positionAt(undefined, undefined);
419         WebInspector.View._originalRemoveChild.call(document.body, this.element);
420         this._disableCSSIfNeeded();
421         return result;
422     },
423
424     __proto__: WebInspector.Object.prototype
425 }
426
427 WebInspector.View._originalAppendChild = Element.prototype.appendChild;
428 WebInspector.View._originalInsertBefore = Element.prototype.insertBefore;
429 WebInspector.View._originalRemoveChild = Element.prototype.removeChild;
430 WebInspector.View._originalRemoveChildren = Element.prototype.removeChildren;
431
432 WebInspector.View._incrementViewCounter = function(parentElement, childElement)
433 {
434     var count = (childElement.__viewCounter || 0) + (childElement.__view ? 1 : 0);
435     if (!count)
436         return;
437
438     while (parentElement) {
439         parentElement.__viewCounter = (parentElement.__viewCounter || 0) + count;
440         parentElement = parentElement.parentElement;
441     }
442 }
443
444 WebInspector.View._decrementViewCounter = function(parentElement, childElement)
445 {
446     var count = (childElement.__viewCounter || 0) + (childElement.__view ? 1 : 0);
447     if (!count)
448         return;
449
450     while (parentElement) {
451         parentElement.__viewCounter -= count;
452         parentElement = parentElement.parentElement;
453     }
454 }
455
456 WebInspector.View._assert = function(condition, message)
457 {
458     if (!condition) {
459         console.trace();
460         throw new Error(message);
461     }
462 }
463
464 Element.prototype.appendChild = function(child)
465 {
466     WebInspector.View._assert(!child.__view, "Attempt to add view via regular DOM operation.");
467     return WebInspector.View._originalAppendChild.call(this, child);
468 }
469
470 Element.prototype.insertBefore = function(child, anchor)
471 {
472     WebInspector.View._assert(!child.__view, "Attempt to add view via regular DOM operation.");
473     return WebInspector.View._originalInsertBefore.call(this, child, anchor);
474 }
475
476
477 Element.prototype.removeChild = function(child)
478 {
479     WebInspector.View._assert(!child.__viewCounter && !child.__view, "Attempt to remove element containing view via regular DOM operation");
480     return WebInspector.View._originalRemoveChild.call(this, child);
481 }
482
483 Element.prototype.removeChildren = function()
484 {
485     WebInspector.View._assert(!this.__viewCounter, "Attempt to remove element containing view via regular DOM operation");
486     WebInspector.View._originalRemoveChildren.call(this);
487 }