1/*
2 * Copyright (C) 2012 Google Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
7 *
8 * 1. Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 *
11 * 2. Redistributions in binary form must reproduce the above
12 * copyright notice, this list of conditions and the following disclaimer
13 * in the documentation and/or other materials provided with the
14 * distribution.
15 *
16 * THIS SOFTWARE IS PROVIDED BY GOOGLE INC. AND ITS CONTRIBUTORS
17 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GOOGLE INC.
20 * OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 */
28
29/**
30 * @constructor
31 * @extends {WebInspector.View}
32 * @param {boolean} isVertical
33 * @param {string=} sidebarSizeSettingName
34 * @param {number=} defaultSidebarWidth
35 * @param {number=} defaultSidebarHeight
36 */
37WebInspector.SplitView = function(isVertical, sidebarSizeSettingName, defaultSidebarWidth, defaultSidebarHeight)
38{
39    WebInspector.View.call(this);
40
41    this.registerRequiredCSS("splitView.css");
42
43    this.element.className = "split-view";
44
45    this._firstElement = this.element.createChild("div", "split-view-contents scroll-target split-view-contents-first");
46    this._secondElement = this.element.createChild("div", "split-view-contents scroll-target split-view-contents-second");
47
48    this._resizerElement = this.element.createChild("div", "split-view-resizer");
49    this.installResizer(this._resizerElement);
50    this._resizable = true;
51
52    this._savedSidebarWidth = defaultSidebarWidth || 200;
53    this._savedSidebarHeight = defaultSidebarHeight || this._savedSidebarWidth;
54
55    if (0 < this._savedSidebarWidth && this._savedSidebarWidth < 1 &&
56        0 < this._savedSidebarHeight && this._savedSidebarHeight < 1)
57        this._useFraction = true;
58
59    this._sidebarSizeSettingName = sidebarSizeSettingName;
60
61    this.setSecondIsSidebar(true);
62
63    this._innerSetVertical(isVertical);
64}
65
66WebInspector.SplitView.prototype = {
67    /**
68     * @return {boolean}
69     */
70    isVertical: function()
71    {
72        return this._isVertical;
73    },
74
75    /**
76     * @param {boolean} isVertical
77     */
78    setVertical: function(isVertical)
79    {
80        if (this._isVertical === isVertical)
81            return;
82
83        this._innerSetVertical(isVertical);
84
85        if (this.isShowing())
86            this._updateLayout();
87    },
88
89    /**
90     * @param {boolean} isVertical
91     */
92    _innerSetVertical: function(isVertical)
93    {
94        this.element.removeStyleClass(this._isVertical ? "split-view-vertical" : "split-view-horizontal");
95        this._isVertical = isVertical;
96        this.element.addStyleClass(this._isVertical ? "split-view-vertical" : "split-view-horizontal");
97        delete this._resizerElementSize;
98        this._sidebarSize = -1;
99    },
100
101    _updateLayout: function()
102    {
103        delete this._totalSize; // Lazy update.
104        this._innerSetSidebarSize(this._lastSidebarSize());
105    },
106
107    /**
108     * @return {Element}
109     */
110    firstElement: function()
111    {
112        return this._firstElement;
113    },
114
115    /**
116     * @return {Element}
117     */
118    secondElement: function()
119    {
120        return this._secondElement;
121    },
122
123    /**
124     * @return {Element}
125     */
126    get mainElement()
127    {
128        return this.isSidebarSecond() ? this.firstElement() : this.secondElement();
129    },
130
131    /**
132     * @return {Element}
133     */
134    get sidebarElement()
135    {
136        return this.isSidebarSecond() ? this.secondElement() : this.firstElement();
137    },
138
139    /**
140     * @return {boolean}
141     */
142    isSidebarSecond: function()
143    {
144        return this._secondIsSidebar;
145    },
146
147    /**
148     * @param {boolean} secondIsSidebar
149     */
150    setSecondIsSidebar: function(secondIsSidebar)
151    {
152        this.sidebarElement.removeStyleClass("split-view-sidebar");
153        this._secondIsSidebar = secondIsSidebar;
154        this.sidebarElement.addStyleClass("split-view-sidebar");
155    },
156
157    /**
158     * @return {Element}
159     */
160    resizerElement: function()
161    {
162        return this._resizerElement;
163    },
164
165    showOnlyFirst: function()
166    {
167        this._showOnly(this._firstElement, this._secondElement);
168    },
169
170    showOnlySecond: function()
171    {
172        this._showOnly(this._secondElement, this._firstElement);
173    },
174
175    /**
176     * @param {Element} sideA
177     * @param {Element} sideB
178     */
179    _showOnly: function(sideA, sideB)
180    {
181        sideA.removeStyleClass("hidden");
182        sideA.addStyleClass("maximized");
183        sideB.addStyleClass("hidden");
184        sideB.removeStyleClass("maximized");
185        this._removeAllLayoutProperties();
186
187        this._isShowingOne = true;
188        this._sidebarSize = -1;
189        this.setResizable(false);
190        this.doResize();
191    },
192
193    _removeAllLayoutProperties: function()
194    {
195        this._firstElement.style.removeProperty("right");
196        this._firstElement.style.removeProperty("bottom");
197        this._firstElement.style.removeProperty("width");
198        this._firstElement.style.removeProperty("height");
199
200        this._secondElement.style.removeProperty("left");
201        this._secondElement.style.removeProperty("top");
202        this._secondElement.style.removeProperty("width");
203        this._secondElement.style.removeProperty("height");
204
205        this._resizerElement.style.removeProperty("left");
206        this._resizerElement.style.removeProperty("right");
207        this._resizerElement.style.removeProperty("top");
208        this._resizerElement.style.removeProperty("bottom");
209
210        this._resizerElement.style.removeProperty("margin-left");
211        this._resizerElement.style.removeProperty("margin-right");
212        this._resizerElement.style.removeProperty("margin-top");
213        this._resizerElement.style.removeProperty("margin-bottom");
214    },
215
216    showBoth: function()
217    {
218        this._firstElement.removeStyleClass("hidden");
219        this._firstElement.removeStyleClass("maximized");
220        this._secondElement.removeStyleClass("hidden");
221        this._secondElement.removeStyleClass("maximized");
222
223        this._isShowingOne = false;
224        this._sidebarSize = -1;
225        this.setResizable(true);
226        this.doResize();
227    },
228
229    /**
230     * @param {boolean} resizable
231     */
232    setResizable: function(resizable)
233    {
234        this._resizable = resizable;
235        this._resizerElement.enableStyleClass("hidden", !resizable);
236    },
237
238    /**
239     * @param {number} size
240     */
241    setSidebarSize: function(size)
242    {
243        this._innerSetSidebarSize(size);
244        this._saveSidebarSize();
245    },
246
247    /**
248     * @return {number}
249     */
250    sidebarSize: function()
251    {
252        return Math.max(0, this._sidebarSize);
253    },
254
255    /**
256     * @return {number}
257     */
258    totalSize: function()
259    {
260        if (!this._totalSize)
261            this._totalSize = this._isVertical ? this.element.offsetWidth : this.element.offsetHeight;
262        return this._totalSize;
263    },
264
265    /**
266     * @param {number} size
267     */
268    _innerSetSidebarSize: function(size)
269    {
270        if (this._isShowingOne) {
271            this._sidebarSize = size;
272            return;
273        }
274
275        size = this._applyConstraints(size);
276        if (this._sidebarSize === size)
277            return;
278
279        if (size < 0) {
280            // Never apply bad values, fix it upon onResize instead.
281            return;
282        }
283
284        this._removeAllLayoutProperties();
285
286        var sizeValue;
287        if (this._useFraction)
288            sizeValue = (size / this.totalSize()) * 100 + "%";
289        else
290            sizeValue = size + "px";
291
292        if (!this._resizerElementSize)
293            this._resizerElementSize = this._isVertical ? this._resizerElement.offsetWidth : this._resizerElement.offsetHeight;
294
295        if (this._isVertical) {
296            if (this._secondIsSidebar) {
297                this._firstElement.style.right = sizeValue;
298                this._secondElement.style.width = sizeValue;
299                this._resizerElement.style.right = sizeValue;
300                this._resizerElement.style.marginRight = -this._resizerElementSize / 2 + "px";
301            } else {
302                this._firstElement.style.width = sizeValue;
303                this._secondElement.style.left = sizeValue;
304                this._resizerElement.style.left = sizeValue;
305                this._resizerElement.style.marginLeft = -this._resizerElementSize / 2 + "px";
306            }
307        } else {
308            if (this._secondIsSidebar) {
309                this._firstElement.style.bottom = sizeValue;
310                this._secondElement.style.height = sizeValue;
311                this._resizerElement.style.bottom = sizeValue;
312                this._resizerElement.style.marginBottom = -this._resizerElementSize / 2 + "px";
313            } else {
314                this._firstElement.style.height = sizeValue;
315                this._secondElement.style.top = sizeValue;
316                this._resizerElement.style.top = sizeValue;
317                this._resizerElement.style.marginTop = -this._resizerElementSize / 2 + "px";
318            }
319        }
320
321        this._sidebarSize = size;
322
323        // No need to recalculate this._sidebarSize and this._totalSize again.
324        this._muteOnResize = true;
325        this.doResize();
326        delete this._muteOnResize;
327    },
328
329    /**
330     * @param {number=} minWidth
331     * @param {number=} minHeight
332     */
333    setSidebarElementConstraints: function(minWidth, minHeight)
334    {
335        if (typeof minWidth === "number")
336            this._minimumSidebarWidth = minWidth;
337        if (typeof minHeight === "number")
338            this._minimumSidebarHeight = minHeight;
339    },
340
341    /**
342     * @param {number=} minWidth
343     * @param {number=} minHeight
344     */
345    setMainElementConstraints: function(minWidth, minHeight)
346    {
347        if (typeof minWidth === "number")
348            this._minimumMainWidth = minWidth;
349        if (typeof minHeight === "number")
350            this._minimumMainHeight = minHeight;
351    },
352
353    /**
354     * @param {number} sidebarSize
355     * @return {number}
356     */
357    _applyConstraints: function(sidebarSize)
358    {
359        const minPadding = 20;
360        var totalSize = this.totalSize();
361        var from = (this.isVertical() ? this._minimumSidebarWidth : this._minimumSidebarHeight) || 0;
362        var fromInPercents = false;
363        if (from && from < 1) {
364            fromInPercents = true;
365            from = Math.round(totalSize * from);
366        }
367        from = Math.max(from, minPadding);
368
369        var minMainSize = (this.isVertical() ? this._minimumMainWidth : this._minimumMainHeight) || 0;
370        var toInPercents = false;
371        if (minMainSize && minMainSize < 1) {
372            toInPercents = true;
373            minMainSize = Math.round(totalSize * minMainSize);
374        }
375        minMainSize = Math.max(minMainSize, minPadding);
376
377        var to = totalSize - minMainSize;
378        if (from <= to)
379            return Number.constrain(sidebarSize, from, to);
380
381        // Respect fixed constraints over percents. This will, for example, shrink
382        // the sidebar to its minimum size when possible.
383        if (!fromInPercents && !toInPercents)
384            return -1;
385        if (toInPercents && sidebarSize >= from && from < totalSize)
386            return from;
387        if (fromInPercents && sidebarSize <= to && to < totalSize)
388            return to;
389
390        return -1;
391    },
392
393    wasShown: function()
394    {
395        this._updateLayout();
396    },
397
398    onResize: function()
399    {
400        if (this._muteOnResize)
401            return;
402        this._updateLayout();
403    },
404
405    /**
406     * @param {Event} event
407     * @return {boolean}
408     */
409    _startResizerDragging: function(event)
410    {
411        if (!this._resizable)
412            return false;
413
414        this._saveSidebarSizeRecursively();
415        this._dragOffset = (this._secondIsSidebar ? this.totalSize() - this._sidebarSize : this._sidebarSize) - (this._isVertical ? event.pageX : event.pageY);
416        return true;
417    },
418
419    /**
420     * @param {Event} event
421     */
422    _resizerDragging: function(event)
423    {
424        var newOffset = (this._isVertical ? event.pageX : event.pageY) + this._dragOffset;
425        var newSize = (this._secondIsSidebar ? this.totalSize() - newOffset : newOffset);
426        this.setSidebarSize(newSize);
427        event.preventDefault();
428    },
429
430    /**
431     * @param {Event} event
432     */
433    _endResizerDragging: function(event)
434    {
435        delete this._dragOffset;
436        this._saveSidebarSizeRecursively();
437    },
438
439    _saveSidebarSizeRecursively: function()
440    {
441        /** @this {WebInspector.View} */
442        function doSaveSidebarSizeRecursively()
443        {
444            if (this._saveSidebarSize)
445                this._saveSidebarSize();
446            this._callOnVisibleChildren(doSaveSidebarSizeRecursively);
447        }
448        this._saveSidebarSize();
449        this._callOnVisibleChildren(doSaveSidebarSizeRecursively);
450    },
451
452    /**
453     * @param {Element} resizerElement
454     */
455    installResizer: function(resizerElement)
456    {
457        resizerElement.addEventListener("mousedown", this._onDragStart.bind(this), false);
458    },
459
460    /**
461     *
462     * @param {Event} event
463     */
464    _onDragStart: function(event)
465    {
466        WebInspector._elementDragStart(this._startResizerDragging.bind(this), this._resizerDragging.bind(this), this._endResizerDragging.bind(this), this._isVertical ? "ew-resize" : "ns-resize", event);
467    },
468
469    /**
470     * @return {WebInspector.Setting}
471     */
472    _sizeSetting: function()
473    {
474        if (!this._sidebarSizeSettingName)
475            return null;
476
477        var settingName = this._sidebarSizeSettingName + (this._isVertical ? "" : "H");
478        if (!WebInspector.settings[settingName])
479            WebInspector.settings[settingName] = WebInspector.settings.createSetting(settingName, undefined);
480
481        return WebInspector.settings[settingName];
482    },
483
484    /**
485     * @return {number}
486     */
487    _lastSidebarSize: function()
488    {
489        var sizeSetting = this._sizeSetting();
490        var size = sizeSetting ? sizeSetting.get() : 0;
491        if (!size)
492             size = this._isVertical ? this._savedSidebarWidth : this._savedSidebarHeight;
493        if (this._useFraction)
494            size *= this.totalSize();
495        return size;
496    },
497
498    _saveSidebarSize: function()
499    {
500        var size = this._sidebarSize;
501        if (size < 0)
502            return;
503
504        if (this._useFraction)
505            size /= this.totalSize();
506
507        if (this._isVertical)
508            this._savedSidebarWidth = size;
509        else
510            this._savedSidebarHeight = size;
511
512        var sizeSetting = this._sizeSetting();
513        if (sizeSetting)
514            sizeSetting.set(size);
515    },
516
517    __proto__: WebInspector.View.prototype
518}
519