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.classList.add("split-view");
44    this.element.classList.add("fill");
45
46    this._firstElement = this.element.createChild("div", "split-view-contents scroll-target split-view-contents-first");
47    this._secondElement = this.element.createChild("div", "split-view-contents scroll-target split-view-contents-second");
48
49    this._resizerElement = this.element.createChild("div", "split-view-resizer");
50    this._onDragStartBound = this._onDragStart.bind(this);
51    this._resizerElements = [];
52
53    this._resizable = true;
54
55    this._savedSidebarWidth = defaultSidebarWidth || 200;
56    this._savedSidebarHeight = defaultSidebarHeight || this._savedSidebarWidth;
57
58    if (0 < this._savedSidebarWidth && this._savedSidebarWidth < 1 &&
59        0 < this._savedSidebarHeight && this._savedSidebarHeight < 1)
60        this._useFraction = true;
61
62    this._sidebarSizeSettingName = sidebarSizeSettingName;
63
64    this.setSecondIsSidebar(true);
65
66    this._innerSetVertical(isVertical);
67
68    // Should be called after isVertical has the right value.
69    this.installResizer(this._resizerElement);
70}
71
72WebInspector.SplitView.prototype = {
73    /**
74     * @return {boolean}
75     */
76    isVertical: function()
77    {
78        return this._isVertical;
79    },
80
81    /**
82     * @param {boolean} isVertical
83     */
84    setVertical: function(isVertical)
85    {
86        if (this._isVertical === isVertical)
87            return;
88
89        this._innerSetVertical(isVertical);
90
91        if (this.isShowing())
92            this._updateLayout();
93
94        for (var i = 0; i < this._resizerElements.length; ++i)
95            this._resizerElements[i].style.setProperty("cursor", this._isVertical ? "ew-resize" : "ns-resize");
96    },
97
98    /**
99     * @param {boolean} isVertical
100     */
101    _innerSetVertical: function(isVertical)
102    {
103        this.element.classList.remove(this._isVertical ? "hbox" : "vbox");
104        this._isVertical = isVertical;
105        this.element.classList.add(this._isVertical ? "hbox" : "vbox");
106        delete this._resizerElementSize;
107        this._sidebarSize = -1;
108    },
109
110    _updateLayout: function()
111    {
112        delete this._totalSize; // Lazy update.
113        this._innerSetSidebarSize(this._lastSidebarSize());
114    },
115
116    /**
117     * @return {!Element}
118     */
119    firstElement: function()
120    {
121        return this._firstElement;
122    },
123
124    /**
125     * @return {!Element}
126     */
127    secondElement: function()
128    {
129        return this._secondElement;
130    },
131
132    /**
133     * @return {!Element}
134     */
135    get mainElement()
136    {
137        return this.isSidebarSecond() ? this.firstElement() : this.secondElement();
138    },
139
140    /**
141     * @return {!Element}
142     */
143    get sidebarElement()
144    {
145        return this.isSidebarSecond() ? this.secondElement() : this.firstElement();
146    },
147
148    /**
149     * @return {boolean}
150     */
151    isSidebarSecond: function()
152    {
153        return this._secondIsSidebar;
154    },
155
156    /**
157     * @param {boolean} secondIsSidebar
158     */
159    setSecondIsSidebar: function(secondIsSidebar)
160    {
161        this.sidebarElement.classList.remove("split-view-sidebar");
162        this.mainElement.classList.remove("split-view-main");
163        this._secondIsSidebar = secondIsSidebar;
164        this.sidebarElement.classList.add("split-view-sidebar");
165        this.mainElement.classList.add("split-view-main");
166    },
167
168    /**
169     * @return {!Element}
170     */
171    resizerElement: function()
172    {
173        return this._resizerElement;
174    },
175
176    showOnlyFirst: function()
177    {
178        this._showOnly(this._firstElement, this._secondElement);
179    },
180
181    showOnlySecond: function()
182    {
183        this._showOnly(this._secondElement, this._firstElement);
184    },
185
186    /**
187     * @param {!Element} sideA
188     * @param {!Element} sideB
189     */
190    _showOnly: function(sideA, sideB)
191    {
192        sideA.classList.remove("hidden");
193        sideA.classList.add("maximized");
194        sideB.classList.add("hidden");
195        sideB.classList.remove("maximized");
196        this._removeAllLayoutProperties();
197
198        this._isShowingOne = true;
199        this._sidebarSize = -1;
200        this.setResizable(false);
201        this.doResize();
202    },
203
204    _removeAllLayoutProperties: function()
205    {
206        this.sidebarElement.style.removeProperty("flexBasis");
207
208        this._resizerElement.style.removeProperty("left");
209        this._resizerElement.style.removeProperty("right");
210        this._resizerElement.style.removeProperty("top");
211        this._resizerElement.style.removeProperty("bottom");
212
213        this._resizerElement.style.removeProperty("margin-left");
214        this._resizerElement.style.removeProperty("margin-right");
215        this._resizerElement.style.removeProperty("margin-top");
216        this._resizerElement.style.removeProperty("margin-bottom");
217    },
218
219    showBoth: function()
220    {
221        this._firstElement.classList.remove("hidden");
222        this._firstElement.classList.remove("maximized");
223        this._secondElement.classList.remove("hidden");
224        this._secondElement.classList.remove("maximized");
225
226        this._isShowingOne = false;
227        this._sidebarSize = -1;
228        this.setResizable(true);
229        this.doResize();
230    },
231
232    /**
233     * @param {boolean} resizable
234     */
235    setResizable: function(resizable)
236    {
237        this._resizable = resizable;
238        this._resizerElement.enableStyleClass("hidden", !resizable);
239    },
240
241    /**
242     * @param {number} size
243     */
244    setSidebarSize: function(size)
245    {
246        this._innerSetSidebarSize(size);
247        this._saveSidebarSize();
248    },
249
250    /**
251     * @return {number}
252     */
253    sidebarSize: function()
254    {
255        return Math.max(0, this._sidebarSize);
256    },
257
258    /**
259     * @return {number}
260     */
261    totalSize: function()
262    {
263        if (!this._totalSize)
264            this._totalSize = this._isVertical ? this.element.offsetWidth : this.element.offsetHeight;
265        return this._totalSize;
266    },
267
268    /**
269     * @param {number} size
270     */
271    _innerSetSidebarSize: function(size)
272    {
273        if (this._isShowingOne) {
274            this._sidebarSize = size;
275            return;
276        }
277
278        size = this._applyConstraints(size);
279        if (this._sidebarSize === size)
280            return;
281
282        if (size < 0) {
283            // Never apply bad values, fix it upon onResize instead.
284            return;
285        }
286
287        this._removeAllLayoutProperties();
288
289        var sizeValue;
290        if (this._useFraction)
291            sizeValue = (size / this.totalSize()) * 100 + "%";
292        else
293            sizeValue = size + "px";
294
295        if (!this._resizerElementSize)
296            this._resizerElementSize = this._isVertical ? this._resizerElement.offsetWidth : this._resizerElement.offsetHeight;
297
298        this.sidebarElement.style.flexBasis = sizeValue;
299        if (this._isVertical) {
300            if (this._secondIsSidebar) {
301                this._resizerElement.style.right = sizeValue;
302                this._resizerElement.style.marginRight = -this._resizerElementSize / 2 + "px";
303            } else {
304                this._resizerElement.style.left = sizeValue;
305                this._resizerElement.style.marginLeft = -this._resizerElementSize / 2 + "px";
306            }
307        } else {
308            if (this._secondIsSidebar) {
309                this._resizerElement.style.bottom = sizeValue;
310                this._resizerElement.style.marginBottom = -this._resizerElementSize / 2 + "px";
311            } else {
312                this._resizerElement.style.top = sizeValue;
313                this._resizerElement.style.marginTop = -this._resizerElementSize / 2 + "px";
314            }
315        }
316
317        this._sidebarSize = size;
318
319        // No need to recalculate this._sidebarSize and this._totalSize again.
320        this._muteOnResize = true;
321        this.doResize();
322        delete this._muteOnResize;
323    },
324
325    /**
326     * @param {number=} minWidth
327     * @param {number=} minHeight
328     */
329    setSidebarElementConstraints: function(minWidth, minHeight)
330    {
331        if (typeof minWidth === "number")
332            this._minimumSidebarWidth = minWidth;
333        if (typeof minHeight === "number")
334            this._minimumSidebarHeight = minHeight;
335    },
336
337    /**
338     * @param {number=} minWidth
339     * @param {number=} minHeight
340     */
341    setMainElementConstraints: function(minWidth, minHeight)
342    {
343        if (typeof minWidth === "number")
344            this._minimumMainWidth = minWidth;
345        if (typeof minHeight === "number")
346            this._minimumMainHeight = minHeight;
347    },
348
349    /**
350     * @param {number} sidebarSize
351     * @return {number}
352     */
353    _applyConstraints: function(sidebarSize)
354    {
355        const minPadding = 20;
356        var totalSize = this.totalSize();
357        var from = (this.isVertical() ? this._minimumSidebarWidth : this._minimumSidebarHeight) || 0;
358        var fromInPercents = false;
359        if (from && from < 1) {
360            fromInPercents = true;
361            from = Math.round(totalSize * from);
362        }
363        from = Math.max(from, minPadding);
364
365        var minMainSize = (this.isVertical() ? this._minimumMainWidth : this._minimumMainHeight) || 0;
366        var toInPercents = false;
367        if (minMainSize && minMainSize < 1) {
368            toInPercents = true;
369            minMainSize = Math.round(totalSize * minMainSize);
370        }
371        minMainSize = Math.max(minMainSize, minPadding);
372
373        var to = totalSize - minMainSize;
374        if (from <= to)
375            return Number.constrain(sidebarSize, from, to);
376
377        // Respect fixed constraints over percents. This will, for example, shrink
378        // the sidebar to its minimum size when possible.
379        if (!fromInPercents && !toInPercents)
380            return -1;
381        if (toInPercents && sidebarSize >= from && from < totalSize)
382            return from;
383        if (fromInPercents && sidebarSize <= to && to < totalSize)
384            return to;
385
386        return -1;
387    },
388
389    wasShown: function()
390    {
391        this._updateLayout();
392    },
393
394    onResize: function()
395    {
396        if (this._muteOnResize)
397            return;
398        this._updateLayout();
399    },
400
401    /**
402     * @param {!MouseEvent} event
403     * @return {boolean}
404     */
405    _startResizerDragging: function(event)
406    {
407        if (!this._resizable)
408            return false;
409
410        this._saveSidebarSizeRecursively();
411        this._dragOffset = (this._secondIsSidebar ? this.totalSize() - this._sidebarSize : this._sidebarSize) - (this._isVertical ? event.pageX : event.pageY);
412        return true;
413    },
414
415    /**
416     * @param {!MouseEvent} event
417     */
418    _resizerDragging: function(event)
419    {
420        var newOffset = (this._isVertical ? event.pageX : event.pageY) + this._dragOffset;
421        var newSize = (this._secondIsSidebar ? this.totalSize() - newOffset : newOffset);
422        this.setSidebarSize(newSize);
423        event.preventDefault();
424    },
425
426    /**
427     * @param {!MouseEvent} event
428     */
429    _endResizerDragging: function(event)
430    {
431        delete this._dragOffset;
432        this._saveSidebarSizeRecursively();
433    },
434
435    _saveSidebarSizeRecursively: function()
436    {
437        /** @this {WebInspector.View} */
438        function doSaveSidebarSizeRecursively()
439        {
440            if (this._saveSidebarSize)
441                this._saveSidebarSize();
442            this._callOnVisibleChildren(doSaveSidebarSizeRecursively);
443        }
444        this._saveSidebarSize();
445        this._callOnVisibleChildren(doSaveSidebarSizeRecursively);
446    },
447
448    /**
449     * @param {!Element} resizerElement
450     */
451    installResizer: function(resizerElement)
452    {
453        resizerElement.addEventListener("mousedown", this._onDragStartBound, false);
454        resizerElement.style.setProperty("cursor", this._isVertical ? "ew-resize" : "ns-resize");
455        this._resizerElements.push(resizerElement);
456    },
457
458    /**
459     * @param {!Element} resizerElement
460     */
461    uninstallResizer: function(resizerElement)
462    {
463        resizerElement.removeEventListener("mousedown", this._onDragStartBound, false);
464        resizerElement.style.removeProperty("cursor");
465        this._resizerElements.remove(resizerElement);
466    },
467
468    /**
469     * @param {?Event} event
470     */
471    _onDragStart: function(event)
472    {
473        WebInspector.elementDragStart(this._startResizerDragging.bind(this), this._resizerDragging.bind(this), this._endResizerDragging.bind(this), this._isVertical ? "ew-resize" : "ns-resize", event);
474    },
475
476    /**
477     * @return {?WebInspector.Setting}
478     */
479    _sizeSetting: function()
480    {
481        if (!this._sidebarSizeSettingName)
482            return null;
483
484        var settingName = this._sidebarSizeSettingName + (this._isVertical ? "" : "H");
485        if (!WebInspector.settings[settingName])
486            WebInspector.settings[settingName] = WebInspector.settings.createSetting(settingName, undefined);
487
488        return WebInspector.settings[settingName];
489    },
490
491    /**
492     * @return {number}
493     */
494    _lastSidebarSize: function()
495    {
496        var sizeSetting = this._sizeSetting();
497        var size = sizeSetting ? sizeSetting.get() : 0;
498        if (!size)
499             size = this._isVertical ? this._savedSidebarWidth : this._savedSidebarHeight;
500        if (this._useFraction)
501            size *= this.totalSize();
502        return size;
503    },
504
505    _saveSidebarSize: function()
506    {
507        var size = this._sidebarSize;
508        if (size < 0)
509            return;
510
511        if (this._useFraction)
512            size /= this.totalSize();
513
514        if (this._isVertical)
515            this._savedSidebarWidth = size;
516        else
517            this._savedSidebarHeight = size;
518
519        var sizeSetting = this._sizeSetting();
520        if (sizeSetting)
521            sizeSetting.set(size);
522    },
523
524    __proto__: WebInspector.View.prototype
525}
526