Dragging the range of a RangeSlider - qt

In my Qt project for viewing logfiles with charts, I need a range slider for easy navigation on the time axis.
What I need is support for dragging both handles; when clicking and dragingg in between the handles, both handles are moved together. The behaviour of the RangeSlider is to position the nearest handle to the clicked position, and I cant find any way of changing this from any properties.
When the provided controls is not providing the required functionality, I wonder if the only approach is to create a new component from scratch, or if there is any way of customizing the existing controls?

What you want is certainly possible:
RangeSlider {
id: sli
width: 300
from: 0
to: 1000
first.value: 300
second.value: 700
Rectangle {
anchors.left: sli.first.handle.right
anchors.right: sli.second.handle.left
anchors.verticalCenter: sli.verticalCenter
height: sli.first.handle.height
color: "red"
MouseArea {
anchors.fill: parent
property real dx: 0
onPressed: dx = mouseX
onPositionChanged: {
var d = ((mouseX - dx) / sli.width) * Math.abs(sli.to - sli.from)
if ((d + sli.first.value) < sli.from) d = sli.from - sli.first.value
if ((d + sli.second.value) > sli.to) d = sli.to - sli.second.value
sli.first.value += d
sli.second.value += d
dx = mouseX
}
}
}
}
The red rectangle is actually redundant, I merely put it there as a visual aid.

Related

TestCase mouseDrag only clicks item inside Flickable but does not drag

In a QML TestCase, I'm trying to setup automatic scrolling of a ListView that is contained inside a Flickable (to add a custom footer that can be flicked into view, which wouldn't happen with just ListView { footer: Component {} })
However, the mouseDrag only seems to click the correct coordinate, but not drag it to any direction. Here is a simplified version that is as close to the real one as possible:
Implementation.qml
import QtQuick 2.5
FocusScope {
width: 1920
height: 1080
Flickable {
objectName: 'flickableList'
boundsBehavior: Flickable.StopAtBounds
clip: true
width: parent.width
height: 240
contentHeight: 500
ListView {
interactive: false
height: parent.height
width: parent.width
model: ['example1', 'example2', 'example3', 'example4', 'example5']
delegate: Item {
width: 300
height: 100
Text {
text: modelData
}
}
}
}
Item {
id: footer
height: 100
width: parent.width
}
}
TheTest.qml
// The relevant part
var theList = findChild(getView(), 'flickableList')
var startY = 220
var endY = 20
mouseDrag(theList, 100, startY, 100, endY, Qt.LeftButton, Qt.NoModifier, 100)
So, when I'm viewing the UI testrunner, I can see it clearly click on the correct delegate (it has a focus highlight in the actual implementation), ie. the third item "example3", which starts at Y 200 and ends at Y 300). But the drag event never happens. Nothing moves on the screen, and compare(theList.contentY, 200) says it is still at position 0. I would expect it to be at 200, since the mouse is supposed to be mouseDragging from position 220 to 20, ie. scrolling the list down by 200. And 220 is also within the visible height (240).
Just to be sure, I also reversed the Y values, but also no movement:
var theList = findChild(getView(), 'flickableList')
var startY = 20
var endY = 220
mouseDrag(theList, 100, startY, 100, endY, Qt.LeftButton, Qt.NoModifier, 100)
Also, as the 3rd item clearly is clicked on (it gets highlighted), the passed item theList (= the Flickable), should be valid.
Edit:
Oh, and this does scroll the list, but it goes all the way to the bottom of the list (388 px down in the actual implementation, even when the delta is just 30 pixels):
mousePress(theList, startX, startY)
mouseMove(theList, endX, endY)
mouseRelease(theList, endX, endY)
So the question is:
Does mouseDrag only work for specific types of components (ie. does not work on Flickable?), or is there something missing? How can I get it to scroll the list down? Thanks!
Your tag says you're using Qt 5.5 - I would recommend trying Qt 5.14 if possible, as there was a fix that might help:
mouseDrag(): ensure that intermediate moves are done for all drags
[...]
In practice, this means that mouseDrag() never did intermediate moves
(i.e. what happens during a drag in real life) for drags that go from
right to left or upwards.
https://codereview.qt-project.org/c/qt/qtdeclarative/+/281903
If that doesn't help, or upgrading is not an option, I would recommend looking at Qt's own tests (although they are written in C++):
https://code.qt.io/cgit/qt/qtdeclarative.git/tree/tests/auto/quick/qquickflickable/tst_qquickflickable.cpp#n1150
I think mouseDrag only works for mouse area. You could wrap every object with that.
But in the end, you need to use a mouse area inside you delegate and Drag and Drop it.
https://doc.qt.io/qt-5/qml-qtquick-drag.html
import QtQuick 2.5
FocusScope {
width: 1920
height: 1080
Flickable {
objectName: 'flickableList'
boundsBehavior: Flickable.StopAtBounds
clip: true
width: parent.width
height: 240
contentHeight: 500
ListView {
interactive: false
height: parent.height
width: parent.width
model: ['example1', 'example2', 'example3', 'example4', 'example5']
delegate: DelegateList{
textAreaText = modelData
}
}
}
Item {
id: footer
height: 100
width: parent.width
}
}
And the DelegateList.qml
Item {
id: root
property alias textAreaText: textArea.text
width: 300
height: 100
Text {
id: textArea
}
Drag.active: dragArea.drag.active
Drag.hotSpot.x: 10
Drag.hotSpot.y: 10
MouseArea {
id: dragArea
anchors.fill: parent
drag.target: parent
}
}

