From 27f11087463e14a4b2e88000ed60a841d64945ee Mon Sep 17 00:00:00 2001 From: Michael Webster Date: Sat, 10 Jan 2026 14:10:57 -0500 Subject: [PATCH] XAppIconChooserDialog: Subclass GtkDialog, not XAppGtkWindow. The way we implement dialog.run() was causing issues when spawned from another dialog with its own event loop. This lets us rely on GtkDialog's handling for this, and allows us to run it non-blocking, and have a 'response' signal like any other dialog. --- libxapp/xapp-icon-chooser-button.h | 1 - libxapp/xapp-icon-chooser-dialog.c | 125 +++-------- libxapp/xapp-icon-chooser-dialog.h | 4 +- .../xapp-icon-chooser-dialog-full-test | 203 ++++++++++++++++++ 4 files changed, 232 insertions(+), 101 deletions(-) create mode 100755 test-scripts/xapp-icon-chooser-dialog-full-test diff --git a/libxapp/xapp-icon-chooser-button.h b/libxapp/xapp-icon-chooser-button.h index 418bf328..fd73f005 100644 --- a/libxapp/xapp-icon-chooser-button.h +++ b/libxapp/xapp-icon-chooser-button.h @@ -4,7 +4,6 @@ #include #include #include "xapp-icon-chooser-dialog.h" -#include "xapp-enums.h" G_BEGIN_DECLS diff --git a/libxapp/xapp-icon-chooser-dialog.c b/libxapp/xapp-icon-chooser-dialog.c index 13257e01..d08e0d11 100644 --- a/libxapp/xapp-icon-chooser-dialog.c +++ b/libxapp/xapp-icon-chooser-dialog.c @@ -48,7 +48,6 @@ typedef struct GtkWidget *default_button; GtkWidget *select_button; GtkWidget *browse_button; - GtkWidget *action_area; GtkWidget *loading_bar; GtkCellArea *ca_box; gchar *icon_string; @@ -61,7 +60,7 @@ typedef struct struct _XAppIconChooserDialog { - XAppGtkWindow parent_instance; + GtkDialog parent_instance; }; typedef struct @@ -128,7 +127,6 @@ static IconCategoryDefinition categories[] = { enum { - CLOSE, SELECT, LAST_SIGNAL }; @@ -153,7 +151,7 @@ static GParamSpec *obj_properties[N_PROPERTIES] = { NULL, }; static guint signals[LAST_SIGNAL] = {0, }; -G_DEFINE_TYPE_WITH_PRIVATE (XAppIconChooserDialog, xapp_icon_chooser_dialog, XAPP_TYPE_GTK_WINDOW) +G_DEFINE_TYPE_WITH_PRIVATE (XAppIconChooserDialog, xapp_icon_chooser_dialog, GTK_TYPE_DIALOG) static void on_category_selected (GtkListBox *list_box, XAppIconChooserDialog *dialog); @@ -172,14 +170,8 @@ static void on_icon_store_icons_added (GtkTreeModel *tree_model, static void on_browse_button_clicked (GtkButton *button, gpointer user_data); -static void on_select_button_clicked (GtkButton *button, - gpointer user_data); - -static void on_cancel_button_clicked (GtkButton *button, - gpointer user_data); - static void on_default_button_clicked (GtkButton *button, - gpointer user_data); + gpointer user_data); static gboolean on_search_bar_key_pressed (GtkWidget *widget, GdkEvent *event, @@ -414,8 +406,6 @@ xapp_icon_chooser_dialog_init (XAppIconChooserDialog *dialog) GtkWidget *right_box; GtkStyleContext *style; GtkSizeGroup *button_size_group; - GtkWidget *cancel_button; - GtkWidget *button_area; GtkWidget *scrolled_window; priv = xapp_icon_chooser_dialog_get_instance_private (dialog); @@ -450,11 +440,10 @@ xapp_icon_chooser_dialog_init (XAppIconChooserDialog *dialog) gtk_window_set_default_size (GTK_WINDOW (dialog), 600, 450); gtk_window_set_skip_taskbar_hint (GTK_WINDOW (dialog), TRUE); - gtk_window_set_type_hint (GTK_WINDOW (dialog), GDK_WINDOW_TYPE_HINT_DIALOG); gtk_window_set_title (GTK_WINDOW (dialog), _("Choose an icon")); main_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); - gtk_container_add (GTK_CONTAINER (dialog), main_box); + gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), main_box, TRUE, TRUE, 0); // toolbar toolbar = gtk_toolbar_new (); @@ -576,11 +565,6 @@ xapp_icon_chooser_dialog_init (XAppIconChooserDialog *dialog) g_signal_connect (priv->icon_view, "item-activated", G_CALLBACK (on_icon_view_item_activated), dialog); - // buttons - button_area = gtk_action_bar_new (); - priv->action_area = button_area; - gtk_box_pack_start (GTK_BOX (main_box), button_area, FALSE, FALSE, 0); - button_size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); priv->default_button = gtk_button_new_with_label (_("Default")); @@ -588,28 +572,17 @@ xapp_icon_chooser_dialog_init (XAppIconChooserDialog *dialog) style = gtk_widget_get_style_context (GTK_WIDGET (priv->default_button)); gtk_style_context_add_class (style, "text-button"); gtk_size_group_add_widget (button_size_group, priv->default_button); - gtk_action_bar_pack_start (GTK_ACTION_BAR (button_area), priv->default_button); - + gtk_dialog_add_action_widget (GTK_DIALOG (dialog), priv->default_button, GTK_RESPONSE_NONE); + gtk_button_box_set_child_secondary (GTK_BUTTON_BOX (gtk_dialog_get_action_area (GTK_DIALOG (dialog))), + priv->default_button, TRUE); g_signal_connect (priv->default_button, "clicked", G_CALLBACK (on_default_button_clicked), dialog); - priv->select_button = gtk_button_new_with_label (_("Select")); - style = gtk_widget_get_style_context (GTK_WIDGET (priv->select_button)); - gtk_style_context_add_class (style, "text-button"); - gtk_size_group_add_widget (button_size_group, priv->select_button); - gtk_action_bar_pack_end (GTK_ACTION_BAR (button_area), priv->select_button); - - g_signal_connect (priv->select_button, "clicked", - G_CALLBACK (on_select_button_clicked), dialog); - - cancel_button = gtk_button_new_with_label (_("Cancel")); - style = gtk_widget_get_style_context (GTK_WIDGET (cancel_button)); - gtk_style_context_add_class (style, "text-button"); - gtk_size_group_add_widget (button_size_group, cancel_button); - gtk_action_bar_pack_end (GTK_ACTION_BAR (button_area), cancel_button); + gtk_dialog_add_button (GTK_DIALOG (dialog), _("Cancel"), GTK_RESPONSE_CANCEL); - g_signal_connect (cancel_button, "clicked", - G_CALLBACK (on_cancel_button_clicked), dialog); + priv->select_button = gtk_dialog_add_button (GTK_DIALOG (dialog), _("Select"), GTK_RESPONSE_OK); + gtk_widget_set_can_default (priv->select_button, TRUE); + gtk_widget_grab_default (priv->select_button); load_categories (dialog); } @@ -669,14 +642,6 @@ xapp_icon_chooser_dialog_class_init (XAppIconChooserDialogClass *klass) g_object_class_install_properties (object_class, N_PROPERTIES, obj_properties); // keybinding signals - signals[CLOSE] = - g_signal_new ("close", - G_TYPE_FROM_CLASS (klass), - G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, - G_STRUCT_OFFSET (GtkWidgetClass, delete_event), - NULL, NULL, NULL, - G_TYPE_NONE, 0); - signals[SELECT] = g_signal_new ("select", G_TYPE_FROM_CLASS (klass), @@ -723,15 +688,16 @@ gint xapp_icon_chooser_dialog_run (XAppIconChooserDialog *dialog) { XAppIconChooserDialogPrivate *priv; + gint response; priv = xapp_icon_chooser_dialog_get_instance_private (dialog); gtk_widget_show_all (GTK_WIDGET (dialog)); gtk_widget_grab_focus (priv->search_bar); - gtk_main (); + response = gtk_dialog_run (GTK_DIALOG (dialog)); - return priv->response; + return response; } /** @@ -758,6 +724,7 @@ xapp_icon_chooser_dialog_run_with_icon (XAppIconChooserDialog *dialog, gchar *icon) { XAppIconChooserDialogPrivate *priv; + gint response; priv = xapp_icon_chooser_dialog_get_instance_private (dialog); @@ -765,9 +732,9 @@ xapp_icon_chooser_dialog_run_with_icon (XAppIconChooserDialog *dialog, gtk_entry_set_text (GTK_ENTRY (priv->search_bar), icon); gtk_widget_grab_focus (priv->search_bar); - gtk_main (); + response = gtk_dialog_run (GTK_DIALOG (dialog)); - return priv->response; + return response; } /** @@ -792,6 +759,7 @@ xapp_icon_chooser_dialog_run_with_category (XAppIconChooserDialog *dialog, { XAppIconChooserDialogPrivate *priv; GList *children; + gint response; priv = xapp_icon_chooser_dialog_get_instance_private (dialog); @@ -815,9 +783,9 @@ xapp_icon_chooser_dialog_run_with_category (XAppIconChooserDialog *dialog, } } - gtk_main (); + response = gtk_dialog_run (GTK_DIALOG (dialog)); - return priv->response; + return response; } /** @@ -935,30 +903,19 @@ xapp_icon_chooser_dialog_close (XAppIconChooserDialog *dialog, priv = xapp_icon_chooser_dialog_get_instance_private (dialog); priv->response = response; - gtk_widget_hide (GTK_WIDGET (dialog)); - - gtk_main_quit (); -} - -static void -on_custom_button_clicked (GtkButton *button, - gpointer user_data) -{ - GtkResponseType response_id; - - response_id = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (button), "response-id")); + gtk_dialog_response (GTK_DIALOG (dialog), response); - xapp_icon_chooser_dialog_close (XAPP_ICON_CHOOSER_DIALOG (user_data), response_id); + gtk_widget_hide (GTK_WIDGET (dialog)); } /** * xapp_icon_chooser_dialog_add_button: * @dialog: an #XAppIconChooserDialog * @button: a #GtkButton to add - * @packing: the #GtkPackType to specify start or end packing to the action bar + * @packing: the #GtkPackType to specify start or end packing to the action area * @response_id: the dialog response id to return when this button is clicked. * - * Allows a button to be added to the #GtkActionBar of the dialog with a custom + * Allows a button to be added to the action area of the dialog with a custom * response id. */ void @@ -967,26 +924,14 @@ xapp_icon_chooser_dialog_add_button (XAppIconChooserDialog *dialog, GtkPackType packing, GtkResponseType response_id) { - XAppIconChooserDialogPrivate *priv; + GtkWidget *action_area; - priv = xapp_icon_chooser_dialog_get_instance_private (dialog); - - g_signal_connect (button, - "clicked", - G_CALLBACK (on_custom_button_clicked), - dialog); - - /* This saves having to use a custom container for callback data. */ - g_object_set_data (G_OBJECT (button), - "response-id", GINT_TO_POINTER (response_id)); + gtk_dialog_add_action_widget (GTK_DIALOG (dialog), button, response_id); if (packing == GTK_PACK_START) { - gtk_action_bar_pack_start (GTK_ACTION_BAR (priv->action_area), button); - } - else - { - gtk_action_bar_pack_end (GTK_ACTION_BAR (priv->action_area), button); + action_area = gtk_dialog_get_action_area (GTK_DIALOG (dialog)); + gtk_button_box_set_child_secondary (GTK_BUTTON_BOX (action_area), button, TRUE); } } @@ -1972,20 +1917,6 @@ on_browse_button_clicked (GtkButton *button, gtk_widget_destroy (file_dialog); } -static void -on_select_button_clicked (GtkButton *button, - gpointer user_data) -{ - xapp_icon_chooser_dialog_close (XAPP_ICON_CHOOSER_DIALOG (user_data), GTK_RESPONSE_OK); -} - -static void -on_cancel_button_clicked (GtkButton *button, - gpointer user_data) -{ - xapp_icon_chooser_dialog_close (XAPP_ICON_CHOOSER_DIALOG (user_data), GTK_RESPONSE_CANCEL); -} - static gboolean on_delete_event (GtkWidget *widget, GdkEventAny *event) diff --git a/libxapp/xapp-icon-chooser-dialog.h b/libxapp/xapp-icon-chooser-dialog.h index 439039ba..5da37ed1 100644 --- a/libxapp/xapp-icon-chooser-dialog.h +++ b/libxapp/xapp-icon-chooser-dialog.h @@ -4,13 +4,11 @@ #include #include -#include "xapp-gtk-window.h" - G_BEGIN_DECLS #define XAPP_TYPE_ICON_CHOOSER_DIALOG (xapp_icon_chooser_dialog_get_type ()) -G_DECLARE_FINAL_TYPE (XAppIconChooserDialog, xapp_icon_chooser_dialog, XAPP, ICON_CHOOSER_DIALOG, XAppGtkWindow) +G_DECLARE_FINAL_TYPE (XAppIconChooserDialog, xapp_icon_chooser_dialog, XAPP, ICON_CHOOSER_DIALOG, GtkDialog) typedef enum { diff --git a/test-scripts/xapp-icon-chooser-dialog-full-test b/test-scripts/xapp-icon-chooser-dialog-full-test new file mode 100755 index 00000000..96db75d2 --- /dev/null +++ b/test-scripts/xapp-icon-chooser-dialog-full-test @@ -0,0 +1,203 @@ +#!/usr/bin/python3 + +import gi +gi.require_version('Gtk', '3.0') +gi.require_version('XApp', '1.0') + +from gi.repository import Gtk, XApp, GLib + +import signal +signal.signal(signal.SIGINT, signal.SIG_DFL) + + +class TestWindow(Gtk.Window): + def __init__(self): + super().__init__(title="XAppIconChooserDialog Test") + self.set_default_size(400, 300) + self.set_border_width(10) + self.connect("destroy", Gtk.main_quit) + + self.selected_icon = None + + main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) + self.add(main_box) + + result_frame = Gtk.Frame(label="Selected Icon") + main_box.pack_start(result_frame, False, False, 0) + + result_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) + result_box.set_margin_start(10) + result_box.set_margin_end(10) + result_box.set_margin_top(5) + result_box.set_margin_bottom(5) + result_frame.add(result_box) + + self.result_image = Gtk.Image() + self.result_image.set_size_request(48, 48) + result_box.pack_start(self.result_image, False, False, 0) + + self.result_label = Gtk.Label(label="(none)") + self.result_label.set_xalign(0) + self.result_label.set_line_wrap(True) + self.result_label.set_selectable(True) + result_box.pack_start(self.result_label, True, True, 0) + + # Test sections + # Section 1: Blocking run() methods + section1 = Gtk.Frame(label="1. Blocking run() Methods") + main_box.pack_start(section1, False, False, 0) + + box1 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) + box1.set_margin_start(10) + box1.set_margin_end(10) + box1.set_margin_top(5) + box1.set_margin_bottom(5) + section1.add(box1) + + btn_run = Gtk.Button(label="run()") + btn_run.connect("clicked", self.on_test_run) + box1.pack_start(btn_run, False, False, 0) + + hbox1 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) + box1.pack_start(hbox1, False, False, 0) + + btn_run_icon = Gtk.Button(label="run_with_icon()") + btn_run_icon.connect("clicked", self.on_test_run_with_icon) + hbox1.pack_start(btn_run_icon, True, True, 0) + + self.icon_entry = Gtk.Entry() + self.icon_entry.set_text("folder") + self.icon_entry.set_placeholder_text("icon name or path") + hbox1.pack_start(self.icon_entry, True, True, 0) + + hbox2 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) + box1.pack_start(hbox2, False, False, 0) + + btn_run_cat = Gtk.Button(label="run_with_category()") + btn_run_cat.connect("clicked", self.on_test_run_with_category) + hbox2.pack_start(btn_run_cat, True, True, 0) + + self.category_entry = Gtk.Entry() + self.category_entry.set_text("Places") + self.category_entry.set_placeholder_text("category name") + hbox2.pack_start(self.category_entry, True, True, 0) + + # Section 2: Non-blocking (response signal) + section2 = Gtk.Frame(label="2. Non-blocking (show + response signal)") + main_box.pack_start(section2, False, False, 0) + + box2 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) + box2.set_margin_start(10) + box2.set_margin_end(10) + box2.set_margin_top(5) + box2.set_margin_bottom(5) + section2.add(box2) + + btn_async = Gtk.Button(label="Show Dialog (async)") + btn_async.connect("clicked", self.on_test_async) + box2.pack_start(btn_async, False, False, 0) + + section3 = Gtk.Frame(label="3. XAppIconChooserButton") + main_box.pack_start(section3, False, False, 0) + + box3 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) + box3.set_margin_start(10) + box3.set_margin_end(10) + box3.set_margin_top(5) + box3.set_margin_bottom(5) + section3.add(box3) + + label3 = Gtk.Label(label="Icon Button:") + box3.pack_start(label3, False, False, 0) + + self.icon_button = XApp.IconChooserButton() + self.icon_button.set_icon("application-x-executable") + self.icon_button.connect("notify::icon", self.on_icon_button_changed) + box3.pack_start(self.icon_button, False, False, 0) + + self.icon_button_label = Gtk.Label(label="(click button to choose)") + self.icon_button_label.set_xalign(0) + box3.pack_start(self.icon_button_label, True, True, 0) + + def update_result(self, icon_string, source): + if icon_string: + self.result_label.set_text(f"{icon_string}\n(from {source})") + if icon_string.startswith("/"): + self.result_image.set_from_file(icon_string) + else: + self.result_image.set_from_icon_name(icon_string, Gtk.IconSize.DIALOG) + else: + self.result_label.set_text("(canceled)") + self.result_image.clear() + + def on_test_run(self, button): + dialog = XApp.IconChooserDialog() + dialog.set_transient_for(self) + dialog.set_default_icon("folder") + + response = dialog.run() + + if response == Gtk.ResponseType.OK: + self.update_result(dialog.get_icon_string(), "run()") + else: + self.update_result(None, "run()") + + dialog.destroy() + + def on_test_run_with_icon(self, button): + icon = self.icon_entry.get_text() + dialog = XApp.IconChooserDialog() + dialog.set_transient_for(self) + + response = dialog.run_with_icon(icon) + + if response == Gtk.ResponseType.OK: + self.update_result(dialog.get_icon_string(), f"run_with_icon('{icon}')") + else: + self.update_result(None, f"run_with_icon('{icon}')") + + dialog.destroy() + + def on_test_run_with_category(self, button): + category = self.category_entry.get_text() + dialog = XApp.IconChooserDialog() + dialog.set_transient_for(self) + + response = dialog.run_with_category(category) + + if response == Gtk.ResponseType.OK: + self.update_result(dialog.get_icon_string(), f"run_with_category('{category}')") + else: + self.update_result(None, f"run_with_category('{category}')") + + dialog.destroy() + + def on_test_async(self, button): + dialog = XApp.IconChooserDialog() + dialog.set_transient_for(self) + dialog.set_modal(True) + dialog.set_default_icon("folder") + + dialog.connect("response", self.on_async_response) + dialog.show_all() + + def on_async_response(self, dialog, response): + if response == Gtk.ResponseType.OK: + self.update_result(dialog.get_icon_string(), "async (response signal)") + else: + self.update_result(None, "async (response signal)") + + dialog.hide() + GLib.idle_add(dialog.destroy) + + def on_icon_button_changed(self, button, pspec): + icon = button.get_icon() + self.icon_button_label.set_text(icon if icon else "(none)") + if icon: + self.update_result(icon, "XAppIconChooserButton") + + +if __name__ == '__main__': + win = TestWindow() + win.show_all() + Gtk.main()