1210 lines
43 KiB
C++
1210 lines
43 KiB
C++
/*
|
|
* filedialog.cpp
|
|
* File open/save dialog implementation
|
|
* Copyright (c) 2026 Daniel Hammer
|
|
*/
|
|
|
|
#include "filedialog.hpp"
|
|
#include "main.hpp"
|
|
#include <gui/svg.hpp>
|
|
#include <gui/canvas.hpp>
|
|
#include <montauk/user.h>
|
|
|
|
extern "C" {
|
|
#include <stdio.h>
|
|
}
|
|
|
|
namespace {
|
|
|
|
using namespace gui;
|
|
using namespace filedialog;
|
|
|
|
constexpr const char* SPECIAL_FOLDER_NAMES[] = {
|
|
"Documents", "Desktop", "Music", "Videos", "Pictures", "Downloads"
|
|
};
|
|
constexpr const char* SPECIAL_FOLDER_ICONS[] = {
|
|
"folder-blue-documents.svg",
|
|
"folder-blue-desktop.svg",
|
|
"folder-blue-music.svg",
|
|
"folder-blue-videos.svg",
|
|
"folder-blue-pictures.svg",
|
|
"folder-blue-downloads.svg"
|
|
};
|
|
constexpr int SPECIAL_FOLDER_COUNT = 6;
|
|
|
|
enum EntryType : uint8_t {
|
|
ENTRY_FILE = 0,
|
|
ENTRY_DIR = 1,
|
|
ENTRY_DRIVE = 3,
|
|
ENTRY_HOME = 4,
|
|
ENTRY_SPECIAL = 7,
|
|
};
|
|
|
|
enum FocusField : uint8_t {
|
|
FOCUS_LIST = 0,
|
|
FOCUS_PATH = 1,
|
|
FOCUS_NAME = 2,
|
|
};
|
|
|
|
struct FileDialogIcons {
|
|
SvgIcon icon_go_back;
|
|
SvgIcon icon_go_forward;
|
|
SvgIcon icon_go_up;
|
|
SvgIcon icon_home;
|
|
SvgIcon icon_folder;
|
|
SvgIcon icon_folder_lg;
|
|
SvgIcon icon_file;
|
|
SvgIcon icon_file_lg;
|
|
SvgIcon icon_drive;
|
|
SvgIcon icon_drive_lg;
|
|
SvgIcon icon_home_folder;
|
|
SvgIcon icon_home_folder_lg;
|
|
SvgIcon icon_special[ SPECIAL_FOLDER_COUNT ];
|
|
SvgIcon icon_special_lg[ SPECIAL_FOLDER_COUNT ];
|
|
};
|
|
|
|
struct FileDialogState {
|
|
FileDialogIcons icons;
|
|
|
|
char home_dir[256];
|
|
char current_path[256];
|
|
char history[MAX_HISTORY][256];
|
|
int history_pos;
|
|
int history_count;
|
|
|
|
char entry_names[MAX_ENTRIES][64];
|
|
uint8_t entry_types[MAX_ENTRIES];
|
|
int entry_sizes[MAX_ENTRIES];
|
|
bool is_dir[MAX_ENTRIES];
|
|
int drive_indices[MAX_ENTRIES];
|
|
int entry_count;
|
|
|
|
int selected;
|
|
int last_click_item;
|
|
uint64_t last_click_time;
|
|
|
|
Scrollbar scrollbar;
|
|
bool grid_view;
|
|
bool at_drives_root;
|
|
FocusField focus;
|
|
|
|
char path_buf[256];
|
|
int path_len;
|
|
int path_cursor;
|
|
|
|
char filename_buf[128];
|
|
int filename_len;
|
|
int filename_cursor;
|
|
|
|
char pending_select[64];
|
|
char error[160];
|
|
|
|
int mouse_x;
|
|
int mouse_y;
|
|
};
|
|
|
|
FileDialogState g_file;
|
|
|
|
void safe_copy(char* dst, int dst_len, const char* src) {
|
|
gui::dialogs::safe_copy(dst, dst_len, src);
|
|
}
|
|
|
|
int str_compare_ci(const char* a, const char* b) {
|
|
while (*a && *b) {
|
|
char ca = (*a >= 'A' && *a <= 'Z') ? (*a + 32) : *a;
|
|
char cb = (*b >= 'A' && *b <= 'Z') ? (*b + 32) : *b;
|
|
if (ca != cb) return ca - cb;
|
|
a++;
|
|
b++;
|
|
}
|
|
return (unsigned char)*a - (unsigned char)*b;
|
|
}
|
|
|
|
bool str_ends_with(const char* s, const char* suffix) {
|
|
int slen = montauk::slen(s);
|
|
int suflen = montauk::slen(suffix);
|
|
if (suflen > slen) return false;
|
|
for (int i = 0; i < suflen; i++) {
|
|
char sc = s[slen - suflen + i];
|
|
char ec = suffix[i];
|
|
if (sc >= 'A' && sc <= 'Z') sc += 32;
|
|
if (ec >= 'A' && ec <= 'Z') ec += 32;
|
|
if (sc != ec) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void str_append(char* dst, const char* src, int max_len) {
|
|
int len = montauk::slen(dst);
|
|
int i = 0;
|
|
while (src[i] && len < max_len - 1) {
|
|
dst[len++] = src[i++];
|
|
}
|
|
dst[len] = '\0';
|
|
}
|
|
|
|
const char* path_basename(const char* path) {
|
|
const char* last = path;
|
|
for (const char* p = path; *p; p++) {
|
|
if (*p == '/' && *(p + 1)) last = p + 1;
|
|
}
|
|
return last;
|
|
}
|
|
|
|
void build_fullpath(char* out, int out_len, const char* dir, const char* name) {
|
|
safe_copy(out, out_len, dir);
|
|
int len = montauk::slen(out);
|
|
if (len > 0 && out[len - 1] != '/') str_append(out, "/", out_len);
|
|
str_append(out, name, out_len);
|
|
}
|
|
|
|
void split_parent_path(const char* path, char* out_dir, int out_dir_len, char* out_name, int out_name_len) {
|
|
safe_copy(out_dir, out_dir_len, "");
|
|
safe_copy(out_name, out_name_len, "");
|
|
if (!path || !path[0]) return;
|
|
|
|
int last_slash = -1;
|
|
for (int i = 0; path[i]; i++)
|
|
if (path[i] == '/') last_slash = i;
|
|
|
|
if (last_slash < 0) {
|
|
safe_copy(out_name, out_name_len, path);
|
|
return;
|
|
}
|
|
|
|
int dir_len = last_slash + 1;
|
|
if (dir_len >= out_dir_len) dir_len = out_dir_len - 1;
|
|
montauk::memcpy(out_dir, path, (uint64_t)dir_len);
|
|
out_dir[dir_len] = '\0';
|
|
safe_copy(out_name, out_name_len, path + last_slash + 1);
|
|
}
|
|
|
|
bool is_dir_path(const char* path) {
|
|
if (!path || !path[0]) return false;
|
|
const char* names[1];
|
|
return montauk::readdir(path, names, 1) >= 0;
|
|
}
|
|
|
|
bool path_exists_via_open(const char* path) {
|
|
if (!path || !path[0]) return false;
|
|
int fd = montauk::open(path);
|
|
if (fd < 0) return false;
|
|
montauk::close(fd);
|
|
return true;
|
|
}
|
|
|
|
void format_size(char* buf, int buf_len, int size) {
|
|
if (size < 1024) snprintf(buf, buf_len, "%d B", size);
|
|
else if (size < 1024 * 1024) {
|
|
int kb = size / 1024;
|
|
int frac = ((size % 1024) * 10) / 1024;
|
|
if (kb < 10) snprintf(buf, buf_len, "%d.%d KB", kb, frac);
|
|
else snprintf(buf, buf_len, "%d KB", kb);
|
|
} else {
|
|
int mb = size / (1024 * 1024);
|
|
int frac = ((size % (1024 * 1024)) * 10) / (1024 * 1024);
|
|
if (mb < 10) snprintf(buf, buf_len, "%d.%d MB", mb, frac);
|
|
else snprintf(buf, buf_len, "%d MB", mb);
|
|
}
|
|
}
|
|
|
|
void fit_text_end(const char* src, char* out, int out_len, int max_px) {
|
|
if (!src || !src[0]) {
|
|
safe_copy(out, out_len, "");
|
|
return;
|
|
}
|
|
if (text_width(src) <= max_px) {
|
|
safe_copy(out, out_len, src);
|
|
return;
|
|
}
|
|
|
|
int slen = montauk::slen(src);
|
|
int keep = slen;
|
|
while (keep > 1) {
|
|
char candidate[256];
|
|
candidate[0] = '.';
|
|
candidate[1] = '.';
|
|
int pos = 2;
|
|
const char* tail = src + (slen - keep);
|
|
while (*tail && pos < (int)sizeof(candidate) - 1)
|
|
candidate[pos++] = *tail++;
|
|
candidate[pos] = '\0';
|
|
if (text_width(candidate) <= max_px) {
|
|
safe_copy(out, out_len, candidate);
|
|
return;
|
|
}
|
|
keep--;
|
|
}
|
|
|
|
safe_copy(out, out_len, src);
|
|
}
|
|
|
|
void draw_input(Canvas& c, const Rect& rect, const char* text, bool focused, int cursor_pos) {
|
|
c.fill_rect(rect.x, rect.y, rect.w, rect.h, colors::WHITE);
|
|
c.rect(rect.x, rect.y, rect.w, rect.h, focused ? colors::ACCENT : colors::BORDER);
|
|
int ty = rect.y + (rect.h - system_font_height()) / 2;
|
|
c.text(rect.x + 6, ty, text, colors::TEXT_COLOR);
|
|
if (focused) {
|
|
char prefix[256];
|
|
int plen = cursor_pos;
|
|
if (plen > 255) plen = 255;
|
|
for (int i = 0; i < plen; i++) prefix[i] = text[i];
|
|
prefix[plen] = '\0';
|
|
int cx = rect.x + 6 + text_width(prefix);
|
|
c.vline(cx, rect.y + 4, rect.h - 8, colors::ACCENT);
|
|
}
|
|
}
|
|
|
|
void draw_action_button(Canvas& c, const Rect& rect, const char* label, bool enabled, bool hovered, bool primary) {
|
|
Color bg = primary ? colors::ACCENT : Color::from_rgb(0xE8, 0xE8, 0xE8);
|
|
Color fg = primary ? colors::WHITE : colors::TEXT_COLOR;
|
|
if (!enabled) {
|
|
bg = Color::from_rgb(0xF0, 0xF0, 0xF0);
|
|
fg = Color::from_rgb(0x9A, 0x9A, 0x9A);
|
|
} else if (hovered) {
|
|
bg = primary ? Color::from_rgb(0x2B, 0x6B, 0xE0) : Color::from_rgb(0xDC, 0xDC, 0xDC);
|
|
}
|
|
c.fill_rounded_rect(rect.x, rect.y, rect.w, rect.h, 4, bg);
|
|
int tx = rect.x + (rect.w - text_width(label)) / 2;
|
|
int ty = rect.y + (rect.h - system_font_height()) / 2;
|
|
c.text(tx, ty, label, fg);
|
|
}
|
|
|
|
void draw_scrollbar(Canvas& c, const Scrollbar& sb) {
|
|
if (sb.content_height <= sb.view_height) return;
|
|
c.fill_rect(sb.bounds.x, sb.bounds.y, sb.bounds.w, sb.bounds.h, sb.bg);
|
|
int th = sb.thumb_height();
|
|
int ty = sb.thumb_y();
|
|
Color thumb = (sb.hovered || sb.dragging) ? sb.hover_fg : sb.fg;
|
|
c.fill_rounded_rect(sb.bounds.x + 1, ty, sb.bounds.w - 2, th, 3, thumb);
|
|
}
|
|
|
|
FileDialogLayout file_layout(const FileDialogState* st) {
|
|
FileDialogLayout lo = {};
|
|
int footer_h = (g_app.request.mode == gui::dialogs::FILE_DIALOG_SAVE) ? FOOTER_H_SAVE : FOOTER_H_OPEN;
|
|
lo.back_btn = {4, 4, 24, 24};
|
|
lo.forward_btn = {32, 4, 24, 24};
|
|
lo.up_btn = {60, 4, 24, 24};
|
|
lo.home_btn = {88, 4, 24, 24};
|
|
lo.view_btn = {120, 4, 24, 24};
|
|
lo.path_rect = {8, TOOLBAR_H + 4, g_app.win.width - 16, PATHBAR_H - 8};
|
|
lo.content_rect = {0, TOOLBAR_H + PATHBAR_H, g_app.win.width, g_app.win.height - TOOLBAR_H - PATHBAR_H - footer_h};
|
|
lo.footer_rect = {0, g_app.win.height - footer_h, g_app.win.width, footer_h};
|
|
lo.list_rect = lo.content_rect;
|
|
int btn_y = (g_app.request.mode == gui::dialogs::FILE_DIALOG_SAVE)
|
|
? lo.footer_rect.y + 12
|
|
: lo.footer_rect.y + (footer_h - BUTTON_H) / 2;
|
|
lo.confirm_btn = {g_app.win.width - BUTTON_W - 8, btn_y, BUTTON_W, BUTTON_H};
|
|
lo.cancel_btn = {lo.confirm_btn.x - BUTTON_W - 8, btn_y, BUTTON_W, BUTTON_H};
|
|
lo.name_rect = {88, lo.footer_rect.y + 12, lo.cancel_btn.x - 100, 30};
|
|
if (lo.name_rect.w < 120) lo.name_rect.w = 120;
|
|
(void)st;
|
|
return lo;
|
|
}
|
|
|
|
void file_sync_path_buffer(FileDialogState* st) {
|
|
if (st->at_drives_root) {
|
|
st->path_buf[0] = '\0';
|
|
st->path_len = 0;
|
|
st->path_cursor = 0;
|
|
return;
|
|
}
|
|
safe_copy(st->path_buf, sizeof(st->path_buf), st->current_path);
|
|
st->path_len = montauk::slen(st->path_buf);
|
|
st->path_cursor = st->path_len;
|
|
}
|
|
|
|
void file_set_filename(FileDialogState* st, const char* name) {
|
|
safe_copy(st->filename_buf, sizeof(st->filename_buf), name);
|
|
st->filename_len = montauk::slen(st->filename_buf);
|
|
st->filename_cursor = st->filename_len;
|
|
}
|
|
|
|
void file_set_error(FileDialogState* st, const char* msg) {
|
|
safe_copy(st->error, sizeof(st->error), msg);
|
|
}
|
|
|
|
int special_folder_index(const char* name) {
|
|
for (int i = 0; i < SPECIAL_FOLDER_COUNT; i++) {
|
|
if (montauk::streq(name, SPECIAL_FOLDER_NAMES[i])) return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
void load_file_icons(FileDialogState* st) {
|
|
Color def = colors::ICON_COLOR;
|
|
st->icons.icon_go_back = svg_load("0:/icons/go-previous-symbolic.svg", 16, 16, def);
|
|
st->icons.icon_go_forward = svg_load("0:/icons/go-next-symbolic.svg", 16, 16, def);
|
|
st->icons.icon_go_up = svg_load("0:/icons/go-up-symbolic.svg", 16, 16, def);
|
|
st->icons.icon_home = svg_load("0:/icons/user-home.svg", 16, 16, def);
|
|
st->icons.icon_folder = svg_load("0:/icons/folder.svg", 16, 16, def);
|
|
st->icons.icon_folder_lg = svg_load("0:/icons/folder.svg", 48, 48, def);
|
|
st->icons.icon_file = svg_load("0:/icons/text-x-generic.svg", 16, 16, def);
|
|
st->icons.icon_file_lg = svg_load("0:/icons/text-x-generic.svg", 48, 48, def);
|
|
st->icons.icon_drive = svg_load("0:/icons/drive-harddisk.svg", 16, 16, def);
|
|
st->icons.icon_drive_lg = svg_load("0:/icons/drive-harddisk.svg", 48, 48, def);
|
|
st->icons.icon_home_folder = svg_load("0:/icons/folder-blue-home.svg", 16, 16, def);
|
|
st->icons.icon_home_folder_lg = svg_load("0:/icons/folder-blue-home.svg", 48, 48, def);
|
|
for (int i = 0; i < SPECIAL_FOLDER_COUNT; i++) {
|
|
char path[128];
|
|
snprintf(path, sizeof(path), "0:/icons/%s", SPECIAL_FOLDER_ICONS[i]);
|
|
st->icons.icon_special[i] = svg_load(path, 16, 16, def);
|
|
st->icons.icon_special_lg[i] = svg_load(path, 48, 48, def);
|
|
}
|
|
}
|
|
|
|
void free_icon(SvgIcon* icon) {
|
|
if (icon && icon->pixels) {
|
|
svg_free(*icon);
|
|
icon->pixels = nullptr;
|
|
}
|
|
}
|
|
|
|
void free_file_icons(FileDialogState* st) {
|
|
free_icon(&st->icons.icon_go_back);
|
|
free_icon(&st->icons.icon_go_forward);
|
|
free_icon(&st->icons.icon_go_up);
|
|
free_icon(&st->icons.icon_home);
|
|
free_icon(&st->icons.icon_folder);
|
|
free_icon(&st->icons.icon_folder_lg);
|
|
free_icon(&st->icons.icon_file);
|
|
free_icon(&st->icons.icon_file_lg);
|
|
free_icon(&st->icons.icon_drive);
|
|
free_icon(&st->icons.icon_drive_lg);
|
|
free_icon(&st->icons.icon_home_folder);
|
|
free_icon(&st->icons.icon_home_folder_lg);
|
|
for (int i = 0; i < SPECIAL_FOLDER_COUNT; i++) {
|
|
free_icon(&st->icons.icon_special[i]);
|
|
free_icon(&st->icons.icon_special_lg[i]);
|
|
}
|
|
}
|
|
|
|
void file_push_history(FileDialogState* st) {
|
|
if (st->history_count > 0 && st->history_pos >= 0) {
|
|
if (montauk::streq(st->history[st->history_pos], st->current_path)) return;
|
|
}
|
|
st->history_pos++;
|
|
if (st->history_pos >= MAX_HISTORY) st->history_pos = MAX_HISTORY - 1;
|
|
safe_copy(st->history[st->history_pos], sizeof(st->history[st->history_pos]), st->current_path);
|
|
st->history_count = st->history_pos + 1;
|
|
}
|
|
|
|
void file_apply_pending_select(FileDialogState* st) {
|
|
st->selected = -1;
|
|
if (!st->pending_select[0]) return;
|
|
for (int i = 0; i < st->entry_count; i++) {
|
|
if (montauk::streq(st->entry_names[i], st->pending_select)) {
|
|
st->selected = i;
|
|
break;
|
|
}
|
|
}
|
|
st->pending_select[0] = '\0';
|
|
}
|
|
|
|
void file_read_drives(FileDialogState* st) {
|
|
st->entry_count = 0;
|
|
st->at_drives_root = true;
|
|
st->current_path[0] = '\0';
|
|
|
|
if (st->home_dir[0] != '\0') {
|
|
int i = st->entry_count++;
|
|
safe_copy(st->entry_names[i], sizeof(st->entry_names[i]), "Home");
|
|
st->entry_types[i] = ENTRY_HOME;
|
|
st->entry_sizes[i] = 0;
|
|
st->is_dir[i] = true;
|
|
st->drive_indices[i] = -1;
|
|
}
|
|
|
|
if (st->home_dir[0] != '\0') {
|
|
for (int sf = 0; sf < SPECIAL_FOLDER_COUNT && st->entry_count < MAX_ENTRIES; sf++) {
|
|
char probe[256];
|
|
safe_copy(probe, sizeof(probe), st->home_dir);
|
|
int plen = montauk::slen(probe);
|
|
if (plen > 0 && probe[plen - 1] != '/') str_append(probe, "/", sizeof(probe));
|
|
str_append(probe, SPECIAL_FOLDER_NAMES[sf], sizeof(probe));
|
|
if (path_exists_via_open(probe)) {
|
|
int i = st->entry_count++;
|
|
safe_copy(st->entry_names[i], sizeof(st->entry_names[i]), SPECIAL_FOLDER_NAMES[sf]);
|
|
st->entry_types[i] = ENTRY_SPECIAL;
|
|
st->entry_sizes[i] = 0;
|
|
st->is_dir[i] = true;
|
|
st->drive_indices[i] = sf;
|
|
}
|
|
}
|
|
}
|
|
|
|
int drives[MAX_DRIVES];
|
|
int drive_count = montauk::drivelist(drives, MAX_DRIVES);
|
|
for (int di = 0; di < drive_count && st->entry_count < MAX_ENTRIES; di++) {
|
|
int d = drives[di];
|
|
int i = st->entry_count++;
|
|
char label[64];
|
|
if (d < 10) snprintf(label, sizeof(label), "Drive %d:/", d);
|
|
else snprintf(label, sizeof(label), "Drive %d:/", d);
|
|
safe_copy(st->entry_names[i], sizeof(st->entry_names[i]), label);
|
|
st->entry_types[i] = ENTRY_DRIVE;
|
|
st->entry_sizes[i] = 0;
|
|
st->is_dir[i] = true;
|
|
st->drive_indices[i] = d;
|
|
}
|
|
|
|
st->scrollbar.scroll_offset = 0;
|
|
st->last_click_item = -1;
|
|
st->last_click_time = 0;
|
|
file_sync_path_buffer(st);
|
|
file_apply_pending_select(st);
|
|
}
|
|
|
|
void file_read_dir(FileDialogState* st) {
|
|
if (!st->current_path[0] || !is_dir_path(st->current_path)) {
|
|
file_read_drives(st);
|
|
return;
|
|
}
|
|
|
|
st->at_drives_root = false;
|
|
const char* names[MAX_ENTRIES];
|
|
st->entry_count = montauk::readdir(st->current_path, names, MAX_ENTRIES);
|
|
if (st->entry_count < 0) st->entry_count = 0;
|
|
|
|
const char* after_drive = st->current_path;
|
|
for (int k = 0; after_drive[k]; k++) {
|
|
if (after_drive[k] == ':' && after_drive[k + 1] == '/') {
|
|
after_drive += k + 2;
|
|
break;
|
|
}
|
|
}
|
|
|
|
char prefix[256] = {};
|
|
int prefix_len = 0;
|
|
if (after_drive[0] != '\0') {
|
|
safe_copy(prefix, sizeof(prefix), after_drive);
|
|
prefix_len = montauk::slen(prefix);
|
|
if (prefix_len > 0 && prefix[prefix_len - 1] != '/') {
|
|
prefix[prefix_len++] = '/';
|
|
prefix[prefix_len] = '\0';
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < st->entry_count; i++) {
|
|
const char* raw = names[i];
|
|
if (prefix_len > 0) {
|
|
bool match = true;
|
|
for (int k = 0; k < prefix_len; k++) {
|
|
if (raw[k] != prefix[k]) { match = false; break; }
|
|
}
|
|
if (match) raw += prefix_len;
|
|
}
|
|
|
|
safe_copy(st->entry_names[i], sizeof(st->entry_names[i]), raw);
|
|
int len = montauk::slen(st->entry_names[i]);
|
|
if (len > 0 && st->entry_names[i][len - 1] == '/') {
|
|
st->is_dir[i] = true;
|
|
st->entry_names[i][len - 1] = '\0';
|
|
} else {
|
|
st->is_dir[i] = false;
|
|
}
|
|
|
|
st->entry_types[i] = st->is_dir[i] ? ENTRY_DIR : ENTRY_FILE;
|
|
st->entry_sizes[i] = 0;
|
|
st->drive_indices[i] = -1;
|
|
|
|
if (!st->is_dir[i]) {
|
|
char full[512];
|
|
build_fullpath(full, sizeof(full), st->current_path, st->entry_names[i]);
|
|
int fd = montauk::open(full);
|
|
if (fd >= 0) {
|
|
st->entry_sizes[i] = (int)montauk::getsize(fd);
|
|
montauk::close(fd);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int i = 1; i < st->entry_count; i++) {
|
|
char tmp_name[64];
|
|
uint8_t tmp_type = st->entry_types[i];
|
|
int tmp_size = st->entry_sizes[i];
|
|
bool tmp_isdir = st->is_dir[i];
|
|
safe_copy(tmp_name, sizeof(tmp_name), st->entry_names[i]);
|
|
|
|
int j = i - 1;
|
|
while (j >= 0) {
|
|
bool swap = false;
|
|
if (tmp_isdir && !st->is_dir[j]) swap = true;
|
|
else if (tmp_isdir == st->is_dir[j] && str_compare_ci(tmp_name, st->entry_names[j]) < 0) swap = true;
|
|
if (!swap) break;
|
|
|
|
safe_copy(st->entry_names[j + 1], sizeof(st->entry_names[j + 1]), st->entry_names[j]);
|
|
st->entry_types[j + 1] = st->entry_types[j];
|
|
st->entry_sizes[j + 1] = st->entry_sizes[j];
|
|
st->is_dir[j + 1] = st->is_dir[j];
|
|
st->drive_indices[j + 1] = st->drive_indices[j];
|
|
j--;
|
|
|
|
safe_copy(st->entry_names[j + 1], sizeof(st->entry_names[j + 1]), tmp_name);
|
|
st->entry_types[j + 1] = tmp_type;
|
|
st->entry_sizes[j + 1] = tmp_size;
|
|
st->is_dir[j + 1] = tmp_isdir;
|
|
st->drive_indices[j + 1] = -1;
|
|
}
|
|
}
|
|
|
|
st->scrollbar.scroll_offset = 0;
|
|
st->last_click_item = -1;
|
|
st->last_click_time = 0;
|
|
file_sync_path_buffer(st);
|
|
file_apply_pending_select(st);
|
|
}
|
|
|
|
void file_ensure_selected_visible(FileDialogState* st) {
|
|
if (st->selected < 0 || st->selected >= st->entry_count) return;
|
|
FileDialogLayout lo = file_layout(st);
|
|
if (st->grid_view) {
|
|
int cols = (lo.content_rect.w - SCROLLBAR_W) / GRID_CELL_W;
|
|
if (cols < 1) cols = 1;
|
|
int row = st->selected / cols;
|
|
int top = row * GRID_CELL_H;
|
|
int bottom = top + GRID_CELL_H;
|
|
if (top < st->scrollbar.scroll_offset) st->scrollbar.scroll_offset = top;
|
|
else if (bottom > st->scrollbar.scroll_offset + lo.content_rect.h)
|
|
st->scrollbar.scroll_offset = bottom - lo.content_rect.h;
|
|
} else {
|
|
int view_h = lo.content_rect.h - HEADER_H;
|
|
int top = st->selected * ITEM_H;
|
|
int bottom = top + ITEM_H;
|
|
if (top < st->scrollbar.scroll_offset) st->scrollbar.scroll_offset = top;
|
|
else if (bottom > st->scrollbar.scroll_offset + view_h)
|
|
st->scrollbar.scroll_offset = bottom - view_h;
|
|
}
|
|
int ms = st->scrollbar.max_scroll();
|
|
if (st->scrollbar.scroll_offset < 0) st->scrollbar.scroll_offset = 0;
|
|
if (st->scrollbar.scroll_offset > ms) st->scrollbar.scroll_offset = ms;
|
|
}
|
|
|
|
void file_navigate_into(FileDialogState* st, int idx) {
|
|
if (idx < 0 || idx >= st->entry_count) return;
|
|
|
|
if (st->at_drives_root && st->entry_types[idx] == ENTRY_HOME) {
|
|
safe_copy(st->current_path, sizeof(st->current_path), st->home_dir);
|
|
file_push_history(st);
|
|
file_read_dir(st);
|
|
return;
|
|
}
|
|
|
|
if (st->at_drives_root && st->entry_types[idx] == ENTRY_SPECIAL) {
|
|
safe_copy(st->current_path, sizeof(st->current_path), st->home_dir);
|
|
int len = montauk::slen(st->current_path);
|
|
if (len > 0 && st->current_path[len - 1] != '/') str_append(st->current_path, "/", sizeof(st->current_path));
|
|
str_append(st->current_path, st->entry_names[idx], sizeof(st->current_path));
|
|
file_push_history(st);
|
|
file_read_dir(st);
|
|
return;
|
|
}
|
|
|
|
if (st->at_drives_root && st->entry_types[idx] == ENTRY_DRIVE) {
|
|
snprintf(st->current_path, sizeof(st->current_path), "%d:/", st->drive_indices[idx]);
|
|
file_push_history(st);
|
|
file_read_dir(st);
|
|
return;
|
|
}
|
|
|
|
if (st->is_dir[idx]) {
|
|
char next[256];
|
|
build_fullpath(next, sizeof(next), st->current_path, st->entry_names[idx]);
|
|
safe_copy(st->current_path, sizeof(st->current_path), next);
|
|
file_push_history(st);
|
|
file_read_dir(st);
|
|
}
|
|
}
|
|
|
|
bool file_accept_open(FileDialogState* st) {
|
|
if (st->selected < 0 || st->selected >= st->entry_count) {
|
|
file_set_error(st, "Select a file to open");
|
|
return false;
|
|
}
|
|
if (st->is_dir[st->selected]) {
|
|
file_navigate_into(st, st->selected);
|
|
return false;
|
|
}
|
|
|
|
char full[512];
|
|
build_fullpath(full, sizeof(full), st->current_path, st->entry_names[st->selected]);
|
|
dialog_finish("ok", full, "", full);
|
|
return true;
|
|
}
|
|
|
|
bool file_accept_save(FileDialogState* st) {
|
|
if (st->at_drives_root) {
|
|
file_set_error(st, "Choose a folder before saving");
|
|
return false;
|
|
}
|
|
if (!st->filename_buf[0]) {
|
|
file_set_error(st, "Enter a file name");
|
|
return false;
|
|
}
|
|
|
|
char full[512];
|
|
build_fullpath(full, sizeof(full), st->current_path, st->filename_buf);
|
|
dialog_finish("ok", full, "", full);
|
|
return true;
|
|
}
|
|
|
|
void file_activate_selected(FileDialogState* st, bool from_double_click) {
|
|
if (st->selected < 0 || st->selected >= st->entry_count) return;
|
|
if (st->is_dir[st->selected]) {
|
|
file_navigate_into(st, st->selected);
|
|
return;
|
|
}
|
|
|
|
if (g_app.request.mode == gui::dialogs::FILE_DIALOG_SAVE) {
|
|
file_set_filename(st, st->entry_names[st->selected]);
|
|
if (from_double_click) file_accept_save(st);
|
|
} else {
|
|
file_accept_open(st);
|
|
}
|
|
}
|
|
|
|
void file_go_home(FileDialogState* st) {
|
|
file_read_drives(st);
|
|
file_push_history(st);
|
|
}
|
|
|
|
void file_go_back(FileDialogState* st) {
|
|
if (st->history_pos <= 0) return;
|
|
st->history_pos--;
|
|
safe_copy(st->current_path, sizeof(st->current_path), st->history[st->history_pos]);
|
|
if (st->current_path[0]) file_read_dir(st);
|
|
else file_read_drives(st);
|
|
}
|
|
|
|
void file_go_forward(FileDialogState* st) {
|
|
if (st->history_pos >= st->history_count - 1) return;
|
|
st->history_pos++;
|
|
safe_copy(st->current_path, sizeof(st->current_path), st->history[st->history_pos]);
|
|
if (st->current_path[0]) file_read_dir(st);
|
|
else file_read_drives(st);
|
|
}
|
|
|
|
void file_go_up(FileDialogState* st) {
|
|
if (st->at_drives_root) return;
|
|
|
|
int len = montauk::slen(st->current_path);
|
|
if (len >= 3 && st->current_path[len - 1] == '/') {
|
|
int colon = -1;
|
|
for (int i = 0; i < len; i++) {
|
|
if (st->current_path[i] == ':') { colon = i; break; }
|
|
}
|
|
if (colon >= 0 && colon + 2 == len) {
|
|
file_read_drives(st);
|
|
file_push_history(st);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (len > 0 && st->current_path[len - 1] == '/') {
|
|
st->current_path[len - 1] = '\0';
|
|
len--;
|
|
}
|
|
int last_slash = -1;
|
|
for (int i = len - 1; i >= 0; i--) {
|
|
if (st->current_path[i] == '/') { last_slash = i; break; }
|
|
}
|
|
if (last_slash >= 0) {
|
|
if (last_slash > 0 && st->current_path[last_slash - 1] == ':') st->current_path[last_slash + 1] = '\0';
|
|
else st->current_path[last_slash] = '\0';
|
|
}
|
|
file_push_history(st);
|
|
file_read_dir(st);
|
|
}
|
|
|
|
void file_open_path(FileDialogState* st, const char* path) {
|
|
if (!path || !path[0]) {
|
|
if (st->home_dir[0]) {
|
|
safe_copy(st->current_path, sizeof(st->current_path), st->home_dir);
|
|
file_push_history(st);
|
|
file_read_dir(st);
|
|
} else {
|
|
file_read_drives(st);
|
|
file_push_history(st);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (is_dir_path(path)) {
|
|
safe_copy(st->current_path, sizeof(st->current_path), path);
|
|
file_push_history(st);
|
|
file_read_dir(st);
|
|
return;
|
|
}
|
|
|
|
char dir[256];
|
|
char name[128];
|
|
split_parent_path(path, dir, sizeof(dir), name, sizeof(name));
|
|
if (dir[0] && is_dir_path(dir)) {
|
|
safe_copy(st->current_path, sizeof(st->current_path), dir);
|
|
safe_copy(st->pending_select, sizeof(st->pending_select), name);
|
|
if (g_app.request.mode == gui::dialogs::FILE_DIALOG_SAVE && name[0]) file_set_filename(st, name);
|
|
file_push_history(st);
|
|
file_read_dir(st);
|
|
if (g_app.request.mode == gui::dialogs::FILE_DIALOG_OPEN && name[0]) {
|
|
for (int i = 0; i < st->entry_count; i++) {
|
|
if (montauk::streq(st->entry_names[i], name)) {
|
|
st->selected = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
file_set_error(st, "Path does not exist");
|
|
}
|
|
|
|
void init_file_dialog(const gui::dialogs::Request& req) {
|
|
FileDialogState* st = &g_file;
|
|
montauk::memset(st, 0, sizeof(*st));
|
|
load_file_icons(st);
|
|
st->selected = -1;
|
|
st->last_click_item = -1;
|
|
st->history_pos = -1;
|
|
st->grid_view = true;
|
|
st->focus = FOCUS_LIST;
|
|
st->scrollbar.init(0, 0, SCROLLBAR_W, 100);
|
|
montauk::user::get_home_dir(st->home_dir, sizeof(st->home_dir));
|
|
|
|
char initial_dir[256] = {};
|
|
char initial_name[128] = {};
|
|
if (req.initial_path[0]) split_parent_path(req.initial_path, initial_dir, sizeof(initial_dir), initial_name, sizeof(initial_name));
|
|
|
|
if (req.initial_path[0] && is_dir_path(req.initial_path)) {
|
|
safe_copy(st->current_path, sizeof(st->current_path), req.initial_path);
|
|
file_read_dir(st);
|
|
file_push_history(st);
|
|
} else if (initial_dir[0] && is_dir_path(initial_dir)) {
|
|
safe_copy(st->current_path, sizeof(st->current_path), initial_dir);
|
|
safe_copy(st->pending_select, sizeof(st->pending_select), initial_name);
|
|
file_read_dir(st);
|
|
file_push_history(st);
|
|
} else if (st->home_dir[0] && is_dir_path(st->home_dir)) {
|
|
safe_copy(st->current_path, sizeof(st->current_path), st->home_dir);
|
|
file_read_dir(st);
|
|
file_push_history(st);
|
|
} else {
|
|
file_read_drives(st);
|
|
file_push_history(st);
|
|
}
|
|
|
|
if (req.mode == gui::dialogs::FILE_DIALOG_SAVE) {
|
|
if (req.suggested_name[0]) file_set_filename(st, req.suggested_name);
|
|
else if (initial_name[0]) file_set_filename(st, initial_name);
|
|
}
|
|
}
|
|
|
|
void draw_toolbar_icon(Canvas& c, const Rect& rect, const SvgIcon& icon) {
|
|
c.fill_rect(rect.x, rect.y, rect.w, rect.h, Color::from_rgb(0xE8, 0xE8, 0xE8));
|
|
if (icon.pixels) {
|
|
int ix = rect.x + (rect.w - icon.width) / 2;
|
|
int iy = rect.y + (rect.h - icon.height) / 2;
|
|
c.icon(ix, iy, icon);
|
|
}
|
|
}
|
|
|
|
void draw_grid_entries(Canvas& c, FileDialogState* st, const FileDialogLayout& lo) {
|
|
int cols = (lo.content_rect.w - SCROLLBAR_W) / GRID_CELL_W;
|
|
if (cols < 1) cols = 1;
|
|
int rows = (st->entry_count + cols - 1) / cols;
|
|
int content_h = rows * GRID_CELL_H;
|
|
st->scrollbar.bounds = {c.w - SCROLLBAR_W, lo.content_rect.y, SCROLLBAR_W, lo.content_rect.h};
|
|
st->scrollbar.content_height = content_h;
|
|
st->scrollbar.view_height = lo.content_rect.h;
|
|
|
|
for (int i = 0; i < st->entry_count; i++) {
|
|
int col = i % cols;
|
|
int row = i / cols;
|
|
int cell_x = col * GRID_CELL_W;
|
|
int cell_y = lo.content_rect.y + row * GRID_CELL_H - st->scrollbar.scroll_offset;
|
|
if (cell_y + GRID_CELL_H <= lo.content_rect.y || cell_y >= lo.content_rect.y + lo.content_rect.h) continue;
|
|
|
|
if (i == st->selected) {
|
|
int sy = gui_max(cell_y, lo.content_rect.y);
|
|
int sh = gui_min(cell_y + GRID_CELL_H, lo.content_rect.y + lo.content_rect.h) - sy;
|
|
int sw = gui_min(GRID_CELL_W, c.w - SCROLLBAR_W - cell_x);
|
|
if (sh > 0 && sw > 0) c.fill_rect(cell_x, sy, sw, sh, colors::MENU_HOVER);
|
|
}
|
|
|
|
int icon_x = cell_x + (GRID_CELL_W - GRID_ICON) / 2;
|
|
int icon_y = cell_y + GRID_PAD;
|
|
SvgIcon* icon = &st->icons.icon_file_lg;
|
|
int sfi = -1;
|
|
if (st->entry_types[i] == ENTRY_SPECIAL) sfi = st->drive_indices[i];
|
|
if (sfi >= 0 && st->icons.icon_special_lg[sfi].pixels) icon = &st->icons.icon_special_lg[sfi];
|
|
else if (st->entry_types[i] == ENTRY_HOME && st->icons.icon_home_folder_lg.pixels) icon = &st->icons.icon_home_folder_lg;
|
|
else if (st->entry_types[i] == ENTRY_DRIVE && st->icons.icon_drive_lg.pixels) icon = &st->icons.icon_drive_lg;
|
|
else if (st->is_dir[i] && st->icons.icon_folder_lg.pixels) icon = &st->icons.icon_folder_lg;
|
|
if (icon->pixels) c.icon(icon_x, icon_y, *icon);
|
|
|
|
char label[16];
|
|
int nlen = montauk::slen(st->entry_names[i]);
|
|
if (nlen > 11) {
|
|
for (int k = 0; k < 9; k++) label[k] = st->entry_names[i][k];
|
|
label[9] = '.';
|
|
label[10] = '.';
|
|
label[11] = '\0';
|
|
} else {
|
|
safe_copy(label, sizeof(label), st->entry_names[i]);
|
|
}
|
|
int tx = cell_x + (GRID_CELL_W - text_width(label)) / 2;
|
|
if (tx < cell_x) tx = cell_x;
|
|
int ty = icon_y + GRID_ICON + 2;
|
|
if (ty >= lo.content_rect.y && ty + system_font_height() <= lo.content_rect.y + lo.content_rect.h)
|
|
c.text(tx, ty, label, colors::TEXT_COLOR);
|
|
}
|
|
}
|
|
|
|
void draw_list_entries(Canvas& c, FileDialogState* st, const FileDialogLayout& lo) {
|
|
int header_y = lo.content_rect.y;
|
|
int rows_y = header_y + HEADER_H;
|
|
int rows_h = lo.content_rect.h - HEADER_H;
|
|
if (rows_h < 0) rows_h = 0;
|
|
|
|
c.fill_rect(0, header_y, c.w, HEADER_H, Color::from_rgb(0xF8, 0xF8, 0xF8));
|
|
c.text(8, header_y + 2, "Name", Color::from_rgb(0x88, 0x88, 0x88));
|
|
c.text(c.w - SCROLLBAR_W - 120, header_y + 2, "Size", Color::from_rgb(0x88, 0x88, 0x88));
|
|
c.text(c.w - SCROLLBAR_W - 60, header_y + 2, "Type", Color::from_rgb(0x88, 0x88, 0x88));
|
|
c.hline(0, header_y + HEADER_H - 1, c.w, colors::BORDER);
|
|
c.vline(c.w - SCROLLBAR_W - 124, header_y, lo.content_rect.h, colors::BORDER);
|
|
c.vline(c.w - SCROLLBAR_W - 64, header_y, lo.content_rect.h, colors::BORDER);
|
|
|
|
st->scrollbar.bounds = {c.w - SCROLLBAR_W, rows_y, SCROLLBAR_W, rows_h};
|
|
st->scrollbar.content_height = st->entry_count * ITEM_H;
|
|
st->scrollbar.view_height = rows_h;
|
|
|
|
for (int i = 0; i < st->entry_count; i++) {
|
|
int y = rows_y + i * ITEM_H - st->scrollbar.scroll_offset;
|
|
if (y + ITEM_H <= rows_y || y >= rows_y + rows_h) continue;
|
|
|
|
if (i == st->selected) c.fill_rect(0, y, c.w - SCROLLBAR_W, ITEM_H, colors::MENU_HOVER);
|
|
|
|
int sfi = -1;
|
|
if (st->entry_types[i] == ENTRY_SPECIAL) sfi = st->drive_indices[i];
|
|
if (sfi >= 0 && st->icons.icon_special[sfi].pixels) c.icon(8, y + 4, st->icons.icon_special[sfi]);
|
|
else if (st->entry_types[i] == ENTRY_HOME && st->icons.icon_home_folder.pixels) c.icon(8, y + 4, st->icons.icon_home_folder);
|
|
else if (st->entry_types[i] == ENTRY_DRIVE && st->icons.icon_drive.pixels) c.icon(8, y + 4, st->icons.icon_drive);
|
|
else if (st->is_dir[i] && st->icons.icon_folder.pixels) c.icon(8, y + 4, st->icons.icon_folder);
|
|
else if (st->icons.icon_file.pixels) c.icon(8, y + 4, st->icons.icon_file);
|
|
|
|
c.text(30, y + 3, st->entry_names[i], colors::TEXT_COLOR);
|
|
|
|
if (!st->is_dir[i]) {
|
|
char size_buf[16];
|
|
format_size(size_buf, sizeof(size_buf), st->entry_sizes[i]);
|
|
c.text(c.w - SCROLLBAR_W - 120, y + 3, size_buf, Color::from_rgb(0x66, 0x66, 0x66));
|
|
}
|
|
|
|
const char* type_label = st->is_dir[i] ? "Folder" : "File";
|
|
if (st->entry_types[i] == ENTRY_DRIVE) type_label = "Drive";
|
|
else if (st->entry_types[i] == ENTRY_HOME) type_label = "Home";
|
|
c.text(c.w - SCROLLBAR_W - 60, y + 3, type_label, Color::from_rgb(0x66, 0x66, 0x66));
|
|
c.hline(0, y + ITEM_H - 1, c.w, Color::from_rgb(0xF0, 0xF0, 0xF0));
|
|
}
|
|
}
|
|
|
|
void draw_file_dialog() {
|
|
FileDialogState* st = &g_file;
|
|
Canvas c = g_app.win.canvas();
|
|
c.fill(colors::WINDOW_BG);
|
|
|
|
FileDialogLayout lo = file_layout(st);
|
|
|
|
c.fill_rect(0, 0, c.w, TOOLBAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5));
|
|
draw_toolbar_icon(c, lo.back_btn, st->icons.icon_go_back);
|
|
draw_toolbar_icon(c, lo.forward_btn, st->icons.icon_go_forward);
|
|
draw_toolbar_icon(c, lo.up_btn, st->icons.icon_go_up);
|
|
draw_toolbar_icon(c, lo.home_btn, st->icons.icon_home);
|
|
|
|
c.fill_rect(lo.view_btn.x, lo.view_btn.y, lo.view_btn.w, lo.view_btn.h, Color::from_rgb(0xE8, 0xE8, 0xE8));
|
|
if (st->grid_view) {
|
|
for (int r = 0; r < 2; r++)
|
|
for (int cc = 0; cc < 2; cc++)
|
|
c.fill_rect(lo.view_btn.x + 5 + cc * 8, lo.view_btn.y + 5 + r * 8, 6, 6, colors::TEXT_COLOR);
|
|
} else {
|
|
for (int r = 0; r < 3; r++)
|
|
c.fill_rect(lo.view_btn.x + 5, lo.view_btn.y + 5 + r * 5, 14, 2, colors::TEXT_COLOR);
|
|
}
|
|
c.hline(0, TOOLBAR_H - 1, c.w, colors::BORDER);
|
|
|
|
char path_label[256];
|
|
if (st->at_drives_root && st->focus != FOCUS_PATH) safe_copy(path_label, sizeof(path_label), "Computer");
|
|
else safe_copy(path_label, sizeof(path_label), st->path_buf);
|
|
draw_input(c, lo.path_rect, path_label, st->focus == FOCUS_PATH, st->path_cursor);
|
|
|
|
if (st->grid_view) draw_grid_entries(c, st, lo);
|
|
else draw_list_entries(c, st, lo);
|
|
|
|
draw_scrollbar(c, st->scrollbar);
|
|
|
|
c.fill_rect(0, lo.footer_rect.y, c.w, lo.footer_rect.h, Color::from_rgb(0xF7, 0xF7, 0xF7));
|
|
c.hline(0, lo.footer_rect.y, c.w, colors::BORDER);
|
|
|
|
bool confirm_enabled = false;
|
|
const char* confirm_label = (g_app.request.mode == gui::dialogs::FILE_DIALOG_SAVE) ? "Save" : "Open";
|
|
if (g_app.request.mode == gui::dialogs::FILE_DIALOG_SAVE) {
|
|
c.text(12, lo.footer_rect.y + 18, "File name", Color::from_rgb(0x66, 0x66, 0x66));
|
|
draw_input(c, lo.name_rect, st->filename_buf, st->focus == FOCUS_NAME, st->filename_cursor);
|
|
confirm_enabled = st->filename_buf[0] != '\0' && !st->at_drives_root;
|
|
} else {
|
|
char selection[256] = {};
|
|
if (st->selected >= 0 && st->selected < st->entry_count) {
|
|
char full[512];
|
|
if (st->at_drives_root && st->entry_types[st->selected] == ENTRY_HOME) safe_copy(full, sizeof(full), st->home_dir);
|
|
else if (st->at_drives_root && st->entry_types[st->selected] == ENTRY_DRIVE) snprintf(full, sizeof(full), "%d:/", st->drive_indices[st->selected]);
|
|
else if (st->at_drives_root && st->entry_types[st->selected] == ENTRY_SPECIAL) {
|
|
safe_copy(full, sizeof(full), st->home_dir);
|
|
int len = montauk::slen(full);
|
|
if (len > 0 && full[len - 1] != '/') str_append(full, "/", sizeof(full));
|
|
str_append(full, st->entry_names[st->selected], sizeof(full));
|
|
} else {
|
|
build_fullpath(full, sizeof(full), st->current_path, st->entry_names[st->selected]);
|
|
}
|
|
fit_text_end(full, selection, sizeof(selection), lo.cancel_btn.x - 16);
|
|
} else {
|
|
safe_copy(selection, sizeof(selection), "Choose a file");
|
|
}
|
|
c.text(12, lo.footer_rect.y + 14, selection, Color::from_rgb(0x66, 0x66, 0x66));
|
|
confirm_enabled = st->selected >= 0;
|
|
}
|
|
|
|
if (st->error[0]) {
|
|
char err[160];
|
|
fit_text_end(st->error, err, sizeof(err), lo.cancel_btn.x - 16);
|
|
c.text(12, lo.footer_rect.y + lo.footer_rect.h - system_font_height() - 12, err, Color::from_rgb(0xC0, 0x33, 0x33));
|
|
}
|
|
|
|
draw_action_button(c, lo.cancel_btn, "Cancel", true, lo.cancel_btn.contains(st->mouse_x, st->mouse_y), false);
|
|
draw_action_button(c, lo.confirm_btn, confirm_label, confirm_enabled, lo.confirm_btn.contains(st->mouse_x, st->mouse_y), true);
|
|
}
|
|
|
|
void text_insert(char* buf, int* len, int* cursor, int max_len, char ch) {
|
|
if (!buf || !len || !cursor || *len >= max_len - 1) return;
|
|
for (int i = *len; i > *cursor; i--) buf[i] = buf[i - 1];
|
|
buf[*cursor] = ch;
|
|
(*len)++;
|
|
(*cursor)++;
|
|
buf[*len] = '\0';
|
|
}
|
|
|
|
void text_backspace(char* buf, int* len, int* cursor) {
|
|
if (!buf || !len || !cursor || *cursor <= 0 || *len <= 0) return;
|
|
for (int i = *cursor - 1; i < *len - 1; i++) buf[i] = buf[i + 1];
|
|
(*len)--;
|
|
(*cursor)--;
|
|
buf[*len] = '\0';
|
|
}
|
|
|
|
void text_delete(char* buf, int* len, int* cursor) {
|
|
if (!buf || !len || !cursor || *cursor >= *len || *len <= 0) return;
|
|
for (int i = *cursor; i < *len - 1; i++) buf[i] = buf[i + 1];
|
|
(*len)--;
|
|
buf[*len] = '\0';
|
|
}
|
|
|
|
int file_entry_at(FileDialogState* st, int mx, int my) {
|
|
FileDialogLayout lo = file_layout(st);
|
|
if (!lo.content_rect.contains(mx, my)) return -1;
|
|
|
|
if (st->grid_view) {
|
|
if (mx >= lo.content_rect.w - SCROLLBAR_W) return -1;
|
|
int cols = (lo.content_rect.w - SCROLLBAR_W) / GRID_CELL_W;
|
|
if (cols < 1) cols = 1;
|
|
int col = mx / GRID_CELL_W;
|
|
int row = (my - lo.content_rect.y + st->scrollbar.scroll_offset) / GRID_CELL_H;
|
|
int idx = row * cols + col;
|
|
if (idx >= 0 && idx < st->entry_count && col < cols) return idx;
|
|
return -1;
|
|
}
|
|
|
|
int rows_y = lo.content_rect.y + HEADER_H;
|
|
if (my < rows_y || mx >= lo.content_rect.w - SCROLLBAR_W) return -1;
|
|
int rel_y = my - rows_y + st->scrollbar.scroll_offset;
|
|
int idx = rel_y / ITEM_H;
|
|
if (idx >= 0 && idx < st->entry_count) return idx;
|
|
return -1;
|
|
}
|
|
|
|
void handle_file_mouse(const Montauk::WinEvent& ev) {
|
|
FileDialogState* st = &g_file;
|
|
st->mouse_x = ev.mouse.x;
|
|
st->mouse_y = ev.mouse.y;
|
|
|
|
FileDialogLayout lo = file_layout(st);
|
|
MouseEvent me = {ev.mouse.x, ev.mouse.y, ev.mouse.buttons, ev.mouse.prev_buttons, ev.mouse.scroll};
|
|
st->scrollbar.handle_mouse(me);
|
|
|
|
if (me.left_pressed()) {
|
|
st->error[0] = '\0';
|
|
|
|
if (lo.back_btn.contains(me.x, me.y)) { file_go_back(st); return; }
|
|
if (lo.forward_btn.contains(me.x, me.y)) { file_go_forward(st); return; }
|
|
if (lo.up_btn.contains(me.x, me.y)) { file_go_up(st); return; }
|
|
if (lo.home_btn.contains(me.x, me.y)) { file_go_home(st); return; }
|
|
if (lo.view_btn.contains(me.x, me.y)) {
|
|
st->grid_view = !st->grid_view;
|
|
st->scrollbar.scroll_offset = 0;
|
|
return;
|
|
}
|
|
if (lo.cancel_btn.contains(me.x, me.y)) { dialog_cancel(); return; }
|
|
if (lo.confirm_btn.contains(me.x, me.y)) {
|
|
if (g_app.request.mode == gui::dialogs::FILE_DIALOG_SAVE) file_accept_save(st);
|
|
else file_accept_open(st);
|
|
return;
|
|
}
|
|
if (lo.path_rect.contains(me.x, me.y)) {
|
|
st->focus = FOCUS_PATH;
|
|
return;
|
|
}
|
|
if (g_app.request.mode == gui::dialogs::FILE_DIALOG_SAVE && lo.name_rect.contains(me.x, me.y)) {
|
|
st->focus = FOCUS_NAME;
|
|
return;
|
|
}
|
|
|
|
int idx = file_entry_at(st, me.x, me.y);
|
|
if (idx >= 0) {
|
|
uint64_t now = montauk::get_milliseconds();
|
|
st->focus = FOCUS_LIST;
|
|
if (st->last_click_item == idx && (now - st->last_click_time) < 400) {
|
|
st->selected = idx;
|
|
file_activate_selected(st, true);
|
|
st->last_click_item = -1;
|
|
st->last_click_time = 0;
|
|
} else {
|
|
st->selected = idx;
|
|
st->last_click_item = idx;
|
|
st->last_click_time = now;
|
|
if (g_app.request.mode == gui::dialogs::FILE_DIALOG_SAVE && !st->is_dir[idx]) {
|
|
file_set_filename(st, st->entry_names[idx]);
|
|
}
|
|
}
|
|
file_ensure_selected_visible(st);
|
|
return;
|
|
}
|
|
|
|
st->selected = -1;
|
|
st->focus = FOCUS_LIST;
|
|
}
|
|
|
|
if (me.scroll != 0 && lo.content_rect.contains(me.x, me.y)) {
|
|
int step = st->grid_view ? GRID_CELL_H : ITEM_H;
|
|
st->scrollbar.scroll_offset -= me.scroll * step;
|
|
int ms = st->scrollbar.max_scroll();
|
|
if (st->scrollbar.scroll_offset < 0) st->scrollbar.scroll_offset = 0;
|
|
if (st->scrollbar.scroll_offset > ms) st->scrollbar.scroll_offset = ms;
|
|
}
|
|
}
|
|
|
|
void handle_file_key(const Montauk::KeyEvent& key) {
|
|
if (!key.pressed) return;
|
|
|
|
FileDialogState* st = &g_file;
|
|
st->error[0] = '\0';
|
|
|
|
if (key.scancode == 0x01) {
|
|
dialog_cancel();
|
|
return;
|
|
}
|
|
|
|
if (st->focus == FOCUS_PATH) {
|
|
if (key.ascii == '\n' || key.ascii == '\r') {
|
|
file_open_path(st, st->path_buf);
|
|
st->focus = FOCUS_LIST;
|
|
return;
|
|
}
|
|
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
|
text_backspace(st->path_buf, &st->path_len, &st->path_cursor);
|
|
return;
|
|
}
|
|
if (key.scancode == 0x53) { text_delete(st->path_buf, &st->path_len, &st->path_cursor); return; }
|
|
if (key.scancode == 0x4B) { if (st->path_cursor > 0) st->path_cursor--; return; }
|
|
if (key.scancode == 0x4D) { if (st->path_cursor < st->path_len) st->path_cursor++; return; }
|
|
if (key.scancode == 0x47) { st->path_cursor = 0; return; }
|
|
if (key.scancode == 0x4F) { st->path_cursor = st->path_len; return; }
|
|
if (key.ascii >= 32 && key.ascii < 127) {
|
|
text_insert(st->path_buf, &st->path_len, &st->path_cursor, (int)sizeof(st->path_buf), key.ascii);
|
|
return;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (st->focus == FOCUS_NAME && g_app.request.mode == gui::dialogs::FILE_DIALOG_SAVE) {
|
|
if (key.ascii == '\n' || key.ascii == '\r') { file_accept_save(st); return; }
|
|
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
|
text_backspace(st->filename_buf, &st->filename_len, &st->filename_cursor);
|
|
return;
|
|
}
|
|
if (key.scancode == 0x53) { text_delete(st->filename_buf, &st->filename_len, &st->filename_cursor); return; }
|
|
if (key.scancode == 0x4B) { if (st->filename_cursor > 0) st->filename_cursor--; return; }
|
|
if (key.scancode == 0x4D) { if (st->filename_cursor < st->filename_len) st->filename_cursor++; return; }
|
|
if (key.scancode == 0x47) { st->filename_cursor = 0; return; }
|
|
if (key.scancode == 0x4F) { st->filename_cursor = st->filename_len; return; }
|
|
if (key.ascii >= 32 && key.ascii < 127 && key.ascii != '/' && key.ascii != '\\') {
|
|
text_insert(st->filename_buf, &st->filename_len, &st->filename_cursor, (int)sizeof(st->filename_buf), key.ascii);
|
|
return;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (key.ascii == '\n' || key.ascii == '\r') {
|
|
if (g_app.request.mode == gui::dialogs::FILE_DIALOG_SAVE) file_accept_save(st);
|
|
else file_accept_open(st);
|
|
return;
|
|
}
|
|
|
|
if (key.scancode == 0x4B || key.scancode == 0x48) {
|
|
if (st->selected > 0) st->selected--;
|
|
else if (st->entry_count > 0) st->selected = 0;
|
|
file_ensure_selected_visible(st);
|
|
return;
|
|
}
|
|
|
|
if (key.scancode == 0x4D || key.scancode == 0x50) {
|
|
if (st->selected < st->entry_count - 1) st->selected++;
|
|
else if (st->entry_count > 0) st->selected = st->entry_count - 1;
|
|
file_ensure_selected_visible(st);
|
|
return;
|
|
}
|
|
|
|
if (key.ctrl && (key.ascii == 'l' || key.ascii == 'L')) {
|
|
st->focus = FOCUS_PATH;
|
|
return;
|
|
}
|
|
|
|
if (key.ctrl && g_app.request.mode == gui::dialogs::FILE_DIALOG_SAVE && (key.ascii == 'n' || key.ascii == 'N')) {
|
|
st->focus = FOCUS_NAME;
|
|
return;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
namespace filedialog {
|
|
|
|
void init(const gui::dialogs::Request& req) {
|
|
init_file_dialog(req);
|
|
}
|
|
|
|
void draw() {
|
|
draw_file_dialog();
|
|
}
|
|
|
|
void handle_mouse(const Montauk::WinEvent& ev) {
|
|
handle_file_mouse(ev);
|
|
}
|
|
|
|
void handle_key(const Montauk::KeyEvent& key) {
|
|
handle_file_key(key);
|
|
}
|
|
|
|
void cleanup() {
|
|
free_file_icons(&g_file);
|
|
}
|
|
|
|
} // namespace filedialog
|