diff options
-rw-r--r-- | src/gui/gcal-search-button.c | 147 | ||||
-rw-r--r-- | src/gui/gcal-search-button.ui | 123 | ||||
-rw-r--r-- | src/gui/gcal-search-hit-row.c | 193 | ||||
-rw-r--r-- | src/gui/gcal-search-hit-row.h | 36 | ||||
-rw-r--r-- | src/gui/gcal-search-hit-row.ui | 53 | ||||
-rw-r--r-- | src/gui/gui.gresource.xml | 1 | ||||
-rw-r--r-- | src/gui/meson.build | 1 | ||||
-rw-r--r-- | src/theme/style-dark.css | 8 | ||||
-rw-r--r-- | src/theme/style.css | 4 |
9 files changed, 228 insertions, 338 deletions
diff --git a/src/gui/gcal-search-button.c b/src/gui/gcal-search-button.c index 3f5e4d13..d8ed14d1 100644 --- a/src/gui/gcal-search-button.c +++ b/src/gui/gcal-search-button.c @@ -24,7 +24,6 @@ #include "gcal-debug.h" #include "gcal-search-button.h" #include "gcal-search-hit.h" -#include "gcal-search-hit-row.h" #include <math.h> @@ -36,8 +35,9 @@ struct _GcalSearchButton GtkEditable *entry; GtkWidget *popover; - GtkListBox *results_listbox; + GtkListView *results_listview; GtkRevealer *results_revealer; + GtkSingleSelection *results_selection_model; GtkStack *stack; GCancellable *cancellable; @@ -88,11 +88,14 @@ quit_search_entry (GcalSearchButton *self) gtk_editable_set_text (self->entry, ""); } -static GtkWidget * -create_widget_func (gpointer item, - gpointer user_data) +static inline void +scroll_to_result (GcalSearchButton *self, + guint position) { - return gcal_search_hit_row_new (item); + gtk_widget_activate_action (GTK_WIDGET (self->results_listview), + "list.scroll-to-item", + "u", + position); } static void @@ -101,11 +104,7 @@ set_model (GcalSearchButton *self, { GCAL_ENTRY; - gtk_list_box_bind_model (self->results_listbox, - model, - create_widget_func, - self, - NULL); + gtk_single_selection_set_model (self->results_selection_model, model); if (model) show_suggestions (self); @@ -120,6 +119,18 @@ set_model (GcalSearchButton *self, * Callbacks */ +static gchar * +escape_markup_cb (GcalSearchHit *hit, + const gchar *string) +{ + g_autofree gchar *escaped_string = NULL; + + escaped_string = g_markup_escape_text (string, -1); + escaped_string = g_strstrip (escaped_string); + + return g_steal_pointer (&escaped_string); +} + static void on_button_clicked_cb (GtkButton *button, GcalSearchButton *self) @@ -165,6 +176,68 @@ on_search_finished_cb (GObject *source_object, } static void +on_entry_activate_cb (GtkSearchEntry *entry, + GcalSearchButton *self) +{ + GcalSearchHit *hit; + + GCAL_ENTRY; + + hit = gtk_single_selection_get_selected_item (self->results_selection_model); + + if (hit) + { + GCAL_TRACE_MSG ("Activating \"%s\"", gcal_search_hit_get_title (hit)); + + gcal_search_hit_activate (hit, GTK_WIDGET (self)); + quit_search_entry (self); + } + + GCAL_EXIT; +} + +static void +on_entry_next_match_cb (GtkSearchEntry *entry, + GcalSearchButton *self) +{ + guint selected; + + GCAL_ENTRY; + + selected = gtk_single_selection_get_selected (self->results_selection_model); + + if (selected != GTK_INVALID_LIST_POSITION && + selected + 1 < g_list_model_get_n_items (G_LIST_MODEL (self->results_selection_model))) + { + GCAL_TRACE_MSG ("Changing selection to %u", selected + 1); + gtk_single_selection_set_selected (self->results_selection_model, selected + 1); + scroll_to_result (self, selected + 1); + } + + GCAL_EXIT; +} + +static void +on_entry_previous_match_cb (GtkSearchEntry *entry, + GcalSearchButton *self) +{ + guint selected; + + GCAL_ENTRY; + + selected = gtk_single_selection_get_selected (self->results_selection_model); + + if (selected > 0 && selected != GTK_INVALID_LIST_POSITION) + { + GCAL_TRACE_MSG ("Changing selection to %u", selected - 1); + gtk_single_selection_set_selected (self->results_selection_model, selected - 1); + scroll_to_result (self, selected - 1); + } + + GCAL_EXIT; +} + +static void on_entry_search_changed_cb (GtkSearchEntry *entry, GcalSearchButton *self) { @@ -204,16 +277,18 @@ on_entry_stop_search_cb (GtkSearchEntry *search_entry, } static void -on_results_listbox_row_activated_cb (GtkListBox *listbox, - GcalSearchHitRow *row, - GcalSearchButton *self) +on_results_listview_activated_cb (GtkListBox *listbox, + guint position, + GcalSearchButton *self) { GcalSearchHit *search_hit; - search_hit = gcal_search_hit_row_get_search_hit (row); + search_hit = g_list_model_get_item (G_LIST_MODEL (self->results_selection_model), position); + g_assert (GCAL_IS_SEARCH_HIT (search_hit)); + gcal_search_hit_activate (search_hit, GTK_WIDGET (self)); - hide_suggestions (self); + quit_search_entry (self); } static void @@ -225,32 +300,11 @@ on_results_revealer_child_reveal_state_changed_cb (GtkRevealer *revealer, gtk_popover_popdown (GTK_POPOVER (self->popover)); } - -/* - * GtkWidget overrides - */ - static gboolean -gcal_search_button_focus (GtkWidget *widget, - GtkDirectionType direction) +string_is_not_empty_cb (GcalSearchHit *hit, + const gchar *string) { - GcalSearchButton *self = GCAL_SEARCH_BUTTON (widget); - - if (!gtk_widget_get_visible (GTK_WIDGET (self->popover))) - return gtk_widget_child_focus (GTK_WIDGET (self->stack), direction); - - if (direction == GTK_DIR_DOWN) - { - GtkListBoxRow *first_row = gtk_list_box_get_row_at_index (self->results_listbox, 0); - - if (!first_row) - return gtk_widget_child_focus (GTK_WIDGET (self->stack), direction); - - gtk_widget_grab_focus (GTK_WIDGET (first_row)); - return TRUE; - } - - return gtk_widget_child_focus (GTK_WIDGET (self->stack), direction); + return string != NULL && *string != '\0'; } @@ -319,7 +373,6 @@ gcal_search_button_set_property (GObject *object, } } - static void gcal_search_button_class_init (GcalSearchButtonClass *klass) { @@ -331,8 +384,6 @@ gcal_search_button_class_init (GcalSearchButtonClass *klass) object_class->get_property = gcal_search_button_get_property; object_class->set_property = gcal_search_button_set_property; - widget_class->focus = gcal_search_button_focus; - /** * GcalSearchButton::context: * @@ -350,16 +401,22 @@ gcal_search_button_class_init (GcalSearchButtonClass *klass) gtk_widget_class_bind_template_child (widget_class, GcalSearchButton, entry); gtk_widget_class_bind_template_child (widget_class, GcalSearchButton, popover); - gtk_widget_class_bind_template_child (widget_class, GcalSearchButton, results_listbox); + gtk_widget_class_bind_template_child (widget_class, GcalSearchButton, results_listview); + gtk_widget_class_bind_template_child (widget_class, GcalSearchButton, results_selection_model); gtk_widget_class_bind_template_child (widget_class, GcalSearchButton, results_revealer); gtk_widget_class_bind_template_child (widget_class, GcalSearchButton, stack); + gtk_widget_class_bind_template_callback (widget_class, escape_markup_cb); gtk_widget_class_bind_template_callback (widget_class, on_button_clicked_cb); gtk_widget_class_bind_template_callback (widget_class, on_focus_controller_leave_cb); + gtk_widget_class_bind_template_callback (widget_class, on_entry_activate_cb); + gtk_widget_class_bind_template_callback (widget_class, on_entry_next_match_cb); + gtk_widget_class_bind_template_callback (widget_class, on_entry_previous_match_cb); gtk_widget_class_bind_template_callback (widget_class, on_entry_search_changed_cb); gtk_widget_class_bind_template_callback (widget_class, on_entry_stop_search_cb); - gtk_widget_class_bind_template_callback (widget_class, on_results_listbox_row_activated_cb); + gtk_widget_class_bind_template_callback (widget_class, on_results_listview_activated_cb); gtk_widget_class_bind_template_callback (widget_class, on_results_revealer_child_reveal_state_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, string_is_not_empty_cb); gtk_widget_class_set_css_name (widget_class, "searchbutton"); } diff --git a/src/gui/gcal-search-button.ui b/src/gui/gcal-search-button.ui index d9bcf1c8..25db262a 100644 --- a/src/gui/gcal-search-button.ui +++ b/src/gui/gcal-search-button.ui @@ -52,8 +52,29 @@ <object class="GtkSearchEntry" id="entry"> <property name="max-width-chars">0</property> <property name="width-chars">0</property> + <signal name="activate" handler="on_entry_activate_cb" object="GcalSearchButton" swapped="no" /> + <signal name="next-match" handler="on_entry_next_match_cb" object="GcalSearchButton" swapped="no" /> + <signal name="previous-match" handler="on_entry_previous_match_cb" object="GcalSearchButton" swapped="no" /> <signal name="search-changed" handler="on_entry_search_changed_cb" object="GcalSearchButton" swapped="no" /> <signal name="stop-search" handler="on_entry_stop_search_cb" object="GcalSearchButton" swapped="no" /> + + <child> + <object class="GtkShortcutController"> + <child> + <object class="GtkShortcut"> + <property name='trigger'>Up</property> + <property name='action'>signal(previous-match)</property> + </object> + </child> + <child> + <object class="GtkShortcut"> + <property name='trigger'>Down</property> + <property name='action'>signal(next-match)</property> + </object> + </child> + </object> + </child> + </object> </property> </object> @@ -65,7 +86,7 @@ <object class="GtkPopover" id="popover"> <property name="position">bottom</property> <property name="autohide">False</property> - <property name="default-widget">results_listbox</property> + <property name="default-widget">results_listview</property> <style> <class name="menu" /> </style> @@ -84,18 +105,102 @@ <property name="hscrollbar-policy">never</property> <child> - <object class="GtkViewport"> - <property name="scroll-to-focus">True</property> + <object class="GtkListView" id="results_listview"> <property name="hscroll-policy">natural</property> <property name="vscroll-policy">natural</property> - - <child> - <object class="GtkListBox" id="results_listbox"> - <property name="selection-mode">none</property> - <signal name="row-activated" handler="on_results_listbox_row_activated_cb" object="GcalSearchButton" swapped="no" /> + <property name="single-click-activate">True</property> + <property name="model"> + <object class="GtkSingleSelection" id="results_selection_model"> + <property name="autoselect">True</property> + <property name="can-unselect">False</property> </object> - </child> + </property> + <property name="factory"> + <object class="GtkBuilderListItemFactory"> + <property name="bytes"><![CDATA[ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="GtkListItem"> + <property name="activatable">True</property> + <property name="selectable">True</property> + <property name="child"> + <object class="GtkBox"> + + <child> + <object class="GtkImage" id="image"> + <property name="valign">center</property> + <property name="hexpand">False</property> + <property name="icon-size">normal</property> + <binding name="paintable"> + <lookup name="primary-icon" type="GcalSearchHit"> + <lookup name="item">GtkListItem</lookup> + </lookup> + </binding> + <style> + <class name="title"/> + </style> + </object> + </child> + + <child> + <object class="GtkLabel" id="title"> + <property name="use-markup">True</property> + <property name="ellipsize">end</property> + <property name="xalign">0.0</property> + <property name="max-width-chars">40</property> + <binding name="label"> + <closure type="gchararray" function="escape_markup_cb"> + <lookup name="title" type="GcalSearchHit"> + <lookup name="item">GtkListItem</lookup> + </lookup> + </closure> + </binding> + </object> + </child> + + <child> + <object class="GtkLabel" id="separator"> + <property name="label">—</property> + <binding name="visible"> + <closure type="gboolean" function="string_is_not_empty_cb"> + <lookup name="subtitle" type="GcalSearchHit"> + <lookup name="item">GtkListItem</lookup> + </lookup> + </closure> + </binding> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + + <child> + <object class="GtkLabel" id="subtitle"> + <property name="hexpand">True</property> + <property name="use-markup">True</property> + <property name="ellipsize">end</property> + <property name="xalign">0.0</property> + <binding name="label"> + <closure type="gchararray" function="escape_markup_cb"> + <lookup name="subtitle" type="GcalSearchHit"> + <lookup name="item">GtkListItem</lookup> + </lookup> + </closure> + </binding> + <style> + <class name="subtitle"/> + </style> + </object> + </child> + </object> + </property> + </template> +</interface> + ]]></property> + </object> + </property> + <signal name="activate" handler="on_results_listview_activated_cb" object="GcalSearchButton" swapped="no" /> </object> </child> diff --git a/src/gui/gcal-search-hit-row.c b/src/gui/gcal-search-hit-row.c deleted file mode 100644 index 6819a270..00000000 --- a/src/gui/gcal-search-hit-row.c +++ /dev/null @@ -1,193 +0,0 @@ -/* gcal-search-hit-row.c - * - * Copyright 2022 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> - * - * 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 <http://www.gnu.org/licenses/>. - * - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -#include "gcal-search-hit-row.h" - -struct _GcalSearchHitRow -{ - GtkListBoxRow parent_instance; - - GtkImage *image; - GtkWidget *separator; - GtkLabel *subtitle; - GtkLabel *title; - - GcalSearchHit *search_hit; -}; - -G_DEFINE_FINAL_TYPE (GcalSearchHitRow, gcal_search_hit_row, GTK_TYPE_LIST_BOX_ROW) - -enum -{ - PROP_0, - PROP_SEARCH_HIT, - N_PROPS -}; - -static GParamSpec *properties [N_PROPS]; - - -/* - * Auxiliary methods - */ - -static void -update_search_hit (GcalSearchHitRow *self) -{ - g_autofree gchar *escaped_title = NULL; - const gchar *subtitle; - - gtk_image_set_from_paintable (self->image, gcal_search_hit_get_primary_icon (self->search_hit)); - - escaped_title = g_markup_escape_text (gcal_search_hit_get_title (self->search_hit), -1); - gtk_label_set_label (self->title, escaped_title); - - subtitle = gcal_search_hit_get_subtitle (self->search_hit); - if (subtitle) - { - g_autofree gchar *escaped_subtitle = NULL; - - escaped_subtitle = g_markup_escape_text (subtitle, -1); - escaped_subtitle = g_strstrip (escaped_subtitle); - gtk_label_set_label (self->subtitle, escaped_subtitle); - - gtk_widget_set_visible (self->separator, escaped_subtitle && *escaped_subtitle != '\0'); - } - else - { - gtk_widget_set_visible (self->separator, FALSE); - } -} - - -/* - * Callbacks - */ - -static void -on_search_hit_changed_cb (GcalSearchHit *search_hit, - GParamSpec *pspec, - GcalSearchHitRow *self) -{ - update_search_hit (self); -} - -/* - * GObject overrides - */ - -static void -gcal_search_hit_row_finalize (GObject *object) -{ - GcalSearchHitRow *self = (GcalSearchHitRow *)object; - - g_clear_object (&self->search_hit); - - G_OBJECT_CLASS (gcal_search_hit_row_parent_class)->finalize (object); -} - -static void -gcal_search_hit_row_get_property (GObject *object, - guint prop_id, - GValue *value, - GParamSpec *pspec) -{ - GcalSearchHitRow *self = GCAL_SEARCH_HIT_ROW (object); - - switch (prop_id) - { - case PROP_SEARCH_HIT: - g_value_set_object (value, self->search_hit); - break; - - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - } -} - -static void -gcal_search_hit_row_set_property (GObject *object, - guint prop_id, - const GValue *value, - GParamSpec *pspec) -{ - GcalSearchHitRow *self = GCAL_SEARCH_HIT_ROW (object); - - switch (prop_id) - { - case PROP_SEARCH_HIT: - g_assert (self->search_hit == NULL); - self->search_hit = g_value_dup_object (value); - g_signal_connect_object (self->search_hit, "notify", G_CALLBACK (on_search_hit_changed_cb), self, 0); - update_search_hit (self); - break; - - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - } -} - -static void -gcal_search_hit_row_class_init (GcalSearchHitRowClass *klass) -{ - GObjectClass *object_class = G_OBJECT_CLASS (klass); - GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); - - object_class->finalize = gcal_search_hit_row_finalize; - object_class->get_property = gcal_search_hit_row_get_property; - object_class->set_property = gcal_search_hit_row_set_property; - - properties[PROP_SEARCH_HIT] = g_param_spec_object ("search-hit", - NULL, - NULL, - GCAL_TYPE_SEARCH_HIT, - G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); - - g_object_class_install_properties (object_class, N_PROPS, properties); - - gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/calendar/ui/gui/gcal-search-hit-row.ui"); - - gtk_widget_class_bind_template_child (widget_class, GcalSearchHitRow, image); - gtk_widget_class_bind_template_child (widget_class, GcalSearchHitRow, separator); - gtk_widget_class_bind_template_child (widget_class, GcalSearchHitRow, subtitle); - gtk_widget_class_bind_template_child (widget_class, GcalSearchHitRow, title); -} - -static void -gcal_search_hit_row_init (GcalSearchHitRow *self) -{ - gtk_widget_init_template (GTK_WIDGET (self)); -} - -GtkWidget * -gcal_search_hit_row_new (GcalSearchHit *search_hit) -{ - return g_object_new (GCAL_TYPE_SEARCH_HIT_ROW, - "search-hit", search_hit, - NULL); -} - -GcalSearchHit * -gcal_search_hit_row_get_search_hit (GcalSearchHitRow *self) -{ - g_return_val_if_fail (GCAL_IS_SEARCH_HIT_ROW (self), NULL); - - return self->search_hit; -} diff --git a/src/gui/gcal-search-hit-row.h b/src/gui/gcal-search-hit-row.h deleted file mode 100644 index 947c964c..00000000 --- a/src/gui/gcal-search-hit-row.h +++ /dev/null @@ -1,36 +0,0 @@ -/* gcal-search-hit-row.h - * - * Copyright 2022 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> - * - * 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 <http://www.gnu.org/licenses/>. - * - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -#pragma once - -#include <gtk/gtk.h> - -#include "gcal-search-hit.h" - -G_BEGIN_DECLS - -#define GCAL_TYPE_SEARCH_HIT_ROW (gcal_search_hit_row_get_type()) -G_DECLARE_FINAL_TYPE (GcalSearchHitRow, gcal_search_hit_row, GCAL, SEARCH_HIT_ROW, GtkListBoxRow) - -GtkWidget * gcal_search_hit_row_new (GcalSearchHit *search_hit); - -GcalSearchHit * gcal_search_hit_row_get_search_hit (GcalSearchHitRow *self); - -G_END_DECLS diff --git a/src/gui/gcal-search-hit-row.ui b/src/gui/gcal-search-hit-row.ui deleted file mode 100644 index 23b0c0ea..00000000 --- a/src/gui/gcal-search-hit-row.ui +++ /dev/null @@ -1,53 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<interface> - <template class="GcalSearchHitRow" parent="GtkListBoxRow"> - - <child> - <object class="GtkBox"> - - <child> - <object class="GtkImage" id="image"> - <property name="valign">center</property> - <property name="hexpand">False</property> - <property name="icon-size">normal</property> - <style> - <class name="title"/> - </style> - </object> - </child> - - <child> - <object class="GtkLabel" id="title"> - <property name="use-markup">True</property> - <property name="ellipsize">end</property> - <property name="xalign">0.0</property> - <property name="max-width-chars">40</property> - </object> - </child> - - <child> - <object class="GtkLabel" id="separator"> - <property name="label">—</property> - <style> - <class name="dim-label"/> - </style> - </object> - </child> - - <child> - <object class="GtkLabel" id="subtitle"> - <property name="hexpand">True</property> - <property name="use-markup">True</property> - <property name="ellipsize">end</property> - <property name="xalign">0.0</property> - <style> - <class name="subtitle"/> - </style> - </object> - </child> - - </object> - </child> - - </template> -</interface> diff --git a/src/gui/gui.gresource.xml b/src/gui/gui.gresource.xml index c9b96b98..41e61589 100644 --- a/src/gui/gui.gresource.xml +++ b/src/gui/gui.gresource.xml @@ -7,7 +7,6 @@ <file compressed="true">gcal-meeting-row.ui</file> <file compressed="true">gcal-quick-add-popover.ui</file> <file compressed="true">gcal-search-button.ui</file> - <file compressed="true">gcal-search-hit-row.ui</file> <file compressed="true">gcal-toolbar-end.ui</file> <file compressed="true">gcal-weather-settings.ui</file> <file compressed="true">gcal-window.ui</file> diff --git a/src/gui/meson.build b/src/gui/meson.build index f5d3cad6..6f167647 100644 --- a/src/gui/meson.build +++ b/src/gui/meson.build @@ -23,7 +23,6 @@ sources += files( 'gcal-overflow-bin-layout.c', 'gcal-quick-add-popover.c', 'gcal-search-button.c', - 'gcal-search-hit-row.c', 'gcal-toolbar-end.c', 'gcal-weather-settings.c', 'gcal-window.c', diff --git a/src/theme/style-dark.css b/src/theme/style-dark.css index 2690aa01..7dbc5740 100644 --- a/src/theme/style-dark.css +++ b/src/theme/style-dark.css @@ -6,3 +6,11 @@ event.timed { color: @light_1; outline-color: rgba(0, 0, 0, 0.3); } + +/* + * Search + */ + +searchbutton > popover listview > row:selected { + background-color: alpha(@accent_bg_color, 0.2); +} diff --git a/src/theme/style.css b/src/theme/style.css index f53956b8..82ae7026 100644 --- a/src/theme/style.css +++ b/src/theme/style.css @@ -392,6 +392,10 @@ searchbutton > popover > arrow { border: none; } +searchbutton > popover listview > row:selected { + background-color: alpha(@accent_bg_color, 0.11); +} + /* * Month selector */ |