Create a binding for a value in ListModel QML - qt

I have a ListModel and ListView where I display the notifications a user has. The ListView has an add transition for every time a new notification pops up. Now, I want to add a timestamp (in minutes) to the ListModel to display how old the notification is, but since I add values to the ListModel when a notification is created, I have to manually update the model every minute to change the timestamp, which in turn triggers my add transition. How can I update the timestamp without re-adding the values every time?
property int numNotifications: backend_service.num_notifications
onNumNotificationsChanged: {
notificationModel.clear()
for(var x=0; x<numNotifications; x++) {
var notif = backend_service.notifications.get(x);
notificationModel.insert(0, {"name":notif.name, "time":notif.time})
}
}
Rectangle {
height: 500
width: 0.90 * parent.width
anchors {
top: parent
topMargin: 30
left: parent.left
leftMargin: 45
}
ListView {
anchors.fill: parent
model: notificationModel
delegate: notificationDelegate
spacing: 30
add: Transition {
NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 1000 }
}
}
}
ListModel {
id: notificationModel
}
Component {
id: notificationDelegate
Row {
spacing: 20
Text { text: name; color: "white" }
Text { text: time; color: "white" }
}
}
time is the measure of how old the notification is in minutes (1 minute, 2 minutes, etc.), I have to update that value. That value is updated in backend_service automatically, but the ListModel holds the old value from when it was first added. I want to update that time value without changing the model. Is there a way to do this without updating the model every time, perhaps by creating a binding? I'm open to other ways of accomplishing this as well.

My recommendation is that the model should hold a fixed timestamp, not a relative one. You can then choose to display it relative to current time in your view (delegate).
property date now: new Date()
Timer {
running: true
repeat: true
interval: 60 * 1000 // Every minute
onTriggered: {
now = new Date()
}
}
Component {
id: notificationDelegate
Row {
spacing: 20
Text { text: name; color: "white" }
Text {
color: "white"
text: {
var diff = now - time
// Display relative time, something like this...
if (diff > [1 minute]) {
return "1 minute ago"
} else if (diff > [2 minutes]) {
return "2 minutes ago"
}
else ... // etc.
}
}
}
}

Related

how to implement nested listmodels in qml

