fdaac43fbce1326906ec51e7ec9d8f1fdc00f056
[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 gchar *filesel_select_file_open_folder(const gchar *title, const gchar *path)
444 {
445         PIDLIST_ABSOLUTE pidl;
446         gchar *str;
447         gunichar2 *path16, *title16;
448         glong conv_items;
449         PIDLIST_ABSOLUTE ppidl;
450         GError *error = NULL;
451         WinChooserCtx *ctx;
452 #ifdef USE_PTHREAD
453         pthread_t pt;
454 #endif
455
456         /* Path needs to be converted to UTF-16, so that the native chooser
457          * can understand it. */
458         path16 = g_utf8_to_utf16(path, -1, NULL, &conv_items, &error);
459         if (error != NULL) {
460                 alertpanel_error(_("Could not convert file path to UTF-16:\n\n%s"),
461                                 error->message);
462                 debug_print("file path '%s' conversion to UTF-16 failed\n", path);
463                 g_error_free(error);
464                 return NULL;
465         } else {
466                 /* Get a PIDL_ABSOLUTE for b.pidlRoot. */
467                 if (SHParseDisplayName(path16, NULL, &ppidl, 0, NULL) == S_OK) {
468                         b.pidlRoot = ppidl;
469                 }
470         }
471         g_free(path16);
472
473         /* Chooser dialog title needs to be UTF-16 as well. */
474         title16 = g_utf8_to_utf16(title, -1, NULL, NULL, &error);
475         if (error != NULL) {
476                 debug_print("dialog title '%s' conversion to UTF-16 failed\n", title);
477                 g_error_free(error);
478         }
479
480         if (focus_window != NULL)
481                 b.hwndOwner = GDK_WINDOW_HWND(gtk_widget_get_window(focus_window));
482         else
483                 b.hwndOwner = NULL;
484         b.pszDisplayName = g_malloc(MAXPATHLEN);
485         b.lpszTitle = title16;
486         b.ulFlags = 0;
487         b.lpfn = NULL;
488
489         CoInitialize(NULL);
490
491         ctx = g_new0(WinChooserCtx, 1);
492         ctx->data = &b;
493         ctx->done = FALSE;
494
495 #ifdef USE_PTHREAD
496         if (pthread_create(&pt, PTHREAD_CREATE_JOINABLE, threaded_SHBrowseForFolder,
497                                 (void *)ctx) != 0) {
498                 debug_print("Couldn't run in a thread, continuing unthreaded.\n");
499                 threaded_SHBrowseForFolder(ctx);
500         } else {
501                 while (!ctx->done) {
502                         claws_do_idle();
503                 }
504                 pthread_join(pt, NULL);
505         }
506         pidl = ctx->return_value_pidl;
507 #else
508         debug_print("No threads available, continuing unthreaded.\n");
509         pidl = SHBrowseForFolder(&b);
510 #endif
511
512         if (pidl == NULL) {
513                 CoUninitialize();
514                 g_free(b.pszDisplayName);
515                 g_free(ctx);
516                 return NULL;
517         }
518
519         g_free(title16);
520
521         /* Now convert the returned file path back from UTF-16. */
522         /* Unfortunately, there is no field in BROWSEINFO struct to indicate
523          * actual length of string in pszDisplayName, so we have to assume
524          * the string is null-terminated. */
525         str = g_utf16_to_utf8(b.pszDisplayName, -1, NULL, NULL, &error);
526         if (error != NULL) {
527                 alertpanel_error(_("Could not convert file path back to UTF-8:\n\n%s"),
528                                 error->message);
529                 debug_print("returned file path conversion to UTF-8 failed\n");
530                 g_error_free(error);
531         }
532         g_free(b.pszDisplayName);
533
534         CoTaskMemFree(pidl);
535         CoUninitialize();
536         g_free(ctx);
537
538         return str;
539 }
540
541 gchar *filesel_select_file_save_folder(const gchar *title, const gchar *path)
542 {
543         return filesel_select_file_open_folder(title, path);
544 }