--- title: Define UI comprising ListView in Blueprint for modern GTK apps date: 2025-12-26 14:42:57.006810 UTC --- In a previous post, ["Define UI comprising Dropdown in Blueprint for modern GTK apps"][dropdown-post], I presented how to use [DropDown][dropdown] in [Blueprint][blueprint]. Now we go with a bit more complex example, with [`ListView`](https://docs.gtk.org/gtk4/class.ListView.html) widget. This is the UI where `ListView` is used, in my [CoBang][cobang] app: ![WiFi list](https://cdn.imgchest.com/files/2e65266f6f59.png) It shows a list of WiFi network config, for which user will pick to generate QR code. The list is accompanied with a search box, via which user will type part of Wi-Fi name to narrow down the list, to quickly find the needed Wi-Fi network. This is the Blueprint code, from [_generator-wifi-page.blp_](https://github.com/hongquan/CoBang/blob/987d9a8d52349f5d27a7841cbd7d2510b4c075d6/src/ui/generator-wifi-page.blp) file: ```blp Gio.ListStore wifi_list_store { item-type: typeof<$WifiNetworkInfo>; } EveryFilter wifi_search_fine_filter { StringFilter wifi_search_filter { expression: expr item as <$WifiNetworkInfo>.ssid; ignore-case: true; match-mode: substring; search: bind wifi_search_entry.text; } BoolFilter { expression: expr item as <$WifiNetworkInfo>.erroneous; invert: true; } } FilterListModel wifi_filter_model { model: wifi_list_store; filter: wifi_search_fine_filter; } template $GeneratorWiFiPage : Adw.Bin { Box { SearchEntry wifi_search_entry { placeholder-text: _("Search WiFi networks..."); stop-search => $on_search_stopped(); } /* Make only the list scroll so the Back button stays visible */ ScrolledWindow { vexpand: true; hexpand: true; hscrollbar-policy: never; child: ListView wifi_list_view { model: SingleSelection wifi_selection { model: wifi_filter_model; }; activate => $on_wifi_list_view_activated(); factory: BuilderListItemFactory { template ListItem { child: Box { spacing: 12; Label { label: bind template.item as <$WifiNetworkInfo>.ssid; ellipsize: middle; hexpand: true; halign: start; } /* Signal strength icon for active connections */ Image { icon-name: bind template.item as <$WifiNetworkInfo>.signal_strength_icon; visible: bind template.item as <$WifiNetworkInfo>.is_active; valign: center; halign: end; } }; } }; }; } } } ``` As in the previous post, first we also need to define a *model*. I also use [`ListStore`][liststore] for this purpose. It holds a list of data item of custom type `WifiNetworkInfo`, which is defined as: ```py class WifiNetworkInfo(GObject.GObject): __gtype_name__ = 'WifiNetworkInfo' # Used for finding object to update password asynchronously. uuid = GObject.Property(type=str, default='') ssid = GObject.Property(type=str) password = GObject.Property(type=str) # Ref: https://lazka.github.io/pgi-docs/#NM-1.0/classes/SettingWirelessSecurity.html#NM.SettingWirelessSecurity.props.key_mgmt # Possible values: 'none', 'ieee8021x', 'owe', 'wpa-psk', 'sae', 'wpa-eap', 'wpa-eap-suite-b-192'. # If seeing unknown value, assume 'wpa-psk'. key_mgmt = GObject.Property(type=str, default='none') # Whether this network is currently active (connected) is_active = GObject.Property(type=bool, default=False) # Whether failed to retrieve password, maybe broken storage in NetworkManager. erroneous = GObject.Property(type=bool, default=False) # Signal strength 0-100 (best effort; 0 if unknown) signal_strength = GObject.Property(type=int, default=0) # Icon name representing signal strength (e.g. network-wireless-signal-excellent-symbolic) signal_strength_icon = GObject.Property(type=str, default='network-wireless-signal-none-symbolic') __gsignals__ = { 'changed': (GObject.SignalFlags.RUN_LAST, None, ()), } def __init__(self, ssid: str, password: str = '', key_mgmt: str = 'none', is_active: bool = False, signal_strength: int = 0, uuid: str = ''): super().__init__() self.uuid = uuid self.ssid = ssid self.password = password self.key_mgmt = key_mgmt self.is_active = is_active self.signal_strength = signal_strength # Caller should update signal_strength_icon after setting strength. ``` When displaying the `WifiNetworkInfo` in `ListView`, we will only display those with `erroneous == False`. It is because, if the config is missing data or has incorrect data, the generated QR code will be useless, we would rather not show it. Now, jump to the `ListView` to see how we connect it to the *model*: ```blp ListView wifi_list_view { model: SingleSelection wifi_selection { model: wifi_filter_model; }; } ``` Like the previous `DropDown` [post][dropdown-post], we also connect them via an intermediate `SingleSelection` model. But what interesting here is that, the `SingleSelection` still does not connect directly to the `ListStore`, it does so via a [`FilterListModel`][filter-list-model] instead: ```blp FilterListModel wifi_filter_model { model: wifi_list_store; filter: wifi_search_fine_filter; } ``` As I said previously, the `ListView` won't show all Wi-Fi networks, it only shows valid ones and those matching the search. The narrowed down results are kept in this `FilterListModel`. This object has a [`model`](https://docs.gtk.org/gtk4/property.FilterListModel.model.html) property to refer to the source of data, and a [`filter`](https://docs.gtk.org/gtk4/property.FilterListModel.filter.html) property refer to a "filter" object. Now see how this filter object is constructed: ```blp EveryFilter wifi_search_fine_filter { StringFilter wifi_search_filter { expression: expr item as <$WifiNetworkInfo>.ssid; ignore-case: true; match-mode: substring; } BoolFilter { expression: expr item as <$WifiNetworkInfo>.erroneous; invert: true; } } ``` We remember that there are two criteria to determine if a WiFi network config is kept: 1. It has `erroneous == False`. For this we use a [`BoolFilter`](https://docs.gtk.org/gtk4/class.BoolFilter.html). 2. Its SSID matches the search string. But empty search string means that all SSIDs are qualified. For this we use a [`StringFilter`](https://docs.gtk.org/gtk4/class.StringFilter.html). Both conditions must be met, so we use [`EveryFilter`](https://docs.gtk.org/gtk4/class.EveryFilter.html) to combine them. When defining the filters, we learn a new syntax, [`expr`](https://gnome.pages.gitlab.gnome.org/blueprint-compiler/reference/expressions.html#expression-values), followed by a fixed expression named `item`, with type cast. This one is Blueprint specific, because the generated Gtk Builder's XML will be like this: ```xml ``` To provide a search string to the `StringFilter`, we use [`SearchEntry`][searchentry]. We use "property-binding" to connect the two. For the rest, it looks pretty the same as the [DropDown post][dropdown-post], so I won't make a duplicate explanation. Hope that you understand and still support 🥰. [dropdown-post]: /post/2025/11/define-ui-comprising-dropdown-in-blueprint-for-modern-gtk-apps [dropdown]: https://docs.gtk.org/gtk4/class.DropDown.html [blueprint]: https://gnome.pages.gitlab.gnome.org/blueprint-compiler/ [cobang]: https://github.com/hongquan/CoBang [liststore]: https://docs.gtk.org/gio/class.ListStore.html [filter-list-model]: https://docs.gtk.org/gtk4/class.FilterListModel.html [searchentry]: https://docs.gtk.org/gtk4/class.SearchEntry.html