QML Subwindow does not close as expected - qt

I'm trying to implement a subwindow as a pop up that should disappear when the user clicks outside of it. Following the example set by this question, I came up with this:
import QtQuick 2.3
import QtQuick.Window 2.2
Window {
id: win
width: 360
height: 360
color: "black"
Rectangle {
id: block
width: 20
height: 20
color: "green"
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: {
console.log( "Entered" );
menu.visible = true;
menu.requestActivate();
}
}
Window {
id: menu
width: 100
height: 100
x: win.x + block.width
y: win.y + block.height
flags: Qt.Popup
color: "red"
visible: false
onActiveChanged: {
console.log( "Pop up:", active );
if ( !active ) {
visible = false;
}
}
}
}
onActiveChanged: {
console.log( "Main win:", active );
}
}
However the popup does not disappear when clicked outside of, the debug output is:
// Main win opens
qml: Main win: true
// Green square entered
qml: Entered
qml: Main win: true
qml: Pop up: true
// Clicked outside of the pop up
qml: Pop up: true
qml: Main win: true
As you can see the main window does not lose focus when the pop up becomes active, so when the user clicks outside of it the overall focus does not change. So how is this approach supposed to work!?

The solution I found feels quite hackish, but it works, it's self-contained, and it's scalable.
import QtQuick 2.3
import QtQuick.Window 2.2
Window {
id: win
width: 360
height: 360
color: "black"
Rectangle {
id: block
width: 20
height: 20
color: "green"
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: {
menu.visible = true;
}
}
Rectangle {
id: menu
width: 100
height: 100
anchors.top: parent.bottom
anchors.left: parent.right
color: "red"
visible: false
Loader {
id: exitAreaLoader
Component {
id: exitArea
MouseArea {
x: 0
y: 0
parent: menu.getRoot()
width: if ( parent !== null ) { return parent.width }
height: if ( parent !== null ) { return parent.height }
onPressed: {
var pnt = Qt.point( mouse.x - menu.parent.width,
mouse.y - menu.parent.height );
if ( !menu.contains( pnt ) )
{
console.log( "Closing", pnt );
menu.visible = false;
exitAreaLoader.sourceComponent = undefined;
} else {
mouse.accepted = false;
}
}
}
}
}
onVisibleChanged: {
if ( visible ) {
console.log( "Opening" );
exitAreaLoader.sourceComponent = exitArea;
}
}
function getRoot() {
var par = parent;
while ( par.parent !== null ) {
par = par.parent;
}
return par;
}
}
}
}
The general principle is that when the menu/popup/drop down is made visible a Loader creates a MouseArea as the most descendent child in the entire QObject tree (this guarantees that all mouse events within the window will be received by it). However this means that mouse events that are destined for whatever the popup contains are also captured by the MouseArea, so we have to explicitly test for that and reject the event.
It's a real shame that there's not an official object for this, one was requested years ago and it seems to have adopted by Ubuntu, but that's not very useful for cross-platform development.

Related

How to make an Item draggable without flicker when parent's position changes during drag

