MenuBar like behaviour in QML - qt

I want to have menubar like behaviour when clicked on a rectangle. Whenever a rectangle is clicked a model is updated and a ListView is shown. I want this ListView to disappear whenever another Rectangle is clicked and the listmodel should not be appended with each click. Here is my sample code.
Card.qml
Rectangle {
id: card
width: 50
height: 100
color: "pink"
Item {
id: rec
width: 50
anchors.bottom: parent.top
ListModel {
id: menuListModel
}
Component {
id: delegate
Rectangle {
width: 50
height: 20
color: "blue"
Text {
anchors.fill: parent
anchors.centerIn: parent
text: commandText
}
}
}
ListView {
anchors.fill: parent
model:menuListModel
delegate: delegate
interactive: false
}
}
MouseArea {
anchors.fill: parent
onClicked: {
rec.height += 40;
menuListModel.append({"commandText" : "Act"});
menuListModel.append({"commandText" : "Set"});
}
}
}
main.qml
Item {
width: 120
height: 200
Row {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
spacing: 10
Card {
id: card1
}
Card {
id: card2
}
}
}
Also I want to call certain function upon clicking menu buttons i.e. Act and Set.
Edit
The following function is called with appropriate flags when a card(here rectangle) is clicked.
property int command_activate: 0x0001
property int command_summon: 0x0002
property int command_spsummon: 0x0004
property int command_mset: 0x0008
property int command_sset: 0x0010
property int command_repos: 0x0020
property int command_attack: 0x0040
property int command_list: 0x0080
function showMenu(flag) {
if(flag & command_activate) {
rec.height += 15;
menuListModel.append({"commandText" : "Activate"});
}
if(flag & command_summon) {
rec.height += 15;
menuListModel.append({"commandText" : "Normal Summon"});
}
if(flag & command_spsummon) {
rec.height += 15;
menuListModel.append({"commandText" : "Special Summon"});
}
if(flag & command_mset) {
rec.height += 15;
menuListModel.append({"commandText" : "Set"});
}
if(flag & command_sset) {
rec.height += 15;
menuListModel.append({"commandText" : "Set"});
}
if(flag & command_repos) {
if(position & pos_facedown) {
rec.height += 15;
menuListModel.append({"commandText" : "Flip Summon"});
}
else if(position & pos_attack) {
rec.height += 15;
menuListModel.append({"commandText" : "To Defense"});
}
else {
rec.height += 15;
menuListModel.append({"commandText" : "To Attack"});
}
}
if(flag & command_attack) {
rec.height += 15;
menuListModel.append({"commandText" : "Attack"});
}
if(flag & command_list) {
rec.height += 15;
menuListModel.append({"commandText" : "View"});
}
}
So, in short when a card is clicked a menu has to be shown according to the flag on the top of the card.

There are several problems with your code.
You cannot name your delegate "delegate". When you do this, the ListView uses its own delegate property to set itself, leading to nothing happening.
Also, why don't you just statically fill your ListView then use the visible property to toggle whether you display it or not? If you want it to disappear whenever another Card is clicked, you may have to use the focus property.
Indeed, setting focus to true will reset the focus of all other Items within the focus scope. Something like this might work:
Rectangle {
id: card
...
property alias model: list.model
Component {
id: mydelegate
Rectangle {
width: 50
height: 20
...
}
}
ListView {
id: list
visible: card.activeFocus
anchors.bottom: parent.top
width: card.width
delegate: mydelegate
interactive: false
height: childrenRect.height
}
MouseArea {
anchors.fill: parent
onClicked: {
card.focus = !card.focus
}
}
}
As for calling a function, you could add the name of the function to call directly in the ListModel. Add a MouseArea in your delegate, and send a signal on clicked. Then, you just have to call the matching slot (Agreed, the this[slot]() syntax is a bit hacky).
In Card.qml
Rectangle {
id: card
...
property alias model: list.model
signal itemClicked(string slot)
Component {
id: mydelegate
Rectangle {
...
MouseArea
{
anchors.fill: parent
onClicked: {
if(model.slot)
itemClicked(slot)
}
}
}
}
...
}
In main.qml
...
Card
{
model: ListModel {
ListElement{commandText: "Act"; slot: "act"}
ListElement{commandText: "Set"; slot: "set"}
}
function act()
{
print("act triggered")
}
function set()
{
print("set trigggered")
}
onItemClicked: { this[slot]() }
}
...

