/* PIKA - Photo and Image Kooker Application * a rebranding of The GNU Image Manipulation Program (created with heckimp) * A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio * * Original copyright, applying to most contents (license remains unchanged): * Copyright (C) 1995 Spencer Kimball and Peter Mattis * * pikasearchpopup.c * Copyright (C) 2015 Jehan * * 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 . */ #include "config.h" #include #include #include #include "libpikabase/pikabase.h" #include "libpikawidgets/pikawidgets.h" #include "widgets-types.h" #include "core/pika.h" #include "pikaaction.h" #include "pikahelp-ids.h" #include "pikapopup.h" #include "pikasearchpopup.h" #include "pikatoggleaction.h" #include "pika-intl.h" enum { COLUMN_ICON, COLUMN_MARKUP, COLUMN_TOOLTIP, COLUMN_ACTION, COLUMN_SENSITIVE, COLUMN_SECTION, N_COL }; enum { PROP_0, PROP_PIKA, PROP_CALLBACK, PROP_CALLBACK_DATA }; struct _PikaSearchPopupPrivate { Pika *pika; GtkWidget *keyword_entry; GtkWidget *results_list; GtkWidget *list_view; PikaSearchPopupCallback build_results; gpointer build_results_data; }; static void pika_search_popup_constructed (GObject *object); static void pika_search_popup_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec); static void pika_search_popup_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec); static void pika_search_popup_size_allocate (GtkWidget *widget, GtkAllocation *allocation); static void pika_search_popup_confirm (PikaPopup *popup); /* Signal handlers on the search entry */ static void keyword_entry_icon_press (GtkEntry *entry, GtkEntryIconPosition icon_pos, GdkEvent *event, PikaSearchPopup *popup); static gboolean keyword_entry_key_press_event (GtkWidget *widget, GdkEventKey *event, PikaSearchPopup *popup); static gboolean keyword_entry_key_release_event (GtkWidget *widget, GdkEventKey *event, PikaSearchPopup *popup); /* Signal handlers on the results list */ static gboolean results_list_key_press_event (GtkWidget *widget, GdkEventKey *kevent, PikaSearchPopup *popup); static void results_list_row_activated (GtkTreeView *treeview, GtkTreePath *path, GtkTreeViewColumn *col, PikaSearchPopup *popup); /* Utils */ static void pika_search_popup_run_selected (PikaSearchPopup *popup); static void pika_search_popup_setup_results (GtkWidget **results_list, GtkWidget **list_view); static void pika_search_popup_help (PikaSearchPopup *popup); G_DEFINE_TYPE_WITH_PRIVATE (PikaSearchPopup, pika_search_popup, PIKA_TYPE_POPUP) #define parent_class pika_search_popup_parent_class static gint window_height = 0; static void pika_search_popup_class_init (PikaSearchPopupClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); PikaPopupClass *popup_class = PIKA_POPUP_CLASS (klass); object_class->constructed = pika_search_popup_constructed; object_class->set_property = pika_search_popup_set_property; object_class->get_property = pika_search_popup_get_property; widget_class->size_allocate = pika_search_popup_size_allocate; popup_class->confirm = pika_search_popup_confirm; /** * PikaSearchPopup:pika: * * The #Pika object. */ g_object_class_install_property (object_class, PROP_PIKA, g_param_spec_object ("pika", NULL, NULL, G_TYPE_OBJECT, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); /** * PikaSearchPopup:callback: * * The #PikaSearchPopupCallback used to fill in results. */ g_object_class_install_property (object_class, PROP_CALLBACK, g_param_spec_pointer ("callback", NULL, NULL, PIKA_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); /** * PikaSearchPopup:callback-data: * * The #GPointer fed as last parameter to the #PikaSearchPopupCallback. */ g_object_class_install_property (object_class, PROP_CALLBACK_DATA, g_param_spec_pointer ("callback-data", NULL, NULL, PIKA_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); } static void pika_search_popup_init (PikaSearchPopup *search_popup) { search_popup->priv = pika_search_popup_get_instance_private (search_popup); } /************ Public Functions ****************/ /** * pika_search_popup_new: * @pika: #Pika object. * @role: the role to give to the #GtkWindow. * @title: the #GtkWindow title. * @callback: the #PikaSearchPopupCallback used to fill in results. * @callback_data: data fed to @callback. * * Returns: a new #PikaSearchPopup. */ GtkWidget * pika_search_popup_new (Pika *pika, const gchar *role, const gchar *title, PikaSearchPopupCallback callback, gpointer callback_data) { GtkWidget *widget; widget = g_object_new (PIKA_TYPE_SEARCH_POPUP, "type", GTK_WINDOW_TOPLEVEL, "type-hint", GDK_WINDOW_TYPE_HINT_DIALOG, "decorated", TRUE, "modal", TRUE, "role", role, "title", title, "pika", pika, "callback", callback, "callback-data", callback_data, NULL); gtk_window_set_modal (GTK_WINDOW (widget), FALSE); return widget; } /** * pika_search_popup_add_result: * @popup: the #PikaSearchPopup. * @action: a #PikaAction to add in results list. * @section: the section to add @action. * * Adds @action in the @popup's results at @section. * The section only indicates relative order. If you want some items * to appear before other, simply use lower @section. */ void pika_search_popup_add_result (PikaSearchPopup *popup, PikaAction *action, gint section) { gchar **accels; GtkTreeIter iter; GtkTreeIter next_section; GtkListStore *store; GtkTreeModel *model; gchar *markup; gchar *action_name; gchar *label; gchar *escaped_label = NULL; const gchar *icon_name; gchar *shortcut_helper = NULL; const gchar *menu_path; gchar *menu_path_helper = NULL; const gchar *tooltip; gchar *escaped_tooltip = NULL; gboolean has_tooltip = FALSE; gboolean sensitive = FALSE; const gchar *sensitive_reason = NULL; gchar *escaped_reason = NULL; label = g_strstrip (pika_strip_uline (pika_action_get_label (action))); if (! label || strlen (label) == 0) { g_free (label); return; } escaped_label = g_markup_escape_text (label, -1); if (PIKA_IS_TOGGLE_ACTION (action)) { if (pika_toggle_action_get_active (PIKA_TOGGLE_ACTION (action))) icon_name = "gtk-ok"; else icon_name = "gtk-no"; } else { icon_name = pika_action_get_icon_name (action); } accels = pika_action_get_display_accels (action); if (accels && accels[0]) { gchar *formatted_label; gchar *formatted_value; /* TRANSLATORS: a helper label indicating the shortcut for an action, * it will be used within the generic "%s: %s" (also localized) string. * e.g.: "shortcut: Ctrl+Alt+O" */ formatted_label = g_markup_printf_escaped ("%s", _("shortcut")); formatted_value = g_markup_escape_text (accels[0], -1); /* TRANSLATORS: generic "title: value" label which will be used in various ways. */ shortcut_helper = g_strdup_printf (_("%s: %s"), formatted_label, formatted_value); g_free (formatted_label); g_free (formatted_value); } g_strfreev (accels); if ((menu_path = pika_action_get_menu_path (action)) != NULL) { if (strlen (menu_path) > 0) { gchar *formatted_label; gchar *formatted_value; /* TRANSLATORS: a helper label indicating the menu path for an action, * it will be used within the generic "%s: %s" (also localized) string. * e.g.: "Menu: Filters > Generic" */ formatted_label = g_markup_printf_escaped ("%s", _("menu")); formatted_value = g_markup_escape_text (menu_path, -1); /* TRANSLATORS: generic "title: value" label which will be used in various ways. */ menu_path_helper = g_strdup_printf (_("%s: %s"), formatted_label, formatted_value); g_free (formatted_label); g_free (formatted_value); } } tooltip = pika_action_get_tooltip (action); if (tooltip != NULL) { escaped_tooltip = g_markup_escape_text (tooltip, -1); has_tooltip = TRUE; } sensitive = pika_action_is_sensitive (action, &sensitive_reason); if (sensitive_reason != NULL) { escaped_reason = g_markup_escape_text (sensitive_reason, -1); } markup = g_strdup_printf ("%s" /* Label */ "%s%s" /* Shortcut */ "%s%s" /* Menu path */ "%s%s" /* Tooltip */ "%s%s" /* Inactive reason */ "", escaped_label, shortcut_helper ? " | " : "", shortcut_helper ? shortcut_helper : "", menu_path_helper ? " | " : "", menu_path_helper ? menu_path_helper : "", has_tooltip ? "\n" : "", has_tooltip ? escaped_tooltip : "", escaped_reason ? "\n" : "", escaped_reason ? escaped_reason : ""); action_name = g_markup_escape_text (pika_action_get_name (action), -1); model = gtk_tree_view_get_model (GTK_TREE_VIEW (popup->priv->results_list)); store = GTK_LIST_STORE (model); if (gtk_tree_model_get_iter_first (model, &next_section)) { while (TRUE) { gint iter_section; gtk_tree_model_get (model, &next_section, COLUMN_SECTION, &iter_section, -1); if (iter_section > section) { gtk_list_store_insert_before (store, &iter, &next_section); break; } else if (! gtk_tree_model_iter_next (model, &next_section)) { gtk_list_store_append (store, &iter); break; } } } else { gtk_list_store_append (store, &iter); } gtk_list_store_set (store, &iter, COLUMN_ICON, icon_name, COLUMN_MARKUP, markup, COLUMN_TOOLTIP, action_name, COLUMN_ACTION, action, COLUMN_SECTION, section, COLUMN_SENSITIVE, sensitive, -1); g_free (markup); g_free (action_name); g_free (label); g_free (escaped_label); g_free (escaped_tooltip); g_free (escaped_reason); g_free (menu_path_helper); g_free (shortcut_helper); } /************ Private Functions ****************/ static void pika_search_popup_constructed (GObject *object) { PikaSearchPopup *popup = PIKA_SEARCH_POPUP (object); GtkWidget *main_vbox; G_OBJECT_CLASS (parent_class)->constructed (object); main_vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 2); gtk_container_add (GTK_CONTAINER (popup), main_vbox); gtk_widget_show (main_vbox); popup->priv->keyword_entry = gtk_entry_new (); gtk_entry_set_icon_from_icon_name (GTK_ENTRY (popup->priv->keyword_entry), GTK_ENTRY_ICON_PRIMARY, "edit-find"); gtk_entry_set_icon_activatable (GTK_ENTRY (popup->priv->keyword_entry), GTK_ENTRY_ICON_PRIMARY, FALSE); gtk_entry_set_icon_from_icon_name (GTK_ENTRY (popup->priv->keyword_entry), GTK_ENTRY_ICON_SECONDARY, PIKA_ICON_HELP); gtk_entry_set_icon_activatable (GTK_ENTRY (popup->priv->keyword_entry), GTK_ENTRY_ICON_SECONDARY, TRUE); gtk_box_pack_start (GTK_BOX (main_vbox), popup->priv->keyword_entry, FALSE, FALSE, 0); gtk_widget_show (popup->priv->keyword_entry); pika_search_popup_setup_results (&popup->priv->results_list, &popup->priv->list_view); gtk_box_pack_start (GTK_BOX (main_vbox), popup->priv->list_view, TRUE, TRUE, 0); gtk_widget_set_events (GTK_WIDGET (object), GDK_KEY_RELEASE_MASK | GDK_KEY_PRESS_MASK | GDK_BUTTON_PRESS_MASK | GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK); g_signal_connect (popup->priv->keyword_entry, "icon-press", G_CALLBACK (keyword_entry_icon_press), popup); g_signal_connect (popup->priv->keyword_entry, "key-press-event", G_CALLBACK (keyword_entry_key_press_event), popup); g_signal_connect (popup->priv->keyword_entry, "key-release-event", G_CALLBACK (keyword_entry_key_release_event), popup); g_signal_connect (popup->priv->results_list, "key-press-event", G_CALLBACK (results_list_key_press_event), popup); g_signal_connect (popup->priv->results_list, "row-activated", G_CALLBACK (results_list_row_activated), popup); } static void pika_search_popup_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { PikaSearchPopup *search_popup = PIKA_SEARCH_POPUP (object); switch (property_id) { case PROP_PIKA: search_popup->priv->pika = g_value_get_object (value); break; case PROP_CALLBACK: search_popup->priv->build_results = g_value_get_pointer (value); break; case PROP_CALLBACK_DATA: search_popup->priv->build_results_data = g_value_get_pointer (value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void pika_search_popup_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { PikaSearchPopup *search_popup = PIKA_SEARCH_POPUP (object); switch (property_id) { case PROP_PIKA: g_value_set_object (value, search_popup->priv->pika); break; case PROP_CALLBACK: g_value_set_pointer (value, search_popup->priv->build_results); break; case PROP_CALLBACK_DATA: g_value_set_pointer (value, search_popup->priv->build_results_data); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void pika_search_popup_size_allocate (GtkWidget *widget, GtkAllocation *allocation) { PikaSearchPopup *popup = PIKA_SEARCH_POPUP (widget); GdkRectangle workarea; GTK_WIDGET_CLASS (parent_class)->size_allocate (widget, allocation); gdk_monitor_get_workarea (pika_widget_get_monitor (widget), &workarea); if (window_height == 0) { /* Default to half the monitor */ window_height = workarea.height / 2; } if (gtk_widget_get_visible (widget) && gtk_widget_get_visible (popup->priv->list_view)) { /* Save the window height when results are shown so that resizes * by the user are saved across searches. */ window_height = MAX (workarea.height / 4, allocation->height); } } static void pika_search_popup_confirm (PikaPopup *popup) { PikaSearchPopup *search_popup = PIKA_SEARCH_POPUP (popup); pika_search_popup_run_selected (search_popup); } static void keyword_entry_icon_press (GtkEntry *entry, GtkEntryIconPosition icon_pos, GdkEvent *event, PikaSearchPopup *popup) { if (icon_pos == GTK_ENTRY_ICON_SECONDARY) pika_search_popup_help (popup); } static gboolean keyword_entry_key_press_event (GtkWidget *widget, GdkEventKey *event, PikaSearchPopup *popup) { gboolean event_processed = FALSE; if (event->keyval == GDK_KEY_Down && gtk_widget_get_visible (popup->priv->list_view)) { GtkTreeView *tree_view = GTK_TREE_VIEW (popup->priv->results_list); GtkTreePath *path; /* When hitting the down key while editing, select directly the * second item, since the first could have run directly with * Enter. */ path = gtk_tree_path_new_from_string ("1"); gtk_tree_selection_select_path (gtk_tree_view_get_selection (tree_view), path); gtk_tree_path_free (path); gtk_widget_grab_focus (GTK_WIDGET (popup->priv->results_list)); event_processed = TRUE; } return event_processed; } static gboolean keyword_entry_key_release_event (GtkWidget *widget, GdkEventKey *event, PikaSearchPopup *popup) { GtkTreeView *tree_view = GTK_TREE_VIEW (popup->priv->results_list); GtkTreePath *path; gchar *entry_text; gint width; /* These keys are already managed by key bindings. */ if (event->keyval == GDK_KEY_Escape || event->keyval == GDK_KEY_Return || event->keyval == GDK_KEY_KP_Enter || event->keyval == GDK_KEY_ISO_Enter) { return FALSE; } gtk_window_get_size (GTK_WINDOW (popup), &width, NULL); entry_text = g_strstrip (gtk_editable_get_chars (GTK_EDITABLE (widget), 0, -1)); if (event->keyval == GDK_KEY_F1) { pika_search_popup_help (popup); } else if (strcmp (entry_text, "") != 0) { path = gtk_tree_path_new_from_string ("0"); gtk_window_resize (GTK_WINDOW (popup), width, window_height); gtk_list_store_clear (GTK_LIST_STORE (gtk_tree_view_get_model (tree_view))); gtk_widget_show_all (popup->priv->list_view); popup->priv->build_results (popup, entry_text, popup->priv->build_results_data); gtk_tree_selection_select_path (gtk_tree_view_get_selection (tree_view), path); gtk_tree_path_free (path); } else if (strcmp (entry_text, "") == 0 && (event->keyval == GDK_KEY_Down)) { path = gtk_tree_path_new_from_string ("0"); gtk_window_resize (GTK_WINDOW (popup), width, window_height); gtk_list_store_clear (GTK_LIST_STORE (gtk_tree_view_get_model (tree_view))); gtk_widget_show_all (popup->priv->list_view); popup->priv->build_results (popup, NULL, popup->priv->build_results_data); gtk_tree_selection_select_path (gtk_tree_view_get_selection (tree_view), path); gtk_tree_path_free (path); } else { GtkTreeSelection *selection; GtkTreeModel *model; GtkTreeIter iter; selection = gtk_tree_view_get_selection (tree_view); gtk_tree_selection_set_mode (selection, GTK_SELECTION_SINGLE); if (gtk_tree_selection_get_selected (selection, &model, &iter)) { path = gtk_tree_model_get_path (model, &iter); gtk_tree_selection_unselect_path (selection, path); gtk_tree_path_free (path); } gtk_widget_hide (popup->priv->list_view); gtk_window_resize (GTK_WINDOW (popup), width, 1); } g_free (entry_text); return TRUE; } static gboolean results_list_key_press_event (GtkWidget *widget, GdkEventKey *kevent, PikaSearchPopup *popup) { /* These keys are already managed by key bindings. */ g_return_val_if_fail (kevent->keyval != GDK_KEY_Escape && kevent->keyval != GDK_KEY_Return && kevent->keyval != GDK_KEY_KP_Enter && kevent->keyval != GDK_KEY_ISO_Enter, FALSE); switch (kevent->keyval) { case GDK_KEY_Up: { gboolean event_processed = FALSE; GtkTreeSelection *selection; GtkTreeModel *model; GtkTreeIter iter; selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (popup->priv->results_list)); gtk_tree_selection_set_mode (selection, GTK_SELECTION_SINGLE); if (gtk_tree_selection_get_selected (selection, &model, &iter)) { GtkTreePath *path = gtk_tree_model_get_path (model, &iter); gchar *path_str; path_str = gtk_tree_path_to_string (path); if (strcmp (path_str, "0") == 0) { gint start_pos; gint end_pos; gtk_editable_get_selection_bounds (GTK_EDITABLE (popup->priv->keyword_entry), &start_pos, &end_pos); gtk_widget_grab_focus ((GTK_WIDGET (popup->priv->keyword_entry))); gtk_editable_select_region (GTK_EDITABLE (popup->priv->keyword_entry), start_pos, end_pos); event_processed = TRUE; } g_free (path_str); gtk_tree_path_free (path); } return event_processed; } case GDK_KEY_Down: { return FALSE; } default: { gint start_pos; gint end_pos; gtk_editable_get_selection_bounds (GTK_EDITABLE (popup->priv->keyword_entry), &start_pos, &end_pos); gtk_widget_grab_focus ((GTK_WIDGET (popup->priv->keyword_entry))); gtk_editable_select_region (GTK_EDITABLE (popup->priv->keyword_entry), start_pos, end_pos); gtk_widget_event (GTK_WIDGET (popup->priv->keyword_entry), (GdkEvent *) kevent); } } return FALSE; } static void results_list_row_activated (GtkTreeView *treeview, GtkTreePath *path, GtkTreeViewColumn *col, PikaSearchPopup *popup) { pika_search_popup_run_selected (popup); } static void pika_search_popup_run_selected (PikaSearchPopup *popup) { GtkTreeSelection *selection; GtkTreeModel *model; GtkTreeIter iter; selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (popup->priv->results_list)); gtk_tree_selection_set_mode (selection, GTK_SELECTION_SINGLE); if (gtk_tree_selection_get_selected (selection, &model, &iter)) { PikaAction *action; gtk_tree_model_get (model, &iter, COLUMN_ACTION, &action, -1); if (pika_action_is_sensitive (action, NULL)) { /* Close the search popup on activation. */ PIKA_POPUP_CLASS (parent_class)->cancel (PIKA_POPUP (popup)); pika_action_activate (action); } g_object_unref (action); } } static void pika_search_popup_setup_results (GtkWidget **results_list, GtkWidget **list_view) { gint wid1 = 100; GtkListStore *store; GtkCellRenderer *cell; GtkTreeViewColumn *column; *list_view = gtk_scrolled_window_new (NULL, NULL); store = gtk_list_store_new (N_COL, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, PIKA_TYPE_ACTION, G_TYPE_BOOLEAN, G_TYPE_INT); *results_list = gtk_tree_view_new_with_model (GTK_TREE_MODEL (store)); gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (*results_list), FALSE); #ifdef PIKA_UNSTABLE gtk_tree_view_set_tooltip_column (GTK_TREE_VIEW (*results_list), COLUMN_TOOLTIP); #endif cell = gtk_cell_renderer_pixbuf_new (); column = gtk_tree_view_column_new_with_attributes (NULL, cell, "icon-name", COLUMN_ICON, "sensitive", COLUMN_SENSITIVE, NULL); gtk_tree_view_append_column (GTK_TREE_VIEW (*results_list), column); gtk_tree_view_column_set_min_width (column, 22); cell = gtk_cell_renderer_text_new (); column = gtk_tree_view_column_new_with_attributes (NULL, cell, "markup", COLUMN_MARKUP, "sensitive", COLUMN_SENSITIVE, NULL); gtk_tree_view_append_column (GTK_TREE_VIEW (*results_list), column); gtk_tree_view_column_set_max_width (column, wid1); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (*list_view), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); gtk_container_add (GTK_CONTAINER (*list_view), *results_list); g_object_unref (G_OBJECT (store)); } static void pika_search_popup_help (PikaSearchPopup *popup) { GtkTreeView *tree_view; const gchar *help_id = NULL; PikaAction *action = NULL; GtkTreeSelection *selection; GtkTreeModel *model; GtkTreeIter iter; tree_view = GTK_TREE_VIEW (popup->priv->results_list); selection = gtk_tree_view_get_selection (tree_view); if (gtk_tree_selection_get_selected (selection, &model, &iter)) { gtk_tree_model_get (model, &iter, COLUMN_ACTION, &action, -1); help_id = pika_action_get_help_id (action); } if (help_id == NULL) help_id = PIKA_HELP_ACTION_SEARCH_DIALOG; pika_help (popup->priv->pika, NULL, NULL, help_id); g_clear_object (&action); }