From 34792696a11de6cc3551de0e0729874e474bed50 Mon Sep 17 00:00:00 2001 From: Zelda Ahmed Date: Tue, 9 Jun 2026 16:49:03 +0100 Subject: [PATCH 1/7] ui: Introduce BzSearchBar This widget is similar to GtkSearchEntry, but with 2 extra properties to provide a spinner and end widget, which the bar in the search page needs, and only providing a signal for search changed, rather then search started/end. BzSearchBar implements GtkEditable and GtkAccessible, to provide accessible semantics for the top level widget, by overriding `Gtk.Accessible.get_platform_state ()` to return the platform state of the GtkText contained within. This fixes an issue with Bazaar's search bars not announcing the placeholder text if they are currently empty. --- src/bazaar.gresource.xml | 1 + src/bz-search-bar.blp | 53 ++++++ src/bz-search-bar.c | 361 +++++++++++++++++++++++++++++++++++++++ src/bz-search-bar.h | 56 ++++++ src/bz-window.c | 2 + src/meson.build | 2 + 6 files changed, 475 insertions(+) create mode 100644 src/bz-search-bar.blp create mode 100644 src/bz-search-bar.c create mode 100644 src/bz-search-bar.h diff --git a/src/bazaar.gresource.xml b/src/bazaar.gresource.xml index 60a0a3b2..8e1fdd2c 100644 --- a/src/bazaar.gresource.xml +++ b/src/bazaar.gresource.xml @@ -60,6 +60,7 @@ bz-screenshot-page.ui bz-screenshots-carousel.ui bz-search-filter-popover.ui + bz-search-bar.ui bz-search-page.ui bz-section-view.ui bz-stats-dialog.ui diff --git a/src/bz-search-bar.blp b/src/bz-search-bar.blp new file mode 100644 index 00000000..950441e1 --- /dev/null +++ b/src/bz-search-bar.blp @@ -0,0 +1,53 @@ +using Gtk 4.0; +using Adw 1; + +template $BzSearchBar: Widget { + height-request: 40; + accessible-role: search_box; + + styles [ + "search-box", + ] + + layout-manager: BoxLayout { + spacing: 8; + }; + + Image search_icon { + icon-name: "system-search-symbolic"; + accessible-role: presentation; + } + + Text search_text { + hexpand: true; + placeholder-text: bind template.placeholder; + activate => $text_activated_cb(template); + changed => $text_changed_cb(template); + } + + Adw.Spinner search_busy { + visible: bind template.busy; + width-request: 16; + height-request: 16; + } + + Adw.Bin end_widget_bin {} + + Button clear_button { + icon-name: "edit-clear"; + valign: center; + focusable: false; + visible: bind $has_text_cb(search_text.text); + clicked => $clear_text_cb(template); + + styles [ + "flat", + "circular", + "searchbar-button", + ] + + accessibility { + label: _("Clear Search"); + } + } +} diff --git a/src/bz-search-bar.c b/src/bz-search-bar.c new file mode 100644 index 00000000..2f0cf3c8 --- /dev/null +++ b/src/bz-search-bar.c @@ -0,0 +1,361 @@ +/* + * bz-search-bar.c + * + * Copyright 2026 Zelda Ahmed + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "bz-search-bar.h" + +struct _BzSearchBar +{ + GtkWidget parent_instance; + + GtkImage *search_icon; + GtkText *search_text; + AdwSpinner *search_busy; + GtkButton *clear_button; + AdwBin *end_widget_bin; + + gboolean busy; + gchar *placeholder; +}; + +static void bz_search_bar_accessible_init (GtkAccessibleInterface *iface); +static void bz_search_bar_editable_init (GtkEditableInterface *iface); + +G_DEFINE_FINAL_TYPE_WITH_CODE (BzSearchBar, bz_search_bar, GTK_TYPE_WIDGET, G_IMPLEMENT_INTERFACE (GTK_TYPE_EDITABLE, bz_search_bar_editable_init) G_IMPLEMENT_INTERFACE (GTK_TYPE_ACCESSIBLE, bz_search_bar_accessible_init)) + +enum +{ + PROP_0, + PROP_BUSY, + PROP_PLACEHOLDER, + PROP_END_WIDGET, + N_PROPS +}; + +enum +{ + SIGNAL_SEARCH_ACTIVATED, + SIGNAL_SEARCH_CHANGED, + N_SIGNALS +}; + +static guint signals[N_SIGNALS]; +static GParamSpec *properties[N_PROPS]; + +static void +text_activated_cb (BzSearchBar *self, + GtkText *text) +{ + g_signal_emit (self, signals[SIGNAL_SEARCH_ACTIVATED], 0); +} + +static void +text_changed_cb (BzSearchBar *self, + GtkText *text) +{ + g_signal_emit (self, signals[SIGNAL_SEARCH_CHANGED], 0); +} + +static gboolean +has_text_cb (BzSearchBar *self, + GtkButton *clear_button, + const gchar *value) +{ + const gchar *text; + + if (!self->search_text) + return FALSE; + + text = gtk_editable_get_text (GTK_EDITABLE (self->search_text)); + + return strlen (text) != 0; +} + +static void +clear_text_cb (BzSearchBar *self, + GtkButton *clear_button) +{ + gtk_editable_set_text (GTK_EDITABLE (self->search_text), ""); +} + +static gboolean +bz_search_bar_grab_focus (GtkWidget *widget) +{ + BzSearchBar *self = BZ_SEARCH_BAR (widget); + + return gtk_widget_grab_focus (GTK_WIDGET (self->search_text)); +} + +BzSearchBar * +bz_search_bar_new (void) +{ + return g_object_new (BZ_TYPE_SEARCH_BAR, NULL); +} + +static void +bz_search_bar_dispose (GObject *object) +{ + BzSearchBar *self = BZ_SEARCH_BAR (object); + + gtk_editable_finish_delegate (GTK_EDITABLE (self)); + gtk_widget_dispose_template (GTK_WIDGET (self), BZ_TYPE_SEARCH_BAR); + + G_OBJECT_CLASS (bz_search_bar_parent_class)->dispose (object); +} + +static void +bz_search_bar_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + BzSearchBar *self = BZ_SEARCH_BAR (object); + + if (gtk_editable_delegate_get_property (object, prop_id, value, pspec)) + return; + + switch (prop_id) + { + case PROP_BUSY: + g_value_set_boolean (value, bz_search_bar_get_busy (self)); + break; + + case PROP_END_WIDGET: + g_value_set_object (value, bz_search_bar_get_end_widget (self)); + break; + + case PROP_PLACEHOLDER: + g_value_set_string (value, bz_search_bar_get_placeholder (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +bz_search_bar_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + BzSearchBar *self = BZ_SEARCH_BAR (object); + + if (gtk_editable_delegate_set_property (object, prop_id, value, pspec)) + { + if (prop_id == N_PROPS + GTK_EDITABLE_PROP_EDITABLE) + { + gtk_accessible_update_property (GTK_ACCESSIBLE (self->search_text), + GTK_ACCESSIBLE_PROPERTY_READ_ONLY, !g_value_get_boolean (value), + -1); + } + + return; + } + + switch (prop_id) + { + case PROP_BUSY: + bz_search_bar_set_busy (self, g_value_get_boolean (value)); + break; + + case PROP_END_WIDGET: + bz_search_bar_set_end_widget (self, GTK_WIDGET (g_value_get_object (value))); + break; + + case PROP_PLACEHOLDER: + bz_search_bar_set_placeholder (self, g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +bz_search_bar_class_init (BzSearchBarClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + gtk_widget_class_set_template_from_resource (widget_class, "/io/github/kolunmi/Bazaar/bz-search-bar.ui"); + + object_class->dispose = bz_search_bar_dispose; + object_class->get_property = bz_search_bar_get_property; + object_class->set_property = bz_search_bar_set_property; + + widget_class->grab_focus = bz_search_bar_grab_focus; + + properties[PROP_BUSY] = + g_param_spec_boolean ("busy", NULL, NULL, FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + properties[PROP_END_WIDGET] = + g_param_spec_object ("end-widget", NULL, NULL, GTK_TYPE_WIDGET, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + properties[PROP_PLACEHOLDER] = + g_param_spec_string ("placeholder", NULL, NULL, "", + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + signals[SIGNAL_SEARCH_ACTIVATED] = + g_signal_new ( + "search-activated", + G_OBJECT_CLASS_TYPE (klass), + G_SIGNAL_RUN_FIRST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); + + signals[SIGNAL_SEARCH_CHANGED] = + g_signal_new ( + "search-changed", + G_OBJECT_CLASS_TYPE (klass), + G_SIGNAL_RUN_FIRST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); + + g_object_class_install_properties (object_class, N_PROPS, properties); + gtk_editable_install_properties (object_class, N_PROPS); + + gtk_widget_class_bind_template_child (widget_class, BzSearchBar, search_icon); + gtk_widget_class_bind_template_child (widget_class, BzSearchBar, search_text); + gtk_widget_class_bind_template_child (widget_class, BzSearchBar, search_busy); + gtk_widget_class_bind_template_child (widget_class, BzSearchBar, clear_button); + gtk_widget_class_bind_template_child (widget_class, BzSearchBar, end_widget_bin); + + gtk_widget_class_bind_template_callback (widget_class, text_activated_cb); + gtk_widget_class_bind_template_callback (widget_class, text_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, has_text_cb); + gtk_widget_class_bind_template_callback (widget_class, clear_text_cb); +} + +static GtkEditable * +bz_search_bar_get_delegate (GtkEditable *editable) +{ + return GTK_EDITABLE (BZ_SEARCH_BAR (editable)->search_text); +} + +static void +bz_search_bar_editable_init (GtkEditableInterface *iface) +{ + iface->get_delegate = bz_search_bar_get_delegate; +} + +static gboolean +bz_search_bar_accessible_get_platform_state (GtkAccessible *self, + GtkAccessiblePlatformState state) +{ + return gtk_editable_delegate_get_accessible_platform_state (GTK_EDITABLE (self), state); +} + +static void +bz_search_bar_accessible_init (GtkAccessibleInterface *iface) +{ + GtkAccessibleInterface *parent_iface = g_type_interface_peek_parent (iface); + iface->get_at_context = parent_iface->get_at_context; + iface->get_platform_state = bz_search_bar_accessible_get_platform_state; +} + +static void +bz_search_bar_init (BzSearchBar *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + gtk_editable_init_delegate (GTK_EDITABLE (self)); +} + +gboolean +bz_search_bar_get_busy (BzSearchBar *self) +{ + g_return_val_if_fail (BZ_IS_SEARCH_BAR (self), FALSE); + + return self->busy; +} + +void +bz_search_bar_set_busy (BzSearchBar *self, + gboolean busy) +{ + g_return_if_fail (BZ_IS_SEARCH_BAR (self)); + + if (self->busy == busy) + return; + + self->busy = busy; + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_BUSY]); +} + +GtkWidget * +bz_search_bar_get_end_widget (BzSearchBar *self) +{ + g_return_val_if_fail (BZ_IS_SEARCH_BAR (self), NULL); + + return adw_bin_get_child (self->end_widget_bin); +} + +void +bz_search_bar_set_end_widget (BzSearchBar *self, + GtkWidget *widget) +{ + GtkWidget *end_widget; + + g_return_if_fail (BZ_IS_SEARCH_BAR (self)); + g_return_if_fail (!widget || GTK_IS_WIDGET (widget)); + + end_widget = adw_bin_get_child (self->end_widget_bin); + + if (end_widget == widget) + return; + + adw_bin_set_child (self->end_widget_bin, widget); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_END_WIDGET]); +} + +const gchar * +bz_search_bar_get_placeholder (BzSearchBar *self) +{ + g_return_val_if_fail (BZ_IS_SEARCH_BAR (self), NULL); + + if (!self->search_text) + return NULL; + + return gtk_text_get_placeholder_text (self->search_text); +} + +void +bz_search_bar_set_placeholder (BzSearchBar *self, + const gchar *text) +{ + g_return_if_fail (BZ_IS_SEARCH_BAR (self)); + + if (g_strcmp0 (bz_search_bar_get_placeholder (self), text) == 0) + return; + + gtk_text_set_placeholder_text (self->search_text, text); + gtk_accessible_update_property (GTK_ACCESSIBLE (self), + GTK_ACCESSIBLE_PROPERTY_PLACEHOLDER, text, + -1); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PLACEHOLDER]); +} diff --git a/src/bz-search-bar.h b/src/bz-search-bar.h new file mode 100644 index 00000000..af4124d2 --- /dev/null +++ b/src/bz-search-bar.h @@ -0,0 +1,56 @@ +/* + * bz-search-bar.h + * + * Copyright 2026 Zelda Ahmed + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define BZ_TYPE_SEARCH_BAR (bz_search_bar_get_type ()) + +G_DECLARE_FINAL_TYPE (BzSearchBar, bz_search_bar, BZ, SEARCH_BAR, GtkWidget) + +BzSearchBar * +bz_search_bar_new (void); + +gboolean +bz_search_bar_get_busy (BzSearchBar *self); + +void +bz_search_bar_set_busy (BzSearchBar *self, + gboolean busy); + +GtkWidget * +bz_search_bar_get_end_widget (BzSearchBar *self); + +void +bz_search_bar_set_end_widget (BzSearchBar *self, + GtkWidget *widget); + +const gchar * +bz_search_bar_get_placeholder (BzSearchBar *self); + +void +bz_search_bar_set_placeholder (BzSearchBar *self, + const gchar *text); + +G_END_DECLS diff --git a/src/bz-window.c b/src/bz-window.c index a08a5fe6..569c513e 100644 --- a/src/bz-window.c +++ b/src/bz-window.c @@ -37,6 +37,7 @@ #include "bz-io.h" #include "bz-library-page.h" #include "bz-progress-bar.h" +#include "bz-search-bar.h" #include "bz-search-page.h" #include "bz-template-callbacks.h" #include "bz-transaction-dialog.h" @@ -688,6 +689,7 @@ bz_window_class_init (BzWindowClass *klass) g_object_class_install_properties (object_class, LAST_PROP, props); + g_type_ensure (BZ_TYPE_SEARCH_BAR); g_type_ensure (BZ_TYPE_SEARCH_PAGE); g_type_ensure (BZ_TYPE_PROGRESS_BAR); g_type_ensure (BZ_TYPE_CURATED_VIEW); diff --git a/src/meson.build b/src/meson.build index 4345049c..95ab7dc5 100644 --- a/src/meson.build +++ b/src/meson.build @@ -138,6 +138,7 @@ bz_sources = files( 'bz-screenshots-carousel.c', 'bz-search-engine.c', 'bz-search-filter-popover.c', + 'bz-search-bar.c', 'bz-search-page.c', 'bz-search-pill-list.c', 'bz-section-view.c', @@ -310,6 +311,7 @@ blueprints = custom_target('blueprints', 'bz-screenshot-page.blp', 'bz-screenshots-carousel.blp', 'bz-search-filter-popover.blp', + 'bz-search-bar.blp', 'bz-search-page.blp', 'bz-section-view.blp', 'bz-stats-dialog.blp', From acf6306ed79b93c4e198cef9f2136623de35503e Mon Sep 17 00:00:00 2001 From: Zelda Ahmed Date: Tue, 9 Jun 2026 17:00:52 +0100 Subject: [PATCH 2/7] style: Tweak search bar CSS padding Since we are using a BoxLayout in BzSearchBar with spacing 8, the extra padding is not necessary anymore --- src/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/style.css b/src/style.css index b91a8343..e8e6ceeb 100644 --- a/src/style.css +++ b/src/style.css @@ -70,7 +70,7 @@ outline-offset: 6px; transition-property: outline, outline-offset; transition-duration: 200ms; - padding: 8px 12px; + padding: 0px 12px; border-radius: 9999px; background-color: var(--card-bg-color); } From 1e136ea7cba8618dc2d4311541502f0c7e8151c0 Mon Sep 17 00:00:00 2001 From: Zelda Ahmed Date: Tue, 9 Jun 2026 17:02:07 +0100 Subject: [PATCH 3/7] search-page: Replace search bar box with BzSearchBar --- src/bz-search-page.blp | 81 ++++++++--------------------- src/bz-search-page.c | 112 +++++++++++++++++++---------------------- 2 files changed, 72 insertions(+), 121 deletions(-) diff --git a/src/bz-search-page.blp b/src/bz-search-page.blp index 5385310a..da32165c 100644 --- a/src/bz-search-page.blp +++ b/src/bz-search-page.blp @@ -37,73 +37,34 @@ template $BzSearchPage: Adw.Bin { Adw.Clamp search_box_clamp { margin-start: 12; margin-end: 12; + margin-top: 10; + margin-bottom: 10; maximum-size: 375; - child: Adw.Bin { - halign: fill; - margin-top: 10; - margin-bottom: 10; + child: $BzSearchBar search_bar { + placeholder: _("Search Apps, Games, Software"); + search-activated => $search_activate(template); + search-changed => $search_changed(template); - child: Box { - spacing: 8; - height-request: 40; - - Image { - icon-name: "system-search-symbolic"; - } - - Text search_bar { - hexpand: true; - max-length: 50; - placeholder-text: _("Search Apps, Games, Software"); - } - - Adw.Spinner search_busy { - visible: false; - width-request: 16; - height-request: 16; - } - - MenuButton filter_button { - visible: bind $is_valid_string(search_bar.text) as ; - icon-name: "sliders-horizontal-symbolic"; - tooltip-text: _("Search Filters"); - - popover: $BzSearchFilterPopover filter_popover { - notify::has-active-filters => $has_active_filters_cb(); - }; - - styles [ - "flat", - "circular", - "searchbar-button", - ] - - accessibility { - label: _("Filters"); - } - } - - Button clear_button { - visible: bind $is_valid_string(search_bar.text) as ; - icon-name: "edit-clear-symbolic"; - - styles [ - "flat", - "circular", - "searchbar-button", - ] - - clicked => $reset_search_cb(template); + end-widget: MenuButton filter_button { + visible: bind $is_valid_string(search_bar.text) as ; + icon-name: "sliders-horizontal-symbolic"; + tooltip-text: _("Search Filters"); + valign: center; - accessibility { - label: _("Clear Search"); - } - } + popover: $BzSearchFilterPopover filter_popover { + notify::has-active-filters => $has_active_filters_cb(); + }; styles [ - "search-box", + "flat", + "circular", + "searchbar-button", ] + + accessibility { + label: _("Filters"); + } }; }; } diff --git a/src/bz-search-page.c b/src/bz-search-page.c index 7529deea..0da02aeb 100644 --- a/src/bz-search-page.c +++ b/src/bz-search-page.c @@ -30,6 +30,7 @@ #include "bz-rich-app-tile.h" #include "bz-screenshot.h" #include "bz-search-filter-popover.h" +#include "bz-search-bar.h" #include "bz-search-page.h" #include "bz-search-pill-list.h" #include "bz-search-result.h" @@ -54,8 +55,7 @@ struct _BzSearchPage DexFuture *search_query; /* Template widgets */ - GtkText *search_bar; - AdwSpinner *search_busy; + BzSearchBar *search_bar; GtkBox *content_box; GtkStack *search_stack; GtkGridView *grid_view; @@ -75,15 +75,8 @@ enum LAST_PROP }; -static GParamSpec *props[LAST_PROP] = { 0 }; - -static void -search_changed (GtkEditable *editable, - BzSearchPage *self); -static void -search_activate (GtkText *text, - BzSearchPage *self); +static GParamSpec *props[LAST_PROP] = { 0 }; static void grid_activate (GtkGridView *grid_view, @@ -299,6 +292,48 @@ bind_category_tile_cb (BzSearchPage *self, g_signal_connect_swapped (tile, "clicked", G_CALLBACK (category_clicked), category); } +static void +search_changed (BzSearchPage *self, + GtkEditable *editable) +{ + g_clear_handle_id (&self->search_update_timeout, g_source_remove); + self->search_update_timeout = g_timeout_add_once ( + 150, (GSourceOnceFunc) update_filter, self); + bz_search_bar_set_busy (BZ_SEARCH_BAR (editable), TRUE); +} + +static void +search_activate (BzSearchPage *self, + BzSearchBar *search_bar) +{ + GtkSelectionModel *model = NULL; + guint n_items = 0; + g_autoptr (BzSearchResult) result = NULL; + BzEntryGroup *group = NULL; + + model = gtk_grid_view_get_model (self->grid_view); + n_items = g_list_model_get_n_items (G_LIST_MODEL (model)); + + bz_search_bar_set_busy (self->search_bar, FALSE); + + if (n_items > 0) + { + result = g_list_model_get_item (G_LIST_MODEL (model), 0); + group = bz_search_result_get_group (result); + + if (bz_entry_group_get_removable_and_available (group) > 0) + { + gtk_widget_activate_action (GTK_WIDGET (self), "window.remove-group", "(sb)", + bz_entry_group_get_id (group), FALSE); + } + else if (bz_entry_group_get_installable_and_available (group) > 0) + { + gtk_widget_activate_action (GTK_WIDGET (self), "window.install-group", "(sb)", + bz_entry_group_get_id (group), FALSE); + } + } +} + static void unbind_category_tile_cb (BzSearchPage *self, BzCategoryTile *tile, @@ -422,7 +457,6 @@ bz_search_page_class_init (BzSearchPageClass *klass) bz_widget_class_bind_all_util_callbacks (widget_class); gtk_widget_class_bind_template_child (widget_class, BzSearchPage, search_bar); - gtk_widget_class_bind_template_child (widget_class, BzSearchPage, search_busy); gtk_widget_class_bind_template_child (widget_class, BzSearchPage, content_box); gtk_widget_class_bind_template_child (widget_class, BzSearchPage, search_stack); gtk_widget_class_bind_template_child (widget_class, BzSearchPage, grid_view); @@ -444,6 +478,8 @@ bz_search_page_class_init (BzSearchPageClass *klass) gtk_widget_class_bind_template_callback (widget_class, tile_activated_cb); gtk_widget_class_bind_template_callback (widget_class, copy_id_cb); gtk_widget_class_bind_template_callback (widget_class, debug_id_inspect_cb); + gtk_widget_class_bind_template_callback (widget_class, search_activate); + gtk_widget_class_bind_template_callback (widget_class, search_changed); } static void @@ -459,8 +495,6 @@ bz_search_page_init (BzSearchPage *self) gtk_no_selection_set_model (GTK_NO_SELECTION (self->selection_model), G_LIST_MODEL (self->search_model)); gtk_grid_view_set_model (self->grid_view, self->selection_model); - g_signal_connect (self->search_bar, "changed", G_CALLBACK (search_changed), self); - g_signal_connect (self->search_bar, "activate", G_CALLBACK (search_activate), self); g_signal_connect (self->grid_view, "activate", G_CALLBACK (grid_activate), self); g_signal_connect_swapped (self->filter_popover, "notify::selected-categories", @@ -633,49 +667,6 @@ bz_search_page_ensure_active (BzSearchPage *self, return TRUE; } -static void -search_changed (GtkEditable *editable, - BzSearchPage *self) -{ - g_clear_handle_id (&self->search_update_timeout, g_source_remove); - self->search_update_timeout = g_timeout_add_once ( - 150, (GSourceOnceFunc) update_filter, self); - gtk_widget_set_visible (GTK_WIDGET (self->search_busy), TRUE); -} - -static void -search_activate (GtkText *text, - BzSearchPage *self) -{ - GtkSelectionModel *model = NULL; - guint n_items = 0; - g_autoptr (BzSearchResult) result = NULL; - BzEntryGroup *group = NULL; - - model = gtk_grid_view_get_model (self->grid_view); - n_items = g_list_model_get_n_items (G_LIST_MODEL (model)); - - if (gtk_widget_get_visible (GTK_WIDGET (self->search_busy))) - return; - - if (n_items > 0) - { - result = g_list_model_get_item (G_LIST_MODEL (model), 0); - group = bz_search_result_get_group (result); - - if (bz_entry_group_get_removable_and_available (group) > 0) - { - gtk_widget_activate_action (GTK_WIDGET (self), "window.remove-group", "(sb)", - bz_entry_group_get_id (group), FALSE); - } - else if (bz_entry_group_get_installable_and_available (group) > 0) - { - gtk_widget_activate_action (GTK_WIDGET (self), "window.install-group", "(sb)", - bz_entry_group_get_id (group), FALSE); - } - } -} - static void grid_activate (GtkGridView *grid_view, guint position, @@ -766,7 +757,7 @@ search_query_then (DexFuture *future, self->search_model, 0, old_length, (gpointer *) filtered->pdata, filtered->len); - gtk_widget_set_visible (GTK_WIDGET (self->search_busy), FALSE); + bz_search_bar_set_busy (self->search_bar, FALSE); if (filtered->len > 0) { @@ -808,7 +799,7 @@ update_filter (BzSearchPage *self) self->current_query = bz_finished_search_query_new (); g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CURRENT_QUERY]); - gtk_widget_set_visible (GTK_WIDGET (self->search_busy), FALSE); + bz_search_bar_set_busy (self->search_bar, FALSE); if (self->state == NULL) return; @@ -853,9 +844,8 @@ update_filter (BzSearchPage *self) future = bz_search_engine_query ( engine, (const char *const *) terms); - gtk_widget_set_visible ( - GTK_WIDGET (self->search_busy), - dex_future_is_pending (future)); + + bz_search_bar_set_busy (self->search_bar, dex_future_is_pending (future)); future = dex_future_then ( future, From f5181c1cc03f45e9e8c1a4a8ef30965f76eee355 Mon Sep 17 00:00:00 2001 From: Zelda Ahmed Date: Tue, 9 Jun 2026 17:12:14 +0100 Subject: [PATCH 4/7] library-page: Replace search bar box with BzSearchBar --- src/bz-library-page.blp | 50 +++++------------------------------------ src/bz-library-page.c | 12 +++++----- 2 files changed, 12 insertions(+), 50 deletions(-) diff --git a/src/bz-library-page.blp b/src/bz-library-page.blp index 7c6e2f59..4a5152ac 100644 --- a/src/bz-library-page.blp +++ b/src/bz-library-page.blp @@ -8,53 +8,15 @@ template $BzLibraryPage: Adw.Bin { Adw.Clamp search_box_clamp { margin-start: 12; margin-end: 12; + margin-top: 10; + margin-bottom: 10; maximum-size: 375; - visible: bind template.has-apps; - child: Adw.Bin { - halign: fill; - margin-top: 10; - margin-bottom: 10; - - child: Box { - orientation: horizontal; - spacing: 8; - height-request: 40; - - Image { - icon-name: "system-search-symbolic"; - } - - Text search_bar { - hexpand: true; - max-length: 50; - placeholder-text: _("Search installed apps"); - notify::text => $search_text_changed(template); - activate => $search_text_activate(template); - } - - Button clear_button { - icon-name: "edit-clear-symbolic"; - visible: bind $not($is_empty_string(search_bar.text) as ) as ; - - styles [ - "flat", - "circular", - "searchbar-button", - ] - - clicked => $reset_search_cb(template); - - accessibility { - label: _("Clear search"); - } - } - - styles [ - "search-box", - ] - }; + child: $BzSearchBar search_bar { + placeholder: _("Search installed apps"); + search-activated => $search_text_activate(template); + search-changed => $search_text_changed(template); }; } diff --git a/src/bz-library-page.c b/src/bz-library-page.c index fa1d9e34..aa421e9a 100644 --- a/src/bz-library-page.c +++ b/src/bz-library-page.c @@ -23,6 +23,7 @@ #include "bz-entry-group.h" #include "bz-installed-tile.h" #include "bz-library-page.h" +#include "bz-search-bar.h" #include "bz-section-view.h" #include "bz-template-callbacks.h" #include "bz-transaction-tile.h" @@ -39,7 +40,7 @@ struct _BzLibraryPage /* Template widgets */ AdwViewStack *stack; - GtkText *search_bar; + BzSearchBar *search_bar; GtkScrolledWindow *scroll; GtkFilterListModel *filter_model; GtkCustomFilter *filter; @@ -248,8 +249,7 @@ tile_activated_cb (BzListTile *tile) static void search_text_changed (BzLibraryPage *self, - GParamSpec *pspec, - GtkText *entry) + BzSearchBar *search_bar) { gtk_filter_changed (GTK_FILTER (self->filter), GTK_FILTER_CHANGE_DIFFERENT); @@ -258,7 +258,7 @@ search_text_changed (BzLibraryPage *self, static void search_text_activate (BzLibraryPage *self, - GtkText *entry) + BzSearchBar *search_bar) { const char *text = NULL; @@ -273,7 +273,7 @@ static void reset_search_cb (BzLibraryPage *self, GtkButton *button) { - gtk_text_set_buffer (self->search_bar, NULL); + gtk_editable_set_text (GTK_EDITABLE (self->search_bar), ""); } static void @@ -567,7 +567,7 @@ bz_library_page_reset_search (BzLibraryPage *self) vadjustment = gtk_scrolled_window_get_vadjustment (self->scroll); gtk_adjustment_set_value (vadjustment, 0.0); - gtk_text_set_buffer (self->search_bar, NULL); + gtk_editable_set_text (GTK_EDITABLE (self->search_bar), ""); } static void From 7cac27796af7733c080dfa69271d73e427f30ca2 Mon Sep 17 00:00:00 2001 From: Zelda Ahmed Date: Tue, 9 Jun 2026 17:25:22 +0100 Subject: [PATCH 5/7] search-page: Override gtk_widget_grab_focus Override `GtkWidgetClass.grab_focus`, and use that to grab focus in `ensure_active` to grab focus on the search bar immediately, even when no text has been entered. This is desirable to the previous behaviour so the screen reader will always read out the placeholder text of the search bar, instead of focusing on the previously foucsed widget, which may not be the search bar and will just read out its tooltip --- src/bz-search-page.c | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/bz-search-page.c b/src/bz-search-page.c index 0da02aeb..806dd180 100644 --- a/src/bz-search-page.c +++ b/src/bz-search-page.c @@ -107,6 +107,14 @@ emit_idx (BzSearchPage *self, GListModel *model, guint selected_idx); +static gboolean +bz_search_page_grab_focus (GtkWidget *widget) +{ + BzSearchPage *self = BZ_SEARCH_PAGE (widget); + + return gtk_widget_grab_focus (GTK_WIDGET (self->search_bar)); +} + static void bz_search_page_dispose (GObject *object) { @@ -421,6 +429,8 @@ bz_search_page_class_init (BzSearchPageClass *klass) object_class->get_property = bz_search_page_get_property; object_class->set_property = bz_search_page_set_property; + widget_class->grab_focus = bz_search_page_grab_focus; + props[PROP_STATE] = g_param_spec_object ( "state", @@ -652,19 +662,11 @@ gboolean bz_search_page_ensure_active (BzSearchPage *self, const char *initial) { - const char *text = NULL; - g_return_val_if_fail (BZ_IS_SEARCH_PAGE (self), FALSE); - text = gtk_editable_get_text (GTK_EDITABLE (self->search_bar)); - if (text != NULL && *text != '\0' && - gtk_widget_has_focus (GTK_WIDGET (self->search_bar))) - return FALSE; - - gtk_widget_grab_focus (GTK_WIDGET (self->search_bar)); bz_search_page_set_text (self, initial); - return TRUE; + return gtk_widget_grab_focus (GTK_WIDGET (self)); } static void From 7c46abd9e5335595897d155dea7183fb68e78796 Mon Sep 17 00:00:00 2001 From: Zelda Ahmed Date: Tue, 9 Jun 2026 17:43:42 +0100 Subject: [PATCH 6/7] search-page: Announce number of search results to screen reader --- src/bz-search-page.c | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/bz-search-page.c b/src/bz-search-page.c index 806dd180..b15db58c 100644 --- a/src/bz-search-page.c +++ b/src/bz-search-page.c @@ -713,6 +713,7 @@ search_query_then (DexFuture *future, gboolean only_free = FALSE; gboolean only_non_eol = FALSE; gboolean only_mobile = FALSE; + const char *search_text = NULL; bz_weak_get_or_return_reject (self, wr); @@ -725,6 +726,7 @@ search_query_then (DexFuture *future, only_mobile = bz_search_filter_popover_get_only_mobile (self->filter_popover); filtered = g_ptr_array_new_with_free_func (g_object_unref); + search_text = gtk_editable_get_text (GTK_EDITABLE (self->search_bar)); for (guint i = 0; i < results->len; i++) { @@ -768,12 +770,20 @@ search_query_then (DexFuture *future, } else { - const char *search_text = NULL; - - search_text = gtk_editable_get_text (GTK_EDITABLE (self->search_bar)); - page_name = (search_text && *search_text) ? "no-results" : "empty"; + page_name = (search_text && *search_text) ? "no-results" : "empty"; } + if (search_text && *search_text) { + const char *message = NULL; + + message = filtered->len == 0 ? _("No applications found") : g_strdup_printf (ngettext ("One application found", + "%u applications found", + filtered->len), + filtered->len); + + gtk_accessible_announce (GTK_ACCESSIBLE (self), message, GTK_ACCESSIBLE_ANNOUNCEMENT_PRIORITY_MEDIUM); + } + self->current_query = g_object_ref (finished); g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CURRENT_QUERY]); From 6390721bb9c503031de80301b98934058b798e9d Mon Sep 17 00:00:00 2001 From: Zelda Ahmed Date: Tue, 9 Jun 2026 17:58:14 +0100 Subject: [PATCH 7/7] library-page: Announce number of search results to screen reader --- src/bz-library-page.c | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/bz-library-page.c b/src/bz-library-page.c index aa421e9a..80ca3fe1 100644 --- a/src/bz-library-page.c +++ b/src/bz-library-page.c @@ -592,9 +592,13 @@ has_transactions_changed (BzLibraryPage *self, static void set_page (BzLibraryPage *self) { - guint n_apps = 0; - guint n_filtered = 0; - gboolean has_transactions = FALSE; + guint n_apps = 0; + guint n_filtered = 0; + gboolean has_transactions = FALSE; + const char *message = NULL; + const char *search_text = NULL; + + search_text = gtk_editable_get_text (GTK_EDITABLE (self->search_bar)); if (self->model != NULL) { @@ -619,6 +623,13 @@ set_page (BzLibraryPage *self) adw_view_stack_set_visible_child_name (self->stack, "no-results"); else adw_view_stack_set_visible_child_name (self->stack, "content"); + + if (search_text && *search_text) + { + message = n_filtered == 0 ? _ ("No applications found") : g_strdup_printf (ngettext ("One application found", "%u applications found", n_filtered), n_filtered); + + gtk_accessible_announce (GTK_ACCESSIBLE (self), message, GTK_ACCESSIBLE_ANNOUNCEMENT_PRIORITY_HIGH); + } } static gboolean