As I can't comment, to ask for clarification, I will state my assumptions from what I understood:
You have a number of ListModel that contain the information of the menu that should pop up, when clicking on the corresponding rectangle item in the bar.
The ListView that pops up shall disappear, once you clicked any other rectangle in this bar. (Also: if something outside the bar or menu is clicked? Or an item in the bar is selected?)
I'd go with only one ListView, and update/change the model, as well as the position (e.g. margin) once a button is clicked, so you don't have multiple unused objects flying around.
To not show anything, I think it is sufficient, to set an empty model.
You can also have a list of functions.
Row {
id: row
property var f: [a, b, c]
function a() { console.log('a'); }
function b() { console.log('b'); }
function c() { console.log('c'); }
width: 300
height: 50
Repeater {
id: rep
anchors.fill: parent
delegate: button
model: 3
Rectangle {
id: button
width: 98
height: 50
border.color: 'black'
Text {
text: "a"
anchors.fill: parent
}
MouseArea {
anchors.fill: parent
onClicked: row.f[index]()
}
}
}
}
e.g. will call the function a, b, or c - depending on the index of the rectangle.
I hope, so far, I could help.

Related

QML ListView populate animation not working

I have a C++ model derived from QAbstractListModel which I am using in a QML ListView
ListView {
id: listView
populate: Transition {
id: addTrans
SequentialAnimation {
PauseAnimation {
duration: (addTrans.ViewTransition.index -
addTrans.ViewTransition.targetIndexes[0]) * 100
}
NumberAnimation { property: "scale"; from: 0; to: 1 }
}
}
anchors.fill: parent
delegate: listDelegate
model: newsModel
}
I tried to add an animation for when the list is populated, but it doesn't work. So what I did was I tried to change populate to add and the animation is triggered, however the problem is that that each item is already in the list when the animation starts: in other words, instead of having an empty list to which items are added, I have a full list of elements that are then individually animated. I have tried also following this suggestion but it doesn't work.
EDIT: as requested I am providing a minimal working example to reproduce my problem.
The problem can be reproduced with the following:
#include <QObject>
#include <QQmlObjectListModel.h>
class Item : public QObject{
Q_OBJECT
Q_PROPERTY(QString name READ name NOTIFY nameChanged)
public:
Item(QObject* parent = nullptr): QObject(parent)
{
}
QString name() const{
return mName;
}
void setName(QString const &name){
if(mName != name){
mName = name;
emit nameChanged();
}
}
signals:
void nameChanged();
private:
QString mName;
};
class DataManager : public QObject{
Q_OBJECT
Q_PROPERTY(QQmlObjectListModel<Item>* itemsModel READ itemsModel CONSTANT)
public:
DataManager(){
mItemsModel = new QQmlObjectListModel<Item>(this);
qRegisterMetaType<QQmlObjectListModel<Item>*>("QQmlObjectListModel<Item>*");
// I have also tried to add items from here, but the result is even worse
// for(int i = 0; i < 20; i++){
// addItem(QString::number(i));
// }
}
QQmlObjectListModel<Item>* itemsModel(){
return mItemsModel;
}
Q_INVOKABLE void addItem(QString const &name){
Item* item = new Item(mItemsModel);
item->setName(name);
mItemsModel->append(item);
}
private:
QQmlObjectListModel<Item>* mItemsModel;
};
Then in qml:
import QtQuick 2.12
import QtQuick.Controls 2.5
ApplicationWindow {
visible: true
width: 640
height: 480
title: qsTr("Scroll")
ScrollView {
anchors.fill: parent
ListView {
width: parent.width
// dataManager is a context property set via c++
model: dataManager.itemsModel
populate: Transition {
id: popTrans
SequentialAnimation {
PauseAnimation {
duration: (popTrans.ViewTransition.index -
popTrans.ViewTransition.targetIndexes[0]) * 100
}
NumberAnimation { property: "scale"; from: 0; to: 1 }
}
}
add: Transition {
id: addTrans
SequentialAnimation {
PauseAnimation {
duration: (addTrans.ViewTransition.index -
addTrans.ViewTransition.targetIndexes[0]) * 100
}
NumberAnimation { property: "scale"; from: 0; to: 1 }
}
}
delegate: ItemDelegate {
text: "Item" + name
width: parent.width
}
}
}
Component.onCompleted: {
var p;
for(p=0; p < 20; p++){
dataManager.addItem(p)
}
}
}
The QQmlObjectListModel is a helper class which I borrowed from here
I am not sure what you want exactly, but I hope my code help you.
populate: Transition {
id: addTrans
SequentialAnimation {
PropertyAction {
property: "visible"
value: false
}
PauseAnimation {
duration: (addTrans.ViewTransition.index -
addTrans.ViewTransition.targetIndexes[0]) * 100
}
PropertyAction {
property: "visible"
value: true
}
NumberAnimation { property: "scale"; from: 0; to: 1 }
}
}

