summaryrefslogtreecommitdiff
path: root/src/controls/Splitter.qml
blob: 16796f1a3ee20215d26c17f17e5959d6ebc3b056 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
/****************************************************************************
**
** Copyright (C) 2012 Digia Plc and/or its subsidiary(-ies).
** Contact: http://www.qt-project.org/legal
**
** This file is part of the Qt Components project.
**
** $QT_BEGIN_LICENSE:BSD$
** You may use this file under the terms of the BSD license as follows:
**
** "Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
**   * Redistributions of source code must retain the above copyright
**     notice, this list of conditions and the following disclaimer.
**   * Redistributions in binary form must reproduce the above copyright
**     notice, this list of conditions and the following disclaimer in
**     the documentation and/or other materials provided with the
**     distribution.
**   * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names
**     of its contributors may be used to endorse or promote products derived
**     from this software without specific prior written permission.
**
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
**
** $QT_END_LICENSE$
**
****************************************************************************/

import QtQuick 2.0
import QtQuick.Controls 1.0
import QtQuick.Controls.Private 1.0 as Private

/*!
    \qmltype Splitter
    \inqmlmodule QtQuick.Controls 1.0
    \brief Splitter is a component that lays out items horisontally or
    vertically with a draggable splitter between each item.
*/

/*
*
* Splitter
*
* Splitter is a component that lays out items horisontally or
* vertically with a draggable splitter between each item.
*
* There will always be one (and only one) item in the Splitter that is 'expanding'.
* Being expanding means that the item will get all the remaining space when other
* items have been laid out according to their own width and height.
* By default, the last visible child of the Splitter will be expanding, but
* this can changed by setting Layout.horizontalSizePolicy to \c Layout.Expanding.
* Since the expanding item will automatically be resized to fit the extra space, it
* will ignore explicit assignments to width and height.
*
* A handle can belong to the item on the left/top side, or the right/bottom side, of the
* handle. Which one depends on the expaning item. If the expanding item is to the right
* of the handle, the handle will belong to the item on the left. If it is to the left, it
* will belong to the item on the right. This will again control which item that gets resized
* when the user drags a handle, and which handle that gets hidden when an item is told to hide.
*
* The Splitter contains the following API:
*
* int orientation - the orientation of the splitter. Can be either Qt.Horizontal
*   or Qt.Vertical.
* Component handleDelegate - delegate that will be instanciated between each
*   child item. Inside the delegate, the following properties are available:
*   int handleIndex - specifies the index of the splitter handle. The handle
*       between the first and the second item will get index 0, the next handle index 1 etc.
*   bool containsMouse - the mouse hovers the handle.
*   bool pressed: the handle is being pressed.
*   bool dragged: the handle is being dragged.
*
* Splitter supports setting Layout properties on child items, which means that you
* can control minimumWidth, minimumHeight, maximumWidth and maximumHeight (in addition
* to horizontalSizePolicy/verticalSizePolicy) for each child.
*
* Example:
*
* To create a Splitter with three items, and let
* the center item be expanding, one could do the following:
*
*    Splitter {
*        anchors.fill: parent
*        orientation: Qt.Horizontal
*
*        Rectangle {
*            width: 200
*            Layout.maximumWidth: 400
*            color: "gray"
*        }
*        Rectangle {
*            id: centerItem
*            Layout.minimumWidth: 50
*            Layout.horizontalSizePolicy: Layout.Expanding
*            color: "darkgray"
*        }
*        Rectangle {
*            width: 200
*            color: "gray"
*        }
*    }
*/

