Migrate from ComboBoxText to ComboRow(DropDown)

Hey folks,

I have a Python app which was using a ComboBoxText in it’s preferences page to bind it with a gsettings. Here is my handler for it:

def on_ActionExitNode_realize(combo):
    nodes = {
        _("Austria"): "au",
        ...
        _("United Kingdom"): "uk",
        _("United States"): "us"}
    combo.append("ww", _("Auto (Best)"))
    for node in sorted(nodes.keys()):
        combo.append(nodes[node], node)
    dconf.bind("exit-node", combo, "active-id", Gio.SettingsBindFlags.DEFAULT)

I’ve read that using ComboBox is not recommended anymore and the replacement is DropDown. So I decided to use AdwComboRow which is mirror of DropDown in libadwaita.

I tried using StringList as model, but it doesn’t support IDs for items and I don’t know how to bind the id with my gsettings entry.

Here is my incomplete try which shows the list, but does nothing else:

def on_exitcountry_realize(combo):
    nodes = {
        _("Austria"): "au",
        ...
        _("United Kingdom"): "uk",
        _("United States"): "us",
    }
    model = Gtk.StringList()
    model.append(_("Auto (Best)"))
    for node in sorted(nodes.keys()):
        model.append(node)
    combo.set_model(model)

Any advices?

You will need to create a GObject that contains the id and the (localised) name and exposes them as properties:

import gi

from gi.repository import Gio, GObject


class Country(GObject.Object):
    __gtype_name__ = 'Country'

    def __init__(self, country_id, country_name):
        super().__init__()

        self._country_id = country_id
        self._country_name = country_name

    @GObject.Property
    def country_id(self):
        return self._country_id

    @GObject.Property
    def country_name(self):
        return self._country_name

Then you can use the object to populate the Gio.ListStore:

nodes = {
    "au": "Austria",
    "uk": "United Kingdom",
    "us": "United States",
}

model = Gio.ListStore(item_type=Country)
for n in nodes.keys():
    model.append(Country(country_id=n, country_name=nodes[n]))

At this point, you have a GObject that contains the data. In order to map the objects inside the model to a widget inside the drop down, you will need to use a list item factory and Gtk.DropDown.set_factory; the factory gets a reference to the item in the row, and creates a widget that maps to it:

import gi

gi.require_version('Adw', '1')
gi.require_version('Gtk', '4.0')

from gi.repository import Adw, Gio, GObject, Gtk

class ExampleWindow(Gtk.ApplicationWindow):
    def __init__(self, app):
        super().__init__(application=app, title="DropDown")
        
        nodes = {
            "au": "Austria",
            "uk": "United Kingdom",
            "us": "United States",
        }

        # Populate the model
        self.model = Gio.ListStore(item_type=Country)
        for n in nodes.keys():
            self.model.append(Country(country_id=n, country_name=nodes[n]))
        
        # Set up the factory
        factory = Gtk.SignalListItemFactory()
        factory.connect("setup", self._on_factory_setup)
        factory.connect("bind", self._on_factory_bind)
        
        self.dd = Gtk.DropDown(model=self.model, factory=factory, hexpand=True)
        self.dd.connect("notify::selected-item", self._on_selected_item_notify)
        
        box = Gtk.Box(spacing=12, hexpand=True, vexpand=True)
        box.props.margin_start = 12
        box.props.margin_end = 12
        box.props.margin_top = 6
        box.props.margin_bottom = 6
        box.append(Gtk.Label(label="Select Country:"))
        box.append(self.dd)
        self.set_child(box)

    # Set up the child of the list item; this can be an arbitrarily
    # complex widget but we use a simple label now
    def _on_factory_setup(self, factory, list_item):
        label = Gtk.Label()
        list_item.set_child(label)

    # Bind the item in the model to the row widget; again, since
    # the object in the model can contain multiple properties, and
    # the list item can contain any arbitrarily complex widget, we
    # can have very complex rows instead of the simple cell renderer
    # layouts in GtkComboBox
    def _on_factory_bind(self, factory, list_item):
        label = list_item.get_child()
        country = list_item.get_item()
        label.set_text(country.country_name)

    # The notify signal is fired when the selection changes
    def _on_selected_item_notify(self, dropdown, _):
        country = dropdown.get_selected_item()
        print(country)


class ExampleApp(Adw.Application):
    def __init__(self):
        super().__init__()
        self.window = None
        
    def do_activate(self):
        if self.window is None:
            self.window = ExampleWindow(self)
        self.window.present()


app = ExampleApp()
app.run([])

IMPORTANT NOTE: There is a caveat when it comes to GtkDropDown (and all the other list widgets in GTK4) and Python: you cannot use the GtkExpression API in your code, as PyGObject does not know how to handle instanced types that do not inherit from GObject, like GtkExpression.

If you want to bind this to GSettings, you can use the selected-item property of the GtkDropDown to update the settings storage.

2 Likes

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.