Show big text in qml

Trying to display a text file in qml. The file size is about 3 megabytes. At the same time there are:
long opening of the form,
large waste of memory.
Tried use ScrollView, Flickable, Text and TextArea. How can these problems be avoided?
QML
ScrollView {
id: idScrollView
anchors {
fill: parent
margins: Dimensions.x(15)
}
Text {
id: idContent
anchors {
left: parent.left
right: parent.right
rightMargin: Dimensions.x(20)
}
text: viewmodel.getLogText()
font.pixelSize: Dimensions.y(10)
wrapMode: Text.Wrap
}
}
C++
QString MainViewModel::getLogText()
{
const int maxSz = 1024 * 200;
QString result;
QFile file(ALog::filePath());
if (file.open(QIODevice::ReadOnly))
{
if (file.size() > maxSz)
file.seek(file.size() - maxSz);
QByteArray arr = file.read(maxSz);
result = QString::fromLatin1(arr);
if (file.size() > maxSz)
result = QString("Skip %1 Kb\n\n").arg((file.size() - maxSz)/1024) + result;
file.close();
}
return result;
}
Found a partial solution. It loads much faster and consumes several times less memory. Among the disadvantages-there is no possibility to convert Text to TextArea to be able to select the text to copy to the clipboard.
property variant stringList: null
function updateText() {
stringList = viewmodel.getLogText().split('\n')
idContentListView.positionViewAtEnd()
}
ListView {
id: idContentListView
model: stringList
anchors {
fill: parent
margins: Dimensions.x(15)
}
delegate: Text {
anchors {
left: parent.left
right: parent.right
}
text: model.modelData
font.pixelSize: Dimensions.y(10)
textFormat: Text.PlainText
wrapMode: Text.Wrap
}
ScrollBar.vertical: ScrollBar {}
}

How to create a round mouse area in QML

