Search in Dropdown with python

As far as I understand, a GtkExpression is needed to lookup what to match in the object populating a GtkDropdown Also, it is not possible to create a GtkExpression in python currently.

I read a solution using an ui file, which demonstrated using a GtkStringObject as the expression, somewhat like so:

_user_search_dropdown_ui = """<interface>
  <requires lib="gtk" version="4.0"/>
  <requires lib="Adw" version="1.0"/>
  <object class="GtkDropDown" id="userchooser">
    <property name="enable-search">true</property>
    <property name="expression">
      <lookup type="GtkStringObject" name="string"></lookup>
    </property>
  </object>
</interface>"""

However, I prefer to use a custom object in the model.

I tried to add an empty template for my custom object in the ui file, but that failed with the GtkBuilder saying templates aren’t allowed there.

I also tried to make my object an child class of GtkStringObject but that ended up not matching anything in the search (probably because GtkStringObject needs to be instantiated with new())

# no way to use with dropdown search
class User(Gtk.StringObject):

    def __init__(self, user, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.user = user
        self.string = str(self)


    def get_string(self):
        return self.string


    def __str__(self):
        return self.user.displayname + " (" + str(self.user.uname) + ")"

(the above works when the GListModel is GtkStringObject but not when it’s User)

So my imagination ran out. Any hints?

Also, it is not possible to create a GtkExpression in python currently.

It’s certainly possible to use GtkExpression in Python :slight_smile:

I tried to add an empty template for my custom object in the ui file, but that failed with the GtkBuilder saying templates aren’t allowed there.

You can’t have multiple templates in a single file, but if you inherit your custom object from GObject.Object and you give it a __gtype_name__, it’ll work inside UI files too (granted that the Python file using your template knows about your custom object via for example an import).

In more practical terms, you can make a custom object like so:

from gi.repository import GObject

class User(GObject.Object):
     # Tip: you can set `__gtype_name__ = __qualname__` to avoid redundancy
    __gtype_name__ = "User"

    def __init__(self, user, **kwargs):
        super().__init__(**kwargs)

        self.user = user

    # Needs to be a GObject.Property for Gtk.Expression to use it
    @GObject.Property(type=str)
    def string(self) -> str:
        return f"{self.user.displayname} ({self.user.uname})"

And then your UI can use it like so:

<lookup type="User" name="string"></lookup>
1 Like

It’s definitely possible to use Gtk.Expression instances in Python.

This example shows how to use custom objects and expressions with a DropDown widget:

import gi
import json

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

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


class Country(GObject.Object):
    """A simple class holding a country name and its ISO 3166-1 identifier"""
    __gtype_name__ = 'Country'

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

        self._country_id = country_id
        self._country_name = country_name

    @GObject.Property(type=str)
    def country_id(self) -> str:
        return self._country_id

    @GObject.Property(type=str)
    def country_name(self) -> str:
        return self._country_name

    def __repr__(self) -> str:
        return f"Country(country_id={self.country_id}, country_name={self.country_name})"  # noqa

    def __str__(self) -> str:
        return f"{self._country_name} (ISO 3166-1: {self._country_id})"


class ExampleWindow(Gtk.ApplicationWindow):
    """Our main application window"""
    def __init__(self, app: Gtk.Application):
        super().__init__(application=app, title="DropDown", default_width=300)

        nodes = {}
        with open("/usr/share/iso-codes/json/iso_3166-1.json", "r") as f:
            iso3661 = json.load(f)
            for country in iso3661.get("3166-1", []):
                nodes[country.get("alpha_2").lower()] = country.get("name")

        # Populate a list model with all the countries
        self.model = Gio.ListStore(item_type=Country)
        for n in nodes.keys():
            self.model.append(Country(country_id=n, country_name=nodes[n]))

        # Lexicographical sorter using the Country:country-name property
        self.name_sorter = Gtk.StringSorter()
        self.name_sorter.set_expression(Gtk.PropertyExpression.new(Country, None, 'country_name'))

        # Lexicographical sorter using the Country:country-id property
        self.id_sorter = Gtk.StringSorter()
        self.id_sorter.set_expression(Gtk.PropertyExpression.new(Country, None, 'country_id'))

        # Wrapper model, taking the original model and a sorter object
        self.sort_model = Gtk.SortListModel(model=self.model, sorter=self.name_sorter)

        # The factory object, responsible for creating the row widgets and
        # binding them to a row item
        factory = Gtk.SignalListItemFactory()
        factory.connect("setup", self._on_factory_setup)
        factory.connect("bind", self._on_factory_bind)

        # The main drop down widget, using the sorted model and the factory
        self.dd = Gtk.DropDown(model=self.sort_model, list_factory=factory, hexpand=True)

        # Use the selected-item property to know when a new item has been selected
        self.dd.connect("notify::selected-item", self._on_selected_item_notify)

        # The expression is used when searching, and matches on the country name
        self.dd.set_expression(Gtk.PropertyExpression.new(Country, None, 'country_name'))
        self.dd.set_enable_search(True)

        # The main contents of the window
        main_box = Gtk.Box(spacing=12, orientation=Gtk.Orientation.VERTICAL)
        self.set_child(main_box)

        # Add a radio toggle to switch between two sorting mechanism
        sort_by_name_button = Gtk.ToggleButton(label="Name", active=True)
        sort_by_name_button.connect("toggled", self._on_sort_button_toggled, "name")
        sort_by_id_button = Gtk.ToggleButton(label="Id", group=sort_by_name_button)
        sort_by_id_button.connect("toggled", self._on_sort_button_toggled, "id")

        sort_box = Gtk.Box()
        sort_box.append(sort_by_name_button)
        sort_box.append(sort_by_id_button)
        sort_box.add_css_class("linked")

        box = Gtk.Box(spacing=12, hexpand=True, vexpand=True, valign=Gtk.Align.CENTER)
        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="Sort by:"))
        box.append(sort_box)
        main_box.append(box)

        box = Gtk.Box(spacing=12, hexpand=True, vexpand=True, valign=Gtk.Align.CENTER)
        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)
        main_box.append(box)

    def _get_country_name(self, country: Country) -> str:
        """Return the country name from the Country object"""
        return country.country_name

    def _on_sort_button_toggled(self, button: Gtk.ToggleButton, toggled: str) -> None:
        """
        The radio button toggles the sorter object that affects the sorted
        model used by the drop down
        """
        if toggled == "name" and button.get_active():
            self.sort_model.set_sorter(self.name_sorter)
            print("Sort by: name")
        elif toggled == "id" and button.get_active():
            self.sort_model.set_sorter(self.id_sorter)
            print("Sort by: id")

    def _on_factory_setup(self, factory: Gtk.SignalListItemFactory, list_item: Gtk.ListItem) -> None:
        """Set up the row item: a simple label"""
        label = Gtk.Label()
        list_item.set_child(label)

    def _on_factory_bind(self, factory: Gtk.SignalListItemFactory, list_item: Gtk.ListItem):
        """
        Bind the country-name property on the row item to the label property
        on the row widget. We also use a transformation function to show that
        it is possible to present a different value on the row item.
        """
        label = list_item.get_child()
        country = list_item.get_item()
        country.bind_property("country_name",
                              label, "label",
                              GObject.BindingFlags.SYNC_CREATE,
                              self._transform_to_country)

    def _on_selected_item_notify(self, dropdown: Gtk.DropDown, _) -> None:
        """Print out the selected item"""
        country = dropdown.get_selected_item()
        print(f"Selected item: {country}")

    def _transform_to_country(self, binding: GObject.Binding, _) -> str:
        """Use the string form of the Country object instead of just its label"""
        return str(binding.dup_source())


class ExampleApp(Adw.Application):
    """Our main application"""
    def __init__(self):
        super().__init__()
        self.window = None

    def do_activate(self) -> None:
        if self.window is None:
            self.window = ExampleWindow(self)
        self.window.present()


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

The main issue is that expressions are also used to control the contents of the list, not just the search; and that searching can only use prefix string matching, which makes it less useful for generic object models. This may get fixed in a future version of GTK.

1 Like

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