I'm trying to make an item that can be resized by its edges.
For showing a minimal testcase of the problem it is enough to have its left edge draggable, so here it is:
Rectangle {
id: root
border.width: 1
border.color: 'black'
color: 'red'
// save original position and size at drag start
property real origX: 0
property real origWidth: 0
// drag this item:
Item {
id: dragDummy
x: 0
onXChanged: {
root.x = root.origX + x
root.width = root.origWidth - x
}
}
MouseArea {
anchors.fill: root
drag.target: dragDummy
drag.axis: Drag.XAxis
drag.onActiveChanged: {
// onDragStarted -> Cannot assign to non-existent property "onDragStarted" ???
if(!active) return
root.origX = root.x
root.origWidth = root.width
}
}
}
the problem seems to be that if drag causes parent position to change, that triggers another drag event, causing this flicker:
I'm guessing MouseArea can't help here? Then low level mouse events should be used like in "old-school" apps (i.e. capturing events at root Item, manually compute offset with respect to initial mouse down position, etc...)?
(or I have to move the MouseArea to an ancestor that won't move during drag, which is almost the same...)
There is a nice QML Item type called DragHandler which people often overlook, but I find that it works very well.
This solution is a little more idiomatic than other suggestions in that it uses a declarative style rather than imperative:
import QtQuick 2.15
Item {
id: root
width: 500
height: 100
Item {
height: 100
width: handle.x + handle.width / 2
}
Rectangle {
x: handle.x + handle.width / 2
width: root.width - (handle.x - handle.width/2)
height: 100
border{
width: 1
color: 'black'
}
color: 'red'
}
Item {
id: handle
x: -width / 2
width: 50
height: 100
DragHandler {
yAxis.enabled: false
xAxis{
minimum: -handle.width
maximum: root.width
}
}
}
}
The solution I come up with consists of having two MouseAreas:
a MouseArea moves with the item to drag, that is used only for hit-testing, so its onPressed handler is something like this:
onPressed: (mouse) => {
mouse.accepted = false
root.container.myDragTarget = root
}
onReleased: root.container.myDragTarget = null
another MouseArea, stacked below the others and not moving, handles the mouse position change and the dragging:
onPressed: _start = Qt.point(mouseX, mouseY)
onPositionChanged: {
if(myDragTarget) {
var delta = Qt.point(mouseX - _start.x, mouseY - _start.y)
// do any rounding/snapping of delta here...
_start.x += delta.x
_start.y += delta.y
myDragTarget.x += delta.x
myDragTarget.y += delta.y
}
}
This is able to drag the item reliably.
This is also what I wanted to avoid, because it reinvents mouse drag, but in absence of a better solution it is what I am going to use.
I won't accept this answer as I'm curious to see other ways to approach this problem.
You can workaround the movement and new positioning of the dragged Item by mapping the coordinates with the mapToItem functions.
In my solution, I've not used the drag functionality of the MouseArea as it needs a drag.target. I've used the pressed and position changed signals to implement drag behavior. The only downside is the background Item which is needed for the mapToItem function as it doesn't accept the Window due to it not being an Item.
import QtQuick
import QtQuick.Window
import QtQuick.Shapes
Window {
id: root
visible: true
width: 400
height: 400
Item {
id: background
anchors.fill: parent
Rectangle {
id: rectangle
property int rightX
x: 50
y: 50
width: 200
height: 80
border.width: 1
border.color: "black"
color: "red"
Rectangle {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: 10
color: mouseArea.containsMouse || mouseArea.pressed ? "#ff808080" : "#aa808080"
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onPressed: rectangle.rightX = rectangle.x + rectangle.width
onPositionChanged: function(mouse) {
if (mouseArea.pressed) {
var tmp = mouseArea.mapToItem(background, mouse.x, 0)
if (tmp.x <= rectangle.rightX)
rectangle.x = tmp.x
else
rectangle.x = rectangle.rightX
rectangle.width = rectangle.rightX - rectangle.x
}
}
}
}
}
}
}

ContainsMouse gives incorrect value on parent change

