QtQuick Grid, mouse events no propagating over adjacent mouse areas - qt

I've written this simple qml app that allows to paint pixels over a grid:
import QtQuick 2.12
import QtQuick.Window 2.12
Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
Grid {
id: grid
anchors.fill: parent
rows: 32
columns: 64
Repeater {
model: grid.columns * grid.rows;
delegate: delegateGridImage
}
}
Component {
id: delegateGridImage
Item {
id: gridItem
property int currentColumn: index % grid.columns
property int currentRow: Math.floor(index / grid.rows);
// Resize to screen size
width: grid.width / grid.columns
height: grid.height / grid.rows
Rectangle {
id: pixel
anchors.fill: parent
property bool pixel_state: true
color: if (pixel_state == true ) { "white" } else { "black" }
MouseArea {
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onEntered: console.log(index)
onPressed: pixel.pixel_state ^= true
}
}
}
}
}
This works fine:
I would like to be able to paint multiple pixels with a single mouse click pressed.
I've tried the onEntered event, but it only listens to the active mouse area until the click button is released. Is there a way to not block the events from the other mouse areas?

You can use a global MouseArea and deduct the current item below the cursor via childAt(...) of the Grid.
Window {
... // remove the MouseArea of pixel
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
property bool pixel_activate: true
onPressed: {
var child = grid.childAt(mouse.x, mouse.y)
child.pixel_state ^= true
pixel_activate = child.pixel_state
}
onPositionChanged: {
if (!pressed) return;
var child = grid.childAt(mouse.x, mouse.y)
child.pixel_state = pixel_activate
}
}
}
You just have to decide what action you want to perform once you hold the button pressed (currently it performs first action activate/deactivate and all following). Also take a look at the MouseEvent passed by the signals pressed and positionChanged so you can differentiate what key was pressed.

Lasall's solution works great. This is the final result:
import QtQuick 2.12
import QtQuick.Window 2.12
Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
Grid {
id: grid
anchors.fill: parent
rows: 32
columns: 64
Repeater {
model: grid.columns * grid.rows;
delegate: delegateGridImage
}
}
Component {
id: delegateGridImage
Item {
id: gridItem
property int currentColumn: index % grid.columns
property int currentRow: Math.floor(index / grid.rows);
property bool pixel_state: false
// Resize to screen size
width: grid.width / grid.columns
height: grid.height / grid.rows
Rectangle {
id: pixel
anchors.fill: parent
color: if (gridItem.pixel_state == true ) { "white" } else { "black" }
}
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
property bool pixel_activate: true
onPressed: {
var child = grid.childAt(mouse.x, mouse.y)
child.pixel_state ^= true
pixel_activate = child.pixel_state
}
onPositionChanged: {
if (!pressed) return;
var child = grid.childAt(mouse.x, mouse.y)
child.pixel_state = pixel_activate
}
}
}

Related

Qt Select At Most 1 Marker on Map

