2 * Copyright (C) 2008 Apple Inc. All Rights Reserved.
3 * Copyright (C) 2011 Google Inc. All Rights Reserved.
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
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.
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.
29 * @extends {WebInspector.Object}
31 WebInspector.View = function()
33 this.element = document.createElement("div");
34 this.element.className = "view";
35 this.element.__view = this;
38 this._isShowing = false;
40 this._hideOnDetach = false;
42 this._notificationDepth = 0;
45 WebInspector.View._cssFileToVisibleViewCount = {};
46 WebInspector.View._cssFileToStyleElement = {};
47 WebInspector.View._cssUnloadTimeout = 2000;
49 WebInspector.View._buildSourceURL = function(cssFile)
51 return "\n/*# sourceURL=" + WebInspector.ParsedURL.completeURL(window.location.href, cssFile) + " */";
55 * @param {string} cssFile
58 WebInspector.View.createStyleElement = function(cssFile)
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);
67 WebInspector.View.prototype = {
68 markAsRoot: function()
70 WebInspector.View._assert(!this.element.parentElement, "Attempt to mark as root attached node");
75 * @return {?WebInspector.View}
77 parentView: function()
79 return this._parentView;
83 * @return {!Array.<!WebInspector.View>}
87 return this._children;
95 return this._isShowing;
98 setHideOnDetach: function()
100 this._hideOnDetach = true;
106 _inNotification: function()
108 return !!this._notificationDepth || (this._parentView && this._parentView._inNotification());
111 _parentIsShowing: function()
115 return this._parentView && this._parentView.isShowing();
119 * @param {function(this:WebInspector.View)} method
121 _callOnVisibleChildren: function(method)
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]);
130 _processWillShow: function()
132 this._loadCSSIfNeeded();
133 this._callOnVisibleChildren(this._processWillShow);
134 this._isShowing = true;
137 _processWasShown: function()
139 if (this._inNotification())
141 this.restoreScrollPositions();
142 this._notify(this.wasShown);
143 this._callOnVisibleChildren(this._processWasShown);
146 _processWillHide: function()
148 if (this._inNotification())
150 this.storeScrollPositions();
152 this._callOnVisibleChildren(this._processWillHide);
153 this._notify(this.willHide);
154 this._isShowing = false;
157 _processWasHidden: function()
159 this._disableCSSIfNeeded();
160 this._callOnVisibleChildren(this._processWasHidden);
163 _processOnResize: function()
165 if (this._inNotification())
167 if (!this.isShowing())
169 this._notify(this.onResize);
170 this._callOnVisibleChildren(this._processOnResize);
174 * @param {function(this:WebInspector.View)} notification
176 _notify: function(notification)
178 ++this._notificationDepth;
180 notification.call(this);
182 --this._notificationDepth;
203 * @param {?Element} parentElement
204 * @param {!Element=} insertBefore
206 show: function(parentElement, insertBefore)
208 WebInspector.View._assert(parentElement, "Attempt to attach view with no parent element");
210 // Update view hierarchy
211 if (this.element.parentElement !== parentElement) {
212 if (this.element.parentElement)
215 var currentParent = parentElement;
216 while (currentParent && !currentParent.__view)
217 currentParent = currentParent.parentElement;
220 this._parentView = currentParent.__view;
221 this._parentView._children.push(this);
222 this._isRoot = false;
224 WebInspector.View._assert(this._isRoot, "Attempt to attach view to orphan node");
225 } else if (this._visible) {
229 this._visible = true;
231 if (this._parentIsShowing())
232 this._processWillShow();
234 this.element.classList.add("visible");
237 if (this.element.parentElement !== parentElement) {
238 WebInspector.View._incrementViewCounter(parentElement, this.element);
240 WebInspector.View._originalInsertBefore.call(parentElement, this.element, insertBefore);
242 WebInspector.View._originalAppendChild.call(parentElement, this.element);
245 if (this._parentIsShowing())
246 this._processWasShown();
248 if (this._parentView && this._hasNonZeroConstraints())
249 this._parentView.invalidateConstraints();
251 this._processOnResize();
255 * @param {boolean=} overrideHideOnDetach
257 detach: function(overrideHideOnDetach)
259 var parentElement = this.element.parentElement;
263 if (this._parentIsShowing())
264 this._processWillHide();
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();
276 // Force legal removal
277 WebInspector.View._decrementViewCounter(parentElement, this.element);
278 WebInspector.View._originalRemoveChild.call(parentElement, this.element);
280 this._visible = false;
281 if (this._parentIsShowing())
282 this._processWasHidden();
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();
294 WebInspector.View._assert(this._isRoot, "Removing non-root view from DOM");
297 detachChildViews: function()
299 var children = this._children.slice();
300 for (var i = 0; i < children.length; ++i)
301 children[i].detach();
305 * @return {!Array.<!Element>}
307 elementsToRestoreScrollPositionsFor: function()
309 return [this.element];
312 storeScrollPositions: function()
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;
322 restoreScrollPositions: function()
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;
336 if (!this.isShowing())
338 // No matter what notification we are in, dispatching onResize is not needed.
339 if (!this._inNotification())
340 this._callOnVisibleChildren(this._processOnResize);
345 if (!this.isShowing())
347 this._notify(this.onLayout);
351 registerRequiredCSS: function(cssFile)
353 if (window.flattenImports)
354 cssFile = cssFile.split("/").reverse()[0];
355 this._cssFiles.push(cssFile);
358 _loadCSSIfNeeded: function()
360 for (var i = 0; i < this._cssFiles.length; ++i) {
361 var cssFile = this._cssFiles[i];
363 var viewsWithCSSFile = WebInspector.View._cssFileToVisibleViewCount[cssFile];
364 WebInspector.View._cssFileToVisibleViewCount[cssFile] = (viewsWithCSSFile || 0) + 1;
365 if (!viewsWithCSSFile)
366 this._doLoadCSS(cssFile);
370 _doLoadCSS: function(cssFile)
372 var styleElement = WebInspector.View._cssFileToStyleElement[cssFile];
374 styleElement.disabled = false;
377 styleElement = WebInspector.View.createStyleElement(cssFile);
378 WebInspector.View._cssFileToStyleElement[cssFile] = styleElement;
381 _disableCSSIfNeeded: function()
383 var scheduleUnload = !!WebInspector.View._cssUnloadTimer;
385 for (var i = 0; i < this._cssFiles.length; ++i) {
386 var cssFile = this._cssFiles[i];
388 if (!--WebInspector.View._cssFileToVisibleViewCount[cssFile])
389 scheduleUnload = true;
392 function doUnloadCSS()
394 delete WebInspector.View._cssUnloadTimer;
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;
402 if (scheduleUnload && !WebInspector.View._cssUnloadTimer)
403 WebInspector.View._cssUnloadTimer = setTimeout(doUnloadCSS, WebInspector.View._cssUnloadTimeout);
406 printViewHierarchy: function()
409 this._collectViewHierarchy("", lines);
410 console.log(lines.join("\n"));
413 _collectViewHierarchy: function(prefix, lines)
415 lines.push(prefix + "[" + this.element.className + "]" + (this._children.length ? " {" : ""));
417 for (var i = 0; i < this._children.length; ++i)
418 this._children[i]._collectViewHierarchy(prefix + " ", lines);
420 if (this._children.length)
421 lines.push(prefix + "}");
427 defaultFocusedElement: function()
429 return this._defaultFocusedElement || this.element;
433 * @param {!Element} element
435 setDefaultFocusedElement: function(element)
437 this._defaultFocusedElement = element;
442 var element = this.defaultFocusedElement();
443 if (!element || element.isAncestor(document.activeElement))
446 WebInspector.setCurrentFocusElement(element);
454 var activeElement = document.activeElement;
455 return activeElement && activeElement.isSelfOrDescendant(this.element);
461 measurePreferredSize: function()
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();
474 * @return {!Constraints}
476 calculateConstraints: function()
478 return new Constraints(new Size(0, 0));
482 * @return {!Constraints}
484 constraints: function()
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;
494 * @param {number} width
495 * @param {number} height
496 * @param {number} preferredWidth
497 * @param {number} preferredHeight
499 setMinimumAndPreferredSizes: function(width, height, preferredWidth, preferredHeight)
501 this._constraints = new Constraints(new Size(width, height), new Size(preferredWidth, preferredHeight));
502 this.invalidateConstraints();
506 * @param {number} width
507 * @param {number} height
509 setMinimumSize: function(width, height)
511 this._constraints = new Constraints(new Size(width, height));
512 this.invalidateConstraints();
518 _hasNonZeroConstraints: function()
520 var constraints = this.constraints();
521 return !!(constraints.minimum.width || constraints.minimum.height || constraints.preferred.width || constraints.preferred.height);
524 invalidateConstraints: function()
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();
535 __proto__: WebInspector.Object.prototype
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;
543 WebInspector.View._incrementViewCounter = function(parentElement, childElement)
545 var count = (childElement.__viewCounter || 0) + (childElement.__view ? 1 : 0);
549 while (parentElement) {
550 parentElement.__viewCounter = (parentElement.__viewCounter || 0) + count;
551 parentElement = parentElement.parentElement;
555 WebInspector.View._decrementViewCounter = function(parentElement, childElement)
557 var count = (childElement.__viewCounter || 0) + (childElement.__view ? 1 : 0);
561 while (parentElement) {
562 parentElement.__viewCounter -= count;
563 parentElement = parentElement.parentElement;
567 WebInspector.View._assert = function(condition, message)
571 throw new Error(message);
577 * @extends {WebInspector.View}
579 WebInspector.VBox = function()
581 WebInspector.View.call(this);
582 this.element.classList.add("vbox");
585 WebInspector.VBox.prototype = {
587 * @return {!Constraints}
589 calculateConstraints: function()
591 var constraints = new Constraints(new Size(0, 0));
594 * @this {!WebInspector.View}
595 * @suppressReceiverCheck
597 function updateForChild()
599 var child = this.constraints();
600 constraints = constraints.widthToMax(child);
601 constraints = constraints.addHeight(child);
604 this._callOnVisibleChildren(updateForChild);
608 __proto__: WebInspector.View.prototype
613 * @extends {WebInspector.View}
615 WebInspector.HBox = function()
617 WebInspector.View.call(this);
618 this.element.classList.add("hbox");
621 WebInspector.HBox.prototype = {
623 * @return {!Constraints}
625 calculateConstraints: function()
627 var constraints = new Constraints(new Size(0, 0));
630 * @this {!WebInspector.View}
631 * @suppressReceiverCheck
633 function updateForChild()
635 var child = this.constraints();
636 constraints = constraints.addWidth(child);
637 constraints = constraints.heightToMax(child);
640 this._callOnVisibleChildren(updateForChild);
644 __proto__: WebInspector.View.prototype
649 * @extends {WebInspector.VBox}
650 * @param {function()} resizeCallback
652 WebInspector.VBoxWithResizeCallback = function(resizeCallback)
654 WebInspector.VBox.call(this);
655 this._resizeCallback = resizeCallback;
658 WebInspector.VBoxWithResizeCallback.prototype = {
661 this._resizeCallback();
664 __proto__: WebInspector.VBox.prototype
667 Element.prototype.appendChild = function(child)
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);
673 Element.prototype.insertBefore = function(child, anchor)
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);
680 Element.prototype.removeChild = function(child)
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);
686 Element.prototype.removeChildren = function()
688 WebInspector.View._assert(!this.__viewCounter, "Attempt to remove element containing view via regular DOM operation");
689 WebInspector.View._originalRemoveChildren.call(this);