Example of Gtk.Dropdown with search enabled without Gtk.Expression

Hi there!

Few months ago, when I started learning GTK4 with Python bindings, I wanted to use Gtk.DropDown with the search enabled. But I couldn’t achieve it because, as it was pointed out in this topic, Gtk.Expression is not working with Python yet.

Then, a few days ago, this other topic pointed me out in the right direction to achieve my goal.

The solution pass by getting the Gtk.SearchEntry widget and connect a signal to check when the content changes.
For the Gtk.DropDown, create a model and also a filter model wrapping it. Then, create a Gtk.CustomFilter which is triggered each time the Gtk.SearchEntry changes.

I know this workaround is a bit lame, but as far as I tested it, it is working as expected. However, I wonder if there is any progress with Gtk.Expression for Python in this regard or there is any other solution better than this one. I can’t find any example. I would appreciate any comment about the current status or any example.

Below you can find an example of this workaround: a small program to get all widgets in Gtk namespace, and for each widget, show only those methods which differ from the root widget (Gtk.Widget). Additionally, a couple of link buttons help to browse either the widget help page or the method help page (some links fails because they aren’t methods but functions or any other type). Hope you find it useful.

GTK4-DropDown-SearchEntry-01

#!/usr/bin/env python3

import gi

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

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

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

    def __init__(self, name):
        super().__init__()
        self._name = name

    @GObject.Property
    def name(self):
        return self._name

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

    def __init__(self, name):
        super().__init__()
        self._name = name

    @GObject.Property
    def name(self):
        return self._name

