DVX_GUI/apps/dvxbasic/ide/ideProperties.c

1527 lines
50 KiB
C

// 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 "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
// ============================================================
// 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
static char **sCellData = NULL; // stb_ds array of strdup'd strings
static int32_t sCellRows = 0;
// ============================================================
// Helpers
// ============================================================
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);
}
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);
sCellRows = 0;
}
static void addPropRow(const char *name, const char *value) {
arrput(sCellData, strdup(name));
arrput(sCellData, strdup(value ? value : ""));
sCellRows++;
}
// ============================================================
// Prototypes
// ============================================================
static int32_t getDataFieldNames(const DsgnStateT *ds, const char *dataSourceName, char names[][DSGN_MAX_NAME], int32_t maxNames);
static int32_t getTableNames(const char *dbName, char names[][DSGN_MAX_NAME], int32_t maxNames);
static void onPrpClose(WindowT *win);
static void onPropDblClick(WidgetT *w);
static void onTreeItemClick(WidgetT *w);
static void onTreeChange(WidgetT *w);
// ============================================================
// onPrpClose
// ============================================================
static void onPrpClose(WindowT *win) {
dvxHideWindow(sPrpCtx, win);
}
// ============================================================
// 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%c%s", sDs->projectDir, DVX_PATH_SEP, dbName);
} else {
snprintf(out, outSize, "%s", dbName);
}
}
// ============================================================
// 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 returns up to maxNames column names. Returns the
// count of names found (0 if anything fails).
static int32_t getDataFieldNames(const DsgnStateT *ds, const char *dataSourceName, char names[][DSGN_MAX_NAME], int32_t maxNames) {
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;
}
for (int32_t j = 0; j < ctrl->propCount; j++) {
if (strcasecmp(ctrl->props[j].name, "DatabaseName") == 0) {
dbName = ctrl->props[j].value;
} else if (strcasecmp(ctrl->props[j].name, "RecordSource") == 0) {
recSrc = ctrl->props[j].value;
}
}
break;
}
if (!dbName || !dbName[0] || !recSrc || !recSrc[0]) {
return 0;
}
// Resolve SQL functions via dlsym
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 void (*SqlFreeResultFnT)(int32_t);
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[512];
if (strncasecmp(recSrc, "SELECT ", 7) == 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);
int32_t count = 0;
for (int32_t i = 0; i < colCount && count < maxNames; i++) {
const char *name = sqlFieldName(rs, i);
if (name) {
snprintf(names[count++], DSGN_MAX_NAME, "%s", name);
}
}
sqlFreeResult(rs);
sqlClose(db);
return count;
}
// ============================================================
// getTableNames -- query table names from a SQLite database
// ============================================================
static int32_t getTableNames(const char *dbName, char names[][DSGN_MAX_NAME], int32_t maxNames) {
if (!dbName || !dbName[0]) {
return 0;
}
typedef int32_t (*SqlOpenFnT)(const char *);
typedef void (*SqlCloseFnT)(int32_t);
typedef int32_t (*SqlQueryFnT)(int32_t, const char *);
typedef bool (*SqlNextFnT)(int32_t);
typedef const char *(*SqlFieldTextFnT)(int32_t, int32_t);
typedef void (*SqlFreeResultFnT)(int32_t);
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;
}
int32_t count = 0;
while (sqlNext(rs) && count < maxNames) {
const char *name = sqlFieldText(rs, 0);
if (name && name[0]) {
snprintf(names[count++], DSGN_MAX_NAME, "%s", name);
}
}
sqlFreeResult(rs);
sqlClose(db);
return count;
}
// ============================================================
// getPropType
// ============================================================
//
// Determine the data type of a property by name. Checks built-in
// properties first, then looks up the widget's interface descriptor.
// Returns WGT_IFACE_STRING, WGT_IFACE_INT, or WGT_IFACE_BOOL.
#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
#define MAX_DATAFIELD_COLS 64
#define MAX_TABLES 64
static uint8_t getPropType(const char *propName, const char *typeName) {
// Read-only properties
if (strcasecmp(propName, "Type") == 0) { return PROP_TYPE_READONLY; }
if (strcasecmp(propName, "Index") == 0) { return PROP_TYPE_READONLY; }
if (strcasecmp(propName, "BOF") == 0) { return PROP_TYPE_READONLY; }
if (strcasecmp(propName, "EOF") == 0) { return PROP_TYPE_READONLY; }
// Known built-in types
if (strcasecmp(propName, "Name") == 0) { return PROP_TYPE_STRING; }
if (strcasecmp(propName, "Caption") == 0) { return PROP_TYPE_STRING; }
if (strcasecmp(propName, "MinWidth") == 0) { return PROP_TYPE_INT; }
if (strcasecmp(propName, "MinHeight") == 0) { return PROP_TYPE_INT; }
if (strcasecmp(propName, "MaxWidth") == 0) { return PROP_TYPE_INT; }
if (strcasecmp(propName, "MaxHeight") == 0) { return PROP_TYPE_INT; }
if (strcasecmp(propName, "Weight") == 0) { return PROP_TYPE_INT; }
if (strcasecmp(propName, "Left") == 0) { return PROP_TYPE_INT; }
if (strcasecmp(propName, "Top") == 0) { return PROP_TYPE_INT; }
if (strcasecmp(propName, "AutoSize") == 0) { return PROP_TYPE_BOOL; }
if (strcasecmp(propName, "Resizable") == 0) { return PROP_TYPE_BOOL; }
if (strcasecmp(propName, "Centered") == 0) { return PROP_TYPE_BOOL; }
if (strcasecmp(propName, "Visible") == 0) { return PROP_TYPE_BOOL; }
if (strcasecmp(propName, "Enabled") == 0) { return PROP_TYPE_BOOL; }
if (strcasecmp(propName, "Layout") == 0) { return PROP_TYPE_LAYOUT; }
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; }
// Look up in the widget's interface descriptor
if (typeName && typeName[0]) {
const char *wgtName = wgtFindByBasName(typeName);
if (wgtName) {
const WgtIfaceT *iface = wgtGetIface(wgtName);
if (iface) {
for (int32_t i = 0; i < iface->propCount; i++) {
if (strcasecmp(iface->props[i].name, propName) == 0) {
return iface->props[i].type;
}
}
}
}
}
return PROP_TYPE_STRING;
}
static const WgtPropDescT *findIfaceProp(const char *typeName, const char *propName) {
if (!typeName || !typeName[0]) {
return NULL;
}
const char *wgtName = wgtFindByBasName(typeName);
if (!wgtName) {
return NULL;
}
const WgtIfaceT *iface = wgtGetIface(wgtName);
if (!iface) {
return NULL;
}
for (int32_t i = 0; i < iface->propCount; i++) {
if (strcasecmp(iface->props[i].name, propName) == 0) {
return &iface->props[i];
}
}
return NULL;
}
// ============================================================
// cascadeToChildren
// ============================================================
//
// 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 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
if (dsgnIsContainer(child->typeName)) {
cascadeToChildren(ds, child->name, visible, enabled);
}
}
}
// ============================================================
// onPropDblClick
// ============================================================
static void onPropDblClick(WidgetT *w) {
if (!sDs || !sDs->form || !sPropList || !sPrpCtx) {
return;
}
int32_t row = wgtListViewGetSelected(w);
if (row < 0 || row >= sCellRows) {
return;
}
const char *propName = sCellData[row * 2];
const char *curValue = sCellData[row * 2 + 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[32];
int32_t layoutCount = 0;
int32_t ifaceTotal = wgtIfaceCount();
for (int32_t i = 0; i < ifaceTotal && layoutCount < 32; 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];
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;
root->firstChild = NULL;
root->lastChild = NULL;
WidgetT *contentBox = dsgnCreateContentBox(root, layoutField);
int32_t cc = (int32_t)arrlen(sDs->form->controls);
for (int32_t ci = 0; ci < cc; ci++) {
sDs->form->controls[ci]->widget = NULL;
}
dsgnCreateWidgets(sDs, contentBox);
if (sDs->formWin) {
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 = (strcasecmp(curValue, "True") == 0);
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[17];
int32_t dataCount = 0;
// First entry is "(none)" to clear binding
dataNames[dataCount++] = "(none)";
for (int32_t i = 0; i < formCtrlCount && dataCount < 16; 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 {
for (int32_t i = 0; i < selCtrl->propCount; i++) {
if (strcasecmp(selCtrl->props[i].name, "DataSource") == 0) {
dataSrc = selCtrl->props[i].value;
break;
}
}
}
}
char fieldNames[MAX_DATAFIELD_COLS][DSGN_MAX_NAME];
int32_t fieldCount = getDataFieldNames(sDs, dataSrc, fieldNames, MAX_DATAFIELD_COLS);
if (fieldCount <= 0) {
// No columns found — fall back to text input
char prompt[128];
snprintf(prompt, sizeof(prompt), "%s:", propName);
snprintf(newValue, sizeof(newValue), "%s", curValue);
if (!dvxInputBox(sPrpCtx, "Edit Property", prompt, curValue, newValue, sizeof(newValue))) {
return;
}
} else {
const char *fieldPtrs[MAX_DATAFIELD_COLS + 1];
fieldPtrs[0] = "(none)";
for (int32_t i = 0; i < fieldCount; i++) {
fieldPtrs[i + 1] = fieldNames[i];
}
int32_t defIdx = 0;
for (int32_t i = 0; i < fieldCount; i++) {
if (strcasecmp(fieldNames[i], curValue) == 0) {
defIdx = i + 1;
break;
}
}
int32_t chosenIdx = 0;
if (!dvxChoiceDialog(sPrpCtx, propName, "Select column:", fieldPtrs, fieldCount + 1, defIdx, &chosenIdx)) {
return;
}
if (chosenIdx == 0) {
newValue[0] = '\0';
} else {
snprintf(newValue, sizeof(newValue), "%s", fieldNames[chosenIdx - 1]);
}
}
} 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];
for (int32_t i = 0; i < selCtrl->propCount; i++) {
if (strcasecmp(selCtrl->props[i].name, "DatabaseName") == 0) {
dbName = selCtrl->props[i].value;
break;
}
}
}
char tableNames[MAX_TABLES][DSGN_MAX_NAME];
int32_t tableCount = getTableNames(dbName, tableNames, MAX_TABLES);
if (tableCount <= 0) {
// No tables or can't open DB — fall back to text input
char prompt[128];
snprintf(prompt, sizeof(prompt), "%s:", propName);
snprintf(newValue, sizeof(newValue), "%s", curValue);
if (!dvxInputBox(sPrpCtx, "Edit Property", prompt, curValue, newValue, sizeof(newValue))) {
return;
}
} else {
const char *tablePtrs[MAX_TABLES + 1];
tablePtrs[0] = "(none)";
for (int32_t i = 0; i < tableCount; i++) {
tablePtrs[i + 1] = tableNames[i];
}
int32_t defIdx = 0;
for (int32_t i = 0; i < tableCount; i++) {
if (strcasecmp(tableNames[i], curValue) == 0) {
defIdx = i + 1;
break;
}
}
int32_t chosenIdx = 0;
if (!dvxChoiceDialog(sPrpCtx, "RecordSource", "Select table:", tablePtrs, tableCount + 1, defIdx, &chosenIdx)) {
return;
}
if (chosenIdx == 0) {
newValue[0] = '\0';
} else {
snprintf(newValue, sizeof(newValue), "%s", tableNames[chosenIdx - 1]);
}
}
} else if (propType == PROP_TYPE_INT) {
// Spinner dialog for integers
char prompt[128];
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[128];
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);
// 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, "%.31s", newValue);
if (c->widget) {
wgtSetName(c->widget, c->name);
}
}
}
// 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 (strcasecmp(propName, "MinWidth") == 0) {
ctrl->width = atoi(newValue);
if (ctrl->widget) {
ctrl->widget->minW = wgtPixels(ctrl->width);
}
} else if (strcasecmp(propName, "MinHeight") == 0) {
ctrl->height = atoi(newValue);
if (ctrl->widget) {
ctrl->widget->minH = wgtPixels(ctrl->height);
}
} else if (strcasecmp(propName, "MaxWidth") == 0) {
ctrl->maxWidth = atoi(newValue);
if (ctrl->widget) {
ctrl->widget->maxW = ctrl->maxWidth > 0 ? wgtPixels(ctrl->maxWidth) : 0;
}
} else if (strcasecmp(propName, "MaxHeight") == 0) {
ctrl->maxHeight = atoi(newValue);
if (ctrl->widget) {
ctrl->widget->maxH = ctrl->maxHeight > 0 ? wgtPixels(ctrl->maxHeight) : 0;
}
} else if (strcasecmp(propName, "Weight") == 0) {
ctrl->weight = atoi(newValue);
if (ctrl->widget) {
ctrl->widget->weight = ctrl->weight;
}
} else if (strcasecmp(propName, "Visible") == 0) {
bool val = (strcasecmp(newValue, "True") == 0);
if (ctrl->widget) {
wgtSetVisible(ctrl->widget, val);
}
if (dsgnIsContainer(ctrl->typeName)) {
bool en = ctrl->widget ? ctrl->widget->enabled : true;
cascadeToChildren(sDs, ctrl->name, val, en);
}
} else if (strcasecmp(propName, "Enabled") == 0) {
bool val = (strcasecmp(newValue, "True") == 0);
if (ctrl->widget) {
wgtSetEnabled(ctrl->widget, val);
}
if (dsgnIsContainer(ctrl->typeName)) {
bool vis = ctrl->widget ? ctrl->widget->visible : true;
cascadeToChildren(sDs, ctrl->name, vis, val);
}
} 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) {
for (int32_t i = 0; i < iface->propCount; i++) {
const WgtPropDescT *p = &iface->props[i];
if (strcasecmp(p->name, propName) != 0 || !p->setFn) {
continue;
}
if (p->type == WGT_IFACE_STRING) {
// Store in props for persistence, set from there
bool found = false;
for (int32_t j = 0; j < ctrl->propCount; j++) {
if (strcasecmp(ctrl->props[j].name, propName) == 0) {
snprintf(ctrl->props[j].value, DSGN_MAX_TEXT, "%s", newValue);
((void (*)(WidgetT *, const char *))p->setFn)(ctrl->widget, ctrl->props[j].value);
found = true;
break;
}
}
if (!found && ctrl->propCount < DSGN_MAX_PROPS) {
snprintf(ctrl->props[ctrl->propCount].name, DSGN_MAX_NAME, "%s", propName);
snprintf(ctrl->props[ctrl->propCount].value, DSGN_MAX_TEXT, "%s", newValue);
((void (*)(WidgetT *, const char *))p->setFn)(ctrl->widget, ctrl->props[ctrl->propCount].value);
ctrl->propCount++;
}
} else if (p->type == WGT_IFACE_ENUM && p->enumNames) {
int32_t enumVal = 0;
for (int32_t en = 0; p->enumNames[en]; en++) {
if (strcasecmp(p->enumNames[en], newValue) == 0) {
enumVal = en;
break;
}
}
((void (*)(WidgetT *, int32_t))p->setFn)(ctrl->widget, enumVal);
} else if (p->type == WGT_IFACE_INT) {
((void (*)(WidgetT *, int32_t))p->setFn)(ctrl->widget, atoi(newValue));
} else if (p->type == WGT_IFACE_BOOL) {
((void (*)(WidgetT *, bool))p->setFn)(ctrl->widget, strcasecmp(newValue, "True") == 0);
}
ifaceHandled = true;
break;
}
}
}
}
if (!ifaceHandled) {
// Custom prop storage
bool found = false;
for (int32_t i = 0; i < ctrl->propCount; i++) {
if (strcasecmp(ctrl->props[i].name, propName) == 0) {
snprintf(ctrl->props[i].value, DSGN_MAX_TEXT, "%s", newValue);
found = true;
break;
}
}
if (!found && ctrl->propCount < DSGN_MAX_PROPS) {
snprintf(ctrl->props[ctrl->propCount].name, DSGN_MAX_NAME, "%s", propName);
snprintf(ctrl->props[ctrl->propCount].value, DSGN_MAX_TEXT, "%s", newValue);
ctrl->propCount++;
}
// Update widget text from the persistent props array
if (ctrl->widget && (strcasecmp(propName, "Caption") == 0 || strcasecmp(propName, "Text") == 0)) {
for (int32_t i = 0; i < ctrl->propCount; i++) {
if (strcasecmp(ctrl->props[i].name, propName) == 0) {
wgtSetText(ctrl->widget, ctrl->props[i].value);
break;
}
}
}
}
}
sDs->form->dirty = true;
if (sDs->formWin) {
dvxInvalidateWindow(sPrpCtx, sDs->formWin);
}
} else {
if (strcasecmp(propName, "Name") == 0) {
char oldName[DSGN_MAX_NAME];
snprintf(oldName, sizeof(oldName), "%s", sDs->form->name);
// Length-clamped memcpy instead of strncpy/snprintf because
// GCC warns about both when source exceeds the buffer.
int32_t nl = (int32_t)strlen(newValue);
if (nl >= DSGN_MAX_NAME) {
nl = DSGN_MAX_NAME - 1;
}
memcpy(sDs->form->name, newValue, nl);
sDs->form->name[nl] = '\0';
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[280];
snprintf(winTitle, sizeof(winTitle), "%s [Design]", sDs->form->caption);
dvxSetTitle(sPrpCtx, sDs->formWin, winTitle);
}
} else if (strcasecmp(propName, "AutoSize") == 0) {
sDs->form->autoSize = (strcasecmp(newValue, "True") == 0);
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 = (strcasecmp(newValue, "True") == 0);
if (sDs->formWin) {
sDs->formWin->resizable = sDs->form->resizable;
dvxInvalidateWindow(sPrpCtx, sDs->formWin);
}
} else if (strcasecmp(propName, "Centered") == 0) {
sDs->form->centered = (strcasecmp(newValue, "True") == 0);
} 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;
}
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);
}
// ============================================================
// onTreeItemClick
// ============================================================
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 from "Name (Type)"
char clickedName[DSGN_MAX_NAME];
int32_t ni = 0;
while (label[ni] && label[ni] != ' ' && ni < DSGN_MAX_NAME - 1) {
clickedName[ni] = label[ni];
ni++;
}
clickedName[ni] = '\0';
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->selectedIdx = i;
if (sDs->formWin) {
dvxInvalidateWindow(sPrpCtx, sDs->formWin);
}
prpRefresh(sDs);
return;
}
}
}
// ============================================================
// onTreeReorder
// ============================================================
// Walk tree items recursively, collecting control names in order.
static void collectTreeOrder(WidgetT *parent, DsgnControlT **srcArr, int32_t srcCount, DsgnControlT ***outArr, 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 ni = 0;
while (label[ni] && label[ni] != ' ' && ni < DSGN_MAX_NAME - 1) {
itemName[ni] = label[ni];
ni++;
}
itemName[ni] = '\0';
for (int32_t i = 0; i < srcCount; i++) {
if (strcmp(srcArr[i]->name, itemName) == 0) {
snprintf(srcArr[i]->parentName, DSGN_MAX_NAME, "%s", parentName);
arrput(*outArr, srcArr[i]);
// Recurse into children (for containers)
if (item->firstChild) {
collectTreeOrder(item, srcArr, srcCount, outArr, itemName);
}
break;
}
}
}
}
// Check whether the tree order matches the controls array.
// Returns true if they match (no reorder happened).
static bool treeOrderMatches(void) {
WidgetT *formItem = sTree->firstChild;
if (!formItem) {
return true;
}
DsgnControlT **newArr = NULL;
int32_t count = (int32_t)arrlen(sDs->form->controls);
collectTreeOrder(formItem, sDs->form->controls, count, &newArr, "");
bool match = ((int32_t)arrlen(newArr) == count);
if (match) {
for (int32_t i = 0; i < count; i++) {
if (strcmp(newArr[i]->name, sDs->form->controls[i]->name) != 0 ||
strcmp(newArr[i]->parentName, sDs->form->controls[i]->parentName) != 0) {
match = false;
break;
}
}
}
arrfree(newArr);
return match;
}
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;
WidgetT *formItem = sTree->firstChild;
if (!formItem) {
return;
}
collectTreeOrder(formItem, sDs->form->controls, count, &newArr, "");
// If we lost items (dragged above form), revert
if ((int32_t)arrlen(newArr) != count) {
arrfree(newArr);
prpRebuildTree(sDs);
return;
}
arrfree(sDs->form->controls);
sDs->form->controls = newArr;
sDs->form->dirty = true;
if (sDs->form->contentBox) {
sDs->form->contentBox->firstChild = NULL;
sDs->form->contentBox->lastChild = NULL;
int32_t newCount = (int32_t)arrlen(sDs->form->controls);
for (int32_t i = 0; i < newCount; i++) {
sDs->form->controls[i]->widget = NULL;
}
dsgnCreateWidgets(sDs, sDs->form->contentBox);
}
prpRebuildTree(sDs);
if (sDs->formWin) {
dvxInvalidateWindow(sPrpCtx, sDs->formWin);
}
prpRefresh(sDs);
}
// ============================================================
// prpCreate
// ============================================================
WindowT *prpCreate(AppContextT *ctx, DsgnStateT *ds) {
sDs = ds;
sPrpCtx = ctx;
int32_t winX = ctx->display.width - PRP_WIN_W - 10;
WindowT *win = dvxCreateWindow(ctx, "Properties", winX, 30, 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 = 100;
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[2] = {
{ "Property", 0, ListViewAlignLeftE },
{ "Value", 0, ListViewAlignLeftE }
};
wgtListViewSetColumns(sPropList, cols, 2);
prpRebuildTree(ds);
prpRefresh(ds);
return win;
}
// ============================================================
// prpDestroy
// ============================================================
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;
}
// ============================================================
// prpRebuildTree
// ============================================================
void prpRebuildTree(DsgnStateT *ds) {
if (!sTree || !ds || !ds->form) {
return;
}
sUpdating = true;
freeTreeLabels();
sTree->firstChild = NULL;
sTree->lastChild = NULL;
// 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[128];
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 && treeItems) {
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;
}
// ============================================================
// prpRefresh
// ============================================================
// Walk tree items recursively to find the one matching a control name.
static WidgetT *findTreeItemByName(WidgetT *parent, const char *name) {
for (WidgetT *item = parent->firstChild; item; item = item->nextSibling) {
const char *label = (const char *)item->userData;
if (label) {
// Labels are "Name (Type)" — match the name portion
int32_t len = 0;
while (label[len] && label[len] != ' ') {
len++;
}
if ((int32_t)strlen(name) == len && strncmp(label, name, len) == 0) {
return item;
}
}
// Recurse into children (containers)
WidgetT *found = findTreeItemByName(item, name);
if (found) {
return found;
}
}
return NULL;
}
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);
}
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[32];
addPropRow("Name", ctrl->name);
if (ctrl->index >= 0) {
snprintf(buf, sizeof(buf), "%d", (int)ctrl->index);
addPropRow("Index", buf);
}
addPropRow("Type", ctrl->typeName);
snprintf(buf, sizeof(buf), "%d", (int)ctrl->width);
addPropRow("MinWidth", buf);
snprintf(buf, sizeof(buf), "%d", (int)ctrl->height);
addPropRow("MinHeight", buf);
snprintf(buf, sizeof(buf), "%d", (int)ctrl->maxWidth);
addPropRow("MaxWidth", buf);
snprintf(buf, sizeof(buf), "%d", (int)ctrl->maxHeight);
addPropRow("MaxHeight", buf);
snprintf(buf, sizeof(buf), "%d", (int)ctrl->weight);
addPropRow("Weight", buf);
addPropRow("Visible", ctrl->widget && ctrl->widget->visible ? "True" : "False");
addPropRow("Enabled", ctrl->widget && ctrl->widget->enabled ? "True" : "False");
for (int32_t i = 0; i < ctrl->propCount; i++) {
addPropRow(ctrl->props[i].name, ctrl->props[i].value);
}
// 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
if (p->type == WGT_IFACE_STRING && p->getFn) {
const char *s = ((const char *(*)(WidgetT *))p->getFn)(ctrl->widget);
addPropRow(p->name, s ? s : "");
} else if (p->type == WGT_IFACE_ENUM && p->getFn && p->enumNames) {
int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget);
const char *name = NULL;
for (int32_t k = 0; p->enumNames[k]; k++) {
if (k == v) {
name = p->enumNames[k];
break;
}
}
addPropRow(p->name, name ? name : "?");
} else if (p->type == WGT_IFACE_INT && p->getFn) {
int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget);
snprintf(buf, sizeof(buf), "%d", (int)v);
addPropRow(p->name, buf);
} else if (p->type == WGT_IFACE_BOOL && p->getFn) {
bool v = ((bool (*)(const WidgetT *))p->getFn)(ctrl->widget);
addPropRow(p->name, v ? "True" : "False");
} else {
addPropRow(p->name, "");
}
}
}
}
} else {
char buf[32];
addPropRow("Name", ds->form->name);
addPropRow("Caption", ds->form->caption);
addPropRow("Layout", ds->form->layout);
addPropRow("AutoSize", ds->form->autoSize ? "True" : "False");
addPropRow("Resizable", ds->form->resizable ? "True" : "False");
addPropRow("Centered", ds->form->centered ? "True" : "False");
snprintf(buf, sizeof(buf), "%d", (int)ds->form->left);
addPropRow("Left", buf);
snprintf(buf, sizeof(buf), "%d", (int)ds->form->top);
addPropRow("Top", buf);
snprintf(buf, sizeof(buf), "%d", (int)ds->form->width);
addPropRow("Width", buf);
snprintf(buf, sizeof(buf), "%d", (int)ds->form->height);
addPropRow("Height", buf);
}
wgtListViewSetData(sPropList, (const char **)sCellData, sCellRows);
}