// 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 #include #include #include #include // ============================================================ // 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; }