// The MIT License (MIT) // // Copyright (C) 2026 Scott Duensing // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. // 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 "dvxDlg.h" #include "dvxWm.h" #include "box/box.h" #include "button/button.h" #include "checkbox/checkbox.h" #include "label/label.h" #include "listBox/listBox.h" #include "textInput/textInpt.h" #include #include #include #include // ============================================================ // Constants // ============================================================ #define ARROW_STR "-> " #define MED_MSG_BUF 128 // error-message scratch buffer #define MED_LABEL_BUF (FRM_MAX_TEXT + FRM_MENU_STACK_DEPTH * (int32_t)sizeof(ARROW_STR)) #define MED_DLG_W 360 #define MED_DLG_H 420 #define MED_ROW_SPACING 4 #define MED_CHECK_SPACING 12 #define MED_OK_SPACING 8 #define MED_LABEL_W 80 #define MED_ARROW_BTN_W 32 #define MED_OK_BTN_W 60 #define MED_DLG_TITLE "Menu Editor" // ============================================================ // Dialog state // ============================================================ typedef struct { // Fields are ordered by alignment to minimize struct padding. AppContextT *ctx; FrmModelT *form; // Working copy of menu items FrmMenuItemT *items; // stb_ds array int32_t selectedIdx; // Widgets WidgetT *captionInput; WidgetT *nameInput; WidgetT *shortcutInput; WidgetT *shortcutTextInput; WidgetT *checkedCb; WidgetT *radioCheckCb; WidgetT *enabledCb; WidgetT *popupCb; // Top-level popup (hidden from menu bar) WidgetT *listBox; bool done; bool accepted; bool nameAutoGen; // true = name was auto-generated, update on caption change } MnuEdStateT; static MnuEdStateT sMed; // Dynamic per-refresh display buffers (stb_ds). The listBox stores the pointer // array, so these must outlive each SetItems call. Freed on dialog teardown. static char **sLabelBufs = NULL; static const char **sLabels = NULL; // ============================================================ // Prototypes // ============================================================ static void applyFields(void); static void loadFields(void); static void onCancel(WidgetT *w); static void onCaptionChange(WidgetT *w); static void onClose(WindowT *win); static void onDelete(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 onNameChange(WidgetT *w); static void onNext(WidgetT *w); static void onOk(WidgetT *w); static void onOutdent(WidgetT *w); static void onShortcutChange(WidgetT *w); static void rebuildList(void); static void applyFields(void) { int32_t count = (int32_t)arrlen(sMed.items); if (sMed.selectedIdx < 0 || sMed.selectedIdx >= count) { return; } FrmMenuItemT *mi = &sMed.items[sMed.selectedIdx]; const char *cap = wgtGetText(sMed.captionInput); const char *nam = wgtGetText(sMed.nameInput); if (cap) { snprintf(mi->caption, FRM_MAX_TEXT, "%s", cap); } if (nam) { snprintf(mi->name, FRM_MAX_NAME, "%s", nam); } const char *key = wgtGetText(sMed.shortcutInput); const char *keyText = wgtGetText(sMed.shortcutTextInput); snprintf(mi->shortcut, sizeof(mi->shortcut), "%s", key ? key : ""); snprintf(mi->shortcutText, sizeof(mi->shortcutText), "%s", keyText ? keyText : ""); // Auto-generate name from caption if name is empty or was auto-generated if ((mi->name[0] == '\0' || sMed.nameAutoGen) && mi->caption[0] != '\0') { frmMenuAutoName(sMed.items, sMed.selectedIdx, mi->name, sizeof(mi->name)); wgtSetText(sMed.nameInput, mi->name); sMed.nameAutoGen = true; } mi->checked = wgtCheckboxIsChecked(sMed.checkedCb); mi->radioCheck = wgtCheckboxIsChecked(sMed.radioCheckCb); mi->enabled = wgtCheckboxIsChecked(sMed.enabledCb); // Popup checkbox only meaningful on top-level items; for nested // items `visible` stays true (the field is ignored there). if (mi->level == 0) { mi->visible = !wgtCheckboxIsChecked(sMed.popupCb); } else { mi->visible = true; } } 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, ""); wgtSetText(sMed.shortcutInput, ""); wgtSetText(sMed.shortcutTextInput, ""); wgtCheckboxSetChecked(sMed.checkedCb, false); wgtCheckboxSetChecked(sMed.radioCheckCb, false); wgtCheckboxSetChecked(sMed.enabledCb, true); wgtCheckboxSetChecked(sMed.popupCb, false); wgtSetEnabled(sMed.popupCb, false); sMed.nameAutoGen = true; // new blank item -- auto-gen eligible return; } FrmMenuItemT *mi = &sMed.items[sMed.selectedIdx]; sMed.nameAutoGen = (mi->name[0] == '\0'); wgtSetText(sMed.captionInput, mi->caption); wgtSetText(sMed.nameInput, mi->name); wgtSetText(sMed.shortcutInput, mi->shortcut); wgtSetText(sMed.shortcutTextInput, mi->shortcutText); wgtCheckboxSetChecked(sMed.checkedCb, mi->checked); wgtCheckboxSetChecked(sMed.radioCheckCb, mi->radioCheck); wgtCheckboxSetChecked(sMed.enabledCb, mi->enabled); // Popup (Visible=False) only applies to top-level menus. wgtCheckboxSetChecked(sMed.popupCb, mi->level == 0 && !mi->visible); wgtSetEnabled(sMed.popupCb, mi->level == 0); } bool mnuEditorDialog(AppContextT *ctx, FrmModelT *form) { memset(&sMed, 0, sizeof(sMed)); sMed.ctx = ctx; sMed.form = form; sMed.selectedIdx = 0; // Clone menu items as working copy, numbering them so the caller (and // onOk) can match each result item to the item it started as. int32_t count = (int32_t)arrlen(form->menuItems); for (int32_t i = 0; i < count; i++) { FrmMenuItemT mi = form->menuItems[i]; mi.id = i + 1; arrput(sMed.items, mi); } // If empty, start with one blank item so the user can type immediately if (arrlen(sMed.items) == 0) { FrmMenuItemT mi; frmMenuItemInit(&mi); arrput(sMed.items, mi); } // Create modal dialog WindowT *win = dvxCreateWindowCentered(ctx, MED_DLG_TITLE, MED_DLG_W, MED_DLG_H, false); if (!win) { arrfree(sMed.items); return false; } win->modal = true; win->onClose = onClose; win->maxW = win->w; win->maxH = win->h; WidgetT *root = wgtInitWindow(ctx, win); root->spacing = wgtPixels(MED_ROW_SPACING); // Caption row. Text inputs hold maxLen characters, so pass one less // than the field size to leave room for the terminator. WidgetT *capRow = wgtHBox(root); capRow->spacing = wgtPixels(MED_ROW_SPACING); WidgetT *capLbl = wgtLabel(capRow, "Caption:"); capLbl->minW = wgtPixels(MED_LABEL_W); sMed.captionInput = wgtTextInput(capRow, FRM_MAX_TEXT - 1); sMed.captionInput->weight = WGT_WEIGHT_FILL; sMed.captionInput->onChange = onCaptionChange; // Name row WidgetT *namRow = wgtHBox(root); namRow->spacing = wgtPixels(MED_ROW_SPACING); WidgetT *namLbl = wgtLabel(namRow, "Name:"); namLbl->minW = wgtPixels(MED_LABEL_W); sMed.nameInput = wgtTextInput(namRow, FRM_MAX_NAME - 1); sMed.nameInput->weight = WGT_WEIGHT_FILL; sMed.nameInput->onChange = onNameChange; // Shortcut row: the key that runs the item, and key text shown only WidgetT *keyRow = wgtHBox(root); keyRow->spacing = wgtPixels(MED_ROW_SPACING); WidgetT *keyLbl = wgtLabel(keyRow, "Shortcut:"); keyLbl->minW = wgtPixels(MED_LABEL_W); sMed.shortcutInput = wgtTextInput(keyRow, BAS_MENU_KEY_TEXT_MAX - 1); sMed.shortcutInput->weight = WGT_WEIGHT_FILL; sMed.shortcutInput->onChange = onShortcutChange; wgtLabel(keyRow, "Text:"); sMed.shortcutTextInput = wgtTextInput(keyRow, BAS_MENU_KEY_TEXT_MAX - 1); sMed.shortcutTextInput->weight = WGT_WEIGHT_FILL; sMed.shortcutTextInput->onChange = onShortcutChange; // Check row WidgetT *chkRow = wgtHBox(root); chkRow->spacing = wgtPixels(MED_CHECK_SPACING); sMed.checkedCb = wgtCheckbox(chkRow, BAS_PROP_CHECKED); sMed.radioCheckCb = wgtCheckbox(chkRow, BAS_PROP_RADIOCHECK); sMed.enabledCb = wgtCheckbox(chkRow, BAS_PROP_ENABLED); sMed.popupCb = wgtCheckbox(chkRow, "Popup"); wgtCheckboxSetChecked(sMed.enabledCb, true); // Listbox sMed.listBox = wgtListBox(root); sMed.listBox->weight = WGT_WEIGHT_FILL; sMed.listBox->onChange = onListClick; // Arrow buttons WidgetT *arrowRow = wgtHBox(root); arrowRow->spacing = wgtPixels(MED_ROW_SPACING); WidgetT *btnOut = wgtButton(arrowRow, "<-"); btnOut->onClick = onOutdent; btnOut->minW = wgtPixels(MED_ARROW_BTN_W); WidgetT *btnIn = wgtButton(arrowRow, "->"); btnIn->onClick = onIndent; btnIn->minW = wgtPixels(MED_ARROW_BTN_W); WidgetT *btnUp = wgtButton(arrowRow, "Up"); btnUp->onClick = onMoveUp; btnUp->minW = wgtPixels(MED_ARROW_BTN_W); WidgetT *btnDn = wgtButton(arrowRow, "Dn"); btnDn->onClick = onMoveDown; btnDn->minW = wgtPixels(MED_ARROW_BTN_W); // Action buttons WidgetT *actRow = wgtHBox(root); actRow->spacing = wgtPixels(MED_ROW_SPACING); 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(MED_OK_SPACING); WidgetT *btnOk = wgtButton(okRow, "OK"); btnOk->onClick = onOk; btnOk->minW = wgtPixels(MED_OK_BTN_W); WidgetT *btnCancel = wgtButton(okRow, "Cancel"); btnCancel->onClick = onCancel; btnCancel->minW = wgtPixels(MED_OK_BTN_W); // 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; // Cleanup display buffers for (int32_t i = 0; i < (int32_t)arrlen(sLabelBufs); i++) { free(sLabelBufs[i]); } arrfree(sLabelBufs); arrfree(sLabels); sLabelBufs = NULL; sLabels = NULL; return sMed.accepted; } static void onCancel(WidgetT *w) { (void)w; sMed.accepted = false; sMed.done = true; } static void onCaptionChange(WidgetT *w) { (void)w; applyFields(); rebuildList(); } // The window close box cancels, like the Cancel button. static void onClose(WindowT *win) { (void)win; onCancel(NULL); } static void onDelete(WidgetT *w) { (void)w; int32_t count = (int32_t)arrlen(sMed.items); if (sMed.selectedIdx < 0 || sMed.selectedIdx >= count) { return; } applyFields(); // The selected item goes with its children frmMenuRemove(&sMed.items, sMed.selectedIdx); count = (int32_t)arrlen(sMed.items); if (sMed.selectedIdx >= count) { sMed.selectedIdx = count - 1; } rebuildList(); loadFields(); wgtSetFocused(sMed.captionInput); } static void onIndent(WidgetT *w) { (void)w; applyFields(); if (frmMenuIndent(sMed.items, sMed.selectedIdx, 1)) { rebuildList(); } } // A new item after the current item's subtree, at the same level. static void onInsert(WidgetT *w) { (void)w; int32_t count = (int32_t)arrlen(sMed.items); bool valid = sMed.selectedIdx >= 0 && sMed.selectedIdx < count; applyFields(); int32_t at = frmMenuInsert(&sMed.items, valid ? frmMenuSubtreeEnd(sMed.items, sMed.selectedIdx) : count, valid ? sMed.items[sMed.selectedIdx].level : 0); if (at >= 0) { sMed.selectedIdx = at; } rebuildList(); loadFields(); wgtSetFocused(sMed.captionInput); } 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(); } static void onMoveDown(WidgetT *w) { (void)w; applyFields(); int32_t to = frmMenuMove(&sMed.items, sMed.selectedIdx, 1); if (to >= 0) { sMed.selectedIdx = to; rebuildList(); } } static void onMoveUp(WidgetT *w) { (void)w; applyFields(); int32_t to = frmMenuMove(&sMed.items, sMed.selectedIdx, -1); if (to >= 0) { sMed.selectedIdx = to; rebuildList(); } } static void onNameChange(WidgetT *w) { (void)w; sMed.nameAutoGen = false; // user took over applyFields(); rebuildList(); } static void onNext(WidgetT *w) { (void)w; int32_t count = (int32_t)arrlen(sMed.items); applyFields(); if (sMed.selectedIdx < count - 1) { sMed.selectedIdx++; } else { // A new item at the end, at the last item's level int32_t at = frmMenuInsert(&sMed.items, count, count > 0 ? sMed.items[count - 1].level : 0); if (at >= 0) { sMed.selectedIdx = at; } } rebuildList(); loadFields(); wgtSetFocused(sMed.captionInput); } // The levels form a tree, and every name is an identifier, unique among // the items and not the form's or a control's (a name with spaces would // be written as "Begin Menu File Menu" and mis-parsed); every shortcut is // a key. Then the items replace the form's, ContextMenu bindings // following renamed items. static void onOk(WidgetT *w) { (void)w; applyFields(); frmMenuStripBlank(&sMed.items); char reason[MED_MSG_BUF]; int32_t badIdx = frmModelMenuValidate(sMed.form, sMed.items, reason, sizeof(reason)); if (badIdx >= 0) { dvxErrorBox(sMed.ctx, MED_DLG_TITLE, reason); sMed.selectedIdx = badIdx; rebuildList(); loadFields(); wgtSetFocused(sMed.captionInput); return; } if (!frmModelMenuReplace(sMed.form, sMed.items)) { dvxErrorBox(sMed.ctx, MED_DLG_TITLE, "Not enough memory to apply the menus."); return; } sMed.accepted = true; sMed.done = true; } static void onOutdent(WidgetT *w) { (void)w; applyFields(); if (frmMenuIndent(sMed.items, sMed.selectedIdx, -1)) { rebuildList(); } } static void onShortcutChange(WidgetT *w) { (void)w; applyFields(); } static void rebuildList(void) { int32_t count = (int32_t)arrlen(sMed.items); // The listBox stores pointers, not copies -- so we must keep these buffers // alive until the next rebuild or dialog destroy. Free prior pass first. for (int32_t i = 0; i < (int32_t)arrlen(sLabelBufs); i++) { free(sLabelBufs[i]); } arrsetlen(sLabelBufs, 0); arrsetlen(sLabels, 0); for (int32_t i = 0; i < count; i++) { char buf[MED_LABEL_BUF]; int32_t pos = 0; buf[0] = '\0'; for (int32_t lv = 0; lv < sMed.items[i].level; lv++) { pos += snprintf(buf + pos, sizeof(buf) - pos, "%s", ARROW_STR); } const char *cap = sMed.items[i].caption; if (cap[0] == '-') { snprintf(buf + pos, sizeof(buf) - pos, "--------"); } else if (cap[0]) { snprintf(buf + pos, sizeof(buf) - pos, "%s", cap); } else { snprintf(buf + pos, sizeof(buf) - pos, "(%s)", sMed.items[i].name[0] ? sMed.items[i].name : "untitled"); } arrput(sLabelBufs, strdup(buf)); } for (int32_t i = 0; i < count; i++) { arrput(sLabels, (const char *)sLabelBufs[i]); } wgtListBoxSetItems(sMed.listBox, sLabels, count); wgtListBoxSetSelected(sMed.listBox, sMed.selectedIdx); }