739 lines
20 KiB
C
739 lines
20 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 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 *popupCb; // Top-level popup (hidden from menu bar)
|
|
WidgetT *listBox;
|
|
} 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 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 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 rebuildList(void);
|
|
|
|
|
|
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);
|
|
// Popup checkbox only meaningful on top-level items; for nested
|
|
// items `visible` stays true (the field is ignored there).
|
|
if (mi->level == 0 && sMed.popupCb) {
|
|
mi->visible = !wgtCheckboxIsChecked(sMed.popupCb);
|
|
} else {
|
|
mi->visible = true;
|
|
}
|
|
}
|
|
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
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);
|
|
if (sMed.popupCb) {
|
|
wgtCheckboxSetChecked(sMed.popupCb, false);
|
|
wgtSetEnabled(sMed.popupCb, false);
|
|
}
|
|
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);
|
|
if (sMed.popupCb) {
|
|
// 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, 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;
|
|
mi.visible = 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 = WGT_WEIGHT_FILL;
|
|
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 = WGT_WEIGHT_FILL;
|
|
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");
|
|
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(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;
|
|
|
|
// 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();
|
|
}
|
|
|
|
|
|
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);
|
|
}
|
|
|
|
|
|
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();
|
|
}
|
|
|
|
|
|
static void onInsert(WidgetT *w) {
|
|
(void)w;
|
|
int32_t count = (int32_t)arrlen(sMed.items);
|
|
|
|
applyFields();
|
|
|
|
DsgnMenuItemT mi;
|
|
memset(&mi, 0, sizeof(mi));
|
|
mi.enabled = true;
|
|
mi.visible = 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);
|
|
}
|
|
|
|
|
|
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;
|
|
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();
|
|
}
|
|
|
|
|
|
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();
|
|
}
|
|
|
|
|
|
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 {
|
|
// Append new item
|
|
DsgnMenuItemT mi;
|
|
memset(&mi, 0, sizeof(mi));
|
|
mi.enabled = true;
|
|
mi.visible = 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);
|
|
}
|
|
|
|
|
|
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') {
|
|
dvxErrorBox(sMed.ctx, "Menu Editor", "All menu items must have a Name.");
|
|
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);
|
|
dvxErrorBox(sMed.ctx, "Menu Editor", msg);
|
|
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;
|
|
}
|
|
|
|
|
|
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();
|
|
}
|
|
|
|
|
|
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[DSGN_MAX_TEXT + 32];
|
|
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);
|
|
}
|