diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index 86e9e2a..6ed5512 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 107 +#define MONTAUK_BUILD_NUMBER 109 diff --git a/programs/src/files/app.cpp b/programs/src/files/app.cpp index 964e5af..8efc510 100644 --- a/programs/src/files/app.cpp +++ b/programs/src/files/app.cpp @@ -43,6 +43,11 @@ static void open_filemanager_internal(DesktopState* ds, const char* initial_path prefs_load(ds); fm->show_hidden = prefs_show_hidden(); fm->sidebar_width = prefs_sidebar_width(); + fm->col_resizing = -1; + for (int col = 0; col < FM_COL_COUNT; col++) { + int w = prefs_column_width(col); + fm->list_col_w[col] = w > 0 ? w : FM_COL_DEFAULT_W[col]; + } fm->scrollbar.init(0, 0, FM_SCROLLBAR_W, 100); diff --git a/programs/src/files/events.cpp b/programs/src/files/events.cpp index 253628e..9135a68 100644 --- a/programs/src/files/events.cpp +++ b/programs/src/files/events.cpp @@ -55,6 +55,53 @@ static bool filemanager_click_entry(FileManagerState* fm, int idx) { return false; } +// List view column divider under (x, y) in the header row, -1 if none. +static int filemanager_column_divider_at(FileManagerState* fm, const FileManagerLayout& l, + int x, int y) { + if (fm->grid_view || y < l.body_y || y >= l.list_y) return -1; + FileManagerListColumns cols = filemanager_list_columns(fm, l.view_x, l.view_w); + for (int col = 0; col < FM_COL_COUNT; col++) { + int cx = cols.x(col); + if (cx >= 0 && x >= cx - 4 - FM_COL_GRIP && x <= cx - 4 + FM_COL_GRIP) return col; + } + return -1; +} + +// Dragging a column divider in the list view header. The divider is the +// column's left edge: the column grows leftwards and Name gives up the room. +// Returns true if the event was consumed; sets *grip over a divider. +static bool filemanager_column_resize_mouse(FileManagerState* fm, const FileManagerLayout& l, + int x, int y, MouseEvent& ev, bool* grip) { + if (fm->col_resizing >= 0) { + int col = fm->col_resizing; + if (ev.left_held()) { + int w = fm->col_drag_right - (x - fm->col_drag_dx); + fm->list_col_w[col] = gui_clamp(w, FM_COL_MIN_W, fm->col_drag_max); + } else { + fm->col_resizing = -1; + prefs_set_column_widths(fm->desktop, fm->list_col_w); + } + return true; + } + + int col = filemanager_column_divider_at(fm, l, x, y); + if (col < 0) return false; + *grip = true; + if (!ev.left_pressed()) return false; + + FileManagerListColumns cols = filemanager_list_columns(fm, l.view_x, l.view_w); + int divider = cols.x(col) - 4; + fm->col_resizing = col; + fm->col_drag_dx = x - divider; + fm->col_drag_right = divider + cols.w[col]; + // Name may shrink to its minimum, never further: past that a column + // would be dropped mid-drag. + int slack = cols.name_right + 8 - (l.view_x + FM_LIST_MIN_NAME_W); + fm->col_drag_max = gui_max(gui_min(cols.w[col] + slack, FM_COL_MAX_W), cols.w[col]); + if (fm->pathbar_editing) filemanager_cancel_pathbar(fm); + return true; +} + void filemanager_on_mouse(Window* win, MouseEvent& ev) { FileManagerState* fm = (FileManagerState*)win->app_data; if (!fm) return; @@ -203,10 +250,14 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) { FileManagerLayout l = filemanager_layout(fm, fm->grid_view, cw); - // ---- Sidebar edge: drag to resize ---- - if (!fm->marquee_active && fm->press_idx < 0 && !fm->scrollbar.dragging && - filemanager_sidebar_resize_mouse(win, fm, l, local_x, local_y, ev)) - return; + // ---- Sidebar edge and list column dividers: drag to resize ---- + if (!fm->marquee_active && fm->press_idx < 0 && !fm->scrollbar.dragging) { + bool grip = false; + if (filemanager_sidebar_resize_mouse(win, fm, l, local_x, local_y, ev, &grip) || + filemanager_column_resize_mouse(fm, l, local_x, local_y, ev, &grip)) + return; + filemanager_set_cursor(win, fm, grip ? CURSOR_RESIZE_H : CURSOR_ARROW); + } // ---- Cancel rename on click outside rename area ---- if (fm->rename_active && ev.left_pressed()) { @@ -277,7 +328,7 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) { // Column header click (list view): sort by that column, again = reverse. if (!fm->grid_view && local_y >= l.body_y && local_y < l.list_y && local_x >= l.view_x && filemanager_is_listing(fm)) { - FileManagerListColumns cols = filemanager_list_columns(l.view_x, l.view_w); + FileManagerListColumns cols = filemanager_list_columns(fm, l.view_x, l.view_w); if (cols.type_x >= 0 && local_x >= cols.type_x) filemanager_set_sort(fm, FM_SORT_TYPE); else if (cols.size_x >= 0 && local_x >= cols.size_x) diff --git a/programs/src/files/filemanager_internal.hpp b/programs/src/files/filemanager_internal.hpp index c70a917..c757790 100644 --- a/programs/src/files/filemanager_internal.hpp +++ b/programs/src/files/filemanager_internal.hpp @@ -205,6 +205,14 @@ struct FileManagerState { int sidebar_width; // wanted width (prefs); the layout clamps it to the window bool sidebar_resizing; // dragging the sidebar's right edge int sidebar_drag_dx; // pointer offset from the edge when the drag began + // List view column widths (FileManagerListColumn order; prefs) and the + // divider being dragged, -1 = none. A drag keeps the column's right edge + // fixed, so the name column absorbs the change. + int list_col_w[3]; + int col_resizing; + int col_drag_dx; // pointer offset from the divider when the drag began + int col_drag_right; // the column's right edge during the drag + int col_drag_max; // widest it may get without dropping a column int cursor_style; // CursorStyle last sent to the Window Server // Undo / redo of the last file operations (undo.cpp). @@ -284,34 +292,56 @@ inline FileManagerLayout filemanager_layout(const FileManagerState* fm, bool gri return l; } +// List view columns right of Name, in screen order. Name takes the rest. +enum FileManagerListColumn : int { + FM_COL_MODIFIED = 0, + FM_COL_SIZE, + FM_COL_TYPE, + FM_COL_COUNT, +}; + +inline constexpr int FM_COL_DEFAULT_W[FM_COL_COUNT] = {132, 76, 96}; +inline constexpr int FM_COL_MIN_W = 48; +inline constexpr int FM_COL_MAX_W = 400; +inline constexpr int FM_LIST_MIN_NAME_W = 160; +inline constexpr int FM_COL_GRIP = 3; // drag zone either side of a divider + // List view column layout. Columns are dropped right to left as the window // narrows (Modified first, then Type, then Size) so the name keeps room. +// A column's text starts at its x; its divider line sits at x - 4. struct FileManagerListColumns { int name_x; int name_right; // right edge available to the name text int modified_x; // -1 = hidden int size_x; // -1 = hidden int type_x; // -1 = hidden + int w[FM_COL_COUNT]; + + int x(int col) const { + return col == FM_COL_MODIFIED ? modified_x : col == FM_COL_SIZE ? size_x : type_x; + } }; // Columns of a list view occupying [x0, x0 + view_w). -inline FileManagerListColumns filemanager_list_columns(int x0, int view_w) { - static constexpr int MIN_NAME_W = 160; - static constexpr int TYPE_W = 96; - static constexpr int SIZE_W = 76; - static constexpr int MODIFIED_W = 132; +inline FileManagerListColumns filemanager_list_columns(const FileManagerState* fm, + int x0, int view_w) { + FileManagerListColumns cols = {x0 + 8, 0, -1, -1, -1, {}}; + for (int i = 0; i < FM_COL_COUNT; i++) cols.w[i] = fm->list_col_w[i]; + const int* w = cols.w; - FileManagerListColumns cols = {x0 + 8, 0, -1, -1, -1}; - int min_right = x0 + MIN_NAME_W; + int min_right = x0 + FM_LIST_MIN_NAME_W; int right = x0 + view_w; - if (right - SIZE_W >= min_right) { right -= SIZE_W; cols.size_x = right; } - if (right - TYPE_W >= min_right) { + if (right - w[FM_COL_SIZE] >= min_right) { right -= w[FM_COL_SIZE]; cols.size_x = right; } + if (right - w[FM_COL_TYPE] >= min_right) { // Type sits right of Size; shift Size left to make room. - cols.type_x = x0 + view_w - TYPE_W; - if (cols.size_x >= 0) cols.size_x -= TYPE_W; - right -= TYPE_W; + cols.type_x = x0 + view_w - w[FM_COL_TYPE]; + if (cols.size_x >= 0) cols.size_x -= w[FM_COL_TYPE]; + right -= w[FM_COL_TYPE]; + } + if (right - w[FM_COL_MODIFIED] >= min_right) { + right -= w[FM_COL_MODIFIED]; + cols.modified_x = right; } - if (right - MODIFIED_W >= min_right) { right -= MODIFIED_W; cols.modified_x = right; } cols.name_right = right - 8; return cols; } @@ -547,6 +577,8 @@ bool prefs_show_hidden(); void prefs_set_show_hidden(DesktopState* ds, bool show); int prefs_sidebar_width(); // 0 = default void prefs_set_sidebar_width(DesktopState* ds, int width); +int prefs_column_width(int col); // FileManagerListColumn, 0 = default +void prefs_set_column_widths(DesktopState* ds, const int* widths); int prefs_bookmark_count(); const char* prefs_bookmark(int i); bool prefs_has_bookmark(const char* path); @@ -564,9 +596,12 @@ void filemanager_sidebar_draw(Canvas& c, FileManagerState* fm, const FileManager bool filemanager_sidebar_mouse(FileManagerState* fm, const FileManagerLayout& l, int x, int y, MouseEvent& ev, int ch); // Dragging the sidebar's right edge to resize it. Returns true if the event -// was consumed (a drag is in progress or just began). -bool filemanager_sidebar_resize_mouse(Window* win, FileManagerState* fm, - const FileManagerLayout& l, int x, int y, MouseEvent& ev); +// was consumed (a drag is in progress or just began); sets *grip when the +// pointer is over the edge. +bool filemanager_sidebar_resize_mouse(Window* win, FileManagerState* fm, const FileManagerLayout& l, + int x, int y, MouseEvent& ev, bool* grip); +// The Window Server cursor for this window (CursorStyle), sent on change only. +void filemanager_set_cursor(Window* win, FileManagerState* fm, int style); int filemanager_sidebar_item_at(FileManagerState* fm, const FileManagerLayout& l, int x, int y); void filemanager_sidebar_open(FileManagerState* fm, int idx); // Folder a sidebar row stands for, for drops ("" for Computer/headers). diff --git a/programs/src/files/prefs.cpp b/programs/src/files/prefs.cpp index c7557af..a650061 100644 --- a/programs/src/files/prefs.cpp +++ b/programs/src/files/prefs.cpp @@ -13,6 +13,8 @@ namespace filemanager { // Layout of files.toml: // [view] // show_hidden = false +// [list] +// modified_width = 132 (also size_width, type_width; absent = default) // [sidebar] // width = 160 // bookmarks = ["0:/users/me/Projects", ...] @@ -24,6 +26,10 @@ namespace filemanager { static constexpr const char* PREFS_NAME = "files"; static constexpr int MAX_BOOKMARKS = 32; static constexpr int MAX_FOLDER_VIEWS = 128; +// Keys of the list view column widths, FileManagerListColumn order. +static constexpr const char* COLUMN_WIDTH_KEYS[FM_COL_COUNT] = { + "list.modified_width", "list.size_width", "list.type_width", +}; struct FolderView { char path[256]; @@ -35,6 +41,7 @@ struct FolderView { struct Prefs { bool show_hidden; int sidebar_width; // 0 = default + int column_widths[FM_COL_COUNT]; // 0 = default char bookmarks[MAX_BOOKMARKS][256]; int bookmark_count; FolderView views[MAX_FOLDER_VIEWS]; @@ -99,6 +106,10 @@ static void read_from_disk(DesktopState* ds) { g_prefs.show_hidden = doc.get_bool("view.show_hidden", false); int64_t width = doc.get_int("sidebar.width", 0); g_prefs.sidebar_width = width > 0 && width <= FM_SIDEBAR_MAX_W ? (int)width : 0; + for (int col = 0; col < FM_COL_COUNT; col++) { + int64_t cw = doc.get_int(COLUMN_WIDTH_KEYS[col], 0); + g_prefs.column_widths[col] = cw >= FM_COL_MIN_W && cw <= FM_COL_MAX_W ? (int)cw : 0; + } if (auto* arr = doc.get_array("sidebar.bookmarks")) { for (int i = 0; i < arr->array.count && g_prefs.bookmark_count < MAX_BOOKMARKS; i++) { @@ -133,6 +144,10 @@ static void write_to_disk(DesktopState* ds) { montauk::config::set_bool(&doc, "view.show_hidden", g_prefs.show_hidden); if (g_prefs.sidebar_width > 0) montauk::config::set_int(&doc, "sidebar.width", g_prefs.sidebar_width); + for (int col = 0; col < FM_COL_COUNT; col++) { + if (g_prefs.column_widths[col] > 0) + montauk::config::set_int(&doc, COLUMN_WIDTH_KEYS[col], g_prefs.column_widths[col]); + } auto* bookmarks = string_array("sidebar.bookmarks"); for (int i = 0; i < g_prefs.bookmark_count; i++) push_string(bookmarks, g_prefs.bookmarks[i]); @@ -177,6 +192,21 @@ void prefs_set_sidebar_width(DesktopState* ds, int width) { write_to_disk(ds); } +int prefs_column_width(int col) { + return col >= 0 && col < FM_COL_COUNT ? g_prefs.column_widths[col] : 0; +} + +void prefs_set_column_widths(DesktopState* ds, const int* widths) { + read_from_disk(ds); + bool changed = false; + for (int col = 0; col < FM_COL_COUNT; col++) { + int w = widths[col] == FM_COL_DEFAULT_W[col] ? 0 : widths[col]; + if (g_prefs.column_widths[col] != w) changed = true; + g_prefs.column_widths[col] = w; + } + if (changed) write_to_disk(ds); +} + int prefs_bookmark_count() { return g_prefs.bookmark_count; } diff --git a/programs/src/files/render.cpp b/programs/src/files/render.cpp index d0155bd..010d0d7 100644 --- a/programs/src/files/render.cpp +++ b/programs/src/files/render.cpp @@ -135,13 +135,16 @@ static void draw_marquee(Canvas& c, FileManagerState* fm, int list_y) { } } -// Column title, plus a small arrow when it is the active sort column. -static void draw_column_header(Canvas& c, int x, int header_y, const char* label, +// Column title, plus a small arrow when it is the active sort column and +// there is room for it. max_w is the width the title may take. +static void draw_column_header(Canvas& c, int x, int header_y, const char* label, int max_w, bool active, bool desc, Color color) { - c.text(x, header_y + 2, label, active ? colors::TEXT_COLOR : color); - if (!active) return; + char fit[32]; + fit_label_with_ellipsis(fit, sizeof(fit), label, max_w); + c.text(x, header_y + 2, fit, active ? colors::TEXT_COLOR : color); + int ax = x + text_width(fit) + 6; + if (!active || ax + 7 > x + max_w) return; - int ax = x + text_width(label) + 6; int ay = header_y + FM_HEADER_H / 2 - 2; for (int r = 0; r < 4; r++) { // Ascending points up, descending points down. @@ -337,7 +340,7 @@ bool filemanager_rename_box(FileManagerState* fm, const FileManagerLayout& l, in r = {x, cell_y + FM_GRID_PAD + FM_GRID_ICON, w, fh + 6}; } else { // Over the name column; text lines up with the names (view_x + 30). - FileManagerListColumns cols = filemanager_list_columns(l.view_x, l.view_w); + FileManagerListColumns cols = filemanager_list_columns(fm, l.view_x, l.view_w); int iy = l.list_y + i * FM_ITEM_H - fm->scrollbar.scroll_offset; int x = l.view_x + 30 - fm->rename.pad; r = {x, iy + 2, gui_max(cols.name_right - x, 60), FM_ITEM_H - 4}; @@ -480,31 +483,42 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) { int header_y = l.body_y; c.fill_rect(l.view_x, header_y, c.w - l.view_x, FM_HEADER_H, Color::from_rgb(0xF8, 0xF8, 0xF8)); - FileManagerListColumns cols = filemanager_list_columns(l.view_x, l.view_w); + FileManagerListColumns cols = filemanager_list_columns(fm, l.view_x, l.view_w); + // Text room of each column: up to the next divider, less a gap. + // Type runs to the view's right edge. + int col_text_w[FM_COL_COUNT]; + for (int col = 0; col < FM_COL_COUNT; col++) col_text_w[col] = cols.w[col] - 8; + col_text_w[FM_COL_TYPE] = l.view_x + l.view_w - cols.type_x - 4; // Sorting only applies to real directories; Computer and the virtual // views keep their curated order and show no sort arrow. bool sortable = !filemanager_is_computer_view(fm) && !filemanager_is_virtual_view(fm); - draw_column_header(c, cols.name_x, header_y, "Name", + draw_column_header(c, cols.name_x, header_y, "Name", cols.name_right - cols.name_x, sortable && fm->sort_key == FM_SORT_NAME, fm->sort_desc, dim); if (cols.modified_x >= 0) - draw_column_header(c, cols.modified_x, header_y, "Modified", + draw_column_header(c, cols.modified_x, header_y, "Modified", col_text_w[FM_COL_MODIFIED], sortable && fm->sort_key == FM_SORT_MODIFIED, fm->sort_desc, dim); if (cols.size_x >= 0) - draw_column_header(c, cols.size_x, header_y, "Size", + draw_column_header(c, cols.size_x, header_y, "Size", col_text_w[FM_COL_SIZE], sortable && fm->sort_key == FM_SORT_SIZE, fm->sort_desc, dim); if (cols.type_x >= 0) - draw_column_header(c, cols.type_x, header_y, "Type", + draw_column_header(c, cols.type_x, header_y, "Type", col_text_w[FM_COL_TYPE], sortable && fm->sort_key == FM_SORT_TYPE, fm->sort_desc, dim); // Header separator c.hline(l.view_x, header_y + FM_HEADER_H - 1, c.w - l.view_x, colors::BORDER); - // Column separator lines + // Column separator lines: the one after Name runs down the list, + // the rest mark the draggable dividers in the header. int first_col_x = cols.modified_x >= 0 ? cols.modified_x : cols.size_x >= 0 ? cols.size_x : cols.type_x; if (first_col_x >= 0) c.vline(first_col_x - 4, header_y, c.h - header_y, colors::BORDER); + for (int col = 0; col < FM_COL_COUNT; col++) { + int cx = cols.x(col); + if (cx >= 0 && cx != first_col_x) + c.vline(cx - 4, header_y, FM_HEADER_H, colors::BORDER); + } int tz = montauk::gettz(); // ---- File entries ---- @@ -605,14 +619,20 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) { if (cols.modified_x >= 0 && row_text_visible && fm->entry_mtimes[i] > 0) { char date_str[24]; format_datetime(date_str, sizeof(date_str), fm->entry_mtimes[i], tz); - c.text(cols.modified_x, ty, date_str, dim); + char date_fit[24]; + fit_label_with_ellipsis(date_fit, sizeof(date_fit), date_str, + col_text_w[FM_COL_MODIFIED]); + c.text(cols.modified_x, ty, date_fit, dim); } // Size if (cols.size_x >= 0 && !fm->is_dir[i] && row_text_visible) { char size_str[16]; format_size(size_str, fm->entry_sizes[i]); - c.text(cols.size_x, ty, size_str, dim); + char size_fit[16]; + fit_label_with_ellipsis(size_fit, sizeof(size_fit), size_str, + col_text_w[FM_COL_SIZE]); + c.text(cols.size_x, ty, size_fit, dim); } // Type @@ -621,7 +641,7 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) { filemanager_type_label(fm, i, type_str, sizeof(type_str)); char type_fit[24]; fit_label_with_ellipsis(type_fit, sizeof(type_fit), type_str, - l.view_x + l.view_w - cols.type_x - 4); + col_text_w[FM_COL_TYPE]); c.text(cols.type_x, ty, type_fit, dim); } } diff --git a/programs/src/files/sidebar.cpp b/programs/src/files/sidebar.cpp index d470c87..e1c27cc 100644 --- a/programs/src/files/sidebar.cpp +++ b/programs/src/files/sidebar.cpp @@ -226,14 +226,14 @@ bool filemanager_sidebar_target_path(FileManagerState* fm, int idx, char* out, i return true; } -static void set_window_cursor(Window* win, FileManagerState* fm, int style) { +void filemanager_set_cursor(Window* win, FileManagerState* fm, int style) { if (fm->cursor_style == style) return; fm->cursor_style = style; montauk::win_setcursor(win->ext_win_id, style); } -bool filemanager_sidebar_resize_mouse(Window* win, FileManagerState* fm, - const FileManagerLayout& l, int x, int y, MouseEvent& ev) { +bool filemanager_sidebar_resize_mouse(Window* win, FileManagerState* fm, const FileManagerLayout& l, + int x, int y, MouseEvent& ev, bool* grip) { if (fm->sidebar_resizing) { if (ev.left_held()) { fm->sidebar_width = filemanager_sidebar_width(x - fm->sidebar_drag_dx, @@ -248,7 +248,7 @@ bool filemanager_sidebar_resize_mouse(Window* win, FileManagerState* fm, // The grip straddles the border line at sidebar_w - 1. bool on_edge = l.sidebar_w > 0 && y >= l.body_y && x >= l.sidebar_w - FM_SIDEBAR_GRIP && x < l.sidebar_w + FM_SIDEBAR_GRIP; - set_window_cursor(win, fm, on_edge ? CURSOR_RESIZE_H : CURSOR_ARROW); + if (on_edge) *grip = true; if (!on_edge || !ev.left_pressed()) return false; fm->sidebar_resizing = true;