Item {
    id: root
    property int orientation: Qt.Horizontal

    property Component handleDelegate:
    Rectangle{
        width: 1
        height: 1
        color: Qt.darker(pal.window, 1.5)
    }

    // **** PRIVATE ****

    clip: true
    default property alias __items: splitterItems.children
    property alias __handles: splitterHandles.children
    Component.onCompleted: d.init()
    onWidthChanged: d.updateLayout()
    onHeightChanged: d.updateLayout()

    SystemPalette { id: pal }

    QtObject {
        id: d
        property bool horizontal: orientation == Qt.Horizontal
        property string minimum: horizontal ? "minimumWidth" : "minimumHeight"
        property string maximum: horizontal ? "maximumWidth" : "maximumHeight"
        property string offset: horizontal ? "x" : "y"
        property string otherOffset: horizontal ? "y" : "x"
        property string size: horizontal ? "width" : "height"
        property string otherSize: horizontal ? "height" : "width"

        property int expandingIndex: -1
        property bool updateLayoutGuard: true

        function init()
        {
            for (var i=0; i<__items.length; ++i) {
                var item = __items[i];
                item.widthChanged.connect(d.updateLayout);
                item.heightChanged.connect(d.updateLayout);
                item.Layout.maximumWidthChanged.connect(d.updateLayout);
                item.Layout.minimumWidthChanged.connect(d.updateLayout);
                item.Layout.maximumHeightChanged.connect(d.updateLayout);
                item.Layout.minimumHeightChanged.connect(d.updateLayout);
                item.Layout.horizontalSizePolicyChanged.connect(d.updateExpandingIndex)
                item.Layout.verticalSizePolicyChanged.connect(d.updateExpandingIndex)
                d.listenForVisibleChanged(item)
                if (i < __items.length-1)
                    handleLoader.createObject(splitterHandles, {"handleIndex":i});
            }

            d.updateExpandingIndex()
            d.updateLayoutGuard = false
            d.updateLayout()
        }

        function listenForVisibleChanged(item) {
            item.visibleChanged.connect(function() {
                if (!root.visible)
                    return
                if (item.visible) {
                    // Try to keep all items within the SplitterRow. When an item
                    // has been hidden, the expanding item might no longer be large enough
                    // to give away space to the new items width. So we need to resize:
                    var overflow = d.accumulatedSize(0, __items.length, true) - root[d.size];
                    if (overflow > 0)
                        item[d.size] -= overflow
                }
                updateExpandingIndex()
            });
        }

        function updateExpandingIndex()
        {
            var policy = (root.orientation === Qt.Horizontal) ? "horizontalSizePolicy" : "verticalSizePolicy"
            for (var i=__items.length-1; i>=0; --i) {
                if (__items[i].visible && __items[i].Layout[policy] === Layout.Expanding) {
                    d.expandingIndex = i
                    break;
                }
            }

            if (i === -1) {
                for (i=__items.length-1; i>0; --i) {
                    if (__items[i].visible)
                        break;
                }
            }

            d.expandingIndex = i
            d.updateLayout()
        }

        function accumulatedSize(firstIndex, lastIndex, includeExpandingMinimum)
        {
            // Go through items and handles, and
            // calculate their acummulated width.
            var w = 0
            for (var i=firstIndex; i<lastIndex; ++i) {
                var item = __items[i]
                if (item.visible) {
                    if (i !== d.expandingIndex)
                        w += item[d.size];
                    else if (includeExpandingMinimum && item.Layout[minimum] !== undefined)
                        w += item.Layout[minimum]
                }

                var handle = __handles[i]
                if (handle && __items[i + ((d.expandingIndex > i) ? 0 : 1)].visible)
                    w += handle[d.size]
            }
            return w
        }

        function updateLayout()
        {
            // This function will reposition both handles and
            // items according to the their width/height:
            if (__items.length === 0)
                return;
            if (d.updateLayoutGuard === true)
                return
            d.updateLayoutGuard = true

            // Ensure all items within their min/max:
            for (var i=0; i<__items.length; ++i) {
                if (i !== d.expandingIndex) {
                    var item = __items[i];
                    if (item.Layout[maximum] !== undefined) {
                        if (item[d.size] > item.Layout[maximum])
                            item[d.size] = item.Layout[maximum]
                    }
                    if (item.Layout[minimum] !== undefined) {
                        if (item[d.size] < item.Layout[minimum])
                            item[d.size] = item.Layout[minimum]
                    }
                }
            }

            // Set size of expanding item to remaining available space:
            var expandingItem = __items[expandingIndex]
            var min = expandingItem.Layout[minimum] !== undefined ? expandingItem.Layout[minimum] : 0
            expandingItem[d.size] = Math.max(min, root[d.size] - d.accumulatedSize(0, __items.length, false))

            // Then, position items and handles according to their width:
            var lastVisibleItem, lastVisibleHandle, handle
            var implicitSize = min - expandingItem[d.size]

            for (i=0; i<__items.length; ++i) {
                // Position item to the right of the previous visible handle:
                item = __items[i];
                if (item.visible) {
                    item[d.offset] = lastVisibleHandle ? lastVisibleHandle[d.offset] + lastVisibleHandle[d.size] : 0
                    item[d.otherOffset] = 0
                    item[d.otherSize] = root[d.otherSize]
                    implicitSize += item[d.size]
                    lastVisibleItem = item
                }

                // Position handle to the right of the previous visible item. We use an alterative way of
                // checking handle visibility because that property might not have updated correctly yet:
                handle = __handles[i]
                if (handle && __items[i + ((d.expandingIndex > i) ? 0 : 1)].visible) {
                    handle[d.offset] = lastVisibleItem[d.offset] + Math.max(0, lastVisibleItem[d.size])
                    handle[d.otherOffset] = 0
                    handle[d.otherSize] = root[d.otherSize]
                    implicitSize += handle[d.size]
                    lastVisibleHandle = handle
                }
            }

            if (root.orientation === Qt.horizontal) {
                root.implicitWidth = implicitSize
                root.implicitHeight = 0
            } else {
                root.implicitWidth = 0
                root.implicitHeight = implicitSize
            }

            d.updateLayoutGuard = false
        }
    }

    Component {
        id: handleLoader
        Loader {
            id: itemHandle
            property int handleIndex: -1
            property alias containsMouse: mouseArea.containsMouse
            property alias pressed: mouseArea.pressed
            property bool dragged: mouseArea.drag.active

            visible: __items[handleIndex + ((d.expandingIndex > handleIndex) ? 0 : 1)].visible
            sourceComponent: handleDelegate
            onWidthChanged: d.updateLayout()
            onHeightChanged: d.updateLayout()
            onXChanged: moveHandle()
            onYChanged: moveHandle()

            MouseArea {
                id: mouseArea
                anchors.fill: parent
                anchors.leftMargin: (parent.width <= 1) ? -2 : 0
                anchors.rightMargin: (parent.width <= 1) ? -2 : 0
                anchors.topMargin: (parent.height <= 1) ? -2 : 0
                anchors.bottomMargin: (parent.height <= 1) ? -2 : 0
                hoverEnabled: true
                drag.target: parent
                drag.axis: root.orientation === Qt.Horizontal ? Drag.XAxis : Drag.YAxis
                cursorShape: root.orientation === Qt.Horizontal ? Qt.SplitHCursor : Qt.SplitVCursor
            }

            function moveHandle() {
                // Moving the handle means resizing an item. Which one,
                // left or right, depends on where the expanding item is.
                // 'updateLayout' will override in case new width violates max/min.
                // And 'updateLayout will be triggered when an item changes width.
                if (d.updateLayoutGuard)
                    return

                var leftHandle, leftItem, rightItem, rightHandle
                var leftEdge, rightEdge, newWidth, leftStopX, rightStopX
                var i

                if (d.expandingIndex > handleIndex) {
                    // Resize item to the left.
                    // Ensure that the handle is not crossing other handles. So
                    // find the first visible handle to the left to determine the left edge:
                    leftEdge = 0
                    for (i=handleIndex-1; i>=0; --i) {
                        leftHandle = __handles[i]
                        if (leftHandle.visible) {
                            leftEdge = leftHandle[d.offset] + leftHandle[d.size]
                            break;
                        }
                    }

                    // Ensure: leftStopX >= itemHandle[d.offset] >= rightStopX
                    var min = d.accumulatedSize(handleIndex+1, __items.length, true)
                    rightStopX = root[d.size] - min - itemHandle[d.size]
                    leftStopX = Math.max(leftEdge, itemHandle[d.offset])
                    itemHandle[d.offset] = Math.min(rightStopX, Math.max(leftStopX, itemHandle[d.offset]))

                    newWidth = itemHandle[d.offset] - leftEdge
                    leftItem = __items[handleIndex]
                    // The next line will trigger 'updateLayout':
                    leftItem[d.size] = newWidth
                } else {
                    // Resize item to the right.
                    // Ensure that the handle is not crossing other handles. So
                    // find the first visible handle to the right to determine the right edge:
                    rightEdge = root[d.size]
                    for (i=handleIndex+1; i<__handles.length; ++i) {
                        rightHandle = __handles[i]
                        if (rightHandle.visible) {
                            rightEdge = rightHandle[d.offset]
                            break;
                        }
                    }

                    // Ensure: leftStopX <= itemHandle[d.offset] <= rightStopX
                    min = d.accumulatedSize(0, handleIndex+1, true)
                    leftStopX = min - itemHandle[d.size]
                    rightStopX = Math.min((rightEdge - itemHandle[d.size]), itemHandle[d.offset])
                    itemHandle[d.offset] = Math.max(leftStopX, Math.min(itemHandle[d.offset], rightStopX))

                    newWidth = rightEdge - (itemHandle[d.offset] + itemHandle[d.size])
                    rightItem = __items[handleIndex+1]
                    // The next line will trigger 'updateLayout':
                    rightItem[d.size] = newWidth
                }
            }
        }
    }

    Item {
        id: splitterItems
        anchors.fill: parent
    }
    Item {
        id: splitterHandles
        anchors.fill: parent
    }

}