1/* -*- Mode: C; c-file-style: "gnu"; tab-width: 8 -*- */
2/* GTK - The GIMP Toolkit
3 * gtkfilechoosernativeportal.c: Portal File selector dialog
4 * Copyright (C) 2015, Red Hat, Inc.
5 *
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2 of the License, or (at your option) any later version.
10 *
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20#include "config.h"
21
22#include "gtkfilechoosernativeprivate.h"
23#include "gtknativedialogprivate.h"
24
25#include "gtkprivate.h"
26#include "gtkfilechooserdialog.h"
27#include "gtkfilechooserprivate.h"
28#include "gtkfilechooserwidget.h"
29#include "gtkfilechooserwidgetprivate.h"
30#include "gtkfilechooserutils.h"
31#include "gtksizerequest.h"
32#include "gtktypebuiltins.h"
33#include "gtkintl.h"
34#include "gtksettings.h"
35#include "gtktogglebutton.h"
36#include "gtkheaderbar.h"
37#include "gtklabel.h"
38#include "gtkmain.h"
39#include "gtkfilefilterprivate.h"
40#include "gtkwindowprivate.h"
41
42
43typedef struct {
44 GtkFileChooserNative *self;
45
46 GtkWidget *grab_widget;
47
48 GDBusConnection *connection;
49 char *portal_handle;
50 guint portal_response_signal_id;
51 gboolean modal;
52
53 gboolean hidden;
54
55 const char *method_name;
56
57 GtkWindow *exported_window;
58 PortalErrorHandler error_handler;
59} FilechooserPortalData;
60
61
62static void
63filechooser_portal_data_free (FilechooserPortalData *data)
64{
65 if (data->portal_response_signal_id != 0)
66 g_dbus_connection_signal_unsubscribe (connection: data->connection,
67 subscription_id: data->portal_response_signal_id);
68
69 g_object_unref (object: data->connection);
70
71 if (data->grab_widget)
72 {
73 gtk_grab_remove (widget: data->grab_widget);
74 g_object_unref (object: data->grab_widget);
75 }
76
77 g_clear_object (&data->self);
78
79 if (data->exported_window)
80 gtk_window_unexport_handle (window: data->exported_window);
81
82 g_clear_object (&data->exported_window);
83
84 g_free (mem: data->portal_handle);
85
86 g_free (mem: data);
87}
88
89static void
90response_cb (GDBusConnection *connection,
91 const char *sender_name,
92 const char *object_path,
93 const char *interface_name,
94 const char *signal_name,
95 GVariant *parameters,
96 gpointer user_data)
97{
98 GtkFileChooserNative *self = user_data;
99 FilechooserPortalData *data = self->mode_data;
100 guint32 portal_response;
101 int gtk_response;
102 const char **uris;
103 int i;
104 GVariant *response_data;
105 GVariant *choices = NULL;
106 GVariant *current_filter = NULL;
107
108 g_variant_get (value: parameters, format_string: "(u@a{sv})", &portal_response, &response_data);
109 g_variant_lookup (dictionary: response_data, key: "uris", format_string: "^a&s", &uris);
110
111 choices = g_variant_lookup_value (dictionary: response_data, key: "choices", G_VARIANT_TYPE ("a(ss)"));
112 if (choices)
113 {
114 for (i = 0; i < g_variant_n_children (value: choices); i++)
115 {
116 const char *id;
117 const char *selected;
118 g_variant_get_child (value: choices, index_: i, format_string: "(&s&s)", &id, &selected);
119 gtk_file_chooser_set_choice (GTK_FILE_CHOOSER (self), id, option: selected);
120 }
121 g_variant_unref (value: choices);
122 }
123
124 current_filter = g_variant_lookup_value (dictionary: response_data, key: "current_filter", G_VARIANT_TYPE ("(sa(us))"));
125 if (current_filter)
126 {
127 GtkFileFilter *filter = gtk_file_filter_new_from_gvariant (variant: current_filter);
128 const char *current_filter_name = gtk_file_filter_get_name (filter);
129
130 /* Try to find the given filter in the list of filters.
131 * Since filters are compared by pointer value, using the passed
132 * filter would otherwise not match in a comparison, even if
133 * a filter in the list of filters has been selected.
134 * We'll use the heuristic that if two filters have the same name,
135 * they must be the same.
136 * If there is no match, just set the filter as it was retrieved.
137 */
138 GtkFileFilter *filter_to_select = filter;
139 GListModel *filters;
140 guint j, n;
141
142 filters = gtk_file_chooser_get_filters (GTK_FILE_CHOOSER (self));
143 n = g_list_model_get_n_items (list: filters);
144 for (j = 0; j < n; j++)
145 {
146 GtkFileFilter *f = g_list_model_get_item (list: filters, position: j);
147 if (g_strcmp0 (str1: gtk_file_filter_get_name (filter: f), str2: current_filter_name) == 0)
148 {
149 filter_to_select = f;
150 break;
151 }
152 g_object_unref (object: f);
153 }
154 g_object_unref (object: filters);
155 gtk_file_chooser_set_filter (GTK_FILE_CHOOSER (self), filter: filter_to_select);
156 g_object_unref (object: filter_to_select);
157 }
158
159 g_slist_free_full (list: self->custom_files, free_func: g_object_unref);
160 self->custom_files = NULL;
161 for (i = 0; uris[i]; i++)
162 self->custom_files = g_slist_prepend (list: self->custom_files, data: g_file_new_for_uri (uri: uris[i]));
163
164 switch (portal_response)
165 {
166 case 0:
167 gtk_response = GTK_RESPONSE_ACCEPT;
168 break;
169 case 1:
170 gtk_response = GTK_RESPONSE_CANCEL;
171 break;
172 case 2:
173 default:
174 gtk_response = GTK_RESPONSE_DELETE_EVENT;
175 break;
176 }
177
178 filechooser_portal_data_free (data);
179 self->mode_data = NULL;
180
181 _gtk_native_dialog_emit_response (self: GTK_NATIVE_DIALOG (ptr: self), response_id: gtk_response);
182}
183
184static void
185send_close (FilechooserPortalData *data)
186{
187 GDBusMessage *message;
188 GError *error = NULL;
189
190 message = g_dbus_message_new_method_call (PORTAL_BUS_NAME,
191 PORTAL_OBJECT_PATH,
192 PORTAL_FILECHOOSER_INTERFACE,
193 method: "Close");
194 g_dbus_message_set_body (message,
195 body: g_variant_new (format_string: "(o)", data->portal_handle));
196
197 if (!g_dbus_connection_send_message (connection: data->connection,
198 message,
199 flags: G_DBUS_SEND_MESSAGE_FLAGS_NONE,
200 NULL, error: &error))
201 {
202 g_warning ("unable to send FileChooser Close message: %s", error->message);
203 g_error_free (error);
204 }
205
206 g_object_unref (object: message);
207}
208
209static void
210open_file_msg_cb (GObject *source_object,
211 GAsyncResult *res,
212 gpointer user_data)
213{
214 FilechooserPortalData *data = user_data;
215 GtkFileChooserNative *self = data->self;
216 GDBusMessage *reply;
217 GError *error = NULL;
218 char *handle = NULL;
219
220 reply = g_dbus_connection_send_message_with_reply_finish (connection: data->connection, res, error: &error);
221
222 if (reply && g_dbus_message_to_gerror (message: reply, error: &error))
223 g_clear_object (&reply);
224
225 if (reply == NULL)
226 {
227 if (!data->hidden && data->error_handler)
228 {
229 data->error_handler (self);
230 filechooser_portal_data_free (data);
231 self->mode_data = NULL;
232 }
233 g_error_free (error);
234 return;
235 }
236
237 g_variant_get_child (value: g_dbus_message_get_body (message: reply), index_: 0, format_string: "o", &handle);
238
239 if (data->hidden)
240 {
241 /* The dialog was hidden before we got the handle, close it now */
242 send_close (data);
243 filechooser_portal_data_free (data);
244 self->mode_data = NULL;
245 }
246 else if (strcmp (s1: handle, s2: data->portal_handle) != 0)
247 {
248 g_free (mem: data->portal_handle);
249 data->portal_handle = g_steal_pointer (&handle);
250 g_dbus_connection_signal_unsubscribe (connection: data->connection,
251 subscription_id: data->portal_response_signal_id);
252
253 data->portal_response_signal_id =
254 g_dbus_connection_signal_subscribe (connection: data->connection,
255 PORTAL_BUS_NAME,
256 PORTAL_REQUEST_INTERFACE,
257 member: "Response",
258 object_path: data->portal_handle,
259 NULL,
260 flags: G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
261 callback: response_cb,
262 user_data: self, NULL);
263 }
264
265 g_object_unref (object: reply);
266 g_free (mem: handle);
267}
268
269static GVariant *
270get_filters (GtkFileChooser *self)
271{
272 GListModel *filters;
273 guint n, i;
274 GVariantBuilder builder;
275
276 g_variant_builder_init (builder: &builder, G_VARIANT_TYPE ("a(sa(us))"));
277 filters = gtk_file_chooser_get_filters (chooser: self);
278 n = g_list_model_get_n_items (list: filters);
279 for (i = 0; i < n; i++)
280 {
281 GtkFileFilter *filter = g_list_model_get_item (list: filters, position: i);
282 g_variant_builder_add (builder: &builder, format_string: "@(sa(us))", gtk_file_filter_to_gvariant (filter));
283 g_object_unref (object: filter);
284 }
285 g_object_unref (object: filters);
286
287 return g_variant_builder_end (builder: &builder);
288}
289
290static GVariant *
291gtk_file_chooser_native_choice_to_variant (GtkFileChooserNativeChoice *choice)
292{
293 GVariantBuilder choices;
294 int i;
295
296 g_variant_builder_init (builder: &choices, G_VARIANT_TYPE ("a(ss)"));
297 if (choice->options)
298 {
299 for (i = 0; choice->options[i]; i++)
300 g_variant_builder_add (builder: &choices, format_string: "(&s&s)", choice->options[i], choice->option_labels[i]);
301 }
302
303 return g_variant_new (format_string: "(&s&s@a(ss)&s)",
304 choice->id,
305 choice->label,
306 g_variant_builder_end (builder: &choices),
307 choice->selected ? choice->selected : "");
308}
309
310static GVariant *
311serialize_choices (GtkFileChooserNative *self)
312{
313 GVariantBuilder builder;
314 GSList *l;
315
316 g_variant_builder_init (builder: &builder, G_VARIANT_TYPE ("a(ssa(ss)s)"));
317 for (l = self->choices; l; l = l->next)
318 {
319 GtkFileChooserNativeChoice *choice = l->data;
320
321 g_variant_builder_add (builder: &builder, format_string: "@(ssa(ss)s)",
322 gtk_file_chooser_native_choice_to_variant (choice));
323 }
324
325 return g_variant_builder_end (builder: &builder);
326}
327
328static void
329show_portal_file_chooser (GtkFileChooserNative *self,
330 const char *parent_window_str)
331{
332 FilechooserPortalData *data = self->mode_data;
333 GDBusMessage *message;
334 GVariantBuilder opt_builder;
335 gboolean multiple;
336 gboolean directory;
337 const char *title;
338 char *token;
339
340 message = g_dbus_message_new_method_call (PORTAL_BUS_NAME,
341 PORTAL_OBJECT_PATH,
342 PORTAL_FILECHOOSER_INTERFACE,
343 method: data->method_name);
344
345 data->portal_handle = gtk_get_portal_request_path (connection: data->connection, token: &token);
346 data->portal_response_signal_id =
347 g_dbus_connection_signal_subscribe (connection: data->connection,
348 PORTAL_BUS_NAME,
349 PORTAL_REQUEST_INTERFACE,
350 member: "Response",
351 object_path: data->portal_handle,
352 NULL,
353 flags: G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
354 callback: response_cb,
355 user_data: self, NULL);
356
357 multiple = gtk_file_chooser_get_select_multiple (GTK_FILE_CHOOSER (self));
358 directory = gtk_file_chooser_get_action (GTK_FILE_CHOOSER (self)) == GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER;
359 g_variant_builder_init (builder: &opt_builder, G_VARIANT_TYPE_VARDICT);
360
361 g_variant_builder_add (builder: &opt_builder, format_string: "{sv}", "handle_token",
362 g_variant_new_string (string: token));
363 g_free (mem: token);
364
365 g_variant_builder_add (builder: &opt_builder, format_string: "{sv}", "multiple",
366 g_variant_new_boolean (value: multiple));
367 g_variant_builder_add (builder: &opt_builder, format_string: "{sv}", "directory",
368 g_variant_new_boolean (value: directory));
369 if (self->accept_label)
370 g_variant_builder_add (builder: &opt_builder, format_string: "{sv}", "accept_label",
371 g_variant_new_string (string: self->accept_label));
372 if (self->cancel_label)
373 g_variant_builder_add (builder: &opt_builder, format_string: "{sv}", "cancel_label",
374 g_variant_new_string (string: self->cancel_label));
375 g_variant_builder_add (builder: &opt_builder, format_string: "{sv}", "modal",
376 g_variant_new_boolean (value: data->modal));
377 g_variant_builder_add (builder: &opt_builder, format_string: "{sv}", "filters", get_filters (GTK_FILE_CHOOSER (self)));
378 if (self->current_filter)
379 g_variant_builder_add (builder: &opt_builder, format_string: "{sv}", "current_filter",
380 gtk_file_filter_to_gvariant (filter: self->current_filter));
381 if (self->current_name)
382 g_variant_builder_add (builder: &opt_builder, format_string: "{sv}", "current_name",
383 g_variant_new_string (string: GTK_FILE_CHOOSER_NATIVE (ptr: self)->current_name));
384 if (self->current_folder)
385 {
386 char *path;
387
388 path = g_file_get_path (file: GTK_FILE_CHOOSER_NATIVE (ptr: self)->current_folder);
389 g_variant_builder_add (builder: &opt_builder, format_string: "{sv}", "current_folder",
390 g_variant_new_bytestring (string: path));
391 g_free (mem: path);
392 }
393 if (self->current_file)
394 {
395 char *path;
396
397 path = g_file_get_path (file: GTK_FILE_CHOOSER_NATIVE (ptr: self)->current_file);
398 g_variant_builder_add (builder: &opt_builder, format_string: "{sv}", "current_file",
399 g_variant_new_bytestring (string: path));
400 g_free (mem: path);
401 }
402
403 if (self->choices)
404 g_variant_builder_add (builder: &opt_builder, format_string: "{sv}", "choices",
405 serialize_choices (self: GTK_FILE_CHOOSER_NATIVE (ptr: self)));
406
407 title = gtk_native_dialog_get_title (self: GTK_NATIVE_DIALOG (ptr: self));
408
409 g_dbus_message_set_body (message,
410 body: g_variant_new (format_string: "(ss@a{sv})",
411 parent_window_str ? parent_window_str : "",
412 title ? title : "",
413 g_variant_builder_end (builder: &opt_builder)));
414
415 g_dbus_connection_send_message_with_reply (connection: data->connection,
416 message,
417 flags: G_DBUS_SEND_MESSAGE_FLAGS_NONE,
418 G_MAXINT,
419 NULL,
420 NULL,
421 callback: open_file_msg_cb,
422 user_data: data);
423
424 g_object_unref (object: message);
425}
426
427static void
428window_handle_exported (GtkWindow *window,
429 const char *handle_str,
430 gpointer user_data)
431{
432 GtkFileChooserNative *self = user_data;
433 FilechooserPortalData *data = self->mode_data;
434
435 if (data->modal)
436 {
437 data->grab_widget = g_object_ref_sink (gtk_label_new (""));
438 gtk_grab_add (GTK_WIDGET (data->grab_widget));
439 }
440
441 show_portal_file_chooser (self, parent_window_str: handle_str);
442}
443
444gboolean
445gtk_file_chooser_native_portal_show (GtkFileChooserNative *self,
446 PortalErrorHandler error_handler)
447{
448 FilechooserPortalData *data;
449 GtkWindow *transient_for;
450 GDBusConnection *connection;
451 GtkFileChooserAction action;
452 const char *method_name;
453
454 if (!gdk_should_use_portal ())
455 return FALSE;
456
457 connection = g_bus_get_sync (bus_type: G_BUS_TYPE_SESSION, NULL, NULL);
458 if (connection == NULL)
459 return FALSE;
460
461 action = gtk_file_chooser_get_action (GTK_FILE_CHOOSER (self));
462
463 if (action == GTK_FILE_CHOOSER_ACTION_OPEN)
464 method_name = "OpenFile";
465 else if (action == GTK_FILE_CHOOSER_ACTION_SAVE)
466 method_name = "SaveFile";
467 else if (action == GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER)
468 {
469 if (gtk_get_portal_interface_version (connection, interface_name: "org.freedesktop.portal.FileChooser") < 3)
470 {
471 g_warning ("GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER is not supported by GtkFileChooserNativePortal because portal is too old");
472 return FALSE;
473 }
474 method_name = "OpenFile";
475 }
476 else
477 {
478 g_warning ("GTK_FILE_CHOOSER_ACTION_CREATE_FOLDER is not supported by GtkFileChooserNativePortal");
479 return FALSE;
480 }
481
482 data = g_new0 (FilechooserPortalData, 1);
483 data->self = g_object_ref (self);
484 data->connection = connection;
485 data->error_handler = error_handler;
486
487 data->method_name = method_name;
488
489 if (gtk_native_dialog_get_modal (self: GTK_NATIVE_DIALOG (ptr: self)))
490 data->modal = TRUE;
491
492 self->mode_data = data;
493
494 transient_for = gtk_native_dialog_get_transient_for (self: GTK_NATIVE_DIALOG (ptr: self));
495 if (transient_for != NULL && gtk_widget_is_visible (GTK_WIDGET (transient_for)))
496 {
497 if (!gtk_window_export_handle (window: transient_for,
498 callback: window_handle_exported,
499 user_data: self))
500 {
501 g_warning ("Failed to export handle, could not set transient for");
502 show_portal_file_chooser (self, NULL);
503 }
504 else
505 {
506 data->exported_window = g_object_ref (transient_for);
507 }
508 }
509 else
510 {
511 show_portal_file_chooser (self, NULL);
512 }
513
514 return TRUE;
515}
516
517void
518gtk_file_chooser_native_portal_hide (GtkFileChooserNative *self)
519{
520 FilechooserPortalData *data = self->mode_data;
521
522 /* This is always set while dialog visible */
523 g_assert (data != NULL);
524
525 data->hidden = TRUE;
526
527 if (data->portal_handle)
528 send_close (data);
529
530 filechooser_portal_data_free (data);
531
532 self->mode_data = NULL;
533}
534

source code of gtk/gtk/gtkfilechoosernativeportal.c