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-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..80ca3fe1 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 @@ -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 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-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..b15db58c 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, @@ -114,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) { @@ -299,6 +300,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, @@ -386,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", @@ -422,7 +467,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 +488,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 +505,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", @@ -618,62 +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; -} - -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); - } - } + return gtk_widget_grab_focus (GTK_WIDGET (self)); } static void @@ -720,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); @@ -732,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++) { @@ -766,7 +761,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) { @@ -775,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]); @@ -808,7 +811,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 +856,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, 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', 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); }