Fix filesel_select_file_open_folder() for Win32.
[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 *str;
206         gchar *win_filter16 = NULL;
207         gunichar2 *path16, *title16, *filter16;
208         glong conv_items;
209         guint sz;
210         GError *error = NULL;
211         WinChooserCtx *ctx;
212 #ifdef USE_PTHREAD
213         pthread_t pt;
214 #endif
215
216         /* Path needs to be converted to UTF-16, so that the native chooser
217          * can understand it. */
218         path16 = g_utf8_to_utf16(path, -1, NULL, NULL, &error);
219         if (error != NULL) {
220                 alertpanel_error(_("Could not convert file path to UTF-16:\n\n%s"),
221                                 error->message);
222                 debug_print("file path '%s' conversion to UTF-16 failed\n", path);
223                 g_error_free(error);
224                 return NULL;
225         }
226
227         /* Chooser dialog title needs to be UTF-16 as well. */
228         title16 = g_utf8_to_utf16(title, -1, NULL, NULL, &error);
229         if (error != NULL) {
230                 debug_print("dialog title '%s' conversion to UTF-16 failed\n", title);
231                 g_error_free(error);
232         }
233
234         o.lStructSize = sizeof(OPENFILENAME);
235         if (focus_window != NULL)
236                 o.hwndOwner = GDK_WINDOW_HWND(gtk_widget_get_window(focus_window));
237         else
238                 o.hwndOwner = NULL;
239         o.lpstrFilter = NULL;
240         o.lpstrCustomFilter = NULL;
241         o.nFilterIndex = 0;
242         o.lpstrFile = g_malloc0(MAXPATHLEN);
243         o.nMaxFile = MAXPATHLEN;
244         o.lpstrFileTitle = NULL;
245         o.lpstrInitialDir = path16;
246         o.lpstrTitle = title16;
247         o.Flags = OFN_LONGNAMES;
248
249         if (filter != NULL && strlen(filter) > 0) {
250                 debug_print("Setting filter '%s'\n", filter);
251                 filter16 = g_utf8_to_utf16(filter, -1, NULL, &conv_items, &error);
252                 /* We're creating a UTF16 (2 bytes for each character) string:
253                  * "filter\0filter\0\0"
254                  * As g_utf8_to_utf16() will stop on first null byte, even if
255                  * we pass string length in its second argument, we have to
256                  * construct this string manually.
257                  * conv_items contains number of UTF16 characters of our filter.
258                  * Therefore we need enough bytes to store the filter string twice
259                  * and three null chars. */
260                 sz = sizeof(gunichar2);
261                 win_filter16 = g_malloc0(conv_items*sz*2 + sz*3);
262                 memcpy(win_filter16, filter16, conv_items*sz);
263                 memcpy(win_filter16 + conv_items*sz + sz, filter16, conv_items*sz);
264                 g_free(filter16);
265
266                 if (error != NULL) {
267                         debug_print("dialog title '%s' conversion to UTF-16 failed\n", title);
268                         g_error_free(error);
269                 }
270                 o.lpstrFilter = (LPCTSTR)win_filter16;
271                 o.nFilterIndex = 1;
272         }
273
274         ctx = g_new0(WinChooserCtx, 1);
275         ctx->data = &o;
276         ctx->done = FALSE;
277
278 #ifdef USE_PTHREAD
279         if (pthread_create(&pt, PTHREAD_CREATE_JOINABLE, threaded_GetOpenFileName,
280                                 (void *)ctx) != 0) {
281                 debug_print("Couldn't run in a thread, continuing unthreaded.\n");
282                 threaded_GetOpenFileName(ctx);
283         } else {
284                 while (!ctx->done) {
285                         claws_do_idle();
286                 }
287                 pthread_join(pt, NULL);
288         }
289         ret = ctx->return_value;
290 #else
291         debug_print("No threads available, continuing unthreaded.\n");
292         ret = GetOpenFileName(&o);
293 #endif
294
295         g_free(win_filter16);
296         g_free(path16);
297         g_free(title16);
298         g_free(ctx);
299
300         if (!ret) {
301                 g_free(o.lpstrFile);
302                 return NULL;
303         }
304
305         /* Now convert the returned file path back from UTF-16. */
306         str = g_utf16_to_utf8(o.lpstrFile, o.nMaxFile, NULL, NULL, &error);
307         if (error != NULL) {
308                 alertpanel_error(_("Could not convert file path back to UTF-8:\n\n%s"),
309                                 error->message);
310                 debug_print("returned file path conversion to UTF-8 failed\n");
311                 g_error_free(error);
312         }
313
314         g_free(o.lpstrFile);
315         return str;
316 }
317
318 /* TODO: Allow selecting of multiple files with OFN_ALLOWMULTISELECT
319  * flag and parsing the long string with returned file names. */
320 GList *filesel_select_multiple_files_open_with_filter(const gchar *title,
321                 const gchar *path, const gchar *filter)
322 {
323         GList *file_list = NULL;
324         gchar *ret = filesel_select_file_open_with_filter(title, path, filter);
325
326         if (ret != NULL)
327                 file_list = g_list_append(file_list, ret);
328
329         return file_list;
330 }
331
332 gchar *filesel_select_file_save(const gchar *title, const gchar *path)
333 {
334         gboolean ret;
335         gchar *str, *filename = NULL;
336         gunichar2 *filename16, *path16, *title16;
337         glong conv_items;
338         GError *error = NULL;
339         WinChooserCtx *ctx;
340 #ifdef USE_PTHREAD
341         pthread_t pt;
342 #endif
343
344         /* Find the filename part, if any */
345         if (path[strlen(path)-1] == G_DIR_SEPARATOR) {
346                 filename = "";
347         } else if ((filename = strrchr(path, G_DIR_SEPARATOR)) != NULL) {
348                 filename++;
349         } else {
350                 filename = (char *) path;
351         }
352
353         /* Convert it to UTF-16. */
354         filename16 = g_utf8_to_utf16(filename, -1, NULL, &conv_items, &error);
355         if (error != NULL) {
356                 alertpanel_error(_("Could not convert attachment name to UTF-16:\n\n%s"),
357                                 error->message);
358                 debug_print("filename '%s' conversion to UTF-16 failed\n", filename);
359                 g_error_free(error);
360                 return NULL;
361         }
362
363         /* Path needs to be converted to UTF-16, so that the native chooser
364          * can understand it. */
365         path16 = g_utf8_to_utf16(path, -1, NULL, NULL, &error);
366         if (error != NULL) {
367                 alertpanel_error(_("Could not convert file path to UTF-16:\n\n%s"),
368                                 error->message);
369                 debug_print("file path '%s' conversion to UTF-16 failed\n", path);
370                 g_error_free(error);
371                 g_free(filename16);
372                 return NULL;
373         }
374
375         /* Chooser dialog title needs to be UTF-16 as well. */
376         title16 = g_utf8_to_utf16(title, -1, NULL, NULL, &error);
377         if (error != NULL) {
378                 debug_print("dialog title '%s' conversion to UTF-16 failed\n", title);
379                 g_error_free(error);
380         }
381
382         o.lStructSize = sizeof(OPENFILENAME);
383         if (focus_window != NULL)
384                 o.hwndOwner = GDK_WINDOW_HWND(gtk_widget_get_window(focus_window));
385         else
386                 o.hwndOwner = NULL;
387         o.lpstrFilter = NULL;
388         o.lpstrCustomFilter = NULL;
389         o.lpstrFile = g_malloc0(MAXPATHLEN);
390         if (path16 != NULL)
391                 memcpy(o.lpstrFile, filename16, conv_items * sizeof(gunichar2));
392         o.nMaxFile = MAXPATHLEN;
393         o.lpstrFileTitle = NULL;
394         o.lpstrInitialDir = path16;
395         o.lpstrTitle = title16;
396         o.Flags = OFN_LONGNAMES;
397
398         ctx = g_new0(WinChooserCtx, 1);
399         ctx->data = &o;
400         ctx->return_value = FALSE;
401         ctx->done = FALSE;
402
403 #ifdef USE_PTHREAD
404         if (pthread_create(&pt, PTHREAD_CREATE_JOINABLE, threaded_GetSaveFileName,
405                                 (void *)ctx) != 0) {
406                 debug_print("Couldn't run in a thread, continuing unthreaded.\n");
407                 threaded_GetSaveFileName(ctx);
408         } else {
409                 while (!ctx->done) {
410                         claws_do_idle();
411                 }
412                 pthread_join(pt, NULL);
413         }
414         ret = ctx->return_value;
415 #else
416         debug_print("No threads available, continuing unthreaded.\n");
417         ret = GetSaveFileName(&o);
418 #endif
419
420         g_free(filename16);
421         g_free(path16);
422         g_free(title16);
423         g_free(ctx);
424
425         if (!ret) {
426                 g_free(o.lpstrFile);
427                 return NULL;
428         }
429
430         /* Now convert the returned file path back from UTF-16. */
431         str = g_utf16_to_utf8(o.lpstrFile, o.nMaxFile, NULL, NULL, &error);
432         if (error != NULL) {
433                 alertpanel_error(_("Could not convert file path back to UTF-8:\n\n%s"),
434                                 error->message);
435                 debug_print("returned file path conversion to UTF-8 failed\n");
436                 g_error_free(error);
437         }
438
439         g_free(o.lpstrFile);
440         return str;
441 }
442
443 /* This callback function is used to set the folder browse dialog
444  * selection from filesel_select_file_open_folder() to set
445  * chosen starting folder ("path" argument to that function. */
446 static int CALLBACK _open_folder_callback(HWND hwnd, UINT uMsg,
447                 LPARAM lParam, LPARAM lpData)
448 {
449         if (uMsg != BFFM_INITIALIZED)
450                 return 0;
451
452         SendMessage(hwnd, BFFM_SETSELECTION, TRUE, lpData);
453         return 0;
454 }
455
456 gchar *filesel_select_file_open_folder(const gchar *title, const gchar *path)
457 {
458         PIDLIST_ABSOLUTE pidl;
459         gchar *str;
460         gunichar2 *path16, *title16;
461         glong conv_items;
462         GError *error = NULL;
463         WinChooserCtx *ctx;
464 #ifdef USE_PTHREAD
465         pthread_t pt;
466 #endif
467
468         /* Path needs to be converted to UTF-16, so that the native chooser
469          * can understand it. */
470         path16 = g_utf8_to_utf16(path, -1, NULL, &conv_items, &error);
471         if (error != NULL) {
472                 alertpanel_error(_("Could not convert file path to UTF-16:\n\n%s"),
473                                 error->message);
474                 debug_print("file path '%s' conversion to UTF-16 failed\n", path);
475                 g_error_free(error);
476                 return NULL;
477         }
478
479         /* Chooser dialog title needs to be UTF-16 as well. */
480         title16 = g_utf8_to_utf16(title, -1, NULL, NULL, &error);
481         if (error != NULL) {
482                 debug_print("dialog title '%s' conversion to UTF-16 failed\n", title);
483                 g_error_free(error);
484         }
485
486         if (focus_window != NULL)
487                 b.hwndOwner = GDK_WINDOW_HWND(gtk_widget_get_window(focus_window));
488         else
489                 b.hwndOwner = NULL;
490         b.pszDisplayName = g_malloc(MAXPATHLEN);
491         b.lpszTitle = title16;
492         b.ulFlags = 0;
493         b.pidlRoot = NULL;
494         b.lpfn = _open_folder_callback;
495         b.lParam = (LPARAM)path16;
496
497         CoInitialize(NULL);
498
499         ctx = g_new0(WinChooserCtx, 1);
500         ctx->data = &b;
501         ctx->done = FALSE;
502
503 #ifdef USE_PTHREAD
504         if (pthread_create(&pt, PTHREAD_CREATE_JOINABLE, threaded_SHBrowseForFolder,
505                                 (void *)ctx) != 0) {
506                 debug_print("Couldn't run in a thread, continuing unthreaded.\n");
507                 threaded_SHBrowseForFolder(ctx);
508         } else {
509                 while (!ctx->done) {
510                         claws_do_idle();
511                 }
512                 pthread_join(pt, NULL);
513         }
514         pidl = ctx->return_value_pidl;
515 #else
516         debug_print("No threads available, continuing unthreaded.\n");
517         pidl = SHBrowseForFolder(&b);
518 #endif
519
520         g_free(b.pszDisplayName);
521         g_free(title16);
522         g_free(path16);
523
524         if (pidl == NULL) {
525                 CoUninitialize();
526                 g_free(ctx);
527                 return NULL;
528         }
529
530         path16 = malloc(MAX_PATH);
531         if (!SHGetPathFromIDList(pidl, path16)) {
532                 CoTaskMemFree(pidl);
533                 CoUninitialize();
534                 g_free(path16);
535                 g_free(ctx);
536                 return NULL;
537         }
538
539         /* Now convert the returned file path back from UTF-16. */
540         /* Unfortunately, there is no field in BROWSEINFO struct to indicate
541          * actual length of string in pszDisplayName, so we have to assume
542          * the string is null-terminated. */
543         str = g_utf16_to_utf8(path16, -1, NULL, NULL, &error);
544         if (error != NULL) {
545                 alertpanel_error(_("Could not convert file path back to UTF-8:\n\n%s"),
546                                 error->message);
547                 debug_print("returned file path conversion to UTF-8 failed\n");
548                 g_error_free(error);
549         }
550         CoTaskMemFree(pidl);
551         CoUninitialize();
552         g_free(ctx);
553         g_free(path16);
554
555         return str;
556 }
557
558 gchar *filesel_select_file_save_folder(const gchar *title, const gchar *path)
559 {
560         return filesel_select_file_open_folder(title, path);
561 }