QT QML ListView - centralised highlight bar with enlarged text - qt

I'm attempting to implement a 5 line centralised menu using ListView. The text in the highlighted position needs to be larger than the other menu items and should transition smoothly as the menu is scrolled. Ideally there should be 3 different text sizes - central biggest, items next to central slightly smaller and items on the outside even smaller, but just bigger text for the central item is ok.
When menu stops scrolling the centralised item needs to scroll horizontally if bigger than the screen width.
This is for an embedded device that has a rotary encoder for menu navigation. setIndex() is used to set the menu position.
The following code works ok when scrolling slowly but gets a bit confused when scrolling faster. Items outwith the central bar sometimes go big etc.
Seems like a problem that should have already been solved but I can't find an example anywhere.
Here is the code -
import QtQuick 1.1
// Menu
// 5 line menu, highlight bar centralised
Rectangle {
id: window
color: "#00000000"
property int numLines: 5
property alias menuIndex: listView.currentIndex
property int lineHeight: window.height/window.numLines
property alias model: listView.model
clip: true
function setIndex(iIndex) {
var displayedIndex = Math.round((listView.contentY/window.lineHeight + Math.floor(numLines/2)));
//console.log("MENU2: " + displayedIndex + " " + iIndex + " " + Math.abs(displayedIndex - iIndex));
// If we're too far behind current index jump straight to it rather than leaving it to smooth scroll
if (Math.abs(displayedIndex - iIndex) > 15)
listView.positionViewAtIndex(iIndex, ListView.Center);
listView.currentIndex = iIndex
}
function setItemSelected(bSelected) {
scrollingText.setItemSelected(bSelected);
}
// Define a delegate component. A component will be
// instantiated for each visible item in the list.
Component {
id: menuItemDelegate
Item {
id:wrapper
height: window.height/window.numLines
width: window.width
Text {id: wrapperText; text: menuText; width: window.width; height: window.height/window.numLines; font.pixelSize: (wrapper.height * 3)/4; color: "white"; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter}
states: [
State {
name: "Current"
when: wrapper.ListView.isCurrentItem
PropertyChanges { target: wrapper; scale: 1.0 }
PropertyChanges { target: scrollingText; currentText: menuText }
PropertyChanges { target: scrollingText; scrollStart: true }
},
State {
name: "NotCurrent"
when: wrapper.ListView.isCurrentItem == 0
PropertyChanges { target: wrapper; scale: 0.7 }
}
]
transitions: Transition {
ParallelAnimation
{
NumberAnimation { properties: "scale"; duration: 100 }
}
}
}
}
// Highlight bar
Image {id: highlightBar; y: 2 * window.lineHeight; height: lineHeight; width:window.width; source: "Menu_HighlightBar.png"; scale: 1; opacity:1}
// Scrolling text that pops up on top of highlight bar area
Menu2ScrollingText {id: scrollingText; x:0; z:1; y: 2 * window.lineHeight; height: lineHeight; width: window.width}
ListView {
id: listView
anchors.fill: window
//model: cppMenuModel
delegate: menuItemDelegate
highlight: Rectangle { color: "lightsteelblue"; opacity: 0 } // not visible
highlightFollowsCurrentItem: true
preferredHighlightBegin: highlightBar.y
preferredHighlightEnd: preferredHighlightBegin + window.lineHeight
highlightRangeMode: ListView.StrictlyEnforceRange
currentIndex: 0
}
}

Related

qml components disappearing after enabeling layers

