slint icon indicating copy to clipboard operation
slint copied to clipboard

Modifying ComboBox.model resets current-index/value

Open JakubKoralewski opened this issue 1 year ago • 6 comments

Problem

When changing the model of a ComboBox the selected value is reset. This breaks UX in some cases.

Motivation

I've already hit 2 instances where I'd like to be able to modify the model of a ComboBox. Maybe it's bad practice, but it's what I'd like my user interface to happen. For example with a SpinBox counter I'd like to conjugate the verbs of hours/minutes depending on the count. So that an "hour" becomes "hours" or in my case "godzina" becomes "godziny". In another case I wanted to add an element to a ComboBox.model instead of replacing it, and succeeded by modifying current-index and current-value from a Rust callback and pushing onto the VecModel instead of replacing it. But in one case I have a component, and the Rust solution requires access to a Window, and awkward to call a property setter from Rust code if the ComboBox is inside a component (if I'm doing things right).

Code

import {ComboBox, Switch} from "std-widgets.slint";

export component Demo {
    property <[string]> model-a: ["A", "B", "C"];
    property <[string]> model-b: ["a", "b", "c"];
    property <bool> v: false;

    VerticalLayout {

        s := Switch {
            checked: false;
        }

        ComboBox {
            model: s.checked == true ? model-a : model-b;
        }
    }
}

If that's the expected behavior that's fine, but at least I'd like to be able to save the current-index to a property and be able to restore that index, but I can't seem to be able to do that -- this code behaves the same as the one above:

import {ComboBox, Switch} from "std-widgets.slint";

export component Demo {
    property <[string]> model-a: ["A", "B", "C"];
    property <[string]> model-b: ["a", "b", "c"];
    property <int> current-index: 0;

    VerticalLayout {

        s := Switch {
            checked: false;
            toggled() => {
                c.current-index = root.current-index;
            }
        }
        

        c := ComboBox {
            model: s.checked == true ? model-a : model-b;
            selected(str) => {
                root.current-index = self.current-index;
            }
            init() => {
                self.current-index = root.current-index;
            }
        }
        Text {
            text: root.current-index + " " + c.current-index + " " +c.current-value;
        }
    }
}

same with current-value instead of current-index

JakubKoralewski avatar Aug 23 '24 14:08 JakubKoralewski

I had expected this to work:

import {ComboBox, Switch} from "std-widgets.slint";

export component Demo {
    property <[string]> model-a: ["A", "B", "C"];
    property <[string]> model-b: ["a", "b", "c"];

    VerticalLayout {

        s := Switch {
            checked: false;

            property <int> keeper;

            toggled => {
                self.keeper = c.current-index;
                debug("Before", self.keeper, c.current-index);
                c.model = self.checked ? root.model-a : root.model-b;
                c.current-index = self.keeper;
                debug("After", self.keeper, c.current-index);   
            }
        }
        
        c := ComboBox {
            model: root.model-b;
        }

        Text {
            text: c.current-index + " " + c.current-value;
        }
    }
}

That debug-prints the expected current-index, before and after setting the model, but the ComboBox still resets to index 0.

hunger avatar Aug 23 '24 16:08 hunger

Here is the workaround I mentioned, maybe it helps in some way https://github.com/JakubKoralewski/slint-workaround-5921

JakubKoralewski avatar Aug 23 '24 17:08 JakubKoralewski

This is probably not a widgets problem, but a problem lazily evaluating the bindings...

            toggled => {
                self.keeper = c.current-index;
                debug("Before", self.keeper, c.current-index);
                c.model = self.checked ? root.model-a : root.model-b;
                c.current-index = self.keeper;
                debug("After", self.keeper, c.current-index);   
            }

This bit of code reports that the current-index is as expected in both places. My theory is that current-index is lazily evaluated to its value of 5 by the debug statements.

The model gets evaluated lazily later -- resetting the current-index again as a side effect.

hunger avatar Aug 27 '24 09:08 hunger

I think it's on purpose, the ComboBox resets the current-index there when the model changes:

https://github.com/slint-ui/slint/blob/45a9c7235a1322d377eb567f8637ec37388a90cd/internal/compiler/widgets/common/combobox-base.slint#L49-L51

ogoffart avatar Aug 27 '24 09:08 ogoffart

Of course changing the model resets the current-index. That is expected.

The code above does this (as I would read it naively):

  1. get the current-index (let's say 2)
  2. debug print 2
  3. Set the model, which I expect to set current-index to 0.
  4. set the current-index to 2.
  5. debug print 2
  6. show index 2 in the ComboBox.

What (I think) happens is this:

  1. get the current-index (2)
  2. debug print 2
  3. set the current-index to 2
  4. debug print 2
  5. set the model, resetting the current-index to 0.
  6. show index 0 in the ComboBox

I find that surprising.

hunger avatar Aug 27 '24 09:08 hunger

Doing some more experiments:

I can debug print the c.current-index and c.model[c.current-index] right after setting the model. That prints the unchanged index and the value taken from the new model.

So the issue seems to be that the model reset delays resetting the current-index to 0 somehow.

hunger avatar Aug 27 '24 10:08 hunger