Is it possible to implement 3 ListModels (one inside the other) and if yes how could I do it?
The 3 ListModels are for hour day and month accordingly.In other words I want one model for hour inside the model for day inside the model for month and I am going to use them in nested ListView s (3 of them ) to display the hour the day and the month in a calendar. I have made a try below :
ListModel{
id:monthlistModel
ListElement {
monthName:0
daylistModel:[
ListElement {
day:0
hourlistModel: [
ListElement{ hour:0;notes:"" }
]
}
]
}
ListElement {
monthName:1
daylistModel:[
ListElement {
day:1
hourlistModel: [
ListElement{ hour:1;notes:"" }
]
}
]
}
but I could not finish it .
Moreover I have some typeerror issues when I am running my code.The hourlistModel insists to be undefined for my nested listview and I dont no why.
Anyway back to my question , how can I go on with the above listmodel to display 24 hours , 31 days and 12 months ?
I suggest doing this imperatively with javascript rather than declaratively in QML, as it can be more dynamic and brief. One downside is that this is not well documented in my experience.
If you append an array to a ListModel, all of the array elements are converted into ListElements. Further than this, if an array is appended, and that array has nested arrays inside of it, the nested arrays are automatically converted to nested ListModels inside.
Here is a full example:
import QtQuick 2.15
import QtQuick.Window 2.0
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.12
Window {
visible: true
width: 1000
height: 600
ListModel {
id: monthsModel
Component.onCompleted: {
let months = [
{
name: "January",
days: createDays(31) // returns an array, nested arrays become nested ListModels when appended
},
{
name: "February",
days: createDays(28)
},
// add more months etc.
]
append(months) // appending a whole array makes each index into a ListElement at the top level
}
function createDays(dayCount) {
let days = []
for (let i = 0; i < dayCount; i++) {
days.push({
day: i + 1,
hours: createHours()
}
)
}
return days
}
function createHours() {
let hours = []
for (let i = 0; i < 24; i++) {
hours.push({
hour: i,
notes: ""
}
)
}
return hours
}
}
// Visual example code starts here ///////////////
ColumnLayout {
id: monthsColumn
Repeater {
model: monthsModel
delegate: Rectangle {
id: month
color: "pink"
implicitWidth: daysRow.implicitWidth + 10
implicitHeight: daysRow.implicitHeight + 10
RowLayout {
id: daysRow
anchors {
centerIn: parent
}
Text {
text: model.name
}
Repeater {
model: days // refers to the "days" entry in monthsModel.get(<monthIndex>)
delegate: Rectangle {
id: day
color: "orange"
implicitWidth: hoursColumn.implicitWidth + 10
implicitHeight: hoursColumn.implicitHeight + 10
ColumnLayout {
id: hoursColumn
anchors {
centerIn: parent
}
Text {
text: model.day
}
Repeater {
model: hours // refers to the "hours" entry in monthsModel.get(<monthIndex>).get(<dayIndex>)
delegate: Rectangle {
id: hour
color: "yellow"
implicitHeight: 5
implicitWidth: 5
// do something here with model.notes for each hour
}
}
}
}
}
}
}
}
}
}
The output of this shows months in pink, days in orange, and hours in yellow:

QML: Bind loop detected without double assignment

As far as I know the bind loop happens when I try to assign two properties each other. Example:
CheckBox {
checked: Settings.someSetting
onCheckedChanged: {
Settings.someSetting = checked;
}
}
but in my scenario I can't see such a "double assignment". I report here the full code:
import QtQuick 2.7
import QtQuick.Window 2.3
Window {
visible: true;
width: 500
height: 500
Rectangle {
id: main
anchors.fill: parent
color: "black"
property bool spinning: true
property bool stopping: false
Rectangle {
x: 0.5 * parent.width
y: 0.5 * parent.height
width: 10
height: 200
radius: 5
color: 'red'
transformOrigin: Item.Top
rotation: {
if (main.stopping)
{
main.spinning = false;
main.stopping = false;
}
return timer.angle
}
}
Timer {
id: timer
interval: 5
repeat: true
running: true
onTriggered: {
if (main.spinning) angle += 1;
}
property real angle
}
MouseArea {
id: control
anchors.fill: parent
onClicked: {
main.stopping = true;
}
}
}
}
When you click with the mouse you will get the warning:
qrc:/main.qml:17:9: QML Rectangle: Binding loop detected for property "rotation"
I don't see my mistake. I'm using flags (bool variables) to control the execution of my code. I know in this case I can just stopping the timer directly, but the actual program is more complex than this example.
The binding is in the following lines:
rotation: {
if (main.stopping)
{
main.spinning = false;
main.stopping = false;
}
return timer.angle
}
The change of rotation is triggered by the change of main.stopping: let's say that change main.stopping is given by the mouseArea, then it will be called a rotation, but inside this there is an if, and in this you are changing back to main.stopping , where he will call rotation back.
If a property in QML changes all the elements that depend on it will change

QML: How to move an object from a previous set postion using states

I have an object that I want to move from it's previously set position every time that particular state is set. I've tried making a separate property called xPos to get around the binding loop error which is set by the object's new position of x after the state is set, then entering a default state just to be able to switch back to that specific state again since calling the same state does nothing but it doesn't seem to work.
Here is a snippet of my code:
property int xPos: 0
states: [
State {
name: "nextStep"
PropertyChanges {
target: progressBar_Id
x: -1*(progressBar_Id.step.width) + xPos
}
},
State {
name: "previousStep"
PropertyChanges {
target: progressBar_Id
x: progressBar_Id.step.width + xPos
}
},
State {
name: "default"
PropertyChanges {
target: progressBar_Id
x: xPos
}
}
]
transitions: [
Transition {
from: "*"
to: "nextStep"
NumberAnimation {properties: "x"; easing.type: Easing.Linear; duration: 1000}
onRunningChanged: {
if(!running) {
xPos = progressBar_Id.x;
console.info("xPos = " + xPos);
state = "default";
}
}
},
Transition {
from: "*"
to: "previousStep"
NumberAnimation {properties: "x"; easing.type: Easing.Linear; duration: 1000}
onRunningChanged: {
if(!running) {
xPos = progressBar_Id.x;
console.info("xPos = " + xPos);
state = "default";
}
}
}
]
xPos seems to get set the first time from the console output but never applies the new xCoord to the object.
Explicitly specify the id of the item on which you wish to set the state, e.g:
idOfComponent.state = "default"
All QML Items and derivatives have a property called state, so the code needs to be more specific.
Actually came up with a much better alternative using a ListView.
ListView {
id: listView_Id
width: contentWidth
height: bubbleSize
anchors.centerIn: parent
layoutDirection: Qt.RightToLeft
orientation: ListView.Horizontal
spacing: 0
delegate: Item {
width: bubbleSize
height: width
ProgressBubble {
stateText: stateNumber
currentState: state
}
}
model: ListModel { id: progressModel_Id }
}
Another qml file:
progressModel.insert(0, {stateNumber: number++, state: "current"});
But I ran into another problem, is there anyway to change a state within a delegate after it's been added to the ListView?
I've already tried progressModel.set and progressModel.setProperty but doesn't seem to work.
state is a property for qml types, so when you are assigning "state" to "currentState" for "ProgressBubble", its taking the value of state in which " ProgressBubble" is currently present.
Rename "state" to something else like "presentState" and then try.
Moreover id of ListView model(progressModel_Id) and the one used to insert model elements(progressModel) in different file are different, both of them must refer to same id.
After these changes, you can try set property of model. Sample code snippet:
import QtQuick 2.0
Rectangle {
id: root
width: 360
height: 360
property int number: 1
ListView {
id: listView_Id
width: 100 //contentWidth // this creates property binding issue
height: 100 // bubbleSize // I was not sure about this value
anchors.centerIn: parent
layoutDirection: Qt.RightToLeft
orientation: ListView.Horizontal
spacing: 0
delegate: Item {
width: 100 // bubbleSize // I was not sure about this value
height: width
ProgressBubble {
state: "default"
stateText: stateNumber
currentState: presentState
MouseArea {
anchors.fill: parent
onClicked: {
progressModel_Id.set(index,{stateNumber: stateNumber, presentState: "not current"})
}
}
}
}
model: ListModel { id: progressModel_Id }
}
Component.onCompleted:
{
progressModel_Id.insert(0, {stateNumber: number++, presentState: "current"});
}
}

QML/QtQuick Binding delegate's property with ListView's currentIndex

Inside the delegate, I bind Image's source property to ListView's currentIndex which determines which image to load. This works great:
ListView {
id: answerListView
model: 5
currentIndex: -1
delegate: answerDelegate
}
Component {
id: answerDelegate
Item {
width: 100
height: 100
Image {
source: answerListView.currentIndex === index
? "selected.png" : "not_selected.png"
}
MouseArea {
anchors.fill: parent
onClicked: {
answerListView.currentIndex = index
}
}
Component.onCompleted: {
answerListView.currentIndex = 1; // doesn't work!!
}
}
}
Since currentIndex: -1, it will always show not_selected.png. To show selected.png, I change currentIndex in Component.onLoaded inside delegate.
I was expecting image to load selected.png since currentIndex was updated.
What is the correct way and what am I misunderstanding here?
Ok, new guess:
You want to have the posibility to select multiple Items. As currentIndex only stores one value, which is the value you assigned it last, you can use it to mark only one Item.
Therefore you need to find another way to store your selection. You might for example have a property in the delegate: property bool selected: false which you set to true upon selection.
The problem with this solution is, that it only works if all Items are instantiated at all times. As soon as one Item will be destroyed again, the information will be lost, and uppon the next creation, the selection/unselection is undone.
The better way would be to introduce a role in your model, that stores the selection outside of the non-persistant delegates for you:
ListView {
id: answerListView
model: lm
delegate: answerDelegate
width: 100
height: 220
}
ListModel {
id: lm
ListElement { selected: false }
ListElement { selected: false }
ListElement { selected: false }
ListElement { selected: false }
ListElement { selected: false }
}
Component {
id: answerDelegate
Item {
width: 100
height: 100
Image {
anchors.fill: parent
source: model.selected ? "selected.png" : "notselected.png"
}
Text {
text: (model.selected ? 'selected ' : 'notselected ')
}
Component.onCompleted: {
model.selected = true // doesn't work!!
}
MouseArea {
anchors.fill: parent
onClicked: {
model.selected = !model.selected
}
}
}
}
Another option would probably be a ItemSelectionModel, but I don't know atm, how it works.
Otherwise your example works as expected:
The Item with index 1 is shown, and displays the Image selected.png. All other Items are not shown (for the ListView is to small) but if the would be shown, they would show notselected.png for the answerListView.currentIndex is not equal to their index.

Possible to update/refresh Flow QML component when item has been removed?

I have a Flow layout where I add items dynamically on user actions. In the same way I remove these items on user actions. The Flow QML component seems to work as expected until an item is removed. The item itself is removed, but the space it occupied is just blank. My intuition tells me the graphical item itself got deleted, but the view is not updating when items are removed.
Is the dynamical deletion of child items outside the scope of the Flow Component? Is there any other layout that behaves equally? GridLayout seems to be the closest, but it does not automatically wrap child items when the layout is resized.
Is there any non-hack way to enable Flow to rearrange when child item is disabled? If not, and if GridLayout is my best shot, how to make it wrap its child items like Flow does?
The code below demonstrates what I want to achieve:
Item {
id: root
Flow {
id: layout
anchors.fill: parent
Loader { id: loader }
}
MouseArea {
anchors.top: parent.top
height: parent.height / 2
width: parent.width
onClicked: loader.source = "SomeQmlComponent.qml"
}
MouseArea {
anchors.bottom: parent.bottom
height: parent.height / 2
width: parent.width
onClicked: loader.source = ""
}
}
Don't use Loader inside Flow. In your case items are parented to Loader and not to Flow so you lose all advantage of that. In normal way items are added and removed w/o problem:
import QtQuick 2.7
import QtQuick.Window 2.2
Window {
width: 600
height: 600
visible: true
Component {
id: element
Rectangle {
width: Math.round(Math.random() * 100) + 50
height: Math.round(Math.random() * 100) + 50
color: Qt.rgba(Math.random(),Math.random(),Math.random(),1)
}
}
Flow {
id: flow
spacing: 2
anchors.fill: parent
add: Transition {
NumberAnimation { properties: "x,y"; easing.type: Easing.OutBack }
}
move: add
}
Timer {
id: timer
property bool is_add: true
interval: 300
repeat: true
running: true
onTriggered: {
if(timer.is_add) {
element.createObject(flow);
if(flow.children.length > 20) {
timer.is_add = false;
}
} else {
var item = flow.children[0];
item.destroy();
if(flow.children.length <= 1) {
timer.is_add = true;
}
}
}
}
}
#folibis - thanks for the answer, it helped me with a problem I was trying to solve which was to dynamically add elements and have them resize to the screen. I took your example and made the rectangles fill the width and the height is filled by the rectangles with their height being even. So the shrink/expand with the number of rectangles. I reduced it to 4 rectangles for simplicity and made it remove a rectangle at random.
Component {
id: element
Rectangle {
width: flow.width
height: flow.height/flow.children.length
color: Qt.rgba(Math.random(),Math.random(),Math.random(),1)
}
}
Flow {
id: flow
spacing: 2
anchors.fill: parent
add: Transition {
NumberAnimation { properties: "x,y"; easing.type: Easing.OutBack }
}
move: add
}
Timer {
id: timer
property bool is_add: true
interval: 1000
repeat: true
running: true
onTriggered: {
if(timer.is_add) {
element.createObject(flow);
if(flow.children.length > 3) {
timer.is_add = false;
}
} else {
var i = Math.floor(Math.random() * Math.floor(flow.children.length ));
console.log(i)
var item = flow.children[i];
item.destroy();
if(flow.children.length <= 1) {
timer.is_add = true;
}
}
}
}

Resources