Menu editor. Focus logic cleanup. Several bugs fixed.

This commit is contained in:
Scott Duensing 2026-04-03 21:22:08 -05:00
parent e36d4b9cec
commit 7bc92549f7
31 changed files with 1264 additions and 122 deletions

View file

@ -31,7 +31,7 @@ COMP_OBJS = $(OBJDIR)/lexer.o $(OBJDIR)/parser.o $(OBJDIR)/codegen.o $(OBJDIR)/s
FORMRT_OBJS = $(OBJDIR)/formrt.o
# IDE app objects
IDE_OBJS = $(OBJDIR)/ideMain.o $(OBJDIR)/ideDesigner.o $(OBJDIR)/ideProject.o $(OBJDIR)/ideToolbox.o $(OBJDIR)/ideProperties.o
IDE_OBJS = $(OBJDIR)/ideMain.o $(OBJDIR)/ideDesigner.o $(OBJDIR)/ideMenuEditor.o $(OBJDIR)/ideProject.o $(OBJDIR)/ideToolbox.o $(OBJDIR)/ideProperties.o
APP_OBJS = $(IDE_OBJS) $(FORMRT_OBJS)
APP_TARGET = $(APPDIR)/dvxbasic.app
@ -97,7 +97,10 @@ $(OBJDIR)/formrt.o: formrt/formrt.c formrt/formrt.h compiler/codegen.h runtime/v
$(OBJDIR)/ideDesigner.o: ide/ideDesigner.c ide/ideDesigner.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/ideMain.o: ide/ideMain.c ide/ideDesigner.h ide/ideProject.h ide/ideToolbox.h ide/ideProperties.h compiler/parser.h runtime/vm.h | $(OBJDIR)
$(OBJDIR)/ideMenuEditor.o: ide/ideMenuEditor.c ide/ideMenuEditor.h ide/ideDesigner.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/ideMain.o: ide/ideMain.c ide/ideDesigner.h ide/ideMenuEditor.h ide/ideProject.h ide/ideToolbox.h ide/ideProperties.h compiler/parser.h runtime/vm.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/ideProject.o: ide/ideProject.c ide/ideProject.h | $(OBJDIR)

View file

