Files
qt-creator/lib/qtcreator/qtcomponents/custom/SplitterRow.qml
hjk 2931a499e6 Long live the king!
Change-Id: I2b72b34c0cfeafc8bdbaf49b83ff723544f2b6e2
Reviewed-by: Daniel Teske <daniel.teske@nokia.com>
2012-01-26 19:55:36 +01:00

361 lines
15 KiB
QML

/**************************************************************************
**
** This file is part of Qt Creator
**
** Copyright (c) 2012 Nokia Corporation and/or its subsidiary(-ies).
**
** Contact: Nokia Corporation (qt-info@nokia.com)
**
**
** GNU Lesser General Public License Usage
**
** This file may be used under the terms of the GNU Lesser General Public
** License version 2.1 as published by the Free Software Foundation and
** appearing in the file LICENSE.LGPL included in the packaging of this file.
** Please review the following information to ensure the GNU Lesser General
** Public License version 2.1 requirements will be met:
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Nokia gives you certain additional
** rights. These rights are described in the Nokia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** Other Usage
**
** Alternatively, this file may be used in accordance with the terms and
** conditions contained in a signed written agreement between you and Nokia.
**
** If you have questions regarding the use of this file, please contact
** Nokia at qt-info@nokia.com.
**
**************************************************************************/
import QtQuick 1.0
import "private"
/*
*
* SplitterRow
*
* SplitterRow is a component that provides a way to layout items horisontally with
* a draggable splitter added in-between each item.
*
* Add items to the SplitterRow by inserting them as child items. The splitter handle
* is outsourced as a delegate (handleBackground). For this delegate to work properly,
* it will need to contain a mouse area that communicates with the SplitterRow by binding
* 'onMouseXChanged: handleDragged(handleIndex)', and 'drag.target: dragTarget'.
*
* The SplitterRow contains the followin API:
*
* Component handleBackground - 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.
* Item handleDragTarget - convenience property that tells which drag target any
* inner mouse areas that controls the handle should bind to.
* function handleDragged(handleIndex) - function that should be called whenever
* the handle is dragged to a new position
*
* The following properties can optionally be added for each direct child item of SplitterRow:
*
* real minimumWidth - if present, ensures that the item cannot be resized below the
* given value. A value of -1 will disable it.
* real maximumWidth - if present, ensures that the item cannot be resized above the
* given value. A value of -1 will disable it.
* real percentageWidth - if present, should be a value between 0-100. This value specifies
* a percentage of the width of the SplitterRow width. If the width of the SplitterRow
* change, the width of the item will change as well. 'percentageWidth' have precedence
* over 'width', which means that SplitterRow will ignore any assignments done to 'width'.
* A value of -1 disables it.
* bool expanding - if present, the item will consume all extra space in the SplitterRow, down to
* minimumWidth. This means that that 'width', 'percentageWidth' and 'maximumWidth' will be ignored.
* There will always be one (and only one) item in the SplitterRow that has this behaviour, and by
* default, it will be the last child item of the SplitterRow. Also note that which item that gets
* resized upon dragging a handle depends on whether the expanding item is located towards the left
* or the right of the handle.
*
* Example:
*
* To create a SplitterRow with three items, and let
* the center item be the one that should be expanding, one
* could do the following:
*
* SplitterRow {
* anchors.fill: parent
*
* handleBackground: Rectangle {
* width: 1
* color: "black"
*
* MouseArea {
* anchors.fill: parent
* anchors.leftMargin: -2
* anchors.rightMargin: -2
* drag.axis: Qt.YAxis
* drag.target: handleDragTarget
* onMouseXChanged: handleDragged(handleIndex)
* }
* }
*
* Rectangle {
* color: "gray"
* width: 200
* }
* Rectangle {
* property real minimumWidth: 50
* property real maximumWidth: 400
* property bool expanding: true
* color: "darkgray"
* }
* Rectangle {
* color: "gray"
* width: 200
* }
* }
*/
Item {
id: root
default property alias items: splitterItems.children
property alias handles: splitterHandles.children
property Component handleBackground: Rectangle { width:3; color: "black" }
clip: true
Component.onCompleted: d.init();
onWidthChanged: d.updateLayout();
QtObject {
id: d
property int expandingIndex: items.length-1
property bool updateOptimizationBlock: true
property bool bindingRecursionGuard: false
function init()
{
for (var i=0; i<items.length-1; ++i) {
// Anchor each item to fill out all space vertically:
var item = items[i];
item.anchors.top = splitterItems.top
item.anchors.bottom = splitterItems.bottom
// Listen for changes to width and expanding:
propertyChangeListener.createObject(item);
// Create a handle for the item:
var handle = handleBackgroundLoader.createObject(splitterHandles, {"handleIndex":i});
handle.anchors.top = splitterHandles.top
handle.anchors.bottom = splitterHandles.bottom
}
item = items[i]
if (item) {
// Do the same for the last item as well, since
// the for-loop skipped the last item:
items[i].anchors.top = splitterItems.top
items[i].anchors.bottom = splitterItems.bottom
propertyChangeListener.createObject(items[i]);
}
d.updateOptimizationBlock = false
d.updateLayout()
}
function accumulatedWidth(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 (i !== d.expandingIndex)
w += item.width;
else if (includeExpandingMinimum && item.minimumWidth != undefined && item.minimumWidth != -1)
w += item.minimumWidth
if (handles[i] && (i !== d.expandingIndex || includeExpandingMinimum === false))
w += handles[i].width
}
return w
}
function updateLayout()
{
if (items.length === 0)
return;
if (d.updateOptimizationBlock === true)
return
d.updateOptimizationBlock = true
// This function will reposition both handles and
// items according to the _width of the each item_
var item, prevItem
var handle, prevHandle
var newValue
// Ensure all items within min/max:
for (var i=0; i<items.length; ++i) {
if (i !== d.expandingIndex) {
item = items[i];
// If the item is using percentage width, convert
// that number to real width now:
if (item.percentageWidth != undefined && item.percentageWidth !== -1) {
newValue = item.percentageWidth * (root.width / 100)
if (newValue !== item.width)
item.width = newValue
}
// Ensure item width is not more than maximumWidth:
if (item.maximumWidth != undefined && item.maximumWidth != -1) {
newValue = Math.min(item.width, item.maximumWidth)
if (newValue !== item.width)
item.width = newValue
}
// Ensure item width is not more less minimumWidth:
if (item.minimumWidth != undefined && item.minimumWidth != -1) {
newValue = Math.max(item.width, item.minimumWidth)
if (newValue !== item.width)
item.width = newValue
}
}
}
// Special case: set width of expanding item to available space:
newValue = root.width - d.accumulatedWidth(0, items.length, false);
var expandingItem = items[d.expandingIndex]
if (expandingItem.minimumWidth != undefined && expandingItem.minimumWidth != -1)
newValue = Math.max(newValue, expandingItem.minimumWidth)
if (expandingItem.width !== newValue)
expandingItem.width = newValue
// Then, position items and handles according to their width:
for (i=0; i<items.length; ++i) {
item = items[i];
handle = handles[i]
// Position item to the right of the previus handle:
if (prevHandle) {
newValue = prevHandle.x + prevHandle.width
if (newValue !== item.x)
item.x = newValue
}
// Position handle to the right of item:
if (handle) {
newValue = item.x + Math.max(0, item.width)
if (newValue !== handle.x)
handle.x = newValue
}
prevItem = item
prevHandle = handle
}
d.updateOptimizationBlock = false
}
}
Component {
id: handleBackgroundLoader
Loader {
id: loader
property int handleIndex: 0
property Item handleDragTarget: loader
sourceComponent: handleBackground
function handleDragged(handleIndex)
{
// 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.
var leftHandle, leftItem, handle, rightItem, rightHandle
var leftEdge, rightEdge, newWidth
handle = handles[handleIndex]
if (d.expandingIndex > handleIndex) {
// Resize item to the left.
// Ensure that the handle is not crossing other handles:
leftHandle = handles[handleIndex-1]
leftItem = items[handleIndex]
leftEdge = leftHandle ? (leftHandle.x + leftHandle.width) : 0
handle.x = Math.max(leftEdge, handle.x)
newWidth = handle.x - leftEdge
if (root.width != 0 && leftItem.percentageWidth != undefined && leftItem.percentageWidth !== -1)
leftItem.percentageWidth = newWidth * (100 / root.width)
// The next line will trigger 'updateLayout' inside 'propertyChangeListener':
leftItem.width = newWidth
} else {
// Resize item to the right:
// Since the first item in the splitter always will have x=0, we need
// to ensure that the user cannot drag the handle more left than what
// we got space for:
var min = d.accumulatedWidth(0, handleIndex+1, true)
// Ensure that the handle is not crossing other handles:
rightItem = items[handleIndex+1]
rightHandle = handles[handleIndex+1]
rightEdge = (rightHandle ? rightHandle.x : root.width)
handle.x = Math.max(min, Math.max(Math.min((rightEdge - handle.width), handle.x)))
newWidth = rightEdge - (handle.x + handle.width)
if (root.width != 0 && rightItem.percentageWidth != undefined && rightItem.percentageWidth !== -1)
rightItem.percentageWidth = newWidth * (100 / root.width)
// The next line will trigger 'updateLayout' inside 'propertyChangeListener':
rightItem.width = newWidth
}
}
}
}
Item {
id: splitterItems
anchors.fill: parent
}
Item {
id: splitterHandles
anchors.fill: parent
}
Component {
// This dummy item becomes a child of all
// items it the splitter, just to provide a way
// to listed for changes to their width, expanding etc.
id: propertyChangeListener
Item {
id: target
width: parent.width
property bool expanding: (parent.expanding != undefined) ? parent.expanding : false
property real percentageWidth: (parent.percentageWidth != undefined) ? parent.percentageWidth : -1
property real minimumWidth: (parent.minimumWidth != undefined) ? parent.minimumWidth : -1
property real maximumWidth: (parent.maximumWidth != undefined) ? parent.maximumWidth : -1
onPercentageWidthChanged: d.updateLayout();
onMinimumWidthChanged: d.updateLayout();
onMaximumWidthChanged: d.updateLayout();
onExpandingChanged: {
// Find out which item that has the expanding flag:
for (var i=0; i<items.length; ++i) {
var item = items[i]
if (item.expanding && item.expanding === true) {
d.expandingIndex = i
d.updateLayout();
return
}
}
d.expandingIndex = i-1
updateLayout();
}
onWidthChanged: {
// We need to update the layout:
if (d.bindingRecursionGuard === true)
return
d.bindingRecursionGuard = true
// Break binding:
width = 0
d.updateLayout()
// Restablish binding:
width = function() { return parent.width; }
d.bindingRecursionGuard = false
}
}
}
}