Attach and detach a Rectangle to a moving Rectangle in QML - qt

I want to implement in qml that as long as I hold down a button, the two rectangles move together, but as soon as I release it, one of the rectangles stays where it was at the moment of release.
Let the moving Rectangle be the master and its code be as follows:
Rectangle {
id: master
x: 10
y: 10
width: 50
height: 50
color: "#00ff00"
Behavior on x {
NumberAnimation {
duration: 2000
}
}
}
The code of the slave that should move with the master as long as the button is held down is as follows:
Rectangle {
id: slave
x: 100
y: 100
width: 50
height: 50
color: "#ff0000"
}
Currently I've made the following attempt to attach and detach the slave to the master:
ToggleButton {
id: attach
x: 300
y: 300
text: qsTr("Attach")
onClicked: {
if (checked) {
slave.parent = master
} else {
slave.parent = mainWindow
}
}
}
The problem is that if the master is already moving, pressing the button will jump the slave to the position it would have been if it had moved with the master from the beginning, then continues to move with the master even after the button is released.
How can I arbitrarily attach other objects to moving objects and then detach them in qml?

Your approach of attaching the slave as a child of master is what is causing the jump. As soon as the slave is reparented, its x and y of 100 place it there in relation to its new parent master, not where it was in relation to mainWindow.
When you reparent it, you need to set slave's x and y relative to master to be the translation of the x and y from the mainWindow's coordinate system into master's coordinate system. Look at Item::mapFromItem and Item::mapToItem to see how to translate the slave's x and y of 100 in the context of mainWindow to the same physical position on the screen but in terms of the new parent master.
Note, you will need to do the same thing in reverse when you stop the drag and reparent the slave back to the mainWindow.
Here's a shot at some code updates:
ToggleButton {
id: attach
x: 300
y: 300
text: qsTr("Attach")
onClicked: {
if (checked) {
var newPoint = mainWindow.contentItem.mapToItem(master, slave.x, slave.y);
slave.parent = master;
slave.x = newPoint.x;
slave.y = newPoint.y;
} else {
var newPoint = master.mapToItem(mainWindow.contentItem, slave.x, slave.y);
slave.parent = mainWindow.contentItem;
slave.x = newPoint.x;
slave.y = newPoint.y;
}
}
}

Related

QML mapToGlobal() relative to screen or application window

I have encountered some strange behaviour.
An item's mapToGlobal() or mapFromGlobal() method, does not seem to be consistent in regard to what the coordinates is relative to.
For some items the position is relative to the application window.
For other item the position is relative to my screen. If I move the application window on the screen they will be different.
Below is a simplification example of my code (I actually have multiple components and signals to decide which component that should be loaded).
MyType.qml:
Item{
Button{
onClicked: {
loader.sourceComponent = component; // Different positions
}
}
Loader{
id: loader
anchors.fill: parent
}
Component{
id: component
Rectangle{
Component.onCompleted: {
var point = mapFromGlobal(0, 0);
console.log("temp 2: ", point.x, point.y);
}
}
}
Component.onCompleted: {
//loader.sourceComponent = component; // Same positions
var point = mapFromGlobal(0, 0);
console.log("temp 1: ", point.x, point.y);
}
}
main.qml
ApplicationWindow {
id: appWindow
visible: true
width: 600
height: 400
MyType{
anchors.fill: parent
}
}
The resulting output is
temp 1: 0 0
temp 2: -199 -85
Does anyone know why the positions are sometimes relative to screen and sometimes the application window? Or knows of another explanation for this strange behaviour?
Edit:
If I load the component directly in Component.onCompleted (outcommented in sample code), then both outputs are 0 0. Unfortunately this didn't get me closer to an explanation, except it has something to do with the Loader elements.

In MouseArea.onEntered, detect if the cause is only that the *MouseArea* moved and came to be under the cursor