I have a basic custom button using a Rectangle with radius: width/2. Now I add a MouseArea to my button. However the MouseArea has a squared shape. That means the click event is also triggered when I click slightly outside the round button, i.e. in the corners of the imaginary square around the round button. Can I somehow make also the MouseArea round?
import QtQuick 2.7
import QtQuick.Window 2.2
Window {
visible: true
width: 640
height: 480
title: qsTr("TestApp")
Rectangle {
id: background
anchors.fill: parent
color: Qt.rgba(0.25, 0.25, 0.25, 1);
Rectangle {
id: button
width: 64
height: 64
color: "transparent"
anchors.centerIn: parent
radius: 32
border.width: 4
border.color: "grey"
MouseArea {
anchors.fill: parent
onPressed: button.color = "red";
onReleased: button.color = "transparent";
}
}
}
}
Stealing code from PieMenu, here's RoundMouseArea.qml:
import QtQuick 2.0
Item {
id: roundMouseArea
property alias mouseX: mouseArea.mouseX
property alias mouseY: mouseArea.mouseY
property bool containsMouse: {
var x1 = width / 2;
var y1 = height / 2;
var x2 = mouseX;
var y2 = mouseY;
var distanceFromCenter = Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2);
var radiusSquared = Math.pow(Math.min(width, height) / 2, 2);
var isWithinOurRadius = distanceFromCenter < radiusSquared;
return isWithinOurRadius;
}
readonly property bool pressed: containsMouse && mouseArea.pressed
signal clicked
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: if (roundMouseArea.containsMouse) roundMouseArea.clicked()
}
}
You can use it like this:
import QtQuick 2.5
import QtQuick.Window 2.2
Window {
width: 640
height: 480
visible: true
RoundMouseArea {
id: roundMouseArea
width: 100
height: 100
anchors.centerIn: parent
onClicked: print("clicked")
// Show the boundary of the area and whether or not it's hovered.
Rectangle {
color: roundMouseArea.pressed ? "red" : (roundMouseArea.containsMouse ? "darkorange" : "transparent")
border.color: "darkorange"
radius: width / 2
anchors.fill: parent
}
}
}
Another option is a C++/QML way as decribed in this example. This example provides a way to use masks of any shapes. It can be customized to fit your needs.
Posting the code as it is:
maskedmousearea.cpp
MaskedMouseArea::MaskedMouseArea(QQuickItem *parent)
: QQuickItem(parent),
m_pressed(false),
m_alphaThreshold(0.0),
m_containsMouse(false)
{
setAcceptHoverEvents(true);
setAcceptedMouseButtons(Qt::LeftButton);
}
void MaskedMouseArea::setPressed(bool pressed)
{
if (m_pressed != pressed) {
m_pressed = pressed;
emit pressedChanged();
}
}
void MaskedMouseArea::setContainsMouse(bool containsMouse)
{
if (m_containsMouse != containsMouse) {
m_containsMouse = containsMouse;
emit containsMouseChanged();
}
}
void MaskedMouseArea::setMaskSource(const QUrl &source)
{
if (m_maskSource != source) {
m_maskSource = source;
m_maskImage = QImage(QQmlFile::urlToLocalFileOrQrc(source));
emit maskSourceChanged();
}
}
void MaskedMouseArea::setAlphaThreshold(qreal threshold)
{
if (m_alphaThreshold != threshold) {
m_alphaThreshold = threshold;
emit alphaThresholdChanged();
}
}
bool MaskedMouseArea::contains(const QPointF &point) const
{
if (!QQuickItem::contains(point) || m_maskImage.isNull())
return false;
QPoint p = point.toPoint();
if (p.x() < 0 || p.x() >= m_maskImage.width() ||
p.y() < 0 || p.y() >= m_maskImage.height())
return false;
qreal r = qBound<int>(0, m_alphaThreshold * 255, 255);
return qAlpha(m_maskImage.pixel(p)) > r;
}
void MaskedMouseArea::mousePressEvent(QMouseEvent *event)
{
setPressed(true);
m_pressPoint = event->pos();
emit pressed();
}
void MaskedMouseArea::mouseReleaseEvent(QMouseEvent *event)
{
setPressed(false);
emit released();
const int threshold = qApp->styleHints()->startDragDistance();
const bool isClick = (threshold >= qAbs(event->x() - m_pressPoint.x()) &&
threshold >= qAbs(event->y() - m_pressPoint.y()));
if (isClick)
emit clicked();
}
void MaskedMouseArea::mouseUngrabEvent()
{
setPressed(false);
emit canceled();
}
void MaskedMouseArea::hoverEnterEvent(QHoverEvent *event)
{
Q_UNUSED(event);
setContainsMouse(true);
}
void MaskedMouseArea::hoverLeaveEvent(QHoverEvent *event)
{
Q_UNUSED(event);
setContainsMouse(false);
}
Usage in QML:
import Example 1.0
MaskedMouseArea {
id: moonArea
anchors.fill: parent
alphaThreshold: 0.4
maskSource: moon.source
}
Register the custom item:
qmlRegisterType<MaskedMouseArea>("Example", 1, 0, "MaskedMouseArea");
Thanks to #Mitch. Sometimes such mousearea says it contains mouse after leaving it, so I've added "if(!mouseArea.containsMouse) return false;" to the beginning of the "containsMouse" property:
property bool containsMouse: {
if(!mouseArea.containsMouse)
return false;
var x1 = width / 2;
var y1 = height / 2;
var x2 = mouseX;
var y2 = mouseY;
var distanceFromCenter = Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2);
var radiusSquared = Math.pow(Math.min(width, height) / 2, 2);
var isWithinOurRadius = distanceFromCenter < radiusSquared;
return isWithinOurRadius;
}

