// 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 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 *radioCheckCb; 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", (int)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->radioCheck = wgtCheckboxIsChecked(sMed.radioCheckCb); 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.radioCheckCb, 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.radioCheckCb, mi->radioCheck); 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.radioCheckCb = wgtCheckbox(chkRow, "RadioCheck"); sMed.enabledCb = wgtCheckbox(chkRow, "Enabled"); wgtCheckboxSetChecked(sMed.enabledCb, true); // Listbox sMed.listBox = wgtListBox(root); sMed.listBox->weight = 100; sMed.listBox->onChange = 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; }