507 lines
11 KiB
C
507 lines
11 KiB
C
|
#include "gl-app.h"
|
||
|
|
||
|
#include <string.h>
|
||
|
#include <stdlib.h>
|
||
|
#include <stdio.h>
|
||
|
#include <limits.h>
|
||
|
|
||
|
#define ICON_PC 0x1f4bb
|
||
|
#define ICON_HOME 0x1f3e0
|
||
|
#define ICON_FOLDER 0x1f4c1
|
||
|
#define ICON_DOCUMENT 0x1f4c4
|
||
|
#define ICON_DISK 0x1f4be
|
||
|
#define ICON_PIN 0x1f4cc
|
||
|
|
||
|
#ifndef PATH_MAX
|
||
|
#define PATH_MAX 2048
|
||
|
#endif
|
||
|
|
||
|
struct entry
|
||
|
{
|
||
|
int is_dir;
|
||
|
char name[FILENAME_MAX];
|
||
|
};
|
||
|
|
||
|
static struct
|
||
|
{
|
||
|
int (*filter)(const char *fn);
|
||
|
struct input input_dir;
|
||
|
struct input input_file;
|
||
|
struct list list_dir;
|
||
|
char curdir[PATH_MAX];
|
||
|
int count;
|
||
|
struct entry files[512];
|
||
|
int selected;
|
||
|
} fc;
|
||
|
|
||
|
static int cmp_entry(const void *av, const void *bv)
|
||
|
{
|
||
|
const struct entry *a = av;
|
||
|
const struct entry *b = bv;
|
||
|
/* "." first */
|
||
|
if (a->name[0] == '.' && a->name[1] == 0) return -1;
|
||
|
if (b->name[0] == '.' && b->name[1] == 0) return 1;
|
||
|
/* ".." second */
|
||
|
if (a->name[0] == '.' && a->name[1] == '.' && a->name[2] == 0) return -1;
|
||
|
if (b->name[0] == '.' && b->name[1] == '.' && b->name[2] == 0) return 1;
|
||
|
/* directories before files */
|
||
|
if (a->is_dir && !b->is_dir) return -1;
|
||
|
if (b->is_dir && !a->is_dir) return 1;
|
||
|
/* then alphabetically */
|
||
|
return strcmp(a->name, b->name);
|
||
|
}
|
||
|
|
||
|
#ifdef _WIN32
|
||
|
|
||
|
#include <strsafe.h>
|
||
|
#include <shlobj.h>
|
||
|
|
||
|
const char *realpath(const char *path, char buf[PATH_MAX])
|
||
|
{
|
||
|
wchar_t wpath[PATH_MAX];
|
||
|
wchar_t wbuf[PATH_MAX];
|
||
|
int i;
|
||
|
MultiByteToWideChar(CP_UTF8, 0, path, -1, wpath, PATH_MAX);
|
||
|
GetFullPathNameW(wpath, PATH_MAX, wbuf, NULL);
|
||
|
WideCharToMultiByte(CP_UTF8, 0, wbuf, -1, buf, PATH_MAX, NULL, NULL);
|
||
|
for (i=0; buf[i]; ++i)
|
||
|
if (buf[i] == '\\')
|
||
|
buf[i] = '/';
|
||
|
return buf;
|
||
|
}
|
||
|
|
||
|
static void load_dir(const char *path)
|
||
|
{
|
||
|
WIN32_FIND_DATA ffd;
|
||
|
HANDLE dir;
|
||
|
wchar_t wpath[PATH_MAX];
|
||
|
char buf[PATH_MAX];
|
||
|
int i;
|
||
|
|
||
|
realpath(path, fc.curdir);
|
||
|
if (!fz_is_directory(ctx, path))
|
||
|
return;
|
||
|
|
||
|
ui_input_init(&fc.input_dir, fc.curdir);
|
||
|
|
||
|
fc.selected = -1;
|
||
|
fc.count = 0;
|
||
|
|
||
|
MultiByteToWideChar(CP_UTF8, 0, path, -1, wpath, PATH_MAX);
|
||
|
for (i=0; wpath[i]; ++i)
|
||
|
if (wpath[i] == '/')
|
||
|
wpath[i] = '\\';
|
||
|
StringCchCat(wpath, PATH_MAX, TEXT("/*"));
|
||
|
dir = FindFirstFileW(wpath, &ffd);
|
||
|
if (dir)
|
||
|
{
|
||
|
do
|
||
|
{
|
||
|
WideCharToMultiByte(CP_UTF8, 0, ffd.cFileName, -1, buf, PATH_MAX, NULL, NULL);
|
||
|
if (ffd.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN)
|
||
|
continue;
|
||
|
fc.files[fc.count].is_dir = ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY;
|
||
|
if (fc.files[fc.count].is_dir || !fc.filter || fc.filter(buf))
|
||
|
{
|
||
|
fz_strlcpy(fc.files[fc.count].name, buf, FILENAME_MAX);
|
||
|
++fc.count;
|
||
|
}
|
||
|
}
|
||
|
while (FindNextFile(dir, &ffd));
|
||
|
FindClose(dir);
|
||
|
}
|
||
|
|
||
|
qsort(fc.files, fc.count, sizeof fc.files[0], cmp_entry);
|
||
|
}
|
||
|
|
||
|
static void list_drives(void)
|
||
|
{
|
||
|
static struct list drive_list;
|
||
|
DWORD drives;
|
||
|
char dir[PATH_MAX], vis[PATH_MAX], buf[100];
|
||
|
const char *user, *home;
|
||
|
char personal[MAX_PATH], desktop[MAX_PATH];
|
||
|
int i, n;
|
||
|
|
||
|
drives = GetLogicalDrives();
|
||
|
n = 5; /* curdir + home + desktop + documents + downloads */
|
||
|
for (i=0; i < 26; ++i)
|
||
|
if (drives & (1<<i))
|
||
|
++n;
|
||
|
|
||
|
ui_list_begin(&drive_list, n, 0, 10 * ui.lineheight + 4);
|
||
|
|
||
|
user = getenv("USERNAME");
|
||
|
home = getenv("USERPROFILE");
|
||
|
if (user && home)
|
||
|
{
|
||
|
fz_snprintf(vis, sizeof vis, "%C %s", ICON_HOME, user);
|
||
|
if (ui_list_item(&drive_list, "~", vis, 0))
|
||
|
load_dir(home);
|
||
|
}
|
||
|
|
||
|
if (SHGetFolderPathA(NULL, CSIDL_DESKTOPDIRECTORY, NULL, 0, desktop) == S_OK)
|
||
|
{
|
||
|
fz_snprintf(vis, sizeof vis, "%C Desktop", ICON_PC);
|
||
|
if (ui_list_item(&drive_list, "~/Desktop", vis, 0))
|
||
|
load_dir(desktop);
|
||
|
}
|
||
|
|
||
|
if (SHGetFolderPathA(NULL, CSIDL_PERSONAL, NULL, 0, personal) == S_OK)
|
||
|
{
|
||
|
fz_snprintf(vis, sizeof vis, "%C Documents", ICON_FOLDER);
|
||
|
if (ui_list_item(&drive_list, "~/Documents", vis, 0))
|
||
|
load_dir(personal);
|
||
|
}
|
||
|
|
||
|
if (home)
|
||
|
{
|
||
|
fz_snprintf(vis, sizeof vis, "%C Downloads", ICON_FOLDER);
|
||
|
fz_snprintf(dir, sizeof dir, "%s/Downloads", home);
|
||
|
if (ui_list_item(&drive_list, "~/Downloads", vis, 0))
|
||
|
load_dir(dir);
|
||
|
}
|
||
|
|
||
|
for (i = 0; i < 26; ++i)
|
||
|
{
|
||
|
if (drives & (1<<i))
|
||
|
{
|
||
|
fz_snprintf(dir, sizeof dir, "%c:\\", i+'A');
|
||
|
if (!GetVolumeInformationA(dir, buf, sizeof buf, NULL, NULL, NULL, NULL, 0))
|
||
|
buf[0] = 0;
|
||
|
fz_snprintf(vis, sizeof vis, "%C %c: %s", ICON_DISK, i+'A', buf);
|
||
|
if (ui_list_item(&drive_list, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"+i, vis, 0))
|
||
|
{
|
||
|
load_dir(dir);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
fz_snprintf(vis, sizeof vis, "%C .", ICON_PIN);
|
||
|
if (ui_list_item(&drive_list, ".", vis, 0))
|
||
|
load_dir(".");
|
||
|
|
||
|
ui_list_end(&drive_list);
|
||
|
}
|
||
|
|
||
|
#else
|
||
|
|
||
|
#include <dirent.h>
|
||
|
|
||
|
static void load_dir(const char *path)
|
||
|
{
|
||
|
char buf[PATH_MAX];
|
||
|
DIR *dir;
|
||
|
struct dirent *dp;
|
||
|
|
||
|
realpath(path, fc.curdir);
|
||
|
if (!fz_is_directory(ctx, fc.curdir))
|
||
|
return;
|
||
|
|
||
|
ui_input_init(&fc.input_dir, fc.curdir);
|
||
|
|
||
|
fc.selected = -1;
|
||
|
fc.count = 0;
|
||
|
dir = opendir(fc.curdir);
|
||
|
if (!dir)
|
||
|
{
|
||
|
fc.files[fc.count].is_dir = 1;
|
||
|
fz_strlcpy(fc.files[fc.count].name, "..", FILENAME_MAX);
|
||
|
++fc.count;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
while ((dp = readdir(dir)) && fc.count < (int)nelem(fc.files))
|
||
|
{
|
||
|
/* skip hidden files */
|
||
|
if (dp->d_name[0] == '.' && strcmp(dp->d_name, ".") && strcmp(dp->d_name, ".."))
|
||
|
continue;
|
||
|
fz_snprintf(buf, sizeof buf, "%s/%s", fc.curdir, dp->d_name);
|
||
|
fc.files[fc.count].is_dir = fz_is_directory(ctx, buf);
|
||
|
if (fc.files[fc.count].is_dir || !fc.filter || fc.filter(buf))
|
||
|
{
|
||
|
fz_strlcpy(fc.files[fc.count].name, dp->d_name, FILENAME_MAX);
|
||
|
++fc.count;
|
||
|
}
|
||
|
}
|
||
|
closedir(dir);
|
||
|
}
|
||
|
|
||
|
qsort(fc.files, fc.count, sizeof fc.files[0], cmp_entry);
|
||
|
}
|
||
|
|
||
|
static const struct {
|
||
|
int icon;
|
||
|
const char *name;
|
||
|
} common_dirs[] = {
|
||
|
{ ICON_HOME, "~" },
|
||
|
{ ICON_PC, "~/Desktop" },
|
||
|
{ ICON_FOLDER, "~/Documents" },
|
||
|
{ ICON_FOLDER, "~/Downloads" },
|
||
|
{ ICON_FOLDER, "/" },
|
||
|
{ ICON_DISK, "/Volumes" },
|
||
|
{ ICON_DISK, "/media" },
|
||
|
{ ICON_DISK, "/mnt" },
|
||
|
{ ICON_PIN, "." },
|
||
|
};
|
||
|
|
||
|
static int has_dir(const char *home, const char *user, int i, char dir[PATH_MAX], char vis[PATH_MAX])
|
||
|
{
|
||
|
const char *subdir = common_dirs[i].name;
|
||
|
int icon = common_dirs[i].icon;
|
||
|
if (subdir[0] == '~')
|
||
|
{
|
||
|
if (!home)
|
||
|
return 0;
|
||
|
if (subdir[1] == '/')
|
||
|
{
|
||
|
fz_snprintf(dir, PATH_MAX, "%s/%s", home, subdir+2);
|
||
|
fz_snprintf(vis, PATH_MAX, "%C %s", icon, subdir+2);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
fz_snprintf(dir, PATH_MAX, "%s", home);
|
||
|
fz_snprintf(vis, PATH_MAX, "%C %s", icon, user ? user : "~");
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
fz_strlcpy(dir, subdir, PATH_MAX);
|
||
|
fz_snprintf(vis, PATH_MAX, "%C %s", icon, subdir);
|
||
|
}
|
||
|
return fz_is_directory(ctx, dir);
|
||
|
}
|
||
|
|
||
|
static void list_drives(void)
|
||
|
{
|
||
|
static struct list drive_list;
|
||
|
char dir[PATH_MAX], vis[PATH_MAX];
|
||
|
const char *home = getenv("HOME");
|
||
|
const char *user = getenv("USER");
|
||
|
int i;
|
||
|
|
||
|
ui_list_begin(&drive_list, nelem(common_dirs), 0, nelem(common_dirs) * ui.lineheight + 4);
|
||
|
|
||
|
for (i = 0; i < (int)nelem(common_dirs); ++i)
|
||
|
if (has_dir(home, user, i, dir, vis))
|
||
|
if (ui_list_item(&drive_list, common_dirs[i].name, vis, 0))
|
||
|
load_dir(dir);
|
||
|
|
||
|
ui_list_end(&drive_list);
|
||
|
}
|
||
|
|
||
|
#endif
|
||
|
|
||
|
void ui_init_open_file(const char *dir, int (*filter)(const char *fn))
|
||
|
{
|
||
|
fc.filter = filter;
|
||
|
load_dir(dir);
|
||
|
}
|
||
|
|
||
|
int ui_open_file(char filename[PATH_MAX])
|
||
|
{
|
||
|
static int last_click_time = 0;
|
||
|
static int last_click_sel = -1;
|
||
|
int i, rv = 0;
|
||
|
|
||
|
ui_panel_begin(0, 0, 4, 4, 1);
|
||
|
{
|
||
|
ui_layout(L, Y, NW, 0, 0);
|
||
|
ui_panel_begin(150, 0, 0, 0, 0);
|
||
|
{
|
||
|
ui_layout(T, X, NW, 2, 2);
|
||
|
list_drives();
|
||
|
ui_layout(B, X, NW, 2, 2);
|
||
|
if (ui_button("Cancel") || (!ui.focus && ui.key == KEY_ESCAPE))
|
||
|
{
|
||
|
filename[0] = 0;
|
||
|
rv = 1;
|
||
|
}
|
||
|
}
|
||
|
ui_panel_end();
|
||
|
|
||
|
ui_layout(T, X, NW, 2, 2);
|
||
|
ui_panel_begin(0, ui.gridsize, 0, 0, 0);
|
||
|
{
|
||
|
if (fc.selected >= 0)
|
||
|
{
|
||
|
ui_layout(R, NONE, CENTER, 0, 0);
|
||
|
if (ui_button("Open") || (!ui.focus && ui.key == KEY_ENTER))
|
||
|
{
|
||
|
fz_snprintf(filename, PATH_MAX, "%s/%s", fc.curdir, fc.files[fc.selected].name);
|
||
|
rv = 1;
|
||
|
}
|
||
|
ui_spacer();
|
||
|
}
|
||
|
ui_layout(ALL, X, CENTER, 0, 0);
|
||
|
if (ui_input(&fc.input_dir, 0, 1) == UI_INPUT_ACCEPT)
|
||
|
load_dir(fc.input_dir.text);
|
||
|
}
|
||
|
ui_panel_end();
|
||
|
|
||
|
ui_layout(ALL, BOTH, NW, 2, 2);
|
||
|
ui_list_begin(&fc.list_dir, fc.count, 0, 0);
|
||
|
for (i = 0; i < fc.count; ++i)
|
||
|
{
|
||
|
const char *name = fc.files[i].name;
|
||
|
char buf[PATH_MAX];
|
||
|
if (fc.files[i].is_dir)
|
||
|
fz_snprintf(buf, sizeof buf, "%C %s", ICON_FOLDER, name);
|
||
|
else
|
||
|
fz_snprintf(buf, sizeof buf, "%C %s", ICON_DOCUMENT, name);
|
||
|
if (ui_list_item(&fc.list_dir, &fc.files[i], buf, i==fc.selected))
|
||
|
{
|
||
|
fc.selected = i;
|
||
|
if (fc.files[i].is_dir)
|
||
|
{
|
||
|
fz_snprintf(buf, sizeof buf, "%s/%s", fc.curdir, name);
|
||
|
load_dir(buf);
|
||
|
ui.active = NULL;
|
||
|
last_click_sel = -1;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
int click_time = glutGet(GLUT_ELAPSED_TIME);
|
||
|
if (i == last_click_sel && click_time < last_click_time + 250)
|
||
|
{
|
||
|
fz_snprintf(filename, PATH_MAX, "%s/%s", fc.curdir, name);
|
||
|
rv = 1;
|
||
|
}
|
||
|
last_click_time = click_time;
|
||
|
last_click_sel = i;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
ui_list_end(&fc.list_dir);
|
||
|
}
|
||
|
ui_panel_end();
|
||
|
|
||
|
return rv;
|
||
|
}
|
||
|
|
||
|
void ui_init_save_file(const char *path, int (*filter)(const char *fn))
|
||
|
{
|
||
|
char dir[PATH_MAX], *p;
|
||
|
fc.filter = filter;
|
||
|
fz_strlcpy(dir, path, sizeof dir);
|
||
|
for (p=dir; *p; ++p)
|
||
|
if (*p == '\\') *p = '/';
|
||
|
fz_cleanname(dir);
|
||
|
p = strrchr(dir, '/');
|
||
|
if (p)
|
||
|
{
|
||
|
*p = 0;
|
||
|
load_dir(dir);
|
||
|
ui_input_init(&fc.input_file, p+1);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
load_dir(".");
|
||
|
ui_input_init(&fc.input_file, dir);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static void bump_file_version(int dir)
|
||
|
{
|
||
|
char buf[PATH_MAX], *p, *n;
|
||
|
char base[PATH_MAX], out[PATH_MAX];
|
||
|
int x;
|
||
|
fz_strlcpy(buf, fc.input_file.text, sizeof buf);
|
||
|
p = strrchr(buf, '.');
|
||
|
if (p)
|
||
|
{
|
||
|
n = p;
|
||
|
while (n > buf && n[-1] >= '0' && n[-1] <= '9')
|
||
|
--n;
|
||
|
if (n != p)
|
||
|
x = atoi(n) + dir;
|
||
|
else
|
||
|
x = dir;
|
||
|
memcpy(base, buf, n-buf);
|
||
|
base[n-buf] = 0;
|
||
|
fz_snprintf(out, sizeof out, "%s%d%s", base, x, p);
|
||
|
ui_input_init(&fc.input_file, out);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
int ui_save_file(char filename[PATH_MAX], void (*extra_panel)(void))
|
||
|
{
|
||
|
int i, rv = 0;
|
||
|
|
||
|
ui_panel_begin(0, 0, 4, 4, 1);
|
||
|
{
|
||
|
ui_layout(L, Y, NW, 0, 0);
|
||
|
ui_panel_begin(150, 0, 0, 0, 0);
|
||
|
{
|
||
|
ui_layout(T, X, NW, 2, 2);
|
||
|
list_drives();
|
||
|
if (extra_panel)
|
||
|
{
|
||
|
ui_spacer();
|
||
|
extra_panel();
|
||
|
}
|
||
|
ui_layout(B, X, NW, 2, 2);
|
||
|
if (ui_button("Cancel") || (!ui.focus && ui.key == KEY_ESCAPE))
|
||
|
{
|
||
|
filename[0] = 0;
|
||
|
rv = 1;
|
||
|
}
|
||
|
}
|
||
|
ui_panel_end();
|
||
|
|
||
|
ui_layout(T, X, NW, 2, 2);
|
||
|
if (ui_input(&fc.input_dir, 0, 1) == UI_INPUT_ACCEPT)
|
||
|
load_dir(fc.input_dir.text);
|
||
|
|
||
|
ui_layout(T, X, NW, 2, 2);
|
||
|
ui_panel_begin(0, ui.gridsize, 0, 0, 0);
|
||
|
{
|
||
|
ui_layout(R, NONE, CENTER, 0, 0);
|
||
|
if (ui_button("Save"))
|
||
|
{
|
||
|
fz_snprintf(filename, PATH_MAX, "%s/%s", fc.curdir, fc.input_file.text);
|
||
|
rv = 1;
|
||
|
}
|
||
|
ui_spacer();
|
||
|
if (ui_button("\xe2\x9e\x95")) /* U+2795 HEAVY PLUS */
|
||
|
bump_file_version(1);
|
||
|
if (ui_button("\xe2\x9e\x96")) /* U+2796 HEAVY MINUS */
|
||
|
bump_file_version(-1);
|
||
|
ui_spacer();
|
||
|
ui_layout(ALL, X, CENTER, 0, 0);
|
||
|
ui_input(&fc.input_file, 0, 1);
|
||
|
}
|
||
|
ui_panel_end();
|
||
|
|
||
|
ui_layout(ALL, BOTH, NW, 2, 2);
|
||
|
ui_list_begin(&fc.list_dir, fc.count, 0, 0);
|
||
|
for (i = 0; i < fc.count; ++i)
|
||
|
{
|
||
|
const char *name = fc.files[i].name;
|
||
|
char buf[PATH_MAX];
|
||
|
if (fc.files[i].is_dir)
|
||
|
fz_snprintf(buf, sizeof buf, "%C %s", ICON_FOLDER, name);
|
||
|
else
|
||
|
fz_snprintf(buf, sizeof buf, "%C %s", ICON_DOCUMENT, name);
|
||
|
if (ui_list_item(&fc.list_dir, &fc.files[i], buf, i==fc.selected))
|
||
|
{
|
||
|
fc.selected = i;
|
||
|
if (fc.files[i].is_dir)
|
||
|
{
|
||
|
fz_snprintf(buf, sizeof buf, "%s/%s", fc.curdir, name);
|
||
|
load_dir(buf);
|
||
|
ui.active = NULL;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
ui_input_init(&fc.input_file, name);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
ui_list_end(&fc.list_dir);
|
||
|
}
|
||
|
ui_panel_end();
|
||
|
|
||
|
return rv;
|
||
|
}
|