In my code every marker that I clicked are selected(turn into green from red). I want just 1 can change. When I click another marker the marker I clicked before turns red again. Or When I click an empty area the marker I clicked before turns red again.
In qml my Item's code:
Component {
id: hazardous_img
MapQuickItem {
id: hazardousitem
anchorPoint.x: image.width/4
anchorPoint.y: image.height
coordinate: position
property bool isClicked: false
MouseArea {
anchors.fill: parent
onDoubleClicked: {
mainwindow.hazardousIconClicked(mapview.toCoordinate(Qt.point(mouse.x,mouse.y)))
}
onClicked: {
if (isClicked === false) {
image.source = "qrc:/grn-pushpin.png"
isClicked = true
} else {
image.source = "qrc:/red-pushpin.png"
isClicked = false
}
}
}
sourceItem: Image {
id: image
source: "qrc:/red-pushpin.png"
}
}
}
In QML this is usually done with using a ButtonGroup, but as you're not using AbstractButtons you need to write it yourself. Here is my solution for it.
I've used the ListModel to not only store the coordinates of each marker, but also a selected flag which is set to false by default. In the delegate I'm using the selected data role to show if a marker is selected or not.
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtLocation 5.15
import QtPositioning 5.15
ApplicationWindow {
id: window
width: 640
height: 480
visible: true
title: qsTr("Map")
ListModel { id: markerModel }
Plugin {
id: mapPlugin
name: "osm"
}
Map {
id: map
anchors.fill: parent
plugin: mapPlugin
center: QtPositioning.coordinate(59.91, 10.75) // Oslo
zoomLevel: 14
MouseArea {
anchors.fill: parent
onDoubleClicked: {
var coordinate = map.toCoordinate(Qt.point(mouse.x, mouse.y))
var jsonObject = JSON.parse(JSON.stringify(coordinate))
jsonObject["selected"] = false
markerModel.append(jsonObject)
}
onClicked: map.deselectAll()
}
MapItemView {
model: markerModel
delegate: markerDelegate
}
function deselectAll() {
for (var i = 0; i < markerModel.count; ++i)
markerModel.setProperty(i, "selected", false)
}
Component {
id: markerDelegate
MapQuickItem {
id: markerItem
required property int index
required property real latitude
required property real longitude
required property bool selected
anchorPoint.x: waypointMarker.width / 2
anchorPoint.y: waypointMarker.height / 2
coordinate: QtPositioning.coordinate(latitude, longitude)
sourceItem: Rectangle {
id: waypointMarker
width: 20
height: 20
radius: 20
border.width: 1
border.color: mouseArea.containsMouse ? "red" : "black"
color: markerItem.selected ? "red" : "gray"
}
MouseArea {
id: mouseArea
hoverEnabled: true
anchors.fill: parent
onClicked: {
map.deselectAll()
markerModel.setProperty(markerItem.index, "selected", true)
}
}
}
}
}
}
I came up with yet another solution without looping over all items in the model. It just stores the index of the selected marker in a dedicated property. This has the drawback that if the model order changes the index can become invalid, also potential multi selection is hard to handle, but on the other hand it is faster because it doesn't need to iterate over all items.
I experimented a lot with DelegateModel, it seems to be a perfect match if one could use it in combination with MapItemView, because of the groups and the attached properties like inGroupName.
After that I've tried ItemSelectionModel, but it seems it is only intended to be used in combination with a view, e.g. TreeView. I couldn't find out how to generate a QModelIndex in QML without a TreeView.
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtLocation 5.15
import QtPositioning 5.15
ApplicationWindow {
id: root
width: 640
height: 480
visible: true
title: qsTr("Map")
property int selectedMarker: -1
Map {
id: map
anchors.fill: parent
plugin: Plugin {
id: mapPlugin
name: "osm"
}
center: QtPositioning.coordinate(59.91, 10.75) // Oslo
zoomLevel: 14
MouseArea {
anchors.fill: parent
onDoubleClicked: {
var coordinate = map.toCoordinate(Qt.point(mouse.x, mouse.y))
markerModel.append(JSON.parse(JSON.stringify(coordinate)))
}
onClicked: root.selectedMarker = -1
}
MapItemView {
model: ListModel { id: markerModel }
delegate: markerDelegate
}
Component {
id: markerDelegate
MapQuickItem {
id: markerItem
required property int index
required property real latitude
required property real longitude
anchorPoint.x: waypointMarker.width / 2
anchorPoint.y: waypointMarker.height / 2
coordinate: QtPositioning.coordinate(latitude, longitude)
sourceItem: Rectangle {
id: waypointMarker
width: 20
height: 20
radius: 20
border.width: 1
border.color: mouseArea.containsMouse ? "red" : "black"
color: markerItem.index === root.selectedMarker ? "red" : "gray"
}
MouseArea {
id: mouseArea
hoverEnabled: true
anchors.fill: parent
onClicked: root.selectedMarker = markerItem.index
}
}
}
}
}
I promise this is the last answer on that question.
This one is using an ItemSelectionModel and a few undocumented functions, e.g. ListModel.index(row, col).
itemSelectionModel.hasSelection is used in the color binding to trigger a reevaluation in order to call isRowSelected and set the color accordingly whenever the selection has changed.
If the user clicks on the background the clear() is called to clear the selection.
I think out of the three this is the best solution. It can be easily upgraded to allow multi selection as shown below. Also the ItemSelectionModel can be used by other views to show the data and selection.
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtLocation 5.15
import QtPositioning 5.15
import QtQml.Models 2.15
ApplicationWindow {
id: root
width: 640
height: 480
visible: true
title: qsTr("Map")
Map {
id: map
anchors.fill: parent
plugin: Plugin {
id: mapPlugin
name: "osm"
}
center: QtPositioning.coordinate(59.91, 10.75) // Oslo
zoomLevel: 14
MouseArea {
anchors.fill: parent
onDoubleClicked: function(mouse) {
markerModel.append(map.toCoordinate(Qt.point(mouse.x, mouse.y)))
}
onClicked: itemSelectionModel.clear()
}
MapItemView {
model: ListModel { id: markerModel }
delegate: markerDelegate
}
ItemSelectionModel {
id: itemSelectionModel
model: markerModel
}
Component {
id: markerDelegate
MapQuickItem {
id: markerItem
required property int index
required property real latitude
required property real longitude
anchorPoint.x: waypointMarker.width / 2
anchorPoint.y: waypointMarker.height / 2
coordinate: QtPositioning.coordinate(latitude, longitude)
sourceItem: Rectangle {
id: waypointMarker
width: 20
height: 20
radius: 20
border.width: 1
border.color: mouseArea.containsMouse ? "red" : "black"
color: {
itemSelectionModel.hasSelection
return itemSelectionModel.isRowSelected(markerItem.index) ? "red" : "gray"
}
}
MouseArea {
id: mouseArea
hoverEnabled: true
anchors.fill: parent
onClicked: itemSelectionModel.select(markerModel.index(markerItem.index, 0),
ItemSelectionModel./*ClearAnd*/Select)
}
}
}
}
}