In QML, the MouseArea's containsMouse property is supposed to return true when the mouse is currently inside the mouse area. Unfortunately, this is not always the case. In the following code, the red square turns blue when the MouseArea within it contains the mouse (ContainsMouse is true). However, if you hit the control key while the square is blue, when the square is reparented to the Window's contentItem, the containsMouse property is not updated (as indicated by the text in the middle of the square). The square will still be blue even though it doesn't contain the mouse anymore. Is there anyway to tell the MouseArea to refresh it's containsMouse property?
Here is the code:
import QtQuick 2.12
import QtQuick.Window 2.12
Window {
width: 800
height: 500
visible: true
Rectangle {
id: square
width: 200
height: 200
focus: true
color: my_mouse_area.containsMouse ? "blue" : "red"
MouseArea {
id: my_mouse_area
anchors.fill: parent
hoverEnabled: true
onClicked: {
my_mouse_area.x = 200
}
}
Text {
anchors.centerIn: parent
text: my_mouse_area.containsMouse + ""
font.pixelSize: 20
}
Keys.onPressed: {
if(event.key === Qt.Key_Control){
second_window.show()
square.parent = second_window.contentItem
}
}
}
Window {
id: second_window
width: 400
height: 400
visible: false
}
}
I don't like my first solution, so I have made another, more sophisticated one, but this is not a pure QML solution. The trick is that on parent change you should call a C++ method where you send a mouse move event back to the mouse area, so it will re-evaluate the hovered aka containsMouse boolean. It is a nicer solution, but still a bit of a workaround.
Make sure you have a simple QObject derived class like MyObject with the following Q_INVOKABLE method:
class MyObject : public QObject
{
Q_OBJECT
//
// constuctor and whatnot
//
Q_INVOKABLE void sendMouseMoveEventTo(QObject* item)
{
QEvent* e = new QEvent(QEvent::MouseMove);
QCoreApplication::sendEvent(item, e);
}
};
Make an instance of it in main.cpp, and set as context property, so you can reach it from QML:
MyObject myObject;
engine.rootContext()->setContextProperty("myObject", &myObject);
And finally in the QML Rectangle add this:
onParentChanged: {
myObject.sendMouseMoveEventTo(my_mouse_area)
}
The solution I came up with uses Timer, but with zero interval, thus zero flickering. You can try setting the interval to higher value, to see what is going on. The trick is to set the rectangle visibility dependent of the timer running using "visible: !tmr.running", and start the timer immediately after the parent change of the rectangle.
import QtQuick 2.12
import QtQuick.Window 2.12
Window {
width: 800
height: 500
visible: true
Rectangle {
id: square
width: 200
height: 200
focus: true
color: my_mouse_area.containsMouse ? "blue" : "red"
visible: !tmr.running
Timer {
id: tmr
interval: 0
}
MouseArea {
id: my_mouse_area
anchors.fill: parent
hoverEnabled: true
onClicked: {
my_mouse_area.x = 200
}
}
Text {
anchors.centerIn: parent
text: my_mouse_area.containsMouse + ""
font.pixelSize: 20
}
Keys.onPressed: {
if(event.key === Qt.Key_Control){
second_window.show()
square.parent = second_window.contentItem
tmr.start()
}
}
}
Window {
id: second_window
width: 400
height: 400
visible: false
}
}

QML SplitView auto collapse on handlebar mouse release

I have a QML Controls 2 SplitView and a redefined handle, which works well, but I want detect mouse release event on the handler, so I could collapse the SplitView under a certain threshold of width. Adding a MouseArea on top of the existing handle will absorb drag events, so I'm unable to move the handlebar. Any idea how could I gather the mouse release event, or any other solution which solves this problem?
Alright, I have created an example application. As you can see in this example, my MouseArea is marked with yellow and collapses the right view programmatically when double clicked, which is nice, but I also want to drag the handlebar and upon mouse release under a certain width threshold I want to collapse the view as well. The black part of the handlebar where my MouseArea is not covering the handlebar, responds to drag, but since there is no signal I can gather from it, the width threshold already set shouldCollapse boolean property, so the view won't update. Probably I could solve this issue with a timer, but I need a more sophisticated solution.
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15
Window {
width: 800
height: 400
visible: true
SplitView {
id: splitView
anchors.fill: parent
orientation: Qt.Horizontal
function toggleCollapse() { collapsibleRect.shouldCollapse = !collapsibleRect.shouldCollapse }
handle: Rectangle {
implicitWidth: 20
implicitHeight: 20
color: "black"
MouseArea {
anchors.centerIn: parent
width: parent.width
height: parent.height / 2
onDoubleClicked: splitView.toggleCollapse()
Rectangle {
anchors.fill: parent
color: "yellow"
Text {
anchors.centerIn: parent
text: "Double click to collapse"
rotation: 90
}
}
}
}
Rectangle {
id: mainRect
color: "green"
SplitView.fillWidth: true
Text {
anchors.centerIn: parent
font.pixelSize: 24
text: "Main scene"
}
}
Rectangle {
id: collapsibleRect
property bool shouldCollapse: false
SplitView.preferredWidth: shouldCollapse ? 0 : 300
color: "purple"
clip: true
onWidthChanged: {
if(width < 200) shouldCollapse = true
else shouldCollapse = false
}
Text {
anchors.centerIn: parent
rotation: parent.shouldCollapse ? 90 : 0
font.pixelSize: 24
text: parent.shouldCollapse ? "SHOULD BE COLLAPSED" : "NOT COLLAPSED"
Behavior on rotation { NumberAnimation { duration: 100 } }
}
}
}
}
I had a similar problem and was able to solve it thanks to the hint of #Ponzifex that the SplitView's resizing property will be set to true as soon as the handle is clicked. Using a Timer I managed to detect whether the handle was quickly pressed twice in a row.
SplitView {
id: view
...
handle: Rectangle {
...
}
//============================================================
// double click behavior
Timer {
id: doubleClickTimer
interval: 300 // number of ms between clicks that should be considered a double click
}
property bool doubleClicked: false
// `resizing` will be set to true even if the handle is just pressed
onResizingChanged: {
if (view.resizing) {
if (!doubleClickTimer.running) {
doubleClickTimer.start();
return;
}
view.doubleClicked = true;
} else {
if (view.doubleClicked) {
// do any manual resizing in here
view.doubleClicked = false;
}
}
}
}
It is important to note, however, that it is only possible to resize the contents of a SplitView when resizing is false. That's why I need to have the doubleClicked helper property.
Add this to MouseArea:
onPressed: {
mouse.accepted = (mouse.flags & Qt.MouseEventCreatedDoubleClick);
}
propagateComposedEvents: true
cursorShape: Qt.SplitHCursor