In MouseArea.onEntered, can I detect if the cause for the event firing is only that the MouseArea moved and came to be under the cursor, rather than the other way round?
I thought of doing something like this: (pseudocode)
MouseArea {
// ...
property bool positionDirty = false
Connections {
target: window
onAfterRendering: {
positionDirty = false;
}
}
onMouseAreaPosChanged: {
positionDirty = true;
}
onEntered: {
if(positionDirty) {
positionDirty = false;
return;
}
// handle event here
}
}
But this makes the assumption that entered will be fired after mouseAreaPosChanged, and before window.afterRendering. And I'm not confident in that assumption.
Also, it doesn't work when an ancestor of the MouseArea moves, or when the MouseArea is positioned/sized via anchoring.
Assumptions:
This only affects the edge case, that both, the cursor and the MouseArea are moving.
My Assumption here is, that the movement of the cursor is handled before the movement of the MouseArea. I don't have any definite proof for this. Only my test with the solution below, suggests that.
Solution
The first challenge is to detect movement of the MouseArea. It might be that it moves, without its own x and y-values changing, e.g. if its parent is moving.
To solve this, I'll introduce two properties globalX and globalX. Then I use the trick from this answer on how to track a gobal position of an object.
Now I'll have two signals to handle: globalXChanged and globalYChanged.
According to my assumption, they are fired, after the mouseXChanged and mouseYChanged. I will use a flag isEntered to make sure, I only handle one of them, by setting it to true, if the first of them is fired.
I will use the cursor position on a globalMouseArea to determine, whether the cursor is within bounds of the MouseArea. This requires, the cursor is not in some other MouseArea at that time, or at least I know of it
-> With this I already succeeded in detecting the entrance.
The second challenge is to detect the exit. Here we have 4 cases to distinguish:
Cursor enters and leaves the MouseArea because of it's movement.
Cursor enters and leaves the MouseArea because of the movement of the MouseArea
Cursor enters because the MouseArea moves, and leaves, because the cursor moves
Cursor enters because it moves, and leaves as the MouseArea moves away.
The first would be easy to handle. After it enters we handle entered and when it leaves we handle exited. But after the fix, mentioned by Mitch we can't rely on this anymore.
So we will not set hoverEnabled: true and map the position of the cursor to the targetMouseArea whenever either the cursor moves, or the targetMouseArea moves, and act accordingly.
import QtQuick 2.7
import QtQuick.Controls 2.0
ApplicationWindow {
id: root
visible: true
width: 400; height: 450
MouseArea {
id: globalMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: ani.restart()
}
Rectangle {
x: 300
y: 300
width: 50
height: 50
color: 'green'
}
Rectangle {
id: rect
width: 50
height: 50
color: 'red'
Text {
text: targetMouseArea.isEntered.toString()
}
MouseArea {
id: targetMouseArea
anchors.fill: parent
signal enteredBySelfMovement
signal enteredByMouseMovement
onEnteredByMouseMovement: console.log('Cause: Mouse')
onEnteredBySelfMovement: console.log('Cause: Self')
property point globalPos: {
var c = Qt.point(0, 0)
var itm = this
for (; itm.parent !== null; itm = itm.parent) {
c.x += itm.x
c.y += itm.y
}
return c
}
property bool isEntered: false
function checkCollision(sig) {
if ((globalPos.y < globalMouseArea.mouseY)
&& (globalPos.y + height > globalMouseArea.mouseY)
&& (globalPos.x < globalMouseArea.mouseX)
&& (globalPos.x + width > globalMouseArea.mouseX)) {
if (!isEntered) {
isEntered = true
sig()
}
}
else if (isEntered && !containsMouse) {
console.log(isEntered = false)
}
}
onGlobalPosChanged: {
checkCollision(enteredBySelfMovement)
}
Connections {
target: globalMouseArea
onPositionChanged: {
targetMouseArea.checkCollision(targetMouseArea.enteredByMouseMovement)
}
}
}
}
NumberAnimation {
id: ani
target: rect
properties: 'x,y'
from: 0
to: 300
running: true
duration: 10000
}
}
Problems left: When we clicked within the targetMouseArea, as long as a button is pressed, we won't detect the leaving.
You can check whether mouseX, mouseY were changed since last event.
property int previousMouseX = mouseX; // or use other values to init
property int previousMouseY = mouseY; // e.g., 0, x, parent.x,
// or set it from extern
onEntered() {
if (mouseX != previousMouseX || mouseY != previousMouseY) {
// TODO do something
previousMouseX = mouseX;
previousMouseY = mouseY;
}
}
In case mouseX, mouseY are relative to the mouse area 0,0 you can use mapFromItem(null, 0, 0) to get the absolute values.

QML ListView - change all but current item