I have a Component for an sddm theme. At the moment I use the theme dark sugar as the base theme. The component looks like the following:
Item {
id: hexagon
property color color:"yellow"
property int radius: 30
//layer.enabled: true
//layer.samples: 8
Shape {
//... Here some Positioning and other Stuff
ShapePath {
//... Here some Options and Pathlines
}
}
}
This works fine, but as soon as I uncomment both layer settings the component disappears. Does this happen, because I load the component like this:
Pane {
...
Item {
...
MyComponent {
z: 1
}
}
}
Nor the Pane or the Item use layer but most Components in the Item use the z: 1 property.
As iam_peter says, the default width and height properties of any Item are 0, and layer.enabled sets the size of the offscreen texture to the item size. By default, the scene graph doesn't do any clipping: a child item can populate scene graph nodes outside its parent's bounds. But when you confine the children's rendering to a specific offscreen texture, anything that doesn't fit is clipped. Here's a more interactive example to play with this:
import QtQuick
import QtQuick.Controls
Rectangle {
width: 640
height: 480
Column {
CheckBox {
id: cbLE
text: "layer enabled"
}
Row {
spacing: 6
TextField {
id: widthField
text: layerItem.width
onEditingFinished: layerItem.width = text
}
Label {
text: "x"
anchors.verticalCenter: parent.verticalCenter
}
TextField {
id: heightField
text: layerItem.height
onEditingFinished: layerItem.height = text
}
}
}
Rectangle {
id: layerItem
x: 100; y: 100
border.color: "black"; border.width: 2
layer.enabled: cbLE.checked
Rectangle {
width: 100
height: 100
color: "tomato"
opacity: 0.5
}
Text {
text: "this text will get clipped even when layer size is defined"
}
}
}
You can use renderdoc to see how the rendering is done; for example you can see the texture that is created by enabling the layer.
This is a small reproducible example:
import QtQuick
Window {
width: 640
height: 480
visible: true
title: qsTr("Hello World")
Item {
//width: 200
//height: 200
//layer.enabled: true
Rectangle {
width: 100
height: 100
color: "red"
}
}
}
I suspect that if you don't set a size on the Item on which you want to enable the layer (layer.enabled: true), it will have a size of 0. Hence the offscreen buffer has a size of 0.
As a side note, this works without layer, because the clip property of an Item by default is set to false. So it won't clip to the bounds of its parent.

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
}
}
}
}
}
}
}

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
...
}

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;
}
}
}
}

How to limit the size of drop-down of a ComboBox in QML

