From 7bc92549f715418930c220be7d1a31d8c597e95d Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Fri, 3 Apr 2026 21:22:08 -0500 Subject: [PATCH] Menu editor. Focus logic cleanup. Several bugs fixed. --- apps/dvxbasic/Makefile | 7 +- apps/dvxbasic/formrt/formrt.c | 151 ++++- apps/dvxbasic/formrt/formrt.h | 12 + apps/dvxbasic/ide/ideDesigner.c | 141 ++++- apps/dvxbasic/ide/ideDesigner.h | 17 + apps/dvxbasic/ide/ideMain.c | 140 ++++- apps/dvxbasic/ide/ideMenuEditor.c | 741 ++++++++++++++++++++++++ apps/dvxbasic/ide/ideMenuEditor.h | 16 + core/dvxApp.c | 17 +- core/dvxWidget.h | 1 - core/dvxWm.c | 25 +- core/dvxWm.h | 3 + core/widgetCore.c | 2 - core/widgetEvent.c | 14 +- core/widgetOps.c | 2 - widgets/ansiTerm/widgetAnsiTerm.c | 4 +- widgets/button/widgetButton.c | 4 +- widgets/checkbox/widgetCheckbox.c | 4 +- widgets/comboBox/widgetComboBox.c | 4 +- widgets/dropdown/widgetDropdown.c | 4 +- widgets/imageButton/widgetImageButton.c | 4 +- widgets/listBox/widgetListBox.c | 8 +- widgets/listView/widgetListView.c | 6 +- widgets/radio/widgetRadio.c | 8 +- widgets/scrollPane/widgetScrollPane.c | 13 +- widgets/slider/widgetSlider.c | 4 +- widgets/spinner/widgetSpinner.c | 6 +- widgets/splitter/widgetSplitter.c | 8 - widgets/tabControl/widgetTabControl.c | 4 +- widgets/textInput/widgetTextInput.c | 10 +- widgets/treeView/widgetTreeView.c | 6 +- 31 files changed, 1264 insertions(+), 122 deletions(-) create mode 100644 apps/dvxbasic/ide/ideMenuEditor.c create mode 100644 apps/dvxbasic/ide/ideMenuEditor.h diff --git a/apps/dvxbasic/Makefile b/apps/dvxbasic/Makefile index 0414d60..905d04e 100644 --- a/apps/dvxbasic/Makefile +++ b/apps/dvxbasic/Makefile @@ -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) diff --git a/apps/dvxbasic/formrt/formrt.c b/apps/dvxbasic/formrt/formrt.c index cc59295..7d89489 100644 --- a/apps/dvxbasic/formrt/formrt.c +++ b/apps/dvxbasic/formrt/formrt.c @@ -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 // ============================================================ diff --git a/apps/dvxbasic/formrt/formrt.h b/apps/dvxbasic/formrt/formrt.h index 6758787..2f6c8ce 100644 --- a/apps/dvxbasic/formrt/formrt.h +++ b/apps/dvxbasic/formrt/formrt.h @@ -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; // ============================================================ diff --git a/apps/dvxbasic/ide/ideDesigner.c b/apps/dvxbasic/ide/ideDesigner.c index b8d2f15..4591ac0 100644 --- a/apps/dvxbasic/ide/ideDesigner.c +++ b/apps/dvxbasic/ide/ideDesigner.c @@ -7,6 +7,7 @@ #include "ideDesigner.h" #include "dvxDraw.h" #include "dvxVideo.h" +#include "dvxWm.h" #include "stb_ds_wrap.h" #include @@ -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); diff --git a/apps/dvxbasic/ide/ideDesigner.h b/apps/dvxbasic/ide/ideDesigner.h index 2b9250f..4f61bc2 100644 --- a/apps/dvxbasic/ide/ideDesigner.h +++ b/apps/dvxbasic/ide/ideDesigner.h @@ -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) // ============================================================ diff --git a/apps/dvxbasic/ide/ideMain.c b/apps/dvxbasic/ide/ideMain.c index 010b68e..8f53d11 100644 --- a/apps/dvxbasic/ide/ideMain.c +++ b/apps/dvxbasic/ide/ideMain.c @@ -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 diff --git a/apps/dvxbasic/ide/ideMenuEditor.c b/apps/dvxbasic/ide/ideMenuEditor.c new file mode 100644 index 0000000..fbf87ce --- /dev/null +++ b/apps/dvxbasic/ide/ideMenuEditor.c @@ -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 +#include +#include +#include + +// ============================================================ +// 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; +} diff --git a/apps/dvxbasic/ide/ideMenuEditor.h b/apps/dvxbasic/ide/ideMenuEditor.h new file mode 100644 index 0000000..a583dba --- /dev/null +++ b/apps/dvxbasic/ide/ideMenuEditor.h @@ -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 diff --git a/core/dvxApp.c b/core/dvxApp.c index ab7057b..a5fc58d 100644 --- a/core/dvxApp.c +++ b/core/dvxApp.c @@ -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; diff --git a/core/dvxWidget.h b/core/dvxWidget.h index 8158572..73c2a93 100644 --- a/core/dvxWidget.h +++ b/core/dvxWidget.h @@ -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 diff --git a/core/dvxWm.c b/core/dvxWm.c index d4ecd6e..e81eb5a 100644 --- a/core/dvxWm.c +++ b/core/dvxWm.c @@ -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); diff --git a/core/dvxWm.h b/core/dvxWm.h index 9733818..8fa82ab 100644 --- a/core/dvxWm.h +++ b/core/dvxWm.h @@ -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); diff --git a/core/widgetCore.c b/core/widgetCore.c index 4a6f464..9e675de 100644 --- a/core/widgetCore.c +++ b/core/widgetCore.c @@ -214,8 +214,6 @@ void widgetClearFocus(WidgetT *root) { return; } - root->focused = false; - for (WidgetT *c = root->firstChild; c; c = c->nextSibling) { widgetClearFocus(c); } diff --git a/core/widgetEvent.c b/core/widgetEvent.c index b8c208d..9b71289 100644 --- a/core/widgetEvent.c +++ b/core/widgetEvent.c @@ -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; } } diff --git a/core/widgetOps.c b/core/widgetOps.c index eda8c26..5added2 100644 --- a/core/widgetOps.c +++ b/core/widgetOps.c @@ -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); diff --git a/widgets/ansiTerm/widgetAnsiTerm.c b/widgets/ansiTerm/widgetAnsiTerm.c index daefae2..bd1a7ac 100644 --- a/widgets/ansiTerm/widgetAnsiTerm.c +++ b/widgets/ansiTerm/widgetAnsiTerm.c @@ -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); } } diff --git a/widgets/button/widgetButton.c b/widgets/button/widgetButton.c index 07ddd38..a9002be 100644 --- a/widgets/button/widgetButton.c +++ b/widgets/button/widgetButton.c @@ -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); } diff --git a/widgets/checkbox/widgetCheckbox.c b/widgets/checkbox/widgetCheckbox.c index d7bf28b..34e98ce 100644 --- a/widgets/checkbox/widgetCheckbox.c +++ b/widgets/checkbox/widgetCheckbox.c @@ -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); } } diff --git a/widgets/comboBox/widgetComboBox.c b/widgets/comboBox/widgetComboBox.c index 58cafe2..4976003 100644 --- a/widgets/comboBox/widgetComboBox.c +++ b/widgets/comboBox/widgetComboBox.c @@ -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 diff --git a/widgets/dropdown/widgetDropdown.c b/widgets/dropdown/widgetDropdown.c index 6f994aa..028c73a 100644 --- a/widgets/dropdown/widgetDropdown.c +++ b/widgets/dropdown/widgetDropdown.c @@ -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); } } diff --git a/widgets/imageButton/widgetImageButton.c b/widgets/imageButton/widgetImageButton.c index 1128893..eb7fb25 100644 --- a/widgets/imageButton/widgetImageButton.c +++ b/widgets/imageButton/widgetImageButton.c @@ -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); diff --git a/widgets/listBox/widgetListBox.c b/widgets/listBox/widgetListBox.c index 80e593b..b76c290 100644 --- a/widgets/listBox/widgetListBox.c +++ b/widgets/listBox/widgetListBox.c @@ -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); } } diff --git a/widgets/listView/widgetListView.c b/widgets/listView/widgetListView.c index 30ef7ce..b497323 100644 --- a/widgets/listView/widgetListView.c +++ b/widgets/listView/widgetListView.c @@ -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); } } diff --git a/widgets/radio/widgetRadio.c b/widgets/radio/widgetRadio.c index ec54550..fe1a7ba 100644 --- a/widgets/radio/widgetRadio.c +++ b/widgets/radio/widgetRadio.c @@ -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); } } diff --git a/widgets/scrollPane/widgetScrollPane.c b/widgets/scrollPane/widgetScrollPane.c index e11c78b..000ce54 100644 --- a/widgets/scrollPane/widgetScrollPane.c +++ b/widgets/scrollPane/widgetScrollPane.c @@ -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); } diff --git a/widgets/slider/widgetSlider.c b/widgets/slider/widgetSlider.c index 2b94b76..1e38a90 100644 --- a/widgets/slider/widgetSlider.c +++ b/widgets/slider/widgetSlider.c @@ -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); } } diff --git a/widgets/spinner/widgetSpinner.c b/widgets/spinner/widgetSpinner.c index 693a5de..148502e 100644 --- a/widgets/spinner/widgetSpinner.c +++ b/widgets/spinner/widgetSpinner.c @@ -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); } } diff --git a/widgets/splitter/widgetSplitter.c b/widgets/splitter/widgetSplitter.c index 41cae69..08e43af 100644 --- a/widgets/splitter/widgetSplitter.c +++ b/widgets/splitter/widgetSplitter.c @@ -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); diff --git a/widgets/tabControl/widgetTabControl.c b/widgets/tabControl/widgetTabControl.c index 175309e..e913505 100644 --- a/widgets/tabControl/widgetTabControl.c +++ b/widgets/tabControl/widgetTabControl.c @@ -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); } } diff --git a/widgets/textInput/widgetTextInput.c b/widgets/textInput/widgetTextInput.c index a7b9432..f0383f6 100644 --- a/widgets/textInput/widgetTextInput.c +++ b/widgets/textInput/widgetTextInput.c @@ -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); } } diff --git a/widgets/treeView/widgetTreeView.c b/widgets/treeView/widgetTreeView.c index c4b83d3..2b46390 100644 --- a/widgets/treeView/widgetTreeView.c +++ b/widgets/treeView/widgetTreeView.c @@ -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); }