feat: improve MTK input boxes, right-click context menus
This commit is contained in:
@@ -183,6 +183,7 @@ void filemanager_open_ctx_menu(FileManagerState* fm, int local_x, int local_y, i
|
||||
|
||||
void filemanager_close_ctx_menu(FileManagerState* fm) {
|
||||
fm->ctx_open = false;
|
||||
fm->ctx_hover = -1;
|
||||
}
|
||||
|
||||
} // namespace filemanager
|
||||
|
||||
@@ -25,50 +25,35 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
||||
|
||||
// ---- Context menu interaction ----
|
||||
if (fm->ctx_open) {
|
||||
int cmx = fm->ctx_x;
|
||||
int cmy = fm->ctx_y;
|
||||
int cmh = fm->ctx_item_count * CTX_ITEM_H + 8;
|
||||
if (cmx + CTX_MENU_W > cw) cmx = cw - CTX_MENU_W;
|
||||
if (cmy + cmh > win->content_h) cmy = win->content_h - cmh;
|
||||
if (cmx < 0) cmx = 0;
|
||||
if (cmy < 0) cmy = 0;
|
||||
mtk::ContextMenuState state = filemanager_context_state(fm);
|
||||
mtk::ContextMenuItem items[8];
|
||||
filemanager_build_context_items(fm, items, 8);
|
||||
int action = mtk::context_menu_handle_mouse(state, items,
|
||||
fm->ctx_item_count,
|
||||
local_x, local_y,
|
||||
ev.buttons, ev.prev_buttons,
|
||||
cw, win->content_h);
|
||||
filemanager_store_context_state(fm, state);
|
||||
|
||||
// Update hover
|
||||
if (local_x >= cmx && local_x < cmx + CTX_MENU_W &&
|
||||
local_y >= cmy + 4 && local_y < cmy + 4 + fm->ctx_item_count * CTX_ITEM_H) {
|
||||
fm->ctx_hover = (local_y - cmy - 4) / CTX_ITEM_H;
|
||||
} else {
|
||||
fm->ctx_hover = -1;
|
||||
}
|
||||
if (action >= 0) {
|
||||
int target = fm->ctx_target_idx;
|
||||
|
||||
if (ev.left_pressed()) {
|
||||
if (fm->ctx_hover >= 0 && fm->ctx_hover < fm->ctx_item_count) {
|
||||
int action = fm->ctx_items[fm->ctx_hover];
|
||||
int target = fm->ctx_target_idx;
|
||||
filemanager_close_ctx_menu(fm);
|
||||
// Select target if needed
|
||||
if (target >= 0 && target < fm->entry_count)
|
||||
fm->selected = target;
|
||||
|
||||
// Select target if needed
|
||||
if (target >= 0 && target < fm->entry_count)
|
||||
fm->selected = target;
|
||||
|
||||
switch (action) {
|
||||
case CTX_OPEN: filemanager_open_entry(fm, target); break;
|
||||
case CTX_COPY: filemanager_do_copy(fm); break;
|
||||
case CTX_CUT: filemanager_do_cut(fm); break;
|
||||
case CTX_PASTE: filemanager_do_paste(fm); break;
|
||||
case CTX_RENAME: filemanager_start_rename(fm); break;
|
||||
case CTX_DELETE: filemanager_delete_selected(fm); break;
|
||||
case CTX_NEW_FOLDER: filemanager_new_folder(fm); break;
|
||||
}
|
||||
} else {
|
||||
filemanager_close_ctx_menu(fm);
|
||||
switch (action) {
|
||||
case CTX_OPEN: filemanager_open_entry(fm, target); break;
|
||||
case CTX_COPY: filemanager_do_copy(fm); break;
|
||||
case CTX_CUT: filemanager_do_cut(fm); break;
|
||||
case CTX_PASTE: filemanager_do_paste(fm); break;
|
||||
case CTX_RENAME: filemanager_start_rename(fm); break;
|
||||
case CTX_DELETE: filemanager_delete_selected(fm); break;
|
||||
case CTX_NEW_FOLDER: filemanager_new_folder(fm); break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (ev.right_pressed()) {
|
||||
filemanager_close_ctx_menu(fm);
|
||||
return;
|
||||
}
|
||||
if (action == mtk::CONTEXT_MENU_DISMISSED) return;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "../apps_common.hpp"
|
||||
#include <gui/mtk/widgets.hpp>
|
||||
|
||||
namespace filemanager {
|
||||
|
||||
@@ -131,9 +132,6 @@ inline constexpr int CTX_RENAME = 4;
|
||||
inline constexpr int CTX_DELETE = 5;
|
||||
inline constexpr int CTX_NEW_FOLDER = 6;
|
||||
|
||||
inline constexpr int CTX_MENU_W = 140;
|
||||
inline constexpr int CTX_ITEM_H = 24;
|
||||
|
||||
inline bool filemanager_is_virtual_view(const FileManagerState* fm) {
|
||||
return fm && fm->virtual_view != FM_VIRTUAL_VIEW_NONE;
|
||||
}
|
||||
@@ -153,6 +151,29 @@ inline const char* filemanager_virtual_view_label(FileManagerVirtualViewKind vie
|
||||
int special_folder_index(const char* name);
|
||||
const char* ctx_label(int action);
|
||||
|
||||
inline mtk::ContextMenuState filemanager_context_state(const FileManagerState* fm) {
|
||||
if (!fm) return {};
|
||||
return {fm->ctx_open, fm->ctx_x, fm->ctx_y, fm->ctx_hover};
|
||||
}
|
||||
|
||||
inline void filemanager_store_context_state(FileManagerState* fm,
|
||||
const mtk::ContextMenuState& state) {
|
||||
if (!fm) return;
|
||||
fm->ctx_open = state.open;
|
||||
fm->ctx_x = state.x;
|
||||
fm->ctx_y = state.y;
|
||||
fm->ctx_hover = state.hover;
|
||||
}
|
||||
|
||||
inline void filemanager_build_context_items(const FileManagerState* fm,
|
||||
mtk::ContextMenuItem* items,
|
||||
int max_items) {
|
||||
if (!fm || !items || max_items <= 0) return;
|
||||
int count = fm->ctx_item_count < max_items ? fm->ctx_item_count : max_items;
|
||||
for (int i = 0; i < count; i++)
|
||||
items[i] = {ctx_label(fm->ctx_items[i]), fm->ctx_items[i], true};
|
||||
}
|
||||
|
||||
bool str_ends_with(const char* s, const char* suffix);
|
||||
bool is_image_file(const char* name);
|
||||
bool is_font_file(const char* name);
|
||||
|
||||
@@ -576,33 +576,14 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
||||
|
||||
// ---- Context menu overlay ----
|
||||
if (fm->ctx_open && fm->ctx_item_count > 0) {
|
||||
int cmx = fm->ctx_x;
|
||||
int cmy = fm->ctx_y;
|
||||
int cmh = fm->ctx_item_count * CTX_ITEM_H + 8;
|
||||
mtk::Theme theme = mtk::make_theme(colors::ACCENT);
|
||||
theme.border = colors::BORDER;
|
||||
theme.text = colors::TEXT_COLOR;
|
||||
|
||||
// Clamp to window bounds
|
||||
if (cmx + CTX_MENU_W > c.w) cmx = c.w - CTX_MENU_W;
|
||||
if (cmy + cmh > c.h) cmy = c.h - cmh;
|
||||
if (cmx < 0) cmx = 0;
|
||||
if (cmy < 0) cmy = 0;
|
||||
|
||||
// Shadow
|
||||
c.fill_rect(cmx + 2, cmy + 2, CTX_MENU_W, cmh, Color::from_rgb(0x80, 0x80, 0x80));
|
||||
// Background
|
||||
c.fill_rounded_rect(cmx, cmy, CTX_MENU_W, cmh, 4, colors::WHITE);
|
||||
c.rect(cmx, cmy, CTX_MENU_W, cmh, colors::BORDER);
|
||||
|
||||
for (int i = 0; i < fm->ctx_item_count; i++) {
|
||||
int iy = cmy + 4 + i * CTX_ITEM_H;
|
||||
|
||||
// Hover highlight
|
||||
if (i == fm->ctx_hover)
|
||||
c.fill_rect(cmx + 2, iy, CTX_MENU_W - 4, CTX_ITEM_H, colors::MENU_HOVER);
|
||||
|
||||
int fh = system_font_height();
|
||||
int ty = iy + (CTX_ITEM_H - fh) / 2;
|
||||
c.text(cmx + 12, ty, ctx_label(fm->ctx_items[i]), colors::TEXT_COLOR);
|
||||
}
|
||||
mtk::ContextMenuItem items[8];
|
||||
filemanager_build_context_items(fm, items, 8);
|
||||
mtk::draw_context_menu(c, filemanager_context_state(fm), items,
|
||||
fm->ctx_item_count, theme);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,21 +15,6 @@ static mtk::WidgetState dialog_button_state(int mouse_x, int mouse_y,
|
||||
return mtk::widget_state(active, rect.contains(mouse_x, mouse_y), enabled);
|
||||
}
|
||||
|
||||
static void dialog_append(char* buf, int* len, int max, char ch) {
|
||||
if (*len < max - 1) {
|
||||
buf[*len] = ch;
|
||||
(*len)++;
|
||||
buf[*len] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
static void dialog_backspace(char* buf, int* len) {
|
||||
if (*len > 0) {
|
||||
(*len)--;
|
||||
buf[*len] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
static void dialog_close_self(DesktopState* ds, void* app_data) {
|
||||
for (int i = 0; i < ds->window_count; i++) {
|
||||
if (ds->windows[i].app_data == app_data) {
|
||||
@@ -55,8 +40,11 @@ struct AddUserDialogState {
|
||||
int mouse_y;
|
||||
int field;
|
||||
char username[32]; int username_len;
|
||||
mtk::TextInputState username_input;
|
||||
char display[64]; int display_len;
|
||||
mtk::TextInputState display_input;
|
||||
char password[64]; int password_len;
|
||||
mtk::TextInputState password_input;
|
||||
bool role_admin;
|
||||
char error[64];
|
||||
};
|
||||
@@ -76,15 +64,18 @@ static void adduser_on_draw(Window* win, Framebuffer& fb) {
|
||||
int sfh = system_font_height();
|
||||
|
||||
mtk::draw_labeled_text_field(c, pad, y, fw, "Username", st->username,
|
||||
st->username_len, st->field == 0, false, theme, USER_FIELD_H);
|
||||
st->username_input.cursor, st->field == 0, false,
|
||||
theme, USER_FIELD_H, st->username_input.selection_anchor);
|
||||
y += mtk::labeled_text_height(theme, USER_FIELD_H) + 10;
|
||||
|
||||
mtk::draw_labeled_text_field(c, pad, y, fw, "Display Name", st->display,
|
||||
st->display_len, st->field == 1, false, theme, USER_FIELD_H);
|
||||
st->display_input.cursor, st->field == 1, false,
|
||||
theme, USER_FIELD_H, st->display_input.selection_anchor);
|
||||
y += mtk::labeled_text_height(theme, USER_FIELD_H) + 10;
|
||||
|
||||
mtk::draw_labeled_text_field(c, pad, y, fw, "Password", st->password,
|
||||
st->password_len, st->field == 2, true, theme, USER_FIELD_H);
|
||||
st->password_input.cursor, st->field == 2, true,
|
||||
theme, USER_FIELD_H, st->password_input.selection_anchor);
|
||||
y += mtk::labeled_text_height(theme, USER_FIELD_H) + 12;
|
||||
|
||||
c.text(pad, y + 4, "Role:", theme.text_muted);
|
||||
@@ -112,6 +103,20 @@ static void adduser_on_draw(Window* win, Framebuffer& fb) {
|
||||
cancel_btn, "Cancel",
|
||||
dialog_button_state(st->mouse_x, st->mouse_y, cancel_btn),
|
||||
theme);
|
||||
|
||||
mtk::draw_text_input_context_menu(
|
||||
c, st->username_input, theme,
|
||||
mtk::text_input_has_selection(st->username_input,
|
||||
st->username, (int)sizeof(st->username)));
|
||||
mtk::draw_text_input_context_menu(
|
||||
c, st->display_input, theme,
|
||||
mtk::text_input_has_selection(st->display_input,
|
||||
st->display, (int)sizeof(st->display)));
|
||||
mtk::draw_text_input_context_menu(
|
||||
c, st->password_input, theme,
|
||||
mtk::text_input_has_selection(st->password_input,
|
||||
st->password, (int)sizeof(st->password)),
|
||||
true);
|
||||
}
|
||||
|
||||
static void adduser_submit(AddUserDialogState* st) {
|
||||
@@ -145,25 +150,90 @@ static void adduser_on_mouse(Window* win, MouseEvent& ev) {
|
||||
if (!host.map_mouse(ev, &mx, &my)) return;
|
||||
st->mouse_x = mx;
|
||||
st->mouse_y = my;
|
||||
if (!ev.left_pressed()) return;
|
||||
|
||||
mtk::Theme theme = mtk::make_theme(st->accent);
|
||||
int pad = 16;
|
||||
int fw = win->content_w - 2 * pad;
|
||||
int y = pad;
|
||||
bool right_pressed = ev.right_pressed();
|
||||
|
||||
Rect username_rect = mtk::labeled_text_input_rect(pad, y, fw, theme, USER_FIELD_H);
|
||||
if (username_rect.contains(mx, my)) { st->field = 0; return; }
|
||||
if (st->username_input.context.open || st->username_input.dragging) {
|
||||
int result = mtk::text_input_handle_mouse(
|
||||
st->username_input, username_rect, st->username, (int)sizeof(st->username),
|
||||
mx, my, ev.buttons, ev.prev_buttons, win->content_w, win->content_h,
|
||||
st->field == 0);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
st->username_len = mtk::text_input_len(st->username, (int)sizeof(st->username));
|
||||
return;
|
||||
}
|
||||
y += mtk::labeled_text_height(theme, USER_FIELD_H) + 10;
|
||||
|
||||
Rect display_rect = mtk::labeled_text_input_rect(pad, y, fw, theme, USER_FIELD_H);
|
||||
if (display_rect.contains(mx, my)) { st->field = 1; return; }
|
||||
if (st->display_input.context.open || st->display_input.dragging) {
|
||||
int result = mtk::text_input_handle_mouse(
|
||||
st->display_input, display_rect, st->display, (int)sizeof(st->display),
|
||||
mx, my, ev.buttons, ev.prev_buttons, win->content_w, win->content_h,
|
||||
st->field == 1);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
st->display_len = mtk::text_input_len(st->display, (int)sizeof(st->display));
|
||||
return;
|
||||
}
|
||||
y += mtk::labeled_text_height(theme, USER_FIELD_H) + 10;
|
||||
|
||||
Rect password_rect = mtk::labeled_text_input_rect(pad, y, fw, theme, USER_FIELD_H);
|
||||
if (password_rect.contains(mx, my)) { st->field = 2; return; }
|
||||
if (st->password_input.context.open || st->password_input.dragging) {
|
||||
int result = mtk::text_input_handle_mouse(
|
||||
st->password_input, password_rect, st->password, (int)sizeof(st->password),
|
||||
mx, my, ev.buttons, ev.prev_buttons, win->content_w, win->content_h,
|
||||
st->field == 2, true);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
st->password_len = mtk::text_input_len(st->password, (int)sizeof(st->password));
|
||||
return;
|
||||
}
|
||||
y += mtk::labeled_text_height(theme, USER_FIELD_H) + 12;
|
||||
|
||||
if (!ev.left_pressed() && !right_pressed) return;
|
||||
|
||||
if (username_rect.contains(mx, my)) {
|
||||
st->field = 0;
|
||||
mtk::context_menu_close(st->display_input.context);
|
||||
mtk::context_menu_close(st->password_input.context);
|
||||
int result = mtk::text_input_handle_mouse(
|
||||
st->username_input, username_rect, st->username, (int)sizeof(st->username),
|
||||
mx, my, ev.buttons, ev.prev_buttons, win->content_w, win->content_h,
|
||||
true);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
st->username_len = mtk::text_input_len(st->username, (int)sizeof(st->username));
|
||||
return;
|
||||
}
|
||||
if (display_rect.contains(mx, my)) {
|
||||
st->field = 1;
|
||||
mtk::context_menu_close(st->username_input.context);
|
||||
mtk::context_menu_close(st->password_input.context);
|
||||
int result = mtk::text_input_handle_mouse(
|
||||
st->display_input, display_rect, st->display, (int)sizeof(st->display),
|
||||
mx, my, ev.buttons, ev.prev_buttons, win->content_w, win->content_h,
|
||||
true);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
st->display_len = mtk::text_input_len(st->display, (int)sizeof(st->display));
|
||||
return;
|
||||
}
|
||||
if (password_rect.contains(mx, my)) {
|
||||
st->field = 2;
|
||||
mtk::context_menu_close(st->username_input.context);
|
||||
mtk::context_menu_close(st->display_input.context);
|
||||
int result = mtk::text_input_handle_mouse(
|
||||
st->password_input, password_rect, st->password, (int)sizeof(st->password),
|
||||
mx, my, ev.buttons, ev.prev_buttons, win->content_w, win->content_h,
|
||||
true, true);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
st->password_len = mtk::text_input_len(st->password, (int)sizeof(st->password));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ev.left_pressed()) return;
|
||||
|
||||
Rect user_btn = {pad + 60, y, 60, USER_BTN_H};
|
||||
Rect admin_btn = {pad + 128, y, 60, USER_BTN_H};
|
||||
if (user_btn.contains(mx, my)) {
|
||||
@@ -203,20 +273,28 @@ static void adduser_on_key(Window* win, const Montauk::KeyEvent& key) {
|
||||
st->field = (st->field + 1) % 3;
|
||||
return;
|
||||
}
|
||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||
switch (st->field) {
|
||||
case 0: dialog_backspace(st->username, &st->username_len); break;
|
||||
case 1: dialog_backspace(st->display, &st->display_len); break;
|
||||
case 2: dialog_backspace(st->password, &st->password_len); break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (key.ascii >= 0x20 && key.ascii < 0x7F) {
|
||||
switch (st->field) {
|
||||
case 0: dialog_append(st->username, &st->username_len, 31, key.ascii); break;
|
||||
case 1: dialog_append(st->display, &st->display_len, 63, key.ascii); break;
|
||||
case 2: dialog_append(st->password, &st->password_len, 63, key.ascii); break;
|
||||
}
|
||||
|
||||
int result = mtk::TEXT_INPUT_NONE;
|
||||
switch (st->field) {
|
||||
case 0:
|
||||
result = mtk::text_input_key(st->username_input, st->username,
|
||||
(int)sizeof(st->username), key);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
st->username_len = mtk::text_input_len(st->username, (int)sizeof(st->username));
|
||||
break;
|
||||
case 1:
|
||||
result = mtk::text_input_key(st->display_input, st->display,
|
||||
(int)sizeof(st->display), key);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
st->display_len = mtk::text_input_len(st->display, (int)sizeof(st->display));
|
||||
break;
|
||||
case 2:
|
||||
result = mtk::text_input_key(st->password_input, st->password,
|
||||
(int)sizeof(st->password), key,
|
||||
nullptr, nullptr, true);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
st->password_len = mtk::text_input_len(st->password, (int)sizeof(st->password));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,7 +340,9 @@ struct ChPwdDialogState {
|
||||
char display_name[64];
|
||||
int field;
|
||||
char new_pwd[64]; int new_len;
|
||||
mtk::TextInputState new_input;
|
||||
char confirm[64]; int confirm_len;
|
||||
mtk::TextInputState confirm_input;
|
||||
char error[64];
|
||||
};
|
||||
|
||||
@@ -286,11 +366,13 @@ static void chpwd_on_draw(Window* win, Framebuffer& fb) {
|
||||
y += sfh + 12;
|
||||
|
||||
mtk::draw_labeled_text_field(c, pad, y, fw, "New Password", st->new_pwd,
|
||||
st->new_len, st->field == 0, true, theme, USER_FIELD_H);
|
||||
st->new_input.cursor, st->field == 0, true,
|
||||
theme, USER_FIELD_H, st->new_input.selection_anchor);
|
||||
y += mtk::labeled_text_height(theme, USER_FIELD_H) + 10;
|
||||
|
||||
mtk::draw_labeled_text_field(c, pad, y, fw, "Confirm Password", st->confirm,
|
||||
st->confirm_len, st->field == 1, true, theme, USER_FIELD_H);
|
||||
st->confirm_input.cursor, st->field == 1, true,
|
||||
theme, USER_FIELD_H, st->confirm_input.selection_anchor);
|
||||
y += mtk::labeled_text_height(theme, USER_FIELD_H) + 12;
|
||||
|
||||
if (st->error[0]) {
|
||||
@@ -306,6 +388,17 @@ static void chpwd_on_draw(Window* win, Framebuffer& fb) {
|
||||
cancel_btn, "Cancel",
|
||||
dialog_button_state(st->mouse_x, st->mouse_y, cancel_btn),
|
||||
theme);
|
||||
|
||||
mtk::draw_text_input_context_menu(
|
||||
c, st->new_input, theme,
|
||||
mtk::text_input_has_selection(st->new_input,
|
||||
st->new_pwd, (int)sizeof(st->new_pwd)),
|
||||
true);
|
||||
mtk::draw_text_input_context_menu(
|
||||
c, st->confirm_input, theme,
|
||||
mtk::text_input_has_selection(st->confirm_input,
|
||||
st->confirm, (int)sizeof(st->confirm)),
|
||||
true);
|
||||
}
|
||||
|
||||
static void chpwd_submit(ChPwdDialogState* st) {
|
||||
@@ -333,19 +426,62 @@ static void chpwd_on_mouse(Window* win, MouseEvent& ev) {
|
||||
if (!host.map_mouse(ev, &mx, &my)) return;
|
||||
st->mouse_x = mx;
|
||||
st->mouse_y = my;
|
||||
if (!ev.left_pressed()) return;
|
||||
|
||||
mtk::Theme theme = mtk::make_theme(st->accent);
|
||||
int pad = 16;
|
||||
int fw = win->content_w - 2 * pad;
|
||||
int y = pad + system_font_height() + 12;
|
||||
bool right_pressed = ev.right_pressed();
|
||||
|
||||
Rect new_rect = mtk::labeled_text_input_rect(pad, y, fw, theme, USER_FIELD_H);
|
||||
if (new_rect.contains(mx, my)) { st->field = 0; return; }
|
||||
if (st->new_input.context.open || st->new_input.dragging) {
|
||||
int result = mtk::text_input_handle_mouse(
|
||||
st->new_input, new_rect, st->new_pwd, (int)sizeof(st->new_pwd),
|
||||
mx, my, ev.buttons, ev.prev_buttons, win->content_w, win->content_h,
|
||||
st->field == 0, true);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
st->new_len = mtk::text_input_len(st->new_pwd, (int)sizeof(st->new_pwd));
|
||||
return;
|
||||
}
|
||||
y += mtk::labeled_text_height(theme, USER_FIELD_H) + 10;
|
||||
|
||||
Rect confirm_rect = mtk::labeled_text_input_rect(pad, y, fw, theme, USER_FIELD_H);
|
||||
if (confirm_rect.contains(mx, my)) { st->field = 1; return; }
|
||||
if (st->confirm_input.context.open || st->confirm_input.dragging) {
|
||||
int result = mtk::text_input_handle_mouse(
|
||||
st->confirm_input, confirm_rect, st->confirm, (int)sizeof(st->confirm),
|
||||
mx, my, ev.buttons, ev.prev_buttons, win->content_w, win->content_h,
|
||||
st->field == 1, true);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
st->confirm_len = mtk::text_input_len(st->confirm, (int)sizeof(st->confirm));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ev.left_pressed() && !right_pressed) return;
|
||||
|
||||
if (new_rect.contains(mx, my)) {
|
||||
st->field = 0;
|
||||
mtk::context_menu_close(st->confirm_input.context);
|
||||
int result = mtk::text_input_handle_mouse(
|
||||
st->new_input, new_rect, st->new_pwd, (int)sizeof(st->new_pwd),
|
||||
mx, my, ev.buttons, ev.prev_buttons, win->content_w, win->content_h,
|
||||
true, true);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
st->new_len = mtk::text_input_len(st->new_pwd, (int)sizeof(st->new_pwd));
|
||||
return;
|
||||
}
|
||||
if (confirm_rect.contains(mx, my)) {
|
||||
st->field = 1;
|
||||
mtk::context_menu_close(st->new_input.context);
|
||||
int result = mtk::text_input_handle_mouse(
|
||||
st->confirm_input, confirm_rect, st->confirm, (int)sizeof(st->confirm),
|
||||
mx, my, ev.buttons, ev.prev_buttons, win->content_w, win->content_h,
|
||||
true, true);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
st->confirm_len = mtk::text_input_len(st->confirm, (int)sizeof(st->confirm));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ev.left_pressed()) return;
|
||||
|
||||
Rect save_btn = dialog_primary_button_rect(pad, win->content_h);
|
||||
Rect cancel_btn = dialog_secondary_button_rect(pad, win->content_h);
|
||||
@@ -375,14 +511,18 @@ static void chpwd_on_key(Window* win, const Montauk::KeyEvent& key) {
|
||||
st->field = (st->field + 1) % 2;
|
||||
return;
|
||||
}
|
||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||
if (st->field == 0) dialog_backspace(st->new_pwd, &st->new_len);
|
||||
else dialog_backspace(st->confirm, &st->confirm_len);
|
||||
return;
|
||||
}
|
||||
if (key.ascii >= 0x20 && key.ascii < 0x7F) {
|
||||
if (st->field == 0) dialog_append(st->new_pwd, &st->new_len, 63, key.ascii);
|
||||
else dialog_append(st->confirm, &st->confirm_len, 63, key.ascii);
|
||||
if (st->field == 0) {
|
||||
int result = mtk::text_input_key(st->new_input, st->new_pwd,
|
||||
(int)sizeof(st->new_pwd), key,
|
||||
nullptr, nullptr, true);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
st->new_len = mtk::text_input_len(st->new_pwd, (int)sizeof(st->new_pwd));
|
||||
} else {
|
||||
int result = mtk::text_input_key(st->confirm_input, st->confirm,
|
||||
(int)sizeof(st->confirm), key,
|
||||
nullptr, nullptr, true);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
st->confirm_len = mtk::text_input_len(st->confirm, (int)sizeof(st->confirm));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+87
-107
@@ -38,7 +38,7 @@ enum Tab {
|
||||
struct Field {
|
||||
const char* label;
|
||||
char value[FIELD_TEXT_CAP];
|
||||
int cursor;
|
||||
mtk::TextInputState input;
|
||||
};
|
||||
|
||||
static const char* const kTabLabels[TAB_COUNT] = {
|
||||
@@ -54,10 +54,10 @@ static int g_focus_field = -1;
|
||||
static Montauk::NetCfg g_cfg = {};
|
||||
static Montauk::NetStatus g_net = {};
|
||||
static Field g_fields[FIELD_COUNT] = {
|
||||
{"IP Address", {}, 0},
|
||||
{"Subnet Mask", {}, 0},
|
||||
{"Gateway", {}, 0},
|
||||
{"DNS Server", {}, 0},
|
||||
{"IP Address", {}, {}},
|
||||
{"Subnet Mask", {}, {}},
|
||||
{"Gateway", {}, {}},
|
||||
{"DNS Server", {}, {}},
|
||||
};
|
||||
static bool g_dirty = false;
|
||||
static char g_status[128] = {};
|
||||
@@ -108,6 +108,11 @@ static bool is_ipv4_input_char(char ch) {
|
||||
return (ch >= '0' && ch <= '9') || ch == '.';
|
||||
}
|
||||
|
||||
static bool accept_ipv4_input_char(char ch, void* userdata) {
|
||||
(void)userdata;
|
||||
return is_ipv4_input_char(ch);
|
||||
}
|
||||
|
||||
static void focus_field(int index, int cursor = -1) {
|
||||
if (index < 0) index = 0;
|
||||
if (index >= FIELD_COUNT) index = FIELD_COUNT - 1;
|
||||
@@ -118,7 +123,10 @@ static void focus_field(int index, int cursor = -1) {
|
||||
if (cursor > len) cursor = len;
|
||||
|
||||
g_focus_field = index;
|
||||
field.cursor = cursor;
|
||||
for (int i = 0; i < FIELD_COUNT; i++) {
|
||||
if (i != index) mtk::context_menu_close(g_fields[i].input.context);
|
||||
}
|
||||
mtk::text_input_reset(field.input, cursor);
|
||||
}
|
||||
|
||||
static bool mac_is_zero(const uint8_t* mac) {
|
||||
@@ -233,7 +241,7 @@ static void copy_cfg_to_fields() {
|
||||
format_ip(g_fields[2].value, sizeof(g_fields[2].value), g_cfg.gateway);
|
||||
format_ip(g_fields[3].value, sizeof(g_fields[3].value), g_cfg.dnsServer);
|
||||
for (int i = 0; i < FIELD_COUNT; i++) {
|
||||
g_fields[i].cursor = str_len(g_fields[i].value);
|
||||
mtk::text_input_reset(g_fields[i].input, str_len(g_fields[i].value));
|
||||
}
|
||||
g_dirty = false;
|
||||
}
|
||||
@@ -324,30 +332,6 @@ static int field_w(int index) {
|
||||
return config_two_columns() ? (available - GAP) / 2 : available;
|
||||
}
|
||||
|
||||
static int field_cursor_from_x(int index, int mx) {
|
||||
if (index < 0 || index >= FIELD_COUNT) return 0;
|
||||
|
||||
Rect input = field_input_rect(index);
|
||||
int rel_x = mx - (input.x + 8);
|
||||
if (rel_x <= 0) return 0;
|
||||
|
||||
Field& field = g_fields[index];
|
||||
int len = str_len(field.value);
|
||||
char prefix[FIELD_TEXT_CAP] = {};
|
||||
int prev_w = 0;
|
||||
|
||||
for (int i = 0; i < len; i++) {
|
||||
prefix[i] = field.value[i];
|
||||
prefix[i + 1] = '\0';
|
||||
int cur_w = text_width(prefix);
|
||||
int midpoint = prev_w + (cur_w - prev_w) / 2;
|
||||
if (rel_x < midpoint) return i;
|
||||
prev_w = cur_w;
|
||||
}
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
static void draw_kv(Canvas& c, int* y, const char* key, const char* value,
|
||||
const mtk::Theme& theme) {
|
||||
int fh = system_font_height();
|
||||
@@ -410,8 +394,19 @@ static void draw_config_tab(Canvas& c, const mtk::Theme& theme) {
|
||||
for (int i = 0; i < FIELD_COUNT; i++) {
|
||||
mtk::draw_labeled_text_field(c, field_x(i), field_label_y(i), field_w(i),
|
||||
g_fields[i].label, g_fields[i].value,
|
||||
g_fields[i].cursor, g_focus_field == i,
|
||||
false, theme, FIELD_H);
|
||||
g_fields[i].input.cursor, g_focus_field == i,
|
||||
false, theme, FIELD_H,
|
||||
g_fields[i].input.selection_anchor);
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_config_context_menus(Canvas& c, const mtk::Theme& theme) {
|
||||
for (int i = 0; i < FIELD_COUNT; i++) {
|
||||
mtk::draw_text_input_context_menu(
|
||||
c, g_fields[i].input, theme,
|
||||
mtk::text_input_has_selection(g_fields[i].input,
|
||||
g_fields[i].value,
|
||||
(int)sizeof(g_fields[i].value)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,6 +459,8 @@ static void render() {
|
||||
draw_config_tab(c, theme);
|
||||
}
|
||||
draw_footer(c, theme);
|
||||
if (g_tab == TAB_CONFIG)
|
||||
draw_config_context_menus(c, theme);
|
||||
|
||||
host.present();
|
||||
}
|
||||
@@ -530,17 +527,27 @@ static void set_tab(Tab tab) {
|
||||
focus_field(0);
|
||||
} else {
|
||||
g_focus_field = -1;
|
||||
for (int i = 0; i < FIELD_COUNT; i++) {
|
||||
g_fields[i].input.dragging = false;
|
||||
mtk::context_menu_close(g_fields[i].input.context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool handle_click(int mx, int my) {
|
||||
int tab = mtk::hit_tab_bar(tab_bar_rect(), TAB_COUNT, mx, my);
|
||||
if (tab >= 0) {
|
||||
set_tab((Tab)tab);
|
||||
return true;
|
||||
static bool handle_mouse(int mx, int my, uint8_t buttons, uint8_t prev_buttons) {
|
||||
bool left_pressed = (buttons & 1) && !(prev_buttons & 1);
|
||||
bool right_pressed = (buttons & 2) && !(prev_buttons & 2);
|
||||
|
||||
if (left_pressed) {
|
||||
int tab = mtk::hit_tab_bar(tab_bar_rect(), TAB_COUNT, mx, my);
|
||||
if (tab >= 0) {
|
||||
set_tab((Tab)tab);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (g_tab == TAB_STATUS) {
|
||||
if (!left_pressed) return false;
|
||||
if (status_refresh_button().contains(mx, my)) {
|
||||
refresh_state(!g_dirty);
|
||||
set_status("Network status refreshed");
|
||||
@@ -558,12 +565,44 @@ static bool handle_click(int mx, int my) {
|
||||
}
|
||||
|
||||
for (int i = 0; i < FIELD_COUNT; i++) {
|
||||
if (field_input_rect(i).contains(mx, my)) {
|
||||
focus_field(i, field_cursor_from_x(i, mx));
|
||||
if (!g_fields[i].input.context.open && !g_fields[i].input.dragging) continue;
|
||||
int result = mtk::text_input_handle_mouse(g_fields[i].input,
|
||||
field_input_rect(i),
|
||||
g_fields[i].value,
|
||||
(int)sizeof(g_fields[i].value),
|
||||
mx, my, buttons, prev_buttons,
|
||||
g_win.width, g_win.height,
|
||||
g_focus_field == i,
|
||||
false, accept_ipv4_input_char);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
g_dirty = true;
|
||||
return result != mtk::TEXT_INPUT_NONE;
|
||||
}
|
||||
|
||||
if (!left_pressed && !right_pressed) return false;
|
||||
|
||||
for (int i = 0; i < FIELD_COUNT; i++) {
|
||||
Rect input = field_input_rect(i);
|
||||
if (input.contains(mx, my)) {
|
||||
g_focus_field = i;
|
||||
for (int j = 0; j < FIELD_COUNT; j++) {
|
||||
if (j != i) mtk::context_menu_close(g_fields[j].input.context);
|
||||
}
|
||||
int result = mtk::text_input_handle_mouse(g_fields[i].input, input,
|
||||
g_fields[i].value,
|
||||
(int)sizeof(g_fields[i].value),
|
||||
mx, my, buttons, prev_buttons,
|
||||
g_win.width, g_win.height,
|
||||
true, false,
|
||||
accept_ipv4_input_char);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
g_dirty = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!left_pressed) return false;
|
||||
|
||||
if (config_dhcp_button().contains(mx, my)) {
|
||||
launch_dhcp();
|
||||
return true;
|
||||
@@ -584,38 +623,6 @@ static bool handle_click(int mx, int my) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static void delete_before_cursor(Field* field) {
|
||||
if (!field || field->cursor <= 0) return;
|
||||
int len = str_len(field->value);
|
||||
for (int i = field->cursor - 1; i < len; i++) {
|
||||
field->value[i] = field->value[i + 1];
|
||||
}
|
||||
field->cursor--;
|
||||
g_dirty = true;
|
||||
}
|
||||
|
||||
static void delete_at_cursor(Field* field) {
|
||||
if (!field) return;
|
||||
int len = str_len(field->value);
|
||||
if (field->cursor >= len) return;
|
||||
for (int i = field->cursor; i < len; i++) {
|
||||
field->value[i] = field->value[i + 1];
|
||||
}
|
||||
g_dirty = true;
|
||||
}
|
||||
|
||||
static void insert_char(Field* field, char ch) {
|
||||
if (!field) return;
|
||||
if (!((ch >= '0' && ch <= '9') || ch == '.')) return;
|
||||
int len = str_len(field->value);
|
||||
if (len >= FIELD_TEXT_CAP - 1) return;
|
||||
for (int i = len; i >= field->cursor; i--) {
|
||||
field->value[i + 1] = field->value[i];
|
||||
}
|
||||
field->value[field->cursor++] = ch;
|
||||
g_dirty = true;
|
||||
}
|
||||
|
||||
static bool handle_key(const Montauk::KeyEvent& key) {
|
||||
if (!key.pressed) return false;
|
||||
|
||||
@@ -664,38 +671,12 @@ static bool handle_key(const Montauk::KeyEvent& key) {
|
||||
}
|
||||
|
||||
Field* field = &g_fields[g_focus_field];
|
||||
int len = str_len(field->value);
|
||||
|
||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||
delete_before_cursor(field);
|
||||
return true;
|
||||
}
|
||||
if (key.scancode == 0x53) {
|
||||
delete_at_cursor(field);
|
||||
return true;
|
||||
}
|
||||
if (key.scancode == 0x4B) {
|
||||
if (field->cursor > 0) field->cursor--;
|
||||
return true;
|
||||
}
|
||||
if (key.scancode == 0x4D) {
|
||||
if (field->cursor < len) field->cursor++;
|
||||
return true;
|
||||
}
|
||||
if (key.scancode == 0x47) {
|
||||
field->cursor = 0;
|
||||
return true;
|
||||
}
|
||||
if (key.scancode == 0x4F) {
|
||||
field->cursor = len;
|
||||
return true;
|
||||
}
|
||||
if (is_ipv4_input_char(key.ascii)) {
|
||||
insert_char(field, key.ascii);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
int result = mtk::text_input_key(field->input, field->value,
|
||||
(int)sizeof(field->value), key,
|
||||
accept_ipv4_input_char);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED)
|
||||
g_dirty = true;
|
||||
return (result & mtk::TEXT_INPUT_CONSUMED) != 0;
|
||||
}
|
||||
|
||||
extern "C" void _start() {
|
||||
@@ -737,9 +718,8 @@ extern "C" void _start() {
|
||||
g_mouse_x = ev.mouse.x;
|
||||
g_mouse_y = ev.mouse.y;
|
||||
redraw = true;
|
||||
|
||||
bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
|
||||
if (clicked && handle_click(ev.mouse.x, ev.mouse.y)) {
|
||||
if (handle_mouse(ev.mouse.x, ev.mouse.y,
|
||||
ev.mouse.buttons, ev.mouse.prev_buttons)) {
|
||||
redraw = true;
|
||||
}
|
||||
} else if (ev.type == 0) {
|
||||
|
||||
@@ -79,6 +79,7 @@ struct AppState {
|
||||
int config_button_down;
|
||||
char edit_uri[MAX_PATH_LEN];
|
||||
int edit_len;
|
||||
mtk::TextInputState edit_input;
|
||||
char edit_error[128];
|
||||
int mouse_x;
|
||||
int mouse_y;
|
||||
@@ -473,6 +474,7 @@ static void close_config_popup() {
|
||||
static void open_config_popup() {
|
||||
safe_copy(g_app.edit_uri, sizeof(g_app.edit_uri), g_app.printer_uri);
|
||||
g_app.edit_len = montauk::slen(g_app.edit_uri);
|
||||
mtk::text_input_reset(g_app.edit_input, g_app.edit_len);
|
||||
g_app.edit_error[0] = '\0';
|
||||
g_app.config_button_down = 0;
|
||||
g_app.config_mouse_x = -1;
|
||||
@@ -535,8 +537,10 @@ static void render_config_popup() {
|
||||
c.text(x, y, "Enter an IPP or IPPS destination URI.", theme.text_muted);
|
||||
|
||||
mtk::draw_labeled_text_field(c, lo.input.x, lo.input_label_y, lo.input.w,
|
||||
"Printer URI", g_app.edit_uri, g_app.edit_len,
|
||||
true, false, theme, lo.input.h);
|
||||
"Printer URI", g_app.edit_uri,
|
||||
g_app.edit_input.cursor,
|
||||
true, false, theme, lo.input.h,
|
||||
g_app.edit_input.selection_anchor);
|
||||
|
||||
y = lo.input.y + lo.input.h + 12;
|
||||
c.text(x, y, "Example: ipp://printer.local/ipp/print", theme.text_muted);
|
||||
@@ -552,6 +556,10 @@ static void render_config_popup() {
|
||||
lo.cancel_btn, "Cancel",
|
||||
config_button_state(lo.cancel_btn, true, g_app.config_button_down == 2),
|
||||
theme);
|
||||
mtk::draw_text_input_context_menu(
|
||||
c, g_app.edit_input, theme,
|
||||
mtk::text_input_has_selection(g_app.edit_input,
|
||||
g_app.edit_uri, MAX_PATH_LEN));
|
||||
montauk::win_present(g_config.win_id);
|
||||
}
|
||||
|
||||
@@ -563,8 +571,38 @@ static bool handle_config_mouse(int mx, int my, uint8_t buttons, uint8_t prev_bu
|
||||
|
||||
bool left_pressed = (buttons & 1) && !(prev_buttons & 1);
|
||||
bool left_released = !(buttons & 1) && (prev_buttons & 1);
|
||||
bool right_pressed = (buttons & 2) && !(prev_buttons & 2);
|
||||
|
||||
if (g_app.edit_input.context.open || g_app.edit_input.dragging) {
|
||||
int result = mtk::text_input_handle_mouse(g_app.edit_input, lo.input,
|
||||
g_app.edit_uri, MAX_PATH_LEN,
|
||||
mx, my, buttons, prev_buttons,
|
||||
g_config.width, g_config.height,
|
||||
true);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED) {
|
||||
g_app.edit_len = mtk::text_input_len(g_app.edit_uri, MAX_PATH_LEN);
|
||||
g_app.edit_error[0] = '\0';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (left_pressed || right_pressed) {
|
||||
if (lo.input.contains(mx, my)) {
|
||||
g_app.config_button_down = 0;
|
||||
int result = mtk::text_input_handle_mouse(g_app.edit_input, lo.input,
|
||||
g_app.edit_uri, MAX_PATH_LEN,
|
||||
mx, my, buttons, prev_buttons,
|
||||
g_config.width, g_config.height,
|
||||
true);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED) {
|
||||
g_app.edit_len = mtk::text_input_len(g_app.edit_uri, MAX_PATH_LEN);
|
||||
g_app.edit_error[0] = '\0';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!left_pressed) return false;
|
||||
|
||||
if (left_pressed) {
|
||||
int button_down = 0;
|
||||
if (lo.save_btn.contains(mx, my))
|
||||
button_down = 1;
|
||||
@@ -601,17 +639,11 @@ static bool handle_config_key(const Montauk::KeyEvent& key) {
|
||||
save_configured_printer();
|
||||
return !g_app.config_open;
|
||||
}
|
||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||
if (g_app.edit_len > 0) {
|
||||
g_app.edit_len--;
|
||||
g_app.edit_uri[g_app.edit_len] = '\0';
|
||||
g_app.edit_error[0] = '\0';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (key.ascii >= 0x20 && key.ascii < 0x7F && g_app.edit_len < MAX_PATH_LEN - 1) {
|
||||
g_app.edit_uri[g_app.edit_len++] = key.ascii;
|
||||
g_app.edit_uri[g_app.edit_len] = '\0';
|
||||
|
||||
int result = mtk::text_input_key(g_app.edit_input, g_app.edit_uri,
|
||||
MAX_PATH_LEN, key);
|
||||
if (result & mtk::TEXT_INPUT_CHANGED) {
|
||||
g_app.edit_len = mtk::text_input_len(g_app.edit_uri, MAX_PATH_LEN);
|
||||
g_app.edit_error[0] = '\0';
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <gui/svg.hpp>
|
||||
#include <gui/dialogs.hpp>
|
||||
#include <gui/clipboard.hpp>
|
||||
#include <gui/mtk/widgets.hpp>
|
||||
#include <gui/stb_math.h>
|
||||
|
||||
extern "C" {
|
||||
@@ -59,6 +60,14 @@ static constexpr Color STATUS_TEXT = Color::from_rgb(0xE0, 0xE0, 0xE0);
|
||||
static constexpr Color PATHBAR_BG = Color::from_rgb(0xF0, 0xF0, 0xF0);
|
||||
static constexpr Color WHITE = Color::from_rgb(0xFF, 0xFF, 0xFF);
|
||||
|
||||
enum ContextItem {
|
||||
CTX_CUT = 0,
|
||||
CTX_COPY,
|
||||
CTX_PASTE,
|
||||
CTX_DELETE,
|
||||
CTX_COUNT,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Global state
|
||||
// ============================================================================
|
||||
@@ -101,6 +110,9 @@ int g_sel_end = 0;
|
||||
bool g_has_selection = false;
|
||||
bool g_mouse_selecting = false;
|
||||
|
||||
// Context menu
|
||||
mtk::ContextMenuState g_ctx_menu = {};
|
||||
|
||||
// Pathbar
|
||||
bool g_pathbar_open = false;
|
||||
bool g_pathbar_save = false;
|
||||
@@ -501,6 +513,57 @@ static bool paste_from_clipboard() {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool context_item_enabled(int item) {
|
||||
int selected = selection_length();
|
||||
switch (item) {
|
||||
case CTX_CUT:
|
||||
case CTX_COPY:
|
||||
case CTX_DELETE:
|
||||
return selected > 0;
|
||||
case CTX_PASTE:
|
||||
return gui::clipboard_has_text();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static void build_context_items(mtk::ContextMenuItem items[CTX_COUNT]) {
|
||||
items[CTX_CUT] = {"Cut", CTX_CUT, context_item_enabled(CTX_CUT)};
|
||||
items[CTX_COPY] = {"Copy", CTX_COPY, context_item_enabled(CTX_COPY)};
|
||||
items[CTX_PASTE] = {"Paste", CTX_PASTE, context_item_enabled(CTX_PASTE)};
|
||||
items[CTX_DELETE] = {"Delete", CTX_DELETE, context_item_enabled(CTX_DELETE)};
|
||||
}
|
||||
|
||||
static bool selection_contains_pos(int pos) {
|
||||
if (!g_has_selection) return false;
|
||||
int ss, se;
|
||||
sel_range(&ss, &se);
|
||||
return pos >= ss && pos < se;
|
||||
}
|
||||
|
||||
static void close_context_menu() {
|
||||
mtk::context_menu_close(g_ctx_menu);
|
||||
}
|
||||
|
||||
static bool perform_context_action(int item) {
|
||||
if (!context_item_enabled(item)) return false;
|
||||
|
||||
switch (item) {
|
||||
case CTX_CUT:
|
||||
return cut_selection_to_clipboard();
|
||||
case CTX_COPY:
|
||||
return copy_selection_to_clipboard();
|
||||
case CTX_PASTE:
|
||||
return paste_from_clipboard();
|
||||
case CTX_DELETE:
|
||||
delete_selection();
|
||||
set_status("Deleted selection");
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool looks_binary(const char* data, int len) {
|
||||
if (!data || len <= 0) return false;
|
||||
|
||||
@@ -625,6 +688,22 @@ static int hit_test(int mx, int my, int editor_y) {
|
||||
return g_line_offsets[clicked_line] + clicked_col;
|
||||
}
|
||||
|
||||
static void draw_context_menu(uint32_t* pixels, int W, int H) {
|
||||
if (!g_ctx_menu.open) return;
|
||||
|
||||
Canvas c(pixels, W, H);
|
||||
mtk::Theme theme = mtk::make_theme(ACCENT);
|
||||
theme.border = BORDER_COLOR;
|
||||
theme.text = TEXT_COLOR;
|
||||
|
||||
mtk::ContextMenuItem items[CTX_COUNT];
|
||||
build_context_items(items);
|
||||
mtk::draw_context_menu(c, g_ctx_menu, items, CTX_COUNT, theme,
|
||||
mtk::CONTEXT_MENU_DEFAULT_W,
|
||||
mtk::CONTEXT_MENU_ITEM_H,
|
||||
g_font, FONT_SIZE);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Rendering
|
||||
// ============================================================================
|
||||
@@ -880,6 +959,8 @@ static void render(uint32_t* pixels) {
|
||||
int rw = g_font->measure_text(status_right, FONT_SIZE);
|
||||
g_font->draw_to_buffer(pixels, W, H, W - rw - 4, sty, status_right, STATUS_TEXT, FONT_SIZE);
|
||||
}
|
||||
|
||||
draw_context_menu(pixels, W, H);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -948,6 +1029,11 @@ static void open_pathbar_save() {
|
||||
static bool handle_key(Montauk::KeyEvent& key, int win_id) {
|
||||
if (!key.pressed) return false;
|
||||
|
||||
if (g_ctx_menu.open) {
|
||||
close_context_menu();
|
||||
if (key.scancode == 0x01) return true;
|
||||
}
|
||||
|
||||
// ---- Pathbar mode ----
|
||||
if (g_pathbar_open) {
|
||||
if (key.ascii == '\n' || key.ascii == '\r') {
|
||||
@@ -1140,6 +1226,7 @@ static bool handle_mouse(Montauk::WinEvent& ev, int win_id) {
|
||||
uint8_t btns = ev.mouse.buttons;
|
||||
uint8_t prev = ev.mouse.prev_buttons;
|
||||
bool clicked = (btns & 1) && !(prev & 1);
|
||||
bool right_clicked = (btns & 2) && !(prev & 2);
|
||||
bool held = (btns & 1) != 0;
|
||||
bool released = !(btns & 1) && (prev & 1);
|
||||
int scroll = ev.mouse.scroll;
|
||||
@@ -1148,6 +1235,22 @@ static bool handle_mouse(Montauk::WinEvent& ev, int win_id) {
|
||||
int editor_y = TOOLBAR_H + (g_pathbar_open ? PATHBAR_H : 0);
|
||||
int text_area_h = g_win_h - editor_y - STATUS_H;
|
||||
|
||||
if (g_ctx_menu.open) {
|
||||
if (scroll != 0) {
|
||||
close_context_menu();
|
||||
return true;
|
||||
}
|
||||
|
||||
mtk::ContextMenuItem items[CTX_COUNT];
|
||||
build_context_items(items);
|
||||
int action = mtk::context_menu_handle_mouse(g_ctx_menu, items, CTX_COUNT,
|
||||
mx, my, btns, prev,
|
||||
g_win_w, g_win_h);
|
||||
if (action >= 0)
|
||||
perform_context_action(action);
|
||||
return action != mtk::CONTEXT_MENU_NONE;
|
||||
}
|
||||
|
||||
// ---- Toolbar clicks ----
|
||||
if (clicked && my < TOOLBAR_H) {
|
||||
// Open button
|
||||
@@ -1180,6 +1283,19 @@ static bool handle_mouse(Montauk::WinEvent& ev, int win_id) {
|
||||
}
|
||||
|
||||
// ---- Editor click ----
|
||||
if (right_clicked && my >= editor_y && my < editor_y + text_area_h
|
||||
&& mx > LINE_NUM_W && mx < g_win_w - SCROLLBAR_W) {
|
||||
int pos = hit_test(mx, my, editor_y);
|
||||
if (!selection_contains_pos(pos)) {
|
||||
g_cursor_pos = pos;
|
||||
update_cursor_pos();
|
||||
clear_selection();
|
||||
}
|
||||
g_mouse_selecting = false;
|
||||
mtk::context_menu_open(g_ctx_menu, mx, my);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (clicked && my >= editor_y && my < editor_y + text_area_h
|
||||
&& mx > LINE_NUM_W && mx < g_win_w - SCROLLBAR_W) {
|
||||
int pos = hit_test(mx, my, editor_y);
|
||||
|
||||
@@ -83,6 +83,7 @@ static constexpr int READER_FONT_FACE_COUNT = (int)ReaderFontFace::COUNT;
|
||||
|
||||
static AppPhase g_phase = AppPhase::IDLE;
|
||||
static char g_query[256] = {};
|
||||
static mtk::TextInputState g_query_input = {};
|
||||
static char g_status[256] = {};
|
||||
static int g_scroll_y = 0;
|
||||
static int g_line_count = 0;
|
||||
@@ -959,8 +960,10 @@ static void render(Canvas& canvas) {
|
||||
|
||||
// ---- Toolbar ----
|
||||
canvas.fill_rect(toolbar.x, toolbar.y, toolbar.w, toolbar.h, theme.surface_alt);
|
||||
mtk::draw_text_field(canvas, lo.search_field, g_query, (int)strlen(g_query),
|
||||
g_search_focused, false, theme);
|
||||
mtk::draw_text_field(canvas, lo.search_field, g_query,
|
||||
g_query_input.cursor,
|
||||
g_search_focused, false, theme,
|
||||
g_query_input.selection_anchor);
|
||||
draw_display_toggle_button(canvas, lo.display_button,
|
||||
mtk::widget_state(g_display_controls_open,
|
||||
mouse_in_rect(lo.display_button), true),
|
||||
@@ -1029,6 +1032,11 @@ static void render(Canvas& canvas) {
|
||||
theme.radius_sm, thumb_color);
|
||||
}
|
||||
}
|
||||
|
||||
mtk::draw_text_input_context_menu(
|
||||
canvas, g_query_input, theme,
|
||||
mtk::text_input_has_selection(g_query_input,
|
||||
g_query, (int)sizeof(g_query)));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -1125,12 +1133,9 @@ extern "C" void _start() {
|
||||
if (g_search_focused) {
|
||||
if (ascii == '\n' || ascii == '\r') {
|
||||
search_pending = true;
|
||||
} else if (ascii == '\b' || scan == 0x0E) {
|
||||
int len = (int)strlen(g_query);
|
||||
if (len > 0) g_query[len - 1] = '\0';
|
||||
} else if (ascii >= 32 && ascii < 127) {
|
||||
int len = (int)strlen(g_query);
|
||||
if (len < 254) { g_query[len] = ascii; g_query[len + 1] = '\0'; }
|
||||
} else {
|
||||
mtk::text_input_key(g_query_input, g_query,
|
||||
(int)sizeof(g_query), ev.key);
|
||||
}
|
||||
} else if (g_phase == AppPhase::DONE) {
|
||||
// Navigation keys
|
||||
@@ -1151,12 +1156,26 @@ extern "C" void _start() {
|
||||
g_mouse_y = my;
|
||||
bool left_down = (ev.mouse.buttons & 1) != 0;
|
||||
bool just_clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
|
||||
bool just_right_clicked = (ev.mouse.buttons & 2) && !(ev.mouse.prev_buttons & 2);
|
||||
bool just_released = !(ev.mouse.buttons & 1) && (ev.mouse.prev_buttons & 1);
|
||||
|
||||
ReaderToolbarLayout lo = toolbar_layout();
|
||||
Rect scrollbar_track = scrollbar_track_rect();
|
||||
Rect scrollbar_thumb = scrollbar_thumb_rect();
|
||||
|
||||
if (g_query_input.context.open || g_query_input.dragging) {
|
||||
int result = mtk::text_input_handle_mouse(g_query_input, lo.search_field,
|
||||
g_query, (int)sizeof(g_query),
|
||||
mx, my, ev.mouse.buttons,
|
||||
ev.mouse.prev_buttons,
|
||||
g_win_w, g_win_h,
|
||||
g_search_focused);
|
||||
if (result & mtk::TEXT_INPUT_CONSUMED) {
|
||||
just_clicked = false;
|
||||
just_right_clicked = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (just_released) {
|
||||
g_scroll_dragging = false;
|
||||
}
|
||||
@@ -1165,23 +1184,33 @@ extern "C" void _start() {
|
||||
set_scroll_from_thumb_top(my - g_scroll_drag_offset);
|
||||
}
|
||||
|
||||
if (just_clicked) {
|
||||
g_search_focused = lo.search_field.contains(mx, my);
|
||||
|
||||
if (scrollbar_thumb.contains(mx, my)) {
|
||||
if (just_clicked || just_right_clicked) {
|
||||
if (lo.search_field.contains(mx, my)) {
|
||||
g_search_focused = true;
|
||||
mtk::text_input_handle_mouse(g_query_input, lo.search_field,
|
||||
g_query, (int)sizeof(g_query),
|
||||
mx, my, ev.mouse.buttons, ev.mouse.prev_buttons,
|
||||
g_win_w, g_win_h, true);
|
||||
} else if (scrollbar_thumb.contains(mx, my)) {
|
||||
g_search_focused = false;
|
||||
g_scroll_dragging = true;
|
||||
g_scroll_drag_offset = my - scrollbar_thumb.y;
|
||||
} else if (scrollbar_track.contains(mx, my)) {
|
||||
g_search_focused = false;
|
||||
g_scroll_dragging = true;
|
||||
g_scroll_drag_offset = scrollbar_thumb.h / 2;
|
||||
set_scroll_from_thumb_top(my - g_scroll_drag_offset);
|
||||
} else if (lo.display_button.contains(mx, my)) {
|
||||
g_search_focused = false;
|
||||
g_display_controls_open = !g_display_controls_open;
|
||||
update_toolbar_height();
|
||||
rebuild_article(true);
|
||||
clamp_scroll();
|
||||
} else if (handle_reader_option_click(mx, my)) {
|
||||
g_search_focused = false;
|
||||
// Reader controls handled.
|
||||
} else {
|
||||
g_search_focused = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -542,6 +542,7 @@ void wp_init_empty_document(WordProcessorState* wp) {
|
||||
wp->line_spacing_dropdown_open = false;
|
||||
wp->divider_flyout_open = false;
|
||||
wp->special_char_flyout_open = false;
|
||||
mtk::context_menu_reset(wp->context_menu);
|
||||
wp->page_view_mode = false;
|
||||
wp->pending_page_view_toggle = false;
|
||||
|
||||
|
||||
@@ -65,6 +65,59 @@ static bool wp_paste_from_clipboard(WordProcessorState* wp) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool wp_context_item_enabled(WordProcessorState* wp, int item) {
|
||||
switch (item) {
|
||||
case WP_CTX_CUT:
|
||||
case WP_CTX_COPY:
|
||||
case WP_CTX_DELETE:
|
||||
return wp && wp->has_selection;
|
||||
case WP_CTX_PASTE:
|
||||
return gui::clipboard_has_text();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void wp_build_context_items(WordProcessorState* wp,
|
||||
mtk::ContextMenuItem items[WP_CTX_COUNT]) {
|
||||
items[WP_CTX_CUT] = {"Cut", WP_CTX_CUT,
|
||||
wp_context_item_enabled(wp, WP_CTX_CUT)};
|
||||
items[WP_CTX_COPY] = {"Copy", WP_CTX_COPY,
|
||||
wp_context_item_enabled(wp, WP_CTX_COPY)};
|
||||
items[WP_CTX_PASTE] = {"Paste", WP_CTX_PASTE,
|
||||
wp_context_item_enabled(wp, WP_CTX_PASTE)};
|
||||
items[WP_CTX_DELETE] = {"Delete", WP_CTX_DELETE,
|
||||
wp_context_item_enabled(wp, WP_CTX_DELETE)};
|
||||
}
|
||||
|
||||
static bool wp_selection_contains_abs(WordProcessorState* wp, int abs) {
|
||||
if (!wp || !wp->has_selection) return false;
|
||||
int sel_s = 0;
|
||||
int sel_e = 0;
|
||||
wp_sel_range(wp, &sel_s, &sel_e);
|
||||
return abs >= sel_s && abs < sel_e;
|
||||
}
|
||||
|
||||
static bool wp_perform_context_action(WordProcessorState* wp, int item) {
|
||||
if (!wp_context_item_enabled(wp, item)) return false;
|
||||
|
||||
switch (item) {
|
||||
case WP_CTX_CUT:
|
||||
return wp_cut_selection_to_clipboard(wp);
|
||||
case WP_CTX_COPY:
|
||||
return wp_copy_selection_to_clipboard(wp);
|
||||
case WP_CTX_PASTE:
|
||||
return wp_paste_from_clipboard(wp);
|
||||
case WP_CTX_DELETE:
|
||||
wp_delete_selection(wp);
|
||||
wp_history_checkpoint(wp);
|
||||
wp_set_status(wp, "Deleted selection");
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static void wp_open_pathbar_for_open(WordProcessorState* wp) {
|
||||
char path[256] = {};
|
||||
char msg[160] = {};
|
||||
@@ -255,6 +308,26 @@ void wp_handle_mouse(const Montauk::WinEvent& ev) {
|
||||
recompute_w = WP_A4_PAGE_W;
|
||||
}
|
||||
|
||||
if (wp->context_menu.open) {
|
||||
if (ev.mouse.scroll != 0) {
|
||||
mtk::context_menu_close(wp->context_menu);
|
||||
return;
|
||||
}
|
||||
|
||||
mtk::ContextMenuItem items[WP_CTX_COUNT];
|
||||
wp_build_context_items(wp, items);
|
||||
int action = mtk::context_menu_handle_mouse(wp->context_menu,
|
||||
items, WP_CTX_COUNT,
|
||||
local_x, local_y,
|
||||
ev.mouse.buttons,
|
||||
ev.mouse.prev_buttons,
|
||||
g_win_w, g_win_h);
|
||||
if (action >= 0)
|
||||
wp_perform_context_action(wp, action);
|
||||
if (action != mtk::CONTEXT_MENU_NONE)
|
||||
return;
|
||||
}
|
||||
|
||||
wp->scrollbar.handle_mouse(local_x, local_y, ev.mouse.buttons, ev.mouse.prev_buttons, ev.mouse.scroll);
|
||||
|
||||
if (wp->font_dropdown_open && wp_left_pressed(ev.mouse.buttons, ev.mouse.prev_buttons)) {
|
||||
@@ -464,6 +537,27 @@ void wp_handle_mouse(const Montauk::WinEvent& ev) {
|
||||
|
||||
wp_recompute_wrap(wp, recompute_w, WP_SCREEN_DPI);
|
||||
|
||||
bool right_pressed = wp_right_pressed(ev.mouse.buttons, ev.mouse.prev_buttons);
|
||||
bool in_text_area = local_y >= edit_y && local_y < edit_y + text_area_h &&
|
||||
local_x >= 0 && local_x < g_win_w - WP_SCROLLBAR_W;
|
||||
if (in_text_area && wp->page_view_mode) {
|
||||
in_text_area = local_x >= page_off_x && local_x < page_off_x + WP_A4_PAGE_W &&
|
||||
local_y >= page_off_y && local_y < page_off_y + WP_A4_PAGE_H;
|
||||
}
|
||||
|
||||
if (right_pressed && in_text_area) {
|
||||
wp_close_dropdowns(wp);
|
||||
|
||||
int abs = wp_hit_test_text(wp, local_x, local_y, edit_y, page_off_x, page_off_y);
|
||||
if (!wp_selection_contains_abs(wp, abs)) {
|
||||
wp_pos_to_run(wp, abs, &wp->cursor_run, &wp->cursor_offset);
|
||||
wp_clear_selection(wp);
|
||||
}
|
||||
wp->mouse_selecting = false;
|
||||
mtk::context_menu_open(wp->context_menu, local_x, local_y);
|
||||
return;
|
||||
}
|
||||
|
||||
if (wp_left_pressed(ev.mouse.buttons, ev.mouse.prev_buttons) &&
|
||||
local_y >= edit_y && local_y < edit_y + text_area_h) {
|
||||
wp_close_dropdowns(wp);
|
||||
@@ -502,6 +596,11 @@ void wp_handle_key(const Montauk::KeyEvent& key) {
|
||||
WordProcessorState* wp = &g_wp;
|
||||
if (!key.pressed) return;
|
||||
|
||||
if (wp->context_menu.open) {
|
||||
mtk::context_menu_close(wp->context_menu);
|
||||
if (key.scancode == 0x01) return;
|
||||
}
|
||||
|
||||
if (wp->show_pathbar) {
|
||||
if (key.ascii == '\n' || key.ascii == '\r') {
|
||||
wp_commit_pathbar(wp);
|
||||
|
||||
@@ -35,6 +35,21 @@ static void wp_draw_ui_icon_button(Canvas& c, int x, int y, int w, int h,
|
||||
wp_draw_ui_button(c, x, y, w, h, fallback, bg, fg, radius);
|
||||
}
|
||||
|
||||
static void wp_draw_context_menu(Canvas& c, WordProcessorState* wp) {
|
||||
if (!wp || !wp->context_menu.open) return;
|
||||
|
||||
mtk::Theme theme = mtk::make_theme(colors::ACCENT);
|
||||
theme.border = colors::BORDER;
|
||||
theme.text = colors::TEXT_COLOR;
|
||||
|
||||
mtk::ContextMenuItem items[WP_CTX_COUNT];
|
||||
wp_build_context_items(wp, items);
|
||||
mtk::draw_context_menu(c, wp->context_menu, items, WP_CTX_COUNT, theme,
|
||||
mtk::CONTEXT_MENU_DEFAULT_W,
|
||||
mtk::CONTEXT_MENU_ITEM_H,
|
||||
g_ui_font, fonts::UI_SIZE);
|
||||
}
|
||||
|
||||
static int wp_special_char_flyout_x() {
|
||||
int dx = WP_BTN_SECTION_X + 24 - WP_SPECIAL_CHAR_FLYOUT_W;
|
||||
if (dx + WP_SPECIAL_CHAR_FLYOUT_W > g_win_w) dx = g_win_w - WP_SPECIAL_CHAR_FLYOUT_W;
|
||||
@@ -666,4 +681,6 @@ void wp_render() {
|
||||
WP_SPECIAL_CHAR_OPTIONS[i].label, colors::TEXT_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
wp_draw_context_menu(c, wp);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <gui/standalone.hpp>
|
||||
#include <gui/truetype.hpp>
|
||||
#include <gui/svg.hpp>
|
||||
#include <gui/mtk/widgets.hpp>
|
||||
|
||||
extern "C" {
|
||||
#include <string.h>
|
||||
@@ -239,6 +240,18 @@ inline bool wp_left_released(uint8_t buttons, uint8_t prev_buttons) {
|
||||
return !(buttons & 0x01) && (prev_buttons & 0x01);
|
||||
}
|
||||
|
||||
inline bool wp_right_pressed(uint8_t buttons, uint8_t prev_buttons) {
|
||||
return (buttons & 0x02) && !(prev_buttons & 0x02);
|
||||
}
|
||||
|
||||
enum WpContextItem : int {
|
||||
WP_CTX_CUT = 0,
|
||||
WP_CTX_COPY,
|
||||
WP_CTX_PASTE,
|
||||
WP_CTX_DELETE,
|
||||
WP_CTX_COUNT,
|
||||
};
|
||||
|
||||
struct WpScrollbar {
|
||||
Rect bounds;
|
||||
int content_height;
|
||||
@@ -370,6 +383,7 @@ struct WordProcessorState {
|
||||
bool line_spacing_dropdown_open;
|
||||
bool divider_flyout_open;
|
||||
bool special_char_flyout_open;
|
||||
mtk::ContextMenuState context_menu;
|
||||
bool page_view_mode;
|
||||
bool pending_page_view_toggle;
|
||||
|
||||
@@ -462,6 +476,9 @@ bool wp_redo(WordProcessorState* wp);
|
||||
|
||||
bool wp_print_document(WordProcessorState* wp, char* out_status, int out_status_len);
|
||||
bool wp_export_pdf_document(WordProcessorState* wp, const char* path, char* out_status, int out_status_len);
|
||||
bool wp_context_item_enabled(WordProcessorState* wp, int item);
|
||||
void wp_build_context_items(WordProcessorState* wp,
|
||||
mtk::ContextMenuItem items[WP_CTX_COUNT]);
|
||||
|
||||
void wp_render();
|
||||
void wp_handle_mouse(const Montauk::WinEvent& ev);
|
||||
|
||||
Reference in New Issue
Block a user