How to make Flickable ensure the visibility of an item inside of it?

I have a Flickable that includes a large number of TextField objects laid out in a column with each TextField anchored to the bottom on the previous TextField. Everything is working fine except that when I use the tab key to navigate through these fields, eventually the focus goes to a TextField that is outside the visible rectangle of the Flickable and then the user can't see the cursor until they scroll down the Flickable manually.
Essentially I'm looking for some kind of ".ensureVisible()" method such that when a TextField receives the focus, the Flickable is automatically scrolled so that the just-focused TextField is entirely visible.
Have you considered a more model-ar approach to this? I mean if you use something like a ListView you can simply change the currentItem at which point the view will automatically scroll to it if it is out of the visible range.
Additionally, it will only load the text elements that are in the visible range, saving on some memory.
But even with your current approach it won't be that complex to ensure visibility.
Flickable {
id: flick
anchors.fill: parent
contentHeight: col.height
function ensureVisible(item) {
var ypos = item.mapToItem(contentItem, 0, 0).y
var ext = item.height + ypos
if ( ypos < contentY // begins before
|| ypos > contentY + height // begins after
|| ext < contentY // ends before
|| ext > contentY + height) { // ends after
// don't exceed bounds
contentY = Math.max(0, Math.min(ypos - height + item.height, contentHeight - height))
}
}
Column {
id: col
Repeater {
id: rep
model: 20
delegate: Text {
id: del
text: "this is item " + index
Keys.onPressed: rep.itemAt((index + 1) % rep.count).focus = true
focus: index === 0
color: focus ? "red" : "black"
font.pointSize: 40
onFocusChanged: if (focus) flick.ensureVisible(del)
}
}
}
}
The solution is quick and cruddy, but it will be trivial to put it into production shape. It is important to map to the contentItem rather than the flickable, as the latter would give the wrong results, taking the amount of current scrolling into account. Using mapping will make the solution agnostic to whatever positioning scheme you might be using, and will also support arbitrary levels of nested objects.
dtech's answer is spot on.
It's easily combined with a nice snap-to animation and easy to modify for x-direction flickables too. Also, the user may be deliberately flicking or dragging the flickable. In my case, C++ code was controlling the text or display effects for the items in a grid layout, contained in the flickable. The flickable needed to flick nicely when the C++ code signalled it to do so, but not if the user was deliberately dragging or flicking. Here is dtech's function modified for an x-direction flickable:
function ensureVisible(item) {
if (moving || dragging)
return;
var xpos = item.mapToItem(contentItem, 0, 0).x
var ext = item.width + xpos
if ( xpos < contentX // begins before
|| xpos > contentX + width // begins after
|| ext < contentX // ends before
|| ext > contentX + width) { // ends after
// don't exceed bounds
var destinationX = Math.max(0, Math.min(xpos - width + item.width, contentWidth - width))
ensureVisAnimation.to = destinationX;
ensureVisAnimation.from = contentX;
ensureVisAnimation.start();
}
}
//This animation is for the ensure-visible feature.
NumberAnimation on contentX {
id: ensureVisAnimation
to: 0 //Dummy value - will be set up when this animation is called.
duration: 300
easing.type: Easing.OutQuad;
}