I am using a ComboBox in QML and when populated with a lot of data it exceeds my main windows bottom boarder. From googling I have learned that the drop-down list of a ComboBox is put on top of the current application window and therefore it does not respect its boundaries.
Ideally I would want the ComboBox to never exceed the main applications boundary, but I can not find any property in the documentation.
A different approach would be to limit the number of visible items of the drop-down list so that it do not exceed the window limits for a given window geometry. I was not able to find this in the documentation either and I have run out of ideas.
Take a look to the ComboBox source code, the popup is of a Menu type and it doesn't have any property to limit its size. Moreover, the z property of the Menu is infinite, i.e. it's always on top.
If you Find no way but to use the ComboBox of Qt you can create two models one for visual purpose, I will call it visual model, you will show it in your ComboBox and the complete one , it will be the reference model. Items count in your VisualModel wil be equal to some int property maximumComboBoxItemsCount that you declare . you'll need o find a way that onHovered find the index under the mouse in the visualmodel if it's === to maximumComboBoxIemsCount you do visualModel.remove(0) et visualModel.add(referenceModel.get(maximum.. + 1) and you'll need another property minimumComboBoxIemsCount, same logic but for Scroll Up , I dont know if it will work. but it's an idea
I think there is no solution using the built-in component and you should create your own comboBox. You can start from the following code.
ComboBox.qml
import QtQuick 2.0
Item {
id: comboBox
property string initialText
property int maxHeight
property int selectedItem:0
property variant listModel
signal expanded
signal closed
// signal sgnSelectedChoice(var choice)
width: 100
height: 40
ComboBoxButton {
id: comboBoxButton
width: comboBox.width
height: 40
borderColor: "#fff"
radius: 10
margin: 5
borderWidth: 2
text: initialText
textSize: 12
onClicked: {
if (listView.height == 0)
{
listView.height = Math.min(maxHeight, listModel.count*comboBoxButton.height)
comboBox.expanded()
source = "qrc:/Images/iconUp.png"
}
else
{
listView.height = 0
comboBox.closed()
source = "qrc:/Images/iconDown.png"
}
}
}
Component {
id: comboBoxDelegate
Rectangle {
id: delegateRectangle
width: comboBoxButton.width
height: comboBoxButton.height
color: "#00000000"
radius: comboBoxButton.radius
border.width: comboBoxButton.borderWidth
border.color: comboBoxButton.borderColor
Text {
color: index == listView.currentIndex ? "#ffff00" : "#ffffff"
anchors.centerIn: parent
anchors.margins: 3
font.pixelSize: 12
text: value
font.bold: true
}
MouseArea {
anchors.fill: parent
onClicked: {
listView.height = 0
listView.currentIndex = index
comboBox.selectedItem = index
tools.writePersistence(index,5)
comboBoxButton.text = value
comboBox.closed()
}
}
}
}
ListView {
id: listView
anchors.top: comboBoxButton.bottom
anchors.left: comboBoxButton.left
width: parent.width
height: 0
clip: true
model: listModel
delegate: comboBoxDelegate
currentIndex: selectedItem
}
onClosed: comboBoxButton.source = "qrc:/Images/iconDown.png"
Component.onCompleted: {
var cacheChoice = tools.getPersistence(5);
listView.currentIndex = tools.toInt(cacheChoice)
selectedItem = listView.currentIndex
comboBoxButton.text = cacheModel.get(selectedItem).value
}
}
ComboBoxButton.qml
import QtQuick 2.0
Item {
id: container
signal clicked
property string text
property alias source : iconDownUp.source
property string color: "#ffffff"
property int textSize: 12
property string borderColor: "#00000000"
property int borderWidth: 0
property int radius: 0
property int margin: 0
Rectangle {
id: buttonRectangle
anchors.fill: parent
color: "#00000000"
radius: container.radius
border.width: container.borderWidth
border.color: container.borderColor
Image {
id: image
anchors.fill: parent
source: "qrc:/Images/buttonBackground.png"
Image {
id: iconDownUp
source: "qrc:/Images/iconDown.png"
sourceSize.height:20
sourceSize.width: 20
anchors.verticalCenter: parent.verticalCenter
}
}
Text {
id:label
color: container.color
anchors.centerIn: parent
font.pixelSize: 10
text: container.text
font.bold: true
}
MouseArea {
id: mouseArea;
anchors.fill: parent
onClicked: {
container.clicked()
buttonRectangle.state = "pressed"
startTimer.start()
}
}
Timer{
id:startTimer
interval: 200
running: false;
repeat: false
onTriggered: buttonRectangle.state = ""
}
states: State {
name: "pressed"
when: mouseArea.pressed
PropertyChanges { target: image; scale: 0.7 }
PropertyChanges { target: label; scale: 0.7 }
}
transitions: Transition {
NumberAnimation { properties: "scale"; duration: 200; easing.type: Easing.InOutQuad }
}
}
}
I've used it in some software of mine, hence it is possible that It could not work "out of the box". I use it like this:
ComboBox{
id:cacheChoice
initialText: "None"
anchors.top: baseContainer.top
anchors.topMargin: 2
anchors.right: baseContainer.right
maxHeight: 500
listModel: cacheModel
onExpanded: {
cacheChoice.height = 500
}
onClosed: {
cacheChoice.height = 20
}
}
In case you are working with ComboBox from Qt Quick Controls 2, here's the source code for it:
https://github.com/qt/qtquickcontrols2/blob/5.12/src/imports/controls/ComboBox.qml
Based on that, this override of the behavior works to limit the height to something reasonable:
myComboBox.popup.contentItem.implicitHeight = Qt.binding(function () {
return Math.min(250, myComboBox.popup.contentItem.contentHeight);
});
It is possible to access the hidden MenuStyle within the ComboBoxStyle component. There you can use all the things and hidden things you have within a MenuStyle, including its maximum height.
The thing looks roughly like this.
Not pretty but it works well enough.
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.3
import QtQuick.Window 2.2
ComboBox {
id: comboBox
style: ComboBoxStyle {
// drop-down customization here
property Component __dropDownStyle: MenuStyle {
__maxPopupHeight: 400
__menuItemType: "comboboxitem" //not 100% sure if this is needed
}
}
As it came up resonantly in our team, here is a updated version of the idea shown above. The new version restricts the size automatically to the size of your application.
ComboBox {
id: root
style: ComboBoxStyle {
id: comboBoxStyle
// drop-down customization here
property Component __dropDownStyle: MenuStyle {
__maxPopupHeight: Math.max(55, //min value to keep it to a functional size even if it would not look nice
Math.min(400,
//limit the max size so the menu is inside the application bounds
comboBoxStyle.control.Window.height
- mapFromItem(comboBoxStyle.control, 0,0).y
- comboBoxStyle.control.height))
__menuItemType: "comboboxitem" //not 100% sure if this is needed
} //Component __dropDownStyle: MenuStyle
} //style: ComboBoxStyle
} //ComboBox

Resources