@ -59,6 +59,7 @@ static BasFormRtT *sFormRt = NULL;
// ============================================================
static BasStringT *basFormRtInputBox(void *ctx, const char *prompt, const char *title, const char *defaultText);
static void onFormMenu(WindowT *win, int32_t menuId);
static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc);
static BasValueT callListBoxMethod(BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc);
static WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent);
@ -321,6 +322,7 @@ void basFormRtDestroy(BasFormRtT *rt) {
}
arrfree(form->controls);
arrfree(form->menuIdMap);
if (form->window) {
dvxDestroyWindow(rt->ctx, form->window);
@ -684,6 +686,20 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
bool isContainer[MAX_FRM_NESTING];
int32_t blockDepth = 0;
// Temporary menu item accumulation
typedef struct {
char caption[256];
char name[BAS_MAX_CTRL_NAME];
int32_t level;
bool checked;
bool enabled;
} TempMenuItemT;
TempMenuItemT *menuItems = NULL; // stb_ds array
TempMenuItemT *curMenuItem = NULL;
int32_t menuNestDepth = 0;
bool inMenu = false;
const char *pos = source;
const char *end = source + sourceLen;
@ -779,6 +795,23 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
if (blockDepth < MAX_FRM_NESTING) {
isContainer[blockDepth++] = true;
}
} else if (strcasecmp(typeName, "Menu") == 0 && form) {
TempMenuItemT mi;
memset(&mi, 0, sizeof(mi));
snprintf(mi.name, BAS_MAX_CTRL_NAME, "%s", ctrlName);
mi.level = menuNestDepth;
mi.enabled = true;
arrput(menuItems, mi);
curMenuItem = &menuItems[arrlen(menuItems) - 1];
current = NULL;
menuNestDepth++;
inMenu = true;
if (blockDepth < MAX_FRM_NESTING) {
isContainer[blockDepth++] = false;
}
continue;
} else if (form && nestDepth > 0) {
// Create the content box on first control if not yet done
if (!form->contentBox && form->root) {
@ -858,6 +891,22 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
// "End"
if (strcasecmp(trimmed, "End") == 0) {
if (inMenu) {
menuNestDepth--;
curMenuItem = NULL;
if (menuNestDepth <= 0) {
menuNestDepth = 0;
inMenu = false;
}
if (blockDepth > 0) {
blockDepth--;
}
continue;
}
if (blockDepth > 0) {
blockDepth--;
@ -880,7 +929,23 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
continue;
}
if (current) {
if (curMenuItem) {
// Menu item properties
char *text = value;
if (text[0] == '"') {
text++;
int32_t len = (int32_t)strlen(text);
if (len > 0 && text[len - 1] == '"') {
text[len - 1] = '\0';
}
}
if (strcasecmp(key, "Caption") == 0) { snprintf(curMenuItem->caption, sizeof(curMenuItem->caption), "%s", text); }
else if (strcasecmp(key, "Checked") == 0) { curMenuItem->checked = (strcasecmp(text, "True") == 0 || strcasecmp(text, "-1") == 0); }
else if (strcasecmp(key, "Enabled") == 0) { curMenuItem->enabled = (strcasecmp(text, "True") == 0 || strcasecmp(text, "-1") == 0 || strcasecmp(text, "False") != 0); }
} else if (current) {
// Control array index is stored on the struct, not as a widget property
if (strcasecmp(key, "Index") == 0) {
current->index = atoi(value);
@ -954,6 +1019,62 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
form->contentBox->weight = 100;
}
// Build menu bar from accumulated menu items
int32_t menuCount = (int32_t)arrlen(menuItems);
if (menuCount > 0 && form->window) {
MenuBarT *bar = wmAddMenuBar(form->window);
if (bar) {
#define MENU_ID_BASE 10000
MenuT *menuStack[16];
memset(menuStack, 0, sizeof(menuStack));
for (int32_t i = 0; i < menuCount; i++) {
TempMenuItemT *mi = &menuItems[i];
bool isSep = (mi->caption[0] == '-' && (mi->caption[1] == '\0' || mi->caption[1] == '-'));
bool isSubParent = (i + 1 < menuCount && menuItems[i + 1].level > mi->level);
if (mi->level == 0) {
// Top-level menu header
menuStack[0] = wmAddMenu(bar, mi->caption);
} else if (isSep && mi->level > 0 && menuStack[mi->level - 1]) {
// Separator
wmAddMenuSeparator(menuStack[mi->level - 1]);
} else if (isSubParent && mi->level > 0 && menuStack[mi->level - 1]) {
// Submenu parent
menuStack[mi->level] = wmAddSubMenu(menuStack[mi->level - 1], mi->caption);
} else if (mi->level > 0 && menuStack[mi->level - 1]) {
// Regular menu item
int32_t id = MENU_ID_BASE + i;
if (mi->checked) {
wmAddMenuCheckItem(menuStack[mi->level - 1], mi->caption, id, true);
} else {
wmAddMenuItem(menuStack[mi->level - 1], mi->caption, id);
}
if (!mi->enabled) {
wmMenuItemSetEnabled(bar, id, false);
}
// Store ID-to-name mapping for event dispatch
BasMenuIdMapT map;
memset(&map, 0, sizeof(map));
map.id = id;
snprintf(map.name, BAS_MAX_CTRL_NAME, "%s", mi->name);
arrput(form->menuIdMap, map);
form->menuIdMapCount = (int32_t)arrlen(form->menuIdMap);
}
}
form->window->onMenu = onFormMenu;
}
}
arrfree(menuItems);
menuItems = NULL;
// Set resizable flag
if (form->frmHasResizable) {
form->window->resizable = form->frmResizable;
@ -1484,6 +1605,34 @@ static ListBoxItemsT *getListBoxItems(BasControlT *ctrl) {
}
// ============================================================
// onFormClose
// ============================================================
// onFormMenu -- dispatch menu clicks as ControlName_Click events
// ============================================================
static void onFormMenu(WindowT *win, int32_t menuId) {
if (!sFormRt) {
return;
}
for (int32_t i = 0; i < sFormRt->formCount; i++) {
BasFormT *form = &sFormRt->forms[i];
if (form->window == win) {
for (int32_t j = 0; j < form->menuIdMapCount; j++) {
if (form->menuIdMap[j].id == menuId) {
basFormRtFireEvent(sFormRt, form, form->menuIdMap[j].name, "Click");
return;
}
}
return;
}
}
}
// ============================================================
// onFormClose
// ============================================================

View file

@ -28,6 +28,15 @@ typedef struct BasControlT BasControlT;
#define BAS_MAX_CTRL_NAME 32
// ============================================================
// Menu ID to name mapping for event dispatch
// ============================================================
typedef struct {
int32_t id;
char name[BAS_MAX_CTRL_NAME];
} BasMenuIdMapT;
// ============================================================
// Control instance (a widget on a form)
// ============================================================
@ -71,6 +80,9 @@ typedef struct BasFormT {
// Per-form variable storage (allocated at load, freed at unload)
BasValueT *formVars;
int32_t formVarCount;
// Menu ID to name mapping (for event dispatch)
BasMenuIdMapT *menuIdMap;
int32_t menuIdMapCount;
} BasFormT;
// ============================================================

View file

@ -7,6 +7,7 @@
#include "ideDesigner.h"
#include "dvxDraw.h"
#include "dvxVideo.h"
#include "dvxWm.h"
#include "stb_ds_wrap.h"
#include <ctype.h>
@ -114,6 +115,48 @@ WidgetT *dsgnCreateDesignWidget(const char *vbTypeName, WidgetT *parent) {
}
// ============================================================
// dsgnBuildPreviewMenuBar
// ============================================================
void dsgnBuildPreviewMenuBar(WindowT *win, const DsgnFormT *form) {
if (!win || !form) {
return;
}
int32_t menuCount = (int32_t)arrlen(form->menuItems);
if (menuCount <= 0) {
return;
}
MenuBarT *bar = wmAddMenuBar(win);
if (!bar) {
return;
}
MenuT *menuStack[8];
memset(menuStack, 0, sizeof(menuStack));
for (int32_t i = 0; i < menuCount; i++) {
const DsgnMenuItemT *mi = &form->menuItems[i];
bool isSep = (mi->caption[0] == '-');
bool isSubParent = (i + 1 < menuCount && form->menuItems[i + 1].level > mi->level);
if (mi->level == 0) {
menuStack[0] = wmAddMenu(bar, mi->caption);
} else if (isSep && mi->level > 0 && mi->level < 8 && menuStack[mi->level - 1]) {
wmAddMenuSeparator(menuStack[mi->level - 1]);
} else if (isSubParent && mi->level > 0 && mi->level < 8 && menuStack[mi->level - 1]) {
menuStack[mi->level] = wmAddSubMenu(menuStack[mi->level - 1], mi->caption);
} else if (mi->level > 0 && mi->level < 8 && menuStack[mi->level - 1]) {
wmAddMenuItem(menuStack[mi->level - 1], mi->caption, -1);
}
}
}
// ============================================================
// dsgnAutoName
// ============================================================
@ -308,6 +351,7 @@ const char *dsgnDefaultEvent(const char *typeName) {
void dsgnFree(DsgnStateT *ds) {
if (ds->form) {
arrfree(ds->form->controls);
arrfree(ds->form->menuItems);
free(ds->form->code);
free(ds->form);
ds->form = NULL;
@ -358,8 +402,11 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
snprintf(form->name, DSGN_MAX_NAME, "Form1");
snprintf(form->caption, DSGN_MAX_TEXT, "Form1");
DsgnControlT *curCtrl = NULL;
DsgnControlT *curCtrl = NULL;
DsgnMenuItemT *curMenuItem = NULL;
bool inForm = false;
bool inMenu = false;
int32_t menuNestDepth = 0;
// Parent name stack for nesting (index 0 = form level)
char parentStack[8][DSGN_MAX_NAME];
@ -449,6 +496,17 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
inForm = true;
nestDepth = 0;
curCtrl = NULL;
} else if (strcasecmp(typeName, "Menu") == 0 && inForm) {
DsgnMenuItemT mi;
memset(&mi, 0, sizeof(mi));
snprintf(mi.name, DSGN_MAX_NAME, "%s", ctrlName);
mi.level = menuNestDepth;
mi.enabled = true;
arrput(form->menuItems, mi);
curMenuItem = &form->menuItems[arrlen(form->menuItems) - 1];
curCtrl = NULL; // not a control
menuNestDepth++;
inMenu = true;
} else if (inForm) {
DsgnControlT ctrl;
memset(&ctrl, 0, sizeof(ctrl));
@ -477,7 +535,15 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
}
if (strcasecmp(trimmed, "End") == 0) {
if (curCtrl) {
if (inMenu) {
menuNestDepth--;
curMenuItem = NULL;
if (menuNestDepth <= 0) {
menuNestDepth = 0;
inMenu = false;
}
} else if (curCtrl) {
// If we're closing a container, pop the parent stack
if (nestDepth > 0 && strcasecmp(parentStack[nestDepth - 1], curCtrl->name) == 0) {
nestDepth--;
@ -552,7 +618,11 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
val[vi] = '\0';
if (curCtrl) {
if (curMenuItem) {
if (strcasecmp(key, "Caption") == 0) { snprintf(curMenuItem->caption, DSGN_MAX_TEXT, "%s", val); }
else if (strcasecmp(key, "Checked") == 0) { curMenuItem->checked = (strcasecmp(val, "True") == 0 || strcasecmp(val, "-1") == 0); }
else if (strcasecmp(key, "Enabled") == 0) { curMenuItem->enabled = (strcasecmp(val, "True") == 0 || strcasecmp(val, "-1") == 0); }
} else if (curCtrl) {
if (strcasecmp(key, "Left") == 0) { curCtrl->left = atoi(val); }
else if (strcasecmp(key, "Top") == 0) { curCtrl->top = atoi(val); }
else if (strcasecmp(key, "MinWidth") == 0 ||
@ -1048,6 +1118,71 @@ int32_t dsgnSaveFrm(const DsgnStateT *ds, char *buf, int32_t bufSize) {
pos += snprintf(buf + pos, bufSize - pos, " Height = %d\n", (int)ds->form->height);
}
// Output menu items as nested Begin Menu blocks
{
int32_t menuCount = (int32_t)arrlen(ds->form->menuItems);
int32_t curLevel = 0;
for (int32_t i = 0; i < menuCount; i++) {
DsgnMenuItemT *mi = &ds->form->menuItems[i];
// Close blocks back to this item's level
while (curLevel > mi->level) {
curLevel--;
for (int32_t p = 0; p < (curLevel + 1) * 4; p++) {
buf[pos++] = ' ';
}
pos += snprintf(buf + pos, bufSize - pos, "End\n");
}
// Indent: (level + 1) * 4 spaces (one extra for being inside Form block)
for (int32_t p = 0; p < (mi->level + 1) * 4; p++) {
buf[pos++] = ' ';
}
pos += snprintf(buf + pos, bufSize - pos, "Begin Menu %s\n", mi->name);
// Caption
for (int32_t p = 0; p < (mi->level + 2) * 4; p++) {
buf[pos++] = ' ';
}
pos += snprintf(buf + pos, bufSize - pos, "Caption = \"%s\"\n", mi->caption);
// Optional properties
if (mi->checked) {
for (int32_t p = 0; p < (mi->level + 2) * 4; p++) {
buf[pos++] = ' ';
}
pos += snprintf(buf + pos, bufSize - pos, "Checked = True\n");
}
if (!mi->enabled) {
for (int32_t p = 0; p < (mi->level + 2) * 4; p++) {
buf[pos++] = ' ';
}
pos += snprintf(buf + pos, bufSize - pos, "Enabled = False\n");
}
curLevel = mi->level + 1;
}
// Close any remaining open menu blocks
while (curLevel > 0) {
curLevel--;
for (int32_t p = 0; p < (curLevel + 1) * 4; p++) {
buf[pos++] = ' ';
}
pos += snprintf(buf + pos, bufSize - pos, "End\n");
}
}
// Output top-level controls (and recurse into containers)
pos = saveControls(ds->form, buf, bufSize, pos, "", 1);

View file

@ -35,6 +35,18 @@ typedef struct {
char value[DSGN_MAX_TEXT];
} DsgnPropT;
// ============================================================
// Design-time menu item (flat array with level for tree structure)
// ============================================================
typedef struct {
char caption[DSGN_MAX_TEXT]; // "&File", "-" for separator
char name[DSGN_MAX_NAME]; // "mnuFile"
int32_t level; // 0 = top-level menu, 1 = item, 2+ = submenu
bool checked;
bool enabled; // default true
} DsgnMenuItemT;
// ============================================================
// Design-time control
// ============================================================
@ -72,6 +84,7 @@ typedef struct {
bool autoSize; // true = dvxFitWindow, false = use width/height
bool resizable; // true = user can resize at runtime
DsgnControlT *controls; // stb_ds dynamic array
DsgnMenuItemT *menuItems; // stb_ds dynamic array (NULL if no menus)
bool dirty;
WidgetT *contentBox; // VBox parent for live widgets
char *code; // BASIC code section (malloc'd, after End block)
@ -180,6 +193,10 @@ void dsgnFree(DsgnStateT *ds);
// Create a live design-time widget for a given VB type name.
WidgetT *dsgnCreateDesignWidget(const char *vbTypeName, WidgetT *parent);
// Build a display-only menu bar on a window from the form's menuItems.
// Used in the form designer to preview the menu layout.
void dsgnBuildPreviewMenuBar(WindowT *win, const DsgnFormT *form);
// ============================================================
// Code rename support (implemented in ideMain.c)
// ============================================================

View file

@ -30,6 +30,7 @@
#include "ideDesigner.h"
#include "ideProject.h"
#include "ideMenuEditor.h"
#include "ideToolbox.h"
#include "ideProperties.h"
@ -92,6 +93,7 @@
#define CMD_FIND 141
#define CMD_REPLACE 142
#define CMD_FIND_NEXT 143
#define CMD_MENU_EDITOR 144
#define IDE_MAX_IMM 1024
#define IDE_DESIGN_W 400
#define IDE_DESIGN_H 300
@ -582,6 +584,8 @@ static void buildWindow(void) {
wmAddMenuSeparator(viewMenu);
wmAddMenuCheckItem(viewMenu, "&Toolbar", CMD_VIEW_TOOLBAR, true);
wmAddMenuCheckItem(viewMenu, "&Status Bar", CMD_VIEW_STATUS, true);
wmAddMenuSeparator(viewMenu);
wmAddMenuItem(viewMenu, "&Menu Editor...\tCtrl+E", CMD_MENU_EDITOR);
MenuT *winMenu = wmAddMenu(menuBar, "&Window");
wmAddMenuItem(winMenu, "&Code Editor", CMD_WIN_CODE);
@ -606,6 +610,7 @@ static void buildWindow(void) {
dvxAddAccel(accel, 'F', ACCEL_CTRL, CMD_FIND);
dvxAddAccel(accel, 'H', ACCEL_CTRL, CMD_REPLACE);
dvxAddAccel(accel, KEY_F3, 0, CMD_FIND_NEXT);
dvxAddAccel(accel, 'E', ACCEL_CTRL, CMD_MENU_EDITOR);
sWin->accelTable = accel;
WidgetT *tbRoot = wgtInitWindow(sAc, sWin);
@ -2980,6 +2985,50 @@ static void handleViewCmd(int32_t cmd) {
prefsSave(sPrefs);
}
break;
case CMD_MENU_EDITOR:
if (sDesigner.form) {
// Snapshot old menu names for rename detection
char **oldNames = NULL;
int32_t oldCount = (int32_t)arrlen(sDesigner.form->menuItems);
for (int32_t mi = 0; mi < oldCount; mi++) {
arrput(oldNames, strdup(sDesigner.form->menuItems[mi].name));
}
if (mnuEditorDialog(sAc, sDesigner.form)) {
sDesigner.form->dirty = true;
// Detect renames: match by position (items may have been
// reordered, but renamed items keep their index)
int32_t newCount = (int32_t)arrlen(sDesigner.form->menuItems);
int32_t minCount = oldCount < newCount ? oldCount : newCount;
for (int32_t mi = 0; mi < minCount; mi++) {
if (oldNames[mi][0] && sDesigner.form->menuItems[mi].name[0] &&
strcasecmp(oldNames[mi], sDesigner.form->menuItems[mi].name) != 0) {
ideRenameInCode(oldNames[mi], sDesigner.form->menuItems[mi].name);
}
}
// Rebuild menu bar preview
if (sFormWin) {
wmDestroyMenuBar(sFormWin);
dsgnBuildPreviewMenuBar(sFormWin, sDesigner.form);
dvxInvalidateWindow(sAc, sFormWin);
}
// Rebuild Object dropdown to reflect added/removed/renamed items
updateDropdowns();
}
for (int32_t mi = 0; mi < oldCount; mi++) {
free(oldNames[mi]);
}
arrfree(oldNames);
}
break;
}
}
@ -3383,10 +3432,27 @@ static void onObjDropdownChange(WidgetT *w) {
availEvents = sFormEvents;
}
// Check if this is a menu item (only event is Click)
bool isMenuItem = false;
if (!isForm && sDesigner.form) {
for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->menuItems); i++) {
if (strcasecmp(sDesigner.form->menuItems[i].name, selObj) == 0) {
isMenuItem = true;
break;
}
}
}
if (isMenuItem) {
static const char *sMenuEvents[] = { "Click", NULL };
availEvents = sMenuEvents;
}
// Get widget-specific events from the interface
const WgtIfaceT *iface = NULL;
if (!isForm && sDesigner.form) {
if (!isForm && !isMenuItem && sDesigner.form) {
for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->controls); i++) {
if (strcasecmp(sDesigner.form->controls[i].name, selObj) == 0) {
const char *wgtName = wgtFindByBasName(sDesigner.form->controls[i].typeName);
@ -4376,6 +4442,9 @@ static void switchToDesign(void) {
sFormWin->accelTable = sWin ? sWin->accelTable : NULL;
sDesigner.formWin = sFormWin;
// Build preview menu bar from form's menu items
dsgnBuildPreviewMenuBar(sFormWin, sDesigner.form);
WidgetT *root = wgtInitWindow(sAc, sFormWin);
WidgetT *contentBox;
@ -4636,6 +4705,8 @@ static void updateProjectMenuState(void) {
}
bool hasProject = (sProject.projectPath[0] != '\0');
bool hasFile = (hasProject && sProject.activeFileIdx >= 0);
bool hasForm = (hasFile && sProject.files[sProject.activeFileIdx].isForm);
wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_SAVE, hasProject);
wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_CLOSE, hasProject);
@ -4644,9 +4715,13 @@ static void updateProjectMenuState(void) {
wmMenuItemSetEnabled(sWin->menuBar, CMD_SAVE, hasProject);
wmMenuItemSetEnabled(sWin->menuBar, CMD_SAVE_ALL, hasProject);
wmMenuItemSetEnabled(sWin->menuBar, CMD_FIND, hasProject);
wmMenuItemSetEnabled(sWin->menuBar, CMD_FIND, hasProject);
wmMenuItemSetEnabled(sWin->menuBar, CMD_FIND_NEXT, hasProject);
wmMenuItemSetEnabled(sWin->menuBar, CMD_REPLACE, hasProject);
wmMenuItemSetEnabled(sWin->menuBar, CMD_VIEW_CODE, hasFile);
wmMenuItemSetEnabled(sWin->menuBar, CMD_VIEW_DESIGN, hasForm);
wmMenuItemSetEnabled(sWin->menuBar, CMD_MENU_EDITOR, hasForm);
}
@ -5467,19 +5542,42 @@ static void updateDropdowns(void) {
memset(&entry, 0, sizeof(entry));
entry.lineNum = lineNum;
char *underscore = strchr(procName, '_');
// Match proc name against known objects: form name,
// controls, menu items. Try each as a prefix followed
// by "_". This handles names with multiple underscores
// correctly (e.g., "cmdOK_Click" matches "cmdOK", not
// "This_Is_A_Dumb_Name" matching a control named "This").
bool isEvent = false;
if (underscore) {
int32_t objLen = (int32_t)(underscore - procName);
if (sDesigner.form) {
// Collect all known object names
const char *objNames[512];
int32_t objNameCount = 0;
if (objLen > 63) {
objLen = 63;
objNames[objNameCount++] = sDesigner.form->name;
for (int32_t ci = 0; ci < (int32_t)arrlen(sDesigner.form->controls) && objNameCount < 511; ci++) {
objNames[objNameCount++] = sDesigner.form->controls[ci].name;
}
memcpy(entry.objName, procName, objLen);
entry.objName[objLen] = '\0';
snprintf(entry.evtName, sizeof(entry.evtName), "%s", underscore + 1);
} else {
for (int32_t mi = 0; mi < (int32_t)arrlen(sDesigner.form->menuItems) && objNameCount < 511; mi++) {
objNames[objNameCount++] = sDesigner.form->menuItems[mi].name;
}
// Try each object name as prefix + "_"
for (int32_t oi = 0; oi < objNameCount; oi++) {
int32_t nameLen = (int32_t)strlen(objNames[oi]);
if (nameLen > 0 && strncasecmp(procName, objNames[oi], nameLen) == 0 && procName[nameLen] == '_') {
snprintf(entry.objName, sizeof(entry.objName), "%s", objNames[oi]);
snprintf(entry.evtName, sizeof(entry.evtName), "%s", procName + nameLen + 1);
isEvent = true;
break;
}
}
}
if (!isEvent) {
snprintf(entry.objName, sizeof(entry.objName), "%s", "(General)");
snprintf(entry.evtName, sizeof(entry.evtName), "%s", procName);
}
@ -5522,25 +5620,15 @@ static void updateDropdowns(void) {
for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->controls); i++) {
arrput(sObjItems, sDesigner.form->controls[i].name);
}
}
// Add any objects from existing procs not already in the list
int32_t procCount = (int32_t)arrlen(sProcTable);
// Add menu item names (non-separator, non-top-level-header-only)
for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->menuItems); i++) {
DsgnMenuItemT *mi = &sDesigner.form->menuItems[i];
for (int32_t i = 0; i < procCount; i++) {
bool found = false;
int32_t objCount = (int32_t)arrlen(sObjItems);
for (int32_t j = 0; j < objCount; j++) {
if (strcasecmp(sObjItems[j], sProcTable[i].objName) == 0) {
found = true;
break;
if (mi->name[0] && mi->caption[0] != '-') {
arrput(sObjItems, mi->name);
}
}
if (!found) {
arrput(sObjItems, sProcTable[i].objName);
}
}
// Sort object items alphabetically, keeping (General) first

View file

@ -0,0 +1,741 @@
// ideMenuEditor.c -- DVX BASIC menu editor dialog
//
// VB3-style modal dialog for designing form menu bars. Works on
// a cloned copy of the menu item array; changes are only applied
// when the user clicks OK.
#include "ideMenuEditor.h"
#include "dvxDialog.h"
#include "dvxWm.h"
#include "widgetBox.h"
#include "widgetButton.h"
#include "widgetCheckbox.h"
#include "widgetLabel.h"
#include "widgetListBox.h"
#include "widgetTextInput.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
// ============================================================
// Constants
// ============================================================
#define MAX_MENU_ITEMS 128
#define MAX_MENU_LEVEL 5
#define ARROW_STR "-> "
// ============================================================
// Dialog state
// ============================================================
typedef struct {
bool done;
bool accepted;
AppContextT *ctx;
DsgnFormT *form;
// Working copy of menu items
DsgnMenuItemT *items; // stb_ds array
int32_t selectedIdx;
bool nameAutoGen; // true = name was auto-generated, update on caption change
// Widgets
WidgetT *captionInput;
WidgetT *nameInput;
WidgetT *checkedCb;
WidgetT *enabledCb;
WidgetT *listBox;
} MnuEdStateT;
static MnuEdStateT sMed;
// ============================================================
// Prototypes
// ============================================================
static void applyFields(void);
static int32_t findSubtreeEnd(int32_t idx);
static void loadFields(void);
static void onCancel(WidgetT *w);
static void onCaptionChange(WidgetT *w);
static void onDelete(WidgetT *w);
static void onNameChange(WidgetT *w);
static void onIndent(WidgetT *w);
static void onInsert(WidgetT *w);
static void onListClick(WidgetT *w);
static void onMoveDown(WidgetT *w);
static void onMoveUp(WidgetT *w);
static void onNext(WidgetT *w);
static void onOk(WidgetT *w);
static void onOutdent(WidgetT *w);
static void rebuildList(void);
// ============================================================
// applyFields -- save current widget values to selected item
// ============================================================
static void applyFields(void) {
int32_t count = (int32_t)arrlen(sMed.items);
if (sMed.selectedIdx < 0 || sMed.selectedIdx >= count) {
return;
}
DsgnMenuItemT *mi = &sMed.items[sMed.selectedIdx];
const char *cap = wgtGetText(sMed.captionInput);
const char *nam = wgtGetText(sMed.nameInput);
if (cap) {
snprintf(mi->caption, DSGN_MAX_TEXT, "%s", cap);
}
if (nam) {
snprintf(mi->name, DSGN_MAX_NAME, "%s", nam);
}
// Auto-generate name from caption if name is empty or was auto-generated
if ((mi->name[0] == '\0' || sMed.nameAutoGen) && mi->caption[0] != '\0') {
char autoName[DSGN_MAX_NAME];
if (mi->caption[0] == '-') {
// Separator: generate mnuSep1, mnuSep2, etc.
int32_t sepNum = 1;
int32_t itemCount = (int32_t)arrlen(sMed.items);
for (int32_t i = 0; i < itemCount; i++) {
if (i != sMed.selectedIdx && strncasecmp(sMed.items[i].name, "mnuSep", 6) == 0) {
int32_t n = atoi(sMed.items[i].name + 6);
if (n >= sepNum) {
sepNum = n + 1;
}
}
}
snprintf(autoName, DSGN_MAX_NAME, "mnuSep%d", sepNum);
} else {
// Normal item: strip & and non-alphanumeric, prefix "mnu"
int32_t p = 0;
autoName[p++] = 'm';
autoName[p++] = 'n';
autoName[p++] = 'u';
for (const char *c = mi->caption; *c && p < DSGN_MAX_NAME - 1; c++) {
if (*c == '&') {
continue;
}
if ((*c >= 'A' && *c <= 'Z') || (*c >= 'a' && *c <= 'z') || (*c >= '0' && *c <= '9')) {
autoName[p++] = *c;
}
}
autoName[p] = '\0';
}
snprintf(mi->name, DSGN_MAX_NAME, "%s", autoName);
wgtSetText(sMed.nameInput, mi->name);
sMed.nameAutoGen = true;
}
mi->checked = wgtCheckboxIsChecked(sMed.checkedCb);
mi->enabled = wgtCheckboxIsChecked(sMed.enabledCb);
}
// ============================================================
// findSubtreeEnd -- index past the last child of items[idx]
// ============================================================
static int32_t findSubtreeEnd(int32_t idx) {
int32_t count = (int32_t)arrlen(sMed.items);
int32_t level = sMed.items[idx].level;
int32_t end = idx + 1;
while (end < count && sMed.items[end].level > level) {
end++;
}
return end;
}
// ============================================================
// loadFields -- populate widgets from selected item
// ============================================================
static void loadFields(void) {
int32_t count = (int32_t)arrlen(sMed.items);
// Reset auto-gen flag — if the loaded item has an empty name, it's
// eligible for auto-generation; if it has a name, the user set it.
sMed.nameAutoGen = false;
if (sMed.selectedIdx < 0 || sMed.selectedIdx >= count) {
wgtSetText(sMed.captionInput, "");
wgtSetText(sMed.nameInput, "");
wgtCheckboxSetChecked(sMed.checkedCb, false);
wgtCheckboxSetChecked(sMed.enabledCb, true);
sMed.nameAutoGen = true; // new blank item — auto-gen eligible
return;
}
DsgnMenuItemT *mi = &sMed.items[sMed.selectedIdx];
sMed.nameAutoGen = (mi->name[0] == '\0');
wgtSetText(sMed.captionInput, mi->caption);
wgtSetText(sMed.nameInput, mi->name);
wgtCheckboxSetChecked(sMed.checkedCb, mi->checked);
wgtCheckboxSetChecked(sMed.enabledCb, mi->enabled);
}
// ============================================================
// onCancel
// ============================================================
static void onCancel(WidgetT *w) {
(void)w;
sMed.accepted = false;
sMed.done = true;
}
// ============================================================
// onCaptionChange -- caption field changed, auto-update name
// ============================================================
static void onCaptionChange(WidgetT *w) {
(void)w;
applyFields();
rebuildList();
}
// ============================================================
// onNameChange -- user manually edited the name field
// ============================================================
static void onNameChange(WidgetT *w) {
(void)w;
sMed.nameAutoGen = false; // user took over
applyFields();
rebuildList();
}
// ============================================================
// onDelete
// ============================================================
static void onDelete(WidgetT *w) {
(void)w;
int32_t count = (int32_t)arrlen(sMed.items);
if (sMed.selectedIdx < 0 || sMed.selectedIdx >= count) {
return;
}
applyFields();
// Delete selected item and its children
int32_t subEnd = findSubtreeEnd(sMed.selectedIdx);
for (int32_t i = subEnd - 1; i >= sMed.selectedIdx; i--) {
arrdel(sMed.items, i);
}
count = (int32_t)arrlen(sMed.items);
if (sMed.selectedIdx >= count) {
sMed.selectedIdx = count - 1;
}
rebuildList();
loadFields();
wgtSetFocused(sMed.captionInput);
}
// ============================================================
// onIndent -- increase level (make child of previous item)
// ============================================================
static void onIndent(WidgetT *w) {
(void)w;
int32_t count = (int32_t)arrlen(sMed.items);
int32_t idx = sMed.selectedIdx;
if (idx <= 0 || idx >= count) {
return;
}
applyFields();
// Can only indent if previous item is at same or higher level
int32_t prevLevel = sMed.items[idx - 1].level;
if (sMed.items[idx].level > prevLevel) {
return; // already deeper than prev
}
if (sMed.items[idx].level >= MAX_MENU_LEVEL) {
return;
}
// Indent this item and all its children
int32_t subEnd = findSubtreeEnd(idx);
for (int32_t i = idx; i < subEnd; i++) {
sMed.items[i].level++;
}
rebuildList();
}
// ============================================================
// onInsert
// ============================================================
static void onInsert(WidgetT *w) {
(void)w;
int32_t count = (int32_t)arrlen(sMed.items);
if (count >= MAX_MENU_ITEMS) {
return;
}
applyFields();
DsgnMenuItemT mi;
memset(&mi, 0, sizeof(mi));
mi.enabled = true;
// Insert after the current item's subtree, at the same level
int32_t insertAt;
if (sMed.selectedIdx >= 0 && sMed.selectedIdx < count) {
mi.level = sMed.items[sMed.selectedIdx].level;
insertAt = findSubtreeEnd(sMed.selectedIdx);
} else {
insertAt = count;
}
arrins(sMed.items, insertAt, mi);
sMed.selectedIdx = insertAt;
rebuildList();
loadFields();
wgtSetFocused(sMed.captionInput);
}
// ============================================================
// onListClick
// ============================================================
static void onListClick(WidgetT *w) {
int32_t prev = sMed.selectedIdx;
sMed.selectedIdx = wgtListBoxGetSelected(w);
if (prev != sMed.selectedIdx && prev >= 0 && prev < (int32_t)arrlen(sMed.items)) {
// Save fields from previously selected item
int32_t saveSel = sMed.selectedIdx;
sMed.selectedIdx = prev;
applyFields();
sMed.selectedIdx = saveSel;
}
loadFields();
}
// ============================================================
// onMoveDown
// ============================================================
static void onMoveDown(WidgetT *w) {
(void)w;
int32_t count = (int32_t)arrlen(sMed.items);
int32_t idx = sMed.selectedIdx;
if (idx < 0 || idx >= count) {
return;
}
applyFields();
int32_t subEnd = findSubtreeEnd(idx);
if (subEnd >= count) {
return; // already at bottom
}
// The item/subtree after us
int32_t nextEnd = findSubtreeEnd(subEnd);
// Rotate: move the next subtree before our subtree
// Simple approach: extract our subtree, delete it, insert after next subtree
int32_t subSize = subEnd - idx;
DsgnMenuItemT *tmp = (DsgnMenuItemT *)malloc(subSize * sizeof(DsgnMenuItemT));
memcpy(tmp, &sMed.items[idx], subSize * sizeof(DsgnMenuItemT));
// Delete our subtree
for (int32_t i = subEnd - 1; i >= idx; i--) {
arrdel(sMed.items, i);
}
// Insert after what was the next subtree (now shifted)
int32_t insertAt = nextEnd - subSize;
for (int32_t i = 0; i < subSize; i++) {
arrins(sMed.items, insertAt + i, tmp[i]);
}
free(tmp);
sMed.selectedIdx = insertAt;
rebuildList();
}
// ============================================================
// onMoveUp
// ============================================================
static void onMoveUp(WidgetT *w) {
(void)w;
int32_t idx = sMed.selectedIdx;
if (idx <= 0) {
return;
}
applyFields();
// Find the start of the previous subtree at the same or lower level
int32_t prevIdx = idx - 1;
while (prevIdx > 0 && sMed.items[prevIdx].level > sMed.items[idx].level) {
prevIdx--;
}
// Move our subtree before the previous item's subtree
int32_t subEnd = findSubtreeEnd(idx);
int32_t subSize = subEnd - idx;
DsgnMenuItemT *tmp = (DsgnMenuItemT *)malloc(subSize * sizeof(DsgnMenuItemT));
memcpy(tmp, &sMed.items[idx], subSize * sizeof(DsgnMenuItemT));
// Delete our subtree
for (int32_t i = subEnd - 1; i >= idx; i--) {
arrdel(sMed.items, i);
}
// Insert at prevIdx
for (int32_t i = 0; i < subSize; i++) {
arrins(sMed.items, prevIdx + i, tmp[i]);
}
free(tmp);
sMed.selectedIdx = prevIdx;
rebuildList();
}
// ============================================================
// onNext
// ============================================================
static void onNext(WidgetT *w) {
(void)w;
int32_t count = (int32_t)arrlen(sMed.items);
applyFields();
if (sMed.selectedIdx < count - 1) {
sMed.selectedIdx++;
} else {
// Append new item
if (count >= MAX_MENU_ITEMS) {
return;
}
DsgnMenuItemT mi;
memset(&mi, 0, sizeof(mi));
mi.enabled = true;
if (count > 0) {
mi.level = sMed.items[count - 1].level;
}
arrput(sMed.items, mi);
sMed.selectedIdx = (int32_t)arrlen(sMed.items) - 1;
}
rebuildList();
loadFields();
wgtSetFocused(sMed.captionInput);
}
// ============================================================
// onOk
// ============================================================
static void onOk(WidgetT *w) {
(void)w;
applyFields();
// Strip blank items (no caption and no name)
for (int32_t i = (int32_t)arrlen(sMed.items) - 1; i >= 0; i--) {
if (sMed.items[i].caption[0] == '\0' && sMed.items[i].name[0] == '\0') {
arrdel(sMed.items, i);
}
}
// Validate: check names are non-empty and unique
int32_t count = (int32_t)arrlen(sMed.items);
for (int32_t i = 0; i < count; i++) {
if (sMed.items[i].name[0] == '\0') {
dvxMessageBox(sMed.ctx, "Menu Editor", "All menu items must have a Name.", MB_OK | MB_ICONERROR);
sMed.selectedIdx = i;
rebuildList();
loadFields();
wgtSetFocused(sMed.captionInput);
return;
}
for (int32_t j = i + 1; j < count; j++) {
if (strcasecmp(sMed.items[i].name, sMed.items[j].name) == 0) {
char msg[128];
snprintf(msg, sizeof(msg), "Duplicate menu name: %s", sMed.items[i].name);
dvxMessageBox(sMed.ctx, "Menu Editor", msg, MB_OK | MB_ICONERROR);
sMed.selectedIdx = j;
rebuildList();
loadFields();
wgtSetFocused(sMed.captionInput);
return;
}
}
}
// Copy working items back to form
arrfree(sMed.form->menuItems);
sMed.form->menuItems = NULL;
for (int32_t i = 0; i < count; i++) {
arrput(sMed.form->menuItems, sMed.items[i]);
}
sMed.accepted = true;
sMed.done = true;
}
// ============================================================
// onOutdent -- decrease level
// ============================================================
static void onOutdent(WidgetT *w) {
(void)w;
int32_t count = (int32_t)arrlen(sMed.items);
int32_t idx = sMed.selectedIdx;
if (idx < 0 || idx >= count) {
return;
}
if (sMed.items[idx].level <= 0) {
return;
}
applyFields();
int32_t subEnd = findSubtreeEnd(idx);
for (int32_t i = idx; i < subEnd; i++) {
sMed.items[i].level--;
}
rebuildList();
}
// ============================================================
// rebuildList -- refresh the listbox display
// ============================================================
static void rebuildList(void) {
int32_t count = (int32_t)arrlen(sMed.items);
// Build display strings
static const char *sLabels[MAX_MENU_ITEMS];
static char sLabelBufs[MAX_MENU_ITEMS][DSGN_MAX_TEXT + 32];
for (int32_t i = 0; i < count && i < MAX_MENU_ITEMS; i++) {
int32_t pos = 0;
for (int32_t lv = 0; lv < sMed.items[i].level; lv++) {
pos += snprintf(sLabelBufs[i] + pos, sizeof(sLabelBufs[i]) - pos, "%s", ARROW_STR);
}
const char *cap = sMed.items[i].caption;
if (cap[0] == '-') {
snprintf(sLabelBufs[i] + pos, sizeof(sLabelBufs[i]) - pos, "--------");
} else if (cap[0]) {
snprintf(sLabelBufs[i] + pos, sizeof(sLabelBufs[i]) - pos, "%s", cap);
} else {
snprintf(sLabelBufs[i] + pos, sizeof(sLabelBufs[i]) - pos, "(%s)", sMed.items[i].name[0] ? sMed.items[i].name : "untitled");
}
sLabels[i] = sLabelBufs[i];
}
wgtListBoxSetItems(sMed.listBox, sLabels, count < MAX_MENU_ITEMS ? count : MAX_MENU_ITEMS);
wgtListBoxSetSelected(sMed.listBox, sMed.selectedIdx);
}
// ============================================================
// mnuEditorDialog
// ============================================================
bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) {
memset(&sMed, 0, sizeof(sMed));
sMed.ctx = ctx;
sMed.form = form;
sMed.selectedIdx = 0;
// Clone menu items as working copy
int32_t count = (int32_t)arrlen(form->menuItems);
for (int32_t i = 0; i < count; i++) {
arrput(sMed.items, form->menuItems[i]);
}
// If empty, start with one blank item so the user can type immediately
if (arrlen(sMed.items) == 0) {
DsgnMenuItemT mi;
memset(&mi, 0, sizeof(mi));
mi.enabled = true;
arrput(sMed.items, mi);
}
// Create modal dialog
WindowT *win = dvxCreateWindowCentered(ctx, "Menu Editor", 360, 420, false);
if (!win) {
arrfree(sMed.items);
return false;
}
win->maxW = win->w;
win->maxH = win->h;
WidgetT *root = wgtInitWindow(ctx, win);
root->spacing = wgtPixels(4);
// Caption row
WidgetT *capRow = wgtHBox(root);
capRow->spacing = wgtPixels(4);
WidgetT *capLbl = wgtLabel(capRow, "Caption:");
capLbl->minW = wgtPixels(60);
sMed.captionInput = wgtTextInput(capRow, DSGN_MAX_TEXT);
sMed.captionInput->weight = 100;
sMed.captionInput->onChange = onCaptionChange;
// Name row
WidgetT *namRow = wgtHBox(root);
namRow->spacing = wgtPixels(4);
WidgetT *namLbl = wgtLabel(namRow, "Name:");
namLbl->minW = wgtPixels(60);
sMed.nameInput = wgtTextInput(namRow, DSGN_MAX_NAME);
sMed.nameInput->weight = 100;
sMed.nameInput->onChange = onNameChange;
// Check row
WidgetT *chkRow = wgtHBox(root);
chkRow->spacing = wgtPixels(12);
sMed.checkedCb = wgtCheckbox(chkRow, "Checked");
sMed.enabledCb = wgtCheckbox(chkRow, "Enabled");
wgtCheckboxSetChecked(sMed.enabledCb, true);
// Listbox
sMed.listBox = wgtListBox(root);
sMed.listBox->weight = 100;
sMed.listBox->onClick = onListClick;
// Arrow buttons
WidgetT *arrowRow = wgtHBox(root);
arrowRow->spacing = wgtPixels(4);
WidgetT *btnOut = wgtButton(arrowRow, "<-");
btnOut->onClick = onOutdent;
btnOut->minW = wgtPixels(32);
WidgetT *btnIn = wgtButton(arrowRow, "->");
btnIn->onClick = onIndent;
btnIn->minW = wgtPixels(32);
WidgetT *btnUp = wgtButton(arrowRow, "Up");
btnUp->onClick = onMoveUp;
btnUp->minW = wgtPixels(32);
WidgetT *btnDn = wgtButton(arrowRow, "Dn");
btnDn->onClick = onMoveDown;
btnDn->minW = wgtPixels(32);
// Action buttons
WidgetT *actRow = wgtHBox(root);
actRow->spacing = wgtPixels(4);
WidgetT *btnNext = wgtButton(actRow, "&Next");
btnNext->onClick = onNext;
WidgetT *btnIns = wgtButton(actRow, "&Insert");
btnIns->onClick = onInsert;
WidgetT *btnDel = wgtButton(actRow, "&Delete");
btnDel->onClick = onDelete;
// OK / Cancel
WidgetT *okRow = wgtHBox(root);
okRow->spacing = wgtPixels(8);
WidgetT *btnOk = wgtButton(okRow, "OK");
btnOk->onClick = onOk;
btnOk->minW = wgtPixels(60);
WidgetT *btnCancel = wgtButton(okRow, "Cancel");
btnCancel->onClick = onCancel;
btnCancel->minW = wgtPixels(60);
// Populate
rebuildList();
loadFields();
wgtSetFocused(sMed.captionInput);
dvxFitWindow(ctx, win);
// Modal loop
WindowT *prevModal = ctx->modalWindow;
ctx->modalWindow = win;
while (!sMed.done && ctx->running) {
dvxUpdate(ctx);
}
ctx->modalWindow = prevModal;
dvxDestroyWindow(ctx, win);
// Cleanup working copy
arrfree(sMed.items);
sMed.items = NULL;
return sMed.accepted;
}

View file

@ -0,0 +1,16 @@
// ideMenuEditor.h -- DVX BASIC menu editor dialog
//
// Modal dialog for designing menu bars on forms. Edits the
// DsgnMenuItemT array on a DsgnFormT. Returns true if the
// user clicked OK and the menu data was modified.
#ifndef IDE_MENUEDITOR_H
#define IDE_MENUEDITOR_H
#include "ideDesigner.h"
#include "dvxApp.h"
// Show the modal Menu Editor dialog.
bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form);
#endif // IDE_MENUEDITOR_H

View file

@ -950,18 +950,14 @@ static bool dispatchAccelKey(AppContextT *ctx, char key) {
WidgetT *next = widgetFindNextFocusable(win->widgetRoot, target);
if (next) {
if (sFocusedWidget) { sFocusedWidget->focused = false; }
sFocusedWidget = next;
next->focused = true;
wclsOnAccelActivate(next, win->widgetRoot);
wgtInvalidate(win->widgetRoot);
}
} else if (wclsHas(target, WGT_METHOD_ON_ACCEL_ACTIVATE)) {
if (sFocusedWidget) { sFocusedWidget->focused = false; }
sFocusedWidget = target;
target->focused = true;
wclsOnAccelActivate(target, win->widgetRoot);
wgtInvalidate(win->widgetRoot);
}
@ -1818,6 +1814,13 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t
wmSetFocus(&ctx->stack, &ctx->dirty, hitIdx);
}
// Clear widget focus for non-content clicks (scrollbars, title bar, etc.)
// Content clicks clear focus inside widgetOnMouse instead.
if (hitPart != HIT_CONTENT && sFocusedWidget) {
wgtInvalidatePaint(sFocusedWidget);
sFocusedWidget = NULL;
}
switch (hitPart) {
case HIT_CONTENT:
if (win->onMouse) {
@ -2904,7 +2907,7 @@ static void pollKeyboard(AppContextT *ctx) {
WidgetT *w = fstack[arrlen(fstack) - 1];
arrsetlen(fstack, arrlen(fstack) - 1);
if (w->focused && widgetIsFocusable(w->type)) {
if (w == sFocusedWidget && widgetIsFocusable(w->type)) {
current = w;
break;
}
@ -2938,11 +2941,7 @@ static void pollKeyboard(AppContextT *ctx) {
if (next) {
sOpenPopup = NULL;
if (sFocusedWidget) {
sFocusedWidget->focused = false;
}
sFocusedWidget = next;
next->focused = true;
// Scroll the widget into view if needed
int32_t scrollX = win->hScroll ? win->hScroll->value : 0;

View file

@ -248,7 +248,6 @@ typedef struct WidgetT {
bool visible;
bool enabled;
bool readOnly;
bool focused;
bool swallowTab; // Tab key goes to widget, not focus nav
char accelKey; // lowercase accelerator character, 0 if none

View file

@ -948,6 +948,22 @@ MenuBarT *wmAddMenuBar(WindowT *win) {
}
void wmDestroyMenuBar(WindowT *win) {
if (!win || !win->menuBar) {
return;
}
for (int32_t i = 0; i < win->menuBar->menuCount; i++) {
freeMenuRecursive(&win->menuBar->menus[i]);
}
free(win->menuBar->menus);
free(win->menuBar);
win->menuBar = NULL;
wmUpdateContentRect(win);
}
// ============================================================
// wmAddMenuItem
// ============================================================
@ -1328,14 +1344,7 @@ void wmDestroyWindow(WindowStackT *stack, WindowT *win) {
free(win->contentBuf);
}
if (win->menuBar) {
for (int32_t i = 0; i < win->menuBar->menuCount; i++) {
freeMenuRecursive(&win->menuBar->menus[i]);
}
free(win->menuBar->menus);
free(win->menuBar);
}
wmDestroyMenuBar(win);
if (win->vScroll) {
free(win->vScroll);

View file

@ -59,6 +59,9 @@ int32_t wmReallocContentBuf(WindowT *win, const DisplayT *d);
// menu bar per window is supported.
MenuBarT *wmAddMenuBar(WindowT *win);
// Free the menu bar and reclaim the content area.
void wmDestroyMenuBar(WindowT *win);
// Append a dropdown menu to the menu bar. Returns the MenuT to populate
// with items. The label supports & accelerator markers (e.g. "&File").
MenuT *wmAddMenu(MenuBarT *bar, const char *label);

View file

@ -214,8 +214,6 @@ void widgetClearFocus(WidgetT *root) {
return;
}
root->focused = false;
for (WidgetT *c = root->firstChild; c; c = c->nextSibling) {
widgetClearFocus(c);
}

View file

@ -168,7 +168,7 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
// Use cached focus pointer -- O(1) instead of O(n) tree walk
WidgetT *focus = sFocusedWidget;
if (!focus || !focus->focused || focus->window != win) {
if (!focus || focus->window != win) {
return;
}
@ -210,7 +210,7 @@ void widgetOnKeyUp(WindowT *win, int32_t scancode, int32_t mod) {
WidgetT *focus = sFocusedWidget;
if (!focus || !focus->focused || focus->window != win) {
if (!focus || focus->window != win) {
return;
}
@ -372,12 +372,12 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
WidgetT *prevFocus = sFocusedWidget;
if (sFocusedWidget) {
sFocusedWidget->focused = false;
wgtInvalidatePaint(sFocusedWidget);
sFocusedWidget = NULL;
}
// Dispatch to the hit widget's mouse handler via vtable. The handler
// is responsible for setting hit->focused=true if it wants focus.
// is responsible for setting sFocusedWidget if it wants focus.
if (hit->enabled) {
wclsOnMouse(hit, root, vx, vy);
}
@ -453,10 +453,7 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
sPrevMouseX = vx;
sPrevMouseY = vy;
// Update the cached focus pointer for O(1) access in widgetOnKey
if (hit->focused) {
sFocusedWidget = hit;
}
// sFocusedWidget is now set directly by the widget's mouse handler
// Fire focus/blur callbacks on transitions
if (prevFocus && prevFocus != sFocusedWidget && prevFocus->onBlur) {
@ -543,7 +540,6 @@ void widgetOnPaint(WindowT *win, RectT *dirtyArea) {
WidgetT *first = widgetFindNextFocusable(root, NULL);
if (first) {
first->focused = true;
sFocusedWidget = first;
}
}

View file

@ -453,11 +453,9 @@ void wgtSetFocused(WidgetT *w) {
WidgetT *prev = sFocusedWidget;
if (prev && prev != w) {
prev->focused = false;
wgtInvalidatePaint(prev);
}
w->focused = true;
sFocusedWidget = w;
wgtInvalidatePaint(w);

View file

@ -784,7 +784,7 @@ void widgetAnsiTermOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
AnsiTermDataT *at = (AnsiTermDataT *)hit->data;
AppContextT *actx = (AppContextT *)root->userData;
const BitmapFontT *font = &actx->font;
hit->focused = true;
sFocusedWidget = hit;
clearOtherSelections(hit);
int32_t cols = at->cols;
int32_t rows = at->rows;
@ -898,7 +898,7 @@ void widgetAnsiTermPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
if (maxScroll > 0 && thumbRange > 0) { thumbY = trackY + (at->scrollPos * thumbRange) / maxScroll; }
drawBevel(d, ops, sbX, thumbY, sbW, thumbH, &btnBevel);
}
if (w->focused) { drawFocusRect(d, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, colors->contentFg); }
if (w == sFocusedWidget) { drawFocusRect(d, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, colors->contentFg); }
}

View file

@ -114,7 +114,7 @@ void widgetButtonOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
(void)vx;
(void)vy;
ButtonDataT *d = (ButtonDataT *)w->data;
w->focused = true;
sFocusedWidget = w;
d->pressed = true;
sDragWidget = w;
}
@ -157,7 +157,7 @@ void widgetButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma
drawTextAccel(d, ops, font, textX, textY, bd->text, fg, bgFace, true);
}
if (w->focused) {
if (w == sFocusedWidget) {
int32_t off = bd->pressed ? BUTTON_PRESS_OFFSET : 0;
drawFocusRect(d, ops, w->x + BUTTON_FOCUS_INSET + off, w->y + BUTTON_FOCUS_INSET + off, w->w - BUTTON_FOCUS_INSET * 2, w->h - BUTTON_FOCUS_INSET * 2, fg);
}

View file

@ -108,7 +108,7 @@ void widgetCheckboxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
(void)vx;
(void)vy;
CheckboxDataT *d = (CheckboxDataT *)w->data;
w->focused = true;
sFocusedWidget = w;
d->checked = !d->checked;
if (w->onChange) {
@ -163,7 +163,7 @@ void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
drawTextAccel(d, ops, font, labelX, labelY, cd->text, fg, bg, false);
}
if (w->focused) {
if (w == sFocusedWidget) {
drawFocusRect(d, ops, labelX - 1, labelY - 1, labelW + 2, font->charHeight + 2, fg);
}
}

View file

@ -200,7 +200,7 @@ void widgetComboBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
// ============================================================
void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
w->focused = true;
sFocusedWidget = w;
ComboBoxDataT *d = (ComboBoxDataT *)w->data;
// If popup is open, this click is on a popup item -- select it
@ -300,7 +300,7 @@ void widgetComboBoxPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const
len = maxChars;
}
widgetTextEditPaintLine(disp, ops, font, colors, textX, textY, d->buf + off, len, off, d->cursorPos, d->selStart, d->selEnd, fg, bg, w->focused && w->enabled && !d->open, w->x + TEXT_INPUT_PAD, w->x + textAreaW - TEXT_INPUT_PAD);
widgetTextEditPaintLine(disp, ops, font, colors, textX, textY, d->buf + off, len, off, d->cursorPos, d->selStart, d->selEnd, fg, bg, w == sFocusedWidget && w->enabled && !d->open, w->x + TEXT_INPUT_PAD, w->x + textAreaW - TEXT_INPUT_PAD);
}
// Drop button

View file

@ -171,7 +171,7 @@ void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) {
void widgetDropdownOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
(void)root;
(void)vx;
w->focused = true;
sFocusedWidget = w;
DropdownDataT *d = (DropdownDataT *)w->data;
// If popup is open, this click is on a popup item -- select it
@ -255,7 +255,7 @@ void widgetDropdownPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const
int32_t arrowY = w->y + w->h / 2 - 1;
widgetDrawDropdownArrow(disp, ops, arrowX, arrowY, arrowFg);
if (w->focused) {
if (w == sFocusedWidget) {
drawFocusRect(disp, ops, w->x + 3, w->y + 3, textAreaW - 6, w->h - 6, fg);
}
}

View file

@ -83,7 +83,7 @@ void widgetImageButtonOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy)
(void)vx;
(void)vy;
ImageButtonDataT *d = (ImageButtonDataT *)w->data;
w->focused = true;
sFocusedWidget = w;
d->pressed = true;
sDragWidget = w;
}
@ -121,7 +121,7 @@ void widgetImageButtonPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, con
0, 0, d->imgW, d->imgH);
}
if (w->focused) {
if (w == sFocusedWidget) {
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
int32_t off = pressed ? 1 : 0;
drawFocusRect(disp, ops, w->x + 3 + off, w->y + 3 + off, w->w - 6, w->h - 6, fg);

View file

@ -317,7 +317,7 @@ void widgetListBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
d->sbDragOff = relY - WGT_SB_W - thumbPos;
}
hit->focused = true;
sFocusedWidget = hit;
return;
}
}
@ -342,7 +342,7 @@ void widgetListBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
bool ctrl = (ctx->keyModifiers & KEY_MOD_CTRL) != 0;
d->selectedIdx = idx;
hit->focused = true;
sFocusedWidget = hit;
if (multi && d->selBits) {
if (ctrl) {
@ -444,7 +444,7 @@ void widgetListBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitm
drawText(d, ops, font, innerX, iy, lb->items[idx], ifg, ibg, false);
// Draw cursor indicator in multi-select (dotted focus rect on cursor item)
if (multi && idx == lb->selectedIdx && w->focused) {
if (multi && idx == lb->selectedIdx && w == sFocusedWidget) {
drawFocusRect(d, ops, w->x + LISTBOX_BORDER, iy, contentW, font->charHeight, fg);
}
}
@ -469,7 +469,7 @@ void widgetListBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitm
widgetDrawScrollbarV(d, ops, colors, sbX, sbY, innerH, lb->itemCount, visibleRows, lb->scrollPos);
}
if (w->focused) {
if (w == sFocusedWidget) {
drawFocusRect(d, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, fg);
}
}

View file

@ -542,7 +542,7 @@ void widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
// because scrollbar presence depends on content/widget dimensions which
// may have changed since the last paint.
void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
hit->focused = true;
sFocusedWidget = hit;
ListViewDataT *lv = (ListViewDataT *)hit->data;
AppContextT *ctx = (AppContextT *)root->userData;
@ -1117,7 +1117,7 @@ void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
}
// Draw cursor focus rect in multi-select mode (on top of text)
if (multi && dataRow == lv->selectedIdx && w->focused) {
if (multi && dataRow == lv->selectedIdx && w == sFocusedWidget) {
drawFocusRect(d, ops, baseX, iy, innerW, font->charHeight, fg);
}
}
@ -1157,7 +1157,7 @@ void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
}
}
if (w->focused) {
if (w == sFocusedWidget) {
drawFocusRect(d, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, fg);
}
}

View file

@ -148,8 +148,6 @@ void widgetRadioOnKey(WidgetT *w, int32_t key, int32_t mod) {
if (next) {
RadioDataT *nd = (RadioDataT *)next->data;
RadioGroupDataT *gd = (RadioGroupDataT *)next->parent->data;
w->focused = false;
next->focused = true;
sFocusedWidget = next;
gd->selectedIdx = nd->index;
@ -174,8 +172,6 @@ void widgetRadioOnKey(WidgetT *w, int32_t key, int32_t mod) {
if (prev) {
RadioDataT *pd = (RadioDataT *)prev->data;
RadioGroupDataT *gd = (RadioGroupDataT *)prev->parent->data;
w->focused = false;
prev->focused = true;
sFocusedWidget = prev;
gd->selectedIdx = pd->index;
@ -198,7 +194,7 @@ void widgetRadioOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
(void)root;
(void)vx;
(void)vy;
w->focused = true;
sFocusedWidget = w;
if (w->parent && w->parent->type == sRadioGroupTypeId) {
RadioDataT *rd = (RadioDataT *)w->data;
@ -290,7 +286,7 @@ void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap
drawTextAccel(d, ops, font, labelX, labelY, rd->text, fg, bg, false);
}
if (w->focused) {
if (w == sFocusedWidget) {
drawFocusRect(d, ops, labelX - 1, labelY - 1, labelW + 2, font->charHeight + 2, fg);
}
}

View file

@ -609,18 +609,9 @@ void widgetScrollPaneOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy
}
if (child && child->enabled && wclsHas(child, WGT_METHOD_ON_MOUSE)) {
// Clear old focus
if (sFocusedWidget && sFocusedWidget != child) {
sFocusedWidget->focused = false;
}
wclsOnMouse(child, root, vx, vy);
if (child->focused) {
sFocusedWidget = child;
}
} else {
hit->focused = true;
sFocusedWidget = hit;
}
wgtInvalidatePaint(hit);
@ -764,7 +755,7 @@ void widgetScrollPanePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B
}
}
if (w->focused) {
if (w == sFocusedWidget) {
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
drawFocusRect(d, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, fg);
}

View file

@ -134,7 +134,7 @@ void widgetSliderOnKey(WidgetT *w, int32_t key, int32_t mod) {
// The event loop checks sDragSlider on mouse-move to continue the drag.
void widgetSliderOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
(void)root;
hit->focused = true;
sFocusedWidget = hit;
SliderDataT *d = (SliderDataT *)hit->data;
int32_t range = d->maxValue - d->minValue;
@ -259,7 +259,7 @@ void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma
drawVLine(d, ops, thumbX + SLIDER_THUMB_W / 2, w->y + 3, w->h - 6, tickFg);
}
if (w->focused) {
if (w == sFocusedWidget) {
drawFocusRect(d, ops, w->x, w->y, w->w, w->h, fg);
}
}

View file

@ -333,7 +333,7 @@ void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod) {
// fixed-width font. Double-click selects all text (select-word doesn't
// make sense for numbers), entering edit mode to allow replacement.
void widgetSpinnerOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
hit->focused = true;
sFocusedWidget = hit;
SpinnerDataT *d = (SpinnerDataT *)hit->data;
int32_t btnX = hit->x + hit->w - SPINNER_BORDER - SPINNER_BTN_W;
@ -424,7 +424,7 @@ void widgetSpinnerPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const B
len = 0;
}
widgetTextEditPaintLine(disp, ops, font, colors, textX, textY, &d->buf[off], len, off, d->cursorPos, d->selStart, d->selEnd, fg, bg, w->focused, w->x + SPINNER_BORDER, btnX - SPINNER_PAD);
widgetTextEditPaintLine(disp, ops, font, colors, textX, textY, &d->buf[off], len, off, d->cursorPos, d->selStart, d->selEnd, fg, bg, w == sFocusedWidget, w->x + SPINNER_BORDER, btnX - SPINNER_PAD);
// Up button (top half)
int32_t btnTopH = w->h / 2;
@ -457,7 +457,7 @@ void widgetSpinnerPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const B
}
// Focus rect around entire widget
if (w->focused) {
if (w == sFocusedWidget) {
drawFocusRect(disp, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, fg);
}
}

View file

@ -272,15 +272,7 @@ void widgetSplitterOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
}
if (child && child->enabled && wclsHas(child, WGT_METHOD_ON_MOUSE)) {
if (sFocusedWidget && sFocusedWidget != child) {
sFocusedWidget->focused = false;
}
wclsOnMouse(child, root, vx, vy);
if (child->focused) {
sFocusedWidget = child;
}
}
wgtInvalidatePaint(hit);

View file

@ -291,7 +291,7 @@ void widgetTabControlOnKey(WidgetT *w, int32_t key, int32_t mod) {
// on the content area go through normal child hit-testing. Scroll
// arrow clicks adjust scrollOffset by 4 character widths at a time.
void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
hit->focused = true;
sFocusedWidget = hit;
TabControlDataT *d = (TabControlDataT *)hit->data;
AppContextT *ctx = (AppContextT *)root->userData;
const BitmapFontT *font = &ctx->font;
@ -494,7 +494,7 @@ void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B
drawTextAccel(d, ops, font, tabX + TAB_PAD_H, labelY, pd->title, colors->contentFg, tabFace, true);
if (isActive && w->focused) {
if (isActive && w == sFocusedWidget) {
drawFocusRect(d, ops, tabX + 3, ty + 3, tw - 6, th - 4, colors->contentFg);
}
}

View file

@ -1449,7 +1449,7 @@ navigation:
// complete.
void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
TextAreaDataT *ta = (TextAreaDataT *)w->data;
w->focused = true;
sFocusedWidget = w;
clearOtherSelections(w);
AppContextT *ctx = (AppContextT *)root->userData;
@ -1895,7 +1895,7 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
}
// Draw cursor (blinks at same rate as terminal cursor)
if (w->focused && sCursorBlinkOn) {
if (w == sFocusedWidget && sCursorBlinkOn) {
int32_t curDrawCol = ta->cursorCol - ta->scrollCol;
int32_t curDrawRow = ta->cursorRow - ta->scrollRow;
@ -2011,7 +2011,7 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
}
// Focus rect
if (w->focused) {
if (w == sFocusedWidget) {
drawFocusRect(d, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, fg);
}
}
@ -2129,7 +2129,7 @@ void widgetTextInputOnKey(WidgetT *w, int32_t key, int32_t mod) {
// position and registering sDragWidget.
void widgetTextInputOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
TextInputDataT *ti = (TextInputDataT *)w->data;
w->focused = true;
sFocusedWidget = w;
clearOtherSelections(w);
AppContextT *ctx = (AppContextT *)root->userData;
@ -2187,7 +2187,7 @@ void widgetTextInputPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bi
memcpy(dispBuf, ti->buf + off, dispLen);
}
widgetTextEditPaintLine(d, ops, font, colors, textX, textY, dispBuf, dispLen, off, ti->cursorPos, ti->selStart, ti->selEnd, fg, bg, w->focused && w->enabled, w->x + TEXT_INPUT_PAD, w->x + w->w - TEXT_INPUT_PAD);
widgetTextEditPaintLine(d, ops, font, colors, textX, textY, dispBuf, dispLen, off, ti->cursorPos, ti->selStart, ti->selEnd, fg, bg, w == sFocusedWidget && w->enabled, w->x + TEXT_INPUT_PAD, w->x + w->w - TEXT_INPUT_PAD);
}
}

View file

@ -439,7 +439,7 @@ static void paintTreeItems(WidgetT *parent, DisplayT *d, const BlitOpsT *ops, co
drawText(d, ops, font, textX, iy, ti->text, fg, bg, isSelected);
// Draw focus rectangle around cursor item in multi-select mode
if (multi && c == tv->selectedItem && treeView->focused) {
if (multi && c == tv->selectedItem && treeView == sFocusedWidget) {
uint32_t focusFg = isSelected ? colors->menuHighlightFg : colors->contentFg;
drawFocusRect(d, ops, baseX, iy, d->clipW, font->charHeight, focusFg);
}
@ -986,7 +986,7 @@ void widgetTreeViewLayout(WidgetT *w, const BitmapFontT *font) {
// tracking happens in widgetEvent.c during mouse-move.
void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
TreeViewDataT *tv = (TreeViewDataT *)hit->data;
hit->focused = true;
sFocusedWidget = hit;
AppContextT *ctx = (AppContextT *)root->userData;
const BitmapFontT *font = &ctx->font;
@ -1300,7 +1300,7 @@ void widgetTreeViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
}
}
if (w->focused) {
if (w == sFocusedWidget) {
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
drawFocusRect(d, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, fg);
}