From 35e61484000bf60d378d8b731eb854310ebb34f7 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Mon, 11 May 2026 08:28:20 +0200 Subject: [PATCH 001/122] gtk: replace deprecated tooltip timestamp ABI field --- gtk/gtktooltips.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gtk/gtktooltips.h b/gtk/gtktooltips.h index eb1e0a0be..ee2cc78bd 100644 --- a/gtk/gtktooltips.h +++ b/gtk/gtktooltips.h @@ -74,7 +74,10 @@ struct _GtkTooltips guint have_grab : 1; guint use_sticky_delay : 1; gint timer_tag; - GTimeVal last_popdown; + struct { + glong tv_sec; + glong tv_usec; + } last_popdown; }; struct _GtkTooltipsClass From c3412528e877d5b0bf5dcc3969f44bec8ea463f5 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Mon, 11 May 2026 17:24:23 +0200 Subject: [PATCH 002/122] gtk: replace deprecated im module parser helpers --- gtk/gtkimmodule.c | 185 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 175 insertions(+), 10 deletions(-) diff --git a/gtk/gtkimmodule.c b/gtk/gtkimmodule.c index a3a77c474..6025a319e 100644 --- a/gtk/gtkimmodule.c +++ b/gtk/gtkimmodule.c @@ -309,6 +309,171 @@ add_builtin_module (const gchar *module_name, return module; } +/* Parser helpers adapted from GTK 3.24 gtkutils.c + * (gtk_read_line(), gtk_skip_space(), gtk_scan_string()). + * GTK 3 notes that these helpers were copied from pango-utils.c. + * + * Keep these helpers local to avoid depending on deprecated Pango parser + * utilities while preserving the old gtk.immodules parser semantics. + */ +#if !defined(HAVE_FLOCKFILE) && !defined(HAVE__LOCK_FILE) +#define flockfile(f) (void)1 +#define funlockfile(f) (void)1 +#define getc_unlocked(f) getc(f) +#elif !defined(HAVE_FLOCKFILE) && defined(HAVE__LOCK_FILE) +#define flockfile(f) _lock_file(f) +#define funlockfile(f) _unlock_file(f) +#define getc_unlocked(f) _getc_nolock(f) +#endif + +static gboolean +gtk_im_module_scan_string (const char **pos, + GString *out) +{ + const char *p = *pos, *q = *pos; + char *tmp, *tmp2; + gboolean quoted; + + while (g_ascii_isspace (*p)) + p++; + + if (!*p) + return FALSE; + else if (*p == '"') + { + p++; + quoted = FALSE; + for (q = p; (*q != '"') || quoted; q++) + { + if (!*q) + return FALSE; + + quoted = (*q == '\\') && !quoted; + } + + tmp = g_strndup (p, q - p); + tmp2 = g_strcompress (tmp); + g_string_truncate (out, 0); + g_string_append (out, tmp2); + g_free (tmp); + g_free (tmp2); + } + + q++; + *pos = q; + + return TRUE; +} + +static gboolean +gtk_im_module_skip_space (const char **pos) +{ + const char *p = *pos; + + while (g_ascii_isspace (*p)) + p++; + + *pos = p; + + return !(*p == '\0'); +} + +static gint +gtk_im_module_read_line (FILE *stream, + GString *str) +{ + gboolean quoted = FALSE; + gboolean comment = FALSE; + int n_read = 0; + int lines = 1; + + flockfile (stream); + + g_string_truncate (str, 0); + + while (1) + { + int c; + + c = getc_unlocked (stream); + + if (c == EOF) + { + if (quoted) + g_string_append_c (str, '\\'); + + goto done; + } + else + n_read++; + + if (quoted) + { + quoted = FALSE; + + switch (c) + { + case '#': + g_string_append_c (str, '#'); + break; + + case '\r': + case '\n': + { + int next_c = getc_unlocked (stream); + + if (!(next_c == EOF || + (c == '\r' && next_c == '\n') || + (c == '\n' && next_c == '\r'))) + ungetc (next_c, stream); + + lines++; + break; + } + + default: + g_string_append_c (str, '\\'); + g_string_append_c (str, c); + } + } + else + { + switch (c) + { + case '#': + comment = TRUE; + break; + + case '\\': + if (!comment) + quoted = TRUE; + break; + + case '\n': + { + int next_c = getc_unlocked (stream); + + if (!(next_c == EOF || + (c == '\r' && next_c == '\n') || + (c == '\n' && next_c == '\r'))) + ungetc (next_c, stream); + + goto done; + } + + default: + if (!comment) + g_string_append_c (str, c); + } + } + } + +done: + funlockfile (stream); + + return (n_read > 0) ? lines : 0; +} + static void gtk_im_module_initialize (void) { @@ -392,13 +557,13 @@ gtk_im_module_initialize (void) return; } - while (!have_error && pango_read_line (file, line_buf)) + while (!have_error && gtk_im_module_read_line (file, line_buf)) { const char *p; p = line_buf->str; - if (!pango_skip_space (&p)) + if (!gtk_im_module_skip_space (&p)) { /* Blank line marking the end of a module */ @@ -418,8 +583,8 @@ gtk_im_module_initialize (void) */ module = g_object_new (GTK_TYPE_IM_MODULE, NULL); - if (!pango_scan_string (&p, tmp_buf) || - pango_skip_space (&p)) + if (!gtk_im_module_scan_string (&p, tmp_buf) || + gtk_im_module_skip_space (&p)) { g_warning ("Error parsing context info in '%s'\n %s", filename, line_buf->str); @@ -438,30 +603,30 @@ gtk_im_module_initialize (void) /* Read information about a context type */ - if (!pango_scan_string (&p, tmp_buf)) + if (!gtk_im_module_scan_string (&p, tmp_buf)) goto context_error; info->context_id = g_strdup (tmp_buf->str); - if (!pango_scan_string (&p, tmp_buf)) + if (!gtk_im_module_scan_string (&p, tmp_buf)) goto context_error; info->context_name = g_strdup (tmp_buf->str); - if (!pango_scan_string (&p, tmp_buf)) + if (!gtk_im_module_scan_string (&p, tmp_buf)) goto context_error; info->domain = g_strdup (tmp_buf->str); - if (!pango_scan_string (&p, tmp_buf)) + if (!gtk_im_module_scan_string (&p, tmp_buf)) goto context_error; info->domain_dirname = g_strdup (tmp_buf->str); #ifdef G_OS_WIN32 correct_localedir_prefix ((char **) &info->domain_dirname); #endif - if (!pango_scan_string (&p, tmp_buf)) + if (!gtk_im_module_scan_string (&p, tmp_buf)) goto context_error; info->default_locales = g_strdup (tmp_buf->str); - if (pango_skip_space (&p)) + if (gtk_im_module_skip_space (&p)) goto context_error; infos = g_slist_prepend (infos, info); From 48e183353bcb41a52a645a3405d5e07ad67dfd65 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Mon, 11 May 2026 18:36:05 +0200 Subject: [PATCH 003/122] cups: use modern HTTP connect helpers when available --- configure.ac | 1 + modules/printbackends/cups/gtkcupsutils.c | 39 ++++++++++++++++++----- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/configure.ac b/configure.ac index 29297423f..5c4593f57 100644 --- a/configure.ac +++ b/configure.ac @@ -1594,6 +1594,7 @@ else gtk_save_libs="$LIBS" LIBS="$CUPS_LIBS" AC_CHECK_FUNCS(httpGetAuthString) + AC_CHECK_FUNCS(httpConnect2 httpReconnect2) LIBS="$gtk_save_libs" fi fi diff --git a/modules/printbackends/cups/gtkcupsutils.c b/modules/printbackends/cups/gtkcupsutils.c index fdfe21e91..36fdd898a 100644 --- a/modules/printbackends/cups/gtkcupsutils.c +++ b/modules/printbackends/cups/gtkcupsutils.c @@ -171,7 +171,7 @@ gtk_cups_request_new_with_username (http_t *connection, else { request->http = NULL; - request->http = httpConnectEncrypt (request->server, + request->http = gtk_cups_http_connect (request->server, ippPort (), cupsEncryption ()); @@ -681,6 +681,29 @@ gtk_cups_request_set_ipp_version (GtkCupsRequest *request, ippSetVersion (request->ipp_request, major, minor); } + +static http_t * +gtk_cups_http_connect (const char *host, + int port, + http_encryption_t encryption) +{ +#ifdef HAVE_HTTPCONNECT2 + return httpConnect2 (host, port, NULL, AF_UNSPEC, encryption, 0, 30000, NULL); +#else + return httpConnectEncrypt (host, port, encryption); +#endif +} + +static int +gtk_cups_http_reconnect (http_t *http) +{ +#ifdef HAVE_HTTPRECONNECT2 + return httpReconnect2 (http, 30000, NULL); +#else + return httpReconnect (http); +#endif +} + static void _connect (GtkCupsRequest *request) { @@ -689,7 +712,7 @@ _connect (GtkCupsRequest *request) if (request->http == NULL) { - request->http = httpConnectEncrypt (request->server, + request->http = gtk_cups_http_connect (request->server, ippPort (), cupsEncryption ()); @@ -744,7 +767,7 @@ _post_send (GtkCupsRequest *request) if (httpPost (request->http, request->resource)) { - if (httpReconnect (request->http)) + if (gtk_cups_http_reconnect (request->http)) { request->state = GTK_CUPS_POST_DONE; request->poll_state = GTK_CUPS_HTTP_IDLE; @@ -1046,7 +1069,7 @@ _post_check (GtkCupsRequest *request) } if (auth_result || - httpReconnect (request->http)) + gtk_cups_http_reconnect (request->http)) { /* if the password has been used, reset password_state * so that we ask for a new one next time around @@ -1105,7 +1128,7 @@ _post_check (GtkCupsRequest *request) request->state = GTK_CUPS_POST_CONNECT; /* Reconnect... */ - httpReconnect (request->http); + gtk_cups_http_reconnect (request->http); /* Upgrade with encryption... */ httpEncryption (request->http, HTTP_ENCRYPT_REQUIRED); @@ -1232,7 +1255,7 @@ _get_send (GtkCupsRequest *request) if (httpGet (request->http, request->resource)) { - if (httpReconnect (request->http)) + if (gtk_cups_http_reconnect (request->http)) { request->state = GTK_CUPS_GET_DONE; request->poll_state = GTK_CUPS_HTTP_IDLE; @@ -1341,7 +1364,7 @@ _get_check (GtkCupsRequest *request) } if (auth_result || - httpReconnect (request->http)) + gtk_cups_http_reconnect (request->http)) { /* if the password has been used, reset password_state * so that we ask for a new one next time around @@ -1373,7 +1396,7 @@ _get_check (GtkCupsRequest *request) request->state = GTK_CUPS_GET_CONNECT; /* Reconnect... */ - httpReconnect (request->http); + gtk_cups_http_reconnect (request->http); /* Upgrade with encryption... */ httpEncryption (request->http, HTTP_ENCRYPT_REQUIRED); From 0451e0ad1dbf6180429b48e38140257a1fb4d4da Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Mon, 11 May 2026 18:46:01 +0200 Subject: [PATCH 004/122] cups: use modern backend HTTP connect helper when available --- modules/printbackends/cups/gtkcupsutils.c | 5 +++++ .../printbackends/cups/gtkprintbackendcups.c | 22 ++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/modules/printbackends/cups/gtkcupsutils.c b/modules/printbackends/cups/gtkcupsutils.c index 36fdd898a..18eedad39 100644 --- a/modules/printbackends/cups/gtkcupsutils.c +++ b/modules/printbackends/cups/gtkcupsutils.c @@ -34,6 +34,11 @@ typedef void (*GtkCupsRequestStateFunc) (GtkCupsRequest *request); +static http_t *gtk_cups_http_connect (const char *host, + int port, + http_encryption_t encryption); +static int gtk_cups_http_reconnect (http_t *http); + static void _connect (GtkCupsRequest *request); static void _post_send (GtkCupsRequest *request); static void _post_write_request (GtkCupsRequest *request); diff --git a/modules/printbackends/cups/gtkprintbackendcups.c b/modules/printbackends/cups/gtkprintbackendcups.c index e17d97d21..068093bac 100644 --- a/modules/printbackends/cups/gtkprintbackendcups.c +++ b/modules/printbackends/cups/gtkprintbackendcups.c @@ -489,6 +489,22 @@ cups_free_print_stream_data (CupsPrintStreamData *data) g_free (data); } + +static http_t * +gtk_cups_backend_http_connect (const char *host, + int port, + http_encryption_t encryption) +{ +#ifdef HAVE_HTTPCONNECT2 + return httpConnect2 (host, port, NULL, AF_UNSPEC, encryption, 1, 30000, NULL); +#else + if (encryption == HTTP_ENCRYPT_NEVER) + return httpConnect (host, port); + + return httpConnectEncrypt (host, port, encryption); +#endif +} + static void cups_print_cb (GtkPrintBackendCups *print_backend, GtkCupsResult *result, @@ -636,7 +652,7 @@ gtk_print_backend_cups_print_stream (GtkPrintBackend *print_backend, { http_t *http; - http = httpConnect (cups_printer->hostname, cups_printer->port); + http = gtk_cups_backend_http_connect (cups_printer->hostname, cups_printer->port, HTTP_ENCRYPT_NEVER); if (http) { request = gtk_cups_request_new_with_username (http, @@ -2555,7 +2571,7 @@ cups_request_avahi_printer_info (const gchar *printer_uri, AvahiService *service; http_t *http; - http = httpConnect (host, port); + http = gtk_cups_backend_http_connect (host, port, HTTP_ENCRYPT_NEVER); if (http) { service = (AvahiService *) g_new0 (AvahiService, 1); @@ -3438,7 +3454,7 @@ cups_request_ppd (GtkPrinter *printer) } } - http = httpConnectEncrypt (cups_printer->hostname, + http = gtk_cups_backend_http_connect (cups_printer->hostname, cups_printer->port, cupsEncryption ()); From 0c99e43e4cfc3a2b7665117782c98673f0bf0c2f Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Mon, 11 May 2026 19:42:43 +0200 Subject: [PATCH 005/122] gail: use modern AtkValue API for progress bars --- modules/other/gail/gailprogressbar.c | 36 +++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/modules/other/gail/gailprogressbar.c b/modules/other/gail/gailprogressbar.c index d70c139be..99f32dc65 100644 --- a/modules/other/gail/gailprogressbar.c +++ b/modules/other/gail/gailprogressbar.c @@ -125,7 +125,17 @@ gail_progress_bar_get_current_value (AtkValue *obj, */ return; - atk_value_get_current_value (ATK_VALUE (progress_bar->adjustment), value); + { + GtkAdjustment *adjustment; + + adjustment = GAIL_ADJUSTMENT (progress_bar->adjustment)->adjustment; + if (adjustment == NULL) + return; + + memset (value, 0, sizeof (GValue)); + g_value_init (value, G_TYPE_DOUBLE); + g_value_set_double (value, adjustment->value); + } } static void @@ -143,7 +153,17 @@ gail_progress_bar_get_maximum_value (AtkValue *obj, */ return; - atk_value_get_maximum_value (ATK_VALUE (progress_bar->adjustment), value); + { + GtkAdjustment *adjustment; + + adjustment = GAIL_ADJUSTMENT (progress_bar->adjustment)->adjustment; + if (adjustment == NULL) + return; + + memset (value, 0, sizeof (GValue)); + g_value_init (value, G_TYPE_DOUBLE); + g_value_set_double (value, adjustment->upper); + } } static void @@ -161,7 +181,17 @@ gail_progress_bar_get_minimum_value (AtkValue *obj, */ return; - atk_value_get_minimum_value (ATK_VALUE (progress_bar->adjustment), value); + { + GtkAdjustment *adjustment; + + adjustment = GAIL_ADJUSTMENT (progress_bar->adjustment)->adjustment; + if (adjustment == NULL) + return; + + memset (value, 0, sizeof (GValue)); + g_value_init (value, G_TYPE_DOUBLE); + g_value_set_double (value, adjustment->lower); + } } static void From bec4c7bdb4d3fbc8fa67774a563de42d944f9621 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Mon, 11 May 2026 20:46:44 +0200 Subject: [PATCH 006/122] gail: read range adjustment values directly --- modules/other/gail/gailrange.c | 62 +++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/modules/other/gail/gailrange.c b/modules/other/gail/gailrange.c index 9a91bb333..56ab04763 100644 --- a/modules/other/gail/gailrange.c +++ b/modules/other/gail/gailrange.c @@ -180,7 +180,17 @@ gail_range_get_current_value (AtkValue *obj, */ return; - atk_value_get_current_value (ATK_VALUE (range->adjustment), value); + { + GtkAdjustment *adjustment; + + adjustment = GAIL_ADJUSTMENT (range->adjustment)->adjustment; + if (adjustment == NULL) + return; + + memset (value, 0, sizeof (GValue)); + g_value_init (value, G_TYPE_DOUBLE); + g_value_set_double (value, adjustment->value); + } } static void @@ -201,7 +211,17 @@ gail_range_get_maximum_value (AtkValue *obj, */ return; - atk_value_get_maximum_value (ATK_VALUE (range->adjustment), value); + { + GtkAdjustment *adjustment; + + adjustment = GAIL_ADJUSTMENT (range->adjustment)->adjustment; + if (adjustment == NULL) + return; + + memset (value, 0, sizeof (GValue)); + g_value_init (value, G_TYPE_DOUBLE); + g_value_set_double (value, adjustment->upper); + } gtk_range = GTK_RANGE (gtk_accessible_get_widget (GTK_ACCESSIBLE (range))); g_return_if_fail (gtk_range); @@ -231,7 +251,17 @@ gail_range_get_minimum_value (AtkValue *obj, */ return; - atk_value_get_minimum_value (ATK_VALUE (range->adjustment), value); + { + GtkAdjustment *adjustment; + + adjustment = GAIL_ADJUSTMENT (range->adjustment)->adjustment; + if (adjustment == NULL) + return; + + memset (value, 0, sizeof (GValue)); + g_value_init (value, G_TYPE_DOUBLE); + g_value_set_double (value, adjustment->lower); + } } static void @@ -248,7 +278,31 @@ gail_range_get_minimum_increment (AtkValue *obj, GValue *value) */ return; - atk_value_get_minimum_increment (ATK_VALUE (range->adjustment), value); + { + GtkAdjustment *adjustment; + gdouble minimum_increment; + + adjustment = GAIL_ADJUSTMENT (range->adjustment)->adjustment; + if (adjustment == NULL) + return; + + if (adjustment->step_increment != 0 && + adjustment->page_increment != 0) + { + if (ABS (adjustment->step_increment) < ABS (adjustment->page_increment)) + minimum_increment = adjustment->step_increment; + else + minimum_increment = adjustment->page_increment; + } + else if (adjustment->step_increment != 0) + minimum_increment = adjustment->step_increment; + else + minimum_increment = adjustment->page_increment; + + memset (value, 0, sizeof (GValue)); + g_value_init (value, G_TYPE_DOUBLE); + g_value_set_double (value, minimum_increment); + } } static gboolean gail_range_set_current_value (AtkValue *obj, From d4729ecf1935b90ec030a1dfe281b18bfb058066 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Mon, 11 May 2026 21:15:37 +0200 Subject: [PATCH 007/122] gail: read spin button adjustment values directly --- modules/other/gail/gailspinbutton.c | 76 +++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 5 deletions(-) diff --git a/modules/other/gail/gailspinbutton.c b/modules/other/gail/gailspinbutton.c index 709a413c0..b209cc403 100644 --- a/modules/other/gail/gailspinbutton.c +++ b/modules/other/gail/gailspinbutton.c @@ -127,7 +127,17 @@ gail_spin_button_get_current_value (AtkValue *obj, */ return; - atk_value_get_current_value (ATK_VALUE (spin_button->adjustment), value); + { + GtkAdjustment *adjustment; + + adjustment = GAIL_ADJUSTMENT (spin_button->adjustment)->adjustment; + if (adjustment == NULL) + return; + + memset (value, 0, sizeof (GValue)); + g_value_init (value, G_TYPE_DOUBLE); + g_value_set_double (value, adjustment->value); + } } static void @@ -145,7 +155,17 @@ gail_spin_button_get_maximum_value (AtkValue *obj, */ return; - atk_value_get_maximum_value (ATK_VALUE (spin_button->adjustment), value); + { + GtkAdjustment *adjustment; + + adjustment = GAIL_ADJUSTMENT (spin_button->adjustment)->adjustment; + if (adjustment == NULL) + return; + + memset (value, 0, sizeof (GValue)); + g_value_init (value, G_TYPE_DOUBLE); + g_value_set_double (value, adjustment->upper); + } } static void @@ -163,7 +183,17 @@ gail_spin_button_get_minimum_value (AtkValue *obj, */ return; - atk_value_get_minimum_value (ATK_VALUE (spin_button->adjustment), value); + { + GtkAdjustment *adjustment; + + adjustment = GAIL_ADJUSTMENT (spin_button->adjustment)->adjustment; + if (adjustment == NULL) + return; + + memset (value, 0, sizeof (GValue)); + g_value_init (value, G_TYPE_DOUBLE); + g_value_set_double (value, adjustment->lower); + } } static void @@ -180,7 +210,31 @@ gail_spin_button_get_minimum_increment (AtkValue *obj, GValue *value) */ return; - atk_value_get_minimum_increment (ATK_VALUE (spin_button->adjustment), value); + { + GtkAdjustment *adjustment; + gdouble minimum_increment; + + adjustment = GAIL_ADJUSTMENT (spin_button->adjustment)->adjustment; + if (adjustment == NULL) + return; + + if (adjustment->step_increment != 0 && + adjustment->page_increment != 0) + { + if (ABS (adjustment->step_increment) < ABS (adjustment->page_increment)) + minimum_increment = adjustment->step_increment; + else + minimum_increment = adjustment->page_increment; + } + else if (adjustment->step_increment != 0) + minimum_increment = adjustment->step_increment; + else + minimum_increment = adjustment->page_increment; + + memset (value, 0, sizeof (GValue)); + g_value_init (value, G_TYPE_DOUBLE); + g_value_set_double (value, minimum_increment); + } } static gboolean @@ -198,7 +252,19 @@ gail_spin_button_set_current_value (AtkValue *obj, */ return FALSE; - return atk_value_set_current_value (ATK_VALUE (spin_button->adjustment), value); + if (G_VALUE_HOLDS_DOUBLE (value)) + { + GtkAdjustment *adjustment; + + adjustment = GAIL_ADJUSTMENT (spin_button->adjustment)->adjustment; + if (adjustment == NULL) + return FALSE; + + gtk_adjustment_set_value (adjustment, g_value_get_double (value)); + return TRUE; + } + + return FALSE; } static void From a221e0da4cc05ea47050eb2e5e7ffec72bcb403f Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 13 May 2026 08:27:49 +0200 Subject: [PATCH 008/122] tests: fix GtkBuilder signal callback prototypes Based on the Guix gtk2-fix-builder-test.patch. The callbacks are connected to notify-style signals and must take a GParamSpec pointer, not a GParamSpec by value. This avoids stack/register ABI problems on platforms such as ppc64el. --- gtk/tests/builder.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/gtk/tests/builder.c b/gtk/tests/builder.c index ebba220e9..121012704 100644 --- a/gtk/tests/builder.c +++ b/gtk/tests/builder.c @@ -132,7 +132,7 @@ static int object = 0; static int object_after = 0; void /* exported for GtkBuilder */ -signal_normal (GtkWindow *window, GParamSpec spec) +signal_normal (GtkWindow *window, GParamSpec *spec) { g_assert (GTK_IS_WINDOW (window)); g_assert (normal == 0); @@ -142,7 +142,7 @@ signal_normal (GtkWindow *window, GParamSpec spec) } void /* exported for GtkBuilder */ -signal_after (GtkWindow *window, GParamSpec spec) +signal_after (GtkWindow *window, GParamSpec *spec) { g_assert (GTK_IS_WINDOW (window)); g_assert (normal == 1); @@ -152,7 +152,7 @@ signal_after (GtkWindow *window, GParamSpec spec) } void /* exported for GtkBuilder */ -signal_object (GtkButton *button, GParamSpec spec) +signal_object (GtkButton *button, GParamSpec *spec) { g_assert (GTK_IS_BUTTON (button)); g_assert (object == 0); @@ -162,7 +162,7 @@ signal_object (GtkButton *button, GParamSpec spec) } void /* exported for GtkBuilder */ -signal_object_after (GtkButton *button, GParamSpec spec) +signal_object_after (GtkButton *button, GParamSpec *spec) { g_assert (GTK_IS_BUTTON (button)); g_assert (object == 1); @@ -172,28 +172,28 @@ signal_object_after (GtkButton *button, GParamSpec spec) } void /* exported for GtkBuilder */ -signal_first (GtkButton *button, GParamSpec spec) +signal_first (GtkButton *button, GParamSpec *spec) { g_assert (normal == 0); normal = 10; } void /* exported for GtkBuilder */ -signal_second (GtkButton *button, GParamSpec spec) +signal_second (GtkButton *button, GParamSpec *spec) { g_assert (normal == 10); normal = 20; } void /* exported for GtkBuilder */ -signal_extra (GtkButton *button, GParamSpec spec) +signal_extra (GtkButton *button, GParamSpec *spec) { g_assert (normal == 20); normal = 30; } void /* exported for GtkBuilder */ -signal_extra2 (GtkButton *button, GParamSpec spec) +signal_extra2 (GtkButton *button, GParamSpec *spec) { g_assert (normal == 30); normal = 40; From 385df5517bddc7326901797c6cbea1b2294c9d2e Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 13 May 2026 08:34:50 +0200 Subject: [PATCH 009/122] gtkrc: search XDG data dirs for themes Based on the Guix gtk2-theme-paths.patch by Jookia. Keep the existing ~/.themes and gtk_rc_get_theme_dir() lookup paths, but check g_get_system_data_dirs() in between so profile- and prefix-based theme installations can be found. --- gtk/gtkrc.c | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/gtk/gtkrc.c b/gtk/gtkrc.c index 7bfbea85c..d316a0d90 100644 --- a/gtk/gtkrc.c +++ b/gtk/gtkrc.c @@ -808,6 +808,8 @@ gtk_rc_parse_named (GtkRcContext *context, gchar *path = NULL; const gchar *home_dir; gchar *subpath; + const gchar * const *xdg_data_dirs; + gint i; if (type) subpath = g_strconcat ("gtk-2.0-", type, @@ -829,6 +831,22 @@ gtk_rc_parse_named (GtkRcContext *context, } } + if (!path) + { + xdg_data_dirs = g_get_system_data_dirs (); + for (i = 0; xdg_data_dirs[i]; i++) + { + path = g_build_filename (xdg_data_dirs[i], "themes", name, subpath, NULL); + if (g_file_test (path, G_FILE_TEST_EXISTS)) + break; + else + { + g_free (path); + path = NULL; + } + } + } + if (!path) { gchar *theme_dir = gtk_rc_get_theme_dir (); From 2d1ba49ab4399c8e35abc2a3756c9bf2015e32a9 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 13 May 2026 09:15:53 +0200 Subject: [PATCH 010/122] build: disable GNU visibility aliases on Apple platforms Based on the MacPorts patch-aliases.diff. Keep the existing GNU visibility alias generation on other platforms, but avoid it on Apple targets where this alias mechanism is not suitable. --- gdk/makegdkalias.pl | 2 +- gtk/makegtkalias.pl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gdk/makegdkalias.pl b/gdk/makegdkalias.pl index 97a6e6007..27646cc0a 100755 --- a/gdk/makegdkalias.pl +++ b/gdk/makegdkalias.pl @@ -14,7 +14,7 @@ print < -#ifdef G_HAVE_GNUC_VISIBILITY +#if defined(G_HAVE_GNUC_VISIBILITY) && !defined(__APPLE__) EOF diff --git a/gtk/makegtkalias.pl b/gtk/makegtkalias.pl index 3f956b05e..67ec7c973 100755 --- a/gtk/makegtkalias.pl +++ b/gtk/makegtkalias.pl @@ -14,7 +14,7 @@ print < -#ifdef G_HAVE_GNUC_VISIBILITY +#if defined(G_HAVE_GNUC_VISIBILITY) && !defined(__APPLE__) EOF From 8347521bfda45433c6c7c303fc7e7745864db18d Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 13 May 2026 09:16:45 +0200 Subject: [PATCH 011/122] quartz: add newer macOS version constants Based on the MacPorts patch-gdkquartz-osx-versions.diff. Extend the Quartz OS version enum so later macOS-specific code can distinguish Catalina, Big Sur, Monterey, Ventura, Sonoma, and Sequoia while preserving the existing older constants. --- gdk/quartz/gdkquartz.h | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/gdk/quartz/gdkquartz.h b/gdk/quartz/gdkquartz.h index 8206db019..42aac50b9 100644 --- a/gdk/quartz/gdkquartz.h +++ b/gdk/quartz/gdkquartz.h @@ -54,7 +54,13 @@ typedef enum GDK_OSX_SIERRA = 12, GDK_OSX_HIGH_SIERRA = 13, GDK_OSX_MOJAVE = 14, - GDK_OSX_CURRENT = 14, + GDK_OSX_CATALINA = 15, + GDK_OSX_BIG_SUR = 16, + GDK_OSX_MONTEREY = 17, + GDK_OSX_VENTURA = 18, + GDK_OSX_SONOMA = 19, + GDK_OSX_SEQUOIA = 20, + GDK_OSX_CURRENT = 20, GDK_OSX_NEW = 99 } GdkOSXVersion; From eebe1148c159186d54b7cb876d0c77953dbfe177 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 13 May 2026 09:27:16 +0200 Subject: [PATCH 012/122] quartz: avoid focus locking on Mojave and newer Based on part of the MacPorts Big Sur performance patch for GIMP issue 5989. The NSView focus-locking API is deprecated on newer macOS releases and can cause expensive redraw behaviour. Keep the old path for older macOS versions, but avoid lockFocusIfCanDraw/unlockFocus on Mojave and newer. --- gdk/quartz/gdkdrawable-quartz.c | 3 ++- gdk/quartz/gdkwindow-quartz.c | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/gdk/quartz/gdkdrawable-quartz.c b/gdk/quartz/gdkdrawable-quartz.c index 11dd12737..eb520f08d 100644 --- a/gdk/quartz/gdkdrawable-quartz.c +++ b/gdk/quartz/gdkdrawable-quartz.c @@ -893,7 +893,8 @@ gdk_quartz_drawable_release_context (GdkDrawable *drawable, if (window_impl->in_paint_rect_count == 0) { _gdk_quartz_drawable_flush (drawable); - [window_impl->view unlockFocus]; + if (gdk_quartz_osx_version () < GDK_OSX_MOJAVE) + [window_impl->view unlockFocus]; } } else if (GDK_IS_PIXMAP_IMPL_QUARTZ (drawable)) diff --git a/gdk/quartz/gdkwindow-quartz.c b/gdk/quartz/gdkwindow-quartz.c index 6c7a2facb..205fd85e6 100644 --- a/gdk/quartz/gdkwindow-quartz.c +++ b/gdk/quartz/gdkwindow-quartz.c @@ -104,7 +104,8 @@ gdk_window_impl_quartz_get_context (GdkDrawable *drawable, * and for widgets that send fake expose events like the arrow * buttons in spinbuttons or the position marker in rulers. */ - if (window_impl->in_paint_rect_count == 0) + if (window_impl->in_paint_rect_count == 0 && + gdk_quartz_osx_version () < GDK_OSX_MOJAVE) { if (![window_impl->view lockFocusIfCanDraw]) return NULL; From 35e2e048fc512fb51f6987c795482222dd693808 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 13 May 2026 09:29:27 +0200 Subject: [PATCH 013/122] quartz: map macOS 11 and newer to internal OS versions Based on part of the MacPorts Big Sur performance patch for GIMP issue 5989. GTK2 represents macOS 10.x releases by their minor version. Read the major version as well and map macOS 11 and newer into the same sequence so Big Sur and later releases can be detected correctly. --- gdk/quartz/gdkglobals-quartz.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/gdk/quartz/gdkglobals-quartz.c b/gdk/quartz/gdkglobals-quartz.c index f013f2830..4e596eef5 100644 --- a/gdk/quartz/gdkglobals-quartz.c +++ b/gdk/quartz/gdkglobals-quartz.c @@ -34,9 +34,20 @@ gdk_quartz_osx_version (void) if (minor == GDK_OSX_UNSUPPORTED) { - OSErr err = Gestalt (gestaltSystemVersionMinor, (SInt32*)&minor); + gint32 major = GDK_OSX_UNSUPPORTED; + OSErr err = Gestalt (gestaltSystemVersionMajor, (SInt32*)&major); g_return_val_if_fail (err == noErr, GDK_OSX_UNSUPPORTED); + + err = Gestalt (gestaltSystemVersionMinor, (SInt32*)&minor); + g_return_val_if_fail (err == noErr, GDK_OSX_UNSUPPORTED); + + /* GTK2 encodes macOS 10.x releases by their minor version. + * Map macOS 11+ into the same sequence: + * 11 -> 16, 12 -> 17, 13 -> 18, ... + */ + if (major > 10) + minor = major + 5; } if (minor < GDK_OSX_MIN) From ceb158a3e1be917b55f2db4ce9b477cc8347c163 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 13 May 2026 09:32:30 +0200 Subject: [PATCH 014/122] quartz: use CGContext accessor on Yosemite and newer Based on part of the MacPorts Big Sur performance patch for GIMP issue 5989. Use the modern NSGraphicsContext CGContext accessor on Yosemite and newer, matching the existing Quartz window code, while preserving graphicsPort for older macOS versions. --- gtk/gtkquartz.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gtk/gtkquartz.c b/gtk/gtkquartz.c index 87ba87745..caf8404f8 100644 --- a/gtk/gtkquartz.c +++ b/gtk/gtkquartz.c @@ -67,7 +67,10 @@ _gtk_quartz_create_image_from_pixbuf (GdkPixbuf *pixbuf) } [nsimage lockFocus]; - context = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort]; + if (gdk_quartz_osx_version () < GDK_OSX_YOSEMITE) + context = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort]; + else + context = [[NSGraphicsContext currentContext] CGContext]; CGContextDrawImage (context, CGRectMake (0, 0, pixbuf_width, pixbuf_height), image); [nsimage unlockFocus]; From 032c5f1efdfaebbff28353308ffce3081b3b2a68 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 13 May 2026 09:41:36 +0200 Subject: [PATCH 015/122] quartz: avoid screen update suppression on Mojave and newer Based on part of the MacPorts Big Sur performance patch for GIMP issue 5989. NSDisableScreenUpdates and NSEnableScreenUpdates are deprecated on newer macOS releases. Keep using them for older macOS versions, but avoid them on Mojave and newer. --- gdk/quartz/gdkwindow-quartz.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gdk/quartz/gdkwindow-quartz.c b/gdk/quartz/gdkwindow-quartz.c index 205fd85e6..5c1bde0cd 100644 --- a/gdk/quartz/gdkwindow-quartz.c +++ b/gdk/quartz/gdkwindow-quartz.c @@ -469,7 +469,8 @@ _gdk_windowing_before_process_all_updates (void) { in_process_all_updates = TRUE; - NSDisableScreenUpdates (); + if (gdk_quartz_osx_version () < GDK_OSX_MOJAVE) + NSDisableScreenUpdates (); } void @@ -503,7 +504,8 @@ _gdk_windowing_after_process_all_updates (void) in_process_all_updates = FALSE; - NSEnableScreenUpdates (); + if (gdk_quartz_osx_version () < GDK_OSX_MOJAVE) + NSEnableScreenUpdates (); } static void From 3b177953c1ac9463a1a24d7371be1107facabd71 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 13 May 2026 09:49:48 +0200 Subject: [PATCH 016/122] quartz: request 8-bit layer contents on Big Sur and newer Based on part of the MacPorts Big Sur performance patch for GIMP issue 5989. Use a dynamic Objective-C call to request 8-bit layer contents on macOS 11 and newer, avoiding a hard QuartzCore/CALayer compile-time dependency and preserving old behaviour on older macOS releases. --- gdk/quartz/GdkQuartzView.c | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/gdk/quartz/GdkQuartzView.c b/gdk/quartz/GdkQuartzView.c index 368e040ec..bad737da2 100644 --- a/gdk/quartz/GdkQuartzView.c +++ b/gdk/quartz/GdkQuartzView.c @@ -599,6 +599,25 @@ gdk_screen_get_rgba_colormap (_gdk_screen); } +-(void)viewWillDraw +{ + /* macOS 11 (Big Sur) and newer may use a higher precision backing + * format for layer contents. For GTK2's 8-bit drawing this can add + * avoidable conversion overhead. Use a dynamic Objective-C call here + * instead of a hard QuartzCore/CALayer dependency, so old SDKs and old + * macOS targets keep building and keep their old behaviour. + */ + if (gdk_quartz_osx_version () >= GDK_OSX_BIG_SUR) + { + id layer = [self layer]; + + if (layer && [layer respondsToSelector: @selector(setContentsFormat:)]) + [layer setValue: @"RGBA8Uint" forKey: @"contentsFormat"]; + } + + [super viewWillDraw]; +} + -(void)drawRect: (NSRect)rect { GdkRectangle gdk_rect; From 243fcad89fe502e4cb1ef11ebe060b3510b7ed99 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 13 May 2026 09:54:51 +0200 Subject: [PATCH 017/122] quartz: avoid extra content invalidation on Big Sur and newer Based on part of the MacPorts Big Sur performance patch for GIMP issue 5989. Keep the existing Mojave-era setNeedsDisplay workaround for macOS 10.14 and 10.15, but avoid the extra full-window invalidation on Big Sur and newer where it can cause expensive redraw behaviour. --- gdk/quartz/gdkwindow-quartz.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gdk/quartz/gdkwindow-quartz.c b/gdk/quartz/gdkwindow-quartz.c index 5c1bde0cd..38ada80b2 100644 --- a/gdk/quartz/gdkwindow-quartz.c +++ b/gdk/quartz/gdkwindow-quartz.c @@ -491,8 +491,9 @@ _gdk_windowing_after_process_all_updates (void) /* 10.14 needs to be told that the view needs to be redrawn, see * https://gitlab.gnome.org/GNOME/gtk/issues/1479 */ - if (gdk_quartz_osx_version() >= GDK_OSX_MOJAVE) - [[nswindow contentView] setNeedsDisplay:YES]; + if (gdk_quartz_osx_version () >= GDK_OSX_MOJAVE && + gdk_quartz_osx_version () < GDK_OSX_BIG_SUR) + [[nswindow contentView] setNeedsDisplay:YES]; [nswindow enableFlushWindow]; [nswindow flushWindow]; [nswindow release]; From b305265deaa1d106cb4a4cb779373160f5439050 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 13 May 2026 09:59:26 +0200 Subject: [PATCH 018/122] quartz: avoid extra resize-idle updates on Big Sur and newer Based on part of the MacPorts Big Sur performance patch for GIMP issue 5989. Keep the existing gdk_window_process_all_updates() call for non-Quartz targets and for older macOS releases, but avoid this extra resize-idle update path on Big Sur and newer where it can contribute to expensive full-window redraw behaviour. --- gtk/gtkcontainer.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/gtk/gtkcontainer.c b/gtk/gtkcontainer.c index 2f7007fd2..e4c98c8c2 100644 --- a/gtk/gtkcontainer.c +++ b/gtk/gtkcontainer.c @@ -40,6 +40,9 @@ #include "gtktoolbar.h" #include #include +#ifdef GDK_WINDOWING_QUARTZ +#include "gdk/quartz/gdkquartz.h" +#endif #include "gtkalias.h" @@ -1357,7 +1360,16 @@ gtk_container_idle_sizer (gpointer data) gtk_container_check_resize (GTK_CONTAINER (widget)); } +#ifdef GDK_WINDOWING_QUARTZ + /* macOS 11 and newer can trigger expensive full-window redraws here. + * Keep the traditional update processing path for older Quartz targets, + * but let the normal GDK update idle handle updates on Big Sur and newer. + */ + if (gdk_quartz_osx_version () < GDK_OSX_BIG_SUR) + gdk_window_process_all_updates (); +#else gdk_window_process_all_updates (); +#endif return FALSE; } From cd4145185c7369249774c183a10809ccd48940c0 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 13 May 2026 10:06:34 +0200 Subject: [PATCH 019/122] win32: improve mouse window lookup through transparent children Based on the MSYS2 0013_fix_mouse_events.patch. Use CWP_SKIPTRANSPARENT while walking from the desktop window, but stop skipping transparent windows after the first level so transparent child windows are still considered correctly. --- gdk/win32/gdkwindow-win32.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/gdk/win32/gdkwindow-win32.c b/gdk/win32/gdkwindow-win32.c index b7ea0404a..1a21a1b67 100644 --- a/gdk/win32/gdkwindow-win32.c +++ b/gdk/win32/gdkwindow-win32.c @@ -2767,6 +2767,10 @@ _gdk_windowing_window_at_pointer (GdkDisplay *display, * WindowFromPoint() can find our windows, we follow similar logic * here, and ignore invisible and disabled windows. */ + UINT cwp_flags = CWP_SKIPDISABLED | + CWP_SKIPINVISIBLE | + CWP_SKIPTRANSPARENT; + hwnd = GetDesktopWindow (); do { window = gdk_win32_handle_table_lookup ((GdkNativeWindow) hwnd); @@ -2777,8 +2781,7 @@ _gdk_windowing_window_at_pointer (GdkDisplay *display, break; screen_to_client (hwnd, screen_pt, &client_pt); - hwndc = ChildWindowFromPointEx (hwnd, client_pt, CWP_SKIPDISABLED | - CWP_SKIPINVISIBLE); + hwndc = ChildWindowFromPointEx (hwnd, client_pt, cwp_flags); /* Verify that we're really inside the client area of the window */ if (hwndc != hwnd) @@ -2789,6 +2792,8 @@ _gdk_windowing_window_at_pointer (GdkDisplay *display, hwndc = hwnd; } + /* Only ignore top-level transparent windows */ + cwp_flags &= ~CWP_SKIPTRANSPARENT; } while (hwndc != hwnd && (hwnd = hwndc, 1)); } From e42049b412566993ba779e8b411a5cc22264d683 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 13 May 2026 10:07:25 +0200 Subject: [PATCH 020/122] icontheme: detect SVG icons by extension on Win32 Based on the MSYS2 0022-icontheme-win32-detect-SVG-files-by-extension.patch. Some Windows systems report .svg files with a generic or incorrect content type. On Win32, fall back to the .svg extension so SVG icons can still be detected. --- gtk/gtkicontheme.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gtk/gtkicontheme.c b/gtk/gtkicontheme.c index 0d4435c72..f8aaf9a3b 100644 --- a/gtk/gtkicontheme.c +++ b/gtk/gtkicontheme.c @@ -2945,6 +2945,10 @@ icon_info_ensure_scale_and_pixbuf (GtkIconInfo *icon_info, if (mime_type && strcmp (mime_type, "image/svg+xml") == 0) is_svg = TRUE; +#ifdef G_OS_WIN32 + else if (content_type && strcmp (content_type, ".svg") == 0) + is_svg = TRUE; +#endif } g_object_unref (file_info); From 45fc03183e4e192baa7596fc7edcc2df666a782d Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 13 May 2026 10:08:01 +0200 Subject: [PATCH 021/122] win32: initialize key state before updating keymap Based on the MSYS2 0023-gdkkeys-win32.c-fix-initialisation-of-key_state-in-u.patch. Clear the key_state array before using it during keymap updates so stale state cannot affect layout handling, including Japanese IME input. --- gdk/win32/gdkkeys-win32.c | 1 + 1 file changed, 1 insertion(+) diff --git a/gdk/win32/gdkkeys-win32.c b/gdk/win32/gdkkeys-win32.c index d8ab30347..9a34083c4 100644 --- a/gdk/win32/gdkkeys-win32.c +++ b/gdk/win32/gdkkeys-win32.c @@ -692,6 +692,7 @@ update_keymap (GdkKeymap *gdk_keymap) if (hkls_len != keymap->layout_handles->len) keymap->keysym_tab = g_renew (guint, keymap->keysym_tab, keysym_tab_size); + memset (key_state, 0, sizeof(key_state)); memset (keymap->keysym_tab, 0, keysym_tab_size); g_array_set_size (keymap->layout_handles, hkls_len); g_array_set_size (keymap->options, hkls_len); From 442630afe580ca7390f18bab56ae293a68ad5c77 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 13 May 2026 10:09:36 +0200 Subject: [PATCH 022/122] win32: quote icon cache manifest in resource file Based on the MSYS2 0024-gtk-update-icon-cache-rc-quote.patch. Quote the manifest path when generating the gtk-update-icon-cache resource file so windres parses it correctly. --- gtk/Makefile.am | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gtk/Makefile.am b/gtk/Makefile.am index 5cbe2a326..4df1c78bd 100644 --- a/gtk/Makefile.am +++ b/gtk/Makefile.am @@ -1139,7 +1139,7 @@ $(GTK_UPDATE_ICON_CACHE_MANIFEST): echo '' ) >$@ $(GTK_UPDATE_ICON_CACHE_RC): - (echo -e '#include \nCREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST '$(GTK_UPDATE_ICON_CACHE_MANIFEST)) >$@ + (echo -e '#include \nCREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "'$(GTK_UPDATE_ICON_CACHE_MANIFEST)'"') >$@ $(GTK_UPDATE_ICON_CACHE_MANIFEST_OBJECT): $(GTK_UPDATE_ICON_CACHE_RC) $(GTK_UPDATE_ICON_CACHE_MANIFEST) $(WINDRES) --input $< --output $@ --output-format=coff From cc3ccba88d065c16657e98b795665ee704b01b91 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 13 May 2026 10:19:49 +0200 Subject: [PATCH 023/122] introspection: pass compiler and GTK DLL path to scanner Based on the MSYS2 0009-give-cc-to-gir-scanner.mingw.patch and 0010-put-gtk-dll-into-path.mingw.patch. Pass CC to g-ir-scanner and add the GTK build directory to PATH while scanning GTK, so MinGW builds can run the scanner with the just-built DLLs. --- gdk/Makefile.am | 2 ++ gtk/Makefile.am | 3 +++ 2 files changed, 5 insertions(+) diff --git a/gdk/Makefile.am b/gdk/Makefile.am index 1b186afa1..35dbc026c 100644 --- a/gdk/Makefile.am +++ b/gdk/Makefile.am @@ -2,6 +2,8 @@ include $(top_srcdir)/Makefile.decl -include $(INTROSPECTION_MAKEFILE) INTROSPECTION_GIRS = +INTROSPECTION_SCANNER_ENV = \ + CC="$(CC)" INTROSPECTION_SCANNER_ARGS = \ --warn-all \ --add-include-path=../gdk diff --git a/gtk/Makefile.am b/gtk/Makefile.am index 4df1c78bd..083fd07b7 100644 --- a/gtk/Makefile.am +++ b/gtk/Makefile.am @@ -2,6 +2,9 @@ include $(top_srcdir)/Makefile.decl -include $(INTROSPECTION_MAKEFILE) INTROSPECTION_GIRS = +INTROSPECTION_SCANNER_ENV = \ + CC="$(CC)" \ + PATH="$(builddir):$(builddir)/.libs:$${PATH}:" INTROSPECTION_SCANNER_ARGS = \ --warn-all \ --add-include-path=../gdk From 4cde9b16fd154de2da39557cbe0eb8488f6d346e Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 13 May 2026 10:48:27 +0200 Subject: [PATCH 024/122] introspection: add GdkWin32 GIR generation Based on the MSYS2 0011-gir-for-gdkwin32.mingw.patch. Add Win32 backend sources to introspection when building the Win32 target and generate GdkWin32-2.0.gir alongside the common Gdk-2.0 GIR. --- gdk/Makefile.am | 61 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/gdk/Makefile.am b/gdk/Makefile.am index 35dbc026c..1b4cd9b50 100644 --- a/gdk/Makefile.am +++ b/gdk/Makefile.am @@ -215,6 +215,11 @@ Gdk_2_0_gir_CFLAGS = \ -I$(top_builddir) \ -I$(top_builddir)/gdk \ $(INCLUDES) + +if OS_WIN32 +Gdk_2_0_gir_CFLAGS += -I$(srcdir)/win32 -I$(srcdir) +endif + INTROSPECTION_GIRS += Gdk-2.0.gir if USE_X11 @@ -270,6 +275,62 @@ introspection_files += $(filter-out x11/gdkx.h, $(x11_introspection_files)) endif # USE_X11 +if OS_WIN32 +win32_introspection_files = \ + win32/gdkapplaunchcontext-win32.c \ + win32/gdkcolor-win32.c \ + win32/gdkcursor-win32.c \ + win32/gdkdisplay-win32.c \ + win32/gdkdnd-win32.c \ + win32/gdkdrawable-win32.c \ + win32/gdkdrawable-win32.h \ + win32/gdkevents-win32.c \ + win32/gdkfont-win32.c \ + win32/gdkgc-win32.c \ + win32/gdkgeometry-win32.c \ + win32/gdkglobals-win32.c \ + win32/gdkimage-win32.c \ + win32/gdkim-win32.c \ + win32/gdkinput.c \ + win32/gdkinput-win32.c \ + win32/gdkinput-win32.h \ + win32/gdkkeys-win32.c \ + win32/gdkmain-win32.c \ + win32/gdkpixmap-win32.c \ + win32/gdkpixmap-win32.h \ + win32/gdkprivate-win32.h \ + win32/gdkproperty-win32.c \ + win32/gdkscreen-win32.c \ + win32/gdkselection-win32.c \ + win32/gdkspawn-win32.c \ + win32/gdktestutils-win32.c \ + win32/gdkvisual-win32.c \ + win32/gdkwin32.h \ + win32/gdkwin32id.c \ + win32/gdkwindow-win32.c \ + win32/gdkwindow-win32.h + +GdkWin32-2.0.gir: $(gdktargetlib) Gdk-2.0.gir Makefile +GdkWin32_2_0_gir_SCANNERFLAGS = --warn-all --identifier-prefix=Gdk --symbol-prefix=gdk --include-uninstalled=Gdk-2.0.gir +GdkWin32_2_0_gir_INCLUDES = Gio-2.0 Gdk-2.0 GdkPixbuf-2.0 Pango-1.0 +GdkWin32_2_0_gir_LIBS = $(gdktargetlib) +GdkWin32_2_0_gir_FILES = $(win32_introspection_files) +GdkWin32_2_0_gir_EXPORT_PACKAGES = gdk-win32-2.0 +GdkWin32_2_0_gir_CFLAGS = \ + -DGDK_COMPILATION \ + -I$(top_srcdir) \ + -I$(top_builddir) \ + -I$(top_builddir)/gdk \ + -I$(srcdir)/win32 \ + -I$(srcdir) \ + $(INCLUDES) \ + -L$(top_builddir)/gdk +INTROSPECTION_GIRS += GdkWin32-2.0.gir + +introspection_files += $(filter-out win32/gdkwin32.h, $(win32_introspection_files)) + +endif # OS_WIN32 + girdir = $(datadir)/gir-1.0 gir_DATA = $(INTROSPECTION_GIRS) From 06974f39fcacb0ea0fbdfd4bbe72b9387e9eea1e Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 13 May 2026 10:48:39 +0200 Subject: [PATCH 025/122] introspection: include Win32 keymap declarations Add gdkwin32keys.h to the Win32 introspection source list so exported Win32 keymap APIs are visible in GdkWin32-2.0.gir. The corresponding gdk_win32_keymap_get_type() and gdk_win32_keymap_check_compose() symbols are already exported, so this keeps the GIR closer to the installed Win32 API surface. --- gdk/Makefile.am | 1 + 1 file changed, 1 insertion(+) diff --git a/gdk/Makefile.am b/gdk/Makefile.am index 1b4cd9b50..f9804aa85 100644 --- a/gdk/Makefile.am +++ b/gdk/Makefile.am @@ -307,6 +307,7 @@ win32_introspection_files = \ win32/gdkvisual-win32.c \ win32/gdkwin32.h \ win32/gdkwin32id.c \ + win32/gdkwin32keys.h \ win32/gdkwindow-win32.c \ win32/gdkwindow-win32.h From 1ae5c7beec862294c56d8e00a2dd1bd9c62c9287 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 13 May 2026 10:55:56 +0200 Subject: [PATCH 026/122] docs: use top_srcdir for GDK X11 header reference Based on the MSYS2 0005-Fix-header-ref.all.patch. Use an explicit top_srcdir-relative path for gdkx.h in the GDK reference documentation instead of a fragile relative path. --- docs/reference/gdk/Makefile.am | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/gdk/Makefile.am b/docs/reference/gdk/Makefile.am index c0facc151..43a67508c 100644 --- a/docs/reference/gdk/Makefile.am +++ b/docs/reference/gdk/Makefile.am @@ -37,7 +37,7 @@ IGNORE_HFILES= \ # Extra files to add when scanning (relative to $srcdir) EXTRA_HFILES= \ - ../../../gdk/x11/gdkx.h + $(top_srcdir)/gdk/x11/gdkx.h # CFLAGS and LDFLAGS for compiling scan program. Only needed # if $(DOC_MODULE).types is non-empty. From 7cc56a996537d7699b6ac7499da0e26d71fabd10 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 13 May 2026 11:25:19 +0200 Subject: [PATCH 027/122] gtktrayicon: use explicit void prototypes for reserved slots Based on the Mageia gtk+-2.24.32-warnings.patch. Declare the reserved GtkTrayIconClass function pointers with explicit void parameter lists to avoid strict-prototype warnings without changing the class layout. --- gtk/gtktrayicon.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/gtk/gtktrayicon.h b/gtk/gtktrayicon.h index 9ad1c54f2..ab4eda2f5 100644 --- a/gtk/gtktrayicon.h +++ b/gtk/gtktrayicon.h @@ -46,12 +46,12 @@ struct _GtkTrayIconClass { GtkPlugClass parent_class; - void (*__gtk_reserved1); - void (*__gtk_reserved2); - void (*__gtk_reserved3); - void (*__gtk_reserved4); - void (*__gtk_reserved5); - void (*__gtk_reserved6); + void (*__gtk_reserved1) (void); + void (*__gtk_reserved2) (void); + void (*__gtk_reserved3) (void); + void (*__gtk_reserved4) (void); + void (*__gtk_reserved5) (void); + void (*__gtk_reserved6) (void); }; GType gtk_tray_icon_get_type (void) G_GNUC_CONST; From 525150055d6fa30265a41df9f92326e1d618de7e Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 13 May 2026 11:46:52 +0200 Subject: [PATCH 028/122] print: preserve default-printer wait during status updates Based on the openSUSE gtk2-default-printer.patch. Avoid treating printer status updates as user-driven printer selection changes, so the print dialog can continue waiting for a default printer that may appear later. --- gtk/gtkprintunixdialog.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gtk/gtkprintunixdialog.c b/gtk/gtkprintunixdialog.c index da9010b96..af64a5a37 100644 --- a/gtk/gtkprintunixdialog.c +++ b/gtk/gtkprintunixdialog.c @@ -796,7 +796,9 @@ printer_status_cb (GtkPrintBackend *backend, /* When the pause state change then we need to update sensitive property * of GTK_RESPONSE_OK button inside of selected_printer_changed function. */ selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (priv->printer_treeview)); + priv->internal_printer_change = TRUE; selected_printer_changed (selection, dialog); + priv->internal_printer_change = FALSE; if (gtk_print_backend_printer_list_is_done (backend) && gtk_printer_is_default (printer) && From 02a2c61f2dfc574a88343c81ddf10fc515136533 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 13 May 2026 11:49:09 +0200 Subject: [PATCH 029/122] gdk: handle 30-bit RGB drawables in GdkRGB Based on the openSUSE gtk2-bgo625202-30-bit-drawables-remain-black.patch. Add a dedicated conversion path for 32bpp 30-bit truecolor visuals so 8-bit RGB source data is expanded to 10-bit channels instead of producing black or overly dark output. --- gdk/gdkrgb.c | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/gdk/gdkrgb.c b/gdk/gdkrgb.c index b0ff66833..1e37a6181 100644 --- a/gdk/gdkrgb.c +++ b/gdk/gdkrgb.c @@ -2242,6 +2242,51 @@ gdk_rgb_convert_8880_br (GdkRgbInfo *image_info, GdkImage *image, } } +static void +gdk_rgb_convert_0AAA_br (GdkRgbInfo *image_info, GdkImage *image, + gint x0, gint y0, gint width, gint height, + const guchar *buf, int rowstride, + gint x_align, gint y_align, GdkRgbCmap *cmap) +{ + int y, w; + guchar *obuf, *p; + gint bpl; + const guchar *bptr, *bp2; + guchar r, g, b; + + bptr = buf; + bpl = image->bpl; + obuf = ((guchar *) image->mem) + y0 * bpl + x0 * 4; + + for (y = 0; y < height; y++) + { + bp2 = bptr; + p = obuf; + w = width; + + while (w--) + { + r = *bp2++; + g = *bp2++; + b = *bp2++; + + /* Since the R, G and B channels are coded in a char, we + * replicate the two most significant bits to the two least + * significant ones. + */ + *((guint32 *) p) = 0x3 << 30 | + b << 22 | (b >> 6) << 20 | + g << 12 | (g >> 6) << 10 | + r << 2 | (r >> 6); + + p += 4; + } + + bptr += rowstride; + obuf += bpl; + } +} + /* Generic truecolor/directcolor conversion function. Slow, but these are oddball modes. */ static void @@ -3195,6 +3240,11 @@ gdk_rgb_select_conv (GdkRgbInfo *image_info) #endif } #endif + else if (bpp == 32 && (depth == 30 || depth == 32) && + vtype == GDK_VISUAL_TRUE_COLOR && + (red_mask == 0x3ff && green_mask == 0xffc00 && + blue_mask == 0x3ff00000 && byte_order == GDK_LSB_FIRST)) + conv = gdk_rgb_convert_0AAA_br; else if (vtype == GDK_VISUAL_TRUE_COLOR && byte_order == GDK_LSB_FIRST) { conv = gdk_rgb_convert_truecolor_lsb; From 438c13366aae3c02a797f8689b447193733a1a7a Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 13 May 2026 12:12:49 +0200 Subject: [PATCH 030/122] fontsel: localize common font style names Based on the openSUSE bugzilla-129753-gtk+-2.8.9-localize-font-style-name.diff. Mark common font style names for translation and use translated names in the font selection UI when Pango returns standard style names. --- gtk/gtkfontbutton.c | 2 +- gtk/gtkfontsel.c | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/gtk/gtkfontbutton.c b/gtk/gtkfontbutton.c index 08aea103e..a52ddd470 100644 --- a/gtk/gtkfontbutton.c +++ b/gtk/gtkfontbutton.c @@ -889,7 +889,7 @@ gtk_font_button_update_font_info (GtkFontButton *font_button) if (style == NULL || !g_ascii_strcasecmp (style, "Regular")) family_style = g_strdup (family); else - family_style = g_strdup_printf ("%s %s", family, style); + family_style = g_strdup_printf ("%s %s", family, _(style)); gtk_label_set_text (GTK_LABEL (font_button->priv->font_label), family_style); diff --git a/gtk/gtkfontsel.c b/gtk/gtkfontsel.c index 701d22841..7dc9a6ee8 100644 --- a/gtk/gtkfontsel.c +++ b/gtk/gtkfontsel.c @@ -76,6 +76,22 @@ #define DEFAULT_FONT_NAME "Sans 10" +/* Common font style names, listed here so they can be translated when + * Pango returns the standard English names. + */ +#define FONT_STYLE_ULTRA_LIGHT N_("Ultra-Light") +#define FONT_STYLE_LIGHT N_("Light") +#define FONT_STYLE_MEDIUM N_("Medium") +#define FONT_STYLE_NORMAL N_("Normal") +#define FONT_STYLE_REGULAR N_("Regular") +#define FONT_STYLE_ITALIC N_("Italic") +#define FONT_STYLE_OBLIQUE N_("Oblique") +#define FONT_STYLE_SEMI_BOLD N_("Semi-Bold") +#define FONT_STYLE_BOLD N_("Bold") +#define FONT_STYLE_ULTRA_BOLD N_("Ultra-Bold") +#define FONT_STYLE_HEAVY N_("Heavy") +#define FONT_STYLE_BOLD_ITALIC N_("Bold Italic") + /* This is the initial and maximum height of the preview entry (it expands when large font sizes are selected). Initial height is also the minimum. */ #define INITIAL_PREVIEW_HEIGHT 44 @@ -869,7 +885,7 @@ gtk_font_selection_show_available_styles (GtkFontSelection *fontsel) gtk_list_store_append (model, &iter); gtk_list_store_set (model, &iter, FACE_COLUMN, faces[i], - FACE_NAME_COLUMN, str, + FACE_NAME_COLUMN, _(str), -1); if (i == 0) From 2901f63d0d3fa8856e46dd68f7a72dd83f8849c6 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 13 May 2026 12:23:35 +0200 Subject: [PATCH 031/122] cups: add Secret Service helper for print authentication Based on the openSUSE gtk2-bgo743166-remember-printing-authentication.patch. Add the CUPS Secret Service helper source files separately before wiring them into the print backend. --- .../printbackends/cups/gtkcupssecretsutils.c | 1044 +++++++++++++++++ .../printbackends/cups/gtkcupssecretsutils.h | 41 + 2 files changed, 1085 insertions(+) create mode 100644 modules/printbackends/cups/gtkcupssecretsutils.c create mode 100644 modules/printbackends/cups/gtkcupssecretsutils.h diff --git a/modules/printbackends/cups/gtkcupssecretsutils.c b/modules/printbackends/cups/gtkcupssecretsutils.c new file mode 100644 index 000000000..2ed0db57c --- /dev/null +++ b/modules/printbackends/cups/gtkcupssecretsutils.c @@ -0,0 +1,1044 @@ +/* gtkcupssecretsutils.h: Helper to use a secrets service for printer passwords + * Copyright (C) 2014, Intevation GmbH + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#include +#include +#include + +#include + +#include "gtkcupssecretsutils.h" + +#define SECRETS_BUS "org.freedesktop.secrets" +#define SECRETS_IFACE(interface) "org.freedesktop.Secret."interface +#define SECRETS_PATH "/org/freedesktop/secrets" +#define SECRETS_TIMEOUT 5000 + +typedef enum +{ + SECRETS_SERVICE_ACTION_QUERY, + SECRETS_SERVICE_ACTION_STORE +} SecretsServiceAction; + +typedef struct +{ + GDBusConnection *dbus_connection; + SecretsServiceAction action; + gchar **auth_info, + **auth_info_labels, + **auth_info_required, + *printer_uri, + *session_path, + *collection_path; + GDBusProxy *item_proxy; + guint prompt_subscription; +} SecretsServiceData; + +/** + * create_attributes: + * @printer_uri: URI for the printer + * @additional_labels: Optional labels for additional attributes + * @additional_attrs: Optional additional attributes + * + * Creates a GVariant dictionary with key / value pairs that + * can be used to identify a secret item. + * + * Returns: A GVariant dictionary of string pairs or NULL on error. + */ +static GVariant * +create_attributes (const gchar *printer_uri, + gchar **additional_attrs, + gchar **additional_labels) +{ + GVariantBuilder *attr_builder = NULL; + GVariant *ret = NULL; + + if (printer_uri == NULL) + { + GTK_NOTE (PRINTING, + g_print ("create_attributes called with invalid parameters.\n")); + return NULL; + } + + attr_builder = g_variant_builder_new (G_VARIANT_TYPE_DICTIONARY); + /* The printer uri is the main identifying part */ + g_variant_builder_add (attr_builder, "{ss}", "uri", printer_uri); + + if (additional_labels != NULL) + { + int i; + for (i = 0; additional_labels[i] != NULL; i++) + { + g_variant_builder_add (attr_builder, "{ss}", + additional_labels[i], + additional_attrs[i]); + } + } + + ret = g_variant_builder_end (attr_builder); + g_variant_builder_unref (attr_builder); + + return ret; +} + +static void +get_secret_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GTask *task; + SecretsServiceData *task_data; + GError *error = NULL; + GVariant *output, + *attributes; + gchar **auth_info = NULL, + *key = NULL, + *value = NULL; + GVariantIter *iter = NULL; + guint i; + gint pw_field = -1; + + task = user_data; + task_data = g_task_get_task_data (task); + + output = g_dbus_proxy_call_finish (G_DBUS_PROXY (source_object), + res, + &error); + if (output == NULL) + { + g_task_return_error (task, error); + return; + } + + attributes = g_dbus_proxy_get_cached_property (task_data->item_proxy, + "Attributes"); + if (attributes == NULL) + { + GTK_NOTE (PRINTING, g_print ("Failed to lookup attributes.\n")); + g_variant_unref (output); + g_task_return_pointer (task, NULL, NULL); + return; + } + + /* Iterate over the attributes to fill the auth info */ + g_variant_get (attributes, "a{ss}", &iter); + + auth_info = g_new0 (gchar *, + g_strv_length (task_data->auth_info_required) + 1); + + while (g_variant_iter_loop (iter, "{ss}", &key, &value)) + { + /* Match attributes with required auth info */ + for (i = 0; task_data->auth_info_required[i] != NULL; i++) + { + if ((strcmp (key, "user") == 0 || + strcmp (key, "username") == 0) && + strcmp (task_data->auth_info_required[i], + "username") == 0) + { + auth_info[i] = g_strdup (value); + } + else if (strcmp (key, "domain") == 0 && + strcmp (task_data->auth_info_required[i], "domain") == 0) + { + auth_info[i] = g_strdup (value); + } + else if ((strcmp (key, "hostname") == 0 || + strcmp (key, "server") == 0 ) && + strcmp (task_data->auth_info_required[i], "hostname") == 0) + { + auth_info[i] = g_strdup (value); + } + else if (strcmp (task_data->auth_info_required[i], "password") == 0) + { + pw_field = i; + } + } + } + + if (pw_field == -1) + { + /* should not happen... */ + GTK_NOTE (PRINTING, g_print ("No password required?.\n")); + g_variant_unref (output); + goto fail; + } + else + { + GVariant *secret, + *s_value; + gconstpointer ba_passwd = NULL; + gsize len = 0; + + secret = g_variant_get_child_value (output, 0); + g_variant_unref (output); + if (secret == NULL || g_variant_n_children (secret) != 4) + { + GTK_NOTE (PRINTING, g_print ("Get secret response invalid.\n")); + if (secret != NULL) + g_variant_unref (secret); + goto fail; + } + s_value = g_variant_get_child_value (secret, 2); + ba_passwd = g_variant_get_fixed_array (s_value, + &len, + sizeof (guchar)); + + g_variant_unref (secret); + + if (ba_passwd == NULL) + { + /* No secret or the secret is not a zero terminated value */ + GTK_NOTE (PRINTING, g_print ("Invalid / no secret found.\n")); + g_variant_unref (s_value); + goto fail; + } + + auth_info[pw_field] = g_strndup (ba_passwd, len); + g_variant_unref (s_value); + } + + for (i = 0; task_data->auth_info_required[i] != NULL; i++) + { + if (auth_info[i] == NULL) + { + /* Error out if we did not find everything */ + GTK_NOTE (PRINTING, g_print ("Failed to lookup required attribute: %s.\n", + task_data->auth_info_required[i])); + goto fail; + } + } + + g_task_return_pointer (task, auth_info, NULL); + return; + +fail: + /* Error out */ + GTK_NOTE (PRINTING, g_print ("Failed to lookup secret.\n")); + for (i = 0; i < g_strv_length (task_data->auth_info_required); i++) + { + /* Not all fields of auth_info are neccessarily written so we can not + use strfreev here */ + g_free (auth_info[i]); + } + g_free (auth_info); + g_task_return_pointer (task, NULL, NULL); +} + +static void +create_item_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GTask *task; + GError *error = NULL; + GVariant *output; + gchar *item = NULL; + + task = user_data; + + output = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), + res, + &error); + if (output == NULL) + { + g_task_return_error (task, error); + return; + } + + g_variant_get (output, "(&o&o)", &item, NULL); + if (item != NULL && strlen (item) > 1) + { + GTK_NOTE (PRINTING, g_print ("Successfully stored auth info.\n")); + g_task_return_pointer (task, NULL, NULL); + return; + } + g_variant_unref (output); +} + +static void +do_store_auth_info (GTask *task) +{ + GVariant *attributes = NULL, + *properties = NULL, + *secret = NULL; + gchar **additional_attrs = NULL, + **additional_labels = NULL, + *password = NULL; + SecretsServiceData *task_data = g_task_get_task_data (task); + guint i, + length, + additional_count = 0; + GVariantBuilder *prop_builder = NULL; + + length = g_strv_length (task_data->auth_info_labels); + + additional_attrs = g_new0 (gchar *, length + 1); + additional_labels = g_new0 (gchar *, length + 1); + /* The labels user and server are chosen to be compatible with + the attributes used by system-config-printer */ + for (i = 0; task_data->auth_info_labels[i] != NULL; i++) + { + if (g_strcmp0 (task_data->auth_info_labels[i], "username") == 0) + { + additional_attrs[additional_count] = task_data->auth_info[i]; + additional_labels[additional_count++] = "user"; + } + else if (g_strcmp0 (task_data->auth_info_labels[i], "hostname") == 0) + { + additional_attrs[additional_count] = task_data->auth_info[i]; + additional_labels[additional_count++] = "server"; + } + else if (g_strcmp0 (task_data->auth_info_labels[i], "password") == 0) + { + password = task_data->auth_info[i]; + } + } + + attributes = create_attributes (task_data->printer_uri, + additional_attrs, + additional_labels); + g_free (additional_labels); + g_free (additional_attrs); + if (attributes == NULL) + { + GTK_NOTE (PRINTING, g_print ("Failed to create attributes.\n")); + g_task_return_pointer (task, NULL, NULL); + return; + } + + if (password == NULL) + { + GTK_NOTE (PRINTING, g_print ("No secret to store.\n")); + g_task_return_pointer (task, NULL, NULL); + return; + } + + prop_builder = g_variant_builder_new (G_VARIANT_TYPE_DICTIONARY); + + g_variant_builder_add (prop_builder, "{sv}", SECRETS_IFACE ("Item.Label"), + g_variant_new_string (task_data->printer_uri)); + g_variant_builder_add (prop_builder, "{sv}", SECRETS_IFACE ("Item.Attributes"), + attributes); + + properties = g_variant_builder_end (prop_builder); + + g_variant_builder_unref (prop_builder); + + secret = g_variant_new ("(oay@ays)", + task_data->session_path, + NULL, + g_variant_new_bytestring (password), + "text/plain"); + + g_dbus_connection_call (task_data->dbus_connection, + SECRETS_BUS, + task_data->collection_path, + SECRETS_IFACE ("Collection"), + "CreateItem", + g_variant_new ("(@a{sv}@(oayays)b)", + properties, + secret, + TRUE), + G_VARIANT_TYPE ("(oo)"), + G_DBUS_CALL_FLAGS_NONE, + SECRETS_TIMEOUT, + g_task_get_cancellable (task), + create_item_cb, + task); +} + +static void +prompt_completed_cb (GDBusConnection *connection, + const gchar *sender_name, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + GTask *task; + SecretsServiceData *task_data; + GVariant *dismissed; + gboolean is_dismissed = TRUE; + + task = user_data; + task_data = g_task_get_task_data (task); + + g_dbus_connection_signal_unsubscribe (task_data->dbus_connection, + task_data->prompt_subscription); + task_data->prompt_subscription = 0; + + dismissed = g_variant_get_child_value (parameters, 0); + + if (dismissed == NULL) + { + GTK_NOTE (PRINTING, g_print ("Invalid prompt signal.\n")); + g_task_return_pointer (task, NULL, NULL); + return; + } + + g_variant_get (dismissed, "b", &is_dismissed); + g_variant_unref (dismissed); + + if (is_dismissed) + { + GTK_NOTE (PRINTING, g_print ("Collection unlock dismissed.\n")); + g_task_return_pointer (task, NULL, NULL); + return; + } + + /* Prompt successfull, proceed to get or store secret */ + switch (task_data->action) + { + case SECRETS_SERVICE_ACTION_STORE: + do_store_auth_info (task); + break; + + case SECRETS_SERVICE_ACTION_QUERY: + g_dbus_proxy_call (task_data->item_proxy, + "GetSecret", + g_variant_new ("(o)", + task_data->session_path), + G_DBUS_CALL_FLAGS_NONE, + SECRETS_TIMEOUT, + g_task_get_cancellable (task), + get_secret_cb, + task); + break; + } +} + +static void +prompt_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GTask *task; + SecretsServiceData *task_data; + GError *error = NULL; + GVariant *output; + + task = user_data; + task_data = g_task_get_task_data (task); + + output = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), + res, + &error); + if (output == NULL) + { + g_task_return_error (task, error); + return; + } + + g_variant_unref (output); + + /* Connect to the prompt's completed signal */ + task_data->prompt_subscription = + g_dbus_connection_signal_subscribe (task_data->dbus_connection, + NULL, + SECRETS_IFACE ("Prompt"), + "Completed", + NULL, + NULL, + G_DBUS_SIGNAL_FLAGS_NONE, + prompt_completed_cb, + task, + NULL); +} + +static void +unlock_collection_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GTask *task; + SecretsServiceData *task_data; + GError *error = NULL; + GVariant *output; + const gchar *prompt_path; + + task = user_data; + task_data = g_task_get_task_data (task); + + output = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), + res, + &error); + if (output == NULL) + { + g_task_return_error (task, error); + return; + } + + g_variant_get (output, "(@ao&o)", NULL, &prompt_path); + + if (prompt_path != NULL && strlen (prompt_path) > 1) + { + g_dbus_connection_call (task_data->dbus_connection, + SECRETS_BUS, + prompt_path, + SECRETS_IFACE ("Prompt"), + "Prompt", + g_variant_new ("(s)", "0"), + G_VARIANT_TYPE ("()"), + G_DBUS_CALL_FLAGS_NONE, + SECRETS_TIMEOUT, + g_task_get_cancellable (task), + prompt_cb, + task); + } + else + { + switch (task_data->action) + { + case SECRETS_SERVICE_ACTION_STORE: + do_store_auth_info (task); + break; + + case SECRETS_SERVICE_ACTION_QUERY: + /* Prompt successfull proceed to get secret */ + g_dbus_proxy_call (task_data->item_proxy, + "GetSecret", + g_variant_new ("(o)", + task_data->session_path), + G_DBUS_CALL_FLAGS_NONE, + SECRETS_TIMEOUT, + g_task_get_cancellable (task), + get_secret_cb, + task); + break; + } + } + g_variant_unref (output); +} + +static void +unlock_read_alias_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GTask *task; + SecretsServiceData *task_data; + GError *error = NULL; + GVariant *output, + *subresult; + gsize path_len = 0; + const gchar *collection_path; + + task = user_data; + task_data = g_task_get_task_data (task); + + output = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), + res, + &error); + if (output == NULL) + { + g_task_return_error (task, error); + return; + } + + subresult = g_variant_get_child_value (output, 0); + g_variant_unref (output); + + if (subresult == NULL) + { + GTK_NOTE (PRINTING, g_print ("Invalid ReadAlias response.\n")); + g_task_return_pointer (task, NULL, NULL); + return; + } + + collection_path = g_variant_get_string (subresult, &path_len); + + const gchar * const to_unlock[] = + { + collection_path, NULL + }; + + task_data->collection_path = g_strdup (collection_path); + + g_dbus_connection_call (task_data->dbus_connection, + SECRETS_BUS, + SECRETS_PATH, + SECRETS_IFACE ("Service"), + "Unlock", + g_variant_new ("(^ao)", to_unlock), + G_VARIANT_TYPE ("(aoo)"), + G_DBUS_CALL_FLAGS_NONE, + SECRETS_TIMEOUT, + g_task_get_cancellable (task), + unlock_collection_cb, + task); + + g_variant_unref (subresult); +} + +static void +item_proxy_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GTask *task; + SecretsServiceData *task_data; + GError *error = NULL; + GDBusProxy *item_proxy; + GVariant *locked; + gboolean is_locked; + + task = user_data; + task_data = g_task_get_task_data (task); + + item_proxy = g_dbus_proxy_new_finish (res, + &error); + if (item_proxy == NULL) + { + g_task_return_error (task, error); + return; + } + + task_data->item_proxy = item_proxy; + + locked = g_dbus_proxy_get_cached_property (item_proxy, "Locked"); + + if (locked == NULL) + { + GTK_NOTE (PRINTING, g_print ("Failed to look up \"Locked\" property on item.\n")); + g_task_return_pointer (task, NULL, NULL); + return; + } + + g_variant_get (locked, "b", &is_locked); + g_variant_unref (locked); + + if (is_locked) + { + /* Go down the unlock -> lookup path */ + g_dbus_connection_call (task_data->dbus_connection, + SECRETS_BUS, + SECRETS_PATH, + SECRETS_IFACE ("Service"), + "ReadAlias", + g_variant_new ("(s)", "default"), + G_VARIANT_TYPE ("(o)"), + G_DBUS_CALL_FLAGS_NONE, + SECRETS_TIMEOUT, + g_task_get_cancellable (task), + unlock_read_alias_cb, + task); + return; + } + + /* Unlocked proceed to get or store secret */ + switch (task_data->action) + { + case SECRETS_SERVICE_ACTION_STORE: + do_store_auth_info (task); + break; + + case SECRETS_SERVICE_ACTION_QUERY: + g_dbus_proxy_call (item_proxy, + "GetSecret", + g_variant_new ("(o)", + task_data->session_path), + G_DBUS_CALL_FLAGS_NONE, + SECRETS_TIMEOUT, + g_task_get_cancellable (task), + get_secret_cb, + task); + break; + } +} + +static void +search_items_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GTask *task; + SecretsServiceData *task_data; + GError *error = NULL; + GVariant *output; + gsize array_cnt, + i; + gboolean found_item = FALSE; + + task = user_data; + task_data = g_task_get_task_data (task); + + output = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), + res, + &error); + if (output == NULL) + { + g_task_return_error (task, error); + return; + } + + array_cnt = g_variant_n_children (output); + + for (i = 0; i < array_cnt; i++) + { + GVariant * const item_paths = g_variant_get_child_value (output, i); + const gchar **items = NULL; + + if (item_paths == NULL) + { + GTK_NOTE (PRINTING, + g_print ("SearchItems returned invalid result.\n")); + continue; + } + + items = g_variant_get_objv (item_paths, NULL); + + if (*items == NULL) + { + g_variant_unref (item_paths); + g_free ((gpointer) items); + continue; + } + + /* Access the first found item. */ + found_item = TRUE; + g_dbus_proxy_new (task_data->dbus_connection, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + SECRETS_BUS, + *items, + SECRETS_IFACE ("Item"), + g_task_get_cancellable (task), + item_proxy_cb, + task); + g_free ((gpointer) items); + g_variant_unref (item_paths); + break; + } + g_variant_unref (output); + + if (!found_item) + { + GTK_NOTE (PRINTING, g_print ("No match found in secrets service.\n")); + g_task_return_pointer (task, NULL, NULL); + return; + } +} + +static void +open_session_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GTask *task; + GVariant *output, + *session_variant; + SecretsServiceData *task_data; + GError *error = NULL; + + task = user_data; + task_data = g_task_get_task_data (task); + + output = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), + res, + &error); + if (output == NULL) + { + g_task_return_error (task, error); + return; + } + + session_variant = g_variant_get_child_value (output, 1); + + if (session_variant == NULL) + { + GTK_NOTE (PRINTING, g_print ("Invalid session path response.\n")); + g_variant_unref (output); + g_task_return_pointer (task, NULL, NULL); + return; + } + + task_data->session_path = g_variant_dup_string (session_variant, NULL); + + if (task_data->session_path == NULL) + { + GTK_NOTE (PRINTING, g_print ("Invalid session path string value.\n")); + g_variant_unref (session_variant); + g_variant_unref (output); + g_task_return_pointer (task, NULL, NULL); + return; + } + + g_variant_unref (session_variant); + g_variant_unref (output); + + switch (task_data->action) + { + case SECRETS_SERVICE_ACTION_QUERY: + { + /* Search for the secret item */ + GVariant *secrets_attrs; + + secrets_attrs = create_attributes (task_data->printer_uri, NULL, NULL); + if (secrets_attrs == NULL) + { + GTK_NOTE (PRINTING, g_print ("Failed to create attributes.\n")); + g_task_return_pointer (task, NULL, NULL); + return; + } + + g_dbus_connection_call (task_data->dbus_connection, + SECRETS_BUS, + SECRETS_PATH, + SECRETS_IFACE ("Service"), + "SearchItems", + g_variant_new ("(@a{ss})", secrets_attrs), + G_VARIANT_TYPE ("(aoao)"), + G_DBUS_CALL_FLAGS_NONE, + SECRETS_TIMEOUT, + g_task_get_cancellable (task), + search_items_cb, + task); + break; + } + + case SECRETS_SERVICE_ACTION_STORE: + { + /* Look up / unlock the default collection for storing */ + g_dbus_connection_call (task_data->dbus_connection, + SECRETS_BUS, + SECRETS_PATH, + SECRETS_IFACE ("Service"), + "ReadAlias", + g_variant_new ("(s)", "default"), + G_VARIANT_TYPE ("(o)"), + G_DBUS_CALL_FLAGS_NONE, + SECRETS_TIMEOUT, + g_task_get_cancellable (task), + unlock_read_alias_cb, + task); + break; + } + } +} + +static void +get_connection_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GTask *task; + SecretsServiceData *task_data; + GError *error = NULL; + + task = user_data; + task_data = g_task_get_task_data (task); + + task_data->dbus_connection = g_bus_get_finish (res, &error); + if (task_data->dbus_connection == NULL) + { + g_task_return_error (task, error); + return; + } + + /* Now open a session */ + g_dbus_connection_call (task_data->dbus_connection, + SECRETS_BUS, + SECRETS_PATH, + SECRETS_IFACE ("Service"), + "OpenSession", + g_variant_new ("(sv)", "plain", + g_variant_new_string ("")), + G_VARIANT_TYPE ("(vo)"), + G_DBUS_CALL_FLAGS_NONE, + SECRETS_TIMEOUT, + g_task_get_cancellable (task), + open_session_cb, + task); +} + +/** + * gtk_cups_secrets_service_watch: + * @appeared: The callback to call when the service interface appears + * @vanished: The callback to call when the service interface vanishes + * @user_data: A reference to the watching printbackend + * + * Registers a watch for the secrets service interface. + * + * Returns: The watcher id + */ +guint +gtk_cups_secrets_service_watch (GBusNameAppearedCallback appeared, + GBusNameVanishedCallback vanished, + gpointer user_data) +{ + return g_bus_watch_name (G_BUS_TYPE_SESSION, + SECRETS_BUS, + G_BUS_NAME_WATCHER_FLAGS_AUTO_START, + appeared, + vanished, + user_data, + NULL); +} + +void +cleanup_task_data (gpointer data) +{ + gint i; + SecretsServiceData *task_data = data; + + g_free (task_data->collection_path); + g_strfreev (task_data->auth_info_labels); + g_strfreev (task_data->auth_info_required); + g_free (task_data->printer_uri); + + if (task_data->auth_info != NULL) + { + for (i = 0; task_data->auth_info[i] != NULL; i++) + { + memset (task_data->auth_info[i], 0, strlen (task_data->auth_info[i])); + g_clear_pointer (&task_data->auth_info[i], g_free); + } + g_clear_pointer (&task_data->auth_info, g_free); + } + + if (task_data->prompt_subscription != 0) + { + g_dbus_connection_signal_unsubscribe (task_data->dbus_connection, + task_data->prompt_subscription); + task_data->prompt_subscription = 0; + } + + if (task_data->session_path != NULL) + { + g_dbus_connection_call (task_data->dbus_connection, + SECRETS_BUS, + task_data->session_path, + SECRETS_IFACE ("Session"), + "Close", + NULL, + G_VARIANT_TYPE ("()"), + G_DBUS_CALL_FLAGS_NONE, + SECRETS_TIMEOUT, + NULL, + NULL, + NULL); + } + + g_clear_object (&task_data->dbus_connection); + g_clear_pointer (&task_data->session_path, g_free); + g_clear_object (&task_data->item_proxy); +} + +/** + * gtk_cups_secrets_service_query_task: + * @source_object: Source object for this task + * @cancellable: Cancellable to cancel this task + * @callback: Callback to call once the query is finished + * @user_data: The user_data passed to the callback + * @printer_uri: URI of the printer + * @auth_info_required: Info required for authentication + * + * Checks if a secrets service as described by the secrets-service standard + * is available and if so it tries to find the authentication info in the + * default collection of the service. + * + * This is the entry point to a chain of async calls to open a session, + * search the secret, unlock the collection (if necessary) and finally + * to lookup the secret. + * + * See: http://standards.freedesktop.org/secret-service/ for documentation + * of the used API. + */ +void +gtk_cups_secrets_service_query_task (gpointer source_object, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data, + const gchar *printer_uri, + gchar **auth_info_required) +{ + GTask *task; + SecretsServiceData *task_data; + + task_data = g_new0 (SecretsServiceData, 1); + task_data->action = SECRETS_SERVICE_ACTION_QUERY; + task_data->printer_uri = g_strdup (printer_uri); + task_data->auth_info_required = g_strdupv (auth_info_required); + + task = g_task_new (source_object, cancellable, callback, user_data); + + g_task_set_task_data (task, task_data, cleanup_task_data); + + g_bus_get (G_BUS_TYPE_SESSION, cancellable, + get_connection_cb, task); +} + +static void +store_done_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GTask *task = (GTask *) res; + GError *error = NULL; + + g_task_propagate_pointer (task, &error); + + if (error != NULL) + { + GTK_NOTE (PRINTING, + g_print ("Failed to store auth info: %s\n", error->message)); + g_error_free (error); + } + + g_object_unref (task); + GTK_NOTE (PRINTING, + g_print ("gtk_cups_secrets_service_store finished.\n")); +} + +/** + * gtk_cups_secrets_service_store: + * @auth_info: Auth info that should be stored + * @auth_info_labels: The keys to use for the auth info + * @printer_uri: URI of the printer + * + * Tries to store the auth_info in a secrets service. + */ +void +gtk_cups_secrets_service_store (gchar **auth_info, + gchar **auth_info_labels, + const gchar *printer_uri) +{ + GTask *task; + SecretsServiceData *task_data; + + if (auth_info == NULL || auth_info_labels == NULL || printer_uri == NULL) + { + GTK_NOTE (PRINTING, + g_print ("Invalid call to gtk_cups_secrets_service_store.\n")); + return; + } + + task_data = g_new0 (SecretsServiceData, 1); + task_data->action = SECRETS_SERVICE_ACTION_STORE; + task_data->printer_uri = g_strdup (printer_uri); + task_data->auth_info = g_strdupv (auth_info); + task_data->auth_info_labels = g_strdupv (auth_info_labels); + + task = g_task_new (NULL, NULL, store_done_cb, NULL); + + g_task_set_task_data (task, task_data, cleanup_task_data); + + g_bus_get (G_BUS_TYPE_SESSION, NULL, + get_connection_cb, task); +} diff --git a/modules/printbackends/cups/gtkcupssecretsutils.h b/modules/printbackends/cups/gtkcupssecretsutils.h new file mode 100644 index 000000000..1a0424a3b --- /dev/null +++ b/modules/printbackends/cups/gtkcupssecretsutils.h @@ -0,0 +1,41 @@ +/* gtkcupssecretsutils.h: Helper to use a secrets service for printer passwords + * Copyright (C) 2014 Intevation GmbH + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ +#ifndef __GTK_SECRETS_UTILS_H__ +#define __GTK_SECRETS_UTILS_H__ + +#include + +#include "gtkcupsutils.h" + +G_BEGIN_DECLS + +void gtk_cups_secrets_service_query_task (gpointer source_object, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data, + const gchar *printer_uri, + gchar **auth_info_required); +guint gtk_cups_secrets_service_watch (GBusNameAppearedCallback appeared, + GBusNameVanishedCallback vanished, + gpointer user_data); +void gtk_cups_secrets_service_store (gchar **auth_info, + gchar **auth_info_labels, + const gchar *printer_uri); + +G_END_DECLS + +#endif /* __GTK_SECRETS_UTILS_H__ */ From 0d089cd6deaf9f89522215c0454d67219075bd47 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 13 May 2026 12:29:23 +0200 Subject: [PATCH 032/122] cups: build Secret Service authentication helper Wire the imported CUPS Secret Service helper into the CUPS print backend build. --- modules/printbackends/cups/Makefile.am | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/printbackends/cups/Makefile.am b/modules/printbackends/cups/Makefile.am index 5bc26cde6..369d8446c 100644 --- a/modules/printbackends/cups/Makefile.am +++ b/modules/printbackends/cups/Makefile.am @@ -27,12 +27,14 @@ backend_LTLIBRARIES = libprintbackend-cups.la libprintbackend_cups_la_SOURCES = \ gtkprintbackendcups.c \ gtkprintercups.c \ - gtkcupsutils.c + gtkcupsutils.c \ + gtkcupssecretsutils.c noinst_HEADERS = \ gtkprintbackendcups.h \ gtkprintercups.h \ - gtkcupsutils.h + gtkcupsutils.h \ + gtkcupssecretsutils.h libprintbackend_cups_la_LDFLAGS = -avoid-version -module $(no_undefined) libprintbackend_cups_la_LIBADD = $(LDADDS) $(CUPS_LIBS) From dab59b874bf35753e6e3c2f1176ea9fff3aeffa5 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 13 May 2026 12:36:45 +0200 Subject: [PATCH 033/122] print: add optional remember-password UI state Prepare the print authentication dialog for backends that can store credentials without changing the request-password signal or gtk_print_backend_set_password() ABI. This keeps the openSUSE CUPS Secret Service feature ABI-conservative for gtk2-ng. --- gtk/gtkprintbackend.c | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/gtk/gtkprintbackend.c b/gtk/gtkprintbackend.c index 66f5b41a9..669e1c1e2 100644 --- a/gtk/gtkprintbackend.c +++ b/gtk/gtkprintbackend.c @@ -349,6 +349,9 @@ gtk_print_backend_load_modules (void) G_DEFINE_TYPE_WITH_PRIVATE (GtkPrintBackend, gtk_print_backend, G_TYPE_OBJECT) +#define GTK_PRINT_BACKEND_CAN_STORE_AUTH_INFO_KEY "gtk-print-backend-can-store-auth-info" +#define GTK_PRINT_BACKEND_STORE_AUTH_INFO_KEY "gtk-print-backend-store-auth-info" + static void fallback_printer_request_details (GtkPrinter *printer); static gboolean fallback_printer_mark_conflicts (GtkPrinter *printer, GtkPrinterOptionSet *options); @@ -690,6 +693,17 @@ store_entry (GtkEntry *entry, *data = g_strdup (gtk_entry_get_text (entry)); } +static void +store_auth_info_toggled (GtkToggleButton *button, + gpointer user_data) +{ + GtkPrintBackend *backend = GTK_PRINT_BACKEND (user_data); + + g_object_set_data (G_OBJECT (backend), + GTK_PRINT_BACKEND_STORE_AUTH_INFO_KEY, + GINT_TO_POINTER (gtk_toggle_button_get_active (button))); +} + static void password_dialog_response (GtkWidget *dialog, gint response_id, @@ -729,7 +743,7 @@ request_password (GtkPrintBackend *backend, const gchar *prompt) { GtkPrintBackendPrivate *priv = backend->priv; - GtkWidget *dialog, *box, *main_box, *label, *icon, *vbox, *entry; + GtkWidget *dialog, *box, *main_box, *label, *icon, *vbox, *entry, *chkbtn; GtkWidget *focus = NULL; gchar *markup; gint length; @@ -743,6 +757,10 @@ request_password (GtkPrintBackend *backend, length = g_strv_length (ai_required); priv->auth_info = g_new0 (gchar *, length + 1); + g_object_set_data (G_OBJECT (backend), + GTK_PRINT_BACKEND_STORE_AUTH_INFO_KEY, + GINT_TO_POINTER (FALSE)); + dialog = gtk_dialog_new_with_buttons ( _("Authentication"), NULL, GTK_DIALOG_MODAL, GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, GTK_STOCK_OK, GTK_RESPONSE_OK, @@ -810,6 +828,17 @@ request_password (GtkPrintBackend *backend, } } + if (g_object_get_data (G_OBJECT (backend), + GTK_PRINT_BACKEND_CAN_STORE_AUTH_INFO_KEY)) + { + chkbtn = gtk_check_button_new_with_mnemonic (_("_Remember password")); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (chkbtn), FALSE); + gtk_box_pack_start (GTK_BOX (vbox), chkbtn, FALSE, FALSE, 6); + g_signal_connect (chkbtn, "toggled", + G_CALLBACK (store_auth_info_toggled), + backend); + } + if (focus != NULL) { gtk_widget_grab_focus (focus); From 0c87680876a415195a8ec74dd347294527854a20 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 13 May 2026 12:56:13 +0200 Subject: [PATCH 034/122] cups: wire Secret Service storage into authentication flow Use the imported Secret Service helper to optionally store CUPS authentication data when a secrets service is available and the user enables password remembering. Keep the GtkPrintBackend request-password signal and gtk_print_backend_set_password() ABI unchanged by passing the transient UI state through private object data. --- .../printbackends/cups/gtkprintbackendcups.c | 74 ++++++++++++++++++- 1 file changed, 71 insertions(+), 3 deletions(-) diff --git a/modules/printbackends/cups/gtkprintbackendcups.c b/modules/printbackends/cups/gtkprintbackendcups.c index e17d97d21..21ab80836 100644 --- a/modules/printbackends/cups/gtkprintbackendcups.c +++ b/modules/printbackends/cups/gtkprintbackendcups.c @@ -60,6 +60,11 @@ #include "gtkprintercups.h" #include "gtkcupsutils.h" +#include "gtkcupssecretsutils.h" + +#define GTK_PRINT_BACKEND_CAN_STORE_AUTH_INFO_KEY "gtk-print-backend-can-store-auth-info" +#define GTK_PRINT_BACKEND_STORE_AUTH_INFO_KEY "gtk-print-backend-store-auth-info" +#define GTK_PRINT_BACKEND_AUTH_PRINTER_URI_KEY "gtk-print-backend-auth-printer-uri" typedef struct _GtkPrintBackendCupsClass GtkPrintBackendCupsClass; @@ -144,6 +149,9 @@ struct _GtkPrintBackendCups GHashTable *auth; gchar *username; gboolean authentication_lock; + + gboolean secrets_service_available; + guint secrets_service_watch_id; #ifdef HAVE_CUPS_API_1_6 GDBusConnection *dbus_connection; gchar *avahi_default_printer; @@ -213,6 +221,14 @@ static cairo_surface_t * cups_printer_create_cairo_surface (GtkPrinter static void gtk_print_backend_cups_set_password (GtkPrintBackend *backend, gchar **auth_info_required, gchar **auth_info); +static void secrets_service_appeared (GDBusConnection *connection, + const gchar *name, + const gchar *name_owner, + gpointer user_data); +static void secrets_service_vanished (GDBusConnection *connection, + const gchar *name, + gpointer user_data); + void overwrite_and_free (gpointer data); static gboolean is_address_local (const gchar *address); @@ -751,6 +767,27 @@ void overwrite_and_free (gpointer data) } } +static void +secrets_service_appeared (GDBusConnection *connection, + const gchar *name, + const gchar *name_owner, + gpointer user_data) +{ + GtkPrintBackendCups *cups_backend = GTK_PRINT_BACKEND_CUPS (user_data); + + cups_backend->secrets_service_available = TRUE; +} + +static void +secrets_service_vanished (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + GtkPrintBackendCups *cups_backend = GTK_PRINT_BACKEND_CUPS (user_data); + + cups_backend->secrets_service_available = FALSE; +} + static void gtk_print_backend_cups_init (GtkPrintBackendCups *backend_cups) { @@ -767,6 +804,11 @@ gtk_print_backend_cups_init (GtkPrintBackendCups *backend_cups) backend_cups->requests = NULL; backend_cups->auth = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, overwrite_and_free); backend_cups->authentication_lock = FALSE; + backend_cups->secrets_service_available = FALSE; + backend_cups->secrets_service_watch_id = + gtk_cups_secrets_service_watch (secrets_service_appeared, + secrets_service_vanished, + backend_cups); backend_cups->covers = NULL; backend_cups->number_of_covers = 0; @@ -836,6 +878,12 @@ gtk_print_backend_cups_dispose (GObject *object) backend_cups = GTK_PRINT_BACKEND_CUPS (object); + if (backend_cups->secrets_service_watch_id != 0) + { + g_bus_unwatch_name (backend_cups->secrets_service_watch_id); + backend_cups->secrets_service_watch_id = 0; + } + if (backend_cups->list_printers_poll > 0) g_source_remove (backend_cups->list_printers_poll); backend_cups->list_printers_poll = 0; @@ -940,7 +988,7 @@ gtk_print_backend_cups_set_password (GtkPrintBackend *backend, GtkPrintBackendCups *cups_backend = GTK_PRINT_BACKEND_CUPS (backend); GList *l; char dispatch_hostname[HTTP_MAX_URI]; - gchar *key; + gchar *key = NULL; gchar *username = NULL; gchar *hostname = NULL; gchar *password = NULL; @@ -962,15 +1010,27 @@ gtk_print_backend_cups_set_password (GtkPrintBackend *backend, if (hostname != NULL && username != NULL && password != NULL) { + const gchar *printer_uri; + gboolean store_auth_info; + key = g_strconcat (username, "@", hostname, NULL); g_hash_table_insert (cups_backend->auth, key, g_strdup (password)); + + printer_uri = g_object_get_data (G_OBJECT (backend), + GTK_PRINT_BACKEND_AUTH_PRINTER_URI_KEY); + store_auth_info = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (backend), + GTK_PRINT_BACKEND_STORE_AUTH_INFO_KEY)); + + if (store_auth_info && cups_backend->secrets_service_available && printer_uri != NULL) + gtk_cups_secrets_service_store (auth_info, auth_info_required, printer_uri); } g_free (cups_backend->username); cups_backend->username = g_strdup (username); - GTK_NOTE (PRINTING, - g_print ("CUPS backend: storing password for %s\n", key)); + if (key != NULL) + GTK_NOTE (PRINTING, + g_print ("CUPS backend: storing password for %s\n", key)); for (l = cups_backend->requests; l; l = l->next) { @@ -1114,6 +1174,14 @@ request_password (gpointer data) g_free (printer_name); + g_object_set_data (G_OBJECT (dispatch->backend), + GTK_PRINT_BACKEND_CAN_STORE_AUTH_INFO_KEY, + GINT_TO_POINTER (dispatch->backend->secrets_service_available)); + g_object_set_data_full (G_OBJECT (dispatch->backend), + GTK_PRINT_BACKEND_AUTH_PRINTER_URI_KEY, + g_strdup (printer_uri), + g_free); + g_signal_emit_by_name (dispatch->backend, "request-password", auth_info_required, auth_info_default, auth_info_display, auth_info_visible, prompt); From 3501d8c4f558e1387faf2c2151d1d4db937d2623 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 13 May 2026 13:20:06 +0200 Subject: [PATCH 035/122] fontsel: defer expensive selection updates Based on the openSUSE gtk2-bnc130159-bgo319483-async-selection-in-gtk-font-selection.diff. Coalesce font family, style and size selection updates with short timeouts to keep GtkFontSelection responsive while preserving immediate updates for internal programmatic selection changes. --- gtk/gtkfontsel.c | 166 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 150 insertions(+), 16 deletions(-) diff --git a/gtk/gtkfontsel.c b/gtk/gtkfontsel.c index 7dc9a6ee8..76927c797 100644 --- a/gtk/gtkfontsel.c +++ b/gtk/gtkfontsel.c @@ -585,6 +585,79 @@ gtk_font_selection_new (void) return GTK_WIDGET (fontsel); } +static GQuark +font_selection_font_timeout_quark (void) +{ + return g_quark_from_static_string ("gtk-font-selection-font-timeout"); +} + +static GQuark +font_selection_style_timeout_quark (void) +{ + return g_quark_from_static_string ("gtk-font-selection-style-timeout"); +} + +static GQuark +font_selection_size_timeout_quark (void) +{ + return g_quark_from_static_string ("gtk-font-selection-size-timeout"); +} + +static GQuark +font_selection_immediate_quark (void) +{ + return g_quark_from_static_string ("gtk-font-selection-immediate"); +} + +static void +gtk_font_selection_remove_timeout (GtkFontSelection *fontsel, + GQuark quark) +{ + guint timeout_id; + + timeout_id = GPOINTER_TO_UINT (g_object_get_qdata (G_OBJECT (fontsel), quark)); + if (timeout_id) + { + g_source_remove (timeout_id); + g_object_set_qdata (G_OBJECT (fontsel), quark, NULL); + } +} + +static gboolean +gtk_font_selection_get_immediate (GtkFontSelection *fontsel) +{ + return GPOINTER_TO_UINT (g_object_get_qdata (G_OBJECT (fontsel), + font_selection_immediate_quark ())) != 0; +} + +static void +gtk_font_selection_set_immediate (GtkFontSelection *fontsel, + gboolean immediate) +{ + g_object_set_qdata (G_OBJECT (fontsel), + font_selection_immediate_quark (), + GUINT_TO_POINTER (immediate ? 1 : 0)); +} + +static void +gtk_font_selection_schedule_selection (GtkFontSelection *fontsel, + GQuark quark, + GSourceFunc func) +{ + guint timeout_id; + + gtk_font_selection_remove_timeout (fontsel, quark); + + if (gtk_font_selection_get_immediate (fontsel)) + { + func (fontsel); + return; + } + + timeout_id = g_timeout_add (200, func, fontsel); + g_object_set_qdata (G_OBJECT (fontsel), quark, GUINT_TO_POINTER (timeout_id)); +} + static void gtk_font_selection_finalize (GObject *object) { @@ -594,6 +667,10 @@ gtk_font_selection_finalize (GObject *object) fontsel = GTK_FONT_SELECTION (object); + gtk_font_selection_remove_timeout (fontsel, font_selection_font_timeout_quark ()); + gtk_font_selection_remove_timeout (fontsel, font_selection_style_timeout_quark ()); + gtk_font_selection_remove_timeout (fontsel, font_selection_size_timeout_quark ()); + if (fontsel->font) gdk_font_unref (fontsel->font); @@ -632,6 +709,8 @@ gtk_font_selection_reload_fonts (GtkFontSelection *fontsel) PangoFontDescription *desc; desc = gtk_font_selection_get_font_description (fontsel); + gtk_font_selection_set_immediate (fontsel, TRUE); + gtk_font_selection_show_available_fonts (fontsel); gtk_font_selection_show_available_sizes (fontsel, TRUE); gtk_font_selection_show_available_styles (fontsel); @@ -639,6 +718,8 @@ gtk_font_selection_reload_fonts (GtkFontSelection *fontsel) gtk_font_selection_select_font_desc (fontsel, desc, NULL, NULL); gtk_font_selection_scroll_to_selection (fontsel); + gtk_font_selection_set_immediate (fontsel, FALSE); + pango_font_description_free (desc); } } @@ -715,11 +796,11 @@ gtk_font_selection_scroll_on_map (GtkWidget *widget, } /* This is called when a family is selected in the list. */ -static void -gtk_font_selection_select_font (GtkTreeSelection *selection, - gpointer data) +static gboolean +gtk_font_selection_real_select_font (gpointer data) { GtkFontSelection *fontsel; + GtkTreeSelection *selection; GtkTreeModel *model; GtkTreeIter iter; #ifdef INCLUDE_FONT_ENTRIES @@ -727,6 +808,7 @@ gtk_font_selection_select_font (GtkTreeSelection *selection, #endif fontsel = GTK_FONT_SELECTION (data); + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (fontsel->family_list)); if (gtk_tree_selection_get_selected (selection, &model, &iter)) { @@ -734,20 +816,35 @@ gtk_font_selection_select_font (GtkTreeSelection *selection, gtk_tree_model_get (model, &iter, FAMILY_COLUMN, &family, -1); if (fontsel->family != family) - { - gtk_font_selection_ref_family (fontsel, family); + { + gtk_font_selection_ref_family (fontsel, family); #ifdef INCLUDE_FONT_ENTRIES - family_name = pango_font_family_get_name (fontsel->family); - gtk_entry_set_text (GTK_ENTRY (fontsel->font_entry), family_name); + family_name = pango_font_family_get_name (fontsel->family); + gtk_entry_set_text (GTK_ENTRY (fontsel->font_entry), family_name); #endif - gtk_font_selection_show_available_styles (fontsel); - gtk_font_selection_select_best_style (fontsel, TRUE); - } + gtk_font_selection_show_available_styles (fontsel); + gtk_font_selection_select_best_style (fontsel, TRUE); + } g_object_unref (family); } + + g_object_set_qdata (G_OBJECT (fontsel), font_selection_font_timeout_quark (), NULL); + + return FALSE; +} + +static void +gtk_font_selection_select_font (GtkTreeSelection *selection, + gpointer data) +{ + GtkFontSelection *fontsel = GTK_FONT_SELECTION (data); + + gtk_font_selection_schedule_selection (fontsel, + font_selection_font_timeout_quark (), + gtk_font_selection_real_select_font); } static int @@ -950,14 +1047,16 @@ gtk_font_selection_select_best_style (GtkFontSelection *fontsel, /* This is called when a style is selected in the list. */ -static void -gtk_font_selection_select_style (GtkTreeSelection *selection, - gpointer data) +static gboolean +gtk_font_selection_real_select_style (gpointer data) { GtkFontSelection *fontsel = GTK_FONT_SELECTION (data); + GtkTreeSelection *selection; GtkTreeModel *model; GtkTreeIter iter; + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (fontsel->face_list)); + if (gtk_tree_selection_get_selected (selection, &model, &iter)) { PangoFontFace *face; @@ -969,6 +1068,21 @@ gtk_font_selection_select_style (GtkTreeSelection *selection, gtk_font_selection_show_available_sizes (fontsel, FALSE); gtk_font_selection_select_best_size (fontsel); + + g_object_set_qdata (G_OBJECT (fontsel), font_selection_style_timeout_quark (), NULL); + + return FALSE; +} + +static void +gtk_font_selection_select_style (GtkTreeSelection *selection, + gpointer data) +{ + GtkFontSelection *fontsel = GTK_FONT_SELECTION (data); + + gtk_font_selection_schedule_selection (fontsel, + font_selection_style_timeout_quark (), + gtk_font_selection_real_select_style); } static void @@ -1101,22 +1215,38 @@ gtk_font_selection_size_focus_out (GtkWidget *w, } /* This is called when a size is selected in the list. */ -static void -gtk_font_selection_select_size (GtkTreeSelection *selection, - gpointer data) +static gboolean +gtk_font_selection_real_select_size (gpointer data) { GtkFontSelection *fontsel; + GtkTreeSelection *selection; GtkTreeModel *model; GtkTreeIter iter; gint new_size; fontsel = GTK_FONT_SELECTION (data); + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (fontsel->size_list)); if (gtk_tree_selection_get_selected (selection, &model, &iter)) { gtk_tree_model_get (model, &iter, SIZE_COLUMN, &new_size, -1); gtk_font_selection_set_size (fontsel, new_size * PANGO_SCALE); } + + g_object_set_qdata (G_OBJECT (fontsel), font_selection_size_timeout_quark (), NULL); + + return FALSE; +} + +static void +gtk_font_selection_select_size (GtkTreeSelection *selection, + gpointer data) +{ + GtkFontSelection *fontsel = GTK_FONT_SELECTION (data); + + gtk_font_selection_schedule_selection (fontsel, + font_selection_size_timeout_quark (), + gtk_font_selection_real_select_size); } static void @@ -1450,6 +1580,8 @@ gtk_font_selection_select_font_desc (GtkFontSelection *fontsel, if (!new_family) return FALSE; + gtk_font_selection_set_immediate (fontsel, TRUE); + if (pfamily) *pfamily = new_family; else @@ -1500,6 +1632,8 @@ gtk_font_selection_select_font_desc (GtkFontSelection *fontsel, gtk_font_selection_set_size (fontsel, pango_font_description_get_size (new_desc)); + gtk_font_selection_set_immediate (fontsel, FALSE); + return TRUE; } From dda8c3764a2c25983199d05d26c9c1dc5663e025 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Thu, 14 May 2026 14:34:46 +0200 Subject: [PATCH 036/122] fontsel: use insert_with_values when populating lists Use gtk_list_store_insert_with_values() for font family, style and size list population to avoid separate append/set updates while preserving the same inserted values and row selection behavior. --- gtk/gtkfontsel.c | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/gtk/gtkfontsel.c b/gtk/gtkfontsel.c index 76927c797..46aa6c01a 100644 --- a/gtk/gtkfontsel.c +++ b/gtk/gtkfontsel.c @@ -878,11 +878,10 @@ gtk_font_selection_show_available_fonts (GtkFontSelection *fontsel) const gchar *name = pango_font_family_get_name (families[i]); GtkTreeIter iter; - gtk_list_store_append (model, &iter); - gtk_list_store_set (model, &iter, - FAMILY_COLUMN, families[i], - FAMILY_NAME_COLUMN, name, - -1); + gtk_list_store_insert_with_values (model, &iter, -1, + FAMILY_COLUMN, families[i], + FAMILY_NAME_COLUMN, name, + -1); if (i == 0 || !g_ascii_strcasecmp (name, "sans")) { @@ -979,11 +978,10 @@ gtk_font_selection_show_available_styles (GtkFontSelection *fontsel) GtkTreeIter iter; const gchar *str = pango_font_face_get_face_name (faces[i]); - gtk_list_store_append (model, &iter); - gtk_list_store_set (model, &iter, - FACE_COLUMN, faces[i], - FACE_NAME_COLUMN, _(str), - -1); + gtk_list_store_insert_with_values (model, &iter, -1, + FACE_COLUMN, faces[i], + FACE_NAME_COLUMN, _(str), + -1); if (i == 0) { @@ -1105,8 +1103,9 @@ gtk_font_selection_show_available_sizes (GtkFontSelection *fontsel, { GtkTreeIter iter; - gtk_list_store_append (model, &iter); - gtk_list_store_set (model, &iter, SIZE_COLUMN, font_sizes[i], -1); + gtk_list_store_insert_with_values (model, &iter, -1, + SIZE_COLUMN, font_sizes[i], + -1); if (font_sizes[i] * PANGO_SCALE == fontsel->size) set_cursor_to_iter (GTK_TREE_VIEW (fontsel->size_list), &iter); From c062dcb54d73dc3600389532e40eb3c2eb778e08 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Thu, 14 May 2026 15:00:25 +0200 Subject: [PATCH 037/122] updateiconcache: sort directory entries before scanning Based on the openSUSE gtk2-updateiconcache_sort.patch. Sort directory entries while building the icon cache so cache output is deterministic, but keep the implementation portable by using GLib arrays instead of scandir()/alphasort(). Also free the scanned directory path before returning. --- gtk/updateiconcache.c | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/gtk/updateiconcache.c b/gtk/updateiconcache.c index 4a3158e12..b46d97c04 100644 --- a/gtk/updateiconcache.c +++ b/gtk/updateiconcache.c @@ -608,6 +608,16 @@ replace_backslashes_with_slashes (gchar *path) path[i] = '/'; } +static gint +compare_string_pointers (gconstpointer a, + gconstpointer b) +{ + const gchar * const *str_a = a; + const gchar * const *str_b = b; + + return strcmp (*str_a, *str_b); +} + static GList * scan_directory (const gchar *base_path, const gchar *subdir, @@ -617,10 +627,12 @@ scan_directory (const gchar *base_path, { GHashTable *dir_hash; GDir *dir; + GPtrArray *names; const gchar *name; gchar *dir_path; gboolean dir_added = FALSE; guint dir_index = 0xffff; + guint i; dir_path = g_build_path ("/", base_path, subdir, NULL); @@ -628,12 +640,25 @@ scan_directory (const gchar *base_path, dir = g_dir_open (dir_path, 0, NULL); if (!dir) - return directories; + { + g_free (dir_path); + return directories; + } + + names = g_ptr_array_new_with_free_func (g_free); + + while ((name = g_dir_read_name (dir))) + g_ptr_array_add (names, g_strdup (name)); + + g_dir_close (dir); + + g_ptr_array_sort (names, compare_string_pointers); dir_hash = g_hash_table_new (g_str_hash, g_str_equal); - while ((name = g_dir_read_name (dir))) + for (i = 0; i < names->len; i++) { + name = g_ptr_array_index (names, i); gchar *path; gboolean retval; int flags = 0; @@ -712,7 +737,8 @@ scan_directory (const gchar *base_path, g_free (path); } - g_dir_close (dir); + g_ptr_array_free (names, TRUE); + g_free (dir_path); /* Move dir into the big file hash */ g_hash_table_foreach_remove (dir_hash, foreach_remove_func, files); From de9f5196faf5d9c09350d636eae272c955091d3c Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 15 May 2026 08:41:24 +0200 Subject: [PATCH 038/122] configure: add rpath-link flags for custom library prefixes When building with a custom library prefix, GNU ld may find a directly linked shared library through -L but fail to locate that library's indirect DT_NEEDED dependencies from the same prefix. Derive -Wl,-rpath-link entries from existing LDFLAGS -L paths on Linux. This improves configure tests for musl, cross-toolchain and custom-prefix builds without changing the pkg-config dependency set. --- configure.ac | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 29297423f..47377e76e 100644 --- a/configure.ac +++ b/configure.ac @@ -718,6 +718,48 @@ fi saved_cflags="$CFLAGS" saved_ldflags="$LDFLAGS" +# Help GNU ld find indirect shared-library dependencies from custom +# library prefixes. This is useful for musl/cross/custom-prefix builds +# where a directly linked .so is found through -L, but its DT_NEEDED +# libraries from the same prefix are not in the default linker search path. +case "$host_os" in + linux*) + GTK_RPATH_LINK_FLAGS= + gtk_next_is_libdir=no + + for gtk_ldflag in $LDFLAGS; do + if test "x$gtk_next_is_libdir" = xyes; then + gtk_libdir="$gtk_ldflag" + gtk_next_is_libdir=no + else + case $gtk_ldflag in + -L) + gtk_next_is_libdir=yes + continue + ;; + -L*) + gtk_libdir=`echo "$gtk_ldflag" | sed 's/^-L//'` + ;; + *) + continue + ;; + esac + fi + + case " $GTK_RPATH_LINK_FLAGS " in + *" -Wl,-rpath-link,$gtk_libdir "*) + ;; + *) + GTK_RPATH_LINK_FLAGS="$GTK_RPATH_LINK_FLAGS -Wl,-rpath-link,$gtk_libdir" + ;; + esac + done + + LDFLAGS="$LDFLAGS $GTK_RPATH_LINK_FLAGS" + ;; +esac + + # Checks for header files. # Checks for typedefs, structures, and compiler characteristics. @@ -1392,7 +1434,7 @@ CFLAGS="$CFLAGS $PANGO_CFLAGS" if $PKG_CONFIG --uninstalled $PANGO_PACKAGES; then : else - gtk_save_LIBS="$LIBS" + gtk_save_LIBS="$LIBS" LIBS="$PANGO_LIBS $LIBS" AC_LINK_IFELSE( [AC_LANG_CALL([], [pango_context_new])], From 9423346201c9fc250eb5de4c628247aa32bd6c9c Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 15 May 2026 09:39:48 +0200 Subject: [PATCH 039/122] gtk: include local module path fallback --- gtk/gtkmodules.c | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/gtk/gtkmodules.c b/gtk/gtkmodules.c index da5568ffa..fa7a6a50b 100644 --- a/gtk/gtkmodules.c +++ b/gtk/gtkmodules.c @@ -131,6 +131,7 @@ get_module_path (void) gchar *home_gtk_dir = NULL; gchar *module_path; gchar *default_dir; + gchar *local_dir = NULL; static gchar **result = NULL; if (result) @@ -146,22 +147,47 @@ get_module_path (void) if (exe_prefix) default_dir = g_build_filename (exe_prefix, "lib", "gtk-2.0", NULL); else - default_dir = g_build_filename (GTK_LIBDIR, "gtk-2.0", NULL); + { + default_dir = g_build_filename (GTK_LIBDIR, "gtk-2.0", NULL); - if (module_path_env && home_gtk_dir) +#ifndef G_OS_WIN32 + if (g_str_has_prefix (GTK_LIBDIR, "/usr/lib")) + { + const gchar *lib_subdir = GTK_LIBDIR + strlen ("/usr/"); + + if (lib_subdir[0] != '\0') + local_dir = g_build_filename ("/usr/local", lib_subdir, "gtk-2.0", NULL); + } +#endif + } + + if (module_path_env && home_gtk_dir && local_dir) module_path = g_build_path (G_SEARCHPATH_SEPARATOR_S, - module_path_env, home_gtk_dir, default_dir, NULL); + module_path_env, home_gtk_dir, local_dir, default_dir, NULL); + else if (module_path_env && home_gtk_dir) + module_path = g_build_path (G_SEARCHPATH_SEPARATOR_S, + module_path_env, home_gtk_dir, default_dir, NULL); + else if (module_path_env && local_dir) + module_path = g_build_path (G_SEARCHPATH_SEPARATOR_S, + module_path_env, local_dir, default_dir, NULL); else if (module_path_env) module_path = g_build_path (G_SEARCHPATH_SEPARATOR_S, - module_path_env, default_dir, NULL); + module_path_env, default_dir, NULL); + else if (home_gtk_dir && local_dir) + module_path = g_build_path (G_SEARCHPATH_SEPARATOR_S, + home_gtk_dir, local_dir, default_dir, NULL); else if (home_gtk_dir) module_path = g_build_path (G_SEARCHPATH_SEPARATOR_S, - home_gtk_dir, default_dir, NULL); + home_gtk_dir, default_dir, NULL); + else if (local_dir) + module_path = g_build_path (G_SEARCHPATH_SEPARATOR_S, + local_dir, default_dir, NULL); else module_path = g_build_path (G_SEARCHPATH_SEPARATOR_S, - default_dir, NULL); + default_dir, NULL); g_free (home_gtk_dir); + g_free (local_dir); g_free (default_dir); result = split_file_list (module_path); From dbe993a4dd92a260fa7c31f991a155c50b0e7319 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 15 May 2026 10:08:06 +0200 Subject: [PATCH 040/122] gtk: use returned text btree string buffer --- gtk/gtktextbtree.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gtk/gtktextbtree.c b/gtk/gtktextbtree.c index 2cd097800..ae3d57ea3 100644 --- a/gtk/gtktextbtree.c +++ b/gtk/gtktextbtree.c @@ -2439,8 +2439,7 @@ _gtk_text_btree_get_text (const GtkTextIter *start_orig, copy_segment (retval, include_hidden, include_nonchars, &iter, &end); - str = retval->str; - g_string_free (retval, FALSE); + str = g_string_free (retval, FALSE); return str; } From 9eb83c3501ebe0245d579f5190ade93e2967f46b Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 15 May 2026 10:15:55 +0200 Subject: [PATCH 041/122] gail: use component extents for image positions --- modules/other/gail/gailbutton.c | 2 +- modules/other/gail/gailimage.c | 2 +- modules/other/gail/gailimagecell.c | 2 +- modules/other/gail/gailpixmap.c | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/other/gail/gailbutton.c b/modules/other/gail/gailbutton.c index 6ecf9cd74..495efce1b 100644 --- a/modules/other/gail/gailbutton.c +++ b/modules/other/gail/gailbutton.c @@ -1021,7 +1021,7 @@ gail_button_get_image_position (AtkImage *image, if (button_image != NULL) { obj = gtk_widget_get_accessible (GTK_WIDGET (button_image)); - atk_component_get_position (ATK_COMPONENT (obj), x, y, coord_type); + atk_component_get_extents (ATK_COMPONENT (obj), x, y, NULL, NULL, coord_type); } else { diff --git a/modules/other/gail/gailimage.c b/modules/other/gail/gailimage.c index 0a33cd1ec..968b1d464 100644 --- a/modules/other/gail/gailimage.c +++ b/modules/other/gail/gailimage.c @@ -179,7 +179,7 @@ gail_image_get_image_position (AtkImage *image, gint *y, AtkCoordType coord_type) { - atk_component_get_position (ATK_COMPONENT (image), x, y, coord_type); + atk_component_get_extents (ATK_COMPONENT (image), x, y, NULL, NULL, coord_type); } static void diff --git a/modules/other/gail/gailimagecell.c b/modules/other/gail/gailimagecell.c index efcded036..5860b723f 100644 --- a/modules/other/gail/gailimagecell.c +++ b/modules/other/gail/gailimagecell.c @@ -150,7 +150,7 @@ gail_image_cell_get_image_position (AtkImage *image, gint *y, AtkCoordType coord_type) { - atk_component_get_position (ATK_COMPONENT (image), x, y, coord_type); + atk_component_get_extents (ATK_COMPONENT (image), x, y, NULL, NULL, coord_type); } static void diff --git a/modules/other/gail/gailpixmap.c b/modules/other/gail/gailpixmap.c index b8ab4b3da..4f44f93be 100644 --- a/modules/other/gail/gailpixmap.c +++ b/modules/other/gail/gailpixmap.c @@ -103,7 +103,7 @@ gail_pixmap_get_image_position (AtkImage *obj, gint *y, AtkCoordType coord_type) { - atk_component_get_position (ATK_COMPONENT (obj), x, y, coord_type); + atk_component_get_extents (ATK_COMPONENT (obj), x, y, NULL, NULL, coord_type); } static void From 839ed23770132e5ee342f3b2e4b41a09c7ac0bb4 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 15 May 2026 10:33:12 +0200 Subject: [PATCH 042/122] gtk: use component extents for accessible positions --- gtk/gtkiconview.c | 4 ++-- modules/other/gail/gailnotebookpage.c | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gtk/gtkiconview.c b/gtk/gtkiconview.c index 618f7132d..5b58cc9c8 100644 --- a/gtk/gtkiconview.c +++ b/gtk/gtkiconview.c @@ -7917,7 +7917,7 @@ gtk_icon_view_item_accessible_image_get_image_position (AtkImage *image, if (atk_state_set_contains_state (item->state_set, ATK_STATE_DEFUNCT)) return; - atk_component_get_position (ATK_COMPONENT (image), x, y, coord_type); + atk_component_get_extents (ATK_COMPONENT (image), x, y, NULL, NULL, coord_type); if (get_pixbuf_box (GTK_ICON_VIEW (item->widget), item->item, &box)) { @@ -8601,7 +8601,7 @@ gtk_icon_view_item_accessible_get_extents (AtkComponent *component, if (gtk_icon_view_item_accessible_is_showing (item)) { parent_obj = gtk_widget_get_accessible (item->widget); - atk_component_get_position (ATK_COMPONENT (parent_obj), &l_x, &l_y, coord_type); + atk_component_get_extents (ATK_COMPONENT (parent_obj), &l_x, &l_y, NULL, NULL, coord_type); *x = l_x + item->item->x; *y = l_y + item->item->y; } diff --git a/modules/other/gail/gailnotebookpage.c b/modules/other/gail/gailnotebookpage.c index 444ddac0e..cd4e18d7d 100644 --- a/modules/other/gail/gailnotebookpage.c +++ b/modules/other/gail/gailnotebookpage.c @@ -467,7 +467,7 @@ gail_notebook_page_get_extents (AtkComponent *component, child = atk_object_ref_accessible_child (ATK_OBJECT (component), 0); gail_return_if_fail (child); - atk_component_get_position (ATK_COMPONENT (child), x, y, coord_type); + atk_component_get_extents (ATK_COMPONENT (child), x, y, NULL, NULL, coord_type); g_object_unref (child); } else From 10e7514987985276c6ccf960c8039291d9e5718a Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 15 May 2026 10:41:08 +0200 Subject: [PATCH 043/122] gail: use component extents in tests --- modules/other/gail/tests/ferret.c | 7 ++++--- modules/other/gail/tests/testcomponent.c | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/modules/other/gail/tests/ferret.c b/modules/other/gail/tests/ferret.c index 711852af0..e4662cd9b 100644 --- a/modules/other/gail/tests/ferret.c +++ b/modules/other/gail/tests/ferret.c @@ -1512,9 +1512,10 @@ _button_watcher (GSignalInvocationHint *ihint, event = (GdkEventButton *) gp; aobject = gtk_widget_get_accessible (widget); aobject_x = aobject_y = 0; - atk_component_get_position (ATK_COMPONENT (aobject), - &aobject_x, &aobject_y, - ATK_XY_WINDOW); + atk_component_get_extents (ATK_COMPONENT (aobject), + &aobject_x, &aobject_y, + NULL, NULL, + ATK_XY_WINDOW); x = aobject_x + (gint) event->x; y = aobject_y + (gint) event->y; child = atk_component_ref_accessible_at_point (ATK_COMPONENT (aobject), diff --git a/modules/other/gail/tests/testcomponent.c b/modules/other/gail/tests/testcomponent.c index a28eccebd..df2d2c925 100644 --- a/modules/other/gail/tests/testcomponent.c +++ b/modules/other/gail/tests/testcomponent.c @@ -16,11 +16,11 @@ static void _check_position (AtkObject *obj) return; atk_component_get_extents (ATK_COMPONENT(obj), &x, &y, &width, &height, ATK_XY_SCREEN); - atk_component_get_position (ATK_COMPONENT(obj), &x1, &y1, ATK_XY_SCREEN ); + atk_component_get_extents (ATK_COMPONENT(obj), &x1, &y1, NULL, NULL, ATK_XY_SCREEN); atk_component_get_size (ATK_COMPONENT(obj), &width1, &height1); if ((x1 != x) || (y1 != y)) { - g_print ("atk_component_get_extents and atk_get_position give different" + g_print ("Repeated atk_component_get_extents calls give different" " values: %d,%d %d,%d\n", x, y, x1, y1); } if ((width1 != width) || (height1 != height)) @@ -29,7 +29,7 @@ static void _check_position (AtkObject *obj) " values: %d,%d %d,%d\n", width, height, width1, height1); } - atk_component_get_position (ATK_COMPONENT(obj), &x1, &y1, ATK_XY_SCREEN); + atk_component_get_extents (ATK_COMPONENT(obj), &x1, &y1, NULL, NULL, ATK_XY_SCREEN); g_print ("Object Type: %s\n", g_type_name (G_OBJECT_TYPE (obj))); g_print ("Object at %d, %d on screen\n", x1, y1); g_print ("Object at %d, %d, size: %d, %d\n", x, y, width, height); From 3591e299130da69fd6f15ef64cf7c6f7deb4e15c Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 15 May 2026 11:07:09 +0200 Subject: [PATCH 044/122] gail: implement modern paned value interface --- modules/other/gail/gailpaned.c | 66 ++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/modules/other/gail/gailpaned.c b/modules/other/gail/gailpaned.c index 5ac2d2340..c224d8e77 100644 --- a/modules/other/gail/gailpaned.c +++ b/modules/other/gail/gailpaned.c @@ -43,6 +43,14 @@ static void gail_paned_get_minimum_value (AtkValue *obj, GValue *value); static gboolean gail_paned_set_current_value (AtkValue *obj, const GValue *value); +#if ATK_CHECK_VERSION (2, 12, 0) +static void gail_paned_get_value_and_text (AtkValue *obj, + gdouble *value, + gchar **text); +static AtkRange *gail_paned_get_range (AtkValue *obj); +static void gail_paned_set_value (AtkValue *obj, + gdouble new_value); +#endif G_DEFINE_TYPE_WITH_CODE (GailPaned, gail_paned, GAIL_TYPE_CONTAINER, G_IMPLEMENT_INTERFACE (ATK_TYPE_VALUE, atk_value_interface_init)) @@ -112,6 +120,11 @@ atk_value_interface_init (AtkValueIface *iface) iface->get_maximum_value = gail_paned_get_maximum_value; iface->get_minimum_value = gail_paned_get_minimum_value; iface->set_current_value = gail_paned_set_current_value; +#if ATK_CHECK_VERSION (2, 12, 0) + iface->get_value_and_text = gail_paned_get_value_and_text; + iface->get_range = gail_paned_get_range; + iface->set_value = gail_paned_set_value; +#endif } static void @@ -197,3 +210,56 @@ gail_paned_set_current_value (AtkValue *obj, else return FALSE; } + +#if ATK_CHECK_VERSION (2, 12, 0) +static void +gail_paned_get_value_and_text (AtkValue *obj, + gdouble *value, + gchar **text) +{ + GtkWidget *widget; + + if (text) + *text = NULL; + + if (!value) + return; + + widget = GTK_ACCESSIBLE (obj)->widget; + if (widget == NULL) + { + *value = 0.0; + return; + } + + *value = gtk_paned_get_position (GTK_PANED (widget)); +} + +static AtkRange * +gail_paned_get_range (AtkValue *obj) +{ + GtkWidget *widget; + GtkPaned *paned; + + widget = GTK_ACCESSIBLE (obj)->widget; + if (widget == NULL) + return NULL; + + paned = GTK_PANED (widget); + + return atk_range_new (paned->min_position, paned->max_position, NULL); +} + +static void +gail_paned_set_value (AtkValue *obj, + gdouble new_value) +{ + GtkWidget *widget; + + widget = GTK_ACCESSIBLE (obj)->widget; + if (widget == NULL) + return; + + gtk_paned_set_position (GTK_PANED (widget), (gint) new_value); +} +#endif From 94f030875f80812bcf010bb94ad305f61dae2eb0 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 15 May 2026 11:09:32 +0200 Subject: [PATCH 045/122] gail: use modern value API in paned test --- modules/other/gail/tests/testpaned.c | 112 ++++++++++++++++++++++----- 1 file changed, 92 insertions(+), 20 deletions(-) diff --git a/modules/other/gail/tests/testpaned.c b/modules/other/gail/tests/testpaned.c index 300031955..57fdcd3ab 100644 --- a/modules/other/gail/tests/testpaned.c +++ b/modules/other/gail/tests/testpaned.c @@ -12,6 +12,91 @@ static void _property_change_handler (AtkObject *obj, #define NUM_VALID_ROLES 1 static gint last_position; +static gint +test_value_get_current_int (AtkValue *obj) +{ +#if ATK_CHECK_VERSION (2, 12, 0) + gdouble value = 0.0; + + atk_value_get_value_and_text (obj, &value, NULL); + + return (gint) value; +#else + GValue value = G_VALUE_INIT; + + atk_value_get_current_value (obj, &value); + g_return_val_if_fail (G_VALUE_HOLDS_INT (&value), 0); + + return g_value_get_int (&value); +#endif +} + +static gint +test_value_get_minimum_int (AtkValue *obj) +{ +#if ATK_CHECK_VERSION (2, 12, 0) + AtkRange *range; + gint value = 0; + + range = atk_value_get_range (obj); + if (range) + { + value = (gint) atk_range_get_lower_limit (range); + atk_range_free (range); + } + + return value; +#else + GValue value = G_VALUE_INIT; + + atk_value_get_minimum_value (obj, &value); + g_return_val_if_fail (G_VALUE_HOLDS_INT (&value), 0); + + return g_value_get_int (&value); +#endif +} + +static gint +test_value_get_maximum_int (AtkValue *obj) +{ +#if ATK_CHECK_VERSION (2, 12, 0) + AtkRange *range; + gint value = 0; + + range = atk_value_get_range (obj); + if (range) + { + value = (gint) atk_range_get_upper_limit (range); + atk_range_free (range); + } + + return value; +#else + GValue value = G_VALUE_INIT; + + atk_value_get_maximum_value (obj, &value); + g_return_val_if_fail (G_VALUE_HOLDS_INT (&value), 0); + + return g_value_get_int (&value); +#endif +} + +static void +test_value_set_current_int (AtkValue *obj, + gint value) +{ +#if ATK_CHECK_VERSION (2, 12, 0) + atk_value_set_value (obj, value); +#else + GValue gvalue = G_VALUE_INIT; + + g_value_init (&gvalue, G_TYPE_INT); + g_value_set_int (&gvalue, value); + atk_value_set_current_value (obj, &gvalue); + g_value_unset (&gvalue); +#endif +} + static void _property_change_handler (AtkObject *obj, AtkPropertyValues *values) { @@ -26,24 +111,17 @@ static void _property_change_handler (AtkObject *obj, values->property_name ? values->property_name: "NULL"); if (strcmp (values->property_name, "accessible-value") == 0) { - GValue *value, val; int position; - value = &val; - memset (value, 0, sizeof (GValue)); - atk_value_get_current_value (ATK_VALUE (obj), value); - g_return_if_fail (G_VALUE_HOLDS_INT (value)); - position = g_value_get_int (value); + position = test_value_get_current_int (ATK_VALUE (obj)); g_print ("Position is %d previous position was %d\n", position, last_position); last_position = position; - atk_value_get_minimum_value (ATK_VALUE (obj), value); - g_return_if_fail (G_VALUE_HOLDS_INT (value)); - position = g_value_get_int (value); + + position = test_value_get_minimum_int (ATK_VALUE (obj)); g_print ("Minimum Value is %d\n", position); - atk_value_get_maximum_value (ATK_VALUE (obj), value); - g_return_if_fail (G_VALUE_HOLDS_INT (value)); - position = g_value_get_int (value); + + position = test_value_get_maximum_int (ATK_VALUE (obj)); g_print ("Maximum Value is %d\n", position); } } @@ -56,19 +134,13 @@ static gint _test_paned (gpointer data) if (role == ATK_ROLE_SPLIT_PANE) { - GValue *value, val; int position; - value = &val; - memset (value, 0, sizeof (GValue)); - atk_value_get_current_value (ATK_VALUE (obj), value); - g_return_val_if_fail (G_VALUE_HOLDS_INT (value), FALSE); - position = g_value_get_int (value); + position = test_value_get_current_int (ATK_VALUE (obj)); g_print ("Position is : %d\n", position); last_position = position; position *= 2; - g_value_set_int (value, position); - atk_value_set_current_value (ATK_VALUE (obj), value); + test_value_set_current_int (ATK_VALUE (obj), position); times++; } if (times < 4) From a0b2f2491f83831253971e687d0b6b985271a7ff Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 15 May 2026 12:04:10 +0200 Subject: [PATCH 046/122] gail: unset temporary values in paned test --- modules/other/gail/tests/testpaned.c | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/modules/other/gail/tests/testpaned.c b/modules/other/gail/tests/testpaned.c index 57fdcd3ab..781750a86 100644 --- a/modules/other/gail/tests/testpaned.c +++ b/modules/other/gail/tests/testpaned.c @@ -23,11 +23,15 @@ test_value_get_current_int (AtkValue *obj) return (gint) value; #else GValue value = G_VALUE_INIT; + gint result; atk_value_get_current_value (obj, &value); g_return_val_if_fail (G_VALUE_HOLDS_INT (&value), 0); - return g_value_get_int (&value); + result = g_value_get_int (&value); + g_value_unset (&value); + + return result; #endif } @@ -48,11 +52,15 @@ test_value_get_minimum_int (AtkValue *obj) return value; #else GValue value = G_VALUE_INIT; + gint result; atk_value_get_minimum_value (obj, &value); g_return_val_if_fail (G_VALUE_HOLDS_INT (&value), 0); - return g_value_get_int (&value); + result = g_value_get_int (&value); + g_value_unset (&value); + + return result; #endif } @@ -73,11 +81,15 @@ test_value_get_maximum_int (AtkValue *obj) return value; #else GValue value = G_VALUE_INIT; + gint result; atk_value_get_maximum_value (obj, &value); g_return_val_if_fail (G_VALUE_HOLDS_INT (&value), 0); - return g_value_get_int (&value); + result = g_value_get_int (&value); + g_value_unset (&value); + + return result; #endif } From 83405a2c8bd25aee82c4d27684cc6d92e9400ebe Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 15 May 2026 13:01:13 +0200 Subject: [PATCH 047/122] gail: clarify paned value conversion --- modules/other/gail/gailpaned.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/other/gail/gailpaned.c b/modules/other/gail/gailpaned.c index c224d8e77..4b106e205 100644 --- a/modules/other/gail/gailpaned.c +++ b/modules/other/gail/gailpaned.c @@ -252,14 +252,16 @@ gail_paned_get_range (AtkValue *obj) static void gail_paned_set_value (AtkValue *obj, - gdouble new_value) + gdouble value) { GtkWidget *widget; + gint new_value; widget = GTK_ACCESSIBLE (obj)->widget; if (widget == NULL) return; - gtk_paned_set_position (GTK_PANED (widget), (gint) new_value); + new_value = (gint) value; + gtk_paned_set_position (GTK_PANED (widget), new_value); } #endif From fedfe60e0e9d375fab3a1f42a513a4a111600d0a Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 15 May 2026 13:11:07 +0200 Subject: [PATCH 048/122] treewide: fix whitespace in changed files --- README.md | 2 +- demos/gtk-demo/main.c | 4 ++-- docs/reference/gdk/Makefile.am | 1 - docs/reference/gtk/Makefile.am | 1 - docs/reference/libgail-util/Makefile.am | 1 - gtk/gtkclipboard-quartz.c | 16 ++++++++-------- gtk/gtkclipboard.c | 18 +++++++++--------- gtk/gtkfilesystemmodel.h | 2 +- gtk/gtkimmodule.c | 6 +++--- gtk/gtkrecentchooser.h | 22 +++++++++++----------- tests/testtreeview.c | 6 +++--- 11 files changed, 38 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 0570a98f7..dcc2d2396 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ emerge --ask --oneshot media-libs/freetype x11-libs/pango ``` ## How to Contribute -Join the main discussion here: +Join the main discussion here: https://devuanusers.com/thread-gtk2-revival-thread--80 Test apps, report issues, submit patches! diff --git a/demos/gtk-demo/main.c b/demos/gtk-demo/main.c index a26e8adcf..529e3ea37 100644 --- a/demos/gtk-demo/main.c +++ b/demos/gtk-demo/main.c @@ -992,11 +992,11 @@ main (int argc, char **argv) NULL); gtk_text_buffer_create_tag (source_buffer, "preprocessor", "style", PANGO_STYLE_OBLIQUE, - "foreground", "burlywood4", + "foreground", "burlywood4", NULL); gtk_text_buffer_create_tag (source_buffer, "function", "weight", PANGO_WEIGHT_BOLD, - "foreground", "DarkGoldenrod4", + "foreground", "DarkGoldenrod4", NULL); g_object_unref (source_buffer); diff --git a/docs/reference/gdk/Makefile.am b/docs/reference/gdk/Makefile.am index 43a67508c..fe5d9742a 100644 --- a/docs/reference/gdk/Makefile.am +++ b/docs/reference/gdk/Makefile.am @@ -159,4 +159,3 @@ EXTRA_DIST += version.xml.in # working when the top-level test target descends into the documentation # reference directories. test: - diff --git a/docs/reference/gtk/Makefile.am b/docs/reference/gtk/Makefile.am index fcd7e9173..05500507e 100644 --- a/docs/reference/gtk/Makefile.am +++ b/docs/reference/gtk/Makefile.am @@ -391,4 +391,3 @@ dist-hook-local: $(BUILT_EXTRA_DIST) # working when the top-level test target descends into the documentation # reference directories. test: - diff --git a/docs/reference/libgail-util/Makefile.am b/docs/reference/libgail-util/Makefile.am index b57a2e788..887c4a7b4 100644 --- a/docs/reference/libgail-util/Makefile.am +++ b/docs/reference/libgail-util/Makefile.am @@ -39,4 +39,3 @@ include $(top_srcdir)/gtk-doc.make # working when the top-level test target descends into the documentation # reference directories. test: - diff --git a/gtk/gtkclipboard-quartz.c b/gtk/gtkclipboard-quartz.c index b8bc78fb0..f6a1f5aa5 100644 --- a/gtk/gtkclipboard-quartz.c +++ b/gtk/gtkclipboard-quartz.c @@ -884,11 +884,11 @@ gtk_clipboard_wait_for_targets (GtkClipboard *clipboard, clipboard->n_cached_targets != -1) { if (n_targets) - *n_targets = clipboard->n_cached_targets; + *n_targets = clipboard->n_cached_targets; if (targets) - *targets = _gtk_memdup2 (clipboard->cached_targets, - clipboard->n_cached_targets * sizeof (GdkAtom)); + *targets = _gtk_memdup2 (clipboard->cached_targets, + clipboard->n_cached_targets * sizeof (GdkAtom)); return TRUE; } @@ -909,11 +909,11 @@ gtk_clipboard_wait_for_targets (GtkClipboard *clipboard, result = gtk_selection_data_get_targets (data, &tmp_targets, &tmp_n_targets); if (gdk_display_supports_selection_notification (gtk_clipboard_get_display (clipboard))) - { - clipboard->n_cached_targets = tmp_n_targets; - clipboard->cached_targets = _gtk_memdup2 (tmp_targets, - tmp_n_targets * sizeof (GdkAtom)); - } + { + clipboard->n_cached_targets = tmp_n_targets; + clipboard->cached_targets = _gtk_memdup2 (tmp_targets, + tmp_n_targets * sizeof (GdkAtom)); + } if (n_targets) *n_targets = tmp_n_targets; diff --git a/gtk/gtkclipboard.c b/gtk/gtkclipboard.c index 132d21f62..e9e913145 100644 --- a/gtk/gtkclipboard.c +++ b/gtk/gtkclipboard.c @@ -548,7 +548,7 @@ gtk_clipboard_set_contents (GtkClipboard *clipboard, clipboard_add_owner_notify (clipboard); } - } + } clipboard->get_func = get_func; clipboard->clear_func = clear_func; @@ -1758,11 +1758,11 @@ gtk_clipboard_wait_for_targets (GtkClipboard *clipboard, clipboard->n_cached_targets != -1) { if (n_targets) - *n_targets = clipboard->n_cached_targets; + *n_targets = clipboard->n_cached_targets; if (targets) - *targets = _gtk_memdup2 (clipboard->cached_targets, - clipboard->n_cached_targets * sizeof (GdkAtom)); + *targets = _gtk_memdup2 (clipboard->cached_targets, + clipboard->n_cached_targets * sizeof (GdkAtom)); return TRUE; } @@ -1783,11 +1783,11 @@ gtk_clipboard_wait_for_targets (GtkClipboard *clipboard, result = gtk_selection_data_get_targets (data, &tmp_targets, &tmp_n_targets); if (gdk_display_supports_selection_notification (gtk_clipboard_get_display (clipboard))) - { - clipboard->n_cached_targets = tmp_n_targets; - clipboard->cached_targets = _gtk_memdup2 (tmp_targets, - tmp_n_targets * sizeof (GdkAtom)); - } + { + clipboard->n_cached_targets = tmp_n_targets; + clipboard->cached_targets = _gtk_memdup2 (tmp_targets, + tmp_n_targets * sizeof (GdkAtom)); + } if (n_targets) *n_targets = tmp_n_targets; diff --git a/gtk/gtkfilesystemmodel.h b/gtk/gtkfilesystemmodel.h index 4bdda0c23..57853a77c 100644 --- a/gtk/gtkfilesystemmodel.h +++ b/gtk/gtkfilesystemmodel.h @@ -92,7 +92,7 @@ void _gtk_file_system_model_set_filter (GtkFileSystemModel GtkFileFilter *filter); void _gtk_file_system_model_add_editable (GtkFileSystemModel *model, - GtkTreeIter *iter); + GtkTreeIter *iter); void _gtk_file_system_model_remove_editable (GtkFileSystemModel *model); G_END_DECLS diff --git a/gtk/gtkimmodule.c b/gtk/gtkimmodule.c index 6025a319e..0b6d5ce4a 100644 --- a/gtk/gtkimmodule.c +++ b/gtk/gtkimmodule.c @@ -79,7 +79,7 @@ struct _GtkIMModule GModule *library; void (*list) (const GtkIMContextInfo ***contexts, - guint *n_contexts); + guint *n_contexts); void (*init) (GTypeModule *module); void (*exit) (void); GtkIMContext *(*create) (const gchar *context_id); @@ -827,7 +827,7 @@ lookup_immodule (gchar **immodules_list) gboolean found; gchar *context_id; found = g_hash_table_lookup_extended (contexts_hash, *immodules_list, - (gpointer *) &context_id, NULL); + (gpointer *) &context_id, NULL); if (found) return context_id; } @@ -886,7 +886,7 @@ _gtk_im_module_get_default_context_id (GdkWindow *client_window) g_strfreev(immodules); g_free (tmp); - if (context_id) + if (context_id) return context_id; } } diff --git a/gtk/gtkrecentchooser.h b/gtk/gtkrecentchooser.h index 77f01aa79..c5d9732f8 100644 --- a/gtk/gtkrecentchooser.h +++ b/gtk/gtkrecentchooser.h @@ -86,12 +86,12 @@ struct _GtkRecentChooserIface * Methods */ gboolean (* set_current_uri) (GtkRecentChooser *chooser, - const gchar *uri, - GError **error); + const gchar *uri, + GError **error); gchar * (* get_current_uri) (GtkRecentChooser *chooser); gboolean (* select_uri) (GtkRecentChooser *chooser, - const gchar *uri, - GError **error); + const gchar *uri, + GError **error); void (* unselect_uri) (GtkRecentChooser *chooser, const gchar *uri); void (* select_all) (GtkRecentChooser *chooser); @@ -99,14 +99,14 @@ struct _GtkRecentChooserIface GList * (* get_items) (GtkRecentChooser *chooser); GtkRecentManager *(* get_recent_manager) (GtkRecentChooser *chooser); void (* add_filter) (GtkRecentChooser *chooser, - GtkRecentFilter *filter); + GtkRecentFilter *filter); void (* remove_filter) (GtkRecentChooser *chooser, - GtkRecentFilter *filter); + GtkRecentFilter *filter); GSList * (* list_filters) (GtkRecentChooser *chooser); void (* set_sort_func) (GtkRecentChooser *chooser, - GtkRecentSortFunc sort_func, - gpointer sort_data, - GDestroyNotify data_destroy); + GtkRecentSortFunc sort_func, + gpointer sort_data, + GDestroyNotify data_destroy); /* * Signals @@ -176,8 +176,8 @@ gchar ** gtk_recent_chooser_get_uris (GtkRecentChooser *chooser, /* * Filters */ -void gtk_recent_chooser_add_filter (GtkRecentChooser *chooser, - GtkRecentFilter *filter); +void gtk_recent_chooser_add_filter (GtkRecentChooser *chooser, + GtkRecentFilter *filter); void gtk_recent_chooser_remove_filter (GtkRecentChooser *chooser, GtkRecentFilter *filter); GSList * gtk_recent_chooser_list_filters (GtkRecentChooser *chooser); diff --git a/tests/testtreeview.c b/tests/testtreeview.c index 12c0d3652..7ba8ba39f 100644 --- a/tests/testtreeview.c +++ b/tests/testtreeview.c @@ -628,9 +628,9 @@ create_tree_model (void) gint i; GType *t; volatile GType dummy G_GNUC_UNUSED; /* G_GNUC_CONST makes the optimizer remove - * get_type calls if you don't do something - * like this - */ + * get_type calls if you don't do something + * like this + */ /* Make the tree more interesting */ dummy = gtk_scrolled_window_get_type (); From f5bafd1b8ee5a7d3c76c4055652f72ec2e1747dd Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 15 May 2026 15:09:45 +0200 Subject: [PATCH 049/122] gtk: reject unquoted strings in immodule parser --- gtk/gtkimmodule.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gtk/gtkimmodule.c b/gtk/gtkimmodule.c index 0b6d5ce4a..35b2d6e5c 100644 --- a/gtk/gtkimmodule.c +++ b/gtk/gtkimmodule.c @@ -358,6 +358,8 @@ gtk_im_module_scan_string (const char **pos, g_free (tmp); g_free (tmp2); } + else + return FALSE; q++; *pos = q; From 76a7700091775f3ca824fce6a0e3996681d3be96 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 15 May 2026 15:21:02 +0200 Subject: [PATCH 050/122] gtk: check win32 stdio lock fallbacks --- configure.ac | 2 +- gtk/gtkimmodule.c | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/configure.ac b/configure.ac index 800d8559a..a8c27ec76 100644 --- a/configure.ac +++ b/configure.ac @@ -469,7 +469,7 @@ if test "x$enable_rebuilds" = "xyes" && \ fi AC_SUBST(REBUILD) -AC_CHECK_FUNCS(lstat mkstemp flockfile getc_unlocked) +AC_CHECK_FUNCS(lstat mkstemp flockfile getc_unlocked _lock_file _unlock_file _getc_nolock) AC_CHECK_FUNCS(localtime_r) # _NL_TIME_FIRST_WEEKDAY is an enum and not a define diff --git a/gtk/gtkimmodule.c b/gtk/gtkimmodule.c index 35b2d6e5c..46b155901 100644 --- a/gtk/gtkimmodule.c +++ b/gtk/gtkimmodule.c @@ -316,14 +316,17 @@ add_builtin_module (const gchar *module_name, * Keep these helpers local to avoid depending on deprecated Pango parser * utilities while preserving the old gtk.immodules parser semantics. */ -#if !defined(HAVE_FLOCKFILE) && !defined(HAVE__LOCK_FILE) -#define flockfile(f) (void)1 -#define funlockfile(f) (void)1 -#define getc_unlocked(f) getc(f) -#elif !defined(HAVE_FLOCKFILE) && defined(HAVE__LOCK_FILE) +#if !defined(HAVE_FLOCKFILE) && \ + defined(HAVE__LOCK_FILE) && \ + defined(HAVE__UNLOCK_FILE) && \ + defined(HAVE__GETC_NOLOCK) #define flockfile(f) _lock_file(f) #define funlockfile(f) _unlock_file(f) #define getc_unlocked(f) _getc_nolock(f) +#elif !defined(HAVE_FLOCKFILE) +#define flockfile(f) (void)1 +#define funlockfile(f) (void)1 +#define getc_unlocked(f) getc(f) #endif static gboolean From 55d5543eedc56a4cc2ab125ec5e85db93477cf1a Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 15 May 2026 15:34:32 +0200 Subject: [PATCH 051/122] gtk: limit direct module paths to GTK_PATH --- gtk/gtkmodules.c | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/gtk/gtkmodules.c b/gtk/gtkmodules.c index fa7a6a50b..de5c5a002 100644 --- a/gtk/gtkmodules.c +++ b/gtk/gtkmodules.c @@ -51,6 +51,8 @@ static gboolean default_display_opened = FALSE; static gint gtk_argc = 0; static gchar **gtk_argv = NULL; +static gint module_path_env_dirs = 0; + static gchar ** split_file_list (const gchar *str) { @@ -191,6 +193,23 @@ get_module_path (void) g_free (default_dir); result = split_file_list (module_path); + + if (module_path_env) + { + gchar **env_paths; + gchar **path; + + env_paths = split_file_list (module_path_env); + + module_path_env_dirs = 0; + for (path = env_paths; *path; path++) + module_path_env_dirs++; + + g_strfreev (env_paths); + } + else + module_path_env_dirs = 0; + g_free (module_path); return result; @@ -221,6 +240,9 @@ _gtk_get_module_path (const gchar *type) for (path = get_module_path (); *path; path++) { gint use_version, use_host; + gboolean path_from_env; + + path_from_env = (path - paths) < module_path_env_dirs; for (use_version = TRUE; use_version >= FALSE; use_version--) for (use_host = TRUE; use_host >= FALSE; use_host--) @@ -245,10 +267,11 @@ _gtk_get_module_path (const gchar *type) * live directly in .../.libs directories, for example print backends, * theme engines or input modules. * - * Keep this limited to absolute paths so the removed fallback that could - * load modules from the current working directory is not reintroduced. + * Keep this limited to absolute GTK_PATH entries so the removed fallback + * that could load modules from the current working directory is not + * reintroduced for built-in default paths. */ - if (g_path_is_absolute (*path)) + if (path_from_env && g_path_is_absolute (*path)) result[count++] = g_strdup (*path); } From e6873c2cd5644d30e0389c32a15b0cb19b4d91a5 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 15 May 2026 15:46:15 +0200 Subject: [PATCH 052/122] gail: avoid formatting string attribute values --- modules/other/gail/libgail-util/gailmisc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/other/gail/libgail-util/gailmisc.c b/modules/other/gail/libgail-util/gailmisc.c index 9dc143d07..8213c716a 100644 --- a/modules/other/gail/libgail-util/gailmisc.c +++ b/modules/other/gail/libgail-util/gailmisc.c @@ -251,7 +251,7 @@ gail_misc_layout_get_run_attributes (AtkAttributeSet *attrib_set, if ((pango_string = (PangoAttrString*) pango_attr_iterator_get (iter, PANGO_ATTR_FAMILY)) != NULL) { - value = g_strdup_printf("%s", pango_string->value); + value = g_strdup (pango_string->value); attrib_set = gail_misc_add_attribute (attrib_set, ATK_TEXT_ATTR_FAMILY_NAME, value); From 095225e496a04ed58a373cc568169401deb38f3d Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 15 May 2026 15:48:44 +0200 Subject: [PATCH 053/122] gtk: preserve strftime calendar names --- gtk/gtkcalendar.c | 36 ++++++++++-------------------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/gtk/gtkcalendar.c b/gtk/gtkcalendar.c index 19dc0196f..f50da2493 100644 --- a/gtk/gtkcalendar.c +++ b/gtk/gtkcalendar.c @@ -688,8 +688,7 @@ gtk_calendar_init (GtkCalendar *calendar) #else static const char *month_format = NULL; char buffer[255]; - GDateTime *tmp_dt; - gchar *tmp_str; + time_t tmp_time; #endif GtkCalendarPrivate *priv; gchar *year_before; @@ -710,15 +709,9 @@ gtk_calendar_init (GtkCalendar *calendar) for (i=0; i<7; i++) { #ifndef G_OS_WIN32 - tmp_dt = g_date_time_new_utc (2001, 1, i + 1, 12, 0, 0.0); - if (tmp_dt) - { - tmp_str = g_date_time_format (tmp_dt, "%a"); - default_abbreviated_dayname[i] = tmp_str ? tmp_str : g_strdup (""); - g_date_time_unref (tmp_dt); - } - else - default_abbreviated_dayname[i] = g_strdup (""); + tmp_time = (i + 3) * 86400; + strftime (buffer, sizeof (buffer), "%a", gmtime (&tmp_time)); + default_abbreviated_dayname[i] = g_locale_to_utf8 (buffer, -1, NULL, NULL, NULL); #else if (!GetLocaleInfoW (GetThreadLocale (), LOCALE_SABBREVDAYNAME1 + (i+6)%7, wbuffer, G_N_ELEMENTS (wbuffer))) @@ -732,30 +725,21 @@ gtk_calendar_init (GtkCalendar *calendar) for (i=0; i<12; i++) { #ifndef G_OS_WIN32 - tmp_dt = g_date_time_new_utc (2001, i + 1, 1, 12, 0, 0.0); + tmp_time = i * 2764800; if (G_UNLIKELY (month_format == NULL)) { buffer[0] = '\0'; month_format = "%OB"; - tmp_str = tmp_dt ? g_date_time_format (tmp_dt, month_format) : NULL; - if (tmp_str) - { - g_strlcpy (buffer, tmp_str, sizeof (buffer)); - g_free (tmp_str); - } + strftime (buffer, sizeof (buffer), month_format, gmtime (&tmp_time)); + /* "%OB" is not supported in Linux with glibc < 2.27 */ if (!strcmp (buffer, "%OB") || !strcmp (buffer, "OB") || !strcmp (buffer, "")) month_format = "%B"; } - - if (tmp_dt) - { - tmp_str = g_date_time_format (tmp_dt, month_format); - default_monthname[i] = tmp_str ? tmp_str : g_strdup (""); - g_date_time_unref (tmp_dt); - } else - default_monthname[i] = g_strdup (""); + strftime (buffer, sizeof (buffer), month_format, gmtime (&tmp_time)); + + default_monthname[i] = g_locale_to_utf8 (buffer, -1, NULL, NULL, NULL); #else if (!GetLocaleInfoW (GetThreadLocale (), LOCALE_SMONTHNAME1 + i, wbuffer, G_N_ELEMENTS (wbuffer))) From d0628c685683da23b101dfc8fa7634590e780727 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 15 May 2026 15:55:10 +0200 Subject: [PATCH 054/122] gtk: avoid unused immodule query symbols --- gtk/queryimmodules.c | 6 ------ 1 file changed, 6 deletions(-) diff --git a/gtk/queryimmodules.c b/gtk/queryimmodules.c index d20637e10..e118b9c19 100644 --- a/gtk/queryimmodules.c +++ b/gtk/queryimmodules.c @@ -159,9 +159,6 @@ query_module (const char *dir, const char *name, GString *contents) { void (*list) (const GtkIMContextInfo ***contexts, guint *n_contexts); - G_GNUC_UNUSED void (*init) (GTypeModule *type_module); - G_GNUC_UNUSED void (*exit) (void); - G_GNUC_UNUSED GtkIMContext *(*create) (const gchar *context_id); gpointer list_ptr; gpointer init_ptr; @@ -196,9 +193,6 @@ query_module (const char *dir, const char *name, GString *contents) int i; list = list_ptr; - init = init_ptr; - exit = exit_ptr; - create = create_ptr; print_escaped (contents, path); g_string_append_c (contents, '\n'); From 7c9df99170f58dfea3151c3a4f4ea066044c7cfe Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 15 May 2026 16:11:57 +0200 Subject: [PATCH 055/122] gtk: reuse cached module path --- gtk/gtkmodules.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gtk/gtkmodules.c b/gtk/gtkmodules.c index de5c5a002..6fa78ea51 100644 --- a/gtk/gtkmodules.c +++ b/gtk/gtkmodules.c @@ -237,7 +237,7 @@ _gtk_get_module_path (const gchar *type) result = g_new (gchar *, count * 5 + 1); count = 0; - for (path = get_module_path (); *path; path++) + for (path = paths; *path; path++) { gint use_version, use_host; gboolean path_from_env; From 01df2249df8cb9ce4b95279f643fcf5cc3d2d4a6 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 15 May 2026 16:19:36 +0200 Subject: [PATCH 056/122] gtk: avoid overallocating module paths --- gtk/gtkmodules.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gtk/gtkmodules.c b/gtk/gtkmodules.c index 6fa78ea51..061873bdf 100644 --- a/gtk/gtkmodules.c +++ b/gtk/gtkmodules.c @@ -234,7 +234,7 @@ _gtk_get_module_path (const gchar *type) for (path = paths; *path; path++) count++; - result = g_new (gchar *, count * 5 + 1); + result = g_new (gchar *, count * 4 + module_path_env_dirs + 1); count = 0; for (path = paths; *path; path++) From 2e454a61ba981275fdbad558928fcb37bccc32b4 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 15 May 2026 16:35:47 +0200 Subject: [PATCH 057/122] cups: release secrets task resources --- modules/printbackends/cups/gtkcupssecretsutils.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/printbackends/cups/gtkcupssecretsutils.c b/modules/printbackends/cups/gtkcupssecretsutils.c index 2ed0db57c..3b1a95db0 100644 --- a/modules/printbackends/cups/gtkcupssecretsutils.c +++ b/modules/printbackends/cups/gtkcupssecretsutils.c @@ -78,7 +78,7 @@ create_attributes (const gchar *printer_uri, /* The printer uri is the main identifying part */ g_variant_builder_add (attr_builder, "{ss}", "uri", printer_uri); - if (additional_labels != NULL) + if (additional_labels != NULL && additional_attrs != NULL) { int i; for (i = 0; additional_labels[i] != NULL; i++) @@ -170,6 +170,9 @@ get_secret_cb (GObject *source_object, } } + g_variant_iter_free (iter); + g_variant_unref (attributes); + if (pw_field == -1) { /* should not happen... */ @@ -938,6 +941,7 @@ cleanup_task_data (gpointer data) g_clear_object (&task_data->dbus_connection); g_clear_pointer (&task_data->session_path, g_free); g_clear_object (&task_data->item_proxy); + g_free (task_data); } /** From 5806b7f34d6bea2aaaa82b7d486d189bc288fd12 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 15 May 2026 17:35:59 +0200 Subject: [PATCH 058/122] gtk: update selected font family immediately --- gtk/gtkfontsel.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gtk/gtkfontsel.c b/gtk/gtkfontsel.c index 46aa6c01a..3f511db0e 100644 --- a/gtk/gtkfontsel.c +++ b/gtk/gtkfontsel.c @@ -842,9 +842,7 @@ gtk_font_selection_select_font (GtkTreeSelection *selection, { GtkFontSelection *fontsel = GTK_FONT_SELECTION (data); - gtk_font_selection_schedule_selection (fontsel, - font_selection_font_timeout_quark (), - gtk_font_selection_real_select_font); + gtk_font_selection_real_select_font (fontsel); } static int From f10e451badc4d686e2af06cf9ae71ade096cfd5d Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 15 May 2026 17:38:55 +0200 Subject: [PATCH 059/122] gtk: update selected font style and size immediately --- gtk/gtkfontsel.c | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/gtk/gtkfontsel.c b/gtk/gtkfontsel.c index 3f511db0e..5489f8062 100644 --- a/gtk/gtkfontsel.c +++ b/gtk/gtkfontsel.c @@ -1076,9 +1076,7 @@ gtk_font_selection_select_style (GtkTreeSelection *selection, { GtkFontSelection *fontsel = GTK_FONT_SELECTION (data); - gtk_font_selection_schedule_selection (fontsel, - font_selection_style_timeout_quark (), - gtk_font_selection_real_select_style); + gtk_font_selection_real_select_style (fontsel); } static void @@ -1241,9 +1239,7 @@ gtk_font_selection_select_size (GtkTreeSelection *selection, { GtkFontSelection *fontsel = GTK_FONT_SELECTION (data); - gtk_font_selection_schedule_selection (fontsel, - font_selection_size_timeout_quark (), - gtk_font_selection_real_select_size); + gtk_font_selection_real_select_size (fontsel); } static void From 4c60091657095c73fb2ac7aa2718ed9229c3cc5f Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 15 May 2026 18:04:40 +0200 Subject: [PATCH 060/122] gtk: cache font family sort keys --- gtk/gtkfontsel.c | 59 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/gtk/gtkfontsel.c b/gtk/gtkfontsel.c index 5489f8062..9dd382662 100644 --- a/gtk/gtkfontsel.c +++ b/gtk/gtkfontsel.c @@ -845,13 +845,26 @@ gtk_font_selection_select_font (GtkTreeSelection *selection, gtk_font_selection_real_select_font (fontsel); } -static int -cmp_families (const void *a, const void *b) +typedef struct { - const char *a_name = pango_font_family_get_name (*(PangoFontFamily **)a); - const char *b_name = pango_font_family_get_name (*(PangoFontFamily **)b); + PangoFontFamily *family; + const gchar *name; + gchar *collate_key; +} FontFamilySortInfo; - return g_utf8_collate (a_name, b_name); +static int +cmp_families (const void *a, + const void *b) +{ + const FontFamilySortInfo *family_a = a; + const FontFamilySortInfo *family_b = b; + gint result; + + result = strcmp (family_a->collate_key, family_b->collate_key); + if (result != 0) + return result; + + return g_strcmp0 (family_a->name, family_b->name); } static void @@ -859,6 +872,7 @@ gtk_font_selection_show_available_fonts (GtkFontSelection *fontsel) { GtkListStore *model; PangoFontFamily **families; + FontFamilySortInfo *sorted_families; PangoFontFamily *match_family = NULL; gint n_families, i; GtkTreeIter match_row; @@ -866,26 +880,37 @@ gtk_font_selection_show_available_fonts (GtkFontSelection *fontsel) model = GTK_LIST_STORE (gtk_tree_view_get_model (GTK_TREE_VIEW (fontsel->family_list))); pango_context_list_families (gtk_widget_get_pango_context (GTK_WIDGET (fontsel)), - &families, &n_families); - qsort (families, n_families, sizeof (PangoFontFamily *), cmp_families); + &families, &n_families); + + sorted_families = g_new (FontFamilySortInfo, n_families); + for (i = 0; i < n_families; i++) + { + const gchar *name = pango_font_family_get_name (families[i]); + + sorted_families[i].family = families[i]; + sorted_families[i].name = name; + sorted_families[i].collate_key = g_utf8_collate_key (name ? name : "", -1); + } + + qsort (sorted_families, n_families, sizeof (FontFamilySortInfo), cmp_families); gtk_list_store_clear (model); - for (i=0; ifamily_list), &match_row); #ifdef INCLUDE_FONT_ENTRIES gtk_entry_set_text (GTK_ENTRY (fontsel->font_entry), - pango_font_family_get_name (match_family)); + pango_font_family_get_name (match_family)); #endif /* INCLUDE_FONT_ENTRIES */ } + for (i = 0; i < n_families; i++) + g_free (sorted_families[i].collate_key); + + g_free (sorted_families); g_free (families); } From b74c87f3550ee5b564ad207bef7c3065270efcdb Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 15 May 2026 18:27:36 +0200 Subject: [PATCH 061/122] gtk: cache font face descriptions while sorting --- gtk/gtkfontsel.c | 72 +++++++++++++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/gtk/gtkfontsel.c b/gtk/gtkfontsel.c index 9dd382662..1094118b6 100644 --- a/gtk/gtkfontsel.c +++ b/gtk/gtkfontsel.c @@ -952,18 +952,26 @@ compare_font_descriptions (const PangoFontDescription *a, const PangoFontDescrip return 0; } -static int -faces_sort_func (const void *a, const void *b) +typedef struct { - PangoFontDescription *desc_a = pango_font_face_describe (*(PangoFontFace **)a); - PangoFontDescription *desc_b = pango_font_face_describe (*(PangoFontFace **)b); + PangoFontFace *face; + PangoFontDescription *desc; + const gchar *name; +} FontFaceSortInfo; - int ord = compare_font_descriptions (desc_a, desc_b); +static int +faces_sort_func (const void *a, + const void *b) +{ + const FontFaceSortInfo *face_a = a; + const FontFaceSortInfo *face_b = b; + gint result; - pango_font_description_free (desc_a); - pango_font_description_free (desc_b); + result = compare_font_descriptions (face_a->desc, face_b->desc); + if (result != 0) + return result; - return ord; + return g_strcmp0 (face_a->name, face_b->name); } static gboolean @@ -983,6 +991,7 @@ gtk_font_selection_show_available_styles (GtkFontSelection *fontsel) { gint n_faces, i; PangoFontFace **faces; + FontFaceSortInfo *sorted_faces; PangoFontDescription *old_desc; GtkListStore *model; GtkTreeIter match_row; @@ -996,37 +1005,40 @@ gtk_font_selection_show_available_styles (GtkFontSelection *fontsel) old_desc= NULL; pango_font_family_list_faces (fontsel->family, &faces, &n_faces); - qsort (faces, n_faces, sizeof (PangoFontFace *), faces_sort_func); + + sorted_faces = g_new (FontFaceSortInfo, n_faces); + for (i = 0; i < n_faces; i++) + { + sorted_faces[i].face = faces[i]; + sorted_faces[i].desc = pango_font_face_describe (faces[i]); + sorted_faces[i].name = pango_font_face_get_face_name (faces[i]); + } + + qsort (sorted_faces, n_faces, sizeof (FontFaceSortInfo), faces_sort_func); gtk_list_store_clear (model); - for (i=0; i < n_faces; i++) + for (i = 0; i < n_faces; i++) { GtkTreeIter iter; - const gchar *str = pango_font_face_get_face_name (faces[i]); + const gchar *str = sorted_faces[i].name; gtk_list_store_insert_with_values (model, &iter, -1, - FACE_COLUMN, faces[i], + FACE_COLUMN, sorted_faces[i].face, FACE_NAME_COLUMN, _(str), -1); if (i == 0) - { - match_row = iter; - match_face = faces[i]; - } - else if (old_desc) - { - PangoFontDescription *tmp_desc = pango_font_face_describe (faces[i]); - - if (font_description_style_equal (tmp_desc, old_desc)) - { - match_row = iter; - match_face = faces[i]; - } - - pango_font_description_free (tmp_desc); - } + { + match_row = iter; + match_face = sorted_faces[i].face; + } + else if (old_desc && + font_description_style_equal (sorted_faces[i].desc, old_desc)) + { + match_row = iter; + match_face = sorted_faces[i].face; + } } if (old_desc) @@ -1043,6 +1055,10 @@ gtk_font_selection_show_available_styles (GtkFontSelection *fontsel) set_cursor_to_iter (GTK_TREE_VIEW (fontsel->face_list), &match_row); } + for (i = 0; i < n_faces; i++) + pango_font_description_free (sorted_faces[i].desc); + + g_free (sorted_faces); g_free (faces); } From 685dbf9bbc19c8f4628171c3b301d03ce4c6be87 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 15 May 2026 22:47:24 +0200 Subject: [PATCH 062/122] fontsel: remove unused selection timeout helpers --- gtk/gtkfontsel.c | 91 ------------------------------------------------ 1 file changed, 91 deletions(-) diff --git a/gtk/gtkfontsel.c b/gtk/gtkfontsel.c index 1094118b6..81daa0fae 100644 --- a/gtk/gtkfontsel.c +++ b/gtk/gtkfontsel.c @@ -585,79 +585,6 @@ gtk_font_selection_new (void) return GTK_WIDGET (fontsel); } -static GQuark -font_selection_font_timeout_quark (void) -{ - return g_quark_from_static_string ("gtk-font-selection-font-timeout"); -} - -static GQuark -font_selection_style_timeout_quark (void) -{ - return g_quark_from_static_string ("gtk-font-selection-style-timeout"); -} - -static GQuark -font_selection_size_timeout_quark (void) -{ - return g_quark_from_static_string ("gtk-font-selection-size-timeout"); -} - -static GQuark -font_selection_immediate_quark (void) -{ - return g_quark_from_static_string ("gtk-font-selection-immediate"); -} - -static void -gtk_font_selection_remove_timeout (GtkFontSelection *fontsel, - GQuark quark) -{ - guint timeout_id; - - timeout_id = GPOINTER_TO_UINT (g_object_get_qdata (G_OBJECT (fontsel), quark)); - if (timeout_id) - { - g_source_remove (timeout_id); - g_object_set_qdata (G_OBJECT (fontsel), quark, NULL); - } -} - -static gboolean -gtk_font_selection_get_immediate (GtkFontSelection *fontsel) -{ - return GPOINTER_TO_UINT (g_object_get_qdata (G_OBJECT (fontsel), - font_selection_immediate_quark ())) != 0; -} - -static void -gtk_font_selection_set_immediate (GtkFontSelection *fontsel, - gboolean immediate) -{ - g_object_set_qdata (G_OBJECT (fontsel), - font_selection_immediate_quark (), - GUINT_TO_POINTER (immediate ? 1 : 0)); -} - -static void -gtk_font_selection_schedule_selection (GtkFontSelection *fontsel, - GQuark quark, - GSourceFunc func) -{ - guint timeout_id; - - gtk_font_selection_remove_timeout (fontsel, quark); - - if (gtk_font_selection_get_immediate (fontsel)) - { - func (fontsel); - return; - } - - timeout_id = g_timeout_add (200, func, fontsel); - g_object_set_qdata (G_OBJECT (fontsel), quark, GUINT_TO_POINTER (timeout_id)); -} - static void gtk_font_selection_finalize (GObject *object) { @@ -667,10 +594,6 @@ gtk_font_selection_finalize (GObject *object) fontsel = GTK_FONT_SELECTION (object); - gtk_font_selection_remove_timeout (fontsel, font_selection_font_timeout_quark ()); - gtk_font_selection_remove_timeout (fontsel, font_selection_style_timeout_quark ()); - gtk_font_selection_remove_timeout (fontsel, font_selection_size_timeout_quark ()); - if (fontsel->font) gdk_font_unref (fontsel->font); @@ -709,8 +632,6 @@ gtk_font_selection_reload_fonts (GtkFontSelection *fontsel) PangoFontDescription *desc; desc = gtk_font_selection_get_font_description (fontsel); - gtk_font_selection_set_immediate (fontsel, TRUE); - gtk_font_selection_show_available_fonts (fontsel); gtk_font_selection_show_available_sizes (fontsel, TRUE); gtk_font_selection_show_available_styles (fontsel); @@ -718,8 +639,6 @@ gtk_font_selection_reload_fonts (GtkFontSelection *fontsel) gtk_font_selection_select_font_desc (fontsel, desc, NULL, NULL); gtk_font_selection_scroll_to_selection (fontsel); - gtk_font_selection_set_immediate (fontsel, FALSE); - pango_font_description_free (desc); } } @@ -831,8 +750,6 @@ gtk_font_selection_real_select_font (gpointer data) g_object_unref (family); } - g_object_set_qdata (G_OBJECT (fontsel), font_selection_font_timeout_quark (), NULL); - return FALSE; } @@ -1110,8 +1027,6 @@ gtk_font_selection_real_select_style (gpointer data) gtk_font_selection_show_available_sizes (fontsel, FALSE); gtk_font_selection_select_best_size (fontsel); - g_object_set_qdata (G_OBJECT (fontsel), font_selection_style_timeout_quark (), NULL); - return FALSE; } @@ -1273,8 +1188,6 @@ gtk_font_selection_real_select_size (gpointer data) gtk_font_selection_set_size (fontsel, new_size * PANGO_SCALE); } - g_object_set_qdata (G_OBJECT (fontsel), font_selection_size_timeout_quark (), NULL); - return FALSE; } @@ -1618,8 +1531,6 @@ gtk_font_selection_select_font_desc (GtkFontSelection *fontsel, if (!new_family) return FALSE; - gtk_font_selection_set_immediate (fontsel, TRUE); - if (pfamily) *pfamily = new_family; else @@ -1670,8 +1581,6 @@ gtk_font_selection_select_font_desc (GtkFontSelection *fontsel, gtk_font_selection_set_size (fontsel, pango_font_description_get_size (new_desc)); - gtk_font_selection_set_immediate (fontsel, FALSE); - return TRUE; } From 06167dfa59ea0011d8cd816cd6dfc6af7b955240 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Sat, 16 May 2026 09:24:49 +0200 Subject: [PATCH 063/122] configure: include pango packages in GTK cflags --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index a8c27ec76..048454a70 100644 --- a/configure.ac +++ b/configure.ac @@ -1512,7 +1512,7 @@ fi GTK_EXTRA_LIBS= GTK_EXTRA_CFLAGS= GTK_DEP_LIBS="$GDK_EXTRA_LIBS $GTK_DEP_LIBS_FOR_X `$PKG_CONFIG --libs $PANGO_PACKAGES $GTK_PACKAGES_FOR_X $GTK_PACKAGES` $GTK_EXTRA_LIBS $MATH_LIB" -GTK_DEP_CFLAGS="`$PKG_CONFIG --cflags gthread-2.0 $GDK_PACKAGES $GTK_PACKAGES` $GDK_EXTRA_CFLAGS $GTK_EXTRA_CFLAGS" +GTK_DEP_CFLAGS="`$PKG_CONFIG --cflags gthread-2.0 $PANGO_PACKAGES $GDK_PACKAGES $GTK_PACKAGES` $GDK_EXTRA_CFLAGS $GTK_EXTRA_CFLAGS" if test x"$os_win32" = xyes; then GTK_EXTRA_CFLAGS="$msnative_struct" From cbfe8477fb0f66d36171a302082665d5455fc717 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Mon, 18 May 2026 17:54:21 +0200 Subject: [PATCH 064/122] gtkfilesystemmodel: avoid sorting twice for newly queried files _gtk_file_system_model_update_file() adds unknown files through add_file(), which already sorts the model. gtk_file_system_model_query_done() then sorted again unconditionally after the update. Remember whether the file was already present before updating it, and only request the extra sort for existing files. This avoids redundant sorting for newly discovered files while keeping the existing update behaviour for known files. --- gtk/gtkfilesystemmodel.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/gtk/gtkfilesystemmodel.c b/gtk/gtkfilesystemmodel.c index 52fd17a08..207cea2c5 100644 --- a/gtk/gtkfilesystemmodel.c +++ b/gtk/gtkfilesystemmodel.c @@ -1230,7 +1230,7 @@ gtk_file_system_model_query_done (GObject * object, GtkFileSystemModel *model = data; /* only a valid pointer if not cancelled */ GFile *file = G_FILE (object); GFileInfo *info; - guint id; + gboolean file_known; info = g_file_query_info_finish (file, res, NULL); if (info == NULL) @@ -1238,10 +1238,12 @@ gtk_file_system_model_query_done (GObject * object, gdk_threads_enter (); + file_known = node_get_for_file (model, file) != 0; + _gtk_file_system_model_update_file (model, file, info); - id = node_get_for_file (model, file); - gtk_file_system_model_sort_node (model, id); + if (file_known) + gtk_file_system_model_sort (model); g_object_unref (info); From 8786714cc9f4290f322d5fa2a70ac198a11ce230 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Mon, 18 May 2026 18:42:03 +0200 Subject: [PATCH 065/122] gtkfilechooser: select clicked file before popup menu --- gtk/gtkfilechooserdefault.c | 89 +++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 2ce2d6c8f..4b02756bb 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -4487,6 +4487,93 @@ popup_position_func (GtkMenu *menu, *push_in = FALSE; } +typedef enum +{ + BROWSE_FILES_WIDGET_TREE, + BROWSE_FILES_WIDGET_ICON +} BrowseFilesWidgetKind; + +static gboolean +browse_files_select_item_at_pos (GtkFileChooserDefault *impl, + BrowseFilesWidgetKind widget_kind, + gint x, + gint y) +{ + GtkTreePath *path; + + switch (widget_kind) + { + case BROWSE_FILES_WIDGET_TREE: + { + GtkTreeView *tree_view; + GtkTreeSelection *selection; + + tree_view = GTK_TREE_VIEW (impl->browse_files_tree_view); + + if (!gtk_tree_view_get_path_at_pos (tree_view, x, y, + &path, NULL, NULL, NULL)) + return FALSE; + + selection = gtk_tree_view_get_selection (tree_view); + + if (!gtk_tree_selection_path_is_selected (selection, path)) + { + gtk_tree_selection_unselect_all (selection); + gtk_tree_selection_select_path (selection, path); + } + + gtk_tree_view_set_cursor (tree_view, path, NULL, FALSE); + gtk_tree_path_free (path); + + return TRUE; + } + + case BROWSE_FILES_WIDGET_ICON: + { + GtkIconView *icon_view; + + icon_view = GTK_ICON_VIEW (impl->browse_files_icon_view); + path = gtk_icon_view_get_path_at_pos (icon_view, x, y); + + if (path == NULL) + return FALSE; + + if (!gtk_icon_view_path_is_selected (icon_view, path)) + { + gtk_icon_view_unselect_all (icon_view); + gtk_icon_view_select_path (icon_view, path); + } + + gtk_icon_view_set_cursor (icon_view, path, NULL, FALSE); + gtk_tree_path_free (path); + + return TRUE; + } + } + + return FALSE; +} + +static gboolean +browse_files_select_clicked_item (GtkFileChooserDefault *impl, + GtkWidget *widget, + GdkEventButton *event) +{ + BrowseFilesWidgetKind widget_kind; + + if (widget == impl->browse_files_tree_view) + widget_kind = BROWSE_FILES_WIDGET_TREE; + else if (widget == impl->browse_files_icon_view) + widget_kind = BROWSE_FILES_WIDGET_ICON; + else + return FALSE; + + return browse_files_select_item_at_pos (impl, + widget_kind, + (gint) event->x, + (gint) event->y); +} + static void file_list_popup_menu (GtkFileChooserDefault *impl, GdkEventButton *event) @@ -4537,6 +4624,8 @@ list_button_press_event_cb (GtkWidget *widget, gtk_widget_event (widget, (GdkEvent *) event); in_press = FALSE; + browse_files_select_clicked_item (impl, widget, event); + file_list_popup_menu (impl, event); return TRUE; } From ff8d4f899d726b266a1d1e268c9a6925a0939945 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Mon, 18 May 2026 19:32:36 +0200 Subject: [PATCH 066/122] gtkfilechooser: add copy location menu item --- gtk/gtkfilechooserdefault.c | 216 ++++++++++++++++++++++++------------ gtk/gtkfilechooserprivate.h | 1 + 2 files changed, 143 insertions(+), 74 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 4b02756bb..dbc09270e 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -3937,6 +3937,7 @@ popup_menu_detach_cb (GtkWidget *attach_widget, impl->browse_files_popup_menu = NULL; impl->browse_files_popup_menu_add_shortcut_item = NULL; + impl->browse_files_popup_menu_copy_location_item = NULL; impl->browse_files_popup_menu_hidden_files_item = NULL; } @@ -3948,6 +3949,73 @@ add_to_shortcuts_cb (GtkMenuItem *item, bookmarks_add_selected_folder (impl); } +struct copy_locations_closure +{ + GString *locations; + gboolean first; +}; + +static void +copy_location_foreach_cb (GtkTreeModel *model, + GtkTreePath *path, + GtkTreeIter *iter, + gpointer data) +{ + struct copy_locations_closure *closure = data; + GFile *file; + gchar *location; + + gtk_tree_model_get (model, iter, + MODEL_COL_FILE, &file, + -1); + + if (file == NULL) + return; + + location = g_file_get_path (file); + + if (location == NULL) + location = g_file_get_uri (file); + + if (location) + { + if (!closure->first) + g_string_append_c (closure->locations, '\n'); + + g_string_append (closure->locations, location); + closure->first = FALSE; + g_free (location); + } + + g_object_unref (file); +} + +/* Callback used when the "Copy Location" menu item is activated */ +static void +copy_location_cb (GtkMenuItem *item, + GtkFileChooserDefault *impl) +{ + struct copy_locations_closure closure; + + closure.locations = g_string_new (NULL); + closure.first = TRUE; + + current_selection_selected_foreach (impl, + copy_location_foreach_cb, + &closure); + + if (closure.locations->len > 0) + { + GtkClipboard *clipboard; + + clipboard = gtk_clipboard_get_for_display (gtk_widget_get_display (impl->browse_files_current_view), + GDK_SELECTION_CLIPBOARD); + gtk_clipboard_set_text (clipboard, closure.locations->str, -1); + } + + g_string_free (closure.locations, TRUE); +} + /* Callback used when the "Show Hidden Files" menu item is toggled */ static void show_hidden_toggled_cb (GtkCheckMenuItem *item, @@ -4297,6 +4365,16 @@ file_list_build_popup_menu (GtkFileChooserDefault *impl) gtk_widget_show (item); gtk_menu_shell_append (GTK_MENU_SHELL (impl->browse_files_popup_menu), item); + + item = gtk_image_menu_item_new_with_mnemonic (_("_Copy Location")); + impl->browse_files_popup_menu_copy_location_item = item; + gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), + gtk_image_new_from_stock (GTK_STOCK_COPY, GTK_ICON_SIZE_MENU)); + g_signal_connect (item, "activate", + G_CALLBACK (copy_location_cb), impl); + gtk_widget_show (item); + gtk_menu_shell_append (GTK_MENU_SHELL (impl->browse_files_popup_menu), item); + item = gtk_separator_menu_item_new (); gtk_widget_show (item); gtk_menu_shell_append (GTK_MENU_SHELL (impl->browse_files_popup_menu), item); @@ -4390,6 +4468,10 @@ file_list_update_popup_menu (GtkFileChooserDefault *impl) * bookmarks_check_add_sensitivity() */ + if (impl->browse_files_popup_menu_copy_location_item) + gtk_widget_set_sensitive (impl->browse_files_popup_menu_copy_location_item, + current_selection_count_selected_rows (impl) > 0); + /* 'Show Hidden Files' */ g_signal_handlers_block_by_func (impl->browse_files_popup_menu_hidden_files_item, G_CALLBACK (show_hidden_toggled_cb), impl); @@ -4487,71 +4569,58 @@ popup_position_func (GtkMenu *menu, *push_in = FALSE; } -typedef enum -{ - BROWSE_FILES_WIDGET_TREE, - BROWSE_FILES_WIDGET_ICON -} BrowseFilesWidgetKind; - static gboolean -browse_files_select_item_at_pos (GtkFileChooserDefault *impl, - BrowseFilesWidgetKind widget_kind, - gint x, - gint y) +browse_files_select_tree_item_at_pos (GtkFileChooserDefault *impl, + gint x, + gint y) { + GtkTreeView *tree_view; + GtkTreeSelection *selection; GtkTreePath *path; - switch (widget_kind) + tree_view = GTK_TREE_VIEW (impl->browse_files_tree_view); + + if (!gtk_tree_view_get_path_at_pos (tree_view, x, y, + &path, NULL, NULL, NULL)) + return FALSE; + + selection = gtk_tree_view_get_selection (tree_view); + + if (!gtk_tree_selection_path_is_selected (selection, path)) { - case BROWSE_FILES_WIDGET_TREE: - { - GtkTreeView *tree_view; - GtkTreeSelection *selection; - - tree_view = GTK_TREE_VIEW (impl->browse_files_tree_view); - - if (!gtk_tree_view_get_path_at_pos (tree_view, x, y, - &path, NULL, NULL, NULL)) - return FALSE; - - selection = gtk_tree_view_get_selection (tree_view); - - if (!gtk_tree_selection_path_is_selected (selection, path)) - { - gtk_tree_selection_unselect_all (selection); - gtk_tree_selection_select_path (selection, path); - } - - gtk_tree_view_set_cursor (tree_view, path, NULL, FALSE); - gtk_tree_path_free (path); - - return TRUE; - } - - case BROWSE_FILES_WIDGET_ICON: - { - GtkIconView *icon_view; - - icon_view = GTK_ICON_VIEW (impl->browse_files_icon_view); - path = gtk_icon_view_get_path_at_pos (icon_view, x, y); - - if (path == NULL) - return FALSE; - - if (!gtk_icon_view_path_is_selected (icon_view, path)) - { - gtk_icon_view_unselect_all (icon_view); - gtk_icon_view_select_path (icon_view, path); - } - - gtk_icon_view_set_cursor (icon_view, path, NULL, FALSE); - gtk_tree_path_free (path); - - return TRUE; - } + gtk_tree_selection_unselect_all (selection); + gtk_tree_selection_select_path (selection, path); } - return FALSE; + gtk_tree_path_free (path); + + return TRUE; +} + +static gboolean +browse_files_select_icon_item_at_pos (GtkFileChooserDefault *impl, + gint x, + gint y) +{ + GtkIconView *icon_view; + GtkTreePath *path; + + icon_view = GTK_ICON_VIEW (impl->browse_files_icon_view); + path = gtk_icon_view_get_path_at_pos (icon_view, x, y); + + if (path == NULL) + return FALSE; + + if (!gtk_icon_view_path_is_selected (icon_view, path)) + { + gtk_icon_view_unselect_all (icon_view); + gtk_icon_view_select_path (icon_view, path); + } + + gtk_icon_view_set_cursor (icon_view, path, NULL, FALSE); + gtk_tree_path_free (path); + + return TRUE; } static gboolean @@ -4559,19 +4628,17 @@ browse_files_select_clicked_item (GtkFileChooserDefault *impl, GtkWidget *widget, GdkEventButton *event) { - BrowseFilesWidgetKind widget_kind; - if (widget == impl->browse_files_tree_view) - widget_kind = BROWSE_FILES_WIDGET_TREE; - else if (widget == impl->browse_files_icon_view) - widget_kind = BROWSE_FILES_WIDGET_ICON; - else - return FALSE; + return browse_files_select_tree_item_at_pos (impl, + (gint) event->x, + (gint) event->y); - return browse_files_select_item_at_pos (impl, - widget_kind, - (gint) event->x, - (gint) event->y); + if (widget == impl->browse_files_icon_view) + return browse_files_select_icon_item_at_pos (impl, + (gint) event->x, + (gint) event->y); + + return FALSE; } static void @@ -4620,11 +4687,12 @@ list_button_press_event_cb (GtkWidget *widget, if (!_gtk_button_event_triggers_context_menu (event)) return FALSE; - in_press = TRUE; - gtk_widget_event (widget, (GdkEvent *) event); - in_press = FALSE; - - browse_files_select_clicked_item (impl, widget, event); + if (!browse_files_select_clicked_item (impl, widget, event)) + { + in_press = TRUE; + gtk_widget_event (widget, (GdkEvent *) event); + in_press = FALSE; + } file_list_popup_menu (impl, event); return TRUE; diff --git a/gtk/gtkfilechooserprivate.h b/gtk/gtkfilechooserprivate.h index 193dea2e3..8df1af951 100644 --- a/gtk/gtkfilechooserprivate.h +++ b/gtk/gtkfilechooserprivate.h @@ -182,6 +182,7 @@ struct _GtkFileChooserDefault GtkWidget *browse_files_icon_view; GtkWidget *browse_files_popup_menu; GtkWidget *browse_files_popup_menu_add_shortcut_item; + GtkWidget *browse_files_popup_menu_copy_location_item; GtkWidget *browse_files_popup_menu_hidden_files_item; GtkWidget *browse_files_popup_menu_size_column_item; GtkWidget *browse_files_popup_menu_sort_by_name_item; From cf8531cd0d0c1d7e1f5e8f23b095c2286e4fa472 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Mon, 18 May 2026 20:15:48 +0200 Subject: [PATCH 067/122] gtkfilechooser: add compact view mode --- gtk/gtkfilechooserdefault.c | 126 +++++++++++++++++++++++++++--------- gtk/gtkfilechooserprivate.h | 3 +- 2 files changed, 98 insertions(+), 31 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index dbc09270e..85f3f690c 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -257,8 +257,15 @@ typedef enum { #define FALLBACK_ICON_VIEW_ICON_SIZE 48 #define ICON_VIEW_ITEM_WIDTH 128 +#define COMPACT_VIEW_ITEM_WIDTH 180 +#define COMPACT_VIEW_SPACING 4 +#define COMPACT_VIEW_ROW_SPACING 0 +#define COMPACT_VIEW_COLUMN_SPACING 12 +#define COMPACT_VIEW_MARGIN 2 #define ICON_VIEW_SCALE_MAX_VISIBLE_OVERSCAN 128 +#define VIEW_MODE_USES_ICON_VIEW(mode) ((mode) == VIEW_MODE_ICON || (mode) == VIEW_MODE_COMPACT) + #define PREVIEW_HBOX_SPACING 12 #define NUM_LINES 45 #define NUM_CHARS 60 @@ -2369,7 +2376,7 @@ new_folder_button_clicked (GtkButton *button, TRUE); gtk_tree_path_free (path); } - else if (impl->view_mode == VIEW_MODE_ICON) + else if (VIEW_MODE_USES_ICON_VIEW (impl->view_mode)) { add_idle_to_edit_icon_view (impl, path); } @@ -4139,7 +4146,7 @@ sort_ascending_toggled_cb (GtkCheckMenuItem *item, // The sort column is explicitly set to mtime for the recent model // This prevents it from switching when changing sort order - if (impl->view_mode == VIEW_MODE_ICON && impl->current_model == GTK_TREE_MODEL (impl->recent_model)) + if (VIEW_MODE_USES_ICON_VIEW (impl->view_mode) && impl->current_model == GTK_TREE_MODEL (impl->recent_model)) gtk_tree_sortable_get_sort_column_id (GTK_TREE_SORTABLE (impl->current_model), &impl->sort_column, NULL); impl->sort_order = GTK_SORT_ASCENDING; @@ -4168,7 +4175,7 @@ sort_descending_toggled_cb (GtkCheckMenuItem *item, // The sort column is explicitly set to mtime for the recent model // This prevents it from switching when changing sort order - if (impl->view_mode == VIEW_MODE_ICON && impl->current_model == GTK_TREE_MODEL (impl->recent_model)) + if (VIEW_MODE_USES_ICON_VIEW (impl->view_mode) && impl->current_model == GTK_TREE_MODEL (impl->recent_model)) gtk_tree_sortable_get_sort_column_id (GTK_TREE_SORTABLE (impl->current_model), &impl->sort_column, NULL); impl->sort_order = GTK_SORT_DESCENDING; @@ -4395,7 +4402,7 @@ file_list_build_popup_menu (GtkFileChooserDefault *impl) gtk_widget_show (item); gtk_menu_shell_append (GTK_MENU_SHELL (impl->browse_files_popup_menu), item); } - else if (impl->view_mode == VIEW_MODE_ICON) + else if (VIEW_MODE_USES_ICON_VIEW (impl->view_mode)) { GtkWidget *menu, *subitem; @@ -4490,7 +4497,7 @@ file_list_update_popup_menu (GtkFileChooserDefault *impl) g_signal_handlers_unblock_by_func (impl->browse_files_popup_menu_size_column_item, G_CALLBACK (show_size_column_toggled_cb), impl); } - else if (impl->view_mode == VIEW_MODE_ICON) + else if (VIEW_MODE_USES_ICON_VIEW (impl->view_mode)) { gint column = impl->sort_column; GtkSortType order = impl->sort_order; @@ -4768,7 +4775,7 @@ set_icon_cell_renderer_fixed_size (GtkFileChooserDefault *impl, int icon_size; gint xpad, ypad; - if (view_mode == VIEW_MODE_LIST) + if (view_mode == VIEW_MODE_LIST || view_mode == VIEW_MODE_COMPACT) { icon_size = impl->list_view_icon_size; @@ -4828,8 +4835,58 @@ update_icon_view_cell_layout (GtkFileChooserDefault *impl) if (!impl->browse_files_icon_view) return; + if (impl->view_mode == VIEW_MODE_COMPACT) + { + GtkIconView *icon_view; + gint icon_cell_width; + gint text_width; + + icon_view = GTK_ICON_VIEW (impl->browse_files_icon_view); + item_width = COMPACT_VIEW_ITEM_WIDTH; + + gtk_icon_view_set_item_orientation (icon_view, GTK_ORIENTATION_HORIZONTAL); + gtk_icon_view_set_item_width (icon_view, item_width); + gtk_icon_view_set_spacing (icon_view, COMPACT_VIEW_SPACING); + gtk_icon_view_set_row_spacing (icon_view, COMPACT_VIEW_ROW_SPACING); + gtk_icon_view_set_column_spacing (icon_view, COMPACT_VIEW_COLUMN_SPACING); + gtk_icon_view_set_margin (icon_view, COMPACT_VIEW_MARGIN); + + icon_size = impl->list_view_icon_size; + if (icon_size <= 0) + icon_size = FALLBACK_LIST_VIEW_ICON_SIZE; + + icon_cell_width = icon_size; + + if (impl->list_icon_renderer) + { + gtk_cell_renderer_get_padding (impl->list_icon_renderer, &xpad, &ypad); + icon_cell_width = xpad * 2 + icon_size; + gtk_cell_renderer_set_fixed_size (impl->list_icon_renderer, + icon_cell_width, + ypad * 2 + icon_size); + } + + if (impl->list_name_renderer) + { + text_width = MAX (1, item_width - icon_cell_width - COMPACT_VIEW_SPACING); + + g_object_set (G_OBJECT (impl->list_name_renderer), + "alignment", PANGO_ALIGN_LEFT, + "ellipsize", PANGO_ELLIPSIZE_END, + "wrap-width", -1, + "yalign", 0.5f, + NULL); + gtk_cell_renderer_set_fixed_size (GTK_CELL_RENDERER (impl->list_name_renderer), + text_width, -1); + } + + return; + } + item_width = get_icon_view_item_width (impl); + gtk_icon_view_set_item_orientation (GTK_ICON_VIEW (impl->browse_files_icon_view), + GTK_ORIENTATION_VERTICAL); gtk_icon_view_set_item_width (GTK_ICON_VIEW (impl->browse_files_icon_view), item_width); @@ -4855,7 +4912,11 @@ update_icon_view_cell_layout (GtkFileChooserDefault *impl) if (impl->list_name_renderer) { g_object_set (G_OBJECT (impl->list_name_renderer), + "alignment", PANGO_ALIGN_CENTER, + "ellipsize", PANGO_ELLIPSIZE_END, + "wrap-mode", PANGO_WRAP_WORD_CHAR, "wrap-width", item_width, + "yalign", 0.0f, NULL); gtk_cell_renderer_set_fixed_size (GTK_CELL_RENDERER (impl->list_name_renderer), item_width, -1); @@ -5030,7 +5091,8 @@ create_browse_files_icon_view (GtkFileChooserDefault *impl) gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (impl->browse_files_icon_view), impl->list_icon_renderer, TRUE); gtk_cell_layout_add_attribute (GTK_CELL_LAYOUT (impl->browse_files_icon_view), - impl->list_icon_renderer, "pixbuf", MODEL_COL_ICON_PIXBUF); + impl->list_icon_renderer, "pixbuf", + impl->view_mode == VIEW_MODE_COMPACT ? MODEL_COL_LIST_PIXBUF : MODEL_COL_ICON_PIXBUF); impl->list_name_renderer = gtk_cell_renderer_text_new (); g_object_set (G_OBJECT (impl->list_name_renderer), @@ -5358,28 +5420,28 @@ location_toggle_popup_handler (GtkFileChooserDefault *impl) static void view_mode_set (GtkFileChooserDefault *impl, ViewMode view_mode) { - GtkWidget *old_view = NULL; + GtkWidget *old_view; ViewMode old_view_mode = impl->view_mode; if (old_view_mode == view_mode) return; + old_view = impl->browse_files_current_view; + impl->view_mode = view_mode; gtk_combo_box_set_active (GTK_COMBO_BOX (impl->view_mode_combo_box), view_mode); /* Creating the target view */ - if (view_mode == VIEW_MODE_ICON) + if (VIEW_MODE_USES_ICON_VIEW (view_mode)) { create_browse_files_icon_view (impl); impl->browse_files_current_view = impl->browse_files_icon_view; - old_view = impl->browse_files_tree_view; } else if (view_mode == VIEW_MODE_LIST) { create_browse_files_tree_view (impl); impl->browse_files_current_view = impl->browse_files_tree_view; - old_view = impl->browse_files_icon_view; } else g_assert_not_reached (); @@ -5389,29 +5451,31 @@ view_mode_set (GtkFileChooserDefault *impl, ViewMode view_mode) current_view_set_select_multiple (impl, impl->select_multiple); copy_old_selection_to_current_view (impl, old_view_mode); - /* Destroy the old view */ if (view_mode == VIEW_MODE_ICON) + gtk_widget_show (impl->icon_view_scale_hbox); + else + gtk_widget_hide (impl->icon_view_scale_hbox); + + if (old_view_mode == VIEW_MODE_LIST) { impl->browse_files_tree_view = NULL; impl->list_name_column = NULL; impl->list_mtime_column = NULL; impl->list_size_column = NULL; - gtk_widget_show (impl->icon_view_scale_hbox); } - else if (view_mode == VIEW_MODE_LIST) + else if (VIEW_MODE_USES_ICON_VIEW (old_view_mode) && + !VIEW_MODE_USES_ICON_VIEW (view_mode)) { impl->browse_files_icon_view = NULL; impl->list_icon_renderer = NULL; impl->list_name_renderer = NULL; - gtk_widget_hide (impl->icon_view_scale_hbox); } - else - g_assert_not_reached (); if (impl->browse_files_popup_menu) gtk_menu_detach (GTK_MENU (impl->browse_files_popup_menu)); - gtk_widget_destroy (old_view); + if (old_view) + gtk_widget_destroy (old_view); /* Display the new view */ gtk_container_add (GTK_CONTAINER (impl->browse_files_scrolled_window), @@ -5864,6 +5928,8 @@ view_mode_combo_box_create (GtkFileChooserDefault *impl) _("List View")); /* VIEW_MODE_LIST */ gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT(impl->view_mode_combo_box), _("Icon View")); /* VIEW_MODE_ICON */ + gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT(impl->view_mode_combo_box), + _("Compact View")); /* VIEW_MODE_COMPACT */ gtk_combo_box_set_active (GTK_COMBO_BOX(impl->view_mode_combo_box), VIEW_MODE_LIST); @@ -7609,7 +7675,7 @@ center_selected_row_foreach_cb (GtkTreeModel *model, if (closure->impl->view_mode == VIEW_MODE_LIST) gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (closure->impl->browse_files_tree_view), path, NULL, TRUE, 0.5, 0.0); - else if (closure->impl->view_mode == VIEW_MODE_ICON) + else if (VIEW_MODE_USES_ICON_VIEW (closure->impl->view_mode)) gtk_icon_view_scroll_to_path (GTK_ICON_VIEW (closure->impl->browse_files_icon_view), path, TRUE, 0.5, 0.0); else g_assert_not_reached (); @@ -8003,7 +8069,7 @@ get_visible_range (GtkTreePath **start, GtkTreePath **end, { if (impl->view_mode == VIEW_MODE_LIST) return gtk_tree_view_get_visible_range (GTK_TREE_VIEW (impl->browse_files_tree_view), start, end); - if (impl->view_mode == VIEW_MODE_ICON) + if (VIEW_MODE_USES_ICON_VIEW (impl->view_mode)) return gtk_icon_view_get_visible_range (GTK_ICON_VIEW (impl->browse_files_icon_view), start, end); g_assert_not_reached (); } @@ -9564,7 +9630,7 @@ get_selected_file_info_from_file_list (GtkFileChooserDefault *impl, } *had_selection = TRUE; } - else if (impl->view_mode == VIEW_MODE_ICON) + else if (VIEW_MODE_USES_ICON_VIEW (impl->view_mode)) { if (!get_selected_tree_iter_from_icon_view (impl, &iter)) { @@ -10971,7 +11037,7 @@ check_preview_change (GtkFileChooserDefault *impl) if (impl->view_mode == VIEW_MODE_LIST) gtk_tree_view_get_cursor (GTK_TREE_VIEW (impl->browse_files_tree_view), &cursor_path, NULL); - else if (impl->view_mode == VIEW_MODE_ICON) + else if (VIEW_MODE_USES_ICON_VIEW (impl->view_mode)) cursor_path = NULL; else g_assert_not_reached (); @@ -11822,7 +11888,7 @@ selection_selected_foreach (GtkFileChooserDefault *impl, selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (impl->browse_files_tree_view)); gtk_tree_selection_selected_foreach (selection, func, data); } - else if (view == VIEW_MODE_ICON) + else if (VIEW_MODE_USES_ICON_VIEW (view)) icon_view_selection_selected_foreach (impl, func, data); else g_assert_not_reached (); @@ -11845,7 +11911,7 @@ current_selection_count_selected_rows (GtkFileChooserDefault *impl) selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (impl->browse_files_tree_view)); return gtk_tree_selection_count_selected_rows (selection); } - if (impl->view_mode == VIEW_MODE_ICON) + if (VIEW_MODE_USES_ICON_VIEW (impl->view_mode)) { GList *icon_selection; icon_selection = gtk_icon_view_get_selected_items (GTK_ICON_VIEW (impl->browse_files_icon_view)); @@ -11869,7 +11935,7 @@ selection_select_iter (GtkFileChooserDefault *impl, selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (impl->browse_files_tree_view)); gtk_tree_selection_select_iter (selection, iter); } - else if (target == VIEW_MODE_ICON) + else if (VIEW_MODE_USES_ICON_VIEW (target)) { GtkTreePath *path; path = gtk_tree_model_get_path (impl->current_model, iter); @@ -11925,7 +11991,7 @@ current_selection_unselect_iter (GtkFileChooserDefault *impl, selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (impl->browse_files_tree_view)); gtk_tree_selection_unselect_iter (selection, iter); } - else if (impl->view_mode == VIEW_MODE_ICON) + else if (VIEW_MODE_USES_ICON_VIEW (impl->view_mode)) { GtkTreePath *path; path = gtk_tree_model_get_path (impl->current_model, iter); @@ -11945,7 +12011,7 @@ current_selection_unselect_all (GtkFileChooserDefault *impl) selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (impl->browse_files_tree_view)); gtk_tree_selection_unselect_all (selection); } - else if (impl->view_mode == VIEW_MODE_ICON) + else if (VIEW_MODE_USES_ICON_VIEW (impl->view_mode)) gtk_icon_view_unselect_all (GTK_ICON_VIEW (impl->browse_files_icon_view)); else g_assert_not_reached (); @@ -11960,7 +12026,7 @@ current_view_set_file_model (GtkFileChooserDefault *impl, GtkTreeModel *model) if (impl->view_mode == VIEW_MODE_LIST) view = impl->browse_files_tree_view; - else if (impl->view_mode == VIEW_MODE_ICON) + else if (VIEW_MODE_USES_ICON_VIEW (impl->view_mode)) view = impl->browse_files_icon_view; else g_assert_not_reached (); @@ -11984,7 +12050,7 @@ current_view_set_cursor (GtkFileChooserDefault *impl, GtkTreePath *path) { if (impl->view_mode == VIEW_MODE_LIST) gtk_tree_view_set_cursor (GTK_TREE_VIEW (impl->browse_files_tree_view), path, NULL, FALSE); - else if (impl->view_mode == VIEW_MODE_ICON) + else if (VIEW_MODE_USES_ICON_VIEW (impl->view_mode)) gtk_icon_view_set_cursor (GTK_ICON_VIEW (impl->browse_files_icon_view), path, NULL, FALSE); else g_assert_not_reached (); @@ -12003,7 +12069,7 @@ current_view_set_select_multiple (GtkFileChooserDefault *impl, gboolean select_m gtk_tree_selection_set_mode (selection, mode); gtk_tree_view_set_rubber_banding (GTK_TREE_VIEW (impl->browse_files_tree_view), select_multiple); } - else if (impl->view_mode == VIEW_MODE_ICON) + else if (VIEW_MODE_USES_ICON_VIEW (impl->view_mode)) gtk_icon_view_set_selection_mode (GTK_ICON_VIEW (impl->browse_files_icon_view), mode); else g_assert_not_reached (); diff --git a/gtk/gtkfilechooserprivate.h b/gtk/gtkfilechooserprivate.h index 8df1af951..dbce6aeb3 100644 --- a/gtk/gtkfilechooserprivate.h +++ b/gtk/gtkfilechooserprivate.h @@ -139,7 +139,8 @@ typedef enum { typedef enum { VIEW_MODE_LIST, - VIEW_MODE_ICON + VIEW_MODE_ICON, + VIEW_MODE_COMPACT } ViewMode; typedef enum { From 1706e775d656746018505f53fad57249f29c0909 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Tue, 19 May 2026 08:33:53 +0200 Subject: [PATCH 068/122] gtkfilechooser: add rename menu item --- gtk/gtkfilechooserdefault.c | 299 ++++++++++++++++++++++++++++++++++- gtk/gtkfilechooserprivate.h | 6 + gtk/gtkfilechoosersettings.c | 5 + 3 files changed, 302 insertions(+), 8 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 85f3f690c..29e30690a 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -286,6 +286,7 @@ static void gtk_file_chooser_default_get_property (GObject *ob GValue *value, GParamSpec *pspec); static void gtk_file_chooser_default_dispose (GObject *object); +static void rename_clear_state (GtkFileChooserDefault *impl); static void gtk_file_chooser_default_show_all (GtkWidget *widget); static void gtk_file_chooser_default_realize (GtkWidget *widget); static void gtk_file_chooser_default_map (GtkWidget *widget); @@ -929,6 +930,21 @@ gtk_file_chooser_default_finalize (GObject *object) g_free (impl->preview_display_name); + if (impl->rename_idle) + { + g_source_destroy (impl->rename_idle); + impl->rename_idle = NULL; + } + + if (impl->rename_file) + { + g_object_unref (impl->rename_file); + impl->rename_file = NULL; + } + + g_free (impl->rename_new_text); + g_free (impl->rename_old_text); + g_free (impl->edited_new_text); G_OBJECT_CLASS (_gtk_file_chooser_default_parent_class)->finalize (object); @@ -1049,6 +1065,17 @@ error_creating_folder_dialog (GtkFileChooserDefault *impl, file, error); } +/* Shows an error dialog about not being able to rename a file */ +static void +error_renaming_file_dialog (GtkFileChooserDefault *impl, + GFile *file, + GError *error) +{ + error_dialog (impl, + _("The file could not be renamed"), + file, error); +} + /* Shows an error about not being able to create a folder because a file with * the same name is already there. */ @@ -2397,6 +2424,129 @@ add_idle_while_impl_is_alive (GtkFileChooserDefault *impl, GCallback callback) return source; } +static GtkCellRenderer * +file_list_get_name_renderer (GtkFileChooserDefault *impl) +{ + if (impl->view_mode == VIEW_MODE_LIST) + { + GList *renderers; + GList *l; + GtkCellRenderer *renderer = NULL; + + if (impl->list_name_column == NULL) + return NULL; + + renderers = gtk_cell_layout_get_cells (GTK_CELL_LAYOUT (impl->list_name_column)); + + for (l = renderers; l; l = l->next) + { + if (GTK_IS_CELL_RENDERER_TEXT (l->data)) + { + renderer = GTK_CELL_RENDERER (l->data); + break; + } + } + + g_list_free (renderers); + return renderer; + } + + if (VIEW_MODE_USES_ICON_VIEW (impl->view_mode)) + return impl->list_name_renderer; + + return NULL; +} + +static void +rename_clear_state (GtkFileChooserDefault *impl) +{ + GtkCellRenderer *renderer; + + if (impl->rename_idle) + { + g_source_destroy (impl->rename_idle); + impl->rename_idle = NULL; + } + + if (impl->rename_file) + { + g_object_unref (impl->rename_file); + impl->rename_file = NULL; + } + + g_free (impl->rename_new_text); + impl->rename_new_text = NULL; + + g_free (impl->rename_old_text); + impl->rename_old_text = NULL; + + renderer = file_list_get_name_renderer (impl); + if (renderer) + g_object_set (renderer, "editable", FALSE, NULL); +} + +static gboolean +rename_idle_cb (GtkFileChooserDefault *impl) +{ + GFile *file; + gchar *new_text; + gchar *old_text; + GtkCellRenderer *renderer; + + GDK_THREADS_ENTER (); + + g_source_destroy (impl->rename_idle); + impl->rename_idle = NULL; + + file = impl->rename_file; + impl->rename_file = NULL; + + new_text = impl->rename_new_text; + impl->rename_new_text = NULL; + + old_text = impl->rename_old_text; + impl->rename_old_text = NULL; + + renderer = file_list_get_name_renderer (impl); + if (renderer) + g_object_set (renderer, "editable", FALSE, NULL); + + if (file && new_text && *new_text && + g_strcmp0 (new_text, old_text) != 0) + { + GError *error = NULL; + GFile *renamed; + + renamed = g_file_set_display_name (file, new_text, NULL, &error); + + if (renamed) + g_object_unref (renamed); + else + error_renaming_file_dialog (impl, file, error); + } + + if (file) + g_object_unref (file); + + g_free (new_text); + g_free (old_text); + + GDK_THREADS_LEAVE (); + + return FALSE; +} + +static void +queue_rename_idle (GtkFileChooserDefault *impl, + const gchar *new_text) +{ + if (!impl->rename_idle) + impl->rename_idle = add_idle_while_impl_is_alive (impl, G_CALLBACK (rename_idle_cb)); + + g_free (impl->rename_new_text); + impl->rename_new_text = g_strdup (new_text); +} + /* Idle handler for creating a new folder after editing its name cell, or for * canceling the editing. */ @@ -2464,14 +2614,18 @@ queue_edited_idle (GtkFileChooserDefault *impl, /* Callback used from the text cell renderer when the new folder is named */ static void renderer_edited_cb (GtkCellRendererText *cell_renderer_text, - const gchar *path, - const gchar *new_text, - GtkFileChooserDefault *impl) + const gchar *path, + const gchar *new_text, + GtkFileChooserDefault *impl) { /* work around bug #154921 */ g_object_set (cell_renderer_text, - "mode", GTK_CELL_RENDERER_MODE_INERT, NULL); - queue_edited_idle (impl, new_text); + "mode", GTK_CELL_RENDERER_MODE_INERT, NULL); + + if (impl->rename_file) + queue_rename_idle (impl, new_text); + else + queue_edited_idle (impl, new_text); } /* Callback used from the icon view text renderer to center editable text */ @@ -2490,12 +2644,16 @@ renderer_editing_started_cb (GtkCellRendererText *cell_renderer_text, */ static void renderer_editing_canceled_cb (GtkCellRendererText *cell_renderer_text, - GtkFileChooserDefault *impl) + GtkFileChooserDefault *impl) { /* work around bug #154921 */ g_object_set (cell_renderer_text, - "mode", GTK_CELL_RENDERER_MODE_INERT, NULL); - queue_edited_idle (impl, NULL); + "mode", GTK_CELL_RENDERER_MODE_INERT, NULL); + + if (impl->rename_file) + rename_clear_state (impl); + else + queue_edited_idle (impl, NULL); } /* Creates the widgets for the filter combo box */ @@ -3945,9 +4103,114 @@ popup_menu_detach_cb (GtkWidget *attach_widget, impl->browse_files_popup_menu = NULL; impl->browse_files_popup_menu_add_shortcut_item = NULL; impl->browse_files_popup_menu_copy_location_item = NULL; + impl->browse_files_popup_menu_rename_item = NULL; impl->browse_files_popup_menu_hidden_files_item = NULL; } +struct rename_selected_file_closure +{ + GFile *file; + GtkTreePath *path; + gchar *old_text; + guint count; +}; + +static void +rename_selected_file_foreach_cb (GtkTreeModel *model, + GtkTreePath *path, + GtkTreeIter *iter, + gpointer data) +{ + struct rename_selected_file_closure *closure = data; + + closure->count++; + + if (closure->count != 1) + return; + + gtk_tree_model_get (model, iter, + MODEL_COL_FILE, &closure->file, + MODEL_COL_NAME, &closure->old_text, + -1); + closure->path = gtk_tree_path_copy (path); +} + +static void +rename_selected_file (GtkFileChooserDefault *impl) +{ + struct rename_selected_file_closure closure; + GtkCellRenderer *renderer; + + if (impl->operation_mode != OPERATION_MODE_BROWSE) + return; + + if (current_selection_count_selected_rows (impl) != 1) + return; + + closure.file = NULL; + closure.path = NULL; + closure.old_text = NULL; + closure.count = 0; + + current_selection_selected_foreach (impl, + rename_selected_file_foreach_cb, + &closure); + + if (closure.count != 1 || closure.file == NULL || closure.path == NULL) + { + if (closure.file) + g_object_unref (closure.file); + if (closure.path) + gtk_tree_path_free (closure.path); + g_free (closure.old_text); + return; + } + + renderer = file_list_get_name_renderer (impl); + if (renderer == NULL) + { + g_object_unref (closure.file); + gtk_tree_path_free (closure.path); + g_free (closure.old_text); + return; + } + + rename_clear_state (impl); + impl->rename_file = closure.file; + impl->rename_old_text = closure.old_text; + + g_object_set (renderer, "editable", TRUE, NULL); + + if (impl->view_mode == VIEW_MODE_LIST) + { + gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (impl->browse_files_tree_view), + closure.path, impl->list_name_column, + FALSE, 0.0, 0.0); + gtk_tree_view_set_cursor_on_cell (GTK_TREE_VIEW (impl->browse_files_tree_view), + closure.path, + impl->list_name_column, + renderer, + TRUE); + gtk_tree_path_free (closure.path); + } + else if (VIEW_MODE_USES_ICON_VIEW (impl->view_mode)) + { + add_idle_to_edit_icon_view (impl, closure.path); + } + else + { + gtk_tree_path_free (closure.path); + g_assert_not_reached (); + } +} + +static void +rename_file_cb (GtkMenuItem *item, + GtkFileChooserDefault *impl) +{ + rename_selected_file (impl); +} + /* Callback used when the "Add to Bookmarks" menu item is activated */ static void add_to_shortcuts_cb (GtkMenuItem *item, @@ -4382,6 +4645,15 @@ file_list_build_popup_menu (GtkFileChooserDefault *impl) gtk_widget_show (item); gtk_menu_shell_append (GTK_MENU_SHELL (impl->browse_files_popup_menu), item); + item = gtk_image_menu_item_new_with_mnemonic (_("_Rename...")); + impl->browse_files_popup_menu_rename_item = item; + gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), + gtk_image_new_from_stock (GTK_STOCK_EDIT, GTK_ICON_SIZE_MENU)); + g_signal_connect (item, "activate", + G_CALLBACK (rename_file_cb), impl); + gtk_widget_show (item); + gtk_menu_shell_append (GTK_MENU_SHELL (impl->browse_files_popup_menu), item); + item = gtk_separator_menu_item_new (); gtk_widget_show (item); gtk_menu_shell_append (GTK_MENU_SHELL (impl->browse_files_popup_menu), item); @@ -4479,6 +4751,17 @@ file_list_update_popup_menu (GtkFileChooserDefault *impl) gtk_widget_set_sensitive (impl->browse_files_popup_menu_copy_location_item, current_selection_count_selected_rows (impl) > 0); + if (impl->browse_files_popup_menu_rename_item) + { + guint selected_count; + + selected_count = current_selection_count_selected_rows (impl); + + gtk_widget_set_sensitive (impl->browse_files_popup_menu_rename_item, + impl->operation_mode == OPERATION_MODE_BROWSE && + selected_count == 1); + } + /* 'Show Hidden Files' */ g_signal_handlers_block_by_func (impl->browse_files_popup_menu_hidden_files_item, G_CALLBACK (show_hidden_toggled_cb), impl); diff --git a/gtk/gtkfilechooserprivate.h b/gtk/gtkfilechooserprivate.h index dbce6aeb3..fca3fcad9 100644 --- a/gtk/gtkfilechooserprivate.h +++ b/gtk/gtkfilechooserprivate.h @@ -184,6 +184,7 @@ struct _GtkFileChooserDefault GtkWidget *browse_files_popup_menu; GtkWidget *browse_files_popup_menu_add_shortcut_item; GtkWidget *browse_files_popup_menu_copy_location_item; + GtkWidget *browse_files_popup_menu_rename_item; GtkWidget *browse_files_popup_menu_hidden_files_item; GtkWidget *browse_files_popup_menu_size_column_item; GtkWidget *browse_files_popup_menu_sort_by_name_item; @@ -296,6 +297,11 @@ struct _GtkFileChooserDefault GSource *edited_idle; char *edited_new_text; + GSource *rename_idle; + char *rename_new_text; + char *rename_old_text; + GFile *rename_file; + gulong settings_signal_id; int list_view_icon_size; int icon_view_icon_size; diff --git a/gtk/gtkfilechoosersettings.c b/gtk/gtkfilechoosersettings.c index ce34291ef..46db7f61b 100644 --- a/gtk/gtkfilechoosersettings.c +++ b/gtk/gtkfilechoosersettings.c @@ -65,6 +65,7 @@ #define MODE_LIST_VIEW "list-view" #define MODE_ICON_VIEW "icon-view" +#define MODE_COMPACT_VIEW "compact-view" #define EQ(a, b) (g_ascii_strcasecmp ((a), (b)) == 0) @@ -174,6 +175,8 @@ ensure_settings_read (GtkFileChooserSettings *settings) settings->view_mode = VIEW_MODE_LIST; else if (EQ (view_mode_str, MODE_ICON_VIEW)) settings->view_mode = VIEW_MODE_ICON; + else if (EQ (view_mode_str, MODE_COMPACT_VIEW)) + settings->view_mode = VIEW_MODE_COMPACT; else g_warning ("Unknown view mode '%s' encountered in filechooser settings", view_mode_str); @@ -477,6 +480,8 @@ _gtk_file_chooser_settings_save (GtkFileChooserSettings *settings, view_mode_str = MODE_LIST_VIEW; else if (settings->view_mode == VIEW_MODE_ICON) view_mode_str = MODE_ICON_VIEW; + else if (settings->view_mode == VIEW_MODE_COMPACT) + view_mode_str = MODE_COMPACT_VIEW; else { g_assert_not_reached (); From 2a67316003d5de41ea24707d5517b898722b7511 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Tue, 19 May 2026 09:53:59 +0200 Subject: [PATCH 069/122] gtkfilechooser: add preview panel toggle --- gtk/gtkfilechooserdefault.c | 333 ++++++++++++++++++++++++++++++++++-- gtk/gtkfilechooserprivate.h | 5 + 2 files changed, 322 insertions(+), 16 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 29e30690a..1258bd2d3 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -267,6 +267,7 @@ typedef enum { #define VIEW_MODE_USES_ICON_VIEW(mode) ((mode) == VIEW_MODE_ICON || (mode) == VIEW_MODE_COMPACT) #define PREVIEW_HBOX_SPACING 12 +#define PREVIEW_PANEL_WIDTH 240 #define NUM_LINES 45 #define NUM_CHARS 60 @@ -493,6 +494,7 @@ typedef struct { GtkTreeModelFilter parent; GtkFileChooserDefault *impl; + } ShortcutsPaneModelFilter; typedef struct { @@ -1181,24 +1183,33 @@ update_preview_widget_visibility (GtkFileChooserDefault *impl) if (impl->use_preview_label) { if (!impl->preview_label) - { - impl->preview_label = gtk_label_new (impl->preview_display_name); - gtk_box_pack_start (GTK_BOX (impl->preview_box), impl->preview_label, FALSE, FALSE, 0); - gtk_box_reorder_child (GTK_BOX (impl->preview_box), impl->preview_label, 0); - gtk_label_set_ellipsize (GTK_LABEL (impl->preview_label), PANGO_ELLIPSIZE_MIDDLE); - gtk_widget_show (impl->preview_label); - } + { + impl->preview_label = gtk_label_new (impl->preview_display_name); + gtk_box_pack_start (GTK_BOX (impl->preview_box), impl->preview_label, FALSE, FALSE, 0); + gtk_box_reorder_child (GTK_BOX (impl->preview_box), impl->preview_label, 0); + gtk_label_set_ellipsize (GTK_LABEL (impl->preview_label), PANGO_ELLIPSIZE_MIDDLE); + gtk_widget_show (impl->preview_label); + } } else { if (impl->preview_label) - { - gtk_widget_destroy (impl->preview_label); - impl->preview_label = NULL; - } + { + gtk_widget_destroy (impl->preview_label); + impl->preview_label = NULL; + } } - if (impl->preview_widget_active && impl->preview_widget) + if (impl->preview_panel) + { + if (impl->preview_panel_active) + gtk_widget_show (impl->preview_panel); + else + gtk_widget_hide (impl->preview_panel); + } + + if ((impl->preview_widget_active && impl->preview_widget) || + (impl->preview_panel_active && impl->preview_panel)) gtk_widget_show (impl->preview_box); else gtk_widget_hide (impl->preview_box); @@ -4105,6 +4116,7 @@ popup_menu_detach_cb (GtkWidget *attach_widget, impl->browse_files_popup_menu_copy_location_item = NULL; impl->browse_files_popup_menu_rename_item = NULL; impl->browse_files_popup_menu_hidden_files_item = NULL; + impl->browse_files_popup_menu_preview_panel_item = NULL; } struct rename_selected_file_closure @@ -4286,6 +4298,231 @@ copy_location_cb (GtkMenuItem *item, g_string_free (closure.locations, TRUE); } +struct preview_panel_update_closure +{ + GFile *file; + GFileInfo *info; + guint count; +}; + +static void +preview_panel_update_foreach_cb (GtkTreeModel *model, + GtkTreePath *path, + GtkTreeIter *iter, + gpointer data) +{ + struct preview_panel_update_closure *closure = data; + GFileInfo *info; + + closure->count++; + + if (closure->count != 1) + return; + + gtk_tree_model_get (model, iter, + MODEL_COL_FILE, &closure->file, + -1); + + if (GTK_IS_FILE_SYSTEM_MODEL (model)) + { + info = _gtk_file_system_model_get_info (GTK_FILE_SYSTEM_MODEL (model), iter); + if (info) + closure->info = g_file_info_dup (info); + } +} + +static gchar * +preview_panel_format_size (GFileInfo *info) +{ + if (info == NULL || + g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY || + !g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_STANDARD_SIZE)) + return g_strdup (_("Not calculated")); + +#if GLIB_CHECK_VERSION(2,30,0) + return g_format_size (g_file_info_get_size (info)); +#else + return g_format_size_for_display (g_file_info_get_size (info)); +#endif +} + +static gchar * +preview_panel_format_modified_time (GFileInfo *info) +{ + GDateTime *date_time; + gchar *formatted; + guint64 secs; + + if (info == NULL || + !g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_TIME_MODIFIED)) + return g_strdup (_("Unknown")); + + secs = g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED); + if (secs == 0) + return g_strdup (_("Unknown")); + + date_time = g_date_time_new_from_unix_local ((gint64) secs); + if (date_time == NULL) + return g_strdup (_("Unknown")); + + formatted = g_date_time_format (date_time, "%c"); + g_date_time_unref (date_time); + + return formatted ? formatted : g_strdup (_("Unknown")); +} + +static gchar * +preview_panel_get_type_name (GFileInfo *info) +{ + const gchar *content_type; + gchar *description; + + if (info == NULL) + return g_strdup (_("Unknown")); + + if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY) + return g_strdup (_("Folder")); + + content_type = g_file_info_get_content_type (info); + if (content_type == NULL) + return g_strdup (_("Unknown")); + + description = g_content_type_get_description (content_type); + return description ? description : g_strdup (content_type); +} + +static void +update_preview_panel (GtkFileChooserDefault *impl) +{ + struct preview_panel_update_closure closure; + GFile *parent_file; + gchar *name; + gchar *location; + gchar *type_name; + gchar *size; + gchar *modified; + gchar *details; + + if (!impl->preview_panel_info_label) + return; + + if (!impl->preview_panel_active) + return; + + closure.file = NULL; + closure.info = NULL; + closure.count = 0; + + if (current_selection_count_selected_rows (impl) == 1) + current_selection_selected_foreach (impl, + preview_panel_update_foreach_cb, + &closure); + + if (closure.count != 1 || closure.file == NULL) + { + gtk_label_set_text (GTK_LABEL (impl->preview_panel_info_label), + _("No file selected")); + + if (closure.file) + g_object_unref (closure.file); + + if (closure.info) + g_object_unref (closure.info); + + return; + } + + if (closure.info && g_file_info_get_display_name (closure.info)) + name = g_strdup (g_file_info_get_display_name (closure.info)); + else + name = g_file_get_basename (closure.file); + + parent_file = g_file_get_parent (closure.file); + if (parent_file) + { + location = g_file_get_parse_name (parent_file); + g_object_unref (parent_file); + } + else + location = g_file_get_parse_name (closure.file); + + type_name = preview_panel_get_type_name (closure.info); + size = preview_panel_format_size (closure.info); + modified = preview_panel_format_modified_time (closure.info); + + details = g_strdup_printf (_("Name: %s\nLocation: %s\nType: %s\nSize: %s\nModified: %s"), + name ? name : _("Unknown"), + location ? location : _("Unknown"), + type_name ? type_name : _("Unknown"), + size ? size : _("Unknown"), + modified ? modified : _("Unknown")); + + gtk_label_set_text (GTK_LABEL (impl->preview_panel_info_label), details); + + g_free (details); + g_free (modified); + g_free (size); + g_free (type_name); + g_free (location); + g_free (name); + + g_object_unref (closure.file); + + if (closure.info) + g_object_unref (closure.info); +} + +static void preview_panel_toggle_button_toggled_cb (GtkToggleButton *button, + GtkFileChooserDefault *impl); +static void show_preview_panel_toggled_cb (GtkCheckMenuItem *item, + GtkFileChooserDefault *impl); + +static void +preview_panel_set_active (GtkFileChooserDefault *impl, + gboolean active) +{ + impl->preview_panel_active = active; + + if (active) + update_preview_panel (impl); + + update_preview_widget_visibility (impl); + + if (impl->preview_panel_toggle_button) + { + g_signal_handlers_block_by_func (impl->preview_panel_toggle_button, + G_CALLBACK (preview_panel_toggle_button_toggled_cb), impl); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (impl->preview_panel_toggle_button), + active); + g_signal_handlers_unblock_by_func (impl->preview_panel_toggle_button, + G_CALLBACK (preview_panel_toggle_button_toggled_cb), impl); + } + + if (impl->browse_files_popup_menu_preview_panel_item) + { + g_signal_handlers_block_by_func (impl->browse_files_popup_menu_preview_panel_item, + G_CALLBACK (show_preview_panel_toggled_cb), impl); + gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (impl->browse_files_popup_menu_preview_panel_item), + active); + g_signal_handlers_unblock_by_func (impl->browse_files_popup_menu_preview_panel_item, + G_CALLBACK (show_preview_panel_toggled_cb), impl); + } +} + +static void +preview_panel_toggle_button_toggled_cb (GtkToggleButton *button, + GtkFileChooserDefault *impl) +{ + preview_panel_set_active (impl, gtk_toggle_button_get_active (button)); +} + +static void +show_preview_panel_toggled_cb (GtkCheckMenuItem *item, + GtkFileChooserDefault *impl) +{ + preview_panel_set_active (impl, gtk_check_menu_item_get_active (item)); +} + /* Callback used when the "Show Hidden Files" menu item is toggled */ static void show_hidden_toggled_cb (GtkCheckMenuItem *item, @@ -4665,6 +4902,14 @@ file_list_build_popup_menu (GtkFileChooserDefault *impl) gtk_widget_show (item); gtk_menu_shell_append (GTK_MENU_SHELL (impl->browse_files_popup_menu), item); + item = gtk_check_menu_item_new_with_mnemonic (_("Show _Preview Panel")); + impl->browse_files_popup_menu_preview_panel_item = item; + g_signal_connect (item, "toggled", + G_CALLBACK (show_preview_panel_toggled_cb), impl); + gtk_widget_show (item); + gtk_menu_shell_append (GTK_MENU_SHELL (impl->browse_files_popup_menu), item); + + if (impl->view_mode == VIEW_MODE_LIST) { item = gtk_check_menu_item_new_with_mnemonic (_("Show _Size Column")); @@ -4765,6 +5010,17 @@ file_list_update_popup_menu (GtkFileChooserDefault *impl) /* 'Show Hidden Files' */ g_signal_handlers_block_by_func (impl->browse_files_popup_menu_hidden_files_item, G_CALLBACK (show_hidden_toggled_cb), impl); + + if (impl->browse_files_popup_menu_preview_panel_item) + { + g_signal_handlers_block_by_func (impl->browse_files_popup_menu_preview_panel_item, + G_CALLBACK (show_preview_panel_toggled_cb), impl); + gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (impl->browse_files_popup_menu_preview_panel_item), + impl->preview_panel_active); + g_signal_handlers_unblock_by_func (impl->browse_files_popup_menu_preview_panel_item, + G_CALLBACK (show_preview_panel_toggled_cb), impl); + } + gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (impl->browse_files_popup_menu_hidden_files_item), impl->show_hidden); g_signal_handlers_unblock_by_func (impl->browse_files_popup_menu_hidden_files_item, @@ -5435,22 +5691,39 @@ file_pane_create (GtkFileChooserDefault *impl, vbox = gtk_vbox_new (FALSE, 6); gtk_widget_show (vbox); - /* Box for lists and preview */ + /* Paned box for file list and preview panel */ - hbox = gtk_hbox_new (FALSE, PREVIEW_HBOX_SPACING); + hbox = gtk_hpaned_new (); gtk_box_pack_start (GTK_BOX (vbox), hbox, TRUE, TRUE, 0); gtk_widget_show (hbox); /* File list */ widget = create_file_list (impl); - gtk_box_pack_start (GTK_BOX (hbox), widget, TRUE, TRUE, 0); + gtk_paned_pack1 (GTK_PANED (hbox), widget, TRUE, FALSE); gtk_size_group_add_widget (size_group, widget); /* Preview */ impl->preview_box = gtk_vbox_new (FALSE, 12); - gtk_box_pack_start (GTK_BOX (hbox), impl->preview_box, FALSE, FALSE, 0); + gtk_widget_set_size_request (impl->preview_box, PREVIEW_PANEL_WIDTH, -1); + gtk_paned_pack2 (GTK_PANED (hbox), impl->preview_box, FALSE, FALSE); + + impl->preview_panel = gtk_vbox_new (FALSE, 6); + gtk_widget_set_size_request (impl->preview_panel, PREVIEW_PANEL_WIDTH, -1); + impl->preview_panel_info_label = gtk_label_new (_("No file selected")); + gtk_widget_set_size_request (impl->preview_panel_info_label, PREVIEW_PANEL_WIDTH, -1); + gtk_misc_set_alignment (GTK_MISC (impl->preview_panel_info_label), 0.0, 0.0); + gtk_label_set_selectable (GTK_LABEL (impl->preview_panel_info_label), TRUE); + gtk_label_set_line_wrap (GTK_LABEL (impl->preview_panel_info_label), FALSE); + gtk_label_set_ellipsize (GTK_LABEL (impl->preview_panel_info_label), PANGO_ELLIPSIZE_MIDDLE); + gtk_box_pack_start (GTK_BOX (impl->preview_panel), + impl->preview_panel_info_label, FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (impl->preview_box), + impl->preview_panel, FALSE, FALSE, 0); + gtk_widget_show_all (impl->preview_panel); + gtk_widget_hide (impl->preview_panel); + /* Don't show preview box initially */ /* Filter combo */ @@ -6271,6 +6544,27 @@ icon_view_scale_create (GtkFileChooserDefault *impl) } /* Creates a toggle button for the location entry. */ +static void +preview_panel_toggle_button_create (GtkFileChooserDefault *impl) +{ + GtkWidget *image; + char *str; + + impl->preview_panel_toggle_button = gtk_toggle_button_new (); + image = gtk_image_new_from_stock (GTK_STOCK_INFO, GTK_ICON_SIZE_BUTTON); + gtk_container_add (GTK_CONTAINER (impl->preview_panel_toggle_button), image); + gtk_widget_show (image); + + g_signal_connect (impl->preview_panel_toggle_button, "toggled", + G_CALLBACK (preview_panel_toggle_button_toggled_cb), impl); + + str = _("Show Preview Panel"); + gtk_widget_set_tooltip_text (impl->preview_panel_toggle_button, str); + atk_object_set_name (gtk_widget_get_accessible (impl->preview_panel_toggle_button), str); + + gtk_widget_show (impl->preview_panel_toggle_button); +} + static void location_button_create (GtkFileChooserDefault *impl) { @@ -6416,6 +6710,10 @@ path_bar_widgets_create (GtkFileChooserDefault *impl) /* Icon view scale */ icon_view_scale_create (impl); + preview_panel_toggle_button_create (impl); + gtk_size_group_add_widget (impl->browse_path_bar_size_group, impl->preview_panel_toggle_button); + gtk_box_pack_end (GTK_BOX (impl->browse_path_bar_hbox), impl->preview_panel_toggle_button, FALSE, FALSE, 0); + gtk_box_pack_end (GTK_BOX (impl->browse_path_bar_hbox), impl->icon_view_scale_hbox, FALSE, FALSE, 0); /* Create Folder */ @@ -11379,6 +11677,9 @@ check_preview_change (GtkFileChooserDefault *impl) g_free (new_display_name); } + + + update_preview_panel (impl); } static void diff --git a/gtk/gtkfilechooserprivate.h b/gtk/gtkfilechooserprivate.h index fca3fcad9..ac6f5de31 100644 --- a/gtk/gtkfilechooserprivate.h +++ b/gtk/gtkfilechooserprivate.h @@ -186,6 +186,7 @@ struct _GtkFileChooserDefault GtkWidget *browse_files_popup_menu_copy_location_item; GtkWidget *browse_files_popup_menu_rename_item; GtkWidget *browse_files_popup_menu_hidden_files_item; + GtkWidget *browse_files_popup_menu_preview_panel_item; GtkWidget *browse_files_popup_menu_size_column_item; GtkWidget *browse_files_popup_menu_sort_by_name_item; GtkWidget *browse_files_popup_menu_sort_by_size_item; @@ -227,10 +228,13 @@ struct _GtkFileChooserDefault GtkWidget *preview_box; GtkWidget *preview_label; GtkWidget *preview_widget; + GtkWidget *preview_panel; + GtkWidget *preview_panel_info_label; GtkWidget *extra_align; GtkWidget *extra_widget; GtkWidget *view_mode_combo_box; + GtkWidget *preview_panel_toggle_button; GtkWidget *icon_view_scale_hbox; GtkWidget *icon_view_scale; GtkWidget *icon_view_scale_zoom_in_icon; @@ -326,6 +330,7 @@ struct _GtkFileChooserDefault guint local_only : 1; guint preview_widget_active : 1; + guint preview_panel_active : 1; guint use_preview_label : 1; guint select_multiple : 1; guint show_hidden : 1; From 281f231d73a148b32dd9e73489a60d4df8f89acb Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Tue, 19 May 2026 11:41:55 +0200 Subject: [PATCH 070/122] gtkfilechooser: improve preview panel --- gtk/gtkfilechooserdefault.c | 510 ++++++++++++++++++++++++++++++++++- gtk/gtkfilechooserprivate.h | 4 + gtk/gtkfilechoosersettings.c | 27 ++ gtk/gtkfilechoosersettings.h | 11 +- 4 files changed, 538 insertions(+), 14 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 1258bd2d3..1349e1a7c 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -268,6 +268,8 @@ typedef enum { #define PREVIEW_HBOX_SPACING 12 #define PREVIEW_PANEL_WIDTH 240 +#define PREVIEW_PANEL_IMAGE_MIN_SIZE 64 +#define PREVIEW_PANEL_IMAGE_MAX_SIZE 1024 #define NUM_LINES 45 #define NUM_CHARS 60 @@ -889,6 +891,12 @@ gtk_file_chooser_default_finalize (GObject *object) icon_view_scale_remove_timeout (impl); + if (impl->preview_panel_resize_timeout_id) + { + g_source_remove (impl->preview_panel_resize_timeout_id); + impl->preview_panel_resize_timeout_id = 0; + } + unset_file_system_backend (impl); if (impl->shortcuts_pane_filter_model) @@ -4391,19 +4399,228 @@ preview_panel_get_type_name (GFileInfo *info) return description ? description : g_strdup (content_type); } +static gint +preview_panel_get_image_size (GtkFileChooserDefault *impl) +{ + GtkAllocation allocation; + gint width; + gint list_height; + gint limit; + + if (impl->preview_box) + gtk_widget_get_allocation (impl->preview_box, &allocation); + else if (impl->preview_panel) + gtk_widget_get_allocation (impl->preview_panel, &allocation); + else + return PREVIEW_PANEL_WIDTH - 12; + + width = allocation.width; + if (width <= 1) + width = PREVIEW_PANEL_WIDTH; + + /* + * Use the file list height as the vertical limit. Do not use the + * preview panel height here: the preview image contributes to that + * height, which can create a resize feedback loop. + */ + list_height = 0; + if (impl->browse_files_scrolled_window) + { + gtk_widget_get_allocation (impl->browse_files_scrolled_window, &allocation); + list_height = allocation.height; + } + + limit = width - 12; + + if (list_height > 180) + limit = MIN (limit, list_height - 160); + else + limit = MIN (limit, PREVIEW_PANEL_WIDTH - 12); + + return CLAMP (limit, + PREVIEW_PANEL_IMAGE_MIN_SIZE, + PREVIEW_PANEL_IMAGE_MAX_SIZE); +} + +static void +preview_panel_clear_image (GtkFileChooserDefault *impl) +{ + if (!impl->preview_panel_image) + return; + + gtk_image_clear (GTK_IMAGE (impl->preview_panel_image)); + gtk_widget_hide (impl->preview_panel_image); + impl->preview_panel_last_image_size = 0; +} + +static GdkPixbuf * +preview_panel_scale_pixbuf_to_fit (GdkPixbuf *pixbuf, + gint size) +{ + GdkPixbuf *scaled; + gint width; + gint height; + gint scaled_width; + gint scaled_height; + gdouble scale; + + if (pixbuf == NULL) + return NULL; + + width = gdk_pixbuf_get_width (pixbuf); + height = gdk_pixbuf_get_height (pixbuf); + + if (width <= 0 || height <= 0) + return pixbuf; + + scale = MIN ((gdouble) size / width, (gdouble) size / height); + scaled_width = MAX (1, (gint) (width * scale + 0.5)); + scaled_height = MAX (1, (gint) (height * scale + 0.5)); + + if (scaled_width == width && scaled_height == height) + return pixbuf; + + scaled = gdk_pixbuf_scale_simple (pixbuf, + scaled_width, + scaled_height, + GDK_INTERP_BILINEAR); + g_object_unref (pixbuf); + + return scaled; +} + +static GdkPixbuf * +preview_panel_render_icon (GtkFileChooserDefault *impl, + GFileInfo *info, + gint image_size) +{ + GtkIconTheme *theme; + GtkIconInfo *icon_info = NULL; + GdkPixbuf *pixbuf = NULL; + GIcon *icon; + GError *error = NULL; + + if (info == NULL) + return NULL; + + if (!g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_STANDARD_ICON)) + return NULL; + + icon = g_file_info_get_icon (info); + if (icon == NULL) + return NULL; + + theme = gtk_icon_theme_get_for_screen (gtk_widget_get_screen (GTK_WIDGET (impl))); + +#ifdef GTK_ICON_LOOKUP_FORCE_SVG + icon_info = gtk_icon_theme_lookup_by_gicon (theme, icon, image_size, + GTK_ICON_LOOKUP_FORCE_SVG); +#endif + + if (icon_info == NULL) + icon_info = gtk_icon_theme_lookup_by_gicon (theme, icon, image_size, 0); + + if (icon_info) + { + pixbuf = gtk_icon_info_load_icon (icon_info, &error); + gtk_icon_info_free (icon_info); + + if (error) + { + g_error_free (error); + error = NULL; + } + } + + if (pixbuf == NULL) + pixbuf = _gtk_file_info_render_icon (info, GTK_WIDGET (impl), image_size); + + return preview_panel_scale_pixbuf_to_fit (pixbuf, image_size); +} + +static void +preview_panel_update_image (GtkFileChooserDefault *impl, + GFile *file, + GFileInfo *info) +{ + const gchar *content_type; + gchar *path; + GdkPixbuf *pixbuf = NULL; + GError *error = NULL; + gint image_size; + + if (!impl->preview_panel_image) + return; + + if (!file || !info) + { + preview_panel_clear_image (impl); + return; + } + + image_size = preview_panel_get_image_size (impl); + impl->preview_panel_last_image_size = image_size; + + content_type = g_file_info_get_content_type (info); + + if (content_type && g_str_has_prefix (content_type, "image/")) + { + path = g_file_get_path (file); + if (path) + { + pixbuf = gdk_pixbuf_new_from_file_at_scale (path, + image_size, + image_size, + TRUE, + &error); + g_free (path); + } + + if (error) + { + g_error_free (error); + error = NULL; + } + + pixbuf = preview_panel_scale_pixbuf_to_fit (pixbuf, image_size); + } + + if (pixbuf == NULL) + pixbuf = preview_panel_render_icon (impl, info, image_size); + + if (pixbuf == NULL) + { + preview_panel_clear_image (impl); + return; + } + + gtk_image_clear (GTK_IMAGE (impl->preview_panel_image)); + gtk_widget_queue_draw (impl->preview_panel_image); + + gtk_widget_set_size_request (impl->preview_panel_image, -1, -1); + gtk_image_set_from_pixbuf (GTK_IMAGE (impl->preview_panel_image), pixbuf); + gtk_widget_show (impl->preview_panel_image); + gtk_widget_queue_resize (impl->preview_panel_image); + gtk_widget_queue_draw (impl->preview_panel); + g_object_unref (pixbuf); +} + + static void update_preview_panel (GtkFileChooserDefault *impl) { struct preview_panel_update_closure closure; GFile *parent_file; gchar *name; + gchar *escaped_name; + gchar *title_markup; gchar *location; gchar *type_name; gchar *size; gchar *modified; gchar *details; - if (!impl->preview_panel_info_label) + if (!impl->preview_panel_title_label || !impl->preview_panel_info_label) return; if (!impl->preview_panel_active) @@ -4420,8 +4637,10 @@ update_preview_panel (GtkFileChooserDefault *impl) if (closure.count != 1 || closure.file == NULL) { - gtk_label_set_text (GTK_LABEL (impl->preview_panel_info_label), + gtk_label_set_text (GTK_LABEL (impl->preview_panel_title_label), _("No file selected")); + gtk_label_set_text (GTK_LABEL (impl->preview_panel_info_label), ""); + preview_panel_clear_image (impl); if (closure.file) g_object_unref (closure.file); @@ -4437,6 +4656,12 @@ update_preview_panel (GtkFileChooserDefault *impl) else name = g_file_get_basename (closure.file); + escaped_name = g_markup_escape_text (name ? name : _("Unknown"), -1); + title_markup = g_strdup_printf ("%s", escaped_name); + gtk_label_set_markup (GTK_LABEL (impl->preview_panel_title_label), title_markup); + + preview_panel_update_image (impl, closure.file, closure.info); + parent_file = g_file_get_parent (closure.file); if (parent_file) { @@ -4450,12 +4675,11 @@ update_preview_panel (GtkFileChooserDefault *impl) size = preview_panel_format_size (closure.info); modified = preview_panel_format_modified_time (closure.info); - details = g_strdup_printf (_("Name: %s\nLocation: %s\nType: %s\nSize: %s\nModified: %s"), - name ? name : _("Unknown"), - location ? location : _("Unknown"), + details = g_strdup_printf (_("Type: %s\nSize: %s\nModified: %s\nLocation: %s"), type_name ? type_name : _("Unknown"), size ? size : _("Unknown"), - modified ? modified : _("Unknown")); + modified ? modified : _("Unknown"), + location ? location : _("Unknown")); gtk_label_set_text (GTK_LABEL (impl->preview_panel_info_label), details); @@ -4464,6 +4688,8 @@ update_preview_panel (GtkFileChooserDefault *impl) g_free (size); g_free (type_name); g_free (location); + g_free (title_markup); + g_free (escaped_name); g_free (name); g_object_unref (closure.file); @@ -4472,6 +4698,56 @@ update_preview_panel (GtkFileChooserDefault *impl) g_object_unref (closure.info); } +static gboolean +preview_panel_resize_timeout_cb (gpointer data) +{ + GtkFileChooserDefault *impl = data; + gint image_size; + + impl->preview_panel_resize_timeout_id = 0; + + if (!impl->preview_panel_active || !impl->preview_panel_image) + return FALSE; + + image_size = preview_panel_get_image_size (impl); + if (image_size == impl->preview_panel_last_image_size) + return FALSE; + + update_preview_panel (impl); + + return FALSE; +} + +static void +preview_panel_queue_resize_update (GtkFileChooserDefault *impl) +{ + if (!impl->preview_panel_active) + return; + + if (impl->preview_panel_resize_timeout_id) + g_source_remove (impl->preview_panel_resize_timeout_id); + + impl->preview_panel_resize_timeout_id = + gdk_threads_add_timeout (35, preview_panel_resize_timeout_cb, impl); +} + +static void +preview_panel_size_allocate_cb (GtkWidget *widget, + GtkAllocation *allocation, + GtkFileChooserDefault *impl) +{ + gint image_size; + + if (!impl->preview_panel_active) + return; + + image_size = preview_panel_get_image_size (impl); + + if (ABS (image_size - impl->preview_panel_last_image_size) >= 8) + preview_panel_queue_resize_update (impl); +} + + static void preview_panel_toggle_button_toggled_cb (GtkToggleButton *button, GtkFileChooserDefault *impl); static void show_preview_panel_toggled_cb (GtkCheckMenuItem *item, @@ -4483,6 +4759,12 @@ preview_panel_set_active (GtkFileChooserDefault *impl, { impl->preview_panel_active = active; + if (!active && impl->preview_panel_resize_timeout_id) + { + g_source_remove (impl->preview_panel_resize_timeout_id); + impl->preview_panel_resize_timeout_id = 0; + } + if (active) update_preview_panel (impl); @@ -4523,6 +4805,8 @@ show_preview_panel_toggled_cb (GtkCheckMenuItem *item, preview_panel_set_active (impl, gtk_check_menu_item_get_active (item)); } + + /* Callback used when the "Show Hidden Files" menu item is toggled */ static void show_hidden_toggled_cb (GtkCheckMenuItem *item, @@ -5026,6 +5310,7 @@ file_list_update_popup_menu (GtkFileChooserDefault *impl) g_signal_handlers_unblock_by_func (impl->browse_files_popup_menu_hidden_files_item, G_CALLBACK (show_hidden_toggled_cb), impl); + if (impl->view_mode == VIEW_MODE_LIST) { /* 'Show Size Column' */ @@ -5709,16 +5994,42 @@ file_pane_create (GtkFileChooserDefault *impl, gtk_widget_set_size_request (impl->preview_box, PREVIEW_PANEL_WIDTH, -1); gtk_paned_pack2 (GTK_PANED (hbox), impl->preview_box, FALSE, FALSE); - impl->preview_panel = gtk_vbox_new (FALSE, 6); + impl->preview_panel = gtk_vbox_new (FALSE, 8); gtk_widget_set_size_request (impl->preview_panel, PREVIEW_PANEL_WIDTH, -1); - impl->preview_panel_info_label = gtk_label_new (_("No file selected")); + gtk_container_set_border_width (GTK_CONTAINER (impl->preview_panel), 6); + g_signal_connect (impl->preview_panel, "size-allocate", + G_CALLBACK (preview_panel_size_allocate_cb), impl); + + + impl->preview_panel_image = gtk_image_new (); + gtk_widget_set_size_request (impl->preview_panel_image, -1, -1); + gtk_misc_set_alignment (GTK_MISC (impl->preview_panel_image), 0.5, 0.0); + gtk_box_pack_start (GTK_BOX (impl->preview_panel), + impl->preview_panel_image, FALSE, FALSE, 0); + + impl->preview_panel_title_label = gtk_label_new (_("No file selected")); + gtk_widget_set_size_request (impl->preview_panel_title_label, PREVIEW_PANEL_WIDTH, -1); + gtk_misc_set_alignment (GTK_MISC (impl->preview_panel_title_label), 0.0, 0.0); + gtk_label_set_selectable (GTK_LABEL (impl->preview_panel_title_label), TRUE); + gtk_label_set_ellipsize (GTK_LABEL (impl->preview_panel_title_label), PANGO_ELLIPSIZE_MIDDLE); + gtk_box_pack_start (GTK_BOX (impl->preview_panel), + impl->preview_panel_title_label, FALSE, FALSE, 0); + + widget = gtk_frame_new (NULL); + gtk_frame_set_shadow_type (GTK_FRAME (widget), GTK_SHADOW_ETCHED_IN); + gtk_widget_set_size_request (widget, -1, 2); + gtk_widget_show (widget); + gtk_box_pack_start (GTK_BOX (impl->preview_panel), widget, FALSE, FALSE, 0); + + impl->preview_panel_info_label = gtk_label_new (""); gtk_widget_set_size_request (impl->preview_panel_info_label, PREVIEW_PANEL_WIDTH, -1); gtk_misc_set_alignment (GTK_MISC (impl->preview_panel_info_label), 0.0, 0.0); gtk_label_set_selectable (GTK_LABEL (impl->preview_panel_info_label), TRUE); - gtk_label_set_line_wrap (GTK_LABEL (impl->preview_panel_info_label), FALSE); - gtk_label_set_ellipsize (GTK_LABEL (impl->preview_panel_info_label), PANGO_ELLIPSIZE_MIDDLE); + gtk_label_set_line_wrap (GTK_LABEL (impl->preview_panel_info_label), TRUE); + gtk_label_set_width_chars (GTK_LABEL (impl->preview_panel_info_label), 28); gtk_box_pack_start (GTK_BOX (impl->preview_panel), impl->preview_panel_info_label, FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (impl->preview_box), impl->preview_panel, FALSE, FALSE, 0); gtk_widget_show_all (impl->preview_panel); @@ -5973,16 +6284,187 @@ location_toggle_popup_handler (GtkFileChooserDefault *impl) } } +static void +view_mode_selected_files_free (GSList *files) +{ + g_slist_foreach (files, (GFunc) g_object_unref, NULL); + g_slist_free (files); +} + +static GSList * +view_mode_get_selected_files (GtkFileChooserDefault *impl, + ViewMode view_mode) +{ + GSList *files = NULL; + + if (!impl->current_model) + return NULL; + + if (view_mode == VIEW_MODE_LIST && impl->browse_files_tree_view) + { + GtkTreeSelection *selection; + GList *rows; + GList *l; + + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (impl->browse_files_tree_view)); + rows = gtk_tree_selection_get_selected_rows (selection, NULL); + + for (l = rows; l; l = l->next) + { + GtkTreePath *path = l->data; + GtkTreeIter iter; + GFile *file = NULL; + + if (gtk_tree_model_get_iter (impl->current_model, &iter, path)) + gtk_tree_model_get (impl->current_model, &iter, + MODEL_COL_FILE, &file, + -1); + + if (file) + files = g_slist_prepend (files, file); + } + + g_list_foreach (rows, (GFunc) gtk_tree_path_free, NULL); + g_list_free (rows); + } + else if (VIEW_MODE_USES_ICON_VIEW (view_mode) && impl->browse_files_icon_view) + { + GList *rows; + GList *l; + + rows = gtk_icon_view_get_selected_items (GTK_ICON_VIEW (impl->browse_files_icon_view)); + + for (l = rows; l; l = l->next) + { + GtkTreePath *path = l->data; + GtkTreeIter iter; + GFile *file = NULL; + + if (gtk_tree_model_get_iter (impl->current_model, &iter, path)) + gtk_tree_model_get (impl->current_model, &iter, + MODEL_COL_FILE, &file, + -1); + + if (file) + files = g_slist_prepend (files, file); + } + + g_list_foreach (rows, (GFunc) gtk_tree_path_free, NULL); + g_list_free (rows); + } + + return files; +} + +static gboolean +view_mode_file_is_selected (GSList *files, + GFile *file) +{ + GSList *l; + + if (!file) + return FALSE; + + for (l = files; l; l = l->next) + { + if (g_file_equal (G_FILE (l->data), file)) + return TRUE; + } + + return FALSE; +} + +static void +view_mode_restore_selected_files (GtkFileChooserDefault *impl, + GSList *files) +{ + GtkTreeIter iter; + GtkTreePath *first_path = NULL; + gboolean valid; + + if (!files || !impl->current_model) + return; + + current_selection_unselect_all (impl); + + valid = gtk_tree_model_get_iter_first (impl->current_model, &iter); + + while (valid) + { + GFile *file = NULL; + + gtk_tree_model_get (impl->current_model, &iter, + MODEL_COL_FILE, &file, + -1); + + if (view_mode_file_is_selected (files, file)) + { + GtkTreePath *path; + + path = gtk_tree_model_get_path (impl->current_model, &iter); + + if (impl->view_mode == VIEW_MODE_LIST) + { + GtkTreeSelection *selection; + + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (impl->browse_files_tree_view)); + gtk_tree_selection_select_path (selection, path); + + if (!first_path) + gtk_tree_view_set_cursor (GTK_TREE_VIEW (impl->browse_files_tree_view), + path, NULL, FALSE); + } + else if (VIEW_MODE_USES_ICON_VIEW (impl->view_mode)) + { + gtk_icon_view_select_path (GTK_ICON_VIEW (impl->browse_files_icon_view), path); + + if (!first_path) + gtk_icon_view_set_cursor (GTK_ICON_VIEW (impl->browse_files_icon_view), + path, NULL, FALSE); + } + else + { + gtk_tree_path_free (path); + g_assert_not_reached (); + } + + if (!first_path) + first_path = gtk_tree_path_copy (path); + + gtk_tree_path_free (path); + } + + if (file) + g_object_unref (file); + + valid = gtk_tree_model_iter_next (impl->current_model, &iter); + } + + if (first_path) + { + if (impl->view_mode == VIEW_MODE_LIST) + gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (impl->browse_files_tree_view), + first_path, NULL, FALSE, 0.0, 0.0); + else if (VIEW_MODE_USES_ICON_VIEW (impl->view_mode)) + gtk_icon_view_scroll_to_path (GTK_ICON_VIEW (impl->browse_files_icon_view), + first_path, FALSE, 0.0, 0.0); + + gtk_tree_path_free (first_path); + } +} + static void view_mode_set (GtkFileChooserDefault *impl, ViewMode view_mode) { GtkWidget *old_view; ViewMode old_view_mode = impl->view_mode; + GSList *selected_files; if (old_view_mode == view_mode) return; old_view = impl->browse_files_current_view; + selected_files = view_mode_get_selected_files (impl, old_view_mode); impl->view_mode = view_mode; gtk_combo_box_set_active (GTK_COMBO_BOX (impl->view_mode_combo_box), @@ -6005,6 +6487,8 @@ view_mode_set (GtkFileChooserDefault *impl, ViewMode view_mode) /* Set model and selection */ current_view_set_file_model (impl, impl->current_model); current_view_set_select_multiple (impl, impl->select_multiple); + view_mode_restore_selected_files (impl, selected_files); + view_mode_selected_files_free (selected_files); copy_old_selection_to_current_view (impl, old_view_mode); if (view_mode == VIEW_MODE_ICON) @@ -6543,7 +7027,7 @@ icon_view_scale_create (GtkFileChooserDefault *impl) G_CALLBACK (icon_view_scale_value_changed_cb), impl); } -/* Creates a toggle button for the location entry. */ +/* Creates a toggle button for the preview panel. */ static void preview_panel_toggle_button_create (GtkFileChooserDefault *impl) { @@ -7833,6 +8317,8 @@ settings_load (GtkFileChooserDefault *impl) view_mode = _gtk_file_chooser_settings_get_view_mode (settings); icon_view_scale = _gtk_file_chooser_settings_get_icon_view_scale (settings); show_hidden = _gtk_file_chooser_settings_get_show_hidden (settings); + preview_panel_set_active (impl, + _gtk_file_chooser_settings_get_show_preview_panel (settings)); show_size_column = _gtk_file_chooser_settings_get_show_size_column (settings); sort_column = _gtk_file_chooser_settings_get_sort_column (settings); sort_order = _gtk_file_chooser_settings_get_sort_order (settings); @@ -7903,6 +8389,7 @@ settings_save (GtkFileChooserDefault *impl) _gtk_file_chooser_settings_set_view_mode (settings, impl->view_mode); _gtk_file_chooser_settings_set_icon_view_scale (settings, impl->icon_view_icon_size); _gtk_file_chooser_settings_set_show_hidden (settings, gtk_file_chooser_get_show_hidden (GTK_FILE_CHOOSER (impl))); + _gtk_file_chooser_settings_set_show_preview_panel (settings, impl->preview_panel_active); _gtk_file_chooser_settings_set_show_size_column (settings, impl->show_size_column); _gtk_file_chooser_settings_set_sort_column (settings, impl->sort_column); _gtk_file_chooser_settings_set_sort_order (settings, impl->sort_order); @@ -12328,6 +12815,7 @@ show_hidden_handler (GtkFileChooserDefault *impl) g_object_set (impl, "show-hidden", !impl->show_hidden, NULL); + } diff --git a/gtk/gtkfilechooserprivate.h b/gtk/gtkfilechooserprivate.h index ac6f5de31..93002e597 100644 --- a/gtk/gtkfilechooserprivate.h +++ b/gtk/gtkfilechooserprivate.h @@ -229,7 +229,11 @@ struct _GtkFileChooserDefault GtkWidget *preview_label; GtkWidget *preview_widget; GtkWidget *preview_panel; + GtkWidget *preview_panel_image; + GtkWidget *preview_panel_title_label; GtkWidget *preview_panel_info_label; + guint preview_panel_resize_timeout_id; + gint preview_panel_last_image_size; GtkWidget *extra_align; GtkWidget *extra_widget; diff --git a/gtk/gtkfilechoosersettings.c b/gtk/gtkfilechoosersettings.c index 46db7f61b..9acf2bdd5 100644 --- a/gtk/gtkfilechoosersettings.c +++ b/gtk/gtkfilechoosersettings.c @@ -42,6 +42,7 @@ #define VIEW_MODE_KEY "ViewMode" #define ICON_VIEW_SCALE_KEY "IconViewScale" #define SHOW_HIDDEN_KEY "ShowHidden" +#define SHOW_PREVIEW_PANEL_KEY "ShowPreviewPanel" #define SHOW_SIZE_COLUMN_KEY "ShowSizeColumn" #define GEOMETRY_X_KEY "GeometryX" #define GEOMETRY_Y_KEY "GeometryY" @@ -197,6 +198,15 @@ ensure_settings_read (GtkFileChooserSettings *settings) else settings->show_hidden = value != FALSE; + /* Show preview panel */ + + value = g_key_file_get_boolean (key_file, SETTINGS_GROUP, + SHOW_PREVIEW_PANEL_KEY, &error); + if (error) + warn_if_invalid_key_and_clear_error (SHOW_PREVIEW_PANEL_KEY, &error); + else + settings->show_preview_panel = value != FALSE; + /* Show size column */ value = g_key_file_get_boolean (key_file, SETTINGS_GROUP, @@ -290,6 +300,7 @@ _gtk_file_chooser_settings_init (GtkFileChooserSettings *settings) settings->sort_order = GTK_SORT_ASCENDING; settings->sort_column = FILE_LIST_COL_NAME; settings->show_hidden = FALSE; + settings->show_preview_panel = FALSE; settings->show_size_column = TRUE; settings->geometry_x = -1; settings->geometry_y = -1; @@ -360,6 +371,20 @@ _gtk_file_chooser_settings_set_show_hidden (GtkFileChooserSettings *settings, settings->show_hidden = show_hidden != FALSE; } +gboolean +_gtk_file_chooser_settings_get_show_preview_panel (GtkFileChooserSettings *settings) +{ + ensure_settings_read (settings); + return settings->show_preview_panel; +} + +void +_gtk_file_chooser_settings_set_show_preview_panel (GtkFileChooserSettings *settings, + gboolean show_preview_panel) +{ + settings->show_preview_panel = show_preview_panel != FALSE; +} + void _gtk_file_chooser_settings_set_show_size_column (GtkFileChooserSettings *settings, gboolean show_column) @@ -550,6 +575,8 @@ _gtk_file_chooser_settings_save (GtkFileChooserSettings *settings, ICON_VIEW_SCALE_KEY, settings->icon_view_scale); g_key_file_set_boolean (key_file, SETTINGS_GROUP, SHOW_HIDDEN_KEY, settings->show_hidden); + g_key_file_set_boolean (key_file, SETTINGS_GROUP, + SHOW_PREVIEW_PANEL_KEY, settings->show_preview_panel); g_key_file_set_boolean (key_file, SETTINGS_GROUP, SHOW_SIZE_COLUMN_KEY, settings->show_size_column); g_key_file_set_integer (key_file, SETTINGS_GROUP, diff --git a/gtk/gtkfilechoosersettings.h b/gtk/gtkfilechoosersettings.h index b987fca8c..f8861a78e 100644 --- a/gtk/gtkfilechoosersettings.h +++ b/gtk/gtkfilechoosersettings.h @@ -56,9 +56,10 @@ struct _GtkFileChooserSettings int geometry_width; int geometry_height; - guint settings_read : 1; - guint show_hidden : 1; - guint show_size_column : 1; + guint settings_read : 1; + guint show_hidden : 1; + guint show_preview_panel : 1; + guint show_size_column : 1; }; struct _GtkFileChooserSettingsClass @@ -86,6 +87,10 @@ gboolean _gtk_file_chooser_settings_get_show_hidden (GtkFileChooserSettings *set void _gtk_file_chooser_settings_set_show_hidden (GtkFileChooserSettings *settings, gboolean show_hidden); +gboolean _gtk_file_chooser_settings_get_show_preview_panel (GtkFileChooserSettings *settings); +void _gtk_file_chooser_settings_set_show_preview_panel (GtkFileChooserSettings *settings, + gboolean show_preview_panel); + gboolean _gtk_file_chooser_settings_get_show_size_column (GtkFileChooserSettings *settings); void _gtk_file_chooser_settings_set_show_size_column (GtkFileChooserSettings *settings, gboolean show_column); From dc0b67f1e26c6c7a32c819c7235c2dcf6ffcc7a1 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Tue, 19 May 2026 13:11:13 +0200 Subject: [PATCH 071/122] gtkfilechooser: add cached icon view tooltips --- gtk/gtkfilechooserdefault.c | 234 +++++++++++++++++++++++++++++++++++- gtk/gtkfilechooserprivate.h | 6 + 2 files changed, 238 insertions(+), 2 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 1349e1a7c..7552047d4 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -259,8 +259,8 @@ typedef enum { #define ICON_VIEW_ITEM_WIDTH 128 #define COMPACT_VIEW_ITEM_WIDTH 180 #define COMPACT_VIEW_SPACING 4 -#define COMPACT_VIEW_ROW_SPACING 0 -#define COMPACT_VIEW_COLUMN_SPACING 12 +#define COMPACT_VIEW_ROW_SPACING 1 +#define COMPACT_VIEW_COLUMN_SPACING 20 #define COMPACT_VIEW_MARGIN 2 #define ICON_VIEW_SCALE_MAX_VISIBLE_OVERSCAN 128 @@ -891,6 +891,16 @@ gtk_file_chooser_default_finalize (GObject *object) icon_view_scale_remove_timeout (impl); + if (impl->icon_tooltip_path) + { + gtk_tree_path_free (impl->icon_tooltip_path); + impl->icon_tooltip_path = NULL; + } + + g_free (impl->icon_tooltip_text); + impl->icon_tooltip_text = NULL; + + if (impl->preview_panel_resize_timeout_id) { g_source_remove (impl->preview_panel_resize_timeout_id); @@ -5591,6 +5601,220 @@ file_list_query_tooltip_cb (GtkWidget *widget, return TRUE; } + + + + + + +static void +file_icon_view_tooltip_cache_clear (GtkFileChooserDefault *impl) +{ + if (impl->icon_tooltip_path) + { + gtk_tree_path_free (impl->icon_tooltip_path); + impl->icon_tooltip_path = NULL; + } + + g_free (impl->icon_tooltip_text); + impl->icon_tooltip_text = NULL; + impl->icon_tooltip_width = 0; + impl->icon_tooltip_visible = FALSE; +} + +static gboolean +file_name_needs_tooltip (GtkFileChooserDefault *impl, + const gchar *name, + gint available_width) +{ + PangoLayout *layout; + gint width; + + if (name == NULL || available_width <= 0) + return FALSE; + + layout = gtk_widget_create_pango_layout (impl->browse_files_current_view, name); + pango_layout_get_pixel_size (layout, &width, NULL); + g_object_unref (layout); + + return width > available_width; +} + +static gint +file_icon_view_get_tooltip_width (GtkFileChooserDefault *impl) +{ + gint text_xpad = 0; + gint width; + + if (impl->list_name_renderer) + gtk_cell_renderer_get_padding (impl->list_name_renderer, &text_xpad, NULL); + + if (impl->view_mode == VIEW_MODE_COMPACT) + { + gint icon_size; + gint icon_xpad = 0; + gint icon_cell_width; + + icon_size = impl->list_view_icon_size; + if (icon_size <= 0) + icon_size = FALLBACK_LIST_VIEW_ICON_SIZE; + + if (impl->list_icon_renderer) + gtk_cell_renderer_get_padding (impl->list_icon_renderer, &icon_xpad, NULL); + + icon_cell_width = icon_xpad * 2 + icon_size; + width = COMPACT_VIEW_ITEM_WIDTH - icon_cell_width - COMPACT_VIEW_SPACING; + } + else if (impl->view_mode == VIEW_MODE_ICON) + { + gint icon_size; + gint icon_xpad = 0; + gint item_width; + + icon_size = impl->icon_view_icon_size; + if (icon_size <= 0) + icon_size = FALLBACK_ICON_VIEW_ICON_SIZE; + + if (impl->list_icon_renderer) + gtk_cell_renderer_get_padding (impl->list_icon_renderer, &icon_xpad, NULL); + + item_width = MAX (ICON_VIEW_ITEM_WIDTH, icon_size + icon_xpad * 2); + width = item_width; + } + else + return 0; + + /* + * GtkCellRendererText still has its own padding and ellipsize threshold. + * Be conservative so that names shortened by only a few characters still + * get a tooltip. + */ + width -= text_xpad * 2; + width -= 12; + + return MAX (1, width); +} + +static gboolean +file_icon_view_tooltip_cache_matches (GtkFileChooserDefault *impl, + GtkTreePath *path, + gint available_width) +{ + return impl->icon_tooltip_path && + gtk_tree_path_compare (impl->icon_tooltip_path, path) == 0 && + impl->icon_tooltip_width == available_width && + impl->icon_tooltip_view_mode == impl->view_mode && + impl->icon_tooltip_operation_mode == impl->operation_mode; +} + +static void +file_icon_view_tooltip_cache_set (GtkFileChooserDefault *impl, + GtkTreePath *path, + gint available_width, + gboolean visible, + const gchar *tooltip_text) +{ + file_icon_view_tooltip_cache_clear (impl); + + impl->icon_tooltip_path = gtk_tree_path_copy (path); + impl->icon_tooltip_width = available_width; + impl->icon_tooltip_view_mode = impl->view_mode; + impl->icon_tooltip_operation_mode = impl->operation_mode; + impl->icon_tooltip_visible = visible; + + if (tooltip_text) + impl->icon_tooltip_text = g_strdup (tooltip_text); +} + +static gboolean +file_icon_view_query_tooltip_cb (GtkWidget *widget, + gint x, + gint y, + gboolean keyboard_tip, + GtkTooltip *tooltip, + gpointer user_data) +{ + GtkFileChooserDefault *impl = user_data; + GtkTreeModel *model; + GtkTreePath *path; + GtkTreeIter iter; + gint available_width; + gchar *display_name; + gchar *tooltip_text; + + if (!gtk_icon_view_get_tooltip_context (GTK_ICON_VIEW (impl->browse_files_icon_view), + &x, &y, + keyboard_tip, + &model, &path, &iter)) + return FALSE; + + available_width = file_icon_view_get_tooltip_width (impl); + + if (file_icon_view_tooltip_cache_matches (impl, path, available_width)) + { + if (!impl->icon_tooltip_visible) + { + gtk_tree_path_free (path); + return FALSE; + } + + gtk_tooltip_set_text (tooltip, impl->icon_tooltip_text); + gtk_icon_view_set_tooltip_item (GTK_ICON_VIEW (impl->browse_files_icon_view), + tooltip, + path); + gtk_tree_path_free (path); + return TRUE; + } + + gtk_tree_model_get (model, &iter, + MODEL_COL_NAME, &display_name, + -1); + + if (!file_name_needs_tooltip (impl, display_name, available_width)) + { + file_icon_view_tooltip_cache_set (impl, path, available_width, FALSE, NULL); + g_free (display_name); + gtk_tree_path_free (path); + return FALSE; + } + + if (impl->operation_mode == OPERATION_MODE_BROWSE) + { + tooltip_text = display_name; + } + else + { + GFile *file = NULL; + + gtk_tree_model_get (model, &iter, + MODEL_COL_FILE, &file, + -1); + + if (file) + { + tooltip_text = g_file_get_parse_name (file); + g_object_unref (file); + g_free (display_name); + } + else + { + tooltip_text = display_name; + } + } + + file_icon_view_tooltip_cache_set (impl, path, available_width, TRUE, tooltip_text); + + gtk_tooltip_set_text (tooltip, tooltip_text); + gtk_icon_view_set_tooltip_item (GTK_ICON_VIEW (impl->browse_files_icon_view), + tooltip, + path); + + g_free (tooltip_text); + gtk_tree_path_free (path); + + return TRUE; +} + static void set_icon_cell_renderer_fixed_size (GtkFileChooserDefault *impl, GtkCellRenderer *renderer, @@ -5659,6 +5883,8 @@ update_icon_view_cell_layout (GtkFileChooserDefault *impl) if (!impl->browse_files_icon_view) return; + file_icon_view_tooltip_cache_clear (impl); + if (impl->view_mode == VIEW_MODE_COMPACT) { GtkIconView *icon_view; @@ -5887,6 +6113,10 @@ create_browse_files_icon_view (GtkFileChooserDefault *impl) g_signal_connect (impl->browse_files_icon_view, "scroll-event", G_CALLBACK (browse_files_icon_view_scroll_event_cb), impl); + g_object_set (impl->browse_files_icon_view, "has-tooltip", TRUE, NULL); + g_signal_connect (impl->browse_files_icon_view, "query-tooltip", + G_CALLBACK (file_icon_view_query_tooltip_cb), impl); + gtk_drag_dest_set (impl->browse_files_icon_view, GTK_DEST_DEFAULT_ALL, NULL, 0, diff --git a/gtk/gtkfilechooserprivate.h b/gtk/gtkfilechooserprivate.h index 93002e597..19c27608f 100644 --- a/gtk/gtkfilechooserprivate.h +++ b/gtk/gtkfilechooserprivate.h @@ -181,6 +181,12 @@ struct _GtkFileChooserDefault GtkWidget *browse_files_scrolled_window; GtkWidget *browse_files_current_view; GtkWidget *browse_files_icon_view; + GtkTreePath *icon_tooltip_path; + gchar *icon_tooltip_text; + gint icon_tooltip_width; + ViewMode icon_tooltip_view_mode; + OperationMode icon_tooltip_operation_mode; + guint icon_tooltip_visible : 1; GtkWidget *browse_files_popup_menu; GtkWidget *browse_files_popup_menu_add_shortcut_item; GtkWidget *browse_files_popup_menu_copy_location_item; From 7b0f4f4b8a2a22394e2d7bee2912dd1e985350fb Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Tue, 19 May 2026 14:17:20 +0200 Subject: [PATCH 072/122] gtkfilechooser: improve file tooltips --- gtk/gtkfilechooserdefault.c | 253 +++++++++++++++++++++++++++++++----- gtk/gtkfilechooserprivate.h | 6 + 2 files changed, 223 insertions(+), 36 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 7552047d4..3305499e4 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -887,6 +887,18 @@ static void gtk_file_chooser_default_finalize (GObject *object) { GtkFileChooserDefault *impl = GTK_FILE_CHOOSER_DEFAULT (object); + + if (impl->list_tooltip_path) + { + gtk_tree_path_free (impl->list_tooltip_path); + impl->list_tooltip_path = NULL; + } + + impl->list_tooltip_model = NULL; + + g_free (impl->list_tooltip_text); + impl->list_tooltip_text = NULL; + GSList *l; icon_view_scale_remove_timeout (impl); @@ -5553,6 +5565,112 @@ file_list_set_sort_column_ids (GtkFileChooserDefault *impl) } } +static gboolean file_name_needs_tooltip (GtkFileChooserDefault *impl, + const gchar *name, + gint available_width); + +static void +file_list_tooltip_cache_clear (GtkFileChooserDefault *impl) +{ + if (impl->list_tooltip_path) + { + gtk_tree_path_free (impl->list_tooltip_path); + impl->list_tooltip_path = NULL; + } + + impl->list_tooltip_model = NULL; + + g_free (impl->list_tooltip_text); + impl->list_tooltip_text = NULL; + impl->list_tooltip_width = 0; + impl->list_tooltip_visible = FALSE; +} + +static gint +file_list_get_tooltip_width (GtkFileChooserDefault *impl) +{ + GtkCellRenderer *text_renderer = NULL; + GList *cells; + GList *l; + gint width = 0; + gint start_pos = 0; + gint text_xpad = 0; + + if (!impl->list_name_column) + return 0; + + cells = gtk_cell_layout_get_cells (GTK_CELL_LAYOUT (impl->list_name_column)); + + for (l = cells; l; l = l->next) + { + if (GTK_IS_CELL_RENDERER_TEXT (l->data)) + { + text_renderer = GTK_CELL_RENDERER (l->data); + break; + } + } + + if (text_renderer && + gtk_tree_view_column_cell_get_position (impl->list_name_column, + text_renderer, + &start_pos, + &width)) + { + gtk_cell_renderer_get_padding (text_renderer, &text_xpad, NULL); + width -= text_xpad * 2; + width -= 12; + } + else + { + gint icon_size; + + width = gtk_tree_view_column_get_width (impl->list_name_column); + + icon_size = impl->list_view_icon_size; + if (icon_size <= 0) + icon_size = FALLBACK_LIST_VIEW_ICON_SIZE; + + width -= icon_size + 16; + } + + g_list_free (cells); + + return MAX (1, width); +} + +static gboolean +file_list_tooltip_cache_matches (GtkFileChooserDefault *impl, + GtkTreeModel *model, + GtkTreePath *path, + gint available_width) +{ + return impl->list_tooltip_path && + impl->list_tooltip_model == model && + gtk_tree_path_compare (impl->list_tooltip_path, path) == 0 && + impl->list_tooltip_width == available_width && + impl->list_tooltip_operation_mode == impl->operation_mode; +} + +static void +file_list_tooltip_cache_set (GtkFileChooserDefault *impl, + GtkTreeModel *model, + GtkTreePath *path, + gint available_width, + gboolean visible, + const gchar *tooltip_text) +{ + file_list_tooltip_cache_clear (impl); + + impl->list_tooltip_path = gtk_tree_path_copy (path); + impl->list_tooltip_model = model; + impl->list_tooltip_width = available_width; + impl->list_tooltip_operation_mode = impl->operation_mode; + impl->list_tooltip_visible = visible; + + if (tooltip_text) + impl->list_tooltip_text = g_strdup (tooltip_text); +} + static gboolean file_list_query_tooltip_cb (GtkWidget *widget, gint x, @@ -5562,40 +5680,94 @@ file_list_query_tooltip_cb (GtkWidget *widget, gpointer user_data) { GtkFileChooserDefault *impl = user_data; + GtkTreeView *tree_view; + GtkTreeViewColumn *column = NULL; GtkTreeModel *model; GtkTreePath *path; GtkTreeIter iter; - GFile *file; - char *parse_name; + gchar *display_name; + gchar *tooltip_text; + gint available_width; - if (impl->operation_mode == OPERATION_MODE_BROWSE) + tree_view = GTK_TREE_VIEW (impl->browse_files_tree_view); + + if (!gtk_tree_view_get_path_at_pos (tree_view, x, y, NULL, &column, NULL, NULL)) return FALSE; + if (column != impl->list_name_column) + return FALSE; - if (!gtk_tree_view_get_tooltip_context (GTK_TREE_VIEW (impl->browse_files_tree_view), + if (!gtk_tree_view_get_tooltip_context (tree_view, &x, &y, keyboard_tip, &model, &path, &iter)) return FALSE; + available_width = file_list_get_tooltip_width (impl); + + if (file_list_tooltip_cache_matches (impl, model, path, available_width)) + { + if (!impl->list_tooltip_visible) + { + gtk_tree_path_free (path); + return FALSE; + } + + gtk_tooltip_set_text (tooltip, impl->list_tooltip_text); + gtk_tree_view_set_tooltip_row (tree_view, tooltip, path); + gtk_tree_path_free (path); + return TRUE; + } + + if (impl->operation_mode != OPERATION_MODE_BROWSE) + { + GFile *file = NULL; + + gtk_tree_model_get (model, &iter, + MODEL_COL_FILE, &file, + -1); + + if (!file) + { + file_list_tooltip_cache_set (impl, model, path, available_width, FALSE, NULL); + gtk_tree_path_free (path); + return FALSE; + } + + tooltip_text = g_file_get_parse_name (file); + g_object_unref (file); + + file_list_tooltip_cache_set (impl, model, path, available_width, TRUE, tooltip_text); + + gtk_tooltip_set_text (tooltip, tooltip_text); + gtk_tree_view_set_tooltip_row (tree_view, tooltip, path); + + g_free (tooltip_text); + gtk_tree_path_free (path); + + return TRUE; + } + gtk_tree_model_get (model, &iter, - MODEL_COL_FILE, &file, + MODEL_COL_NAME, &display_name, -1); - if (file == NULL) + if (!file_name_needs_tooltip (impl, display_name, available_width)) { + file_list_tooltip_cache_set (impl, model, path, available_width, FALSE, NULL); + g_free (display_name); gtk_tree_path_free (path); return FALSE; } - parse_name = g_file_get_parse_name (file); - gtk_tooltip_set_text (tooltip, parse_name); - gtk_tree_view_set_tooltip_row (GTK_TREE_VIEW (impl->browse_files_tree_view), - tooltip, - path); + tooltip_text = display_name; - g_free (parse_name); - g_object_unref (file); + file_list_tooltip_cache_set (impl, model, path, available_width, TRUE, tooltip_text); + + gtk_tooltip_set_text (tooltip, tooltip_text); + gtk_tree_view_set_tooltip_row (tree_view, tooltip, path); + + g_free (tooltip_text); gtk_tree_path_free (path); return TRUE; @@ -5766,6 +5938,37 @@ file_icon_view_query_tooltip_cb (GtkWidget *widget, return TRUE; } + if (impl->operation_mode != OPERATION_MODE_BROWSE) + { + GFile *file = NULL; + + gtk_tree_model_get (model, &iter, + MODEL_COL_FILE, &file, + -1); + + if (!file) + { + file_icon_view_tooltip_cache_set (impl, path, available_width, FALSE, NULL); + gtk_tree_path_free (path); + return FALSE; + } + + tooltip_text = g_file_get_parse_name (file); + g_object_unref (file); + + file_icon_view_tooltip_cache_set (impl, path, available_width, TRUE, tooltip_text); + + gtk_tooltip_set_text (tooltip, tooltip_text); + gtk_icon_view_set_tooltip_item (GTK_ICON_VIEW (impl->browse_files_icon_view), + tooltip, + path); + + g_free (tooltip_text); + gtk_tree_path_free (path); + + return TRUE; + } + gtk_tree_model_get (model, &iter, MODEL_COL_NAME, &display_name, -1); @@ -5778,29 +5981,7 @@ file_icon_view_query_tooltip_cb (GtkWidget *widget, return FALSE; } - if (impl->operation_mode == OPERATION_MODE_BROWSE) - { - tooltip_text = display_name; - } - else - { - GFile *file = NULL; - - gtk_tree_model_get (model, &iter, - MODEL_COL_FILE, &file, - -1); - - if (file) - { - tooltip_text = g_file_get_parse_name (file); - g_object_unref (file); - g_free (display_name); - } - else - { - tooltip_text = display_name; - } - } + tooltip_text = display_name; file_icon_view_tooltip_cache_set (impl, path, available_width, TRUE, tooltip_text); diff --git a/gtk/gtkfilechooserprivate.h b/gtk/gtkfilechooserprivate.h index 19c27608f..8f1062f4e 100644 --- a/gtk/gtkfilechooserprivate.h +++ b/gtk/gtkfilechooserprivate.h @@ -187,6 +187,12 @@ struct _GtkFileChooserDefault ViewMode icon_tooltip_view_mode; OperationMode icon_tooltip_operation_mode; guint icon_tooltip_visible : 1; + GtkTreePath *list_tooltip_path; + GtkTreeModel *list_tooltip_model; + gchar *list_tooltip_text; + gint list_tooltip_width; + OperationMode list_tooltip_operation_mode; + guint list_tooltip_visible : 1; GtkWidget *browse_files_popup_menu; GtkWidget *browse_files_popup_menu_add_shortcut_item; GtkWidget *browse_files_popup_menu_copy_location_item; From a87dbdb9a9ac71c0f4816819ae2af953bdc6e01a Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 20 May 2026 10:28:23 +0200 Subject: [PATCH 073/122] gtkfilechooser: improve file chooser tooltips --- gtk/gtkfilechooserdefault.c | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 3305499e4..81c12408a 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -887,6 +887,7 @@ static void gtk_file_chooser_default_finalize (GObject *object) { GtkFileChooserDefault *impl = GTK_FILE_CHOOSER_DEFAULT (object); + GSList *l; if (impl->list_tooltip_path) { @@ -899,8 +900,6 @@ gtk_file_chooser_default_finalize (GObject *object) g_free (impl->list_tooltip_text); impl->list_tooltip_text = NULL; - GSList *l; - icon_view_scale_remove_timeout (impl); if (impl->icon_tooltip_path) @@ -5618,7 +5617,13 @@ file_list_get_tooltip_width (GtkFileChooserDefault *impl) { gtk_cell_renderer_get_padding (text_renderer, &text_xpad, NULL); width -= text_xpad * 2; - width -= 12; + + /* Match GtkCellRendererText's practical ellipsize threshold more + * closely. gtk_tree_view_column_cell_get_position() plus renderer + * padding is still slightly wider than the text area where the + * renderer starts drawing an ellipsis. + */ + width -= 2; } else { @@ -5800,16 +5805,20 @@ file_name_needs_tooltip (GtkFileChooserDefault *impl, gint available_width) { PangoLayout *layout; - gint width; + gboolean ellipsized; if (name == NULL || available_width <= 0) return FALSE; layout = gtk_widget_create_pango_layout (impl->browse_files_current_view, name); - pango_layout_get_pixel_size (layout, &width, NULL); + pango_layout_set_width (layout, available_width * PANGO_SCALE); + pango_layout_set_ellipsize (layout, PANGO_ELLIPSIZE_END); + + ellipsized = pango_layout_is_ellipsized (layout); + g_object_unref (layout); - return width > available_width; + return ellipsized; } static gint @@ -5856,13 +5865,7 @@ file_icon_view_get_tooltip_width (GtkFileChooserDefault *impl) else return 0; - /* - * GtkCellRendererText still has its own padding and ellipsize threshold. - * Be conservative so that names shortened by only a few characters still - * get a tooltip. - */ width -= text_xpad * 2; - width -= 12; return MAX (1, width); } @@ -6877,6 +6880,9 @@ view_mode_set (GtkFileChooserDefault *impl, ViewMode view_mode) old_view = impl->browse_files_current_view; selected_files = view_mode_get_selected_files (impl, old_view_mode); + file_list_tooltip_cache_clear (impl); + file_icon_view_tooltip_cache_clear (impl); + impl->view_mode = view_mode; gtk_combo_box_set_active (GTK_COMBO_BOX (impl->view_mode_combo_box), view_mode); @@ -8104,6 +8110,9 @@ operation_mode_set (GtkFileChooserDefault *impl, OperationMode mode) { ShortcutsIndex shortcut_to_select; + file_list_tooltip_cache_clear (impl); + file_icon_view_tooltip_cache_clear (impl); + operation_mode_stop (impl, impl->operation_mode); impl->operation_mode = mode; @@ -13505,6 +13514,9 @@ current_view_set_file_model (GtkFileChooserDefault *impl, GtkTreeModel *model) { GtkWidget *view; + file_list_tooltip_cache_clear (impl); + file_icon_view_tooltip_cache_clear (impl); + impl->current_model = model; if (impl->view_mode == VIEW_MODE_LIST) From fb60703d211f3eeecadd551fdb86d01b4b9ad017 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 20 May 2026 10:41:05 +0200 Subject: [PATCH 074/122] gtkfilechooser: improve preview panel details --- gtk/gtkfilechooserdefault.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 81c12408a..bd9a15c52 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -4696,7 +4696,8 @@ update_preview_panel (GtkFileChooserDefault *impl) size = preview_panel_format_size (closure.info); modified = preview_panel_format_modified_time (closure.info); - details = g_strdup_printf (_("Type: %s\nSize: %s\nModified: %s\nLocation: %s"), + details = g_strdup_printf (_("Name: %s\nType: %s\nSize: %s\nModified: %s\nLocation: %s"), + name ? name : _("Unknown"), type_name ? type_name : _("Unknown"), size ? size : _("Unknown"), modified ? modified : _("Unknown"), From aa46d1cd22dbb1428d1a918095bf3f5798374016 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 20 May 2026 10:54:16 +0200 Subject: [PATCH 075/122] gtkfilechooser: tune compact view spacing --- gtk/gtkfilechooserdefault.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index bd9a15c52..81ffa682e 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -257,10 +257,10 @@ typedef enum { #define FALLBACK_ICON_VIEW_ICON_SIZE 48 #define ICON_VIEW_ITEM_WIDTH 128 -#define COMPACT_VIEW_ITEM_WIDTH 180 +#define COMPACT_VIEW_ITEM_WIDTH 170 #define COMPACT_VIEW_SPACING 4 #define COMPACT_VIEW_ROW_SPACING 1 -#define COMPACT_VIEW_COLUMN_SPACING 20 +#define COMPACT_VIEW_COLUMN_SPACING 16 #define COMPACT_VIEW_MARGIN 2 #define ICON_VIEW_SCALE_MAX_VISIBLE_OVERSCAN 128 From 7974c984906c97f29dc3768f37e2e78bc8b250e1 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 20 May 2026 12:09:01 +0200 Subject: [PATCH 076/122] gtkfilechooser: query metadata for thumbnail generation --- gtk/gtkfilechooserdefault.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 81ffa682e..a973f3d39 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -9543,6 +9543,9 @@ file_system_model_got_thumbnail (GObject *object, GAsyncResult *res, gpointer da copy_attribute (info, queried, G_FILE_ATTRIBUTE_THUMBNAIL_PATH); copy_attribute (info, queried, G_FILE_ATTRIBUTE_THUMBNAILING_FAILED); copy_attribute (info, queried, G_FILE_ATTRIBUTE_STANDARD_ICON); + copy_attribute (info, queried, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE); + copy_attribute (info, queried, G_FILE_ATTRIBUTE_STANDARD_SIZE); + copy_attribute (info, queried, G_FILE_ATTRIBUTE_TIME_MODIFIED); _gtk_file_system_model_update_file (model, file, info); @@ -9686,7 +9689,10 @@ file_system_model_set (GtkFileSystemModel *model, g_file_query_info_async (file, G_FILE_ATTRIBUTE_THUMBNAIL_PATH "," G_FILE_ATTRIBUTE_THUMBNAILING_FAILED "," - G_FILE_ATTRIBUTE_STANDARD_ICON, + G_FILE_ATTRIBUTE_STANDARD_ICON "," + G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE "," + G_FILE_ATTRIBUTE_STANDARD_SIZE "," + G_FILE_ATTRIBUTE_TIME_MODIFIED, G_FILE_QUERY_INFO_NONE, G_PRIORITY_DEFAULT, _gtk_file_system_model_get_cancellable (model), From bd62ff6717af301141cf500d513bbcf0d41f80e3 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 20 May 2026 12:27:57 +0200 Subject: [PATCH 077/122] gtkfilechooser: query metadata for thumbnail generation --- gtk/gtkfilechooserdefault.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index a973f3d39..73af9fe51 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -9690,9 +9690,9 @@ file_system_model_set (GtkFileSystemModel *model, G_FILE_ATTRIBUTE_THUMBNAIL_PATH "," G_FILE_ATTRIBUTE_THUMBNAILING_FAILED "," G_FILE_ATTRIBUTE_STANDARD_ICON "," - G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE "," - G_FILE_ATTRIBUTE_STANDARD_SIZE "," - G_FILE_ATTRIBUTE_TIME_MODIFIED, + G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE "," + G_FILE_ATTRIBUTE_STANDARD_SIZE "," + G_FILE_ATTRIBUTE_TIME_MODIFIED, G_FILE_QUERY_INFO_NONE, G_PRIORITY_DEFAULT, _gtk_file_system_model_get_cancellable (model), From 0bd9ec1e5e0f59e9086b9a4a46e4ec7e0efeea2b Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 20 May 2026 12:35:50 +0200 Subject: [PATCH 078/122] gtkfilechooser: add thumbnail queue state --- gtk/gtkfilechooserdefault.c | 38 +++++++++++++++++++++++++++++++++++++ gtk/gtkfilechooserprivate.h | 11 +++++++++++ 2 files changed, 49 insertions(+) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 73af9fe51..c67fea5d2 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -883,6 +883,36 @@ pending_select_files_add (GtkFileChooserDefault *impl, g_slist_prepend (impl->pending_select_files, g_object_ref (file)); } +static void +thumbnail_queue_clear (GtkFileChooserDefault *impl) +{ + if (impl->thumbnail_queue_idle_id) + { + g_source_remove (impl->thumbnail_queue_idle_id); + impl->thumbnail_queue_idle_id = 0; + } + + if (impl->thumbnail_cancellable) + { + g_cancellable_cancel (impl->thumbnail_cancellable); + g_object_unref (impl->thumbnail_cancellable); + impl->thumbnail_cancellable = NULL; + } + + g_slist_foreach (impl->thumbnail_queue, (GFunc) g_object_unref, NULL); + g_slist_free (impl->thumbnail_queue); + impl->thumbnail_queue = NULL; + + if (impl->thumbnail_queued_files) + { + g_hash_table_destroy (impl->thumbnail_queued_files); + impl->thumbnail_queued_files = NULL; + } + + impl->thumbnail_worker_running = FALSE; + impl->thumbnail_generation_serial++; +} + static void gtk_file_chooser_default_finalize (GObject *object) { @@ -902,6 +932,8 @@ gtk_file_chooser_default_finalize (GObject *object) icon_view_scale_remove_timeout (impl); + thumbnail_queue_clear (impl); + if (impl->icon_tooltip_path) { gtk_tree_path_free (impl->icon_tooltip_path); @@ -6884,6 +6916,8 @@ view_mode_set (GtkFileChooserDefault *impl, ViewMode view_mode) file_list_tooltip_cache_clear (impl); file_icon_view_tooltip_cache_clear (impl); + thumbnail_queue_clear (impl); + impl->view_mode = view_mode; gtk_combo_box_set_active (GTK_COMBO_BOX (impl->view_mode_combo_box), view_mode); @@ -9787,6 +9821,8 @@ set_list_model (GtkFileChooserDefault *impl, profile_start ("start", NULL); + thumbnail_queue_clear (impl); + stop_loading_and_clear_list_model (impl, TRUE); set_busy_cursor (impl, TRUE); @@ -13524,6 +13560,8 @@ current_view_set_file_model (GtkFileChooserDefault *impl, GtkTreeModel *model) file_list_tooltip_cache_clear (impl); file_icon_view_tooltip_cache_clear (impl); + thumbnail_queue_clear (impl); + impl->current_model = model; if (impl->view_mode == VIEW_MODE_LIST) diff --git a/gtk/gtkfilechooserprivate.h b/gtk/gtkfilechooserprivate.h index 8f1062f4e..2d1e11f70 100644 --- a/gtk/gtkfilechooserprivate.h +++ b/gtk/gtkfilechooserprivate.h @@ -262,6 +262,17 @@ struct _GtkFileChooserDefault gint icon_view_scale_visible_overscan; ViewMode view_mode; + /* Visible-only thumbnail generation state. Thumbnail workers are + * cancellable so changing folders or views never leaves stale decode work + * running for files that are no longer useful to the user. + */ + GSList *thumbnail_queue; + GHashTable *thumbnail_queued_files; + GCancellable *thumbnail_cancellable; + guint thumbnail_queue_idle_id; + guint thumbnail_generation_serial; + guint thumbnail_worker_running : 1; + GtkWidget *location_button; GtkWidget *location_entry_box; GtkWidget *location_label; From 67820cc624b1d36e4808e84f34ea908720e104b3 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 20 May 2026 13:24:50 +0200 Subject: [PATCH 079/122] gtkfilechooser: add thumbnail toggle setting --- gtk/gtkfilechooserdefault.c | 93 +++++++++++++++++++++++++++++++++++- gtk/gtkfilechooserprivate.h | 2 + gtk/gtkfilechoosersettings.c | 27 +++++++++++ gtk/gtkfilechoosersettings.h | 5 ++ 4 files changed, 126 insertions(+), 1 deletion(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index c67fea5d2..f49104d16 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -787,6 +787,7 @@ _gtk_file_chooser_default_init (GtkFileChooserDefault *impl) impl->use_preview_label = TRUE; impl->select_multiple = FALSE; impl->show_hidden = FALSE; + impl->show_thumbnails = TRUE; impl->show_size_column = TRUE; impl->list_view_icon_size = FALLBACK_LIST_VIEW_ICON_SIZE; impl->icon_view_icon_size = FALLBACK_ICON_VIEW_ICON_SIZE; @@ -913,6 +914,37 @@ thumbnail_queue_clear (GtkFileChooserDefault *impl) impl->thumbnail_generation_serial++; } +static void +thumbnail_file_list_clear_pixbuf_caches (GtkFileChooserDefault *impl) +{ + if (impl->browse_files_model) + { + _gtk_file_system_model_clear_cache (impl->browse_files_model, + MODEL_COL_LIST_PIXBUF); + _gtk_file_system_model_clear_cache (impl->browse_files_model, + MODEL_COL_ICON_PIXBUF); + } + + if (impl->search_model) + { + _gtk_file_system_model_clear_cache (impl->search_model, + MODEL_COL_LIST_PIXBUF); + _gtk_file_system_model_clear_cache (impl->search_model, + MODEL_COL_ICON_PIXBUF); + } + + if (impl->recent_model) + { + _gtk_file_system_model_clear_cache (impl->recent_model, + MODEL_COL_LIST_PIXBUF); + _gtk_file_system_model_clear_cache (impl->recent_model, + MODEL_COL_ICON_PIXBUF); + } + + if (impl->browse_files_current_view) + gtk_widget_queue_draw (impl->browse_files_current_view); +} + static void gtk_file_chooser_default_finalize (GObject *object) { @@ -4861,6 +4893,23 @@ show_preview_panel_toggled_cb (GtkCheckMenuItem *item, +/* Callback used when the "Show Thumbnails" menu item is toggled */ +static void +show_thumbnails_toggled_cb (GtkCheckMenuItem *item, + GtkFileChooserDefault *impl) +{ + gboolean show_thumbnails; + + show_thumbnails = gtk_check_menu_item_get_active (item); + if (impl->show_thumbnails == show_thumbnails) + return; + + impl->show_thumbnails = show_thumbnails; + + thumbnail_queue_clear (impl); + thumbnail_file_list_clear_pixbuf_caches (impl); +} + /* Callback used when the "Show Hidden Files" menu item is toggled */ static void show_hidden_toggled_cb (GtkCheckMenuItem *item, @@ -5247,6 +5296,13 @@ file_list_build_popup_menu (GtkFileChooserDefault *impl) gtk_widget_show (item); gtk_menu_shell_append (GTK_MENU_SHELL (impl->browse_files_popup_menu), item); + item = gtk_check_menu_item_new_with_mnemonic (_("Show _Thumbnails")); + impl->browse_files_popup_menu_thumbnails_item = item; + g_signal_connect (item, "toggled", + G_CALLBACK (show_thumbnails_toggled_cb), impl); + gtk_widget_show (item); + gtk_menu_shell_append (GTK_MENU_SHELL (impl->browse_files_popup_menu), item); + if (impl->view_mode == VIEW_MODE_LIST) { @@ -5364,6 +5420,15 @@ file_list_update_popup_menu (GtkFileChooserDefault *impl) g_signal_handlers_unblock_by_func (impl->browse_files_popup_menu_hidden_files_item, G_CALLBACK (show_hidden_toggled_cb), impl); + if (impl->browse_files_popup_menu_thumbnails_item) + { + g_signal_handlers_block_by_func (impl->browse_files_popup_menu_thumbnails_item, + G_CALLBACK (show_thumbnails_toggled_cb), impl); + gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (impl->browse_files_popup_menu_thumbnails_item), + impl->show_thumbnails); + g_signal_handlers_unblock_by_func (impl->browse_files_popup_menu_thumbnails_item, + G_CALLBACK (show_thumbnails_toggled_cb), impl); + } if (impl->view_mode == VIEW_MODE_LIST) { @@ -8761,6 +8826,7 @@ settings_load (GtkFileChooserDefault *impl) LocationMode location_mode; ViewMode view_mode; gboolean show_hidden; + gboolean show_thumbnails; gboolean show_size_column; gint sort_column, icon_view_scale; GtkSortType sort_order; @@ -8774,6 +8840,7 @@ settings_load (GtkFileChooserDefault *impl) show_hidden = _gtk_file_chooser_settings_get_show_hidden (settings); preview_panel_set_active (impl, _gtk_file_chooser_settings_get_show_preview_panel (settings)); + show_thumbnails = _gtk_file_chooser_settings_get_show_thumbnails (settings); show_size_column = _gtk_file_chooser_settings_get_show_size_column (settings); sort_column = _gtk_file_chooser_settings_get_sort_column (settings); sort_order = _gtk_file_chooser_settings_get_sort_order (settings); @@ -8800,6 +8867,7 @@ settings_load (GtkFileChooserDefault *impl) gtk_file_chooser_set_show_hidden (GTK_FILE_CHOOSER (impl), show_hidden); + impl->show_thumbnails = show_thumbnails; impl->show_size_column = show_size_column; if (impl->list_size_column) gtk_tree_view_column_set_visible (impl->list_size_column, show_size_column); @@ -8845,6 +8913,7 @@ settings_save (GtkFileChooserDefault *impl) _gtk_file_chooser_settings_set_icon_view_scale (settings, impl->icon_view_icon_size); _gtk_file_chooser_settings_set_show_hidden (settings, gtk_file_chooser_get_show_hidden (GTK_FILE_CHOOSER (impl))); _gtk_file_chooser_settings_set_show_preview_panel (settings, impl->preview_panel_active); + _gtk_file_chooser_settings_set_show_thumbnails (settings, impl->show_thumbnails); _gtk_file_chooser_settings_set_show_size_column (settings, impl->show_size_column); _gtk_file_chooser_settings_set_sort_column (settings, impl->sort_column); _gtk_file_chooser_settings_set_sort_order (settings, impl->sort_order); @@ -9589,6 +9658,28 @@ file_system_model_got_thumbnail (GObject *object, GAsyncResult *res, gpointer da GDK_THREADS_LEAVE (); } +static GdkPixbuf * +file_system_model_render_icon (GtkFileChooserDefault *impl, + GFileInfo *info, + gint icon_size) +{ + GFileInfo *icon_info; + GdkPixbuf *pixbuf; + + if (impl->show_thumbnails || + !g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_THUMBNAIL_PATH)) + return _gtk_file_info_render_icon (info, GTK_WIDGET (impl), icon_size); + + icon_info = g_file_info_dup (info); + g_file_info_remove_attribute (icon_info, G_FILE_ATTRIBUTE_THUMBNAIL_PATH); + + pixbuf = _gtk_file_info_render_icon (icon_info, GTK_WIDGET (impl), icon_size); + + g_object_unref (icon_info); + + return pixbuf; +} + static gboolean get_visible_range (GtkTreePath **start, GtkTreePath **end, GtkFileChooserDefault *impl) @@ -9694,7 +9785,7 @@ file_system_model_set (GtkFileSystemModel *model, icon_size = FALLBACK_LIST_VIEW_ICON_SIZE; } - g_value_take_object (value, _gtk_file_info_render_icon (info, GTK_WIDGET (impl), icon_size)); + g_value_take_object (value, file_system_model_render_icon (impl, info, icon_size)); return TRUE; } diff --git a/gtk/gtkfilechooserprivate.h b/gtk/gtkfilechooserprivate.h index 2d1e11f70..49762da1c 100644 --- a/gtk/gtkfilechooserprivate.h +++ b/gtk/gtkfilechooserprivate.h @@ -199,6 +199,7 @@ struct _GtkFileChooserDefault GtkWidget *browse_files_popup_menu_rename_item; GtkWidget *browse_files_popup_menu_hidden_files_item; GtkWidget *browse_files_popup_menu_preview_panel_item; + GtkWidget *browse_files_popup_menu_thumbnails_item; GtkWidget *browse_files_popup_menu_size_column_item; GtkWidget *browse_files_popup_menu_sort_by_name_item; GtkWidget *browse_files_popup_menu_sort_by_size_item; @@ -361,6 +362,7 @@ struct _GtkFileChooserDefault guint use_preview_label : 1; guint select_multiple : 1; guint show_hidden : 1; + guint show_thumbnails : 1; guint do_overwrite_confirmation : 1; guint list_sort_ascending : 1; guint changing_folder : 1; diff --git a/gtk/gtkfilechoosersettings.c b/gtk/gtkfilechoosersettings.c index 9acf2bdd5..ceca18535 100644 --- a/gtk/gtkfilechoosersettings.c +++ b/gtk/gtkfilechoosersettings.c @@ -43,6 +43,7 @@ #define ICON_VIEW_SCALE_KEY "IconViewScale" #define SHOW_HIDDEN_KEY "ShowHidden" #define SHOW_PREVIEW_PANEL_KEY "ShowPreviewPanel" +#define SHOW_THUMBNAILS_KEY "ShowThumbnails" #define SHOW_SIZE_COLUMN_KEY "ShowSizeColumn" #define GEOMETRY_X_KEY "GeometryX" #define GEOMETRY_Y_KEY "GeometryY" @@ -207,6 +208,15 @@ ensure_settings_read (GtkFileChooserSettings *settings) else settings->show_preview_panel = value != FALSE; + /* Show thumbnails */ + + value = g_key_file_get_boolean (key_file, SETTINGS_GROUP, + SHOW_THUMBNAILS_KEY, &error); + if (error) + warn_if_invalid_key_and_clear_error (SHOW_THUMBNAILS_KEY, &error); + else + settings->show_thumbnails = value != FALSE; + /* Show size column */ value = g_key_file_get_boolean (key_file, SETTINGS_GROUP, @@ -301,6 +311,7 @@ _gtk_file_chooser_settings_init (GtkFileChooserSettings *settings) settings->sort_column = FILE_LIST_COL_NAME; settings->show_hidden = FALSE; settings->show_preview_panel = FALSE; + settings->show_thumbnails = TRUE; settings->show_size_column = TRUE; settings->geometry_x = -1; settings->geometry_y = -1; @@ -385,6 +396,20 @@ _gtk_file_chooser_settings_set_show_preview_panel (GtkFileChooserSettings *setti settings->show_preview_panel = show_preview_panel != FALSE; } +gboolean +_gtk_file_chooser_settings_get_show_thumbnails (GtkFileChooserSettings *settings) +{ + ensure_settings_read (settings); + return settings->show_thumbnails; +} + +void +_gtk_file_chooser_settings_set_show_thumbnails (GtkFileChooserSettings *settings, + gboolean show_thumbnails) +{ + settings->show_thumbnails = show_thumbnails != FALSE; +} + void _gtk_file_chooser_settings_set_show_size_column (GtkFileChooserSettings *settings, gboolean show_column) @@ -577,6 +602,8 @@ _gtk_file_chooser_settings_save (GtkFileChooserSettings *settings, SHOW_HIDDEN_KEY, settings->show_hidden); g_key_file_set_boolean (key_file, SETTINGS_GROUP, SHOW_PREVIEW_PANEL_KEY, settings->show_preview_panel); + g_key_file_set_boolean (key_file, SETTINGS_GROUP, + SHOW_THUMBNAILS_KEY, settings->show_thumbnails); g_key_file_set_boolean (key_file, SETTINGS_GROUP, SHOW_SIZE_COLUMN_KEY, settings->show_size_column); g_key_file_set_integer (key_file, SETTINGS_GROUP, diff --git a/gtk/gtkfilechoosersettings.h b/gtk/gtkfilechoosersettings.h index f8861a78e..464cef15b 100644 --- a/gtk/gtkfilechoosersettings.h +++ b/gtk/gtkfilechoosersettings.h @@ -59,6 +59,7 @@ struct _GtkFileChooserSettings guint settings_read : 1; guint show_hidden : 1; guint show_preview_panel : 1; + guint show_thumbnails : 1; guint show_size_column : 1; }; @@ -91,6 +92,10 @@ gboolean _gtk_file_chooser_settings_get_show_preview_panel (GtkFileChooserSettin void _gtk_file_chooser_settings_set_show_preview_panel (GtkFileChooserSettings *settings, gboolean show_preview_panel); +gboolean _gtk_file_chooser_settings_get_show_thumbnails (GtkFileChooserSettings *settings); +void _gtk_file_chooser_settings_set_show_thumbnails (GtkFileChooserSettings *settings, + gboolean show_thumbnails); + gboolean _gtk_file_chooser_settings_get_show_size_column (GtkFileChooserSettings *settings); void _gtk_file_chooser_settings_set_show_size_column (GtkFileChooserSettings *settings, gboolean show_column); From fd799a203b3ec45a20fd14dff38f98d06e72395e Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 20 May 2026 13:28:45 +0200 Subject: [PATCH 080/122] gtkfilechooser: queue visible image files for thumbnails --- gtk/gtkfilechooserdefault.c | 71 +++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index f49104d16..b6122d15e 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -945,6 +945,75 @@ thumbnail_file_list_clear_pixbuf_caches (GtkFileChooserDefault *impl) gtk_widget_queue_draw (impl->browse_files_current_view); } +static gboolean +thumbnail_queue_should_enqueue (GtkFileChooserDefault *impl, + GFile *file, + GFileInfo *info) +{ + const gchar *content_type; + + if (!impl->show_thumbnails) + return FALSE; + + if (!VIEW_MODE_USES_ICON_VIEW (impl->view_mode)) + return FALSE; + + if (file == NULL || info == NULL) + return FALSE; + + if (g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_THUMBNAIL_PATH)) + return FALSE; + + if (g_file_info_get_attribute_boolean (info, G_FILE_ATTRIBUTE_THUMBNAILING_FAILED)) + return FALSE; + + content_type = g_file_info_get_content_type (info); + if (content_type == NULL || !g_str_has_prefix (content_type, "image/")) + return FALSE; + + /* Keep the first implementation conservative: only local files can be + * thumbnailed without surprising network or VFS latency. + */ + if (!g_file_is_native (file)) + return FALSE; + + return TRUE; +} + +static void +thumbnail_queue_ensure (GtkFileChooserDefault *impl) +{ + if (!impl->thumbnail_cancellable) + impl->thumbnail_cancellable = g_cancellable_new (); + + if (!impl->thumbnail_queued_files) + impl->thumbnail_queued_files = + g_hash_table_new_full ((GHashFunc) g_file_hash, + (GEqualFunc) g_file_equal, + g_object_unref, + NULL); +} + +static void +thumbnail_queue_enqueue (GtkFileChooserDefault *impl, + GFile *file, + GFileInfo *info) +{ + if (!thumbnail_queue_should_enqueue (impl, file, info)) + return; + + thumbnail_queue_ensure (impl); + + if (g_hash_table_lookup (impl->thumbnail_queued_files, file)) + return; + + g_hash_table_insert (impl->thumbnail_queued_files, + g_object_ref (file), + GUINT_TO_POINTER (1)); + impl->thumbnail_queue = g_slist_prepend (impl->thumbnail_queue, + g_object_ref (file)); +} + static void gtk_file_chooser_default_finalize (GObject *object) { @@ -9807,6 +9876,8 @@ file_system_model_set (GtkFileSystemModel *model, if (file_visible) { + thumbnail_queue_enqueue (impl, file, info); + /* #2 start loading standard icon (callback will be handled by #1) */ if (!g_file_info_has_attribute (info, "filechooser::icon_queried")) { From ac2c4e808dbd203d9caac4245f44011cb58f8209 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 20 May 2026 14:37:34 +0200 Subject: [PATCH 081/122] gtkfilechooser: drain thumbnail queue asynchronously --- gtk/gtkfilechooserdefault.c | 44 +++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index b6122d15e..bf0441ee7 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -994,6 +994,48 @@ thumbnail_queue_ensure (GtkFileChooserDefault *impl) NULL); } +static gboolean +thumbnail_queue_idle_cb (gpointer data) +{ + GtkFileChooserDefault *impl = data; + GFile *file; + GSList *link; + + impl->thumbnail_queue_idle_id = 0; + + if (!impl->show_thumbnails || impl->thumbnail_queue == NULL) + return FALSE; + + link = impl->thumbnail_queue; + file = link->data; + + impl->thumbnail_queue = g_slist_delete_link (impl->thumbnail_queue, link); + + if (impl->thumbnail_queued_files) + g_hash_table_remove (impl->thumbnail_queued_files, file); + + g_object_unref (file); + + if (impl->thumbnail_queue) + impl->thumbnail_queue_idle_id = gdk_threads_add_idle (thumbnail_queue_idle_cb, + impl); + + return FALSE; +} + +static void +thumbnail_queue_schedule (GtkFileChooserDefault *impl) +{ + if (impl->thumbnail_queue_idle_id) + return; + + if (!impl->show_thumbnails || impl->thumbnail_queue == NULL) + return; + + impl->thumbnail_queue_idle_id = gdk_threads_add_idle (thumbnail_queue_idle_cb, + impl); +} + static void thumbnail_queue_enqueue (GtkFileChooserDefault *impl, GFile *file, @@ -1012,6 +1054,8 @@ thumbnail_queue_enqueue (GtkFileChooserDefault *impl, GUINT_TO_POINTER (1)); impl->thumbnail_queue = g_slist_prepend (impl->thumbnail_queue, g_object_ref (file)); + + thumbnail_queue_schedule (impl); } static void From 59679d00f1994ec27a0e7bf38e3a1a9bf1b3d8e1 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 20 May 2026 14:46:45 +0200 Subject: [PATCH 082/122] gtkfilechooser: prepare thumbnail worker scheduling --- gtk/gtkfilechooserdefault.c | 50 +++++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index bf0441ee7..efe4a3fb9 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -994,17 +994,14 @@ thumbnail_queue_ensure (GtkFileChooserDefault *impl) NULL); } -static gboolean -thumbnail_queue_idle_cb (gpointer data) +static GFile * +thumbnail_queue_pop_next (GtkFileChooserDefault *impl) { - GtkFileChooserDefault *impl = data; GFile *file; GSList *link; - impl->thumbnail_queue_idle_id = 0; - - if (!impl->show_thumbnails || impl->thumbnail_queue == NULL) - return FALSE; + if (impl->thumbnail_queue == NULL) + return NULL; link = impl->thumbnail_queue; file = link->data; @@ -1014,11 +1011,43 @@ thumbnail_queue_idle_cb (gpointer data) if (impl->thumbnail_queued_files) g_hash_table_remove (impl->thumbnail_queued_files, file); + return file; +} + +static void thumbnail_queue_schedule (GtkFileChooserDefault *impl); + +static gboolean +thumbnail_queue_idle_cb (gpointer data) +{ + GtkFileChooserDefault *impl = data; + GFile *file; + + impl->thumbnail_queue_idle_id = 0; + + if (!impl->show_thumbnails) + { + impl->thumbnail_worker_running = FALSE; + return FALSE; + } + + file = thumbnail_queue_pop_next (impl); + if (file == NULL) + { + impl->thumbnail_worker_running = FALSE; + return FALSE; + } + + /* + * Dummy worker stage: for now we only drain one queued file at a time. + * The real thumbnail generator will replace this point with background + * decoding and main-thread result application. + */ g_object_unref (file); + impl->thumbnail_worker_running = FALSE; + if (impl->thumbnail_queue) - impl->thumbnail_queue_idle_id = gdk_threads_add_idle (thumbnail_queue_idle_cb, - impl); + thumbnail_queue_schedule (impl); return FALSE; } @@ -1026,12 +1055,13 @@ thumbnail_queue_idle_cb (gpointer data) static void thumbnail_queue_schedule (GtkFileChooserDefault *impl) { - if (impl->thumbnail_queue_idle_id) + if (impl->thumbnail_queue_idle_id || impl->thumbnail_worker_running) return; if (!impl->show_thumbnails || impl->thumbnail_queue == NULL) return; + impl->thumbnail_worker_running = TRUE; impl->thumbnail_queue_idle_id = gdk_threads_add_idle (thumbnail_queue_idle_cb, impl); } From 1ad3bfdd647d17b272a6bc4df055603c0cb80911 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 20 May 2026 16:26:34 +0200 Subject: [PATCH 083/122] gtkfilechooser: persist generated thumbnails --- gtk/gtkfilechooserdefault.c | 326 ++++++++++++++++++++++++++++++++++-- 1 file changed, 315 insertions(+), 11 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index efe4a3fb9..fa48318fc 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -20,6 +20,7 @@ */ #include "config.h" +#include #include "gdk/gdkkeysyms.h" #include "gtkalignment.h" @@ -945,12 +946,58 @@ thumbnail_file_list_clear_pixbuf_caches (GtkFileChooserDefault *impl) gtk_widget_queue_draw (impl->browse_files_current_view); } +static gint +thumbnail_queue_get_icon_size (GtkFileChooserDefault *impl) +{ + gint icon_size; + + if (impl->view_mode == VIEW_MODE_ICON) + { + icon_size = impl->icon_view_icon_size; + if (icon_size <= 0) + icon_size = FALLBACK_ICON_VIEW_ICON_SIZE; + } + else + { + icon_size = impl->list_view_icon_size; + if (icon_size <= 0) + icon_size = FALLBACK_LIST_VIEW_ICON_SIZE; + } + + return MAX (1, icon_size); +} + +#define FILECHOOSER_GENERATED_THUMBNAIL "filechooser::generated-thumbnail" +#define FILECHOOSER_GENERATED_THUMBNAIL_SIZE "filechooser::generated-thumbnail-size" + +/* First conservative limit. It avoids surprising CPU/I/O spikes on older + * hardware while still covering typical camera and screenshot files. + */ +#define FILECHOOSER_THUMBNAIL_MAX_FILE_SIZE (20 * 1024 * 1024) + +typedef struct +{ + GtkFileChooserDefault *impl; + GFile *file; + GCancellable *cancellable; + guint generation_serial; + gint icon_size; + GdkPixbuf *pixbuf; + gchar *uri; + gchar *thumbnail_path; + gboolean failed; +} ThumbnailJob; + static gboolean thumbnail_queue_should_enqueue (GtkFileChooserDefault *impl, GFile *file, GFileInfo *info) { + GObject *thumbnail; const gchar *content_type; + goffset size; + gint icon_size; + gint thumbnail_size; if (!impl->show_thumbnails) return FALSE; @@ -967,10 +1014,25 @@ thumbnail_queue_should_enqueue (GtkFileChooserDefault *impl, if (g_file_info_get_attribute_boolean (info, G_FILE_ATTRIBUTE_THUMBNAILING_FAILED)) return FALSE; + if (g_file_info_get_attribute_boolean (info, "filechooser::thumbnail_generation_failed")) + return FALSE; + + icon_size = thumbnail_queue_get_icon_size (impl); + thumbnail_size = g_file_info_get_attribute_int32 (info, + FILECHOOSER_GENERATED_THUMBNAIL_SIZE); + thumbnail = g_file_info_get_attribute_object (info, + FILECHOOSER_GENERATED_THUMBNAIL); + if (thumbnail && thumbnail_size == icon_size) + return FALSE; + content_type = g_file_info_get_content_type (info); if (content_type == NULL || !g_str_has_prefix (content_type, "image/")) return FALSE; + size = g_file_info_get_size (info); + if (size > FILECHOOSER_THUMBNAIL_MAX_FILE_SIZE) + return FALSE; + /* Keep the first implementation conservative: only local files can be * thumbnailed without surprising network or VFS latency. */ @@ -1014,7 +1076,239 @@ thumbnail_queue_pop_next (GtkFileChooserDefault *impl) return file; } -static void thumbnail_queue_schedule (GtkFileChooserDefault *impl); +static void thumbnail_queue_schedule (GtkFileChooserDefault *impl); +static gboolean thumbnail_job_apply_cb (gpointer data); +static gchar * +thumbnail_cache_save_pixbuf (ThumbnailJob *job, + const gchar *source_path) +{ + GStatBuf st; + gchar *cache_dir; + gchar *hash; + gchar *basename; + gchar *thumbnail_path; + gchar *tmp_path; + gchar *mtime_str; + gchar *size_str; + gboolean saved; + + if (job->pixbuf == NULL || job->uri == NULL || source_path == NULL) + return NULL; + + if (g_stat (source_path, &st) != 0) + return NULL; + + cache_dir = g_build_filename (g_get_user_cache_dir (), + "thumbnails", + "normal", + NULL); + + if (g_mkdir_with_parents (cache_dir, 0700) != 0) + { + g_free (cache_dir); + return NULL; + } + + hash = g_compute_checksum_for_string (G_CHECKSUM_MD5, job->uri, -1); + basename = g_strconcat (hash, ".png", NULL); + thumbnail_path = g_build_filename (cache_dir, basename, NULL); + tmp_path = g_strdup_printf ("%s.%08x.tmp", thumbnail_path, g_random_int ()); + + mtime_str = g_strdup_printf ("%" G_GINT64_FORMAT, (gint64) st.st_mtime); + size_str = g_strdup_printf ("%" G_GINT64_FORMAT, (gint64) st.st_size); + + saved = gdk_pixbuf_save (job->pixbuf, + tmp_path, + "png", + NULL, + "tEXt::Thumb::URI", job->uri, + "tEXt::Thumb::MTime", mtime_str, + "tEXt::Thumb::Size", size_str, + "tEXt::Software", "gtk2-ng", + NULL); + + if (saved && g_rename (tmp_path, thumbnail_path) == 0) + { + g_free (cache_dir); + g_free (hash); + g_free (basename); + g_free (tmp_path); + g_free (mtime_str); + g_free (size_str); + + return thumbnail_path; + } + + g_remove (tmp_path); + + g_free (cache_dir); + g_free (hash); + g_free (basename); + g_free (thumbnail_path); + g_free (tmp_path); + g_free (mtime_str); + g_free (size_str); + + return NULL; +} + +static gpointer thumbnail_worker_thread (gpointer data); + +static void +thumbnail_job_free (ThumbnailJob *job) +{ + if (!job) + return; + + if (job->pixbuf) + g_object_unref (job->pixbuf); + + if (job->cancellable) + g_object_unref (job->cancellable); + + g_free (job->thumbnail_path); + g_free (job->uri); + + if (job->file) + g_object_unref (job->file); + + if (job->impl) + g_object_unref (job->impl); + + g_free (job); +} + +static gboolean +thumbnail_job_apply_cb (gpointer data) +{ + ThumbnailJob *job = data; + GtkFileChooserDefault *impl = job->impl; + + if (!g_cancellable_is_cancelled (job->cancellable) && + impl->show_thumbnails && + job->generation_serial == impl->thumbnail_generation_serial && + impl->current_model && + GTK_IS_FILE_SYSTEM_MODEL (impl->current_model)) + { + GtkFileSystemModel *model; + GtkTreeIter iter; + + model = GTK_FILE_SYSTEM_MODEL (impl->current_model); + + if (_gtk_file_system_model_get_iter_for_file (model, &iter, job->file)) + { + GFileInfo *info; + + info = g_file_info_dup (_gtk_file_system_model_get_info (model, &iter)); + + if (job->pixbuf) + { + g_file_info_set_attribute_object (info, + FILECHOOSER_GENERATED_THUMBNAIL, + G_OBJECT (job->pixbuf)); + g_file_info_set_attribute_int32 (info, + FILECHOOSER_GENERATED_THUMBNAIL_SIZE, + job->icon_size); + + if (job->thumbnail_path) + g_file_info_set_attribute_byte_string (info, + G_FILE_ATTRIBUTE_THUMBNAIL_PATH, + job->thumbnail_path); + } + else if (job->failed) + { + g_file_info_set_attribute_boolean (info, + "filechooser::thumbnail_generation_failed", + TRUE); + } + + _gtk_file_system_model_update_file (model, job->file, info); + g_object_unref (info); + } + } + + impl->thumbnail_worker_running = FALSE; + + if (impl->thumbnail_queue) + thumbnail_queue_schedule (impl); + + thumbnail_job_free (job); + + return FALSE; +} + +static gpointer +thumbnail_worker_thread (gpointer data) +{ + ThumbnailJob *job = data; + gchar *path; + + job->uri = g_file_get_uri (job->file); + path = g_file_get_path (job->file); + + if (path && !g_cancellable_is_cancelled (job->cancellable)) + { + job->pixbuf = gdk_pixbuf_new_from_file_at_scale (path, + job->icon_size, + job->icon_size, + TRUE, + NULL); + job->failed = job->pixbuf == NULL; + + if (job->pixbuf) + job->thumbnail_path = thumbnail_cache_save_pixbuf (job, path); + } + else + { + job->failed = TRUE; + } + + g_free (path); + + gdk_threads_add_idle (thumbnail_job_apply_cb, job); + + return NULL; +} + +static gboolean +thumbnail_worker_start (GtkFileChooserDefault *impl, + GFile *file) +{ + ThumbnailJob *job; +#if GLIB_CHECK_VERSION (2, 32, 0) + GThread *thread; +#else + GError *error = NULL; +#endif + + if (!impl->thumbnail_cancellable) + return FALSE; + + job = g_new0 (ThumbnailJob, 1); + job->impl = g_object_ref (impl); + job->file = file; /* takes ownership of the queue reference */ + job->cancellable = g_object_ref (impl->thumbnail_cancellable); + job->generation_serial = impl->thumbnail_generation_serial; + job->icon_size = thumbnail_queue_get_icon_size (impl); + +#if GLIB_CHECK_VERSION (2, 32, 0) + thread = g_thread_new ("gtk-filechooser-thumbnail", + thumbnail_worker_thread, + job); + g_thread_unref (thread); +#else + if (!g_thread_create (thumbnail_worker_thread, job, FALSE, &error)) + { + if (error) + g_error_free (error); + + thumbnail_job_free (job); + return FALSE; + } +#endif + + return TRUE; +} static gboolean thumbnail_queue_idle_cb (gpointer data) @@ -1037,17 +1331,14 @@ thumbnail_queue_idle_cb (gpointer data) return FALSE; } - /* - * Dummy worker stage: for now we only drain one queued file at a time. - * The real thumbnail generator will replace this point with background - * decoding and main-thread result application. - */ - g_object_unref (file); + if (!thumbnail_worker_start (impl, file)) + { + g_object_unref (file); + impl->thumbnail_worker_running = FALSE; - impl->thumbnail_worker_running = FALSE; - - if (impl->thumbnail_queue) - thumbnail_queue_schedule (impl); + if (impl->thumbnail_queue) + thumbnail_queue_schedule (impl); + } return FALSE; } @@ -9806,9 +10097,22 @@ file_system_model_render_icon (GtkFileChooserDefault *impl, GFileInfo *info, gint icon_size) { + GObject *thumbnail; + gint thumbnail_size; GFileInfo *icon_info; GdkPixbuf *pixbuf; + if (impl->show_thumbnails && VIEW_MODE_USES_ICON_VIEW (impl->view_mode)) + { + thumbnail = g_file_info_get_attribute_object (info, + FILECHOOSER_GENERATED_THUMBNAIL); + thumbnail_size = g_file_info_get_attribute_int32 (info, + FILECHOOSER_GENERATED_THUMBNAIL_SIZE); + + if (thumbnail && GDK_IS_PIXBUF (thumbnail) && thumbnail_size == icon_size) + return GDK_PIXBUF (g_object_ref (thumbnail)); + } + if (impl->show_thumbnails || !g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_THUMBNAIL_PATH)) return _gtk_file_info_render_icon (info, GTK_WIDGET (impl), icon_size); From 793d059d382185d3c13d806fac1ce04e2a2bac65 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 20 May 2026 16:35:56 +0200 Subject: [PATCH 084/122] gtkfilechooser: document thumbnail loader requirements --- gtk/gtkfilechooserdefault.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index fa48318fc..296469f8d 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -1248,6 +1248,11 @@ thumbnail_worker_thread (gpointer data) if (path && !g_cancellable_is_cancelled (job->cancellable)) { + /* Thumbnail generation intentionally goes through GdkPixbuf loaders. + * Formats such as WebP, AVIF, HEIF or JPEG XL work when the matching + * system GdkPixbuf loader is installed; gtk2-ng should not duplicate + * image decoders in the file chooser itself. + */ job->pixbuf = gdk_pixbuf_new_from_file_at_scale (path, job->icon_size, job->icon_size, From ed029de53d89ee8ee0c68b02a80d05b11131f173 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 20 May 2026 18:04:25 +0200 Subject: [PATCH 085/122] gtkfilechooser: choose thumbnail cache size --- gtk/gtkfilechooserdefault.c | 140 +++++++++++++++++++++++++++++++----- 1 file changed, 124 insertions(+), 16 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 296469f8d..fb6e981e2 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -951,7 +951,7 @@ thumbnail_queue_get_icon_size (GtkFileChooserDefault *impl) { gint icon_size; - if (impl->view_mode == VIEW_MODE_ICON) + if (VIEW_MODE_USES_ICON_VIEW (impl->view_mode)) { icon_size = impl->icon_view_icon_size; if (icon_size <= 0) @@ -973,7 +973,11 @@ thumbnail_queue_get_icon_size (GtkFileChooserDefault *impl) /* First conservative limit. It avoids surprising CPU/I/O spikes on older * hardware while still covering typical camera and screenshot files. */ -#define FILECHOOSER_THUMBNAIL_MAX_FILE_SIZE (20 * 1024 * 1024) +#define FILECHOOSER_THUMBNAIL_MAX_FILE_SIZE (20 * 1024 * 1024) +#define FILECHOOSER_THUMBNAIL_NORMAL_SIZE 128 +#define FILECHOOSER_THUMBNAIL_LARGE_SIZE 256 +#define FILECHOOSER_THUMBNAIL_X_LARGE_SIZE 512 +#define FILECHOOSER_THUMBNAIL_XX_LARGE_SIZE 1024 typedef struct { @@ -988,6 +992,9 @@ typedef struct gboolean failed; } ThumbnailJob; +static gboolean thumbnail_cache_path_is_sufficient (const gchar *path, + gint icon_size); + static gboolean thumbnail_queue_should_enqueue (GtkFileChooserDefault *impl, GFile *file, @@ -995,6 +1002,7 @@ thumbnail_queue_should_enqueue (GtkFileChooserDefault *impl, { GObject *thumbnail; const gchar *content_type; + const gchar *thumbnail_path; goffset size; gint icon_size; gint thumbnail_size; @@ -1002,13 +1010,14 @@ thumbnail_queue_should_enqueue (GtkFileChooserDefault *impl, if (!impl->show_thumbnails) return FALSE; - if (!VIEW_MODE_USES_ICON_VIEW (impl->view_mode)) - return FALSE; - if (file == NULL || info == NULL) return FALSE; - if (g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_THUMBNAIL_PATH)) + icon_size = thumbnail_queue_get_icon_size (impl); + + thumbnail_path = g_file_info_get_attribute_byte_string (info, + G_FILE_ATTRIBUTE_THUMBNAIL_PATH); + if (thumbnail_path && thumbnail_cache_path_is_sufficient (thumbnail_path, icon_size)) return FALSE; if (g_file_info_get_attribute_boolean (info, G_FILE_ATTRIBUTE_THUMBNAILING_FAILED)) @@ -1017,7 +1026,6 @@ thumbnail_queue_should_enqueue (GtkFileChooserDefault *impl, if (g_file_info_get_attribute_boolean (info, "filechooser::thumbnail_generation_failed")) return FALSE; - icon_size = thumbnail_queue_get_icon_size (impl); thumbnail_size = g_file_info_get_attribute_int32 (info, FILECHOOSER_GENERATED_THUMBNAIL_SIZE); thumbnail = g_file_info_get_attribute_object (info, @@ -1078,11 +1086,87 @@ thumbnail_queue_pop_next (GtkFileChooserDefault *impl) static void thumbnail_queue_schedule (GtkFileChooserDefault *impl); static gboolean thumbnail_job_apply_cb (gpointer data); +static const gchar * +thumbnail_cache_size_dir_for_dimension (gint dimension) +{ + if (dimension <= FILECHOOSER_THUMBNAIL_NORMAL_SIZE) + return "normal"; + if (dimension <= FILECHOOSER_THUMBNAIL_LARGE_SIZE) + return "large"; + if (dimension <= FILECHOOSER_THUMBNAIL_X_LARGE_SIZE) + return "x-large"; + if (dimension <= FILECHOOSER_THUMBNAIL_XX_LARGE_SIZE) + return "xx-large"; + + return NULL; +} + +static gint +thumbnail_cache_max_dimension_for_path (const gchar *path) +{ + if (path == NULL) + return 0; + + if (strstr (path, G_DIR_SEPARATOR_S "normal" G_DIR_SEPARATOR_S)) + return FILECHOOSER_THUMBNAIL_NORMAL_SIZE; + if (strstr (path, G_DIR_SEPARATOR_S "large" G_DIR_SEPARATOR_S)) + return FILECHOOSER_THUMBNAIL_LARGE_SIZE; + if (strstr (path, G_DIR_SEPARATOR_S "x-large" G_DIR_SEPARATOR_S)) + return FILECHOOSER_THUMBNAIL_X_LARGE_SIZE; + if (strstr (path, G_DIR_SEPARATOR_S "xx-large" G_DIR_SEPARATOR_S)) + return FILECHOOSER_THUMBNAIL_XX_LARGE_SIZE; + + return G_MAXINT; +} + +static gboolean +thumbnail_cache_path_is_sufficient (const gchar *path, + gint icon_size) +{ + return thumbnail_cache_max_dimension_for_path (path) >= icon_size; +} + +static gchar * +thumbnail_cache_lookup (ThumbnailJob *job) +{ + const gchar *cache_size_dir; + gchar *cache_dir; + gchar *hash; + gchar *basename; + gchar *thumbnail_path; + + if (job->uri == NULL) + return NULL; + + cache_size_dir = thumbnail_cache_size_dir_for_dimension (job->icon_size); + if (cache_size_dir == NULL) + return NULL; + + cache_dir = g_build_filename (g_get_user_cache_dir (), + "thumbnails", + cache_size_dir, + NULL); + hash = g_compute_checksum_for_string (G_CHECKSUM_MD5, job->uri, -1); + basename = g_strconcat (hash, ".png", NULL); + thumbnail_path = g_build_filename (cache_dir, basename, NULL); + + g_free (cache_dir); + g_free (hash); + g_free (basename); + + if (g_file_test (thumbnail_path, G_FILE_TEST_IS_REGULAR)) + return thumbnail_path; + + g_free (thumbnail_path); + return NULL; +} + static gchar * thumbnail_cache_save_pixbuf (ThumbnailJob *job, const gchar *source_path) { GStatBuf st; + const gchar *cache_size_dir; gchar *cache_dir; gchar *hash; gchar *basename; @@ -1091,6 +1175,9 @@ thumbnail_cache_save_pixbuf (ThumbnailJob *job, gchar *mtime_str; gchar *size_str; gboolean saved; + gint width; + gint height; + gint max_dimension; if (job->pixbuf == NULL || job->uri == NULL || source_path == NULL) return NULL; @@ -1098,9 +1185,17 @@ thumbnail_cache_save_pixbuf (ThumbnailJob *job, if (g_stat (source_path, &st) != 0) return NULL; + width = gdk_pixbuf_get_width (job->pixbuf); + height = gdk_pixbuf_get_height (job->pixbuf); + max_dimension = MAX (width, height); + + cache_size_dir = thumbnail_cache_size_dir_for_dimension (max_dimension); + if (cache_size_dir == NULL) + return NULL; + cache_dir = g_build_filename (g_get_user_cache_dir (), "thumbnails", - "normal", + cache_size_dir, NULL); if (g_mkdir_with_parents (cache_dir, 0700) != 0) @@ -1201,14 +1296,17 @@ thumbnail_job_apply_cb (gpointer data) info = g_file_info_dup (_gtk_file_system_model_get_info (model, &iter)); - if (job->pixbuf) + if (job->pixbuf || job->thumbnail_path) { - g_file_info_set_attribute_object (info, - FILECHOOSER_GENERATED_THUMBNAIL, - G_OBJECT (job->pixbuf)); - g_file_info_set_attribute_int32 (info, - FILECHOOSER_GENERATED_THUMBNAIL_SIZE, - job->icon_size); + if (job->pixbuf) + { + g_file_info_set_attribute_object (info, + FILECHOOSER_GENERATED_THUMBNAIL, + G_OBJECT (job->pixbuf)); + g_file_info_set_attribute_int32 (info, + FILECHOOSER_GENERATED_THUMBNAIL_SIZE, + job->icon_size); + } if (job->thumbnail_path) g_file_info_set_attribute_byte_string (info, @@ -1244,6 +1342,13 @@ thumbnail_worker_thread (gpointer data) gchar *path; job->uri = g_file_get_uri (job->file); + job->thumbnail_path = thumbnail_cache_lookup (job); + if (job->thumbnail_path) + { + gdk_threads_add_idle (thumbnail_job_apply_cb, job); + return NULL; + } + path = g_file_get_path (job->file); if (path && !g_cancellable_is_cancelled (job->cancellable)) @@ -7733,6 +7838,9 @@ icon_view_scale_apply_visible_size_cb (gpointer data) */ impl->icon_view_icon_size = icon_size; + thumbnail_queue_clear (impl); + thumbnail_file_list_clear_pixbuf_caches (impl); + if (impl->view_mode != VIEW_MODE_ICON || !impl->browse_files_icon_view) return FALSE; @@ -10107,7 +10215,7 @@ file_system_model_render_icon (GtkFileChooserDefault *impl, GFileInfo *icon_info; GdkPixbuf *pixbuf; - if (impl->show_thumbnails && VIEW_MODE_USES_ICON_VIEW (impl->view_mode)) + if (impl->show_thumbnails) { thumbnail = g_file_info_get_attribute_object (info, FILECHOOSER_GENERATED_THUMBNAIL); From e79b989e7abd7fdc49bd8b0dff271e000c402cd5 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Thu, 21 May 2026 09:20:46 +0200 Subject: [PATCH 086/122] gtkfilechooser: tidy thumbnail cache prototype --- gtk/gtkfilechooserdefault.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index fb6e981e2..9ad16df28 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -993,7 +993,7 @@ typedef struct } ThumbnailJob; static gboolean thumbnail_cache_path_is_sufficient (const gchar *path, - gint icon_size); + gint icon_size); static gboolean thumbnail_queue_should_enqueue (GtkFileChooserDefault *impl, From 8571f591360d3b54ad567eb6dfb9ea1c44918f3a Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Thu, 21 May 2026 10:13:23 +0200 Subject: [PATCH 087/122] gtkfilechooser: queue thumbnails from cached icon path --- gtk/gtkfilechooserdefault.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 9ad16df28..23856af3f 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -10345,6 +10345,8 @@ file_system_model_set (GtkFileSystemModel *model, icon_size = FALLBACK_LIST_VIEW_ICON_SIZE; } + thumbnail_queue_enqueue (impl, file, info); + g_value_take_object (value, file_system_model_render_icon (impl, info, icon_size)); return TRUE; } From 01d7b188022dfb9fbef5909753ccd1b6c16f1086 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Thu, 21 May 2026 10:21:30 +0200 Subject: [PATCH 088/122] gtkfilechooser: keep thumbnail queue across view changes --- gtk/gtkfilechooserdefault.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 23856af3f..20582ca74 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -7525,8 +7525,6 @@ view_mode_set (GtkFileChooserDefault *impl, ViewMode view_mode) file_list_tooltip_cache_clear (impl); file_icon_view_tooltip_cache_clear (impl); - thumbnail_queue_clear (impl); - impl->view_mode = view_mode; gtk_combo_box_set_active (GTK_COMBO_BOX (impl->view_mode_combo_box), view_mode); @@ -14215,7 +14213,8 @@ current_view_set_file_model (GtkFileChooserDefault *impl, GtkTreeModel *model) file_list_tooltip_cache_clear (impl); file_icon_view_tooltip_cache_clear (impl); - thumbnail_queue_clear (impl); + if (impl->current_model != model) + thumbnail_queue_clear (impl); impl->current_model = model; From bdad1edd931438ff923b8f7fb271534b9c50b201 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Thu, 21 May 2026 19:52:59 +0200 Subject: [PATCH 089/122] gtkfilechooser: refresh thumbnails after folder load --- gtk/gtkfilechooserdefault.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 20582ca74..71efbb5ce 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -10007,6 +10007,8 @@ browse_files_model_finished_loading_cb (GtkFileSystemModel *model, impl->load_state = LOAD_FINISHED; + thumbnail_file_list_clear_pixbuf_caches (impl); + pending_select_files_process (impl); set_busy_cursor (impl, FALSE); #ifdef PROFILE_FILE_CHOOSER From 6594e77f7a88af5d2f829103b07fab4fc0df8a8d Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Thu, 21 May 2026 19:55:16 +0200 Subject: [PATCH 090/122] gtkfilechooser: add file copy and cut menu items --- gtk/gtkfilechooserdefault.c | 232 +++++++++++++++++++++++++++++++++++- gtk/gtkfilechooserprivate.h | 2 + 2 files changed, 233 insertions(+), 1 deletion(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 71efbb5ce..b030c908b 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -4750,6 +4750,8 @@ popup_menu_detach_cb (GtkWidget *attach_widget, impl->browse_files_popup_menu = NULL; impl->browse_files_popup_menu_add_shortcut_item = NULL; + impl->browse_files_popup_menu_copy_item = NULL; + impl->browse_files_popup_menu_cut_item = NULL; impl->browse_files_popup_menu_copy_location_item = NULL; impl->browse_files_popup_menu_rename_item = NULL; impl->browse_files_popup_menu_hidden_files_item = NULL; @@ -4868,6 +4870,211 @@ add_to_shortcuts_cb (GtkMenuItem *item, bookmarks_add_selected_folder (impl); } +enum { + FILE_CLIPBOARD_TARGET_GNOME_COPIED_FILES, + FILE_CLIPBOARD_TARGET_URI_LIST, + FILE_CLIPBOARD_TARGET_TEXT_PLAIN, + FILE_CLIPBOARD_TARGET_KDE_CUT_SELECTION, + FILE_CLIPBOARD_TARGET_TDE_CUT_SELECTION, + FILE_CLIPBOARD_TARGET_EMPTY +}; + +struct file_clipboard_data +{ + gchar *gnome_copied_files; + gchar *uri_list; + gchar *plain_text; + gchar *cut_selection; +}; + +struct file_clipboard_closure +{ + GString *gnome_copied_files; + GString *uri_list; + GString *plain_text; + gboolean first; +}; + +static const GtkTargetEntry file_clipboard_targets[] = { + { "x-special/gnome-copied-files", 0, FILE_CLIPBOARD_TARGET_GNOME_COPIED_FILES }, + { "text/uri-list", 0, FILE_CLIPBOARD_TARGET_URI_LIST }, + { "text/plain", 0, FILE_CLIPBOARD_TARGET_TEXT_PLAIN }, + { "text/plain;charset=utf-8", 0, FILE_CLIPBOARD_TARGET_TEXT_PLAIN }, + { "text/plain;charset=UTF-8", 0, FILE_CLIPBOARD_TARGET_TEXT_PLAIN }, + { "UTF8_STRING", 0, FILE_CLIPBOARD_TARGET_TEXT_PLAIN }, + { "text/x-moz-url", 0, FILE_CLIPBOARD_TARGET_URI_LIST }, + { "application/x-kde4-urilist", 0, FILE_CLIPBOARD_TARGET_URI_LIST }, + { "application/x-tde-urilist", 0, FILE_CLIPBOARD_TARGET_URI_LIST }, + { "application/x-kde-cutselection", 0, FILE_CLIPBOARD_TARGET_KDE_CUT_SELECTION }, + { "application/x-tde-cutselection", 0, FILE_CLIPBOARD_TARGET_TDE_CUT_SELECTION }, + { "application/x-tdeio-metadata", 0, FILE_CLIPBOARD_TARGET_EMPTY } +}; + +static void +file_clipboard_data_free (struct file_clipboard_data *data) +{ + if (!data) + return; + + g_free (data->gnome_copied_files); + g_free (data->uri_list); + g_free (data->plain_text); + g_free (data->cut_selection); + g_free (data); +} + +static void +file_clipboard_get_cb (GtkClipboard *clipboard, + GtkSelectionData *selection_data, + guint info, + gpointer user_data) +{ + struct file_clipboard_data *data = user_data; + const gchar *text; + + switch (info) + { + case FILE_CLIPBOARD_TARGET_GNOME_COPIED_FILES: + text = data->gnome_copied_files; + break; + + case FILE_CLIPBOARD_TARGET_TEXT_PLAIN: + text = data->plain_text; + break; + + case FILE_CLIPBOARD_TARGET_KDE_CUT_SELECTION: + case FILE_CLIPBOARD_TARGET_TDE_CUT_SELECTION: + text = data->cut_selection; + break; + + case FILE_CLIPBOARD_TARGET_EMPTY: + text = ""; + break; + + case FILE_CLIPBOARD_TARGET_URI_LIST: + default: + text = data->uri_list; + break; + } + + gtk_selection_data_set (selection_data, + gtk_selection_data_get_target (selection_data), + 8, + (const guchar *) text, + strlen (text) + (info == FILE_CLIPBOARD_TARGET_URI_LIST ? 1 : 0)); +} + +static void +file_clipboard_clear_cb (GtkClipboard *clipboard, + gpointer user_data) +{ + file_clipboard_data_free (user_data); +} + +static void +file_clipboard_foreach_cb (GtkTreeModel *model, + GtkTreePath *path, + GtkTreeIter *iter, + gpointer data) +{ + struct file_clipboard_closure *closure = data; + GFile *file = NULL; + gchar *uri; + + gtk_tree_model_get (model, iter, + MODEL_COL_FILE, &file, + -1); + + if (file == NULL) + return; + + uri = g_file_get_uri (file); + + if (uri) + { + g_string_append (closure->gnome_copied_files, uri); + g_string_append_c (closure->gnome_copied_files, '\n'); + + g_string_append (closure->uri_list, uri); + g_string_append (closure->uri_list, "\r\n"); + + if (!closure->first) + g_string_append_c (closure->plain_text, '\n'); + + g_string_append (closure->plain_text, uri); + closure->first = FALSE; + } + + g_free (uri); + g_object_unref (file); +} + +static void +copy_or_cut_files_to_clipboard (GtkFileChooserDefault *impl, + gboolean cut) +{ + struct file_clipboard_closure closure; + struct file_clipboard_data *data; + GtkClipboard *clipboard; + + closure.gnome_copied_files = g_string_new (cut ? "cut\n" : "copy\n"); + closure.uri_list = g_string_new (NULL); + closure.plain_text = g_string_new (NULL); + closure.first = TRUE; + + current_selection_selected_foreach (impl, + file_clipboard_foreach_cb, + &closure); + + if (closure.uri_list->len == 0) + { + g_string_free (closure.gnome_copied_files, TRUE); + g_string_free (closure.uri_list, TRUE); + g_string_free (closure.plain_text, TRUE); + return; + } + + if (closure.gnome_copied_files->len > 0 && + closure.gnome_copied_files->str[closure.gnome_copied_files->len - 1] == '\n') + g_string_truncate (closure.gnome_copied_files, + closure.gnome_copied_files->len - 1); + + data = g_new0 (struct file_clipboard_data, 1); + data->gnome_copied_files = g_string_free (closure.gnome_copied_files, FALSE); + data->uri_list = g_string_free (closure.uri_list, FALSE); + data->plain_text = g_string_free (closure.plain_text, FALSE); + data->cut_selection = g_strdup (cut ? "1" : "0"); + + clipboard = gtk_clipboard_get_for_display (gtk_widget_get_display (impl->browse_files_current_view), + GDK_SELECTION_CLIPBOARD); + + if (gtk_clipboard_set_with_data (clipboard, + file_clipboard_targets, + G_N_ELEMENTS (file_clipboard_targets), + file_clipboard_get_cb, + file_clipboard_clear_cb, + data)) + gtk_clipboard_set_can_store (clipboard, + file_clipboard_targets, + G_N_ELEMENTS (file_clipboard_targets)); + else + file_clipboard_data_free (data); +} + +static void +copy_files_cb (GtkMenuItem *item, + GtkFileChooserDefault *impl) +{ + copy_or_cut_files_to_clipboard (impl, FALSE); +} + +static void +cut_files_cb (GtkMenuItem *item, + GtkFileChooserDefault *impl) +{ + copy_or_cut_files_to_clipboard (impl, TRUE); +} + struct copy_locations_closure { GString *locations; @@ -5804,10 +6011,24 @@ file_list_build_popup_menu (GtkFileChooserDefault *impl) gtk_menu_shell_append (GTK_MENU_SHELL (impl->browse_files_popup_menu), item); + item = gtk_image_menu_item_new_from_stock (GTK_STOCK_COPY, NULL); + impl->browse_files_popup_menu_copy_item = item; + g_signal_connect (item, "activate", + G_CALLBACK (copy_files_cb), impl); + gtk_widget_show (item); + gtk_menu_shell_append (GTK_MENU_SHELL (impl->browse_files_popup_menu), item); + + item = gtk_image_menu_item_new_from_stock (GTK_STOCK_CUT, NULL); + impl->browse_files_popup_menu_cut_item = item; + g_signal_connect (item, "activate", + G_CALLBACK (cut_files_cb), impl); + gtk_widget_show (item); + gtk_menu_shell_append (GTK_MENU_SHELL (impl->browse_files_popup_menu), item); + item = gtk_image_menu_item_new_with_mnemonic (_("_Copy Location")); impl->browse_files_popup_menu_copy_location_item = item; gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), - gtk_image_new_from_stock (GTK_STOCK_COPY, GTK_ICON_SIZE_MENU)); + gtk_image_new_from_stock (GTK_STOCK_JUMP_TO, GTK_ICON_SIZE_MENU)); g_signal_connect (item, "activate", G_CALLBACK (copy_location_cb), impl); gtk_widget_show (item); @@ -5930,6 +6151,15 @@ file_list_update_popup_menu (GtkFileChooserDefault *impl) * bookmarks_check_add_sensitivity() */ + if (impl->browse_files_popup_menu_copy_item) + gtk_widget_set_sensitive (impl->browse_files_popup_menu_copy_item, + current_selection_count_selected_rows (impl) > 0); + + if (impl->browse_files_popup_menu_cut_item) + gtk_widget_set_sensitive (impl->browse_files_popup_menu_cut_item, + impl->operation_mode == OPERATION_MODE_BROWSE && + current_selection_count_selected_rows (impl) > 0); + if (impl->browse_files_popup_menu_copy_location_item) gtk_widget_set_sensitive (impl->browse_files_popup_menu_copy_location_item, current_selection_count_selected_rows (impl) > 0); diff --git a/gtk/gtkfilechooserprivate.h b/gtk/gtkfilechooserprivate.h index 49762da1c..d1ef33e2c 100644 --- a/gtk/gtkfilechooserprivate.h +++ b/gtk/gtkfilechooserprivate.h @@ -195,6 +195,8 @@ struct _GtkFileChooserDefault guint list_tooltip_visible : 1; GtkWidget *browse_files_popup_menu; GtkWidget *browse_files_popup_menu_add_shortcut_item; + GtkWidget *browse_files_popup_menu_copy_item; + GtkWidget *browse_files_popup_menu_cut_item; GtkWidget *browse_files_popup_menu_copy_location_item; GtkWidget *browse_files_popup_menu_rename_item; GtkWidget *browse_files_popup_menu_hidden_files_item; From 359911d11a859bc8d6c29ad19229b5fc649c3891 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Thu, 21 May 2026 20:13:53 +0200 Subject: [PATCH 091/122] gtkfilechooser: add paste menu item --- gtk/gtkfilechooserdefault.c | 38 +++++++++++++++++++++++++++++++++++++ gtk/gtkfilechooserprivate.h | 1 + 2 files changed, 39 insertions(+) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index b030c908b..3a8dac675 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -4752,6 +4752,7 @@ popup_menu_detach_cb (GtkWidget *attach_widget, impl->browse_files_popup_menu_add_shortcut_item = NULL; impl->browse_files_popup_menu_copy_item = NULL; impl->browse_files_popup_menu_cut_item = NULL; + impl->browse_files_popup_menu_paste_item = NULL; impl->browse_files_popup_menu_copy_location_item = NULL; impl->browse_files_popup_menu_rename_item = NULL; impl->browse_files_popup_menu_hidden_files_item = NULL; @@ -5075,6 +5076,30 @@ cut_files_cb (GtkMenuItem *item, copy_or_cut_files_to_clipboard (impl, TRUE); } +static gboolean +file_clipboard_has_file_targets (GtkFileChooserDefault *impl) +{ + GtkClipboard *clipboard; + + if (!impl->browse_files_current_view) + return FALSE; + + clipboard = gtk_clipboard_get_for_display (gtk_widget_get_display (impl->browse_files_current_view), + GDK_SELECTION_CLIPBOARD); + + return gtk_clipboard_wait_is_target_available (clipboard, + gdk_atom_intern_static_string ("x-special/gnome-copied-files")) || + gtk_clipboard_wait_is_target_available (clipboard, + gdk_atom_intern_static_string ("text/uri-list")); +} + +static void +paste_files_cb (GtkMenuItem *item, + GtkFileChooserDefault *impl) +{ + /* File paste is implemented in a later step. */ +} + struct copy_locations_closure { GString *locations; @@ -6025,6 +6050,13 @@ file_list_build_popup_menu (GtkFileChooserDefault *impl) gtk_widget_show (item); gtk_menu_shell_append (GTK_MENU_SHELL (impl->browse_files_popup_menu), item); + item = gtk_image_menu_item_new_from_stock (GTK_STOCK_PASTE, NULL); + impl->browse_files_popup_menu_paste_item = item; + g_signal_connect (item, "activate", + G_CALLBACK (paste_files_cb), impl); + gtk_widget_show (item); + gtk_menu_shell_append (GTK_MENU_SHELL (impl->browse_files_popup_menu), item); + item = gtk_image_menu_item_new_with_mnemonic (_("_Copy Location")); impl->browse_files_popup_menu_copy_location_item = item; gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), @@ -6160,6 +6192,12 @@ file_list_update_popup_menu (GtkFileChooserDefault *impl) impl->operation_mode == OPERATION_MODE_BROWSE && current_selection_count_selected_rows (impl) > 0); + if (impl->browse_files_popup_menu_paste_item) + gtk_widget_set_sensitive (impl->browse_files_popup_menu_paste_item, + impl->operation_mode == OPERATION_MODE_BROWSE && + impl->current_folder != NULL && + file_clipboard_has_file_targets (impl)); + if (impl->browse_files_popup_menu_copy_location_item) gtk_widget_set_sensitive (impl->browse_files_popup_menu_copy_location_item, current_selection_count_selected_rows (impl) > 0); diff --git a/gtk/gtkfilechooserprivate.h b/gtk/gtkfilechooserprivate.h index d1ef33e2c..416795034 100644 --- a/gtk/gtkfilechooserprivate.h +++ b/gtk/gtkfilechooserprivate.h @@ -197,6 +197,7 @@ struct _GtkFileChooserDefault GtkWidget *browse_files_popup_menu_add_shortcut_item; GtkWidget *browse_files_popup_menu_copy_item; GtkWidget *browse_files_popup_menu_cut_item; + GtkWidget *browse_files_popup_menu_paste_item; GtkWidget *browse_files_popup_menu_copy_location_item; GtkWidget *browse_files_popup_menu_rename_item; GtkWidget *browse_files_popup_menu_hidden_files_item; From 3f6d93db49881dc6afc97b3ae538356d558fe018 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Thu, 21 May 2026 20:39:01 +0200 Subject: [PATCH 092/122] gtkfilechooser: paste files from clipboard --- gtk/gtkfilechooserdefault.c | 335 +++++++++++++++++++++++++++++++++++- 1 file changed, 334 insertions(+), 1 deletion(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 3a8dac675..9343e6f52 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -5093,11 +5093,344 @@ file_clipboard_has_file_targets (GtkFileChooserDefault *impl) gdk_atom_intern_static_string ("text/uri-list")); } +struct file_clipboard_paste_data +{ + GSList *files; + gboolean cut; +}; + +static void +file_clipboard_file_list_free (GSList *files) +{ + g_slist_foreach (files, (GFunc) g_object_unref, NULL); + g_slist_free (files); +} + +static void +file_clipboard_paste_data_free (struct file_clipboard_paste_data *data) +{ + if (!data) + return; + + file_clipboard_file_list_free (data->files); + g_free (data); +} + +static gboolean +file_clipboard_paste_data_add_uris (struct file_clipboard_paste_data *data, + const gchar *uri_text) +{ + gchar **uris; + gint i; + + if (uri_text == NULL || *uri_text == '\0') + return FALSE; + + uris = g_uri_list_extract_uris (uri_text); + + for (i = 0; uris && uris[i]; i++) + { + GFile *file; + + file = g_file_new_for_uri (uris[i]); + data->files = g_slist_prepend (data->files, file); + } + + g_strfreev (uris); + + if (data->files) + { + data->files = g_slist_reverse (data->files); + return TRUE; + } + + return FALSE; +} + +static struct file_clipboard_paste_data * +file_clipboard_paste_data_from_selection (GtkSelectionData *selection_data, + gboolean has_action) +{ + struct file_clipboard_paste_data *data; + const guchar *selection; + gint length; + gchar *text; + gchar *uris; + + if (selection_data == NULL) + return NULL; + + length = gtk_selection_data_get_length (selection_data); + if (length <= 0) + return NULL; + + selection = gtk_selection_data_get_data (selection_data); + if (selection == NULL) + return NULL; + + text = g_strndup ((const gchar *) selection, length); + data = g_new0 (struct file_clipboard_paste_data, 1); + + if (has_action) + { + gchar *newline; + + newline = strchr (text, '\n'); + if (newline == NULL) + { + g_free (text); + file_clipboard_paste_data_free (data); + return NULL; + } + + *newline = '\0'; + data->cut = strcmp (text, "cut") == 0; + uris = newline + 1; + } + else + { + data->cut = FALSE; + uris = text; + } + + if (!file_clipboard_paste_data_add_uris (data, uris)) + { + g_free (text); + file_clipboard_paste_data_free (data); + return NULL; + } + + g_free (text); + return data; +} + +static gboolean +file_clipboard_cut_selection_is_set (GtkClipboard *clipboard, + const gchar *target_name) +{ + GtkSelectionData *selection_data; + const guchar *selection; + gboolean cut = FALSE; + + selection_data = + gtk_clipboard_wait_for_contents (clipboard, + gdk_atom_intern_static_string (target_name)); + + if (selection_data) + { + selection = gtk_selection_data_get_data (selection_data); + + if (gtk_selection_data_get_length (selection_data) > 0 && + selection && + selection[0] == '1') + cut = TRUE; + + gtk_selection_data_free (selection_data); + } + + return cut; +} + +static struct file_clipboard_paste_data * +file_clipboard_read_paste_data (GtkFileChooserDefault *impl) +{ + GtkClipboard *clipboard; + GtkSelectionData *selection_data; + struct file_clipboard_paste_data *data; + + clipboard = gtk_clipboard_get_for_display (gtk_widget_get_display (impl->browse_files_current_view), + GDK_SELECTION_CLIPBOARD); + + selection_data = + gtk_clipboard_wait_for_contents (clipboard, + gdk_atom_intern_static_string ("x-special/gnome-copied-files")); + + data = file_clipboard_paste_data_from_selection (selection_data, TRUE); + + if (selection_data) + gtk_selection_data_free (selection_data); + + if (data) + return data; + + selection_data = + gtk_clipboard_wait_for_contents (clipboard, + gdk_atom_intern_static_string ("text/uri-list")); + + data = file_clipboard_paste_data_from_selection (selection_data, FALSE); + + if (selection_data) + gtk_selection_data_free (selection_data); + + if (data && + (file_clipboard_cut_selection_is_set (clipboard, "application/x-kde-cutselection") || + file_clipboard_cut_selection_is_set (clipboard, "application/x-tde-cutselection"))) + data->cut = TRUE; + + return data; +} + +struct file_paste_operation +{ + GtkFileChooserDefault *impl; + GFile *target_folder; + GSList *files; + gboolean cut; + gchar *error_message; + gchar *error_detail; +}; + +static void +file_paste_operation_free (struct file_paste_operation *op) +{ + if (!op) + return; + + file_clipboard_file_list_free (op->files); + + if (op->target_folder) + g_object_unref (op->target_folder); + + if (op->impl) + g_object_unref (op->impl); + + g_free (op->error_message); + g_free (op->error_detail); + g_free (op); +} + +static gboolean +file_paste_operation_finish_cb (gpointer data) +{ + struct file_paste_operation *op = data; + + if (op->error_message) + error_message (op->impl, op->error_message, op->error_detail); + + if (op->impl->current_folder && + g_file_equal (op->impl->current_folder, op->target_folder)) + change_folder_and_display_error (op->impl, op->target_folder, FALSE); + + file_paste_operation_free (op); + + return FALSE; +} + +static gpointer +file_paste_operation_thread (gpointer data) +{ + struct file_paste_operation *op = data; + GSList *l; + + for (l = op->files; l; l = l->next) + { + GFile *source = l->data; + GFile *destination; + GError *error = NULL; + gchar *basename; + gboolean success; + + basename = g_file_get_basename (source); + if (basename == NULL) + continue; + + destination = g_file_get_child (op->target_folder, basename); + + if (op->cut) + success = g_file_move (source, + destination, + G_FILE_COPY_NONE, + NULL, + NULL, + NULL, + &error); + else + success = g_file_copy (source, + destination, + G_FILE_COPY_NONE, + NULL, + NULL, + NULL, + &error); + + g_object_unref (destination); + + if (!success) + { + op->error_message = + g_strdup_printf (op->cut ? _("Could not move %s") : _("Could not copy %s"), + basename); + op->error_detail = g_strdup (error ? error->message : _("Unknown error")); + + if (error) + g_error_free (error); + + g_free (basename); + break; + } + + g_free (basename); + } + + gdk_threads_add_idle (file_paste_operation_finish_cb, op); + + return NULL; +} + +static void +file_paste_operation_start (GtkFileChooserDefault *impl, + struct file_clipboard_paste_data *paste_data) +{ + struct file_paste_operation *op; +#if GLIB_CHECK_VERSION (2, 32, 0) + GThread *thread; +#else + GError *error = NULL; +#endif + + if (impl->current_folder == NULL || paste_data == NULL || paste_data->files == NULL) + return; + + op = g_new0 (struct file_paste_operation, 1); + op->impl = g_object_ref (impl); + op->target_folder = g_object_ref (impl->current_folder); + op->files = paste_data->files; + op->cut = paste_data->cut; + + paste_data->files = NULL; + +#if GLIB_CHECK_VERSION (2, 32, 0) + thread = g_thread_new ("gtk-filechooser-paste", + file_paste_operation_thread, + op); + g_thread_unref (thread); +#else + if (!g_thread_create (file_paste_operation_thread, op, FALSE, &error)) + { + error_message (impl, + _("Could not paste files"), + error ? error->message : _("Unknown error")); + + if (error) + g_error_free (error); + + file_paste_operation_free (op); + } +#endif +} + static void paste_files_cb (GtkMenuItem *item, GtkFileChooserDefault *impl) { - /* File paste is implemented in a later step. */ + struct file_clipboard_paste_data *data; + + data = file_clipboard_read_paste_data (impl); + if (data == NULL) + return; + + file_paste_operation_start (impl, data); + file_clipboard_paste_data_free (data); } struct copy_locations_closure From bbea4b5e3372c3b9f5de82b29ce6155d325aa66d Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Thu, 21 May 2026 21:15:13 +0200 Subject: [PATCH 093/122] gtkfilechooser: move files to trash from popup menu --- gtk/gtkfilechooserdefault.c | 173 ++++++++++++++++++++++++++++++++++++ gtk/gtkfilechooserprivate.h | 1 + 2 files changed, 174 insertions(+) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 9343e6f52..03680f960 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -4755,6 +4755,7 @@ popup_menu_detach_cb (GtkWidget *attach_widget, impl->browse_files_popup_menu_paste_item = NULL; impl->browse_files_popup_menu_copy_location_item = NULL; impl->browse_files_popup_menu_rename_item = NULL; + impl->browse_files_popup_menu_trash_item = NULL; impl->browse_files_popup_menu_hidden_files_item = NULL; impl->browse_files_popup_menu_preview_panel_item = NULL; } @@ -5433,6 +5434,164 @@ paste_files_cb (GtkMenuItem *item, file_clipboard_paste_data_free (data); } +struct file_trash_operation +{ + GtkFileChooserDefault *impl; + GFile *target_folder; + GSList *files; + gchar *error_message; + gchar *error_detail; +}; + +static void +file_trash_operation_free (struct file_trash_operation *op) +{ + if (!op) + return; + + file_clipboard_file_list_free (op->files); + + if (op->target_folder) + g_object_unref (op->target_folder); + + if (op->impl) + g_object_unref (op->impl); + + g_free (op->error_message); + g_free (op->error_detail); + g_free (op); +} + +static gboolean +file_trash_operation_finish_cb (gpointer data) +{ + struct file_trash_operation *op = data; + + if (op->error_message) + error_message (op->impl, op->error_message, op->error_detail); + + if (op->impl->current_folder && + op->target_folder && + g_file_equal (op->impl->current_folder, op->target_folder)) + change_folder_and_display_error (op->impl, op->target_folder, FALSE); + + file_trash_operation_free (op); + + return FALSE; +} + +static gpointer +file_trash_operation_thread (gpointer data) +{ + struct file_trash_operation *op = data; + GSList *l; + + for (l = op->files; l; l = l->next) + { + GFile *file = l->data; + GError *error = NULL; + gchar *basename; + gboolean success; + + basename = g_file_get_basename (file); + success = g_file_trash (file, NULL, &error); + + if (!success) + { + op->error_message = + g_strdup_printf (_("Could not move %s to the trash"), + basename ? basename : _("file")); + op->error_detail = g_strdup (error ? error->message : _("Unknown error")); + + if (error) + g_error_free (error); + + g_free (basename); + break; + } + + g_free (basename); + } + + gdk_threads_add_idle (file_trash_operation_finish_cb, op); + + return NULL; +} + +static void +trash_selected_file_foreach_cb (GtkTreeModel *model, + GtkTreePath *path, + GtkTreeIter *iter, + gpointer data) +{ + struct file_trash_operation *op = data; + GFile *file = NULL; + + gtk_tree_model_get (model, iter, + MODEL_COL_FILE, &file, + -1); + + if (file) + op->files = g_slist_prepend (op->files, file); +} + +static void +trash_selected_files (GtkFileChooserDefault *impl) +{ + struct file_trash_operation *op; +#if GLIB_CHECK_VERSION (2, 32, 0) + GThread *thread; +#else + GError *error = NULL; +#endif + + if (impl->operation_mode != OPERATION_MODE_BROWSE || + impl->current_folder == NULL) + return; + + op = g_new0 (struct file_trash_operation, 1); + op->impl = g_object_ref (impl); + op->target_folder = g_object_ref (impl->current_folder); + + current_selection_selected_foreach (impl, + trash_selected_file_foreach_cb, + op); + + if (op->files == NULL) + { + file_trash_operation_free (op); + return; + } + + op->files = g_slist_reverse (op->files); + +#if GLIB_CHECK_VERSION (2, 32, 0) + thread = g_thread_new ("gtk-filechooser-trash", + file_trash_operation_thread, + op); + g_thread_unref (thread); +#else + if (!g_thread_create (file_trash_operation_thread, op, FALSE, &error)) + { + error_message (impl, + _("Could not move files to the trash"), + error ? error->message : _("Unknown error")); + + if (error) + g_error_free (error); + + file_trash_operation_free (op); + } +#endif +} + +static void +trash_files_cb (GtkMenuItem *item, + GtkFileChooserDefault *impl) +{ + trash_selected_files (impl); +} + struct copy_locations_closure { GString *locations; @@ -6408,6 +6567,15 @@ file_list_build_popup_menu (GtkFileChooserDefault *impl) gtk_widget_show (item); gtk_menu_shell_append (GTK_MENU_SHELL (impl->browse_files_popup_menu), item); + item = gtk_image_menu_item_new_with_mnemonic (_("Move to _Trash")); + impl->browse_files_popup_menu_trash_item = item; + gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), + gtk_image_new_from_stock (GTK_STOCK_DELETE, GTK_ICON_SIZE_MENU)); + g_signal_connect (item, "activate", + G_CALLBACK (trash_files_cb), impl); + gtk_widget_show (item); + gtk_menu_shell_append (GTK_MENU_SHELL (impl->browse_files_popup_menu), item); + item = gtk_separator_menu_item_new (); gtk_widget_show (item); gtk_menu_shell_append (GTK_MENU_SHELL (impl->browse_files_popup_menu), item); @@ -6546,6 +6714,11 @@ file_list_update_popup_menu (GtkFileChooserDefault *impl) selected_count == 1); } + if (impl->browse_files_popup_menu_trash_item) + gtk_widget_set_sensitive (impl->browse_files_popup_menu_trash_item, + impl->operation_mode == OPERATION_MODE_BROWSE && + current_selection_count_selected_rows (impl) > 0); + /* 'Show Hidden Files' */ g_signal_handlers_block_by_func (impl->browse_files_popup_menu_hidden_files_item, G_CALLBACK (show_hidden_toggled_cb), impl); diff --git a/gtk/gtkfilechooserprivate.h b/gtk/gtkfilechooserprivate.h index 416795034..f2aaf9c84 100644 --- a/gtk/gtkfilechooserprivate.h +++ b/gtk/gtkfilechooserprivate.h @@ -200,6 +200,7 @@ struct _GtkFileChooserDefault GtkWidget *browse_files_popup_menu_paste_item; GtkWidget *browse_files_popup_menu_copy_location_item; GtkWidget *browse_files_popup_menu_rename_item; + GtkWidget *browse_files_popup_menu_trash_item; GtkWidget *browse_files_popup_menu_hidden_files_item; GtkWidget *browse_files_popup_menu_preview_panel_item; GtkWidget *browse_files_popup_menu_thumbnails_item; From 54e988a8adef5069b2be94363546c5edbeb1a633 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Thu, 21 May 2026 21:58:31 +0200 Subject: [PATCH 094/122] gtkfilechooser: open containing folders from popup menus --- gtk/gtkfilechooserdefault.c | 112 ++++++++++++++++++++++++++++++++++++ gtk/gtkfilechooserprivate.h | 2 + 2 files changed, 114 insertions(+) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 03680f960..ea52b6f77 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -3771,6 +3771,8 @@ static void shortcuts_check_popup_sensitivity (GtkFileChooserDefault *impl) { GtkTreeIter iter; + ShortcutType shortcut_type = SHORTCUT_TYPE_SEPARATOR; + gpointer col_data = NULL; gboolean removable = FALSE; if (impl->browse_shortcuts_popup_menu == NULL) @@ -3778,11 +3780,27 @@ shortcuts_check_popup_sensitivity (GtkFileChooserDefault *impl) if (shortcuts_get_selected (impl, &iter)) gtk_tree_model_get (GTK_TREE_MODEL (impl->shortcuts_model), &iter, + SHORTCUTS_COL_DATA, &col_data, + SHORTCUTS_COL_TYPE, &shortcut_type, SHORTCUTS_COL_REMOVABLE, &removable, -1); gtk_widget_set_sensitive (impl->browse_shortcuts_popup_menu_remove_item, removable); gtk_widget_set_sensitive (impl->browse_shortcuts_popup_menu_rename_item, removable); + if (impl->browse_shortcuts_popup_menu_open_containing_folder_item) + { + gboolean show_item; + + show_item = shortcut_type == SHORTCUT_TYPE_FILE && col_data != NULL; + + if (show_item) + gtk_widget_show (impl->browse_shortcuts_popup_menu_open_containing_folder_item); + else + gtk_widget_hide (impl->browse_shortcuts_popup_menu_open_containing_folder_item); + + gtk_widget_set_sensitive (impl->browse_shortcuts_popup_menu_open_containing_folder_item, + show_item); + } } /* GtkWidget::drag-begin handler for the shortcuts list. */ @@ -4284,6 +4302,7 @@ shortcuts_popup_menu_detach_cb (GtkWidget *attach_widget, impl->browse_shortcuts_popup_menu = NULL; impl->browse_shortcuts_popup_menu_remove_item = NULL; impl->browse_shortcuts_popup_menu_rename_item = NULL; + impl->browse_shortcuts_popup_menu_open_containing_folder_item = NULL; } static void @@ -4325,6 +4344,10 @@ rename_shortcut_cb (GtkMenuItem *item, } /* Constructs the popup menu for the file list if needed */ +static void +open_shortcut_containing_folder_cb (GtkMenuItem *item, + GtkFileChooserDefault *impl); + static void shortcuts_build_popup_menu (GtkFileChooserDefault *impl) { @@ -4354,6 +4377,15 @@ shortcuts_build_popup_menu (GtkFileChooserDefault *impl) gtk_widget_show (item); gtk_menu_shell_append (GTK_MENU_SHELL (impl->browse_shortcuts_popup_menu), item); + item = gtk_image_menu_item_new_with_mnemonic (_("Open _Containing Folder")); + impl->browse_shortcuts_popup_menu_open_containing_folder_item = item; + gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), + gtk_image_new_from_stock (GTK_STOCK_OPEN, GTK_ICON_SIZE_MENU)); + g_signal_connect (item, "activate", + G_CALLBACK (open_shortcut_containing_folder_cb), impl); + gtk_widget_show (item); + gtk_menu_shell_append (GTK_MENU_SHELL (impl->browse_shortcuts_popup_menu), item); + item = gtk_separator_menu_item_new (); gtk_widget_show (item); gtk_menu_shell_append (GTK_MENU_SHELL (impl->browse_shortcuts_popup_menu), item); @@ -4754,6 +4786,7 @@ popup_menu_detach_cb (GtkWidget *attach_widget, impl->browse_files_popup_menu_cut_item = NULL; impl->browse_files_popup_menu_paste_item = NULL; impl->browse_files_popup_menu_copy_location_item = NULL; + impl->browse_files_popup_menu_open_containing_folder_item = NULL; impl->browse_files_popup_menu_rename_item = NULL; impl->browse_files_popup_menu_trash_item = NULL; impl->browse_files_popup_menu_hidden_files_item = NULL; @@ -5592,6 +5625,60 @@ trash_files_cb (GtkMenuItem *item, trash_selected_files (impl); } +static void +open_containing_folder_for_file (GtkFileChooserDefault *impl, + GFile *file) +{ + GFile *parent; + + if (file == NULL) + return; + + parent = g_file_get_parent (file); + if (parent == NULL) + return; + + change_folder_and_display_error (impl, parent, FALSE); + g_object_unref (parent); +} + +static void +open_file_list_containing_folder_cb (GtkMenuItem *item, + GtkFileChooserDefault *impl) +{ + GFile *file; + + if (impl->operation_mode != OPERATION_MODE_RECENT || + current_selection_count_selected_rows (impl) != 1) + return; + + file = get_selected_file (impl); + open_containing_folder_for_file (impl, file); + + if (file) + g_object_unref (file); +} + +static void +open_shortcut_containing_folder_cb (GtkMenuItem *item, + GtkFileChooserDefault *impl) +{ + GtkTreeIter iter; + ShortcutType shortcut_type; + gpointer col_data = NULL; + + if (!shortcuts_get_selected (impl, &iter)) + return; + + gtk_tree_model_get (GTK_TREE_MODEL (impl->shortcuts_model), &iter, + SHORTCUTS_COL_DATA, &col_data, + SHORTCUTS_COL_TYPE, &shortcut_type, + -1); + + if (shortcut_type == SHORTCUT_TYPE_FILE && col_data != NULL) + open_containing_folder_for_file (impl, G_FILE (col_data)); +} + struct copy_locations_closure { GString *locations; @@ -6558,6 +6645,15 @@ file_list_build_popup_menu (GtkFileChooserDefault *impl) gtk_widget_show (item); gtk_menu_shell_append (GTK_MENU_SHELL (impl->browse_files_popup_menu), item); + item = gtk_image_menu_item_new_with_mnemonic (_("Open _Containing Folder")); + impl->browse_files_popup_menu_open_containing_folder_item = item; + gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), + gtk_image_new_from_stock (GTK_STOCK_OPEN, GTK_ICON_SIZE_MENU)); + g_signal_connect (item, "activate", + G_CALLBACK (open_file_list_containing_folder_cb), impl); + gtk_widget_show (item); + gtk_menu_shell_append (GTK_MENU_SHELL (impl->browse_files_popup_menu), item); + item = gtk_image_menu_item_new_with_mnemonic (_("_Rename...")); impl->browse_files_popup_menu_rename_item = item; gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), @@ -6703,6 +6799,22 @@ file_list_update_popup_menu (GtkFileChooserDefault *impl) gtk_widget_set_sensitive (impl->browse_files_popup_menu_copy_location_item, current_selection_count_selected_rows (impl) > 0); + if (impl->browse_files_popup_menu_open_containing_folder_item) + { + gboolean show_item; + + show_item = impl->operation_mode == OPERATION_MODE_RECENT; + + if (show_item) + gtk_widget_show (impl->browse_files_popup_menu_open_containing_folder_item); + else + gtk_widget_hide (impl->browse_files_popup_menu_open_containing_folder_item); + + gtk_widget_set_sensitive (impl->browse_files_popup_menu_open_containing_folder_item, + show_item && + current_selection_count_selected_rows (impl) == 1); + } + if (impl->browse_files_popup_menu_rename_item) { guint selected_count; diff --git a/gtk/gtkfilechooserprivate.h b/gtk/gtkfilechooserprivate.h index f2aaf9c84..8d5782f47 100644 --- a/gtk/gtkfilechooserprivate.h +++ b/gtk/gtkfilechooserprivate.h @@ -177,6 +177,7 @@ struct _GtkFileChooserDefault GtkWidget *browse_shortcuts_popup_menu; GtkWidget *browse_shortcuts_popup_menu_remove_item; GtkWidget *browse_shortcuts_popup_menu_rename_item; + GtkWidget *browse_shortcuts_popup_menu_open_containing_folder_item; GtkWidget *browse_files_tree_view; GtkWidget *browse_files_scrolled_window; GtkWidget *browse_files_current_view; @@ -199,6 +200,7 @@ struct _GtkFileChooserDefault GtkWidget *browse_files_popup_menu_cut_item; GtkWidget *browse_files_popup_menu_paste_item; GtkWidget *browse_files_popup_menu_copy_location_item; + GtkWidget *browse_files_popup_menu_open_containing_folder_item; GtkWidget *browse_files_popup_menu_rename_item; GtkWidget *browse_files_popup_menu_trash_item; GtkWidget *browse_files_popup_menu_hidden_files_item; From da0dd4441250cd06a30f47b53cd8c4981dd5091e Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Thu, 21 May 2026 22:22:27 +0200 Subject: [PATCH 095/122] gtkfilechooser: avoid repeated selection counts in popup menu --- gtk/gtkfilechooserdefault.c | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index ea52b6f77..b54e79e8c 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -6772,8 +6772,12 @@ file_list_build_popup_menu (GtkFileChooserDefault *impl) static void file_list_update_popup_menu (GtkFileChooserDefault *impl) { + guint selected_count; + file_list_build_popup_menu (impl); + selected_count = current_selection_count_selected_rows (impl); + /* FIXME - handle OPERATION_MODE_SEARCH and OPERATION_MODE_RECENT */ /* The sensitivity of the Add to Bookmarks item is set in @@ -6782,12 +6786,12 @@ file_list_update_popup_menu (GtkFileChooserDefault *impl) if (impl->browse_files_popup_menu_copy_item) gtk_widget_set_sensitive (impl->browse_files_popup_menu_copy_item, - current_selection_count_selected_rows (impl) > 0); + selected_count > 0); if (impl->browse_files_popup_menu_cut_item) gtk_widget_set_sensitive (impl->browse_files_popup_menu_cut_item, impl->operation_mode == OPERATION_MODE_BROWSE && - current_selection_count_selected_rows (impl) > 0); + selected_count > 0); if (impl->browse_files_popup_menu_paste_item) gtk_widget_set_sensitive (impl->browse_files_popup_menu_paste_item, @@ -6797,7 +6801,7 @@ file_list_update_popup_menu (GtkFileChooserDefault *impl) if (impl->browse_files_popup_menu_copy_location_item) gtk_widget_set_sensitive (impl->browse_files_popup_menu_copy_location_item, - current_selection_count_selected_rows (impl) > 0); + selected_count > 0); if (impl->browse_files_popup_menu_open_containing_folder_item) { @@ -6812,24 +6816,18 @@ file_list_update_popup_menu (GtkFileChooserDefault *impl) gtk_widget_set_sensitive (impl->browse_files_popup_menu_open_containing_folder_item, show_item && - current_selection_count_selected_rows (impl) == 1); + selected_count == 1); } if (impl->browse_files_popup_menu_rename_item) - { - guint selected_count; - - selected_count = current_selection_count_selected_rows (impl); - - gtk_widget_set_sensitive (impl->browse_files_popup_menu_rename_item, - impl->operation_mode == OPERATION_MODE_BROWSE && - selected_count == 1); - } + gtk_widget_set_sensitive (impl->browse_files_popup_menu_rename_item, + impl->operation_mode == OPERATION_MODE_BROWSE && + selected_count == 1); if (impl->browse_files_popup_menu_trash_item) gtk_widget_set_sensitive (impl->browse_files_popup_menu_trash_item, impl->operation_mode == OPERATION_MODE_BROWSE && - current_selection_count_selected_rows (impl) > 0); + selected_count > 0); /* 'Show Hidden Files' */ g_signal_handlers_block_by_func (impl->browse_files_popup_menu_hidden_files_item, From 27e4b3901a0e9bc3b478d5af2453e01104fc0c4b Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Thu, 21 May 2026 22:58:44 +0200 Subject: [PATCH 096/122] gtkfilechooser: let users rename pasted files on conflict --- gtk/gtkfilechooserdefault.c | 197 +++++++++++++++++++++++++++++++----- 1 file changed, 172 insertions(+), 25 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index b54e79e8c..ffe97c4b7 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -1767,6 +1767,80 @@ error_building_filename_dialog (GtkFileChooserDefault *impl, NULL, error); } +static gchar * +paste_conflict_rename_dialog (GtkFileChooserDefault *impl, + const gchar *basename) +{ + GtkWindow *parent; + GtkWidget *dialog; + GtkWidget *content_area; + GtkWidget *box; + GtkWidget *label; + GtkWidget *entry; + gchar *result = NULL; + + parent = get_toplevel (GTK_WIDGET (impl)); + + dialog = gtk_message_dialog_new (parent, + GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_CANCEL, + _("A file named \"%s\" already exists."), + basename ? basename : _("file")); + + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), + "%s", + _("Choose a new name for the pasted file.")); + + gtk_dialog_add_button (GTK_DIALOG (dialog), _("_Rename"), GTK_RESPONSE_ACCEPT); + gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT); + + content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog)); + box = gtk_vbox_new (FALSE, 6); + gtk_box_pack_start (GTK_BOX (content_area), box, FALSE, FALSE, 0); + gtk_widget_show (box); + + label = gtk_label_new (_("New name:")); + gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.5); + gtk_box_pack_start (GTK_BOX (box), label, FALSE, FALSE, 0); + gtk_widget_show (label); + + entry = gtk_entry_new (); + gtk_entry_set_text (GTK_ENTRY (entry), basename ? basename : ""); + gtk_entry_set_activates_default (GTK_ENTRY (entry), TRUE); + gtk_box_pack_start (GTK_BOX (box), entry, FALSE, FALSE, 0); + gtk_widget_show (entry); + + if (parent && gtk_window_has_group (parent)) + gtk_window_group_add_window (gtk_window_get_group (parent), + GTK_WINDOW (dialog)); + + gtk_widget_grab_focus (entry); + + while (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT) + { + const gchar *name; + + name = gtk_entry_get_text (GTK_ENTRY (entry)); + + if (name && *name && + strchr (name, G_DIR_SEPARATOR) == NULL && + strchr (name, '/') == NULL) + { + result = g_strdup (name); + break; + } + + error_message (impl, + _("Invalid file name"), + _("The name must not be empty and must not contain a slash.")); + } + + gtk_widget_destroy (dialog); + + return result; +} + /* Shows an error dialog when we cannot switch to a folder */ static void error_changing_folder_dialog (GtkFileChooserDefault *impl, @@ -5140,6 +5214,18 @@ file_clipboard_file_list_free (GSList *files) g_slist_free (files); } +static GSList * +file_clipboard_file_list_copy (GSList *files) +{ + GSList *copy = NULL; + GSList *l; + + for (l = files; l; l = l->next) + copy = g_slist_prepend (copy, g_object_ref (l->data)); + + return g_slist_reverse (copy); +} + static void file_clipboard_paste_data_free (struct file_clipboard_paste_data *data) { @@ -5310,6 +5396,10 @@ struct file_paste_operation GFile *target_folder; GSList *files; gboolean cut; + gchar *first_destination_name; + GFile *conflict_source; + GSList *conflict_remaining_files; + gchar *conflict_basename; gchar *error_message; gchar *error_detail; }; @@ -5321,6 +5411,10 @@ file_paste_operation_free (struct file_paste_operation *op) return; file_clipboard_file_list_free (op->files); + file_clipboard_file_list_free (op->conflict_remaining_files); + + if (op->conflict_source) + g_object_unref (op->conflict_source); if (op->target_folder) g_object_unref (op->target_folder); @@ -5328,17 +5422,76 @@ file_paste_operation_free (struct file_paste_operation *op) if (op->impl) g_object_unref (op->impl); + g_free (op->first_destination_name); + g_free (op->conflict_basename); g_free (op->error_message); g_free (op->error_detail); g_free (op); } +static gpointer file_paste_operation_thread (gpointer data); + +static gboolean +file_paste_operation_start_thread (struct file_paste_operation *op) +{ +#if GLIB_CHECK_VERSION (2, 32, 0) + GThread *thread; +#else + GError *error = NULL; +#endif + +#if GLIB_CHECK_VERSION (2, 32, 0) + thread = g_thread_new ("gtk-filechooser-paste", + file_paste_operation_thread, + op); + g_thread_unref (thread); + return TRUE; +#else + if (g_thread_create (file_paste_operation_thread, op, FALSE, &error)) + return TRUE; + + error_message (op->impl, + _("Could not paste files"), + error ? error->message : _("Unknown error")); + + if (error) + g_error_free (error); + + return FALSE; +#endif +} + static gboolean file_paste_operation_finish_cb (gpointer data) { struct file_paste_operation *op = data; - if (op->error_message) + if (op->conflict_source) + { + gchar *new_name; + + new_name = paste_conflict_rename_dialog (op->impl, op->conflict_basename); + + if (new_name) + { + struct file_paste_operation *new_op; + + new_op = g_new0 (struct file_paste_operation, 1); + new_op->impl = g_object_ref (op->impl); + new_op->target_folder = g_object_ref (op->target_folder); + new_op->cut = op->cut; + new_op->first_destination_name = new_name; + new_op->files = g_slist_prepend (file_clipboard_file_list_copy (op->conflict_remaining_files), + g_object_ref (op->conflict_source)); + + if (!file_paste_operation_start_thread (new_op)) + file_paste_operation_free (new_op); + + file_paste_operation_free (op); + return FALSE; + } + } + else if (op->error_message) error_message (op->impl, op->error_message, op->error_detail); if (op->impl->current_folder && @@ -5355,6 +5508,7 @@ file_paste_operation_thread (gpointer data) { struct file_paste_operation *op = data; GSList *l; + gboolean first = TRUE; for (l = op->files; l; l = l->next) { @@ -5362,13 +5516,15 @@ file_paste_operation_thread (gpointer data) GFile *destination; GError *error = NULL; gchar *basename; + const gchar *destination_name; gboolean success; basename = g_file_get_basename (source); if (basename == NULL) continue; - destination = g_file_get_child (op->target_folder, basename); + destination_name = (first && op->first_destination_name) ? op->first_destination_name : basename; + destination = g_file_get_child (op->target_folder, destination_name); if (op->cut) success = g_file_move (source, @@ -5391,6 +5547,16 @@ file_paste_operation_thread (gpointer data) if (!success) { + if (error && g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS)) + { + op->conflict_source = g_object_ref (source); + op->conflict_remaining_files = file_clipboard_file_list_copy (l->next); + op->conflict_basename = g_strdup (destination_name); + g_error_free (error); + g_free (basename); + break; + } + op->error_message = g_strdup_printf (op->cut ? _("Could not move %s") : _("Could not copy %s"), basename); @@ -5404,6 +5570,7 @@ file_paste_operation_thread (gpointer data) } g_free (basename); + first = FALSE; } gdk_threads_add_idle (file_paste_operation_finish_cb, op); @@ -5411,16 +5578,12 @@ file_paste_operation_thread (gpointer data) return NULL; } + static void file_paste_operation_start (GtkFileChooserDefault *impl, struct file_clipboard_paste_data *paste_data) { struct file_paste_operation *op; -#if GLIB_CHECK_VERSION (2, 32, 0) - GThread *thread; -#else - GError *error = NULL; -#endif if (impl->current_folder == NULL || paste_data == NULL || paste_data->files == NULL) return; @@ -5433,24 +5596,8 @@ file_paste_operation_start (GtkFileChooserDefault *impl, paste_data->files = NULL; -#if GLIB_CHECK_VERSION (2, 32, 0) - thread = g_thread_new ("gtk-filechooser-paste", - file_paste_operation_thread, - op); - g_thread_unref (thread); -#else - if (!g_thread_create (file_paste_operation_thread, op, FALSE, &error)) - { - error_message (impl, - _("Could not paste files"), - error ? error->message : _("Unknown error")); - - if (error) - g_error_free (error); - - file_paste_operation_free (op); - } -#endif + if (!file_paste_operation_start_thread (op)) + file_paste_operation_free (op); } static void From d0f1c474deab72165d116cc0ab8e8d92e8776ac3 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 22 May 2026 09:05:01 +0200 Subject: [PATCH 097/122] gtkfilechooser: avoid refresh after cancelled paste conflict --- gtk/gtkfilechooserdefault.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index ffe97c4b7..a03c561dc 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -5396,6 +5396,7 @@ struct file_paste_operation GFile *target_folder; GSList *files; gboolean cut; + gboolean made_changes; gchar *first_destination_name; GFile *conflict_source; GSList *conflict_remaining_files; @@ -5480,6 +5481,7 @@ file_paste_operation_finish_cb (gpointer data) new_op->impl = g_object_ref (op->impl); new_op->target_folder = g_object_ref (op->target_folder); new_op->cut = op->cut; + new_op->made_changes = op->made_changes; new_op->first_destination_name = new_name; new_op->files = g_slist_prepend (file_clipboard_file_list_copy (op->conflict_remaining_files), g_object_ref (op->conflict_source)); @@ -5494,7 +5496,8 @@ file_paste_operation_finish_cb (gpointer data) else if (op->error_message) error_message (op->impl, op->error_message, op->error_detail); - if (op->impl->current_folder && + if (op->made_changes && + op->impl->current_folder && g_file_equal (op->impl->current_folder, op->target_folder)) change_folder_and_display_error (op->impl, op->target_folder, FALSE); @@ -5569,6 +5572,8 @@ file_paste_operation_thread (gpointer data) break; } + op->made_changes = TRUE; + g_free (basename); first = FALSE; } From 325da813f30d01dcbe773ea4530a463ed05b8610 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 22 May 2026 09:20:17 +0200 Subject: [PATCH 098/122] gtkfilechooser: add file list keyboard shortcuts --- gtk/gtkfilechooserdefault.c | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index a03c561dc..7540049e8 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -291,6 +291,13 @@ static void gtk_file_chooser_default_get_property (GObject *ob GParamSpec *pspec); static void gtk_file_chooser_default_dispose (GObject *object); static void rename_clear_state (GtkFileChooserDefault *impl); +static void rename_selected_file (GtkFileChooserDefault *impl); +static void copy_files_cb (GtkMenuItem *item, + GtkFileChooserDefault *impl); +static void cut_files_cb (GtkMenuItem *item, + GtkFileChooserDefault *impl); +static void paste_files_cb (GtkMenuItem *item, + GtkFileChooserDefault *impl); static void gtk_file_chooser_default_show_all (GtkWidget *widget); static void gtk_file_chooser_default_realize (GtkWidget *widget); static void gtk_file_chooser_default_map (GtkWidget *widget); @@ -4795,6 +4802,34 @@ browse_files_key_press_event_cb (GtkWidget *widget, impl = (GtkFileChooserDefault *) data; + if ((event->state & gtk_accelerator_get_default_mod_mask ()) == GDK_CONTROL_MASK) + { + switch (event->keyval) + { + case GDK_KEY_c: + copy_files_cb (NULL, impl); + return TRUE; + + case GDK_KEY_x: + cut_files_cb (NULL, impl); + return TRUE; + + case GDK_KEY_v: + paste_files_cb (NULL, impl); + return TRUE; + + default: + break; + } + } + + if (event->keyval == GDK_KEY_F2 && + (event->state & gtk_accelerator_get_default_mod_mask ()) == 0) + { + rename_selected_file (impl); + return TRUE; + } + if ((event->keyval == GDK_KEY_slash || event->keyval == GDK_KEY_KP_Divide #ifdef G_OS_UNIX From 3961ed8f46c5d2928662690a2c336dce9e7956f8 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 22 May 2026 09:30:26 +0200 Subject: [PATCH 099/122] gtkfilechooser: paste files from file list shortcut --- gtk/gtkfilechooserdefault.c | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 7540049e8..4850ad2f7 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -13329,11 +13329,29 @@ paste_text_received (GtkClipboard *clipboard, static void location_popup_on_paste_handler (GtkFileChooserDefault *impl) { - GtkClipboard *clipboard = gtk_widget_get_clipboard (GTK_WIDGET (impl), - GDK_SELECTION_CLIPBOARD); + GtkWidget *toplevel; + GtkWidget *current_focus; + GtkClipboard *clipboard; + + toplevel = gtk_widget_get_toplevel (GTK_WIDGET (impl)); + + if (GTK_IS_WINDOW (toplevel)) + { + current_focus = gtk_window_get_focus (GTK_WINDOW (toplevel)); + + if (current_focus == impl->browse_files_tree_view || + current_focus == impl->browse_files_icon_view) + { + paste_files_cb (NULL, impl); + return; + } + } + + clipboard = gtk_widget_get_clipboard (GTK_WIDGET (impl), + GDK_SELECTION_CLIPBOARD); gtk_clipboard_request_text (clipboard, - (GtkClipboardTextReceivedFunc) paste_text_received, - impl); + (GtkClipboardTextReceivedFunc) paste_text_received, + impl); } /* Implementation for GtkFileChooserEmbed::should_respond() */ From 9436bc7ce0ba9b2a4425efa19ede73edbe0dadb8 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 22 May 2026 09:51:44 +0200 Subject: [PATCH 100/122] gtkfilechooser: track cut files for visual feedback --- gtk/gtkfilechooserdefault.c | 62 ++++++++++++++++++++++++++++++++++--- gtk/gtkfilechooserprivate.h | 1 + 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 4850ad2f7..dada516b6 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -1517,6 +1517,12 @@ gtk_file_chooser_default_finalize (GObject *object) thumbnail_queue_clear (impl); + if (impl->file_clipboard_cut_files) + { + g_hash_table_destroy (impl->file_clipboard_cut_files); + impl->file_clipboard_cut_files = NULL; + } + if (impl->icon_tooltip_path) { gtk_tree_path_free (impl->icon_tooltip_path); @@ -5036,6 +5042,7 @@ struct file_clipboard_closure GString *gnome_copied_files; GString *uri_list; GString *plain_text; + GHashTable *cut_files; gboolean first; }; @@ -5054,6 +5061,25 @@ static const GtkTargetEntry file_clipboard_targets[] = { { "application/x-tdeio-metadata", 0, FILE_CLIPBOARD_TARGET_EMPTY } }; +static void +file_clipboard_queue_draw_current_view (GtkFileChooserDefault *impl) +{ + if (impl->browse_files_current_view) + gtk_widget_queue_draw (impl->browse_files_current_view); +} + +static void +file_clipboard_cut_files_clear (GtkFileChooserDefault *impl) +{ + if (!impl->file_clipboard_cut_files) + return; + + g_hash_table_destroy (impl->file_clipboard_cut_files); + impl->file_clipboard_cut_files = NULL; + + file_clipboard_queue_draw_current_view (impl); +} + static void file_clipboard_data_free (struct file_clipboard_data *data) { @@ -5147,6 +5173,11 @@ file_clipboard_foreach_cb (GtkTreeModel *model, g_string_append (closure->plain_text, uri); closure->first = FALSE; + + if (closure->cut_files) + g_hash_table_insert (closure->cut_files, + g_object_ref (file), + GUINT_TO_POINTER (1)); } g_free (uri); @@ -5164,6 +5195,10 @@ copy_or_cut_files_to_clipboard (GtkFileChooserDefault *impl, closure.gnome_copied_files = g_string_new (cut ? "cut\n" : "copy\n"); closure.uri_list = g_string_new (NULL); closure.plain_text = g_string_new (NULL); + closure.cut_files = cut ? g_hash_table_new_full ((GHashFunc) g_file_hash, + (GEqualFunc) g_file_equal, + g_object_unref, + NULL) : NULL; closure.first = TRUE; current_selection_selected_foreach (impl, @@ -5175,6 +5210,8 @@ copy_or_cut_files_to_clipboard (GtkFileChooserDefault *impl, g_string_free (closure.gnome_copied_files, TRUE); g_string_free (closure.uri_list, TRUE); g_string_free (closure.plain_text, TRUE); + if (closure.cut_files) + g_hash_table_destroy (closure.cut_files); return; } @@ -5198,11 +5235,28 @@ copy_or_cut_files_to_clipboard (GtkFileChooserDefault *impl, file_clipboard_get_cb, file_clipboard_clear_cb, data)) - gtk_clipboard_set_can_store (clipboard, - file_clipboard_targets, - G_N_ELEMENTS (file_clipboard_targets)); + { + file_clipboard_cut_files_clear (impl); + + if (cut) + { + impl->file_clipboard_cut_files = closure.cut_files; + closure.cut_files = NULL; + } + + file_clipboard_queue_draw_current_view (impl); + + gtk_clipboard_set_can_store (clipboard, + file_clipboard_targets, + G_N_ELEMENTS (file_clipboard_targets)); + } else - file_clipboard_data_free (data); + { + file_clipboard_data_free (data); + } + + if (closure.cut_files) + g_hash_table_destroy (closure.cut_files); } static void diff --git a/gtk/gtkfilechooserprivate.h b/gtk/gtkfilechooserprivate.h index 8d5782f47..319e96052 100644 --- a/gtk/gtkfilechooserprivate.h +++ b/gtk/gtkfilechooserprivate.h @@ -275,6 +275,7 @@ struct _GtkFileChooserDefault */ GSList *thumbnail_queue; GHashTable *thumbnail_queued_files; + GHashTable *file_clipboard_cut_files; GCancellable *thumbnail_cancellable; guint thumbnail_queue_idle_id; guint thumbnail_generation_serial; From 073b38a0f5ac55c21ac5e5b4ec4f1db9329e9995 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 22 May 2026 09:55:28 +0200 Subject: [PATCH 101/122] gtkfilechooser: clear cut feedback when clipboard changes --- gtk/gtkfilechooserdefault.c | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index dada516b6..dd3e45ce4 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -5035,6 +5035,7 @@ struct file_clipboard_data gchar *uri_list; gchar *plain_text; gchar *cut_selection; + GtkFileChooserDefault *impl; }; struct file_clipboard_closure @@ -5086,6 +5087,10 @@ file_clipboard_data_free (struct file_clipboard_data *data) if (!data) return; + if (data->impl) + g_object_remove_weak_pointer (G_OBJECT (data->impl), + (gpointer *) &data->impl); + g_free (data->gnome_copied_files); g_free (data->uri_list); g_free (data->plain_text); @@ -5138,7 +5143,12 @@ static void file_clipboard_clear_cb (GtkClipboard *clipboard, gpointer user_data) { - file_clipboard_data_free (user_data); + struct file_clipboard_data *data = user_data; + + if (data->impl) + file_clipboard_cut_files_clear (data->impl); + + file_clipboard_data_free (data); } static void @@ -5225,6 +5235,9 @@ copy_or_cut_files_to_clipboard (GtkFileChooserDefault *impl, data->uri_list = g_string_free (closure.uri_list, FALSE); data->plain_text = g_string_free (closure.plain_text, FALSE); data->cut_selection = g_strdup (cut ? "1" : "0"); + data->impl = impl; + g_object_add_weak_pointer (G_OBJECT (impl), + (gpointer *) &data->impl); clipboard = gtk_clipboard_get_for_display (gtk_widget_get_display (impl->browse_files_current_view), GDK_SELECTION_CLIPBOARD); From badfe1295aaa4d7036b48d8d7b5a106068a4efa3 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 22 May 2026 10:06:24 +0200 Subject: [PATCH 102/122] gtkfilechooser: dim cut file icons --- gtk/gtkfilechooserdefault.c | 48 +++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index dd3e45ce4..0801f3681 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -5081,6 +5081,48 @@ file_clipboard_cut_files_clear (GtkFileChooserDefault *impl) file_clipboard_queue_draw_current_view (impl); } +static gboolean +file_clipboard_file_is_cut (GtkFileChooserDefault *impl, + GFile *file) +{ + return (impl->file_clipboard_cut_files && + file && + g_hash_table_lookup (impl->file_clipboard_cut_files, file) != NULL); +} + +static void +file_clipboard_icon_cell_data_func (GtkCellLayout *cell_layout, + GtkCellRenderer *renderer, + GtkTreeModel *model, + GtkTreeIter *iter, + gpointer data) +{ + GtkFileChooserDefault *impl = data; + GFile *file = NULL; + gboolean is_sensitive = TRUE; + gboolean is_cut; + + gtk_tree_model_get (model, iter, + MODEL_COL_FILE, &file, + MODEL_COL_IS_SENSITIVE, &is_sensitive, + -1); + + is_cut = file_clipboard_file_is_cut (impl, file); + + /* + * Match common file-manager feedback for cut files: dim only the icon. + * The row remains selectable and activatable; text, size and mtime stay + * normally readable. Hidden-file dimming can be added here later as a + * separate visual state. + */ + g_object_set (renderer, + "sensitive", is_sensitive && !is_cut, + NULL); + + if (file) + g_object_unref (file); +} + static void file_clipboard_data_free (struct file_clipboard_data *data) { @@ -8101,6 +8143,7 @@ create_browse_files_icon_view (GtkFileChooserDefault *impl) impl->list_icon_renderer = gtk_cell_renderer_pixbuf_new (); g_object_set (G_OBJECT (impl->list_icon_renderer), "ypad", 3u, + "follow-state", TRUE, NULL); set_icon_cell_renderer_fixed_size (impl, GTK_CELL_RENDERER (impl->list_icon_renderer), @@ -8118,6 +8161,11 @@ create_browse_files_icon_view (GtkFileChooserDefault *impl) "wrap-mode", PANGO_WRAP_WORD_CHAR, "yalign", 0.0f, NULL); + gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (impl->browse_files_icon_view), + impl->list_icon_renderer, + file_clipboard_icon_cell_data_func, + impl, + NULL); update_icon_view_cell_layout (impl); g_signal_connect (impl->list_name_renderer, "edited", From d416fe6936d51dae6d0835f28aa245bea37839eb Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 22 May 2026 10:54:35 +0200 Subject: [PATCH 103/122] gtkfilechooser: provide URI data when dragging files --- gtk/gtkfilechooserdefault.c | 265 ++++++++++++++++++++++++++++++++++++ 1 file changed, 265 insertions(+) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 0801f3681..5bb6893eb 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -6888,6 +6888,77 @@ file_list_drag_motion_cb (GtkWidget *widget, } /* Constructs the popup menu for the file list if needed */ +#define FILE_LIST_DRAG_URIS_KEY "gtk-file-chooser-drag-uris" +#define FILE_LIST_DRAG_ACTIVE_KEY "gtk-file-chooser-drag-active" +#define FILE_LIST_DRAG_STARTED_KEY "gtk-file-chooser-drag-started" +#define FILE_LIST_DRAG_START_X_KEY "gtk-file-chooser-drag-start-x" +#define FILE_LIST_DRAG_START_Y_KEY "gtk-file-chooser-drag-start-y" + +static void +file_list_drag_uri_foreach_cb (GtkTreeModel *model, + GtkTreePath *path, + GtkTreeIter *iter, + gpointer data) +{ + GPtrArray *uris = data; + GFile *file = NULL; + gchar *uri; + + gtk_tree_model_get (model, iter, + MODEL_COL_FILE, &file, + -1); + + if (!file) + return; + + uri = g_file_get_uri (file); + if (uri) + g_ptr_array_add (uris, uri); + + g_object_unref (file); +} + +static void +file_list_drag_data_get_cb (GtkWidget *widget, + GdkDragContext *context, + GtkSelectionData *selection_data, + guint info, + guint time_, + GtkFileChooserDefault *impl) +{ + gchar **stored_uris; + GPtrArray *uris; + + stored_uris = g_object_get_data (G_OBJECT (widget), FILE_LIST_DRAG_URIS_KEY); + if (stored_uris && + g_object_get_data (G_OBJECT (widget), FILE_LIST_DRAG_ACTIVE_KEY)) + { + gtk_selection_data_set_uris (selection_data, stored_uris); + g_object_set_data (G_OBJECT (widget), FILE_LIST_DRAG_ACTIVE_KEY, NULL); + g_object_set_data (G_OBJECT (widget), FILE_LIST_DRAG_STARTED_KEY, NULL); + g_object_set_data (G_OBJECT (widget), FILE_LIST_DRAG_URIS_KEY, NULL); + return; + } + + g_object_set_data (G_OBJECT (widget), FILE_LIST_DRAG_ACTIVE_KEY, NULL); + g_object_set_data (G_OBJECT (widget), FILE_LIST_DRAG_STARTED_KEY, NULL); + + uris = g_ptr_array_new (); + + current_selection_selected_foreach (impl, + file_list_drag_uri_foreach_cb, + uris); + + if (uris->len > 0) + { + g_ptr_array_add (uris, NULL); + gtk_selection_data_set_uris (selection_data, (gchar **) uris->pdata); + } + + g_ptr_array_foreach (uris, (GFunc) g_free, NULL); + g_ptr_array_free (uris, TRUE); +} + static void file_list_build_popup_menu (GtkFileChooserDefault *impl) { @@ -7297,6 +7368,179 @@ browse_files_select_icon_item_at_pos (GtkFileChooserDefault *impl, return TRUE; } + +static gboolean +browse_files_clicked_item_is_selected (GtkFileChooserDefault *impl, + GtkWidget *widget, + GdkEventButton *event) +{ + GtkTreePath *path; + gboolean selected = FALSE; + + if (widget == impl->browse_files_tree_view) + { + GtkTreeSelection *selection; + + if (!gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (impl->browse_files_tree_view), + (gint) event->x, (gint) event->y, + &path, NULL, NULL, NULL)) + return FALSE; + + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (impl->browse_files_tree_view)); + selected = gtk_tree_selection_path_is_selected (selection, path); + gtk_tree_path_free (path); + + return selected; + } + + if (widget == impl->browse_files_icon_view) + { + path = gtk_icon_view_get_path_at_pos (GTK_ICON_VIEW (impl->browse_files_icon_view), + (gint) event->x, (gint) event->y); + if (!path) + return FALSE; + + selected = gtk_icon_view_path_is_selected (GTK_ICON_VIEW (impl->browse_files_icon_view), path); + gtk_tree_path_free (path); + + return selected; + } + + return FALSE; +} + +static void +file_list_store_drag_uris (GtkWidget *widget, + GtkFileChooserDefault *impl) +{ + GPtrArray *uris; + gchar **stored_uris; + + uris = g_ptr_array_new (); + + current_selection_selected_foreach (impl, + file_list_drag_uri_foreach_cb, + uris); + + if (uris->len == 0) + { + g_ptr_array_free (uris, TRUE); + g_object_set_data (G_OBJECT (widget), FILE_LIST_DRAG_URIS_KEY, NULL); + return; + } + + g_ptr_array_add (uris, NULL); + stored_uris = (gchar **) g_ptr_array_free (uris, FALSE); + + g_object_set_data_full (G_OBJECT (widget), + FILE_LIST_DRAG_URIS_KEY, + stored_uris, + (GDestroyNotify) g_strfreev); +} + +static void +file_list_clear_drag_state (GtkWidget *widget) +{ + g_object_set_data (G_OBJECT (widget), FILE_LIST_DRAG_ACTIVE_KEY, NULL); + g_object_set_data (G_OBJECT (widget), FILE_LIST_DRAG_STARTED_KEY, NULL); + g_object_set_data (G_OBJECT (widget), FILE_LIST_DRAG_START_X_KEY, NULL); + g_object_set_data (G_OBJECT (widget), FILE_LIST_DRAG_START_Y_KEY, NULL); + g_object_set_data (G_OBJECT (widget), FILE_LIST_DRAG_URIS_KEY, NULL); +} + +static gboolean +file_list_maybe_store_drag_uris (GtkWidget *widget, + GdkEventButton *event, + GtkFileChooserDefault *impl) +{ + if (event->button != 1 || + current_selection_count_selected_rows (impl) <= 1 || + !browse_files_clicked_item_is_selected (impl, widget, event)) + { + file_list_clear_drag_state (widget); + return FALSE; + } + + file_list_store_drag_uris (widget, impl); + if (!g_object_get_data (G_OBJECT (widget), FILE_LIST_DRAG_URIS_KEY)) + return FALSE; + + g_object_set_data (G_OBJECT (widget), + FILE_LIST_DRAG_ACTIVE_KEY, + GINT_TO_POINTER (1)); + g_object_set_data (G_OBJECT (widget), + FILE_LIST_DRAG_START_X_KEY, + GINT_TO_POINTER ((gint) event->x)); + g_object_set_data (G_OBJECT (widget), + FILE_LIST_DRAG_START_Y_KEY, + GINT_TO_POINTER ((gint) event->y)); + + return TRUE; +} + +static gboolean +file_list_multi_drag_motion_cb (GtkWidget *widget, + GdkEventMotion *event, + GtkFileChooserDefault *impl) +{ + gint start_x; + gint start_y; + GtkTargetList *target_list; + + if (!g_object_get_data (G_OBJECT (widget), FILE_LIST_DRAG_ACTIVE_KEY)) + return FALSE; + + if (!(event->state & GDK_BUTTON1_MASK)) + { + file_list_clear_drag_state (widget); + return FALSE; + } + + if (g_object_get_data (G_OBJECT (widget), FILE_LIST_DRAG_STARTED_KEY)) + return TRUE; + + start_x = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (widget), + FILE_LIST_DRAG_START_X_KEY)); + start_y = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (widget), + FILE_LIST_DRAG_START_Y_KEY)); + + if (!gtk_drag_check_threshold (widget, start_x, start_y, + (gint) event->x, (gint) event->y)) + return TRUE; + + g_object_set_data (G_OBJECT (widget), + FILE_LIST_DRAG_STARTED_KEY, + GINT_TO_POINTER (1)); + + target_list = gtk_target_list_new (NULL, 0); + gtk_target_list_add_uri_targets (target_list, 0); + + gtk_drag_begin (widget, + target_list, + GDK_ACTION_COPY | GDK_ACTION_MOVE, + 1, + (GdkEvent *) event); + + gtk_target_list_unref (target_list); + + return TRUE; +} + +static gboolean +file_list_multi_drag_button_release_cb (GtkWidget *widget, + GdkEventButton *event, + GtkFileChooserDefault *impl) +{ + if (event->button == 1 && + g_object_get_data (G_OBJECT (widget), FILE_LIST_DRAG_ACTIVE_KEY)) + { + file_list_clear_drag_state (widget); + return TRUE; + } + + return FALSE; +} + static gboolean browse_files_select_clicked_item (GtkFileChooserDefault *impl, GtkWidget *widget, @@ -7358,6 +7602,9 @@ list_button_press_event_cb (GtkWidget *widget, if (in_press) return FALSE; + if (file_list_maybe_store_drag_uris (widget, event, impl)) + return TRUE; + if (!_gtk_button_event_triggers_context_menu (event)) return FALSE; @@ -8033,6 +8280,15 @@ create_browse_files_tree_view (GtkFileChooserDefault *impl) NULL, 0, GDK_ACTION_COPY | GDK_ACTION_MOVE); gtk_drag_source_add_uri_targets (impl->browse_files_tree_view); + g_signal_connect (impl->browse_files_tree_view, "drag-data-get", + G_CALLBACK (file_list_drag_data_get_cb), impl); + g_signal_connect (impl->browse_files_tree_view, "motion-notify-event", + G_CALLBACK (file_list_multi_drag_motion_cb), impl); + g_signal_connect (impl->browse_files_tree_view, "button-release-event", + G_CALLBACK (file_list_multi_drag_button_release_cb), impl); + + gtk_widget_add_events (impl->browse_files_tree_view, + GDK_BUTTON1_MOTION_MASK | GDK_BUTTON_RELEASE_MASK); g_signal_connect (selection, "changed", G_CALLBACK (list_selection_changed), impl); @@ -8139,6 +8395,15 @@ create_browse_files_icon_view (GtkFileChooserDefault *impl) NULL, 0, GDK_ACTION_COPY | GDK_ACTION_MOVE); gtk_drag_source_add_uri_targets (impl->browse_files_icon_view); + g_signal_connect (impl->browse_files_icon_view, "drag-data-get", + G_CALLBACK (file_list_drag_data_get_cb), impl); + g_signal_connect (impl->browse_files_icon_view, "motion-notify-event", + G_CALLBACK (file_list_multi_drag_motion_cb), impl); + g_signal_connect (impl->browse_files_icon_view, "button-release-event", + G_CALLBACK (file_list_multi_drag_button_release_cb), impl); + + gtk_widget_add_events (impl->browse_files_icon_view, + GDK_BUTTON1_MOTION_MASK | GDK_BUTTON_RELEASE_MASK); impl->list_icon_renderer = gtk_cell_renderer_pixbuf_new (); g_object_set (G_OBJECT (impl->list_icon_renderer), From 6c9ebd69ca0aa9d943877bd9406a7b6175ba8308 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 22 May 2026 11:00:49 +0200 Subject: [PATCH 104/122] gtkfilechooser: copy dropped files into current folder --- gtk/gtkfilechooserdefault.c | 49 +++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 5bb6893eb..e8da318a5 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -5411,6 +5411,37 @@ file_clipboard_paste_data_add_uris (struct file_clipboard_paste_data *data, return FALSE; } +static struct file_clipboard_paste_data * +file_clipboard_paste_data_from_uris (gchar **uris) +{ + struct file_clipboard_paste_data *data; + gint i; + + if (!uris || !uris[0]) + return NULL; + + data = g_new0 (struct file_clipboard_paste_data, 1); + data->cut = FALSE; + + for (i = 0; uris[i]; i++) + { + GFile *file; + + file = g_file_new_for_uri (uris[i]); + data->files = g_slist_prepend (data->files, file); + } + + if (!data->files) + { + file_clipboard_paste_data_free (data); + return NULL; + } + + data->files = g_slist_reverse (data->files); + + return data; +} + static struct file_clipboard_paste_data * file_clipboard_paste_data_from_selection (GtkSelectionData *selection_data, gboolean has_action) @@ -6823,6 +6854,7 @@ file_list_drag_data_received_cb (GtkWidget *widget, gchar **uris; char *uri; GFile *file; + struct file_clipboard_paste_data *paste_data; impl = GTK_FILE_CHOOSER_DEFAULT (data); @@ -6835,6 +6867,23 @@ file_list_drag_data_received_cb (GtkWidget *widget, /* Parse the text/uri-list string, navigate to the first one */ uris = gtk_selection_data_get_uris (selection_data); + if (uris && uris[0] && + impl->operation_mode == OPERATION_MODE_BROWSE && + impl->current_folder) + { + paste_data = file_clipboard_paste_data_from_uris (uris); + if (paste_data) + { + file_paste_operation_start (impl, paste_data); + file_clipboard_paste_data_free (paste_data); + + g_strfreev (uris); + g_signal_stop_emission_by_name (widget, "drag-data-received"); + gtk_drag_finish (context, TRUE, FALSE, time_); + return; + } + } + if (uris && uris[0]) { struct FileListDragData *data; From 7a557c554037a023ed52b717a9e93f783464b4c5 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 22 May 2026 11:13:05 +0200 Subject: [PATCH 105/122] po: add German file chooser translations --- po/de.po | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/po/de.po b/po/de.po index 49e158bfb..360aabc76 100644 --- a/po/de.po +++ b/po/de.po @@ -2743,7 +2743,7 @@ msgstr "_Aufsteigend" #: ../gtk/gtkstock.c:427 msgctxt "Stock label" msgid "_Descending" -msgstr "_Absteigend" +msgstr "A_bsteigend" #: ../gtk/gtkstock.c:428 msgctxt "Stock label" @@ -6086,3 +6086,95 @@ msgstr "" #~ msgid "X" #~ msgstr "X" + +#: gtk/gtkfilechooserdefault.c +msgid "A file named \"%s\" already exists." +msgstr "Eine Datei namens „%s“ existiert bereits." + +#: gtk/gtkfilechooserdefault.c +msgid "Choose a new name for the pasted file." +msgstr "Neuen Namen für die eingefügte Datei wählen." + +#: gtk/gtkfilechooserdefault.c +msgid "New name:" +msgstr "Neuer Name:" + +#: gtk/gtkfilechooserdefault.c +msgid "Open _Containing Folder" +msgstr "_Dateipfad öffnen" + +#: gtk/gtkfilechooserdefault.c +msgid "Could not paste files" +msgstr "Dateien konnten nicht eingefügt werden" + +#: gtk/gtkfilechooserdefault.c +msgid "Could not move %s" +msgstr "%s konnte nicht verschoben werden" + +#: gtk/gtkfilechooserdefault.c +msgid "Could not copy %s" +msgstr "%s konnte nicht kopiert werden" + +#: gtk/gtkfilechooserdefault.c +msgid "Could not move %s to the trash" +msgstr "%s konnte nicht in den Papierkorb verschoben werden" + +#: gtk/gtkfilechooserdefault.c +msgid "Could not move files to the trash" +msgstr "Dateien konnten nicht in den Papierkorb verschoben werden" + +#: gtk/gtkfilechooserdefault.c +msgid "_Copy Location" +msgstr "_Pfad kopieren" + +#: gtk/gtkfilechooserdefault.c +msgid "_Rename..." +msgstr "_Umbenennen …" + +#: gtk/gtkfilechooserdefault.c +msgid "Move to _Trash" +msgstr "In den _Papierkorb verschieben" + +#: gtk/gtkfilechooserdefault.c +msgid "Show _Preview Panel" +msgstr "_Vorschauleiste anzeigen" + +#: gtk/gtkfilechooserdefault.c +msgid "Show _Thumbnails" +msgstr "_Vorschaubilder anzeigen" + +#: gtk/gtkfilechooserdefault.c +msgid "List View" +msgstr "Listenansicht" + +#: gtk/gtkfilechooserdefault.c +msgid "Icon View" +msgstr "Symbolansicht" + +#: gtk/gtkfilechooserdefault.c +msgid "Compact View" +msgstr "Kompaktansicht" + +#: gtk/gtkfilechooserdefault.c +msgid "Show Preview Panel" +msgstr "Vorschauleiste anzeigen" + +#: gtk/gtkfilechooserdefault.c +msgid "Arrange Items" +msgstr "Objekte anordnen" + +#: gtk/gtkfilechooserdefault.c +msgid "Arrange _Items" +msgstr "_Objekte anordnen" + +#: gtk/gtkfilechooserdefault.c +msgid "By _Name" +msgstr "Nach _Name" + +#: gtk/gtkfilechooserdefault.c +msgid "By _Size" +msgstr "Nach _Größe" + +#: gtk/gtkfilechooserdefault.c +msgid "By _Modification Date" +msgstr "Nach _Änderungsdatum" From b98239c5a47c646c7b106f9487096a78737f12ba Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Tue, 26 May 2026 11:53:13 +0200 Subject: [PATCH 106/122] gtkfilechooser: dim hidden file icons --- gtk/gtkfilechooserdefault.c | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index e8da318a5..2a79d8025 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -208,6 +208,7 @@ enum { MODEL_COL_NAME_COLLATED, MODEL_COL_IS_FOLDER, MODEL_COL_IS_SENSITIVE, + MODEL_COL_IS_HIDDEN, MODEL_COL_LIST_PIXBUF, MODEL_COL_ICON_PIXBUF, MODEL_COL_SIZE_TEXT, @@ -226,6 +227,7 @@ enum { G_TYPE_STRING, /* MODEL_COL_NAME_COLLATED */ \ G_TYPE_BOOLEAN, /* MODEL_COL_IS_FOLDER */ \ G_TYPE_BOOLEAN, /* MODEL_COL_IS_SENSITIVE */ \ + G_TYPE_BOOLEAN, /* MODEL_COL_IS_HIDDEN */ \ GDK_TYPE_PIXBUF, /* MODEL_COL_LIST_PIXBUF */ \ GDK_TYPE_PIXBUF, /* MODEL_COL_ICON_PIXBUF */ \ G_TYPE_STRING, /* MODEL_COL_SIZE_TEXT */ \ @@ -5100,23 +5102,24 @@ file_clipboard_icon_cell_data_func (GtkCellLayout *cell_layout, GtkFileChooserDefault *impl = data; GFile *file = NULL; gboolean is_sensitive = TRUE; + gboolean is_hidden = FALSE; gboolean is_cut; gtk_tree_model_get (model, iter, MODEL_COL_FILE, &file, MODEL_COL_IS_SENSITIVE, &is_sensitive, + MODEL_COL_IS_HIDDEN, &is_hidden, -1); is_cut = file_clipboard_file_is_cut (impl, file); /* - * Match common file-manager feedback for cut files: dim only the icon. - * The row remains selectable and activatable; text, size and mtime stay - * normally readable. Hidden-file dimming can be added here later as a - * separate visual state. + * Match common file-manager feedback for cut and hidden files: dim only + * the icon. The row remains selectable and activatable; text, size and + * mtime stay normally readable. */ g_object_set (renderer, - "sensitive", is_sensitive && !is_cut, + "sensitive", is_sensitive && !is_cut && !is_hidden, NULL); if (file) @@ -11808,6 +11811,23 @@ file_system_model_set (GtkFileSystemModel *model, else g_value_set_boolean (value, TRUE); break; + case MODEL_COL_IS_HIDDEN: + if (info) + { + gboolean is_hidden = FALSE; + gboolean is_backup = FALSE; + + if (g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN)) + is_hidden = g_file_info_get_is_hidden (info); + + if (g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_STANDARD_IS_BACKUP)) + is_backup = g_file_info_get_attribute_boolean (info, G_FILE_ATTRIBUTE_STANDARD_IS_BACKUP); + + g_value_set_boolean (value, is_hidden || is_backup); + } + else + g_value_set_boolean (value, FALSE); + break; case MODEL_COL_LIST_PIXBUF: case MODEL_COL_ICON_PIXBUF: if (info) From e6f3ed4f69a4d43075396762c361619c425e7f23 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Tue, 26 May 2026 13:14:39 +0200 Subject: [PATCH 107/122] gtkfilechooser: support dropping files onto folders --- gtk/gtkfilechooserdefault.c | 94 +++++++++++++++++++++++++++++++++---- 1 file changed, 86 insertions(+), 8 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 2a79d8025..839763add 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -5764,16 +5764,17 @@ file_paste_operation_thread (gpointer data) static void file_paste_operation_start (GtkFileChooserDefault *impl, - struct file_clipboard_paste_data *paste_data) + struct file_clipboard_paste_data *paste_data, + GFile *target_folder) { struct file_paste_operation *op; - if (impl->current_folder == NULL || paste_data == NULL || paste_data->files == NULL) + if (target_folder == NULL || paste_data == NULL || paste_data->files == NULL) return; op = g_new0 (struct file_paste_operation, 1); op->impl = g_object_ref (impl); - op->target_folder = g_object_ref (impl->current_folder); + op->target_folder = g_object_ref (target_folder); op->files = paste_data->files; op->cut = paste_data->cut; @@ -5783,6 +5784,7 @@ file_paste_operation_start (GtkFileChooserDefault *impl, file_paste_operation_free (op); } + static void paste_files_cb (GtkMenuItem *item, GtkFileChooserDefault *impl) @@ -5793,7 +5795,7 @@ paste_files_cb (GtkMenuItem *item, if (data == NULL) return; - file_paste_operation_start (impl, data); + file_paste_operation_start (impl, data, impl->current_folder); file_clipboard_paste_data_free (data); } @@ -6843,6 +6845,61 @@ out: g_object_unref (cancellable); } +static GFile * +file_list_get_drop_target_folder (GtkFileChooserDefault *impl, + GtkWidget *widget, + gint x, + gint y) +{ + GtkTreeModel *model = NULL; + GtkTreePath *path = NULL; + GtkTreeIter iter; + GFile *file = NULL; + gboolean is_folder = FALSE; + gboolean is_sensitive = FALSE; + + if (widget == impl->browse_files_tree_view) + { + if (!gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (widget), x, y, + &path, NULL, NULL, NULL)) + return NULL; + + model = gtk_tree_view_get_model (GTK_TREE_VIEW (widget)); + } + else if (widget == impl->browse_files_icon_view) + { + path = gtk_icon_view_get_path_at_pos (GTK_ICON_VIEW (widget), x, y); + if (!path) + return NULL; + + model = gtk_icon_view_get_model (GTK_ICON_VIEW (widget)); + } + else + return NULL; + + if (!model || !gtk_tree_model_get_iter (model, &iter, path)) + { + gtk_tree_path_free (path); + return NULL; + } + + gtk_tree_model_get (model, &iter, + MODEL_COL_FILE, &file, + MODEL_COL_IS_FOLDER, &is_folder, + MODEL_COL_IS_SENSITIVE, &is_sensitive, + -1); + + gtk_tree_path_free (path); + + if (file && is_folder && is_sensitive) + return file; + + if (file) + g_object_unref (file); + + return NULL; +} + static void file_list_drag_data_received_cb (GtkWidget *widget, GdkDragContext *context, @@ -6858,14 +6915,26 @@ file_list_drag_data_received_cb (GtkWidget *widget, char *uri; GFile *file; struct file_clipboard_paste_data *paste_data; + GFile *target_folder = NULL; + gboolean internal_drag = FALSE; impl = GTK_FILE_CHOOSER_DEFAULT (data); - /* Allow only drags from other widgets; see bug #533891. */ + /* + * Historically, bug #533891 disabled drops from the file list into itself. + * Keep that protection for drops onto empty space or regular files, but + * allow the useful case of dropping selected files onto a real folder. + */ if (gtk_drag_get_source_widget (context) == widget) { - g_signal_stop_emission_by_name (widget, "drag-data-received"); - return; + internal_drag = TRUE; + target_folder = file_list_get_drop_target_folder (impl, widget, x, y); + + if (!target_folder) + { + g_signal_stop_emission_by_name (widget, "drag-data-received"); + return; + } } /* Parse the text/uri-list string, navigate to the first one */ @@ -6877,7 +6946,16 @@ file_list_drag_data_received_cb (GtkWidget *widget, paste_data = file_clipboard_paste_data_from_uris (uris); if (paste_data) { - file_paste_operation_start (impl, paste_data); + if (!target_folder) + target_folder = file_list_get_drop_target_folder (impl, widget, x, y); + + if (internal_drag) + paste_data->cut = TRUE; + + file_paste_operation_start (impl, paste_data, + target_folder ? target_folder : impl->current_folder); + if (target_folder) + g_object_unref (target_folder); file_clipboard_paste_data_free (paste_data); g_strfreev (uris); From d24ca42b244ab2dddab8d29750f0260d133a98d0 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Tue, 26 May 2026 20:45:56 +0200 Subject: [PATCH 108/122] gtkfilechooser: keep location entry path current --- gtk/gtkfilechooserdefault.c | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 839763add..1b01a8636 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -8890,6 +8890,26 @@ location_mode_set (GtkFileChooserDefault *impl, impl->location_mode = new_mode; } +static void +location_set_current_folder_text (GtkFileChooserDefault *impl, + gboolean select_text) +{ + gchar *path; + + if (!impl->location_entry || !impl->current_folder) + return; + + path = g_file_get_parse_name (impl->current_folder); + gtk_entry_set_text (GTK_ENTRY (impl->location_entry), path); + + if (select_text) + gtk_editable_select_region (GTK_EDITABLE (impl->location_entry), 0, -1); + else + gtk_editable_set_position (GTK_EDITABLE (impl->location_entry), -1); + + g_free (path); +} + static void location_toggle_popup_handler (GtkFileChooserDefault *impl) { @@ -8907,6 +8927,7 @@ location_toggle_popup_handler (GtkFileChooserDefault *impl) if (impl->location_mode == LOCATION_MODE_PATH_BAR) { location_mode_set (impl, LOCATION_MODE_FILENAME_ENTRY, TRUE); + location_set_current_folder_text (impl, TRUE); } else if (impl->location_mode == LOCATION_MODE_FILENAME_ENTRY) { @@ -12474,6 +12495,10 @@ update_current_folder_get_info_cb (GCancellable *cancellable, if (data->clear_entry) gtk_entry_set_text (GTK_ENTRY (impl->location_entry), ""); + else if ((impl->action == GTK_FILE_CHOOSER_ACTION_OPEN || + impl->action == GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER) && + impl->location_mode == LOCATION_MODE_FILENAME_ENTRY) + location_set_current_folder_text (impl, FALSE); } /* Create a new list model. This is slightly evil; we store the result value From d4f2313aee7d05ece4e91e410f99603073df0691 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Wed, 27 May 2026 15:14:49 +0200 Subject: [PATCH 109/122] gtkfilechooser: improve location edit button --- gtk/gtkfilechooserdefault.c | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 1b01a8636..5135417d1 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -8900,6 +8900,16 @@ location_set_current_folder_text (GtkFileChooserDefault *impl, return; path = g_file_get_parse_name (impl->current_folder); + + if (!g_str_has_suffix (path, G_DIR_SEPARATOR_S)) + { + gchar *folder_path; + + folder_path = g_strconcat (path, G_DIR_SEPARATOR_S, NULL); + g_free (path); + path = folder_path; + } + gtk_entry_set_text (GTK_ENTRY (impl->location_entry), path); if (select_text) @@ -9621,6 +9631,9 @@ location_button_toggled_cb (GtkToggleButton *toggle, } location_mode_set (impl, new_mode, FALSE); + + if (is_active) + location_set_current_folder_text (impl, TRUE); } /* Creates a combo box with two items: List View and Icon View. */ @@ -9729,7 +9742,7 @@ location_button_create (GtkFileChooserDefault *impl) g_signal_connect (impl->location_button, "toggled", G_CALLBACK (location_button_toggled_cb), impl); - str = _("Type a file name"); + str = _("Edit Location"); gtk_widget_set_tooltip_text (impl->location_button, str); atk_object_set_name (gtk_widget_get_accessible (impl->location_button), str); From 2ccdfc445d9a3cfc09c2e0bfddd07aa37fc456c9 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Thu, 28 May 2026 11:00:41 +0200 Subject: [PATCH 110/122] gtkfilechooser: preserve location entry selection after typed folder changes --- gtk/gtkfilechooserdefault.c | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 5135417d1..1c593c572 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -8890,6 +8890,20 @@ location_mode_set (GtkFileChooserDefault *impl, impl->location_mode = new_mode; } +static gboolean +location_select_current_folder_text_idle (gpointer data) +{ + GtkFileChooserDefault *impl = GTK_FILE_CHOOSER_DEFAULT (data); + + if (impl->location_entry && + (impl->action == GTK_FILE_CHOOSER_ACTION_OPEN || + impl->action == GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER) && + impl->location_mode == LOCATION_MODE_FILENAME_ENTRY) + gtk_editable_select_region (GTK_EDITABLE (impl->location_entry), 0, -1); + + return FALSE; +} + static void location_set_current_folder_text (GtkFileChooserDefault *impl, gboolean select_text) @@ -8913,7 +8927,18 @@ location_set_current_folder_text (GtkFileChooserDefault *impl, gtk_entry_set_text (GTK_ENTRY (impl->location_entry), path); if (select_text) - gtk_editable_select_region (GTK_EDITABLE (impl->location_entry), 0, -1); + { + gtk_editable_select_region (GTK_EDITABLE (impl->location_entry), 0, -1); + + /* Folder changes from typed paths can trigger entry/completion updates + * after the text was set. Queue a final selection so keyboard-entered + * folders keep the full path selected. + */ + g_idle_add_full (G_PRIORITY_DEFAULT_IDLE, + location_select_current_folder_text_idle, + g_object_ref (impl), + g_object_unref); + } else gtk_editable_set_position (GTK_EDITABLE (impl->location_entry), -1); @@ -12506,12 +12531,12 @@ update_current_folder_get_info_cb (GCancellable *cancellable, _gtk_file_chooser_entry_set_base_folder (GTK_FILE_CHOOSER_ENTRY (impl->location_entry), impl->current_folder); - if (data->clear_entry) + if ((impl->action == GTK_FILE_CHOOSER_ACTION_OPEN || + impl->action == GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER) && + impl->location_mode == LOCATION_MODE_FILENAME_ENTRY) + location_set_current_folder_text (impl, data->clear_entry); + else if (data->clear_entry) gtk_entry_set_text (GTK_ENTRY (impl->location_entry), ""); - else if ((impl->action == GTK_FILE_CHOOSER_ACTION_OPEN || - impl->action == GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER) && - impl->location_mode == LOCATION_MODE_FILENAME_ENTRY) - location_set_current_folder_text (impl, FALSE); } /* Create a new list model. This is slightly evil; we store the result value From 95a727c219b2bc2b8a1cfd5d574d3d2e7d3f86bf Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Thu, 28 May 2026 12:03:22 +0200 Subject: [PATCH 111/122] gtkfilechooser: improve preview and thumbnail responsiveness --- gtk/gtkfilechooserdefault.c | 119 +++++++++++++++++++++++++++++++++--- gtk/gtkfilechooserprivate.h | 1 + 2 files changed, 113 insertions(+), 7 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 1c593c572..5f57fb1d8 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -1004,6 +1004,29 @@ typedef struct static gboolean thumbnail_cache_path_is_sufficient (const gchar *path, gint icon_size); +static gint +thumbnail_queue_get_cache_size (GtkFileChooserDefault *impl) +{ + gint icon_size; + + icon_size = thumbnail_queue_get_icon_size (impl); + + /* Request the next freedesktop thumbnail cache size before the + * visible icon reaches the previous cache limit. This avoids + * upscaling 128px thumbnails at larger icon-view zoom levels. + */ + if (icon_size >= (FILECHOOSER_THUMBNAIL_X_LARGE_SIZE * 5) / 8) + return FILECHOOSER_THUMBNAIL_XX_LARGE_SIZE; + + if (icon_size >= (FILECHOOSER_THUMBNAIL_LARGE_SIZE * 5) / 8) + return FILECHOOSER_THUMBNAIL_X_LARGE_SIZE; + + if (icon_size >= (FILECHOOSER_THUMBNAIL_NORMAL_SIZE * 5) / 8) + return FILECHOOSER_THUMBNAIL_LARGE_SIZE; + + return FILECHOOSER_THUMBNAIL_NORMAL_SIZE; +} + static gboolean thumbnail_queue_should_enqueue (GtkFileChooserDefault *impl, GFile *file, @@ -1013,7 +1036,7 @@ thumbnail_queue_should_enqueue (GtkFileChooserDefault *impl, const gchar *content_type; const gchar *thumbnail_path; goffset size; - gint icon_size; + gint cache_size; gint thumbnail_size; if (!impl->show_thumbnails) @@ -1022,11 +1045,11 @@ thumbnail_queue_should_enqueue (GtkFileChooserDefault *impl, if (file == NULL || info == NULL) return FALSE; - icon_size = thumbnail_queue_get_icon_size (impl); + cache_size = thumbnail_queue_get_cache_size (impl); thumbnail_path = g_file_info_get_attribute_byte_string (info, G_FILE_ATTRIBUTE_THUMBNAIL_PATH); - if (thumbnail_path && thumbnail_cache_path_is_sufficient (thumbnail_path, icon_size)) + if (thumbnail_path && thumbnail_cache_path_is_sufficient (thumbnail_path, cache_size)) return FALSE; if (g_file_info_get_attribute_boolean (info, G_FILE_ATTRIBUTE_THUMBNAILING_FAILED)) @@ -1039,7 +1062,7 @@ thumbnail_queue_should_enqueue (GtkFileChooserDefault *impl, FILECHOOSER_GENERATED_THUMBNAIL_SIZE); thumbnail = g_file_info_get_attribute_object (info, FILECHOOSER_GENERATED_THUMBNAIL); - if (thumbnail && thumbnail_size == icon_size) + if (thumbnail && thumbnail_size >= cache_size) return FALSE; content_type = g_file_info_get_content_type (info); @@ -1408,7 +1431,7 @@ thumbnail_worker_start (GtkFileChooserDefault *impl, job->file = file; /* takes ownership of the queue reference */ job->cancellable = g_object_ref (impl->thumbnail_cancellable); job->generation_serial = impl->thumbnail_generation_serial; - job->icon_size = thumbnail_queue_get_icon_size (impl); + job->icon_size = thumbnail_queue_get_cache_size (impl); #if GLIB_CHECK_VERSION (2, 32, 0) thread = g_thread_new ("gtk-filechooser-thumbnail", @@ -1535,6 +1558,12 @@ gtk_file_chooser_default_finalize (GObject *object) impl->icon_tooltip_text = NULL; + if (impl->preview_panel_update_timeout_id) + { + g_source_remove (impl->preview_panel_update_timeout_id); + impl->preview_panel_update_timeout_id = 0; + } + if (impl->preview_panel_resize_timeout_id) { g_source_remove (impl->preview_panel_resize_timeout_id); @@ -6471,6 +6500,31 @@ update_preview_panel (GtkFileChooserDefault *impl) g_object_unref (closure.info); } +static gboolean +preview_panel_update_timeout_cb (gpointer data) +{ + GtkFileChooserDefault *impl = GTK_FILE_CHOOSER_DEFAULT (data); + + impl->preview_panel_update_timeout_id = 0; + + update_preview_panel (impl); + + return FALSE; +} + +static void +preview_panel_queue_update (GtkFileChooserDefault *impl) +{ + if (!impl->preview_panel_active) + return; + + if (impl->preview_panel_update_timeout_id) + g_source_remove (impl->preview_panel_update_timeout_id); + + impl->preview_panel_update_timeout_id = + gdk_threads_add_timeout (75, preview_panel_update_timeout_cb, impl); +} + static gboolean preview_panel_resize_timeout_cb (gpointer data) { @@ -6479,6 +6533,12 @@ preview_panel_resize_timeout_cb (gpointer data) impl->preview_panel_resize_timeout_id = 0; + if (impl->preview_panel_update_timeout_id) + { + g_source_remove (impl->preview_panel_update_timeout_id); + impl->preview_panel_update_timeout_id = 0; + } + if (!impl->preview_panel_active || !impl->preview_panel_image) return FALSE; @@ -6532,6 +6592,12 @@ preview_panel_set_active (GtkFileChooserDefault *impl, { impl->preview_panel_active = active; + if (!active && impl->preview_panel_update_timeout_id) + { + g_source_remove (impl->preview_panel_update_timeout_id); + impl->preview_panel_update_timeout_id = 0; + } + if (!active && impl->preview_panel_resize_timeout_id) { g_source_remove (impl->preview_panel_resize_timeout_id); @@ -11845,6 +11911,42 @@ file_system_model_got_thumbnail (GObject *object, GAsyncResult *res, gpointer da GDK_THREADS_LEAVE (); } +static GdkPixbuf * +file_system_model_scale_pixbuf_to_fit (GdkPixbuf *pixbuf, + gint size) +{ + GdkPixbuf *scaled; + gint width; + gint height; + gint scaled_width; + gint scaled_height; + gdouble scale; + + if (pixbuf == NULL) + return NULL; + + width = gdk_pixbuf_get_width (pixbuf); + height = gdk_pixbuf_get_height (pixbuf); + + if (width <= 0 || height <= 0) + return pixbuf; + + if (width <= size && height <= size) + return pixbuf; + + scale = MIN ((gdouble) size / width, (gdouble) size / height); + scaled_width = MAX (1, (gint) (width * scale + 0.5)); + scaled_height = MAX (1, (gint) (height * scale + 0.5)); + + scaled = gdk_pixbuf_scale_simple (pixbuf, + scaled_width, + scaled_height, + GDK_INTERP_BILINEAR); + g_object_unref (pixbuf); + + return scaled; +} + static GdkPixbuf * file_system_model_render_icon (GtkFileChooserDefault *impl, GFileInfo *info, @@ -11862,8 +11964,9 @@ file_system_model_render_icon (GtkFileChooserDefault *impl, thumbnail_size = g_file_info_get_attribute_int32 (info, FILECHOOSER_GENERATED_THUMBNAIL_SIZE); - if (thumbnail && GDK_IS_PIXBUF (thumbnail) && thumbnail_size == icon_size) - return GDK_PIXBUF (g_object_ref (thumbnail)); + if (thumbnail && GDK_IS_PIXBUF (thumbnail) && thumbnail_size >= icon_size) + return file_system_model_scale_pixbuf_to_fit (GDK_PIXBUF (g_object_ref (thumbnail)), + icon_size); } if (impl->show_thumbnails || @@ -15321,6 +15424,8 @@ list_selection_changed (void *selection, path_bar_update (impl); check_preview_change (impl); + + preview_panel_queue_update (impl); bookmarks_check_add_sensitivity (impl); g_signal_emit_by_name (impl, "selection-changed", 0); diff --git a/gtk/gtkfilechooserprivate.h b/gtk/gtkfilechooserprivate.h index 319e96052..245dcf188 100644 --- a/gtk/gtkfilechooserprivate.h +++ b/gtk/gtkfilechooserprivate.h @@ -252,6 +252,7 @@ struct _GtkFileChooserDefault GtkWidget *preview_panel_title_label; GtkWidget *preview_panel_info_label; guint preview_panel_resize_timeout_id; + guint preview_panel_update_timeout_id; gint preview_panel_last_image_size; GtkWidget *extra_align; GtkWidget *extra_widget; From d83e126644b128b70156db7a0ca353ccd1c8dd76 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 29 May 2026 11:01:30 +0200 Subject: [PATCH 112/122] filechooser: validate cached thumbnail dimensions --- gtk/gtkfilechooserdefault.c | 231 ++++++++++++++++++++++++++++++------ 1 file changed, 195 insertions(+), 36 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 5f57fb1d8..c6dc58f95 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -1004,29 +1004,43 @@ typedef struct static gboolean thumbnail_cache_path_is_sufficient (const gchar *path, gint icon_size); +static gboolean thumbnail_debug_enabled (void); +static void thumbnail_debug_log_file (const gchar *where, + GFile *file, + const gchar *format, + ...) G_GNUC_PRINTF (3, 4); + static gint thumbnail_queue_get_cache_size (GtkFileChooserDefault *impl) { gint icon_size; + gint cache_size; icon_size = thumbnail_queue_get_icon_size (impl); - /* Request the next freedesktop thumbnail cache size before the - * visible icon reaches the previous cache limit. This avoids - * upscaling 128px thumbnails at larger icon-view zoom levels. + /* Use the smallest freedesktop thumbnail cache class that can cover + * the visible icon size. The real thumbnail geometry is checked later, + * so broken undersized cache files do not get accepted just because they + * live in thumbnails/normal/. */ - if (icon_size >= (FILECHOOSER_THUMBNAIL_X_LARGE_SIZE * 5) / 8) - return FILECHOOSER_THUMBNAIL_XX_LARGE_SIZE; + if (icon_size <= FILECHOOSER_THUMBNAIL_NORMAL_SIZE) + cache_size = FILECHOOSER_THUMBNAIL_NORMAL_SIZE; + else if (icon_size <= FILECHOOSER_THUMBNAIL_LARGE_SIZE) + cache_size = FILECHOOSER_THUMBNAIL_LARGE_SIZE; + else if (icon_size <= FILECHOOSER_THUMBNAIL_X_LARGE_SIZE) + cache_size = FILECHOOSER_THUMBNAIL_X_LARGE_SIZE; + else + cache_size = FILECHOOSER_THUMBNAIL_XX_LARGE_SIZE; - if (icon_size >= (FILECHOOSER_THUMBNAIL_LARGE_SIZE * 5) / 8) - return FILECHOOSER_THUMBNAIL_X_LARGE_SIZE; + if (thumbnail_debug_enabled ()) + g_debug ("GtkFileChooser thumbnail: cache-size: visible=%d cache=%d", + icon_size, + cache_size); - if (icon_size >= (FILECHOOSER_THUMBNAIL_NORMAL_SIZE * 5) / 8) - return FILECHOOSER_THUMBNAIL_LARGE_SIZE; - - return FILECHOOSER_THUMBNAIL_NORMAL_SIZE; + return cache_size; } + static gboolean thumbnail_queue_should_enqueue (GtkFileChooserDefault *impl, GFile *file, @@ -1136,21 +1150,23 @@ thumbnail_cache_size_dir_for_dimension (gint dimension) static gint thumbnail_cache_max_dimension_for_path (const gchar *path) { + gint width = 0; + gint height = 0; + if (path == NULL) return 0; - if (strstr (path, G_DIR_SEPARATOR_S "normal" G_DIR_SEPARATOR_S)) - return FILECHOOSER_THUMBNAIL_NORMAL_SIZE; - if (strstr (path, G_DIR_SEPARATOR_S "large" G_DIR_SEPARATOR_S)) - return FILECHOOSER_THUMBNAIL_LARGE_SIZE; - if (strstr (path, G_DIR_SEPARATOR_S "x-large" G_DIR_SEPARATOR_S)) - return FILECHOOSER_THUMBNAIL_X_LARGE_SIZE; - if (strstr (path, G_DIR_SEPARATOR_S "xx-large" G_DIR_SEPARATOR_S)) - return FILECHOOSER_THUMBNAIL_XX_LARGE_SIZE; + /* Do not infer the thumbnail size from the cache directory name. + * Existing cache files can be smaller than their freedesktop cache class + * allows, for example a real 32x32 PNG inside thumbnails/normal/. + */ + if (gdk_pixbuf_get_file_info (path, &width, &height) == NULL) + return 0; - return G_MAXINT; + return MAX (width, height); } + static gboolean thumbnail_cache_path_is_sufficient (const gchar *path, gint icon_size) @@ -1158,6 +1174,45 @@ thumbnail_cache_path_is_sufficient (const gchar *path, return thumbnail_cache_max_dimension_for_path (path) >= icon_size; } +static gboolean +thumbnail_debug_enabled (void) +{ + static gint enabled = -1; + + if (enabled == -1) + enabled = g_getenv ("GTK_FILE_CHOOSER_THUMBNAIL_DEBUG") != NULL; + + return enabled; +} + +static void +thumbnail_debug_log_file (const gchar *where, + GFile *file, + const gchar *format, + ...) +{ + va_list args; + gchar *message; + gchar *uri; + + if (!thumbnail_debug_enabled ()) + return; + + va_start (args, format); + message = g_strdup_vprintf (format, args); + va_end (args); + + uri = file ? g_file_get_uri (file) : NULL; + + g_debug ("GtkFileChooser thumbnail: %s: %s: %s", + where, + uri ? uri : "(no file)", + message); + + g_free (uri); + g_free (message); +} + static gchar * thumbnail_cache_lookup (ThumbnailJob *job) { @@ -1327,7 +1382,6 @@ thumbnail_job_apply_cb (gpointer data) GFileInfo *info; info = g_file_info_dup (_gtk_file_system_model_get_info (model, &iter)); - if (job->pixbuf || job->thumbnail_path) { if (job->pixbuf) @@ -1341,9 +1395,51 @@ thumbnail_job_apply_cb (gpointer data) } if (job->thumbnail_path) - g_file_info_set_attribute_byte_string (info, - G_FILE_ATTRIBUTE_THUMBNAIL_PATH, - job->thumbnail_path); + { + const gchar *current_thumbnail_path; + gchar *debug_current_thumbnail_path; + gboolean accepted_thumbnail_path; + gint current_dimension; + gint job_dimension; + + current_thumbnail_path = g_file_info_get_attribute_byte_string (info, + G_FILE_ATTRIBUTE_THUMBNAIL_PATH); + debug_current_thumbnail_path = NULL; + if (thumbnail_debug_enabled ()) + debug_current_thumbnail_path = g_strdup (current_thumbnail_path); + + current_dimension = thumbnail_cache_max_dimension_for_path (current_thumbnail_path); + job_dimension = thumbnail_cache_max_dimension_for_path (job->thumbnail_path); + + /* Prefer the smallest thumbnail that is still sufficient for + * the current target size. This allows replacing an existing + * x-large thumbnail with a normal one at 64px icon size, but + * still prevents replacing a sufficient large thumbnail with + * an undersized one at higher zoom levels. + */ + accepted_thumbnail_path = (!current_thumbnail_path || + job_dimension >= current_dimension || + job_dimension >= job->icon_size); + + if (accepted_thumbnail_path) + g_file_info_set_attribute_byte_string (info, + G_FILE_ATTRIBUTE_THUMBNAIL_PATH, + job->thumbnail_path); + + if (thumbnail_debug_enabled ()) + { + thumbnail_debug_log_file ("worker-path-decision", + job->file, + "target-size=%d current=%s current-dim=%d job=%s job-dim=%d accepted=%d", + job->icon_size, + debug_current_thumbnail_path ? debug_current_thumbnail_path : "(none)", + current_dimension, + job->thumbnail_path, + job_dimension, + accepted_thumbnail_path); + g_free (debug_current_thumbnail_path); + } + } } else if (job->failed) { @@ -1352,6 +1448,22 @@ thumbnail_job_apply_cb (gpointer data) TRUE); } + thumbnail_debug_log_file ("worker-apply", + job->file, + "target-size=%d pixbuf=%dx%d path=%s failed=%d", + job->icon_size, + job->pixbuf ? gdk_pixbuf_get_width (job->pixbuf) : 0, + job->pixbuf ? gdk_pixbuf_get_height (job->pixbuf) : 0, + job->thumbnail_path ? job->thumbnail_path : "(none)", + job->failed); + + _gtk_file_system_model_clear_cache_for_iter (model, + &iter, + MODEL_COL_LIST_PIXBUF); + _gtk_file_system_model_clear_cache_for_iter (model, + &iter, + MODEL_COL_ICON_PIXBUF); + _gtk_file_system_model_update_file (model, job->file, info); g_object_unref (info); } @@ -1433,6 +1545,11 @@ thumbnail_worker_start (GtkFileChooserDefault *impl, job->generation_serial = impl->thumbnail_generation_serial; job->icon_size = thumbnail_queue_get_cache_size (impl); + thumbnail_debug_log_file ("worker-start", + job->file, + "target-size=%d", + job->icon_size); + #if GLIB_CHECK_VERSION (2, 32, 0) thread = g_thread_new ("gtk-filechooser-thumbnail", thumbnail_worker_thread, @@ -7335,11 +7452,11 @@ file_list_build_popup_menu (GtkFileChooserDefault *impl) static void file_list_update_popup_menu (GtkFileChooserDefault *impl) { - guint selected_count; + gint file_count; file_list_build_popup_menu (impl); - selected_count = current_selection_count_selected_rows (impl); + selection_check (impl, &file_count, NULL, NULL); /* FIXME - handle OPERATION_MODE_SEARCH and OPERATION_MODE_RECENT */ @@ -7349,12 +7466,12 @@ file_list_update_popup_menu (GtkFileChooserDefault *impl) if (impl->browse_files_popup_menu_copy_item) gtk_widget_set_sensitive (impl->browse_files_popup_menu_copy_item, - selected_count > 0); + file_count > 0); if (impl->browse_files_popup_menu_cut_item) gtk_widget_set_sensitive (impl->browse_files_popup_menu_cut_item, impl->operation_mode == OPERATION_MODE_BROWSE && - selected_count > 0); + file_count > 0); if (impl->browse_files_popup_menu_paste_item) gtk_widget_set_sensitive (impl->browse_files_popup_menu_paste_item, @@ -7364,7 +7481,7 @@ file_list_update_popup_menu (GtkFileChooserDefault *impl) if (impl->browse_files_popup_menu_copy_location_item) gtk_widget_set_sensitive (impl->browse_files_popup_menu_copy_location_item, - selected_count > 0); + file_count > 0); if (impl->browse_files_popup_menu_open_containing_folder_item) { @@ -7379,18 +7496,18 @@ file_list_update_popup_menu (GtkFileChooserDefault *impl) gtk_widget_set_sensitive (impl->browse_files_popup_menu_open_containing_folder_item, show_item && - selected_count == 1); + file_count == 1); } if (impl->browse_files_popup_menu_rename_item) gtk_widget_set_sensitive (impl->browse_files_popup_menu_rename_item, impl->operation_mode == OPERATION_MODE_BROWSE && - selected_count == 1); + file_count == 1); if (impl->browse_files_popup_menu_trash_item) gtk_widget_set_sensitive (impl->browse_files_popup_menu_trash_item, impl->operation_mode == OPERATION_MODE_BROWSE && - selected_count > 0); + file_count > 0); /* 'Show Hidden Files' */ g_signal_handlers_block_by_func (impl->browse_files_popup_menu_hidden_files_item, @@ -11896,15 +12013,41 @@ file_system_model_got_thumbnail (GObject *object, GAsyncResult *res, gpointer da info = g_file_info_dup (_gtk_file_system_model_get_info (model, &iter)); - copy_attribute (info, queried, G_FILE_ATTRIBUTE_THUMBNAIL_PATH); - copy_attribute (info, queried, G_FILE_ATTRIBUTE_THUMBNAILING_FAILED); + thumbnail_debug_log_file ("gio-info", + file, + "queried-path=%s current-path=%s generated-size=%d generated=%d", + g_file_info_get_attribute_byte_string (queried, G_FILE_ATTRIBUTE_THUMBNAIL_PATH) ? + g_file_info_get_attribute_byte_string (queried, G_FILE_ATTRIBUTE_THUMBNAIL_PATH) : "(none)", + g_file_info_get_attribute_byte_string (info, G_FILE_ATTRIBUTE_THUMBNAIL_PATH) ? + g_file_info_get_attribute_byte_string (info, G_FILE_ATTRIBUTE_THUMBNAIL_PATH) : "(none)", + g_file_info_get_attribute_int32 (info, FILECHOOSER_GENERATED_THUMBNAIL_SIZE), + g_file_info_get_attribute_object (info, FILECHOOSER_GENERATED_THUMBNAIL) != NULL); + + { + const gchar *current_thumbnail_path; + const gchar *queried_thumbnail_path; + + current_thumbnail_path = g_file_info_get_attribute_byte_string (info, + G_FILE_ATTRIBUTE_THUMBNAIL_PATH); + queried_thumbnail_path = g_file_info_get_attribute_byte_string (queried, + G_FILE_ATTRIBUTE_THUMBNAIL_PATH); + + /* GIO may report an already-existing larger freedesktop thumbnail. + * Keep it, but never replace an existing better path with a smaller one. + */ + if (queried_thumbnail_path && + (!current_thumbnail_path || + thumbnail_cache_max_dimension_for_path (queried_thumbnail_path) >= + thumbnail_cache_max_dimension_for_path (current_thumbnail_path))) + copy_attribute (info, queried, G_FILE_ATTRIBUTE_THUMBNAIL_PATH); + } + copy_attribute (info, queried, G_FILE_ATTRIBUTE_STANDARD_ICON); copy_attribute (info, queried, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE); copy_attribute (info, queried, G_FILE_ATTRIBUTE_STANDARD_SIZE); copy_attribute (info, queried, G_FILE_ATTRIBUTE_TIME_MODIFIED); _gtk_file_system_model_update_file (model, file, info); - g_object_unref (info); g_object_unref (queried); @@ -11971,7 +12114,23 @@ file_system_model_render_icon (GtkFileChooserDefault *impl, if (impl->show_thumbnails || !g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_THUMBNAIL_PATH)) - return _gtk_file_info_render_icon (info, GTK_WIDGET (impl), icon_size); + { + const gchar *thumbnail_path; + + thumbnail_path = g_file_info_get_attribute_byte_string (info, + G_FILE_ATTRIBUTE_THUMBNAIL_PATH); + pixbuf = _gtk_file_info_render_icon (info, GTK_WIDGET (impl), icon_size); + + if (thumbnail_debug_enabled ()) + g_debug ("GtkFileChooser thumbnail: render-path: icon-size=%d path=%s path-dim=%d pixbuf=%dx%d", + icon_size, + thumbnail_path ? thumbnail_path : "(none)", + thumbnail_cache_max_dimension_for_path (thumbnail_path), + pixbuf ? gdk_pixbuf_get_width (pixbuf) : 0, + pixbuf ? gdk_pixbuf_get_height (pixbuf) : 0); + + return pixbuf; + } icon_info = g_file_info_dup (info); g_file_info_remove_attribute (icon_info, G_FILE_ATTRIBUTE_THUMBNAIL_PATH); From 1d4ea5dac3f02a9558d4cf46198bd50a7b6098a4 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 29 May 2026 11:15:07 +0200 Subject: [PATCH 113/122] filechooser: skip stale thumbnail queue entries --- gtk/gtkfilechooserdefault.c | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index c6dc58f95..1bf79db86 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -1569,6 +1569,27 @@ thumbnail_worker_start (GtkFileChooserDefault *impl, return TRUE; } +static gboolean +thumbnail_queue_file_still_needed (GtkFileChooserDefault *impl, + GFile *file) +{ + GtkFileSystemModel *model; + GtkTreeIter iter; + GFileInfo *info; + + if (!impl->current_model || !GTK_IS_FILE_SYSTEM_MODEL (impl->current_model)) + return FALSE; + + model = GTK_FILE_SYSTEM_MODEL (impl->current_model); + + if (!_gtk_file_system_model_get_iter_for_file (model, &iter, file)) + return FALSE; + + info = _gtk_file_system_model_get_info (model, &iter); + + return thumbnail_queue_should_enqueue (impl, file, info); +} + static gboolean thumbnail_queue_idle_cb (gpointer data) { @@ -1590,6 +1611,17 @@ thumbnail_queue_idle_cb (gpointer data) return FALSE; } + if (!thumbnail_queue_file_still_needed (impl, file)) + { + g_object_unref (file); + impl->thumbnail_worker_running = FALSE; + + if (impl->thumbnail_queue) + thumbnail_queue_schedule (impl); + + return FALSE; + } + if (!thumbnail_worker_start (impl, file)) { g_object_unref (file); From f9c14692670528b6e69a13a0789cd18d2be90cf8 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 29 May 2026 11:41:25 +0200 Subject: [PATCH 114/122] filechooser: render cached thumbnails asynchronously --- gtk/gtkfilechooserdefault.c | 87 +++++++++++++++++++++++++++++++++---- 1 file changed, 78 insertions(+), 9 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 1bf79db86..84df3d172 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -998,6 +998,7 @@ typedef struct GdkPixbuf *pixbuf; gchar *uri; gchar *thumbnail_path; + gchar *queried_thumbnail_path; gboolean failed; } ThumbnailJob; @@ -1048,7 +1049,6 @@ thumbnail_queue_should_enqueue (GtkFileChooserDefault *impl, { GObject *thumbnail; const gchar *content_type; - const gchar *thumbnail_path; goffset size; gint cache_size; gint thumbnail_size; @@ -1061,11 +1061,6 @@ thumbnail_queue_should_enqueue (GtkFileChooserDefault *impl, cache_size = thumbnail_queue_get_cache_size (impl); - thumbnail_path = g_file_info_get_attribute_byte_string (info, - G_FILE_ATTRIBUTE_THUMBNAIL_PATH); - if (thumbnail_path && thumbnail_cache_path_is_sufficient (thumbnail_path, cache_size)) - return FALSE; - if (g_file_info_get_attribute_boolean (info, G_FILE_ATTRIBUTE_THUMBNAILING_FAILED)) return FALSE; @@ -1349,6 +1344,7 @@ thumbnail_job_free (ThumbnailJob *job) g_object_unref (job->cancellable); g_free (job->thumbnail_path); + g_free (job->queried_thumbnail_path); g_free (job->uri); if (job->file) @@ -1487,7 +1483,33 @@ thumbnail_worker_thread (gpointer data) job->uri = g_file_get_uri (job->file); job->thumbnail_path = thumbnail_cache_lookup (job); - if (job->thumbnail_path) + + if (job->thumbnail_path && !g_cancellable_is_cancelled (job->cancellable)) + job->pixbuf = gdk_pixbuf_new_from_file_at_scale (job->thumbnail_path, + job->icon_size, + job->icon_size, + TRUE, + NULL); + + if (!job->pixbuf && job->thumbnail_path) + { + g_free (job->thumbnail_path); + job->thumbnail_path = NULL; + } + + if (!job->pixbuf && job->queried_thumbnail_path && !g_cancellable_is_cancelled (job->cancellable)) + { + job->pixbuf = gdk_pixbuf_new_from_file_at_scale (job->queried_thumbnail_path, + job->icon_size, + job->icon_size, + TRUE, + NULL); + + if (job->pixbuf) + job->thumbnail_path = g_strdup (job->queried_thumbnail_path); + } + + if (job->pixbuf) { gdk_threads_add_idle (thumbnail_job_apply_cb, job); return NULL; @@ -1524,6 +1546,34 @@ thumbnail_worker_thread (gpointer data) return NULL; } + +static gchar * +thumbnail_queue_get_current_thumbnail_path (GtkFileChooserDefault *impl, + GFile *file) +{ + GtkFileSystemModel *model; + GtkTreeIter iter; + GFileInfo *info; + const gchar *thumbnail_path; + + if (!impl->current_model || !GTK_IS_FILE_SYSTEM_MODEL (impl->current_model)) + return NULL; + + model = GTK_FILE_SYSTEM_MODEL (impl->current_model); + + if (!_gtk_file_system_model_get_iter_for_file (model, &iter, file)) + return NULL; + + info = _gtk_file_system_model_get_info (model, &iter); + if (info == NULL) + return NULL; + + thumbnail_path = g_file_info_get_attribute_byte_string (info, + G_FILE_ATTRIBUTE_THUMBNAIL_PATH); + + return g_strdup (thumbnail_path); +} + static gboolean thumbnail_worker_start (GtkFileChooserDefault *impl, GFile *file) @@ -1544,6 +1594,7 @@ thumbnail_worker_start (GtkFileChooserDefault *impl, job->cancellable = g_object_ref (impl->thumbnail_cancellable); job->generation_serial = impl->thumbnail_generation_serial; job->icon_size = thumbnail_queue_get_cache_size (impl); + job->queried_thumbnail_path = thumbnail_queue_get_current_thumbnail_path (impl, file); thumbnail_debug_log_file ("worker-start", job->file, @@ -12144,8 +12195,26 @@ file_system_model_render_icon (GtkFileChooserDefault *impl, icon_size); } - if (impl->show_thumbnails || - !g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_THUMBNAIL_PATH)) + if (impl->show_thumbnails && + g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_THUMBNAIL_PATH)) + { + icon_info = g_file_info_dup (info); + g_file_info_remove_attribute (icon_info, G_FILE_ATTRIBUTE_THUMBNAIL_PATH); + + pixbuf = _gtk_file_info_render_icon (icon_info, GTK_WIDGET (impl), icon_size); + + if (thumbnail_debug_enabled ()) + g_debug ("GtkFileChooser thumbnail: render-standard-icon: icon-size=%d pixbuf=%dx%d", + icon_size, + pixbuf ? gdk_pixbuf_get_width (pixbuf) : 0, + pixbuf ? gdk_pixbuf_get_height (pixbuf) : 0); + + g_object_unref (icon_info); + + return pixbuf; + } + + if (!g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_THUMBNAIL_PATH)) { const gchar *thumbnail_path; From 0d8a8a1183a1dc671d656db4ffdcaaeed05a16a7 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 29 May 2026 11:52:39 +0200 Subject: [PATCH 115/122] filechooser: cache thumbnail placeholder icons --- gtk/gtkfilechooserdefault.c | 51 ++++++++++++++++++++++++++++++++----- gtk/gtkfilechooserprivate.h | 2 ++ 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 84df3d172..4300897ee 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -894,6 +894,48 @@ pending_select_files_add (GtkFileChooserDefault *impl, g_slist_prepend (impl->pending_select_files, g_object_ref (file)); } + +static void +thumbnail_standard_icon_cache_clear (GtkFileChooserDefault *impl) +{ + if (impl->thumbnail_standard_icon_pixbuf) + { + g_object_unref (impl->thumbnail_standard_icon_pixbuf); + impl->thumbnail_standard_icon_pixbuf = NULL; + } + + impl->thumbnail_standard_icon_size = 0; +} + +static GdkPixbuf * +thumbnail_standard_icon_cache_get (GtkFileChooserDefault *impl, + GFileInfo *info, + gint icon_size) +{ + GFileInfo *icon_info; + GdkPixbuf *pixbuf; + + if (impl->thumbnail_standard_icon_pixbuf && + impl->thumbnail_standard_icon_size == icon_size) + return g_object_ref (impl->thumbnail_standard_icon_pixbuf); + + thumbnail_standard_icon_cache_clear (impl); + + icon_info = g_file_info_dup (info); + g_file_info_remove_attribute (icon_info, G_FILE_ATTRIBUTE_THUMBNAIL_PATH); + + pixbuf = _gtk_file_info_render_icon (icon_info, GTK_WIDGET (impl), icon_size); + g_object_unref (icon_info); + + if (pixbuf == NULL) + return NULL; + + impl->thumbnail_standard_icon_pixbuf = g_object_ref (pixbuf); + impl->thumbnail_standard_icon_size = icon_size; + + return pixbuf; +} + static void thumbnail_queue_clear (GtkFileChooserDefault *impl) { @@ -1741,6 +1783,7 @@ gtk_file_chooser_default_finalize (GObject *object) icon_view_scale_remove_timeout (impl); thumbnail_queue_clear (impl); + thumbnail_standard_icon_cache_clear (impl); if (impl->file_clipboard_cut_files) { @@ -11221,6 +11264,7 @@ gtk_file_chooser_default_style_set (GtkWidget *widget, profile_start ("start", NULL); impl = GTK_FILE_CHOOSER_DEFAULT (widget); + thumbnail_standard_icon_cache_clear (impl); profile_msg (" parent class style_set start", NULL); GTK_WIDGET_CLASS (_gtk_file_chooser_default_parent_class)->style_set (widget, previous_style); @@ -12198,10 +12242,7 @@ file_system_model_render_icon (GtkFileChooserDefault *impl, if (impl->show_thumbnails && g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_THUMBNAIL_PATH)) { - icon_info = g_file_info_dup (info); - g_file_info_remove_attribute (icon_info, G_FILE_ATTRIBUTE_THUMBNAIL_PATH); - - pixbuf = _gtk_file_info_render_icon (icon_info, GTK_WIDGET (impl), icon_size); + pixbuf = thumbnail_standard_icon_cache_get (impl, info, icon_size); if (thumbnail_debug_enabled ()) g_debug ("GtkFileChooser thumbnail: render-standard-icon: icon-size=%d pixbuf=%dx%d", @@ -12209,8 +12250,6 @@ file_system_model_render_icon (GtkFileChooserDefault *impl, pixbuf ? gdk_pixbuf_get_width (pixbuf) : 0, pixbuf ? gdk_pixbuf_get_height (pixbuf) : 0); - g_object_unref (icon_info); - return pixbuf; } diff --git a/gtk/gtkfilechooserprivate.h b/gtk/gtkfilechooserprivate.h index 245dcf188..606ea0b7e 100644 --- a/gtk/gtkfilechooserprivate.h +++ b/gtk/gtkfilechooserprivate.h @@ -281,6 +281,8 @@ struct _GtkFileChooserDefault guint thumbnail_queue_idle_id; guint thumbnail_generation_serial; guint thumbnail_worker_running : 1; + GdkPixbuf *thumbnail_standard_icon_pixbuf; + gint thumbnail_standard_icon_size; GtkWidget *location_button; GtkWidget *location_entry_box; From 79f3ac57e57b6dd2911ed4f7688386da5d1330c3 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 29 May 2026 13:05:22 +0200 Subject: [PATCH 116/122] filechooser: prioritize visible thumbnail jobs --- gtk/gtkfilechooserdefault.c | 102 ++++++++++++++++++++++++++++-------- gtk/gtkfilechooserprivate.h | 1 + 2 files changed, 81 insertions(+), 22 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 4300897ee..1c2e8ecd5 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -955,6 +955,7 @@ thumbnail_queue_clear (GtkFileChooserDefault *impl) g_slist_foreach (impl->thumbnail_queue, (GFunc) g_object_unref, NULL); g_slist_free (impl->thumbnail_queue); impl->thumbnail_queue = NULL; + impl->thumbnail_queue_tail = NULL; if (impl->thumbnail_queued_files) { @@ -1161,6 +1162,9 @@ thumbnail_queue_pop_next (GtkFileChooserDefault *impl) impl->thumbnail_queue = g_slist_delete_link (impl->thumbnail_queue, link); + if (impl->thumbnail_queue == NULL) + impl->thumbnail_queue_tail = NULL; + if (impl->thumbnail_queued_files) g_hash_table_remove (impl->thumbnail_queued_files, file); @@ -1741,24 +1745,77 @@ thumbnail_queue_schedule (GtkFileChooserDefault *impl) impl); } +static void +thumbnail_queue_promote (GtkFileChooserDefault *impl, + GFile *file) +{ + GSList *prev; + GSList *link; + + prev = NULL; + link = impl->thumbnail_queue; + + while (link) + { + if (g_file_equal (G_FILE (link->data), file)) + break; + + prev = link; + link = link->next; + } + + if (link == NULL || prev == NULL) + return; + + prev->next = link->next; + + if (impl->thumbnail_queue_tail == link) + impl->thumbnail_queue_tail = prev; + + link->next = impl->thumbnail_queue; + impl->thumbnail_queue = link; +} + static void thumbnail_queue_enqueue (GtkFileChooserDefault *impl, GFile *file, - GFileInfo *info) + GFileInfo *info, + gboolean high_priority) { + GSList *link; if (!thumbnail_queue_should_enqueue (impl, file, info)) return; thumbnail_queue_ensure (impl); if (g_hash_table_lookup (impl->thumbnail_queued_files, file)) - return; + { + if (high_priority) + thumbnail_queue_promote (impl, file); + + return; + } g_hash_table_insert (impl->thumbnail_queued_files, g_object_ref (file), GUINT_TO_POINTER (1)); - impl->thumbnail_queue = g_slist_prepend (impl->thumbnail_queue, - g_object_ref (file)); + link = g_slist_alloc (); + link->data = g_object_ref (file); + link->next = NULL; + + if (high_priority || impl->thumbnail_queue == NULL) + { + link->next = impl->thumbnail_queue; + impl->thumbnail_queue = link; + + if (impl->thumbnail_queue_tail == NULL) + impl->thumbnail_queue_tail = link; + } + else + { + impl->thumbnail_queue_tail->next = link; + impl->thumbnail_queue_tail = link; + } thumbnail_queue_schedule (impl); } @@ -12384,6 +12441,23 @@ file_system_model_set (GtkFileSystemModel *model, if (tree_model != GTK_TREE_MODEL (model)) return FALSE; + file_visible = FALSE; + if (get_visible_range (&start, &end, impl)) + { + if (!_gtk_file_system_model_get_iter_for_file (model, + &iter, + file)) + g_assert_not_reached (); + + path = gtk_tree_model_get_path (tree_model, &iter); + file_visible = (gtk_tree_path_compare (start, path) != 1 && + gtk_tree_path_compare (path, end) != 1); + + gtk_tree_path_free (path); + gtk_tree_path_free (start); + gtk_tree_path_free (end); + } + /* #1 use standard icon if it is loaded */ if (g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_STANDARD_ICON)) { @@ -12404,31 +12478,15 @@ file_system_model_set (GtkFileSystemModel *model, icon_size = FALLBACK_LIST_VIEW_ICON_SIZE; } - thumbnail_queue_enqueue (impl, file, info); + thumbnail_queue_enqueue (impl, file, info, file_visible); g_value_take_object (value, file_system_model_render_icon (impl, info, icon_size)); return TRUE; } - if (!get_visible_range (&start, &end, impl)) - return FALSE; - - if (!_gtk_file_system_model_get_iter_for_file (model, - &iter, - file)) - g_assert_not_reached (); - - path = gtk_tree_model_get_path (tree_model, &iter); - file_visible = (gtk_tree_path_compare (start, path) != 1 && - gtk_tree_path_compare (path, end) != 1); - - gtk_tree_path_free (path); - gtk_tree_path_free (start); - gtk_tree_path_free (end); - if (file_visible) { - thumbnail_queue_enqueue (impl, file, info); + thumbnail_queue_enqueue (impl, file, info, file_visible); /* #2 start loading standard icon (callback will be handled by #1) */ if (!g_file_info_has_attribute (info, "filechooser::icon_queried")) diff --git a/gtk/gtkfilechooserprivate.h b/gtk/gtkfilechooserprivate.h index 606ea0b7e..d879bc997 100644 --- a/gtk/gtkfilechooserprivate.h +++ b/gtk/gtkfilechooserprivate.h @@ -275,6 +275,7 @@ struct _GtkFileChooserDefault * running for files that are no longer useful to the user. */ GSList *thumbnail_queue; + GSList *thumbnail_queue_tail; GHashTable *thumbnail_queued_files; GHashTable *file_clipboard_cut_files; GCancellable *thumbnail_cancellable; From 781d8c974a91f10c0150a65b24628a086494e9f6 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 29 May 2026 13:12:33 +0200 Subject: [PATCH 117/122] filechooser: prioritize nearby thumbnail jobs --- gtk/gtkfilechooserdefault.c | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 1c2e8ecd5..f58b3ee83 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -12432,6 +12432,11 @@ file_system_model_set (GtkFileSystemModel *model, GtkTreePath *path, *start, *end; GtkTreeIter iter; gboolean file_visible; + gint path_index; + gint start_index; + gint end_index; + gint visible_items; + gint overscan_items; /* not loading icon view's icon in the list view */ if (column == MODEL_COL_ICON_PIXBUF && impl->view_mode == VIEW_MODE_LIST) @@ -12450,8 +12455,15 @@ file_system_model_set (GtkFileSystemModel *model, g_assert_not_reached (); path = gtk_tree_model_get_path (tree_model, &iter); - file_visible = (gtk_tree_path_compare (start, path) != 1 && - gtk_tree_path_compare (path, end) != 1); + path_index = gtk_tree_path_get_indices (path)[0]; + start_index = gtk_tree_path_get_indices (start)[0]; + end_index = gtk_tree_path_get_indices (end)[0]; + + visible_items = MAX (1, end_index - start_index + 1); + overscan_items = CLAMP (visible_items * 2, 16, 64); + + file_visible = (path_index >= start_index - overscan_items && + path_index <= end_index + overscan_items); gtk_tree_path_free (path); gtk_tree_path_free (start); From 437b7949dd39bd24381df5dfb2203e2aa16284d0 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Fri, 29 May 2026 13:28:47 +0200 Subject: [PATCH 118/122] filechooser: clarify nearby thumbnail priority --- gtk/gtkfilechooserdefault.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index f58b3ee83..6d465cae2 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -12431,7 +12431,7 @@ file_system_model_set (GtkFileSystemModel *model, GtkTreeModel *tree_model; GtkTreePath *path, *start, *end; GtkTreeIter iter; - gboolean file_visible; + gboolean file_near_visible; gint path_index; gint start_index; gint end_index; @@ -12446,7 +12446,7 @@ file_system_model_set (GtkFileSystemModel *model, if (tree_model != GTK_TREE_MODEL (model)) return FALSE; - file_visible = FALSE; + file_near_visible = FALSE; if (get_visible_range (&start, &end, impl)) { if (!_gtk_file_system_model_get_iter_for_file (model, @@ -12462,7 +12462,7 @@ file_system_model_set (GtkFileSystemModel *model, visible_items = MAX (1, end_index - start_index + 1); overscan_items = CLAMP (visible_items * 2, 16, 64); - file_visible = (path_index >= start_index - overscan_items && + file_near_visible = (path_index >= start_index - overscan_items && path_index <= end_index + overscan_items); gtk_tree_path_free (path); @@ -12490,15 +12490,15 @@ file_system_model_set (GtkFileSystemModel *model, icon_size = FALLBACK_LIST_VIEW_ICON_SIZE; } - thumbnail_queue_enqueue (impl, file, info, file_visible); + thumbnail_queue_enqueue (impl, file, info, file_near_visible); g_value_take_object (value, file_system_model_render_icon (impl, info, icon_size)); return TRUE; } - if (file_visible) + if (file_near_visible) { - thumbnail_queue_enqueue (impl, file, info, file_visible); + thumbnail_queue_enqueue (impl, file, info, file_near_visible); /* #2 start loading standard icon (callback will be handled by #1) */ if (!g_file_info_has_attribute (info, "filechooser::icon_queried")) From d0c833a8a7fcfa4929bdf589d229764bdd512feb Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Tue, 1 Sep 2026 11:28:00 +0200 Subject: [PATCH 119/122] gtkfilesystemmodel: make directory loading cooperative and race-safe Rework GtkFileSystemModel around stable node IDs, sparse value caching and cooperative directory integration. Time-slice initial sorting, visibility publication and monitor replay so large directories do not block the main loop. Queue monitor changes during loading and prevent stale asynchronous monitor queries from resurrecting deleted or superseded files. Delay FileChooser model publication until loading and replay complete, and apply the initial sort directly to the browse model. --- gtk/gtkfilechooserdefault.c | 81 +- gtk/gtkfilechooserprivate.h | 4 +- gtk/gtkfilesystemmodel.c | 1848 ++++++++++++++++++++++------------- 3 files changed, 1199 insertions(+), 734 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 6d465cae2..10e303dcf 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -141,14 +141,12 @@ _gtk_file_chooser_profile_log (const char *func, int indent, const char *msg1, c #endif - typedef struct _GtkFileChooserDefaultClass GtkFileChooserDefaultClass; #define GTK_FILE_CHOOSER_DEFAULT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_FILE_CHOOSER_DEFAULT, GtkFileChooserDefaultClass)) #define GTK_IS_FILE_CHOOSER_DEFAULT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_FILE_CHOOSER_DEFAULT)) #define GTK_FILE_CHOOSER_DEFAULT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GTK_TYPE_FILE_CHOOSER_DEFAULT, GtkFileChooserDefaultClass)) -#define MAX_LOADING_TIME 500 #define DEFAULT_NEW_FOLDER_NAME _("Type name of new folder") @@ -427,7 +425,6 @@ static void remove_bookmark_button_clicked_cb (GtkButton *button, static void update_cell_renderer_attributes (GtkFileChooserDefault *impl); -static void load_remove_timer (GtkFileChooserDefault *impl, LoadState new_load_state); static void browse_files_center_selected_row (GtkFileChooserDefault *impl); static void view_mode_set (GtkFileChooserDefault *impl, ViewMode view_mode); @@ -1908,9 +1905,6 @@ gtk_file_chooser_default_finalize (GObject *object) search_clear_model (impl, FALSE); recent_clear_model (impl, FALSE); - /* stopping the load above should have cleared this */ - g_assert (impl->load_timeout_id == 0); - g_free (impl->preview_display_name); if (impl->rename_idle) @@ -11719,61 +11713,8 @@ load_set_model (GtkFileChooserDefault *impl) profile_end ("end", NULL); } -/* Timeout callback used when the loading timer expires */ -static gboolean -load_timeout_cb (gpointer data) -{ - GtkFileChooserDefault *impl; - profile_start ("start", NULL); - impl = GTK_FILE_CHOOSER_DEFAULT (data); - g_assert (impl->load_state == LOAD_PRELOAD); - g_assert (impl->load_timeout_id != 0); - g_assert (impl->browse_files_model != NULL); - - impl->load_timeout_id = 0; - impl->load_state = LOAD_LOADING; - - load_set_model (impl); - - profile_end ("end", NULL); - - return FALSE; -} - -/* Sets up a new load timer for the model and switches to the LOAD_PRELOAD state */ -static void -load_setup_timer (GtkFileChooserDefault *impl) -{ - g_assert (impl->load_timeout_id == 0); - g_assert (impl->load_state != LOAD_PRELOAD); - - impl->load_timeout_id = gdk_threads_add_timeout (MAX_LOADING_TIME, load_timeout_cb, impl); - impl->load_state = LOAD_PRELOAD; -} - -/* Removes the load timeout; changes the impl->load_state to the specified value. */ -static void -load_remove_timer (GtkFileChooserDefault *impl, LoadState new_load_state) -{ - if (impl->load_timeout_id != 0) - { - g_assert (impl->load_state == LOAD_PRELOAD); - - g_source_remove (impl->load_timeout_id); - impl->load_timeout_id = 0; - } - else - g_assert (impl->load_state == LOAD_EMPTY || - impl->load_state == LOAD_LOADING || - impl->load_state == LOAD_FINISHED); - - g_assert (new_load_state == LOAD_EMPTY || - new_load_state == LOAD_LOADING || - new_load_state == LOAD_FINISHED); - impl->load_state = new_load_state; -} /* Selects the first row in the file list */ static void @@ -11992,15 +11933,8 @@ browse_files_model_finished_loading_cb (GtkFileSystemModel *model, if (error) show_error_on_reading_current_folder (impl, error); - if (impl->load_state == LOAD_PRELOAD) - { - load_remove_timer (impl, LOAD_FINISHED); - load_set_model (impl); - } - else if (impl->load_state == LOAD_LOADING) - { - /* Nothing */ - } + if (impl->load_state == LOAD_LOADING) + load_set_model (impl); else { /* We can't g_assert_not_reached(), as something other than us may have @@ -12010,8 +11944,6 @@ browse_files_model_finished_loading_cb (GtkFileSystemModel *model, return; } - g_assert (impl->load_timeout_id == 0); - impl->load_state = LOAD_FINISHED; thumbnail_file_list_clear_pixbuf_caches (impl); @@ -12029,7 +11961,7 @@ static void stop_loading_and_clear_list_model (GtkFileChooserDefault *impl, gboolean remove_from_treeview) { - load_remove_timer (impl, LOAD_EMPTY); + impl->load_state = LOAD_EMPTY; if (impl->browse_files_model) { @@ -12625,12 +12557,15 @@ set_list_model (GtkFileChooserDefault *impl, gtk_tree_sortable_set_sort_func (GTK_TREE_SORTABLE (impl->browse_files_model), MODEL_COL_SIZE, size_sort_func, impl, NULL); gtk_tree_sortable_set_sort_func (GTK_TREE_SORTABLE (impl->browse_files_model), MODEL_COL_MTIME, mtime_sort_func, impl, NULL); gtk_tree_sortable_set_default_sort_func (GTK_TREE_SORTABLE (impl->browse_files_model), NULL, NULL, NULL); - set_sort_column (impl); + gtk_tree_sortable_set_sort_column_id + (GTK_TREE_SORTABLE (impl->browse_files_model), + impl->sort_column, + impl->sort_order); impl->list_sort_ascending = TRUE; g_signal_connect (impl->browse_files_model, "sort-column-changed", G_CALLBACK (list_sort_column_changed_cb), impl); - load_setup_timer (impl); /* This changes the state to LOAD_PRELOAD */ + impl->load_state = LOAD_LOADING; g_signal_connect (impl->browse_files_model, "finished-loading", G_CALLBACK (browse_files_model_finished_loading_cb), impl); diff --git a/gtk/gtkfilechooserprivate.h b/gtk/gtkfilechooserprivate.h index d879bc997..00f94a209 100644 --- a/gtk/gtkfilechooserprivate.h +++ b/gtk/gtkfilechooserprivate.h @@ -122,8 +122,7 @@ struct _GtkFileChooserWidgetPrivate typedef enum { LOAD_EMPTY, /* There is no model */ - LOAD_PRELOAD, /* Model is loading and a timer is running; model isn't inserted into the tree yet */ - LOAD_LOADING, /* Timeout expired, model is inserted into the tree, but not fully loaded yet */ + LOAD_LOADING, /* Model is loading; inserted into the view when loading finishes */ LOAD_FINISHED /* Model is fully loaded and inserted into the tree */ } LoadState; @@ -310,7 +309,6 @@ struct _GtkFileChooserDefault LoadState load_state; ReloadState reload_state; - guint load_timeout_id; OperationMode operation_mode; diff --git a/gtk/gtkfilesystemmodel.c b/gtk/gtkfilesystemmodel.c index 207cea2c5..5e2c6b7c8 100644 --- a/gtk/gtkfilesystemmodel.c +++ b/gtk/gtkfilesystemmodel.c @@ -36,89 +36,17 @@ /*** Structure: how GtkFileSystemModel works * - * This is a custom GtkTreeModel used to hold a collection of files for GtkFileChooser. There are two use cases: + * File nodes have stable IDs in fixed-size slot storage. model->order holds + * logical sort order and model->visible_order is the GtkTreeModel projection. + * file_lookup permanently maps GFile to stable node ID. * - * 1. The model populates itself from a folder, using the GIO file enumerator API. This happens if you use - * _gtk_file_system_model_new_for_directory(). This is the normal usage for showing the contents of a folder. + * Cached column values live in sparse 64-ID pages allocated per used column, + * so unused columns have no per-node GValue storage. * - * 2. The caller populates the model by hand, with files not necessarily in the same folder. This happens - * if you use _gtk_file_system_model_new() and then _gtk_file_system_model_add_and_query_file(). This is - * the special kind of usage for "search" and "recent-files", where the file chooser gives the model the - * files to be displayed. - * - * Internal data structure - * ----------------------- - * - * Each file is kept in a FileModelNode structure. Each FileModelNode holds a GFile* and other data. All the - * node structures have the same size, determined at runtime, depending on the number of columns that were passed - * to _gtk_file_system_model_new() or _gtk_file_system_model_new_for_directory() (that is, the size of a node is - * not sizeof (FileModelNode), but rather model->node_size). The last field in the FileModelNode structure, - * node->values[], is an array of GValue, used to hold the data for those columns. - * - * The model stores an array of FileModelNode structures in model->files. This is a GArray where each element is - * model->node_size bytes in size (the model computes that node size when initializing itself). There are - * convenience macros, get_node() and node_index(), to access that array based on an array index or a pointer to - * a node inside the array. - * - * The model accesses files through two of its fields: - * - * model->files - GArray of FileModelNode structures. - * - * model->file_lookup - hash table that maps a GFile* to an index inside the model->files array. - * - * The model->file_lookup hash table is populated lazily. It is both accessed and populated with the - * node_get_for_file() function. The invariant is that the files in model->files[n] for n < g_hash_table_size - * (model->file_lookup) are already added to the hash table. The hash table will get cleared when we re-sort the - * files, as the array will be in a different order and the indexes need to be rebuilt. - * - * Each FileModelNode has a node->visible field, which indicates whether the node is visible in the GtkTreeView. - * A node may be invisible if, for example, it corresponds to a hidden file and the file chooser is not showing - * hidden files. Also, a file filter may be explicitly set onto the model, for example, to only show files that - * match "*.jpg". In this case, node->filtered_out says whether the node failed the filter. The ultimate - * decision on whether a node is visible or not in the treeview is distilled into the node->visible field. - * The reason for having a separate node->filtered_out field is so that the file chooser can query whether - * a (filtered-out) folder should be made sensitive in the GUI. - * - * Visible rows vs. possibly-invisible nodes - * ----------------------------------------- - * - * Since not all nodes in the model->files array may be visible, we need a way to map visible row indexes from - * the treeview to array indexes in our array of files. And thus we introduce a bit of terminology: - * - * index - An index in the model->files array. All variables/fields that represent indexes are either called - * "index" or "i_*", or simply "i" for things like loop counters. - * - * row - An index in the GtkTreeView, i.e. the index of a row within the outward-facing API of the - * GtkFileSystemModel. However, note that our rows are 1-based, not 0-based, for the reason explained in the - * following paragraph. Variables/fields that represent visible rows are called "row", or "r_*", or simply - * "r". - * - * Each FileModelNode has a node->row field which is the number of visible rows in the treeview, *before and - * including* that node. This means that node->row is 1-based, instead of 0-based --- this makes some code - * simpler, believe it or not :) This also means that when the calling GtkTreeView gives us a GtkTreePath, we - * turn the 0-based treepath into a 1-based row for our purposes. If a node is not visible, it will have the - * same row number as its closest preceding visible node. - * - * We try to compute the node->row fields lazily. A node is said to be "valid" if its node->row is accurate. - * For this, the model keeps a model->n_nodes_valid field which is the count of valid nodes starting from the - * beginning of the model->files array. When a node changes its information, or when a node gets deleted, that - * node and the following ones get invalidated by simply setting model->n_nodes_valid to the array index of the - * node. If the model happens to need a node's row number and that node is in the model->files array after - * model->n_nodes_valid, then the nodes get re-validated up to the sought node. See node_validate_rows() for - * this logic. - * - * You never access a node->row directly. Instead, call node_get_tree_row(). That function will validate the nodes - * up to the sought one if the node is not valid yet, and it will return a proper 0-based row. - * - * Sorting - * ------- - * - * The model implements the GtkTreeSortable interface. To avoid re-sorting - * every time a node gets added (which would lead to O(n^2) performance during - * the initial population of the model), the model can freeze itself (with - * freeze_updates()) during the intial population process. When the model is - * frozen, sorting will not happen. The model will sort itself when the freeze - * count goes back to zero, via corresponding calls to thaw_updates(). + * Directory enumeration uses one bounded GIO batch as its pending queue. + * Integration is cooperatively drained in short main-loop slices; the next + * GIO batch is requested only after the current one has been consumed. + * Editing has its own hold state and no longer reuses directory batching. */ /*** DEFINES ***/ @@ -130,69 +58,107 @@ /* random number that everyone else seems to use, too */ #define FILES_PER_QUERY 100 +#define MODEL_DRAIN_SLICE_US 2000 +#define MODEL_DRAIN_CLOCK_GRANULARITY 4 +#define MODEL_FINALIZE_CLOCK_GRANULARITY 8 + +enum +{ + MODEL_FINALIZE_NONE, + MODEL_FINALIZE_MERGE, + MODEL_FINALIZE_VISIBLE, + MODEL_FINALIZE_REPLAY +}; + +#define VALUE_PAGE_SHIFT 6 +#define VALUE_PAGE_SIZE (1u << VALUE_PAGE_SHIFT) +#define VALUE_PAGE_MASK (VALUE_PAGE_SIZE - 1u) typedef struct _FileModelNode FileModelNode; typedef struct _GtkFileSystemModelClass GtkFileSystemModelClass; struct _FileModelNode { - GFile * file; /* file represented by this node or NULL for editable */ - GFileInfo * info; /* info for this file or NULL if unknown */ + GFile * file; + GFileInfo * info; - guint row; /* if valid (see model->n_valid_indexes), visible nodes before and including - * this one - see the "Structure" comment above. - */ + guint order_pos; + guint visible_pos; + guint generation; - guint visible :1; /* if the file is currently visible */ - guint filtered_out :1;/* if the file is currently filtered out (i.e. it didn't pass the filters) */ - guint frozen_add :1; /* true if the model was frozen and the entry has not been added yet */ - - GValue values[1]; /* actually n_columns values */ + guint visible :1; + guint filtered_out :1; + guint held_add :1; }; +typedef struct +{ + GValue values[VALUE_PAGE_SIZE]; +} ValuePage; + +typedef struct +{ + GFile *file; + GFileMonitorEvent type; +} PendingMonitorChange; + +typedef GWeakRef PendingMonitorQuery; + struct _GtkFileSystemModel { GObject parent_instance; + GFile * dir; + char * attributes; + GFileMonitor * dir_monitor; + GFileEnumerator * dir_enumerator; + GCancellable * cancellable; + GList * pending_files; + guint drain_source; - GFile * dir; /* directory that's displayed */ - guint dir_thaw_source;/* GSource id for unfreezing the model */ - char * attributes; /* attributes the file info must contain, or NULL for all attributes */ - GFileMonitor * dir_monitor; /* directory that is monitored, or NULL if monitoring was not supported */ + GArray * sort_scratch; + guint sort_width; + guint sort_left; + guint sort_i; + guint sort_j; + guint sort_k; + guint sort_mid; + guint sort_right; + guint finalize_phase; + GError * load_error; - GCancellable * cancellable; /* cancellable in use for all operations - cancelled on dispose */ - GArray * files; /* array of FileModelNode containing all our files */ - gsize node_size; /* Size of a FileModelNode structure once its ->values field has n_columns */ - guint n_nodes_valid; /* count of valid nodes (i.e. those whose node->row is accurate) */ - GHashTable * file_lookup; /* mapping of GFile => array index in model->files - * This hash table doesn't always have the same number of entries as the files array; - * it can get cleared completely when we resort. - * The hash table gets re-populated in node_get_for_file() if this mismatch is - * detected. - */ + GQueue * pending_monitor_changes; + GHashTable * pending_monitor_queries; - guint n_columns; /* number of columns */ - GType * column_types; /* types of each column */ - GtkFileSystemModelGetValue get_func; /* function to call to fill in values in columns */ - gpointer get_data; /* data to pass to get_func */ + GArray * files; + GArray * order; + GArray * visible_order; + guint free_id_head; + GHashTable * file_lookup; - GtkFileFilter * filter; /* filter to use for deciding which nodes are visible */ + guint n_columns; + GType * column_types; + GPtrArray ** value_pages; + GtkFileSystemModelGetValue get_func; + gpointer get_data; - int sort_column_id; /* current sorting column */ - GtkSortType sort_order; /* current sorting order */ - GList * sort_list; /* list of sorting functions */ - GtkTreeIterCompareFunc default_sort_func; /* default sort function */ - gpointer default_sort_data; /* data to pass to default sort func */ - GDestroyNotify default_sort_destroy; /* function to call to destroy default_sort_data */ + GtkFileFilter * filter; - guint frozen; /* number of times we're frozen */ + int sort_column_id; + GtkSortType sort_order; + GList * sort_list; + GtkTreeIterCompareFunc default_sort_func; + gpointer default_sort_data; + GDestroyNotify default_sort_destroy; - gboolean filter_on_thaw :1;/* set when filtering needs to happen upon thawing */ - gboolean sort_on_thaw :1;/* set when sorting needs to happen upon thawing */ + guint editing_hold :1; + guint refilter_pending :1; + guint resort_pending :1; + guint directory_loading :1; - guint show_hidden :1; /* whether to show hidden files */ - guint show_folders :1;/* whether to show folders */ - guint show_files :1; /* whether to show files */ - guint filter_folders :1;/* whether filter applies to folders */ + guint show_hidden :1; + guint show_folders :1; + guint show_files :1; + guint filter_folders :1; }; #define GTK_FILE_SYSTEM_MODEL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_FILE_SYSTEM_MODEL, GtkFileSystemModelClass)) @@ -208,8 +174,17 @@ struct _GtkFileSystemModelClass void (*finished_loading) (GtkFileSystemModel *model, GError *error); }; -static void freeze_updates (GtkFileSystemModel *model); -static void thaw_updates (GtkFileSystemModel *model); +static void gtk_file_system_model_got_files (GObject *object, + GAsyncResult *res, + gpointer data); +static gboolean gtk_file_system_model_drain (gpointer data); +static void gtk_file_system_model_schedule_work (GtkFileSystemModel *model); +static gboolean gtk_file_system_model_finalize_load (gpointer data); +static void gtk_file_system_model_begin_finalize_load (GtkFileSystemModel *model, + GError *error); +static void gtk_file_system_model_apply_monitor_change (GtkFileSystemModel *model, + GFile *file, + GFileMonitorEvent type); static guint node_get_for_file (GtkFileSystemModel *model, GFile *file); @@ -221,89 +196,118 @@ static void remove_file (GtkFileSystemModel *model, GFile *file); /* iter setup: - * @user_data: the model - * @user_data2: GUINT_TO_POINTER of array index of current entry - * - * All other fields are unused. Note that the array index does not corrspond - * 1:1 with the path index as entries might not be visible. + * @user_data: the model + * @user_data2: stable node ID + * @user_data3: node generation */ -#define ITER_INDEX(iter) GPOINTER_TO_UINT((iter)->user_data2) -#define ITER_IS_VALID(model, iter) ((model) == (iter)->user_data) -#define ITER_INIT_FROM_INDEX(model, _iter, _index) G_STMT_START {\ - g_assert (_index < (model)->files->len); \ +#define ITER_INDEX(iter) GPOINTER_TO_UINT ((iter)->user_data2) + +#define get_node(_model, _index) \ + (&g_array_index ((_model)->files, FileModelNode, (_index))) + +static gboolean +iter_is_valid (GtkFileSystemModel *model, + GtkTreeIter *iter) +{ + guint id; + FileModelNode *node; + + if (iter == NULL || iter->user_data != model) + return FALSE; + + id = ITER_INDEX (iter); + if (id >= model->files->len) + return FALSE; + + node = get_node (model, id); + return (id == 0 || node->file != NULL) && + node->generation == GPOINTER_TO_UINT (iter->user_data3); +} + +#define ITER_IS_VALID(model, iter) iter_is_valid ((model), (iter)) + +#define ITER_INIT_FROM_INDEX(model, _iter, _index) G_STMT_START { \ + FileModelNode *_node; \ + g_assert ((_index) < (model)->files->len); \ + _node = get_node ((model), (_index)); \ + g_assert ((_index) == 0 || _node->file != NULL); \ (_iter)->user_data = (model); \ (_iter)->user_data2 = GUINT_TO_POINTER (_index); \ -}G_STMT_END + (_iter)->user_data3 = GUINT_TO_POINTER (_node->generation); \ +} G_STMT_END -/*** FileModelNode ***/ - -/* Get a FileModelNode structure given an index in the model->files array of nodes */ -#define get_node(_model, _index) ((FileModelNode *) ((_model)->files->data + (_index) * (_model)->node_size)) - -/* Get an index within the model->files array of nodes, given a FileModelNode* */ -#define node_index(_model, _node) (((gchar *) (_node) - (_model)->files->data) / (_model)->node_size) - -/* @up_to_index: smallest model->files array index that will be valid after this call - * @up_to_row: smallest node->row that will be valid after this call - * - * If you want to validate up to an index or up to a row, specify the index or - * the row you want and specify G_MAXUINT for the other argument. Pass - * G_MAXUINT for both arguments for "validate everything". - */ static void -node_validate_rows (GtkFileSystemModel *model, guint up_to_index, guint up_to_row) +order_update_positions_from (GtkFileSystemModel *model, + guint first) { - guint i, row; + guint i; - if (model->files->len == 0) - return; - - up_to_index = MIN (up_to_index, model->files->len - 1); - - i = model->n_nodes_valid; - if (i != 0) - row = get_node (model, i - 1)->row; - else - row = 0; - - while (i <= up_to_index && row <= up_to_row) + for (i = first; i < model->order->len; i++) { - FileModelNode *node = get_node (model, i); - if (node->visible) - row++; - node->row = row; - i++; + guint id = g_array_index (model->order, guint, i); + get_node (model, id)->order_pos = i; + } +} + +static void +visible_update_positions_from (GtkFileSystemModel *model, + guint first) +{ + guint i; + + for (i = first; i < model->visible_order->len; i++) + { + guint id = g_array_index (model->visible_order, guint, i); + get_node (model, id)->visible_pos = i; } - model->n_nodes_valid = i; } static guint -node_get_tree_row (GtkFileSystemModel *model, guint index) +visible_find_order_pos (GtkFileSystemModel *model, + guint order_pos, + gboolean after) { - if (model->n_nodes_valid <= index) - node_validate_rows (model, index, G_MAXUINT); + guint lo = get_node (model, 0)->visible ? 1 : 0; + guint hi = model->visible_order->len; - return get_node (model, index)->row - 1; -} + while (lo < hi) + { + guint mid = lo + (hi - lo) / 2; + guint id = g_array_index (model->visible_order, guint, mid); + guint pos = get_node (model, id)->order_pos; -static void -node_invalidate_index (GtkFileSystemModel *model, guint id) -{ - model->n_nodes_valid = MIN (model->n_nodes_valid, id); + if (pos < order_pos || (after && pos == order_pos)) + lo = mid + 1; + else + hi = mid; + } + + return lo; } static GtkTreePath * -tree_path_new_from_node (GtkFileSystemModel *model, guint id) +tree_path_new_from_node (GtkFileSystemModel *model, + guint id) { - guint r = node_get_tree_row (model, id); + FileModelNode *node = get_node (model, id); + guint row = G_MAXUINT; - g_assert (r < model->files->len); + if (node->visible) + row = node->visible_pos; + else if (id != 0) + { + guint pos = visible_find_order_pos (model, node->order_pos, TRUE); + if (pos > 0) + row = pos - 1; + } - return gtk_tree_path_new_from_indices (r, -1); + g_assert (row != G_MAXUINT); + return gtk_tree_path_new_from_indices (row, -1); } static void -emit_row_inserted_for_node (GtkFileSystemModel *model, guint id) +emit_row_inserted_for_node (GtkFileSystemModel *model, + guint id) { GtkTreePath *path; GtkTreeIter iter; @@ -315,7 +319,8 @@ emit_row_inserted_for_node (GtkFileSystemModel *model, guint id) } static void -emit_row_changed_for_node (GtkFileSystemModel *model, guint id) +emit_row_changed_for_node (GtkFileSystemModel *model, + guint id) { GtkTreePath *path; GtkTreeIter iter; @@ -327,7 +332,8 @@ emit_row_changed_for_node (GtkFileSystemModel *model, guint id) } static void -emit_row_deleted_for_row (GtkFileSystemModel *model, guint row) +emit_row_deleted_for_row (GtkFileSystemModel *model, + guint row) { GtkTreePath *path; @@ -337,11 +343,167 @@ emit_row_deleted_for_row (GtkFileSystemModel *model, guint row) } static void -node_set_visible_and_filtered_out (GtkFileSystemModel *model, guint id, gboolean visible, gboolean filtered_out) +visible_remove_node (GtkFileSystemModel *model, + guint id) { FileModelNode *node = get_node (model, id); + guint pos; - /* Filteredness */ + g_assert (node->visible); + pos = node->visible_pos; + + g_array_remove_index (model->visible_order, pos); + node->visible = FALSE; + visible_update_positions_from (model, pos); +} + +static void +value_page_free (gpointer data) +{ + ValuePage *page = data; + guint slot; + + if (page) + for (slot = 0; slot < VALUE_PAGE_SIZE; slot++) + if (G_VALUE_TYPE (&page->values[slot]) != G_TYPE_INVALID) + g_value_unset (&page->values[slot]); + + g_free (page); +} + +static void +file_model_node_clear (gpointer data) +{ + FileModelNode *node = data; + + g_clear_object (&node->file); + g_clear_object (&node->info); +} + +static GValue * +value_cache_slot (GtkFileSystemModel *model, + guint id, + guint column, + gboolean create) +{ + GPtrArray *pages; + ValuePage *page; + guint page_index; + + g_assert (column < model->n_columns); + + pages = model->value_pages[column]; + if (pages == NULL) + { + if (!create) + return NULL; + + pages = g_ptr_array_new_with_free_func (value_page_free); + model->value_pages[column] = pages; + } + + page_index = id >> VALUE_PAGE_SHIFT; + if (page_index >= pages->len) + { + if (!create) + return NULL; + + g_ptr_array_set_size (pages, page_index + 1); + } + + page = pages->pdata[page_index]; + if (page == NULL && create) + { + page = g_new0 (ValuePage, 1); + pages->pdata[page_index] = page; + } + + if (page == NULL) + return NULL; + + return &page->values[id & VALUE_PAGE_MASK]; +} + +static gboolean +value_cache_clear_slot (GtkFileSystemModel *model, + guint id, + guint column) +{ + GValue *value = value_cache_slot (model, id, column, FALSE); + + if (value == NULL || G_VALUE_TYPE (value) == G_TYPE_INVALID) + return FALSE; + + g_value_unset (value); + return TRUE; +} + +static void +value_cache_clear_node (GtkFileSystemModel *model, + guint id) +{ + guint column; + + for (column = 0; column < model->n_columns; column++) + value_cache_clear_slot (model, id, column); +} + + + +static guint +node_allocate_slot (GtkFileSystemModel *model) +{ + FileModelNode *node; + guint id; + + if (model->free_id_head != 0) + { + id = model->free_id_head; + node = get_node (model, id); + model->free_id_head = node->order_pos; + } + else + { + id = model->files->len; + g_array_set_size (model->files, id + 1); + node = get_node (model, id); + node->generation = 1; + } + + return id; +} + +static void +node_retire_slot (GtkFileSystemModel *model, + guint id) +{ + FileModelNode *node; + guint generation; + + g_assert (id != 0); + + node = get_node (model, id); + file_model_node_clear (node); + value_cache_clear_node (model, id); + + generation = node->generation + 1; + if (generation == 0) + generation = 1; + + memset (node, 0, sizeof (*node)); + node->generation = generation; + node->order_pos = model->free_id_head; + + model->free_id_head = id; +} + +static void +node_set_visible_and_filtered_out (GtkFileSystemModel *model, + guint id, + gboolean visible, + gboolean filtered_out) +{ + FileModelNode *node = get_node (model, id); if (node->filtered_out != filtered_out) { @@ -350,27 +512,25 @@ node_set_visible_and_filtered_out (GtkFileSystemModel *model, guint id, gboolean emit_row_changed_for_node (model, id); } - /* Visibility */ - - if (node->visible == visible || - node->frozen_add) + if (node->visible == visible || node->held_add) return; if (visible) { + guint pos; + + g_assert (!node->visible); + pos = id == 0 ? 0 : + visible_find_order_pos (model, node->order_pos, FALSE); + g_array_insert_val (model->visible_order, pos, id); node->visible = TRUE; - node_invalidate_index (model, id); + visible_update_positions_from (model, pos); emit_row_inserted_for_node (model, id); } else { - guint row; - - row = node_get_tree_row (model, id); - g_assert (row < model->files->len); - - node->visible = FALSE; - node_invalidate_index (model, id); + guint row = node->visible_pos; + visible_remove_node (model, id); emit_row_deleted_for_row (model, row); } } @@ -524,73 +684,29 @@ gtk_file_system_model_get_column_type (GtkTreeModel *tree_model, return model->column_types[i]; } -static int -compare_indices (gconstpointer key, gconstpointer _node) -{ - const FileModelNode *node = _node; - - return GPOINTER_TO_UINT (key) - node->row; -} - static gboolean gtk_file_system_model_iter_nth_child (GtkTreeModel *tree_model, - GtkTreeIter *iter, - GtkTreeIter *parent, - gint n) + GtkTreeIter *iter, + GtkTreeIter *parent, + gint n) { GtkFileSystemModel *model = GTK_FILE_SYSTEM_MODEL (tree_model); - char *node; guint id; - guint row_to_find; g_return_val_if_fail (n >= 0, FALSE); - if (parent != NULL) + if (parent != NULL || (guint) n >= model->visible_order->len) return FALSE; - row_to_find = n + 1; /* plus one as our node->row numbers are 1-based; see the "Structure" comment at the beginning */ - - if (model->n_nodes_valid > 0 && - get_node (model, model->n_nodes_valid - 1)->row >= row_to_find) - { - /* Fast path - the nodes are valid up to the sought one. - * - * First, find a node with the sought row number...*/ - - node = bsearch (GUINT_TO_POINTER (row_to_find), - model->files->data, - model->n_nodes_valid, - model->node_size, - compare_indices); - if (node == NULL) - return FALSE; - - /* ... Second, back up until we find the first visible node with that row number */ - - id = node_index (model, node); - while (!get_node (model, id)->visible) - id--; - - g_assert (get_node (model, id)->row == row_to_find); - } - else - { - /* Slow path - the nodes need to be validated up to the sought one */ - - node_validate_rows (model, G_MAXUINT, n); /* note that this is really "n", not row_to_find - see node_validate_rows() */ - id = model->n_nodes_valid - 1; - if (model->n_nodes_valid == 0 || get_node (model, id)->row != row_to_find) - return FALSE; - } - + id = g_array_index (model->visible_order, guint, n); ITER_INIT_FROM_INDEX (model, iter, id); return TRUE; } static gboolean gtk_file_system_model_get_iter (GtkTreeModel *tree_model, - GtkTreeIter *iter, - GtkTreePath *path) + GtkTreeIter *iter, + GtkTreePath *path) { g_return_val_if_fail (gtk_tree_path_get_depth (path) > 0, FALSE); @@ -605,12 +721,11 @@ gtk_file_system_model_get_iter (GtkTreeModel *tree_model, static GtkTreePath * gtk_file_system_model_get_path (GtkTreeModel *tree_model, - GtkTreeIter *iter) + GtkTreeIter *iter) { GtkFileSystemModel *model = GTK_FILE_SYSTEM_MODEL (tree_model); g_return_val_if_fail (ITER_IS_VALID (model, iter), NULL); - return tree_path_new_from_node (model, ITER_INDEX (iter)); } @@ -638,25 +753,29 @@ gtk_file_system_model_get_value (GtkTreeModel *tree_model, static gboolean gtk_file_system_model_iter_next (GtkTreeModel *tree_model, - GtkTreeIter *iter) + GtkTreeIter *iter) { GtkFileSystemModel *model = GTK_FILE_SYSTEM_MODEL (tree_model); - guint i; + FileModelNode *node; + guint pos; g_return_val_if_fail (ITER_IS_VALID (model, iter), FALSE); - for (i = ITER_INDEX (iter) + 1; i < model->files->len; i++) - { - FileModelNode *node = get_node (model, i); + node = get_node (model, ITER_INDEX (iter)); - if (node->visible) - { - ITER_INIT_FROM_INDEX (model, iter, i); - return TRUE; - } - } + if (node->visible) + pos = node->visible_pos + 1; + else if (ITER_INDEX (iter) != 0) + pos = visible_find_order_pos (model, node->order_pos, TRUE); + else + return FALSE; - return FALSE; + if (pos >= model->visible_order->len) + return FALSE; + + ITER_INIT_FROM_INDEX (model, iter, + g_array_index (model->visible_order, guint, pos)); + return TRUE; } static gboolean @@ -676,14 +795,14 @@ gtk_file_system_model_iter_has_child (GtkTreeModel *tree_model, static gint gtk_file_system_model_iter_n_children (GtkTreeModel *tree_model, - GtkTreeIter *iter) + GtkTreeIter *iter) { GtkFileSystemModel *model = GTK_FILE_SYSTEM_MODEL (tree_model); if (iter) return 0; - return node_get_tree_row (model, model->files->len - 1) + 1; + return model->visible_order->len; } static gboolean @@ -743,7 +862,7 @@ sort_data_init (SortData *data, GtkFileSystemModel *model) { GtkTreeDataSortHeader *header; - if (model->files->len <= 2) + if (model->order->len <= 1) return FALSE; switch (model->sort_column_id) @@ -757,7 +876,8 @@ sort_data_init (SortData *data, GtkFileSystemModel *model) case GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID: return FALSE; default: - header = _gtk_tree_data_list_get_header (model->sort_list, model->sort_column_id); + header = _gtk_tree_data_list_get_header (model->sort_list, + model->sort_column_id); if (header == NULL) return FALSE; data->func = header->func; @@ -771,14 +891,67 @@ sort_data_init (SortData *data, GtkFileSystemModel *model) } static int -compare_array_element (gconstpointer a, gconstpointer b, gpointer user_data) +compare_order_element (gconstpointer a, gconstpointer b, gpointer user_data) { SortData *data = user_data; GtkTreeIter itera, iterb; + guint ida = *(const guint *) a; + guint idb = *(const guint *) b; - ITER_INIT_FROM_INDEX (data->model, &itera, node_index (data->model, a)); - ITER_INIT_FROM_INDEX (data->model, &iterb, node_index (data->model, b)); - return data->func (GTK_TREE_MODEL (data->model), &itera, &iterb, data->data) * data->order; + ITER_INIT_FROM_INDEX (data->model, &itera, ida); + ITER_INIT_FROM_INDEX (data->model, &iterb, idb); + + return data->func (GTK_TREE_MODEL (data->model), + &itera, &iterb, data->data) * data->order; +} + +static void +visible_order_rebuild_after_sort (GtkFileSystemModel *model) +{ + guint old_len = model->visible_order->len; + int *new_order; + guint row = 0; + guint i; + FileModelNode *editable; + + if (old_len == 0) + return; + + new_order = g_new (int, old_len); + g_array_set_size (model->visible_order, 0); + + editable = get_node (model, 0); + if (editable->visible) + { + guint id = 0; + new_order[row] = editable->visible_pos; + g_array_append_val (model->visible_order, id); + editable->visible_pos = row++; + } + + for (i = 0; i < model->order->len; i++) + { + guint id = g_array_index (model->order, guint, i); + FileModelNode *node = get_node (model, id); + + if (!node->visible) + continue; + + new_order[row] = node->visible_pos; + g_array_append_val (model->visible_order, id); + node->visible_pos = row++; + } + + g_assert (row == old_len); + + { + GtkTreePath *path = gtk_tree_path_new (); + gtk_tree_model_rows_reordered (GTK_TREE_MODEL (model), + path, NULL, new_order); + gtk_tree_path_free (path); + } + + g_free (new_order); } static void @@ -786,67 +959,26 @@ gtk_file_system_model_sort (GtkFileSystemModel *model) { SortData data; - if (model->frozen) + if (model->directory_loading || model->editing_hold) { - model->sort_on_thaw = TRUE; + model->resort_pending = TRUE; return; } if (sort_data_init (&data, model)) { - GtkTreePath *path; - guint i; - guint r, n_visible_rows; - - node_validate_rows (model, G_MAXUINT, G_MAXUINT); - n_visible_rows = node_get_tree_row (model, model->files->len - 1) + 1; - model->n_nodes_valid = 0; - g_hash_table_remove_all (model->file_lookup); - _gtk_sort_array (get_node (model, 1), /* start at index 1; don't sort the editable row */ - model->files->len - 1, - model->node_size, - compare_array_element, + _gtk_sort_array (model->order->data, + model->order->len, + sizeof (guint), + compare_order_element, &data); - g_assert (model->n_nodes_valid == 0); - g_assert (g_hash_table_size (model->file_lookup) == 0); - if (n_visible_rows) - { - int *new_order = g_new (int, n_visible_rows); - - r = 0; - for (i = 0; i < model->files->len; i++) - { - FileModelNode *node = get_node (model, i); - if (!node->visible) - { - node->row = r; - continue; - } - - new_order[r] = node->row - 1; - r++; - node->row = r; - } - g_assert (r == n_visible_rows); - path = gtk_tree_path_new (); - gtk_tree_model_rows_reordered (GTK_TREE_MODEL (model), - path, - NULL, - new_order); - gtk_tree_path_free (path); - g_free (new_order); - } + order_update_positions_from (model, 0); + visible_order_rebuild_after_sort (model); } - model->sort_on_thaw = FALSE; + model->resort_pending = FALSE; } -static void -gtk_file_system_model_sort_node (GtkFileSystemModel *model, guint node) -{ - /* FIXME: improve */ - gtk_file_system_model_sort (model); -} static gboolean gtk_file_system_model_get_sort_column_id (GtkTreeSortable *sortable, @@ -1023,6 +1155,280 @@ enum { static guint file_system_model_signals[LAST_SIGNAL] = { 0 }; +static void +pending_monitor_change_free (gpointer data) +{ + PendingMonitorChange *change = data; + + g_object_unref (change->file); + g_free (change); +} + +static void +gtk_file_system_model_prepare_merge (GtkFileSystemModel *model) +{ + guint n = model->order->len; + + model->sort_mid = MIN (model->sort_left + model->sort_width, n); + model->sort_right = MIN (model->sort_left + 2 * model->sort_width, n); + model->sort_i = model->sort_left; + model->sort_j = model->sort_mid; + model->sort_k = model->sort_left; +} + +static void +gtk_file_system_model_prepare_visible (GtkFileSystemModel *model) +{ + FileModelNode *editable = get_node (model, 0); + + g_array_set_size (model->visible_order, 0); + if (editable->visible) + { + guint id = 0; + + editable->visible_pos = 0; + g_array_append_val (model->visible_order, id); + } + + model->sort_left = 0; + model->finalize_phase = MODEL_FINALIZE_VISIBLE; + model->refilter_pending = FALSE; +} + +static void +gtk_file_system_model_restart_initial_sort (GtkFileSystemModel *model) +{ + SortData data; + guint n = model->order->len; + + model->resort_pending = FALSE; + + if (sort_data_init (&data, model)) + { + if (model->sort_scratch == NULL) + model->sort_scratch = + g_array_sized_new (FALSE, FALSE, sizeof (guint), n); + g_array_set_size (model->sort_scratch, n); + + model->sort_width = 1; + model->sort_left = 0; + gtk_file_system_model_prepare_merge (model); + model->finalize_phase = MODEL_FINALIZE_MERGE; + } + else + gtk_file_system_model_prepare_visible (model); +} + +static void +gtk_file_system_model_schedule_work (GtkFileSystemModel *model) +{ + GSourceFunc func; + + if (model->drain_source != 0 || model->editing_hold) + return; + + if (model->pending_files != NULL) + func = gtk_file_system_model_drain; + else if (model->finalize_phase != MODEL_FINALIZE_NONE) + func = gtk_file_system_model_finalize_load; + else + return; + + model->drain_source = + gdk_threads_add_idle_full (G_PRIORITY_DEFAULT_IDLE, + func, + g_object_ref (model), + g_object_unref); +} + +static void +gtk_file_system_model_begin_finalize_load (GtkFileSystemModel *model, + GError *error) +{ + g_assert (model->directory_loading); + g_assert (model->finalize_phase == MODEL_FINALIZE_NONE); + g_assert (model->pending_files == NULL); + + g_clear_error (&model->load_error); + model->load_error = error ? g_error_copy (error) : NULL; + + gtk_file_system_model_restart_initial_sort (model); + gtk_file_system_model_schedule_work (model); +} + +static gboolean +gtk_file_system_model_finalize_load (gpointer data) +{ + GtkFileSystemModel *model = data; + gint64 deadline = g_get_monotonic_time () + MODEL_DRAIN_SLICE_US; + guint processed = 0; + + if (model->editing_hold && model->directory_loading) + { + model->drain_source = 0; + return FALSE; + } + + if (model->resort_pending) + gtk_file_system_model_restart_initial_sort (model); + + while (TRUE) + { + guint n = model->order->len; + + if (model->finalize_phase == MODEL_FINALIZE_MERGE) + { + SortData data; + + if (!sort_data_init (&data, model)) + { + gtk_file_system_model_prepare_visible (model); + continue; + } + + while (model->sort_k < model->sort_right) + { + guint id; + + if (model->sort_i < model->sort_mid && + (model->sort_j >= model->sort_right || + compare_order_element + (&g_array_index (model->order, guint, model->sort_i), + &g_array_index (model->order, guint, model->sort_j), + &data) <= 0)) + id = g_array_index (model->order, guint, model->sort_i++); + else + id = g_array_index (model->order, guint, model->sort_j++); + + g_array_index (model->sort_scratch, guint, model->sort_k++) = id; + + if ((++processed % MODEL_FINALIZE_CLOCK_GRANULARITY) == 0 && + g_get_monotonic_time () >= deadline) + return TRUE; + } + + model->sort_left = model->sort_right; + if (model->sort_left < n) + { + gtk_file_system_model_prepare_merge (model); + continue; + } + + /* Make the completed pass canonical and reuse the old buffer. */ + { + GArray *tmp = model->order; + + model->order = model->sort_scratch; + model->sort_scratch = tmp; + } + + if (model->resort_pending) + { + gtk_file_system_model_restart_initial_sort (model); + continue; + } + + if (model->sort_width >= n / 2 + n % 2) + { + gtk_file_system_model_prepare_visible (model); + continue; + } + + model->sort_width *= 2; + model->sort_left = 0; + gtk_file_system_model_prepare_merge (model); + continue; + } + + if (model->finalize_phase == MODEL_FINALIZE_VISIBLE) + { + if (model->refilter_pending) + gtk_file_system_model_prepare_visible (model); + + while (model->sort_left < n) + { + guint pos = model->sort_left++; + guint id = g_array_index (model->order, guint, pos); + FileModelNode *node = get_node (model, id); + gboolean filtered_out; + gboolean visible; + + node->order_pos = pos; + filtered_out = node_should_be_filtered_out (model, id); + visible = node_should_be_visible (model, id, filtered_out); + + node->filtered_out = filtered_out; + node->visible = visible; + + if (visible) + { + node->visible_pos = model->visible_order->len; + g_array_append_val (model->visible_order, id); + } + + if ((++processed % MODEL_FINALIZE_CLOCK_GRANULARITY) == 0 && + g_get_monotonic_time () >= deadline) + return TRUE; + } + + if (model->resort_pending) + { + gtk_file_system_model_restart_initial_sort (model); + continue; + } + + if (model->refilter_pending) + { + gtk_file_system_model_prepare_visible (model); + continue; + } + + model->directory_loading = FALSE; + model->refilter_pending = FALSE; + model->resort_pending = FALSE; + g_clear_pointer (&model->sort_scratch, g_array_unref); + + model->finalize_phase = MODEL_FINALIZE_REPLAY; + if (g_get_monotonic_time () >= deadline) + return TRUE; + continue; + } + + if (model->finalize_phase == MODEL_FINALIZE_REPLAY) + { + while (model->pending_monitor_changes != NULL && + !g_queue_is_empty (model->pending_monitor_changes)) + { + PendingMonitorChange *change; + + change = g_queue_pop_head (model->pending_monitor_changes); + + gtk_file_system_model_apply_monitor_change (model, + change->file, + change->type); + pending_monitor_change_free (change); + + if ((++processed % MODEL_DRAIN_CLOCK_GRANULARITY) == 0 && + g_get_monotonic_time () >= deadline) + return TRUE; + } + + model->finalize_phase = MODEL_FINALIZE_NONE; + model->drain_source = 0; + + g_signal_emit (model, + file_system_model_signals[FINISHED_LOADING], + 0, model->load_error); + g_clear_error (&model->load_error); + + return FALSE; + } + + g_assert_not_reached (); + } +} + + G_DEFINE_TYPE_WITH_CODE (GtkFileSystemModel, _gtk_file_system_model, G_TYPE_OBJECT, @@ -1038,16 +1444,23 @@ gtk_file_system_model_dispose (GObject *object) { GtkFileSystemModel *model = GTK_FILE_SYSTEM_MODEL (object); - if (model->dir_thaw_source) + if (model->drain_source != 0) { - g_source_remove (model->dir_thaw_source); - model->dir_thaw_source = 0; + g_source_remove (model->drain_source); + model->drain_source = 0; } g_cancellable_cancel (model->cancellable); + g_clear_pointer (&model->pending_monitor_queries, g_hash_table_destroy); + if (model->dir_monitor) g_file_monitor_cancel (model->dir_monitor); + g_list_free_full (model->pending_files, g_object_unref); + model->pending_files = NULL; + + g_clear_object (&model->dir_enumerator); + G_OBJECT_CLASS (_gtk_file_system_model_parent_class)->dispose (object); } @@ -1058,35 +1471,41 @@ gtk_file_system_model_finalize (GObject *object) GtkFileSystemModel *model = GTK_FILE_SYSTEM_MODEL (object); guint i; - for (i = 0; i < model->files->len; i++) + if (model->value_pages) { - int v; - - FileModelNode *node = get_node (model, i); - if (node->file) - g_object_unref (node->file); - if (node->info) - g_object_unref (node->info); - - for (v = 0; v < model->n_columns; v++) - if (G_VALUE_TYPE (&node->values[v]) != G_TYPE_INVALID) - g_value_unset (&node->values[v]); + for (i = 0; i < model->n_columns; i++) + if (model->value_pages[i]) + g_ptr_array_free (model->value_pages[i], TRUE); + g_free (model->value_pages); } - g_array_free (model->files, TRUE); + + g_clear_pointer (&model->files, g_array_unref); + + g_clear_pointer (&model->sort_scratch, g_array_unref); + g_clear_error (&model->load_error); + if (model->pending_monitor_changes) + g_queue_free_full (model->pending_monitor_changes, + pending_monitor_change_free); + + g_clear_pointer (&model->order, g_array_unref); + g_clear_pointer (&model->visible_order, g_array_unref); g_object_unref (model->cancellable); g_free (model->attributes); + if (model->dir) g_object_unref (model->dir); if (model->dir_monitor) g_object_unref (model->dir_monitor); + g_hash_table_destroy (model->file_lookup); + if (model->filter) g_object_unref (model->filter); g_slice_free1 (sizeof (GType) * model->n_columns, model->column_types); - _gtk_tree_data_list_header_free (model->sort_list); + if (model->default_sort_destroy) model->default_sort_destroy (model->default_sort_data); @@ -1133,95 +1552,113 @@ gtk_file_system_model_closed_enumerator (GObject *object, GAsyncResult *res, gpo g_file_enumerator_close_finish (G_FILE_ENUMERATOR (object), res, NULL); } + +static void +gtk_file_system_model_request_next_files (GtkFileSystemModel *model) +{ + if (model->dir_enumerator == NULL || + g_cancellable_is_cancelled (model->cancellable)) + return; + + g_file_enumerator_next_files_async (model->dir_enumerator, + FILES_PER_QUERY, + IO_PRIORITY, + model->cancellable, + gtk_file_system_model_got_files, + model); +} + static gboolean -thaw_func (gpointer data) +gtk_file_system_model_drain (gpointer data) { GtkFileSystemModel *model = data; + gint64 deadline; + guint processed = 0; - thaw_updates (model); - model->dir_thaw_source = 0; + if (model->editing_hold) + { + model->drain_source = 0; + return FALSE; + } + deadline = g_get_monotonic_time () + MODEL_DRAIN_SLICE_US; + + while (model->pending_files != NULL) + { + GList *link = model->pending_files; + GFileInfo *info = link->data; + const char *name; + + model->pending_files = + g_list_delete_link (model->pending_files, link); + + name = g_file_info_get_name (info); + if (name != NULL) + { + GFile *file = g_file_get_child (model->dir, name); + add_file (model, file, info); + g_object_unref (file); + } + + g_object_unref (info); + processed++; + + if ((processed % MODEL_DRAIN_CLOCK_GRANULARITY) == 0 && + g_get_monotonic_time () >= deadline) + break; + } + + if (model->pending_files != NULL) + return TRUE; + + model->drain_source = 0; + gtk_file_system_model_request_next_files (model); return FALSE; } static void -gtk_file_system_model_got_files (GObject *object, GAsyncResult *res, gpointer data) +gtk_file_system_model_got_files (GObject *object, + GAsyncResult *res, + gpointer data) { GFileEnumerator *enumerator = G_FILE_ENUMERATOR (object); - GtkFileSystemModel *model = data; - GList *walk, *files; + GtkFileSystemModel *model = data; /* only a valid pointer if not cancelled */ + GList *files; GError *error = NULL; - gdk_threads_enter (); - files = g_file_enumerator_next_files_finish (enumerator, res, &error); - if (files) + if (files != NULL) { - if (model->dir_thaw_source == 0) - { - freeze_updates (model); - model->dir_thaw_source = gdk_threads_add_timeout_full (IO_PRIORITY + 1, - 50, - thaw_func, - model, - NULL); - } - - for (walk = files; walk; walk = walk->next) - { - const char *name; - GFileInfo *info; - GFile *file; - - info = walk->data; - name = g_file_info_get_name (info); - if (name == NULL) - { - /* Shouldn't happen, but the APIs allow it */ - g_object_unref (info); - continue; - } - file = g_file_get_child (model->dir, name); - add_file (model, file, info); - g_object_unref (file); - g_object_unref (info); - } - g_list_free (files); - - g_file_enumerator_next_files_async (enumerator, - g_file_is_native (model->dir) ? 50 * FILES_PER_QUERY : FILES_PER_QUERY, - IO_PRIORITY, - model->cancellable, - gtk_file_system_model_got_files, - model); - } - else - { - if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - { - g_file_enumerator_close_async (enumerator, - IO_PRIORITY, - model->cancellable, - gtk_file_system_model_closed_enumerator, - NULL); - if (model->dir_thaw_source != 0) - { - g_source_remove (model->dir_thaw_source); - model->dir_thaw_source = 0; - thaw_updates (model); - } - - g_signal_emit (model, file_system_model_signals[FINISHED_LOADING], 0, error); - } - - if (error) - g_error_free (error); + gdk_threads_enter (); + g_assert (model->pending_files == NULL); + model->pending_files = files; + gtk_file_system_model_schedule_work (model); + gdk_threads_leave (); + return; } - gdk_threads_leave (); + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + gdk_threads_enter (); + + g_file_enumerator_close_async (enumerator, + IO_PRIORITY, + model->cancellable, + gtk_file_system_model_closed_enumerator, + NULL); + + g_clear_object (&model->dir_enumerator); + + gtk_file_system_model_begin_finalize_load (model, error); + gdk_threads_leave (); + } + + if (error) + g_error_free (error); } + static void gtk_file_system_model_query_done (GObject * object, GAsyncResult *res, @@ -1250,12 +1687,45 @@ gtk_file_system_model_query_done (GObject * object, gdk_threads_leave (); } + static void -gtk_file_system_model_monitor_change (GFileMonitor * monitor, - GFile * file, - GFile * other_file, - GFileMonitorEvent type, - GtkFileSystemModel *model) +gtk_file_system_model_monitor_query_done (GObject *object, + GAsyncResult *res, + gpointer data) +{ + PendingMonitorQuery *query = data; + GtkFileSystemModel *model = g_weak_ref_get (query); + GFileInfo *info = g_file_query_info_finish (G_FILE (object), res, NULL); + + if (model) + { + gdk_threads_enter (); + + if (model->pending_monitor_queries && + g_hash_table_lookup (model->pending_monitor_queries, + G_FILE (object)) == query) + { + g_hash_table_remove (model->pending_monitor_queries, + G_FILE (object)); + if (info) + _gtk_file_system_model_update_file (model, G_FILE (object), info); + } + + gdk_threads_leave (); + g_object_unref (model); + } + + g_clear_object (&info); + + g_weak_ref_clear (query); + g_free (query); +} + + +static void +gtk_file_system_model_apply_monitor_change (GtkFileSystemModel *model, + GFile *file, + GFileMonitorEvent type) { switch (type) { @@ -1263,21 +1733,43 @@ gtk_file_system_model_monitor_change (GFileMonitor * monitor, case G_FILE_MONITOR_EVENT_CHANGED: case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED: /* We can treat all of these the same way */ - g_file_query_info_async (file, - model->attributes, - G_FILE_QUERY_INFO_NONE, - IO_PRIORITY, - model->cancellable, - gtk_file_system_model_query_done, - model); + { + PendingMonitorQuery *query; + + query = g_new (PendingMonitorQuery, 1); + g_weak_ref_init (query, G_OBJECT (model)); + + gdk_threads_enter (); + + if (model->pending_monitor_queries == NULL) + model->pending_monitor_queries = + g_hash_table_new_full (g_file_hash, (GEqualFunc) g_file_equal, + (GDestroyNotify) g_object_unref, NULL); + + g_hash_table_replace (model->pending_monitor_queries, + g_object_ref (file), query); + + gdk_threads_leave (); + + g_file_query_info_async (file, + model->attributes, + G_FILE_QUERY_INFO_NONE, + IO_PRIORITY, + model->cancellable, + gtk_file_system_model_monitor_query_done, + query); + } break; + case G_FILE_MONITOR_EVENT_DELETED: - gdk_threads_enter (); + gdk_threads_enter (); + if (model->pending_monitor_queries) + g_hash_table_remove (model->pending_monitor_queries, file); remove_file (model, file); - gdk_threads_leave (); + gdk_threads_leave (); break; + case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT: - /* FIXME: use freeze/thaw with this somehow? */ case G_FILE_MONITOR_EVENT_PRE_UNMOUNT: case G_FILE_MONITOR_EVENT_UNMOUNTED: default: @@ -1287,46 +1779,73 @@ gtk_file_system_model_monitor_change (GFileMonitor * monitor, } static void -gtk_file_system_model_got_enumerator (GObject *dir, GAsyncResult *res, gpointer data) +gtk_file_system_model_monitor_change (GFileMonitor * monitor, + GFile * file, + GFile * other_file, + GFileMonitorEvent type, + GtkFileSystemModel *model) { - GtkFileSystemModel *model = data; + if (model->directory_loading || + model->drain_source != 0) + { + PendingMonitorChange *change; + + change = g_new (PendingMonitorChange, 1); + change->file = g_object_ref (file); + change->type = type; + + if (model->pending_monitor_changes == NULL) + model->pending_monitor_changes = g_queue_new (); + + g_queue_push_tail (model->pending_monitor_changes, change); + return; + } + + gtk_file_system_model_apply_monitor_change (model, file, type); +} + +static void +gtk_file_system_model_got_enumerator (GObject *object, + GAsyncResult *res, + gpointer data) +{ + GtkFileSystemModel *model = data; /* only a valid pointer if not cancelled */ GFileEnumerator *enumerator; GError *error = NULL; - gdk_threads_enter (); - - enumerator = g_file_enumerate_children_finish (G_FILE (dir), res, &error); + enumerator = g_file_enumerate_children_finish (G_FILE (object), res, &error); if (enumerator == NULL) { if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - { - g_signal_emit (model, file_system_model_signals[FINISHED_LOADING], 0, error); + { + gdk_threads_enter (); + gtk_file_system_model_begin_finalize_load (model, error); + gdk_threads_leave (); + } + + if (error) g_error_free (error); - } - } - else - { - g_file_enumerator_next_files_async (enumerator, - g_file_is_native (model->dir) ? 50 * FILES_PER_QUERY : FILES_PER_QUERY, - IO_PRIORITY, - model->cancellable, - gtk_file_system_model_got_files, - model); - g_object_unref (enumerator); - model->dir_monitor = g_file_monitor_directory (model->dir, - G_FILE_MONITOR_NONE, - model->cancellable, - NULL); /* we don't mind if directory monitoring isn't supported, so the GError is NULL here */ - if (model->dir_monitor) - g_signal_connect (model->dir_monitor, - "changed", - G_CALLBACK (gtk_file_system_model_monitor_change), - model); + return; } + gdk_threads_enter (); + + g_assert (model->dir_enumerator == NULL); + model->dir_enumerator = enumerator; + + model->dir_monitor = g_file_monitor_directory (model->dir, + G_FILE_MONITOR_NONE, + model->cancellable, + NULL); + if (model->dir_monitor) + g_signal_connect (model->dir_monitor, "changed", + G_CALLBACK (gtk_file_system_model_monitor_change), model); + + gtk_file_system_model_request_next_files (model); gdk_threads_leave (); } + static void gtk_file_system_model_set_n_columns (GtkFileSystemModel *model, gint n_columns, @@ -1339,27 +1858,34 @@ gtk_file_system_model_set_n_columns (GtkFileSystemModel *model, model->n_columns = n_columns; model->column_types = g_slice_alloc (sizeof (GType) * n_columns); - - model->node_size = sizeof (FileModelNode) + sizeof (GValue) * (n_columns - 1); /* minus 1 because FileModelNode.values[] has a default size of 1 */ + model->value_pages = g_new0 (GPtrArray *, n_columns); for (i = 0; i < (guint) n_columns; i++) { GType type = va_arg (args, GType); - if (! _gtk_tree_data_list_check_type (type)) - { - g_error ("%s: type %s cannot be a column type for GtkFileSystemModel\n", G_STRLOC, g_type_name (type)); - return; /* not reached */ - } + if (!_gtk_tree_data_list_check_type (type)) + { + g_error ("%s: type %s cannot be a column type for GtkFileSystemModel\n", + G_STRLOC, g_type_name (type)); + return; + } model->column_types[i] = type; } - model->sort_list = _gtk_tree_data_list_header_new (n_columns, model->column_types); + model->sort_list = + _gtk_tree_data_list_header_new (n_columns, model->column_types); - model->files = g_array_sized_new (FALSE, FALSE, model->node_size, FILES_PER_QUERY); - /* add editable node at start */ + model->files = + g_array_sized_new (FALSE, TRUE, sizeof (FileModelNode), FILES_PER_QUERY); + g_array_set_clear_func (model->files, file_model_node_clear); + model->order = + g_array_sized_new (FALSE, FALSE, sizeof (guint), FILES_PER_QUERY); + model->visible_order = + g_array_sized_new (FALSE, FALSE, sizeof (guint), FILES_PER_QUERY); g_array_set_size (model->files, 1); - memset (get_node (model, 0), 0, model->node_size); + + get_node (model, 0)->generation = 1; } static void @@ -1371,6 +1897,7 @@ gtk_file_system_model_set_directory (GtkFileSystemModel *model, model->dir = g_object_ref (dir); model->attributes = g_strdup (attributes); + model->directory_loading = TRUE; g_file_enumerate_children_async (model->dir, attributes, @@ -1478,22 +2005,22 @@ gtk_file_system_model_refilter_all (GtkFileSystemModel *model) { guint i; - if (model->frozen) + if (model->directory_loading || model->editing_hold) { - model->filter_on_thaw = TRUE; + model->refilter_pending = TRUE; return; } - freeze_updates (model); + for (i = 0; i < model->order->len; i++) + { + guint id = g_array_index (model->order, guint, i); + node_compute_visibility_and_filters (model, id); + } - /* start at index 1, don't change the editable */ - for (i = 1; i < model->files->len; i++) - node_compute_visibility_and_filters (model, i); - - model->filter_on_thaw = FALSE; - thaw_updates (model); + model->refilter_pending = FALSE; } + /** * _gtk_file_system_model_set_show_hidden: * @model: a #GtkFileSystemModel @@ -1622,12 +2149,13 @@ _gtk_file_system_model_get_cancellable (GtkFileSystemModel *model) **/ gboolean _gtk_file_system_model_iter_is_visible (GtkFileSystemModel *model, - GtkTreeIter *iter) + GtkTreeIter *iter) { FileModelNode *node; g_return_val_if_fail (GTK_IS_FILE_SYSTEM_MODEL (model), FALSE); g_return_val_if_fail (iter != NULL, FALSE); + g_return_val_if_fail (ITER_IS_VALID (model, iter), FALSE); node = get_node (model, ITER_INDEX (iter)); return node->visible; @@ -1648,12 +2176,13 @@ _gtk_file_system_model_iter_is_visible (GtkFileSystemModel *model, **/ gboolean _gtk_file_system_model_iter_is_filtered_out (GtkFileSystemModel *model, - GtkTreeIter *iter) + GtkTreeIter *iter) { FileModelNode *node; g_return_val_if_fail (GTK_IS_FILE_SYSTEM_MODEL (model), FALSE); g_return_val_if_fail (iter != NULL, FALSE); + g_return_val_if_fail (ITER_IS_VALID (model, iter), FALSE); node = get_node (model, ITER_INDEX (iter)); return node->filtered_out; @@ -1678,12 +2207,13 @@ _gtk_file_system_model_iter_is_filtered_out (GtkFileSystemModel *model, **/ GFileInfo * _gtk_file_system_model_get_info (GtkFileSystemModel *model, - GtkTreeIter *iter) + GtkTreeIter *iter) { FileModelNode *node; g_return_val_if_fail (GTK_IS_FILE_SYSTEM_MODEL (model), NULL); g_return_val_if_fail (iter != NULL, NULL); + g_return_val_if_fail (ITER_IS_VALID (model, iter), NULL); node = get_node (model, ITER_INDEX (iter)); g_assert (node->info == NULL || G_IS_FILE_INFO (node->info)); @@ -1704,11 +2234,13 @@ _gtk_file_system_model_get_info (GtkFileSystemModel *model, **/ GFile * _gtk_file_system_model_get_file (GtkFileSystemModel *model, - GtkTreeIter *iter) + GtkTreeIter *iter) { FileModelNode *node; g_return_val_if_fail (GTK_IS_FILE_SYSTEM_MODEL (model), NULL); + g_return_val_if_fail (iter != NULL, NULL); + g_return_val_if_fail (ITER_IS_VALID (model, iter), NULL); node = get_node (model, ITER_INDEX (iter)); return node->file; @@ -1733,63 +2265,43 @@ _gtk_file_system_model_get_file (GtkFileSystemModel *model, **/ const GValue * _gtk_file_system_model_get_value (GtkFileSystemModel *model, - GtkTreeIter * iter, + GtkTreeIter *iter, int column) { FileModelNode *node; + GValue *value; + guint id; g_return_val_if_fail (GTK_IS_FILE_SYSTEM_MODEL (model), NULL); - g_return_val_if_fail (column >= 0 && (guint) column < model->n_columns, NULL); + g_return_val_if_fail (iter != NULL, NULL); + g_return_val_if_fail (ITER_IS_VALID (model, iter), NULL); + g_return_val_if_fail (column >= 0 && + (guint) column < model->n_columns, NULL); - node = get_node (model, ITER_INDEX (iter)); + id = ITER_INDEX (iter); + node = get_node (model, id); + value = value_cache_slot (model, id, column, TRUE); - if (!G_VALUE_TYPE (&node->values[column])) + if (G_VALUE_TYPE (value) == G_TYPE_INVALID) { - g_value_init (&node->values[column], model->column_types[column]); - if (!model->get_func (model, - node->file, - node->info, - column, - &node->values[column], - model->get_data)) + g_value_init (value, model->column_types[column]); + + if (!model->get_func (model, node->file, node->info, column, + value, model->get_data)) { - g_value_unset (&node->values[column]); + g_value_unset (value); return NULL; } } - return &node->values[column]; + return value; } static guint node_get_for_file (GtkFileSystemModel *model, - GFile * file) + GFile *file) { - guint i; - - i = GPOINTER_TO_UINT (g_hash_table_lookup (model->file_lookup, file)); - if (i != 0) - return i; - - /* Node 0 is the editable row and has no associated file or entry in the table, so we start counting from 1. - * - * The invariant here is that the files in model->files[n] for n < g_hash_table_size (model->file_lookup) - * are already added to the hash table. The table can get cleared when we re-sort; this loop merely rebuilds - * our (file -> index) mapping on demand. - * - * If we exit the loop, the next pending batch of mappings will be resolved when this function gets called again - * with another file that is not yet in the mapping. - */ - for (i = g_hash_table_size (model->file_lookup) + 1; i < model->files->len; i++) - { - FileModelNode *node = get_node (model, i); - - g_hash_table_insert (model->file_lookup, node->file, GUINT_TO_POINTER (i)); - if (g_file_equal (node->file, file)) - return i; - } - - return 0; + return GPOINTER_TO_UINT (g_hash_table_lookup (model->file_lookup, file)); } /** @@ -1832,26 +2344,6 @@ _gtk_file_system_model_get_iter_for_file (GtkFileSystemModel *model, * if the index is equal or after the specified id. We use this to slide the * mappings up or down when a node is added or removed, respectively. */ -static void -adjust_file_lookup (GtkFileSystemModel *model, guint id, int increment) -{ - GHashTableIter iter; - gpointer key; - gpointer value; - - g_hash_table_iter_init (&iter, model->file_lookup); - - while (g_hash_table_iter_next (&iter, &key, &value)) - { - guint index = GPOINTER_TO_UINT (value); - - if (index >= id) - { - index += increment; - g_hash_table_iter_replace (&iter, GUINT_TO_POINTER (index)); - } - } -} /** * add_file: @@ -1864,30 +2356,71 @@ adjust_file_lookup (GtkFileSystemModel *model, guint id, int increment) **/ static void add_file (GtkFileSystemModel *model, - GFile *file, - GFileInfo *info) + GFile *file, + GFileInfo *info) { FileModelNode *node; + guint id; g_return_if_fail (GTK_IS_FILE_SYSTEM_MODEL (model)); g_return_if_fail (G_IS_FILE (file)); g_return_if_fail (G_IS_FILE_INFO (info)); - node = g_slice_alloc0 (model->node_size); + if (node_get_for_file (model, file) != 0) + return; + + id = node_allocate_slot (model); + node = get_node (model, id); node->file = g_object_ref (file); - if (info) - node->info = g_object_ref (info); - node->frozen_add = model->frozen ? TRUE : FALSE; + node->info = g_object_ref (info); - g_array_append_vals (model->files, node, 1); - g_slice_free1 (model->node_size, node); + g_hash_table_insert (model->file_lookup, + node->file, GUINT_TO_POINTER (id)); - if (!model->frozen) - node_compute_visibility_and_filters (model, model->files->len -1); + g_array_append_val (model->order, id); + node->order_pos = model->order->len - 1; - gtk_file_system_model_sort_node (model, model->files->len -1); + if (model->directory_loading) + return; + + if (model->editing_hold) + { + node->held_add = TRUE; + model->resort_pending = TRUE; + return; + } + + { + SortData data; + guint lo = 0; + guint hi = node->order_pos; + + if (sort_data_init (&data, model)) + { + while (lo < hi) + { + guint mid = lo + (hi - lo) / 2; + guint mid_id = g_array_index (model->order, guint, mid); + int cmp = compare_order_element (&id, &mid_id, &data); + + if (cmp < 0) + hi = mid; + else + lo = mid + 1; + } + + if (lo != node->order_pos) + { + g_array_remove_index (model->order, node->order_pos); + g_array_insert_val (model->order, lo, id); + order_update_positions_from (model, lo); + } + } + } + node_compute_visibility_and_filters (model, id); } + /** * remove_file: * @model: the model @@ -1899,12 +2432,12 @@ add_file (GtkFileSystemModel *model, **/ static void remove_file (GtkFileSystemModel *model, - GFile *file) + GFile *file) { FileModelNode *node; gboolean was_visible; + guint row = G_MAXUINT; guint id; - guint row; g_return_if_fail (GTK_IS_FILE_SYSTEM_MODEL (model)); g_return_if_fail (G_IS_FILE (file)); @@ -1915,20 +2448,22 @@ remove_file (GtkFileSystemModel *model, node = get_node (model, id); was_visible = node->visible; - row = node_get_tree_row (model, id); - node_invalidate_index (model, id); + if (was_visible) + { + row = node->visible_pos; + visible_remove_node (model, id); + } + g_assert (id != 0); + { + guint pos = node->order_pos; + + g_array_remove_index (model->order, pos); + order_update_positions_from (model, pos); + } g_hash_table_remove (model->file_lookup, file); - g_object_unref (node->file); - adjust_file_lookup (model, id, -1); - - if (node->info) - g_object_unref (node->info); - - g_array_remove_index (model->files, id); - - /* We don't need to resort, as removing a row doesn't change the sorting order of the other rows */ + node_retire_slot (model, id); if (was_visible) emit_row_deleted_for_row (model, row); @@ -1950,7 +2485,7 @@ _gtk_file_system_model_update_file (GtkFileSystemModel *model, GFileInfo *info) { FileModelNode *node; - guint i, id; + guint id; GFileInfo *old_info; g_return_if_fail (GTK_IS_FILE_SYSTEM_MODEL (model)); @@ -1961,7 +2496,7 @@ _gtk_file_system_model_update_file (GtkFileSystemModel *model, if (id == 0) { add_file (model, file, info); - id = node_get_for_file (model, file); + return; } node = get_node (model, id); @@ -1972,11 +2507,7 @@ _gtk_file_system_model_update_file (GtkFileSystemModel *model, if (old_info) g_object_unref (old_info); - for (i = 0; i < model->n_columns; i++) - { - if (G_VALUE_TYPE (&node->values[i])) - g_value_unset (&node->values[i]); - } + value_cache_clear_node (model, id); if (node->visible) emit_row_changed_for_node (model, id); @@ -2012,62 +2543,6 @@ _gtk_file_system_model_set_filter (GtkFileSystemModel *model, gtk_file_system_model_refilter_all (model); } -/** - * freeze_updates: - * @model: a #GtkFileSystemModel - * - * Freezes most updates on the model, so that performing multiple operations on - * the files in the model do not cause any events. Use thaw_updates() to resume - * proper operations. It is fine to call this function multiple times as long as - * freeze and thaw calls are balanced. - **/ -static void -freeze_updates (GtkFileSystemModel *model) -{ - g_return_if_fail (GTK_IS_FILE_SYSTEM_MODEL (model)); - - model->frozen++; -} - -/** - * thaw_updates: - * @model: a #GtkFileSystemModel - * - * Undoes the effect of a previous call to freeze_updates() - **/ -static void -thaw_updates (GtkFileSystemModel *model) -{ - gboolean stuff_added; - - g_return_if_fail (GTK_IS_FILE_SYSTEM_MODEL (model)); - g_return_if_fail (model->frozen > 0); - - model->frozen--; - if (model->frozen > 0) - return; - - stuff_added = get_node (model, model->files->len - 1)->frozen_add; - - if (model->filter_on_thaw) - gtk_file_system_model_refilter_all (model); - if (model->sort_on_thaw) - gtk_file_system_model_sort (model); - if (stuff_added) - { - guint i; - - for (i = 0; i < model->files->len; i++) - { - FileModelNode *node = get_node (model, i); - - if (!node->frozen_add) - continue; - node->frozen_add = FALSE; - node_compute_visibility_and_filters (model, i); - } - } -} /** * _gtk_file_system_model_clear_cache: @@ -2082,16 +2557,17 @@ thaw_updates (GtkFileSystemModel *model) **/ void _gtk_file_system_model_clear_cache (GtkFileSystemModel *model, - int column) + int column) { - guint i; + guint8 *changed; int start, end; - gboolean changed; + int c; + guint i; g_return_if_fail (GTK_IS_FILE_SYSTEM_MODEL (model)); g_return_if_fail (column >= -1 && (guint) column < model->n_columns); - if (column > -1) + if (column >= 0) { start = column; end = column + 1; @@ -2102,42 +2578,75 @@ _gtk_file_system_model_clear_cache (GtkFileSystemModel *model, end = model->n_columns; } - for (i = 0; i < model->files->len; i++) - { - FileModelNode *node = get_node (model, i); + changed = g_new0 (guint8, model->files->len); - changed = FALSE; - for (column = start; column < end; column++) + for (c = start; c < end; c++) + { + GPtrArray *pages = model->value_pages[c]; + guint page_index; + + if (pages == NULL) + continue; + + for (page_index = 0; page_index < pages->len; page_index++) { - if (!G_VALUE_TYPE (&node->values[column])) + ValuePage *page = pages->pdata[page_index]; + guint slot; + guint base; + + if (page == NULL) continue; - g_value_unset (&node->values[column]); - changed = TRUE; - } + base = page_index << VALUE_PAGE_SHIFT; + for (slot = 0; slot < VALUE_PAGE_SIZE; slot++) + { + guint id = base + slot; + GValue *value; - if (changed && node->visible) - emit_row_changed_for_node (model, i); + if (id >= model->files->len) + break; + + value = &page->values[slot]; + if (G_VALUE_TYPE (value) == G_TYPE_INVALID) + continue; + + g_value_unset (value); + changed[id] = TRUE; + } + } } + /* Emit at most one change signal for each visible row, in row order. */ + for (i = 0; i < model->visible_order->len; i++) + { + guint id = g_array_index (model->visible_order, guint, i); + if (changed[id]) + emit_row_changed_for_node (model, id); + } + + g_free (changed); + /* FIXME: resort? */ } + void _gtk_file_system_model_clear_cache_for_iter (GtkFileSystemModel *model, - GtkTreeIter *iter, - int column) + GtkTreeIter *iter, + int column) { FileModelNode *node; - int start, end; gboolean changed = FALSE; + guint id; + int start, end; + int c; g_return_if_fail (GTK_IS_FILE_SYSTEM_MODEL (model)); g_return_if_fail (iter != NULL); g_return_if_fail (ITER_IS_VALID (model, iter)); g_return_if_fail (column >= -1 && (guint) column < model->n_columns); - if (column > -1) + if (column >= 0) { start = column; end = column + 1; @@ -2148,19 +2657,14 @@ _gtk_file_system_model_clear_cache_for_iter (GtkFileSystemModel *model, end = model->n_columns; } - node = get_node (model, ITER_INDEX (iter)); + id = ITER_INDEX (iter); + node = get_node (model, id); - for (column = start; column < end; column++) - { - if (!G_VALUE_TYPE (&node->values[column])) - continue; - - g_value_unset (&node->values[column]); - changed = TRUE; - } + for (c = start; c < end; c++) + changed |= value_cache_clear_slot (model, id, c); if (changed && node->visible) - emit_row_changed_for_node (model, ITER_INDEX (iter)); + emit_row_changed_for_node (model, id); } /** @@ -2202,20 +2706,26 @@ _gtk_file_system_model_add_and_query_file (GtkFileSystemModel *model, * using this temporary row, call _gtk_file_system_model_remove_editable(). **/ void -_gtk_file_system_model_add_editable (GtkFileSystemModel *model, GtkTreeIter *iter) +_gtk_file_system_model_add_editable (GtkFileSystemModel *model, + GtkTreeIter *iter) { g_return_if_fail (GTK_IS_FILE_SYSTEM_MODEL (model)); + g_return_if_fail (iter != NULL); g_return_if_fail (!get_node (model, 0)->visible); node_set_visible_and_filtered_out (model, 0, TRUE, FALSE); ITER_INIT_FROM_INDEX (model, iter, 0); - /* we don't want file system changes to affect the model while - * editing is in place - */ - freeze_updates (model); + model->editing_hold = TRUE; + + if (model->directory_loading && model->drain_source != 0) + { + g_source_remove (model->drain_source); + model->drain_source = 0; + } } + /** * _gtk_file_system_model_remove_editable: * @model: a #GtkFileSystemModel @@ -2227,10 +2737,32 @@ _gtk_file_system_model_add_editable (GtkFileSystemModel *model, GtkTreeIter *ite void _gtk_file_system_model_remove_editable (GtkFileSystemModel *model) { + guint i; + g_return_if_fail (GTK_IS_FILE_SYSTEM_MODEL (model)); - g_return_if_fail (get_node (model, 0)->visible); + g_return_if_fail (model->editing_hold); - thaw_updates (model); + model->editing_hold = FALSE; - node_set_visible_and_filtered_out (model, 0, FALSE, FALSE); + if (model->resort_pending) + gtk_file_system_model_sort (model); + if (model->refilter_pending) + gtk_file_system_model_refilter_all (model); + + for (i = 0; i < model->order->len; i++) + { + guint id = g_array_index (model->order, guint, i); + FileModelNode *node = get_node (model, id); + + if (!node->held_add) + continue; + + node->held_add = FALSE; + node_compute_visibility_and_filters (model, id); + } + + if (get_node (model, 0)->visible) + node_set_visible_and_filtered_out (model, 0, FALSE, FALSE); + + gtk_file_system_model_schedule_work (model); } From 6501755d7b40cedf6e6c16d242c6f14f632ffe75 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Tue, 1 Sep 2026 11:38:46 +0200 Subject: [PATCH 120/122] filechooser: avoid repeated icon view visible-range scans Cache the GtkIconView viewport and refresh it cooperatively on layout, allocation and adjustment changes instead of repeatedly scanning visible ranges for individual cells. Reuse the cached range for nearby thumbnail prioritization and icon cache invalidation, while preserving the existing list-view path and thumbnail scheduling semantics. --- gtk/gtkfilechooserdefault.c | 259 ++++++++++++++++++++++++------------ gtk/gtkfilechooserprivate.h | 6 + 2 files changed, 178 insertions(+), 87 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 10e303dcf..f481d8cf1 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -444,6 +444,12 @@ static void icon_view_scale_queue_cache_clear (GtkFileChooserDefault *impl); static gboolean get_visible_range (GtkTreePath **start, GtkTreePath **end, GtkFileChooserDefault *impl); +static void icon_view_visible_range_schedule (GtkFileChooserDefault *impl); +static void icon_view_visible_range_size_allocate_cb (GtkWidget *widget, + GtkAllocation *allocation, + GtkFileChooserDefault *impl); +static void icon_view_visible_range_adjustment_changed_cb (GtkAdjustment *adjustment, + GtkFileChooserDefault *impl); static void location_button_toggled_cb (GtkToggleButton *toggle, GtkFileChooserDefault *impl); @@ -1042,8 +1048,6 @@ typedef struct gboolean failed; } ThumbnailJob; -static gboolean thumbnail_cache_path_is_sufficient (const gchar *path, - gint icon_size); static gboolean thumbnail_debug_enabled (void); static void thumbnail_debug_log_file (const gchar *where, @@ -1205,13 +1209,6 @@ thumbnail_cache_max_dimension_for_path (const gchar *path) } -static gboolean -thumbnail_cache_path_is_sufficient (const gchar *path, - gint icon_size) -{ - return thumbnail_cache_max_dimension_for_path (path) >= icon_size; -} - static gboolean thumbnail_debug_enabled (void) { @@ -1836,6 +1833,12 @@ gtk_file_chooser_default_finalize (GObject *object) icon_view_scale_remove_timeout (impl); + if (impl->icon_view_visible_range_idle_id) + { + g_source_remove (impl->icon_view_visible_range_idle_id); + impl->icon_view_visible_range_idle_id = 0; + } + thumbnail_queue_clear (impl); thumbnail_standard_icon_cache_clear (impl); @@ -6939,7 +6942,6 @@ show_preview_panel_toggled_cb (GtkCheckMenuItem *item, } - /* Callback used when the "Show Thumbnails" menu item is toggled */ static void show_thumbnails_toggled_cb (GtkCheckMenuItem *item, @@ -8338,11 +8340,6 @@ file_list_query_tooltip_cb (GtkWidget *widget, } - - - - - static void file_icon_view_tooltip_cache_clear (GtkFileChooserDefault *impl) { @@ -8864,6 +8861,8 @@ create_browse_files_icon_view (GtkFileChooserDefault *impl) gtk_widget_add_events (impl->browse_files_icon_view, GDK_SCROLL_MASK); g_signal_connect (impl->browse_files_icon_view, "scroll-event", G_CALLBACK (browse_files_icon_view_scroll_event_cb), impl); + g_signal_connect (impl->browse_files_icon_view, "size-allocate", + G_CALLBACK (icon_view_visible_range_size_allocate_cb), impl); g_object_set (impl->browse_files_icon_view, "has-tooltip", TRUE, NULL); g_signal_connect (impl->browse_files_icon_view, "query-tooltip", @@ -8946,6 +8945,12 @@ create_file_list (GtkFileChooserDefault *impl) /* Scrolled window */ swin = gtk_scrolled_window_new (NULL, NULL); impl->browse_files_scrolled_window = swin; + g_signal_connect (gtk_scrolled_window_get_hadjustment (GTK_SCROLLED_WINDOW (swin)), + "value-changed", + G_CALLBACK (icon_view_visible_range_adjustment_changed_cb), impl); + g_signal_connect (gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (swin)), + "value-changed", + G_CALLBACK (icon_view_visible_range_adjustment_changed_cb), impl); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (swin), GTK_POLICY_AUTOMATIC, GTK_POLICY_ALWAYS); gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (swin), @@ -9675,85 +9680,154 @@ icon_view_scale_queue_visible_after_layout_clear (GtkFileChooserDefault *impl) (GDestroyNotify) g_object_unref); } +static gboolean +file_list_get_visible_indices (GtkFileChooserDefault *impl, + gint *first, + gint *last) +{ + GtkTreePath *start, *end; + gint *start_indices, *end_indices; + gboolean valid; + + if (!impl->current_model || !GTK_IS_FILE_SYSTEM_MODEL (impl->current_model) || + !get_visible_range (&start, &end, impl)) + return FALSE; + + start_indices = gtk_tree_path_get_indices (start); + end_indices = gtk_tree_path_get_indices (end); + valid = start_indices && end_indices && end_indices[0] >= start_indices[0]; + + if (valid) + { + *first = start_indices[0]; + *last = end_indices[0]; + } + + gtk_tree_path_free (start); + gtk_tree_path_free (end); + return valid; +} + +#define FILE_LIST_VISIBLE_OVERSCAN(first, last) \ + CLAMP (MAX (1, (last) - (first) + 1) * 2, 16, 64) + +static void +file_list_clear_cache_range (GtkFileChooserDefault *impl, + gint first, + gint last, + gint column) +{ + GtkTreeModel *model = impl->current_model; + GtkTreePath *path; + GtkTreeIter iter; + gint i; + + first = MAX (0, first); + path = gtk_tree_path_new_from_indices (first, -1); + + for (i = first; i <= last; i++) + { + if (!gtk_tree_model_get_iter (model, &iter, path)) + break; + + _gtk_file_system_model_clear_cache_for_iter (GTK_FILE_SYSTEM_MODEL (model), + &iter, column); + gtk_tree_path_next (path); + } + + gtk_tree_path_free (path); +} + static void icon_view_scale_clear_visible_model_cache (GtkFileChooserDefault *impl, gboolean include_overscan) { - GtkTreeModel *tree_model; - GtkTreePath *start, *end, *path; - GtkTreeIter iter; - gint *start_indices; - gint *end_indices; - gint first; - gint last; - gint i; + gint first, last; - if (!impl->current_model || !GTK_IS_FILE_SYSTEM_MODEL (impl->current_model)) - return; - - /* - * A full _gtk_file_system_model_clear_cache() is O(total files). During - * icon-view zooming we refresh only the visible range. - * - * When shrinking icons, the new layout can make rows visible that were just - * outside the old visible range. Only in that case do we add a small margin. - */ - if (!get_visible_range (&start, &end, impl)) + if (!file_list_get_visible_indices (impl, &first, &last)) { - _gtk_file_system_model_clear_cache (GTK_FILE_SYSTEM_MODEL (impl->current_model), - MODEL_COL_ICON_PIXBUF); - return; - } - - start_indices = gtk_tree_path_get_indices (start); - end_indices = gtk_tree_path_get_indices (end); - - if (!start_indices || !end_indices) - { - gtk_tree_path_free (start); - gtk_tree_path_free (end); - _gtk_file_system_model_clear_cache (GTK_FILE_SYSTEM_MODEL (impl->current_model), - MODEL_COL_ICON_PIXBUF); + if (impl->current_model && GTK_IS_FILE_SYSTEM_MODEL (impl->current_model)) + _gtk_file_system_model_clear_cache (GTK_FILE_SYSTEM_MODEL (impl->current_model), + MODEL_COL_ICON_PIXBUF); return; } if (include_overscan) { - gint overscan; + gint overscan = + MIN (impl->icon_view_scale_visible_overscan > 0 + ? impl->icon_view_scale_visible_overscan : 16, + ICON_VIEW_SCALE_MAX_VISIBLE_OVERSCAN); - overscan = impl->icon_view_scale_visible_overscan; - if (overscan <= 0) - overscan = 16; - - overscan = MIN (overscan, ICON_VIEW_SCALE_MAX_VISIBLE_OVERSCAN); - - first = MAX (0, start_indices[0] - overscan); - last = end_indices[0] + overscan; - } - else - { - first = start_indices[0]; - last = end_indices[0]; + first -= overscan; + last += overscan; } - tree_model = impl->current_model; - path = gtk_tree_path_new_from_indices (first, -1); + file_list_clear_cache_range (impl, first, last, MODEL_COL_ICON_PIXBUF); +} - for (i = first; i <= last; i++) - { - if (!gtk_tree_model_get_iter (tree_model, &iter, path)) - break; +/* Cache the GtkIconView viewport; per-cell visible-range scans are O(n^2). */ +static gboolean +icon_view_visible_range_refresh_cb (gpointer data) +{ + GtkFileChooserDefault *impl = data; + gint first, last; + gint overscan; - _gtk_file_system_model_clear_cache_for_iter (GTK_FILE_SYSTEM_MODEL (tree_model), - &iter, - MODEL_COL_ICON_PIXBUF); + impl->icon_view_visible_range_idle_id = 0; + impl->icon_view_visible_range_valid = FALSE; - gtk_tree_path_next (path); - } + if (!VIEW_MODE_USES_ICON_VIEW (impl->view_mode) || + !impl->browse_files_icon_view || + !file_list_get_visible_indices (impl, &first, &last)) + return FALSE; - gtk_tree_path_free (path); - gtk_tree_path_free (start); - gtk_tree_path_free (end); + impl->icon_view_visible_first = first; + impl->icon_view_visible_last = last; + impl->icon_view_visible_range_valid = TRUE; + + overscan = FILE_LIST_VISIBLE_OVERSCAN (first, last); + file_list_clear_cache_range + (impl, first - overscan, last + overscan, + impl->view_mode == VIEW_MODE_COMPACT + ? MODEL_COL_LIST_PIXBUF : MODEL_COL_ICON_PIXBUF); + gtk_widget_queue_draw (impl->browse_files_icon_view); + + return FALSE; +} + +static void +icon_view_visible_range_schedule (GtkFileChooserDefault *impl) +{ + impl->icon_view_visible_range_valid = FALSE; + + if (impl->icon_view_visible_range_idle_id || + !VIEW_MODE_USES_ICON_VIEW (impl->view_mode) || + !impl->browse_files_icon_view || + !impl->current_model) + return; + + /* Run after GtkIconView's layout and coalesce repeated requests. */ + impl->icon_view_visible_range_idle_id = + gdk_threads_add_idle_full (G_PRIORITY_LOW, + icon_view_visible_range_refresh_cb, + impl, + NULL); +} + +static void +icon_view_visible_range_size_allocate_cb (GtkWidget *widget, + GtkAllocation *allocation, + GtkFileChooserDefault *impl) +{ + icon_view_visible_range_schedule (impl); +} + +static void +icon_view_visible_range_adjustment_changed_cb (GtkAdjustment *adjustment, + GtkFileChooserDefault *impl) +{ + icon_view_visible_range_schedule (impl); } static gboolean @@ -12361,13 +12435,12 @@ file_system_model_set (GtkFileSystemModel *model, if (info) { GtkTreeModel *tree_model; - GtkTreePath *path, *start, *end; + GtkTreePath *path; GtkTreeIter iter; gboolean file_near_visible; gint path_index; gint start_index; gint end_index; - gint visible_items; gint overscan_items; /* not loading icon view's icon in the list view */ @@ -12379,7 +12452,23 @@ file_system_model_set (GtkFileSystemModel *model, return FALSE; file_near_visible = FALSE; - if (get_visible_range (&start, &end, impl)) + start_index = -1; + end_index = -1; + + if (impl->view_mode == VIEW_MODE_LIST) + file_list_get_visible_indices (impl, &start_index, &end_index); + else if (impl->icon_view_visible_range_valid) + { + start_index = impl->icon_view_visible_first; + end_index = impl->icon_view_visible_last; + } + else + { + /* Wait for valid GtkIconView geometry. */ + icon_view_visible_range_schedule (impl); + } + + if (start_index >= 0 && end_index >= start_index) { if (!_gtk_file_system_model_get_iter_for_file (model, &iter, @@ -12388,18 +12477,12 @@ file_system_model_set (GtkFileSystemModel *model, path = gtk_tree_model_get_path (tree_model, &iter); path_index = gtk_tree_path_get_indices (path)[0]; - start_index = gtk_tree_path_get_indices (start)[0]; - end_index = gtk_tree_path_get_indices (end)[0]; - - visible_items = MAX (1, end_index - start_index + 1); - overscan_items = CLAMP (visible_items * 2, 16, 64); + overscan_items = FILE_LIST_VISIBLE_OVERSCAN (start_index, end_index); file_near_visible = (path_index >= start_index - overscan_items && - path_index <= end_index + overscan_items); + path_index <= end_index + overscan_items); gtk_tree_path_free (path); - gtk_tree_path_free (start); - gtk_tree_path_free (end); } /* #1 use standard icon if it is loaded */ @@ -16322,6 +16405,8 @@ current_view_set_file_model (GtkFileChooserDefault *impl, GtkTreeModel *model) * can become visible before that timeout fires. Clear the newly visible model * immediately so it cannot show pixbufs cached for an older icon size. */ + icon_view_visible_range_schedule (impl); + if (impl->view_mode == VIEW_MODE_ICON && impl->icon_view_scale_timeout_id && GTK_IS_FILE_SYSTEM_MODEL (impl->current_model)) diff --git a/gtk/gtkfilechooserprivate.h b/gtk/gtkfilechooserprivate.h index 00f94a209..01e15dc40 100644 --- a/gtk/gtkfilechooserprivate.h +++ b/gtk/gtkfilechooserprivate.h @@ -267,6 +267,12 @@ struct _GtkFileChooserDefault guint icon_view_scale_after_layout_id; gint pending_icon_view_icon_size; gint icon_view_scale_visible_overscan; + + /* Cached viewport avoids an O(n) visible-range scan per icon cell. */ + guint icon_view_visible_range_idle_id; + gint icon_view_visible_first, icon_view_visible_last; + guint icon_view_visible_range_valid : 1; + ViewMode view_mode; /* Visible-only thumbnail generation state. Thumbnail workers are From 7af69af9c8b7a2d2edcecd79fd507e0e3fdd32c3 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Tue, 1 Sep 2026 11:52:17 +0200 Subject: [PATCH 121/122] gtkiconview: slice layout work across main loop iterations Make GtkIconView layout resumable and time-slice row processing so large models do not monopolize the main loop. Preserve layout state across idle iterations, avoid redundant relayout from size allocation, and keep hit testing and visible-range queries safe while layout is still in progress. --- gtk/gtkiconview.c | 158 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 117 insertions(+), 41 deletions(-) diff --git a/gtk/gtkiconview.c b/gtk/gtkiconview.c index 5b58cc9c8..71e4fb82a 100644 --- a/gtk/gtkiconview.c +++ b/gtk/gtkiconview.c @@ -120,6 +120,12 @@ struct _GtkIconViewPrivate GtkAdjustment *vadjustment; guint layout_idle_id; + GList *layout_items; + gint layout_y, layout_maximum_width; + gint layout_row, layout_item_width, layout_next_index; + guint layout_in_progress : 1; + guint layout_scanning_width : 1; + guint layout_restart : 1; gboolean doing_rubberband; gint rubberband_x1, rubberband_y1; @@ -304,6 +310,7 @@ static void gtk_icon_view_queue_draw_path (GtkIco GtkTreePath *path); static void gtk_icon_view_queue_draw_item (GtkIconView *icon_view, GtkIconViewItem *item); +static gboolean layout_callback (gpointer user_data); static void gtk_icon_view_queue_layout (GtkIconView *icon_view); static void gtk_icon_view_set_cursor_item (GtkIconView *icon_view, GtkIconViewItem *item, @@ -1491,6 +1498,8 @@ gtk_icon_view_size_allocate (GtkWidget *widget, GtkIconView *icon_view = GTK_ICON_VIEW (widget); GtkAdjustment *hadjustment, *vadjustment; + gboolean relayout = (icon_view->priv->layout_idle_id != 0 || + widget->allocation.width != allocation->width); widget->allocation = *allocation; @@ -1504,7 +1513,8 @@ gtk_icon_view_size_allocate (GtkWidget *widget, MAX (icon_view->priv->height, allocation->height)); } - gtk_icon_view_layout (icon_view); + if (relayout) + gtk_icon_view_layout (icon_view); gtk_icon_view_allocate_children (icon_view); @@ -1588,6 +1598,10 @@ gtk_icon_view_expose (GtkWidget *widget, for (icons = icon_view->priv->items; icons; icons = icons->next) { GtkIconViewItem *item = icons->data; + + if (icon_view->priv->layout_in_progress && + item->index >= icon_view->priv->layout_next_index) + break; GdkRectangle area; area.x = item->x; @@ -2810,11 +2824,8 @@ gtk_icon_view_set_adjustment_upper (GtkAdjustment *adj, static void gtk_icon_view_layout (GtkIconView *icon_view) { - gint y = 0, maximum_width = 0; - GList *icons; GtkWidget *widget; - gint row; - gint item_width; + gint64 deadline; if (icon_view->priv->layout_idle_id != 0) { @@ -2823,54 +2834,100 @@ gtk_icon_view_layout (GtkIconView *icon_view) } if (icon_view->priv->model == NULL) - return; + { + icon_view->priv->layout_in_progress = FALSE; + icon_view->priv->layout_items = NULL; + return; + } widget = GTK_WIDGET (icon_view); + deadline = g_get_monotonic_time () + 2000; - item_width = icon_view->priv->item_width; - - if (item_width < 0) + if (!icon_view->priv->layout_in_progress || + icon_view->priv->layout_restart) { - for (icons = icon_view->priv->items; icons; icons = icons->next) - { - GtkIconViewItem *item = icons->data; - gtk_icon_view_calculate_item_size (icon_view, item); - item_width = MAX (item_width, item->width); - } + icon_view->priv->layout_in_progress = TRUE; + icon_view->priv->layout_restart = FALSE; + icon_view->priv->layout_items = icon_view->priv->items; + icon_view->priv->layout_y = icon_view->priv->margin; + icon_view->priv->layout_maximum_width = 0; + icon_view->priv->layout_row = 0; + icon_view->priv->layout_item_width = icon_view->priv->item_width; + icon_view->priv->layout_next_index = 0; + icon_view->priv->layout_scanning_width = + icon_view->priv->layout_item_width < 0; + + if (!icon_view->priv->layout_scanning_width && + icon_view->priv->layout_items) + { + gtk_icon_view_set_cell_data (icon_view, + icon_view->priv->layout_items->data); + adjust_wrap_width (icon_view, icon_view->priv->layout_items->data); + } } - - icons = icon_view->priv->items; - y += icon_view->priv->margin; - row = 0; - - if (icons) + while (icon_view->priv->layout_scanning_width && + icon_view->priv->layout_items) { - gtk_icon_view_set_cell_data (icon_view, icons->data); - adjust_wrap_width (icon_view, icons->data); + GtkIconViewItem *item = icon_view->priv->layout_items->data; + + gtk_icon_view_calculate_item_size (icon_view, item); + icon_view->priv->layout_item_width = + MAX (icon_view->priv->layout_item_width, item->width); + icon_view->priv->layout_items = icon_view->priv->layout_items->next; + + if (g_get_monotonic_time () >= deadline) + goto yield; } - do + if (icon_view->priv->layout_scanning_width) { - icons = gtk_icon_view_layout_single_row (icon_view, icons, - item_width, row, - &y, &maximum_width); - row++; + icon_view->priv->layout_scanning_width = FALSE; + icon_view->priv->layout_items = icon_view->priv->items; + + if (icon_view->priv->layout_items) + { + gtk_icon_view_set_cell_data (icon_view, + icon_view->priv->layout_items->data); + adjust_wrap_width (icon_view, icon_view->priv->layout_items->data); + } } - while (icons != NULL); - if (maximum_width != icon_view->priv->width) - icon_view->priv->width = maximum_width; + while (icon_view->priv->layout_items) + { + icon_view->priv->layout_items = + gtk_icon_view_layout_single_row (icon_view, + icon_view->priv->layout_items, + icon_view->priv->layout_item_width, + icon_view->priv->layout_row++, + &icon_view->priv->layout_y, + &icon_view->priv->layout_maximum_width); - y += icon_view->priv->margin; + icon_view->priv->layout_next_index = + icon_view->priv->layout_items + ? ((GtkIconViewItem *) icon_view->priv->layout_items->data)->index + : G_MAXINT; - if (y != icon_view->priv->height) - icon_view->priv->height = y; + if (icon_view->priv->layout_restart || + (icon_view->priv->layout_items && + g_get_monotonic_time () >= deadline)) + goto yield; + } + + icon_view->priv->layout_in_progress = FALSE; + + if (icon_view->priv->layout_maximum_width != icon_view->priv->width) + icon_view->priv->width = icon_view->priv->layout_maximum_width; + + icon_view->priv->layout_y += icon_view->priv->margin; + + if (icon_view->priv->layout_y != icon_view->priv->height) + icon_view->priv->height = icon_view->priv->layout_y; gtk_icon_view_set_adjustment_upper (icon_view->priv->hadjustment, - icon_view->priv->width); + icon_view->priv->width); gtk_icon_view_set_adjustment_upper (icon_view->priv->vadjustment, - icon_view->priv->height); + icon_view->priv->height); if (icon_view->priv->width != widget->requisition.width || icon_view->priv->height != widget->requisition.height) @@ -2878,8 +2935,8 @@ gtk_icon_view_layout (GtkIconView *icon_view) if (gtk_widget_get_realized (GTK_WIDGET (icon_view))) gdk_window_resize (icon_view->priv->bin_window, - MAX (icon_view->priv->width, widget->allocation.width), - MAX (icon_view->priv->height, widget->allocation.height)); + MAX (icon_view->priv->width, widget->allocation.width), + MAX (icon_view->priv->height, widget->allocation.height)); if (icon_view->priv->scroll_to_path) { @@ -2890,13 +2947,21 @@ gtk_icon_view_layout (GtkIconView *icon_view) icon_view->priv->scroll_to_path = NULL; gtk_icon_view_scroll_to_path (icon_view, path, - icon_view->priv->scroll_to_use_align, - icon_view->priv->scroll_to_row_align, - icon_view->priv->scroll_to_col_align); + icon_view->priv->scroll_to_use_align, + icon_view->priv->scroll_to_row_align, + icon_view->priv->scroll_to_col_align); gtk_tree_path_free (path); } gtk_widget_queue_draw (widget); + return; + +yield: + if (icon_view->priv->layout_idle_id == 0) + icon_view->priv->layout_idle_id = + gdk_threads_add_idle (layout_callback, icon_view); + + gtk_widget_queue_draw (widget); } static void @@ -3409,6 +3474,9 @@ layout_callback (gpointer user_data) static void gtk_icon_view_queue_layout (GtkIconView *icon_view) { + if (icon_view->priv->layout_in_progress) + icon_view->priv->layout_restart = TRUE; + if (icon_view->priv->layout_idle_id != 0) return; @@ -3500,6 +3568,10 @@ gtk_icon_view_get_item_at_coords (GtkIconView *icon_view, { GtkIconViewItem *item = items->data; + if (icon_view->priv->layout_in_progress && + item->index >= icon_view->priv->layout_next_index) + break; + if (x >= item->x - icon_view->priv->column_spacing/2 && x <= item->x + item->width + icon_view->priv->column_spacing/2 && y >= item->y - icon_view->priv->row_spacing/2 && y <= item->y + item->height + icon_view->priv->row_spacing/2) { @@ -5207,6 +5279,10 @@ gtk_icon_view_get_visible_range (GtkIconView *icon_view, { GtkIconViewItem *item = icons->data; + if (icon_view->priv->layout_in_progress && + item->index >= icon_view->priv->layout_next_index) + break; + if ((item->x + item->width >= (int)icon_view->priv->hadjustment->value) && (item->y + item->height >= (int)icon_view->priv->vadjustment->value) && (item->x <= (int) (icon_view->priv->hadjustment->value + icon_view->priv->hadjustment->page_size)) && From 608c99d92dd634d27ef12120d480e22867bd7d14 Mon Sep 17 00:00:00 2001 From: Daemonratte Date: Tue, 1 Sep 2026 11:56:04 +0200 Subject: [PATCH 122/122] gtktreeview: reduce validation stalls for fixed-height file lists Use a shorter validation idle budget on general targets while retaining a longer ARMv6 budget for weak single-core hardware. Add an internal fixed-height-mode helper for FileChooser rows whose height is width-independent, and avoid unnecessary scrolling work when the first root row is already visible. --- gtk/gtkfilechooserdefault.c | 9 +++++++-- gtk/gtktreeprivate.h | 4 ++++ gtk/gtktreeview.c | 35 ++++++++++++++++++++++++++++++++++- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index f481d8cf1..ec6e86843 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -11770,6 +11770,11 @@ load_set_model (GtkFileChooserDefault *impl) g_assert (impl->browse_files_model != NULL); profile_msg (" gtk_tree_view_set_model start", NULL); + /* FileChooser list rows have width-independent height. */ + if (impl->view_mode == VIEW_MODE_LIST) + _gtk_tree_view_set_fixed_height_mode_unchecked + (GTK_TREE_VIEW (impl->browse_files_tree_view)); + current_view_set_file_model (impl, GTK_TREE_MODEL (impl->browse_files_model)); if (impl->view_mode == VIEW_MODE_LIST) @@ -16400,13 +16405,13 @@ current_view_set_file_model (GtkFileChooserDefault *impl, GtkTreeModel *model) g_object_set (view, "model", impl->current_model, NULL); + icon_view_visible_range_schedule (impl); + /* * If an icon-view scale change has queued a delayed full cache clear, a model * can become visible before that timeout fires. Clear the newly visible model * immediately so it cannot show pixbufs cached for an older icon size. */ - icon_view_visible_range_schedule (impl); - if (impl->view_mode == VIEW_MODE_ICON && impl->icon_view_scale_timeout_id && GTK_IS_FILE_SYSTEM_MODEL (impl->current_model)) diff --git a/gtk/gtktreeprivate.h b/gtk/gtktreeprivate.h index b97110029..d93a94a6e 100644 --- a/gtk/gtktreeprivate.h +++ b/gtk/gtktreeprivate.h @@ -465,6 +465,10 @@ void _gtk_tree_view_column_get_neighbor_sizes (GtkTreeViewColumn *c gint *right); +G_GNUC_INTERNAL +void _gtk_tree_view_set_fixed_height_mode_unchecked + (GtkTreeView *tree_view); + G_END_DECLS diff --git a/gtk/gtktreeview.c b/gtk/gtktreeview.c index fc4d79780..767da2ff0 100644 --- a/gtk/gtktreeview.c +++ b/gtk/gtktreeview.c @@ -48,7 +48,11 @@ #define GTK_TREE_VIEW_PRIORITY_VALIDATE (GDK_PRIORITY_REDRAW + 5) #define GTK_TREE_VIEW_PRIORITY_SCROLL_SYNC (GTK_TREE_VIEW_PRIORITY_VALIDATE + 2) -#define GTK_TREE_VIEW_TIME_MS_PER_IDLE 30 +#if defined(__arm__) && defined(__ARM_ARCH) && (__ARM_ARCH == 6) +#define GTK_TREE_VIEW_TIME_MS_PER_IDLE 10 +#else +#define GTK_TREE_VIEW_TIME_MS_PER_IDLE 2 +#endif #define SCROLL_EDGE_SIZE 15 #define EXPANDER_EXTRA_PADDING 4 #define GTK_TREE_VIEW_SEARCH_DIALOG_TIMEOUT 5000 @@ -7732,6 +7736,25 @@ column_sizing_notify (GObject *object, g_object_set (data, "fixed-height-mode", FALSE, NULL); } +/* Internal fixed-height mode for width-independent rows. */ +G_GNUC_INTERNAL +void +_gtk_tree_view_set_fixed_height_mode_unchecked (GtkTreeView *tree_view) +{ + g_return_if_fail (GTK_IS_TREE_VIEW (tree_view)); + + if (tree_view->priv->fixed_height_mode) + return; + + tree_view->priv->fixed_height_mode = TRUE; + tree_view->priv->fixed_height = -1; + + if (tree_view->priv->tree) + initialize_fixed_height_mode (tree_view); + + g_object_notify (G_OBJECT (tree_view), "fixed-height-mode"); +} + /** * gtk_tree_view_set_fixed_height_mode: * @tree_view: a #GtkTreeView @@ -8927,6 +8950,16 @@ gtk_tree_view_clamp_node_visible (GtkTreeView *tree_view, path = _gtk_tree_view_find_path (tree_view, tree, node); if (path) { + /* The first root row at the top cannot require scrolling. */ + if (tree_view->priv->vadjustment->value <= + tree_view->priv->vadjustment->lower && + gtk_tree_path_get_depth (path) == 1 && + gtk_tree_path_get_indices (path)[0] == 0) + { + gtk_tree_path_free (path); + return; + } + /* We process updates because we want to clear old selected items when we scroll. * if this is removed, we get a "selection streak" at the bottom. */ gdk_window_process_updates (tree_view->priv->bin_window, TRUE);