Add remote forms system: DFM converter, server library, and client engine
Text-based protocol for serving Delphi-designed forms over serial. dfm2form converts binary DFM (TPF0) to protocol commands on Linux. formsrv loads .form files and sends/receives via pluggable transport. formcli creates native Win 3.1 controls and routes events back to server. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9a735c6adf
commit
ae2aef0119
6 changed files with 2888 additions and 0 deletions
976
forms/dfm2form.c
Normal file
976
forms/dfm2form.c
Normal file
|
|
@ -0,0 +1,976 @@
|
||||||
|
// dfm2form.c - Convert Delphi 1.0 binary DFM (TPF0) to .form protocol text
|
||||||
|
//
|
||||||
|
// Usage: dfm2form [-i <formId>] <input.dfm> [output.form]
|
||||||
|
//
|
||||||
|
// Reads a binary DFM file, extracts the form and control definitions,
|
||||||
|
// and outputs protocol commands (FORM.CREATE, CTRL.CREATE, EVENT.BIND,
|
||||||
|
// FORM.SHOW) suitable for the remote forms system.
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Types
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
ctUnknown = 0,
|
||||||
|
ctLabel,
|
||||||
|
ctEdit,
|
||||||
|
ctButton,
|
||||||
|
ctCheckBox,
|
||||||
|
ctListBox,
|
||||||
|
ctComboBox,
|
||||||
|
ctMemo
|
||||||
|
} CtrlTypeE;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char name[64];
|
||||||
|
CtrlTypeE type;
|
||||||
|
int32_t left;
|
||||||
|
int32_t top;
|
||||||
|
int32_t width;
|
||||||
|
int32_t height;
|
||||||
|
char caption[256];
|
||||||
|
char text[4096];
|
||||||
|
char items[4096];
|
||||||
|
int32_t checked;
|
||||||
|
int32_t enabled;
|
||||||
|
int32_t visible;
|
||||||
|
int32_t maxLength;
|
||||||
|
int32_t readOnly;
|
||||||
|
int32_t scrollBars;
|
||||||
|
int32_t tabOrder;
|
||||||
|
int32_t itemIndex;
|
||||||
|
bool hasCaption;
|
||||||
|
bool hasText;
|
||||||
|
bool hasItems;
|
||||||
|
bool hasChecked;
|
||||||
|
bool hasEnabled;
|
||||||
|
bool hasVisible;
|
||||||
|
bool hasMaxLength;
|
||||||
|
bool hasReadOnly;
|
||||||
|
bool hasScrollBars;
|
||||||
|
bool hasTabOrder;
|
||||||
|
bool hasItemIndex;
|
||||||
|
bool hasOnClick;
|
||||||
|
bool hasOnChange;
|
||||||
|
bool hasOnDblClick;
|
||||||
|
bool hasOnEnter;
|
||||||
|
bool hasOnExit;
|
||||||
|
bool hasOnKeyDown;
|
||||||
|
bool hasOnKeyUp;
|
||||||
|
bool hasOnMouseDown;
|
||||||
|
bool hasOnMouseUp;
|
||||||
|
} DfmCtrlT;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char name[64];
|
||||||
|
int32_t width;
|
||||||
|
int32_t height;
|
||||||
|
char caption[256];
|
||||||
|
DfmCtrlT ctrls[256];
|
||||||
|
int32_t ctrlCount;
|
||||||
|
} DfmFormT;
|
||||||
|
|
||||||
|
// DFM value type tags
|
||||||
|
enum {
|
||||||
|
vaNull = 0x00,
|
||||||
|
vaList = 0x01,
|
||||||
|
vaInt8 = 0x02,
|
||||||
|
vaInt16 = 0x03,
|
||||||
|
vaInt32 = 0x04,
|
||||||
|
vaExtended = 0x05,
|
||||||
|
vaString = 0x06,
|
||||||
|
vaIdent = 0x07,
|
||||||
|
vaFalse = 0x08,
|
||||||
|
vaTrue = 0x09,
|
||||||
|
vaBinary = 0x0A,
|
||||||
|
vaSet = 0x0B,
|
||||||
|
vaLString = 0x0C,
|
||||||
|
vaNil = 0x0D,
|
||||||
|
vaCollection = 0x0E
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Prototypes
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static void emitCtrl(FILE *out, int32_t formId, int32_t ctrlId, DfmCtrlT *ctrl);
|
||||||
|
static void emitForm(FILE *out, int32_t formId, DfmFormT *form);
|
||||||
|
static void escapeStr(const char *src, char *dst, int32_t dstSize);
|
||||||
|
static void initCtrl(DfmCtrlT *ctrl);
|
||||||
|
static void initForm(DfmFormT *form);
|
||||||
|
static CtrlTypeE mapClassName(const char *className);
|
||||||
|
static bool parseComponent(const uint8_t *data, int32_t size, int32_t *pos, DfmFormT *form, bool isRoot);
|
||||||
|
static void parseProperties(const uint8_t *data, int32_t size, int32_t *pos, DfmFormT *form, DfmCtrlT *ctrl, bool isForm);
|
||||||
|
static int32_t readByte(const uint8_t *data, int32_t size, int32_t *pos);
|
||||||
|
static int32_t readInt16LE(const uint8_t *data, int32_t size, int32_t *pos);
|
||||||
|
static int32_t readInt32LE(const uint8_t *data, int32_t size, int32_t *pos);
|
||||||
|
static int32_t readIntValue(const uint8_t *data, int32_t size, int32_t *pos, uint8_t tag);
|
||||||
|
static bool readStr(const uint8_t *data, int32_t size, int32_t *pos, char *buf, int32_t bufSize);
|
||||||
|
static void skipValue(const uint8_t *data, int32_t size, int32_t *pos, uint8_t tag);
|
||||||
|
static int stricmp_local(const char *a, const char *b);
|
||||||
|
static void usage(const char *progName);
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Low-level DFM readers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static int32_t readByte(const uint8_t *data, int32_t size, int32_t *pos)
|
||||||
|
{
|
||||||
|
if (*pos >= size) {
|
||||||
|
fprintf(stderr, "Error: unexpected end of file at offset %d\n", *pos);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
return data[(*pos)++];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t readInt16LE(const uint8_t *data, int32_t size, int32_t *pos)
|
||||||
|
{
|
||||||
|
if (*pos + 2 > size) {
|
||||||
|
fprintf(stderr, "Error: unexpected end of file at offset %d\n", *pos);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
int16_t val = (int16_t)(data[*pos] | (data[*pos + 1] << 8));
|
||||||
|
*pos += 2;
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t readInt32LE(const uint8_t *data, int32_t size, int32_t *pos)
|
||||||
|
{
|
||||||
|
if (*pos + 4 > size) {
|
||||||
|
fprintf(stderr, "Error: unexpected end of file at offset %d\n", *pos);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
int32_t val = (int32_t)(data[*pos] | (data[*pos + 1] << 8) |
|
||||||
|
(data[*pos + 2] << 16) | (data[*pos + 3] << 24));
|
||||||
|
*pos += 4;
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool readStr(const uint8_t *data, int32_t size, int32_t *pos, char *buf, int32_t bufSize)
|
||||||
|
{
|
||||||
|
int32_t len = readByte(data, size, pos);
|
||||||
|
if (*pos + len > size) {
|
||||||
|
fprintf(stderr, "Error: string overflows file at offset %d\n", *pos);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
int32_t copyLen = (len < bufSize - 1) ? len : bufSize - 1;
|
||||||
|
memcpy(buf, data + *pos, copyLen);
|
||||||
|
buf[copyLen] = '\0';
|
||||||
|
*pos += len;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t readIntValue(const uint8_t *data, int32_t size, int32_t *pos, uint8_t tag)
|
||||||
|
{
|
||||||
|
switch (tag) {
|
||||||
|
case vaInt8:
|
||||||
|
return (int8_t)readByte(data, size, pos);
|
||||||
|
case vaInt16:
|
||||||
|
return readInt16LE(data, size, pos);
|
||||||
|
case vaInt32:
|
||||||
|
return readInt32LE(data, size, pos);
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Skip unknown property values
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static void skipValue(const uint8_t *data, int32_t size, int32_t *pos, uint8_t tag)
|
||||||
|
{
|
||||||
|
int32_t len;
|
||||||
|
char buf[256];
|
||||||
|
|
||||||
|
switch (tag) {
|
||||||
|
case vaNull:
|
||||||
|
break;
|
||||||
|
case vaList:
|
||||||
|
// skip items until vaNull
|
||||||
|
while (*pos < size) {
|
||||||
|
uint8_t itemTag = readByte(data, size, pos);
|
||||||
|
if (itemTag == vaNull) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
skipValue(data, size, pos, itemTag);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case vaInt8:
|
||||||
|
*pos += 1;
|
||||||
|
break;
|
||||||
|
case vaInt16:
|
||||||
|
*pos += 2;
|
||||||
|
break;
|
||||||
|
case vaInt32:
|
||||||
|
*pos += 4;
|
||||||
|
break;
|
||||||
|
case vaExtended:
|
||||||
|
*pos += 10;
|
||||||
|
break;
|
||||||
|
case vaString:
|
||||||
|
len = readByte(data, size, pos);
|
||||||
|
*pos += len;
|
||||||
|
break;
|
||||||
|
case vaIdent:
|
||||||
|
len = readByte(data, size, pos);
|
||||||
|
*pos += len;
|
||||||
|
break;
|
||||||
|
case vaFalse:
|
||||||
|
case vaTrue:
|
||||||
|
case vaNil:
|
||||||
|
break;
|
||||||
|
case vaBinary:
|
||||||
|
len = readInt32LE(data, size, pos);
|
||||||
|
*pos += len;
|
||||||
|
break;
|
||||||
|
case vaSet:
|
||||||
|
// repeated strings, empty string terminates
|
||||||
|
while (*pos < size) {
|
||||||
|
readStr(data, size, pos, buf, sizeof(buf));
|
||||||
|
if (buf[0] == '\0') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case vaLString:
|
||||||
|
len = readInt32LE(data, size, pos);
|
||||||
|
*pos += len;
|
||||||
|
break;
|
||||||
|
case vaCollection:
|
||||||
|
// items bracketed by vaList/vaNull
|
||||||
|
while (*pos < size) {
|
||||||
|
uint8_t itemTag = readByte(data, size, pos);
|
||||||
|
if (itemTag == vaNull) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
skipValue(data, size, pos, itemTag);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
fprintf(stderr, "Warning: unknown value tag 0x%02X at offset %d\n", tag, *pos);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Case-insensitive string compare
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static int stricmp_local(const char *a, const char *b)
|
||||||
|
{
|
||||||
|
while (*a && *b) {
|
||||||
|
int ca = tolower((unsigned char)*a);
|
||||||
|
int cb = tolower((unsigned char)*b);
|
||||||
|
if (ca != cb) {
|
||||||
|
return ca - cb;
|
||||||
|
}
|
||||||
|
a++;
|
||||||
|
b++;
|
||||||
|
}
|
||||||
|
return (unsigned char)*a - (unsigned char)*b;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Map Delphi class name to control type
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static CtrlTypeE mapClassName(const char *className)
|
||||||
|
{
|
||||||
|
if (stricmp_local(className, "TLabel") == 0) {
|
||||||
|
return ctLabel;
|
||||||
|
}
|
||||||
|
if (stricmp_local(className, "TEdit") == 0) {
|
||||||
|
return ctEdit;
|
||||||
|
}
|
||||||
|
if (stricmp_local(className, "TButton") == 0) {
|
||||||
|
return ctButton;
|
||||||
|
}
|
||||||
|
if (stricmp_local(className, "TCheckBox") == 0) {
|
||||||
|
return ctCheckBox;
|
||||||
|
}
|
||||||
|
if (stricmp_local(className, "TListBox") == 0) {
|
||||||
|
return ctListBox;
|
||||||
|
}
|
||||||
|
if (stricmp_local(className, "TComboBox") == 0) {
|
||||||
|
return ctComboBox;
|
||||||
|
}
|
||||||
|
if (stricmp_local(className, "TMemo") == 0) {
|
||||||
|
return ctMemo;
|
||||||
|
}
|
||||||
|
return ctUnknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Init helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static void initCtrl(DfmCtrlT *ctrl)
|
||||||
|
{
|
||||||
|
memset(ctrl, 0, sizeof(DfmCtrlT));
|
||||||
|
ctrl->enabled = 1;
|
||||||
|
ctrl->visible = 1;
|
||||||
|
ctrl->itemIndex = -1;
|
||||||
|
ctrl->tabOrder = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void initForm(DfmFormT *form)
|
||||||
|
{
|
||||||
|
memset(form, 0, sizeof(DfmFormT));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Parse properties from DFM binary
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static void parseProperties(const uint8_t *data, int32_t size, int32_t *pos,
|
||||||
|
DfmFormT *form, DfmCtrlT *ctrl, bool isForm)
|
||||||
|
{
|
||||||
|
char propName[64];
|
||||||
|
char strBuf[4096];
|
||||||
|
|
||||||
|
while (*pos < size) {
|
||||||
|
// Property name (empty = end of properties)
|
||||||
|
int32_t nameLen = readByte(data, size, pos);
|
||||||
|
if (nameLen == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the property name
|
||||||
|
(*pos)--; // back up, readStr reads the length byte
|
||||||
|
readStr(data, size, pos, propName, sizeof(propName));
|
||||||
|
|
||||||
|
// Read value tag
|
||||||
|
uint8_t tag = readByte(data, size, pos);
|
||||||
|
|
||||||
|
// Match known properties
|
||||||
|
if (isForm && stricmp_local(propName, "Caption") == 0) {
|
||||||
|
if (tag == vaString) {
|
||||||
|
readStr(data, size, pos, form->caption, sizeof(form->caption));
|
||||||
|
} else if (tag == vaLString) {
|
||||||
|
int32_t len = readInt32LE(data, size, pos);
|
||||||
|
int32_t copyLen = (len < (int32_t)sizeof(form->caption) - 1) ? len : (int32_t)sizeof(form->caption) - 1;
|
||||||
|
memcpy(form->caption, data + *pos, copyLen);
|
||||||
|
form->caption[copyLen] = '\0';
|
||||||
|
*pos += len;
|
||||||
|
} else {
|
||||||
|
skipValue(data, size, pos, tag);
|
||||||
|
}
|
||||||
|
} else if (isForm && stricmp_local(propName, "ClientWidth") == 0) {
|
||||||
|
form->width = readIntValue(data, size, pos, tag);
|
||||||
|
} else if (isForm && stricmp_local(propName, "ClientHeight") == 0) {
|
||||||
|
form->height = readIntValue(data, size, pos, tag);
|
||||||
|
} else if (isForm && stricmp_local(propName, "Width") == 0 && form->width == 0) {
|
||||||
|
form->width = readIntValue(data, size, pos, tag);
|
||||||
|
} else if (isForm && stricmp_local(propName, "Height") == 0 && form->height == 0) {
|
||||||
|
form->height = readIntValue(data, size, pos, tag);
|
||||||
|
} else if (!isForm && stricmp_local(propName, "Left") == 0) {
|
||||||
|
ctrl->left = readIntValue(data, size, pos, tag);
|
||||||
|
} else if (!isForm && stricmp_local(propName, "Top") == 0) {
|
||||||
|
ctrl->top = readIntValue(data, size, pos, tag);
|
||||||
|
} else if (!isForm && stricmp_local(propName, "Width") == 0) {
|
||||||
|
ctrl->width = readIntValue(data, size, pos, tag);
|
||||||
|
} else if (!isForm && stricmp_local(propName, "Height") == 0) {
|
||||||
|
ctrl->height = readIntValue(data, size, pos, tag);
|
||||||
|
} else if (!isForm && stricmp_local(propName, "Caption") == 0) {
|
||||||
|
if (tag == vaString) {
|
||||||
|
readStr(data, size, pos, ctrl->caption, sizeof(ctrl->caption));
|
||||||
|
} else if (tag == vaLString) {
|
||||||
|
int32_t len = readInt32LE(data, size, pos);
|
||||||
|
int32_t copyLen = (len < (int32_t)sizeof(ctrl->caption) - 1) ? len : (int32_t)sizeof(ctrl->caption) - 1;
|
||||||
|
memcpy(ctrl->caption, data + *pos, copyLen);
|
||||||
|
ctrl->caption[copyLen] = '\0';
|
||||||
|
*pos += len;
|
||||||
|
} else {
|
||||||
|
skipValue(data, size, pos, tag);
|
||||||
|
}
|
||||||
|
ctrl->hasCaption = true;
|
||||||
|
} else if (!isForm && stricmp_local(propName, "Text") == 0) {
|
||||||
|
if (tag == vaString) {
|
||||||
|
readStr(data, size, pos, ctrl->text, sizeof(ctrl->text));
|
||||||
|
} else if (tag == vaLString) {
|
||||||
|
int32_t len = readInt32LE(data, size, pos);
|
||||||
|
int32_t copyLen = (len < (int32_t)sizeof(ctrl->text) - 1) ? len : (int32_t)sizeof(ctrl->text) - 1;
|
||||||
|
memcpy(ctrl->text, data + *pos, copyLen);
|
||||||
|
ctrl->text[copyLen] = '\0';
|
||||||
|
*pos += len;
|
||||||
|
} else {
|
||||||
|
skipValue(data, size, pos, tag);
|
||||||
|
}
|
||||||
|
ctrl->hasText = true;
|
||||||
|
} else if (!isForm && stricmp_local(propName, "Items.Strings") == 0 && tag == vaList) {
|
||||||
|
// List of strings for ListBox/ComboBox
|
||||||
|
ctrl->items[0] = '\0';
|
||||||
|
int32_t itemsLen = 0;
|
||||||
|
while (*pos < size) {
|
||||||
|
uint8_t itemTag = readByte(data, size, pos);
|
||||||
|
if (itemTag == vaNull) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (itemTag == vaString) {
|
||||||
|
readStr(data, size, pos, strBuf, sizeof(strBuf));
|
||||||
|
} else if (itemTag == vaLString) {
|
||||||
|
int32_t len = readInt32LE(data, size, pos);
|
||||||
|
int32_t copyLen = (len < (int32_t)sizeof(strBuf) - 1) ? len : (int32_t)sizeof(strBuf) - 1;
|
||||||
|
memcpy(strBuf, data + *pos, copyLen);
|
||||||
|
strBuf[copyLen] = '\0';
|
||||||
|
*pos += len;
|
||||||
|
} else {
|
||||||
|
skipValue(data, size, pos, itemTag);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int32_t slen = (int32_t)strlen(strBuf);
|
||||||
|
if (itemsLen + slen + 2 < (int32_t)sizeof(ctrl->items)) {
|
||||||
|
if (itemsLen > 0) {
|
||||||
|
ctrl->items[itemsLen++] = '\n';
|
||||||
|
}
|
||||||
|
memcpy(ctrl->items + itemsLen, strBuf, slen);
|
||||||
|
itemsLen += slen;
|
||||||
|
ctrl->items[itemsLen] = '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctrl->hasItems = true;
|
||||||
|
} else if (!isForm && stricmp_local(propName, "Checked") == 0) {
|
||||||
|
if (tag == vaTrue) {
|
||||||
|
ctrl->checked = 1;
|
||||||
|
} else if (tag == vaFalse) {
|
||||||
|
ctrl->checked = 0;
|
||||||
|
} else {
|
||||||
|
ctrl->checked = readIntValue(data, size, pos, tag);
|
||||||
|
}
|
||||||
|
ctrl->hasChecked = true;
|
||||||
|
} else if (!isForm && stricmp_local(propName, "State") == 0) {
|
||||||
|
// TCheckBox.State: cbUnchecked=0, cbChecked=1, cbGrayed=2
|
||||||
|
if (tag == vaIdent) {
|
||||||
|
readStr(data, size, pos, strBuf, sizeof(strBuf));
|
||||||
|
if (stricmp_local(strBuf, "cbChecked") == 0) {
|
||||||
|
ctrl->checked = 1;
|
||||||
|
} else {
|
||||||
|
ctrl->checked = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctrl->checked = readIntValue(data, size, pos, tag);
|
||||||
|
}
|
||||||
|
ctrl->hasChecked = true;
|
||||||
|
} else if (!isForm && stricmp_local(propName, "Enabled") == 0) {
|
||||||
|
if (tag == vaTrue) {
|
||||||
|
ctrl->enabled = 1;
|
||||||
|
} else if (tag == vaFalse) {
|
||||||
|
ctrl->enabled = 0;
|
||||||
|
} else {
|
||||||
|
ctrl->enabled = readIntValue(data, size, pos, tag);
|
||||||
|
}
|
||||||
|
ctrl->hasEnabled = true;
|
||||||
|
} else if (!isForm && stricmp_local(propName, "Visible") == 0) {
|
||||||
|
if (tag == vaTrue) {
|
||||||
|
ctrl->visible = 1;
|
||||||
|
} else if (tag == vaFalse) {
|
||||||
|
ctrl->visible = 0;
|
||||||
|
} else {
|
||||||
|
ctrl->visible = readIntValue(data, size, pos, tag);
|
||||||
|
}
|
||||||
|
ctrl->hasVisible = true;
|
||||||
|
} else if (!isForm && stricmp_local(propName, "MaxLength") == 0) {
|
||||||
|
ctrl->maxLength = readIntValue(data, size, pos, tag);
|
||||||
|
ctrl->hasMaxLength = true;
|
||||||
|
} else if (!isForm && stricmp_local(propName, "ReadOnly") == 0) {
|
||||||
|
if (tag == vaTrue) {
|
||||||
|
ctrl->readOnly = 1;
|
||||||
|
} else if (tag == vaFalse) {
|
||||||
|
ctrl->readOnly = 0;
|
||||||
|
} else {
|
||||||
|
ctrl->readOnly = readIntValue(data, size, pos, tag);
|
||||||
|
}
|
||||||
|
ctrl->hasReadOnly = true;
|
||||||
|
} else if (!isForm && stricmp_local(propName, "ScrollBars") == 0) {
|
||||||
|
if (tag == vaIdent) {
|
||||||
|
readStr(data, size, pos, strBuf, sizeof(strBuf));
|
||||||
|
if (stricmp_local(strBuf, "ssNone") == 0) {
|
||||||
|
ctrl->scrollBars = 0;
|
||||||
|
} else if (stricmp_local(strBuf, "ssHorizontal") == 0) {
|
||||||
|
ctrl->scrollBars = 1;
|
||||||
|
} else if (stricmp_local(strBuf, "ssVertical") == 0) {
|
||||||
|
ctrl->scrollBars = 2;
|
||||||
|
} else if (stricmp_local(strBuf, "ssBoth") == 0) {
|
||||||
|
ctrl->scrollBars = 3;
|
||||||
|
} else {
|
||||||
|
ctrl->scrollBars = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctrl->scrollBars = readIntValue(data, size, pos, tag);
|
||||||
|
}
|
||||||
|
ctrl->hasScrollBars = true;
|
||||||
|
} else if (!isForm && stricmp_local(propName, "TabOrder") == 0) {
|
||||||
|
ctrl->tabOrder = readIntValue(data, size, pos, tag);
|
||||||
|
ctrl->hasTabOrder = true;
|
||||||
|
} else if (!isForm && stricmp_local(propName, "ItemIndex") == 0) {
|
||||||
|
ctrl->itemIndex = readIntValue(data, size, pos, tag);
|
||||||
|
ctrl->hasItemIndex = true;
|
||||||
|
} else if (stricmp_local(propName, "OnClick") == 0) {
|
||||||
|
if (tag == vaIdent) {
|
||||||
|
readStr(data, size, pos, strBuf, sizeof(strBuf));
|
||||||
|
} else {
|
||||||
|
skipValue(data, size, pos, tag);
|
||||||
|
}
|
||||||
|
if (!isForm) {
|
||||||
|
ctrl->hasOnClick = true;
|
||||||
|
}
|
||||||
|
} else if (stricmp_local(propName, "OnChange") == 0) {
|
||||||
|
if (tag == vaIdent) {
|
||||||
|
readStr(data, size, pos, strBuf, sizeof(strBuf));
|
||||||
|
} else {
|
||||||
|
skipValue(data, size, pos, tag);
|
||||||
|
}
|
||||||
|
if (!isForm) {
|
||||||
|
ctrl->hasOnChange = true;
|
||||||
|
}
|
||||||
|
} else if (stricmp_local(propName, "OnDblClick") == 0) {
|
||||||
|
if (tag == vaIdent) {
|
||||||
|
readStr(data, size, pos, strBuf, sizeof(strBuf));
|
||||||
|
} else {
|
||||||
|
skipValue(data, size, pos, tag);
|
||||||
|
}
|
||||||
|
if (!isForm) {
|
||||||
|
ctrl->hasOnDblClick = true;
|
||||||
|
}
|
||||||
|
} else if (stricmp_local(propName, "OnEnter") == 0) {
|
||||||
|
if (tag == vaIdent) {
|
||||||
|
readStr(data, size, pos, strBuf, sizeof(strBuf));
|
||||||
|
} else {
|
||||||
|
skipValue(data, size, pos, tag);
|
||||||
|
}
|
||||||
|
if (!isForm) {
|
||||||
|
ctrl->hasOnEnter = true;
|
||||||
|
}
|
||||||
|
} else if (stricmp_local(propName, "OnExit") == 0) {
|
||||||
|
if (tag == vaIdent) {
|
||||||
|
readStr(data, size, pos, strBuf, sizeof(strBuf));
|
||||||
|
} else {
|
||||||
|
skipValue(data, size, pos, tag);
|
||||||
|
}
|
||||||
|
if (!isForm) {
|
||||||
|
ctrl->hasOnExit = true;
|
||||||
|
}
|
||||||
|
} else if (stricmp_local(propName, "OnKeyDown") == 0) {
|
||||||
|
if (tag == vaIdent) {
|
||||||
|
readStr(data, size, pos, strBuf, sizeof(strBuf));
|
||||||
|
} else {
|
||||||
|
skipValue(data, size, pos, tag);
|
||||||
|
}
|
||||||
|
if (!isForm) {
|
||||||
|
ctrl->hasOnKeyDown = true;
|
||||||
|
}
|
||||||
|
} else if (stricmp_local(propName, "OnKeyUp") == 0) {
|
||||||
|
if (tag == vaIdent) {
|
||||||
|
readStr(data, size, pos, strBuf, sizeof(strBuf));
|
||||||
|
} else {
|
||||||
|
skipValue(data, size, pos, tag);
|
||||||
|
}
|
||||||
|
if (!isForm) {
|
||||||
|
ctrl->hasOnKeyUp = true;
|
||||||
|
}
|
||||||
|
} else if (stricmp_local(propName, "OnMouseDown") == 0) {
|
||||||
|
if (tag == vaIdent) {
|
||||||
|
readStr(data, size, pos, strBuf, sizeof(strBuf));
|
||||||
|
} else {
|
||||||
|
skipValue(data, size, pos, tag);
|
||||||
|
}
|
||||||
|
if (!isForm) {
|
||||||
|
ctrl->hasOnMouseDown = true;
|
||||||
|
}
|
||||||
|
} else if (stricmp_local(propName, "OnMouseUp") == 0) {
|
||||||
|
if (tag == vaIdent) {
|
||||||
|
readStr(data, size, pos, strBuf, sizeof(strBuf));
|
||||||
|
} else {
|
||||||
|
skipValue(data, size, pos, tag);
|
||||||
|
}
|
||||||
|
if (!isForm) {
|
||||||
|
ctrl->hasOnMouseUp = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
skipValue(data, size, pos, tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Parse a component (form or child control) recursively
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static bool parseComponent(const uint8_t *data, int32_t size, int32_t *pos,
|
||||||
|
DfmFormT *form, bool isRoot)
|
||||||
|
{
|
||||||
|
// Check for flags byte (Delphi 1.0 rarely uses this but handle it)
|
||||||
|
uint8_t peek = data[*pos];
|
||||||
|
if ((peek & 0xF0) == 0xF0) {
|
||||||
|
uint8_t flags = readByte(data, size, pos);
|
||||||
|
if (flags & 0x02) {
|
||||||
|
// ffChildPos: skip tagged integer
|
||||||
|
uint8_t childTag = readByte(data, size, pos);
|
||||||
|
readIntValue(data, size, pos, childTag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Class name
|
||||||
|
char className[64];
|
||||||
|
readStr(data, size, pos, className, sizeof(className));
|
||||||
|
|
||||||
|
// Instance name
|
||||||
|
char instName[64];
|
||||||
|
readStr(data, size, pos, instName, sizeof(instName));
|
||||||
|
|
||||||
|
if (isRoot) {
|
||||||
|
// This is the form itself
|
||||||
|
snprintf(form->name, sizeof(form->name), "%s", instName);
|
||||||
|
parseProperties(data, size, pos, form, NULL, true);
|
||||||
|
|
||||||
|
// Parse child components
|
||||||
|
while (*pos < size) {
|
||||||
|
peek = data[*pos];
|
||||||
|
if (peek == 0x00) {
|
||||||
|
(*pos)++; // consume terminator
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
parseComponent(data, size, pos, form, false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Child control
|
||||||
|
CtrlTypeE type = mapClassName(className);
|
||||||
|
if (type == ctUnknown) {
|
||||||
|
fprintf(stderr, "Warning: unknown control class '%s' (%s), skipping\n", className, instName);
|
||||||
|
// Still need to parse properties and children to advance pos
|
||||||
|
DfmCtrlT dummy;
|
||||||
|
initCtrl(&dummy);
|
||||||
|
parseProperties(data, size, pos, form, &dummy, false);
|
||||||
|
// Skip nested children
|
||||||
|
while (*pos < size) {
|
||||||
|
peek = data[*pos];
|
||||||
|
if (peek == 0x00) {
|
||||||
|
(*pos)++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
parseComponent(data, size, pos, form, false);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form->ctrlCount >= 256) {
|
||||||
|
fprintf(stderr, "Error: too many controls (max 256)\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
DfmCtrlT *ctrl = &form->ctrls[form->ctrlCount];
|
||||||
|
initCtrl(ctrl);
|
||||||
|
ctrl->type = type;
|
||||||
|
snprintf(ctrl->name, sizeof(ctrl->name), "%s", instName);
|
||||||
|
|
||||||
|
parseProperties(data, size, pos, form, ctrl, false);
|
||||||
|
form->ctrlCount++;
|
||||||
|
|
||||||
|
// Skip nested children (controls within controls, e.g., panels)
|
||||||
|
while (*pos < size) {
|
||||||
|
peek = data[*pos];
|
||||||
|
if (peek == 0x00) {
|
||||||
|
(*pos)++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
parseComponent(data, size, pos, form, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Escape a string for protocol output
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static void escapeStr(const char *src, char *dst, int32_t dstSize)
|
||||||
|
{
|
||||||
|
int32_t di = 0;
|
||||||
|
|
||||||
|
for (int32_t si = 0; src[si] != '\0' && di < dstSize - 2; si++) {
|
||||||
|
char c = src[si];
|
||||||
|
if (c == '"') {
|
||||||
|
if (di + 2 >= dstSize - 1) { break; }
|
||||||
|
dst[di++] = '\\';
|
||||||
|
dst[di++] = '"';
|
||||||
|
} else if (c == '\\') {
|
||||||
|
if (di + 2 >= dstSize - 1) { break; }
|
||||||
|
dst[di++] = '\\';
|
||||||
|
dst[di++] = '\\';
|
||||||
|
} else if (c == '\n') {
|
||||||
|
if (di + 2 >= dstSize - 1) { break; }
|
||||||
|
dst[di++] = '\\';
|
||||||
|
dst[di++] = 'n';
|
||||||
|
} else if (c == '\r') {
|
||||||
|
if (di + 2 >= dstSize - 1) { break; }
|
||||||
|
dst[di++] = '\\';
|
||||||
|
dst[di++] = 'r';
|
||||||
|
} else if (c == '\t') {
|
||||||
|
if (di + 2 >= dstSize - 1) { break; }
|
||||||
|
dst[di++] = '\\';
|
||||||
|
dst[di++] = 't';
|
||||||
|
} else {
|
||||||
|
dst[di++] = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dst[di] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Emit protocol commands for a single control
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static void emitCtrl(FILE *out, int32_t formId, int32_t ctrlId, DfmCtrlT *ctrl)
|
||||||
|
{
|
||||||
|
static const char *typeNames[] = {
|
||||||
|
"Unknown", "Label", "Edit", "Button",
|
||||||
|
"CheckBox", "ListBox", "ComboBox", "Memo"
|
||||||
|
};
|
||||||
|
char escaped[8192];
|
||||||
|
|
||||||
|
fprintf(out, "CTRL.CREATE %d %d %s %d %d %d %d",
|
||||||
|
formId, ctrlId, typeNames[ctrl->type],
|
||||||
|
ctrl->left, ctrl->top, ctrl->width, ctrl->height);
|
||||||
|
|
||||||
|
// Inline properties
|
||||||
|
if (ctrl->hasCaption) {
|
||||||
|
escapeStr(ctrl->caption, escaped, sizeof(escaped));
|
||||||
|
fprintf(out, " Caption=\"%s\"", escaped);
|
||||||
|
}
|
||||||
|
if (ctrl->hasText) {
|
||||||
|
escapeStr(ctrl->text, escaped, sizeof(escaped));
|
||||||
|
fprintf(out, " Text=\"%s\"", escaped);
|
||||||
|
}
|
||||||
|
if (ctrl->hasItems) {
|
||||||
|
escapeStr(ctrl->items, escaped, sizeof(escaped));
|
||||||
|
fprintf(out, " Items=\"%s\"", escaped);
|
||||||
|
}
|
||||||
|
if (ctrl->hasChecked) {
|
||||||
|
fprintf(out, " Checked=%d", ctrl->checked);
|
||||||
|
}
|
||||||
|
if (ctrl->hasEnabled && ctrl->enabled == 0) {
|
||||||
|
fprintf(out, " Enabled=0");
|
||||||
|
}
|
||||||
|
if (ctrl->hasVisible && ctrl->visible == 0) {
|
||||||
|
fprintf(out, " Visible=0");
|
||||||
|
}
|
||||||
|
if (ctrl->hasMaxLength && ctrl->maxLength > 0) {
|
||||||
|
fprintf(out, " MaxLength=%d", ctrl->maxLength);
|
||||||
|
}
|
||||||
|
if (ctrl->hasReadOnly && ctrl->readOnly) {
|
||||||
|
fprintf(out, " ReadOnly=1");
|
||||||
|
}
|
||||||
|
if (ctrl->hasScrollBars && ctrl->scrollBars != 0) {
|
||||||
|
fprintf(out, " ScrollBars=%d", ctrl->scrollBars);
|
||||||
|
}
|
||||||
|
if (ctrl->hasTabOrder && ctrl->tabOrder >= 0) {
|
||||||
|
fprintf(out, " TabOrder=%d", ctrl->tabOrder);
|
||||||
|
}
|
||||||
|
if (ctrl->hasItemIndex && ctrl->itemIndex >= 0) {
|
||||||
|
fprintf(out, " ItemIndex=%d", ctrl->itemIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(out, "\n");
|
||||||
|
|
||||||
|
// Emit EVENT.BIND for non-auto-wired events
|
||||||
|
// Auto-wired: Button/CheckBox→Click, Edit→Change, ListBox→Select,
|
||||||
|
// ComboBox→Select+Change, Memo→Change
|
||||||
|
|
||||||
|
bool autoClick = (ctrl->type == ctButton || ctrl->type == ctCheckBox);
|
||||||
|
bool autoChange = (ctrl->type == ctEdit || ctrl->type == ctComboBox || ctrl->type == ctMemo);
|
||||||
|
bool autoSelect = (ctrl->type == ctListBox || ctrl->type == ctComboBox);
|
||||||
|
|
||||||
|
if (ctrl->hasOnClick && !autoClick) {
|
||||||
|
fprintf(out, "EVENT.BIND %d %d Click\n", formId, ctrlId);
|
||||||
|
}
|
||||||
|
if (ctrl->hasOnChange && !autoChange) {
|
||||||
|
fprintf(out, "EVENT.BIND %d %d Change\n", formId, ctrlId);
|
||||||
|
}
|
||||||
|
if (ctrl->hasOnDblClick) {
|
||||||
|
fprintf(out, "EVENT.BIND %d %d DblClick\n", formId, ctrlId);
|
||||||
|
}
|
||||||
|
if (ctrl->hasOnEnter) {
|
||||||
|
fprintf(out, "EVENT.BIND %d %d Enter\n", formId, ctrlId);
|
||||||
|
}
|
||||||
|
if (ctrl->hasOnExit) {
|
||||||
|
fprintf(out, "EVENT.BIND %d %d Exit\n", formId, ctrlId);
|
||||||
|
}
|
||||||
|
if (ctrl->hasOnKeyDown) {
|
||||||
|
fprintf(out, "EVENT.BIND %d %d KeyDown\n", formId, ctrlId);
|
||||||
|
}
|
||||||
|
if (ctrl->hasOnKeyUp) {
|
||||||
|
fprintf(out, "EVENT.BIND %d %d KeyUp\n", formId, ctrlId);
|
||||||
|
}
|
||||||
|
if (ctrl->hasOnMouseDown) {
|
||||||
|
fprintf(out, "EVENT.BIND %d %d MouseDown\n", formId, ctrlId);
|
||||||
|
}
|
||||||
|
if (ctrl->hasOnMouseUp) {
|
||||||
|
fprintf(out, "EVENT.BIND %d %d MouseUp\n", formId, ctrlId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suppress unused variable warning for autoSelect
|
||||||
|
(void)autoSelect;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Emit the complete form
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static void emitForm(FILE *out, int32_t formId, DfmFormT *form)
|
||||||
|
{
|
||||||
|
char escaped[512];
|
||||||
|
|
||||||
|
escapeStr(form->caption, escaped, sizeof(escaped));
|
||||||
|
fprintf(out, "FORM.CREATE %d %d %d \"%s\"\n",
|
||||||
|
formId, form->width, form->height, escaped);
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < form->ctrlCount; i++) {
|
||||||
|
emitCtrl(out, formId, i + 1, &form->ctrls[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(out, "FORM.SHOW %d\n", formId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Usage
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static void usage(const char *progName)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "Usage: %s [-i <formId>] <input.dfm> [output.form]\n", progName);
|
||||||
|
fprintf(stderr, " -i <formId> Set form ID (default: 1)\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Main
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
int32_t formId = 1;
|
||||||
|
const char *inputPath = NULL;
|
||||||
|
const char *outputPath = NULL;
|
||||||
|
|
||||||
|
// Parse arguments
|
||||||
|
int32_t i = 1;
|
||||||
|
while (i < argc) {
|
||||||
|
if (strcmp(argv[i], "-i") == 0) {
|
||||||
|
if (i + 1 >= argc) {
|
||||||
|
usage(argv[0]);
|
||||||
|
}
|
||||||
|
formId = atoi(argv[++i]);
|
||||||
|
if (formId <= 0) {
|
||||||
|
fprintf(stderr, "Error: form ID must be positive\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
} else if (argv[i][0] == '-') {
|
||||||
|
usage(argv[0]);
|
||||||
|
} else if (inputPath == NULL) {
|
||||||
|
inputPath = argv[i];
|
||||||
|
} else if (outputPath == NULL) {
|
||||||
|
outputPath = argv[i];
|
||||||
|
} else {
|
||||||
|
usage(argv[0]);
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputPath == NULL) {
|
||||||
|
usage(argv[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read input file
|
||||||
|
FILE *fin = fopen(inputPath, "rb");
|
||||||
|
if (fin == NULL) {
|
||||||
|
fprintf(stderr, "Error: cannot open '%s': %s\n", inputPath, strerror(errno));
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
fseek(fin, 0, SEEK_END);
|
||||||
|
long fileSize = ftell(fin);
|
||||||
|
fseek(fin, 0, SEEK_SET);
|
||||||
|
|
||||||
|
uint8_t *data = (uint8_t *)malloc(fileSize);
|
||||||
|
if (data == NULL) {
|
||||||
|
fprintf(stderr, "Error: out of memory\n");
|
||||||
|
fclose(fin);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((long)fread(data, 1, fileSize, fin) != fileSize) {
|
||||||
|
fprintf(stderr, "Error: failed to read '%s'\n", inputPath);
|
||||||
|
free(data);
|
||||||
|
fclose(fin);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
fclose(fin);
|
||||||
|
|
||||||
|
// Verify TPF0 signature
|
||||||
|
if (fileSize < 4 || memcmp(data, "TPF0", 4) != 0) {
|
||||||
|
fprintf(stderr, "Error: '%s' is not a Delphi binary DFM (missing TPF0 signature)\n", inputPath);
|
||||||
|
free(data);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse
|
||||||
|
DfmFormT form;
|
||||||
|
initForm(&form);
|
||||||
|
int32_t pos = 4; // skip TPF0
|
||||||
|
parseComponent(data, (int32_t)fileSize, &pos, &form, true);
|
||||||
|
|
||||||
|
// Default caption if empty
|
||||||
|
if (form.caption[0] == '\0') {
|
||||||
|
snprintf(form.caption, sizeof(form.caption), "%s", form.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output
|
||||||
|
FILE *fout;
|
||||||
|
if (outputPath != NULL) {
|
||||||
|
fout = fopen(outputPath, "w");
|
||||||
|
if (fout == NULL) {
|
||||||
|
fprintf(stderr, "Error: cannot create '%s': %s\n", outputPath, strerror(errno));
|
||||||
|
free(data);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fout = stdout;
|
||||||
|
}
|
||||||
|
|
||||||
|
emitForm(fout, formId, &form);
|
||||||
|
|
||||||
|
if (fout != stdout) {
|
||||||
|
fclose(fout);
|
||||||
|
}
|
||||||
|
|
||||||
|
free(data);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
1290
forms/formcli.pas
Normal file
1290
forms/formcli.pas
Normal file
File diff suppressed because it is too large
Load diff
388
forms/formsrv.c
Normal file
388
forms/formsrv.c
Normal file
|
|
@ -0,0 +1,388 @@
|
||||||
|
// formsrv.c - Remote forms server library implementation
|
||||||
|
|
||||||
|
#define _POSIX_C_SOURCE 200809L
|
||||||
|
|
||||||
|
#include "formsrv.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Constants
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#define MAX_FORMS 64
|
||||||
|
#define MAX_LINES 1024
|
||||||
|
#define MAX_MSG_LEN 4096
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Types
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int32_t formId;
|
||||||
|
char *lines[MAX_LINES];
|
||||||
|
int32_t lineCount;
|
||||||
|
} FormDataT;
|
||||||
|
|
||||||
|
struct FormServerS {
|
||||||
|
FormTransportT *transport;
|
||||||
|
EventCallbackT eventCallback;
|
||||||
|
void *eventUserData;
|
||||||
|
FormDataT forms[MAX_FORMS];
|
||||||
|
int32_t formCount;
|
||||||
|
char msgBuf[MAX_MSG_LEN];
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Prototypes
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static FormDataT *findForm(FormServerT *server, int32_t formId);
|
||||||
|
static void freeFormData(FormDataT *fd);
|
||||||
|
static int32_t parseFormId(const char *line);
|
||||||
|
static void sendCommand(FormServerT *server, const char *fmt, ...);
|
||||||
|
static bool skipSpaces(const char **p);
|
||||||
|
static bool parseToken(const char **p, char *buf, int32_t bufSize);
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Internal helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static FormDataT *findForm(FormServerT *server, int32_t formId)
|
||||||
|
{
|
||||||
|
for (int32_t i = 0; i < server->formCount; i++) {
|
||||||
|
if (server->forms[i].formId == formId) {
|
||||||
|
return &server->forms[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void freeFormData(FormDataT *fd)
|
||||||
|
{
|
||||||
|
for (int32_t i = 0; i < fd->lineCount; i++) {
|
||||||
|
free(fd->lines[i]);
|
||||||
|
fd->lines[i] = NULL;
|
||||||
|
}
|
||||||
|
fd->lineCount = 0;
|
||||||
|
fd->formId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int32_t parseFormId(const char *line)
|
||||||
|
{
|
||||||
|
// Skip command prefix (e.g., "FORM.CREATE "), then read first integer
|
||||||
|
const char *p = line;
|
||||||
|
// Skip non-space command name
|
||||||
|
while (*p && *p != ' ') {
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
// Skip space
|
||||||
|
while (*p == ' ') {
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
// Parse integer
|
||||||
|
return (int32_t)atoi(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool skipSpaces(const char **p)
|
||||||
|
{
|
||||||
|
while (**p == ' ' || **p == '\t') {
|
||||||
|
(*p)++;
|
||||||
|
}
|
||||||
|
return **p != '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool parseToken(const char **p, char *buf, int32_t bufSize)
|
||||||
|
{
|
||||||
|
skipSpaces(p);
|
||||||
|
if (**p == '\0') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t i = 0;
|
||||||
|
|
||||||
|
if (**p == '"') {
|
||||||
|
// Quoted string — read until closing quote
|
||||||
|
(*p)++;
|
||||||
|
while (**p != '\0' && **p != '"') {
|
||||||
|
if (**p == '\\' && *(*p + 1) != '\0') {
|
||||||
|
(*p)++;
|
||||||
|
char c = **p;
|
||||||
|
switch (c) {
|
||||||
|
case 'n': c = '\n'; break;
|
||||||
|
case 'r': c = '\r'; break;
|
||||||
|
case 't': c = '\t'; break;
|
||||||
|
case '"': c = '"'; break;
|
||||||
|
case '\\': c = '\\'; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
if (i < bufSize - 1) {
|
||||||
|
buf[i++] = c;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (i < bufSize - 1) {
|
||||||
|
buf[i++] = **p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(*p)++;
|
||||||
|
}
|
||||||
|
if (**p == '"') {
|
||||||
|
(*p)++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Bare token — read until whitespace
|
||||||
|
while (**p != '\0' && **p != ' ' && **p != '\t') {
|
||||||
|
if (i < bufSize - 1) {
|
||||||
|
buf[i++] = **p;
|
||||||
|
}
|
||||||
|
(*p)++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf[i] = '\0';
|
||||||
|
return i > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#include <stdarg.h>
|
||||||
|
|
||||||
|
static void sendCommand(FormServerT *server, const char *fmt, ...)
|
||||||
|
{
|
||||||
|
char buf[MAX_MSG_LEN];
|
||||||
|
va_list args;
|
||||||
|
|
||||||
|
va_start(args, fmt);
|
||||||
|
vsnprintf(buf, sizeof(buf), fmt, args);
|
||||||
|
va_end(args);
|
||||||
|
|
||||||
|
server->transport->writeMessage(buf, server->transport->ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Public API
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
FormServerT *formServerCreate(FormTransportT *transport)
|
||||||
|
{
|
||||||
|
FormServerT *server = (FormServerT *)calloc(1, sizeof(FormServerT));
|
||||||
|
if (server == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
server->transport = transport;
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void formServerDestroy(FormServerT *server)
|
||||||
|
{
|
||||||
|
if (server == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int32_t i = 0; i < server->formCount; i++) {
|
||||||
|
freeFormData(&server->forms[i]);
|
||||||
|
}
|
||||||
|
free(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int32_t formServerLoadFile(FormServerT *server, const char *path)
|
||||||
|
{
|
||||||
|
FILE *f = fopen(path, "r");
|
||||||
|
if (f == NULL) {
|
||||||
|
fprintf(stderr, "formsrv: cannot open '%s': %s\n", path, strerror(errno));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server->formCount >= MAX_FORMS) {
|
||||||
|
fprintf(stderr, "formsrv: too many forms (max %d)\n", MAX_FORMS);
|
||||||
|
fclose(f);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
FormDataT *fd = &server->forms[server->formCount];
|
||||||
|
memset(fd, 0, sizeof(FormDataT));
|
||||||
|
|
||||||
|
char lineBuf[MAX_MSG_LEN];
|
||||||
|
int32_t formId = -1;
|
||||||
|
|
||||||
|
while (fgets(lineBuf, sizeof(lineBuf), f) != NULL) {
|
||||||
|
// Strip trailing newline
|
||||||
|
int32_t len = (int32_t)strlen(lineBuf);
|
||||||
|
while (len > 0 && (lineBuf[len - 1] == '\n' || lineBuf[len - 1] == '\r')) {
|
||||||
|
lineBuf[--len] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip empty lines
|
||||||
|
if (len == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fd->lineCount >= MAX_LINES) {
|
||||||
|
fprintf(stderr, "formsrv: too many lines in '%s' (max %d)\n", path, MAX_LINES);
|
||||||
|
freeFormData(fd);
|
||||||
|
fclose(f);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fd->lines[fd->lineCount] = strdup(lineBuf);
|
||||||
|
if (fd->lines[fd->lineCount] == NULL) {
|
||||||
|
fprintf(stderr, "formsrv: out of memory\n");
|
||||||
|
freeFormData(fd);
|
||||||
|
fclose(f);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
fd->lineCount++;
|
||||||
|
|
||||||
|
// Extract form ID from first FORM.CREATE line
|
||||||
|
if (formId == -1 && strncmp(lineBuf, "FORM.CREATE ", 12) == 0) {
|
||||||
|
formId = parseFormId(lineBuf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose(f);
|
||||||
|
|
||||||
|
if (formId <= 0) {
|
||||||
|
fprintf(stderr, "formsrv: no FORM.CREATE found in '%s'\n", path);
|
||||||
|
freeFormData(fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fd->formId = formId;
|
||||||
|
server->formCount++;
|
||||||
|
return formId;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void formServerSendForm(FormServerT *server, int32_t formId)
|
||||||
|
{
|
||||||
|
FormDataT *fd = findForm(server, formId);
|
||||||
|
if (fd == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < fd->lineCount; i++) {
|
||||||
|
server->transport->writeMessage(fd->lines[i], server->transport->ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void formServerShowForm(FormServerT *server, int32_t formId)
|
||||||
|
{
|
||||||
|
sendCommand(server, "FORM.SHOW %d", formId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void formServerHideForm(FormServerT *server, int32_t formId)
|
||||||
|
{
|
||||||
|
sendCommand(server, "FORM.HIDE %d", formId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void formServerDestroyForm(FormServerT *server, int32_t formId)
|
||||||
|
{
|
||||||
|
sendCommand(server, "FORM.DESTROY %d", formId);
|
||||||
|
|
||||||
|
// Remove from form store
|
||||||
|
for (int32_t i = 0; i < server->formCount; i++) {
|
||||||
|
if (server->forms[i].formId == formId) {
|
||||||
|
freeFormData(&server->forms[i]);
|
||||||
|
// Shift remaining forms down
|
||||||
|
for (int32_t j = i; j < server->formCount - 1; j++) {
|
||||||
|
server->forms[j] = server->forms[j + 1];
|
||||||
|
}
|
||||||
|
server->formCount--;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void formServerSetProp(FormServerT *server, int32_t formId, int32_t ctrlId,
|
||||||
|
const char *prop, const char *value)
|
||||||
|
{
|
||||||
|
sendCommand(server, "CTRL.SET %d %d %s=%s", formId, ctrlId, prop, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void formServerBindEvent(FormServerT *server, int32_t formId, int32_t ctrlId,
|
||||||
|
const char *eventName)
|
||||||
|
{
|
||||||
|
sendCommand(server, "EVENT.BIND %d %d %s", formId, ctrlId, eventName);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void formServerUnbindEvent(FormServerT *server, int32_t formId, int32_t ctrlId,
|
||||||
|
const char *eventName)
|
||||||
|
{
|
||||||
|
sendCommand(server, "EVENT.UNBIND %d %d %s", formId, ctrlId, eventName);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void formServerSetEventCallback(FormServerT *server, EventCallbackT cb,
|
||||||
|
void *userData)
|
||||||
|
{
|
||||||
|
server->eventCallback = cb;
|
||||||
|
server->eventUserData = userData;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool formServerPollEvent(FormServerT *server)
|
||||||
|
{
|
||||||
|
int bytesRead = server->transport->readMessage(
|
||||||
|
server->msgBuf, MAX_MSG_LEN - 1, server->transport->ctx);
|
||||||
|
|
||||||
|
if (bytesRead <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
server->msgBuf[bytesRead] = '\0';
|
||||||
|
|
||||||
|
// Parse: EVENT <formId> <ctrlId> <eventName> [<data>]
|
||||||
|
const char *p = server->msgBuf;
|
||||||
|
char token[256];
|
||||||
|
|
||||||
|
if (!parseToken(&p, token, sizeof(token))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (strcmp(token, "EVENT") != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// formId
|
||||||
|
if (!parseToken(&p, token, sizeof(token))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int32_t formId = (int32_t)atoi(token);
|
||||||
|
|
||||||
|
// ctrlId
|
||||||
|
if (!parseToken(&p, token, sizeof(token))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int32_t ctrlId = (int32_t)atoi(token);
|
||||||
|
|
||||||
|
// eventName
|
||||||
|
char eventName[64];
|
||||||
|
if (!parseToken(&p, eventName, sizeof(eventName))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remaining data (rest of line after skipping spaces)
|
||||||
|
skipSpaces(&p);
|
||||||
|
const char *data = p;
|
||||||
|
|
||||||
|
if (server->eventCallback != NULL) {
|
||||||
|
server->eventCallback(formId, ctrlId, eventName, data,
|
||||||
|
server->eventUserData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
83
forms/formsrv.h
Normal file
83
forms/formsrv.h
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
// formsrv.h - Remote forms server library
|
||||||
|
//
|
||||||
|
// Loads .form files (protocol command sequences) and sends them to a
|
||||||
|
// remote client via a transport interface. Receives EVENT messages
|
||||||
|
// from the client and dispatches them to a callback.
|
||||||
|
|
||||||
|
#ifndef FORMSRV_H
|
||||||
|
#define FORMSRV_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Transport interface
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
// Read a complete message into buf. Returns bytes read, 0 if no
|
||||||
|
// message is available. Must not block.
|
||||||
|
int (*readMessage)(char *buf, int32_t maxLen, void *ctx);
|
||||||
|
|
||||||
|
// Write a null-terminated message string. Transport adds framing
|
||||||
|
// (e.g., CR+LF for serial).
|
||||||
|
void (*writeMessage)(const char *buf, void *ctx);
|
||||||
|
|
||||||
|
// Opaque context pointer passed to readMessage/writeMessage.
|
||||||
|
void *ctx;
|
||||||
|
} FormTransportT;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Event callback
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
typedef void (*EventCallbackT)(int32_t formId, int32_t ctrlId,
|
||||||
|
const char *eventName, const char *data,
|
||||||
|
void *userData);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Server handle (opaque)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
typedef struct FormServerS FormServerT;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// API
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
FormServerT *formServerCreate(FormTransportT *transport);
|
||||||
|
void formServerDestroy(FormServerT *server);
|
||||||
|
|
||||||
|
// Load a .form file into the server's form store. Returns the form ID
|
||||||
|
// parsed from the first FORM.CREATE line, or -1 on error.
|
||||||
|
int32_t formServerLoadFile(FormServerT *server, const char *path);
|
||||||
|
|
||||||
|
// Send all commands for a loaded form to the client.
|
||||||
|
void formServerSendForm(FormServerT *server, int32_t formId);
|
||||||
|
|
||||||
|
// Send FORM.SHOW / FORM.HIDE / FORM.DESTROY commands.
|
||||||
|
void formServerShowForm(FormServerT *server, int32_t formId);
|
||||||
|
void formServerHideForm(FormServerT *server, int32_t formId);
|
||||||
|
void formServerDestroyForm(FormServerT *server, int32_t formId);
|
||||||
|
|
||||||
|
// Send a CTRL.SET command to update a property on a control.
|
||||||
|
void formServerSetProp(FormServerT *server, int32_t formId,
|
||||||
|
int32_t ctrlId, const char *prop,
|
||||||
|
const char *value);
|
||||||
|
|
||||||
|
// Send an EVENT.BIND command.
|
||||||
|
void formServerBindEvent(FormServerT *server, int32_t formId,
|
||||||
|
int32_t ctrlId, const char *eventName);
|
||||||
|
|
||||||
|
// Send an EVENT.UNBIND command.
|
||||||
|
void formServerUnbindEvent(FormServerT *server, int32_t formId,
|
||||||
|
int32_t ctrlId, const char *eventName);
|
||||||
|
|
||||||
|
// Set the callback for incoming events.
|
||||||
|
void formServerSetEventCallback(FormServerT *server,
|
||||||
|
EventCallbackT cb, void *userData);
|
||||||
|
|
||||||
|
// Poll for one incoming event. Returns true if an event was processed.
|
||||||
|
bool formServerPollEvent(FormServerT *server);
|
||||||
|
|
||||||
|
#endif // FORMSRV_H
|
||||||
16
forms/makefile
Normal file
16
forms/makefile
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
CC = gcc
|
||||||
|
CFLAGS = -Wall -Wextra -std=c99 -O2
|
||||||
|
LDFLAGS =
|
||||||
|
|
||||||
|
all: dfm2form formsrv.o
|
||||||
|
|
||||||
|
dfm2form: dfm2form.c
|
||||||
|
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
|
||||||
|
|
||||||
|
formsrv.o: formsrv.c formsrv.h
|
||||||
|
$(CC) $(CFLAGS) -c -o $@ formsrv.c
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f dfm2form formsrv.o
|
||||||
|
|
||||||
|
.PHONY: all clean
|
||||||
135
forms/protocol.md
Normal file
135
forms/protocol.md
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
# Remote Forms Protocol
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Text-based protocol for remote GUI. A C server on Linux sends form/control
|
||||||
|
commands over a transport layer; a Delphi 1.0 client on Windows 3.1 creates
|
||||||
|
native controls and sends user events back.
|
||||||
|
|
||||||
|
## Message Format
|
||||||
|
|
||||||
|
- One command per message.
|
||||||
|
- Transport delivers whole messages (today: newline-delimited over serial).
|
||||||
|
- Strings are double-quoted with escapes: `\"` `\\` `\n` `\r` `\t`.
|
||||||
|
- Bare tokens (IDs, numbers, type names) are whitespace-delimited.
|
||||||
|
- IDs are positive integers assigned by the server.
|
||||||
|
|
||||||
|
## Server → Client Commands
|
||||||
|
|
||||||
|
### FORM.CREATE
|
||||||
|
|
||||||
|
FORM.CREATE <formId> <width> <height> "<title>"
|
||||||
|
|
||||||
|
Create a new form with the given dimensions and title. The form is not
|
||||||
|
shown until FORM.SHOW is sent.
|
||||||
|
|
||||||
|
### FORM.SHOW
|
||||||
|
|
||||||
|
FORM.SHOW <formId>
|
||||||
|
|
||||||
|
### FORM.HIDE
|
||||||
|
|
||||||
|
FORM.HIDE <formId>
|
||||||
|
|
||||||
|
### FORM.DESTROY
|
||||||
|
|
||||||
|
FORM.DESTROY <formId>
|
||||||
|
|
||||||
|
Free the form and all its controls.
|
||||||
|
|
||||||
|
### CTRL.CREATE
|
||||||
|
|
||||||
|
CTRL.CREATE <formId> <ctrlId> <type> <left> <top> <width> <height> [Key="val" ...]
|
||||||
|
|
||||||
|
Create a control on the specified form. Inline key/value properties are
|
||||||
|
applied immediately after creation. See Control Types and Properties below.
|
||||||
|
|
||||||
|
### CTRL.SET
|
||||||
|
|
||||||
|
CTRL.SET <formId> <ctrlId> Key="val" [Key="val" ...]
|
||||||
|
|
||||||
|
Update one or more properties on an existing control.
|
||||||
|
|
||||||
|
### EVENT.BIND
|
||||||
|
|
||||||
|
EVENT.BIND <formId> <ctrlId> <eventName>
|
||||||
|
|
||||||
|
Wire an opt-in event handler. Auto-wired events do not need explicit binding.
|
||||||
|
|
||||||
|
### EVENT.UNBIND
|
||||||
|
|
||||||
|
EVENT.UNBIND <formId> <ctrlId> <eventName>
|
||||||
|
|
||||||
|
Remove an event handler.
|
||||||
|
|
||||||
|
## Client → Server Events
|
||||||
|
|
||||||
|
EVENT <formId> <ctrlId> <eventName> [<data>]
|
||||||
|
|
||||||
|
Event data varies by event type:
|
||||||
|
|
||||||
|
| Event | Data |
|
||||||
|
|-----------|-------------------------------|
|
||||||
|
| Click | (none) |
|
||||||
|
| DblClick | (none) |
|
||||||
|
| Change | `"new text"` |
|
||||||
|
| Select | `<index> "selected text"` |
|
||||||
|
| KeyDown | `<vkCode>` |
|
||||||
|
| KeyUp | `<vkCode>` |
|
||||||
|
| MouseDown | `<x> <y> <button>` |
|
||||||
|
| MouseUp | `<x> <y> <button>` |
|
||||||
|
| MouseMove | `<x> <y> <button>` |
|
||||||
|
| Enter | (none) |
|
||||||
|
| Exit | (none) |
|
||||||
|
| Close | (none) |
|
||||||
|
|
||||||
|
## Control Types
|
||||||
|
|
||||||
|
| Type | Delphi Class | Auto-wired Events |
|
||||||
|
|----------|-------------|-------------------|
|
||||||
|
| Label | TLabel | (none) |
|
||||||
|
| Edit | TEdit | Change |
|
||||||
|
| Button | TButton | Click |
|
||||||
|
| CheckBox | TCheckBox | Click |
|
||||||
|
| ListBox | TListBox | Select |
|
||||||
|
| ComboBox | TComboBox | Select, Change |
|
||||||
|
| Memo | TMemo | Change |
|
||||||
|
|
||||||
|
Opt-in events (require EVENT.BIND): DblClick, KeyDown, KeyUp, Enter, Exit,
|
||||||
|
MouseDown, MouseUp, MouseMove.
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
| Property | Applies To | Value Format |
|
||||||
|
|------------|-------------------------------|-------------------------------------------|
|
||||||
|
| Caption | Label, Button, CheckBox | Quoted string |
|
||||||
|
| Text | Edit, ComboBox, Memo | Quoted string (`\n` for line breaks) |
|
||||||
|
| Items | ListBox, ComboBox | Quoted string (`\n`-delimited) |
|
||||||
|
| Checked | CheckBox | 0 or 1 |
|
||||||
|
| Enabled | All | 0 or 1 |
|
||||||
|
| Visible | All | 0 or 1 |
|
||||||
|
| MaxLength | Edit | Integer |
|
||||||
|
| ReadOnly | Edit, Memo | 0 or 1 |
|
||||||
|
| ScrollBars | Memo | 0-3 (ssNone..ssBoth) |
|
||||||
|
| ItemIndex | ListBox, ComboBox | Integer (-1 = none) |
|
||||||
|
| TabOrder | All windowed controls | Integer |
|
||||||
|
|
||||||
|
## String Encoding
|
||||||
|
|
||||||
|
- Strings in the protocol are always double-quoted.
|
||||||
|
- Escape sequences: `\"` (literal quote), `\\` (literal backslash),
|
||||||
|
`\n` (newline), `\r` (carriage return), `\t` (tab).
|
||||||
|
- Multi-line values (Memo text, ListBox items) use `\n` within a single
|
||||||
|
quoted string.
|
||||||
|
|
||||||
|
## Transport Layer
|
||||||
|
|
||||||
|
The protocol is transport-agnostic. Messages are delivered via:
|
||||||
|
|
||||||
|
```
|
||||||
|
int ReadMessage(char *buf, int maxLen); // returns bytes read, 0 = none
|
||||||
|
void WriteMessage(const char *buf); // sends complete message
|
||||||
|
```
|
||||||
|
|
||||||
|
Current transport: newline-delimited serial (messages terminated by CR+LF).
|
||||||
|
The transport handles framing; protocol layer never sees delimiters.
|
||||||
Loading…
Add table
Reference in a new issue