Adaptable UI in QML

I am developing a QML application where I have three main views. My code looks like below.
SplitView{
ListView{
id: firstView
}
ListView{
id: secondView
}
WebView{
id: thirdView
}
}
Now this is working fine, but I would like to do this: when my main window is resized below a certain width (500) I would like to show only one view, so that clicking on the delegate will show the next view (with the possibility of going back to the previous view). So for example clicking on the first view will show the second view and clicking on the second view will show the third view. The approach I want is very similar to the Mail application in Windows 10.
Does anyone know how can this be achieved in QML?
OK, I've cooked up a crude example, I didn't modularize it deliberately, so you can see how it works in a single source. Also, I don't know how the windows 10 mail application does it, since I don't have it, but it still close enough to your description.
You begin with 3 list views in a row, which are sized to fill the entire UI, but if you reduce the UI size to the minimum of 500, the views will increase in size to fill almost the entire ui, and when you click on a view item, it will move you to the next view, and if you click the showing previous view, you will be returned back to it.
ApplicationWindow {
title: qsTr("Hello World")
width: 800
height: 300
minimumWidth: 500
visible: true
Item {
id: adapt
width: parent.width
height: parent.height
property int sizeUnit: width > 500 ? width / 5 : 400
property bool isPaged: width == 500 ? true : false
onIsPagedChanged: { if (!isPaged) { x = 0; page = 0; } }
property int page: 0
Behavior on x { NumberAnimation { duration: 250; easing.type: Easing.OutBack } }
ListModel {
id: mod
ListElement { name: "one" }
ListElement { name: "two" }
ListElement { name: "three" }
ListElement { name: "four" }
ListElement { name: "five" }
}
ListView {
id: v1
width: adapt.sizeUnit
height: parent.height
model: mod
delegate: Rectangle {
height: 70
width: v1.width
color: "red"
border.color: "black"
Text { anchors.centerIn: parent; text: name }
MouseArea {
anchors.fill: parent
onClicked: {
if (adapt.isPaged) {
if (adapt.page == 0) {
adapt.x = -(v2.x - 100)
adapt.page = 1
} else {
adapt.x = 0
adapt.page = 0
}
}
}
}
}
}
ListView {
id: v2
width: adapt.sizeUnit
height: parent.height
x: adapt.sizeUnit + 10
model: mod
delegate: Rectangle {
height: 70
width: v2.width
color: "cyan"
border.color: "black"
Text { anchors.centerIn: parent; text: name }
MouseArea {
anchors.fill: parent
onClicked: {
if (adapt.isPaged) {
if (adapt.page == 1) {
adapt.x = -(v3.x - 100)
adapt.page = 2
} else {
adapt.x = -(v2.x - 100)
adapt.page = 1
}
}
}
}
}
}
ListView {
id: v3
width: adapt.isPaged ? adapt.sizeUnit : 3 * adapt.sizeUnit - 20
height: parent.height
x: v2.x + v2.width + 10
model: mod
delegate: Rectangle {
height: 70
width: v3.width
color: "yellow"
border.color: "black"
Text { anchors.centerIn: parent; text: name }
}
}
}
}
This should be enough to get you going. Obviously, for production you can go for more elegant layouting and nagivation, such as use a function for the actual "page sliding" and anchors, the above is just for the purpose of example.
Here is my idea of how to solve the problem.
// When width of MainWindow is smaller than 500 stop using SplitView
property bool useSplitView: (width < 500 ? false : true)
onUseSplitViewChanged: {
if (useSplitView) {
firstView.anchors.fill = undefined
secondView.anchors.fill = undefined
thirdView.anchors.fill = undefined
firstView.parent = splitView // splitView is id of SplitView
secondView.parent = splitView
thirdView.parent = splitView
}
else {
firstView.parent = mainWindow.contentItem // mainWindow is id of MainWindow
secondView.parent = mainWindow.contentItem
thirdView.parent = mainWindow.contentItem
secondView.visible = false
thirdView.visible = false
firstView.anchors.fill = firstView.parent
secondView.anchors.fill = secondView.parent
thirdView.anchors.fill = thirdView.parent
}
}
When useSplitView flag changes you need to enable some kind of touch area on views and switch between them on click.