I'm following this tutorial (without the flickable content in each entry) for Qt 4.8 while using Qt 5.7 with QtQuick 2.0. The way the ListView there works is as follows:
User clicks on item in list
Alternative (detailed) view of item is displayed
User has to click on Close button in detailed view to reset the state of entry to its default compact view.
This leads to a clutter where at some point if the user clicks on all items in which case all will be shown in their full view. Having the user click on the Close button every time he/she opens a detailed view also is (omho) not that handy.
I've altered the entry to close when the user clicks on the view. I'm also trying to prevent this clutter and achieve a more (omho) flowing behaviour:
User clicks on item in list
Alternative view of item is displayed
User clicks on detailed view to reset state of entry to its default compact view OR
User clicks on another entry and all currently in detailed view entries are reset to their compact view
Currently I'm looping through my ListView's contentItem.children[loop_index] and setting the state to "" ("Details" = show detailed view | "" = show compact view). Due to the way ListView works (loading/unloading delegates on demand) this is quite unreliable and I often get an undefined reference when I try to access the state of other delegates. The following MouseArea, which I'm using to do all that, is part of every delegate:
// state is a QML `State` that is bound to the delegate (see below for the details on it)
MouseArea {
anchors.fill: background
onClicked: {
// Iterate through all other entries and close them
for (var entry = 0; entry < listView.count; ++entry) {
if(listView.contentItem.children[entry] !== gestureEntry) {
console.log("Hide other element");
listView.contentItem.children[entry].state = ""; // IT FAILS HERE (SOMETIMES)
}
}
// Change view of current entry
if(gestureEntry.state === "Details") {
gestureEntry.state = "";
console.log("Hiding details")
}
else {
gestureEntry.state = "Details";
console.log("Showing details");
}
}
}
with state being a delegate's state:
states: State {
name: "Details"
PropertyChanges { target: background; color: "white" }
PropertyChanges { target: gestureImage; width: 130; height: 130 } // Make picture bigger
PropertyChanges { target: gestureEntry; detailsOpacity: 1; x: 0; y: 0 } // Make details visible
PropertyChanges { target: gestureEntry; height: listView.height } // Fill the entire list area with the detailed view
}
I'm thinking that the state information can be stored inside the ListModel itself making it possible to iterate through the model's contents (which are always there unlike the contents of the delegates) however I don't know how to automatically update my list (and the currently visible/invisible delegates) when an entry changes in the model. From what I've found so far it seems not possible to do that since the ListView doesn't actively monitor its ListModel.
Is this indeed the case? If yes, then is it possible to go around this problem in a different way?
Why don't you use the currentIndex property of your ListView?
Just modify your delegate like this:
Item {
id: gestureEntry
...
state: ListView.isCurrentItem?"Details":""
...
MouseArea {
anchors.fill: background
onClicked: {
if(listView.currentIndex == index)
listView.currentIndex = -1
else
listView.currentIndex = index
}
}
}
EDIT:
The only issue with the solution above is that - upon loading - an entry in the ListView is preselected which automatically triggers the detailed view of that entry. In order to avoid that the following needs to be added to listView:
Component.onCompleted: {
listView.currentIndex = -1;
}
This ensures that no entry will be preselected.
guess it is an issue because you stored a state in your delegate. You should not do this as described in the delegate-property (Link), because the delegates get reused when they get out of view.
At least you should use a when: ListView.isCurrentItem in the State and depend on a value of the ListView. So only your current delegate is maximized. Then in the MouseArea only set `ListView.view.currentIndex = index'. Don't change the state manually in the function!
I ran in the same trouble, removed the states completely and just used the attached property ListView.isCurrentItem. But binding the state to a Value from the ListView should also work, because it's not stored in the delegate.
Minimal example:
import QtQuick 2.0
Item {
width: 800
height: 600
ListView {
id: view
anchors.fill: parent
model: 3
spacing: 5
currentIndex: -1
delegate: Rectangle {
id: delegate
color: ListView.isCurrentItem ? "lightblue" : "green" // directly change properties depending on isCurrentItem
height: 100
width: 100
states: State {
name: "maximized"
when: delegate.ListView.isCurrentItem // bind to isCurrentItem to set the state
PropertyChanges {
target: delegate
height: 200
}
}
MouseArea {
anchors.fill: parent
//onClicked: delegate.ListView.view.currentIndex = model.index // if only selection is wanted
onClicked: {
//console.debug("click");
if (delegate.ListView.isCurrentItem)
{
delegate.ListView.view.currentIndex = -1;
}
else
{
delegate.ListView.view.currentIndex = model.index;
}
}
}
Text {
anchors.centerIn: parent
text: index
}
}
Text {
text: "CurrentIndex: " + parent.currentIndex
}
}
}

propagateComposedEvents: mouse data not accurate?

Update: Isn't it often the way: you ask a question and then discover the answer on your own a short time later.
It seems I have some confusion between referencing mouseX and mouseY in a MouseArea and getting the mouse data from the MouseEvent
If I change my code below to:
var pt = Qt.point( mouse.x, mouse.y)
from what I had:
var pt = Qt.point(mouseX, mouseY)
Then the newly created sprites are located at the click point. That's great but I am still not understanding the difference, particularly in this case since the MouseArea fills the entire window (parent).
The mouse data is the same in either approach, unless the mouse event has been propagated – then the mouse approach gives the correct data while mouseX, mouseY does not. That is the part that is confusing me.
Can anyone explain the difference in what is going on here?
I have made a reusable QML component which can load .png images with an alpha channel and handle clicks on transparent pixels by propagating the mouse event. I've got it working in the code below (any suggestions for improvement much welcomed) but it seems the mouse data is wrong.
In the image below I have marked the order and location of 3 clicks. In the log statements even though the mouse click position has changed, the reported position stays the same. So what is occurring is that although 3 sprites have been created, they are stacking on top of each other
Am I missing something about how a MouseArea reports the mouse position or how propagateComposedEvents works?
main clicked. Creating sprite at: 598.01953125 492.953125
graphic alpha clicked: 5.69921875 103.41015625 <----- EVENT PASSED THROUGH
main clicked. Creating sprite at: 598.01953125 492.953125
graphic alpha clicked: 121.953125 103.01953125 <----- EVENT PASSED THROUGH
graphic alpha clicked: 5.69921875 103.41015625 <----- EVENT PASSED THROUGH
main clicked. Creating sprite at: 598.01953125 492.953125
// main.qml
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Dialogs 1.2
ApplicationWindow {
id: appWindow
visible: true
width: 1024
height: 768
title: qsTr("QtQuick")
Item {
id: container
anchors.fill: parent
property var events: new Array()
property int counter: 0;
MouseArea {
anchors.fill: parent
onClicked: {
console.log("---------main clicked. Creating sprite at:", mouseX, mouseY);
var pt = Qt.point(mouseX, mouseY)
var component = Qt.createComponent("graphicAsset.qml");
var imageName = "Earth-icon.png";
var sprite = component.createObject(container, {"x": pt.x, "y": pt.y, "imageSource": imageName});
}
}
}
}
//graphicAsset.qml
import QtQuick 2.5
Canvas {
id: graphicAsset
width: 50
height: 50
property string imageSource;
onImageSourceChanged:loadImage(imageSource)
onImageLoaded: {
var ctx = getContext("2d")
var imageData = ctx.createImageData(imageSource)
graphicAsset.width = imageData.width;
graphicAsset.height = imageData.height;
x = x - (imageData.width /2);
y = y - (imageData.height /2);
requestPaint();
}
onPaint: {
if (isImageLoaded(imageSource)){
var ctx = getContext("2d")
var imageData = ctx.createImageData(imageSource)
ctx.drawImage(imageData, 0, 0)
}
}
MouseArea {
anchors.fill: parent
drag.target: parent
propagateComposedEvents: true
onClicked: {
var ctx = parent.getContext("2d")
var imageData = ctx.getImageData(mouseX, mouseY, 1, 1)
if (imageData.data[3] == 0 ){
console.log("graphic alpha clicked:", mouseX, mouseY);
mouse.accepted = false;
} else {
mouse.accepted = true;
}
}
}
}
Mouse events' x and y positions are relative to the MouseArea that generated the event, as well as the coordinates of the mouse cursor within the same area (named mouseX and mouseY).

QML: public variable

I have two QML files.
In First.qml I can make visible Second.qml. In Second.qml I have selectedParts variable.
I want to set selectedParts to value 1 always, when I make Second.qml visible. That works only when I load
Second.qml for first time. If I make Second.qml invisible and then visible, selectedParts value is 2. Is there anyway
to make selectedParts variable public and set its value always when I click on myImage?
First.qml
Item {
Image {
id: myImage
MouseArea{
anchors.fill: parent
onClicked: {
second.visible = true
...
}
}
}
}
Second.qml
Item {
property int selectedParts: 1
Image {
id: myImage2
MouseArea{
anchors.fill: parent
onClicked: {
selectedParts = 2
...
}
}
}
}
QML public variable? Look up for MessageBoard in Defining QML types from C++. We are using that approach. All you need is to create C++ MessageBoard object, put some data in there and reference it via the QML context given to every QML root object:
m_quickView.engine()->rootContext()->setContextProperty("myMsgBoard", MyQmlMsgBoard::instance());
And in QML:
Rectangle {
id: topRect
scale: myMsgBoard.scale // or anywhere in QML
// ....
}
Of course that "message board" C++ object exposes to QML something like:
Q_PROPERTY(qreal scale READ scale CONSTANT);
I solved my problem by adding back button into the Second.qml file. And in this button I put statement selectedParts = 1.

Resources