/* * Copyright (C) 2024 GNOME Foundation Inc. * * SPDX-License-Identifier: GPL-3.0-or-later * * Original Author: António Fernandes */ #define G_LOG_DOMAIN "nautilus-dbus" #include "nautilus-portal.h" #include #include #include #include #include #include "nautilus-file-chooser.h" #include "nautilus-file-utilities.h" #include "nautilus-global-preferences.h" #include "nautilus-portal-request.h" #define DESKTOP_PORTAL_OBJECT_PATH "/org/freedesktop/portal/desktop" /* https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html#org-freedesktop-portal-request-response */ #define RESPONSE_SUCCESS 0 #define RESPONSE_USER_CANCELLED 1 #define RESPONSE_OTHER 2 struct _NautilusPortal { GObject parent; XdpImplFileChooser *impl_file_chooser_skeleton; }; G_DEFINE_FINAL_TYPE (NautilusPortal, nautilus_portal, G_TYPE_OBJECT); typedef struct { NautilusPortal *self; GDBusMethodInvocation *invocation; NautilusPortalRequest *request; GxdpExternalWindow *external_parent; GtkWindow *window; GActionGroup *choices_action_group; GHashTable *action_to_choice_list_map; char **filenames_to_save; } FileChooserData; static void file_chooser_data_free (gpointer data) { FileChooserData *fc_data = data; if (fc_data->request != NULL) { g_signal_handlers_disconnect_by_data (fc_data->request, fc_data); g_clear_object (&fc_data->request); } g_clear_object (&fc_data->external_parent); if (fc_data->window != NULL) { g_signal_handlers_disconnect_by_data (fc_data->window, fc_data); g_clear_object (&fc_data->window); } g_clear_pointer (&fc_data->filenames_to_save, g_strfreev); g_clear_object (&fc_data->choices_action_group); g_clear_pointer (&fc_data->action_to_choice_list_map, g_hash_table_destroy); g_free (fc_data); } G_DEFINE_AUTOPTR_CLEANUP_FUNC (FileChooserData, file_chooser_data_free) static void save_window_size (GtkWindow *window) { int width, height; gtk_window_get_default_size (window, &width, &height); g_settings_set (nautilus_window_state, NAUTILUS_WINDOW_STATE_INITIAL_SIZE_FILE_CHOOSER, "(ii)", width, height); } static void complete_file_chooser (FileChooserData *data, int response, GVariantBuilder *results) { const char *method_name = g_dbus_method_invocation_get_method_name (data->invocation); if (strcmp (method_name, "OpenFile") == 0) { xdp_impl_file_chooser_complete_open_file (data->self->impl_file_chooser_skeleton, data->invocation, response, g_variant_builder_end (results)); } else if (strcmp (method_name, "SaveFile") == 0) { xdp_impl_file_chooser_complete_save_file (data->self->impl_file_chooser_skeleton, data->invocation, response, g_variant_builder_end (results)); } else if (strcmp (method_name, "SaveFiles") == 0) { xdp_impl_file_chooser_complete_save_files (data->self->impl_file_chooser_skeleton, data->invocation, response, g_variant_builder_end (results)); } else { g_assert_not_reached (); } nautilus_portal_request_unexport (data->request); save_window_size (data->window); gtk_window_destroy (data->window); g_application_release (g_application_get_default ()); } static void build_uris_variant (GVariantBuilder *uris, GList *locations) { for (GList *l = locations; l != NULL; l = l->next) { GFile *location = G_FILE (l->data); const char *path = g_file_peek_path (location); g_autofree char *file_uri = NULL; /* Backends must normalize URIs of locations selected by the user into * “file://” URIs. URIs that cannot be normalized should be discarded. */ if (g_file_has_uri_scheme (location, "file")) { file_uri = g_file_get_uri (location); } else if (path != NULL) { file_uri = g_filename_to_uri (path, NULL, NULL); } if (file_uri != NULL) { g_variant_builder_add_value (uris, g_variant_new_take_string (g_steal_pointer (&file_uri))); } } } typedef struct { char *choice_id; GPtrArray *choices_strings; } FileChooserChoiceList; static void choice_list_free (FileChooserChoiceList *chooser_choice_list) { g_clear_pointer (&chooser_choice_list->choice_id, g_free); g_clear_pointer (&chooser_choice_list->choices_strings, g_ptr_array_unref); g_free (chooser_choice_list); } static FileChooserChoiceList * choice_list_new (const gchar *choice_id) { FileChooserChoiceList *chooser_choice_list = g_new0 (FileChooserChoiceList, 1); chooser_choice_list->choice_id = g_strdup (choice_id); chooser_choice_list->choices_strings = g_ptr_array_new_with_free_func (g_free); return chooser_choice_list; } static GVariant * get_choices (FileChooserData *data, gboolean *writable) { g_auto (GStrv) actions = g_action_group_list_actions (data->choices_action_group); g_auto (GVariantBuilder) builder = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("a(ss)")); for (guint i = 0; actions[i] != NULL; i++) { GAction *action = g_action_map_lookup_action (G_ACTION_MAP (data->choices_action_group), actions[i]); const char *action_name = g_action_get_name (action); const GVariantType *state_type = g_action_get_state_type (action); FileChooserChoiceList *choice_list = g_hash_table_lookup (data->action_to_choice_list_map, action_name); g_autoptr (GVariant) state = g_action_get_state (action); if (g_strcmp0 (actions[i], "x-nautilus-is-read-only") == 0) { *writable = !g_variant_get_boolean (state); } else if (g_strcmp0 (g_variant_type_peek_string (state_type), "b") == 0) { g_assert (choice_list != NULL); char *value = g_variant_get_boolean (state) ? "true" : "false"; g_variant_builder_add (&builder, "(ss)", choice_list->choice_id, value); } else if (g_strcmp0 (g_variant_type_peek_string (state_type), "u") == 0) { g_assert (choice_list != NULL); guint32 picked_choice_index = g_variant_get_uint32 (state); const gchar *picked_choice = choice_list->choices_strings->pdata[picked_choice_index]; g_variant_builder_add (&builder, "(ss)", choice_list->choice_id, picked_choice); } else { g_assert_not_reached (); } } return g_variant_builder_end (&builder); } static gboolean on_file_chooser_accepted (gpointer user_data, GList *locations, GtkFileFilter *current_filter) { g_autoptr (FileChooserData) data = (FileChooserData *) user_data; g_auto (GVariantBuilder) results = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_VARDICT); g_auto (GVariantBuilder) uris = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_STRING_ARRAY); const char *method_name = g_dbus_method_invocation_get_method_name (data->invocation); gboolean writable = FALSE; GVariant *choices = get_choices (data, &writable); if (strcmp (method_name, "SaveFiles") == 0 && data->filenames_to_save != NULL) { GFile *directory = G_FILE (locations->data); g_autolist (GFile) unique_locations = NULL; for (gsize i = 0; data->filenames_to_save[i] != NULL; i++) { g_autoptr (GFile) location = NULL; location = nautilus_generate_unique_file_in_directory (directory, data->filenames_to_save[i], FALSE); unique_locations = g_list_prepend (unique_locations, g_steal_pointer (&location)); } build_uris_variant (&uris, unique_locations); } else { build_uris_variant (&uris, locations); } /* TODO: add to recents */ if (current_filter != NULL) { g_variant_builder_add (&results, "{sv}", "current_filter", gtk_file_filter_to_gvariant (current_filter)); } g_variant_builder_add (&results, "{sv}", "uris", g_variant_builder_end (&uris)); g_variant_builder_add (&results, "{sv}", "choices", choices); g_variant_builder_add (&results, "{sv}", "writable", g_variant_new_boolean (writable)); complete_file_chooser (data, RESPONSE_SUCCESS, &results); return TRUE; } static gboolean on_window_close_request (gpointer user_data) { g_autoptr (FileChooserData) data = (FileChooserData *) user_data; g_auto (GVariantBuilder) results = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_VARDICT); complete_file_chooser (data, RESPONSE_USER_CANCELLED, &results); return TRUE; } static gboolean handle_close (XdpImplRequest *request, GDBusMethodInvocation *invocation, gpointer user_data) { g_autoptr (FileChooserData) data = (FileChooserData *) user_data; g_auto (GVariantBuilder) results = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_VARDICT); complete_file_chooser (data, RESPONSE_OTHER, &results); xdp_impl_request_complete_close (request, invocation); return G_DBUS_METHOD_INVOCATION_HANDLED; } static GActionGroup * create_choices_action_group (void) { GSimpleActionGroup *action_group = g_simple_action_group_new (); g_autoptr (GSimpleAction) action = NULL; action = g_simple_action_new_stateful ("x-nautilus-is-read-only", NULL, g_variant_new_boolean (FALSE)); g_action_map_add_action (G_ACTION_MAP (action_group), G_ACTION (action)); g_simple_action_set_enabled (action, TRUE); return G_ACTION_GROUP (action_group); } static GMenu * get_menu_from_choices (GVariant *arg_options, FileChooserData *data) { GMenu *menu = g_menu_new (); g_autoptr (GVariantIter) iter = NULL; GVariant *choices_list; guint i = 0; const char *choice_id; const char *label; const char *selected; char *option_id; char *option_label; data->choices_action_group = create_choices_action_group (); data->action_to_choice_list_map = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) choice_list_free); if (!g_variant_lookup (arg_options, "choices", "a(ssa(ss)s)", &iter)) { return menu; } while (g_variant_iter_next (iter, "(&s&s@a(ss)&s)", &choice_id, &label, &choices_list, &selected)) { g_autoptr (GSimpleAction) action = NULL; /* The action name has a number corresponding to the n-th multi choice * list. */ g_autofree char *action_name = g_strdup_printf ("choice_%u", i++); FileChooserChoiceList *chooser_choice_list = choice_list_new (choice_id); if (g_variant_n_children (choices_list) > 0) { g_autoptr (GMenu) submenu = g_menu_new (); g_autoptr (GMenuItem) item = g_menu_item_new_submenu (label, G_MENU_MODEL (submenu)); guint32 j = 0; g_menu_append_item (menu, item); /* Action state is used to store an index to a string in the choice * list. */ action = g_simple_action_new_stateful (action_name, G_VARIANT_TYPE_UINT32, g_variant_new_uint32 (0)); g_action_map_add_action (G_ACTION_MAP (data->choices_action_group), G_ACTION (action)); g_simple_action_set_enabled (action, TRUE); GVariantIter list_iter; g_variant_iter_init (&list_iter, choices_list); while (g_variant_iter_next (&list_iter, "(ss)", &option_id, &option_label)) { /* The "(uint32 %u)" indicates that when this action is * activated, it will set the state of the action to an index * corresponding to choice string. */ g_autofree char *detailed_action_name = g_strdup_printf ("choices.%s(uint32 %u)", action_name, j); g_menu_append (submenu, option_label, detailed_action_name); g_ptr_array_add (chooser_choice_list->choices_strings, g_strdup (option_id)); if (g_strcmp0 (option_id, selected) == 0) { g_simple_action_set_state (G_SIMPLE_ACTION (action), g_variant_new_uint32 (j)); } g_free (option_id); g_free (option_label); j++; } g_hash_table_insert (data->action_to_choice_list_map, g_strdup (action_name), g_steal_pointer (&chooser_choice_list)); } else { /* When this action is activated, it will toggle the state of the * action between true and false. */ g_autofree char *detailed_action_name = g_strdup_printf ("choices.%s", action_name); GVariant *state = g_variant_new_boolean (g_strcmp0 (selected, "true") == 0); /* Action state is used to store a boolean for the choice. */ action = g_simple_action_new_stateful (action_name, NULL, state); g_action_map_add_action (G_ACTION_MAP (data->choices_action_group), G_ACTION (action)); g_simple_action_set_enabled (action, TRUE); g_menu_append (menu, label, detailed_action_name); g_hash_table_insert (data->action_to_choice_list_map, g_strdup (action_name), g_steal_pointer (&chooser_choice_list)); } g_variant_unref (choices_list); } return menu; } static gboolean handle_file_chooser_methods (XdpImplFileChooser *object, GDBusMethodInvocation *invocation, const char *arg_handle, const char *arg_app_id, const char *arg_parent_window, const char *arg_title, GVariant *arg_options, gpointer user_data) { NautilusPortal *self = NAUTILUS_PORTAL (user_data); const char *method_name = g_dbus_method_invocation_get_method_name (invocation); GApplication *app = g_application_get_default (); g_application_hold (app); /* Decide mode */ NautilusMode mode = NAUTILUS_MODE_BROWSE; gboolean open_multiple = FALSE; gboolean open_directory = FALSE; if (strcmp (method_name, "OpenFile") == 0) { (void) g_variant_lookup (arg_options, "multiple", "b", &open_multiple); (void) g_variant_lookup (arg_options, "directory", "b", &open_directory); mode = (open_directory ? (open_multiple ? NAUTILUS_MODE_OPEN_FOLDERS : NAUTILUS_MODE_OPEN_FOLDER) : (open_multiple ? NAUTILUS_MODE_OPEN_FILES : NAUTILUS_MODE_OPEN_FILE)); } else if (strcmp (method_name, "SaveFile") == 0) { mode = NAUTILUS_MODE_SAVE_FILE; } else if (strcmp (method_name, "SaveFiles") == 0) { mode = NAUTILUS_MODE_SAVE_FILES; } else { g_return_val_if_reached (G_DBUS_METHOD_INVOCATION_UNHANDLED); } g_assert (mode != NAUTILUS_MODE_BROWSE); /* Define label */ const char *accept_label; if (!g_variant_lookup (arg_options, "accept_label", "&s", &accept_label)) { if (strcmp (method_name, "OpenFile") == 0) { accept_label = open_multiple ? _("_Open") : _("_Select"); } else { accept_label = _("_Save"); } } /* Define starting location (and name, for SAVE_FILE mode)*/ const char *path; g_autofree char *suggested_filename = NULL; g_autoptr (GFile) starting_location = NULL; FileChooserData *data = g_new0 (FileChooserData, 1); data->self = self; data->invocation = invocation; if (mode == NAUTILUS_MODE_SAVE_FILE) { if (g_variant_lookup (arg_options, "current_file", "^&ay", &path)) { g_autoptr (GFile) file = g_file_new_for_path (path); suggested_filename = g_file_get_basename (file); starting_location = g_file_get_parent (file); } else { (void) g_variant_lookup (arg_options, "current_name", "s", &suggested_filename); } } else if (mode == NAUTILUS_MODE_SAVE_FILES) { (void) g_variant_lookup (arg_options, "files", "^aay", &data->filenames_to_save); } if (starting_location == NULL) { if (g_variant_lookup (arg_options, "current_folder", "^&ay", &path) && path[0] != '\0') { starting_location = g_file_new_for_path (path); } } /* Define filters */ g_autoptr (GVariant) current_filter = NULL; g_autoptr (GVariantIter) filters_iter = NULL; g_autoptr (GListStore) filters = g_list_store_new (GTK_TYPE_FILE_FILTER); guint current_filter_position = GTK_INVALID_LIST_POSITION; (void) g_variant_lookup (arg_options, "current_filter", "@(sa(us))", ¤t_filter); if (g_variant_lookup (arg_options, "filters", "a(sa(us))", &filters_iter)) { GVariant *variant; guint position = 0; while (g_variant_iter_next (filters_iter, "@(sa(us))", &variant)) { g_autoptr (GtkFileFilter) filter = gtk_file_filter_new_from_gvariant (variant); g_list_store_append (filters, filter); if (current_filter != NULL && g_variant_equal (variant, current_filter)) { current_filter_position = position; } g_variant_unref (variant); position++; } } if (current_filter != NULL && g_list_model_get_n_items (G_LIST_MODEL (filters)) == 0) { g_autoptr (GtkFileFilter) filter = gtk_file_filter_new_from_gvariant (current_filter); /* We are setting a single, unchangeable filter. */ g_list_store_append (filters, filter); current_filter_position = 0; } /* Get choices */ g_autoptr (GMenu) menu = get_menu_from_choices (arg_options, data); if (g_strcmp0 (method_name, "OpenFile") == 0) { g_menu_prepend (menu, _("Open Read-Only"), "choices.x-nautilus-is-read-only"); } /* Prepare window */ NautilusFileChooser *window = nautilus_file_chooser_new (mode); g_set_object (&data->window, GTK_WINDOW (window)); nautilus_file_chooser_set_accept_label (window, accept_label); nautilus_file_chooser_set_filters (window, G_LIST_MODEL (filters)); nautilus_file_chooser_set_current_filter (window, current_filter_position); nautilus_file_chooser_set_starting_location (window, starting_location); nautilus_file_chooser_set_suggested_name (window, suggested_filename); nautilus_file_chooser_add_choices (window, data->choices_action_group, G_MENU_MODEL (menu)); const char *title; title = (arg_title[0] == '\0') ? _("Files") : arg_title; gtk_window_set_title (GTK_WINDOW (window), title); g_signal_connect_swapped (window, "close-request", G_CALLBACK (on_window_close_request), data); g_signal_connect_swapped (window, "accepted", G_CALLBACK (on_file_chooser_accepted), data); /* Show window */ if (arg_parent_window != NULL && *arg_parent_window != '\0') { data->external_parent = gxdp_external_window_new_from_handle (arg_parent_window); if (data->external_parent == NULL) { g_warning ("Failed to associate portal window with parent window %s", arg_parent_window); } else { gtk_widget_realize (GTK_WIDGET (window)); GdkSurface *surface = gtk_native_get_surface (GTK_NATIVE (window)); gboolean modal = TRUE; (void) g_variant_lookup (arg_options, "modal", "b", &modal); gxdp_external_window_set_parent_of (data->external_parent, surface); gtk_window_set_modal (data->window, modal); } } gtk_application_add_window (GTK_APPLICATION (app), GTK_WINDOW (window)); gtk_window_present (data->window); /* Setup request. */ data->request = nautilus_portal_request_new (g_dbus_method_invocation_get_sender (invocation), arg_app_id, arg_handle); g_signal_connect (data->request, "handle-close", G_CALLBACK (handle_close), data); nautilus_portal_request_export (data->request, g_dbus_method_invocation_get_connection (invocation)); return G_DBUS_METHOD_INVOCATION_HANDLED; } static void nautilus_portal_dispose (GObject *object) { NautilusPortal *self = (NautilusPortal *) object; if (self->impl_file_chooser_skeleton != NULL) { g_signal_handlers_disconnect_by_data (self->impl_file_chooser_skeleton, self); g_clear_object (&self->impl_file_chooser_skeleton); } G_OBJECT_CLASS (nautilus_portal_parent_class)->dispose (object); } static void nautilus_portal_class_init (NautilusPortalClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->dispose = nautilus_portal_dispose; } static void nautilus_portal_init (NautilusPortal *self) { self->impl_file_chooser_skeleton = xdp_impl_file_chooser_skeleton_new (); } NautilusPortal * nautilus_portal_new (void) { return g_object_new (NAUTILUS_TYPE_PORTAL, NULL); } gboolean nautilus_portal_register (NautilusPortal *self, GDBusConnection *connection, GError **error) { if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self->impl_file_chooser_skeleton), connection, DESKTOP_PORTAL_OBJECT_PATH, error)) { return FALSE; } g_signal_connect (self->impl_file_chooser_skeleton, "handle-open-file", G_CALLBACK (handle_file_chooser_methods), self); g_signal_connect (self->impl_file_chooser_skeleton, "handle-save-file", G_CALLBACK (handle_file_chooser_methods), self); g_signal_connect (self->impl_file_chooser_skeleton, "handle-save-files", G_CALLBACK (handle_file_chooser_methods), self); return TRUE; } void nautilus_portal_unregister (NautilusPortal *self) { g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self->impl_file_chooser_skeleton)); }