DVX_GUI/src/apps/kpunch/dvxbasic/ide/ideProperties.c

1505 lines
50 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.
// ideProperties.c -- DVX BASIC form designer properties window
//
// A floating window with a TreeView listing all controls on the
// form (for selection and drag-reorder) and a ListView showing
// editable properties of the selected control. Double-click a
// property value to edit it via an InputBox dialog.
#include "ideProperties.h"
#include "../formrt/formrt.h"
#include "../formrt/frmParser.h"
#include "dvxDlg.h"
#include "dvxWm.h"
#include "box/box.h"
#include "listView/listView.h"
#include "splitter/splitter.h"
#include "treeView/treeView.h"
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
// ============================================================
// Constants
// ============================================================
#define PRP_WIN_W 220
#define PRP_WIN_H 400
#define PRP_WIN_RIGHT_MARGIN 10 // gap between the window and the screen edge
#define PRP_WIN_Y 30
#define PRP_INT_BUF 32 // decimal int32_t text
#define PRP_QUERY_BUF 512 // SQL probe scratch buffer
#define PRP_PROMPT_BUF 128 // input-dialog prompt buffer
#define PRP_TITLE_SUFFIX_PAD 16 // pad over DSGN_MAX_TEXT for " [Design]" suffix
#define DSGN_NO_INDEX (-1) // tree label has no "(idx)" / control not in an array
#define PROP_TYPE_STRING WGT_IFACE_STRING
#define PROP_TYPE_INT WGT_IFACE_INT
#define PROP_TYPE_BOOL WGT_IFACE_BOOL
#define PROP_TYPE_ENUM WGT_IFACE_ENUM
#define PROP_TYPE_READONLY 255
#define PROP_TYPE_LAYOUT 251
#define PROP_TYPE_DATASOURCE 254
#define PROP_TYPE_DATAFIELD 253
#define PROP_TYPE_RECORDSRC 252
// No fixed caps: field and table arrays are heap-allocated and sized to the
// actual count reported by SQL.
// ============================================================
// SQL engine function-pointer types (resolved at runtime via dlsym so the
// designer does not hard-link the optional dvxSql* engine). Declared once
// here; the column-name and table-name probes resolve the subset they use.
// ============================================================
typedef int32_t (*SqlOpenFnT)(const char *);
typedef void (*SqlCloseFnT)(int32_t);
typedef int32_t (*SqlQueryFnT)(int32_t, const char *);
typedef int32_t (*SqlFieldCountFnT)(int32_t);
typedef const char *(*SqlFieldNameFnT)(int32_t, int32_t);
typedef bool (*SqlNextFnT)(int32_t);
typedef const char *(*SqlFieldTextFnT)(int32_t, int32_t);
typedef void (*SqlFreeResultFnT)(int32_t);
// ============================================================
// Module state
// ============================================================
static DsgnStateT *sDs = NULL;
static WindowT *sPrpWin = NULL;
static WidgetT *sTree = NULL;
static WidgetT *sPropList = NULL;
static AppContextT *sPrpCtx = NULL;
static bool sUpdating = false;
static char **sTreeLabels = NULL; // stb_ds array of strdup'd strings
#define PRP_CELL_COLUMNS 2 // property grid: name column + value column
#define PRP_MAX_LAYOUT_NAMES 32 // designer dropdown: max layout container types
#define PRP_MAX_DATA_NAMES 16 // designer dropdown: max Data controls (excludes "(none)")
#define PRP_SELECT_PREFIX "SELECT " // RecordSource that is already a query
#define PRP_SELECT_PREFIX_LEN ((int32_t)(sizeof(PRP_SELECT_PREFIX) - 1))
#define PRP_DIALOG_TITLE "Properties"
// Tree parent recorded for a control by collectTreeOrder, parallel to the
// collected control array.
typedef struct {
char name[DSGN_MAX_NAME];
} PrpParentT;
static char **sCellData = NULL; // stb_ds array of strdup'd strings
// ============================================================
// Prototypes
// ============================================================
static void addPropRow(const char *name, const char *value);
static void cascadeToChildren(DsgnStateT *ds, const char *parentName, bool visible, bool enabled, int32_t depth);
static void collectTreeOrder(WidgetT *parent, DsgnControlT **srcArr, int32_t srcCount, DsgnControlT ***outArr, PrpParentT **outParents, const char *parentName);
static WidgetT *findTreeItemByName(WidgetT *parent, const char *name, int32_t index);
static void freeCellData(void);
static void freeTreeLabels(void);
static int32_t getDataFieldNames(const DsgnStateT *ds, const char *dataSourceName, char (**outNames)[DSGN_MAX_NAME]);
static uint8_t getPropType(const char *propName, const char *typeName);
static int32_t getTableNames(const char *dbName, char (**outNames)[DSGN_MAX_NAME]);
static void onPropDblClick(WidgetT *w);
static void onPrpClose(WindowT *win);
static void onTreeChange(WidgetT *w);
static void onTreeItemClick(WidgetT *w);
static void parseTreeLabel(const char *label, char *outName, int32_t outNameSize, int32_t *outIndex);
static bool propDropdownOrInput(AppContextT *ctx, const char *propName, const char *selectPrompt, char (*names)[DSGN_MAX_NAME], int32_t count, const char *curValue, char *newValue, int32_t newValueSize);
static void resolveDbPath(const char *dbName, char *out, int32_t outSize);
static bool treeOrderMatches(void);
static bool validateNewName(const char *name, int32_t maxLen, const char *exceptName);
static void addPropRow(const char *name, const char *value) {
arrput(sCellData, strdup(name));
arrput(sCellData, strdup(value ? value : ""));
}
// Recursively apply Visible or Enabled to all descendants of a
// container control.
static void cascadeToChildren(DsgnStateT *ds, const char *parentName, bool visible, bool enabled, int32_t depth) {
int32_t count = (int32_t)arrlen(ds->form->controls);
for (int32_t i = 0; i < count; i++) {
DsgnControlT *child = ds->form->controls[i];
if (strcasecmp(child->parentName, parentName) != 0) {
continue;
}
if (child->widget) {
wgtSetVisible(child->widget, visible);
wgtSetEnabled(child->widget, enabled);
}
// Recurse into nested containers. Guard against a self-parenting
// container and runaway cycles (A->B->A) via a depth cap so the
// recursion always terminates.
if (dsgnIsContainer(child->typeName) &&
strcasecmp(child->name, parentName) != 0 &&
depth < DSGN_MAX_NEST_DEPTH) {
cascadeToChildren(ds, child->name, visible, enabled, depth + 1);
}
}
}
// Walk tree items recursively, collecting controls in tree order together
// with the tree parent each one sits under. The controls themselves are
// not modified; the caller decides whether to apply the new parents.
static void collectTreeOrder(WidgetT *parent, DsgnControlT **srcArr, int32_t srcCount, DsgnControlT ***outArr, PrpParentT **outParents, const char *parentName) {
for (WidgetT *item = parent->firstChild; item; item = item->nextSibling) {
const char *label = (const char *)item->userData;
if (!label) {
continue;
}
char itemName[DSGN_MAX_NAME];
int32_t itemIndex = DSGN_NO_INDEX;
parseTreeLabel(label, itemName, DSGN_MAX_NAME, &itemIndex);
for (int32_t i = 0; i < srcCount; i++) {
if (strcmp(srcArr[i]->name, itemName) == 0 && srcArr[i]->index == itemIndex) {
PrpParentT tp;
snprintf(tp.name, DSGN_MAX_NAME, "%s", parentName);
arrput(*outArr, srcArr[i]);
arrput(*outParents, tp);
// Recurse into children (for containers)
if (item->firstChild) {
collectTreeOrder(item, srcArr, srcCount, outArr, outParents, itemName);
}
break;
}
}
}
}
// Walk tree items recursively to find the one matching a control name.
static WidgetT *findTreeItemByName(WidgetT *parent, const char *name, int32_t index) {
for (WidgetT *item = parent->firstChild; item; item = item->nextSibling) {
const char *label = (const char *)item->userData;
if (label) {
char itemName[DSGN_MAX_NAME];
int32_t itemIndex = DSGN_NO_INDEX;
parseTreeLabel(label, itemName, DSGN_MAX_NAME, &itemIndex);
if (itemIndex == index && strcmp(itemName, name) == 0) {
return item;
}
}
// Recurse into children (containers)
WidgetT *found = findTreeItemByName(item, name, index);
if (found) {
return found;
}
}
return NULL;
}
static void freeCellData(void) {
int32_t count = (int32_t)arrlen(sCellData);
for (int32_t i = 0; i < count; i++) {
free(sCellData[i]);
}
arrsetlen(sCellData, 0);
}
static void freeTreeLabels(void) {
int32_t count = (int32_t)arrlen(sTreeLabels);
for (int32_t i = 0; i < count; i++) {
free(sTreeLabels[i]);
}
arrsetlen(sTreeLabels, 0);
}
// getDataFieldNames -- query column names from a Data control's database
//
// Finds the named Data control in the designer, reads its DatabaseName
// and RecordSource properties, opens the database via dvxSql* (resolved
// through dlsym), and heap-allocates *outNames sized to the actual column
// count. Caller must free *outNames. Returns the count of names (0 if
// anything fails).
static int32_t getDataFieldNames(const DsgnStateT *ds, const char *dataSourceName, char (**outNames)[DSGN_MAX_NAME]) {
*outNames = NULL;
if (!ds || !ds->form || !dataSourceName || !dataSourceName[0]) {
return 0;
}
// Find the Data control in the designer
const char *dbName = NULL;
const char *recSrc = NULL;
int32_t ctrlCount = (int32_t)arrlen(ds->form->controls);
for (int32_t i = 0; i < ctrlCount; i++) {
DsgnControlT *ctrl = ds->form->controls[i];
if (strcasecmp(ctrl->typeName, "Data") != 0 || strcasecmp(ctrl->name, dataSourceName) != 0) {
continue;
}
dbName = dsgnControlGetPropValue(ctrl, "DatabaseName");
recSrc = dsgnControlGetPropValue(ctrl, "RecordSource");
break;
}
if (!dbName || !dbName[0] || !recSrc || !recSrc[0]) {
return 0;
}
// Resolve SQL functions via dlsym
SqlOpenFnT sqlOpen = (SqlOpenFnT)dlsym(NULL, "_dvxSqlOpen");
SqlCloseFnT sqlClose = (SqlCloseFnT)dlsym(NULL, "_dvxSqlClose");
SqlQueryFnT sqlQuery = (SqlQueryFnT)dlsym(NULL, "_dvxSqlQuery");
SqlFieldCountFnT sqlFieldCount = (SqlFieldCountFnT)dlsym(NULL, "_dvxSqlFieldCount");
SqlFieldNameFnT sqlFieldName = (SqlFieldNameFnT)dlsym(NULL, "_dvxSqlFieldName");
SqlFreeResultFnT sqlFreeResult = (SqlFreeResultFnT)dlsym(NULL, "_dvxSqlFreeResult");
if (!sqlOpen || !sqlClose || !sqlQuery || !sqlFieldCount || !sqlFieldName || !sqlFreeResult) {
return 0;
}
char fullPath[DVX_MAX_PATH];
resolveDbPath(dbName, fullPath, sizeof(fullPath));
int32_t db = sqlOpen(fullPath);
if (db <= 0) {
return 0;
}
// Query with LIMIT 0 to get column names without fetching rows
char query[PRP_QUERY_BUF];
if (strncasecmp(recSrc, PRP_SELECT_PREFIX, PRP_SELECT_PREFIX_LEN) == 0) {
snprintf(query, sizeof(query), "%s LIMIT 0", recSrc);
} else {
snprintf(query, sizeof(query), "SELECT * FROM %s LIMIT 0", recSrc);
}
int32_t rs = sqlQuery(db, query);
if (rs <= 0) {
sqlClose(db);
return 0;
}
int32_t colCount = sqlFieldCount(rs);
if (colCount <= 0) {
sqlFreeResult(rs);
sqlClose(db);
return 0;
}
char (*names)[DSGN_MAX_NAME] = (char (*)[DSGN_MAX_NAME])calloc(colCount, DSGN_MAX_NAME);
if (!names) {
sqlFreeResult(rs);
sqlClose(db);
return 0;
}
int32_t count = 0;
for (int32_t i = 0; i < colCount; i++) {
const char *name = sqlFieldName(rs, i);
if (name) {
snprintf(names[count++], DSGN_MAX_NAME, "%s", name);
}
}
sqlFreeResult(rs);
sqlClose(db);
*outNames = names;
return count;
}
// Determine the editor type of a property by name. Designer-only rows
// and the special editors come first, then the widget's own interface
// (which wins over the runtime tables, matching setProp dispatch), then
// the runtime form/control property tables for the rest.
static uint8_t getPropType(const char *propName, const char *typeName) {
// Designer-only read-only rows
if (strcasecmp(propName, "Type") == 0) { return PROP_TYPE_READONLY; }
if (strcasecmp(propName, "Index") == 0) { return PROP_TYPE_READONLY; }
// Designer-editable at design time even though the runtime rejects
// assignment (Name is the object identity, Layout the container type).
if (strcasecmp(propName, "Name") == 0) { return PROP_TYPE_STRING; }
if (strcasecmp(propName, "Layout") == 0) { return PROP_TYPE_LAYOUT; }
// Special editors
if (strcasecmp(propName, "DataSource") == 0) { return PROP_TYPE_DATASOURCE; }
if (strcasecmp(propName, "DataField") == 0) { return PROP_TYPE_DATAFIELD; }
if (strcasecmp(propName, "RecordSource") == 0) { return PROP_TYPE_RECORDSRC; }
if (strcasecmp(propName, "KeyColumn") == 0) { return PROP_TYPE_DATAFIELD; }
if (strcasecmp(propName, "MasterSource") == 0) { return PROP_TYPE_DATASOURCE; }
if (strcasecmp(propName, "MasterField") == 0) { return PROP_TYPE_DATAFIELD; }
if (strcasecmp(propName, "DetailField") == 0) { return PROP_TYPE_DATAFIELD; }
const WgtPropDescT *p = findIfaceProp(typeName, propName);
if (p) {
return p->setFn ? p->type : PROP_TYPE_READONLY;
}
// Runtime tables: the form object when no control is selected, else
// the common control properties. Non-writable entries are read-only.
const BasPropDescT *pd = typeName[0] ? basFormRtFindCommonProp(propName) : basFormRtFindFormProp(propName);
if (pd) {
return pd->writable ? pd->type : PROP_TYPE_READONLY;
}
// Designer storage fields not in the runtime tables (MinWidth alias
// Width etc. are; this covers any table drift) and unknown keys.
return dsgnFindIntProp(propName) ? PROP_TYPE_INT : PROP_TYPE_STRING;
}
// getTableNames -- query table names from a SQLite database
//
// Heap-allocates *outNames and grows it as tables are enumerated. Caller
// must free *outNames. Returns the number of tables found (0 if anything
// fails).
static int32_t getTableNames(const char *dbName, char (**outNames)[DSGN_MAX_NAME]) {
*outNames = NULL;
if (!dbName || !dbName[0]) {
return 0;
}
SqlOpenFnT sqlOpen = (SqlOpenFnT)dlsym(NULL, "_dvxSqlOpen");
SqlCloseFnT sqlClose = (SqlCloseFnT)dlsym(NULL, "_dvxSqlClose");
SqlQueryFnT sqlQuery = (SqlQueryFnT)dlsym(NULL, "_dvxSqlQuery");
SqlNextFnT sqlNext = (SqlNextFnT)dlsym(NULL, "_dvxSqlNext");
SqlFieldTextFnT sqlFieldText = (SqlFieldTextFnT)dlsym(NULL, "_dvxSqlFieldText");
SqlFreeResultFnT sqlFreeResult = (SqlFreeResultFnT)dlsym(NULL, "_dvxSqlFreeResult");
if (!sqlOpen || !sqlClose || !sqlQuery || !sqlNext || !sqlFieldText || !sqlFreeResult) {
return 0;
}
char fullPath[DVX_MAX_PATH];
resolveDbPath(dbName, fullPath, sizeof(fullPath));
int32_t db = sqlOpen(fullPath);
if (db <= 0) {
return 0;
}
int32_t rs = sqlQuery(db, "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name");
if (rs <= 0) {
sqlClose(db);
return 0;
}
// Grow with realloc doubling
char (*names)[DSGN_MAX_NAME] = NULL;
int32_t count = 0;
int32_t cap = 0;
while (sqlNext(rs)) {
const char *name = sqlFieldText(rs, 0);
if (!name || !name[0]) {
continue;
}
if (count >= cap) {
int32_t newCap = cap == 0 ? 16 : cap * 2;
char (*grown)[DSGN_MAX_NAME] = (char (*)[DSGN_MAX_NAME])realloc(names, (size_t)newCap * DSGN_MAX_NAME);
if (!grown) {
break;
}
names = grown;
cap = newCap;
}
snprintf(names[count++], DSGN_MAX_NAME, "%s", name);
}
sqlFreeResult(rs);
sqlClose(db);
*outNames = names;
return count;
}
static void onPropDblClick(WidgetT *w) {
if (!sDs || !sDs->form || !sPropList || !sPrpCtx) {
return;
}
int32_t row = wgtListViewGetSelected(w);
int32_t rowCount = (int32_t)arrlen(sCellData) / PRP_CELL_COLUMNS;
if (row < 0 || row >= rowCount) {
return;
}
const char *propName = sCellData[row * PRP_CELL_COLUMNS];
const char *curValue = sCellData[row * PRP_CELL_COLUMNS + 1];
// Layout -- select from discovered layout containers
if (strcasecmp(propName, "Layout") == 0) {
// Discover available layout types from loaded widget interfaces.
// A layout container is isContainer with WGT_CREATE_PARENT (no extra args).
const char *layoutNames[PRP_MAX_LAYOUT_NAMES];
int32_t layoutCount = 0;
int32_t ifaceTotal = wgtIfaceCount();
for (int32_t i = 0; i < ifaceTotal && layoutCount < PRP_MAX_LAYOUT_NAMES; i++) {
const WgtIfaceT *iface = wgtIfaceAt(i, NULL);
if (iface && iface->isContainer && iface->createSig == WGT_CREATE_PARENT && iface->basName) {
layoutNames[layoutCount++] = iface->basName;
}
}
if (layoutCount == 0) {
return;
}
// Determine whose layout we're changing
char *layoutField = NULL;
if (sDs->selectedIdx < 0) {
layoutField = sDs->form->layout;
} else {
DsgnControlT *ctrl = sDs->form->controls[sDs->selectedIdx];
// A freshly placed container has no Layout entry yet; create
// the default so the grid row is editable.
if (!dsgnControlGetPropValue(ctrl, "Layout")) {
dsgnSetPropValue(ctrl, "Layout", DSGN_VBOX_LAYOUT);
}
for (int32_t pi = 0; pi < ctrl->propCount; pi++) {
if (strcasecmp(ctrl->props[pi].name, "Layout") == 0) {
layoutField = ctrl->props[pi].value;
break;
}
}
if (!layoutField) {
return;
}
}
// Find current selection
int32_t defIdx = 0;
for (int32_t i = 0; i < layoutCount; i++) {
if (strcasecmp(layoutField, layoutNames[i]) == 0) {
defIdx = i;
break;
}
}
int32_t chosenIdx = 0;
if (!dvxChoiceDialog(sPrpCtx, "Layout", "Select layout type:", layoutNames, layoutCount, defIdx, &chosenIdx)) {
return;
}
snprintf(layoutField, DSGN_MAX_NAME, "%s", layoutNames[chosenIdx]);
sDs->form->dirty = true;
// Rebuild the form designer to apply the new layout
if (sDs->formWin && sDs->formWin->widgetRoot) {
WidgetT *root = sDs->formWin->widgetRoot;
wgtDestroyChildren(root);
// The root content box must use the FORM's layout, not whatever
// layoutField points at (it points at a selected container's
// Layout prop when one is selected). Per-container layouts are
// applied separately inside dsgnCreateWidgets.
sDs->form->contentBox = basFormRtCreateContentBox(root, sDs->form->layout);
dsgnRebuildWidgets(sDs);
dvxInvalidateWindow(sPrpCtx, sDs->formWin);
}
prpRefresh(sDs);
return;
}
// Get the control's type name for iface lookup
const char *ctrlTypeName = "";
int32_t ctrlCount = (int32_t)arrlen(sDs->form->controls);
if (sDs->selectedIdx >= 0 && sDs->selectedIdx < ctrlCount) {
ctrlTypeName = sDs->form->controls[sDs->selectedIdx]->typeName;
}
uint8_t propType = getPropType(propName, ctrlTypeName);
if (propType == PROP_TYPE_READONLY) {
return;
}
char newValue[DSGN_MAX_TEXT];
if (propType == PROP_TYPE_BOOL) {
// Toggle boolean on double-click -- no input box
bool cur = frmParseBool(curValue);
snprintf(newValue, sizeof(newValue), "%s", cur ? "False" : "True");
} else if (propType == PROP_TYPE_ENUM) {
// Enum: cycle to next value on double-click
const WgtPropDescT *pd = findIfaceProp(ctrlTypeName, propName);
if (!pd || !pd->enumNames) {
return;
}
// Find current value and advance to next
int32_t enumCount = 0;
int32_t curIdx = 0;
while (pd->enumNames[enumCount]) {
if (strcasecmp(pd->enumNames[enumCount], curValue) == 0) {
curIdx = enumCount;
}
enumCount++;
}
if (enumCount == 0) {
return;
}
int32_t nextIdx = (curIdx + 1) % enumCount;
snprintf(newValue, sizeof(newValue), "%s", pd->enumNames[nextIdx]);
} else if (propType == PROP_TYPE_DATASOURCE) {
// Show dropdown of Data control names on the form
int32_t formCtrlCount = (int32_t)arrlen(sDs->form->controls);
const char *dataNames[PRP_MAX_DATA_NAMES + 1];
int32_t dataCount = 0;
// First entry is "(none)" to clear binding
dataNames[dataCount++] = "(none)";
for (int32_t i = 0; i < formCtrlCount && dataCount < PRP_MAX_DATA_NAMES; i++) {
if (strcasecmp(sDs->form->controls[i]->typeName, "Data") == 0) {
dataNames[dataCount++] = sDs->form->controls[i]->name;
}
}
if (dataCount <= 1) {
return;
}
// Find current selection
int32_t defIdx = 0;
for (int32_t i = 1; i < dataCount; i++) {
if (strcasecmp(dataNames[i], curValue) == 0) {
defIdx = i;
break;
}
}
int32_t chosenIdx = 0;
if (!dvxChoiceDialog(sPrpCtx, "DataSource", "Select Data control:", dataNames, dataCount, defIdx, &chosenIdx)) {
return;
}
if (chosenIdx == 0) {
newValue[0] = '\0';
} else {
snprintf(newValue, sizeof(newValue), "%s", dataNames[chosenIdx]);
}
} else if (propType == PROP_TYPE_DATAFIELD) {
// Show dropdown of column names from the Data control's database.
// If the selected control IS a Data control (e.g. editing KeyColumn),
// use its own name. Otherwise look up its DataSource property.
int32_t selCount = (int32_t)arrlen(sDs->form->controls);
const char *dataSrc = "";
if (sDs->selectedIdx >= 0 && sDs->selectedIdx < selCount) {
DsgnControlT *selCtrl = sDs->form->controls[sDs->selectedIdx];
if (strcasecmp(selCtrl->typeName, "Data") == 0) {
dataSrc = selCtrl->name;
} else {
const char *dataSrcProp = dsgnControlGetPropValue(selCtrl, "DataSource");
if (dataSrcProp) {
dataSrc = dataSrcProp;
}
}
}
char (*fieldNames)[DSGN_MAX_NAME] = NULL;
int32_t fieldCount = getDataFieldNames(sDs, dataSrc, &fieldNames);
bool ok = propDropdownOrInput(sPrpCtx, propName, "Select column:", fieldNames, fieldCount, curValue, newValue, sizeof(newValue));
free(fieldNames);
if (!ok) {
return;
}
} else if (propType == PROP_TYPE_RECORDSRC) {
// Show dropdown of table names from the Data control's database
// Find DatabaseName on this control (which is a Data control)
int32_t selCount = (int32_t)arrlen(sDs->form->controls);
const char *dbName = "";
if (sDs->selectedIdx >= 0 && sDs->selectedIdx < selCount) {
DsgnControlT *selCtrl = sDs->form->controls[sDs->selectedIdx];
const char *dbNameProp = dsgnControlGetPropValue(selCtrl, "DatabaseName");
if (dbNameProp) {
dbName = dbNameProp;
}
}
char (*tableNames)[DSGN_MAX_NAME] = NULL;
int32_t tableCount = getTableNames(dbName, &tableNames);
bool ok = propDropdownOrInput(sPrpCtx, propName, "Select table:", tableNames, tableCount, curValue, newValue, sizeof(newValue));
free(tableNames);
if (!ok) {
return;
}
} else if (propType == PROP_TYPE_INT) {
// Spinner dialog for integers
char prompt[PRP_PROMPT_BUF];
snprintf(prompt, sizeof(prompt), "%s:", propName);
int32_t intVal = atoi(curValue);
if (!dvxIntInputBox(sPrpCtx, "Edit Property", prompt, intVal, INT32_MIN, INT32_MAX, 1, &intVal)) {
return;
}
snprintf(newValue, sizeof(newValue), "%d", (int)intVal);
} else {
// Text input for strings
char prompt[PRP_PROMPT_BUF];
snprintf(prompt, sizeof(prompt), "%s:", propName);
snprintf(newValue, sizeof(newValue), "%s", curValue);
if (!dvxInputBox(sPrpCtx, "Edit Property", prompt, curValue, newValue, sizeof(newValue))) {
return;
}
}
int32_t count = (int32_t)arrlen(sDs->form->controls);
if (sDs->selectedIdx >= 0 && sDs->selectedIdx < count) {
DsgnControlT *ctrl = sDs->form->controls[sDs->selectedIdx];
if (strcasecmp(propName, "Name") == 0) {
char oldName[DSGN_MAX_NAME];
snprintf(oldName, sizeof(oldName), "%s", ctrl->name);
if (!validateNewName(newValue, DSGN_MAX_NAME, oldName)) {
return;
}
// Rename all members of a control array, not just the selected one
for (int32_t i = 0; i < count; i++) {
DsgnControlT *c = sDs->form->controls[i];
if (strcasecmp(c->name, oldName) == 0) {
snprintf(c->name, DSGN_MAX_NAME, "%s", newValue);
if (c->widget) {
wgtSetName(c->widget, c->name);
}
}
}
// Children reference their container by parentName; if the
// renamed control was a container, update them too, or they
// would be orphaned -- dropped from the saved .frm and
// reparented to the form node in the tree.
for (int32_t i = 0; i < count; i++) {
DsgnControlT *c = sDs->form->controls[i];
if (strcasecmp(c->parentName, oldName) == 0) {
snprintf(c->parentName, DSGN_MAX_NAME, "%s", newValue);
}
}
// If this is a Data control, update DataSource and MasterSource
// references on all other controls that pointed to the old name
if (strcasecmp(ctrl->typeName, "Data") == 0) {
for (int32_t i = 0; i < count; i++) {
DsgnControlT *c = sDs->form->controls[i];
for (int32_t j = 0; j < c->propCount; j++) {
if ((strcasecmp(c->props[j].name, "DataSource") == 0 ||
strcasecmp(c->props[j].name, "MasterSource") == 0) &&
strcasecmp(c->props[j].value, oldName) == 0) {
snprintf(c->props[j].value, DSGN_MAX_TEXT, "%s", newValue);
}
}
}
}
ideRenameInCode(oldName, newValue);
prpRebuildTree(sDs);
} else if (dsgnFindIntProp(propName)) {
const DsgnIntPropT *ip = dsgnFindIntProp(propName);
*(int32_t *)((char *)ctrl + ip->offset) = atoi(newValue);
dsgnSyncWidgetGeom(ctrl);
} else if (strcasecmp(propName, "Visible") == 0 && !dsgnIfaceHasProp(ctrl->typeName, "Visible")) {
bool val = frmParseBool(newValue);
ctrl->visible = val;
if (ctrl->widget) {
wgtSetVisible(ctrl->widget, val);
}
if (dsgnIsContainer(ctrl->typeName)) {
cascadeToChildren(sDs, ctrl->name, val, ctrl->enabled, 0);
}
} else if (strcasecmp(propName, "Enabled") == 0 && !dsgnIfaceHasProp(ctrl->typeName, "Enabled")) {
bool val = frmParseBool(newValue);
ctrl->enabled = val;
if (ctrl->widget) {
wgtSetEnabled(ctrl->widget, val);
}
if (dsgnIsContainer(ctrl->typeName)) {
cascadeToChildren(sDs, ctrl->name, ctrl->visible, val, 0);
}
} else if (strcasecmp(propName, "HelpTopic") == 0) {
snprintf(ctrl->helpTopic, DSGN_MAX_NAME, "%s", newValue);
} else {
// Try widget iface setter first
bool ifaceHandled = false;
if (ctrl->widget) {
const char *wgtName = wgtFindByBasName(ctrl->typeName);
if (wgtName) {
const WgtIfaceT *iface = wgtGetIface(wgtName);
if (iface) {
const WgtPropDescT *p = wgtIfaceFindProp(iface, propName);
if (p && p->setFn) {
// props[] is the design-time store (it survives a
// widget rebuild and is what gets saved); the live
// widget mirrors it. Strings are passed from the
// props[] copy so they outlive this function.
dsgnSetPropValue(ctrl, propName, newValue);
const char *stored = dsgnControlGetPropValue(ctrl, propName);
if (stored) {
wgtApplyPropFromString(ctrl->widget, p, stored);
}
ifaceHandled = true;
}
}
}
}
if (!ifaceHandled) {
// Custom prop storage
dsgnSetPropValue(ctrl, propName, newValue);
// Update widget text from the persistent props array
if (ctrl->widget && (strcasecmp(propName, "Caption") == 0 || strcasecmp(propName, "Text") == 0)) {
const char *stored = dsgnControlGetPropValue(ctrl, propName);
if (stored) {
wgtSetText(ctrl->widget, stored);
}
}
}
}
sDs->form->dirty = true;
if (sDs->formWin) {
dvxInvalidateWindow(sPrpCtx, sDs->formWin);
}
} else {
if (strcasecmp(propName, "Name") == 0) {
char oldName[BAS_MAX_IDENT];
snprintf(oldName, sizeof(oldName), "%s", sDs->form->name);
if (!validateNewName(newValue, BAS_MAX_IDENT, oldName)) {
return;
}
snprintf(sDs->form->name, sizeof(sDs->form->name), "%s", newValue);
ideRenameInCode(oldName, sDs->form->name);
prpRebuildTree(sDs);
} else if (strcasecmp(propName, "Caption") == 0) {
snprintf(sDs->form->caption, DSGN_MAX_TEXT, "%s", newValue);
if (sDs->formWin) {
char winTitle[DSGN_MAX_TEXT + PRP_TITLE_SUFFIX_PAD];
snprintf(winTitle, sizeof(winTitle), "%s [Design]", sDs->form->caption);
dvxSetTitle(sPrpCtx, sDs->formWin, winTitle);
}
} else if (strcasecmp(propName, "AutoSize") == 0) {
sDs->form->autoSize = frmParseBool(newValue);
if (sDs->form->autoSize && sDs->formWin) {
dvxFitWindow(sPrpCtx, sDs->formWin);
sDs->form->width = sDs->formWin->w;
sDs->form->height = sDs->formWin->h;
}
} else if (strcasecmp(propName, "Resizable") == 0) {
sDs->form->resizable = frmParseBool(newValue);
if (sDs->formWin) {
sDs->formWin->resizable = sDs->form->resizable;
dvxInvalidateWindow(sPrpCtx, sDs->formWin);
}
} else if (strcasecmp(propName, "Centered") == 0) {
sDs->form->centered = frmParseBool(newValue);
} else if (strcasecmp(propName, "Left") == 0) {
sDs->form->left = atoi(newValue);
} else if (strcasecmp(propName, "Top") == 0) {
sDs->form->top = atoi(newValue);
} else if (strcasecmp(propName, "Width") == 0) {
sDs->form->width = atoi(newValue);
sDs->form->autoSize = false;
} else if (strcasecmp(propName, "Height") == 0) {
sDs->form->height = atoi(newValue);
sDs->form->autoSize = false;
} else if (strcasecmp(propName, "HelpTopic") == 0) {
snprintf(sDs->form->helpTopic, DSGN_MAX_NAME, "%s", newValue);
}
sDs->form->dirty = true;
// Resize the form designer window
if (sDs->formWin) {
if (sDs->form->autoSize) {
dvxFitWindow(sPrpCtx, sDs->formWin);
sDs->form->width = sDs->formWin->w;
sDs->form->height = sDs->formWin->h;
} else {
dvxResizeWindow(sPrpCtx, sDs->formWin, sDs->form->width, sDs->form->height);
}
}
}
prpRefresh(sDs);
}
static void onPrpClose(WindowT *win) {
dvxHideWindow(sPrpCtx, win);
}
static void onTreeChange(WidgetT *w) {
(void)w;
if (!sDs || !sDs->form || !sTree || sUpdating) {
return;
}
// If the order hasn't changed, this is just a selection or expand/collapse.
// The onClick handler on individual items handles selection updates.
if (treeOrderMatches()) {
return;
}
// Actual reorder happened -- rebuild the controls array from tree order.
int32_t count = (int32_t)arrlen(sDs->form->controls);
DsgnControlT **newArr = NULL;
PrpParentT *newParents = NULL;
WidgetT *formItem = sTree->firstChild;
if (!formItem) {
return;
}
collectTreeOrder(formItem, sDs->form->controls, count, &newArr, &newParents, "");
// Revert if items were lost (dragged above the form) or a control was
// dropped into something that cannot hold children: only containers
// are written with nested blocks, so a non-container parent would drop
// the control from the saved .frm.
bool valid = ((int32_t)arrlen(newArr) == count);
for (int32_t i = 0; valid && i < count; i++) {
const char *pName = newParents[i].name;
if (pName[0] == '\0') {
continue;
}
valid = false;
for (int32_t j = 0; j < count; j++) {
if (strcasecmp(newArr[j]->name, pName) == 0) {
valid = dsgnIsContainer(newArr[j]->typeName);
break;
}
}
}
if (!valid) {
arrfree(newArr);
arrfree(newParents);
prpRebuildTree(sDs);
return;
}
for (int32_t i = 0; i < count; i++) {
snprintf(newArr[i]->parentName, DSGN_MAX_NAME, "%s", newParents[i].name);
}
arrfree(newParents);
arrfree(sDs->form->controls);
sDs->form->controls = newArr;
sDs->form->dirty = true;
dsgnRebuildWidgets(sDs);
prpRebuildTree(sDs);
if (sDs->formWin) {
dvxInvalidateWindow(sPrpCtx, sDs->formWin);
}
prpRefresh(sDs);
}
static void onTreeItemClick(WidgetT *w) {
(void)w;
if (!sDs || !sDs->form || !sTree || sUpdating) {
return;
}
// Check if it's the form item
WidgetT *formItem = sTree->firstChild;
if (w == formItem) {
sDs->selectedIdx = -1;
if (sDs->formWin) {
dvxInvalidateWindow(sPrpCtx, sDs->formWin);
}
prpRefresh(sDs);
return;
}
// Match by label text against control names
const char *label = (const char *)w->userData;
if (!label) {
return;
}
// Extract name and control-array index from "Name(idx) (Type)"
char clickedName[DSGN_MAX_NAME];
int32_t clickedIndex = DSGN_NO_INDEX;
parseTreeLabel(label, clickedName, DSGN_MAX_NAME, &clickedIndex);
int32_t count = (int32_t)arrlen(sDs->form->controls);
for (int32_t i = 0; i < count; i++) {
if (strcmp(sDs->form->controls[i]->name, clickedName) == 0 && sDs->form->controls[i]->index == clickedIndex) {
sDs->selectedIdx = i;
if (sDs->formWin) {
dvxInvalidateWindow(sPrpCtx, sDs->formWin);
}
prpRefresh(sDs);
return;
}
}
}
// Parse a tree label "name (Type)" or "name(idx) (Type)" into the bare name
// and, when present, the array index used to disambiguate control-array
// members (which all share one name but have distinct ->index).
static void parseTreeLabel(const char *label, char *outName, int32_t outNameSize, int32_t *outIndex) {
int32_t ni = 0;
while (label[ni] && label[ni] != ' ' && label[ni] != '(' && ni < outNameSize - 1) {
outName[ni] = label[ni];
ni++;
}
outName[ni] = '\0';
*outIndex = DSGN_NO_INDEX;
if (label[ni] == '(') {
*outIndex = (int32_t)atoi(&label[ni + 1]);
}
}
// propDropdownOrInput -- shared editor for the DataField / RecordSource
// properties. When count <= 0 it falls back to a free-text input box;
// otherwise it offers a "(none)"-prefixed dropdown of names, preselecting
// the entry matching curValue. Writes the chosen value (empty for "(none)")
// into newValue. Returns false if the user cancelled.
static bool propDropdownOrInput(AppContextT *ctx, const char *propName, const char *selectPrompt, char (*names)[DSGN_MAX_NAME], int32_t count, const char *curValue, char *newValue, int32_t newValueSize) {
if (count <= 0) {
// No names available -- fall back to text input
char prompt[PRP_PROMPT_BUF];
snprintf(prompt, sizeof(prompt), "%s:", propName);
snprintf(newValue, newValueSize, "%s", curValue);
return dvxInputBox(ctx, "Edit Property", prompt, curValue, newValue, newValueSize);
}
const char **ptrs = (const char **)calloc((size_t)count + 1, sizeof(const char *));
if (!ptrs) {
return false;
}
ptrs[0] = "(none)";
for (int32_t i = 0; i < count; i++) {
ptrs[i + 1] = names[i];
}
int32_t defIdx = 0;
for (int32_t i = 0; i < count; i++) {
if (strcasecmp(names[i], curValue) == 0) {
defIdx = i + 1;
break;
}
}
int32_t chosenIdx = 0;
bool ok = dvxChoiceDialog(ctx, propName, selectPrompt, ptrs, count + 1, defIdx, &chosenIdx);
free(ptrs);
if (!ok) {
return false;
}
if (chosenIdx == 0) {
newValue[0] = '\0';
} else {
snprintf(newValue, newValueSize, "%s", names[chosenIdx - 1]);
}
return true;
}
WindowT *prpCreate(AppContextT *ctx, DsgnStateT *ds) {
sDs = ds;
sPrpCtx = ctx;
int32_t winX = ctx->display.width - PRP_WIN_W - PRP_WIN_RIGHT_MARGIN;
WindowT *win = dvxCreateWindow(ctx, PRP_DIALOG_TITLE, winX, PRP_WIN_Y, PRP_WIN_W, PRP_WIN_H, true);
if (!win) {
return NULL;
}
win->onClose = onPrpClose;
sPrpWin = win;
WidgetT *root = wgtInitWindow(ctx, win);
// Splitter: tree on top, property list on bottom
WidgetT *splitter = wgtSplitter(root, false);
splitter->weight = WGT_WEIGHT_FILL;
wgtSplitterSetPos(splitter, (PRP_WIN_H - CHROME_TOTAL_TOP - CHROME_TOTAL_BOTTOM) / 2);
// Control tree (top pane)
sTree = wgtTreeView(splitter);
sTree->onChange = onTreeChange;
wgtTreeViewSetReorderable(sTree, true);
// Property ListView (bottom pane)
sPropList = wgtListView(splitter);
sPropList->onDblClick = onPropDblClick;
static const ListViewColT cols[PRP_CELL_COLUMNS] = {
{ "Property", 0, ListViewAlignLeftE },
{ "Value", 0, ListViewAlignLeftE }
};
wgtListViewSetColumns(sPropList, cols, PRP_CELL_COLUMNS);
prpRebuildTree(ds);
prpRefresh(ds);
return win;
}
void prpDestroy(AppContextT *ctx, WindowT *win) {
freeTreeLabels();
arrfree(sTreeLabels);
sTreeLabels = NULL;
freeCellData();
arrfree(sCellData);
sCellData = NULL;
if (win) {
dvxDestroyWindow(ctx, win);
}
sPrpWin = NULL;
sTree = NULL;
sPropList = NULL;
sDs = NULL;
}
void prpRebuildTree(DsgnStateT *ds) {
if (!sTree || !ds || !ds->form) {
return;
}
sUpdating = true;
freeTreeLabels();
wgtDestroyChildren(sTree);
// Form entry at the top
char *formLabel = strdup(ds->form->name);
arrput(sTreeLabels, formLabel);
WidgetT *formItem = wgtTreeItem(sTree, formLabel);
formItem->userData = formLabel;
formItem->onClick = onTreeItemClick;
wgtTreeItemSetExpanded(formItem, true);
if (ds->selectedIdx < 0) {
wgtTreeItemSetSelected(formItem, true);
}
// Control entries -- nest children under container parents
int32_t count = (int32_t)arrlen(ds->form->controls);
// Temporary array to map control index -> tree item
WidgetT **treeItems = NULL;
for (int32_t i = 0; i < count; i++) {
DsgnControlT *ctrl = ds->form->controls[i];
char buf[PRP_PROMPT_BUF];
if (ctrl->index >= 0) {
snprintf(buf, sizeof(buf), "%s(%d) (%s)", ctrl->name, (int)ctrl->index, ctrl->typeName);
} else {
snprintf(buf, sizeof(buf), "%s (%s)", ctrl->name, ctrl->typeName);
}
char *label = strdup(buf);
arrput(sTreeLabels, label);
// Find the tree parent: form item or a container's tree item
WidgetT *treeParent = formItem;
if (ctrl->parentName[0]) {
for (int32_t j = 0; j < i; j++) {
if (strcasecmp(ds->form->controls[j]->name, ctrl->parentName) == 0) {
treeParent = treeItems[j];
break;
}
}
}
WidgetT *item = wgtTreeItem(treeParent, label);
item->userData = label;
item->onClick = onTreeItemClick;
arrput(treeItems, item);
if (dsgnIsContainer(ctrl->typeName)) {
wgtTreeItemSetExpanded(item, true);
}
if (i == ds->selectedIdx) {
wgtTreeItemSetSelected(item, true);
}
}
arrfree(treeItems);
sUpdating = false;
}
void prpRefresh(DsgnStateT *ds) {
if (!ds || !ds->form) {
return;
}
// Sync tree selection to match selectedIdx
if (sTree && !sUpdating) {
WidgetT *formItem = sTree->firstChild;
if (formItem) {
WidgetT *target = NULL;
if (ds->selectedIdx < 0) {
target = formItem;
} else if (ds->selectedIdx < (int32_t)arrlen(ds->form->controls)) {
target = findTreeItemByName(formItem, ds->form->controls[ds->selectedIdx]->name, ds->form->controls[ds->selectedIdx]->index);
}
if (target) {
sUpdating = true;
wgtTreeViewSetSelected(sTree, target);
sUpdating = false;
}
}
}
// Update property ListView
if (!sPropList) {
return;
}
freeCellData();
int32_t count = (int32_t)arrlen(ds->form->controls);
if (ds->selectedIdx >= 0 && ds->selectedIdx < count) {
DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx];
char buf[PRP_INT_BUF];
addPropRow("Name", ctrl->name);
if (ctrl->index >= 0) {
snprintf(buf, sizeof(buf), "%d", (int)ctrl->index);
addPropRow("Index", buf);
}
addPropRow("Type", ctrl->typeName);
for (int32_t i = 0; dsgnIntPropAt(i); i++) {
const DsgnIntPropT *ip = dsgnIntPropAt(i);
snprintf(buf, sizeof(buf), "%d", (int)*(const int32_t *)((const char *)ctrl + ip->offset));
addPropRow(ip->name, buf);
}
if (!dsgnIfaceHasProp(ctrl->typeName, "Visible")) {
addPropRow("Visible", ctrl->visible ? "True" : "False");
}
if (!dsgnIfaceHasProp(ctrl->typeName, "Enabled")) {
addPropRow("Enabled", ctrl->enabled ? "True" : "False");
}
addPropRow("HelpTopic", ctrl->helpTopic);
for (int32_t i = 0; i < ctrl->propCount; i++) {
addPropRow(ctrl->props[i].name, ctrl->props[i].value);
}
// A container placed on the canvas has no Layout entry until the
// user picks one; show the default so the row is there to edit.
if (dsgnIsContainer(ctrl->typeName) && !dsgnControlGetPropValue(ctrl, "Layout")) {
addPropRow("Layout", DSGN_VBOX_LAYOUT);
}
// Widget interface properties (from the .wgt descriptor)
const char *wgtName = wgtFindByBasName(ctrl->typeName);
if (wgtName) {
const WgtIfaceT *iface = wgtGetIface(wgtName);
if (iface && ctrl->widget) {
for (int32_t i = 0; i < iface->propCount; i++) {
const WgtPropDescT *p = &iface->props[i];
// Skip read-only runtime properties (no setter)
if (!p->setFn) {
continue;
}
// Skip if already shown as a custom prop
bool already = false;
for (int32_t j = 0; j < ctrl->propCount; j++) {
if (strcasecmp(ctrl->props[j].name, p->name) == 0) {
already = true;
break;
}
}
if (already) {
continue;
}
// Read the current value from the widget. Enum values
// that don't map to a name are shown as "?".
char valBuf[DSGN_MAX_TEXT];
if (wgtPropValueToString(ctrl->widget, p, valBuf, sizeof(valBuf))) {
addPropRow(p->name, valBuf);
} else if (p->type == WGT_IFACE_ENUM) {
addPropRow(p->name, "?");
} else {
addPropRow(p->name, "");
}
}
}
}
} else {
// One row per runtime form property the designer stores, in
// table order; runtime-only ones (Visible, ContextMenu) have no
// designer field and are skipped.
int32_t formPropCount = 0;
const BasPropDescT *formProps = basFormRtFormProps(&formPropCount);
for (int32_t i = 0; i < formPropCount; i++) {
char valBuf[DSGN_MAX_TEXT];
if (dsgnFormPropValue(ds->form, formProps[i].name, valBuf, sizeof(valBuf))) {
addPropRow(formProps[i].name, valBuf);
}
}
}
wgtListViewSetData(sPropList, (const char **)sCellData, (int32_t)arrlen(sCellData) / PRP_CELL_COLUMNS);
}
// resolveDbPath -- resolve a DatabaseName against the project directory
static void resolveDbPath(const char *dbName, char *out, int32_t outSize) {
// If it's already an absolute path (starts with drive letter or /), use as-is
if ((dbName[0] && dbName[1] == ':') || dbName[0] == '/' || dbName[0] == '\\') {
snprintf(out, outSize, "%s", dbName);
return;
}
// Resolve relative to project directory
if (sDs && sDs->projectDir && sDs->projectDir[0]) {
snprintf(out, outSize, "%s" DVX_PATH_SEP "%s", sDs->projectDir, dbName);
} else {
snprintf(out, outSize, "%s", dbName);
}
}
// Check whether the tree order matches the controls array.
// Returns true if they match (no reorder AND no reparent happened).
static bool treeOrderMatches(void) {
WidgetT *formItem = sTree->firstChild;
if (!formItem) {
return true;
}
int32_t count = (int32_t)arrlen(sDs->form->controls);
DsgnControlT **newArr = NULL;
PrpParentT *newParents = NULL;
// Order is detected via pointer identity (tree DFS order vs model
// order); a reparent shows up as a differing tree parent name.
collectTreeOrder(formItem, sDs->form->controls, count, &newArr, &newParents, "");
bool match = ((int32_t)arrlen(newArr) == count);
if (match) {
for (int32_t i = 0; i < count; i++) {
if (newArr[i] != sDs->form->controls[i] ||
strcasecmp(sDs->form->controls[i]->parentName, newParents[i].name) != 0) {
match = false;
break;
}
}
}
arrfree(newArr);
arrfree(newParents);
return match;
}
// Reject a control or form rename that is not a BASIC identifier, is too
// long for a maxLen-byte field, or collides with another object on the form.
// Shows the reason and returns false on rejection.
static bool validateNewName(const char *name, int32_t maxLen, const char *exceptName) {
char msg[PRP_PROMPT_BUF];
if (!basIsValidIdent(name)) {
dvxErrorBox(sPrpCtx, PRP_DIALOG_TITLE, "Name must start with a letter or underscore and contain only letters, digits, and underscores.");
return false;
}
if ((int32_t)strlen(name) >= maxLen) {
snprintf(msg, sizeof(msg), "Name must be shorter than %d characters.", (int)maxLen);
dvxErrorBox(sPrpCtx, PRP_DIALOG_TITLE, msg);
return false;
}
if (dsgnNameInUse(sDs->form, name, exceptName, true)) {
snprintf(msg, sizeof(msg), "The name %s is already in use.", name);
dvxErrorBox(sPrpCtx, PRP_DIALOG_TITLE, msg);
return false;
}
return true;
}