QML: binding loop when calculating Y position - qt

Here my code:
FontLoader { id: font_bold; source: "qrc:/font/MyFont Bold.ttf" }
FontLoader { id: font_medium; source: "qrc:/font/MyFont Medium.ttf" }
FontMetrics { id: fontMetrics }
function getBaseline(fontFamily, fontPixelSize)
{
fontMetrics.font.family = fontFamily;
fontMetrics.font.pixelSize = fontPixelSize;
return fontMetrics.ascent;
}
function getY(desiredY, fontFamily, fontPixelSize)
{
return desiredY - getBaseline(fontFamily, fontPixelSize);
}
Text {
x: 100
y: getY(100, font.family, font.pixelSize)
font.family: font_bold.name
font.pixelSize: 96
text: "foo"
}
Text {
x: 200
y: getY(150, font.family, font.pixelSize)
font.family: font_medium.name
font.pixelSize: 48
text: "foo"
}
The goal is to calculate the actual y position from the desired one, removing the ascent offset of the current font, so the baseline will sit on the desired position.
I got this error for both lines y: getY(...):
QML QQuickText: Binding loop detected for property "y"
I don't see where is the binding loop. The y property is calculated as desired position - font ascent. Both are not related to y itself.

This is because you are using the FontMetrics for both font's, changing the ascent continuously. Remember that you are binding a function to the y-position and thus every time the FontMetrics is set to another font it will trigger an update on the y-position of both Text's.
So, I propose to use two FontMetrics, one for the bold and one for the medium.
FontMetrics { id: fontMetricsBold; font: font_bold }
FontMetrics { id: fontMetricsMedium; font: font_medium }
Text {
x: 100
y: 100 - fontMetricsBold.ascent
font.family: font_bold.name
font.pixelSize: 96
text: "foo"
}
Text {
x: 200
y: 150 - fontMetricsMedium.ascent
font.family: font_medium.name
font.pixelSize: 48
text: "bar"
}
Note, I don't have the fonts so I leave that as an exercise. Maybe configuring the FontMetrics has to be done differently with respect to the FontLoader.

Related

How to display different sizes for inactive items in Tumbler component

I am trying to build an application and for the time settings i am trying to use Tumbler component for this item. I checked on qml documentation for Tumbler but i couldn't find any size settings for Tumbler. I can change the whole Tumbler font size but what i am looking for is changing the sizes for not current items. If i choose time as 12:24:AM i want to see 11,13,23and 25 on some different font sizes. Here is the example
import QtQuick
import QtQuick.Window
import QtQuick.Controls
Rectangle {
width: frame.implicitWidth + 10
height: frame.implicitHeight + 10
function formatText(count, modelData) {
var data = count === 12 ? modelData + 1 : modelData;
return data.toString().length < 2 ? "0" + data : data;
}
FontMetrics {
id: fontMetrics
}
Component {
id: delegateComponent
Label {
text: formatText(Tumbler.tumbler.count, modelData)
opacity: 1.0 - Math.abs(Tumbler.displacement) / (Tumbler.tumbler.visibleItemCount / 2)
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.pixelSize: fontMetrics.font.pixelSize * 1.25
}
}
Frame {
id: frame
padding: 0
anchors.centerIn: parent
Row {
id: row
Tumbler {
id: hoursTumbler
model: 12
delegate: delegateComponent
}
Tumbler {
id: minutesTumbler
model: 60
delegate: delegateComponent
}
Tumbler {
id: amPmTumbler
model: ["AM", "PM"]
delegate: delegateComponent
}
}
}
}
The line of "font.pixelSize: fontMetrics.font.pixelSize * 1.25" is changing the whole component's font size. How can i change font sizes for upper and lower values on Tumbler?
You can use the Tumbler.displacement property that represents how far away this item is from being the current item.
Tumbler {
anchors.centerIn: parent
model: 60
delegate: Text {
text: modelData
font.pixelSize: 16 + Math.abs(Tumbler.displacement) * 10
horizontalAlignment: Text.AlignHCenter
}
}
Try setting your font.pixelSize by checking Tumbler.displacement === 0 as follows:
font.pixelSize: Tumbler.displacement === 0
? fontMetrics.font.pixelSize * 1.25
: fontMetrics.font.pixelSize
After looking at #folibis answer, it inspired me to write this variation which causes the font to diminish in value the further it is away from the current item:
font.pixelSize: fontMetrics.font.pixelSize * 1.25 / ( 1 + Math.abs(Tumbler.displacement))

