QML ListView - change all but current item - qt

I'm following this tutorial (without the flickable content in each entry) for Qt 4.8 while using Qt 5.7 with QtQuick 2.0. The way the ListView there works is as follows:
User clicks on item in list
Alternative (detailed) view of item is displayed
User has to click on Close button in detailed view to reset the state of entry to its default compact view.
This leads to a clutter where at some point if the user clicks on all items in which case all will be shown in their full view. Having the user click on the Close button every time he/she opens a detailed view also is (omho) not that handy.
I've altered the entry to close when the user clicks on the view. I'm also trying to prevent this clutter and achieve a more (omho) flowing behaviour:
User clicks on item in list
Alternative view of item is displayed
User clicks on detailed view to reset state of entry to its default compact view OR
User clicks on another entry and all currently in detailed view entries are reset to their compact view
Currently I'm looping through my ListView's contentItem.children[loop_index] and setting the state to "" ("Details" = show detailed view | "" = show compact view). Due to the way ListView works (loading/unloading delegates on demand) this is quite unreliable and I often get an undefined reference when I try to access the state of other delegates. The following MouseArea, which I'm using to do all that, is part of every delegate:
// state is a QML `State` that is bound to the delegate (see below for the details on it)
MouseArea {
anchors.fill: background
onClicked: {
// Iterate through all other entries and close them
for (var entry = 0; entry < listView.count; ++entry) {
if(listView.contentItem.children[entry] !== gestureEntry) {
console.log("Hide other element");
listView.contentItem.children[entry].state = ""; // IT FAILS HERE (SOMETIMES)
}
}
// Change view of current entry
if(gestureEntry.state === "Details") {
gestureEntry.state = "";
console.log("Hiding details")
}
else {
gestureEntry.state = "Details";
console.log("Showing details");
}
}
}
with state being a delegate's state:
states: State {
name: "Details"
PropertyChanges { target: background; color: "white" }
PropertyChanges { target: gestureImage; width: 130; height: 130 } // Make picture bigger
PropertyChanges { target: gestureEntry; detailsOpacity: 1; x: 0; y: 0 } // Make details visible
PropertyChanges { target: gestureEntry; height: listView.height } // Fill the entire list area with the detailed view
}
I'm thinking that the state information can be stored inside the ListModel itself making it possible to iterate through the model's contents (which are always there unlike the contents of the delegates) however I don't know how to automatically update my list (and the currently visible/invisible delegates) when an entry changes in the model. From what I've found so far it seems not possible to do that since the ListView doesn't actively monitor its ListModel.
Is this indeed the case? If yes, then is it possible to go around this problem in a different way?

Why don't you use the currentIndex property of your ListView?
Just modify your delegate like this:
Item {
id: gestureEntry
...
state: ListView.isCurrentItem?"Details":""
...
MouseArea {
anchors.fill: background
onClicked: {
if(listView.currentIndex == index)
listView.currentIndex = -1
else
listView.currentIndex = index
}
}
}
EDIT:
The only issue with the solution above is that - upon loading - an entry in the ListView is preselected which automatically triggers the detailed view of that entry. In order to avoid that the following needs to be added to listView:
Component.onCompleted: {
listView.currentIndex = -1;
}
This ensures that no entry will be preselected.