How can I get the drop effect of a DragArea to animate towards the DropArea that received it?

Hope this makes some sense as a question. In my app, I have a DragArea defined which I use to start dragging things over top of various Rectangles that each contain a DropArea. Everything is working fine in my code except for a cosmetic effect that I would like to change.
In QML, when you start dragging from a DragArea and eventually drop, the animation effect is such that the thing you're dragging animates (while fading out) back to the spot from which you started dragging. This happens even when you drop over a DropArea that successfully captures the drop.
What I would like to do is have the drop effect animate towards the DropArea that received the drop - so that it appears I am dragging-and-dropping things into the Rectangle. Is there any way to do this?
I'm guessing that this in some way involves the .source and .target properties of these areas, but no luck so far in having any effect on where the drop animation goes.
By default, QML will give you no cosmetic behavior for drag and drop whatsoever. The drag target will begin at the drag start location, and will end wherever it is dropped, regardless of whether the drag is accepted or not.
Thus I assume the behavior you describe is implemented in your user code, which you have not disclosed. Regardless, what you want to do is quite easy, it involves tracking the position the drag originates at and it ends at, so you can use the two coordinates to animate the position.
In the following example the red rectangle can be dragged, and if dropped outside of a drop area it will animate from its current to its initial position, whereas if dropped in the yellow rectangle, it will animate from its initial to its drop position.
Window {
width: 600
height: 600
visible: true
Rectangle {
width: 200
height: 200
color: "yellow"
DropArea {
anchors.fill: parent
onEntered: drag.source.accepted = true
onExited: drag.source.accepted = false
}
}
Rectangle {
id: rect
width: 50
height: 50
color: "red"
x: parent.width * 0.5
y: parent.height * 0.5
Drag.active: mouseArea.drag.active
property point begin
property point end
property bool accepted : false
MouseArea {
id: mouseArea
anchors.fill: parent
drag.target: parent
onPressed: rect.begin = Qt.point(rect.x, rect.y)
onReleased: {
rect.end = Qt.point(rect.x, rect.y)
aX.from = rect.accepted ? rect.begin.x : rect.end.x
aX.to = rect.accepted ? rect.end.x : rect.begin.x
aY.from = rect.accepted ? rect.begin.y : rect.end.y
aY.to = rect.accepted ? rect.end.y : rect.begin.y
anim.start()
}
ParallelAnimation {
id: anim
NumberAnimation { id: aX; target: rect; property: "x"; duration: 200 }
NumberAnimation { id: aY; target: rect; property: "y"; duration: 200 }
}
}
}
}

How to use QML Scale Element for incremental scaling with different origin