QML: 'Steal' events from dynamic MouseArea

I'm currently trying to implement a drag-to-create mechanism in QML, but I've hit upon a problem where I need the newly created MouseArea to become the target of mouse events even though the original MouseArea hasn't had a mouse button release event yet.
Window {
id: window
width: 300
height: 300
Rectangle {
id: base
width: 20
height: 20
color: "red"
MouseArea {
anchors.fill: parent
property var lastPoint
property var draggedObj: null
function vecLength( vec ) {
return Math.abs( Math.sqrt( Math.pow( vec.x, 2 ) +
Math.pow( vec.y, 2 ) ) );
}
onPressed: lastPoint = Qt.point( mouse.x, mouse.y )
onPositionChanged: {
if ( !draggedObj ) {
var diff = Qt.point( mouse.x - lastPoint.x,
mouse.y - lastPoint.y );
if ( vecLength( diff ) > 4 ) {
draggedObj = dragObj.createObject( window );
}
}
mouse.accepted = !draggedObj;
}
}
}
Component {
id: dragObj
Rectangle {
width: 20
height: 20
color: "blue"
Drag.active: dragArea.drag.active
Drag.hotSpot.x: 10
Drag.hotSpot.y: 10
MouseArea {
id: dragArea
anchors.fill: parent
drag.target: parent
}
}
}
}
If you run this code and try it, you will see that dragging in the red Rectangle causes the creation of the draggable blue Rectangle, but it won't follow the mouse because the red MouseArea is still receiving the mouse events despite the blue MouseArea being above it.
Is there any way of forcing the blue MouseArea to receive the mouse events?
I experienced with this before and had a beginning of solution in my attic.
The trick here is calling QQuickItem::grabMouse() and sending a mouse press event to the newly created object.
Unfortunately I believe this can only be done from c++.
I then created a helper class to expose this functionality to qml:
MouseGrabber.h
#ifndef MOUSEGRABBER
#define MOUSEGRABBER
#include <QObject>
#include <QQuickItem>
#include <QGuiApplication>
#include <QMouseEvent>
class MouseGrabber : public QObject
{
Q_OBJECT
Q_PROPERTY(QQuickItem* target READ target WRITE setTarget NOTIFY targetChanged)
Q_PROPERTY(bool active READ active WRITE setActive NOTIFY activeChanged)
public:
explicit MouseGrabber(QObject *parent = 0) : QObject(parent), m_target(nullptr), m_active(true) { }
QQuickItem* target() const { return m_target; }
bool active() const { return m_active;}
signals:
void targetChanged();
void activeChanged();
public slots:
void setTarget(QQuickItem* target)
{
if (m_target == target)
return;
ungrabMouse(m_target);
if (m_active)
grabMouse(target);
m_target = target;
emit targetChanged();
}
void setActive(bool arg)
{
if (m_active == arg)
return;
m_active = arg;
if (m_active)
grabMouse(m_target);
else
ungrabMouse(m_target);
emit activeChanged();
}
private:
static void grabMouse(QQuickItem* target)
{
if (target)
{
target->grabMouse();
QMouseEvent event(QEvent::MouseButtonPress, QPointF(), Qt::LeftButton, QGuiApplication::mouseButtons(), QGuiApplication::keyboardModifiers());
QGuiApplication::sendEvent(target, &event);
}
}
static void ungrabMouse(QQuickItem* target)
{
if (target)
target->ungrabMouse();
}
QQuickItem* m_target;
bool m_active;
};
#endif // MOUSEGRABBER
This could have been made more convenient by directly calling slots instead of manipulating proprieties, but that's what I had in stock. For example a slot called grabMouseUntilRelease(QQuickItem* item), that grabs the mouse for this item, listen for a mouse release event with installEventFilter and ungrab it automatically.
Register the class so it can be instantiated in QML with qmlRegisterType somewhere in your code :
qmlRegisterType<MouseGrabber>("com.mycompany.qmlcomponents", 1, 0, "MouseGrabber");
After that you can instantiate a MouseGrabber in QML and use it by modifying its proprieties ( target and active ) :
QML
import com.mycompany.qmlcomponents 1.0
Window {
id: window
width: 300
height: 300
Rectangle {
id: base
width: 20
height: 20
color: "red"
MouseArea {
anchors.fill: parent
property var lastPoint
property var draggedObj: null
function vecLength( vec ) {
return Math.abs( Math.sqrt( Math.pow( vec.x, 2 ) +
Math.pow( vec.y, 2 ) ) );
}
onPressed: lastPoint = Qt.point( mouse.x, mouse.y )
onPositionChanged: {
if ( !draggedObj ) {
var diff = Qt.point( mouse.x - lastPoint.x,
mouse.y - lastPoint.y );
if ( vecLength( diff ) > 4 ) {
draggedObj = dragObj.createObject( window );
grabber.target = draggedObj.dragArea; // grab the mouse
}
}
mouse.accepted = !draggedObj;
}
}
}
MouseGrabber {
id: grabber
}
Component {
id: dragObj
Rectangle {
property alias dragArea: dragArea
width: 20
height: 20
color: "blue"
Drag.active: dragArea.drag.active
Drag.hotSpot.x: 10
Drag.hotSpot.y: 10
MouseArea {
id: dragArea
anchors.fill: parent
drag.target: parent
onReleased: {
if (grabber.target === this)
grabber.target = null; // ungrab the mouse
}
}
}
}
}
My other answer is way too much over engineered.
There is no need to steal the mouse events in your situation, you just want to update the position of the dragged blue rectangle in the onPositionChanged handler (or with a Binding or directly inside the Rectangle component).
Wrtiting this in your MouseArea is enough :
onPositionChanged: {
if ( !draggedObj ) {
var diff = Qt.point( mouse.x - lastPoint.x,
mouse.y - lastPoint.y );
if ( vecLength( diff ) > 4 ) {
draggedObj = dragObj.createObject( window );
}
} else { //update the position of the dragged rectangle
draggedObj.x = mouse.x - draggedObj.width/2;
draggedObj.y = mouse.y - draggedObj.height/2;
}
}
onReleased: draggedObj = null

