Checkboxes and radio buttons in menus.
This commit is contained in:
parent
6f8aeda7b2
commit
7476367ace
6 changed files with 188 additions and 14 deletions
102
dvx/dvxApp.c
102
dvx/dvxApp.c
|
|
@ -15,6 +15,7 @@
|
||||||
#define DBLCLICK_THRESHOLD (CLOCKS_PER_SEC / 2)
|
#define DBLCLICK_THRESHOLD (CLOCKS_PER_SEC / 2)
|
||||||
#define ICON_REFRESH_INTERVAL 8
|
#define ICON_REFRESH_INTERVAL 8
|
||||||
#define KB_MOVE_STEP 8
|
#define KB_MOVE_STEP 8
|
||||||
|
#define MENU_CHECK_WIDTH 14
|
||||||
#define SUBMENU_ARROW_WIDTH 12
|
#define SUBMENU_ARROW_WIDTH 12
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -22,6 +23,7 @@
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw, int32_t *ph);
|
static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw, int32_t *ph);
|
||||||
|
static void clickMenuCheckRadio(MenuT *menu, int32_t itemIdx);
|
||||||
static void closeAllPopups(AppContextT *ctx);
|
static void closeAllPopups(AppContextT *ctx);
|
||||||
static void closePopupLevel(AppContextT *ctx);
|
static void closePopupLevel(AppContextT *ctx);
|
||||||
static void closeSysMenu(AppContextT *ctx);
|
static void closeSysMenu(AppContextT *ctx);
|
||||||
|
|
@ -74,6 +76,7 @@ static const char sAltScanToAscii[256] = {
|
||||||
static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw, int32_t *ph) {
|
static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw, int32_t *ph) {
|
||||||
int32_t maxW = 0;
|
int32_t maxW = 0;
|
||||||
bool hasSub = false;
|
bool hasSub = false;
|
||||||
|
bool hasCheck = false;
|
||||||
|
|
||||||
for (int32_t k = 0; k < menu->itemCount; k++) {
|
for (int32_t k = 0; k < menu->itemCount; k++) {
|
||||||
int32_t itemW = textWidthAccel(&ctx->font, menu->items[k].label);
|
int32_t itemW = textWidthAccel(&ctx->font, menu->items[k].label);
|
||||||
|
|
@ -85,13 +88,50 @@ static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw
|
||||||
if (menu->items[k].subMenu) {
|
if (menu->items[k].subMenu) {
|
||||||
hasSub = true;
|
hasSub = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (menu->items[k].type == MenuItemCheckE || menu->items[k].type == MenuItemRadioE) {
|
||||||
|
hasCheck = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
*pw = maxW + CHROME_TITLE_PAD * 2 + 8 + (hasSub ? SUBMENU_ARROW_WIDTH : 0);
|
*pw = maxW + CHROME_TITLE_PAD * 2 + 8 + (hasSub ? SUBMENU_ARROW_WIDTH : 0) + (hasCheck ? MENU_CHECK_WIDTH : 0);
|
||||||
*ph = menu->itemCount * ctx->font.charHeight + 4;
|
*ph = menu->itemCount * ctx->font.charHeight + 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// clickMenuCheckRadio — toggle check or select radio on click
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void clickMenuCheckRadio(MenuT *menu, int32_t itemIdx) {
|
||||||
|
MenuItemT *item = &menu->items[itemIdx];
|
||||||
|
|
||||||
|
if (item->type == MenuItemCheckE) {
|
||||||
|
item->checked = !item->checked;
|
||||||
|
} else if (item->type == MenuItemRadioE) {
|
||||||
|
// Uncheck all radio items in the same group (consecutive radio items)
|
||||||
|
// Search backward to find group start
|
||||||
|
int32_t groupStart = itemIdx;
|
||||||
|
|
||||||
|
while (groupStart > 0 && menu->items[groupStart - 1].type == MenuItemRadioE) {
|
||||||
|
groupStart--;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search forward to find group end
|
||||||
|
int32_t groupEnd = itemIdx;
|
||||||
|
|
||||||
|
while (groupEnd < menu->itemCount - 1 && menu->items[groupEnd + 1].type == MenuItemRadioE) {
|
||||||
|
groupEnd++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uncheck all in group, check the clicked one
|
||||||
|
for (int32_t i = groupStart; i <= groupEnd; i++) {
|
||||||
|
menu->items[i].checked = (i == itemIdx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// closeAllPopups — dirty all popup levels and deactivate
|
// closeAllPopups — dirty all popup levels and deactivate
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -587,6 +627,11 @@ static void dispatchEvents(AppContextT *ctx) {
|
||||||
// Clicking a submenu item opens it (already open from hover, but ensure)
|
// Clicking a submenu item opens it (already open from hover, but ensure)
|
||||||
openSubMenu(ctx);
|
openSubMenu(ctx);
|
||||||
} else if (item->enabled && !item->separator) {
|
} else if (item->enabled && !item->separator) {
|
||||||
|
// Toggle check/radio state before closing
|
||||||
|
if (item->type == MenuItemCheckE || item->type == MenuItemRadioE) {
|
||||||
|
clickMenuCheckRadio(ctx->popup.menu, itemIdx);
|
||||||
|
}
|
||||||
|
|
||||||
// Close popup BEFORE calling onMenu (onMenu may run a nested event loop)
|
// Close popup BEFORE calling onMenu (onMenu may run a nested event loop)
|
||||||
int32_t menuId = item->id;
|
int32_t menuId = item->id;
|
||||||
WindowT *win = findWindowById(ctx, ctx->popup.windowId);
|
WindowT *win = findWindowById(ctx, ctx->popup.windowId);
|
||||||
|
|
@ -738,6 +783,18 @@ static void drawPopupLevel(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, c
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect if menu has check/radio items (for left margin)
|
||||||
|
bool hasCheck = false;
|
||||||
|
|
||||||
|
for (int32_t k = 0; k < menu->itemCount; k++) {
|
||||||
|
if (menu->items[k].type == MenuItemCheckE || menu->items[k].type == MenuItemRadioE) {
|
||||||
|
hasCheck = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t checkMargin = hasCheck ? MENU_CHECK_WIDTH : 0;
|
||||||
|
|
||||||
// Draw popup background
|
// Draw popup background
|
||||||
BevelStyleT popBevel;
|
BevelStyleT popBevel;
|
||||||
popBevel.highlight = ctx->colors.windowHighlight;
|
popBevel.highlight = ctx->colors.windowHighlight;
|
||||||
|
|
@ -767,7 +824,30 @@ static void drawPopupLevel(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, c
|
||||||
}
|
}
|
||||||
|
|
||||||
rectFill(d, ops, px + 2, itemY, pw - 4, ctx->font.charHeight, bg);
|
rectFill(d, ops, px + 2, itemY, pw - 4, ctx->font.charHeight, bg);
|
||||||
drawTextAccel(d, ops, &ctx->font, px + CHROME_TITLE_PAD + 2, itemY, item->label, fg, bg, true);
|
drawTextAccel(d, ops, &ctx->font, px + CHROME_TITLE_PAD + 2 + checkMargin, itemY, item->label, fg, bg, true);
|
||||||
|
|
||||||
|
// Draw check/radio indicator
|
||||||
|
if (item->checked) {
|
||||||
|
int32_t cy = itemY + ctx->font.charHeight / 2;
|
||||||
|
int32_t cx = px + 2 + MENU_CHECK_WIDTH / 2;
|
||||||
|
|
||||||
|
if (item->type == MenuItemCheckE) {
|
||||||
|
// Checkmark: small tick shape
|
||||||
|
drawVLine(d, ops, cx - 2, cy - 1, 2, fg);
|
||||||
|
drawVLine(d, ops, cx - 1, cy, 2, fg);
|
||||||
|
drawVLine(d, ops, cx, cy + 1, 2, fg);
|
||||||
|
drawVLine(d, ops, cx + 1, cy, 2, fg);
|
||||||
|
drawVLine(d, ops, cx + 2, cy - 1, 2, fg);
|
||||||
|
drawVLine(d, ops, cx + 3, cy - 2, 2, fg);
|
||||||
|
} else if (item->type == MenuItemRadioE) {
|
||||||
|
// Filled circle bullet (5x5)
|
||||||
|
drawHLine(d, ops, cx - 1, cy - 2, 3, fg);
|
||||||
|
drawHLine(d, ops, cx - 2, cy - 1, 5, fg);
|
||||||
|
drawHLine(d, ops, cx - 2, cy, 5, fg);
|
||||||
|
drawHLine(d, ops, cx - 2, cy + 1, 5, fg);
|
||||||
|
drawHLine(d, ops, cx - 1, cy + 2, 3, fg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Draw submenu arrow indicator
|
// Draw submenu arrow indicator
|
||||||
if (item->subMenu) {
|
if (item->subMenu) {
|
||||||
|
|
@ -971,6 +1051,24 @@ void dvxInvalidateWindow(AppContextT *ctx, WindowT *win) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// dvxMaximizeWindow
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void dvxMaximizeWindow(AppContextT *ctx, WindowT *win) {
|
||||||
|
wmMaximize(&ctx->stack, &ctx->dirty, &ctx->display, win);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// dvxMinimizeWindow
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void dvxMinimizeWindow(AppContextT *ctx, WindowT *win) {
|
||||||
|
wmMinimize(&ctx->stack, &ctx->dirty, win);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// dvxQuit
|
// dvxQuit
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,12 @@ void dvxInvalidateRect(AppContextT *ctx, WindowT *win, int32_t x, int32_t y, int
|
||||||
// Invalidate entire window content
|
// Invalidate entire window content
|
||||||
void dvxInvalidateWindow(AppContextT *ctx, WindowT *win);
|
void dvxInvalidateWindow(AppContextT *ctx, WindowT *win);
|
||||||
|
|
||||||
|
// Minimize a window (show as icon at bottom of screen)
|
||||||
|
void dvxMinimizeWindow(AppContextT *ctx, WindowT *win);
|
||||||
|
|
||||||
|
// Maximize a window (expand to fill screen or maxW/maxH)
|
||||||
|
void dvxMaximizeWindow(AppContextT *ctx, WindowT *win);
|
||||||
|
|
||||||
// Request exit from main loop
|
// Request exit from main loop
|
||||||
void dvxQuit(AppContextT *ctx);
|
void dvxQuit(AppContextT *ctx);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -155,14 +155,21 @@ typedef struct {
|
||||||
// Forward declaration for submenu pointers
|
// Forward declaration for submenu pointers
|
||||||
typedef struct MenuT MenuT;
|
typedef struct MenuT MenuT;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
MenuItemNormalE,
|
||||||
|
MenuItemCheckE,
|
||||||
|
MenuItemRadioE,
|
||||||
|
} MenuItemTypeE;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char label[MAX_MENU_LABEL];
|
char label[MAX_MENU_LABEL];
|
||||||
int32_t id; // application-defined command ID
|
int32_t id; // application-defined command ID
|
||||||
bool separator; // true = this is a separator line, not a clickable item
|
MenuItemTypeE type; // normal, checkbox, or radio
|
||||||
bool enabled;
|
bool separator; // true = this is a separator line, not a clickable item
|
||||||
bool checked;
|
bool enabled;
|
||||||
char accelKey; // lowercase accelerator character, 0 if none
|
bool checked;
|
||||||
MenuT *subMenu; // child menu for cascading submenus (NULL if leaf item)
|
char accelKey; // lowercase accelerator character, 0 if none
|
||||||
|
MenuT *subMenu; // child menu for cascading submenus (NULL if leaf item)
|
||||||
} MenuItemT;
|
} MenuItemT;
|
||||||
|
|
||||||
struct MenuT {
|
struct MenuT {
|
||||||
|
|
|
||||||
44
dvx/dvxWm.c
44
dvx/dvxWm.c
|
|
@ -645,6 +645,50 @@ void wmAddMenuItem(MenuT *menu, const char *label, int32_t id) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// wmAddMenuCheckItem
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void wmAddMenuCheckItem(MenuT *menu, const char *label, int32_t id, bool checked) {
|
||||||
|
if (menu->itemCount >= MAX_MENU_ITEMS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuItemT *item = &menu->items[menu->itemCount];
|
||||||
|
memset(item, 0, sizeof(*item));
|
||||||
|
strncpy(item->label, label, MAX_MENU_LABEL - 1);
|
||||||
|
item->label[MAX_MENU_LABEL - 1] = '\0';
|
||||||
|
item->id = id;
|
||||||
|
item->type = MenuItemCheckE;
|
||||||
|
item->enabled = true;
|
||||||
|
item->checked = checked;
|
||||||
|
item->accelKey = accelParse(label);
|
||||||
|
menu->itemCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// wmAddMenuRadioItem
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void wmAddMenuRadioItem(MenuT *menu, const char *label, int32_t id, bool checked) {
|
||||||
|
if (menu->itemCount >= MAX_MENU_ITEMS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuItemT *item = &menu->items[menu->itemCount];
|
||||||
|
memset(item, 0, sizeof(*item));
|
||||||
|
strncpy(item->label, label, MAX_MENU_LABEL - 1);
|
||||||
|
item->label[MAX_MENU_LABEL - 1] = '\0';
|
||||||
|
item->id = id;
|
||||||
|
item->type = MenuItemRadioE;
|
||||||
|
item->enabled = true;
|
||||||
|
item->checked = checked;
|
||||||
|
item->accelKey = accelParse(label);
|
||||||
|
menu->itemCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// wmAddMenuSeparator
|
// wmAddMenuSeparator
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,12 @@ MenuT *wmAddMenu(MenuBarT *bar, const char *label);
|
||||||
// Add an item to a menu
|
// Add an item to a menu
|
||||||
void wmAddMenuItem(MenuT *menu, const char *label, int32_t id);
|
void wmAddMenuItem(MenuT *menu, const char *label, int32_t id);
|
||||||
|
|
||||||
|
// Add a checkbox item to a menu
|
||||||
|
void wmAddMenuCheckItem(MenuT *menu, const char *label, int32_t id, bool checked);
|
||||||
|
|
||||||
|
// Add a radio item to a menu (radio group = consecutive radio items)
|
||||||
|
void wmAddMenuRadioItem(MenuT *menu, const char *label, int32_t id, bool checked);
|
||||||
|
|
||||||
// Add a separator to a menu
|
// Add a separator to a menu
|
||||||
void wmAddMenuSeparator(MenuT *menu);
|
void wmAddMenuSeparator(MenuT *menu);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,16 @@
|
||||||
#define CMD_EDIT_CUT 200
|
#define CMD_EDIT_CUT 200
|
||||||
#define CMD_EDIT_COPY 201
|
#define CMD_EDIT_COPY 201
|
||||||
#define CMD_EDIT_PASTE 202
|
#define CMD_EDIT_PASTE 202
|
||||||
#define CMD_VIEW_TERM 300
|
#define CMD_VIEW_TERM 300
|
||||||
#define CMD_VIEW_CTRL 301
|
#define CMD_VIEW_CTRL 301
|
||||||
#define CMD_VIEW_ZOOM_IN 302
|
#define CMD_VIEW_ZOOM_IN 302
|
||||||
#define CMD_VIEW_ZOOM_OUT 303
|
#define CMD_VIEW_ZOOM_OUT 303
|
||||||
#define CMD_VIEW_ZOOM_FIT 304
|
#define CMD_VIEW_ZOOM_FIT 304
|
||||||
|
#define CMD_VIEW_TOOLBAR 305
|
||||||
|
#define CMD_VIEW_STATUSBAR 306
|
||||||
|
#define CMD_VIEW_SIZE_SMALL 307
|
||||||
|
#define CMD_VIEW_SIZE_MED 308
|
||||||
|
#define CMD_VIEW_SIZE_LARGE 309
|
||||||
#define CMD_HELP_ABOUT 400
|
#define CMD_HELP_ABOUT 400
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -594,6 +599,13 @@ static void setupMainWindow(AppContextT *ctx) {
|
||||||
wmAddMenuItem(viewMenu, "&Terminal", CMD_VIEW_TERM);
|
wmAddMenuItem(viewMenu, "&Terminal", CMD_VIEW_TERM);
|
||||||
wmAddMenuItem(viewMenu, "&Controls", CMD_VIEW_CTRL);
|
wmAddMenuItem(viewMenu, "&Controls", CMD_VIEW_CTRL);
|
||||||
wmAddMenuSeparator(viewMenu);
|
wmAddMenuSeparator(viewMenu);
|
||||||
|
wmAddMenuCheckItem(viewMenu, "Tool&bar", CMD_VIEW_TOOLBAR, true);
|
||||||
|
wmAddMenuCheckItem(viewMenu, "&Status Bar", CMD_VIEW_STATUSBAR, true);
|
||||||
|
wmAddMenuSeparator(viewMenu);
|
||||||
|
wmAddMenuRadioItem(viewMenu, "S&mall", CMD_VIEW_SIZE_SMALL, false);
|
||||||
|
wmAddMenuRadioItem(viewMenu, "Me&dium", CMD_VIEW_SIZE_MED, true);
|
||||||
|
wmAddMenuRadioItem(viewMenu, "&Large", CMD_VIEW_SIZE_LARGE, false);
|
||||||
|
wmAddMenuSeparator(viewMenu);
|
||||||
|
|
||||||
MenuT *zoomMenu = wmAddSubMenu(viewMenu, "&Zoom");
|
MenuT *zoomMenu = wmAddSubMenu(viewMenu, "&Zoom");
|
||||||
|
|
||||||
|
|
@ -727,6 +739,7 @@ static void setupTerminalWindow(AppContextT *ctx) {
|
||||||
|
|
||||||
dvxFitWindow(ctx, win);
|
dvxFitWindow(ctx, win);
|
||||||
wgtInvalidate(root);
|
wgtInvalidate(root);
|
||||||
|
dvxMinimizeWindow(ctx, win);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue