1 /*
2 * Copyright (C) 2018, Matthias Clasen
3 *
4 * This file is free software; you can redistribute it and/or modify it
5 * under the terms of the GNU Lesser General Public License as
6 * published by the Free Software Foundation, version 3.0 of the
7 * License.
8 *
9 * This file is distributed in the hope that it will be useful, but
10 * WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * SPDX-License-Identifier: LGPL-3.0-only
18 */
19
20 #include "config.h"
21
22 #include "filechooser.h"
23 #include "portal-private.h"
24
25 typedef struct {
26 XdpPortal *portal;
27 XdpParent *parent;
28 char *parent_handle;
29 char *method;
30 char *title;
31 gboolean multiple;
32 char *current_name;
33 char *current_folder;
34 char *current_file;
35 GVariant *files;
36 GVariant *filters;
37 GVariant *current_filter;
38 GVariant *choices;
39 guint signal_id;
40 GTask *task;
41 char *request_path;
42 guint cancelled_id;
43 } FileCall;
44
45 static void
file_call_free(FileCall * call)46 file_call_free (FileCall *call)
47 {
48 if (call->parent)
49 {
50 call->parent->parent_unexport (call->parent);
51 xdp_parent_free (call->parent);
52 }
53 g_free (call->parent_handle);
54
55 if (call->signal_id)
56 g_dbus_connection_signal_unsubscribe (call->portal->bus, call->signal_id);
57
58 if (call->cancelled_id)
59 g_signal_handler_disconnect (g_task_get_cancellable (call->task), call->cancelled_id);
60
61 g_free (call->request_path);
62
63 g_object_unref (call->portal);
64 g_object_unref (call->task);
65
66 // call->method not free'ed as it is assigned from static strings
67 g_free (call->title);
68 g_free (call->current_name);
69 g_free (call->current_folder);
70 g_free (call->current_file);
71 if (call->files)
72 g_variant_unref (call->files);
73 if (call->filters)
74 g_variant_unref (call->filters);
75 if (call->current_filter)
76 g_variant_unref (call->current_filter);
77 if (call->choices)
78 g_variant_unref (call->choices);
79
80 g_free (call);
81 }
82
83 static void
response_received(GDBusConnection * bus,const char * sender_name,const char * object_path,const char * interface_name,const char * signal_name,GVariant * parameters,gpointer data)84 response_received (GDBusConnection *bus,
85 const char *sender_name,
86 const char *object_path,
87 const char *interface_name,
88 const char *signal_name,
89 GVariant *parameters,
90 gpointer data)
91 {
92 FileCall *call = data;
93 guint32 response;
94 g_autoptr(GVariant) ret = NULL;
95
96 if (call->cancelled_id)
97 {
98 g_signal_handler_disconnect (g_task_get_cancellable (call->task), call->cancelled_id);
99 call->cancelled_id = 0;
100 }
101
102 g_variant_get (parameters, "(u@a{sv})", &response, &ret);
103
104 if (response == 0)
105 g_task_return_pointer (call->task, g_variant_ref (ret), (GDestroyNotify)g_variant_unref);
106 else if (response == 1)
107 g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_CANCELLED, "Filechooser canceled");
108 else
109 g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_FAILED, "Filechooser failed");
110
111 file_call_free (call);
112 }
113
114 static void open_file (FileCall *call);
115
116 static void
parent_exported(XdpParent * parent,const char * handle,gpointer data)117 parent_exported (XdpParent *parent,
118 const char *handle,
119 gpointer data)
120 {
121 FileCall *call = data;
122 call->parent_handle = g_strdup (handle);
123 open_file (call);
124 }
125
126 static void
cancelled_cb(GCancellable * cancellable,gpointer data)127 cancelled_cb (GCancellable *cancellable,
128 gpointer data)
129 {
130 FileCall *call = data;
131
132 g_dbus_connection_call (call->portal->bus,
133 PORTAL_BUS_NAME,
134 call->request_path,
135 REQUEST_INTERFACE,
136 "Close",
137 NULL,
138 NULL,
139 G_DBUS_CALL_FLAGS_NONE,
140 -1,
141 NULL, NULL, NULL);
142
143 g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_CANCELLED, "OpenFile call canceled by caller");
144
145 file_call_free (call);
146 }
147
148 static void
call_returned(GObject * object,GAsyncResult * result,gpointer data)149 call_returned (GObject *object,
150 GAsyncResult *result,
151 gpointer data)
152 {
153 FileCall *call = data;
154 GError *error = NULL;
155 g_autoptr(GVariant) ret = NULL;
156
157 ret = g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), result, &error);
158 if (error)
159 {
160 g_task_return_error (call->task, error);
161 file_call_free (call);
162 }
163 }
164
165 static void
open_file(FileCall * call)166 open_file (FileCall *call)
167 {
168 GVariantBuilder options;
169 g_autofree char *token = NULL;
170 GCancellable *cancellable;
171
172 if (call->parent_handle == NULL)
173 {
174 call->parent->parent_export (call->parent, parent_exported, call);
175 return;
176 }
177
178 token = g_strdup_printf ("portal%d", g_random_int_range (0, G_MAXINT));
179 call->request_path = g_strconcat (REQUEST_PATH_PREFIX, call->portal->sender, "/", token, NULL);
180 call->signal_id = g_dbus_connection_signal_subscribe (call->portal->bus,
181 PORTAL_BUS_NAME,
182 REQUEST_INTERFACE,
183 "Response",
184 call->request_path,
185 NULL,
186 G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
187 response_received,
188 call,
189 NULL);
190
191 cancellable = g_task_get_cancellable (call->task);
192 if (cancellable)
193 call->cancelled_id = g_signal_connect (cancellable, "cancelled", G_CALLBACK (cancelled_cb), call);
194
195 g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT);
196 g_variant_builder_add (&options, "{sv}", "handle_token", g_variant_new_string (token));
197 if (call->multiple)
198 g_variant_builder_add (&options, "{sv}", "multiple", g_variant_new_boolean (call->multiple));
199 if (call->files)
200 g_variant_builder_add (&options, "{sv}", "files", call->files);
201 if (call->filters)
202 g_variant_builder_add (&options, "{sv}", "filters", call->filters);
203 if (call->current_filter)
204 g_variant_builder_add (&options, "{sv}", "current_filter", call->current_filter);
205 if (call->choices)
206 g_variant_builder_add (&options, "{sv}", "choices", call->choices);
207 if (call->current_name)
208 g_variant_builder_add (&options, "{sv}", "current_name", g_variant_new_string (call->current_name));
209 if (call->current_folder)
210 g_variant_builder_add (&options, "{sv}", "current_folder", g_variant_new_bytestring (call->current_folder));
211 if (call->current_file)
212 g_variant_builder_add (&options, "{sv}", "current_file", g_variant_new_bytestring (call->current_file));
213
214 g_dbus_connection_call (call->portal->bus,
215 PORTAL_BUS_NAME,
216 PORTAL_OBJECT_PATH,
217 "org.freedesktop.portal.FileChooser",
218 call->method,
219 g_variant_new ("(ssa{sv})", call->parent_handle, call->title, &options),
220 NULL,
221 G_DBUS_CALL_FLAGS_NONE,
222 -1,
223 NULL,
224 call_returned,
225 call);
226 }
227
228 /**
229 * xdp_portal_open_file:
230 * @portal: a [class@Portal]
231 * @parent: (nullable): parent window information
232 * @title: title for the file chooser dialog
233 * @filters: (nullable): a [struct@GLib.Variant] describing file filters
234 * @current_filter: (nullable): a [struct@GLib.Variant] describing the current file filter
235 * @choices: (nullable): a [struct@GLib.Variant] describing extra widgets
236 * @flags: options for this call
237 * @cancellable: (nullable): optional [class@Gio.Cancellable]
238 * @callback: (scope async): a callback to call when the request is done
239 * @data: (closure): data to pass to @callback
240 *
241 * Asks the user to open one or more files.
242 *
243 * The format for the @filters argument is a(sa(us)).
244 * Each item in the array specifies a single filter to offer to the user.
245 * The first string is a user-visible name for the filter. The a(us)
246 * specifies a list of filter strings, which can be either a glob pattern
247 * (indicated by 0) or a mimetype (indicated by 1).
248 *
249 * Example: [('Images', [(0, '*.ico'), (1, 'image/png')]), ('Text', [(0, '*.txt')])]
250 *
251 * The format for the @choices argument is a(ssa(ss)s).
252 * For each element, the first string is an ID that will be returned
253 * with the response, te second string is a user-visible label. The
254 * a(ss) is the list of choices, each being a is an ID and a
255 * user-visible label. The final string is the initial selection,
256 * or "", to let the portal decide which choice will be initially selected.
257 * None of the strings, except for the initial selection, should be empty.
258 *
259 * As a special case, passing an empty array for the list of choices
260 * indicates a boolean choice that is typically displayed as a check
261 * button, using "true" and "false" as the choices.
262 *
263 * Example: [('encoding', 'Encoding', [('utf8', 'Unicode (UTF-8)'), ('latin15', 'Western')], 'latin15'), ('reencode', 'Reencode', [], 'false')]
264 *
265 * When the request is done, @callback will be called. You can then
266 * call [method@Portal.open_file_finish] to get the results.
267 */
268 void
xdp_portal_open_file(XdpPortal * portal,XdpParent * parent,const char * title,GVariant * filters,GVariant * current_filter,GVariant * choices,XdpOpenFileFlags flags,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer data)269 xdp_portal_open_file (XdpPortal *portal,
270 XdpParent *parent,
271 const char *title,
272 GVariant *filters,
273 GVariant *current_filter,
274 GVariant *choices,
275 XdpOpenFileFlags flags,
276 GCancellable *cancellable,
277 GAsyncReadyCallback callback,
278 gpointer data)
279 {
280 FileCall *call;
281
282 g_return_if_fail (XDP_IS_PORTAL (portal));
283 g_return_if_fail ((flags & ~(XDP_OPEN_FILE_FLAG_MULTIPLE)) == 0);
284
285 call = g_new0 (FileCall, 1);
286 call->portal = g_object_ref (portal);
287 if (parent)
288 call->parent = xdp_parent_copy (parent);
289 else
290 call->parent_handle = g_strdup ("");
291 call->method = "OpenFile";
292 call->title = g_strdup (title);
293 call->multiple = (flags & XDP_OPEN_FILE_FLAG_MULTIPLE) != 0;
294 call->filters = filters ? g_variant_ref (filters) : NULL;
295 call->current_filter = current_filter ? g_variant_ref (current_filter) : NULL;
296 call->choices = choices ? g_variant_ref (choices) : NULL;
297 call->task = g_task_new (portal, cancellable, callback, data);
298 g_task_set_source_tag (call->task, xdp_portal_open_file);
299
300 open_file (call);
301 }
302
303 /**
304 * xdp_portal_open_file_finish:
305 * @portal: a [class@Portal]
306 * @result: a [iface@Gio.AsyncResult]
307 * @error: return location for an error
308 *
309 * Finishes the open-file request, and returns
310 * the result in the form of a [struct@GLib.Variant] dictionary containing
311 * the following fields:
312 *
313 * - uris `as`: an array of strings containing the uris of selected files
314 * - choices `a(ss)`: an array of pairs of strings, the first string being the
315 * ID of a combobox that was passed into this call, the second string
316 * being the selected option.
317 *
318 * Returns: (transfer full): a [struct@GLib.Variant] dictionary with the results
319 */
320 GVariant *
xdp_portal_open_file_finish(XdpPortal * portal,GAsyncResult * result,GError ** error)321 xdp_portal_open_file_finish (XdpPortal *portal,
322 GAsyncResult *result,
323 GError **error)
324 {
325 GVariant *ret;
326
327 g_return_val_if_fail (XDP_IS_PORTAL (portal), NULL);
328 g_return_val_if_fail (g_task_is_valid (result, portal), NULL);
329 g_return_val_if_fail (g_task_get_source_tag (G_TASK (result)) == xdp_portal_open_file, NULL);
330
331 ret = g_task_propagate_pointer (G_TASK (result), error);
332 return ret ? g_variant_ref (ret) : NULL;
333 }
334
335 /**
336 * xdp_portal_save_file:
337 * @portal: a [class@Portal]
338 * @parent: (nullable): parent window information
339 * @title: title for the file chooser dialog
340 * @current_name: (nullable): suggested filename
341 * @current_folder: (nullable): suggested folder to save the file in
342 * @current_file: (nullable): the current file (when saving an existing file)
343 * @filters: (nullable): a [struct@GLib.Variant] describing file filters
344 * @current_filter: (nullable): a [struct@GLib.Variant] describing the current file filter
345 * @choices: (nullable): a [struct@GLib.Variant] describing extra widgets
346 * @flags: options for this call
347 * @cancellable: (nullable): optional [class@Gio.Cancellable]
348 * @callback: (scope async): a callback to call when the request is done
349 * @data: (closure): data to pass to @callback
350 *
351 * Asks the user for a location to save a file.
352 *
353 * The format for the @filters argument is the same as for [method@Portal.open_file].
354
355 * The format for the @choices argument is the same as for [method@Portal.open_file].
356 *
357 * When the request is done, @callback will be called. You can then
358 * call [method@Portal.save_file_finish] to get the results.
359 */
360 void
xdp_portal_save_file(XdpPortal * portal,XdpParent * parent,const char * title,const char * current_name,const char * current_folder,const char * current_file,GVariant * filters,GVariant * current_filter,GVariant * choices,XdpSaveFileFlags flags,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer data)361 xdp_portal_save_file (XdpPortal *portal,
362 XdpParent *parent,
363 const char *title,
364 const char *current_name,
365 const char *current_folder,
366 const char *current_file,
367 GVariant *filters,
368 GVariant *current_filter,
369 GVariant *choices,
370 XdpSaveFileFlags flags,
371 GCancellable *cancellable,
372 GAsyncReadyCallback callback,
373 gpointer data)
374 {
375 FileCall *call;
376
377 g_return_if_fail (XDP_IS_PORTAL (portal));
378 g_return_if_fail (flags == XDP_SAVE_FILE_FLAG_NONE);
379
380 call = g_new0 (FileCall, 1);
381 call->portal = g_object_ref (portal);
382 if (parent)
383 call->parent = xdp_parent_copy (parent);
384 else
385 call->parent_handle = g_strdup ("");
386 call->method = "SaveFile";
387 call->title = g_strdup (title);
388 call->current_name = g_strdup (current_name);
389 call->current_folder = g_strdup (current_folder);
390 call->current_file = g_strdup (current_file);
391 call->filters = filters ? g_variant_ref (filters) : NULL;
392 call->current_filter = current_filter ? g_variant_ref (current_filter) : NULL;
393 call->choices = choices ? g_variant_ref (choices) : NULL;
394 call->task = g_task_new (portal, cancellable, callback, data);
395 g_task_set_source_tag (call->task, xdp_portal_save_file);
396
397 open_file (call);
398 }
399
400 /**
401 * xdp_portal_save_file_finish:
402 * @portal: a [class@Portal]
403 * @result: a [iface@Gio.AsyncResult]
404 * @error: return location for an error
405 *
406 * Finishes the save-file request, and returns
407 * the result in the form of a [struct@GLib.Variant] dictionary containing
408 * the following fields:
409 *
410 * - uris `(as)`: an array of strings containing the uri of the selected file
411 * - choices `a(ss)`: an array of pairs of strings, the first string being the
412 * ID of a combobox that was passed into this call, the second string
413 * being the selected option.
414 *
415 * Returns: (transfer full): a [struct@GLib.Variant] dictionary with the results
416 */
417 GVariant *
xdp_portal_save_file_finish(XdpPortal * portal,GAsyncResult * result,GError ** error)418 xdp_portal_save_file_finish (XdpPortal *portal,
419 GAsyncResult *result,
420 GError **error)
421 {
422 GVariant *ret;
423
424 g_return_val_if_fail (XDP_IS_PORTAL (portal), NULL);
425 g_return_val_if_fail (g_task_is_valid (result, portal), NULL);
426 g_return_val_if_fail (g_task_get_source_tag (G_TASK (result)) == xdp_portal_save_file, NULL);
427
428 ret = g_task_propagate_pointer (G_TASK (result), error);
429 return ret ? g_variant_ref (ret) : NULL;
430 }
431
432 /**
433 * xdp_portal_save_files:
434 * @portal: a [class@Portal]
435 * @parent: (nullable): parent window information
436 * @title: title for the file chooser dialog
437 * @current_name: (nullable): suggested filename
438 * @current_folder: (nullable): suggested folder to save the file in
439 * @files: An array of file names to be saved
440 * @choices: (nullable): a [struct@GLib.Variant] describing extra widgets
441 * @flags: options for this call
442 * @cancellable: (nullable): optional [class@Gio.Cancellable]
443 * @callback: (scope async): a callback to call when the request is done
444 * @data: (closure): data to pass to @callback
445 *
446 * Asks for a folder as a location to save one or more files. The
447 * names of the files will be used as-is and appended to the selected
448 * folder's path in the list of returned files. If the selected folder
449 * already contains a file with one of the given names, the portal may
450 * prompt or take some other action to construct a unique file name and
451 * return that instead.
452 *
453 * The format for the @choices argument is the same as for [method@Portal.open_file].
454 *
455 * When the request is done, @callback will be called. You can then
456 * call [method@Portal.save_file_finish] to get the results.
457 */
458 void
xdp_portal_save_files(XdpPortal * portal,XdpParent * parent,const char * title,const char * current_name,const char * current_folder,GVariant * files,GVariant * choices,XdpSaveFileFlags flags,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer data)459 xdp_portal_save_files (XdpPortal *portal,
460 XdpParent *parent,
461 const char *title,
462 const char *current_name,
463 const char *current_folder,
464 GVariant *files,
465 GVariant *choices,
466 XdpSaveFileFlags flags,
467 GCancellable *cancellable,
468 GAsyncReadyCallback callback,
469 gpointer data)
470 {
471 FileCall *call;
472
473 g_return_if_fail (XDP_IS_PORTAL (portal));
474 g_return_if_fail (files != NULL);
475 g_return_if_fail (flags == XDP_SAVE_FILE_FLAG_NONE);
476
477 call = g_new0 (FileCall, 1);
478 call->portal = g_object_ref (portal);
479 if (parent)
480 call->parent = xdp_parent_copy (parent);
481 else
482 call->parent_handle = g_strdup ("");
483 call->method = "SaveFiles";
484 call->title = g_strdup (title);
485 call->current_name = g_strdup (current_name);
486 call->current_folder = g_strdup (current_folder);
487 call->files = g_variant_ref (files);
488 call->choices = choices ? g_variant_ref (choices) : NULL;
489 call->task = g_task_new (portal, cancellable, callback, data);
490 g_task_set_source_tag (call->task, xdp_portal_save_files);
491
492 open_file (call);
493 }
494
495 /**
496 * xdp_portal_save_files_finish:
497 * @portal: a [class@Portal]
498 * @result: a [iface@Gio.AsyncResult]
499 * @error: return location for an error
500 *
501 * Finishes the save-files request, and returns
502 * the result in the form of a [struct@GLib.Variant] dictionary containing
503 * the following fields:
504 *
505 * - uris `(as)`: an array of strings containing the uri corresponding to each
506 * file passed to the save-files request, in the same order. Note that the
507 * file names may have changed, for example if a file with the same name in
508 * the selected folder already exists.
509 * - choices `a(ss)`: an array of pairs of strings, the first string being the
510 * ID of a combobox that was passed into this call, the second string
511 * being the selected option.
512 *
513 * Returns: (transfer full): a [struct@GLib.Variant] dictionary with the results
514 */
515 GVariant *
xdp_portal_save_files_finish(XdpPortal * portal,GAsyncResult * result,GError ** error)516 xdp_portal_save_files_finish (XdpPortal *portal,
517 GAsyncResult *result,
518 GError **error)
519 {
520 GVariant *ret;
521
522 g_return_val_if_fail (XDP_IS_PORTAL (portal), NULL);
523 g_return_val_if_fail (g_task_is_valid (result, portal), NULL);
524 g_return_val_if_fail (g_task_get_source_tag (G_TASK (result)) == xdp_portal_save_files, NULL);
525
526 ret = g_task_propagate_pointer (G_TASK (result), error);
527 return ret ? g_variant_ref (ret) : NULL;
528 }
529