1 /*
2   Native File Dialog
3 
4   http://www.frogtoss.com/labs
5 */
6 
7 #include <stdio.h>
8 #include <assert.h>
9 #include <string.h>
10 #include <gtk/gtk.h>
11 #include "nfd.h"
12 #include "nfd_common.h"
13 
14 
15 const char INIT_FAIL_MSG[] = "gtk_init_check failed to initilaize GTK+";
16 
17 
AddTypeToFilterName(const char * typebuf,char * filterName,size_t bufsize)18 static void AddTypeToFilterName( const char *typebuf, char *filterName, size_t bufsize )
19 {
20     const char SEP[] = ", ";
21 
22     size_t len = strlen(filterName);
23     if ( len != 0 )
24     {
25         strncat( filterName, SEP, bufsize - len - 1 );
26         len += strlen(SEP);
27     }
28 
29     strncat( filterName, typebuf, bufsize - len - 1 );
30 }
31 
AddFiltersToDialog(GtkWidget * dialog,const char * filterList)32 static void AddFiltersToDialog( GtkWidget *dialog, const char *filterList )
33 {
34     GtkFileFilter *filter;
35     char typebuf[NFD_MAX_STRLEN] = {0};
36     const char *p_filterList = filterList;
37     char *p_typebuf = typebuf;
38     char filterName[NFD_MAX_STRLEN] = {0};
39 
40     if ( !filterList || strlen(filterList) == 0 )
41         return;
42 
43     filter = gtk_file_filter_new();
44     while ( 1 )
45     {
46 
47         if ( NFDi_IsFilterSegmentChar(*p_filterList) )
48         {
49             char typebufWildcard[NFD_MAX_STRLEN];
50             /* add another type to the filter */
51             assert( strlen(typebuf) > 0 );
52             assert( strlen(typebuf) < NFD_MAX_STRLEN-1 );
53 
54             snprintf( typebufWildcard, NFD_MAX_STRLEN, "*.%s", typebuf );
55             AddTypeToFilterName( typebuf, filterName, NFD_MAX_STRLEN );
56 
57             gtk_file_filter_add_pattern( filter, typebufWildcard );
58 
59             p_typebuf = typebuf;
60             memset( typebuf, 0, sizeof(char) * NFD_MAX_STRLEN );
61         }
62 
63         if ( *p_filterList == ';' || *p_filterList == '\0' )
64         {
65             /* end of filter -- add it to the dialog */
66 
67             gtk_file_filter_set_name( filter, filterName );
68             gtk_file_chooser_add_filter( GTK_FILE_CHOOSER(dialog), filter );
69 
70             filterName[0] = '\0';
71 
72             if ( *p_filterList == '\0' )
73                 break;
74 
75             filter = gtk_file_filter_new();
76         }
77 
78         if ( !NFDi_IsFilterSegmentChar( *p_filterList ) )
79         {
80             *p_typebuf = *p_filterList;
81             p_typebuf++;
82         }
83 
84         p_filterList++;
85     }
86 
87     /* always append a wildcard option to the end*/
88 
89     filter = gtk_file_filter_new();
90     gtk_file_filter_set_name( filter, "*.*" );
91     gtk_file_filter_add_pattern( filter, "*" );
92     gtk_file_chooser_add_filter( GTK_FILE_CHOOSER(dialog), filter );
93 }
94 
SetDefaultPath(GtkWidget * dialog,const char * defaultPath)95 static void SetDefaultPath( GtkWidget *dialog, const char *defaultPath )
96 {
97     if ( !defaultPath || strlen(defaultPath) == 0 )
98         return;
99 
100     /* GTK+ manual recommends not specifically setting the default path.
101        We do it anyway in order to be consistent across platforms.
102 
103        If consistency with the native OS is preferred, this is the line
104        to comment out. -ml */
105     gtk_file_chooser_set_current_folder( GTK_FILE_CHOOSER(dialog), defaultPath );
106 }
107 
AllocPathSet(GSList * fileList,nfdpathset_t * pathSet)108 static nfdresult_t AllocPathSet( GSList *fileList, nfdpathset_t *pathSet )
109 {
110     size_t bufSize = 0;
111     GSList *node;
112     nfdchar_t *p_buf;
113     size_t count = 0;
114 
115     assert(fileList);
116     assert(pathSet);
117 
118     pathSet->count = (size_t)g_slist_length( fileList );
119     assert( pathSet->count > 0 );
120 
121     pathSet->indices = NFDi_Malloc( sizeof(size_t)*pathSet->count );
122     if ( !pathSet->indices )
123     {
124         return NFD_ERROR;
125     }
126 
127     /* count the total space needed for buf */
128     for ( node = fileList; node; node = node->next )
129     {
130         assert(node->data);
131         bufSize += strlen( (const gchar*)node->data ) + 1;
132     }
133 
134     pathSet->buf = NFDi_Malloc( sizeof(nfdchar_t) * bufSize );
135 
136     /* fill buf */
137     p_buf = pathSet->buf;
138     for ( node = fileList; node; node = node->next )
139     {
140         nfdchar_t *path = (nfdchar_t*)(node->data);
141         size_t byteLen = strlen(path)+1;
142         ptrdiff_t index;
143 
144         memcpy( p_buf, path, byteLen );
145         g_free(node->data);
146 
147         index = p_buf - pathSet->buf;
148         assert( index >= 0 );
149         pathSet->indices[count] = (size_t)index;
150 
151         p_buf += byteLen;
152         ++count;
153     }
154 
155     g_slist_free( fileList );
156 
157     return NFD_OKAY;
158 }
159 
WaitForCleanup(void)160 static void WaitForCleanup(void)
161 {
162     while (gtk_events_pending())
163         gtk_main_iteration();
164 }
165 
166 /* public */
167 
NFD_OpenDialog(const char * filterList,const nfdchar_t * defaultPath,nfdchar_t ** outPath)168 nfdresult_t NFD_OpenDialog( const char *filterList,
169                             const nfdchar_t *defaultPath,
170                             nfdchar_t **outPath )
171 {
172     GtkWidget *dialog;
173     nfdresult_t result;
174 
175     if ( !gtk_init_check( NULL, NULL ) )
176     {
177         NFDi_SetError(INIT_FAIL_MSG);
178         return NFD_ERROR;
179     }
180 
181     dialog = gtk_file_chooser_dialog_new( "Open File",
182                                           NULL,
183                                           GTK_FILE_CHOOSER_ACTION_OPEN,
184                                           "_Cancel", GTK_RESPONSE_CANCEL,
185                                           "_Open", GTK_RESPONSE_ACCEPT,
186                                           NULL );
187 
188     /* Build the filter list */
189     AddFiltersToDialog(dialog, filterList);
190 
191     /* Set the default path */
192     SetDefaultPath(dialog, defaultPath);
193 
194     result = NFD_CANCEL;
195     if ( gtk_dialog_run( GTK_DIALOG(dialog) ) == GTK_RESPONSE_ACCEPT )
196     {
197         char *filename;
198 
199         filename = gtk_file_chooser_get_filename( GTK_FILE_CHOOSER(dialog) );
200 
201         {
202             size_t len = strlen(filename);
203             *outPath = NFDi_Malloc( len + 1 );
204             memcpy( *outPath, filename, len + 1 );
205             if ( !*outPath )
206             {
207                 g_free( filename );
208                 gtk_widget_destroy(dialog);
209                 return NFD_ERROR;
210             }
211         }
212         g_free( filename );
213 
214         result = NFD_OKAY;
215     }
216 
217     WaitForCleanup();
218     gtk_widget_destroy(dialog);
219     WaitForCleanup();
220 
221     return result;
222 }
223 
224 
NFD_OpenDialogMultiple(const nfdchar_t * filterList,const nfdchar_t * defaultPath,nfdpathset_t * outPaths)225 nfdresult_t NFD_OpenDialogMultiple( const nfdchar_t *filterList,
226                                     const nfdchar_t *defaultPath,
227                                     nfdpathset_t *outPaths )
228 {
229     GtkWidget *dialog;
230     nfdresult_t result;
231 
232     if ( !gtk_init_check( NULL, NULL ) )
233     {
234         NFDi_SetError(INIT_FAIL_MSG);
235         return NFD_ERROR;
236     }
237 
238     dialog = gtk_file_chooser_dialog_new( "Open Files",
239                                           NULL,
240                                           GTK_FILE_CHOOSER_ACTION_OPEN,
241                                           "_Cancel", GTK_RESPONSE_CANCEL,
242                                           "_Open", GTK_RESPONSE_ACCEPT,
243                                           NULL );
244     gtk_file_chooser_set_select_multiple( GTK_FILE_CHOOSER(dialog), TRUE );
245 
246     /* Build the filter list */
247     AddFiltersToDialog(dialog, filterList);
248 
249     /* Set the default path */
250     SetDefaultPath(dialog, defaultPath);
251 
252     result = NFD_CANCEL;
253     if ( gtk_dialog_run( GTK_DIALOG(dialog) ) == GTK_RESPONSE_ACCEPT )
254     {
255         GSList *fileList = gtk_file_chooser_get_filenames( GTK_FILE_CHOOSER(dialog) );
256         if ( AllocPathSet( fileList, outPaths ) == NFD_ERROR )
257         {
258             gtk_widget_destroy(dialog);
259             return NFD_ERROR;
260         }
261 
262         result = NFD_OKAY;
263     }
264 
265     WaitForCleanup();
266     gtk_widget_destroy(dialog);
267     WaitForCleanup();
268 
269     return result;
270 }
271 
NFD_SaveDialog(const nfdchar_t * filterList,const nfdchar_t * defaultPath,nfdchar_t ** outPath)272 nfdresult_t NFD_SaveDialog( const nfdchar_t *filterList,
273                             const nfdchar_t *defaultPath,
274                             nfdchar_t **outPath )
275 {
276     GtkWidget *dialog;
277     nfdresult_t result;
278 
279     if ( !gtk_init_check( NULL, NULL ) )
280     {
281         NFDi_SetError(INIT_FAIL_MSG);
282         return NFD_ERROR;
283     }
284 
285     dialog = gtk_file_chooser_dialog_new( "Save File",
286                                           NULL,
287                                           GTK_FILE_CHOOSER_ACTION_SAVE,
288                                           "_Cancel", GTK_RESPONSE_CANCEL,
289                                           "_Save", GTK_RESPONSE_ACCEPT,
290                                           NULL );
291     gtk_file_chooser_set_do_overwrite_confirmation( GTK_FILE_CHOOSER(dialog), TRUE );
292 
293     /* Build the filter list */
294     AddFiltersToDialog(dialog, filterList);
295 
296     /* Set the default path */
297     SetDefaultPath(dialog, defaultPath);
298 
299     result = NFD_CANCEL;
300     if ( gtk_dialog_run( GTK_DIALOG(dialog) ) == GTK_RESPONSE_ACCEPT )
301     {
302         char *filename;
303         filename = gtk_file_chooser_get_filename( GTK_FILE_CHOOSER(dialog) );
304 
305         {
306             size_t len = strlen(filename);
307             *outPath = NFDi_Malloc( len + 1 );
308             memcpy( *outPath, filename, len + 1 );
309             if ( !*outPath )
310             {
311                 g_free( filename );
312                 gtk_widget_destroy(dialog);
313                 return NFD_ERROR;
314             }
315         }
316         g_free(filename);
317 
318         result = NFD_OKAY;
319     }
320 
321     WaitForCleanup();
322     gtk_widget_destroy(dialog);
323     WaitForCleanup();
324 
325     return result;
326 }
327 
NFD_PickFolder(const nfdchar_t * defaultPath,nfdchar_t ** outPath)328 nfdresult_t NFD_PickFolder(const nfdchar_t *defaultPath,
329     nfdchar_t **outPath)
330 {
331     GtkWidget *dialog;
332     nfdresult_t result;
333 
334     if (!gtk_init_check(NULL, NULL))
335     {
336         NFDi_SetError(INIT_FAIL_MSG);
337         return NFD_ERROR;
338     }
339 
340     dialog = gtk_file_chooser_dialog_new( "Select folder",
341                                           NULL,
342                                           GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
343                                           "_Cancel", GTK_RESPONSE_CANCEL,
344                                           "_Select", GTK_RESPONSE_ACCEPT,
345                                           NULL );
346     gtk_file_chooser_set_do_overwrite_confirmation( GTK_FILE_CHOOSER(dialog), TRUE );
347 
348 
349     /* Set the default path */
350     SetDefaultPath(dialog, defaultPath);
351 
352     result = NFD_CANCEL;
353     if ( gtk_dialog_run( GTK_DIALOG(dialog) ) == GTK_RESPONSE_ACCEPT )
354     {
355         char *filename;
356         filename = gtk_file_chooser_get_filename( GTK_FILE_CHOOSER(dialog) );
357 
358         {
359             size_t len = strlen(filename);
360             *outPath = NFDi_Malloc( len + 1 );
361             memcpy( *outPath, filename, len + 1 );
362             if ( !*outPath )
363             {
364                 g_free( filename );
365                 gtk_widget_destroy(dialog);
366                 return NFD_ERROR;
367             }
368         }
369         g_free(filename);
370 
371         result = NFD_OKAY;
372     }
373 
374     WaitForCleanup();
375     gtk_widget_destroy(dialog);
376     WaitForCleanup();
377 
378     return result;
379 }
380