1527 lines
52 KiB
C
1527 lines
52 KiB
C
// ideDesigner.c -- DVX BASIC form designer implementation
|
|
//
|
|
// WYSIWYG design surface using real DVX widgets positioned
|
|
// identically to how the form runtime lays them out. Selection
|
|
// handles and grid dots are drawn in the window's onPaint callback.
|
|
|
|
#include "ideDesigner.h"
|
|
#include "dvxDraw.h"
|
|
#include "dvxVideo.h"
|
|
#include "dvxWm.h"
|
|
#include "stb_ds_wrap.h"
|
|
|
|
#include <ctype.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <strings.h>
|
|
|
|
// ============================================================
|
|
// Constants
|
|
// ============================================================
|
|
|
|
#define FRM_LINE_MAX 512
|
|
#define DEFAULT_FORM_W 400
|
|
#define DEFAULT_FORM_H 300
|
|
#define DEFAULT_CTRL_W 100
|
|
#define DEFAULT_CTRL_H 30
|
|
#define MIN_CTRL_SIZE 8
|
|
|
|
|
|
// ============================================================
|
|
// Default event for the Form type (not a widget, so not in iface)
|
|
// ============================================================
|
|
|
|
static const char *FORM_DEFAULT_EVENT = "Load";
|
|
|
|
// ============================================================
|
|
// Prototypes
|
|
// ============================================================
|
|
|
|
// dsgnCreateDesignWidget is declared in ideDesigner.h (non-static)
|
|
static const char *getPropValue(const DsgnControlT *ctrl, const char *name);
|
|
static DsgnHandleE hitTestHandles(const DsgnControlT *ctrl, int32_t x, int32_t y);
|
|
static int32_t hitTestControl(const DsgnStateT *ds, int32_t x, int32_t y);
|
|
static const char *resolveTypeName(const char *typeName);
|
|
static void setPropValue(DsgnControlT *ctrl, const char *name, const char *value);
|
|
static void rebuildWidgets(DsgnStateT *ds);
|
|
static void syncWidgetGeom(DsgnControlT *ctrl);
|
|
|
|
|
|
// ============================================================
|
|
// dsgnCreateDesignWidget
|
|
// ============================================================
|
|
//
|
|
// Create a real DVX widget for design-time display. Mirrors the
|
|
// logic in formrt.c createWidget().
|
|
|
|
WidgetT *dsgnCreateDesignWidget(const char *vbTypeName, WidgetT *parent) {
|
|
const char *wgtName = resolveTypeName(vbTypeName);
|
|
|
|
if (!wgtName) {
|
|
return NULL;
|
|
}
|
|
|
|
const void *api = wgtGetApi(wgtName);
|
|
|
|
if (!api) {
|
|
return NULL;
|
|
}
|
|
|
|
const WgtIfaceT *iface = wgtGetIface(wgtName);
|
|
uint8_t sig = iface ? iface->createSig : WGT_CREATE_PARENT;
|
|
|
|
typedef WidgetT *(*CreateParentFnT)(WidgetT *);
|
|
typedef WidgetT *(*CreateParentTextFnT)(WidgetT *, const char *);
|
|
typedef WidgetT *(*CreateParentIntFnT)(WidgetT *, int32_t);
|
|
typedef WidgetT *(*CreateParentIntIntFnT)(WidgetT *, int32_t, int32_t);
|
|
typedef WidgetT *(*CreateParentIntIntIntFnT)(WidgetT *, int32_t, int32_t, int32_t);
|
|
typedef WidgetT *(*CreateParentIntBoolFnT)(WidgetT *, int32_t, bool);
|
|
typedef WidgetT *(*CreateParentBoolFnT)(WidgetT *, bool);
|
|
|
|
switch (sig) {
|
|
case WGT_CREATE_PARENT_TEXT: {
|
|
CreateParentTextFnT fn = *(CreateParentTextFnT *)api;
|
|
return fn(parent, "");
|
|
}
|
|
case WGT_CREATE_PARENT_INT: {
|
|
CreateParentIntFnT fn = *(CreateParentIntFnT *)api;
|
|
return fn(parent, iface->createArgs[0]);
|
|
}
|
|
case WGT_CREATE_PARENT_INT_INT: {
|
|
CreateParentIntIntFnT fn = *(CreateParentIntIntFnT *)api;
|
|
return fn(parent, iface->createArgs[0], iface->createArgs[1]);
|
|
}
|
|
case WGT_CREATE_PARENT_INT_INT_INT: {
|
|
CreateParentIntIntIntFnT fn = *(CreateParentIntIntIntFnT *)api;
|
|
return fn(parent, iface->createArgs[0], iface->createArgs[1], iface->createArgs[2]);
|
|
}
|
|
case WGT_CREATE_PARENT_INT_BOOL: {
|
|
CreateParentIntBoolFnT fn = *(CreateParentIntBoolFnT *)api;
|
|
return fn(parent, iface->createArgs[0], (bool)iface->createArgs[1]);
|
|
}
|
|
case WGT_CREATE_PARENT_BOOL: {
|
|
CreateParentBoolFnT fn = *(CreateParentBoolFnT *)api;
|
|
return fn(parent, (bool)iface->createArgs[0]);
|
|
}
|
|
case WGT_CREATE_PARENT_DATA:
|
|
// Image/ImageButton -- cannot auto-create without pixel data
|
|
return NULL;
|
|
default: {
|
|
CreateParentFnT fn = *(CreateParentFnT *)api;
|
|
return fn(parent);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dsgnCreateFormWindow
|
|
// ============================================================
|
|
|
|
WidgetT *dsgnCreateContentBox(WidgetT *root, const char *layout) {
|
|
// wgtInitWindow creates a VBox root. If the requested layout is VBox
|
|
// (or empty/missing), reuse root directly to avoid double-nesting.
|
|
if (!layout || !layout[0] || strcasecmp(layout, "VBox") == 0) {
|
|
return root;
|
|
}
|
|
|
|
// Look up the layout widget by BASIC name and create it dynamically
|
|
const char *wgtName = wgtFindByBasName(layout);
|
|
|
|
if (wgtName) {
|
|
const WgtIfaceT *iface = wgtGetIface(wgtName);
|
|
|
|
if (iface && iface->isContainer && iface->createSig == WGT_CREATE_PARENT) {
|
|
const void *api = wgtGetApi(wgtName);
|
|
|
|
if (api) {
|
|
// All WGT_CREATE_PARENT APIs have create(parent) as the first function pointer
|
|
WidgetT *(*createFn)(WidgetT *) = *(WidgetT *(*const *)(WidgetT *))api;
|
|
WidgetT *box = createFn(root);
|
|
box->weight = 100;
|
|
return box;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Unknown layout — fall back to root VBox
|
|
return root;
|
|
}
|
|
|
|
|
|
WindowT *dsgnCreateFormWindow(AppContextT *ctx, const char *title, const char *layout, bool resizable, bool centered, bool autoSize, int32_t width, int32_t height, int32_t left, int32_t top, WidgetT **outRoot, WidgetT **outContentBox) {
|
|
int32_t defW = (width > 0) ? width : 400;
|
|
int32_t defH = (height > 0) ? height : 300;
|
|
|
|
WindowT *win = dvxCreateWindowCentered(ctx, title, defW, defH, resizable);
|
|
|
|
if (!win) {
|
|
return NULL;
|
|
}
|
|
|
|
win->visible = false;
|
|
|
|
WidgetT *root = wgtInitWindow(ctx, win);
|
|
|
|
if (!root) {
|
|
dvxDestroyWindow(ctx, win);
|
|
return NULL;
|
|
}
|
|
|
|
WidgetT *contentBox = dsgnCreateContentBox(root, layout);
|
|
|
|
// Apply sizing
|
|
if (autoSize) {
|
|
dvxFitWindow(ctx, win);
|
|
} else if (width > 0 && height > 0) {
|
|
dvxResizeWindow(ctx, win, width, height);
|
|
}
|
|
|
|
// Apply positioning
|
|
if (centered) {
|
|
win->x = (ctx->display.width - win->w) / 2;
|
|
win->y = (ctx->display.height - win->h) / 2;
|
|
} else if (left > 0 || top > 0) {
|
|
win->x = left;
|
|
win->y = top;
|
|
}
|
|
|
|
*outRoot = root;
|
|
*outContentBox = contentBox;
|
|
return win;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dsgnBuildPreviewMenuBar
|
|
// ============================================================
|
|
|
|
void dsgnBuildPreviewMenuBar(WindowT *win, const DsgnFormT *form) {
|
|
if (!win || !form) {
|
|
return;
|
|
}
|
|
|
|
int32_t menuCount = (int32_t)arrlen(form->menuItems);
|
|
|
|
if (menuCount <= 0) {
|
|
return;
|
|
}
|
|
|
|
MenuBarT *bar = wmAddMenuBar(win);
|
|
|
|
if (!bar) {
|
|
return;
|
|
}
|
|
|
|
MenuT *menuStack[8];
|
|
memset(menuStack, 0, sizeof(menuStack));
|
|
|
|
for (int32_t i = 0; i < menuCount; i++) {
|
|
const DsgnMenuItemT *mi = &form->menuItems[i];
|
|
bool isSep = (mi->caption[0] == '-');
|
|
bool isSubParent = (i + 1 < menuCount && form->menuItems[i + 1].level > mi->level);
|
|
|
|
if (mi->level == 0) {
|
|
menuStack[0] = wmAddMenu(bar, mi->caption);
|
|
} else if (isSep && mi->level > 0 && mi->level < 8 && menuStack[mi->level - 1]) {
|
|
wmAddMenuSeparator(menuStack[mi->level - 1]);
|
|
} else if (isSubParent && mi->level > 0 && mi->level < 8 && menuStack[mi->level - 1]) {
|
|
menuStack[mi->level] = wmAddSubMenu(menuStack[mi->level - 1], mi->caption);
|
|
} else if (mi->level > 0 && mi->level < 8 && menuStack[mi->level - 1]) {
|
|
int32_t id = DSGN_MENU_ID_BASE + i;
|
|
|
|
if (mi->radioCheck) {
|
|
wmAddMenuRadioItem(menuStack[mi->level - 1], mi->caption, id, mi->checked);
|
|
} else if (mi->checked) {
|
|
wmAddMenuCheckItem(menuStack[mi->level - 1], mi->caption, id, true);
|
|
} else {
|
|
wmAddMenuItem(menuStack[mi->level - 1], mi->caption, id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dsgnAutoName
|
|
// ============================================================
|
|
|
|
void dsgnAutoName(const DsgnStateT *ds, const char *typeName, char *buf, int32_t bufSize) {
|
|
// Look up the name prefix from the widget interface descriptor.
|
|
// Falls back to the type name itself if no prefix is registered.
|
|
const char *prefix = typeName;
|
|
const char *wgtName = wgtFindByBasName(typeName);
|
|
|
|
if (wgtName) {
|
|
const WgtIfaceT *iface = wgtGetIface(wgtName);
|
|
|
|
if (iface && iface->namePrefix) {
|
|
prefix = iface->namePrefix;
|
|
}
|
|
}
|
|
|
|
int32_t highest = 0;
|
|
int32_t prefixLen = (int32_t)strlen(prefix);
|
|
int32_t count = ds->form ? (int32_t)arrlen(ds->form->controls) : 0;
|
|
|
|
for (int32_t i = 0; i < count; i++) {
|
|
if (strncasecmp(ds->form->controls[i]->name, prefix, prefixLen) == 0) {
|
|
int32_t num = atoi(ds->form->controls[i]->name + prefixLen);
|
|
|
|
if (num > highest) {
|
|
highest = num;
|
|
}
|
|
}
|
|
}
|
|
|
|
snprintf(buf, bufSize, "%s%d", prefix, (int)(highest + 1));
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dsgnCreateWidgets
|
|
// ============================================================
|
|
|
|
void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox) {
|
|
if (!ds->form || !contentBox) {
|
|
return;
|
|
}
|
|
|
|
ds->form->contentBox = contentBox;
|
|
int32_t count = (int32_t)arrlen(ds->form->controls);
|
|
|
|
// Two passes: first create all controls (so containers exist),
|
|
// then parent children inside their containers.
|
|
// Pass 1: create all widgets as top-level children
|
|
for (int32_t i = 0; i < count; i++) {
|
|
DsgnControlT *ctrl = ds->form->controls[i];
|
|
|
|
if (ctrl->widget) {
|
|
continue;
|
|
}
|
|
|
|
// Find the parent widget. For containers with a non-VBox Layout
|
|
// property, create a content box inside so children use the
|
|
// correct layout direction.
|
|
WidgetT *parent = contentBox;
|
|
|
|
if (ctrl->parentName[0]) {
|
|
for (int32_t j = 0; j < count; j++) {
|
|
if (j != i && ds->form->controls[j]->widget &&
|
|
strcasecmp(ds->form->controls[j]->name, ctrl->parentName) == 0) {
|
|
DsgnControlT *pc = ds->form->controls[j];
|
|
parent = pc->widget;
|
|
const char *layout = getPropValue(pc, "Layout");
|
|
|
|
if (layout && layout[0] && strcasecmp(layout, "VBox") != 0) {
|
|
// Check if we already created a content box inside
|
|
if (!parent->firstChild || !parent->firstChild->userData ||
|
|
parent->firstChild->userData != (void *)pc) {
|
|
WidgetT *box = dsgnCreateContentBox(parent, layout);
|
|
box->userData = (void *)pc;
|
|
}
|
|
|
|
parent = parent->firstChild;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
WidgetT *w = dsgnCreateDesignWidget(ctrl->typeName, parent);
|
|
|
|
if (!w) {
|
|
continue;
|
|
}
|
|
|
|
ctrl->widget = w;
|
|
wgtSetName(w, ctrl->name);
|
|
|
|
// Set Caption/Text
|
|
const char *caption = getPropValue(ctrl, "Caption");
|
|
const char *text = getPropValue(ctrl, "Text");
|
|
|
|
if (caption) { wgtSetText(w, caption); }
|
|
if (text) { wgtSetText(w, text); }
|
|
|
|
// Set size hints for the layout engine.
|
|
// minW/minH set the floor; maxW/maxH cap the size.
|
|
if (ctrl->width > 0) {
|
|
w->minW = wgtPixels(ctrl->width);
|
|
}
|
|
|
|
if (ctrl->height > 0) {
|
|
w->minH = wgtPixels(ctrl->height);
|
|
}
|
|
|
|
if (ctrl->maxWidth > 0) {
|
|
w->maxW = wgtPixels(ctrl->maxWidth);
|
|
}
|
|
|
|
if (ctrl->maxHeight > 0) {
|
|
w->maxH = wgtPixels(ctrl->maxHeight);
|
|
}
|
|
|
|
w->weight = ctrl->weight;
|
|
|
|
// Apply interface properties (Alignment, etc.) from FRM data
|
|
const char *wgtName = wgtFindByBasName(ctrl->typeName);
|
|
const WgtIfaceT *iface = wgtName ? wgtGetIface(wgtName) : NULL;
|
|
|
|
if (iface) {
|
|
for (int32_t pi = 0; pi < iface->propCount; pi++) {
|
|
const WgtPropDescT *p = &iface->props[pi];
|
|
|
|
if (!p->setFn) {
|
|
continue;
|
|
}
|
|
|
|
const char *val = getPropValue(ctrl, p->name);
|
|
|
|
if (!val) {
|
|
continue;
|
|
}
|
|
|
|
if (p->type == WGT_IFACE_ENUM && p->enumNames) {
|
|
for (int32_t en = 0; p->enumNames[en]; en++) {
|
|
if (strcasecmp(p->enumNames[en], val) == 0) {
|
|
((void (*)(WidgetT *, int32_t))p->setFn)(w, en);
|
|
break;
|
|
}
|
|
}
|
|
} else if (p->type == WGT_IFACE_INT) {
|
|
((void (*)(WidgetT *, int32_t))p->setFn)(w, atoi(val));
|
|
} else if (p->type == WGT_IFACE_BOOL) {
|
|
((void (*)(WidgetT *, bool))p->setFn)(w, strcasecmp(val, "True") == 0);
|
|
} else if (p->type == WGT_IFACE_STRING) {
|
|
((void (*)(WidgetT *, const char *))p->setFn)(w, val);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dsgnIsContainer
|
|
// ============================================================
|
|
|
|
bool dsgnIsContainer(const char *typeName) {
|
|
const char *wgtName = wgtFindByBasName(typeName);
|
|
|
|
if (wgtName) {
|
|
const WgtIfaceT *iface = wgtGetIface(wgtName);
|
|
|
|
if (iface) {
|
|
return iface->isContainer;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dsgnDefaultEvent
|
|
// ============================================================
|
|
|
|
const char *dsgnDefaultEvent(const char *typeName) {
|
|
if (strcasecmp(typeName, "Form") == 0) {
|
|
return FORM_DEFAULT_EVENT;
|
|
}
|
|
|
|
const char *wgtName = wgtFindByBasName(typeName);
|
|
|
|
if (wgtName) {
|
|
const WgtIfaceT *iface = wgtGetIface(wgtName);
|
|
|
|
if (iface && iface->defaultEvent) {
|
|
return iface->defaultEvent;
|
|
}
|
|
}
|
|
|
|
return "Click";
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dsgnFree
|
|
// ============================================================
|
|
|
|
void dsgnFree(DsgnStateT *ds) {
|
|
if (ds->form) {
|
|
for (int32_t i = 0; i < arrlen(ds->form->controls); i++) {
|
|
free(ds->form->controls[i]);
|
|
}
|
|
arrfree(ds->form->controls);
|
|
arrfree(ds->form->menuItems);
|
|
free(ds->form->code);
|
|
free(ds->form);
|
|
ds->form = NULL;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dsgnInit
|
|
// ============================================================
|
|
|
|
void dsgnInit(DsgnStateT *ds, AppContextT *ctx) {
|
|
memset(ds, 0, sizeof(*ds));
|
|
ds->selectedIdx = -1;
|
|
ds->activeTool[0] = '\0';
|
|
ds->mode = DSGN_IDLE;
|
|
ds->activeHandle = HANDLE_NONE;
|
|
ds->ctx = ctx;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dsgnLoadFrm
|
|
// ============================================================
|
|
|
|
bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
|
|
if (!source || sourceLen <= 0) {
|
|
return false;
|
|
}
|
|
|
|
dsgnFree(ds);
|
|
|
|
DsgnFormT *form = (DsgnFormT *)calloc(1, sizeof(DsgnFormT));
|
|
|
|
if (!form) {
|
|
return false;
|
|
}
|
|
|
|
form->controls = NULL;
|
|
form->width = DEFAULT_FORM_W;
|
|
form->height = DEFAULT_FORM_H;
|
|
form->left = 0;
|
|
form->top = 0;
|
|
snprintf(form->layout, DSGN_MAX_NAME, "VBox");
|
|
form->centered = true;
|
|
form->autoSize = true;
|
|
form->resizable = true;
|
|
snprintf(form->name, DSGN_MAX_NAME, "Form1");
|
|
snprintf(form->caption, DSGN_MAX_TEXT, "Form1");
|
|
|
|
DsgnControlT *curCtrl = NULL;
|
|
DsgnMenuItemT *curMenuItem = NULL;
|
|
bool inForm = false;
|
|
bool inMenu = false;
|
|
int32_t menuNestDepth = 0;
|
|
int32_t blockDepth = 0; // Begin/End nesting depth (0 = form level)
|
|
bool blockIsContainer[DSGN_MAX_FRM_NESTING]; // whether each block is a container
|
|
|
|
// Parent name stack for nesting (index 0 = form level)
|
|
char parentStack[8][DSGN_MAX_NAME];
|
|
int32_t nestDepth = 0;
|
|
parentStack[0][0] = '\0';
|
|
|
|
const char *pos = source;
|
|
const char *end = source + sourceLen;
|
|
|
|
while (pos < end) {
|
|
const char *lineStart = pos;
|
|
|
|
while (pos < end && *pos != '\n' && *pos != '\r') {
|
|
pos++;
|
|
}
|
|
|
|
int32_t lineLen = (int32_t)(pos - lineStart);
|
|
|
|
if (pos < end && *pos == '\r') { pos++; }
|
|
if (pos < end && *pos == '\n') { pos++; }
|
|
|
|
char line[FRM_LINE_MAX];
|
|
|
|
if (lineLen >= FRM_LINE_MAX) {
|
|
lineLen = FRM_LINE_MAX - 1;
|
|
}
|
|
|
|
memcpy(line, lineStart, lineLen);
|
|
line[lineLen] = '\0';
|
|
|
|
char *trimmed = line;
|
|
|
|
while (*trimmed == ' ' || *trimmed == '\t') {
|
|
trimmed++;
|
|
}
|
|
|
|
if (*trimmed == '\0' || *trimmed == '\'') {
|
|
continue;
|
|
}
|
|
|
|
if (strncasecmp(trimmed, "VERSION ", 8) == 0) {
|
|
// Accept "VERSION DVX x.xx" (native) and "VERSION x.xx" (VB import).
|
|
// Reject VB forms with version > 1.xx (VB4+/VB6 use features we
|
|
// don't support like OLE controls and binary properties).
|
|
const char *ver = trimmed + 8;
|
|
|
|
if (strncasecmp(ver, "DVX ", 4) == 0) {
|
|
// Native DVX BASIC form — always accepted
|
|
} else {
|
|
// VB form — check version number
|
|
double vbVer = atof(ver);
|
|
|
|
if (vbVer > 2.0) {
|
|
return false; // VB4+ form, not compatible
|
|
}
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// Begin TypeName CtrlName
|
|
if (strncasecmp(trimmed, "Begin ", 6) == 0) {
|
|
char *rest = trimmed + 6;
|
|
char typeName[DSGN_MAX_NAME];
|
|
char ctrlName[DSGN_MAX_NAME];
|
|
int32_t ti = 0;
|
|
|
|
while (*rest && *rest != ' ' && *rest != '\t' && ti < DSGN_MAX_NAME - 1) {
|
|
typeName[ti++] = *rest++;
|
|
}
|
|
|
|
typeName[ti] = '\0';
|
|
|
|
while (*rest == ' ' || *rest == '\t') { rest++; }
|
|
|
|
int32_t ci = 0;
|
|
|
|
while (*rest && *rest != ' ' && *rest != '\t' && *rest != '\r' && *rest != '\n' && ci < DSGN_MAX_NAME - 1) {
|
|
ctrlName[ci++] = *rest++;
|
|
}
|
|
|
|
ctrlName[ci] = '\0';
|
|
|
|
if (strcasecmp(typeName, "Form") == 0) {
|
|
snprintf(form->name, DSGN_MAX_NAME, "%s", ctrlName);
|
|
snprintf(form->caption, DSGN_MAX_TEXT, "%s", ctrlName);
|
|
inForm = true;
|
|
nestDepth = 0;
|
|
curCtrl = NULL;
|
|
} else if (strcasecmp(typeName, "Menu") == 0 && inForm) {
|
|
DsgnMenuItemT mi;
|
|
memset(&mi, 0, sizeof(mi));
|
|
snprintf(mi.name, DSGN_MAX_NAME, "%s", ctrlName);
|
|
mi.level = menuNestDepth;
|
|
mi.enabled = true;
|
|
arrput(form->menuItems, mi);
|
|
curMenuItem = &form->menuItems[arrlen(form->menuItems) - 1];
|
|
curCtrl = NULL; // not a control
|
|
menuNestDepth++;
|
|
inMenu = true;
|
|
if (blockDepth < DSGN_MAX_FRM_NESTING) { blockIsContainer[blockDepth] = false; }
|
|
blockDepth++;
|
|
} else if (inForm) {
|
|
DsgnControlT *cp = (DsgnControlT *)calloc(1, sizeof(DsgnControlT));
|
|
cp->index = -1;
|
|
snprintf(cp->name, DSGN_MAX_NAME, "%s", ctrlName);
|
|
snprintf(cp->typeName, DSGN_MAX_NAME, "%s", typeName);
|
|
|
|
// Set parent from current nesting
|
|
if (nestDepth > 0) {
|
|
snprintf(cp->parentName, DSGN_MAX_NAME, "%s", parentStack[nestDepth - 1]);
|
|
}
|
|
|
|
cp->width = DEFAULT_CTRL_W;
|
|
cp->height = DEFAULT_CTRL_H;
|
|
arrput(form->controls, cp);
|
|
curCtrl = form->controls[arrlen(form->controls) - 1];
|
|
bool isCtrl = dsgnIsContainer(typeName);
|
|
if (blockDepth < DSGN_MAX_FRM_NESTING) { blockIsContainer[blockDepth] = isCtrl; }
|
|
blockDepth++;
|
|
|
|
// If this is a container, push onto parent stack
|
|
if (isCtrl && nestDepth < 7) {
|
|
snprintf(parentStack[nestDepth], DSGN_MAX_NAME, "%s", ctrlName);
|
|
nestDepth++;
|
|
}
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (strcasecmp(trimmed, "End") == 0) {
|
|
if (blockDepth > 0) {
|
|
blockDepth--;
|
|
|
|
if (inMenu) {
|
|
menuNestDepth--;
|
|
curMenuItem = NULL;
|
|
|
|
if (menuNestDepth <= 0) {
|
|
menuNestDepth = 0;
|
|
inMenu = false;
|
|
}
|
|
} else {
|
|
// If we're closing a container, pop the parent stack
|
|
if (blockDepth < DSGN_MAX_FRM_NESTING && blockIsContainer[blockDepth] && nestDepth > 0) {
|
|
nestDepth--;
|
|
}
|
|
|
|
curCtrl = NULL;
|
|
}
|
|
} else {
|
|
// blockDepth == 0: this is the form's closing End
|
|
inForm = false;
|
|
|
|
// Everything after the form's closing End is code
|
|
if (pos < end) {
|
|
// Skip leading blank lines
|
|
const char *codeStart = pos;
|
|
|
|
while (codeStart < end && (*codeStart == '\r' || *codeStart == '\n' || *codeStart == ' ' || *codeStart == '\t')) {
|
|
codeStart++;
|
|
}
|
|
|
|
if (codeStart < end) {
|
|
int32_t codeLen = (int32_t)(end - codeStart);
|
|
form->code = (char *)malloc(codeLen + 1);
|
|
|
|
if (form->code) {
|
|
memcpy(form->code, codeStart, codeLen);
|
|
form->code[codeLen] = '\0';
|
|
}
|
|
}
|
|
}
|
|
|
|
break; // done parsing
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// Property = Value
|
|
char *eq = strchr(trimmed, '=');
|
|
|
|
if (eq && inForm) {
|
|
char key[DSGN_MAX_NAME];
|
|
char *kend = eq - 1;
|
|
|
|
while (kend > trimmed && (*kend == ' ' || *kend == '\t')) { kend--; }
|
|
|
|
int32_t klen = (int32_t)(kend - trimmed + 1);
|
|
|
|
if (klen >= DSGN_MAX_NAME) { klen = DSGN_MAX_NAME - 1; }
|
|
|
|
memcpy(key, trimmed, klen);
|
|
key[klen] = '\0';
|
|
|
|
char *vstart = eq + 1;
|
|
|
|
while (*vstart == ' ' || *vstart == '\t') { vstart++; }
|
|
|
|
char val[DSGN_MAX_TEXT];
|
|
int32_t vi = 0;
|
|
|
|
if (*vstart == '"') {
|
|
vstart++;
|
|
|
|
while (*vstart && *vstart != '"' && vi < DSGN_MAX_TEXT - 1) {
|
|
val[vi++] = *vstart++;
|
|
}
|
|
} else {
|
|
while (*vstart && *vstart != '\r' && *vstart != '\n' && vi < DSGN_MAX_TEXT - 1) {
|
|
val[vi++] = *vstart++;
|
|
}
|
|
|
|
while (vi > 0 && (val[vi - 1] == ' ' || val[vi - 1] == '\t')) { vi--; }
|
|
}
|
|
|
|
val[vi] = '\0';
|
|
|
|
if (curMenuItem) {
|
|
if (strcasecmp(key, "Caption") == 0) { snprintf(curMenuItem->caption, DSGN_MAX_TEXT, "%s", val); }
|
|
else if (strcasecmp(key, "Checked") == 0) { curMenuItem->checked = (strcasecmp(val, "True") == 0 || strcasecmp(val, "-1") == 0); }
|
|
else if (strcasecmp(key, "RadioCheck") == 0) { curMenuItem->radioCheck = (strcasecmp(val, "True") == 0 || strcasecmp(val, "-1") == 0); }
|
|
else if (strcasecmp(key, "Enabled") == 0) { curMenuItem->enabled = (strcasecmp(val, "True") == 0 || strcasecmp(val, "-1") == 0); }
|
|
} else if (curCtrl) {
|
|
if (strcasecmp(key, "Left") == 0) { curCtrl->left = atoi(val); }
|
|
else if (strcasecmp(key, "Top") == 0) { curCtrl->top = atoi(val); }
|
|
else if (strcasecmp(key, "MinWidth") == 0 ||
|
|
strcasecmp(key, "Width") == 0) { curCtrl->width = atoi(val); }
|
|
else if (strcasecmp(key, "MinHeight") == 0 ||
|
|
strcasecmp(key, "Height") == 0) { curCtrl->height = atoi(val); }
|
|
else if (strcasecmp(key, "MaxWidth") == 0) { curCtrl->maxWidth = atoi(val); }
|
|
else if (strcasecmp(key, "MaxHeight") == 0) { curCtrl->maxHeight = atoi(val); }
|
|
else if (strcasecmp(key, "Weight") == 0) { curCtrl->weight = atoi(val); }
|
|
else if (strcasecmp(key, "Index") == 0) { curCtrl->index = atoi(val); }
|
|
else if (strcasecmp(key, "TabIndex") == 0) { /* ignored -- DVX has no tab order */ }
|
|
else { setPropValue(curCtrl, key, val); }
|
|
} else {
|
|
if (strcasecmp(key, "Caption") == 0) { snprintf(form->caption, DSGN_MAX_TEXT, "%s", val); }
|
|
else if (strcasecmp(key, "Layout") == 0) { strncpy(form->layout, val, DSGN_MAX_NAME - 1); form->layout[DSGN_MAX_NAME - 1] = '\0'; }
|
|
else if (strcasecmp(key, "AutoSize") == 0) { form->autoSize = (strcasecmp(val, "True") == 0); }
|
|
else if (strcasecmp(key, "Resizable") == 0) { form->resizable = (strcasecmp(val, "True") == 0); }
|
|
else if (strcasecmp(key, "Centered") == 0) { form->centered = (strcasecmp(val, "True") == 0); }
|
|
else if (strcasecmp(key, "Left") == 0) { form->left = atoi(val); }
|
|
else if (strcasecmp(key, "Top") == 0) { form->top = atoi(val); }
|
|
else if (strcasecmp(key, "Width") == 0) { form->width = atoi(val); form->autoSize = false; }
|
|
else if (strcasecmp(key, "Height") == 0) { form->height = atoi(val); form->autoSize = false; }
|
|
}
|
|
}
|
|
}
|
|
|
|
ds->form = form;
|
|
ds->selectedIdx = -1;
|
|
return true;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dsgnNewForm
|
|
// ============================================================
|
|
|
|
void dsgnNewForm(DsgnStateT *ds, const char *name) {
|
|
dsgnFree(ds);
|
|
|
|
DsgnFormT *form = (DsgnFormT *)calloc(1, sizeof(DsgnFormT));
|
|
form->controls = NULL;
|
|
form->width = DEFAULT_FORM_W;
|
|
form->height = DEFAULT_FORM_H;
|
|
form->left = 0;
|
|
form->top = 0;
|
|
snprintf(form->layout, DSGN_MAX_NAME, "VBox");
|
|
form->centered = true;
|
|
form->autoSize = false;
|
|
form->resizable = true;
|
|
snprintf(form->name, DSGN_MAX_NAME, "%s", name);
|
|
snprintf(form->caption, DSGN_MAX_TEXT, "%s", name);
|
|
|
|
ds->form = form;
|
|
ds->selectedIdx = -1;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dsgnOnKey
|
|
// ============================================================
|
|
|
|
void dsgnOnKey(DsgnStateT *ds, int32_t key) {
|
|
if (!ds->form) {
|
|
return;
|
|
}
|
|
|
|
int32_t count = (int32_t)arrlen(ds->form->controls);
|
|
|
|
// Delete key -- remove the selected control and any children
|
|
if (key == 0x153 && ds->selectedIdx >= 0 && ds->selectedIdx < count) {
|
|
const char *delName = ds->form->controls[ds->selectedIdx]->name;
|
|
|
|
// Remove children first (controls whose parentName matches)
|
|
for (int32_t i = count - 1; i >= 0; i--) {
|
|
if (i != ds->selectedIdx && strcasecmp(ds->form->controls[i]->parentName, delName) == 0) {
|
|
free(ds->form->controls[i]);
|
|
arrdel(ds->form->controls, i);
|
|
|
|
if (i < ds->selectedIdx) {
|
|
ds->selectedIdx--;
|
|
}
|
|
}
|
|
}
|
|
|
|
free(ds->form->controls[ds->selectedIdx]);
|
|
arrdel(ds->form->controls, ds->selectedIdx);
|
|
ds->selectedIdx = -1;
|
|
ds->form->dirty = true;
|
|
|
|
rebuildWidgets(ds);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dsgnOnMouse
|
|
// ============================================================
|
|
|
|
void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) {
|
|
if (!ds->form) {
|
|
return;
|
|
}
|
|
|
|
int32_t ctrlCount = (int32_t)arrlen(ds->form->controls);
|
|
|
|
if (drag) {
|
|
if (ds->mode == DSGN_RESIZING && ds->selectedIdx >= 0 && ds->selectedIdx < ctrlCount) {
|
|
DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx];
|
|
int32_t dx = x - ds->dragStartX;
|
|
int32_t dy = y - ds->dragStartY;
|
|
|
|
switch (ds->activeHandle) {
|
|
case HANDLE_E:
|
|
ctrl->width = ds->dragOrigWidth + dx;
|
|
break;
|
|
case HANDLE_S:
|
|
ctrl->height = ds->dragOrigHeight + dy;
|
|
break;
|
|
case HANDLE_SE:
|
|
ctrl->width = ds->dragOrigWidth + dx;
|
|
ctrl->height = ds->dragOrigHeight + dy;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (ctrl->width < MIN_CTRL_SIZE) { ctrl->width = MIN_CTRL_SIZE; }
|
|
if (ctrl->height < MIN_CTRL_SIZE) { ctrl->height = MIN_CTRL_SIZE; }
|
|
|
|
syncWidgetGeom(ctrl);
|
|
ds->form->dirty = true;
|
|
} else if (ds->mode == DSGN_REORDERING && ds->selectedIdx >= 0 && ds->selectedIdx < ctrlCount) {
|
|
// Determine if we should swap with a neighbor based on drag direction
|
|
int32_t dy = y - ds->dragStartY;
|
|
DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx];
|
|
|
|
if (dy > 0 && ctrl->widget) {
|
|
// Dragging down -- swap with next control if past its midpoint
|
|
if (ds->selectedIdx < ctrlCount - 1) {
|
|
DsgnControlT *next = ds->form->controls[ds->selectedIdx + 1];
|
|
|
|
if (next->widget && y > next->widget->y + next->widget->h / 2) {
|
|
DsgnControlT *tmp = ds->form->controls[ds->selectedIdx];
|
|
ds->form->controls[ds->selectedIdx] = ds->form->controls[ds->selectedIdx + 1];
|
|
ds->form->controls[ds->selectedIdx + 1] = tmp;
|
|
rebuildWidgets(ds);
|
|
ds->selectedIdx++;
|
|
ds->dragStartY = y;
|
|
ds->form->dirty = true;
|
|
}
|
|
}
|
|
} else if (dy < 0 && ctrl->widget) {
|
|
// Dragging up -- swap with previous control if past its midpoint
|
|
if (ds->selectedIdx > 0) {
|
|
DsgnControlT *prev = ds->form->controls[ds->selectedIdx - 1];
|
|
|
|
if (prev->widget && y < prev->widget->y + prev->widget->h / 2) {
|
|
DsgnControlT *tmp = ds->form->controls[ds->selectedIdx];
|
|
ds->form->controls[ds->selectedIdx] = ds->form->controls[ds->selectedIdx - 1];
|
|
ds->form->controls[ds->selectedIdx - 1] = tmp;
|
|
rebuildWidgets(ds);
|
|
ds->selectedIdx--;
|
|
ds->dragStartY = y;
|
|
ds->form->dirty = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Mouse click (not drag) -- end any ongoing operation
|
|
if (ds->mode == DSGN_REORDERING || ds->mode == DSGN_RESIZING) {
|
|
ds->mode = DSGN_IDLE;
|
|
return;
|
|
}
|
|
|
|
// Pointer tool: select, start resize or reorder
|
|
if (ds->activeTool[0] == '\0') {
|
|
// Check grab handles of selected control first
|
|
if (ds->selectedIdx >= 0 && ds->selectedIdx < ctrlCount) {
|
|
DsgnHandleE handle = hitTestHandles(ds->form->controls[ds->selectedIdx], x, y);
|
|
|
|
if (handle != HANDLE_NONE) {
|
|
DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx];
|
|
ds->mode = DSGN_RESIZING;
|
|
ds->activeHandle = handle;
|
|
ds->dragStartX = x;
|
|
ds->dragStartY = y;
|
|
ds->dragOrigWidth = ctrl->widget ? ctrl->widget->w : ctrl->width;
|
|
ds->dragOrigHeight = ctrl->widget ? ctrl->widget->h : ctrl->height;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Hit test controls -- click to select, drag to reorder
|
|
int32_t hit = hitTestControl(ds, x, y);
|
|
|
|
if (hit >= 0) {
|
|
ds->selectedIdx = hit;
|
|
ds->mode = DSGN_REORDERING;
|
|
ds->dragStartY = y;
|
|
} else {
|
|
ds->selectedIdx = -1;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Non-pointer tool: place a new control
|
|
const char *typeName = ds->activeTool;
|
|
DsgnControlT *cp = (DsgnControlT *)calloc(1, sizeof(DsgnControlT));
|
|
cp->index = -1;
|
|
dsgnAutoName(ds, typeName, cp->name, DSGN_MAX_NAME);
|
|
snprintf(cp->typeName, DSGN_MAX_NAME, "%s", typeName);
|
|
cp->width = DEFAULT_CTRL_W;
|
|
cp->height = DEFAULT_CTRL_H;
|
|
setPropValue(cp, "Caption", cp->name);
|
|
|
|
// Determine parent: if click is inside a container, nest there
|
|
WidgetT *parentWidget = ds->form->contentBox;
|
|
|
|
for (int32_t i = ctrlCount - 1; i >= 0; i--) {
|
|
DsgnControlT *pc = ds->form->controls[i];
|
|
|
|
if (pc->widget && dsgnIsContainer(pc->typeName)) {
|
|
int32_t wx = pc->widget->x;
|
|
int32_t wy = pc->widget->y;
|
|
int32_t ww = pc->widget->w;
|
|
int32_t wh = pc->widget->h;
|
|
|
|
if (x >= wx && x < wx + ww && y >= wy && y < wy + wh) {
|
|
snprintf(cp->parentName, DSGN_MAX_NAME, "%s", pc->name);
|
|
parentWidget = pc->widget;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create the live widget
|
|
if (parentWidget) {
|
|
cp->widget = dsgnCreateDesignWidget(typeName, parentWidget);
|
|
|
|
if (cp->widget) {
|
|
cp->widget->minW = wgtPixels(cp->width);
|
|
cp->widget->minH = wgtPixels(cp->height);
|
|
}
|
|
}
|
|
|
|
arrput(ds->form->controls, cp);
|
|
ds->selectedIdx = (int32_t)arrlen(ds->form->controls) - 1;
|
|
|
|
// Set text AFTER arrput so pointers are stable (heap-allocated, so always stable)
|
|
DsgnControlT *stable = ds->form->controls[ds->selectedIdx];
|
|
|
|
if (stable->widget) {
|
|
const char *caption = getPropValue(stable, "Caption");
|
|
wgtSetName(stable->widget, stable->name);
|
|
wgtSetText(stable->widget, caption ? caption : stable->name);
|
|
}
|
|
ds->activeTool[0] = '\0';
|
|
ds->mode = DSGN_IDLE;
|
|
ds->form->dirty = true;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dsgnPaint
|
|
// ============================================================
|
|
//
|
|
// Draw grid dots and selection handles over the live widgets.
|
|
// Called from the form window's onPaint callback.
|
|
|
|
void dsgnPaint(DsgnStateT *ds) {
|
|
if (!ds->form || !ds->ctx) {
|
|
return;
|
|
}
|
|
|
|
// Grid dots and selection handles are drawn directly onto
|
|
// the window via the display backbuffer. The live widgets
|
|
// handle their own rendering.
|
|
|
|
// Nothing to do here for now -- the onPaint hook in ideMain
|
|
// calls dsgnPaintOverlay with the display pointer.
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dsgnPaintOverlay
|
|
// ============================================================
|
|
//
|
|
// Draw selection handles on the window's painted surface.
|
|
// Called after widgets have painted, using direct display drawing.
|
|
|
|
void dsgnPaintOverlay(DsgnStateT *ds, int32_t winX, int32_t winY) {
|
|
(void)winX;
|
|
(void)winY;
|
|
|
|
if (!ds->form || !ds->form->controls || !ds->ctx || !ds->formWin || !ds->formWin->contentBuf) {
|
|
return;
|
|
}
|
|
|
|
int32_t count = (int32_t)arrlen(ds->form->controls);
|
|
|
|
if (ds->selectedIdx < 0 || ds->selectedIdx >= count) {
|
|
return;
|
|
}
|
|
|
|
DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx];
|
|
|
|
if (!ctrl->widget || !ctrl->widget->visible || ctrl->widget->w <= 0 || ctrl->widget->h <= 0) {
|
|
return;
|
|
}
|
|
|
|
// Draw into the window's content buffer (same coordinate space as widgets)
|
|
DisplayT cd = ds->ctx->display;
|
|
cd.backBuf = ds->formWin->contentBuf;
|
|
cd.width = ds->formWin->contentW;
|
|
cd.height = ds->formWin->contentH;
|
|
cd.pitch = ds->formWin->contentPitch;
|
|
cd.clipX = 0;
|
|
cd.clipY = 0;
|
|
cd.clipW = ds->formWin->contentW;
|
|
cd.clipH = ds->formWin->contentH;
|
|
|
|
const BlitOpsT *ops = &ds->ctx->blitOps;
|
|
uint32_t black = packColor(&cd, 0, 0, 0);
|
|
uint32_t gray = packColor(&cd, 128, 128, 128);
|
|
|
|
int32_t cx = ctrl->widget->x;
|
|
int32_t cy = ctrl->widget->y;
|
|
int32_t cw = ctrl->widget->w;
|
|
int32_t ch = ctrl->widget->h;
|
|
int32_t hs = DSGN_HANDLE_SIZE;
|
|
|
|
// All 8 handles: NW, N, NE, E, SE, S, SW, W
|
|
int32_t hx[8] = { cx - hs/2, cx + cw/2 - hs/2, cx + cw - hs/2, cx + cw - hs/2, cx + cw - hs/2, cx + cw/2 - hs/2, cx - hs/2, cx - hs/2 };
|
|
int32_t hy[8] = { cy - hs/2, cy - hs/2, cy - hs/2, cy + ch/2 - hs/2, cy + ch - hs/2, cy + ch - hs/2, cy + ch - hs/2, cy + ch/2 - hs/2 };
|
|
|
|
// E (idx 3), S (idx 5), SE (idx 4) are active (black); rest are inactive (gray)
|
|
for (int32_t i = 0; i < 8; i++) {
|
|
bool active = (i == 3 || i == 4 || i == 5);
|
|
rectFill(&cd, ops, hx[i], hy[i], hs, hs, active ? black : gray);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dsgnSaveFrm
|
|
// ============================================================
|
|
|
|
// Write controls at a given nesting level with the specified parent name.
|
|
|
|
static int32_t saveControls(const DsgnFormT *form, char *buf, int32_t bufSize, int32_t pos, const char *parentName, int32_t indent) {
|
|
int32_t count = (int32_t)arrlen(form->controls);
|
|
|
|
for (int32_t i = 0; i < count; i++) {
|
|
const DsgnControlT *ctrl = form->controls[i];
|
|
|
|
// Only output controls whose parent matches
|
|
if (parentName[0] == '\0' && ctrl->parentName[0] != '\0') { continue; }
|
|
if (parentName[0] != '\0' && strcasecmp(ctrl->parentName, parentName) != 0) { continue; }
|
|
|
|
// Indent
|
|
char pad[32];
|
|
int32_t padLen = indent * 4;
|
|
if (padLen > 31) { padLen = 31; }
|
|
memset(pad, ' ', padLen);
|
|
pad[padLen] = '\0';
|
|
|
|
pos += snprintf(buf + pos, bufSize - pos, "%sBegin %s %s\n", pad, ctrl->typeName, ctrl->name);
|
|
|
|
if (ctrl->index >= 0) {
|
|
pos += snprintf(buf + pos, bufSize - pos, "%s Index = %d\n", pad, (int)ctrl->index);
|
|
}
|
|
|
|
const char *caption = getPropValue(ctrl, "Caption");
|
|
const char *text = getPropValue(ctrl, "Text");
|
|
|
|
if (caption) { pos += snprintf(buf + pos, bufSize - pos, "%s Caption = \"%s\"\n", pad, caption); }
|
|
if (text) { pos += snprintf(buf + pos, bufSize - pos, "%s Text = \"%s\"\n", pad, text); }
|
|
|
|
pos += snprintf(buf + pos, bufSize - pos, "%s Left = %d\n", pad, (int)ctrl->left);
|
|
pos += snprintf(buf + pos, bufSize - pos, "%s Top = %d\n", pad, (int)ctrl->top);
|
|
pos += snprintf(buf + pos, bufSize - pos, "%s MinWidth = %d\n", pad, (int)ctrl->width);
|
|
pos += snprintf(buf + pos, bufSize - pos, "%s MinHeight = %d\n", pad, (int)ctrl->height);
|
|
|
|
if (ctrl->maxWidth > 0) {
|
|
pos += snprintf(buf + pos, bufSize - pos, "%s MaxWidth = %d\n", pad, (int)ctrl->maxWidth);
|
|
}
|
|
|
|
if (ctrl->maxHeight > 0) {
|
|
pos += snprintf(buf + pos, bufSize - pos, "%s MaxHeight = %d\n", pad, (int)ctrl->maxHeight);
|
|
}
|
|
|
|
if (ctrl->weight > 0) {
|
|
pos += snprintf(buf + pos, bufSize - pos, "%s Weight = %d\n", pad, (int)ctrl->weight);
|
|
}
|
|
|
|
for (int32_t j = 0; j < ctrl->propCount; j++) {
|
|
if (strcasecmp(ctrl->props[j].name, "Caption") == 0) { continue; }
|
|
if (strcasecmp(ctrl->props[j].name, "Text") == 0) { continue; }
|
|
|
|
pos += snprintf(buf + pos, bufSize - pos, "%s %s = \"%s\"\n", pad, ctrl->props[j].name, ctrl->props[j].value);
|
|
}
|
|
|
|
// Save interface properties (Alignment, etc.) read from the live widget
|
|
if (ctrl->widget) {
|
|
const char *wgtName = wgtFindByBasName(ctrl->typeName);
|
|
const WgtIfaceT *iface = wgtName ? wgtGetIface(wgtName) : NULL;
|
|
|
|
if (iface) {
|
|
for (int32_t j = 0; j < iface->propCount; j++) {
|
|
const WgtPropDescT *p = &iface->props[j];
|
|
|
|
if (!p->getFn || !p->setFn) {
|
|
continue;
|
|
}
|
|
|
|
// Skip if already saved as a custom prop
|
|
bool already = false;
|
|
|
|
for (int32_t k = 0; k < ctrl->propCount; k++) {
|
|
if (strcasecmp(ctrl->props[k].name, p->name) == 0) {
|
|
already = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (already) {
|
|
continue;
|
|
}
|
|
|
|
if (p->type == WGT_IFACE_ENUM && p->enumNames) {
|
|
int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget);
|
|
const char *name = NULL;
|
|
|
|
for (int32_t en = 0; p->enumNames[en]; en++) {
|
|
if (en == v) {
|
|
name = p->enumNames[en];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (name) {
|
|
pos += snprintf(buf + pos, bufSize - pos, "%s %s = %s\n", pad, p->name, name);
|
|
}
|
|
} else if (p->type == WGT_IFACE_INT) {
|
|
int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget);
|
|
pos += snprintf(buf + pos, bufSize - pos, "%s %s = %d\n", pad, p->name, (int)v);
|
|
} else if (p->type == WGT_IFACE_BOOL) {
|
|
bool v = ((bool (*)(const WidgetT *))p->getFn)(ctrl->widget);
|
|
pos += snprintf(buf + pos, bufSize - pos, "%s %s = %s\n", pad, p->name, v ? "True" : "False");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Recursively output children of this container
|
|
if (dsgnIsContainer(ctrl->typeName)) {
|
|
pos = saveControls(form, buf, bufSize, pos, ctrl->name, indent + 1);
|
|
}
|
|
|
|
pos += snprintf(buf + pos, bufSize - pos, "%sEnd\n", pad);
|
|
}
|
|
|
|
return pos;
|
|
}
|
|
|
|
|
|
int32_t dsgnSaveFrm(const DsgnStateT *ds, char *buf, int32_t bufSize) {
|
|
if (!ds->form || !buf || bufSize <= 0) {
|
|
return -1;
|
|
}
|
|
|
|
int32_t pos = 0;
|
|
|
|
pos += snprintf(buf + pos, bufSize - pos, "VERSION DVX 1.00\n");
|
|
pos += snprintf(buf + pos, bufSize - pos, "Begin Form %s\n", ds->form->name);
|
|
pos += snprintf(buf + pos, bufSize - pos, " Caption = \"%s\"\n", ds->form->caption);
|
|
pos += snprintf(buf + pos, bufSize - pos, " Layout = %s\n", ds->form->layout);
|
|
pos += snprintf(buf + pos, bufSize - pos, " AutoSize = %s\n", ds->form->autoSize ? "True" : "False");
|
|
pos += snprintf(buf + pos, bufSize - pos, " Resizable = %s\n", ds->form->resizable ? "True" : "False");
|
|
pos += snprintf(buf + pos, bufSize - pos, " Centered = %s\n", ds->form->centered ? "True" : "False");
|
|
|
|
if (!ds->form->centered) {
|
|
pos += snprintf(buf + pos, bufSize - pos, " Left = %d\n", (int)ds->form->left);
|
|
pos += snprintf(buf + pos, bufSize - pos, " Top = %d\n", (int)ds->form->top);
|
|
}
|
|
|
|
if (!ds->form->autoSize) {
|
|
pos += snprintf(buf + pos, bufSize - pos, " Width = %d\n", (int)ds->form->width);
|
|
pos += snprintf(buf + pos, bufSize - pos, " Height = %d\n", (int)ds->form->height);
|
|
}
|
|
|
|
// Output menu items as nested Begin Menu blocks
|
|
{
|
|
int32_t menuCount = (int32_t)arrlen(ds->form->menuItems);
|
|
int32_t curLevel = 0;
|
|
|
|
for (int32_t i = 0; i < menuCount; i++) {
|
|
DsgnMenuItemT *mi = &ds->form->menuItems[i];
|
|
|
|
// Close blocks back to this item's level
|
|
while (curLevel > mi->level) {
|
|
curLevel--;
|
|
|
|
for (int32_t p = 0; p < (curLevel + 1) * 4; p++) {
|
|
buf[pos++] = ' ';
|
|
}
|
|
|
|
pos += snprintf(buf + pos, bufSize - pos, "End\n");
|
|
}
|
|
|
|
// Indent: (level + 1) * 4 spaces (one extra for being inside Form block)
|
|
for (int32_t p = 0; p < (mi->level + 1) * 4; p++) {
|
|
buf[pos++] = ' ';
|
|
}
|
|
|
|
pos += snprintf(buf + pos, bufSize - pos, "Begin Menu %s\n", mi->name);
|
|
|
|
// Caption
|
|
for (int32_t p = 0; p < (mi->level + 2) * 4; p++) {
|
|
buf[pos++] = ' ';
|
|
}
|
|
|
|
pos += snprintf(buf + pos, bufSize - pos, "Caption = \"%s\"\n", mi->caption);
|
|
|
|
// Optional properties
|
|
if (mi->checked) {
|
|
for (int32_t p = 0; p < (mi->level + 2) * 4; p++) {
|
|
buf[pos++] = ' ';
|
|
}
|
|
|
|
pos += snprintf(buf + pos, bufSize - pos, "Checked = True\n");
|
|
}
|
|
|
|
if (mi->radioCheck) {
|
|
for (int32_t p = 0; p < (mi->level + 2) * 4; p++) {
|
|
buf[pos++] = ' ';
|
|
}
|
|
|
|
pos += snprintf(buf + pos, bufSize - pos, "RadioCheck = True\n");
|
|
}
|
|
|
|
if (!mi->enabled) {
|
|
for (int32_t p = 0; p < (mi->level + 2) * 4; p++) {
|
|
buf[pos++] = ' ';
|
|
}
|
|
|
|
pos += snprintf(buf + pos, bufSize - pos, "Enabled = False\n");
|
|
}
|
|
|
|
curLevel = mi->level + 1;
|
|
}
|
|
|
|
// Close any remaining open menu blocks
|
|
while (curLevel > 0) {
|
|
curLevel--;
|
|
|
|
for (int32_t p = 0; p < (curLevel + 1) * 4; p++) {
|
|
buf[pos++] = ' ';
|
|
}
|
|
|
|
pos += snprintf(buf + pos, bufSize - pos, "End\n");
|
|
}
|
|
}
|
|
|
|
// Output top-level controls (and recurse into containers)
|
|
pos = saveControls(ds->form, buf, bufSize, pos, "", 1);
|
|
|
|
pos += snprintf(buf + pos, bufSize - pos, "End\n");
|
|
|
|
// Append code section if present
|
|
if (ds->form->code && ds->form->code[0]) {
|
|
int32_t codeLen = (int32_t)strlen(ds->form->code);
|
|
int32_t avail = bufSize - pos - 2; // room for \n prefix and \n suffix
|
|
|
|
if (avail > 0) {
|
|
buf[pos++] = '\n';
|
|
|
|
if (codeLen > avail) {
|
|
codeLen = avail;
|
|
}
|
|
|
|
memcpy(buf + pos, ds->form->code, codeLen);
|
|
pos += codeLen;
|
|
|
|
// Ensure trailing newline
|
|
if (pos > 0 && buf[pos - 1] != '\n' && pos < bufSize - 1) {
|
|
buf[pos++] = '\n';
|
|
}
|
|
|
|
buf[pos] = '\0';
|
|
}
|
|
}
|
|
|
|
return pos;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dsgnSelectedName
|
|
// ============================================================
|
|
|
|
const char *dsgnSelectedName(const DsgnStateT *ds) {
|
|
if (!ds->form) {
|
|
return "";
|
|
}
|
|
|
|
if (ds->selectedIdx >= 0 && ds->selectedIdx < (int32_t)arrlen(ds->form->controls)) {
|
|
return ds->form->controls[ds->selectedIdx]->name;
|
|
}
|
|
|
|
return ds->form->name;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
// getPropValue
|
|
// ============================================================
|
|
|
|
static const char *getPropValue(const DsgnControlT *ctrl, const char *name) {
|
|
for (int32_t i = 0; i < ctrl->propCount; i++) {
|
|
if (strcasecmp(ctrl->props[i].name, name) == 0) {
|
|
return ctrl->props[i].value;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// hitTestControl
|
|
// ============================================================
|
|
|
|
static int32_t hitTestControl(const DsgnStateT *ds, int32_t x, int32_t y) {
|
|
int32_t count = (int32_t)arrlen(ds->form->controls);
|
|
|
|
for (int32_t i = count - 1; i >= 0; i--) {
|
|
const DsgnControlT *ctrl = ds->form->controls[i];
|
|
|
|
if (!ctrl->widget || !ctrl->widget->visible) {
|
|
continue;
|
|
}
|
|
|
|
// Use the widget's actual laid-out position
|
|
int32_t wx = ctrl->widget->x;
|
|
int32_t wy = ctrl->widget->y;
|
|
int32_t ww = ctrl->widget->w;
|
|
int32_t wh = ctrl->widget->h;
|
|
|
|
if (x >= wx && x < wx + ww && y >= wy && y < wy + wh) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// hitTestHandles
|
|
// ============================================================
|
|
|
|
static DsgnHandleE hitTestHandles(const DsgnControlT *ctrl, int32_t x, int32_t y) {
|
|
if (!ctrl->widget) {
|
|
return HANDLE_NONE;
|
|
}
|
|
|
|
int32_t cx = ctrl->widget->x;
|
|
int32_t cy = ctrl->widget->y;
|
|
int32_t cw = ctrl->widget->w;
|
|
int32_t ch = ctrl->widget->h;
|
|
int32_t hs = DSGN_HANDLE_SIZE;
|
|
|
|
// 3 handles: E (right edge), S (bottom edge), SE (corner)
|
|
int32_t hx[HANDLE_COUNT] = { cx + cw - hs/2, cx + cw/2 - hs/2, cx + cw - hs/2 };
|
|
int32_t hy[HANDLE_COUNT] = { cy + ch/2 - hs/2, cy + ch - hs/2, cy + ch - hs/2 };
|
|
|
|
for (int32_t i = 0; i < HANDLE_COUNT; i++) {
|
|
if (x >= hx[i] && x < hx[i] + hs && y >= hy[i] && y < hy[i] + hs) {
|
|
return (DsgnHandleE)i;
|
|
}
|
|
}
|
|
|
|
return HANDLE_NONE;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// resolveTypeName
|
|
// ============================================================
|
|
|
|
static const char *resolveTypeName(const char *typeName) {
|
|
const char *wgtName = wgtFindByBasName(typeName);
|
|
|
|
if (wgtName) {
|
|
return wgtName;
|
|
}
|
|
|
|
if (wgtGetApi(typeName)) {
|
|
return typeName;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// setPropValue
|
|
// ============================================================
|
|
|
|
static void setPropValue(DsgnControlT *ctrl, const char *name, const char *value) {
|
|
for (int32_t i = 0; i < ctrl->propCount; i++) {
|
|
if (strcasecmp(ctrl->props[i].name, name) == 0) {
|
|
snprintf(ctrl->props[i].value, DSGN_MAX_TEXT, "%s", value);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (ctrl->propCount < DSGN_MAX_PROPS) {
|
|
snprintf(ctrl->props[ctrl->propCount].name, DSGN_MAX_NAME, "%s", name);
|
|
snprintf(ctrl->props[ctrl->propCount].value, DSGN_MAX_TEXT, "%s", value);
|
|
ctrl->propCount++;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
// rebuildWidgets
|
|
// ============================================================
|
|
//
|
|
// Destroy all live widgets and recreate them in the current
|
|
// array order. This is the safest way to reorder since the
|
|
// widget tree child list matches the creation order.
|
|
|
|
static void rebuildWidgets(DsgnStateT *ds) {
|
|
WidgetT *parent = ds->form ? ds->form->contentBox : NULL;
|
|
|
|
if (!parent) {
|
|
return;
|
|
}
|
|
|
|
// Destroy all existing widget children
|
|
// (wgtDestroy not available, so just unlink and let the window own them)
|
|
parent->firstChild = NULL;
|
|
parent->lastChild = NULL;
|
|
|
|
// Clear widget pointers
|
|
int32_t count = (int32_t)arrlen(ds->form->controls);
|
|
|
|
for (int32_t i = 0; i < count; i++) {
|
|
ds->form->controls[i]->widget = NULL;
|
|
}
|
|
|
|
// Recreate all widgets in current array order
|
|
dsgnCreateWidgets(ds, parent);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// syncWidgetGeom
|
|
// ============================================================
|
|
//
|
|
// Update the live widget's position and size from the design data.
|
|
|
|
static void syncWidgetGeom(DsgnControlT *ctrl) {
|
|
if (!ctrl->widget) {
|
|
return;
|
|
}
|
|
|
|
ctrl->widget->minW = wgtPixels(ctrl->width);
|
|
ctrl->widget->minH = wgtPixels(ctrl->height);
|
|
ctrl->widget->maxW = ctrl->maxWidth > 0 ? wgtPixels(ctrl->maxWidth) : 0;
|
|
ctrl->widget->maxH = ctrl->maxHeight > 0 ? wgtPixels(ctrl->maxHeight) : 0;
|
|
}
|