QML/MouseArea: onEntered not triggered when mouse pressed on another mouse area

I need to detect a selection range with MouseAreas. So I want to detect a mousePressed on any object, before the release I need to detect onEntered on any other object and then mouseRelease on anyObject
I'm focusing on detecting enter events on an item after pressing on another item.
A simplification of my objects:
import QtQuick 2.15
Rectangle {
property color color: "red"
property string name: "rect1"
property Item mousePressedObserver
width: 100
height: 20
border.color: ma.visible ? color: "grey"
border.width: 1
MouseArea {
id: ma
acceptedButtons: Qt.LeftButton
anchors.fill: parent
hoverEnabled: true
preventStealing: false
propagateComposedEvents: true
drag.target: undefined
onPressed: {
if(mousePressedObserver) mousePressedObserver.onPressed()
mouse.accepted = true
}
onPositionChanged: mouse.accepted = true
onReleased: if(mousePressedObserver) mousePressedObserver.onReleased()
onEntered: {border.width= 2; console.log(name + " entered")}
onExited: {border.width= 1; console.log(name + " exited"); hoverEnabled= false}
onContainsMouseChanged: console.log(name + " containsMouse: " + containsMouse)
}
}
A simplification of my view:
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Layouts 1.15
Window {
width: 640
height: 480
visible: true
title: qsTr("Hello World")
MouseArea {
acceptedButtons: Qt.LeftButton
anchors.fill: parent
hoverEnabled: true
onPressed: indicator.onPressed()
onReleased: indicator.onReleased()
ColumnLayout {
CustomRect {mousePressedObserver: indicator}
CustomRect { name: "rect2"; color: "blue"; mousePressedObserver: indicator}
CustomRect { name: "rect3"; color: "green"; mousePressedObserver: indicator}
MouseStatusIndicator { id: indicator}
}
}
}
If I press on the first rect, and without releasing, mouve to the second rect, the onEntered handler is called only after releasing the mouse. If i don't press the mouse, enter is detected "real-time". Is it possible to detect the enter event even if mousse is pressed?

Get mouse x & y in nested MouseArea with hoverEnabled