Mouse events not handled within QQuickItem inheritor

I want to write simple Qt Quick app with draggable QQuickItems. The items are well draggeble because of embedded MouseArea in the items. But a problem is that mouse events are not fired into C++ code in virtual overloaded functions. How to solve this problem or maybe there are some examples that I didn't find?
The QML file:
import QtQuick 2.0
import SimpleMaterial 1.0
Rectangle {
width: 320
height: 480
color: "black"
SimpleMaterialItem {
width: parent.width;
height: parent.height / 3;
color: "steelblue"
MouseArea {
anchors.fill: parent
width: 64
height: 64
drag.target: parent
drag.axis: Drag.XandYAxis
}
}
}
The C++ class:
class Item : public QQuickItem
{
Q_OBJECT
Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)
public:
Item()
{
setFlag(ItemHasContents, true);
setFlag(ItemAcceptsDrops, true);
setFlag(ItemAcceptsInputMethod, true);
setAcceptedMouseButtons(Qt::AllButtons);
}
void mousePressEvent(QMouseEvent * event)
{
qDebug("Press"); // NOT CALLED!
}
public:
QSGNode *updatePaintNode(QSGNode *node, UpdatePaintNodeData *)
{
...
}
};
If MouseArea handles mouse event it doesn't pass event to its parent.
You need:
onPressed: {
mouse.accepted = false;
}
in mouse area to let the SimpleMaterialItem handle onPressed event.

Resources