1 #include "gl-app.h"
2
3 #include <string.h>
4 #include <stdlib.h>
5 #include <stdio.h>
6 #include <limits.h>
7
8 #define ICON_PC 0x1f4bb
9 #define ICON_HOME 0x1f3e0
10 #define ICON_FOLDER 0x1f4c1
11 #define ICON_DOCUMENT 0x1f4c4
12 #define ICON_DISK 0x1f4be
13 #define ICON_PIN 0x1f4cc
14
15 #ifndef PATH_MAX
16 #define PATH_MAX 4096
17 #endif
18
19 struct entry
20 {
21 int is_dir;
22 char name[FILENAME_MAX];
23 };
24
25 static struct
26 {
27 int (*filter)(const char *fn);
28 struct input input_dir;
29 struct input input_file;
30 struct list list_dir;
31 char curdir[PATH_MAX];
32 int count;
33 int max;
34 struct entry *files;
35 int selected;
36 } fc;
37
cmp_entry(const void * av,const void * bv)38 static int cmp_entry(const void *av, const void *bv)
39 {
40 const struct entry *a = av;
41 const struct entry *b = bv;
42 /* "." first */
43 if (a->name[0] == '.' && a->name[1] == 0) return -1;
44 if (b->name[0] == '.' && b->name[1] == 0) return 1;
45 /* ".." second */
46 if (a->name[0] == '.' && a->name[1] == '.' && a->name[2] == 0) return -1;
47 if (b->name[0] == '.' && b->name[1] == '.' && b->name[2] == 0) return 1;
48 /* directories before files */
49 if (a->is_dir && !b->is_dir) return -1;
50 if (b->is_dir && !a->is_dir) return 1;
51 /* then alphabetically */
52 return strcmp(a->name, b->name);
53 }
54
55 static void
ensure_one_more_file(void)56 ensure_one_more_file(void)
57 {
58 if (fc.count == fc.max)
59 {
60 int new_max = fc.max == 0 ? 512 : fc.max*2;
61 fc.files = fz_realloc_array(ctx, fc.files, new_max, struct entry);
62 fc.max = new_max;
63 }
64 }
65
66 #ifdef _WIN32
67
68 #include <strsafe.h>
69 #include <shlobj.h>
70
load_dir(const char * path)71 static void load_dir(const char *path)
72 {
73 WIN32_FIND_DATAW ffd;
74 HANDLE dir;
75 wchar_t wpath[PATH_MAX];
76 char buf[PATH_MAX];
77 int i;
78
79 fz_realpath(path, fc.curdir);
80 if (!fz_is_directory(ctx, path))
81 return;
82
83 ui_input_init(&fc.input_dir, fc.curdir);
84
85 fc.selected = -1;
86 fc.count = 0;
87
88 MultiByteToWideChar(CP_UTF8, 0, path, -1, wpath, PATH_MAX);
89 for (i=0; wpath[i]; ++i)
90 if (wpath[i] == '/')
91 wpath[i] = '\\';
92 StringCchCatW(wpath, PATH_MAX, L"/*");
93 dir = FindFirstFileW(wpath, &ffd);
94 if (dir)
95 {
96 do
97 {
98 WideCharToMultiByte(CP_UTF8, 0, ffd.cFileName, -1, buf, PATH_MAX, NULL, NULL);
99 if (ffd.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN)
100 continue;
101 ensure_one_more_file();
102 fc.files[fc.count].is_dir = ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY;
103 if (fc.files[fc.count].is_dir || !fc.filter || fc.filter(buf))
104 {
105 fz_strlcpy(fc.files[fc.count].name, buf, FILENAME_MAX);
106 ++fc.count;
107 }
108 }
109 while (FindNextFileW(dir, &ffd));
110 FindClose(dir);
111 }
112
113 qsort(fc.files, fc.count, sizeof fc.files[0], cmp_entry);
114 }
115
list_drives(void)116 static void list_drives(void)
117 {
118 static struct list drive_list;
119 DWORD drives;
120 char dir[PATH_MAX], vis[PATH_MAX], buf[100];
121 const char *user, *home;
122 char personal[MAX_PATH], desktop[MAX_PATH];
123 int i, n;
124
125 drives = GetLogicalDrives();
126 n = 5; /* curdir + home + desktop + documents + downloads */
127 for (i=0; i < 26; ++i)
128 if (drives & (1<<i))
129 ++n;
130
131 ui_list_begin(&drive_list, n, 0, 10 * ui.lineheight + 4);
132
133 user = getenv("USERNAME");
134 home = getenv("USERPROFILE");
135 if (user && home)
136 {
137 fz_snprintf(vis, sizeof vis, "%C %s", ICON_HOME, user);
138 if (ui_list_item(&drive_list, "~", vis, 0))
139 load_dir(home);
140 }
141
142 if (SHGetFolderPathA(NULL, CSIDL_DESKTOPDIRECTORY, NULL, 0, desktop) == S_OK)
143 {
144 fz_snprintf(vis, sizeof vis, "%C Desktop", ICON_PC);
145 if (ui_list_item(&drive_list, "~/Desktop", vis, 0))
146 load_dir(desktop);
147 }
148
149 if (SHGetFolderPathA(NULL, CSIDL_PERSONAL, NULL, 0, personal) == S_OK)
150 {
151 fz_snprintf(vis, sizeof vis, "%C Documents", ICON_FOLDER);
152 if (ui_list_item(&drive_list, "~/Documents", vis, 0))
153 load_dir(personal);
154 }
155
156 if (home)
157 {
158 fz_snprintf(vis, sizeof vis, "%C Downloads", ICON_FOLDER);
159 fz_snprintf(dir, sizeof dir, "%s/Downloads", home);
160 if (ui_list_item(&drive_list, "~/Downloads", vis, 0))
161 load_dir(dir);
162 }
163
164 for (i = 0; i < 26; ++i)
165 {
166 if (drives & (1<<i))
167 {
168 fz_snprintf(dir, sizeof dir, "%c:\\", i+'A');
169 if (!GetVolumeInformationA(dir, buf, sizeof buf, NULL, NULL, NULL, NULL, 0))
170 buf[0] = 0;
171 fz_snprintf(vis, sizeof vis, "%C %c: %s", ICON_DISK, i+'A', buf);
172 if (ui_list_item(&drive_list, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"+i, vis, 0))
173 {
174 load_dir(dir);
175 }
176 }
177 }
178
179 fz_snprintf(vis, sizeof vis, "%C .", ICON_PIN);
180 if (ui_list_item(&drive_list, ".", vis, 0))
181 load_dir(".");
182
183 ui_list_end(&drive_list);
184 }
185
186 #else
187
188 #include <dirent.h>
189
load_dir(const char * path)190 static void load_dir(const char *path)
191 {
192 char buf[PATH_MAX];
193 DIR *dir;
194 struct dirent *dp;
195
196 fz_realpath(path, fc.curdir);
197 if (!fz_is_directory(ctx, fc.curdir))
198 return;
199
200 ui_input_init(&fc.input_dir, fc.curdir);
201
202 fc.selected = -1;
203 fc.count = 0;
204
205 dir = opendir(fc.curdir);
206 if (!dir)
207 {
208 ensure_one_more_file();
209 fc.files[fc.count].is_dir = 1;
210 fz_strlcpy(fc.files[fc.count].name, "..", FILENAME_MAX);
211 ++fc.count;
212 }
213 else
214 {
215 while ((dp = readdir(dir)))
216 {
217 /* skip hidden files */
218 if (dp->d_name[0] == '.' && strcmp(dp->d_name, ".") && strcmp(dp->d_name, ".."))
219 continue;
220 fz_snprintf(buf, sizeof buf, "%s/%s", fc.curdir, dp->d_name);
221 ensure_one_more_file();
222 fc.files[fc.count].is_dir = fz_is_directory(ctx, buf);
223 if (fc.files[fc.count].is_dir || !fc.filter || fc.filter(buf))
224 {
225 fz_strlcpy(fc.files[fc.count].name, dp->d_name, FILENAME_MAX);
226 ++fc.count;
227 }
228 }
229 closedir(dir);
230 }
231
232 qsort(fc.files, fc.count, sizeof fc.files[0], cmp_entry);
233 }
234
235 static const struct {
236 int icon;
237 const char *name;
238 } common_dirs[] = {
239 { ICON_HOME, "~" },
240 { ICON_PC, "~/Desktop" },
241 { ICON_FOLDER, "~/Documents" },
242 { ICON_FOLDER, "~/Downloads" },
243 { ICON_FOLDER, "/" },
244 { ICON_DISK, "/Volumes" },
245 { ICON_DISK, "/media" },
246 { ICON_DISK, "/mnt" },
247 { ICON_PIN, "." },
248 };
249
has_dir(const char * home,const char * user,int i,char dir[PATH_MAX],char vis[PATH_MAX])250 static int has_dir(const char *home, const char *user, int i, char dir[PATH_MAX], char vis[PATH_MAX])
251 {
252 const char *subdir = common_dirs[i].name;
253 int icon = common_dirs[i].icon;
254 if (subdir[0] == '~')
255 {
256 if (!home)
257 return 0;
258 if (subdir[1] == '/')
259 {
260 fz_snprintf(dir, PATH_MAX, "%s/%s", home, subdir+2);
261 fz_snprintf(vis, PATH_MAX, "%C %s", icon, subdir+2);
262 }
263 else
264 {
265 fz_snprintf(dir, PATH_MAX, "%s", home);
266 fz_snprintf(vis, PATH_MAX, "%C %s", icon, user ? user : "~");
267 }
268 }
269 else
270 {
271 fz_strlcpy(dir, subdir, PATH_MAX);
272 fz_snprintf(vis, PATH_MAX, "%C %s", icon, subdir);
273 }
274 return fz_is_directory(ctx, dir);
275 }
276
list_drives(void)277 static void list_drives(void)
278 {
279 static struct list drive_list;
280 char dir[PATH_MAX], vis[PATH_MAX];
281 const char *home = getenv("HOME");
282 const char *user = getenv("USER");
283 int i;
284
285 ui_list_begin(&drive_list, nelem(common_dirs), 0, nelem(common_dirs) * ui.lineheight + 4);
286
287 for (i = 0; i < (int)nelem(common_dirs); ++i)
288 if (has_dir(home, user, i, dir, vis))
289 if (ui_list_item(&drive_list, common_dirs[i].name, vis, 0))
290 load_dir(dir);
291
292 ui_list_end(&drive_list);
293 }
294
295 #endif
296
ui_init_open_file(const char * dir,int (* filter)(const char * fn))297 void ui_init_open_file(const char *dir, int (*filter)(const char *fn))
298 {
299 fc.filter = filter;
300 load_dir(dir);
301 }
302
ui_open_file(char filename[PATH_MAX],const char * label)303 int ui_open_file(char filename[PATH_MAX], const char *label)
304 {
305 static int last_click_time = 0;
306 static int last_click_sel = -1;
307 int i, rv = 0;
308
309 ui_panel_begin(0, 0, 4, 4, 1);
310 {
311 if (label)
312 {
313 ui_layout(T, X, NW, 4, 2);
314 ui_label(label);
315 }
316 ui_layout(L, Y, NW, 0, 0);
317 ui_panel_begin(150, 0, 0, 0, 0);
318 {
319 ui_layout(T, X, NW, 2, 2);
320 list_drives();
321 ui_layout(B, X, NW, 2, 2);
322 if (ui_button("Cancel") || (!ui.focus && ui.key == KEY_ESCAPE))
323 {
324 filename[0] = 0;
325 rv = 1;
326 }
327 }
328 ui_panel_end();
329
330 ui_layout(T, X, NW, 2, 2);
331 ui_panel_begin(0, ui.gridsize, 0, 0, 0);
332 {
333 if (fc.selected >= 0)
334 {
335 ui_layout(R, NONE, CENTER, 0, 0);
336 if (ui_button("Open") || (!ui.focus && ui.key == KEY_ENTER))
337 {
338 fz_snprintf(filename, PATH_MAX, "%s/%s", fc.curdir, fc.files[fc.selected].name);
339 rv = 1;
340 }
341 ui_spacer();
342 }
343 ui_layout(ALL, X, CENTER, 0, 0);
344 if (ui_input(&fc.input_dir, 0, 1) == UI_INPUT_ACCEPT)
345 load_dir(fc.input_dir.text);
346 }
347 ui_panel_end();
348
349 ui_layout(ALL, BOTH, NW, 2, 2);
350 ui_list_begin(&fc.list_dir, fc.count, 0, 0);
351 for (i = 0; i < fc.count; ++i)
352 {
353 const char *name = fc.files[i].name;
354 char buf[PATH_MAX];
355 if (fc.files[i].is_dir)
356 fz_snprintf(buf, sizeof buf, "%C %s", ICON_FOLDER, name);
357 else
358 fz_snprintf(buf, sizeof buf, "%C %s", ICON_DOCUMENT, name);
359 if (ui_list_item(&fc.list_dir, &fc.files[i], buf, i==fc.selected))
360 {
361 fc.selected = i;
362 if (fc.files[i].is_dir)
363 {
364 fz_snprintf(buf, sizeof buf, "%s/%s", fc.curdir, name);
365 load_dir(buf);
366 ui.active = NULL;
367 last_click_sel = -1;
368 }
369 else
370 {
371 int click_time = glutGet(GLUT_ELAPSED_TIME);
372 if (i == last_click_sel && click_time < last_click_time + 250)
373 {
374 fz_snprintf(filename, PATH_MAX, "%s/%s", fc.curdir, name);
375 rv = 1;
376 }
377 last_click_time = click_time;
378 last_click_sel = i;
379 }
380 }
381 }
382 ui_list_end(&fc.list_dir);
383 }
384 ui_panel_end();
385
386 return rv;
387 }
388
ui_init_save_file(const char * path,int (* filter)(const char * fn))389 void ui_init_save_file(const char *path, int (*filter)(const char *fn))
390 {
391 char dir[PATH_MAX], *p;
392 fc.filter = filter;
393 fz_strlcpy(dir, path, sizeof dir);
394 for (p=dir; *p; ++p)
395 if (*p == '\\') *p = '/';
396 fz_cleanname(dir);
397 p = strrchr(dir, '/');
398 if (p)
399 {
400 *p = 0;
401 load_dir(dir);
402 ui_input_init(&fc.input_file, p+1);
403 }
404 else
405 {
406 load_dir(".");
407 ui_input_init(&fc.input_file, dir);
408 }
409 }
410
bump_file_version(int dir)411 static void bump_file_version(int dir)
412 {
413 char buf[PATH_MAX], *p, *n;
414 char base[PATH_MAX], out[PATH_MAX];
415 int x;
416 fz_strlcpy(buf, fc.input_file.text, sizeof buf);
417 p = strrchr(buf, '.');
418 if (p)
419 {
420 n = p;
421 while (n > buf && n[-1] >= '0' && n[-1] <= '9')
422 --n;
423 if (n != p)
424 x = atoi(n) + dir;
425 else
426 x = dir;
427 memcpy(base, buf, n-buf);
428 base[n-buf] = 0;
429 fz_snprintf(out, sizeof out, "%s%d%s", base, x, p);
430 ui_input_init(&fc.input_file, out);
431 }
432 }
433
ui_save_file(char filename[PATH_MAX],void (* extra_panel)(void),const char * label)434 int ui_save_file(char filename[PATH_MAX], void (*extra_panel)(void), const char *label)
435 {
436 int i, rv = 0;
437
438 ui_panel_begin(0, 0, 4, 4, 1);
439 {
440 if (label)
441 {
442 ui_layout(T, X, NW, 4, 2);
443 ui_label(label);
444 }
445 ui_layout(L, Y, NW, 0, 0);
446 ui_panel_begin(150, 0, 0, 0, 0);
447 {
448 ui_layout(T, X, NW, 2, 2);
449 list_drives();
450 if (extra_panel)
451 {
452 ui_spacer();
453 extra_panel();
454 }
455 ui_layout(B, X, NW, 2, 2);
456 if (ui_button("Cancel") || (!ui.focus && ui.key == KEY_ESCAPE))
457 {
458 filename[0] = 0;
459 rv = 1;
460 }
461 }
462 ui_panel_end();
463
464 ui_layout(T, X, NW, 2, 2);
465 if (ui_input(&fc.input_dir, 0, 1) == UI_INPUT_ACCEPT)
466 load_dir(fc.input_dir.text);
467
468 ui_layout(T, X, NW, 2, 2);
469 ui_panel_begin(0, ui.gridsize, 0, 0, 0);
470 {
471 ui_layout(R, NONE, CENTER, 0, 0);
472 if (ui_button("Save"))
473 {
474 fz_snprintf(filename, PATH_MAX, "%s/%s", fc.curdir, fc.input_file.text);
475 rv = 1;
476 }
477 ui_spacer();
478 if (ui_button("\xe2\x9e\x95")) /* U+2795 HEAVY PLUS */
479 bump_file_version(1);
480 if (ui_button("\xe2\x9e\x96")) /* U+2796 HEAVY MINUS */
481 bump_file_version(-1);
482 ui_spacer();
483 ui_layout(ALL, X, CENTER, 0, 0);
484 ui_input(&fc.input_file, 0, 1);
485 }
486 ui_panel_end();
487
488 ui_layout(ALL, BOTH, NW, 2, 2);
489 ui_list_begin(&fc.list_dir, fc.count, 0, 0);
490 for (i = 0; i < fc.count; ++i)
491 {
492 const char *name = fc.files[i].name;
493 char buf[PATH_MAX];
494 if (fc.files[i].is_dir)
495 fz_snprintf(buf, sizeof buf, "%C %s", ICON_FOLDER, name);
496 else
497 fz_snprintf(buf, sizeof buf, "%C %s", ICON_DOCUMENT, name);
498 if (ui_list_item(&fc.list_dir, &fc.files[i], buf, i==fc.selected))
499 {
500 fc.selected = i;
501 if (fc.files[i].is_dir)
502 {
503 fz_snprintf(buf, sizeof buf, "%s/%s", fc.curdir, name);
504 load_dir(buf);
505 ui.active = NULL;
506 }
507 else
508 {
509 ui_input_init(&fc.input_file, name);
510 }
511 }
512 }
513 ui_list_end(&fc.list_dir);
514 }
515 ui_panel_end();
516
517 return rv;
518 }
519