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