DVX_GUI/src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c
2026-10-20 17:24:15 -05:00

608 lines
18 KiB
C

// 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
// ============================================================
// 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);
}