/* -*- Mode: C; c-file-style: "gnu"; tab-width: 8 -*- */ /* GTK - The GIMP Toolkit * gtkfilechoosernative.c: Native File selector dialog * Copyright (C) 2015, Red Hat, Inc. * * 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 "config.h" #include "gtkfilechoosernativeprivate.h" #include "gtknativedialogprivate.h" #include "gtkprivate.h" #include "gtkfilechooserdialog.h" #include "gtkfilechooserprivate.h" #include "gtkfilechooserwidget.h" #include "gtkfilechooserwidgetprivate.h" #include "gtkfilechooserutils.h" #include "gtksizerequest.h" #include "gtktypebuiltins.h" #include "gtkintl.h" #include "gtksettings.h" #include "gtktogglebutton.h" #include "gtkheaderbar.h" #include "gtklabel.h" #include "gtkfilefilterprivate.h" /** * GtkFileChooserNative: * * `GtkFileChooserNative` is an abstraction of a dialog suitable * for use with “File Open” or “File Save as” commands. * * By default, this just uses a `GtkFileChooserDialog` to implement * the actual dialog. However, on some platforms, such as Windows and * macOS, the native platform file chooser is used instead. When the * application is running in a sandboxed environment without direct * filesystem access (such as Flatpak), `GtkFileChooserNative` may call * the proper APIs (portals) to let the user choose a file and make it * available to the application. * * While the API of `GtkFileChooserNative` closely mirrors `GtkFileChooserDialog`, * the main difference is that there is no access to any `GtkWindow` or `GtkWidget` * for the dialog. This is required, as there may not be one in the case of a * platform native dialog. * * Showing, hiding and running the dialog is handled by the * [class@Gtk.NativeDialog] functions. * * Note that unlike `GtkFileChooserDialog`, `GtkFileChooserNative` objects * are not toplevel widgets, and GTK does not keep them alive. It is your * responsibility to keep a reference until you are done with the * object. * ## Typical usage * * In the simplest of cases, you can the following code to use * `GtkFileChooserNative` to select a file for opening: * * ```c * static void * on_response (GtkNativeDialog *native, * int response) * { * if (response == GTK_RESPONSE_ACCEPT) * { * GtkFileChooser *chooser = GTK_FILE_CHOOSER (native); * GFile *file = gtk_file_chooser_get_file (chooser); * * open_file (file); * * g_object_unref (file); * } * * g_object_unref (native); * } * * // ... * GtkFileChooserNative *native; * GtkFileChooserAction action = GTK_FILE_CHOOSER_ACTION_OPEN; * * native = gtk_file_chooser_native_new ("Open File", * parent_window, * action, * "_Open", * "_Cancel"); * * g_signal_connect (native, "response", G_CALLBACK (on_response), NULL); * gtk_native_dialog_show (GTK_NATIVE_DIALOG (native)); * ``` * * To use a `GtkFileChooserNative` for saving, you can use this: * * ```c * static void * on_response (GtkNativeDialog *native, * int response) * { * if (response == GTK_RESPONSE_ACCEPT) * { * GtkFileChooser *chooser = GTK_FILE_CHOOSER (native); * GFile *file = gtk_file_chooser_get_file (chooser); * * save_to_file (file); * * g_object_unref (file); * } * * g_object_unref (native); * } * * // ... * GtkFileChooserNative *native; * GtkFileChooser *chooser; * GtkFileChooserAction action = GTK_FILE_CHOOSER_ACTION_SAVE; * * native = gtk_file_chooser_native_new ("Save File", * parent_window, * action, * "_Save", * "_Cancel"); * chooser = GTK_FILE_CHOOSER (native); * * if (user_edited_a_new_document) * gtk_file_chooser_set_current_name (chooser, _("Untitled document")); * else * gtk_file_chooser_set_file (chooser, existing_file, NULL); * * g_signal_connect (native, "response", G_CALLBACK (on_response), NULL); * gtk_native_dialog_show (GTK_NATIVE_DIALOG (native)); * ``` * * For more information on how to best set up a file dialog, * see the [class@Gtk.FileChooserDialog] documentation. * * ## Response Codes * * `GtkFileChooserNative` inherits from [class@Gtk.NativeDialog], * which means it will return %GTK_RESPONSE_ACCEPT if the user accepted, * and %GTK_RESPONSE_CANCEL if he pressed cancel. It can also return * %GTK_RESPONSE_DELETE_EVENT if the window was unexpectedly closed. * * ## Differences from `GtkFileChooserDialog` * * There are a few things in the [iface@Gtk.FileChooser] interface that * are not possible to use with `GtkFileChooserNative`, as such use would * prohibit the use of a native dialog. * * No operations that change the dialog work while the dialog is visible. * Set all the properties that are required before showing the dialog. * * ## Win32 details * * On windows the `IFileDialog` implementation (added in Windows Vista) is * used. It supports many of the features that `GtkFileChooser` has, but * there are some things it does not handle: * * * Any [class@Gtk.FileFilter] added using a mimetype * * If any of these features are used the regular `GtkFileChooserDialog` * will be used in place of the native one. * * ## Portal details * * When the `org.freedesktop.portal.FileChooser` portal is available on * the session bus, it is used to bring up an out-of-process file chooser. * Depending on the kind of session the application is running in, this may * or may not be a GTK file chooser. * * ## macOS details * * On macOS the `NSSavePanel` and `NSOpenPanel` classes are used to provide * native file chooser dialogs. Some features provided by `GtkFileChooser` * are not supported: * * * Shortcut folders. */ enum { MODE_FALLBACK, MODE_WIN32, MODE_QUARTZ, MODE_PORTAL, }; enum { PROP_0, PROP_ACCEPT_LABEL, PROP_CANCEL_LABEL, LAST_ARG, }; static GParamSpec *native_props[LAST_ARG] = { NULL, }; static void _gtk_file_chooser_native_iface_init (GtkFileChooserIface *iface); G_DEFINE_TYPE_WITH_CODE (GtkFileChooserNative, gtk_file_chooser_native, GTK_TYPE_NATIVE_DIALOG, G_IMPLEMENT_INTERFACE (GTK_TYPE_FILE_CHOOSER, _gtk_file_chooser_native_iface_init)) /** * gtk_file_chooser_native_get_accept_label: (attributes org.gtk.Method.get_property=accept-label) * @self: a `GtkFileChooserNative` * * Retrieves the custom label text for the accept button. * * Returns: (nullable): The custom label */ const char * gtk_file_chooser_native_get_accept_label (GtkFileChooserNative *self) { g_return_val_if_fail (GTK_IS_FILE_CHOOSER_NATIVE (self), NULL); return self->accept_label; } /** * gtk_file_chooser_native_set_accept_label: (attributes org.gtk.Method.set_property=accept-label) * @self: a `GtkFileChooserNative` * @accept_label: (nullable): custom label * * Sets the custom label text for the accept button. * * If characters in @label are preceded by an underscore, they are * underlined. If you need a literal underscore character in a label, * use “__” (two underscores). The first underlined character represents * a keyboard accelerator called a mnemonic. * * Pressing Alt and that key should activate the button. */ void gtk_file_chooser_native_set_accept_label (GtkFileChooserNative *self, const char *accept_label) { g_return_if_fail (GTK_IS_FILE_CHOOSER_NATIVE (self)); g_free (self->accept_label); self->accept_label = g_strdup (accept_label); g_object_notify_by_pspec (G_OBJECT (self), native_props[PROP_ACCEPT_LABEL]); } /** * gtk_file_chooser_native_get_cancel_label: (attributes org.gtk.Method.get_property=cancel-label) * @self: a `GtkFileChooserNative` * * Retrieves the custom label text for the cancel button. * * Returns: (nullable): The custom label */ const char * gtk_file_chooser_native_get_cancel_label (GtkFileChooserNative *self) { g_return_val_if_fail (GTK_IS_FILE_CHOOSER_NATIVE (self), NULL); return self->cancel_label; } /** * gtk_file_chooser_native_set_cancel_label: (attributes org.gtk.Method.set_property=cancel-label) * @self: a `GtkFileChooserNative` * @cancel_label: (nullable): custom label * * Sets the custom label text for the cancel button. * * If characters in @label are preceded by an underscore, they are * underlined. If you need a literal underscore character in a label, * use “__” (two underscores). The first underlined character represents * a keyboard accelerator called a mnemonic. * * Pressing Alt and that key should activate the button. */ void gtk_file_chooser_native_set_cancel_label (GtkFileChooserNative *self, const char *cancel_label) { g_return_if_fail (GTK_IS_FILE_CHOOSER_NATIVE (self)); g_free (self->cancel_label); self->cancel_label = g_strdup (cancel_label); g_object_notify_by_pspec (G_OBJECT (self), native_props[PROP_CANCEL_LABEL]); } static GtkFileChooserNativeChoice * find_choice (GtkFileChooserNative *self, const char *id) { GSList *l; for (l = self->choices; l; l = l->next) { GtkFileChooserNativeChoice *choice = l->data; if (strcmp (choice->id, id) == 0) return choice; } return NULL; } static void gtk_file_chooser_native_choice_free (GtkFileChooserNativeChoice *choice) { g_free (choice->id); g_free (choice->label); g_strfreev (choice->options); g_strfreev (choice->option_labels); g_free (choice->selected); g_free (choice); } static void gtk_file_chooser_native_add_choice (GtkFileChooser *chooser, const char *id, const char *label, const char **options, const char **option_labels) { GtkFileChooserNative *self = GTK_FILE_CHOOSER_NATIVE (chooser); GtkFileChooserNativeChoice *choice = find_choice (self, id); if (choice != NULL) { g_warning ("Choice with id %s already added to %s %p", id, G_OBJECT_TYPE_NAME (self), self); return; } g_assert ((options == NULL && option_labels == NULL) || g_strv_length ((char **)options) == g_strv_length ((char **)option_labels)); choice = g_new0 (GtkFileChooserNativeChoice, 1); choice->id = g_strdup (id); choice->label = g_strdup (label); choice->options = g_strdupv ((char **)options); choice->option_labels = g_strdupv ((char **)option_labels); self->choices = g_slist_append (self->choices, choice); gtk_file_chooser_add_choice (GTK_FILE_CHOOSER (self->dialog), id, label, options, option_labels); } static void gtk_file_chooser_native_remove_choice (GtkFileChooser *chooser, const char *id) { GtkFileChooserNative *self = GTK_FILE_CHOOSER_NATIVE (chooser); GtkFileChooserNativeChoice *choice = find_choice (self, id); if (choice == NULL) { g_warning ("No choice with id %s found in %s %p", id, G_OBJECT_TYPE_NAME (self), self); return; } self->choices = g_slist_remove (self->choices, choice); gtk_file_chooser_native_choice_free (choice); gtk_file_chooser_remove_choice (GTK_FILE_CHOOSER (self->dialog), id); } static void gtk_file_chooser_native_set_choice (GtkFileChooser *chooser, const char *id, const char *selected) { GtkFileChooserNative *self = GTK_FILE_CHOOSER_NATIVE (chooser); GtkFileChooserNativeChoice *choice = find_choice (self, id); if (choice == NULL) { g_warning ("No choice with id %s found in %s %p", id, G_OBJECT_TYPE_NAME (self), self); return; } if ((choice->options && !g_strv_contains ((const char *const*)choice->options, selected)) || (!choice->options && !g_str_equal (selected, "true") && !g_str_equal (selected, "false"))) { g_warning ("Not a valid option for %s: %s", id, selected); return; } g_free (choice->selected); choice->selected = g_strdup (selected); gtk_file_chooser_set_choice (GTK_FILE_CHOOSER (self->dialog), id, selected); } static const char * gtk_file_chooser_native_get_choice (GtkFileChooser *chooser, const char *id) { GtkFileChooserNative *self = GTK_FILE_CHOOSER_NATIVE (chooser); GtkFileChooserNativeChoice *choice = find_choice (self, id); if (choice == NULL) { g_warning ("No choice with id %s found in %s %p", id, G_OBJECT_TYPE_NAME (self), self); return NULL; } if (self->mode == MODE_FALLBACK) return gtk_file_chooser_get_choice (GTK_FILE_CHOOSER (self->dialog), id); return choice->selected; } static void gtk_file_chooser_native_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { GtkFileChooserNative *self = GTK_FILE_CHOOSER_NATIVE (object); switch (prop_id) { case PROP_ACCEPT_LABEL: gtk_file_chooser_native_set_accept_label (self, g_value_get_string (value)); break; case PROP_CANCEL_LABEL: gtk_file_chooser_native_set_cancel_label (self, g_value_get_string (value)); break; case GTK_FILE_CHOOSER_PROP_FILTER: self->current_filter = g_value_get_object (value); gtk_file_chooser_set_filter (GTK_FILE_CHOOSER (self->dialog), self->current_filter); g_object_notify (G_OBJECT (self), "filter"); break; default: g_object_set_property (G_OBJECT (self->dialog), pspec->name, value); break; } } static void gtk_file_chooser_native_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { GtkFileChooserNative *self = GTK_FILE_CHOOSER_NATIVE (object); switch (prop_id) { case PROP_ACCEPT_LABEL: g_value_set_string (value, self->accept_label); break; case PROP_CANCEL_LABEL: g_value_set_string (value, self->cancel_label); break; case GTK_FILE_CHOOSER_PROP_FILTER: self->current_filter = gtk_file_chooser_get_filter (GTK_FILE_CHOOSER (self->dialog)); g_value_set_object (value, self->current_filter); break; default: g_object_get_property (G_OBJECT (self->dialog), pspec->name, value); break; } } static void gtk_file_chooser_native_finalize (GObject *object) { GtkFileChooserNative *self = GTK_FILE_CHOOSER_NATIVE (object); g_clear_pointer (&self->current_name, g_free); g_clear_object (&self->current_file); g_clear_object (&self->current_folder); g_clear_pointer (&self->accept_label, g_free); g_clear_pointer (&self->cancel_label, g_free); gtk_window_destroy (GTK_WINDOW (self->dialog)); g_slist_free_full (self->custom_files, g_object_unref); g_slist_free_full (self->choices, (GDestroyNotify)gtk_file_chooser_native_choice_free); G_OBJECT_CLASS (gtk_file_chooser_native_parent_class)->finalize (object); } static void gtk_file_chooser_native_init (GtkFileChooserNative *self) { /* We always create a File chooser dialog and delegate all properties to it. * This way we can reuse that store, plus we always have a dialog we can use * in case something makes the native one not work (like the custom widgets) */ self->dialog = g_object_new (GTK_TYPE_FILE_CHOOSER_DIALOG, NULL); self->cancel_button = gtk_dialog_add_button (GTK_DIALOG (self->dialog), _("_Cancel"), GTK_RESPONSE_CANCEL); self->accept_button = gtk_dialog_add_button (GTK_DIALOG (self->dialog), _("_Open"), GTK_RESPONSE_ACCEPT); gtk_dialog_set_default_response (GTK_DIALOG (self->dialog), GTK_RESPONSE_ACCEPT); gtk_window_set_hide_on_close (GTK_WINDOW (self->dialog), TRUE); /* This is used, instead of the standard delegate, to ensure that signals are not delegated. */ g_object_set_qdata (G_OBJECT (self), GTK_FILE_CHOOSER_DELEGATE_QUARK, self->dialog); } /** * gtk_file_chooser_native_new: * @title: (nullable): Title of the native * @parent: (nullable): Transient parent of the native * @action: Open or save mode for the dialog * @accept_label: (nullable): text to go in the accept button, or %NULL for the default * @cancel_label: (nullable): text to go in the cancel button, or %NULL for the default * * Creates a new `GtkFileChooserNative`. * * Returns: a new `GtkFileChooserNative` */ GtkFileChooserNative * gtk_file_chooser_native_new (const char *title, GtkWindow *parent, GtkFileChooserAction action, const char *accept_label, const char *cancel_label) { GtkFileChooserNative *result; result = g_object_new (GTK_TYPE_FILE_CHOOSER_NATIVE, "title", title, "action", action, "transient-for", parent, "accept-label", accept_label, "cancel-label", cancel_label, NULL); return result; } static void dialog_response_cb (GtkDialog *dialog, int response_id, gpointer data) { GtkFileChooserNative *self = data; g_signal_handlers_disconnect_by_func (self->dialog, dialog_response_cb, self); gtk_widget_hide (self->dialog); _gtk_native_dialog_emit_response (GTK_NATIVE_DIALOG (self), response_id); } static void show_dialog (GtkFileChooserNative *self) { GtkFileChooserAction action; const char *accept_label, *cancel_label; action = gtk_file_chooser_get_action (GTK_FILE_CHOOSER (self->dialog)); accept_label = self->accept_label; if (accept_label == NULL) accept_label = (action == GTK_FILE_CHOOSER_ACTION_SAVE) ? _("_Save") : _("_Open"); gtk_button_set_label (GTK_BUTTON (self->accept_button), accept_label); cancel_label = self->cancel_label; if (cancel_label == NULL) cancel_label = _("_Cancel"); gtk_button_set_label (GTK_BUTTON (self->cancel_button), cancel_label); gtk_window_set_title (GTK_WINDOW (self->dialog), gtk_native_dialog_get_title (GTK_NATIVE_DIALOG (self))); gtk_window_set_transient_for (GTK_WINDOW (self->dialog), gtk_native_dialog_get_transient_for (GTK_NATIVE_DIALOG (self))); gtk_window_set_modal (GTK_WINDOW (self->dialog), gtk_native_dialog_get_modal (GTK_NATIVE_DIALOG (self))); g_signal_connect (self->dialog, "response", G_CALLBACK (dialog_response_cb), self); gtk_window_present (GTK_WINDOW (self->dialog)); } static void hide_dialog (GtkFileChooserNative *self) { g_signal_handlers_disconnect_by_func (self->dialog, dialog_response_cb, self); gtk_widget_hide (self->dialog); } static gboolean gtk_file_chooser_native_set_current_folder (GtkFileChooser *chooser, GFile *file, GError **error) { GtkFileChooserNative *self = GTK_FILE_CHOOSER_NATIVE (chooser); gboolean res; res = gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (self->dialog), file, error); if (res) { g_set_object (&self->current_folder, file); g_clear_object (&self->current_file); } return res; } static gboolean gtk_file_chooser_native_select_file (GtkFileChooser *chooser, GFile *file, GError **error) { GtkFileChooserNative *self = GTK_FILE_CHOOSER_NATIVE (chooser); gboolean res; res = gtk_file_chooser_select_file (GTK_FILE_CHOOSER (self->dialog), file, error); if (res) { g_set_object (&self->current_file, file); g_clear_object (&self->current_folder); g_clear_pointer (&self->current_name, g_free); } return res; } static void gtk_file_chooser_native_set_current_name (GtkFileChooser *chooser, const char *name) { GtkFileChooserNative *self = GTK_FILE_CHOOSER_NATIVE (chooser); gtk_file_chooser_set_current_name (GTK_FILE_CHOOSER (self->dialog), name); g_clear_pointer (&self->current_name, g_free); self->current_name = g_strdup (name); g_clear_object (&self->current_file); } static GListModel * gtk_file_chooser_native_get_files (GtkFileChooser *chooser) { GtkFileChooserNative *self = GTK_FILE_CHOOSER_NATIVE (chooser); switch (self->mode) { case MODE_PORTAL: case MODE_WIN32: case MODE_QUARTZ: { GListStore *store; GSList *l; store = g_list_store_new (G_TYPE_FILE); for (l = self->custom_files; l; l = l->next) g_list_store_append (store, l->data); return G_LIST_MODEL (store); } case MODE_FALLBACK: default: return gtk_file_chooser_get_files (GTK_FILE_CHOOSER (self->dialog)); } } static void portal_error_handler (GtkFileChooserNative *self) { self->mode = MODE_FALLBACK; show_dialog (self); } static void gtk_file_chooser_native_show (GtkNativeDialog *native) { GtkFileChooserNative *self = GTK_FILE_CHOOSER_NATIVE (native); self->mode = MODE_FALLBACK; #ifdef GDK_WINDOWING_WIN32 if (gtk_file_chooser_native_win32_show (self)) self->mode = MODE_WIN32; #endif #ifdef GDK_WINDOWING_MACOS if (gtk_file_chooser_native_quartz_show (self)) self->mode = MODE_QUARTZ; #endif if (self->mode == MODE_FALLBACK && gtk_file_chooser_native_portal_show (self, portal_error_handler)) self->mode = MODE_PORTAL; if (self->mode == MODE_FALLBACK) show_dialog (self); } static void gtk_file_chooser_native_hide (GtkNativeDialog *native) { GtkFileChooserNative *self = GTK_FILE_CHOOSER_NATIVE (native); switch (self->mode) { case MODE_FALLBACK: hide_dialog (self); break; case MODE_WIN32: #ifdef GDK_WINDOWING_WIN32 gtk_file_chooser_native_win32_hide (self); #endif break; case MODE_QUARTZ: #ifdef GDK_WINDOWING_MACOS gtk_file_chooser_native_quartz_hide (self); #endif break; case MODE_PORTAL: gtk_file_chooser_native_portal_hide (self); break; default: break; } } static void gtk_file_chooser_native_class_init (GtkFileChooserNativeClass *class) { GObjectClass *gobject_class = G_OBJECT_CLASS (class); GtkNativeDialogClass *native_dialog_class = GTK_NATIVE_DIALOG_CLASS (class); gobject_class->finalize = gtk_file_chooser_native_finalize; gobject_class->set_property = gtk_file_chooser_native_set_property; gobject_class->get_property = gtk_file_chooser_native_get_property; native_dialog_class->show = gtk_file_chooser_native_show; native_dialog_class->hide = gtk_file_chooser_native_hide; _gtk_file_chooser_install_properties (gobject_class); /** * GtkFileChooserNative:accept-label: (attributes org.gtk.Property.get=gtk_file_chooser_native_get_accept_label org.gtk.Property.set=gtk_file_chooser_native_set_accept_label) * * The text used for the label on the accept button in the dialog, or * %NULL to use the default text. */ native_props[PROP_ACCEPT_LABEL] = g_param_spec_string ("accept-label", NULL, NULL, NULL, GTK_PARAM_READWRITE); /** * GtkFileChooserNative:cancel-label: (attributes org.gtk.Property.get=gtk_file_chooser_native_get_cancel_label org.gtk.Property.set=gtk_file_chooser_native_set_cancel_label) * * The text used for the label on the cancel button in the dialog, or * %NULL to use the default text. */ native_props[PROP_CANCEL_LABEL] = g_param_spec_string ("cancel-label", NULL, NULL, NULL, GTK_PARAM_READWRITE); g_object_class_install_properties (gobject_class, LAST_ARG, native_props); } static void _gtk_file_chooser_native_iface_init (GtkFileChooserIface *iface) { _gtk_file_chooser_delegate_iface_init (iface); iface->select_file = gtk_file_chooser_native_select_file; iface->set_current_name = gtk_file_chooser_native_set_current_name; iface->set_current_folder = gtk_file_chooser_native_set_current_folder; iface->get_files = gtk_file_chooser_native_get_files; iface->add_choice = gtk_file_chooser_native_add_choice; iface->remove_choice = gtk_file_chooser_native_remove_choice; iface->set_choice = gtk_file_chooser_native_set_choice; iface->get_choice = gtk_file_chooser_native_get_choice; }