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