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