How to bind to a property of a repeater-generated item outside of the repeater?

I would like to be able to bind to a property of an item generated by Repeater to do something with it, e.g. to show its coordinates. For that purpose I am using itemAt() like this:
ListModel {
id: modelNodes
ListElement { name: "Banana"; x: 100; y: 200 }
ListElement { name: "Orange"; x: 150; y: 100 }
}
Repeater {
id: foo
model: modelNodes
Rectangle {
x: model.x; y: model.y
width: textBox.implicitWidth + 20
height: textBox.implicitHeight + 20
color: "red"
Drag.active: true
Text {
id: textBox
anchors.centerIn: parent
color: "white"
text: model.name + ": " + foo.itemAt(index).x
}
MouseArea {
anchors.fill: parent
drag.target: parent
}
}
}
Text {
id: moo
Binding {
target: moo
property: "text"
value: foo.itemAt(0).x + " -> " + foo.itemAt(1).x
}
}
Inside the delegate this works fine, but when I attempt to use it outside of the Repeater (i.e. to bind moo's text to it), I get the following error:
TypeError: Cannot read property 'x' of null
How to fix this?
The reason the Binding object doesn't work outside of the Repeater is because the Repeater has not constructed its items yet when the binding is being evaluated. To fix this, you can move the binding into the Component.onCompleted handler. Then just use the Qt.binding() function to do binding from javascript (docs).
Text {
Component.onCompleted: {
text = Qt.binding(function() { return foo.itemAt(0).x + ", " + foo.itemAt(1).x })
}
}
You don't.
(or more precisely, you shouldn't)
Delegates shouldn't store state or data, just display it or be able to interact with it.
In your case what you are after is the data stored in the model.
Your solution should be to modify your model in your delegates and get the data from your model if you want.
I've created a small example of what I mean:
import QtQuick 2.15
import QtQuick.Window 2.12
import QtQuick.Controls 2.12
Window {
visible: true
width: 800
height: 640
ListModel {
id: modelNodes
ListElement { name: "Banana"; x: 50; y: 50 }
ListElement { name: "Orange"; x: 50; y: 100 }
}
Row {
anchors.centerIn: parent
spacing: 1
Repeater {
model: 2 // display 2 copy of the delegates for demonstration purposes
Rectangle {
color: "transparent"
width: 300
height: 300
border.width: 1
Repeater {
id: foo
model: modelNodes
Rectangle {
x: model.x; y: model.y
width: textBox.implicitWidth + 20
height: textBox.implicitHeight + 20
color: "red"
DragHandler {
dragThreshold: 0
}
onXChanged: model.x = x // modify model data when dragging
onYChanged: model.y = y
Text {
id: textBox
anchors.centerIn: parent
color: "white"
text: model.name + ": " + foo.itemAt(index).x
}
}
}
}
}
}
Instantiator {
model: modelNodes
delegate: Binding { // the hacky solution to the initial problem.
target: myText
property: model.name.toLowerCase() + "Point"
value: Qt.point(model.x, model.y)
}
}
Text {
id: myText
property point bananaPoint
property point orangePoint
anchors.right: parent.right
text: JSON.stringify(bananaPoint)
}
ListView {
anchors.fill: parent
model: modelNodes
delegate: Text {
text: `${model.name} - (${model.x} - ${model.y})`
}
}
}
I've used a hacky solution to your initial problem with an Instantiator of Bindings, I don't really understand the usecase so that might not be the ideal solution. Here it creates a binding for every element of your model but that's weird. If you only want data from your first row, you may want to do when: index === 0 in the Binding. I've created a third party library to get a cleaner code : https://github.com/okcerg/qmlmodelhelper
This will result in the following code for your outside Text (and allowing you to get rid of the weird Instantiator + Binding part):
Text {
readonly property var firstRowData: modelNodes.ModelHelper.map(0)
text: firstRowData.x + ", " + firstRowData.y
}
Note that my point about not storing data in delegates (or accessing them from outside) still stands for whatever solution you chose.

QML: Show object over non siblings

I have a somehow very hard to solve problem in my QML code. I will summarize it the best I can since it is very long code..
I write a color picker qml file that is called when the user wants to pick a color. This is done in a big rectangle with little rectangles in it evenly distributed that have flat colors to choose from.
I have a parent rectangle, 1 outer repeater and nested in this repeater is another inner repeater that creates little rectangle in a row. The outer repeater places the inner repeaters under another so it fills the rectangle with little rectangles fully, preferably with different colors.
Every little rectangle also has a function that highlights itself with an animation. This animation is a circle that gets bigger than the rectangle itself. This is done so when the user clicks a color from e.g. a color history on the right, it should highlight the corresponding colors rectangle if is there.
Now, the problem:
No matter what z values I use, this animation won't show above the other rectangles. It will get blocked by neighboring rectangles. I have researched and it seems that z values don't account for non siblings, just for all items in a single parent.
Here's some code that leaves out all the unnecessary junk.
To note is that every rectangle has its own animation and mousearea.
import QtQuick 2.12
import QtQuick.Window 2.12
Window {
visible: true
color: 'black'
width: 640
height: 480
title: qsTr("Hello World")
Rectangle {
id: parentRectangle
width: 400
height: 400
property int numberOfBoxesInARow: 5
property int numberOfBoxesInAColumn: 5
Repeater {
id: outerRepeater
model: parentRectangle.numberOfBoxesInARow
Repeater {
id: innerRepeater
model: parentRectangle.numberOfBoxesInAColumn
y: parentRectangle.height / parentRectangle.numberOfBoxesInAColumn * outerIndex
x: 0
height: parent.height / parentRectangle.numberOfBoxesInAColumn
width: parent.width
property int outerIndex: index
Rectangle {
id: individualRectangle
color: Qt.rgba(1 / (outerIndex + 1), 0, 1 / (index + 1), 1)
x: parentRectangle.width / parentRectangle.numberOfBoxesInARow * index
y: outerIndex * parentRectangle.height / parentRectangle.numberOfBoxesInAColumn
width: parentRectangle.width / parentRectangle.numberOfBoxesInARow
height: parent.height / parentRectangle.numberOfBoxesInAColumn
Component.onCompleted: {
console.log("Rectangle at " + outerIndex + "|" + index + " created, width / height: " + width.toFixed(2) + "| " + height.toFixed(2))
}
MouseArea {
anchors.fill: parent
onClicked: {
highlightAnimation.running = true
}
}
Rectangle {
id: highlightCircle
visible: highlightAnimation.running
color: 'white'
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
property real size: 0
width: size
height: size
radius: size/2
}
PropertyAnimation {
id: highlightAnimation
target: highlightCircle
property: 'size'
from: 200
to: 0
duration: 500
}
}
}
}
}
}
Ok, to paint an item over another one you have at least 2 ways:
z (for siblings items only)
creating order (the last created is the highest)
I guess that the second way is preferable for you. So you just need to create the circle item after all others. For example:
Repeater {
id: outerRepeater
Repeater {
id: innerRepeater
...
MouseArea {
anchors.fill: parent
onClicked: {
highlightCircle.item = individualRectangle;
highlightAnimation.running = true;
}
}
}
}
}
Rectangle {
id: highlightCircle
property var item
...
anchors.horizontalCenter: item ? item.horizontalCenter : undefined
anchors.verticalCenter: item ? item.verticalCenter : undefined
}
PropertyAnimation {
id: highlightAnimation
...
}

Binding text from a TextField that belongs to a Repeater

My main.qml:
Window
{
visible: true
width: 640
height: 480
color: "grey"
GridLayout
{
anchors.fill: parent
columns : 2
rows : 2
Repeater
{
id: rectRepeater
model: 3
TextField
{
text: "hi"
}
}
}
Rectangle
{
id: r1
width: 100
height: 100
x: 200
y: 200
border.color: "red"
Text
{
id: t1
}
}
Component.onCompleted:
{
t1.text= rectRepeater.itemAt(0).text
}
}
The Text in the rectangle r1 displays the text at the start, but if I enter new text to the TextField, the rectangle will not be updated. How can I solve this?
A more elegant and maintainable solution is to implement a model that reflects the changes, and then make a binding of the first element with the text that shows Text:
Window{
visible: true
width: 640
height: 480
color: "grey"
ListModel{
id: mymodel
}
Component.onCompleted: {
for(var i=0; i<3; i++){
mymodel.append({"text" : "hi"})
}
}
GridLayout{
anchors.fill: parent
columns : 2
rows : 2
Repeater{
model: mymodel
TextField{
id: tf
onTextChanged: model.text = tf.text
Component.onCompleted: tf.text= model.text
}
}
}
Rectangle{
id: r1
width: 100
height: 100
x: 200
y: 200
border.color: "red"
Text {
id: t1
text: mymodel.count > 1 ? mymodel.get(0).text : ""
}
}
}
What you want, is to create a binding between the two.
Component.onCompleted:
{
t1.text = Qt.binding(function() { return rectRepeater.itemAt(0).text })
}
That being said, we would need to know exactly what you are trying to do, because creating bindings manually is an anti-pattern when not required. It is much better to bind directly, or to use signals.
Do you need the first elements, and the repeater, or is this just an test for you? What is your UI and what are you trying to achieve? This is some context worth giving for a proper answer.
One possible simpler solution
Repeater
{
id: rectRepeater
model: 3
TextField
{
text: "hi"
// See also `onEditingFinished` and `onValidated`
onTextChanged: {
if (index == 0)
t1.text = text
}
}
}
For more details about the property thing, look at my answers from your other question: Qml Repeater with ids

Holding common objects as a list in QML

I am having an architectural problem in the app I am developing in QML. Please consider the following figure:
Some facts about the application:
I need to store an array of elements names, here it is Orange, Apple and Banana.
The amount of elements is fixed and will not change at runtime.
Although there are only 1 array of elements, it should be possible to present the in different graphical forms at the same time. In the example, the elements are once represented as yellow squares, and other time as green triangles. They do not necessarily have to be shown in the same order. But the order also doesn't change at runtime.
I want to avoid unnecessary code copying, thus, wanted to use only 1 list with different graphical representations. I am having problems implementing this however.
I don't quite understand what OP wants to archive, but I guess that model is what you need.
This is a simple example of reusable model:
import QtQuick 2.11
import QtQuick.Window 2.2
import QtQuick.Controls 2.2
Window {
visible: true
width: 600
height: 400
title: qsTr("Model example")
ListModel {
id: myModel;
ListElement { name: "Apple" }
ListElement { name: "Orange" }
ListElement { name: "Banana" }
}
Repeater {
model: myModel
delegate: type1
}
Repeater {
model: myModel
delegate: type2
}
ListView {
model: myModel
delegate: Text { text: name; height: 30; }
width: 100
height: 200
}
ComboBox {
width: 100
y: 200
model: myModel
}
Component {
id: type1
Canvas {
x: 100 + Math.round(Math.random() * 400)
y: Math.round(Math.random() * 100)
rotation: Math.round(Math.random() * 360)
antialiasing: true
width: 100
height: 100
onPaint: {
var ctx = getContext("2d");
ctx.fillStyle = "#00DD00";
ctx.beginPath();
ctx.moveTo(50, 0);
ctx.lineTo(100, 100);
ctx.lineTo(0, 100);
ctx.fill();
}
Text {
anchors.centerIn: parent
text: name
}
}
}
Component {
id: type2
Rectangle {
x: 100 + Math.round(Math.random() * 400)
y: 200 + Math.round(Math.random() * 100)
rotation: Math.round(Math.random() * 360)
antialiasing: true
width: 100
height: 100
color: "orange"
Text {
anchors.centerIn: parent
text: name
}
}
}
}
Welp, can't comment until I have enough reputation, but can't you just use QAbstractListModel for this? Then you could use two path views that determine where the objects go. Here's an example, but you would have your own QAbstractListModel instead of the examples ListModel. The delegate would determine the shape of the items.
The reason to use QAbstractListModel over QML's ListModel is because ListModel is created runtime.

Resources