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.