Accessing Attributes of a ListView

I am writing a QML application that loads and displays images, I have a central frame that show the image the user is currently looking at as well as a sidebar that lets the user select images. What I am having trouble with is getting the information about what the currently selected item is in the sidebar (ListView).
Additionally, I am having trouble accessing the sourceChanged signal in the delegate for the ListView and therefore can't update the images in the list without the user scrolling down and then back up to force them to reload. Is there any easy way to access these attributes even though they are nested within a ListView?
Here is the code for my ListView. The issue is that I want to be able to access the Image from outside of the ListView in order to send a sourceChanged signal but I'm not sure how you would access a specific item in the list.
//The list of frames that have been loaded
ListView {
id: frameList
anchors {top: parent.top; bottom: parent.bottom; left: frameViewer.right; leftMargin: 5; topMargin: 5}
spacing: 5
width:300
height: parent.height
Component {
id: frameDelegate
Rectangle {
id: wrapper
anchors {horizontalCenter: parent.horizontalCenter}
height: 300
width: 300
//If there is an image in that space and if it's selected, highlight it
color: frames[number] === undefined ? "white" : wrapper.ListView.view.currentIndex === index ? "yellow" : "white"
Image {
id: image
height: 280
width: 280
anchors.centerIn: parent
fillMode: Image.PreserveAspectFit
source: "image://images/" + frames[number]
}
MouseArea {
height: 280
width: 280
anchors.fill: image
hoverEnabled: true
onClicked: frames[index] === undefined ? console.log(""): wrapper.ListView.view.currentIndex = index
}
}
}
ListModel {
id: frameModel
ListElement {
number: 0
}
ListElement {
number: 1
}
ListElement {
number: 2
}
ListElement {
number: 3
}
ListElement {
number: 4
}
ListElement {
number: 5
}
}
model: frameModel
delegate: frameDelegate
focus: true
}
Ok so I fixed the issue, although I may not have done it in the best way possible.
My solution is to create a signal newFrame in the window which would be sent every time a new frame is loaded. Then I use a Connections in the Image to update the source, every time that signal is sent.
signal newFrame()
//Stores all the fileURLs
function loadImages()
{
for(var i = 0; i < fileDialog.fileUrls.length; i++)
{
var exists = false
for(var j = 0; j < frames.length; j++)
{
if(frames[j] === fileDialog.fileUrls[i])
{
exists = true
}
}
if(exists == false)
{
if(frames == [])
{
frames = fileDialog.fileUrls[i]
}
else
{
frames.push(fileDialog.fileUrls[i])
}
frame.sourceChanged(frame.source)
newFrame()
}
}
}
Component
{
id: frameDelegate
Rectangle
{
id: wrapper
anchors {horizontalCenter: parent.horizontalCenter}
height: 300
width: 300
//If there is an image in that space and if it's selected, highlight it
color: frames[number] === undefined ? "white" : wrapper.ListView.view.currentIndex === index ? "yellow" : "white"
Image
{
id: image
height: 280
width: 280
anchors.centerIn: parent
fillMode: Image.PreserveAspectFit
source: "image://images/" + frames[number]
Connections {
target: window
onNewFrame: {
image.source = "image://images/" + frames[number]
}
}
}
MouseArea
{
height: 280
width: 280
anchors.fill: image
hoverEnabled: true
onClicked: {frames[index] === undefined ? console.log(""): wrapper.ListView.view.currentIndex = index; frame.sourceChanged(frame.source)}
}
}
}

Resources