I'm trying to use a QML Scale Element to perform view scaling around a point clicked by the user, but it's not always working as documented.
To reproduce the problem, run the minimal QML example below (I'm using Qt 5.3.1 on Ubuntu 14.04 x86_64) and then:
Click in the center of the blue rectangle at the top left.
See that everything is scaled up, but the center of the blue rectangle remains at your click location. This is as documented in http://doc.qt.io/qt-5/qml-qtquick-scale.html - "[The origin] holds the point that the item is scaled from (that is, the point that stays fixed relative to the parent as the rest of the item grows)."
Now click in the center of the red rectangle.
See that everything is scaled up, but the center of the red rectangle did not remain at your click point, it was translated up and to the left. This is not as documented.
My goal is to have it always zoom correctly maintaining the click point as the origin, as stated in the documentation.
P.S. Interestingly, if you now click again in the center of the red rectangle, it scales up around that point as promised. Clicking again now on the center of the blue rectangle, you see the same unexpected translation behaviour.
P.P.S. I'm working on an application where the user can mouse-wheel / pinch anywhere on the containing rectangle, and everything inside should scale up or down around the mouse / pinch position. Many applications have exactly this behaviour. See for example inkscape.
import QtQuick 2.2
import QtQuick.Controls 1.1
ApplicationWindow {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
Rectangle {
x: 100
y: 100
width: 300
height: 300
transform: Scale {
id: tform
}
MouseArea {
anchors.fill: parent
onClicked: {
console.log(mouse.x + " " + mouse.y)
tform.xScale += 0.5
tform.yScale += 0.5
tform.origin.x = mouse.x
tform.origin.y = mouse.y
}
}
Rectangle {
x: 50
y: 50
width: 50
height: 50
color: "blue"
}
Rectangle {
x: 100
y: 100
width: 50
height: 50
color: "red"
}
}
}
(I filed this as a Qt bug, because the behaviour does not follow the documentation. At the moment of writing, the bug seems to have been triaged as "important". https://bugreports.qt.io/browse/QTBUG-40005 - I'm still very much open to suggestions of work-arounds / fixes over here)
In fact it is not a deviant behavior, just a different one from what you may expect after reading the doc.
When you change the Scale transform of your rectangle, the transformation is applied on the original rectangle. The point you click on stay in the same place from the original rectangle point of view.
That's why your rectangle "moves" so much when you click on one corner then the opposite corner.
In order to achieve what you want, you can't rely on transform origin. You have to set the actual x y coordinates of your rectangle.
Here's a working example:
ApplicationWindow {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
Rectangle {
id: rect
x: 100
y: 100
width: 300
height: 300
Rectangle {
x: 50
y: 50
width: 50
height: 50
color: "blue"
}
Rectangle {
x: 100
y: 100
width: 50
height: 50
color: "red"
}
transform: Scale {
id: tform
}
MouseArea {
anchors.fill: parent
property double factor: 2.0
onWheel:
{
if(wheel.angleDelta.y > 0) // zoom in
var zoomFactor = factor
else // zoom out
zoomFactor = 1/factor
var realX = wheel.x * tform.xScale
var realY = wheel.y * tform.yScale
rect.x += (1-zoomFactor)*realX
rect.y += (1-zoomFactor)*realY
tform.xScale *=zoomFactor
tform.yScale *=zoomFactor
}
}
}
}

QML NumberAnimation.duration behaviour

I'm trying to make a player move smoothly towards a destination in QML. I'm using a NumberAnimation to animate the x,y position changes. The NumberAnimation's duration should be proportional to the distance the player has to travel, so that the player moves at the same speed regardless of how far away they are from their destination.
import QtQuick 1.1
Item {
width: 720
height: 720
MouseArea {
anchors.fill: parent
onClicked: {
var newXDest = mouse.x - player.width / 2;
var newYDest = mouse.y - player.height / 2;
var dist = player.distanceFrom(newXDest, newYDest);
// Make duration proportional to distance.
player.xMovementDuration = dist; // 1 msec per pixel
player.yMovementDuration = dist; // 1 msec per pixel
console.log("dist = " + dist);
player.xDest = newXDest;
player.yDest = newYDest;
}
}
Rectangle {
id: player
x: xDest
y: yDest
width: 32
height: 32
color: "blue"
property int xDest: 0
property int yDest: 0
property int xMovementDuration: 0
property int yMovementDuration: 0
function distanceFrom(x, y) {
var xDist = x - player.x;
var yDist = y - player.y;
return Math.sqrt(xDist * xDist + yDist * yDist);
}
Behavior on x {
NumberAnimation {
duration: player.xMovementDuration
// duration: 1000
}
}
Behavior on y {
NumberAnimation {
duration: player.yMovementDuration
// duration: 1000
}
}
}
Rectangle {
x: player.xDest
y: player.yDest
width: player.width
height: player.height
color: "transparent"
border.color: "red"
}
}
My problem can be demonstrated by running the application above and following these steps:
Click on the bottom right hand corner of the screen.
Immediately click in the centre (or closer towards the top left) of the screen.
On the second click (while the rectangle is still moving), it seems that the rectangle's number animation is stopped (which is what I want) but it assumes the position of the destination (not what I want). Instead, I want the animation to stop and the rectangle to assume the position at which it was stopped, then to continue on to the new destination.
The correct behaviour - ignoring that the movement speed becomes disproportional - can be seen by setting both of the NumberAnimation.durations to 1000.
I think that you are looking for SmoothedAnimation. There are only two types of animation that deal nicely with the destination changing before the animation is completed. That is SmoothedAnimation and SpringAnimation. Both of these use the current position and velocity to determine the position in the next frame. Other animation types move the position along a predetermined curve.
Simply changing NumberAnimation to SmoothedAnimation makes your example look correct to me.

Resources