Search code examples
qmlproperty-binding

Controling an Item "enabled" property in a repeater from a checkbox in another repeater


This sounded as an easy task, but it doesn't work like I expected.

I've got one repeater with a checkbox, laid out vertically. Then I've a grid of Combo-boxes. I'd like to have the combo-boxes disabled when their corresponding checkbox is checked.

This is an extract from the code:

Repeater {
    id: idChkUseChordNotes
    model: _max_patterns

    SmallCheckBox {
        onClicked: {
            console.log("Clicked at " + index + "!! ==> " + idChkUseChordNotes.itemAt(index).checked);
            // attempt to force a refresh of the idStepNotes repeater => No effect
            //resetP=false;
            //resetP=true;
        }
    }
}

Repeater {
    id: idStepNotes
    model: getPatterns(resetP)

    StackLayout {
        width: parent.width
        currentIndex: modeIndex()

        property int stepIndex: index % _max_steps
        property int patternIndex: Math.floor(index / _max_steps)

        Label {
            text: "dummy"
        }

        ComboBox {
            id: lstGStep
            property var step: patterns[patternIndex * _max_steps + stepIndex]
            editable: false
            enabled: !idChkUseChordNotes.itemAt(patternIndex).checked
            model: _ddGridNotes
            currentIndex: find(step.degree, Qt.MatchExactly)
            onCurrentIndexChanged: {
                step.degree = model[currentIndex];
                // debug
                console.log(idChkUseChordNotes.itemAt(patternIndex).checked); // <-- working fine
            }
        }
    }
}

Though the combobox has access to the right checkbox (see "debug" in the "onCurrentIndexChanged") checking on&off the checkbox doesn't affect the combox-box property.

I know QML has some limitations with the level property-binding. I don't know if this is the issue here.

IMPORTANT: I'm limited to these versions : QtQuick 2.9 and QtQuick.Controls 2.2


Solution

  • You should be seeing a bunch of warning like this: TypeError: Cannot read property 'checked' of null. It is very surprising that (according to your comments) you don't. The checkboxes are simply not yet instantiated at the time the binding gets evaluated. Fixing this requires procedural code in various Component.onCompleted handlers, as you have already noted in the comments. This is, however, not the way QML is intended to be used.

    Instead of thinking about your problem in terms of UI elements interacting with eachother, consider a more data-centric approach. Both the checkboxes and the comboboxes are really just different views or aspects of the same set of data. Every data point has a boolean aspect (or property in Qt terms) "Use Chord Notes" and another aspect ("degree"?) with another meaning. The checkboxes are visual and interactable representations of the boolean aspect and the comboboxes do the same for the other one, but using the first to decide interactability.

    The Qt/QML way of representing such a structure in code is through views, bindings and models. I have mocked up an example of how this may look using your snippet as a starting point. Notice how there is no declarative code (except the button's onClicked handler, but that is for illustrative purposes).

    A couple of thing to note:

    • The model is a QML ListModel here for the sake of simplicity. In a real-world application this should most likely be implemented in C++.
    • Both the checkbox and the combobox use two-way bindings to ensure their state is always up-to-date with the model, even when the model changes due to something else. This is illustrated with the buttons in the third column. If you don't need this, the Binding objects can be replace with simple binding expressions: checked: useChordNotes.
    • The currentIndex binding on the combobox uses the indexOf method of the JS array used as a model here. In your code this must be replaced with something that works for whatever type _ddGridNotes has. ComboBox.find() does not work here because the initialization order. At the moment the Binding is evaluated for the first time after construction, the combobox has not yet instantiated its items thereby always returning -1 for any call to find. This is a similar problem to your original one.

    Code

    import QtQuick 2.9
    import QtQuick.Layouts 1.2
    import QtQuick.Controls 2.2
    import QtQuick.Window 2.9
    
    Window {
        width: 640
        height: 480
        visible: true
    
        ListModel {
            id: patterns
    
            ListElement {
                useChordNotes: false
                gridNote: "1"
            }
    
            ListElement {
                useChordNotes: true
                gridNote: "3"
            }
    
            ListElement {
                useChordNotes: false
                gridNote: "2"
            }
        }
    
        RowLayout {
            anchors.fill: parent
    
            ColumnLayout {
                Repeater {
                    id: idChkUseChordNotes
    
                    model: patterns
    
                    CheckBox {
                        // Use a two-way binding here, in case something else in the program changes the model (e.g. loading a file)
    
                        // #1: change model value on user interaction
                        onCheckedChanged: useChordNotes = checked
    
                        // #2: update value on model change from another source
                        Binding on checked {
                            value: useChordNotes
                        }
                    }
                }
            }
    
            ColumnLayout {
                Repeater {
                    id: idStepNotes
    
                    model: patterns
    
                    ComboBox {
                        id: lstGStep
    
                        enabled: !useChordNotes
                        model: ["1", "2", "3"]
    
                        onActivated: gridNote = currentValue
    
                        Binding on currentIndex {
                            // Replace this to something that works with whatever type lstGStep.model might be
                            value: lstGStep.model.indexOf(gridNote)
                        }
                    }
                }
            }
    
            ColumnLayout {
                Repeater {
                    model: patterns
    
                    Button {
                        text: "useChordNotes: " + (useChordNotes ? "true" : "false") + "; gridNote: " + gridNote
                        onClicked: {
                            gridNote = "3";
                            useChordNotes = !useChordNotes;
                        }
                    }
                }
            }
        }
    }