I made a simplified MapImage component which allows to zoom and pan an image with the mouse. This component uses Flickable and MouseArea components. The MapImage component just handles image display, zooming and panning. I want to use another MouseArea in the MapImage instance in main.qml (to be able to place objects using a Canvas but this is not important here). This is not the job of MapImage, so I really need this second MouseArea component.
I need to set the hoverEnabled property to true because I need onPositionChanged and others events... But this property seems to cause problems with mouseX and mouseY values taken from my updateFlickable function. When I'm zooming with the mouse wheel, zoom does not occur at the mouse position...
I've made a minimal example available here or in a gist.
Any hint to solve this?
main.qml
import QtQml.Models 2.11
import QtQuick 2.11
import QtQuick.Controls 2.4
import QtQuick.Layouts 1.11
MapImage {
id: map
height: 600
width: 800
imageSource: "https://images.unsplash.com/photo-1651634099253-720df02a0d50"
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
hoverEnabled: true // this is required to use onPositionChanged
preventStealing: false
onPressed: {
// needed for flickable
mouse.accepted = false;
}
onPositionChanged: {
// do something.
}
}
}
MapImage.qml
import QtQuick 2.11
Item {
id: root
property alias imageSource: image.source
Flickable {
id: flickable
anchors.fill: parent
contentWidth: props.originalImageWidth
contentHeight: props.originalImageHeight
Image {
id: image
fillMode: Image.PreserveAspectFit
onStatusChanged: {
if (status === Image.Ready) {
props.originalImageWidth = sourceSize.width;
props.originalImageHeight = sourceSize.height;
props.changeCurrentScale(1);
}
}
// define the image display size
width: flickable.contentWidth;
height: flickable.contentHeight;
MouseArea {
id: mouseArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton
hoverEnabled: true
onWheel: {
wheel.accepted = false;
props.changeCurrentScale(wheel.angleDelta.y);
}
}
}
QtObject {
id: props
// original image size
property int originalImageWidth
property int originalImageHeight
property real scaleStep: 0.2
property real currentScale: 0.1
onCurrentScaleChanged: updateFlickable(currentScale);
function updateFlickable(scale) {
console.log(mouseArea.mouseX, mouseArea.mouseY); // <------ I am no longer able to get mouse x and y coordinates
flickable.resizeContent(originalImageWidth * scale, originalImageHeight * scale, Qt.point(mouseArea.mouseX, mouseArea.mouseY));
flickable.returnToBounds();
}
function changeCurrentScale(wheelDelta) {
if (wheelDelta > 0) currentScale = currentScale * (1 + scaleStep);
else currentScale = currentScale / (1 + scaleStep);
}
}
}
}
Finally found a solution. I had to add a new property in my MapImage component. This property role is to store the updated position of the mouse in the parent mouse area in the parent coordinate system. After that, I have to use mapToItem to convert in the flickable.contentItem coordinate system.
main.qml
import QtQml.Models 2.11
import QtQuick 2.11
import QtQuick.Controls 2.4
import QtQuick.Layouts 1.11
MapImage {
id: map
height: 600
width: 800
imageSource: "https://images.unsplash.com/photo-1651634099253-720df02a0d50"
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
hoverEnabled: true // this is required to use onPositionChanged
preventStealing: false
onPressed: {
// needed for flickable
mouse.accepted = false;
}
onPositionChanged: {
// CHANGE HERE
// the position must be updated on every position change
map.parentMouseAreaPosition = Qt.point(mouse.x, mouse.y);
// TO HERE
}
}
}
MapImage.qml
import QtQuick 2.11
Item {
id: root
property alias imageSource: image.source
// CHANGE HERE
// the current mouse position in the parent mouse area in parent coordinate system
property var parentMouseAreaPosition: Qt.point(0, 0)
// this function maps the parent coordinate system to that of contentItem
function __mapToContentItem(x, y) {
return mapToItem(flickable.contentItem, x, y);
}
// TO HERE
Flickable {
id: flickable
anchors.fill: parent
contentWidth: props.originalImageWidth
contentHeight: props.originalImageHeight
Image {
id: image
fillMode: Image.PreserveAspectFit
onStatusChanged: {
if (status === Image.Ready) {
props.originalImageWidth = sourceSize.width;
props.originalImageHeight = sourceSize.height;
props.changeCurrentScale(1);
}
}
// define the image display size
width: flickable.contentWidth;
height: flickable.contentHeight;
MouseArea {
id: mouseArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton
hoverEnabled: true
onWheel: {
wheel.accepted = false;
props.changeCurrentScale(wheel.angleDelta.y);
}
}
}
QtObject {
id: props
// original image size
property int originalImageWidth
property int originalImageHeight
property real scaleStep: 0.2
property real currentScale: 0.1
onCurrentScaleChanged: updateFlickable(currentScale);
function updateFlickable(scale) {
// CHANGE HERE
// get the mapped point
let point = __mapToContentItem(root.parentMouseAreaPosition.x, root.parentMouseAreaPosition.y);
console.log(point.x, point.y);
flickable.resizeContent(originalImageWidth * scale, originalImageHeight * scale, point);
// TO HERE
flickable.returnToBounds();
}
function changeCurrentScale(wheelDelta) {
if (wheelDelta > 0) currentScale = currentScale * (1 + scaleStep);
else currentScale = currentScale / (1 + scaleStep);
}
}
}
}
I don't know if there is a better solution.

Move items between ListModels

