1527 lines
50 KiB
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);
|
|
}
|