class ExampleWindow(Gtk.ApplicationWindow):
    def __init__(self, app):
        super().__init__(application=app, title="GTK4 Widgets and methods")
        self.search_text_widget = '' # Initial search text for widgets
        self.search_text_method = '' # Initial search text for methods

        # Setup DropDown for Widgets
        ## Create model
        self.model_widget = Gio.ListStore(item_type=Widget)
        self.sort_model_widget  = Gtk.SortListModel(model=self.model_widget) # FIXME: Gtk.Sorter?
        self.filter_model_widget = Gtk.FilterListModel(model=self.sort_model_widget)
        self.filter_widget = Gtk.CustomFilter.new(self._do_filter_widget_view, self.filter_model_widget)
        self.filter_model_widget.set_filter(self.filter_widget)

        ## Populate it
        for item in dir(Gtk):
            aclass = type(eval('Gtk.%s' % item))
            if 'gi.types.GObjectMeta' in str(aclass):
                if item[0].isupper() and item != 'Widget':
                    self.model_widget.append(Widget(name='Gtk.%s' % item))

        ## Create factory
        factory_widget = Gtk.SignalListItemFactory()
        factory_widget.connect("setup", self._on_factory_widget_setup)
        factory_widget.connect("bind", self._on_factory_widget_bind)

        ## Create DropDown
        self.ddwdg = Gtk.DropDown(model=self.filter_model_widget, factory=factory_widget)
        self.ddwdg.set_enable_search(True)
        self.ddwdg.connect("notify::selected-item", self._on_selected_widget)

        ## Get SearchEntry
        search_entry_widget = self._get_search_entry_widget(self.ddwdg)
        search_entry_widget.connect('search-changed', self._on_search_widget_changed)


        # Setup DropDown for Methods
        ## Create model
        self.model_method = Gio.ListStore(item_type=Method)
        self.sort_model_method  = Gtk.SortListModel(model=self.model_method) # FIXME: Gtk.Sorter?
        self.filter_model_method = Gtk.FilterListModel(model=self.sort_model_method)
        self.filter_method = Gtk.CustomFilter.new(self._do_filter_method_view, self.filter_model_method)
        self.filter_model_method.set_filter(self.filter_method)

        ## Create factory
        factory_method = Gtk.SignalListItemFactory()
        factory_method.connect("setup", self._on_factory_method_setup)
        factory_method.connect("bind", self._on_factory_method_bind)

        ## Create DropDown
        self.ddmth = Gtk.DropDown(model=self.filter_model_method, factory=factory_method)
        self.ddmth.set_enable_search(True)
        self.ddmth.connect("notify::selected-item", self._on_selected_method)

        ## Get SearchEntry
        search_entry_method = self._get_search_entry_widget(self.ddmth)
        search_entry_method.connect('search-changed', self._on_search_method_changed)

        # Setup Link buttons
        self.btlWidget = Gtk.LinkButton.new_with_label(label='Widget', uri='https://docs.gtk.org/gtk4/class.Widget.html') # Widget Link button
        self.btlMethod = Gtk.LinkButton() # Method Link button

        # Setup main window
        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12, hexpand=True, vexpand=False)
        box.props.margin_start = 12
        box.props.margin_end = 12
        box.props.margin_top = 6
        box.props.margin_bottom = 6
        boxDD = Gtk.Box(spacing=12, hexpand=True, vexpand=False)
        boxDD.set_homogeneous(True)
        boxDD.append(self.ddwdg)
        boxDD.append(self.ddmth)
        boxLB = Gtk.Box(spacing=12, hexpand=True, vexpand=False)
        boxLB.set_homogeneous(True)
        boxLB.append(self.btlWidget)
        boxLB.append(self.btlMethod)
        box.append(boxDD)
        box.append(boxLB)
        self.set_child(box)

    def _get_search_entry_widget(self, dropdown):
        popover = dropdown.get_last_child()
        box = popover.get_child()
        box2 = box.get_first_child()
        search_entry = box2.get_first_child() # Gtk.SearchEntry
        return search_entry

    def _on_factory_widget_setup(self, factory, list_item):
        box = Gtk.Box(spacing=6, orientation=Gtk.Orientation.HORIZONTAL)
        label = Gtk.Label()
        box.append(label)
        list_item.set_child(box)

    def _on_factory_widget_bind(self, factory, list_item):
        box = list_item.get_child()
        label = box.get_first_child()
        widget = list_item.get_item()
        label.set_text(widget.name)

    def _on_factory_method_setup(self, factory, list_item):
        box = Gtk.Box(spacing=6, orientation=Gtk.Orientation.HORIZONTAL)
        label = Gtk.Label()
        box.append(label)
        list_item.set_child(box)

    def _on_factory_method_bind(self, factory, list_item):
        box = list_item.get_child()
        label = box.get_first_child()
        method = list_item.get_item()
        label.set_text(method.name)

    def _on_selected_widget(self, dropdown, data):
        widget = dropdown.get_selected_item()
        self.btlWidget.set_label(widget.name)
        self.btlWidget.set_uri('https://docs.gtk.org/gtk4/class.%s.html' % widget.name[4:])
        self.model_method.remove_all()

        name = widget.name
        obj = eval(name)
        a = set(dir(obj))
        b = set(dir(Gtk.Widget))
        c = a - b
        for item in sorted(list(c)):
            self.model_method.append(Method(name=item))

    def _on_selected_method(self, dropdown, data):
        widget = self.ddwdg.get_selected_item()
        method = dropdown.get_selected_item()
        if method is not None:
            self.btlMethod.set_label(method.name)
            if method.name.startswith('new'):
                self.btlMethod.set_uri('https://docs.gtk.org/gtk4/ctor.%s.%s.html' % (widget.name[4:], method.name))
            else:
                self.btlMethod.set_uri('https://docs.gtk.org/gtk4/method.%s.%s.html' % (widget.name[4:], method.name))

    def _on_search_method_changed(self, search_entry):
        self.search_text_method = search_entry.get_text()
        self.filter_method.changed(Gtk.FilterChange.DIFFERENT)

    def _on_search_widget_changed(self, search_entry):
        self.search_text_widget = search_entry.get_text()
        self.filter_widget.changed(Gtk.FilterChange.DIFFERENT)

    def _do_filter_widget_view(self, item, filter_list_model):
        return self.search_text_widget.upper() in item.name.upper()

    def _do_filter_method_view(self, item, filter_list_model):
        return self.search_text_method.upper() in item.name.upper()

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([])

Greetings!

2 Likes

@ebassi wrote an article on this topic very recently.

1 Like

I wish it could be linked in official documents of pygobject

Thanks for sharing, I have be doing the same workaround in Dialect to filtering models for lists:

I think adding a new example inside pygobject would be perfectly fine.

I remember to have cloned your repo to read the code (along with many others), so maybe the workaround I just published is thanks to you, as it was in my subconscious :wink:

Well there we’re building a custom drop down (though mostly because we have two separated list inside).

Btw I just have pushed a basic GtkDropDown example on the PyGobject guide I’m building. Would be nice to have your example there as an extended thing explaining how to workaround the issue of using custom list models. Also adding it to the upstream examples would be nice as ebassi pointed.

1 Like

Sure. I’d be happy to contribute with this example there. Btw, I didn’t know your guide. Thanks. It looks very useful and promising.
Right now I’m trying to unlock my gitlab account. But, even if I am able to login in, I’ll have to read quite a lot documents to learn how to contribute to the community. I don’t have too much experience working with git. So, feel free to use it anywhere you please.

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