Assume a QML application with two list models, each holding, say 3, items. Graphically the two lists are displayed in two containers. I'd like to implement a drag-and-drop functionality to reorder the items inside their list and to swap two items between different lists.
Reordering in one list poses no problem due to the move method of the ListModel. For swapping items between the two list models, however, I think I have to use the remove and insert or set operations of ListModel. First experiments using set didn't work.
What is the canonical way to achieve this that also works with transitions? (E.g. just swapping the items' roles is no solution, I will have to swap the items themselves.)
You can just use ListModel.get() to fetch an element from one view and ListModel.append() or ListModel.insert() to put it in another one (not exactly with same type)
The simple example:
import QtQuick 2.4
import QtQuick.Window 2.2
import QtQuick.Layouts 1.1
import QtQuick.Controls 1.4
Window {
width: 400
height: 400
visible: true
Component.onCompleted: {
fillModel(model1);
fillModel(model2);
}
Component {
id: delegate
Rectangle {
width: parent.width
property int itemIndex: index
property var view: ListView.view
color: (itemIndex === view.currentIndex) ? "orange" : "white"
height: 20
Text { text: name; color: moved ? "red" : "black"; anchors.centerIn: parent }
MouseArea {
anchors.fill: parent
onClicked: {
view.currentIndex = itemIndex;
}
}
}
}
RowLayout {
anchors.fill: parent
ListView {
id: list1
Layout.fillHeight: true
Layout.fillWidth: true
model: ListModel { id: model1 }
delegate: delegate
}
ColumnLayout {
Layout.alignment: Qt.AlignCenter
width: 50
Button { text: ">>>"; onClicked: moveItem(list1,list2) }
Button { text: "<<<"; onClicked: moveItem(list2,list1) }
}
ListView {
id: list2
Layout.fillHeight: true
Layout.fillWidth: true
model: ListModel { id: model2 }
delegate: delegate
add: Transition {
id: list2Transition
enabled: false
property int fromX
property int fromY
ParallelAnimation {
NumberAnimation { properties: "x"; from: list2Transition.fromX; duration: 300; }
NumberAnimation { properties: "y"; from: list2Transition.fromY; duration: 300;easing.type: Easing.OutCirc; }
PropertyAnimation {property: "color"; from: "red"; to: "white"; duration: 500 }
}
}
}
}
function fillModel(model) {
for(var i = 0;i < 15;i ++)
model.append({name: "item" + i, moved: false});
}
function moveItem(listfrom,listto) {
var item = listfrom.model.get(listfrom.currentIndex);
var newPos1 = listfrom.parent.mapFromItem(listfrom,0,listfrom.currentIndex * 20);
var newPos2 = listto.parent.mapFromItem(listto,0,listto.currentIndex * 20);
list2Transition.fromX = newPos1.x - newPos2.x;
list2Transition.fromY = newPos1.y;
list2Transition.enabled = true;
item.moved = true;
listto.model.insert(listto.currentIndex, item);
listfrom.model.remove(listfrom.currentIndex, 1);
}
}
You can also do the same action by using drag & drop functionality.

Combine 2 MouseAreas on GridView

I have code like this:
GridView {
// ... declarations ...
model: theModel
delegate: MouseArea {
id: cellMouseArea
onClicked: // open the cell
}
MouseArea {
id: gridViewMouseArea
// here process horizontal mouse press/release actions
}
}
with a MouseArea defined in each delegate and an overall MouseArea covering my GridView. In the cellMouseArea I want to perform an open item action whereas in the gridViewMouseArea I want to implement mouseX handle to open/close a sidebar. However, the two MouseAreas do not work together. How can I carry it out?
You can exploit propagateComposedEvents:
If propagateComposedEvents is set to true, then composed events will
be automatically propagated to other MouseAreas in the same location
in the scene. Each event is propagated to the next enabled MouseArea
beneath it in the stacking order, propagating down this visual
hierarchy until a MouseArea accepts the event. Unlike pressed events,
composed events will not be automatically accepted if no handler is
present.
You can set the property to true on the GridView MouseArea. In this way click events are propagated to the MouseAreas in the delegates whereas the outer MouseArea can implement other behaviours such as drag or hoven.
Here is an example in which outer MouseArea defines drag property to slide in/out a Rectangle ( simulating your sidebar) and thanks to the propagateComposedEvents clicks are managed by the single delegates.
import QtQuick 2.4
import QtQuick.Window 2.2
ApplicationWindow {
width: 300; height: 400
color: "white"
Component {
id: appDelegate
Item {
width: 100; height: 100
Text {
anchors.centerIn: parent
text: index
}
MouseArea {
anchors.fill: parent
onClicked: {
parent.GridView.view.currentIndex = index
console.info("Index clicked: " + index)
}
}
}
}
Component {
id: appHighlight
Rectangle { width: 80; height: 80; color: "lightsteelblue" }
}
GridView {
anchors.fill: parent
cellWidth: 100; cellHeight: 100
highlight: appHighlight
focus: true
model: 12
delegate: appDelegate
MouseArea {
anchors.fill: parent
z:1
propagateComposedEvents: true // the key property!
drag.target: dragged
drag.axis: Drag.XAxis
drag.minimumX: - parent.width
drag.maximumX: parent.width / 2
onMouseXChanged: console.info(mouseX)
}
}
Rectangle{
id: dragged
width: parent.width
height: parent.height
color: "steelblue"
x: -parent.width
}
}

Resources