gl-file.c 14 KB

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