diff --git a/libgimpwidgets/gimpfilechooser.c b/libgimpwidgets/gimpfilechooser.c
new file mode 100644
index 0000000000..2b78fa740e
--- /dev/null
+++ b/libgimpwidgets/gimpfilechooser.c
@@ -0,0 +1,751 @@
+/* LIBGIMP - The GIMP Library
+ * Copyright (C) 1995-1997 Peter Mattis and Spencer Kimball
+ *
+ * gimpfilechooser.h
+ * Copyright (C) 2025 Jehan
+ *
+ * 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 3 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
+ * Library 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 "config.h"
+
+#include
+
+#include "libgimpbase/gimpbase.h"
+
+#include "gimpicons.h"
+#include "gimpwidgetstypes.h"
+
+#include "gimpfilechooser.h"
+
+#include "libgimp/libgimp-intl.h"
+
+
+/**
+ * SECTION: gimpfilechooser
+ * @title: GimpFileChooser
+ * @short_description: A widget allowing to select a file.
+ *
+ * The chooser contains an optional label and other interface allowing
+ * to select files for different use cases.
+ *
+ * Since: 3.0
+ **/
+
+
+enum
+{
+ PROP_0,
+ PROP_ACTION,
+ PROP_LABEL,
+ PROP_TITLE,
+ PROP_FILE,
+ N_PROPS
+};
+
+struct _GimpFileChooser
+{
+ GtkBox parent_instance;
+
+ GFile *file;
+ gchar *title;
+ gchar *label;
+
+ GimpFileChooserAction action;
+ GtkWidget *label_widget;
+ GtkWidget *button;
+ GtkWidget *entry;
+
+ GtkWidget *dialog;
+
+ gboolean invalid_file;
+};
+
+
+static void gimp_file_chooser_constructed (GObject *object);
+static void gimp_file_chooser_finalize (GObject *object);
+
+static void gimp_file_chooser_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec);
+static void gimp_file_chooser_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec);
+
+static void gimp_file_chooser_button_selection_changed (GtkFileChooser *widget,
+ GimpFileChooser *chooser);
+static void gimp_file_chooser_dialog_response (GtkDialog *dialog,
+ gint response_id,
+ GimpFileChooser *chooser);
+static void gimp_file_chooser_button_clicked (GtkButton *button,
+ GimpFileChooser *chooser);
+static void gimp_file_chooser_entry_text_notify (GtkEntry *entry,
+ const GParamSpec *pspec,
+ GimpFileChooser *chooser);
+
+
+static GParamSpec *file_button_props[N_PROPS] = { NULL, };
+
+/* Note: I initially wanted to implement the GtkFileChooser interface,
+ * but it looks like GTK made GtkFileChooserIface private. So it can't
+ * be implemented outside of core GTK widgets.
+ */
+G_DEFINE_FINAL_TYPE (GimpFileChooser, gimp_file_chooser, GTK_TYPE_BOX)
+
+
+static void
+gimp_file_chooser_class_init (GimpFileChooserClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->constructed = gimp_file_chooser_constructed;
+ object_class->finalize = gimp_file_chooser_finalize;
+ object_class->set_property = gimp_file_chooser_set_property;
+ object_class->get_property = gimp_file_chooser_get_property;
+
+ /**
+ * GimpFileChooser:action:
+ *
+ * The action determining the chooser UI.
+ *
+ * Since: 3.0
+ */
+ file_button_props[PROP_ACTION] =
+ g_param_spec_enum ("action",
+ "Action",
+ "The action determining the chooser UI",
+ GIMP_TYPE_FILE_CHOOSER_ACTION,
+ GTK_FILE_CHOOSER_ACTION_OPEN,
+ GIMP_PARAM_READWRITE |
+ G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * GimpFileChooser:label:
+ *
+ * Label text with mnemonic.
+ *
+ * Since: 3.0
+ */
+ file_button_props[PROP_LABEL] =
+ g_param_spec_string ("label",
+ "Label",
+ "The label to be used next to the button",
+ NULL,
+ GIMP_PARAM_READWRITE |
+ G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * GimpFileChooser:title:
+ *
+ * The title to be used for the file selection popup dialog.
+ * If %NULL, the "label" property is used instead.
+ *
+ * Since: 3.0
+ */
+ file_button_props[PROP_TITLE] =
+ g_param_spec_string ("title",
+ "Title",
+ "The title to be used for the file selection popup dialog "
+ "and as placeholder text in file entry.",
+ "File Selection",
+ GIMP_PARAM_READWRITE |
+ G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * GimpFileChooser:file:
+ *
+ * The currently selected file.
+ *
+ * Since: 3.0
+ */
+ file_button_props[PROP_FILE] =
+ gimp_param_spec_file ("file", "File",
+ "The currently selected file",
+ GIMP_FILE_CHOOSER_ACTION_ANY,
+ TRUE, NULL,
+ GIMP_PARAM_READWRITE |
+ G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class,
+ N_PROPS, file_button_props);
+}
+
+static void
+gimp_file_chooser_init (GimpFileChooser *chooser)
+{
+ gtk_orientable_set_orientation (GTK_ORIENTABLE (chooser),
+ GTK_ORIENTATION_HORIZONTAL);
+ gtk_box_set_spacing (GTK_BOX (chooser), 6);
+
+ chooser->action = GIMP_FILE_CHOOSER_ACTION_OPEN;
+ chooser->button = NULL;
+ chooser->entry = NULL;
+ chooser->dialog = NULL;
+ chooser->file = NULL;
+
+ chooser->invalid_file = FALSE;
+}
+
+static void
+gimp_file_chooser_constructed (GObject *object)
+{
+ GimpFileChooser *chooser = GIMP_FILE_CHOOSER (object);
+
+ chooser->label_widget = gtk_label_new (NULL);
+ gtk_box_pack_start (GTK_BOX (chooser), chooser->label_widget, FALSE, FALSE, 0);
+ if (chooser->label)
+ gtk_label_set_text_with_mnemonic (GTK_LABEL (chooser->label_widget), chooser->label);
+ gtk_widget_set_visible (chooser->label_widget, chooser->label != NULL);
+
+ gimp_file_chooser_set_action (chooser, chooser->action);
+
+ G_OBJECT_CLASS (gimp_file_chooser_parent_class)->constructed (object);
+}
+
+static void
+gimp_file_chooser_finalize (GObject *object)
+{
+ GimpFileChooser *chooser = GIMP_FILE_CHOOSER (object);
+
+ g_clear_pointer (&chooser->title, g_free);
+ g_clear_pointer (&chooser->label, g_free);
+ g_clear_object (&chooser->file);
+
+ G_OBJECT_CLASS (gimp_file_chooser_parent_class)->finalize (object);
+}
+
+static void
+gimp_file_chooser_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GimpFileChooser *chooser = GIMP_FILE_CHOOSER (object);
+
+ switch (property_id)
+ {
+ case PROP_ACTION:
+ gimp_file_chooser_set_action (chooser, g_value_get_enum (value));
+ break;
+
+ case PROP_LABEL:
+ gimp_file_chooser_set_label (chooser, g_value_get_string (value));
+ break;
+
+ case PROP_TITLE:
+ gimp_file_chooser_set_title (chooser, g_value_get_string (value));
+ break;
+
+ case PROP_FILE:
+ gimp_file_chooser_set_file (chooser, g_value_get_object (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+gimp_file_chooser_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GimpFileChooser *chooser = GIMP_FILE_CHOOSER (object);
+
+ switch (property_id)
+ {
+ case PROP_ACTION:
+ g_value_set_enum (value, chooser->action);
+ break;
+
+ case PROP_LABEL:
+ g_value_set_string (value, chooser->label);
+ break;
+
+ case PROP_TITLE:
+ g_value_set_string (value, chooser->title);
+ break;
+
+ case PROP_FILE:
+ g_value_set_object (value, chooser->file);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+/**
+ * gimp_file_chooser_new:
+ * @action: the action determining the UI created for this widget.
+ * @label: (nullable): Label or %NULL for no label.
+ * @title: (nullable): Title of the dialog to use or %NULL to reuse @label.
+ * @file: (nullable): Initial file.
+ *
+ * Creates a new #GtkWidget that lets a user choose a file according to
+ * @action.
+ *
+ * [enum@Gimp.FileChooserAction.ANY] is not a valid value for @action.
+ *
+ * Returns: A [class@GimpUi.FileChooser].
+ *
+ * Since: 3.0
+ */
+GtkWidget *
+gimp_file_chooser_new (GimpFileChooserAction action,
+ const gchar *label,
+ const gchar *title,
+ GFile *file)
+{
+ GtkWidget *chooser;
+
+ g_return_val_if_fail (action != GIMP_FILE_CHOOSER_ACTION_ANY, NULL);
+ g_return_val_if_fail (file == NULL || G_IS_FILE (file), NULL);
+
+ chooser = g_object_new (GIMP_TYPE_FILE_CHOOSER,
+ "action", action,
+ "label", label,
+ "title", title,
+ "file", file,
+ NULL);
+
+ return chooser;
+}
+
+/**
+ * gimp_file_chooser_get_action:
+ * @chooser: A #GimpFileChooser
+ *
+ * Gets the current action.
+ *
+ * Returns: the action which determined the UI of @chooser.
+ *
+ * Since: 3.0
+ */
+GimpFileChooserAction
+gimp_file_chooser_get_action (GimpFileChooser *chooser)
+{
+ g_return_val_if_fail (GIMP_IS_FILE_CHOOSER (chooser), GIMP_FILE_CHOOSER_ACTION_ANY);
+
+ return chooser->action;
+}
+
+/**
+ * gimp_file_chooser_set_action:
+ * @chooser: A #GimpFileChooser
+ * @action: Action to set.
+ *
+ * Changes how @chooser is set to select a file. It may completely
+ * change the internal widget structure so you should not depend on a
+ * specific widget composition.
+ *
+ * Warning: with GTK deprecations, we may have soon to change the
+ * internal implementation. So this is all the more reason for you not
+ * to rely on specific child widgets being present (e.g.: we use
+ * currently [class@Gtk.FileChooserButton] internally but it was removed
+ * in GTK4 so we will eventually replace it by custom code). We will
+ * also likely move to native file dialogs at some point.
+ *
+ * [enum@Gimp.FileChooserAction.ANY] is not a valid value for @action.
+ *
+ * Since: 3.0
+ */
+void
+gimp_file_chooser_set_action (GimpFileChooser *chooser,
+ GimpFileChooserAction action)
+{
+ gchar *uri_path = NULL;
+
+ g_return_if_fail (GIMP_IS_FILE_CHOOSER (chooser));
+ g_return_if_fail (action != GIMP_FILE_CHOOSER_ACTION_ANY);
+
+ if (chooser->button)
+ gtk_widget_destroy (chooser->button);
+ if (chooser->entry)
+ gtk_widget_destroy (chooser->entry);
+ if (chooser->dialog)
+ gtk_widget_destroy (chooser->dialog);
+ chooser->button = NULL;
+ chooser->entry = NULL;
+ chooser->dialog = NULL;
+
+ switch (action)
+ {
+ case GIMP_FILE_CHOOSER_ACTION_OPEN:
+ case GIMP_FILE_CHOOSER_ACTION_SELECT_FOLDER:
+ chooser->button = gtk_file_chooser_button_new (chooser->title, (GtkFileChooserAction) action);
+ gtk_box_pack_start (GTK_BOX (chooser), chooser->button, FALSE, FALSE, 0);
+ g_signal_connect (chooser->button, "selection-changed",
+ G_CALLBACK (gimp_file_chooser_button_selection_changed),
+ chooser);
+ gtk_widget_set_visible (chooser->button, TRUE);
+
+ gtk_label_set_mnemonic_widget (GTK_LABEL (chooser->label_widget), chooser->button);
+ break;
+ case GIMP_FILE_CHOOSER_ACTION_SAVE:
+ case GIMP_FILE_CHOOSER_ACTION_CREATE_FOLDER:
+ chooser->entry = gtk_entry_new ();
+ gtk_box_pack_start (GTK_BOX (chooser), chooser->entry, TRUE, TRUE, 0);
+ if (chooser->file)
+ {
+ uri_path = g_file_get_path (chooser->file);
+ if (! uri_path)
+ uri_path = g_file_get_uri (chooser->file);
+ }
+ if (! uri_path)
+ uri_path = g_strdup ("");
+ gtk_entry_set_text (GTK_ENTRY (chooser->entry), uri_path);
+ g_signal_connect (chooser->entry, "notify::text",
+ G_CALLBACK (gimp_file_chooser_entry_text_notify),
+ chooser);
+ gtk_entry_set_placeholder_text (GTK_ENTRY (chooser->entry), chooser->title);
+ gtk_widget_set_visible (chooser->entry, TRUE);
+
+ chooser->button = gtk_button_new_from_icon_name (GIMP_ICON_FILE_MANAGER, GTK_ICON_SIZE_SMALL_TOOLBAR);
+ gtk_box_pack_start (GTK_BOX (chooser), chooser->button, FALSE, FALSE, 0);
+ gtk_widget_set_visible (chooser->button, TRUE);
+ g_signal_connect (chooser->button, "clicked",
+ G_CALLBACK (gimp_file_chooser_button_clicked),
+ chooser);
+
+ gtk_label_set_mnemonic_widget (GTK_LABEL (chooser->label_widget), chooser->entry);
+ break;
+ case GIMP_FILE_CHOOSER_ACTION_ANY:
+ g_return_if_reached ();
+ }
+
+ chooser->action = action;
+ gimp_param_spec_file_set_action (file_button_props[PROP_FILE], action);
+
+ g_object_notify_by_pspec (G_OBJECT (chooser), file_button_props[PROP_ACTION]);
+
+ g_free (uri_path);
+}
+
+/**
+ * gimp_file_chooser_get_file:
+ * @chooser: A #GimpFileChooser
+ *
+ * Gets the currently selected file.
+ *
+ * Returns: (transfer none): an internal copy of the file which must not be freed.
+ *
+ * Since: 3.0
+ */
+GFile *
+gimp_file_chooser_get_file (GimpFileChooser *chooser)
+{
+ g_return_val_if_fail (GIMP_IS_FILE_CHOOSER (chooser), NULL);
+
+ return chooser->file;
+}
+
+/**
+ * gimp_file_chooser_set_file:
+ * @chooser: A #GimpFileChooser
+ * @file: File to set.
+ *
+ * Sets the currently selected file.
+ *
+ * Since: 3.0
+ */
+void
+gimp_file_chooser_set_file (GimpFileChooser *chooser,
+ GFile *file)
+{
+ GFile *current_file = NULL;
+ gchar *uri_path = NULL;
+
+ g_return_if_fail (GIMP_IS_FILE_CHOOSER (chooser));
+ g_return_if_fail (file == NULL || G_IS_FILE (file));
+
+ current_file = chooser->file ? g_object_ref (chooser->file) : NULL;
+ g_clear_object (&chooser->file);
+ chooser->file = file ? g_object_ref (file) : NULL;
+
+ if ((current_file != NULL && file == NULL) ||
+ (file != NULL && current_file == NULL) ||
+ (file != NULL && current_file != NULL && ! g_file_equal (file, current_file)))
+ {
+ switch (chooser->action)
+ {
+ case GTK_FILE_CHOOSER_ACTION_OPEN:
+ case GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER:
+ gtk_file_chooser_set_file (GTK_FILE_CHOOSER (chooser->button), file, NULL);
+ break;
+ case GTK_FILE_CHOOSER_ACTION_SAVE:
+ case GTK_FILE_CHOOSER_ACTION_CREATE_FOLDER:
+ if (chooser->dialog)
+ gtk_file_chooser_set_file (GTK_FILE_CHOOSER (chooser->dialog), file, NULL);
+
+ if (file)
+ {
+ uri_path = g_file_get_path (file);
+ if (! uri_path)
+ uri_path = g_file_get_uri (file);
+ }
+ if (! uri_path)
+ uri_path = g_strdup ("");
+ g_signal_handlers_block_by_func (chooser->entry,
+ G_CALLBACK (gimp_file_chooser_entry_text_notify),
+ chooser);
+ if (! chooser->invalid_file)
+ gtk_entry_set_text (GTK_ENTRY (chooser->entry), uri_path);
+ g_signal_handlers_unblock_by_func (chooser->entry,
+ G_CALLBACK (gimp_file_chooser_entry_text_notify),
+ chooser);
+ break;
+ case GIMP_FILE_CHOOSER_ACTION_ANY:
+ g_return_if_reached ();
+ }
+ }
+
+ g_object_notify_by_pspec (G_OBJECT (chooser), file_button_props[PROP_FILE]);
+
+ g_clear_object (¤t_file);
+ g_free (uri_path);
+}
+
+/**
+ * gimp_file_chooser_get_label:
+ * @chooser: A #GimpFileChooser
+ *
+ * Gets the current label text. A %NULL label means that the label
+ * widget is hidden.
+ *
+ * Note: the label text may contain a mnemonic.
+ *
+ * Returns: (nullable): the label set.
+ *
+ * Since: 3.0
+ */
+const gchar *
+gimp_file_chooser_get_label (GimpFileChooser *chooser)
+{
+ g_return_val_if_fail (GIMP_IS_FILE_CHOOSER (chooser), NULL);
+
+ return chooser->label;
+}
+
+/**
+ * gimp_file_chooser_set_label:
+ * @chooser: A [class@FileChooser].
+ * @text: (nullable): Label text.
+ *
+ * Set the label text with mnemonic.
+ *
+ * Setting a %NULL label text will hide the label widget.
+ *
+ * Since: 3.0
+ */
+void
+gimp_file_chooser_set_label (GimpFileChooser *chooser,
+ const gchar *text)
+{
+ g_return_if_fail (GIMP_IS_FILE_CHOOSER (chooser));
+
+ g_free (chooser->label);
+ chooser->label = g_strdup (text);
+ gtk_widget_set_visible (chooser->label_widget, text != NULL);
+
+ if (chooser->label_widget)
+ {
+ if (text != NULL)
+ gtk_label_set_text_with_mnemonic (GTK_LABEL (chooser->label_widget), text);
+
+ gtk_widget_set_visible (chooser->label_widget, text != NULL);
+ }
+
+ g_object_notify_by_pspec (G_OBJECT (chooser), file_button_props[PROP_LABEL]);
+}
+
+/**
+ * gimp_file_chooser_get_title:
+ * @chooser: A #GimpFileChooser
+ *
+ * Gets the text currently used for the file dialog's title and for
+ * entry's placeholder text.
+ *
+ * A %NULL value means that the file dialog uses default title and the
+ * entry has no placeholder text.
+ *
+ * Returns: (nullable): the text used for the title of @chooser's dialog.
+ *
+ * Since: 3.0
+ */
+const gchar *
+gimp_file_chooser_get_title (GimpFileChooser *chooser)
+{
+ g_return_val_if_fail (GIMP_IS_FILE_CHOOSER (chooser), NULL);
+
+ return chooser->title;
+}
+
+/**
+ * gimp_file_chooser_set_title:
+ * @chooser: A [class@FileChooser].
+ * @text: (nullable): Dialog's title text.
+ *
+ * Set the text to be used for the file dialog's title and for entry's
+ * placeholder text.
+ *
+ * Setting a %NULL title @text will mean that the file dialog will use a
+ * generic title and there will be no placeholder text in the entry.
+ *
+ * Since: 3.0
+ */
+void
+gimp_file_chooser_set_title (GimpFileChooser *chooser,
+ const gchar *text)
+{
+ g_return_if_fail (GIMP_IS_FILE_CHOOSER (chooser));
+
+ g_free (chooser->title);
+ chooser->title = g_strdup (text);
+
+ if (chooser->dialog)
+ gtk_window_set_title (GTK_WINDOW (chooser->dialog), chooser->title);
+
+ if (chooser->entry)
+ gtk_entry_set_placeholder_text (GTK_ENTRY (chooser->entry), chooser->title);
+
+ g_object_notify_by_pspec (G_OBJECT (chooser), file_button_props[PROP_TITLE]);
+}
+
+/**
+ * gimp_file_chooser_get_label_widget:
+ * @chooser: A [class@FileChooser].
+ *
+ * Returns the label widget. This can be useful for instance when
+ * aligning dialog's widgets with a [class@Gtk.SizeGroup].
+ *
+ * Returns: (transfer none): the [class@Gtk.Widget] showing the label text.
+ *
+ * Since: 3.0
+ */
+GtkWidget *
+gimp_file_chooser_get_label_widget (GimpFileChooser *chooser)
+{
+ g_return_val_if_fail (GIMP_IS_FILE_CHOOSER (chooser), NULL);
+
+ return chooser->label_widget;
+}
+
+
+/* Private Functions */
+
+static void
+gimp_file_chooser_button_selection_changed (GtkFileChooser *widget,
+ GimpFileChooser *chooser)
+{
+ GFile *file;
+
+ file = gtk_file_chooser_get_file (widget);
+ gimp_file_chooser_set_file (chooser, file);
+ g_clear_object (&file);
+}
+
+static void
+gimp_file_chooser_dialog_response (GtkDialog *dialog,
+ gint response_id,
+ GimpFileChooser *chooser)
+{
+ GFile *file = NULL;
+
+ switch (response_id)
+ {
+ case GTK_RESPONSE_OK:
+ file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog));
+ gimp_file_chooser_set_file (chooser, file);
+ break;
+ case GTK_RESPONSE_CANCEL:
+ case GTK_RESPONSE_DELETE_EVENT:
+ default:
+ break;
+ }
+
+ gtk_widget_destroy (GTK_WIDGET (dialog));
+ chooser->dialog = NULL;
+
+ g_clear_object (&file);
+}
+
+static void
+gimp_file_chooser_button_clicked (GtkButton *button,
+ GimpFileChooser *chooser)
+{
+ GtkWidget *toplevel;
+
+ if (chooser->dialog)
+ {
+ gtk_window_present (GTK_WINDOW (chooser->dialog));
+ return;
+ }
+
+ toplevel = gtk_widget_get_toplevel (GTK_WIDGET (chooser));
+ chooser->dialog = gtk_file_chooser_dialog_new ((const gchar *) chooser->title,
+ GTK_WINDOW (toplevel),
+ (GtkFileChooserAction) chooser->action,
+ _("_OK"), GTK_RESPONSE_OK,
+ _("_Cancel"), GTK_RESPONSE_CANCEL,
+ NULL);
+ if (chooser->file)
+ gtk_file_chooser_set_file (GTK_FILE_CHOOSER (chooser->dialog), chooser->file, NULL);
+
+ g_signal_connect (chooser->dialog, "response",
+ G_CALLBACK (gimp_file_chooser_dialog_response),
+ chooser);
+ gtk_widget_set_visible (chooser->dialog, TRUE);
+}
+
+static void
+gimp_file_chooser_entry_text_notify (GtkEntry *entry,
+ const GParamSpec *pspec,
+ GimpFileChooser *chooser)
+{
+ GParamSpec *chooser_pspec;
+ GFile *file;
+ GValue value = G_VALUE_INIT;
+
+ chooser_pspec = file_button_props[PROP_FILE];
+ file = g_file_parse_name (gtk_entry_get_text (entry));
+ g_value_init (&value, G_PARAM_SPEC_VALUE_TYPE (chooser_pspec));
+ g_value_set_object (&value, G_OBJECT (file));
+
+ if (! g_param_value_validate (chooser_pspec, &value))
+ {
+ gimp_file_chooser_set_file (chooser, file);
+ gtk_entry_set_icon_from_icon_name (entry, GTK_ENTRY_ICON_SECONDARY, NULL);
+ }
+ else
+ {
+ chooser->invalid_file = TRUE;
+ gimp_file_chooser_set_file (chooser, NULL);
+ chooser->invalid_file = FALSE;
+ /* XXX When not validating, I initially wanted to set the entry
+ * borders to the error_color from the current theme. But I
+ * settled with a simpler icon, which works well too IMO.
+ */
+ gtk_entry_set_icon_from_icon_name (entry, GTK_ENTRY_ICON_SECONDARY, GIMP_ICON_WILBER_EEK);
+ }
+
+ g_value_unset (&value);
+ g_clear_object (&file);
+}
diff --git a/libgimpwidgets/gimpfilechooser.h b/libgimpwidgets/gimpfilechooser.h
new file mode 100644
index 0000000000..b856aae614
--- /dev/null
+++ b/libgimpwidgets/gimpfilechooser.h
@@ -0,0 +1,61 @@
+/* LIBGIMP - The GIMP Library
+ * Copyright (C) 1995-1997 Peter Mattis and Spencer Kimball
+ *
+ * gimpfilechooser.c
+ * Copyright (C) 2025 Jehan
+ *
+ * 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 3 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
+ * .
+ */
+
+#if !defined (__GIMP_WIDGETS_H_INSIDE__) && !defined (GIMP_WIDGETS_COMPILATION)
+#error "Only can be included directly."
+#endif
+
+#ifndef __GIMP_FILE_CHOOSER_H__
+#define __GIMP_FILE_CHOOSER_H__
+
+G_BEGIN_DECLS
+
+#define GIMP_TYPE_FILE_CHOOSER (gimp_file_chooser_get_type ())
+G_DECLARE_FINAL_TYPE (GimpFileChooser, gimp_file_chooser, GIMP, FILE_CHOOSER, GtkBox)
+
+
+GtkWidget * gimp_file_chooser_new (GimpFileChooserAction action,
+ const gchar *label,
+ const gchar *title,
+ GFile *file);
+
+GimpFileChooserAction gimp_file_chooser_get_action (GimpFileChooser *chooser);
+void gimp_file_chooser_set_action (GimpFileChooser *chooser,
+ GimpFileChooserAction action);
+
+GFile * gimp_file_chooser_get_file (GimpFileChooser *chooser);
+void gimp_file_chooser_set_file (GimpFileChooser *chooser,
+ GFile *file);
+
+const gchar * gimp_file_chooser_get_label (GimpFileChooser *chooser);
+void gimp_file_chooser_set_label (GimpFileChooser *chooser,
+ const gchar *text);
+
+const gchar * gimp_file_chooser_get_title (GimpFileChooser *chooser);
+void gimp_file_chooser_set_title (GimpFileChooser *chooser,
+ const gchar *text);
+
+GtkWidget * gimp_file_chooser_get_label_widget (GimpFileChooser *chooser);
+
+
+G_END_DECLS
+
+#endif /* __GIMP_FILE_CHOOSER_H__ */
diff --git a/libgimpwidgets/gimppropwidgets.c b/libgimpwidgets/gimppropwidgets.c
index 7777b30ba2..085785192b 100644
--- a/libgimpwidgets/gimppropwidgets.c
+++ b/libgimpwidgets/gimppropwidgets.c
@@ -2893,6 +2893,84 @@ static void gimp_prop_file_chooser_button_notify (GObject *confi
GtkFileChooser *button);
+/**
+ * gimp_prop_file_chooser_new:
+ * @config: Object to which property is attached.
+ * @property_name: Name of a %GimpParamSpecFile property.
+ * @label: (nullable): Label of the widget.
+ * @title: (nullable): Title of the file dialog.
+ *
+ * Creates a [class@GimpUi.FileChooser] to edit the specified file
+ * property. @property_name must be a %GimpParamSpecFile with an action
+ * other than [enum@Gimp.FileChooserAction.ANY].
+ *
+ * If @label is %NULL, @property_name's `nick` text will be used
+ * instead.
+ *
+ * Returns: (transfer full): A new #GtkFileChooserButton.
+ *
+ * Since: 3.0
+ */
+GtkWidget *
+gimp_prop_file_chooser_new (GObject *config,
+ const gchar *property_name,
+ const gchar *label,
+ const gchar *title)
+{
+ GimpFileChooserAction action;
+ GParamSpec *pspec;
+ GtkWidget *widget;
+ GFile *file = NULL;
+ const gchar *tooltip;
+
+ g_return_val_if_fail (G_IS_OBJECT (config), NULL);
+ g_return_val_if_fail (property_name != NULL, NULL);
+
+ pspec = find_param_spec (config, property_name, G_STRFUNC);
+ if (! pspec)
+ {
+ g_warning ("%s: %s has no property named '%s'",
+ G_STRFUNC, g_type_name (G_TYPE_FROM_INSTANCE (config)),
+ property_name);
+ return NULL;
+ }
+
+ if (! GIMP_IS_PARAM_SPEC_FILE (pspec))
+ {
+ g_warning ("%s: property '%s' of %s is not a GIMP_PARAM_SPEC_FILE.",
+ G_STRFUNC, pspec->name, g_type_name (pspec->owner_type));
+ return NULL;
+ }
+
+ action = gimp_param_spec_file_get_action (pspec);
+ if (action == GIMP_FILE_CHOOSER_ACTION_ANY)
+ {
+ g_warning ("%s: property '%s' of %s must not use action GIMP_FILE_CHOOSER_ACTION_ANY.",
+ G_STRFUNC, pspec->name, g_type_name (pspec->owner_type));
+ return NULL;
+ }
+
+ if (! label)
+ label = g_param_spec_get_nick (pspec);
+
+ g_object_get (config,
+ property_name, &file,
+ NULL);
+
+ widget = gimp_file_chooser_new (action, label, title, file);
+
+ tooltip = g_param_spec_get_blurb (pspec);
+ gimp_help_set_help_data (widget, tooltip, NULL);
+
+ g_object_bind_property (config, property_name,
+ widget, "file",
+ G_BINDING_BIDIRECTIONAL);
+
+ g_clear_object (&file);
+
+ return widget;
+}
+
/**
* gimp_prop_file_chooser_button_new:
* @config: object to which property is attached.
diff --git a/libgimpwidgets/gimppropwidgets.h b/libgimpwidgets/gimppropwidgets.h
index 4c6e52f993..5008a1d2cc 100644
--- a/libgimpwidgets/gimppropwidgets.h
+++ b/libgimpwidgets/gimppropwidgets.h
@@ -174,6 +174,11 @@ GtkWidget * gimp_prop_choice_radio_frame_new (GObject *config,
/* GimpParamPath */
+GtkWidget * gimp_prop_file_chooser_new (GObject *config,
+ const gchar *property_name,
+ const gchar *label,
+ const gchar *title);
+
GtkWidget * gimp_prop_file_chooser_button_new (GObject *config,
const gchar *property_name,
const gchar *title,
diff --git a/libgimpwidgets/gimpwidgets.def b/libgimpwidgets/gimpwidgets.def
index 2705100947..32336b6603 100644
--- a/libgimpwidgets/gimpwidgets.def
+++ b/libgimpwidgets/gimpwidgets.def
@@ -194,6 +194,17 @@ EXPORTS
gimp_enum_store_new_with_values_valist
gimp_enum_store_set_icon_prefix
gimp_event_triggers_context_menu
+ gimp_file_chooser_get_action
+ gimp_file_chooser_get_file
+ gimp_file_chooser_get_label
+ gimp_file_chooser_get_label_widget
+ gimp_file_chooser_get_title
+ gimp_file_chooser_get_type
+ gimp_file_chooser_new
+ gimp_file_chooser_set_action
+ gimp_file_chooser_set_file
+ gimp_file_chooser_set_label
+ gimp_file_chooser_set_title
gimp_float_adjustment_update
gimp_frame_get_type
gimp_frame_new
@@ -383,6 +394,7 @@ EXPORTS
gimp_prop_expander_new
gimp_prop_file_chooser_button_new
gimp_prop_file_chooser_button_new_with_dialog
+ gimp_prop_file_chooser_new
gimp_prop_hscale_new
gimp_prop_icon_image_new
gimp_prop_int_combo_box_new
diff --git a/libgimpwidgets/gimpwidgets.h b/libgimpwidgets/gimpwidgets.h
index 909a42fde0..79a7487e37 100644
--- a/libgimpwidgets/gimpwidgets.h
+++ b/libgimpwidgets/gimpwidgets.h
@@ -54,6 +54,7 @@
#include
#include
#include
+#include
#include
#include
#include
diff --git a/libgimpwidgets/gimpwidgetstypes.h b/libgimpwidgets/gimpwidgetstypes.h
index 449101282f..9fad1b98bf 100644
--- a/libgimpwidgets/gimpwidgetstypes.h
+++ b/libgimpwidgets/gimpwidgetstypes.h
@@ -57,6 +57,7 @@ typedef struct _GimpDialog GimpDialog;
typedef struct _GimpEnumStore GimpEnumStore;
typedef struct _GimpEnumComboBox GimpEnumComboBox;
typedef struct _GimpEnumLabel GimpEnumLabel;
+typedef struct _GimpFileChooser GimpFileChooser;
typedef struct _GimpFileEntry GimpFileEntry;
typedef struct _GimpFrame GimpFrame;
typedef struct _GimpHintBox GimpHintBox;
diff --git a/libgimpwidgets/meson.build b/libgimpwidgets/meson.build
index 447f8c7477..e348bc31e2 100644
--- a/libgimpwidgets/meson.build
+++ b/libgimpwidgets/meson.build
@@ -51,6 +51,7 @@ libgimpwidgets_sources_introspectable = files(
'gimpenumlabel.c',
'gimpenumstore.c',
'gimpenumwidgets.c',
+ 'gimpfilechooser.c',
'gimpframe.c',
'gimphelpui.c',
'gimphintbox.c',
@@ -134,6 +135,7 @@ libgimpwidgets_headers_introspectable = files(
'gimpenumlabel.h',
'gimpenumstore.h',
'gimpenumwidgets.h',
+ 'gimpfilechooser.h',
'gimpframe.h',
'gimphelpui.h',
'gimphintbox.h',
diff --git a/po-libgimp/POTFILES.in b/po-libgimp/POTFILES.in
index c3220c9f56..9e4f540bc5 100644
--- a/po-libgimp/POTFILES.in
+++ b/po-libgimp/POTFILES.in
@@ -56,6 +56,7 @@ libgimpwidgets/gimpcolorselect.c
libgimpwidgets/gimpcolorselection.c
libgimpwidgets/gimpcontroller.c
libgimpwidgets/gimpdialog.c
+libgimpwidgets/gimpfilechooser.c
libgimpwidgets/gimpfileentry.c
libgimpwidgets/gimphelpui.c
libgimpwidgets/gimpicons.c