Hopefully fixed sporadic crashes in Windows native file chooser.
[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 /* This function handles calling GetOpenFilename(), using
100  * global static variable o.
101  * It expects o.lpstrFile to point to an already allocated buffer,
102  * of size at least MAXPATHLEN. */
103 static const gboolean _file_open_dialog(const gchar *path, const gchar *title,
104                 const gchar *filter, const gboolean multi)
105 {
106         gboolean ret;
107         gunichar2 *path16 = NULL;
108         gunichar2 *title16 = NULL;
109         gunichar2 *filter16 = NULL;
110         gunichar2 *win_filter16 = NULL;
111         glong conv_items, sz;
112         GError *error = NULL;
113         WinChooserCtx *ctx;
114 #ifdef USE_PTHREAD
115         pthread_t pt;
116 #endif
117
118         /* Path needs to be converted to UTF-16, so that the native chooser
119          * can understand it. */
120         path16 = g_utf8_to_utf16(path ? path : "",
121                         -1, NULL, NULL, &error);
122         if (error != NULL) {
123                 alertpanel_error(_("Could not convert file path to UTF-16:\n\n%s"),
124                                 error->message);
125                 debug_print("file path '%s' conversion to UTF-16 failed\n", path);
126                 g_error_free(error);
127                 error = NULL;
128                 return FALSE;
129         }
130
131         /* Chooser dialog title needs to be UTF-16 as well. */
132         title16 = g_utf8_to_utf16(title ? title : "",
133                         -1, NULL, NULL, &error);
134         if (error != NULL) {
135                 debug_print("dialog title '%s' conversion to UTF-16 failed\n", title);
136                 g_error_free(error);
137                 error = NULL;
138         }
139
140         o.lStructSize = sizeof(OPENFILENAME);
141         if (focus_window != NULL)
142                 o.hwndOwner = GDK_WINDOW_HWND(gtk_widget_get_window(focus_window));
143         else
144                 o.hwndOwner = NULL;
145         o.hInstance = NULL;
146         o.lpstrFilter = NULL;
147         o.lpstrCustomFilter = NULL;
148         o.nFilterIndex = 0;
149         o.nMaxFile = MAXPATHLEN;
150         o.lpstrFileTitle = NULL;
151         o.lpstrInitialDir = path16;
152         o.lpstrTitle = title16;
153         if (multi)
154                 o.Flags = OFN_LONGNAMES | OFN_EXPLORER | OFN_ALLOWMULTISELECT;
155         else
156                 o.Flags = OFN_LONGNAMES | OFN_EXPLORER;
157
158         if (filter != NULL && strlen(filter) > 0) {
159                 debug_print("Setting filter '%s'\n", filter);
160                 filter16 = g_utf8_to_utf16(filter, -1, NULL, &conv_items, &error);
161                 /* We're creating a UTF16 (2 bytes for each character) string:
162                  * "filter\0filter\0\0"
163                  * As g_utf8_to_utf16() will stop on first null byte, even if
164                  * we pass string length in its second argument, we have to
165                  * construct this string manually.
166                  * conv_items contains number of UTF16 characters of our filter.
167                  * Therefore we need enough bytes to store the filter string twice
168                  * and three null chars. */
169                 sz = sizeof(gunichar2);
170                 win_filter16 = g_malloc0(conv_items*sz*2 + sz*3);
171                 memcpy(win_filter16, filter16, conv_items*sz);
172                 memcpy(win_filter16 + conv_items*sz + sz, filter16, conv_items*sz);
173                 g_free(filter16);
174
175                 if (error != NULL) {
176                         debug_print("dialog title '%s' conversion to UTF-16 failed\n", title);
177                         g_error_free(error);
178                         error = NULL;
179                 }
180                 o.lpstrFilter = (LPCTSTR)win_filter16;
181                 o.nFilterIndex = 1;
182         }
183
184         ctx = g_new0(WinChooserCtx, 1);
185         ctx->data = &o;
186         ctx->done = FALSE;
187
188 #ifdef USE_PTHREAD
189         if (pthread_create(&pt, PTHREAD_CREATE_JOINABLE, threaded_GetOpenFileName,
190                                 (void *)ctx) != 0) {
191                 debug_print("Couldn't run in a thread, continuing unthreaded.\n");
192                 threaded_GetOpenFileName(ctx);
193         } else {
194                 while (!ctx->done) {
195                         claws_do_idle();
196                 }
197                 pthread_join(pt, NULL);
198         }
199         ret = ctx->return_value;
200 #else
201         debug_print("No threads available, continuing unthreaded.\n");
202         ret = GetOpenFileName(&o);
203 #endif
204
205         g_free(win_filter16);
206         if (path16 != NULL) {
207                 g_free(path16);
208         }
209         g_free(title16);
210         g_free(ctx);
211
212         return ret;
213 }
214
215 gchar *filesel_select_file_open_with_filter(const gchar *title, const gchar *path,
216                               const gchar *filter)
217 {
218         gchar *str = NULL;
219         GError *error = NULL;
220
221         o.lpstrFile = g_malloc0(MAXPATHLEN);
222         if (!_file_open_dialog(path, title, filter, FALSE)) {
223                 g_free(o.lpstrFile);
224                 return NULL;
225         }
226
227         /* Now convert the returned file path back from UTF-16. */
228         str = g_utf16_to_utf8(o.lpstrFile, o.nMaxFile, NULL, NULL, &error);
229         if (error != NULL) {
230                 alertpanel_error(_("Could not convert file path back to UTF-8:\n\n%s"),
231                                 error->message);
232                 debug_print("returned file path conversion to UTF-8 failed\n");
233                 g_error_free(error);
234         }
235
236         g_free(o.lpstrFile);
237         return str;
238 }
239
240 GList *filesel_select_multiple_files_open_with_filter(const gchar *title,
241                 const gchar *path, const gchar *filter)
242 {
243         GList *file_list = NULL;
244         gchar *str = NULL;
245         gchar *dir = NULL;
246         gunichar2 *f;
247         GError *error = NULL;
248         glong n, items_read;
249
250         o.lpstrFile = g_malloc0(MAXPATHLEN);
251         if (!_file_open_dialog(path, title, filter, TRUE)) {
252                 g_free(o.lpstrFile);
253                 return NULL;
254         }
255
256         /* Now convert the returned directory and file names back from UTF-16.
257          * The content of o.lpstrFile is:
258          * "directory0file0file0...0file00" for multiple files selected,
259          * "fullfilepath0" for single file. */
260         str = g_utf16_to_utf8(o.lpstrFile, -1, &items_read, NULL, &error);
261         g_free(o.lpstrFile);
262
263         if (error != NULL) {
264                 alertpanel_error(_("Could not convert file path back to UTF-8:\n\n%s"),
265                                 error->message);
266                 debug_print("returned file path conversion to UTF-8 failed\n");
267                 g_error_free(error);
268                 return NULL;
269         }
270
271         /* The part before the first null char is always a full path. If it is
272          * a path to a file, then only this one file has been selected,
273          * and we can bail out early. */
274         if (g_file_test(str, G_FILE_TEST_IS_REGULAR)) {
275                 debug_print("Selected one file: '%s'\n", str);
276                 file_list = g_list_append(file_list, g_strdup(str));
277                 g_free(str);
278                 return file_list;
279         }
280
281         /* So the path was to a directory. We need to parse more after
282          * the fist null char, until we get to two null chars in a row. */
283         dir = g_strdup(str);
284         g_free(str);
285         debug_print("Selected multiple files in dir '%s'\n", dir);
286
287         n = items_read + 1;
288         f = &o.lpstrFile[n];
289         while (items_read > 0) {
290                 str = g_utf16_to_utf8(f, -1, &items_read, NULL, &error);
291                 if (error != NULL) {
292                         alertpanel_error(_("Could not convert file path back to UTF-8:\n\n%s"),
293                                         error->message);
294                         debug_print("returned file path conversion to UTF-8 failed\n");
295                         g_error_free(error);
296                         return NULL;
297                 }
298
299                 if (items_read > 0) {
300                         debug_print("selected file '%s'\n", str);
301                         file_list = g_list_append(file_list,
302                                         g_strconcat(dir, G_DIR_SEPARATOR_S, str, NULL));
303                 }
304
305                 n += items_read + 1;
306                 f = &o.lpstrFile[n];
307         }
308
309
310
311
312
313         return file_list;
314 }
315
316 gchar *filesel_select_file_open(const gchar *title, const gchar *path)
317 {
318         return filesel_select_file_open_with_filter(title, path, NULL);
319 }
320
321 GList *filesel_select_multiple_files_open(const gchar *title, const gchar *path)
322 {
323         return filesel_select_multiple_files_open_with_filter(title, path, NULL);
324 }
325
326 gchar *filesel_select_file_save(const gchar *title, const gchar *path)
327 {
328         gboolean ret;
329         gchar *str, *filename = NULL;
330         gunichar2 *filename16, *path16, *title16;
331         glong conv_items;
332         GError *error = NULL;
333         WinChooserCtx *ctx;
334 #ifdef USE_PTHREAD
335         pthread_t pt;
336 #endif
337
338         /* Find the filename part, if any */
339         if (path == NULL || path[strlen(path)-1] == G_DIR_SEPARATOR) {
340                 filename = "";
341         } else if ((filename = strrchr(path, G_DIR_SEPARATOR)) != NULL) {
342                 filename++;
343         } else {
344                 filename = (char *) path;
345         }
346
347         /* Convert it to UTF-16. */
348         filename16 = g_utf8_to_utf16(filename, -1, NULL, &conv_items, &error);
349         if (error != NULL) {
350                 alertpanel_error(_("Could not convert attachment name to UTF-16:\n\n%s"),
351                                 error->message);
352                 debug_print("filename '%s' conversion to UTF-16 failed\n", filename);
353                 g_error_free(error);
354                 return NULL;
355         }
356
357         /* Path needs to be converted to UTF-16, so that the native chooser
358          * can understand it. */
359         path16 = g_utf8_to_utf16(path, -1, NULL, NULL, &error);
360         if (error != NULL) {
361                 alertpanel_error(_("Could not convert file path to UTF-16:\n\n%s"),
362                                 error->message);
363                 debug_print("file path '%s' conversion to UTF-16 failed\n", path);
364                 g_error_free(error);
365                 g_free(filename16);
366                 return NULL;
367         }
368
369         /* Chooser dialog title needs to be UTF-16 as well. */
370         title16 = g_utf8_to_utf16(title, -1, NULL, NULL, &error);
371         if (error != NULL) {
372                 debug_print("dialog title '%s' conversion to UTF-16 failed\n", title);
373                 g_error_free(error);
374         }
375
376         o.lStructSize = sizeof(OPENFILENAME);
377         if (focus_window != NULL)
378                 o.hwndOwner = GDK_WINDOW_HWND(gtk_widget_get_window(focus_window));
379         else
380                 o.hwndOwner = NULL;
381         o.lpstrFilter = NULL;
382         o.lpstrCustomFilter = NULL;
383         o.lpstrFile = g_malloc0(MAXPATHLEN);
384         if (path16 != NULL)
385                 memcpy(o.lpstrFile, filename16, conv_items * sizeof(gunichar2));
386         o.nMaxFile = MAXPATHLEN;
387         o.lpstrFileTitle = NULL;
388         o.lpstrInitialDir = path16;
389         o.lpstrTitle = title16;
390         o.Flags = OFN_LONGNAMES | OFN_EXPLORER;
391
392         ctx = g_new0(WinChooserCtx, 1);
393         ctx->data = &o;
394         ctx->return_value = FALSE;
395         ctx->done = FALSE;
396
397 #ifdef USE_PTHREAD
398         if (pthread_create(&pt, PTHREAD_CREATE_JOINABLE, threaded_GetSaveFileName,
399                                 (void *)ctx) != 0) {
400                 debug_print("Couldn't run in a thread, continuing unthreaded.\n");
401                 threaded_GetSaveFileName(ctx);
402         } else {
403                 while (!ctx->done) {
404                         claws_do_idle();
405                 }
406                 pthread_join(pt, NULL);
407         }
408         ret = ctx->return_value;
409 #else
410         debug_print("No threads available, continuing unthreaded.\n");
411         ret = GetSaveFileName(&o);
412 #endif
413
414         g_free(filename16);
415         g_free(path16);
416         g_free(title16);
417         g_free(ctx);
418
419         if (!ret) {
420                 g_free(o.lpstrFile);
421                 return NULL;
422         }
423
424         /* Now convert the returned file path back from UTF-16. */
425         str = g_utf16_to_utf8(o.lpstrFile, o.nMaxFile, NULL, NULL, &error);
426         if (error != NULL) {
427                 alertpanel_error(_("Could not convert file path back to UTF-8:\n\n%s"),
428                                 error->message);
429                 debug_print("returned file path conversion to UTF-8 failed\n");
430                 g_error_free(error);
431         }
432
433         g_free(o.lpstrFile);
434         return str;
435 }
436
437 /* This callback function is used to set the folder browse dialog
438  * selection from filesel_select_file_open_folder() to set
439  * chosen starting folder ("path" argument to that function. */
440 static int CALLBACK _open_folder_callback(HWND hwnd, UINT uMsg,
441                 LPARAM lParam, LPARAM lpData)
442 {
443         if (uMsg != BFFM_INITIALIZED)
444                 return 0;
445
446         SendMessage(hwnd, BFFM_SETSELECTION, TRUE, lpData);
447         return 0;
448 }
449
450 gchar *filesel_select_file_open_folder(const gchar *title, const gchar *path)
451 {
452         PIDLIST_ABSOLUTE pidl;
453         gchar *str;
454         gunichar2 *path16, *title16;
455         glong conv_items;
456         GError *error = NULL;
457         WinChooserCtx *ctx;
458 #ifdef USE_PTHREAD
459         pthread_t pt;
460 #endif
461
462         /* Path needs to be converted to UTF-16, so that the native chooser
463          * can understand it. */
464         path16 = g_utf8_to_utf16(path ? path : "",
465                         -1, NULL, &conv_items, &error);
466         if (error != NULL) {
467                 alertpanel_error(_("Could not convert file path to UTF-16:\n\n%s"),
468                                 error->message);
469                 debug_print("file path '%s' conversion to UTF-16 failed\n", path);
470                 g_error_free(error);
471                 return NULL;
472         }
473
474         /* Chooser dialog title needs to be UTF-16 as well. */
475         title16 = g_utf8_to_utf16(title ? title : "",
476                         -1, NULL, NULL, &error);
477         if (error != NULL) {
478                 debug_print("dialog title '%s' conversion to UTF-16 failed\n", title);
479                 g_error_free(error);
480         }
481
482         if (focus_window != NULL)
483                 b.hwndOwner = GDK_WINDOW_HWND(gtk_widget_get_window(focus_window));
484         else
485                 b.hwndOwner = NULL;
486         b.pszDisplayName = g_malloc(MAXPATHLEN);
487         b.lpszTitle = title16;
488         b.ulFlags = 0;
489         b.pidlRoot = NULL;
490         b.lpfn = _open_folder_callback;
491         b.lParam = (LPARAM)path16;
492
493         CoInitialize(NULL);
494
495         ctx = g_new0(WinChooserCtx, 1);
496         ctx->data = &b;
497         ctx->done = FALSE;
498
499 #ifdef USE_PTHREAD
500         if (pthread_create(&pt, PTHREAD_CREATE_JOINABLE, threaded_SHBrowseForFolder,
501                                 (void *)ctx) != 0) {
502                 debug_print("Couldn't run in a thread, continuing unthreaded.\n");
503                 threaded_SHBrowseForFolder(ctx);
504         } else {
505                 while (!ctx->done) {
506                         claws_do_idle();
507                 }
508                 pthread_join(pt, NULL);
509         }
510         pidl = ctx->return_value_pidl;
511 #else
512         debug_print("No threads available, continuing unthreaded.\n");
513         pidl = SHBrowseForFolder(&b);
514 #endif
515
516         g_free(b.pszDisplayName);
517         g_free(title16);
518         g_free(path16);
519
520         if (pidl == NULL) {
521                 CoUninitialize();
522                 g_free(ctx);
523                 return NULL;
524         }
525
526         path16 = malloc(MAX_PATH);
527         if (!SHGetPathFromIDList(pidl, path16)) {
528                 CoTaskMemFree(pidl);
529                 CoUninitialize();
530                 g_free(path16);
531                 g_free(ctx);
532                 return NULL;
533         }
534
535         /* Now convert the returned file path back from UTF-16. */
536         /* Unfortunately, there is no field in BROWSEINFO struct to indicate
537          * actual length of string in pszDisplayName, so we have to assume
538          * the string is null-terminated. */
539         str = g_utf16_to_utf8(path16, -1, NULL, NULL, &error);
540         if (error != NULL) {
541                 alertpanel_error(_("Could not convert file path back to UTF-8:\n\n%s"),
542                                 error->message);
543                 debug_print("returned file path conversion to UTF-8 failed\n");
544                 g_error_free(error);
545         }
546         CoTaskMemFree(pidl);
547         CoUninitialize();
548         g_free(ctx);
549         g_free(path16);
550
551         return str;
552 }
553
554 gchar *filesel_select_file_save_folder(const gchar *title, const gchar *path)
555 {
556         return filesel_select_file_open_folder(title, path);
557 }