// 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 "dvxDialog.h" #include "dvxWm.h" #include "widgetBox.h" #include "widgetListView.h" #include "widgetSplitter.h" #include "widgetTreeView.h" #include #include #include #include #include // ============================================================ // 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 onTreeReorder(WidgetT *w); // ============================================================ // onPrpClose // ============================================================ static void onPrpClose(WindowT *win) { dvxHideWindow(sPrpCtx, win); } // ============================================================ // 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; } int32_t db = sqlOpen(dbName); 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; } int32_t db = sqlOpen(dbName); 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_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; } // 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, "DataSource") == 0) { return PROP_TYPE_DATASOURCE; } if (strcasecmp(propName, "DataField") == 0) { return PROP_TYPE_DATAFIELD; } if (strcasecmp(propName, "RecordSource") == 0) { return PROP_TYPE_RECORDSRC; } // 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 toggles directly -- no input box needed if (strcasecmp(propName, "Layout") == 0 && sDs->selectedIdx < 0) { if (strcasecmp(sDs->form->layout, "VBox") == 0) { snprintf(sDs->form->layout, DSGN_MAX_NAME, "HBox"); } else { snprintf(sDs->form->layout, DSGN_MAX_NAME, "VBox"); } sDs->form->dirty = true; // Replace the content box with the new layout type if (sDs->formWin && sDs->formWin->widgetRoot) { WidgetT *root = sDs->formWin->widgetRoot; // Remove old content box root->firstChild = NULL; root->lastChild = NULL; // Create new content box WidgetT *contentBox; if (strcasecmp(sDs->form->layout, "HBox") == 0) { contentBox = wgtHBox(root); } else { contentBox = wgtVBox(root); } contentBox->weight = 100; // Clear widget pointers and recreate 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) { snprintf(newValue, sizeof(newValue), ""); } 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 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]; 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]; for (int32_t i = 0; i < fieldCount; i++) { fieldPtrs[i] = fieldNames[i]; } int32_t defIdx = -1; for (int32_t i = 0; i < fieldCount; i++) { if (strcasecmp(fieldNames[i], curValue) == 0) { defIdx = i; break; } } int32_t chosenIdx = 0; if (!dvxChoiceDialog(sPrpCtx, "DataField", "Select column:", fieldPtrs, fieldCount, defIdx, &chosenIdx)) { return; } snprintf(newValue, sizeof(newValue), "%s", fieldNames[chosenIdx]); } } 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]; for (int32_t i = 0; i < tableCount; i++) { tablePtrs[i] = tableNames[i]; } int32_t defIdx = -1; for (int32_t i = 0; i < tableCount; i++) { if (strcasecmp(tableNames[i], curValue) == 0) { defIdx = i; break; } } int32_t chosenIdx = 0; if (!dvxChoiceDialog(sPrpCtx, "RecordSource", "Select table:", tablePtrs, tableCount, defIdx, &chosenIdx)) { return; } snprintf(newValue, sizeof(newValue), "%s", tableNames[chosenIdx]); } } 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); } } } 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) { DsgnControlT ctrl = srcArr[i]; snprintf(ctrl.parentName, DSGN_MAX_NAME, "%s", parentName); arrput(*outArr, ctrl); // Recurse into children (for containers) if (item->firstChild) { collectTreeOrder(item, srcArr, srcCount, outArr, itemName); } break; } } } } static void onTreeReorder(WidgetT *w) { (void)w; if (!sDs || !sDs->form || !sTree || sUpdating) { return; } int32_t count = (int32_t)arrlen(sDs->form->controls); DsgnControlT *newArr = NULL; WidgetT *formItem = sTree->firstChild; if (!formItem) { return; } // Collect all controls from the tree in their new order, // handling nesting (items dragged into containers get parentName updated). 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 = onTreeReorder; 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 // ============================================================ void prpRefresh(DsgnStateT *ds) { if (!ds || !ds->form) { return; } // Don't rebuild the tree here -- just update selection on existing items. // prpRebuildTree destroys all items which loses TreeView selection state. // 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 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); }