forked from AuroraMiddleware/gtk
196ec107d1
Even if the FileChooserNative instance drops out on us while we're still waiting for the portal to answer, we should keep the data and pointers alive until the sequence of asynchronous operations is running. The code already tries to do that, by acquiring a strong reference to the GtkFileChooserNative instance, but it's also freeing data as soon as the dialog is hidden, while asynchronous callbacks that will look at the fields on that data are still in flight. To avoid that, we defer freeing the data until the asynchronous callbacks are invoked, and we keep a reference on the dialog while we're emitting signals on it. Fixes: #4883
555 lines
18 KiB
C
555 lines
18 KiB
C
/* -*- Mode: C; c-file-style: "gnu"; tab-width: 8 -*- */
|
|
/* GTK - The GIMP Toolkit
|
|
* gtkfilechoosernativeportal.c: Portal 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#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 "gtkmain.h"
|
|
#include "gtkfilefilterprivate.h"
|
|
#include "gtkwindowprivate.h"
|
|
|
|
|
|
typedef struct {
|
|
GtkFileChooserNative *self;
|
|
|
|
GtkWidget *grab_widget;
|
|
|
|
GDBusConnection *connection;
|
|
char *portal_handle;
|
|
guint portal_response_signal_id;
|
|
gboolean modal;
|
|
|
|
gboolean hidden;
|
|
|
|
const char *method_name;
|
|
|
|
GtkWindow *exported_window;
|
|
PortalErrorHandler error_handler;
|
|
} FilechooserPortalData;
|
|
|
|
|
|
static void
|
|
filechooser_portal_data_clear (FilechooserPortalData *data)
|
|
{
|
|
if (data->portal_response_signal_id != 0)
|
|
{
|
|
g_dbus_connection_signal_unsubscribe (data->connection,
|
|
data->portal_response_signal_id);
|
|
data->portal_response_signal_id = 0;
|
|
}
|
|
|
|
g_clear_object (&data->connection);
|
|
|
|
if (data->grab_widget)
|
|
{
|
|
gtk_grab_remove (data->grab_widget);
|
|
g_clear_object (&data->grab_widget);
|
|
}
|
|
|
|
g_clear_object (&data->self);
|
|
|
|
if (data->exported_window)
|
|
{
|
|
gtk_window_unexport_handle (data->exported_window);
|
|
g_clear_object (&data->exported_window);
|
|
}
|
|
|
|
g_clear_pointer (&data->portal_handle, g_free);
|
|
}
|
|
|
|
static void
|
|
filechooser_portal_data_free (FilechooserPortalData *data)
|
|
{
|
|
if (data != NULL)
|
|
{
|
|
filechooser_portal_data_clear (data);
|
|
g_free (data);
|
|
}
|
|
}
|
|
|
|
static void
|
|
response_cb (GDBusConnection *connection,
|
|
const char *sender_name,
|
|
const char *object_path,
|
|
const char *interface_name,
|
|
const char *signal_name,
|
|
GVariant *parameters,
|
|
gpointer user_data)
|
|
{
|
|
GtkFileChooserNative *self = user_data;
|
|
FilechooserPortalData *data = self->mode_data;
|
|
guint32 portal_response;
|
|
int gtk_response;
|
|
const char **uris;
|
|
int i;
|
|
GVariant *response_data;
|
|
GVariant *choices = NULL;
|
|
GVariant *current_filter = NULL;
|
|
|
|
g_variant_get (parameters, "(u@a{sv})", &portal_response, &response_data);
|
|
g_variant_lookup (response_data, "uris", "^a&s", &uris);
|
|
|
|
choices = g_variant_lookup_value (response_data, "choices", G_VARIANT_TYPE ("a(ss)"));
|
|
if (choices)
|
|
{
|
|
for (i = 0; i < g_variant_n_children (choices); i++)
|
|
{
|
|
const char *id;
|
|
const char *selected;
|
|
g_variant_get_child (choices, i, "(&s&s)", &id, &selected);
|
|
gtk_file_chooser_set_choice (GTK_FILE_CHOOSER (self), id, selected);
|
|
}
|
|
g_variant_unref (choices);
|
|
}
|
|
|
|
current_filter = g_variant_lookup_value (response_data, "current_filter", G_VARIANT_TYPE ("(sa(us))"));
|
|
if (current_filter)
|
|
{
|
|
GtkFileFilter *filter = gtk_file_filter_new_from_gvariant (current_filter);
|
|
const char *current_filter_name = gtk_file_filter_get_name (filter);
|
|
|
|
/* Try to find the given filter in the list of filters.
|
|
* Since filters are compared by pointer value, using the passed
|
|
* filter would otherwise not match in a comparison, even if
|
|
* a filter in the list of filters has been selected.
|
|
* We'll use the heuristic that if two filters have the same name,
|
|
* they must be the same.
|
|
* If there is no match, just set the filter as it was retrieved.
|
|
*/
|
|
GtkFileFilter *filter_to_select = filter;
|
|
GListModel *filters;
|
|
guint j, n;
|
|
|
|
filters = gtk_file_chooser_get_filters (GTK_FILE_CHOOSER (self));
|
|
n = g_list_model_get_n_items (filters);
|
|
for (j = 0; j < n; j++)
|
|
{
|
|
GtkFileFilter *f = g_list_model_get_item (filters, j);
|
|
if (g_strcmp0 (gtk_file_filter_get_name (f), current_filter_name) == 0)
|
|
{
|
|
filter_to_select = f;
|
|
break;
|
|
}
|
|
g_object_unref (f);
|
|
}
|
|
g_object_unref (filters);
|
|
gtk_file_chooser_set_filter (GTK_FILE_CHOOSER (self), filter_to_select);
|
|
g_object_unref (filter_to_select);
|
|
}
|
|
|
|
g_slist_free_full (self->custom_files, g_object_unref);
|
|
self->custom_files = NULL;
|
|
for (i = 0; uris[i]; i++)
|
|
self->custom_files = g_slist_prepend (self->custom_files, g_file_new_for_uri (uris[i]));
|
|
|
|
switch (portal_response)
|
|
{
|
|
case 0:
|
|
gtk_response = GTK_RESPONSE_ACCEPT;
|
|
break;
|
|
case 1:
|
|
gtk_response = GTK_RESPONSE_CANCEL;
|
|
break;
|
|
case 2:
|
|
default:
|
|
gtk_response = GTK_RESPONSE_DELETE_EVENT;
|
|
break;
|
|
}
|
|
|
|
/* Keep a reference on the native dialog until we can emit the response
|
|
* signal; filechooser_portal_data_free() will drop a reference on the
|
|
* dialog as well
|
|
*/
|
|
g_object_ref (self);
|
|
|
|
filechooser_portal_data_free (data);
|
|
self->mode_data = NULL;
|
|
|
|
_gtk_native_dialog_emit_response (GTK_NATIVE_DIALOG (self), gtk_response);
|
|
|
|
g_object_unref (self);
|
|
}
|
|
|
|
static void
|
|
send_close (FilechooserPortalData *data)
|
|
{
|
|
GDBusMessage *message;
|
|
GError *error = NULL;
|
|
|
|
message = g_dbus_message_new_method_call (PORTAL_BUS_NAME,
|
|
PORTAL_OBJECT_PATH,
|
|
PORTAL_FILECHOOSER_INTERFACE,
|
|
"Close");
|
|
g_dbus_message_set_body (message,
|
|
g_variant_new ("(o)", data->portal_handle));
|
|
|
|
if (!g_dbus_connection_send_message (data->connection,
|
|
message,
|
|
G_DBUS_SEND_MESSAGE_FLAGS_NONE,
|
|
NULL, &error))
|
|
{
|
|
g_warning ("unable to send FileChooser Close message: %s", error->message);
|
|
g_error_free (error);
|
|
}
|
|
|
|
g_object_unref (message);
|
|
}
|
|
|
|
static void
|
|
open_file_msg_cb (GObject *source_object,
|
|
GAsyncResult *res,
|
|
gpointer user_data)
|
|
{
|
|
FilechooserPortalData *data = user_data;
|
|
GtkFileChooserNative *self = data->self;
|
|
GDBusMessage *reply;
|
|
GError *error = NULL;
|
|
char *handle = NULL;
|
|
|
|
reply = g_dbus_connection_send_message_with_reply_finish (data->connection, res, &error);
|
|
|
|
if (reply && g_dbus_message_to_gerror (reply, &error))
|
|
g_clear_object (&reply);
|
|
|
|
if (reply == NULL)
|
|
{
|
|
if (!data->hidden && data->error_handler)
|
|
{
|
|
data->error_handler (self);
|
|
filechooser_portal_data_free (data);
|
|
self->mode_data = NULL;
|
|
}
|
|
g_error_free (error);
|
|
return;
|
|
}
|
|
|
|
g_variant_get_child (g_dbus_message_get_body (reply), 0, "o", &handle);
|
|
|
|
if (data->hidden)
|
|
{
|
|
/* The dialog was hidden before we got the handle, close it now */
|
|
filechooser_portal_data_free (data);
|
|
self->mode_data = NULL;
|
|
}
|
|
else if (strcmp (handle, data->portal_handle) != 0)
|
|
{
|
|
g_free (data->portal_handle);
|
|
data->portal_handle = g_steal_pointer (&handle);
|
|
g_dbus_connection_signal_unsubscribe (data->connection,
|
|
data->portal_response_signal_id);
|
|
|
|
data->portal_response_signal_id =
|
|
g_dbus_connection_signal_subscribe (data->connection,
|
|
PORTAL_BUS_NAME,
|
|
PORTAL_REQUEST_INTERFACE,
|
|
"Response",
|
|
data->portal_handle,
|
|
NULL,
|
|
G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
|
|
response_cb,
|
|
self, NULL);
|
|
}
|
|
|
|
g_object_unref (reply);
|
|
g_free (handle);
|
|
}
|
|
|
|
static GVariant *
|
|
get_filters (GtkFileChooser *self)
|
|
{
|
|
GListModel *filters;
|
|
guint n, i;
|
|
GVariantBuilder builder;
|
|
|
|
g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(sa(us))"));
|
|
filters = gtk_file_chooser_get_filters (self);
|
|
n = g_list_model_get_n_items (filters);
|
|
for (i = 0; i < n; i++)
|
|
{
|
|
GtkFileFilter *filter = g_list_model_get_item (filters, i);
|
|
g_variant_builder_add (&builder, "@(sa(us))", gtk_file_filter_to_gvariant (filter));
|
|
g_object_unref (filter);
|
|
}
|
|
g_object_unref (filters);
|
|
|
|
return g_variant_builder_end (&builder);
|
|
}
|
|
|
|
static GVariant *
|
|
gtk_file_chooser_native_choice_to_variant (GtkFileChooserNativeChoice *choice)
|
|
{
|
|
GVariantBuilder choices;
|
|
int i;
|
|
|
|
g_variant_builder_init (&choices, G_VARIANT_TYPE ("a(ss)"));
|
|
if (choice->options)
|
|
{
|
|
for (i = 0; choice->options[i]; i++)
|
|
g_variant_builder_add (&choices, "(&s&s)", choice->options[i], choice->option_labels[i]);
|
|
}
|
|
|
|
return g_variant_new ("(&s&s@a(ss)&s)",
|
|
choice->id,
|
|
choice->label,
|
|
g_variant_builder_end (&choices),
|
|
choice->selected ? choice->selected : "");
|
|
}
|
|
|
|
static GVariant *
|
|
serialize_choices (GtkFileChooserNative *self)
|
|
{
|
|
GVariantBuilder builder;
|
|
GSList *l;
|
|
|
|
g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(ssa(ss)s)"));
|
|
for (l = self->choices; l; l = l->next)
|
|
{
|
|
GtkFileChooserNativeChoice *choice = l->data;
|
|
|
|
g_variant_builder_add (&builder, "@(ssa(ss)s)",
|
|
gtk_file_chooser_native_choice_to_variant (choice));
|
|
}
|
|
|
|
return g_variant_builder_end (&builder);
|
|
}
|
|
|
|
static void
|
|
show_portal_file_chooser (GtkFileChooserNative *self,
|
|
const char *parent_window_str)
|
|
{
|
|
FilechooserPortalData *data = self->mode_data;
|
|
GDBusMessage *message;
|
|
GVariantBuilder opt_builder;
|
|
gboolean multiple;
|
|
gboolean directory;
|
|
const char *title;
|
|
char *token;
|
|
|
|
message = g_dbus_message_new_method_call (PORTAL_BUS_NAME,
|
|
PORTAL_OBJECT_PATH,
|
|
PORTAL_FILECHOOSER_INTERFACE,
|
|
data->method_name);
|
|
|
|
data->portal_handle = gtk_get_portal_request_path (data->connection, &token);
|
|
data->portal_response_signal_id =
|
|
g_dbus_connection_signal_subscribe (data->connection,
|
|
PORTAL_BUS_NAME,
|
|
PORTAL_REQUEST_INTERFACE,
|
|
"Response",
|
|
data->portal_handle,
|
|
NULL,
|
|
G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
|
|
response_cb,
|
|
self, NULL);
|
|
|
|
multiple = gtk_file_chooser_get_select_multiple (GTK_FILE_CHOOSER (self));
|
|
directory = gtk_file_chooser_get_action (GTK_FILE_CHOOSER (self)) == GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER;
|
|
g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT);
|
|
|
|
g_variant_builder_add (&opt_builder, "{sv}", "handle_token",
|
|
g_variant_new_string (token));
|
|
g_free (token);
|
|
|
|
g_variant_builder_add (&opt_builder, "{sv}", "multiple",
|
|
g_variant_new_boolean (multiple));
|
|
g_variant_builder_add (&opt_builder, "{sv}", "directory",
|
|
g_variant_new_boolean (directory));
|
|
if (self->accept_label)
|
|
g_variant_builder_add (&opt_builder, "{sv}", "accept_label",
|
|
g_variant_new_string (self->accept_label));
|
|
if (self->cancel_label)
|
|
g_variant_builder_add (&opt_builder, "{sv}", "cancel_label",
|
|
g_variant_new_string (self->cancel_label));
|
|
g_variant_builder_add (&opt_builder, "{sv}", "modal",
|
|
g_variant_new_boolean (data->modal));
|
|
g_variant_builder_add (&opt_builder, "{sv}", "filters", get_filters (GTK_FILE_CHOOSER (self)));
|
|
if (self->current_filter)
|
|
g_variant_builder_add (&opt_builder, "{sv}", "current_filter",
|
|
gtk_file_filter_to_gvariant (self->current_filter));
|
|
if (self->current_name)
|
|
g_variant_builder_add (&opt_builder, "{sv}", "current_name",
|
|
g_variant_new_string (GTK_FILE_CHOOSER_NATIVE (self)->current_name));
|
|
if (self->current_folder)
|
|
{
|
|
char *path;
|
|
|
|
path = g_file_get_path (GTK_FILE_CHOOSER_NATIVE (self)->current_folder);
|
|
g_variant_builder_add (&opt_builder, "{sv}", "current_folder",
|
|
g_variant_new_bytestring (path));
|
|
g_free (path);
|
|
}
|
|
if (self->current_file)
|
|
{
|
|
char *path;
|
|
|
|
path = g_file_get_path (GTK_FILE_CHOOSER_NATIVE (self)->current_file);
|
|
g_variant_builder_add (&opt_builder, "{sv}", "current_file",
|
|
g_variant_new_bytestring (path));
|
|
g_free (path);
|
|
}
|
|
|
|
if (self->choices)
|
|
g_variant_builder_add (&opt_builder, "{sv}", "choices",
|
|
serialize_choices (GTK_FILE_CHOOSER_NATIVE (self)));
|
|
|
|
title = gtk_native_dialog_get_title (GTK_NATIVE_DIALOG (self));
|
|
|
|
g_dbus_message_set_body (message,
|
|
g_variant_new ("(ss@a{sv})",
|
|
parent_window_str ? parent_window_str : "",
|
|
title ? title : "",
|
|
g_variant_builder_end (&opt_builder)));
|
|
|
|
g_dbus_connection_send_message_with_reply (data->connection,
|
|
message,
|
|
G_DBUS_SEND_MESSAGE_FLAGS_NONE,
|
|
G_MAXINT,
|
|
NULL,
|
|
NULL,
|
|
open_file_msg_cb,
|
|
data);
|
|
|
|
g_object_unref (message);
|
|
}
|
|
|
|
static void
|
|
window_handle_exported (GtkWindow *window,
|
|
const char *handle_str,
|
|
gpointer user_data)
|
|
{
|
|
GtkFileChooserNative *self = user_data;
|
|
FilechooserPortalData *data = self->mode_data;
|
|
|
|
if (data->modal)
|
|
{
|
|
data->grab_widget = g_object_ref_sink (gtk_label_new (""));
|
|
gtk_grab_add (GTK_WIDGET (data->grab_widget));
|
|
}
|
|
|
|
show_portal_file_chooser (self, handle_str);
|
|
}
|
|
|
|
gboolean
|
|
gtk_file_chooser_native_portal_show (GtkFileChooserNative *self,
|
|
PortalErrorHandler error_handler)
|
|
{
|
|
FilechooserPortalData *data;
|
|
GtkWindow *transient_for;
|
|
GDBusConnection *connection;
|
|
GtkFileChooserAction action;
|
|
const char *method_name;
|
|
|
|
if (!gdk_should_use_portal ())
|
|
return FALSE;
|
|
|
|
connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL);
|
|
if (connection == NULL)
|
|
return FALSE;
|
|
|
|
action = gtk_file_chooser_get_action (GTK_FILE_CHOOSER (self));
|
|
|
|
if (action == GTK_FILE_CHOOSER_ACTION_OPEN)
|
|
method_name = "OpenFile";
|
|
else if (action == GTK_FILE_CHOOSER_ACTION_SAVE)
|
|
method_name = "SaveFile";
|
|
else if (action == GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER)
|
|
{
|
|
if (gtk_get_portal_interface_version (connection, "org.freedesktop.portal.FileChooser") < 3)
|
|
{
|
|
g_warning ("GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER is not supported by GtkFileChooserNativePortal because portal is too old");
|
|
return FALSE;
|
|
}
|
|
method_name = "OpenFile";
|
|
}
|
|
else
|
|
{
|
|
g_warning ("GTK_FILE_CHOOSER_ACTION_CREATE_FOLDER is not supported by GtkFileChooserNativePortal");
|
|
return FALSE;
|
|
}
|
|
|
|
data = g_new0 (FilechooserPortalData, 1);
|
|
data->self = g_object_ref (self);
|
|
data->connection = connection;
|
|
data->error_handler = error_handler;
|
|
|
|
data->method_name = method_name;
|
|
|
|
if (gtk_native_dialog_get_modal (GTK_NATIVE_DIALOG (self)))
|
|
data->modal = TRUE;
|
|
|
|
self->mode_data = data;
|
|
|
|
transient_for = gtk_native_dialog_get_transient_for (GTK_NATIVE_DIALOG (self));
|
|
if (transient_for != NULL && gtk_widget_is_visible (GTK_WIDGET (transient_for)))
|
|
{
|
|
if (!gtk_window_export_handle (transient_for,
|
|
window_handle_exported,
|
|
self))
|
|
{
|
|
g_warning ("Failed to export handle, could not set transient for");
|
|
show_portal_file_chooser (self, NULL);
|
|
}
|
|
else
|
|
{
|
|
data->exported_window = g_object_ref (transient_for);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
show_portal_file_chooser (self, NULL);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
void
|
|
gtk_file_chooser_native_portal_hide (GtkFileChooserNative *self)
|
|
{
|
|
FilechooserPortalData *data = self->mode_data;
|
|
|
|
/* This is always set while dialog visible */
|
|
g_assert (data != NULL);
|
|
|
|
data->hidden = TRUE;
|
|
|
|
if (data->portal_handle)
|
|
send_close (data);
|
|
|
|
/* We clear the data because we might have in-flight async
|
|
* operations that can still access it
|
|
*/
|
|
filechooser_portal_data_clear (data);
|
|
self->mode_data = NULL;
|
|
}
|