From 8b2a1ec43134e191a5f9bb08a73f1a694d52927d Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 30 Jan 2015 20:57:40 +0100 Subject: Import the Qt Quick Controls 2 prototype Change-Id: Ib8c0c4160958e5cfea29a6e9df1b3f1fb19715fc Reviewed-by: Jari-Pekka Nurmi --- src/imports/extras/SplitView.qml | 556 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 556 insertions(+) create mode 100644 src/imports/extras/SplitView.qml (limited to 'src/imports/extras/SplitView.qml') diff --git a/src/imports/extras/SplitView.qml b/src/imports/extras/SplitView.qml new file mode 100644 index 00000000..56eb56f4 --- /dev/null +++ b/src/imports/extras/SplitView.qml @@ -0,0 +1,556 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Quick Extras module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.2 +import QtQuick.Controls 1.2 +import QtQuick.Layouts 1.0 +import QtQuick.Controls.Private 1.0 as Private +import QtQuick.Window 2.1 + +/*! + \qmltype SplitView + \inqmlmodule QtQuick.Controls + \since 5.1 + \ingroup views + \brief Lays out items with a draggable splitter between each item. + + \image splitview.png + + SplitView is a control that lays out items horizontally or + vertically with a draggable splitter between each item. + + There will always be one (and only one) item in the SplitView that has \l{Layout::fillWidth}{Layout.fillWidth} + set to \c true (or \l{Layout::fillHeight}{Layout.fillHeight}, if orientation is Qt.Vertical). This means that the + item will get all leftover space when other items have been laid out. + By default, the last visible child of the SplitView will have this set, but + it can be changed by explicitly setting fillWidth to \c true on another item. + As the fillWidth item will automatically be resized to fit the extra space, explicit assignments + to width and height will be ignored (but \l{Layout::minimumWidth}{Layout.minimumWidth} and + \l{Layout::maximumWidth}{Layout.maximumWidth} will still be respected). + + A handle can belong to the item either on the left or top side, or on the right or bottom side: + \list + \li If the fillWidth item is to the right: the handle belongs to the left item. + \li if the fillWidth item is on the left: the handle belongs to the right item. + \endlist + + This will again control which item gets resized when the user drags a handle, + and which handle gets hidden when an item is told to hide. + + SplitView supports setting attached Layout properties on child items, which + means that you can set the following attached properties for each child: + \list + \li \l{Layout::minimumWidth}{Layout.minimumWidth} + \li \l{Layout::minimumHeight}{Layout.minimumHeight} + \li \l{Layout::maximumWidth}{Layout.maximumWidth} + \li \l{Layout::maximumHeight}{Layout.maximumHeight} + \li \l{Layout::fillWidth}{Layout.fillWidth} (\c true for only one child) + \li \l{Layout::fillHeight}{Layout.fillHeight} (\c true for only one child) + \endlist + + \note Please import QtQuick.Layout 1.0 in your QML file in order to use the Layout + attached properties inside SplitView. + + Example: + + To create a SplitView with three items, and let the center item get superfluous space, one + could do the following: + + \qml + SplitView { + anchors.fill: parent + orientation: Qt.Horizontal + + Rectangle { + width: 200 + Layout.maximumWidth: 400 + color: "lightblue" + Text { + text: "View 1" + anchors.centerIn: parent + } + } + Rectangle { + id: centerItem + Layout.minimumWidth: 50 + Layout.fillWidth: true + color: "lightgray" + Text { + text: "View 2" + anchors.centerIn: parent + } + } + Rectangle { + width: 200 + color: "lightgreen" + Text { + text: "View 3" + anchors.centerIn: parent + } + } + } + + \endqml +*/ + +Item { + id: root + + /*! + \qmlproperty enumeration SplitView::orientation + + This property holds the orientation of the SplitView. + The value can be either \c Qt.Horizontal or \c Qt.Vertical. + The default value is \c Qt.Horizontal. + */ + property int orientation: Qt.Horizontal + + /*! + This property holds the delegate that will be instantiated between each + child item. Inside the delegate the following properties are available: + + \table + \row \li readonly property bool styleData.index \li 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. + \row \li readonly property bool styleData.hovered \li The handle is being hovered. + \row \li readonly property bool styleData.pressed \li The handle is being pressed. + \row \li readonly property bool styleData.resizing \li The handle is being dragged. + \endtable + +*/ + property Component handleDelegate: Rectangle { + width: 1 + height: 1 + color: Qt.darker(pal.window, 1.5) + } + + /*! + This property is \c true when the user is resizing any of the items by + dragging on the splitter handles. + */ + property bool resizing: false + + /*! \internal */ + default property alias __contents: contents.data + /*! \internal */ + property alias __items: splitterItems.children + /*! \internal */ + property alias __handles: splitterHandles.children + + clip: true + Component.onCompleted: d.init() + onWidthChanged: d.updateLayout() + onHeightChanged: d.updateLayout() + onOrientationChanged: d.changeOrientation() + + /*! Add an item to the end of the view. + \since QtQuick.Controls 1.3 */ + function addItem(item) { + d.updateLayoutGuard = true + + d.addItem_impl(item) + + d.calculateImplicitSize() + d.updateLayoutGuard = false + d.updateFillIndex() + } + + SystemPalette { id: pal } + + QtObject { + id: d + + readonly property string leftMargin: horizontal ? "leftMargin" : "topMargin" + readonly property string topMargin: horizontal ? "topMargin" : "leftMargin" + readonly property string rightMargin: horizontal ? "rightMargin" : "bottomMargin" + + property bool horizontal: orientation == Qt.Horizontal + readonly property string minimum: horizontal ? "minimumWidth" : "minimumHeight" + readonly property string maximum: horizontal ? "maximumWidth" : "maximumHeight" + readonly property string otherMinimum: horizontal ? "minimumHeight" : "minimumWidth" + readonly property string otherMaximum: horizontal ? "maximumHeight" : "maximumWidth" + readonly property string offset: horizontal ? "x" : "y" + readonly property string otherOffset: horizontal ? "y" : "x" + readonly property string size: horizontal ? "width" : "height" + readonly property string otherSize: horizontal ? "height" : "width" + readonly property string implicitSize: horizontal ? "implicitWidth" : "implicitHeight" + readonly property string implicitOtherSize: horizontal ? "implicitHeight" : "implicitWidth" + + property int fillIndex: -1 + property bool updateLayoutGuard: true + + function extraMarginSize(item, other) { + if (typeof(other) === 'undefined') + other = false; + if (other === horizontal) + // vertical + return item.Layout.topMargin + item.Layout.bottomMargin + return item.Layout.leftMargin + item.Layout.rightMargin + } + + function addItem_impl(item) + { + // temporarily set fillIndex to new item + fillIndex = __items.length + if (splitterItems.children.length > 0) + handleLoader.createObject(splitterHandles, {"__handleIndex":splitterItems.children.length - 1}) + + item.parent = splitterItems + + // should match disconnections in Component.onDestruction + 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.leftMarginChanged.connect(d.updateLayout) + item.Layout.topMarginChanged.connect(d.updateLayout) + item.Layout.rightMarginChanged.connect(d.updateLayout) + item.Layout.bottomMarginChanged.connect(d.updateLayout) + item.visibleChanged.connect(d.updateFillIndex) + item.Layout.fillWidthChanged.connect(d.updateFillIndex) + item.Layout.fillHeightChanged.connect(d.updateFillIndex) + } + + function init() + { + for (var i=0; i<__contents.length; ++i) { + var item = __contents[i]; + if (!item.hasOwnProperty("x")) + continue + addItem_impl(item) + i-- // item was removed from list + } + + d.calculateImplicitSize() + d.updateLayoutGuard = false + d.updateFillIndex() + } + + function updateFillIndex() + { + if (lastItem.visible !== root.visible) + return + var policy = (root.orientation === Qt.Horizontal) ? "fillWidth" : "fillHeight" + for (var i=0; i<__items.length-1; ++i) { + if (__items[i].Layout[policy] === true) + break; + } + + d.fillIndex = i + d.updateLayout() + } + + function changeOrientation() + { + if (__items.length == 0) + return; + d.updateLayoutGuard = true + + // Swap width/height for items and handles: + for (var i=0; i<__items.length; ++i) { + var item = __items[i] + var tmp = item.x + item.x = item.y + item.y = tmp + tmp = item.width + item.width = item.height + item.height = tmp + + var handle = __handles[i] + if (handle) { + tmp = handle.x + handle.x = handle.y + handle.y = handle.x + tmp = handle.width + handle.width = handle.height + handle.height = tmp + } + } + + // Change d.horizontal explicit, since the binding will change too late: + d.horizontal = orientation == Qt.Horizontal + d.updateLayoutGuard = false + d.updateFillIndex() + } + + function calculateImplicitSize() + { + var implicitSize = 0 + var implicitOtherSize = 0 + + for (var i=0; i<__items.length; ++i) { + var item = __items[i]; + implicitSize += clampedMinMax(item[d.size], item.Layout[minimum], item.Layout[maximum]) + extraMarginSize(item) + var os = clampedMinMax(item[otherSize], item.Layout[otherMinimum], item.Layout[otherMaximum]) + implicitOtherSize = Math.max(implicitOtherSize, os) + + var handle = __handles[i] + if (handle) + implicitSize += handle[d.size] //### Can handles have margins?? + } + + root[d.implicitSize] = implicitSize + root[d.implicitOtherSize] = implicitOtherSize + extraMarginSize(item, true) + } + + function clampedMinMax(value, minimum, maximum) + { + if (value < minimum) + value = minimum + if (value > maximum) + value = maximum + return value + } + + function accumulatedSize(firstIndex, lastIndex, includeFillItemMinimum) + { + // Go through items and handles, and + // calculate their accummulated width. + var w = 0 + for (var i=firstIndex; i __handleIndex) + visible: __items[__handleIndex + (resizeLeftItem ? 0 : 1)].visible + sourceComponent: handleDelegate + onWidthChanged: d.updateLayout() + onHeightChanged: d.updateLayout() + onXChanged: moveHandle() + onYChanged: moveHandle() + + MouseArea { + id: mouseArea + anchors.fill: parent + property real defaultMargin: Private.Settings.hasTouchScreen ? Screen.pixelDensity * 3.5 : 2 + anchors.leftMargin: (parent.width <= 1) ? -defaultMargin : 0 + anchors.rightMargin: (parent.width <= 1) ? -defaultMargin : 0 + anchors.topMargin: (parent.height <= 1) ? -defaultMargin : 0 + anchors.bottomMargin: (parent.height <= 1) ? -defaultMargin : 0 + hoverEnabled: true + drag.threshold: 0 + 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 fillItem is. + // 'updateLayout' will be overridden in case new width violates max/min. + // '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 (resizeLeftItem) { + // 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: contents + visible: false + anchors.fill: parent + } + Item { + id: splitterItems + anchors.fill: parent + } + Item { + id: splitterHandles + anchors.fill: parent + } + + Item { + id: lastItem + onVisibleChanged: d.updateFillIndex() + } + + Component.onDestruction: { + for (var i=0; i