guess it is an issue because you stored a state in your delegate. You should not do this as described in the delegate-property (Link), because the delegates get reused when they get out of view.
At least you should use a when: ListView.isCurrentItem in the State and depend on a value of the ListView. So only your current delegate is maximized. Then in the MouseArea only set `ListView.view.currentIndex = index'. Don't change the state manually in the function!
I ran in the same trouble, removed the states completely and just used the attached property ListView.isCurrentItem. But binding the state to a Value from the ListView should also work, because it's not stored in the delegate.
Minimal example:
import QtQuick 2.0
Item {
width: 800
height: 600
ListView {
id: view
anchors.fill: parent
model: 3
spacing: 5
currentIndex: -1
delegate: Rectangle {
id: delegate
color: ListView.isCurrentItem ? "lightblue" : "green" // directly change properties depending on isCurrentItem
height: 100
width: 100
states: State {
name: "maximized"
when: delegate.ListView.isCurrentItem // bind to isCurrentItem to set the state
PropertyChanges {
target: delegate
height: 200
}
}
MouseArea {
anchors.fill: parent
//onClicked: delegate.ListView.view.currentIndex = model.index // if only selection is wanted
onClicked: {
//console.debug("click");
if (delegate.ListView.isCurrentItem)
{
delegate.ListView.view.currentIndex = -1;
}
else
{
delegate.ListView.view.currentIndex = model.index;
}
}
}
Text {
anchors.centerIn: parent
text: index
}
}
Text {
text: "CurrentIndex: " + parent.currentIndex
}
}
}

Related

Multiple modes in a Qt Quick QML Window?

Suppose I have a QML Window that contains a bunch of different controls:
Window {
...
TextEdit { ... }
CheckBox { ... }
Button { ... }
etc
}
Now I want my window to change "mode" on a certain event, and display a completely different set of controls.
Imagine its part of a multi-screen form. The current state is the first page of the form. When the user clicks "Next" button it goes to page 2 of the form. I want to add a new set of controls that represents page 2.
What is the correct way to organize this in QtQuick / QML ?
A common way to do that is with a StackView. The organization would be something like this:
Window {
StackView {
id: stackView
initialItem: page1
}
Item {
id: footerItem
// Maybe add other buttons here too
Button {
id: nextBtn
text: "Next"
onClicked: {
stackView.push(page2);
}
}
}
Component {
id: page1
Page1 {
// Define this in separate Page1.qml file
// This is where your page 1 controls go.
}
}
Component {
id: page2
Page2 {
// Define this in separate Page2.qml file
// This is where your page 2 controls go.
}
}
}
I would probably implement a mode changing view or form with "Back" and "Next" buttons using StackLayout. The StackLayout class provides a stack of items where only one item is visible at a time. You go to the next or previous mode by updating currentIndex.
StackLayout {
id: layout
anchors.fill: parent
currentIndex: 1
Rectangle {
color: 'teal'
implicitWidth: 200
implicitHeight: 200
}
Rectangle {
color: 'plum'
implicitWidth: 300
implicitHeight: 200
}
}

Qt QML ComboBox override wheel events

Is there any way to override ComboBox MouseArea to ignore wheel event instead of changing current index? ComboBox itself has no option to change wheel focus behaviour. So far I've tried to override onWheel from CB MouseArea with code like this:
ComboBox {
Component.onCompleted: {
for (var i = 0; i < combobox_ctrl.children.length; ++i) {
console.log(combobox_ctrl.children[i])
console.log(combobox_ctrl.children[i].hasOwnProperty('onWheel'))
if (combobox_ctrl.children[i].hasOwnProperty('onWheel')) {
console.log(combobox_ctrl.children[i]['onWheel'])
combobox_ctrl.children[i]['onWheel'] = function() { console.log("CB on wheel!") }
//combobox_ctrl.children[i]onWheel = function() { console.log("CB on wheel!")
//combobox_ctrl.children[i].destroy()
}
}
}
}
But I get
TypeError: Cannot assign to read-only property "wheel"
Did anyone was able to disable wheel events on ComboBox in Qml?
// EDIT
for example in Slider control I was able to remove wheel event handling like this:
Slider {
Component.onCompleted: {
for (var i = 0; i < slider.children.length; ++i) {
console.log(slider.children[i])
if (slider.children[i].hasOwnProperty("onVerticalWheelMoved") && slider.children[i].hasOwnProperty("onHorizontalWheelMoved")) {
console.log("Found wheel area!")
slider.children[i].destroy()
}
}
}
}
But in slider WheelArea is not responsible for handling "click" events.
You can place MouseArea over ComboBox and steel wheel event.
ComboBox {
anchors.centerIn: parent
model: [ "Banana", "Apple", "Coconut" ]
MouseArea {
anchors.fill: parent
onWheel: {
// do nothing
}
onPressed: {
// propogate to ComboBox
mouse.accepted = false;
}
onReleased: {
// propogate to ComboBox
mouse.accepted = false;
}
}
}
It's not currently possible, as ComboBox is not derived from MouseArea, but FocusScope, which has no support for these kinds of events.
A similar problem was mentioned in a suggestion recently:
Disable mouse wheel scroll event on QtQuick.Controls
If you're after a hacky way of doing it, it seems like the only option you have left is to apply a patch to ComboBox.qml that removes the onWheel handler:
diff --git a/src/controls/ComboBox.qml b/src/controls/ComboBox.qml
index 4e29dfe..3413cac 100644
--- a/src/controls/ComboBox.qml
+++ b/src/controls/ComboBox.qml
## -407,13 +407,6 ## Control {
popup.toggleShow()
overridePressed = false
}
- onWheel: {
- if (wheel.angleDelta.y > 0) {
- __selectPrevItem();
- } else if (wheel.angleDelta.y < 0){
- __selectNextItem();
- }
- }
}
Another alternative that doesn't involve modifying Qt code would be to add an intermediate MouseArea above ComboBox's, and then somehow only forward specific events through to ComboBox's MouseArea. Or, create a custom C++ item that does the equivalent. You may have more control that way.
Ok. After hacking around I've managed to come with solution that is acceptable for me but may introduce some regressions in some situations. pressed and hovered properties are no longer usable
import QtQuick.Controls.Private 1.0
ComboBox {
Component.onCompleted: {
for (var i = 0; i < combobox_ctrl.children.length; ++i) {
if (combobox_ctrl.children[i].hasOwnProperty('onWheel') && combobox_ctrl.children[i] !== mouseArea) {
combobox_ctrl.children[i].destroy()
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
onPressed: {
if (combobox_ctrl.activeFocusOnPress)
forceActiveFocus()
if (!Settings.hasTouchScreen)
combobox_ctrl.__popup.toggleShow()
}
onClicked: {
if (Settings.hasTouchScreen)
combobox_ctrl.__popup.toggleShow()
}
}
}
This way we can mimic mouse area that was originaly inside the ComboBox. Popup is shown as it was (at least I didn't see any regresion in it yet). However two properties are inaccesible right now
I created a separate file called NonScrollingComboBox.qml with the following code following this post: https://stackoverflow.com/a/33080217/969016
Now I can just use NonScrollingComboBox as a component instead of ComboBox on places where I don't want the mouse scroll to change the value
import QtQuick 2.0
import QtQuick.Controls 1.4
ComboBox {
id: combobox_ctrl
Component.onCompleted: {
for (var i = 0; i < combobox_ctrl.children.length; ++i) {
if (combobox_ctrl.children[i].hasOwnProperty('onWheel')
&& combobox_ctrl.children[i] !== mouseArea) {
combobox_ctrl.children[i].destroy()
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
onPressed: {
if (combobox_ctrl.activeFocusOnPress)
forceActiveFocus()
combobox_ctrl.__popup.toggleShow()
}
onClicked: {
combobox_ctrl.__popup.toggleShow()
}
}
}
usage:
NonScrollingComboBox {
anchors.verticalCenter: parent.verticalCenter
model: ["item one", "item 2"]
}
This seems to apply only to Qt Quick Controls 1 ComboBox. On Qt Quick Controls 2 ComboBox the wheel mouse event is not enabled by default and can be enabled manually by setting to true the property wheelEnabled (documented in the base class Control). Also the combobox won't keep a "focus" on mouse events so you can freely use the wheel on other mouse areas by just entering them.

How to vertical scroll to a specific item

I have a ListView including a lot of radio buttons. The list is bigger than the visible area. One of the radio buttons is checked. Sometimes, if the selected radio button is outside the visible area I want to scroll to it:
ScrollView {
anchors.fill:parent
ListView {
anchors.fill: parent
model: valuesList
delegate: RadioButton {
id: radioBtn
//check of value is index type and do the corresponding checked? test
checked: valueIsIndex ? (parseInt(valueFromParent) == index ? true : false) : (valueFromParent == valueString ? true : false)
onClicked: {
root.selected(valueString, index)
}
Component.onCompleted: {
if(checked)
//Here i want to scroll the list to display this radiobutton
}
}
}
}
Any ideas howto to get the list scrolled? I've played around a lot of with hightlights and contentY but nothing worked.
I've use the ScrollView around the ListView to automatically get the systems scrollbars on the desktop. On mobile devices i have just the flickable ListView.
EDIT
I get it on the way with the help of BaCaRoZzo. Here is my current working example:
ScrollView {
id: scrollView
anchors.fill:parent
property int yOfCheckedRadioButton: 0
ListView {
id:listView
anchors.fill: parent
spacing: Math.round(appWindow.height*0.05)
model: internalValuesList
delegate: RadioButton {
id: radioBtn
//check of value is index type and do the corresponding checked? test
checked: checktest()
style: MyRadioButtonStyle {
myRadioBtn: radioBtn
labelString: value
}
Component.onCompleted: {
//set the position of the checked RadioButton to scroll to it later onContentHeightChange
if(checked) {
var checkedRadioBtnPositionY = Math.round((radioBtn.height + listView.spacing) * index - radioBtn.height * 1.5)
if( checkedRadioBtnPositionY > 0)
scrollView.yOfCheckedRadioButton = checkedRadioBtnPositionY
else
scrollView.yOfCheckedRadioButton = 0
}
}
}
onContentHeightChanged: {
//scroll to the checked RadioButton
contentY = scrollView.yOfCheckedRadioButton
}
}
}
I recall problems with scroll before Qt 5.4 when I found a workaround like:
ScrollView {
anchors.fill: parent // mind how you stretch it
contentItem:
Flow {
id: flow
spacing: 10 // mind gaps
width: parent.parent.width - 20 // select proper width
// Put anything you would like to scroll in here
// Mind that Flow positions items one after another
// left to right, top to bottom
// You can also try containers other than Flow
// but whether it works or not may depend on Qt version
ExclusiveGroup { id: tabPositionGroup }
RadioButton {
text: "RB1"
checked: true
exclusiveGroup: tabPositionGroup
}
RadioButton {
text: "RB2"
exclusiveGroup: tabPositionGroup
}
}
}
Whether ScrollView needs an explicit contentItem or not is another matter and it certainly may not need it but that does not hurt if the SrollView needs to resolve what it actually scrolls.

QML: public variable

I have two QML files.
In First.qml I can make visible Second.qml. In Second.qml I have selectedParts variable.
I want to set selectedParts to value 1 always, when I make Second.qml visible. That works only when I load
Second.qml for first time. If I make Second.qml invisible and then visible, selectedParts value is 2. Is there anyway
to make selectedParts variable public and set its value always when I click on myImage?
First.qml
Item {
Image {
id: myImage
MouseArea{
anchors.fill: parent
onClicked: {
second.visible = true
...
}
}
}
}
Second.qml
Item {
property int selectedParts: 1
Image {
id: myImage2
MouseArea{
anchors.fill: parent
onClicked: {
selectedParts = 2
...
}
}
}
}
QML public variable? Look up for MessageBoard in Defining QML types from C++. We are using that approach. All you need is to create C++ MessageBoard object, put some data in there and reference it via the QML context given to every QML root object:
m_quickView.engine()->rootContext()->setContextProperty("myMsgBoard", MyQmlMsgBoard::instance());
And in QML:
Rectangle {
id: topRect
scale: myMsgBoard.scale // or anywhere in QML
// ....
}
Of course that "message board" C++ object exposes to QML something like:
Q_PROPERTY(qreal scale READ scale CONSTANT);
I solved my problem by adding back button into the Second.qml file. And in this button I put statement selectedParts = 1.

QML iterating over custom elements and calling function

I have a custom element called MenuButton:
import QtQuick 1.1
import VPlay 1.0
Image {
property alias text: buttontext.text
property alias mouseArea: area
property alias fontBold: buttontext.font.bold
property alias textSize: buttontext.font.pixelSize
id: button
source: "img/cloudButton.png"
opacity: 1
Text {
id: buttontext
color: "black"
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 50
font.bold: true
}
MouseArea {
id: area
anchors.fill: parent
onPressed: button.opacity = 0.7
onReleased: button.opacity = 1.0
onCanceled: button.opacity = 1.0
}
function doStuff {
// do something here
}
width: 200
height: 60
}
Now, in my main View, I have a Column with 5 of those MenuButtons. And I want to iterate over them and call the function doStuff(). How do I do that?
I tried with column.childAt(i) and stuff like that, nothing worked.
MainView.qml
Rectangle {
width: 480; height: 320
// HERE IS MY PROBLEM, how do I iterate over all my elements in the column?
function update() {
for(var i = 0; i < 5; i++) {
column.childAt(i).doStuff(); // THIS IS WHAT I WANT TO DO
}
}
Column {
id: column
spacing: 5
anchors.centerIn: parent
Repeater {
id: repeater
model: 5
MenuButton {
id: levelbutton
text: "Level " + (modelData+1);
source: "img/cloud4.png"
}
}
}
}
The problem is in my update function in the MainView.qml
I dont know how to iterate over the elements and call the doStuff() function.
You may use Component.onCompleted attached signal, like this:
import QtQuick 1.0
Rectangle {
height: 600
width: 600
Repeater {
model: 5
Item {
Component.onCompleted: console.log('Component ' + index + ' completed!')
}
}
}
However, please consider, that this imperative operation is not good, because it will be called all time after model update. Probably you have problem X and asking how to get Y, that (you think) will solve your X?
From what I've seen in the source code in the QDeclarativePositioners class you cannot access the children elements!
But you could turn the way you call your doStuff() method: when do you want it to be called? After some time has passed (then add a Timer element to your MenuButton), or when a signal occurs? In the latter case, you could use the Connections element and listen to the signal beign emitted in your calling qml file where you use the Column and Repeater.
Cheers, Chris
You can access the children of an element via the children property that exists on all QtObjects. It contains an array of child elements and can be freely acccessed in javascript.
eg. element.children[0].doStuff()
Generally speaking, you should avoid actions that require you to manually iterate over the children. However, if you are attempting to write code that generically calls something on each of the children you do not always have a choice.

Resources