diff options
Diffstat (limited to 'src/controls/Splitter.qml')
-rw-r--r-- | src/controls/Splitter.qml | 401 |
1 files changed, 401 insertions, 0 deletions
diff --git a/src/controls/Splitter.qml b/src/controls/Splitter.qml new file mode 100644 index 00000000..16796f1a --- /dev/null +++ b/src/controls/Splitter.qml @@ -0,0 +1,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 + } + +} |