More fixes for the Windows native file choosers.
[claws.git] / src / gtk / w32_filesel.c
1 /*
2  * Claws Mail -- a GTK+ based, lightweight, and fast e-mail client
3  * Copyright (C) 2016 The Claws Mail Team
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program. If not, see <http://www.gnu.org/licenses/>.
17  *
18  */
19
20 #ifdef HAVE_CONFIG_H
21 #  include "config.h"
22 #endif
23
24 #define UNICODE
25 #define _UNICODE
26
27 #include <glib.h>
28 #include <glib/gi18n.h>
29 #include <gdk/gdkwin32.h>
30 #include <pthread.h>
31
32 #include <windows.h>
33 #include <shlobj.h>
34
35 #include "claws.h"
36 #include "alertpanel.h"
37 #include "manage_window.h"
38 #include "utils.h"
39
40 static OPENFILENAME o;
41 static BROWSEINFO b;
42
43 /* Since running the native dialogs in the same thread stops GTK+
44  * loop from redrawing other windows on the background, we need
45  * to run the dialogs in a separate thread. */
46
47 /* TODO: There's a lot of code repeat in this file, it could be
48  * refactored to be neater. */
49
50 struct _WinChooserCtx {
51         void *data;
52         gboolean return_value;
53         PIDLIST_ABSOLUTE return_value_pidl;
54         gboolean done;
55 };
56
57 typedef struct _WinChooserCtx WinChooserCtx;
58
59 static void *threaded_GetOpenFileName(void *arg)
60 {
61         WinChooserCtx *ctx = (WinChooserCtx *)arg;
62
63         g_return_val_if_fail(ctx != NULL, NULL);
64         g_return_val_if_fail(ctx->data != NULL, NULL);
65
66         ctx->return_value = GetOpenFileName(ctx->data);
67         ctx->done = TRUE;
68
69         return NULL;
70 }
71
72 static void *threaded_GetSaveFileName(void *arg)
73 {
74         WinChooserCtx *ctx = (WinChooserCtx *)arg;
75
76         g_return_val_if_fail(ctx != NULL, NULL);
77         g_return_val_if_fail(ctx->data != NULL, NULL);
78
79         ctx->return_value = GetSaveFileName(ctx->data);
80         ctx->done = TRUE;
81
82         return NULL;
83 }
84
85 static void *threaded_SHBrowseForFolder(void *arg)
86 {
87         WinChooserCtx *ctx = (WinChooserCtx *)arg;
88
89         g_return_val_if_fail(ctx != NULL, NULL);
90         g_return_val_if_fail(ctx->data != NULL, NULL);
91
92         ctx->return_value_pidl = SHBrowseForFolder(ctx->data);
93
94         ctx->done = TRUE;
95
96         return NULL;
97 }
98
99 gchar *filesel_select_file_open(const gchar *title, const gchar *path)
100 {
101         gboolean ret;
102         gchar *str;
103         gunichar2 *path16, *title16;
104         glong conv_items;
105         GError *error = NULL;
106         WinChooserCtx *ctx;
107 #ifdef USE_PTHREAD
108         pthread_t pt;
109 #endif
110
111         /* Path needs to be converted to UTF-16, so that the native chooser
112          * can understand it. */
113         path16 = g_utf8_to_utf16(path, -1, NULL, &conv_items, &error);
114         if (error != NULL) {
115                 alertpanel_error(_("Could not convert file path to UTF-16:\n\n%s"),
116                                 error->message);
117                 debug_print("file path '%s' conversion to UTF-16 failed\n", path);
118                 g_error_free(error);
119                 return NULL;
120         }
121
122         /* Chooser dialog title needs to be UTF-16 as well. */
123         title16 = g_utf8_to_utf16(title, -1, NULL, NULL, &error);
124         if (error != NULL) {
125                 debug_print("dialog title '%s' conversion to UTF-16 failed\n", title);
126                 g_error_free(error);
127         }
128
129         o.lStructSize = sizeof(OPENFILENAME);
130         if (focus_window != NULL)
131                 o.hwndOwner = GDK_WINDOW_HWND(gtk_widget_get_window(focus_window));
132         else
133                 o.hwndOwner = NULL;
134         o.hInstance = NULL;
135         o.lpstrFilter = NULL;
136         o.lpstrCustomFilter = NULL;
137         o.nFilterIndex = 0;
138         o.lpstrFile = g_malloc0(MAXPATHLEN);
139         o.nMaxFile = MAXPATHLEN;
140         o.lpstrFileTitle = NULL;
141         o.lpstrInitialDir = path16;
142         o.lpstrTitle = title16;
143         o.Flags = OFN_LONGNAMES;
144
145         ctx = g_new0(WinChooserCtx, 1);
146         ctx->data = &o;
147         ctx->done = FALSE;
148
149 #ifdef USE_PTHREAD
150         if (pthread_create(&pt, PTHREAD_CREATE_JOINABLE, threaded_GetOpenFileName,
151                                 (void *)ctx) != 0) {
152                 debug_print("Couldn't run in a thread, continuing unthreaded.\n");
153                 threaded_GetOpenFileName(ctx);
154         } else {
155                 while (!ctx->done) {
156                         claws_do_idle();
157                 }
158                 pthread_join(pt, NULL);
159         }
160         ret = ctx->return_value;
161 #else
162         debug_print("No threads available, continuing unthreaded.\n");
163         ret = GetOpenFileName(&o);
164 #endif
165
166         g_free(path16);
167         g_free(title16);
168         g_free(ctx);
169
170         if (!ret) {
171                 g_free(o.lpstrFile);
172                 return NULL;
173         }
174
175         /* Now convert the returned file path back from UTF-16. */
176         str = g_utf16_to_utf8(o.lpstrFile, o.nMaxFile, NULL, NULL, &error);
177         if (error != NULL) {
178                 alertpanel_error(_("Could not convert file path back to UTF-8:\n\n%s"),
179                                 error->message);
180                 debug_print("returned file path conversion to UTF-8 failed\n");
181                 g_error_free(error);
182         }
183
184         g_free(o.lpstrFile);
185         return str;
186 }
187
188 /* TODO: Allow selecting of multiple files with OFN_ALLOWMULTISELECT
189  * flag and parsing the long string with returned file names. */
190 GList *filesel_select_multiple_files_open(const gchar *title)
191 {
192         GList *file_list = NULL;
193         gchar *ret = filesel_select_file_open(title, NULL);
194
195         if (ret != NULL)
196                 file_list = g_list_append(file_list, ret);
197
198         return file_list;
199 }
200
201 gchar *filesel_select_file_open_with_filter(const gchar *title, const gchar *path,
202                               const gchar *filter)
203 {
204         gboolean ret;
205         gchar *win_filter = NULL, *str;
206         gunichar2 *path16, *title16, *win_filter16 = NULL;
207         glong conv_items;
208         GError *error = NULL;
209         WinChooserCtx *ctx;
210 #ifdef USE_PTHREAD
211         pthread_t pt;
212 #endif
213
214         /* Path needs to be converted to UTF-16, so that the native chooser
215          * can understand it. */
216         path16 = g_utf8_to_utf16(path, -1, NULL, &conv_items, &error);
217         if (error != NULL) {
218                 alertpanel_error(_("Could not convert file path to UTF-16:\n\n%s"),
219                                 error->message);
220                 debug_print("file path '%s' conversion to UTF-16 failed\n", path);
221                 g_error_free(error);
222                 return NULL;
223         }
224
225         /* Chooser dialog title needs to be UTF-16 as well. */
226         title16 = g_utf8_to_utf16(title, -1, NULL, NULL, &error);
227         if (error != NULL) {
228                 debug_print("dialog title '%s' conversion to UTF-16 failed\n", title);
229                 g_error_free(error);
230         }
231
232         o.lStructSize = sizeof(OPENFILENAME);
233         if (focus_window != NULL)
234                 o.hwndOwner = GDK_WINDOW_HWND(gtk_widget_get_window(focus_window));
235         else
236                 o.hwndOwner = NULL;
237         o.lpstrFilter = NULL;
238         o.lpstrCustomFilter = NULL;
239         o.nFilterIndex = 0;
240         o.lpstrFile = g_malloc0(MAXPATHLEN);
241         o.nMaxFile = MAXPATHLEN;
242         o.lpstrFileTitle = NULL;
243         o.lpstrInitialDir = path16;
244         o.lpstrTitle = title16;
245         o.Flags = OFN_LONGNAMES;
246
247         if (filter != NULL && strlen(filter) > 0) {
248                 win_filter = g_strdup_printf("%s%c%s%c%c",
249                                 filter, '\0', filter, '\0', '\0');
250                 win_filter16 = g_utf8_to_utf16(win_filter, -1, NULL, NULL, &error);
251                 g_free(win_filter);
252                 if (error != NULL) {
253                         debug_print("dialog title '%s' conversion to UTF-16 failed\n", title);
254                         g_error_free(error);
255                 }
256                 o.lpstrFilter = win_filter16;
257                 o.nFilterIndex = 1;
258         }
259
260         ctx = g_new0(WinChooserCtx, 1);
261         ctx->data = &o;
262         ctx->done = FALSE;
263
264 #ifdef USE_PTHREAD
265         if (pthread_create(&pt, PTHREAD_CREATE_JOINABLE, threaded_GetOpenFileName,
266                                 (void *)ctx) != 0) {
267                 debug_print("Couldn't run in a thread, continuing unthreaded.\n");
268                 threaded_GetOpenFileName(ctx);
269         } else {
270                 while (!ctx->done) {
271                         claws_do_idle();
272                 }
273                 pthread_join(pt, NULL);
274         }
275         ret = ctx->return_value;
276 #else
277         debug_print("No threads available, continuing unthreaded.\n");
278         ret = GetOpenFileName(&o);
279 #endif
280
281         g_free(win_filter16);
282         g_free(path16);
283         g_free(title16);
284         g_free(ctx);
285
286         if (!ret) {
287                 g_free(o.lpstrFile);
288                 return NULL;
289         }
290
291         /* Now convert the returned file path back from UTF-16. */
292         str = g_utf16_to_utf8(o.lpstrFile, o.nMaxFile, NULL, NULL, &error);
293         if (error != NULL) {
294                 alertpanel_error(_("Could not convert file path back to UTF-8:\n\n%s"),
295                                 error->message);
296                 debug_print("returned file path conversion to UTF-8 failed\n");
297                 g_error_free(error);
298         }
299
300         g_free(o.lpstrFile);
301         return str;
302 }
303
304 /* TODO: Allow selecting of multiple files with OFN_ALLOWMULTISELECT
305  * flag and parsing the long string with returned file names. */
306 GList *filesel_select_multiple_files_open_with_filter(const gchar *title,
307                 const gchar *path, const gchar *filter)
308 {
309         GList *file_list = NULL;
310         gchar *ret = filesel_select_file_open_with_filter(title, path, filter);
311
312         if (ret != NULL)
313                 file_list = g_list_append(file_list, ret);
314
315         return file_list;
316 }
317
318 gchar *filesel_select_file_save(const gchar *title, const gchar *path)
319 {
320         gboolean ret;
321         gchar *str, *filename = NULL;
322         gunichar2 *filename16, *path16, *title16;
323         glong conv_items;
324         GError *error = NULL;
325         WinChooserCtx *ctx;
326 #ifdef USE_PTHREAD
327         pthread_t pt;
328 #endif
329
330         /* Find the filename part, if any */
331         if (path[strlen(path)-1] == G_DIR_SEPARATOR) {
332                 filename = "";
333         } else if ((filename = strrchr(path, G_DIR_SEPARATOR)) != NULL) {
334                 filename++;
335         } else {
336                 filename = (char *) path;
337         }
338
339         /* Convert it to UTF-16. */
340         filename16 = g_utf8_to_utf16(filename, -1, NULL, &conv_items, &error);
341         if (error != NULL) {
342                 alertpanel_error(_("Could not convert attachment name to UTF-16:\n\n%s"),
343                                 error->message);
344                 debug_print("filename '%s' conversion to UTF-16 failed\n", filename);
345                 g_error_free(error);
346                 return NULL;
347         }
348
349         /* Path needs to be converted to UTF-16, so that the native chooser
350          * can understand it. */
351         path16 = g_utf8_to_utf16(path, -1, NULL, NULL, &error);
352         if (error != NULL) {
353                 alertpanel_error(_("Could not convert file path to UTF-16:\n\n%s"),
354                                 error->message);
355                 debug_print("file path '%s' conversion to UTF-16 failed\n", path);
356                 g_error_free(error);
357                 g_free(filename16);
358                 return NULL;
359         }
360
361         /* Chooser dialog title needs to be UTF-16 as well. */
362         title16 = g_utf8_to_utf16(title, -1, NULL, NULL, &error);
363         if (error != NULL) {
364                 debug_print("dialog title '%s' conversion to UTF-16 failed\n", title);
365                 g_error_free(error);
366         }
367
368         o.lStructSize = sizeof(OPENFILENAME);
369         if (focus_window != NULL)
370                 o.hwndOwner = GDK_WINDOW_HWND(gtk_widget_get_window(focus_window));
371         else
372                 o.hwndOwner = NULL;
373         o.lpstrFilter = NULL;
374         o.lpstrCustomFilter = NULL;
375         o.lpstrFile = g_malloc0(MAXPATHLEN);
376         if (path16 != NULL)
377                 memcpy(o.lpstrFile, filename16, conv_items * sizeof(gunichar2));
378         o.nMaxFile = MAXPATHLEN;
379         o.lpstrFileTitle = NULL;
380         o.lpstrInitialDir = path16;
381         o.lpstrTitle = title16;
382         o.Flags = OFN_LONGNAMES;
383
384         ctx = g_new0(WinChooserCtx, 1);
385         ctx->data = &o;
386         ctx->return_value = FALSE;
387         ctx->done = FALSE;
388
389 #ifdef USE_PTHREAD
390         if (pthread_create(&pt, PTHREAD_CREATE_JOINABLE, threaded_GetSaveFileName,
391                                 (void *)ctx) != 0) {
392                 debug_print("Couldn't run in a thread, continuing unthreaded.\n");
393                 threaded_GetSaveFileName(ctx);
394         } else {
395                 while (!ctx->done) {
396                         claws_do_idle();
397                 }
398                 pthread_join(pt, NULL);
399         }
400         ret = ctx->return_value;
401 #else
402         debug_print("No threads available, continuing unthreaded.\n");
403         ret = GetSaveFileName(&o);
404 #endif
405
406         g_free(filename16);
407         g_free(path16);
408         g_free(title16);
409         g_free(ctx);
410
411         if (!ret) {
412                 g_free(o.lpstrFile);
413                 return NULL;
414         }
415
416         /* Now convert the returned file path back from UTF-16. */
417         str = g_utf16_to_utf8(o.lpstrFile, o.nMaxFile, NULL, NULL, &error);
418         if (error != NULL) {
419                 alertpanel_error(_("Could not convert file path back to UTF-8:\n\n%s"),
420                                 error->message);
421                 debug_print("returned file path conversion to UTF-8 failed\n");
422                 g_error_free(error);
423         }
424
425         g_free(o.lpstrFile);
426         return str;
427 }
428
429 gchar *filesel_select_file_open_folder(const gchar *title, const gchar *path)
430 {
431         PIDLIST_ABSOLUTE pidl;
432         gchar *str;
433         gunichar2 *path16, *title16;
434         glong conv_items;
435         PIDLIST_ABSOLUTE ppidl;
436         GError *error = NULL;
437         WinChooserCtx *ctx;
438 #ifdef USE_PTHREAD
439         pthread_t pt;
440 #endif
441
442         /* Path needs to be converted to UTF-16, so that the native chooser
443          * can understand it. */
444         path16 = g_utf8_to_utf16(path, -1, NULL, &conv_items, &error);
445         if (error != NULL) {
446                 alertpanel_error(_("Could not convert file path to UTF-16:\n\n%s"),
447                                 error->message);
448                 debug_print("file path '%s' conversion to UTF-16 failed\n", path);
449                 g_error_free(error);
450                 return NULL;
451         } else {
452                 /* Get a PIDL_ABSOLUTE for b.pidlRoot. */
453                 if (SHParseDisplayName(path16, NULL, &ppidl, 0, NULL) == S_OK) {
454                         b.pidlRoot = ppidl;
455                 }
456         }
457         g_free(path16);
458
459         /* Chooser dialog title needs to be UTF-16 as well. */
460         title16 = g_utf8_to_utf16(title, -1, NULL, NULL, &error);
461         if (error != NULL) {
462                 debug_print("dialog title '%s' conversion to UTF-16 failed\n", title);
463                 g_error_free(error);
464         }
465
466         if (focus_window != NULL)
467                 b.hwndOwner = GDK_WINDOW_HWND(gtk_widget_get_window(focus_window));
468         else
469                 b.hwndOwner = NULL;
470         b.pszDisplayName = g_malloc(MAXPATHLEN);
471         b.lpszTitle = title16;
472         b.ulFlags = 0;
473         b.lpfn = NULL;
474
475         CoInitialize(NULL);
476
477         ctx = g_new0(WinChooserCtx, 1);
478         ctx->data = &b;
479         ctx->done = FALSE;
480
481 #ifdef USE_PTHREAD
482         if (pthread_create(&pt, PTHREAD_CREATE_JOINABLE, threaded_SHBrowseForFolder,
483                                 (void *)ctx) != 0) {
484                 debug_print("Couldn't run in a thread, continuing unthreaded.\n");
485                 threaded_SHBrowseForFolder(ctx);
486         } else {
487                 while (!ctx->done) {
488                         claws_do_idle();
489                 }
490                 pthread_join(pt, NULL);
491         }
492         pidl = ctx->return_value_pidl;
493 #else
494         debug_print("No threads available, continuing unthreaded.\n");
495         pidl = SHBrowseForFolder(&b);
496 #endif
497
498         if (pidl == NULL) {
499                 CoUninitialize();
500                 g_free(b.pszDisplayName);
501                 g_free(ctx);
502                 return NULL;
503         }
504
505         g_free(title16);
506
507         /* Now convert the returned file path back from UTF-16. */
508         /* Unfortunately, there is no field in BROWSEINFO struct to indicate
509          * actual length of string in pszDisplayName, so we have to assume
510          * the string is null-terminated. */
511         str = g_utf16_to_utf8(b.pszDisplayName, -1, NULL, NULL, &error);
512         if (error != NULL) {
513                 alertpanel_error(_("Could not convert file path back to UTF-8:\n\n%s"),
514                                 error->message);
515                 debug_print("returned file path conversion to UTF-8 failed\n");
516                 g_error_free(error);
517         }
518         g_free(b.pszDisplayName);
519
520         CoTaskMemFree(pidl);
521         CoUninitialize();
522         g_free(ctx);
523
524         return str;
525 }
526
527 gchar *filesel_select_file_save_folder(const gchar *title, const gchar *path)
528 {
529         return filesel_select_file_open_folder(title, path);
530 }