diff --git a/Makefile b/Makefile index f3d724c..04ba596 100644 --- a/Makefile +++ b/Makefile @@ -3,9 +3,9 @@ # Builds the full DVX stack: core library, task switcher, # bootstrap loader, text help library, widgets, shell, and apps. -.PHONY: all clean core tasks loader texthelp listhelp widgets shell taskmgr serial dvxbasic apps tools +.PHONY: all clean core tasks loader texthelp listhelp widgets shell taskmgr serial apps tools -all: core tasks loader texthelp listhelp widgets shell taskmgr serial dvxbasic apps tools +all: core tasks loader texthelp listhelp tools widgets shell taskmgr serial apps core: $(MAKE) -C core @@ -34,9 +34,6 @@ taskmgr: shell serial: core tasks $(MAKE) -C serial -dvxbasic: core tasks shell tools - $(MAKE) -C dvxbasic - tools: $(MAKE) -C tools @@ -53,7 +50,6 @@ clean: $(MAKE) -C shell clean $(MAKE) -C taskmgr clean $(MAKE) -C serial clean - $(MAKE) -C dvxbasic clean $(MAKE) -C apps clean $(MAKE) -C tools clean -rmdir obj 2>/dev/null diff --git a/apps/Makefile b/apps/Makefile index b3d9e0e..9dd8285 100644 --- a/apps/Makefile +++ b/apps/Makefile @@ -13,9 +13,12 @@ DVXRES = ../bin/dvxres # App definitions: each is a subdir with a single .c file APPS = progman notepad clock dvxdemo cpanel imgview -.PHONY: all clean $(APPS) +.PHONY: all clean $(APPS) dvxbasic -all: $(APPS) +all: $(APPS) dvxbasic + +dvxbasic: + $(MAKE) -C dvxbasic cpanel: $(BINDIR)/cpanel/cpanel.app imgview: $(BINDIR)/imgview/imgview.app @@ -105,3 +108,4 @@ clean: rm -f $(BINDIR)/notepad/notepad.app rm -f $(BINDIR)/clock/clock.app rm -f $(BINDIR)/dvxdemo/dvxdemo.app $(addprefix $(BINDIR)/dvxdemo/,$(DVXDEMO_BMPS)) + $(MAKE) -C dvxbasic clean diff --git a/apps/clock/clock.c b/apps/clock/clock.c index 4d9ceea..d15c809 100644 --- a/apps/clock/clock.c +++ b/apps/clock/clock.c @@ -203,10 +203,8 @@ int32_t appMain(DxeAppContextT *ctx) { sWin->onClose = onClose; sWin->onPaint = onPaint; - // Initial paint -- dvxInvalidateWindow calls onPaint automatically - dvxInvalidateWindow(ac, sWin); - // Main loop: check if the second has changed, repaint if so, then yield. + // First paint happens automatically on the next dvxUpdate frame. // tsYield() transfers control back to the shell's task scheduler. // On a 486, time() resolution is 1 second, so we yield many times per // second between actual updates -- this keeps CPU usage near zero. diff --git a/dvxbasic/Makefile b/apps/dvxbasic/Makefile similarity index 94% rename from dvxbasic/Makefile rename to apps/dvxbasic/Makefile index 4cd82fa..6bb6546 100644 --- a/dvxbasic/Makefile +++ b/apps/dvxbasic/Makefile @@ -12,12 +12,12 @@ DJGPP_PREFIX = $(HOME)/djgpp/djgpp CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc DXE3GEN = PATH=$(DJGPP_PREFIX)/bin:$(PATH) DJDIR=$(DJGPP_PREFIX)/i586-pc-msdosdjgpp $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/dxe3gen -CFLAGS = -O2 -Wall -Wextra -Wno-type-limits -Wno-sign-compare -march=i486 -mtune=i586 -I../core -I../core/platform -I../widgets -I../shell -I../tasks -I../core/thirdparty -I. +CFLAGS = -O2 -Wall -Wextra -Wno-type-limits -Wno-sign-compare -march=i486 -mtune=i586 -I../../core -I../../core/platform -I../../widgets -I../../shell -I../../tasks -I../../core/thirdparty -I. -OBJDIR = ../obj/dvxbasic -LIBSDIR = ../bin/libs -APPDIR = ../bin/apps/dvxbasic -DVXRES = ../bin/dvxres +OBJDIR = ../../obj/dvxbasic +LIBSDIR = ../../bin/libs +APPDIR = ../../bin/apps/dvxbasic +DVXRES = ../../bin/dvxres SAMPLES = samples/hello.bas samples/formtest.bas samples/clickme.bas samples/clickme.frm samples/input.bas # Runtime library objects (VM + values) @@ -75,7 +75,7 @@ $(RT_TARGET): $(RT_OBJS) | $(LIBSDIR) -U $(RT_OBJS) mv $(LIBSDIR)/basrt.dxe $@ -$(LIBSDIR)/basrt.dep: ../config/basrt.dep | $(LIBSDIR) +$(LIBSDIR)/basrt.dep: ../../config/basrt.dep | $(LIBSDIR) sed 's/$$/\r/' $< > $@ # IDE app DXE (compiler linked in, runtime from basrt.lib) diff --git a/dvxbasic/compiler/codegen.c b/apps/dvxbasic/compiler/codegen.c similarity index 100% rename from dvxbasic/compiler/codegen.c rename to apps/dvxbasic/compiler/codegen.c diff --git a/dvxbasic/compiler/codegen.h b/apps/dvxbasic/compiler/codegen.h similarity index 100% rename from dvxbasic/compiler/codegen.h rename to apps/dvxbasic/compiler/codegen.h diff --git a/dvxbasic/compiler/lexer.c b/apps/dvxbasic/compiler/lexer.c similarity index 100% rename from dvxbasic/compiler/lexer.c rename to apps/dvxbasic/compiler/lexer.c diff --git a/dvxbasic/compiler/lexer.h b/apps/dvxbasic/compiler/lexer.h similarity index 100% rename from dvxbasic/compiler/lexer.h rename to apps/dvxbasic/compiler/lexer.h diff --git a/dvxbasic/compiler/opcodes.h b/apps/dvxbasic/compiler/opcodes.h similarity index 100% rename from dvxbasic/compiler/opcodes.h rename to apps/dvxbasic/compiler/opcodes.h diff --git a/dvxbasic/compiler/parser.c b/apps/dvxbasic/compiler/parser.c similarity index 100% rename from dvxbasic/compiler/parser.c rename to apps/dvxbasic/compiler/parser.c diff --git a/dvxbasic/compiler/parser.h b/apps/dvxbasic/compiler/parser.h similarity index 100% rename from dvxbasic/compiler/parser.h rename to apps/dvxbasic/compiler/parser.h diff --git a/dvxbasic/compiler/symtab.c b/apps/dvxbasic/compiler/symtab.c similarity index 100% rename from dvxbasic/compiler/symtab.c rename to apps/dvxbasic/compiler/symtab.c diff --git a/dvxbasic/compiler/symtab.h b/apps/dvxbasic/compiler/symtab.h similarity index 100% rename from dvxbasic/compiler/symtab.h rename to apps/dvxbasic/compiler/symtab.h diff --git a/dvxbasic/dvxbasic.res b/apps/dvxbasic/dvxbasic.res similarity index 100% rename from dvxbasic/dvxbasic.res rename to apps/dvxbasic/dvxbasic.res diff --git a/dvxbasic/formrt/formrt.c b/apps/dvxbasic/formrt/formrt.c similarity index 86% rename from dvxbasic/formrt/formrt.c rename to apps/dvxbasic/formrt/formrt.c index c0a3bc9..73a5dfd 100644 --- a/dvxbasic/formrt/formrt.c +++ b/apps/dvxbasic/formrt/formrt.c @@ -23,7 +23,6 @@ #define DEFAULT_FORM_W 400 #define DEFAULT_FORM_H 300 -#define DEFAULT_CREATE_ARG 256 // default maxLen/interval for widget creation #define MAX_EVENT_NAME_LEN 128 #define MAX_LISTBOX_ITEMS 256 #define MAX_FRM_LINE_LEN 512 @@ -479,15 +478,6 @@ void *basFormRtLoadForm(void *ctx, const char *formName) { return NULL; } - WidgetT *contentBox = wgtVBox(root); - - if (!contentBox) { - dvxDestroyWindow(rt->ctx, win); - return NULL; - } - - contentBox->weight = 100; - BasFormT *form = &rt->forms[rt->formCount++]; memset(form, 0, sizeof(*form)); snprintf(form->name, BAS_MAX_CTRL_NAME, "%s", formName); @@ -495,7 +485,7 @@ void *basFormRtLoadForm(void *ctx, const char *formName) { win->onResize = onFormResize; form->window = win; form->root = root; - form->contentBox = contentBox; + form->contentBox = NULL; // created lazily after Layout property is known form->ctx = rt->ctx; form->vm = rt->vm; form->module = rt->module; @@ -600,14 +590,26 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen return NULL; } - parentStack[0] = form->contentBox; - nestDepth = 1; - current = NULL; + // contentBox is created lazily (see below) after + // form-level properties like Layout are parsed. + nestDepth = 1; + current = NULL; if (blockDepth < MAX_FRM_NESTING) { isContainer[blockDepth++] = true; } } else if (form && nestDepth > 0) { + // Create the content box on first control if not yet done + if (!form->contentBox && form->root) { + if (form->frmHBox) { + form->contentBox = wgtHBox(form->root); + } else { + form->contentBox = wgtVBox(form->root); + } + + form->contentBox->weight = 100; + parentStack[0] = form->contentBox; + } WidgetT *parent = parentStack[nestDepth - 1]; const char *wgtTypeName = resolveTypeName(typeName); @@ -628,6 +630,7 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen current = &form->controls[form->controlCount++]; memset(current, 0, sizeof(*current)); snprintf(current->name, BAS_MAX_CTRL_NAME, "%s", ctrlName); + snprintf(current->typeName, BAS_MAX_CTRL_NAME, "%s", typeName); current->widget = widget; current->form = form; current->iface = wgtGetIface(wgtTypeName); @@ -641,8 +644,10 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen } // Track block type for End handling - if (strcasecmp(typeName, "Frame") == 0 && nestDepth < MAX_FRM_NESTING) { - // Frame is a container -- children go inside it + const WgtIfaceT *ctrlIface = wgtGetIface(wgtTypeName); + bool isCtrlContainer = ctrlIface && ctrlIface->isContainer; + + if (isCtrlContainer && nestDepth < MAX_FRM_NESTING) { parentStack[nestDepth++] = widget; if (blockDepth < MAX_FRM_NESTING) { @@ -700,27 +705,80 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen basFormRtSetProp(rt, current, key, val); basValRelease(&val); } else if (nestDepth > 0) { - // Form-level property - if (strcasecmp(key, "Caption") == 0) { - char *text = value; + // Form-level property -- strip quotes from string values + char *text = value; - if (text[0] == '"') { - text++; - int32_t len = (int32_t)strlen(text); + if (text[0] == '"') { + text++; + int32_t len = (int32_t)strlen(text); - if (len > 0 && text[len - 1] == '"') { - text[len - 1] = '\0'; - } + if (len > 0 && text[len - 1] == '"') { + text[len - 1] = '\0'; } + } + if (strcasecmp(key, "Caption") == 0) { dvxSetTitle(rt->ctx, form->window, text); + } else if (strcasecmp(key, "Width") == 0) { + form->frmWidth = atoi(value); + } else if (strcasecmp(key, "Height") == 0) { + form->frmHeight = atoi(value); + } else if (strcasecmp(key, "Left") == 0) { + form->frmLeft = atoi(value); + } else if (strcasecmp(key, "Top") == 0) { + form->frmTop = atoi(value); + } else if (strcasecmp(key, "Resizable") == 0) { + form->frmResizable = (strcasecmp(text, "True") == 0); + form->frmHasResizable = true; + } else if (strcasecmp(key, "Centered") == 0) { + form->frmCentered = (strcasecmp(text, "True") == 0); + } else if (strcasecmp(key, "AutoSize") == 0) { + form->frmAutoSize = (strcasecmp(text, "True") == 0); + } else if (strcasecmp(key, "Layout") == 0) { + if (strcasecmp(text, "HBox") == 0) { + form->frmHBox = true; + } } } } - // Force layout recalculation now that all controls and properties are set + // Apply accumulated form-level properties if (form) { - dvxFitWindow(rt->ctx, form->window); + // Ensure content box exists even if form has no controls + if (!form->contentBox && form->root) { + if (form->frmHBox) { + form->contentBox = wgtHBox(form->root); + } else { + form->contentBox = wgtVBox(form->root); + } + + form->contentBox->weight = 100; + } + + // Set resizable flag + if (form->frmHasResizable) { + form->window->resizable = form->frmResizable; + } + + // Size and position the window + if (form->frmAutoSize) { + dvxFitWindow(rt->ctx, form->window); + } else if (form->frmWidth > 0 && form->frmHeight > 0) { + dvxResizeWindow(rt->ctx, form->window, form->frmWidth, form->frmHeight); + } else { + dvxFitWindow(rt->ctx, form->window); + } + + // Position: centered or explicit left/top + if (form->frmCentered) { + int32_t scrW = rt->ctx->display.width; + int32_t scrH = rt->ctx->display.height; + form->window->x = (scrW - form->window->w) / 2; + form->window->y = (scrH - form->window->h) / 2; + } else if (form->frmLeft > 0 || form->frmTop > 0) { + form->window->x = form->frmLeft; + form->window->y = form->frmTop; + } } return form; @@ -789,7 +847,6 @@ void basFormRtShowForm(void *ctx, void *formRef, bool modal) { form->window->visible = true; dvxFitWindow(rt->ctx, form->window); - dvxInvalidateWindow(rt->ctx, form->window); if (modal) { rt->ctx->modalWindow = form->window; @@ -953,65 +1010,50 @@ static WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent) { return NULL; } - // All widget APIs have create as the first function pointer. - // The signature varies but the first arg is always parent. - // We handle the common patterns. + // Determine creation signature from the widget interface descriptor. + const WgtIfaceT *iface = wgtGetIface(wgtTypeName); + 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 *(*CreateParentIntBoolFnT)(WidgetT *, int32_t, bool); typedef WidgetT *(*CreateParentIntIntIntFnT)(WidgetT *, int32_t, int32_t, int32_t); + typedef WidgetT *(*CreateParentIntBoolFnT)(WidgetT *, int32_t, bool); + typedef WidgetT *(*CreateParentBoolFnT)(WidgetT *, bool); - // Determine creation pattern by widget type name - if (strcasecmp(wgtTypeName, "button") == 0 || - strcasecmp(wgtTypeName, "checkbox") == 0 || - strcasecmp(wgtTypeName, "label") == 0) { - // create(parent, text) - CreateParentTextFnT fn = *(CreateParentTextFnT *)api; - return fn(parent, ""); + 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: + return NULL; + default: { + CreateParentFnT fn = *(CreateParentFnT *)api; + return fn(parent); + } } - - if (strcasecmp(wgtTypeName, "textinput") == 0 || - strcasecmp(wgtTypeName, "combobox") == 0) { - // create(parent, maxLen) - CreateParentIntFnT fn = *(CreateParentIntFnT *)api; - return fn(parent, DEFAULT_CREATE_ARG); - } - - if (strcasecmp(wgtTypeName, "slider") == 0) { - // create(parent, minVal, maxVal) - CreateParentIntIntFnT fn = *(CreateParentIntIntFnT *)api; - return fn(parent, 0, 100); - } - - if (strcasecmp(wgtTypeName, "spinner") == 0) { - // create(parent, minVal, maxVal, step) - CreateParentIntIntIntFnT fn = *(CreateParentIntIntIntFnT *)api; - return fn(parent, 0, 100, 1); - } - - if (strcasecmp(wgtTypeName, "timer") == 0) { - // create(parent, intervalMs, repeat) - CreateParentIntBoolFnT fn = *(CreateParentIntBoolFnT *)api; - return fn(parent, 1000, true); - } - - if (strcasecmp(wgtTypeName, "box") == 0) { - // box API: first fn is vbox(parent), second is hbox, third is frame - // For BASIC "Frame", use frame (third slot) - typedef struct { - WidgetT *(*vbox)(WidgetT *); - WidgetT *(*hbox)(WidgetT *); - WidgetT *(*frame)(WidgetT *, const char *); - } BoxApiPatternT; - const BoxApiPatternT *boxApi = (const BoxApiPatternT *)api; - return boxApi->frame(parent, ""); - } - - // Default: assume create(parent) with no extra args - CreateParentFnT fn = *(CreateParentFnT *)api; - return fn(parent); } @@ -1064,14 +1106,26 @@ static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *ha return basValLong(ctrl->widget->y); } - if (strcasecmp(propName, "Width") == 0) { + if (strcasecmp(propName, "Width") == 0 || strcasecmp(propName, "MinWidth") == 0) { return basValLong(ctrl->widget->w); } - if (strcasecmp(propName, "Height") == 0) { + if (strcasecmp(propName, "Height") == 0 || strcasecmp(propName, "MinHeight") == 0) { return basValLong(ctrl->widget->h); } + if (strcasecmp(propName, "MaxWidth") == 0) { + return basValLong(ctrl->widget->maxW ? (ctrl->widget->maxW & WGT_SIZE_VAL_MASK) : 0); + } + + if (strcasecmp(propName, "MaxHeight") == 0) { + return basValLong(ctrl->widget->maxH ? (ctrl->widget->maxH & WGT_SIZE_VAL_MASK) : 0); + } + + if (strcasecmp(propName, "Weight") == 0) { + return basValLong(ctrl->widget->weight); + } + if (strcasecmp(propName, "Visible") == 0) { return basValBool(ctrl->widget->visible); } @@ -1517,16 +1571,36 @@ static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT val return true; } - if (strcasecmp(propName, "Width") == 0) { + if (strcasecmp(propName, "Width") == 0 || strcasecmp(propName, "MinWidth") == 0) { int32_t w = (int32_t)basValToNumber(value); - ctrl->widget->prefW = wgtPixels(w); + ctrl->widget->minW = wgtPixels(w); wgtInvalidate(ctrl->widget); return true; } - if (strcasecmp(propName, "Height") == 0) { + if (strcasecmp(propName, "Height") == 0 || strcasecmp(propName, "MinHeight") == 0) { int32_t h = (int32_t)basValToNumber(value); - ctrl->widget->prefH = wgtPixels(h); + ctrl->widget->minH = wgtPixels(h); + wgtInvalidate(ctrl->widget); + return true; + } + + if (strcasecmp(propName, "MaxWidth") == 0) { + int32_t mw = (int32_t)basValToNumber(value); + ctrl->widget->maxW = mw > 0 ? wgtPixels(mw) : 0; + wgtInvalidate(ctrl->widget); + return true; + } + + if (strcasecmp(propName, "MaxHeight") == 0) { + int32_t mh = (int32_t)basValToNumber(value); + ctrl->widget->maxH = mh > 0 ? wgtPixels(mh) : 0; + wgtInvalidate(ctrl->widget); + return true; + } + + if (strcasecmp(propName, "Weight") == 0) { + ctrl->widget->weight = (int32_t)basValToNumber(value); wgtInvalidate(ctrl->widget); return true; } diff --git a/dvxbasic/formrt/formrt.h b/apps/dvxbasic/formrt/formrt.h similarity index 80% rename from dvxbasic/formrt/formrt.h rename to apps/dvxbasic/formrt/formrt.h index de10c90..54cd389 100644 --- a/dvxbasic/formrt/formrt.h +++ b/apps/dvxbasic/formrt/formrt.h @@ -37,11 +37,12 @@ typedef struct BasControlT BasControlT; #define BAS_MAX_TEXT_BUF 256 typedef struct BasControlT { - char name[BAS_MAX_CTRL_NAME]; // VB control name (e.g. "Command1") - WidgetT *widget; // the DVX widget - BasFormT *form; // owning form - const WgtIfaceT *iface; // interface descriptor (from .wgt) - char textBuf[BAS_MAX_TEXT_BUF]; // persistent text for Caption/Text + char name[BAS_MAX_CTRL_NAME]; // VB control name (e.g. "Command1") + char typeName[BAS_MAX_CTRL_NAME]; // VB type name (e.g. "CommandButton") + WidgetT *widget; // the DVX widget + BasFormT *form; // owning form + const WgtIfaceT *iface; // interface descriptor (from .wgt) + char textBuf[BAS_MAX_TEXT_BUF]; // persistent text for Caption/Text } BasControlT; // ============================================================ @@ -52,12 +53,22 @@ typedef struct BasFormT { char name[BAS_MAX_CTRL_NAME]; // form name (e.g. "Form1") WindowT *window; // DVX window WidgetT *root; // widget root (from wgtInitWindow) - WidgetT *contentBox; // VBox for user controls + WidgetT *contentBox; // VBox/HBox for user controls AppContextT *ctx; // DVX app context BasControlT controls[BAS_MAX_CTRLS]; // controls on this form int32_t controlCount; BasVmT *vm; // VM for event dispatch BasModuleT *module; // compiled module (for SUB lookup) + // Form-level properties (accumulated during .frm parsing) + int32_t frmWidth; + int32_t frmHeight; + int32_t frmLeft; + int32_t frmTop; + bool frmResizable; + bool frmHasResizable; // true if Resizable was explicitly set + bool frmCentered; + bool frmAutoSize; + bool frmHBox; // true if Layout = "HBox" } BasFormT; // ============================================================ diff --git a/dvxbasic/icon32.bmp b/apps/dvxbasic/icon32.bmp similarity index 100% rename from dvxbasic/icon32.bmp rename to apps/dvxbasic/icon32.bmp diff --git a/dvxbasic/ide/ideDesigner.c b/apps/dvxbasic/ide/ideDesigner.c similarity index 63% rename from dvxbasic/ide/ideDesigner.c rename to apps/dvxbasic/ide/ideDesigner.c index 8085e37..efec9c0 100644 --- a/dvxbasic/ide/ideDesigner.c +++ b/apps/dvxbasic/ide/ideDesigner.c @@ -25,53 +25,13 @@ #define DEFAULT_CTRL_W 100 #define DEFAULT_CTRL_H 30 #define MIN_CTRL_SIZE 8 -#define DEFAULT_CREATE_ARG 256 + // ============================================================ -// Tool type name table +// Default event for the Form type (not a widget, so not in iface) // ============================================================ -static const char *sToolTypeNames[TOOL_COUNT] = { - "", // TOOL_POINTER - "CommandButton", // TOOL_BUTTON - "Label", // TOOL_LABEL - "TextBox", // TOOL_TEXTBOX - "CheckBox", // TOOL_CHECKBOX - "OptionButton", // TOOL_OPTION - "Frame", // TOOL_FRAME - "ListBox", // TOOL_LISTBOX - "ComboBox", // TOOL_COMBOBOX - "HScrollBar", // TOOL_HSCROLL - "VScrollBar", // TOOL_VSCROLL - "Timer", // TOOL_TIMER - "PictureBox", // TOOL_PICTURE - "Image" // TOOL_IMAGE -}; - -// ============================================================ -// Default event table -// ============================================================ - -typedef struct { - const char *typeName; - const char *eventName; -} DefaultEventT; - -static const DefaultEventT sDefaultEvents[] = { - { "CommandButton", "Click" }, - { "Label", "Click" }, - { "TextBox", "Change" }, - { "CheckBox", "Click" }, - { "OptionButton", "Click" }, - { "Frame", "Click" }, - { "ListBox", "Click" }, - { "ComboBox", "Click" }, - { "Timer", "Timer" }, - { "PictureBox", "Click" }, - { "Image", "Click" }, - { "Form", "Load" }, - { NULL, NULL } -}; +static const char *FORM_DEFAULT_EVENT = "Load"; // ============================================================ // Prototypes @@ -107,37 +67,50 @@ static WidgetT *createDesignWidget(const char *vbTypeName, WidgetT *parent) { 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); - if (strcasecmp(wgtName, "button") == 0 || - strcasecmp(wgtName, "checkbox") == 0 || - strcasecmp(wgtName, "label") == 0) { - CreateParentTextFnT fn = *(CreateParentTextFnT *)api; - return fn(parent, ""); + 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); + } } - - if (strcasecmp(wgtName, "textinput") == 0 || - strcasecmp(wgtName, "combobox") == 0) { - CreateParentIntFnT fn = *(CreateParentIntFnT *)api; - return fn(parent, DEFAULT_CREATE_ARG); - } - - if (strcasecmp(wgtName, "timer") == 0) { - CreateParentIntBoolFnT fn = *(CreateParentIntBoolFnT *)api; - return fn(parent, 1000, false); - } - - if (strcasecmp(wgtName, "listbox") == 0) { - CreateParentFnT fn = *(CreateParentFnT *)api; - return fn(parent); - } - - // Fallback: try simple create(parent) - CreateParentFnT fn = *(CreateParentFnT *)api; - return fn(parent); } @@ -146,13 +119,18 @@ static WidgetT *createDesignWidget(const char *vbTypeName, WidgetT *parent) { // ============================================================ void dsgnAutoName(const DsgnStateT *ds, const char *typeName, char *buf, int32_t bufSize) { - const char *prefix = typeName; + // 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 (strcasecmp(typeName, "CommandButton") == 0) { prefix = "Command"; } - else if (strcasecmp(typeName, "OptionButton") == 0) { prefix = "Option"; } - else if (strcasecmp(typeName, "HScrollBar") == 0) { prefix = "HScroll"; } - else if (strcasecmp(typeName, "VScrollBar") == 0) { prefix = "VScroll"; } - else if (strcasecmp(typeName, "PictureBox") == 0) { prefix = "Picture"; } + 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); @@ -184,14 +162,30 @@ void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox) { 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; // already created + continue; } - WidgetT *w = createDesignWidget(ctrl->typeName, contentBox); + // Find the parent widget + 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) { + parent = ds->form->controls[j].widget; + break; + } + } + } + + WidgetT *w = createDesignWidget(ctrl->typeName, parent); if (!w) { continue; @@ -207,12 +201,45 @@ void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox) { if (caption) { wgtSetText(w, caption); } if (text) { wgtSetText(w, text); } - // Set size hints for the layout engine + // 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); - w->prefH = wgtPixels(ctrl->height); + 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; + } +} + + +// ============================================================ +// 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; } @@ -221,9 +248,17 @@ void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox) { // ============================================================ const char *dsgnDefaultEvent(const char *typeName) { - for (int32_t i = 0; sDefaultEvents[i].typeName; i++) { - if (strcasecmp(typeName, sDefaultEvents[i].typeName) == 0) { - return sDefaultEvents[i].eventName; + 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; } } @@ -250,9 +285,9 @@ void dsgnFree(DsgnStateT *ds) { void dsgnInit(DsgnStateT *ds, AppContextT *ctx) { memset(ds, 0, sizeof(*ds)); - ds->selectedIdx = -1; - ds->activeTool = TOOL_POINTER; - ds->mode = DSGN_IDLE; + ds->selectedIdx = -1; + ds->activeTool[0] = '\0'; + ds->mode = DSGN_IDLE; ds->activeHandle = HANDLE_NONE; ds->ctx = ctx; } @@ -278,12 +313,23 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) { 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; bool inForm = false; + // 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; @@ -348,18 +394,30 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) { if (strcasecmp(typeName, "Form") == 0) { snprintf(form->name, DSGN_MAX_NAME, "%s", ctrlName); snprintf(form->caption, DSGN_MAX_TEXT, "%s", ctrlName); - inForm = true; - curCtrl = NULL; + inForm = true; + nestDepth = 0; + curCtrl = NULL; } else if (inForm) { DsgnControlT ctrl; memset(&ctrl, 0, sizeof(ctrl)); snprintf(ctrl.name, DSGN_MAX_NAME, "%s", ctrlName); snprintf(ctrl.typeName, DSGN_MAX_NAME, "%s", typeName); + + // Set parent from current nesting + if (nestDepth > 0) { + snprintf(ctrl.parentName, DSGN_MAX_NAME, "%s", parentStack[nestDepth - 1]); + } + ctrl.width = DEFAULT_CTRL_W; ctrl.height = DEFAULT_CTRL_H; - ctrl.tabIndex = (int32_t)arrlen(form->controls); arrput(form->controls, ctrl); curCtrl = &form->controls[arrlen(form->controls) - 1]; + + // If this is a container, push onto parent stack + if (dsgnIsContainer(typeName) && nestDepth < 7) { + snprintf(parentStack[nestDepth], DSGN_MAX_NAME, "%s", ctrlName); + nestDepth++; + } } continue; @@ -367,6 +425,11 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) { if (strcasecmp(trimmed, "End") == 0) { if (curCtrl) { + // If we're closing a container, pop the parent stack + if (nestDepth > 0 && strcasecmp(parentStack[nestDepth - 1], curCtrl->name) == 0) { + nestDepth--; + } + curCtrl = NULL; } else { inForm = false; @@ -415,16 +478,27 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) { val[vi] = '\0'; 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, "Width") == 0) { curCtrl->width = atoi(val); } - else if (strcasecmp(key, "Height") == 0) { curCtrl->height = atoi(val); } - else if (strcasecmp(key, "TabIndex") == 0) { curCtrl->tabIndex = atoi(val); } + 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, "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, "Width") == 0) { form->width = atoi(val); } - else if (strcasecmp(key, "Height") == 0) { form->height = atoi(val); } + if (strcasecmp(key, "Caption") == 0) { snprintf(form->caption, DSGN_MAX_TEXT, "%s", val); } + else if (strcasecmp(key, "Layout") == 0) { snprintf(form->layout, DSGN_MAX_NAME, "%.31s", val); } + 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; } } } } @@ -443,9 +517,15 @@ 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->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, "%s", name); snprintf(form->caption, DSGN_MAX_TEXT, "%s", name); @@ -463,11 +543,28 @@ void dsgnOnKey(DsgnStateT *ds, int32_t key) { return; } - // Delete key - if (key == 0x153 && ds->selectedIdx >= 0 && ds->selectedIdx < (int32_t)arrlen(ds->form->controls)) { + 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) { + arrdel(ds->form->controls, i); + + if (i < ds->selectedIdx) { + ds->selectedIdx--; + } + } + } + arrdel(ds->form->controls, ds->selectedIdx); ds->selectedIdx = -1; ds->form->dirty = true; + + rebuildWidgets(ds); } } @@ -557,7 +654,7 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) { } // Pointer tool: select, start resize or reorder - if (ds->activeTool == TOOL_POINTER) { + 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); @@ -588,32 +685,58 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) { return; } - // Non-pointer tool: place a new control (appended to VBox) - const char *typeName = dsgnToolTypeName(ds->activeTool); + // Non-pointer tool: place a new control + const char *typeName = ds->activeTool; DsgnControlT ctrl; memset(&ctrl, 0, sizeof(ctrl)); dsgnAutoName(ds, typeName, ctrl.name, DSGN_MAX_NAME); snprintf(ctrl.typeName, DSGN_MAX_NAME, "%s", typeName); - ctrl.width = DEFAULT_CTRL_W; - ctrl.height = DEFAULT_CTRL_H; - ctrl.tabIndex = ctrlCount; + ctrl.width = DEFAULT_CTRL_W; + ctrl.height = DEFAULT_CTRL_H; setPropValue(&ctrl, "Caption", ctrl.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(ctrl.parentName, DSGN_MAX_NAME, "%s", pc->name); + parentWidget = pc->widget; + break; + } + } + } + // Create the live widget - if (ds->form->contentBox) { - ctrl.widget = createDesignWidget(typeName, ds->form->contentBox); + if (parentWidget) { + ctrl.widget = createDesignWidget(typeName, parentWidget); if (ctrl.widget) { - wgtSetName(ctrl.widget, ctrl.name); - wgtSetText(ctrl.widget, ctrl.name); - ctrl.widget->prefW = wgtPixels(ctrl.width); - ctrl.widget->prefH = wgtPixels(ctrl.height); + ctrl.widget->minW = wgtPixels(ctrl.width); + ctrl.widget->minH = wgtPixels(ctrl.height); } } arrput(ds->form->controls, ctrl); ds->selectedIdx = (int32_t)arrlen(ds->form->controls) - 1; - ds->activeTool = TOOL_POINTER; + + // Set text AFTER arrput so pointers into the array element are 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; } @@ -663,7 +786,7 @@ void dsgnPaintOverlay(DsgnStateT *ds, int32_t winX, int32_t winY) { DsgnControlT *ctrl = &ds->form->controls[ds->selectedIdx]; - if (!ctrl->widget || ctrl->widget->w <= 0 || ctrl->widget->h <= 0) { + if (!ctrl->widget || !ctrl->widget->visible || ctrl->widget->w <= 0 || ctrl->widget->h <= 0) { return; } @@ -680,6 +803,7 @@ void dsgnPaintOverlay(DsgnStateT *ds, int32_t winX, int32_t winY) { 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; @@ -687,12 +811,14 @@ void dsgnPaintOverlay(DsgnStateT *ds, int32_t winX, int32_t winY) { 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 }; + // 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 }; - for (int32_t i = 0; i < HANDLE_COUNT; i++) { - rectFill(&cd, ops, hx[i], hy[i], hs, hs, black); + // 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); } } @@ -701,6 +827,69 @@ void dsgnPaintOverlay(DsgnStateT *ds, int32_t winX, int32_t winY) { // 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); + + 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); + } + + // 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; @@ -710,37 +899,25 @@ int32_t dsgnSaveFrm(const DsgnStateT *ds, char *buf, int32_t bufSize) { pos += snprintf(buf + pos, bufSize - pos, "VERSION 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, " Width = %d\n", (int)ds->form->width); - pos += snprintf(buf + pos, bufSize - pos, " Height = %d\n", (int)ds->form->height); + 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"); - int32_t count = (int32_t)arrlen(ds->form->controls); - - for (int32_t i = 0; i < count; i++) { - const DsgnControlT *ctrl = &ds->form->controls[i]; - pos += snprintf(buf + pos, bufSize - pos, " Begin %s %s\n", ctrl->typeName, ctrl->name); - - const char *caption = getPropValue(ctrl, "Caption"); - const char *text = getPropValue(ctrl, "Text"); - - if (caption) { pos += snprintf(buf + pos, bufSize - pos, " Caption = \"%s\"\n", caption); } - if (text) { pos += snprintf(buf + pos, bufSize - pos, " Text = \"%s\"\n", text); } - - pos += snprintf(buf + pos, bufSize - pos, " Left = %d\n", (int)ctrl->left); - pos += snprintf(buf + pos, bufSize - pos, " Top = %d\n", (int)ctrl->top); - pos += snprintf(buf + pos, bufSize - pos, " Width = %d\n", (int)ctrl->width); - pos += snprintf(buf + pos, bufSize - pos, " Height = %d\n", (int)ctrl->height); - - 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\"\n", ctrl->props[j].name, ctrl->props[j].value); - } - - pos += snprintf(buf + pos, bufSize - pos, " End\n"); + 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 top-level controls (and recurse into containers) + pos = saveControls(ds->form, buf, bufSize, pos, "", 1); + pos += snprintf(buf + pos, bufSize - pos, "End\n"); return pos; } @@ -765,17 +942,6 @@ const char *dsgnSelectedName(const DsgnStateT *ds) { -// ============================================================ -// dsgnToolTypeName -// ============================================================ - -const char *dsgnToolTypeName(DsgnToolE tool) { - if (tool >= 0 && tool < TOOL_COUNT) { - return sToolTypeNames[tool]; - } - - return ""; -} // ============================================================ @@ -803,7 +969,7 @@ static int32_t hitTestControl(const DsgnStateT *ds, int32_t x, int32_t y) { for (int32_t i = count - 1; i >= 0; i--) { const DsgnControlT *ctrl = &ds->form->controls[i]; - if (!ctrl->widget) { + if (!ctrl->widget || !ctrl->widget->visible) { continue; } @@ -935,9 +1101,8 @@ static void syncWidgetGeom(DsgnControlT *ctrl) { return; } - // Set minW/minH -- the layout engine uses these as the floor - // during the measure pass, so the arrange pass allocates this - // exact size (assuming weight=0). - ctrl->widget->minH = wgtPixels(ctrl->height); - ctrl->widget->prefH = wgtPixels(ctrl->height); + 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; } diff --git a/dvxbasic/ide/ideDesigner.h b/apps/dvxbasic/ide/ideDesigner.h similarity index 82% rename from dvxbasic/ide/ideDesigner.h rename to apps/dvxbasic/ide/ideDesigner.h index b1b98ac..cd7d2a3 100644 --- a/dvxbasic/ide/ideDesigner.h +++ b/apps/dvxbasic/ide/ideDesigner.h @@ -42,11 +42,14 @@ typedef struct { typedef struct { char name[DSGN_MAX_NAME]; char typeName[DSGN_MAX_NAME]; + char parentName[DSGN_MAX_NAME]; // empty = top-level (child of form) int32_t left; int32_t top; int32_t width; int32_t height; - int32_t tabIndex; + int32_t maxWidth; // 0 = no cap (stretch to fill) + int32_t maxHeight; // 0 = no cap (stretch to fill) + int32_t weight; // layout weight (0 = fixed size, >0 = share extra space) DsgnPropT props[DSGN_MAX_PROPS]; int32_t propCount; WidgetT *widget; // live widget (created at design time for WYSIWYG) @@ -61,32 +64,25 @@ typedef struct { char caption[DSGN_MAX_TEXT]; int32_t width; int32_t height; + int32_t left; // initial X position (0 = default) + int32_t top; // initial Y position (0 = default) + char layout[DSGN_MAX_NAME]; // "VBox" or "HBox" (contentBox type) + bool centered; // true = center on screen, false = use left/top + bool autoSize; // true = dvxFitWindow, false = use width/height + bool resizable; // true = user can resize at runtime DsgnControlT *controls; // stb_ds dynamic array bool dirty; WidgetT *contentBox; // VBox parent for live widgets } DsgnFormT; // ============================================================ -// Toolbox tool IDs +// Active tool // ============================================================ +// +// Empty string = pointer (select/move) mode. +// Non-empty = basName of the widget type to place. -typedef enum { - TOOL_POINTER = 0, - TOOL_BUTTON, - TOOL_LABEL, - TOOL_TEXTBOX, - TOOL_CHECKBOX, - TOOL_OPTION, - TOOL_FRAME, - TOOL_LISTBOX, - TOOL_COMBOBOX, - TOOL_HSCROLL, - TOOL_VSCROLL, - TOOL_TIMER, - TOOL_PICTURE, - TOOL_IMAGE, - TOOL_COUNT -} DsgnToolE; +#define DSGN_TOOL_NONE "" // pointer mode // ============================================================ // Grab handle IDs @@ -117,7 +113,7 @@ typedef enum { typedef struct { DsgnFormT *form; int32_t selectedIdx; // -1 = form itself selected - DsgnToolE activeTool; + char activeTool[DSGN_MAX_NAME]; // "" = pointer, else basName to place DsgnModeE mode; DsgnHandleE activeHandle; int32_t dragStartY; // mouse Y at drag start (for reorder) @@ -170,12 +166,12 @@ const char *dsgnSelectedName(const DsgnStateT *ds); // Get the default event name for a control type. const char *dsgnDefaultEvent(const char *typeName); -// Get the VB type name for a tool ID. -const char *dsgnToolTypeName(DsgnToolE tool); - // Auto-generate a unique control name for the given type. void dsgnAutoName(const DsgnStateT *ds, const char *typeName, char *buf, int32_t bufSize); +// Check if a control type is a container (can hold children). +bool dsgnIsContainer(const char *typeName); + // Free designer resources. void dsgnFree(DsgnStateT *ds); diff --git a/dvxbasic/ide/ideMain.c b/apps/dvxbasic/ide/ideMain.c similarity index 74% rename from dvxbasic/ide/ideMain.c rename to apps/dvxbasic/ide/ideMain.c index 07e5cfc..2ec7e75 100644 --- a/dvxbasic/ide/ideMain.c +++ b/apps/dvxbasic/ide/ideMain.c @@ -9,6 +9,7 @@ #include "dvxApp.h" #include "dvxCursor.h" +#include "dvxPlatform.h" #include "dvxDialog.h" #include "dvxWidget.h" #include "dvxWidgetPlugin.h" @@ -18,8 +19,10 @@ #include "widgetLabel.h" #include "widgetTextInput.h" #include "widgetDropdown.h" +#include "widgetButton.h" #include "widgetSplitter.h" #include "widgetStatusBar.h" +#include "widgetToolbar.h" #include "ideDesigner.h" #include "ideToolbox.h" @@ -43,10 +46,6 @@ // Constants // ============================================================ -#define IDE_WIN_X 10 -#define IDE_WIN_Y 10 -#define IDE_WIN_W 620 -#define IDE_WIN_H 460 #define IDE_MAX_SOURCE 65536 #define IDE_MAX_OUTPUT 32768 #define IDE_STEP_SLICE 10000 // VM steps per slice before yielding to DVX @@ -60,6 +59,13 @@ #define CMD_RUN_NOCMP 105 #define CMD_VIEW_CODE 106 #define CMD_VIEW_DESIGN 107 +#define CMD_SAVE 108 +#define CMD_WIN_CODE 109 +#define CMD_WIN_OUTPUT 110 +#define CMD_WIN_IMM 111 +#define CMD_WIN_TOOLBOX 112 +#define CMD_WIN_PROPS 113 +#define CMD_DELETE 114 #define IDE_MAX_IMM 1024 #define IDE_DESIGN_W 400 #define IDE_DESIGN_H 300 @@ -73,6 +79,9 @@ static void buildWindow(void); static void clearOutput(void); static void compileAndRun(void); static void loadFile(void); +static void loadFilePath(const char *path); +static void saveFile(void); +static void onTbSave(WidgetT *w); static void onClose(WindowT *win); static void onMenu(WindowT *win, int32_t menuId); static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx); @@ -91,8 +100,17 @@ static void setStatus(const char *text); static void switchToCode(void); static void switchToDesign(void); static int32_t onFormWinCursorQuery(WindowT *win, int32_t x, int32_t y); +static void onFormWinKey(WindowT *win, int32_t key, int32_t mod); static void onFormWinMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons); static void onFormWinPaint(WindowT *win, RectT *dirtyArea); +static void onTbRun(WidgetT *w); +static void showCodeWindow(void); +static void showOutputWindow(void); +static void showImmediateWindow(void); +static void onTbStop(WidgetT *w); +static void onTbOpen(WidgetT *w); +static void onTbCode(WidgetT *w); +static void onTbDesign(WidgetT *w); static void updateDropdowns(void); // ============================================================ @@ -101,13 +119,16 @@ static void updateDropdowns(void); static DxeAppContextT *sCtx = NULL; static AppContextT *sAc = NULL; -static WindowT *sWin = NULL; -static WidgetT *sEditor = NULL; // TextArea for source code -static WidgetT *sOutput = NULL; // TextArea for program output -static WidgetT *sImmediate = NULL; // TextArea for immediate window -static WidgetT *sObjDropdown = NULL; // Object dropdown -static WidgetT *sEvtDropdown = NULL; // Event dropdown -static WidgetT *sStatus = NULL; // Status bar label +static WindowT *sWin = NULL; // Main toolbar window +static WindowT *sCodeWin = NULL; // Code editor window +static WindowT *sOutWin = NULL; // Output window +static WindowT *sImmWin = NULL; // Immediate window +static WidgetT *sEditor = NULL; +static WidgetT *sOutput = NULL; +static WidgetT *sImmediate = NULL; +static WidgetT *sObjDropdown = NULL; +static WidgetT *sEvtDropdown = NULL; +static WidgetT *sStatus = NULL; static BasVmT *sVm = NULL; // VM instance (non-NULL while running) static BasModuleT *sCachedModule = NULL; // Last compiled module (for Ctrl+F5) static DsgnStateT sDesigner; @@ -159,6 +180,9 @@ int32_t appMain(DxeAppContextT *ctx) { sOutputBuf[0] = '\0'; sOutputLen = 0; + // Auto-load clickme.bas for development/testing + loadFilePath("C:\\BIN\\APPS\\DVXBASIC\\CLICKME.BAS"); + setStatus("Ready. Open a .BAS file or type code and press Run."); return 0; } @@ -168,7 +192,8 @@ int32_t appMain(DxeAppContextT *ctx) { // ============================================================ static void buildWindow(void) { - sWin = dvxCreateWindow(sAc, "DVX BASIC", IDE_WIN_X, IDE_WIN_Y, IDE_WIN_W, IDE_WIN_H, true); + // ---- Main toolbar window (top of screen) ---- + sWin = dvxCreateWindow(sAc, "DVX BASIC", 0, 0, sAc->display.width, 200, false); if (!sWin) { return; @@ -181,9 +206,13 @@ static void buildWindow(void) { MenuBarT *menuBar = wmAddMenuBar(sWin); MenuT *fileMenu = wmAddMenu(menuBar, "&File"); wmAddMenuItem(fileMenu, "&Open...\tCtrl+O", CMD_OPEN); + wmAddMenuItem(fileMenu, "&Save\tCtrl+S", CMD_SAVE); wmAddMenuSeparator(fileMenu); wmAddMenuItem(fileMenu, "E&xit", CMD_EXIT); + MenuT *editMenu = wmAddMenu(menuBar, "&Edit"); + wmAddMenuItem(editMenu, "&Delete\tDel", CMD_DELETE); + MenuT *runMenu = wmAddMenu(menuBar, "&Run"); wmAddMenuItem(runMenu, "&Run\tF5", CMD_RUN); wmAddMenuItem(runMenu, "Run &Without Recompile\tCtrl+F5", CMD_RUN_NOCMP); @@ -195,8 +224,17 @@ static void buildWindow(void) { wmAddMenuItem(viewMenu, "&Code\tF7", CMD_VIEW_CODE); wmAddMenuItem(viewMenu, "&Object\tShift+F7", CMD_VIEW_DESIGN); + MenuT *winMenu = wmAddMenu(menuBar, "&Window"); + wmAddMenuItem(winMenu, "&Code Editor", CMD_WIN_CODE); + wmAddMenuItem(winMenu, "&Output", CMD_WIN_OUTPUT); + wmAddMenuItem(winMenu, "&Immediate", CMD_WIN_IMM); + wmAddMenuSeparator(winMenu); + wmAddMenuItem(winMenu, "&Toolbox", CMD_WIN_TOOLBOX); + wmAddMenuItem(winMenu, "&Properties", CMD_WIN_PROPS); + AccelTableT *accel = dvxCreateAccelTable(); dvxAddAccel(accel, 'O', ACCEL_CTRL, CMD_OPEN); + dvxAddAccel(accel, 'S', ACCEL_CTRL, CMD_SAVE); dvxAddAccel(accel, KEY_F5, 0, CMD_RUN); dvxAddAccel(accel, KEY_F5, ACCEL_CTRL, CMD_RUN_NOCMP); dvxAddAccel(accel, KEY_F7, 0, CMD_VIEW_CODE); @@ -204,59 +242,41 @@ static void buildWindow(void) { dvxAddAccel(accel, 0x1B, 0, CMD_STOP); sWin->accelTable = accel; - // Widget tree - WidgetT *root = wgtInitWindow(sAc, sWin); + WidgetT *tbRoot = wgtInitWindow(sAc, sWin); + WidgetT *tb = wgtToolbar(tbRoot); - // Horizontal splitter: editor on top, output+immediate on bottom - WidgetT *splitter = wgtSplitter(root, false); - splitter->weight = 100; + WidgetT *tbOpen = wgtButton(tb, "Open"); + tbOpen->onClick = onTbOpen; - // Top pane: source code editor - WidgetT *editorFrame = wgtFrame(splitter, "Source"); + WidgetT *tbSave = wgtButton(tb, "Save"); + tbSave->onClick = onTbSave; - // Object/Event dropdown row - WidgetT *dropdownRow = wgtHBox(editorFrame); - dropdownRow->spacing = wgtPixels(4); + WidgetT *tbRun = wgtButton(tb, "Run"); + tbRun->onClick = onTbRun; - sObjDropdown = wgtDropdown(dropdownRow); - sObjDropdown->weight = 100; - sObjDropdown->onChange = onObjDropdownChange; - wgtDropdownSetItems(sObjDropdown, NULL, 0); + WidgetT *tbStop = wgtButton(tb, "Stop"); + tbStop->onClick = onTbStop; - sEvtDropdown = wgtDropdown(dropdownRow); - sEvtDropdown->weight = 100; - sEvtDropdown->onChange = onEvtDropdownChange; - wgtDropdownSetItems(sEvtDropdown, NULL, 0); + WidgetT *tbCode = wgtButton(tb, "Code"); + tbCode->onClick = onTbCode; - sEditor = wgtTextArea(editorFrame, IDE_MAX_SOURCE); - sEditor->weight = 100; - wgtTextAreaSetColorize(sEditor, basicColorize, NULL); - wgtTextAreaSetShowLineNumbers(sEditor, true); - wgtTextAreaSetAutoIndent(sEditor, true); + WidgetT *tbDesign = wgtButton(tb, "Design"); + tbDesign->onClick = onTbDesign; + + WidgetT *statusBar = wgtStatusBar(tbRoot); + sStatus = wgtLabel(statusBar, ""); + sStatus->weight = 100; + + // Fit height to content, keeping full screen width + dvxFitWindowH(sAc, sWin); // Initialize designer (form window created on demand) dsgnInit(&sDesigner, sAc); - // Bottom pane: output + immediate - WidgetT *bottomPane = wgtVBox(splitter); - - WidgetT *outputFrame = wgtFrame(bottomPane, "Output"); - outputFrame->weight = 70; - sOutput = wgtTextArea(outputFrame, IDE_MAX_OUTPUT); - sOutput->weight = 100; - sOutput->readOnly = true; - - WidgetT *immFrame = wgtFrame(bottomPane, "Immediate"); - immFrame->weight = 30; - sImmediate = wgtTextArea(immFrame, IDE_MAX_IMM); - sImmediate->weight = 100; - sImmediate->onChange = onImmediateChange; - - // Status bar - WidgetT *statusBar = wgtStatusBar(root); - sStatus = wgtLabel(statusBar, ""); - sStatus->weight = 100; - + // Create child windows + showCodeWindow(); + showOutputWindow(); + showImmediateWindow(); } // ============================================================ @@ -635,6 +655,17 @@ static void immPrintCallback(void *ctx, const char *text, bool newline) { immBuf[curLen] = '\0'; wgtSetText(sImmediate, immBuf); + + // Move cursor to end so user can keep typing + int32_t lines = 1; + + for (int32_t i = 0; i < curLen; i++) { + if (immBuf[i] == '\n') { + lines++; + } + } + + wgtTextAreaGoToLine(sImmediate, lines); } } @@ -716,6 +747,44 @@ static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufS // loadFile // ============================================================ +static void loadFilePath(const char *path) { + FILE *f = fopen(path, "r"); + + if (!f) { + return; + } + + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + + if (size >= IDE_MAX_SOURCE - 1) { + fclose(f); + return; + } + + int32_t bytesRead = (int32_t)fread(sSourceBuf, 1, size, f); + fclose(f); + sSourceBuf[bytesRead] = '\0'; + + wgtSetText(sEditor, sSourceBuf); + snprintf(sFilePath, sizeof(sFilePath), "%s", path); + + char title[300]; + snprintf(title, sizeof(title), "DVX BASIC - %s", path); + dvxSetTitle(sAc, sWin, title); + + if (sFormWin) { + onFormWinClose(sFormWin); + } + + dsgnFree(&sDesigner); + + updateDropdowns(); + setStatus("File loaded."); +} + + static void loadFile(void) { FileFilterT filters[] = { { "BASIC Files (*.bas)", "*.bas" }, @@ -726,48 +795,72 @@ static void loadFile(void) { char path[260]; if (dvxFileDialog(sAc, "Open BASIC File", FD_OPEN, NULL, filters, 3, path, sizeof(path))) { - FILE *f = fopen(path, "r"); - - if (!f) { - dvxMessageBox(sAc, "Error", "Could not open file.", MB_OK | MB_ICONERROR); - return; - } - - fseek(f, 0, SEEK_END); - long size = ftell(f); - fseek(f, 0, SEEK_SET); - - if (size >= IDE_MAX_SOURCE - 1) { - fclose(f); - dvxMessageBox(sAc, "Error", "File too large.", MB_OK | MB_ICONERROR); - return; - } - - int32_t bytesRead = (int32_t)fread(sSourceBuf, 1, size, f); - fclose(f); - sSourceBuf[bytesRead] = '\0'; - - wgtSetText(sEditor, sSourceBuf); - strncpy(sFilePath, path, sizeof(sFilePath) - 1); - sFilePath[sizeof(sFilePath) - 1] = '\0'; - - // Update window title - char title[300]; - snprintf(title, sizeof(title), "DVX BASIC - %s", path); - dvxSetTitle(sAc, sWin, title); - - // Close any open form designer and clear cached form - if (sFormWin) { - onFormWinClose(sFormWin); - } - - dsgnFree(&sDesigner); - - updateDropdowns(); - setStatus("File loaded."); + loadFilePath(path); } } +// ============================================================ +// saveFile +// ============================================================ + +static void saveFile(void) { + if (sFilePath[0] == '\0') { + // No file loaded -- use Save As dialog + FileFilterT filters[] = { + { "BASIC Files (*.bas)", "*.bas" }, + { "All Files (*.*)", "*.*" } + }; + + if (!dvxFileDialog(sAc, "Save BASIC File", FD_SAVE, NULL, filters, 2, sFilePath, sizeof(sFilePath))) { + return; + } + } + + // Save the .bas source + const char *src = wgtGetText(sEditor); + + if (src) { + FILE *f = fopen(sFilePath, "w"); + + if (f) { + fputs(src, f); + fclose(f); + } else { + dvxMessageBox(sAc, "Error", "Could not write file.", MB_OK | MB_ICONERROR); + return; + } + } + + // Save the .frm if the designer has form data + if (sDesigner.form && sDesigner.form->dirty) { + char frmPath[260]; + snprintf(frmPath, sizeof(frmPath), "%s", sFilePath); + char *dot = strrchr(frmPath, '.'); + + if (dot) { + strcpy(dot, ".frm"); + } else { + strcat(frmPath, ".frm"); + } + + char frmBuf[IDE_MAX_SOURCE]; + int32_t frmLen = dsgnSaveFrm(&sDesigner, frmBuf, sizeof(frmBuf)); + + if (frmLen > 0) { + FILE *f = fopen(frmPath, "w"); + + if (f) { + fwrite(frmBuf, 1, frmLen, f); + fclose(f); + sDesigner.form->dirty = false; + } + } + } + + setStatus("Saved."); +} + + // ============================================================ // loadFrmFiles // ============================================================ @@ -840,7 +933,27 @@ static void loadFrmFiles(BasFormRtT *rt) { // ============================================================ static void onClose(WindowT *win) { + // Close all child windows + if (sCodeWin && sCodeWin != win) { + dvxDestroyWindow(sAc, sCodeWin); + } + + if (sOutWin && sOutWin != win) { + dvxDestroyWindow(sAc, sOutWin); + } + + if (sImmWin && sImmWin != win) { + dvxDestroyWindow(sAc, sImmWin); + } + + if (sFormWin) { + onFormWinClose(sFormWin); + } + sWin = NULL; + sCodeWin = NULL; + sOutWin = NULL; + sImmWin = NULL; sEditor = NULL; sOutput = NULL; sImmediate = NULL; @@ -853,10 +966,6 @@ static void onClose(WindowT *win) { sCachedModule = NULL; } - if (sFormWin) { - onFormWinClose(sFormWin); - } - dsgnFree(&sDesigner); arrfree(sProcTable); @@ -881,6 +990,10 @@ static void onMenu(WindowT *win, int32_t menuId) { loadFile(); break; + case CMD_SAVE: + saveFile(); + break; + case CMD_RUN: compileAndRun(); break; @@ -908,6 +1021,44 @@ static void onMenu(WindowT *win, int32_t menuId) { switchToDesign(); break; + case CMD_WIN_CODE: + showCodeWindow(); + break; + + case CMD_WIN_OUTPUT: + showOutputWindow(); + break; + + case CMD_WIN_IMM: + showImmediateWindow(); + break; + + case CMD_WIN_TOOLBOX: + if (!sToolboxWin) { + sToolboxWin = tbxCreate(sAc, &sDesigner); + } + break; + + case CMD_WIN_PROPS: + if (!sPropsWin) { + sPropsWin = prpCreate(sAc, &sDesigner); + } + break; + + case CMD_DELETE: + if (sFormWin && sDesigner.selectedIdx >= 0) { + int32_t prevCount = (int32_t)arrlen(sDesigner.form->controls); + dsgnOnKey(&sDesigner, KEY_DELETE); + int32_t newCount = (int32_t)arrlen(sDesigner.form->controls); + + if (newCount != prevCount) { + prpRebuildTree(&sDesigner); + prpRefresh(&sDesigner); + dvxInvalidateWindow(sAc, sFormWin); + } + } + break; + case CMD_EXIT: if (sWin) { onClose(sWin); @@ -1090,6 +1241,35 @@ static void printCallback(void *ctx, const char *text, bool newline) { // Handle mouse events on the form designer window. Coordinates // are relative to the window's client area (content box origin). +// ============================================================ +// onFormWinKey +// ============================================================ + +static void onFormWinKey(WindowT *win, int32_t key, int32_t mod) { + (void)mod; + + if (key == KEY_DELETE && sDesigner.selectedIdx >= 0) { + int32_t prevCount = sDesigner.form ? (int32_t)arrlen(sDesigner.form->controls) : 0; + dsgnOnKey(&sDesigner, KEY_DELETE); + int32_t newCount = sDesigner.form ? (int32_t)arrlen(sDesigner.form->controls) : 0; + + if (newCount != prevCount) { + prpRebuildTree(&sDesigner); + prpRefresh(&sDesigner); + + if (sFormWin) { + dvxInvalidateWindow(sAc, sFormWin); + } + } + + return; + } + + // Forward unhandled keys to the widget system + widgetOnKey(win, key, mod); +} + + // ============================================================ // onFormWinCursorQuery // ============================================================ @@ -1097,7 +1277,16 @@ static void printCallback(void *ctx, const char *text, bool newline) { static int32_t onFormWinCursorQuery(WindowT *win, int32_t x, int32_t y) { (void)win; - if (!sDesigner.form || !sDesigner.form->controls) { + if (!sDesigner.form) { + return 0; + } + + // Crosshair when placing a new control + if (sDesigner.activeTool[0] != '\0') { + return CURSOR_CROSSHAIR; + } + + if (!sDesigner.form->controls) { return 0; } @@ -1109,7 +1298,7 @@ static int32_t onFormWinCursorQuery(WindowT *win, int32_t x, int32_t y) { DsgnControlT *ctrl = &sDesigner.form->controls[sDesigner.selectedIdx]; - if (!ctrl->widget || ctrl->widget->w <= 0 || ctrl->widget->h <= 0) { + if (!ctrl->widget || !ctrl->widget->visible || ctrl->widget->w <= 0 || ctrl->widget->h <= 0) { return 0; } @@ -1150,8 +1339,19 @@ static void onFormWinMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) } if (isDown) { - bool drag = wasDown; + int32_t prevCount = sDesigner.form ? (int32_t)arrlen(sDesigner.form->controls) : 0; + bool wasDirty = sDesigner.form ? sDesigner.form->dirty : false; + bool drag = wasDown; dsgnOnMouse(&sDesigner, x, y, drag); + + // Rebuild tree if controls were added, removed, or reordered + int32_t newCount = sDesigner.form ? (int32_t)arrlen(sDesigner.form->controls) : 0; + bool nowDirty = sDesigner.form ? sDesigner.form->dirty : false; + + if (newCount != prevCount || (nowDirty && !wasDirty)) { + prpRebuildTree(&sDesigner); + } + prpRefresh(&sDesigner); if (sFormWin) { @@ -1298,13 +1498,21 @@ static void switchToDesign(void) { sFormWin->onClose = onFormWinClose; sDesigner.formWin = sFormWin; - WidgetT *root = wgtInitWindow(sAc, sFormWin); - WidgetT *contentBox = wgtVBox(root); - contentBox->weight = 100; + WidgetT *root = wgtInitWindow(sAc, sFormWin); + WidgetT *contentBox; + + if (sDesigner.form && strcasecmp(sDesigner.form->layout, "HBox") == 0) { + contentBox = wgtHBox(root); + } else { + contentBox = wgtVBox(root); + } + + contentBox->weight = 100; // Override paint and mouse AFTER wgtInitWindow (which sets widgetOnPaint) sFormWin->onPaint = onFormWinPaint; sFormWin->onMouse = onFormWinMouse; + sFormWin->onKey = onFormWinKey; sFormWin->onCursorQuery = onFormWinCursorQuery; // Create live widgets for each control @@ -1317,8 +1525,14 @@ static void switchToDesign(void) { dvxSetTitle(sAc, sFormWin, winTitle); } - // Shrink-wrap the window to its content (matches runtime behavior) - dvxFitWindow(sAc, sFormWin); + // Size the form window + if (sDesigner.form && sDesigner.form->autoSize) { + dvxFitWindow(sAc, sFormWin); + sDesigner.form->width = sFormWin->w; + sDesigner.form->height = sFormWin->h; + } else if (sDesigner.form) { + dvxResizeWindow(sAc, sFormWin, sDesigner.form->width, sDesigner.form->height); + } // Create toolbox and properties windows if (!sToolboxWin) { @@ -1333,6 +1547,129 @@ static void switchToDesign(void) { } +// ============================================================ +// Toolbar button handlers +// ============================================================ + +static void onTbOpen(WidgetT *w) { (void)w; loadFile(); } +static void onTbSave(WidgetT *w) { (void)w; saveFile(); } +static void onTbRun(WidgetT *w) { (void)w; compileAndRun(); } +static void onTbStop(WidgetT *w) { (void)w; if (sVm) { sVm->running = false; setStatus("Program stopped."); } } +static void onTbCode(WidgetT *w) { (void)w; switchToCode(); } +static void onTbDesign(WidgetT *w) { (void)w; switchToDesign(); } + + +// ============================================================ +// showCodeWindow +// ============================================================ + +static void showCodeWindow(void) { + if (sCodeWin) { + return; // already open + } + + int32_t codeY = sWin ? sWin->y + sWin->h + 2 : 60; + int32_t codeH = sAc->display.height - codeY - 122; + + sCodeWin = dvxCreateWindow(sAc, "Code", 0, codeY, sAc->display.width, codeH, true); + + if (sCodeWin) { + sCodeWin->onMenu = onMenu; + sCodeWin->accelTable = sWin ? sWin->accelTable : NULL; + + WidgetT *codeRoot = wgtInitWindow(sAc, sCodeWin); + + WidgetT *dropdownRow = wgtHBox(codeRoot); + dropdownRow->spacing = wgtPixels(4); + + sObjDropdown = wgtDropdown(dropdownRow); + sObjDropdown->weight = 100; + sObjDropdown->onChange = onObjDropdownChange; + wgtDropdownSetItems(sObjDropdown, NULL, 0); + + sEvtDropdown = wgtDropdown(dropdownRow); + sEvtDropdown->weight = 100; + sEvtDropdown->onChange = onEvtDropdownChange; + wgtDropdownSetItems(sEvtDropdown, NULL, 0); + + sEditor = wgtTextArea(codeRoot, IDE_MAX_SOURCE); + sEditor->weight = 100; + wgtTextAreaSetColorize(sEditor, basicColorize, NULL); + wgtTextAreaSetShowLineNumbers(sEditor, true); + wgtTextAreaSetAutoIndent(sEditor, true); + + if (sFilePath[0] && sSourceBuf[0]) { + wgtSetText(sEditor, sSourceBuf); + } + + updateDropdowns(); + } +} + + +// ============================================================ +// showOutputWindow +// ============================================================ + +static void showOutputWindow(void) { + if (sOutWin) { + return; + } + + int32_t outH = 120; + int32_t outY = sAc->display.height - outH; + + sOutWin = dvxCreateWindow(sAc, "Output", 0, outY, sAc->display.width / 2, outH, true); + + if (sOutWin) { + WidgetT *outRoot = wgtInitWindow(sAc, sOutWin); + sOutput = wgtTextArea(outRoot, IDE_MAX_OUTPUT); + sOutput->weight = 100; + sOutput->readOnly = true; + + if (sOutputLen > 0) { + wgtSetText(sOutput, sOutputBuf); + } + + } +} + + +// ============================================================ +// showImmediateWindow +// ============================================================ + +static void showImmediateWindow(void) { + if (sImmWin) { + return; + } + + int32_t outH = 120; + int32_t outY = sAc->display.height - outH; + + sImmWin = dvxCreateWindow(sAc, "Immediate", sAc->display.width / 2, outY, sAc->display.width / 2, outH, true); + + if (sImmWin) { + WidgetT *immRoot = wgtInitWindow(sAc, sImmWin); + + if (immRoot) { + sImmediate = wgtTextArea(immRoot, IDE_MAX_IMM); + + if (sImmediate) { + sImmediate->weight = 100; + sImmediate->readOnly = false; + sImmediate->onChange = onImmediateChange; + } else { + dvxLog("IDE: failed to create immediate TextArea"); + } + + } else { + dvxLog("IDE: failed to init immediate window root"); + } + } +} + + // ============================================================ // setStatus // ============================================================ diff --git a/apps/dvxbasic/ide/ideProperties.c b/apps/dvxbasic/ide/ideProperties.c new file mode 100644 index 0000000..79e9ce5 --- /dev/null +++ b/apps/dvxbasic/ide/ideProperties.c @@ -0,0 +1,906 @@ +// ideProperties.c -- DVX BASIC form designer properties window +// +// A floating window with a TreeView listing all controls on the +// form (for selection and drag-reorder) and a ListView showing +// editable properties of the selected control. Double-click a +// property value to edit it via an InputBox dialog. + +#include "ideProperties.h" +#include "dvxDialog.h" +#include "dvxWm.h" +#include "widgetBox.h" +#include "widgetListView.h" +#include "widgetSplitter.h" +#include "widgetTreeView.h" + +#include +#include +#include +#include + +// ============================================================ +// Constants +// ============================================================ + +#define PRP_WIN_W 220 +#define PRP_WIN_H 400 + +// ============================================================ +// Module state +// ============================================================ + +static DsgnStateT *sDs = NULL; +static WindowT *sPrpWin = NULL; +static WidgetT *sTree = NULL; +static WidgetT *sPropList = NULL; +static AppContextT *sPrpCtx = NULL; +static bool sUpdating = false; +static char **sTreeLabels = NULL; // stb_ds array of strdup'd strings +static char **sCellData = NULL; // stb_ds array of strdup'd strings +static int32_t sCellRows = 0; + +// ============================================================ +// Helpers +// ============================================================ + +static void freeTreeLabels(void) { + int32_t count = (int32_t)arrlen(sTreeLabels); + + for (int32_t i = 0; i < count; i++) { + free(sTreeLabels[i]); + } + + arrsetlen(sTreeLabels, 0); +} + + +static void freeCellData(void) { + int32_t count = (int32_t)arrlen(sCellData); + + for (int32_t i = 0; i < count; i++) { + free(sCellData[i]); + } + + arrsetlen(sCellData, 0); + sCellRows = 0; +} + + +static void addPropRow(const char *name, const char *value) { + arrput(sCellData, strdup(name)); + arrput(sCellData, strdup(value ? value : "")); + sCellRows++; +} + +// ============================================================ +// Prototypes +// ============================================================ + +static void onPrpClose(WindowT *win); +static void onPropDblClick(WidgetT *w); +static void onTreeItemClick(WidgetT *w); +static void onTreeReorder(WidgetT *w); + + +// ============================================================ +// onPrpClose +// ============================================================ + +static void onPrpClose(WindowT *win) { + (void)win; +} + + +// ============================================================ +// getPropType +// ============================================================ +// +// Determine the data type of a property by name. Checks built-in +// properties first, then looks up the widget's interface descriptor. +// Returns WGT_IFACE_STRING, WGT_IFACE_INT, or WGT_IFACE_BOOL. + +#define PROP_TYPE_STRING WGT_IFACE_STRING +#define PROP_TYPE_INT WGT_IFACE_INT +#define PROP_TYPE_BOOL WGT_IFACE_BOOL +#define PROP_TYPE_READONLY 255 + +static uint8_t getPropType(const char *propName, const char *typeName) { + // Read-only properties + if (strcasecmp(propName, "Type") == 0) { return PROP_TYPE_READONLY; } + + // Known built-in types + if (strcasecmp(propName, "Name") == 0) { return PROP_TYPE_STRING; } + if (strcasecmp(propName, "Caption") == 0) { return PROP_TYPE_STRING; } + if (strcasecmp(propName, "MinWidth") == 0) { return PROP_TYPE_INT; } + if (strcasecmp(propName, "MinHeight") == 0) { return PROP_TYPE_INT; } + if (strcasecmp(propName, "MaxWidth") == 0) { return PROP_TYPE_INT; } + if (strcasecmp(propName, "MaxHeight") == 0) { return PROP_TYPE_INT; } + if (strcasecmp(propName, "Weight") == 0) { return PROP_TYPE_INT; } + if (strcasecmp(propName, "Left") == 0) { return PROP_TYPE_INT; } + if (strcasecmp(propName, "Top") == 0) { return PROP_TYPE_INT; } + if (strcasecmp(propName, "AutoSize") == 0) { return PROP_TYPE_BOOL; } + if (strcasecmp(propName, "Resizable") == 0) { return PROP_TYPE_BOOL; } + if (strcasecmp(propName, "Centered") == 0) { return PROP_TYPE_BOOL; } + if (strcasecmp(propName, "Visible") == 0) { return PROP_TYPE_BOOL; } + if (strcasecmp(propName, "Enabled") == 0) { return PROP_TYPE_BOOL; } + + // Look up in the widget's interface descriptor + if (typeName && typeName[0]) { + const char *wgtName = wgtFindByBasName(typeName); + + if (wgtName) { + const WgtIfaceT *iface = wgtGetIface(wgtName); + + if (iface) { + for (int32_t i = 0; i < iface->propCount; i++) { + if (strcasecmp(iface->props[i].name, propName) == 0) { + return iface->props[i].type; + } + } + } + } + } + + return PROP_TYPE_STRING; +} + + +// ============================================================ +// cascadeToChildren +// ============================================================ +// +// Recursively apply Visible or Enabled to all descendants of a +// container control. + +static void cascadeToChildren(DsgnStateT *ds, const char *parentName, bool visible, bool enabled) { + int32_t count = (int32_t)arrlen(ds->form->controls); + + for (int32_t i = 0; i < count; i++) { + DsgnControlT *child = &ds->form->controls[i]; + + if (strcasecmp(child->parentName, parentName) != 0) { + continue; + } + + if (child->widget) { + wgtSetVisible(child->widget, visible); + wgtSetEnabled(child->widget, enabled); + } + + // Recurse into nested containers + if (dsgnIsContainer(child->typeName)) { + cascadeToChildren(ds, child->name, visible, enabled); + } + } +} + + +// ============================================================ +// onPropDblClick +// ============================================================ + +static void onPropDblClick(WidgetT *w) { + if (!sDs || !sDs->form || !sPropList || !sPrpCtx) { + return; + } + + int32_t row = wgtListViewGetSelected(w); + + if (row < 0 || row >= sCellRows) { + return; + } + + const char *propName = sCellData[row * 2]; + const char *curValue = sCellData[row * 2 + 1]; + + // Layout toggles directly -- no input box needed + if (strcasecmp(propName, "Layout") == 0 && sDs->selectedIdx < 0) { + if (strcasecmp(sDs->form->layout, "VBox") == 0) { + snprintf(sDs->form->layout, DSGN_MAX_NAME, "HBox"); + } else { + snprintf(sDs->form->layout, DSGN_MAX_NAME, "VBox"); + } + + sDs->form->dirty = true; + + // Replace the content box with the new layout type + if (sDs->formWin && sDs->formWin->widgetRoot) { + WidgetT *root = sDs->formWin->widgetRoot; + + // Remove old content box + root->firstChild = NULL; + root->lastChild = NULL; + + // Create new content box + WidgetT *contentBox; + + if (strcasecmp(sDs->form->layout, "HBox") == 0) { + contentBox = wgtHBox(root); + } else { + contentBox = wgtVBox(root); + } + + contentBox->weight = 100; + + // Clear widget pointers and recreate + int32_t cc = (int32_t)arrlen(sDs->form->controls); + + for (int32_t ci = 0; ci < cc; ci++) { + sDs->form->controls[ci].widget = NULL; + } + + dsgnCreateWidgets(sDs, contentBox); + + if (sDs->formWin) { + dvxInvalidateWindow(sPrpCtx, sDs->formWin); + } + } + + prpRefresh(sDs); + return; + } + + // Get the control's type name for iface lookup + const char *ctrlTypeName = ""; + int32_t ctrlCount = (int32_t)arrlen(sDs->form->controls); + + if (sDs->selectedIdx >= 0 && sDs->selectedIdx < ctrlCount) { + ctrlTypeName = sDs->form->controls[sDs->selectedIdx].typeName; + } + + uint8_t propType = getPropType(propName, ctrlTypeName); + + if (propType == PROP_TYPE_READONLY) { + return; + } + + char newValue[DSGN_MAX_TEXT]; + + if (propType == PROP_TYPE_BOOL) { + // Toggle boolean on double-click -- no input box + bool cur = (strcasecmp(curValue, "True") == 0); + snprintf(newValue, sizeof(newValue), "%s", cur ? "False" : "True"); + } else if (propType == PROP_TYPE_INT) { + // Spinner dialog for integers + char prompt[128]; + snprintf(prompt, sizeof(prompt), "%s:", propName); + int32_t intVal = atoi(curValue); + + if (!dvxIntInputBox(sPrpCtx, "Edit Property", prompt, intVal, INT32_MIN, INT32_MAX, 1, &intVal)) { + return; + } + + snprintf(newValue, sizeof(newValue), "%d", (int)intVal); + } else { + // Text input for strings + char prompt[128]; + snprintf(prompt, sizeof(prompt), "%s:", propName); + snprintf(newValue, sizeof(newValue), "%s", curValue); + + if (!dvxInputBox(sPrpCtx, "Edit Property", prompt, curValue, newValue, sizeof(newValue))) { + return; + } + } + + int32_t count = (int32_t)arrlen(sDs->form->controls); + + if (sDs->selectedIdx >= 0 && sDs->selectedIdx < count) { + DsgnControlT *ctrl = &sDs->form->controls[sDs->selectedIdx]; + + if (strcasecmp(propName, "Name") == 0) { + snprintf(ctrl->name, DSGN_MAX_NAME, "%.31s", newValue); + + if (ctrl->widget) { + wgtSetName(ctrl->widget, ctrl->name); + } + + prpRebuildTree(sDs); + } else if (strcasecmp(propName, "MinWidth") == 0) { + ctrl->width = atoi(newValue); + + if (ctrl->widget) { + ctrl->widget->minW = wgtPixels(ctrl->width); + } + } else if (strcasecmp(propName, "MinHeight") == 0) { + ctrl->height = atoi(newValue); + + if (ctrl->widget) { + ctrl->widget->minH = wgtPixels(ctrl->height); + } + } else if (strcasecmp(propName, "MaxWidth") == 0) { + ctrl->maxWidth = atoi(newValue); + + if (ctrl->widget) { + ctrl->widget->maxW = ctrl->maxWidth > 0 ? wgtPixels(ctrl->maxWidth) : 0; + } + } else if (strcasecmp(propName, "MaxHeight") == 0) { + ctrl->maxHeight = atoi(newValue); + + if (ctrl->widget) { + ctrl->widget->maxH = ctrl->maxHeight > 0 ? wgtPixels(ctrl->maxHeight) : 0; + } + } else if (strcasecmp(propName, "Weight") == 0) { + ctrl->weight = atoi(newValue); + + if (ctrl->widget) { + ctrl->widget->weight = ctrl->weight; + } + } else if (strcasecmp(propName, "Visible") == 0) { + bool val = (strcasecmp(newValue, "True") == 0); + + if (ctrl->widget) { + wgtSetVisible(ctrl->widget, val); + } + + if (dsgnIsContainer(ctrl->typeName)) { + bool en = ctrl->widget ? ctrl->widget->enabled : true; + cascadeToChildren(sDs, ctrl->name, val, en); + } + } else if (strcasecmp(propName, "Enabled") == 0) { + bool val = (strcasecmp(newValue, "True") == 0); + + if (ctrl->widget) { + wgtSetEnabled(ctrl->widget, val); + } + + if (dsgnIsContainer(ctrl->typeName)) { + bool vis = ctrl->widget ? ctrl->widget->visible : true; + cascadeToChildren(sDs, ctrl->name, vis, val); + } + } else { + // Try widget iface setter first + bool ifaceHandled = false; + + if (ctrl->widget) { + const char *wgtName = wgtFindByBasName(ctrl->typeName); + + if (wgtName) { + const WgtIfaceT *iface = wgtGetIface(wgtName); + + if (iface) { + for (int32_t i = 0; i < iface->propCount; i++) { + const WgtPropDescT *p = &iface->props[i]; + + if (strcasecmp(p->name, propName) != 0 || !p->setFn) { + continue; + } + + if (p->type == WGT_IFACE_STRING) { + // Store in props for persistence, set from there + bool found = false; + + for (int32_t j = 0; j < ctrl->propCount; j++) { + if (strcasecmp(ctrl->props[j].name, propName) == 0) { + snprintf(ctrl->props[j].value, DSGN_MAX_TEXT, "%s", newValue); + ((void (*)(WidgetT *, const char *))p->setFn)(ctrl->widget, ctrl->props[j].value); + found = true; + break; + } + } + + if (!found && ctrl->propCount < DSGN_MAX_PROPS) { + snprintf(ctrl->props[ctrl->propCount].name, DSGN_MAX_NAME, "%s", propName); + snprintf(ctrl->props[ctrl->propCount].value, DSGN_MAX_TEXT, "%s", newValue); + ((void (*)(WidgetT *, const char *))p->setFn)(ctrl->widget, ctrl->props[ctrl->propCount].value); + ctrl->propCount++; + } + } else if (p->type == WGT_IFACE_INT) { + ((void (*)(WidgetT *, int32_t))p->setFn)(ctrl->widget, atoi(newValue)); + } else if (p->type == WGT_IFACE_BOOL) { + ((void (*)(WidgetT *, bool))p->setFn)(ctrl->widget, strcasecmp(newValue, "True") == 0); + } + + ifaceHandled = true; + break; + } + } + } + } + + if (!ifaceHandled) { + // Custom prop storage + bool found = false; + + for (int32_t i = 0; i < ctrl->propCount; i++) { + if (strcasecmp(ctrl->props[i].name, propName) == 0) { + snprintf(ctrl->props[i].value, DSGN_MAX_TEXT, "%s", newValue); + found = true; + break; + } + } + + if (!found && ctrl->propCount < DSGN_MAX_PROPS) { + snprintf(ctrl->props[ctrl->propCount].name, DSGN_MAX_NAME, "%s", propName); + snprintf(ctrl->props[ctrl->propCount].value, DSGN_MAX_TEXT, "%s", newValue); + ctrl->propCount++; + } + + // Update widget text from the persistent props array + if (ctrl->widget && (strcasecmp(propName, "Caption") == 0 || strcasecmp(propName, "Text") == 0)) { + for (int32_t i = 0; i < ctrl->propCount; i++) { + if (strcasecmp(ctrl->props[i].name, propName) == 0) { + wgtSetText(ctrl->widget, ctrl->props[i].value); + break; + } + } + } + } + } + + sDs->form->dirty = true; + + if (sDs->formWin) { + dvxInvalidateWindow(sPrpCtx, sDs->formWin); + } + } else { + if (strcasecmp(propName, "Caption") == 0) { + snprintf(sDs->form->caption, DSGN_MAX_TEXT, "%s", newValue); + + if (sDs->formWin) { + char winTitle[280]; + snprintf(winTitle, sizeof(winTitle), "%s [Design]", sDs->form->caption); + dvxSetTitle(sPrpCtx, sDs->formWin, winTitle); + } + } else if (strcasecmp(propName, "AutoSize") == 0) { + sDs->form->autoSize = (strcasecmp(newValue, "True") == 0); + + if (sDs->form->autoSize && sDs->formWin) { + dvxFitWindow(sPrpCtx, sDs->formWin); + sDs->form->width = sDs->formWin->w; + sDs->form->height = sDs->formWin->h; + } + } else if (strcasecmp(propName, "Resizable") == 0) { + sDs->form->resizable = (strcasecmp(newValue, "True") == 0); + + if (sDs->formWin) { + sDs->formWin->resizable = sDs->form->resizable; + dvxInvalidateWindow(sPrpCtx, sDs->formWin); + } + } else if (strcasecmp(propName, "Centered") == 0) { + sDs->form->centered = (strcasecmp(newValue, "True") == 0); + } else if (strcasecmp(propName, "Left") == 0) { + sDs->form->left = atoi(newValue); + } else if (strcasecmp(propName, "Top") == 0) { + sDs->form->top = atoi(newValue); + } else if (strcasecmp(propName, "Width") == 0) { + sDs->form->width = atoi(newValue); + sDs->form->autoSize = false; + } else if (strcasecmp(propName, "Height") == 0) { + sDs->form->height = atoi(newValue); + sDs->form->autoSize = false; + } + + sDs->form->dirty = true; + + // Resize the form designer window + if (sDs->formWin) { + if (sDs->form->autoSize) { + dvxFitWindow(sPrpCtx, sDs->formWin); + sDs->form->width = sDs->formWin->w; + sDs->form->height = sDs->formWin->h; + } else { + dvxResizeWindow(sPrpCtx, sDs->formWin, sDs->form->width, sDs->form->height); + } + } + } + + prpRefresh(sDs); +} + + +// ============================================================ +// onTreeItemClick +// ============================================================ + +static void onTreeItemClick(WidgetT *w) { + (void)w; + + if (!sDs || !sDs->form || !sTree || sUpdating) { + return; + } + + // Check if it's the form item + WidgetT *formItem = sTree->firstChild; + + if (w == formItem) { + sDs->selectedIdx = -1; + + if (sDs->formWin) { + dvxInvalidateWindow(sPrpCtx, sDs->formWin); + } + + prpRefresh(sDs); + return; + } + + // Match by label text against control names + const char *label = (const char *)w->userData; + + if (!label) { + return; + } + + // Extract name from "Name (Type)" + char clickedName[DSGN_MAX_NAME]; + int32_t ni = 0; + + while (label[ni] && label[ni] != ' ' && ni < DSGN_MAX_NAME - 1) { + clickedName[ni] = label[ni]; + ni++; + } + + clickedName[ni] = '\0'; + + int32_t count = (int32_t)arrlen(sDs->form->controls); + + for (int32_t i = 0; i < count; i++) { + if (strcmp(sDs->form->controls[i].name, clickedName) == 0) { + sDs->selectedIdx = i; + + if (sDs->formWin) { + dvxInvalidateWindow(sPrpCtx, sDs->formWin); + } + + prpRefresh(sDs); + return; + } + } +} + + +// ============================================================ +// onTreeReorder +// ============================================================ + +// Walk tree items recursively, collecting control names in order. + +static void collectTreeOrder(WidgetT *parent, DsgnControlT *srcArr, int32_t srcCount, DsgnControlT **outArr, const char *parentName) { + for (WidgetT *item = parent->firstChild; item; item = item->nextSibling) { + const char *label = (const char *)item->userData; + + if (!label) { + continue; + } + + char itemName[DSGN_MAX_NAME]; + int32_t ni = 0; + + while (label[ni] && label[ni] != ' ' && ni < DSGN_MAX_NAME - 1) { + itemName[ni] = label[ni]; + ni++; + } + + itemName[ni] = '\0'; + + for (int32_t i = 0; i < srcCount; i++) { + if (strcmp(srcArr[i].name, itemName) == 0) { + DsgnControlT ctrl = srcArr[i]; + snprintf(ctrl.parentName, DSGN_MAX_NAME, "%s", parentName); + arrput(*outArr, ctrl); + + // Recurse into children (for containers) + if (item->firstChild) { + collectTreeOrder(item, srcArr, srcCount, outArr, itemName); + } + + break; + } + } + } +} + + +static void onTreeReorder(WidgetT *w) { + (void)w; + + if (!sDs || !sDs->form || !sTree || sUpdating) { + return; + } + + int32_t count = (int32_t)arrlen(sDs->form->controls); + DsgnControlT *newArr = NULL; + WidgetT *formItem = sTree->firstChild; + + if (!formItem) { + return; + } + + // Collect all controls from the tree in their new order, + // handling nesting (items dragged into containers get parentName updated). + collectTreeOrder(formItem, sDs->form->controls, count, &newArr, ""); + + // If we lost items (dragged above form), revert + if ((int32_t)arrlen(newArr) != count) { + arrfree(newArr); + prpRebuildTree(sDs); + return; + } + + arrfree(sDs->form->controls); + sDs->form->controls = newArr; + sDs->form->dirty = true; + + if (sDs->form->contentBox) { + sDs->form->contentBox->firstChild = NULL; + sDs->form->contentBox->lastChild = NULL; + + int32_t newCount = (int32_t)arrlen(sDs->form->controls); + + for (int32_t i = 0; i < newCount; i++) { + sDs->form->controls[i].widget = NULL; + } + + dsgnCreateWidgets(sDs, sDs->form->contentBox); + } + + prpRebuildTree(sDs); + + if (sDs->formWin) { + dvxInvalidateWindow(sPrpCtx, sDs->formWin); + } + + prpRefresh(sDs); +} + + +// ============================================================ +// prpCreate +// ============================================================ + +WindowT *prpCreate(AppContextT *ctx, DsgnStateT *ds) { + sDs = ds; + sPrpCtx = ctx; + + int32_t winX = ctx->display.width - PRP_WIN_W - 10; + WindowT *win = dvxCreateWindow(ctx, "Properties", winX, 30, PRP_WIN_W, PRP_WIN_H, true); + + if (!win) { + return NULL; + } + + win->onClose = onPrpClose; + sPrpWin = win; + + WidgetT *root = wgtInitWindow(ctx, win); + + // Splitter: tree on top, property list on bottom + WidgetT *splitter = wgtSplitter(root, false); + splitter->weight = 100; + wgtSplitterSetPos(splitter, (PRP_WIN_H - CHROME_TOTAL_TOP - CHROME_TOTAL_BOTTOM) / 2); + + // Control tree (top pane) + sTree = wgtTreeView(splitter); + sTree->onChange = onTreeReorder; + wgtTreeViewSetReorderable(sTree, true); + + // Property ListView (bottom pane) + sPropList = wgtListView(splitter); + sPropList->onDblClick = onPropDblClick; + + static const ListViewColT cols[2] = { + { "Property", 0, ListViewAlignLeftE }, + { "Value", 0, ListViewAlignLeftE } + }; + + wgtListViewSetColumns(sPropList, cols, 2); + + prpRebuildTree(ds); + prpRefresh(ds); + return win; +} + + +// ============================================================ +// prpDestroy +// ============================================================ + +void prpDestroy(AppContextT *ctx, WindowT *win) { + freeTreeLabels(); + arrfree(sTreeLabels); + sTreeLabels = NULL; + + freeCellData(); + arrfree(sCellData); + sCellData = NULL; + + if (win) { + dvxDestroyWindow(ctx, win); + } + + sPrpWin = NULL; + sTree = NULL; + sPropList = NULL; + sDs = NULL; +} + + +// ============================================================ +// prpRebuildTree +// ============================================================ + +void prpRebuildTree(DsgnStateT *ds) { + if (!sTree || !ds || !ds->form) { + return; + } + + sUpdating = true; + + freeTreeLabels(); + sTree->firstChild = NULL; + sTree->lastChild = NULL; + + // Form entry at the top + char *formLabel = strdup(ds->form->name); + arrput(sTreeLabels, formLabel); + + WidgetT *formItem = wgtTreeItem(sTree, formLabel); + formItem->userData = formLabel; + formItem->onClick = onTreeItemClick; + wgtTreeItemSetExpanded(formItem, true); + + if (ds->selectedIdx < 0) { + wgtTreeItemSetSelected(formItem, true); + } + + // Control entries -- nest children under container parents + int32_t count = (int32_t)arrlen(ds->form->controls); + + // Temporary array to map control index -> tree item + WidgetT **treeItems = NULL; + + for (int32_t i = 0; i < count; i++) { + DsgnControlT *ctrl = &ds->form->controls[i]; + char buf[128]; + snprintf(buf, sizeof(buf), "%s (%s)", ctrl->name, ctrl->typeName); + + char *label = strdup(buf); + arrput(sTreeLabels, label); + + // Find the tree parent: form item or a container's tree item + WidgetT *treeParent = formItem; + + if (ctrl->parentName[0]) { + for (int32_t j = 0; j < i; j++) { + if (strcasecmp(ds->form->controls[j].name, ctrl->parentName) == 0 && treeItems) { + treeParent = treeItems[j]; + break; + } + } + } + + WidgetT *item = wgtTreeItem(treeParent, label); + item->userData = label; + item->onClick = onTreeItemClick; + arrput(treeItems, item); + + if (dsgnIsContainer(ctrl->typeName)) { + wgtTreeItemSetExpanded(item, true); + } + + if (i == ds->selectedIdx) { + wgtTreeItemSetSelected(item, true); + } + } + + arrfree(treeItems); + + sUpdating = false; +} + + +// ============================================================ +// prpRefresh +// ============================================================ + +void prpRefresh(DsgnStateT *ds) { + if (!ds || !ds->form) { + return; + } + + // Don't rebuild the tree here -- just update selection on existing items. + // prpRebuildTree destroys all items which loses TreeView selection state. + + // Update property ListView + if (!sPropList) { + return; + } + + freeCellData(); + + int32_t count = (int32_t)arrlen(ds->form->controls); + + if (ds->selectedIdx >= 0 && ds->selectedIdx < count) { + DsgnControlT *ctrl = &ds->form->controls[ds->selectedIdx]; + char buf[32]; + + addPropRow("Name", ctrl->name); + addPropRow("Type", ctrl->typeName); + + snprintf(buf, sizeof(buf), "%d", (int)ctrl->width); + addPropRow("MinWidth", buf); + + snprintf(buf, sizeof(buf), "%d", (int)ctrl->height); + addPropRow("MinHeight", buf); + + snprintf(buf, sizeof(buf), "%d", (int)ctrl->maxWidth); + addPropRow("MaxWidth", buf); + + snprintf(buf, sizeof(buf), "%d", (int)ctrl->maxHeight); + addPropRow("MaxHeight", buf); + + snprintf(buf, sizeof(buf), "%d", (int)ctrl->weight); + addPropRow("Weight", buf); + + addPropRow("Visible", ctrl->widget && ctrl->widget->visible ? "True" : "False"); + addPropRow("Enabled", ctrl->widget && ctrl->widget->enabled ? "True" : "False"); + + for (int32_t i = 0; i < ctrl->propCount; i++) { + addPropRow(ctrl->props[i].name, ctrl->props[i].value); + } + + // Widget interface properties (from the .wgt descriptor) + const char *wgtName = wgtFindByBasName(ctrl->typeName); + + if (wgtName) { + const WgtIfaceT *iface = wgtGetIface(wgtName); + + if (iface && ctrl->widget) { + for (int32_t i = 0; i < iface->propCount; i++) { + const WgtPropDescT *p = &iface->props[i]; + + // Skip if already shown as a custom prop + bool already = false; + + for (int32_t j = 0; j < ctrl->propCount; j++) { + if (strcasecmp(ctrl->props[j].name, p->name) == 0) { + already = true; + break; + } + } + + if (already) { + continue; + } + + // Read the current value from the widget + if (p->type == WGT_IFACE_STRING && p->getFn) { + const char *s = ((const char *(*)(WidgetT *))p->getFn)(ctrl->widget); + addPropRow(p->name, s ? s : ""); + } else if (p->type == WGT_IFACE_INT && p->getFn) { + int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget); + snprintf(buf, sizeof(buf), "%d", (int)v); + addPropRow(p->name, buf); + } else if (p->type == WGT_IFACE_BOOL && p->getFn) { + bool v = ((bool (*)(const WidgetT *))p->getFn)(ctrl->widget); + addPropRow(p->name, v ? "True" : "False"); + } else { + addPropRow(p->name, ""); + } + } + } + } + } else { + char buf[32]; + + addPropRow("Name", ds->form->name); + addPropRow("Caption", ds->form->caption); + addPropRow("Layout", ds->form->layout); + addPropRow("AutoSize", ds->form->autoSize ? "True" : "False"); + addPropRow("Resizable", ds->form->resizable ? "True" : "False"); + addPropRow("Centered", ds->form->centered ? "True" : "False"); + + snprintf(buf, sizeof(buf), "%d", (int)ds->form->left); + addPropRow("Left", buf); + + snprintf(buf, sizeof(buf), "%d", (int)ds->form->top); + addPropRow("Top", buf); + + snprintf(buf, sizeof(buf), "%d", (int)ds->form->width); + addPropRow("Width", buf); + + snprintf(buf, sizeof(buf), "%d", (int)ds->form->height); + addPropRow("Height", buf); + } + + wgtListViewSetData(sPropList, (const char **)sCellData, sCellRows); +} diff --git a/dvxbasic/ide/ideProperties.h b/apps/dvxbasic/ide/ideProperties.h similarity index 67% rename from dvxbasic/ide/ideProperties.h rename to apps/dvxbasic/ide/ideProperties.h index 042467b..a14a0fe 100644 --- a/dvxbasic/ide/ideProperties.h +++ b/apps/dvxbasic/ide/ideProperties.h @@ -11,7 +11,10 @@ WindowT *prpCreate(AppContextT *ctx, DsgnStateT *ds); // Destroy the properties window. void prpDestroy(AppContextT *ctx, WindowT *win); -// Refresh the properties list for the currently selected control. +// Rebuild the tree from the controls array (after add/remove/reorder). +void prpRebuildTree(DsgnStateT *ds); + +// Refresh selection and properties display (lightweight, no tree rebuild). void prpRefresh(DsgnStateT *ds); #endif // IDE_PROPERTIES_H diff --git a/apps/dvxbasic/ide/ideToolbox.c b/apps/dvxbasic/ide/ideToolbox.c new file mode 100644 index 0000000..e1e6d56 --- /dev/null +++ b/apps/dvxbasic/ide/ideToolbox.c @@ -0,0 +1,213 @@ +// ideToolbox.c -- DVX BASIC form designer toolbox window +// +// A small floating window with buttons for each registered widget +// type that has a basName (VB-style name). Built dynamically from +// the widget interface registry. + +#include "ideToolbox.h" +#include "dvxApp.h" +#include "dvxResource.h" +#include "dvxWm.h" +#include "widgetBox.h" +#include "widgetButton.h" +#include "widgetImageButton.h" + +#include +#include +#include +#include + +// ============================================================ +// Constants +// ============================================================ + +#define TBX_COLS 4 +#define TBX_WIN_W 90 +#define TBX_WIN_H 200 + +// ============================================================ +// Per-tool entry +// ============================================================ + +typedef struct { + char typeName[DSGN_MAX_NAME]; + char tooltip[64]; +} TbxToolEntryT; + +// ============================================================ +// Module state +// ============================================================ + +static DsgnStateT *sDs = NULL; +static TbxToolEntryT *sTbxTools = NULL; // stb_ds dynamic array + +// ============================================================ +// Callbacks +// ============================================================ + +static void onToolClick(WidgetT *w) { + if (!sDs) { + return; + } + + int32_t toolIdx = (int32_t)(intptr_t)w->userData; + + if (toolIdx >= 0 && toolIdx < (int32_t)arrlen(sTbxTools)) { + const char *typeName = sTbxTools[toolIdx].typeName; + + // Toggle: clicking the same tool again deselects it + if (strcasecmp(sDs->activeTool, typeName) == 0) { + sDs->activeTool[0] = '\0'; + } else { + snprintf(sDs->activeTool, DSGN_MAX_NAME, "%s", typeName); + } + + sDs->mode = DSGN_IDLE; + } +} + + +static void onTbxClose(WindowT *win) { + (void)win; +} + + +// ============================================================ +// tbxCreate +// ============================================================ + +WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds) { + sDs = ds; + arrsetlen(sTbxTools, 0); + + WindowT *win = dvxCreateWindow(ctx, "Toolbox", 0, 30, TBX_WIN_W, TBX_WIN_H, false); + + if (!win) { + return NULL; + } + + win->onClose = onTbxClose; + + WidgetT *root = wgtInitWindow(ctx, win); + + // Enumerate all registered widget interfaces with a basName + int32_t ifaceCount = wgtIfaceCount(); + int32_t col = 0; + WidgetT *row = NULL; + WidgetT **buttons = NULL; // stb_ds array, parallel to sTbxTools + + for (int32_t i = 0; i < ifaceCount; i++) { + const char *wgtName = NULL; + const WgtIfaceT *iface = wgtIfaceAt(i, &wgtName); + + if (!iface || !iface->basName || !iface->basName[0]) { + continue; + } + + // Add tool entry + TbxToolEntryT entry; + memset(&entry, 0, sizeof(entry)); + snprintf(entry.typeName, DSGN_MAX_NAME, "%s", iface->basName); + + // Load icon data and tooltip from the .wgt file resources + uint8_t *iconData = NULL; + int32_t iconW = 0; + int32_t iconH = 0; + int32_t iconPitch = 0; + const char *wgtPath = wgtIfaceGetPath(wgtName); + int32_t pathIdx = wgtIfaceGetPathIndex(wgtName); + + if (wgtPath) { + DvxResHandleT *res = dvxResOpen(wgtPath); + + if (res) { + // Build suffixed resource names: "icon16", "icon16-2", "icon16-3", etc. + char iconResName[32]; + char nameResName[32]; + + if (pathIdx <= 1) { + snprintf(iconResName, sizeof(iconResName), "icon16"); + snprintf(nameResName, sizeof(nameResName), "name"); + } else { + snprintf(iconResName, sizeof(iconResName), "icon16-%d", (int)pathIdx); + snprintf(nameResName, sizeof(nameResName), "name-%d", (int)pathIdx); + } + + uint32_t iconSize = 0; + void *iconBuf = dvxResRead(res, iconResName, &iconSize); + + if (iconBuf) { + iconData = dvxLoadImageFromMemory(ctx, (const uint8_t *)iconBuf, (int32_t)iconSize, &iconW, &iconH, &iconPitch); + free(iconBuf); + } + + uint32_t nameSize = 0; + void *nameBuf = dvxResRead(res, nameResName, &nameSize); + + if (nameBuf) { + snprintf(entry.tooltip, sizeof(entry.tooltip), "%s", (const char *)nameBuf); + free(nameBuf); + } + + dvxResClose(res); + } + } + + if (!entry.tooltip[0]) { + snprintf(entry.tooltip, sizeof(entry.tooltip), "%s", iface->basName); + } + + arrput(sTbxTools, entry); + int32_t toolIdx = (int32_t)arrlen(sTbxTools) - 1; + + // Start a new row every TBX_COLS buttons + if (col == 0 || !row) { + row = wgtHBox(root); + } + + // Create button in the current row + WidgetT *btn = NULL; + + if (iconData) { + btn = wgtImageButton(row, iconData, iconW, iconH, iconPitch); + } else { + btn = wgtButton(row, iface->basName); + } + + btn->onClick = onToolClick; + btn->userData = (void *)(intptr_t)toolIdx; + arrput(buttons, btn); + + col = (col + 1) % TBX_COLS; + + // Yield to keep the UI responsive while loading icons + dvxUpdate(ctx); + } + + // Set tooltips after array is finalized (arrput can reallocate) + int32_t btnCount = (int32_t)arrlen(buttons); + + for (int32_t i = 0; i < btnCount; i++) { + wgtSetTooltip(buttons[i], sTbxTools[i].tooltip); + } + + arrfree(buttons); + + dvxFitWindow(ctx, win); + return win; +} + + +// ============================================================ +// tbxDestroy +// ============================================================ + +void tbxDestroy(AppContextT *ctx, WindowT *win) { + if (win) { + dvxDestroyWindow(ctx, win); + } + + arrfree(sTbxTools); + sTbxTools = NULL; + sDs = NULL; +} diff --git a/dvxbasic/ide/ideToolbox.h b/apps/dvxbasic/ide/ideToolbox.h similarity index 100% rename from dvxbasic/ide/ideToolbox.h rename to apps/dvxbasic/ide/ideToolbox.h diff --git a/dvxbasic/runtime/values.c b/apps/dvxbasic/runtime/values.c similarity index 100% rename from dvxbasic/runtime/values.c rename to apps/dvxbasic/runtime/values.c diff --git a/dvxbasic/runtime/values.h b/apps/dvxbasic/runtime/values.h similarity index 100% rename from dvxbasic/runtime/values.h rename to apps/dvxbasic/runtime/values.h diff --git a/dvxbasic/runtime/vm.c b/apps/dvxbasic/runtime/vm.c similarity index 100% rename from dvxbasic/runtime/vm.c rename to apps/dvxbasic/runtime/vm.c diff --git a/dvxbasic/runtime/vm.h b/apps/dvxbasic/runtime/vm.h similarity index 100% rename from dvxbasic/runtime/vm.h rename to apps/dvxbasic/runtime/vm.h diff --git a/dvxbasic/samples/clickme.bas b/apps/dvxbasic/samples/clickme.bas similarity index 100% rename from dvxbasic/samples/clickme.bas rename to apps/dvxbasic/samples/clickme.bas diff --git a/dvxbasic/samples/clickme.frm b/apps/dvxbasic/samples/clickme.frm similarity index 100% rename from dvxbasic/samples/clickme.frm rename to apps/dvxbasic/samples/clickme.frm diff --git a/dvxbasic/samples/formtest.bas b/apps/dvxbasic/samples/formtest.bas similarity index 100% rename from dvxbasic/samples/formtest.bas rename to apps/dvxbasic/samples/formtest.bas diff --git a/dvxbasic/samples/hello.bas b/apps/dvxbasic/samples/hello.bas similarity index 100% rename from dvxbasic/samples/hello.bas rename to apps/dvxbasic/samples/hello.bas diff --git a/dvxbasic/samples/input.bas b/apps/dvxbasic/samples/input.bas similarity index 100% rename from dvxbasic/samples/input.bas rename to apps/dvxbasic/samples/input.bas diff --git a/dvxbasic/test_compiler.c b/apps/dvxbasic/test_compiler.c similarity index 100% rename from dvxbasic/test_compiler.c rename to apps/dvxbasic/test_compiler.c diff --git a/dvxbasic/test_lex.c b/apps/dvxbasic/test_lex.c similarity index 100% rename from dvxbasic/test_lex.c rename to apps/dvxbasic/test_lex.c diff --git a/dvxbasic/test_quick.c b/apps/dvxbasic/test_quick.c similarity index 100% rename from dvxbasic/test_quick.c rename to apps/dvxbasic/test_quick.c diff --git a/dvxbasic/test_vm.c b/apps/dvxbasic/test_vm.c similarity index 100% rename from dvxbasic/test_vm.c rename to apps/dvxbasic/test_vm.c diff --git a/apps/dvxdemo/dvxdemo.c b/apps/dvxdemo/dvxdemo.c index d16fe38..7be9850 100644 --- a/apps/dvxdemo/dvxdemo.c +++ b/apps/dvxdemo/dvxdemo.c @@ -807,8 +807,6 @@ static void setupControlsWindow(void) { sbLabel->weight = 100; wgtSetName(sbLabel, "advStatus"); wgtLabel(sb, "Line 1, Col 1"); - - wgtInvalidate(root); } @@ -1126,8 +1124,6 @@ static void setupWidgetDemo(void) { WidgetT *okBtn = wgtButton(btnRow, "&OK"); okBtn->onClick = onOkClick; wgtButton(btnRow, "&Cancel"); - - wgtInvalidate(root); } diff --git a/apps/imgview/imgview.c b/apps/imgview/imgview.c index 541c3cc..3943621 100644 --- a/apps/imgview/imgview.c +++ b/apps/imgview/imgview.c @@ -212,9 +212,6 @@ static void loadAndDisplay(const char *path) { buildScaled(sWin->contentW, sWin->contentH); dvxSetBusy(sAc, false); - RectT fullRect = {0, 0, sWin->contentW, sWin->contentH}; - sWin->onPaint(sWin, &fullRect); - sWin->contentDirty = true; dvxInvalidateWindow(sAc, sWin); } diff --git a/apps/notepad/notepad.c b/apps/notepad/notepad.c index bb9304e..abb65d7 100644 --- a/apps/notepad/notepad.c +++ b/apps/notepad/notepad.c @@ -396,6 +396,5 @@ int32_t appMain(DxeAppContextT *ctx) { sFilePath[0] = '\0'; markClean(); - wgtInvalidate(root); return 0; } diff --git a/apps/progman/progman.c b/apps/progman/progman.c index da71646..0c3e9cc 100644 --- a/apps/progman/progman.c +++ b/apps/progman/progman.c @@ -453,15 +453,21 @@ static void scanAppsDirRecurse(const char *dirPath) { if (iconBuf) { entry->iconData = dvxLoadImageFromMemory(sAc, (const uint8_t *)iconBuf, (int32_t)iconSize, &entry->iconW, &entry->iconH, &entry->iconPitch); + + if (!entry->iconData) { + dvxLog("Progman: failed to decode icon for %s (%d bytes)", ent->d_name, (int)iconSize); + } + free(iconBuf); + } else { + dvxLog("Progman: no icon32 resource in %s", ent->d_name); } uint32_t nameSize = 0; void *nameBuf = dvxResRead(res, "name", &nameSize); if (nameBuf) { - strncpy(entry->name, (const char *)nameBuf, SHELL_APP_NAME_MAX - 1); - entry->name[SHELL_APP_NAME_MAX - 1] = '\0'; + snprintf(entry->name, SHELL_APP_NAME_MAX, "%s", (const char *)nameBuf); free(nameBuf); } @@ -469,14 +475,16 @@ static void scanAppsDirRecurse(const char *dirPath) { void *descBuf = dvxResRead(res, "description", &descSize); if (descBuf) { - strncpy(entry->tooltip, (const char *)descBuf, sizeof(entry->tooltip) - 1); - entry->tooltip[sizeof(entry->tooltip) - 1] = '\0'; + snprintf(entry->tooltip, sizeof(entry->tooltip), "%s", (const char *)descBuf); free(descBuf); } dvxResClose(res); + } else { + dvxLog("Progman: no resources in %s", ent->d_name); } + dvxLog("Progman: found %s (%s) icon=%s", entry->name, ent->d_name, entry->iconData ? "yes" : "no"); sAppCount++; } diff --git a/core/Makefile b/core/Makefile index 76ce067..9372323 100644 --- a/core/Makefile +++ b/core/Makefile @@ -55,14 +55,14 @@ CORE_HDRS = dvxTypes.h dvxApp.h dvxDraw.h dvxWm.h dvxVideo.h dvxWidget.h platfor $(OBJDIR)/dvxVideo.o: dvxVideo.c dvxVideo.h platform/dvxPlatform.h dvxTypes.h dvxPalette.h $(OBJDIR)/dvxDraw.o: dvxDraw.c dvxDraw.h platform/dvxPlatform.h dvxTypes.h $(OBJDIR)/dvxComp.o: dvxComp.c dvxComp.h platform/dvxPlatform.h dvxTypes.h -$(OBJDIR)/dvxWm.o: dvxWm.c dvxWm.h dvxTypes.h dvxDraw.h dvxComp.h dvxVideo.h dvxWidget.h thirdparty/stb_image.h +$(OBJDIR)/dvxWm.o: dvxWm.c dvxWm.h dvxTypes.h dvxDraw.h dvxComp.h dvxVideo.h dvxWidget.h platform/dvxPlatform.h thirdparty/stb_image.h $(OBJDIR)/dvxImage.o: dvxImage.c thirdparty/stb_image.h $(OBJDIR)/dvxImageWrite.o: dvxImageWrite.c thirdparty/stb_image_write.h $(OBJDIR)/dvxApp.o: dvxApp.c dvxApp.h platform/dvxPlatform.h dvxTypes.h dvxVideo.h dvxDraw.h dvxComp.h dvxWm.h dvxFont.h dvxCursor.h $(OBJDIR)/dvxDialog.o: dvxDialog.c dvxDialog.h platform/dvxPlatform.h dvxApp.h dvxWidget.h dvxWidgetPlugin.h dvxTypes.h dvxDraw.h $(OBJDIR)/dvxPrefs.o: dvxPrefs.c dvxPrefs.h -WIDGET_DEPS = dvxWidgetPlugin.h dvxWidget.h dvxTypes.h dvxApp.h dvxDraw.h dvxWm.h dvxVideo.h +WIDGET_DEPS = dvxWidgetPlugin.h dvxWidget.h dvxTypes.h dvxApp.h dvxDraw.h dvxWm.h dvxVideo.h platform/dvxPlatform.h $(OBJDIR)/widgetClass.o: widgetClass.c $(WIDGET_DEPS) $(OBJDIR)/widgetCore.o: widgetCore.c $(WIDGET_DEPS) $(OBJDIR)/widgetScrollbar.o: widgetScrollbar.c $(WIDGET_DEPS) diff --git a/core/dvxApp.c b/core/dvxApp.c index 52d21e3..83136f8 100644 --- a/core/dvxApp.c +++ b/core/dvxApp.c @@ -3863,26 +3863,9 @@ void dvxDestroyWindow(AppContextT *ctx, WindowT *win) { // Used after building a dialog's widget tree to size the dialog // automatically rather than requiring the caller to compute sizes manually. -void dvxFitWindow(AppContextT *ctx, WindowT *win) { - if (!ctx || !win || !win->widgetRoot) { - return; - } - - // Measure the widget tree to get minimum content size - widgetCalcMinSizeTree(win->widgetRoot, &ctx->font); - - int32_t contentW = win->widgetRoot->calcMinW; - int32_t contentH = win->widgetRoot->calcMinH; - - // Compute chrome overhead - int32_t topChrome = CHROME_TOTAL_TOP; - if (win->menuBar) { - topChrome += CHROME_MENU_HEIGHT; - } - - int32_t newW = contentW + CHROME_TOTAL_SIDE * 2; - int32_t newH = contentH + topChrome + CHROME_TOTAL_BOTTOM; +// dvxResizeWindow -- resize a window to the given outer dimensions +void dvxResizeWindow(AppContextT *ctx, WindowT *win, int32_t newW, int32_t newH) { // Dirty old position dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); @@ -3918,6 +3901,58 @@ void dvxFitWindow(AppContextT *ctx, WindowT *win) { } +void dvxFitWindow(AppContextT *ctx, WindowT *win) { + if (!ctx || !win || !win->widgetRoot) { + return; + } + + widgetCalcMinSizeTree(win->widgetRoot, &ctx->font); + + int32_t topChrome = CHROME_TOTAL_TOP; + + if (win->menuBar) { + topChrome += CHROME_MENU_HEIGHT; + } + + int32_t newW = win->widgetRoot->calcMinW + CHROME_TOTAL_SIDE * 2; + int32_t newH = win->widgetRoot->calcMinH + topChrome + CHROME_TOTAL_BOTTOM; + + dvxResizeWindow(ctx, win, newW, newH); +} + + +void dvxFitWindowW(AppContextT *ctx, WindowT *win) { + if (!ctx || !win || !win->widgetRoot) { + return; + } + + widgetCalcMinSizeTree(win->widgetRoot, &ctx->font); + + int32_t newW = win->widgetRoot->calcMinW + CHROME_TOTAL_SIDE * 2; + + dvxResizeWindow(ctx, win, newW, win->h); +} + + +void dvxFitWindowH(AppContextT *ctx, WindowT *win) { + if (!ctx || !win || !win->widgetRoot) { + return; + } + + widgetCalcMinSizeTree(win->widgetRoot, &ctx->font); + + int32_t topChrome = CHROME_TOTAL_TOP; + + if (win->menuBar) { + topChrome += CHROME_MENU_HEIGHT; + } + + int32_t newH = win->widgetRoot->calcMinH + topChrome + CHROME_TOTAL_BOTTOM; + + dvxResizeWindow(ctx, win, win->w, newH); +} + + // ============================================================ // dvxFreeAccelTable // ============================================================ @@ -4024,6 +4059,7 @@ int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_ platformVideoEnumModes(enumModeCb, ctx); if (videoInit(&ctx->display, requestedW, requestedH, preferredBpp) != 0) { + dvxLog("App: video init failed for %dx%d@%dbpp", requestedW, requestedH, preferredBpp); return -1; } @@ -4831,6 +4867,18 @@ bool dvxUpdate(AppContextT *ctx) { refreshMinimizedIcons(ctx); } + // Auto-paint windows that haven't had their first paint yet. + // This fires one frame after creation, giving the app time to + // add all widgets before the first onPaint. + for (int32_t i = 0; i < ctx->stack.count; i++) { + WindowT *win = ctx->stack.windows[i]; + + if (win->needsPaint && win->onPaint) { + win->needsPaint = false; + dvxInvalidateWindow(ctx, win); + } + } + if (ctx->dirty.count > 0) { compositeAndFlush(ctx); } else if (ctx->idleCallback) { diff --git a/core/dvxApp.h b/core/dvxApp.h index 5f7b768..85445c1 100644 --- a/core/dvxApp.h +++ b/core/dvxApp.h @@ -13,6 +13,7 @@ #ifndef DVX_APP_H #define DVX_APP_H +#include "dvxCursor.h" #include "dvxTypes.h" #include "dvxVideo.h" #include "dvxDraw.h" @@ -42,7 +43,7 @@ typedef struct AppContextT { PopupStateT popup; SysMenuStateT sysMenu; KbMoveResizeT kbMoveResize; - CursorT cursors[6]; // indexed by CURSOR_xxx + CursorT cursors[CURSOR_COUNT]; // indexed by CURSOR_xxx int32_t cursorId; // active cursor shape uint32_t cursorFg; // pre-packed cursor colors uint32_t cursorBg; @@ -206,6 +207,9 @@ void dvxDestroyWindow(AppContextT *ctx, WindowT *win); // (plus chrome). Used for dialog boxes and other fixed-layout windows // where the window should shrink-wrap its content. void dvxFitWindow(AppContextT *ctx, WindowT *win); +void dvxFitWindowW(AppContextT *ctx, WindowT *win); +void dvxFitWindowH(AppContextT *ctx, WindowT *win); +void dvxResizeWindow(AppContextT *ctx, WindowT *win, int32_t newW, int32_t newH); // Mark a sub-region of a window's content area as needing repaint. The // coordinates are relative to the content area, not the screen. The diff --git a/core/dvxCursor.h b/core/dvxCursor.h index 68a2660..a9adf5e 100644 --- a/core/dvxCursor.h +++ b/core/dvxCursor.h @@ -29,7 +29,8 @@ #define CURSOR_RESIZE_DIAG_NWSE 3 // NW-SE diagonal (top-left / bottom-right) #define CURSOR_RESIZE_DIAG_NESW 4 // NE-SW diagonal (top-right / bottom-left) #define CURSOR_BUSY 5 // hourglass (wait) -#define CURSOR_COUNT 6 +#define CURSOR_CROSSHAIR 6 // crosshair (+) for placement +#define CURSOR_COUNT 7 // ============================================================ // Standard arrow cursor, 16x16 @@ -354,6 +355,50 @@ static const uint16_t cursorBusyXor[16] = { }; +// ============================================================ +// Crosshair cursor (+), 16x16 +// ============================================================ +// Hot spot at center (7, 7) + +static const uint16_t cursorCrosshairAnd[16] = { + 0xFFFF, // row 0 + 0xFC3F, // 1111110000111111 row 1 black outline top + 0xFC3F, // 1111110000111111 row 2 + 0xFC3F, // 1111110000111111 row 3 + 0xFC3F, // 1111110000111111 row 4 + 0xFC3F, // 1111110000111111 row 5 + 0x0000, // 0000000000000000 row 6 outline + horizontal bar + 0x0000, // 0000000000000000 row 7 horizontal bar center + 0x0000, // 0000000000000000 row 8 outline + horizontal bar + 0xFC3F, // 1111110000111111 row 9 + 0xFC3F, // 1111110000111111 row 10 + 0xFC3F, // 1111110000111111 row 11 + 0xFC3F, // 1111110000111111 row 12 + 0xFC3F, // 1111110000111111 row 13 black outline bottom + 0xFFFF, // row 14 + 0xFFFF // row 15 +}; + +static const uint16_t cursorCrosshairXor[16] = { + 0x0000, // row 0 + 0x0180, // 0000000110000000 row 1 white inner + 0x0180, // row 2 + 0x0180, // row 3 + 0x0180, // row 4 + 0x0180, // row 5 + 0x0180, // 0000000110000000 row 6 white inner (black on sides) + 0x3FFC, // 0011111111111100 row 7 white horizontal (black edges) + 0x0180, // 0000000110000000 row 8 white inner (black on sides) + 0x0180, // row 9 + 0x0180, // row 10 + 0x0180, // row 11 + 0x0180, // row 12 + 0x0180, // row 13 white inner + 0x0000, // row 14 + 0x0000 // row 15 +}; + + // ============================================================ // Cursor table // ============================================================ @@ -365,6 +410,7 @@ static const CursorT dvxCursors[CURSOR_COUNT] = { { 16, 16, 7, 7, cursorResizeDiagNWSEAnd, cursorResizeDiagNWSEXor }, // CURSOR_RESIZE_DIAG_NWSE { 16, 16, 7, 7, cursorResizeDiagNESWAnd, cursorResizeDiagNESWXor }, // CURSOR_RESIZE_DIAG_NESW { 16, 16, 7, 7, cursorBusyAnd, cursorBusyXor }, // CURSOR_BUSY + { 16, 16, 7, 7, cursorCrosshairAnd, cursorCrosshairXor }, // CURSOR_CROSSHAIR }; // Legacy alias -- kept for backward compatibility with code that predates diff --git a/core/dvxDialog.c b/core/dvxDialog.c index 54abdb2..93b866b 100644 --- a/core/dvxDialog.c +++ b/core/dvxDialog.c @@ -30,6 +30,7 @@ #include "../widgets/widgetLabel.h" #include "../widgets/widgetListBox.h" #include "../widgets/widgetSpacer.h" +#include "../widgets/widgetSpinner.h" #include "../widgets/widgetTextInput.h" #include @@ -758,6 +759,120 @@ static void ibOnClose(WindowT *win) { } +// ============================================================ +// dvxIntInputBox +// ============================================================ + +static struct { + bool done; + bool accepted; + int32_t *outVal; + WidgetT *spinner; +} sIntBox; + +static void iibOnOk(WidgetT *w) { + (void)w; + + if (sIntBox.spinner && sIntBox.outVal) { + *sIntBox.outVal = wgtSpinnerGetValue(sIntBox.spinner); + } + + sIntBox.accepted = true; + sIntBox.done = true; +} + +static void iibOnCancel(WidgetT *w) { + (void)w; + sIntBox.accepted = false; + sIntBox.done = true; +} + +static void iibOnClose(WindowT *win) { + (void)win; + sIntBox.accepted = false; + sIntBox.done = true; +} + +bool dvxIntInputBox(AppContextT *ctx, const char *title, const char *prompt, int32_t defaultVal, int32_t minVal, int32_t maxVal, int32_t step, int32_t *outVal) { + if (!ctx || !outVal) { + return false; + } + + int32_t promptH = 0; + + if (prompt && prompt[0]) { + int32_t textMaxW = IB_DIALOG_WIDTH - IB_PADDING * 2; + promptH = wordWrapHeight(&ctx->font, prompt, textMaxW); + } + + int32_t contentH = IB_PADDING + promptH + IB_PADDING + ctx->font.charHeight + 8 + IB_PADDING + BUTTON_HEIGHT + IB_PADDING; + int32_t contentW = IB_DIALOG_WIDTH; + + int32_t winX = (ctx->display.width - contentW) / 2 - CHROME_TOTAL_SIDE; + int32_t winY = (ctx->display.height - contentH) / 2 - CHROME_TOTAL_TOP; + + WindowT *win = dvxCreateWindow(ctx, title ? title : "Input", + winX, winY, + contentW + CHROME_TOTAL_SIDE * 2, + contentH + CHROME_TOTAL_TOP + CHROME_TOTAL_BOTTOM, + false); + + if (!win) { + return false; + } + + win->modal = true; + win->onClose = iibOnClose; + win->maxW = win->w; + win->maxH = win->h; + + sIntBox.done = false; + sIntBox.accepted = false; + sIntBox.outVal = outVal; + sIntBox.spinner = NULL; + + WidgetT *root = wgtInitWindow(ctx, win); + + if (root) { + if (prompt && prompt[0]) { + wgtLabel(root, prompt); + } + + WidgetT *spinner = wgtSpinner(root, minVal, maxVal, step); + wgtSpinnerSetValue(spinner, defaultVal); + sIntBox.spinner = spinner; + + WidgetT *btnRow = wgtHBox(root); + btnRow->align = AlignCenterE; + + WidgetT *okBtn = wgtButton(btnRow, "&OK"); + okBtn->minW = wgtPixels(BUTTON_WIDTH); + okBtn->minH = wgtPixels(BUTTON_HEIGHT); + okBtn->onClick = iibOnOk; + + WidgetT *cancelBtn = wgtButton(btnRow, "&Cancel"); + cancelBtn->minW = wgtPixels(BUTTON_WIDTH); + cancelBtn->minH = wgtPixels(BUTTON_HEIGHT); + cancelBtn->onClick = iibOnCancel; + } + + dvxFitWindow(ctx, win); + + WindowT *prevModal = ctx->modalWindow; + ctx->modalWindow = win; + + while (!sIntBox.done && ctx->running) { + dvxUpdate(ctx); + } + + ctx->modalWindow = prevModal; + dvxDestroyWindow(ctx, win); + sIntBox.spinner = NULL; + + return sIntBox.accepted; +} + + // ============================================================ // ibOnOk // ============================================================ @@ -993,6 +1108,7 @@ static void fdLoadDir(void) { sFd.entryIsDir = (bool *)realloc(sFd.entryIsDir, (idx + 1) * sizeof(bool)); if (!sFd.entryNames || !sFd.entryIsDir) { + dvxLog("Dialog: failed to realloc entry arrays"); break; } @@ -1021,6 +1137,7 @@ static void fdLoadDir(void) { int32_t *sortIdx = (int32_t *)malloc(sFd.entryCount * sizeof(int32_t)); if (!sortIdx) { + dvxLog("Dialog: failed to allocate sort index"); return; } @@ -1034,6 +1151,10 @@ static void fdLoadDir(void) { char **tmpNames = (char **)malloc(sFd.entryCount * sizeof(char *)); bool *tmpIsDir = (bool *)malloc(sFd.entryCount * sizeof(bool)); + if (!tmpNames || !tmpIsDir) { + dvxLog("Dialog: failed to allocate temp sort arrays"); + } + if (tmpNames && tmpIsDir) { for (int32_t i = 0; i < sFd.entryCount; i++) { tmpNames[i] = sFd.entryNames[sortIdx[i]]; @@ -1051,6 +1172,11 @@ static void fdLoadDir(void) { // Build listItems pointer array for the listbox sFd.listItems = (const char **)realloc(sFd.listItems, sFd.entryCount * sizeof(const char *)); + if (!sFd.listItems) { + dvxLog("Dialog: failed to realloc listItems"); + return; + } + for (int32_t i = 0; i < sFd.entryCount; i++) { sFd.listItems[i] = sFd.entryNames[i]; } diff --git a/core/dvxDialog.h b/core/dvxDialog.h index d1390db..c507253 100644 --- a/core/dvxDialog.h +++ b/core/dvxDialog.h @@ -83,4 +83,8 @@ bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const cha // false if cancelled or closed. defaultText may be NULL. bool dvxInputBox(AppContextT *ctx, const char *title, const char *prompt, const char *defaultText, char *outBuf, int32_t outBufSize); +// Display a modal integer input box with a spinner (up/down arrows). +// Returns true if the user clicked OK, false if cancelled. +bool dvxIntInputBox(AppContextT *ctx, const char *title, const char *prompt, int32_t defaultVal, int32_t minVal, int32_t maxVal, int32_t step, int32_t *outVal); + #endif // DVX_DIALOG_H diff --git a/core/dvxTypes.h b/core/dvxTypes.h index 27147c7..f12cfe1 100644 --- a/core/dvxTypes.h +++ b/core/dvxTypes.h @@ -507,6 +507,7 @@ typedef struct WindowT { bool resizable; bool modal; bool contentDirty; // true when contentBuf has changed since last icon refresh + bool needsPaint; // true until first onPaint call (auto-paint on next frame) int32_t maxW; // maximum width (WM_MAX_FROM_SCREEN = use screen width) int32_t maxH; // maximum height (WM_MAX_FROM_SCREEN = use screen height) // Pre-maximize geometry is saved so wmRestore() can put the window diff --git a/core/dvxWidget.h b/core/dvxWidget.h index 316e946..43cba4b 100644 --- a/core/dvxWidget.h +++ b/core/dvxWidget.h @@ -581,15 +581,35 @@ typedef struct { // Widget descriptors only need to list EXTRA events beyond these. // Click, DblClick, Change, GotFocus, LostFocus +// Create function signature types for design-time / runtime instantiation. +// The create function is always the first slot in the widget API struct. +typedef enum { + WGT_CREATE_PARENT = 0, // fn(parent) + WGT_CREATE_PARENT_TEXT, // fn(parent, const char *text) + WGT_CREATE_PARENT_INT, // fn(parent, int32_t) + WGT_CREATE_PARENT_INT_INT, // fn(parent, int32_t, int32_t) + WGT_CREATE_PARENT_INT_INT_INT, // fn(parent, int32_t, int32_t, int32_t) + WGT_CREATE_PARENT_INT_BOOL, // fn(parent, int32_t, bool) + WGT_CREATE_PARENT_BOOL, // fn(parent, bool) + WGT_CREATE_PARENT_DATA, // fn(parent, data, w, h, pitch) -- not auto-creatable +} WgtCreateSigE; + +#define WGT_MAX_CREATE_ARGS 3 + // Widget interface descriptor (registered by each .wgt) typedef struct { - const char *basName; // VB-style name (e.g. "CommandButton"), or NULL - const WgtPropDescT *props; // type-specific properties + const char *basName; // VB-style name (e.g. "CommandButton"), or NULL + const WgtPropDescT *props; // type-specific properties int32_t propCount; - const WgtMethodDescT *methods; // type-specific methods + const WgtMethodDescT *methods; // type-specific methods int32_t methodCount; - const WgtEventDescT *events; // extra events beyond common set + const WgtEventDescT *events; // extra events beyond common set int32_t eventCount; + uint8_t createSig; // WgtCreateSigE: how to call create fn + int32_t createArgs[WGT_MAX_CREATE_ARGS]; // default numeric args + bool isContainer; // can hold child widgets + const char *defaultEvent; // default event name (e.g. "Click") + const char *namePrefix; // auto-name prefix (NULL = use basName) } WgtIfaceT; // Register/retrieve interface descriptors by widget type name. @@ -600,4 +620,16 @@ const WgtIfaceT *wgtGetIface(const char *name); // Returns NULL if no widget has that basName. Case-insensitive. const char *wgtFindByBasName(const char *basName); +// Enumerate all registered widget interfaces. +int32_t wgtIfaceCount(void); +const WgtIfaceT *wgtIfaceAt(int32_t idx, const char **outName); + +// Get/set the .wgt file path for a registered widget (set by loader). +const char *wgtIfaceGetPath(const char *name); +void wgtIfaceSetPath(const char *name, const char *path); + +// Get the 1-based index of this widget within its .wgt file. +// Used to construct suffixed resource names (e.g. "name-2", "icon16-2"). +int32_t wgtIfaceGetPathIndex(const char *name); + #endif // DVX_WIDGET_H diff --git a/core/dvxWm.c b/core/dvxWm.c index c77d6f5..8da6899 100644 --- a/core/dvxWm.c +++ b/core/dvxWm.c @@ -34,6 +34,7 @@ #include "dvxDraw.h" #include "dvxComp.h" #include "dvxWidget.h" +#include "dvxPlatform.h" #include "thirdparty/stb_image_wrap.h" #include @@ -847,6 +848,7 @@ ScrollbarT *wmAddHScrollbar(WindowT *win, int32_t min, int32_t max, int32_t page win->hScroll = (ScrollbarT *)malloc(sizeof(ScrollbarT)); if (!win->hScroll) { + dvxLog("WM: failed to allocate horizontal scrollbar"); return NULL; } @@ -934,6 +936,7 @@ MenuBarT *wmAddMenuBar(WindowT *win) { win->menuBar = (MenuBarT *)malloc(sizeof(MenuBarT)); if (!win->menuBar) { + dvxLog("WM: failed to allocate menu bar"); return NULL; } @@ -1076,6 +1079,7 @@ ScrollbarT *wmAddVScrollbar(WindowT *win, int32_t min, int32_t max, int32_t page win->vScroll = (ScrollbarT *)malloc(sizeof(ScrollbarT)); if (!win->vScroll) { + dvxLog("WM: failed to allocate vertical scrollbar"); return NULL; } @@ -1164,7 +1168,7 @@ WindowT *wmCreateWindow(WindowStackT *stack, DisplayT *d, const char *title, int win->contentBuf = (uint8_t *)malloc(bufSize); if (!win->contentBuf) { - fprintf(stderr, "WM: Failed to allocate content buffer (%ld bytes)\n", (long)bufSize); + dvxLog("WM: failed to allocate content buffer (%ld bytes)", (long)bufSize); free(win); return NULL; } @@ -1172,6 +1176,8 @@ WindowT *wmCreateWindow(WindowStackT *stack, DisplayT *d, const char *title, int memset(win->contentBuf, 0xFF, bufSize); } + win->needsPaint = true; + stack->windows[stack->count] = win; stack->count++; diff --git a/core/widgetClass.c b/core/widgetClass.c index 18fe7f9..cfbdffe 100644 --- a/core/widgetClass.c +++ b/core/widgetClass.c @@ -29,10 +29,16 @@ typedef struct { static ApiMapEntryT *sApiMap = NULL; -// stb_ds string hashmap: key = widget name, value = interface descriptor +// stb_ds string hashmap: key = widget name, value = iface + path typedef struct { - char *key; - const WgtIfaceT *value; + const WgtIfaceT *iface; + char path[260]; + int32_t pathIndex; // 1-based: 1 = first widget from this file, 2 = second, etc. +} IfaceEntryT; + +typedef struct { + char *key; + IfaceEntryT value; } IfaceMapEntryT; static IfaceMapEntryT *sIfaceMap = NULL; @@ -74,8 +80,8 @@ const char *wgtFindByBasName(const char *basName) { } for (size_t i = 0; i < shlenu(sIfaceMap); i++) { - if (sIfaceMap[i].value && sIfaceMap[i].value->basName) { - if (strcasecmp(sIfaceMap[i].value->basName, basName) == 0) { + if (sIfaceMap[i].value.iface && sIfaceMap[i].value.iface->basName) { + if (strcasecmp(sIfaceMap[i].value.iface->basName, basName) == 0) { return sIfaceMap[i].key; } } @@ -102,7 +108,97 @@ const WgtIfaceT *wgtGetIface(const char *name) { return NULL; } - return sIfaceMap[idx].value; + return sIfaceMap[idx].value.iface; +} + + +// ============================================================ +// wgtIfaceCount / wgtIfaceAt +// ============================================================ +// +// Enumerate all registered widget interfaces. + +int32_t wgtIfaceCount(void) { + return sIfaceMap ? (int32_t)shlenu(sIfaceMap) : 0; +} + + +const WgtIfaceT *wgtIfaceAt(int32_t idx, const char **outName) { + if (!sIfaceMap || idx < 0 || idx >= (int32_t)shlenu(sIfaceMap)) { + return NULL; + } + + if (outName) { + *outName = sIfaceMap[idx].key; + } + + return sIfaceMap[idx].value.iface; +} + + +// ============================================================ +// wgtIfaceGetPath +// ============================================================ + +const char *wgtIfaceGetPath(const char *name) { + if (!name || !sIfaceMap) { + return NULL; + } + + int32_t idx = shgeti(sIfaceMap, name); + + if (idx < 0) { + return NULL; + } + + return sIfaceMap[idx].value.path[0] ? sIfaceMap[idx].value.path : NULL; +} + + +// ============================================================ +// wgtIfaceGetPathIndex +// ============================================================ + +int32_t wgtIfaceGetPathIndex(const char *name) { + if (!name || !sIfaceMap) { + return 1; + } + + int32_t idx = shgeti(sIfaceMap, name); + + if (idx < 0) { + return 1; + } + + return sIfaceMap[idx].value.pathIndex > 0 ? sIfaceMap[idx].value.pathIndex : 1; +} + + +// ============================================================ +// wgtIfaceSetPath +// ============================================================ + +void wgtIfaceSetPath(const char *name, const char *path) { + if (!name || !path || !sIfaceMap) { + return; + } + + int32_t idx = shgeti(sIfaceMap, name); + + if (idx >= 0) { + snprintf(sIfaceMap[idx].value.path, sizeof(sIfaceMap[idx].value.path), "%s", path); + + // Count how many ifaces already have this path to assign a 1-based index + int32_t count = 0; + + for (size_t i = 0; i < shlenu(sIfaceMap); i++) { + if (sIfaceMap[i].value.path[0] && strcmp(sIfaceMap[i].value.path, path) == 0) { + count++; + } + } + + sIfaceMap[idx].value.pathIndex = count; + } } @@ -134,7 +230,10 @@ void wgtRegisterIface(const char *name, const WgtIfaceT *iface) { return; } - shput(sIfaceMap, name, iface); + IfaceEntryT entry; + memset(&entry, 0, sizeof(entry)); + entry.iface = iface; + shput(sIfaceMap, name, entry); } diff --git a/core/widgetCore.c b/core/widgetCore.c index 32405ea..4a6f464 100644 --- a/core/widgetCore.c +++ b/core/widgetCore.c @@ -19,6 +19,7 @@ // which doesn't map cleanly to an arena pattern. #include "dvxWidgetPlugin.h" +#include "dvxPlatform.h" #include "stb_ds_wrap.h" #include @@ -185,6 +186,7 @@ WidgetT *widgetAlloc(WidgetT *parent, int32_t type) { WidgetT *w = (WidgetT *)malloc(sizeof(WidgetT)); if (!w) { + dvxLog("Widget: failed to allocate widget (type=%d)", type); return NULL; } diff --git a/core/widgetLayout.c b/core/widgetLayout.c index 883427a..070b32b 100644 --- a/core/widgetLayout.c +++ b/core/widgetLayout.c @@ -292,6 +292,22 @@ void widgetLayoutBox(WidgetT *w, const BitmapFontT *font) { // Assign geometry int32_t crossSize = availCross; + // Apply preferred size as cross-axis cap (e.g. Width in a VBox). + // When set, the widget won't stretch beyond this on the cross axis. + if (horiz && c->prefH) { + int32_t prefPx = wgtResolveSize(c->prefH, innerH, font->charWidth); + + if (crossSize > prefPx) { + crossSize = prefPx; + } + } else if (!horiz && c->prefW) { + int32_t prefPx = wgtResolveSize(c->prefW, innerW, font->charWidth); + + if (crossSize > prefPx) { + crossSize = prefPx; + } + } + // Apply max size on cross axis if (horiz && c->maxH) { int32_t maxPx = wgtResolveSize(c->maxH, innerH, font->charWidth); @@ -322,21 +338,6 @@ void widgetLayoutBox(WidgetT *w, const BitmapFontT *font) { c->h = mainSize; } - // Apply preferred/max on cross axis - if (horiz && c->maxH) { - int32_t maxPx = wgtResolveSize(c->maxH, innerH, font->charWidth); - - if (c->h > maxPx) { - c->h = maxPx; - } - } else if (!horiz && c->maxW) { - int32_t maxPx = wgtResolveSize(c->maxW, innerW, font->charWidth); - - if (c->w > maxPx) { - c->w = maxPx; - } - } - pos += mainSize + gap; // Recurse into child containers diff --git a/core/widgetOps.c b/core/widgetOps.c index 42a0157..9346c34 100644 --- a/core/widgetOps.c +++ b/core/widgetOps.c @@ -12,6 +12,7 @@ // and wgtInvalidatePaint). #include "dvxWidgetPlugin.h" +#include "dvxPlatform.h" #include "../widgets/widgetBox.h" @@ -295,6 +296,7 @@ WidgetT *wgtInitWindow(AppContextT *ctx, WindowT *win) { WidgetT *root = dvxBoxApi() ? dvxBoxApi()->vBox(NULL) : NULL; if (!root) { + dvxLog("Widget: wgtInitWindow failed (%s)", dvxBoxApi() ? "vBox returned NULL" : "dvxBoxApi returned NULL"); return NULL; } diff --git a/dvxbasic/ide/ideProperties.c b/dvxbasic/ide/ideProperties.c deleted file mode 100644 index ec4ff0f..0000000 --- a/dvxbasic/ide/ideProperties.c +++ /dev/null @@ -1,276 +0,0 @@ -// ideProperties.c -- DVX BASIC form designer properties window -// -// A floating window with a TreeView listing all controls on the -// form (for selection and drag-reorder) and a read-only TextArea -// showing properties of the selected control. - -#include "ideProperties.h" -#include "dvxWm.h" -#include "widgetBox.h" -#include "widgetLabel.h" -#include "widgetTextInput.h" -#include "widgetTreeView.h" -#include "widgetSplitter.h" - -#include -#include -#include -#include - -// ============================================================ -// Constants -// ============================================================ - -#define PRP_WIN_W 200 -#define PRP_WIN_H 340 - -// ============================================================ -// Module state -// ============================================================ - -static DsgnStateT *sDs = NULL; -static WindowT *sPrpWin = NULL; -static WidgetT *sTree = NULL; -static WidgetT *sPropsArea = NULL; -static AppContextT *sPrpCtx = NULL; -static bool sUpdating = false; // prevent feedback loops - -// ============================================================ -// Prototypes -// ============================================================ - -static void onPrpClose(WindowT *win); -static void onTreeChange(WidgetT *w); - -// ============================================================ -// onPrpClose -// ============================================================ - -static void onPrpClose(WindowT *win) { - (void)win; -} - - -// ============================================================ -// onTreeChange -// ============================================================ -// -// Called when the TreeView selection changes or when items are -// reordered by drag. Sync the designer state from the tree. - -static void onTreeChange(WidgetT *w) { - (void)w; - - if (!sDs || !sDs->form || !sTree || sUpdating) { - return; - } - - // Find which tree item is selected and map to control index - int32_t count = (int32_t)arrlen(sDs->form->controls); - int32_t selIdx = -1; - - // Iterate tree items (children of sTree) to find the selected one - int32_t idx = 0; - - for (WidgetT *item = sTree->firstChild; item; item = item->nextSibling, idx++) { - if (wgtTreeItemIsSelected(item)) { - selIdx = idx; - break; - } - } - - // Check if the tree order differs from the design array - // (drag-reorder happened). If so, rebuild the array to match. - bool reordered = false; - idx = 0; - - for (WidgetT *item = sTree->firstChild; item; item = item->nextSibling, idx++) { - if (idx >= count) { - break; - } - - // Each tree item's userData stores the control name - const char *itemName = (const char *)item->userData; - - if (itemName && strcmp(itemName, sDs->form->controls[idx].name) != 0) { - reordered = true; - break; - } - } - - if (reordered) { - // Rebuild the controls array to match tree order - DsgnControlT *newArr = NULL; - idx = 0; - - for (WidgetT *item = sTree->firstChild; item; item = item->nextSibling) { - const char *itemName = (const char *)item->userData; - - if (!itemName) { - continue; - } - - // Find this control in the original array - for (int32_t i = 0; i < count; i++) { - if (strcmp(sDs->form->controls[i].name, itemName) == 0) { - arrput(newArr, sDs->form->controls[i]); - break; - } - } - } - - // Replace the controls array - arrfree(sDs->form->controls); - sDs->form->controls = newArr; - sDs->form->dirty = true; - - // Rebuild live widgets in new order - if (sDs->form->contentBox) { - sDs->form->contentBox->firstChild = NULL; - sDs->form->contentBox->lastChild = NULL; - - int32_t newCount = (int32_t)arrlen(sDs->form->controls); - - for (int32_t i = 0; i < newCount; i++) { - sDs->form->controls[i].widget = NULL; - } - - dsgnCreateWidgets(sDs, sDs->form->contentBox); - } - - // Update selection to match tree - count = (int32_t)arrlen(sDs->form->controls); - } - - // Update designer selection - if (selIdx != sDs->selectedIdx) { - sDs->selectedIdx = selIdx; - - if (sDs->formWin) { - dvxInvalidateWindow(sPrpCtx, sDs->formWin); - } - } - - // Refresh properties display - prpRefresh(sDs); -} - - -// ============================================================ -// prpCreate -// ============================================================ - -WindowT *prpCreate(AppContextT *ctx, DsgnStateT *ds) { - sDs = ds; - sPrpCtx = ctx; - - int32_t winX = ctx->display.width - PRP_WIN_W - 10; - WindowT *win = dvxCreateWindow(ctx, "Properties", winX, 30, PRP_WIN_W, PRP_WIN_H, false); - - if (!win) { - return NULL; - } - - win->onClose = onPrpClose; - sPrpWin = win; - - WidgetT *root = wgtInitWindow(ctx, win); - - // Splitter: tree on top, properties on bottom - WidgetT *splitter = wgtSplitter(root, false); - splitter->weight = 100; - - // Control tree (top pane) - sTree = wgtTreeView(splitter); - sTree->onChange = onTreeChange; - wgtTreeViewSetReorderable(sTree, true); - - // Properties text (bottom pane) - sPropsArea = wgtTextArea(splitter, 4096); - sPropsArea->readOnly = true; - - prpRefresh(ds); - return win; -} - - -// ============================================================ -// prpDestroy -// ============================================================ - -void prpDestroy(AppContextT *ctx, WindowT *win) { - if (win) { - dvxDestroyWindow(ctx, win); - } - - sPrpWin = NULL; - sTree = NULL; - sPropsArea = NULL; - sDs = NULL; -} - - -// ============================================================ -// prpRefresh -// ============================================================ - -void prpRefresh(DsgnStateT *ds) { - if (!ds || !ds->form) { - return; - } - - // Rebuild tree items - if (sTree) { - sUpdating = true; - - // Clear existing tree items - sTree->firstChild = NULL; - sTree->lastChild = NULL; - - int32_t count = (int32_t)arrlen(ds->form->controls); - - for (int32_t i = 0; i < count; i++) { - DsgnControlT *ctrl = &ds->form->controls[i]; - char label[128]; - snprintf(label, sizeof(label), "%s (%s)", ctrl->name, ctrl->typeName); - - WidgetT *item = wgtTreeItem(sTree, label); - item->userData = ctrl->name; // store name for reorder tracking - - if (i == ds->selectedIdx) { - wgtTreeItemSetSelected(item, true); - } - } - - sUpdating = false; - } - - // Update properties text - if (!sPropsArea) { - return; - } - - char buf[4096]; - int32_t pos = 0; - - if (ds->selectedIdx >= 0 && ds->selectedIdx < (int32_t)arrlen(ds->form->controls)) { - DsgnControlT *ctrl = &ds->form->controls[ds->selectedIdx]; - - pos += snprintf(buf + pos, sizeof(buf) - pos, "Name = %s\n", ctrl->name); - pos += snprintf(buf + pos, sizeof(buf) - pos, "Type = %s\n", ctrl->typeName); - pos += snprintf(buf + pos, sizeof(buf) - pos, "Width = %d\n", (int)ctrl->width); - pos += snprintf(buf + pos, sizeof(buf) - pos, "Height = %d\n", (int)ctrl->height); - pos += snprintf(buf + pos, sizeof(buf) - pos, "TabIndex = %d\n", (int)ctrl->tabIndex); - - for (int32_t i = 0; i < ctrl->propCount; i++) { - pos += snprintf(buf + pos, sizeof(buf) - pos, "%-8s = %s\n", ctrl->props[i].name, ctrl->props[i].value); - } - } else { - pos += snprintf(buf + pos, sizeof(buf) - pos, "%s (Form)\n", ds->form->name); - pos += snprintf(buf + pos, sizeof(buf) - pos, "Caption = %s\n", ds->form->caption); - pos += snprintf(buf + pos, sizeof(buf) - pos, "Width = %d\n", (int)ds->form->width); - pos += snprintf(buf + pos, sizeof(buf) - pos, "Height = %d\n", (int)ds->form->height); - } - - wgtSetText(sPropsArea, buf); -} diff --git a/dvxbasic/ide/ideToolbox.c b/dvxbasic/ide/ideToolbox.c deleted file mode 100644 index 104dc6a..0000000 --- a/dvxbasic/ide/ideToolbox.c +++ /dev/null @@ -1,110 +0,0 @@ -// ideToolbox.c -- DVX BASIC form designer toolbox window -// -// A small floating window with buttons for each control type. -// Clicking a tool sets the designer's activeTool. - -#include "ideToolbox.h" -#include "dvxWm.h" -#include "widgetBox.h" -#include "widgetButton.h" - -#include - -// ============================================================ -// Constants -// ============================================================ - -#define TBX_WIN_W 72 -#define TBX_WIN_H 320 -#define TBX_BTN_H 20 - -// ============================================================ -// Module state -// ============================================================ - -static DsgnStateT *sDs = NULL; - -// ============================================================ -// Tool labels (short names for buttons) -// ============================================================ - -static const char *sToolLabels[TOOL_COUNT] = { - "Pointer", - "Button", - "Label", - "TextBox", - "CheckBox", - "Option", - "Frame", - "ListBox", - "ComboBox", - "HScroll", - "VScroll", - "Timer", - "Picture", - "Image" -}; - -// ============================================================ -// Callbacks -// ============================================================ - -static void onToolClick(WidgetT *w) { - if (!sDs) { - return; - } - - int32_t toolIdx = (int32_t)(intptr_t)w->userData; - - if (toolIdx >= 0 && toolIdx < TOOL_COUNT) { - sDs->activeTool = (DsgnToolE)toolIdx; - sDs->mode = DSGN_IDLE; - } -} - - -static void onTbxClose(WindowT *win) { - // Don't allow closing the toolbox independently - (void)win; -} - - -// ============================================================ -// tbxCreate -// ============================================================ - -WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds) { - sDs = ds; - - WindowT *win = dvxCreateWindow(ctx, "Toolbox", 0, 30, TBX_WIN_W, TBX_WIN_H, false); - - if (!win) { - return NULL; - } - - win->onClose = onTbxClose; - - WidgetT *root = wgtInitWindow(ctx, win); - - for (int32_t i = 0; i < TOOL_COUNT; i++) { - WidgetT *btn = wgtButton(root, sToolLabels[i]); - btn->onClick = onToolClick; - btn->userData = (void *)(intptr_t)i; - } - - dvxFitWindow(ctx, win); - return win; -} - - -// ============================================================ -// tbxDestroy -// ============================================================ - -void tbxDestroy(AppContextT *ctx, WindowT *win) { - if (win) { - dvxDestroyWindow(ctx, win); - } - - sDs = NULL; -} diff --git a/loader/loaderMain.c b/loader/loaderMain.c index 19efeea..a8c2aea 100644 --- a/loader/loaderMain.c +++ b/loader/loaderMain.c @@ -295,6 +295,32 @@ static void loadInOrder(ModuleT *mods) { if (regFn) { regFn(); + + // Record the .wgt path for any newly registered ifaces + typedef int32_t (*IfaceCountFnT)(void); + typedef const void *(*IfaceAtFnT)(int32_t, const char **); + typedef const char *(*IfaceGetPathFnT)(const char *); + typedef void (*IfaceSetPathFnT)(const char *, const char *); + + IfaceCountFnT countFn = (IfaceCountFnT)dlsym(NULL, "_wgtIfaceCount"); + IfaceAtFnT atFn = (IfaceAtFnT)dlsym(NULL, "_wgtIfaceAt"); + IfaceGetPathFnT getPathFn = (IfaceGetPathFnT)dlsym(NULL, "_wgtIfaceGetPath"); + IfaceSetPathFnT setPathFn = (IfaceSetPathFnT)dlsym(NULL, "_wgtIfaceSetPath"); + + if (countFn && atFn && getPathFn && setPathFn) { + int32_t ic = countFn(); + + for (int32_t k = 0; k < ic; k++) { + const char *ifaceName = NULL; + atFn(k, &ifaceName); + + if (ifaceName && !getPathFn(ifaceName)) { + setPathFn(ifaceName, mods[i].path); + } + } + } + } else if (strstr(mods[i].path, ".wgt") || strstr(mods[i].path, ".WGT")) { + dvxLog(" No _wgtRegister in %s", mods[i].baseName); } mods[i].loaded = true; @@ -332,11 +358,14 @@ static void logAndReadDeps(ModuleT *mods) { readDeps(&mods[i]); if (arrlen(mods[i].deps) > 0) { - dvxLog(" %s deps:", mods[i].baseName); + char line[512]; + int32_t pos = snprintf(line, sizeof(line), " %s deps:", mods[i].baseName); for (int32_t d = 0; d < arrlen(mods[i].deps); d++) { - dvxLog(" %s", mods[i].deps[d]); + pos += snprintf(line + pos, sizeof(line) - pos, " %s", mods[i].deps[d]); } + + dvxLog("%s", line); } } } diff --git a/shell/shellApp.c b/shell/shellApp.c index 6651a41..26068b7 100644 --- a/shell/shellApp.c +++ b/shell/shellApp.c @@ -418,6 +418,7 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) { app->dxeCtx = (DxeAppContextT *)calloc(1, sizeof(DxeAppContextT)); if (!app->dxeCtx) { + dvxLog("Shell: failed to allocate app context for %s", app->name); ctx->currentAppId = 0; dlclose(handle); app->state = AppStateFreeE; diff --git a/shell/shellMain.c b/shell/shellMain.c index fa29141..6327861 100644 --- a/shell/shellMain.c +++ b/shell/shellMain.c @@ -388,6 +388,9 @@ int shellMain(int argc, char *argv[]) { dvxLog("Recovering from crash, killing app %ld", (long)sCtx.currentAppId); + // Clear busy cursor so the fault dialog is interactive + dvxSetBusy(&sCtx, false); + if (sCtx.currentAppId > 0) { ShellAppT *app = shellGetApp(sCtx.currentAppId); @@ -396,12 +399,10 @@ int shellMain(int argc, char *argv[]) { snprintf(msg, sizeof(msg), "'%s' has caused a fault and will be terminated.", app->name); shellForceKillApp(&sCtx, app); sCtx.currentAppId = 0; - sCtx.currentAppId = 0; dvxMessageBox(&sCtx, "Application Error", msg, MB_OK | MB_ICONERROR); } } - sCtx.currentAppId = 0; sCtx.currentAppId = 0; sCrashSignal = 0; shellDesktopUpdate(); diff --git a/widgets/Makefile b/widgets/Makefile index c2bdf6b..a3bf403 100644 --- a/widgets/Makefile +++ b/widgets/Makefile @@ -1,118 +1,78 @@ # DVX Widget Modules Makefile for DJGPP cross-compilation # -# Builds individual .wgt modules from widget source files. +# Each widget lives in its own subdirectory with its .c, .res, and .bmp. # Each .wgt is a DXE loaded by the loader at startup. DJGPP_PREFIX = $(HOME)/djgpp/djgpp CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc DXE3GEN = PATH=$(DJGPP_PREFIX)/bin:$(PATH) DJDIR=$(DJGPP_PREFIX)/i586-pc-msdosdjgpp $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/dxe3gen CFLAGS = -O2 -Wall -Wextra -Wno-type-limits -Wno-sign-compare -march=i486 -mtune=i586 -I../core -I../core/platform -I../tasks -I../core/thirdparty +DVXRES = ../bin/dvxres OBJDIR = ../obj/widgets WGTDIR = ../bin/widgets -SRCS = widgetAnsiTerm.c \ - widgetBox.c \ - widgetButton.c \ - widgetCanvas.c \ - widgetCheckbox.c \ - widgetComboBox.c \ - widgetDropdown.c \ - widgetImage.c \ - widgetImageButton.c \ - widgetLabel.c \ - widgetListBox.c \ - widgetListView.c \ - widgetProgressBar.c \ - widgetRadio.c \ - widgetScrollPane.c \ - widgetSeparator.c \ - widgetSlider.c \ - widgetSpacer.c \ - widgetSpinner.c \ - widgetSplitter.c \ - widgetStatusBar.c \ - widgetTabControl.c \ - widgetTextInput.c \ - widgetTimer.c \ - widgetToolbar.c \ - widgetTreeView.c +# Widget name -> directory, source, short name for .wgt/.res +# Format: wgtShortName:dirName:srcFile:resFile +WIDGETS = \ + box:box:widgetBox:box \ + button:button:widgetButton:button \ + canvas:canvas:widgetCanvas:canvas \ + checkbox:checkbox:widgetCheckbox:checkbox \ + combobox:comboBox:widgetComboBox:combobox \ + dropdown:dropdown:widgetDropdown:dropdown \ + imgbtn:imageButton:widgetImageButton:imgbtn \ + image:image:widgetImage:image \ + label:label:widgetLabel:label \ + listbox:listBox:widgetListBox:listbox \ + listview:listView:widgetListView:listview \ + progress:progressBar:widgetProgressBar:progress \ + radio:radio:widgetRadio:radio \ + scrlpane:scrollPane:widgetScrollPane:scrlpane \ + separatr:separator:widgetSeparator:separatr \ + slider:slider:widgetSlider:slider \ + spacer:spacer:widgetSpacer:spacer \ + spinner:spinner:widgetSpinner:spinner \ + splitter:splitter:widgetSplitter:splitter \ + statbar:statusBar:widgetStatusBar:statbar \ + tabctrl:tabControl:widgetTabControl:tabctrl \ + terminal:ansiTerm:widgetAnsiTerm:terminal \ + textinpt:textInput:widgetTextInput:textinpt \ + timer:timer:widgetTimer:timer \ + toolbar:toolbar:widgetToolbar:toolbar \ + treeview:treeView:widgetTreeView:treeview -OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS)) - -WGT_MODS = $(WGTDIR)/box.wgt \ - $(WGTDIR)/button.wgt \ - $(WGTDIR)/canvas.wgt \ - $(WGTDIR)/checkbox.wgt \ - $(WGTDIR)/combobox.wgt \ - $(WGTDIR)/dropdown.wgt \ - $(WGTDIR)/imgbtn.wgt \ - $(WGTDIR)/image.wgt \ - $(WGTDIR)/label.wgt \ - $(WGTDIR)/listbox.wgt \ - $(WGTDIR)/listview.wgt \ - $(WGTDIR)/progress.wgt \ - $(WGTDIR)/radio.wgt \ - $(WGTDIR)/scrlpane.wgt \ - $(WGTDIR)/separatr.wgt \ - $(WGTDIR)/slider.wgt \ - $(WGTDIR)/spacer.wgt \ - $(WGTDIR)/spinner.wgt \ - $(WGTDIR)/splitter.wgt \ - $(WGTDIR)/statbar.wgt \ - $(WGTDIR)/tabctrl.wgt \ - $(WGTDIR)/terminal.wgt \ - $(WGTDIR)/textinpt.wgt \ - $(WGTDIR)/timer.wgt \ - $(WGTDIR)/toolbar.wgt \ - $(WGTDIR)/treeview.wgt - -.PHONY: all clean +# Extract lists +WGT_NAMES = $(foreach w,$(WIDGETS),$(word 1,$(subst :, ,$w))) +WGT_MODS = $(WGT_NAMES:%=$(WGTDIR)/%.wgt) +OBJS = $(foreach w,$(WIDGETS),$(OBJDIR)/$(word 3,$(subst :, ,$w)).o) DEPFILES = textinpt combobox spinner terminal listbox dropdown listview treeview WGT_DEPS = $(DEPFILES:%=$(WGTDIR)/%.dep) +.PHONY: all clean + all: $(WGT_MODS) $(WGT_DEPS) $(WGTDIR)/%.dep: ../config/%.dep | $(WGTDIR) sed 's/$$/\r/' $< > $@ -# Generic widget module rule: export only the registration function -$(WGTDIR)/%.wgt: | $(WGTDIR) - $(DXE3GEN) -o $(WGTDIR)/$*.dxe -E _wgtRegister -U $< - mv $(WGTDIR)/$*.dxe $@ +# Compile: source is in subdirectory +WIDGET_DEPS_H = ../core/dvxWidgetPlugin.h ../core/dvxWidget.h ../core/dvxTypes.h ../core/dvxApp.h ../core/dvxDraw.h ../core/dvxWm.h ../core/dvxVideo.h ../core/platform/dvxPlatform.h -# Map .wgt name to .o file -$(WGTDIR)/box.wgt: $(OBJDIR)/widgetBox.o -$(WGTDIR)/button.wgt: $(OBJDIR)/widgetButton.o -$(WGTDIR)/canvas.wgt: $(OBJDIR)/widgetCanvas.o -$(WGTDIR)/checkbox.wgt: $(OBJDIR)/widgetCheckbox.o -$(WGTDIR)/combobox.wgt: $(OBJDIR)/widgetComboBox.o -$(WGTDIR)/dropdown.wgt: $(OBJDIR)/widgetDropdown.o -$(WGTDIR)/imgbtn.wgt: $(OBJDIR)/widgetImageButton.o -$(WGTDIR)/image.wgt: $(OBJDIR)/widgetImage.o -$(WGTDIR)/label.wgt: $(OBJDIR)/widgetLabel.o -$(WGTDIR)/listbox.wgt: $(OBJDIR)/widgetListBox.o -$(WGTDIR)/listview.wgt: $(OBJDIR)/widgetListView.o -$(WGTDIR)/progress.wgt: $(OBJDIR)/widgetProgressBar.o -$(WGTDIR)/radio.wgt: $(OBJDIR)/widgetRadio.o -$(WGTDIR)/scrlpane.wgt: $(OBJDIR)/widgetScrollPane.o -$(WGTDIR)/separatr.wgt: $(OBJDIR)/widgetSeparator.o -$(WGTDIR)/slider.wgt: $(OBJDIR)/widgetSlider.o -$(WGTDIR)/spacer.wgt: $(OBJDIR)/widgetSpacer.o -$(WGTDIR)/spinner.wgt: $(OBJDIR)/widgetSpinner.o -$(WGTDIR)/splitter.wgt: $(OBJDIR)/widgetSplitter.o -$(WGTDIR)/statbar.wgt: $(OBJDIR)/widgetStatusBar.o -$(WGTDIR)/tabctrl.wgt: $(OBJDIR)/widgetTabControl.o -$(WGTDIR)/terminal.wgt: $(OBJDIR)/widgetAnsiTerm.o -$(WGTDIR)/textinpt.wgt: $(OBJDIR)/widgetTextInput.o -$(WGTDIR)/timer.wgt: $(OBJDIR)/widgetTimer.o -$(WGTDIR)/toolbar.wgt: $(OBJDIR)/widgetToolbar.o -$(WGTDIR)/treeview.wgt: $(OBJDIR)/widgetTreeView.o +define WIDGET_RULES +$(OBJDIR)/$(word 3,$(subst :, ,$1)).o: $(word 2,$(subst :, ,$1))/$(word 3,$(subst :, ,$1)).c $$(WIDGET_DEPS_H) | $(OBJDIR) + $$(CC) $$(CFLAGS) -c -o $$@ $$< -# Compile -$(OBJDIR)/%.o: %.c | $(OBJDIR) - $(CC) $(CFLAGS) -c -o $@ $< +$(WGTDIR)/$(word 1,$(subst :, ,$1)).wgt: $(OBJDIR)/$(word 3,$(subst :, ,$1)).o | $(WGTDIR) + $$(DXE3GEN) -o $(WGTDIR)/$(word 1,$(subst :, ,$1)).dxe -E _wgtRegister -U $$< + mv $(WGTDIR)/$(word 1,$(subst :, ,$1)).dxe $$@ + @if [ -f $(word 2,$(subst :, ,$1))/$(word 4,$(subst :, ,$1)).res ]; then \ + cd $(word 2,$(subst :, ,$1)) && ../$(DVXRES) build ../$$@ $(word 4,$(subst :, ,$1)).res; \ + fi +endef + +$(foreach w,$(WIDGETS),$(eval $(call WIDGET_RULES,$w))) $(OBJDIR): mkdir -p $(OBJDIR) @@ -120,35 +80,6 @@ $(OBJDIR): $(WGTDIR): mkdir -p $(WGTDIR) -# Dependencies -WIDGET_DEPS = ../core/dvxWidgetPlugin.h ../core/dvxWidget.h ../core/dvxTypes.h ../core/dvxApp.h ../core/dvxDraw.h ../core/dvxWm.h ../core/dvxVideo.h -$(OBJDIR)/widgetAnsiTerm.o: widgetAnsiTerm.c $(WIDGET_DEPS) -$(OBJDIR)/widgetBox.o: widgetBox.c $(WIDGET_DEPS) -$(OBJDIR)/widgetButton.o: widgetButton.c $(WIDGET_DEPS) -$(OBJDIR)/widgetCanvas.o: widgetCanvas.c $(WIDGET_DEPS) ../core/thirdparty/stb_image.h ../core/thirdparty/stb_image_write.h -$(OBJDIR)/widgetCheckbox.o: widgetCheckbox.c $(WIDGET_DEPS) -$(OBJDIR)/widgetComboBox.o: widgetComboBox.c $(WIDGET_DEPS) -$(OBJDIR)/widgetDropdown.o: widgetDropdown.c $(WIDGET_DEPS) -$(OBJDIR)/widgetImage.o: widgetImage.c $(WIDGET_DEPS) ../core/thirdparty/stb_image.h -$(OBJDIR)/widgetImageButton.o: widgetImageButton.c $(WIDGET_DEPS) -$(OBJDIR)/widgetLabel.o: widgetLabel.c $(WIDGET_DEPS) -$(OBJDIR)/widgetListBox.o: widgetListBox.c $(WIDGET_DEPS) -$(OBJDIR)/widgetListView.o: widgetListView.c $(WIDGET_DEPS) -$(OBJDIR)/widgetProgressBar.o: widgetProgressBar.c $(WIDGET_DEPS) -$(OBJDIR)/widgetRadio.o: widgetRadio.c $(WIDGET_DEPS) -$(OBJDIR)/widgetScrollPane.o: widgetScrollPane.c $(WIDGET_DEPS) -$(OBJDIR)/widgetSeparator.o: widgetSeparator.c $(WIDGET_DEPS) -$(OBJDIR)/widgetSlider.o: widgetSlider.c $(WIDGET_DEPS) -$(OBJDIR)/widgetSpacer.o: widgetSpacer.c $(WIDGET_DEPS) -$(OBJDIR)/widgetSpinner.o: widgetSpinner.c $(WIDGET_DEPS) -$(OBJDIR)/widgetSplitter.o: widgetSplitter.c $(WIDGET_DEPS) -$(OBJDIR)/widgetStatusBar.o: widgetStatusBar.c $(WIDGET_DEPS) -$(OBJDIR)/widgetTabControl.o: widgetTabControl.c $(WIDGET_DEPS) -$(OBJDIR)/widgetTextInput.o: widgetTextInput.c $(WIDGET_DEPS) -$(OBJDIR)/widgetTimer.o: widgetTimer.c $(WIDGET_DEPS) -$(OBJDIR)/widgetToolbar.o: widgetToolbar.c $(WIDGET_DEPS) -$(OBJDIR)/widgetTreeView.o: widgetTreeView.c $(WIDGET_DEPS) - clean: rm -f $(OBJS) $(WGT_MODS) $(WGT_DEPS) -rmdir $(WGTDIR) 2>/dev/null diff --git a/widgets/ansiTerm/terminal.bmp b/widgets/ansiTerm/terminal.bmp new file mode 100644 index 0000000..e86c25a --- /dev/null +++ b/widgets/ansiTerm/terminal.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:01e1ac43ec156f06fafcb9b97a03a65913131da28736736226c041a91e456e60 +size 822 diff --git a/widgets/ansiTerm/terminal.res b/widgets/ansiTerm/terminal.res new file mode 100644 index 0000000..1f32996 --- /dev/null +++ b/widgets/ansiTerm/terminal.res @@ -0,0 +1,5 @@ +icon16 icon terminal.bmp +name text "Terminal" +author text "DVX Project" +description text "ANSI terminal emulator widget" +version text "1.0" diff --git a/widgets/widgetAnsiTerm.c b/widgets/ansiTerm/widgetAnsiTerm.c similarity index 99% rename from widgets/widgetAnsiTerm.c rename to widgets/ansiTerm/widgetAnsiTerm.c index e61b464..daefae2 100644 --- a/widgets/widgetAnsiTerm.c +++ b/widgets/ansiTerm/widgetAnsiTerm.c @@ -1081,13 +1081,15 @@ static const WgtMethodDescT sMethods[] = { }; static const WgtIfaceT sIface = { - .basName = "Terminal", - .props = sProps, - .propCount = 3, - .methods = sMethods, - .methodCount = 2, - .events = NULL, - .eventCount = 0 + .basName = "Terminal", + .props = sProps, + .propCount = 3, + .methods = sMethods, + .methodCount = 2, + .events = NULL, + .eventCount = 0, + .createSig = WGT_CREATE_PARENT_INT_INT, + .createArgs = { 80, 25 } }; void wgtRegister(void) { diff --git a/widgets/box/box.bmp b/widgets/box/box.bmp new file mode 100644 index 0000000..e67c85c --- /dev/null +++ b/widgets/box/box.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9776ab163e9676a9a3cc01841187bf5d8230d41108767c4f9b7652a65b362f4 +size 822 diff --git a/widgets/box/box.res b/widgets/box/box.res new file mode 100644 index 0000000..d0b9166 --- /dev/null +++ b/widgets/box/box.res @@ -0,0 +1,14 @@ +# box.res -- Resources for VBox, HBox, and Frame widgets +# +# VBox (first registered, uses unsuffixed names) +icon16 icon box.bmp +name text "VBox" +# HBox (second registered) +icon16-2 icon box.bmp +name-2 text "HBox" +# Frame (third registered) +icon16-3 icon box.bmp +name-3 text "Frame" +author text "DVX Project" +description text "VBox, HBox, and Frame container widgets" +version text "1.0" diff --git a/widgets/widgetBox.c b/widgets/box/widgetBox.c similarity index 80% rename from widgets/widgetBox.c rename to widgets/box/widgetBox.c index 680e2ea..bef74f0 100644 --- a/widgets/widgetBox.c +++ b/widgets/box/widgetBox.c @@ -205,20 +205,66 @@ static const struct { .frame = wgtFrame }; -static const WgtIfaceT sIface = { - .basName = "Frame", - .props = NULL, - .propCount = 0, - .methods = NULL, - .methodCount = 0, - .events = NULL, - .eventCount = 0 +// Separate API structs for each box type so the designer can +// create them independently. Each has create as the first slot. +static const struct { WidgetT *(*create)(WidgetT *); } sVBoxApi = { .create = wgtVBox }; +static const struct { WidgetT *(*create)(WidgetT *); } sHBoxApi = { .create = wgtHBox }; +static const struct { WidgetT *(*create)(WidgetT *, const char *); } sFrameApi = { .create = wgtFrame }; + +static const WgtIfaceT sIfaceVBox = { + .basName = "VBox", + .props = NULL, + .propCount = 0, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0, + .createSig = WGT_CREATE_PARENT, + .isContainer = true, + .defaultEvent = "Click" +}; + +static const WgtIfaceT sIfaceHBox = { + .basName = "HBox", + .props = NULL, + .propCount = 0, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0, + .createSig = WGT_CREATE_PARENT, + .isContainer = true, + .defaultEvent = "Click" +}; + +static const WgtIfaceT sIfaceFrame = { + .basName = "Frame", + .props = NULL, + .propCount = 0, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0, + .createSig = WGT_CREATE_PARENT_TEXT, + .isContainer = true, + .defaultEvent = "Click" }; void wgtRegister(void) { sVBoxTypeId = wgtRegisterClass(&sClassVBox); sHBoxTypeId = wgtRegisterClass(&sClassHBox); sFrameTypeId = wgtRegisterClass(&sClassFrame); + + // Original combined API (for widgetBox.h compatibility) wgtRegisterApi("box", &sApi); - wgtRegisterIface("box", &sIface); + + // Per-type APIs and ifaces (for designer/toolbox) + wgtRegisterApi("vbox", &sVBoxApi); + wgtRegisterIface("vbox", &sIfaceVBox); + + wgtRegisterApi("hbox", &sHBoxApi); + wgtRegisterIface("hbox", &sIfaceHBox); + + wgtRegisterApi("frame", &sFrameApi); + wgtRegisterIface("frame", &sIfaceFrame); } diff --git a/widgets/button/button.bmp b/widgets/button/button.bmp new file mode 100644 index 0000000..8fbf71b --- /dev/null +++ b/widgets/button/button.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5f904ed890b190a3540a2c03cf188d7de8572b4eb6aca14c70fbd24cc977717 +size 822 diff --git a/widgets/button/button.res b/widgets/button/button.res new file mode 100644 index 0000000..eb37152 --- /dev/null +++ b/widgets/button/button.res @@ -0,0 +1,5 @@ +icon16 icon button.bmp +name text "Button" +author text "DVX Project" +description text "Command button widget" +version text "1.0" diff --git a/widgets/widgetButton.c b/widgets/button/widgetButton.c similarity index 96% rename from widgets/widgetButton.c rename to widgets/button/widgetButton.c index 686fc48..07ddd38 100644 --- a/widgets/widgetButton.c +++ b/widgets/button/widgetButton.c @@ -262,13 +262,16 @@ static const struct { }; static const WgtIfaceT sIface = { - .basName = "CommandButton", - .props = NULL, - .propCount = 0, - .methods = NULL, - .methodCount = 0, - .events = NULL, - .eventCount = 0 + .basName = "CommandButton", + .props = NULL, + .propCount = 0, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0, + .createSig = WGT_CREATE_PARENT_TEXT, + .defaultEvent = "Click", + .namePrefix = "Command" }; void wgtRegister(void) { diff --git a/widgets/canvas/canvas.bmp b/widgets/canvas/canvas.bmp new file mode 100644 index 0000000..6cea4fb --- /dev/null +++ b/widgets/canvas/canvas.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f7c9e190321523549a3433b39b9e1df0421cf8117f99e1bd0d9d86cdd01f708d +size 822 diff --git a/widgets/canvas/canvas.res b/widgets/canvas/canvas.res new file mode 100644 index 0000000..580ac29 --- /dev/null +++ b/widgets/canvas/canvas.res @@ -0,0 +1,5 @@ +icon16 icon canvas.bmp +name text "Canvas" +author text "DVX Project" +description text "Pixel drawing surface" +version text "1.0" diff --git a/widgets/widgetCanvas.c b/widgets/canvas/widgetCanvas.c similarity index 98% rename from widgets/widgetCanvas.c rename to widgets/canvas/widgetCanvas.c index 1691d74..ec31250 100644 --- a/widgets/widgetCanvas.c +++ b/widgets/canvas/widgetCanvas.c @@ -931,13 +931,17 @@ static const WgtMethodDescT sMethods[] = { }; static const WgtIfaceT sIface = { - .basName = "PictureBox", - .props = NULL, - .propCount = 0, - .methods = sMethods, - .methodCount = 1, - .events = NULL, - .eventCount = 0 + .basName = "PictureBox", + .props = NULL, + .propCount = 0, + .methods = sMethods, + .methodCount = 1, + .events = NULL, + .eventCount = 0, + .createSig = WGT_CREATE_PARENT_INT_INT, + .createArgs = { 64, 64 }, + .defaultEvent = "Click", + .namePrefix = "Picture" }; void wgtRegister(void) { diff --git a/widgets/checkbox/checkbox.bmp b/widgets/checkbox/checkbox.bmp new file mode 100644 index 0000000..def31eb --- /dev/null +++ b/widgets/checkbox/checkbox.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab65cd229fecdd971fa926fe28a77f1f4c8689b9388fd3937fa89462d7e549a4 +size 822 diff --git a/widgets/checkbox/checkbox.res b/widgets/checkbox/checkbox.res new file mode 100644 index 0000000..d9c2922 --- /dev/null +++ b/widgets/checkbox/checkbox.res @@ -0,0 +1,5 @@ +icon16 icon checkbox.bmp +name text "CheckBox" +author text "DVX Project" +description text "Check box toggle widget" +version text "1.0" diff --git a/widgets/widgetCheckbox.c b/widgets/checkbox/widgetCheckbox.c similarity index 97% rename from widgets/widgetCheckbox.c rename to widgets/checkbox/widgetCheckbox.c index e5ebb3f..d7bf28b 100644 --- a/widgets/widgetCheckbox.c +++ b/widgets/checkbox/widgetCheckbox.c @@ -256,13 +256,15 @@ static const WgtPropDescT sProps[] = { }; static const WgtIfaceT sIface = { - .basName = "CheckBox", - .props = sProps, - .propCount = 1, - .methods = NULL, - .methodCount = 0, - .events = NULL, - .eventCount = 0 + .basName = "CheckBox", + .props = sProps, + .propCount = 1, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0, + .createSig = WGT_CREATE_PARENT_TEXT, + .defaultEvent = "Click" }; void wgtRegister(void) { diff --git a/widgets/comboBox/combobox.bmp b/widgets/comboBox/combobox.bmp new file mode 100644 index 0000000..8224f76 --- /dev/null +++ b/widgets/comboBox/combobox.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4000edcb72f3493ad904f20a9dc25e11a214bbf498f9c8399413f81891bc2f56 +size 822 diff --git a/widgets/comboBox/combobox.res b/widgets/comboBox/combobox.res new file mode 100644 index 0000000..380a22b --- /dev/null +++ b/widgets/comboBox/combobox.res @@ -0,0 +1,5 @@ +icon16 icon combobox.bmp +name text "ComboBox" +author text "DVX Project" +description text "Editable dropdown combo box" +version text "1.0" diff --git a/widgets/widgetComboBox.c b/widgets/comboBox/widgetComboBox.c similarity index 98% rename from widgets/widgetComboBox.c rename to widgets/comboBox/widgetComboBox.c index 2a86bb5..58cafe2 100644 --- a/widgets/widgetComboBox.c +++ b/widgets/comboBox/widgetComboBox.c @@ -542,13 +542,16 @@ static const WgtPropDescT sProps[] = { }; static const WgtIfaceT sIface = { - .basName = "ComboBox", - .props = sProps, - .propCount = 1, - .methods = NULL, - .methodCount = 0, - .events = NULL, - .eventCount = 0 + .basName = "ComboBox", + .props = sProps, + .propCount = 1, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0, + .createSig = WGT_CREATE_PARENT_INT, + .createArgs = { 256 }, + .defaultEvent = "Click" }; void wgtRegister(void) { diff --git a/widgets/dropdown/dropdown.bmp b/widgets/dropdown/dropdown.bmp new file mode 100644 index 0000000..cd99773 --- /dev/null +++ b/widgets/dropdown/dropdown.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36ee05870d744082189729e6ded3fddf9eea4e98c0f278df7c8dfc46955252d0 +size 822 diff --git a/widgets/dropdown/dropdown.res b/widgets/dropdown/dropdown.res new file mode 100644 index 0000000..9ce2611 --- /dev/null +++ b/widgets/dropdown/dropdown.res @@ -0,0 +1,5 @@ +icon16 icon dropdown.bmp +name text "DropDown" +author text "DVX Project" +description text "Non-editable dropdown selector" +version text "1.0" diff --git a/widgets/widgetDropdown.c b/widgets/dropdown/widgetDropdown.c similarity index 98% rename from widgets/widgetDropdown.c rename to widgets/dropdown/widgetDropdown.c index 6f6620f..2cef5be 100644 --- a/widgets/widgetDropdown.c +++ b/widgets/dropdown/widgetDropdown.c @@ -405,13 +405,15 @@ static const WgtPropDescT sProps[] = { }; static const WgtIfaceT sIface = { - .basName = "DropDown", - .props = sProps, - .propCount = 1, - .methods = NULL, - .methodCount = 0, - .events = NULL, - .eventCount = 0 + .basName = "DropDown", + .props = sProps, + .propCount = 1, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0, + .createSig = WGT_CREATE_PARENT, + .defaultEvent = "Click" }; void wgtRegister(void) { diff --git a/widgets/image/image.bmp b/widgets/image/image.bmp new file mode 100644 index 0000000..525a3d8 --- /dev/null +++ b/widgets/image/image.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f75286cb420ece30ab7216087ee979c0759d7d583e99c18c703054691f96976 +size 822 diff --git a/widgets/image/image.res b/widgets/image/image.res new file mode 100644 index 0000000..bcac7bd --- /dev/null +++ b/widgets/image/image.res @@ -0,0 +1,5 @@ +icon16 icon image.bmp +name text "Image" +author text "DVX Project" +description text "Static image display" +version text "1.0" diff --git a/widgets/widgetImage.c b/widgets/image/widgetImage.c similarity index 96% rename from widgets/widgetImage.c rename to widgets/image/widgetImage.c index 846d1b5..131251a 100644 --- a/widgets/widgetImage.c +++ b/widgets/image/widgetImage.c @@ -255,13 +255,15 @@ static const WgtPropDescT sProps[] = { }; static const WgtIfaceT sIface = { - .basName = "Image", - .props = sProps, - .propCount = 3, - .methods = NULL, - .methodCount = 0, - .events = NULL, - .eventCount = 0 + .basName = "Image", + .props = sProps, + .propCount = 3, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0, + .createSig = WGT_CREATE_PARENT_DATA, + .defaultEvent = "Click" }; void wgtRegister(void) { diff --git a/widgets/imageButton/imgbtn.bmp b/widgets/imageButton/imgbtn.bmp new file mode 100644 index 0000000..2913575 --- /dev/null +++ b/widgets/imageButton/imgbtn.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:098d6e6ebbd0b4d1d80b8e80b30d3120ebf11486f721fed7f9f729afb30ae25c +size 822 diff --git a/widgets/imageButton/imgbtn.res b/widgets/imageButton/imgbtn.res new file mode 100644 index 0000000..36d0ec8 --- /dev/null +++ b/widgets/imageButton/imgbtn.res @@ -0,0 +1,5 @@ +icon16 icon imgbtn.bmp +name text "ImageButton" +author text "DVX Project" +description text "Button with bitmap icon" +version text "1.0" diff --git a/widgets/widgetImageButton.c b/widgets/imageButton/widgetImageButton.c similarity index 97% rename from widgets/widgetImageButton.c rename to widgets/imageButton/widgetImageButton.c index da0c904..1128893 100644 --- a/widgets/widgetImageButton.c +++ b/widgets/imageButton/widgetImageButton.c @@ -329,13 +329,15 @@ static const WgtPropDescT sImgBtnProps[] = { }; static const WgtIfaceT sIface = { - .basName = "ImageButton", - .props = sImgBtnProps, - .propCount = 3, - .methods = NULL, - .methodCount = 0, - .events = NULL, - .eventCount = 0 + .basName = "ImageButton", + .props = sImgBtnProps, + .propCount = 3, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0, + .createSig = WGT_CREATE_PARENT_DATA, + .defaultEvent = "Click" }; void wgtRegister(void) { diff --git a/widgets/label/label.bmp b/widgets/label/label.bmp new file mode 100644 index 0000000..468285e --- /dev/null +++ b/widgets/label/label.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:203f8b671300e5a5f06bf6d8fcca4c17077596739515b8dc8edd6ca2e6d1f911 +size 822 diff --git a/widgets/label/label.res b/widgets/label/label.res new file mode 100644 index 0000000..f889423 --- /dev/null +++ b/widgets/label/label.res @@ -0,0 +1,5 @@ +icon16 icon label.bmp +name text "Label" +author text "DVX Project" +description text "Static text label" +version text "1.0" diff --git a/widgets/widgetLabel.c b/widgets/label/widgetLabel.c similarity index 95% rename from widgets/widgetLabel.c rename to widgets/label/widgetLabel.c index df03b0b..4a1a778 100644 --- a/widgets/widgetLabel.c +++ b/widgets/label/widgetLabel.c @@ -165,13 +165,15 @@ static const WgtPropDescT sProps[] = { }; static const WgtIfaceT sIface = { - .basName = "Label", - .props = sProps, - .propCount = 1, - .methods = NULL, - .methodCount = 0, - .events = NULL, - .eventCount = 0 + .basName = "Label", + .props = sProps, + .propCount = 1, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0, + .createSig = WGT_CREATE_PARENT_TEXT, + .defaultEvent = "Click" }; void wgtRegister(void) { diff --git a/widgets/listBox/listbox.bmp b/widgets/listBox/listbox.bmp new file mode 100644 index 0000000..955ef9a --- /dev/null +++ b/widgets/listBox/listbox.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2ff816c7be758522c1b5429f49c39580791ad251e8748790b7b8f548fccc6d2 +size 822 diff --git a/widgets/listBox/listbox.res b/widgets/listBox/listbox.res new file mode 100644 index 0000000..56d9aca --- /dev/null +++ b/widgets/listBox/listbox.res @@ -0,0 +1,5 @@ +icon16 icon listbox.bmp +name text "ListBox" +author text "DVX Project" +description text "Scrollable item list with single or multi-select" +version text "1.0" diff --git a/widgets/widgetListBox.c b/widgets/listBox/widgetListBox.c similarity index 99% rename from widgets/widgetListBox.c rename to widgets/listBox/widgetListBox.c index 9cd6871..80e593b 100644 --- a/widgets/widgetListBox.c +++ b/widgets/listBox/widgetListBox.c @@ -871,13 +871,15 @@ static const WgtMethodDescT sMethods[] = { }; static const WgtIfaceT sIface = { - .basName = "ListBox", - .props = sProps, - .propCount = 1, - .methods = sMethods, - .methodCount = 6, - .events = NULL, - .eventCount = 0 + .basName = "ListBox", + .props = sProps, + .propCount = 1, + .methods = sMethods, + .methodCount = 6, + .events = NULL, + .eventCount = 0, + .createSig = WGT_CREATE_PARENT, + .defaultEvent = "Click" }; void wgtRegister(void) { diff --git a/widgets/listView/listview.bmp b/widgets/listView/listview.bmp new file mode 100644 index 0000000..955ef9a --- /dev/null +++ b/widgets/listView/listview.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2ff816c7be758522c1b5429f49c39580791ad251e8748790b7b8f548fccc6d2 +size 822 diff --git a/widgets/listView/listview.res b/widgets/listView/listview.res new file mode 100644 index 0000000..0e2d928 --- /dev/null +++ b/widgets/listView/listview.res @@ -0,0 +1,5 @@ +icon16 icon listview.bmp +name text "ListView" +author text "DVX Project" +description text "Multi-column list with sortable headers" +version text "1.0" diff --git a/widgets/widgetListView.c b/widgets/listView/widgetListView.c similarity index 99% rename from widgets/widgetListView.c rename to widgets/listView/widgetListView.c index 9d4dd9a..30ef7ce 100644 --- a/widgets/widgetListView.c +++ b/widgets/listView/widgetListView.c @@ -855,7 +855,10 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) hit->onChange(hit); } - // onDblClick is handled by the central dispatcher in widgetEvent.c + // Double-click: fire onDblClick + if (hit->onDblClick && multiClickDetect(vx, vy) >= 2) { + hit->onDblClick(hit); + } // Initiate drag-reorder if enabled (not from modifier clicks) if (lv->reorderable && !shift && !ctrl) { @@ -1747,13 +1750,15 @@ static const WgtMethodDescT sMethods[] = { }; static const WgtIfaceT sIface = { - .basName = "ListView", - .props = sProps, - .propCount = 1, - .methods = sMethods, - .methodCount = 6, - .events = NULL, - .eventCount = 0 + .basName = "ListView", + .props = sProps, + .propCount = 1, + .methods = sMethods, + .methodCount = 6, + .events = NULL, + .eventCount = 0, + .createSig = WGT_CREATE_PARENT, + .defaultEvent = "Click" }; void wgtRegister(void) { diff --git a/widgets/progressBar/progress.bmp b/widgets/progressBar/progress.bmp new file mode 100644 index 0000000..da4c7a2 --- /dev/null +++ b/widgets/progressBar/progress.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c4859d3b3c2e726ceb5837b86203d496f744c2c4a3323e8aca05fbb04c920ba3 +size 822 diff --git a/widgets/progressBar/progress.res b/widgets/progressBar/progress.res new file mode 100644 index 0000000..25bffd8 --- /dev/null +++ b/widgets/progressBar/progress.res @@ -0,0 +1,5 @@ +icon16 icon progress.bmp +name text "ProgressBar" +author text "DVX Project" +description text "Progress indicator bar" +version text "1.0" diff --git a/widgets/widgetProgressBar.c b/widgets/progressBar/widgetProgressBar.c similarity index 96% rename from widgets/widgetProgressBar.c rename to widgets/progressBar/widgetProgressBar.c index 29e7227..7f9efd9 100644 --- a/widgets/widgetProgressBar.c +++ b/widgets/progressBar/widgetProgressBar.c @@ -214,13 +214,14 @@ static const WgtPropDescT sProps[] = { }; static const WgtIfaceT sIface = { - .basName = "ProgressBar", - .props = sProps, - .propCount = 1, - .methods = NULL, - .methodCount = 0, - .events = NULL, - .eventCount = 0 + .basName = "ProgressBar", + .props = sProps, + .propCount = 1, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0, + .createSig = WGT_CREATE_PARENT }; void wgtRegister(void) { diff --git a/widgets/radio/radio.bmp b/widgets/radio/radio.bmp new file mode 100644 index 0000000..ff1efe0 --- /dev/null +++ b/widgets/radio/radio.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d4e43ccc711a9d1889cfb6bf43e69fcbadecb52febbcf669f000e55f82551ca +size 822 diff --git a/widgets/radio/radio.res b/widgets/radio/radio.res new file mode 100644 index 0000000..6d4f6b8 --- /dev/null +++ b/widgets/radio/radio.res @@ -0,0 +1,5 @@ +icon16 icon radio.bmp +name text "RadioButton" +author text "DVX Project" +description text "Radio button option selector" +version text "1.0" diff --git a/widgets/widgetRadio.c b/widgets/radio/widgetRadio.c similarity index 97% rename from widgets/widgetRadio.c rename to widgets/radio/widgetRadio.c index 58a61b7..ec54550 100644 --- a/widgets/widgetRadio.c +++ b/widgets/radio/widgetRadio.c @@ -420,13 +420,16 @@ static const WgtMethodDescT sMethods[] = { }; static const WgtIfaceT sIface = { - .basName = "OptionButton", - .props = sProps, - .propCount = 1, - .methods = sMethods, - .methodCount = 1, - .events = NULL, - .eventCount = 0 + .basName = "OptionButton", + .props = sProps, + .propCount = 1, + .methods = sMethods, + .methodCount = 1, + .events = NULL, + .eventCount = 0, + .createSig = WGT_CREATE_PARENT_TEXT, + .defaultEvent = "Click", + .namePrefix = "Option" }; void wgtRegister(void) { diff --git a/widgets/scrollPane/scrlpane.bmp b/widgets/scrollPane/scrlpane.bmp new file mode 100644 index 0000000..68db515 --- /dev/null +++ b/widgets/scrollPane/scrlpane.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd2cc5bfdc83377fa772a994a990dbcc46b28f062e3304536cae20efdd8a6f52 +size 822 diff --git a/widgets/scrollPane/scrlpane.res b/widgets/scrollPane/scrlpane.res new file mode 100644 index 0000000..2a7516b --- /dev/null +++ b/widgets/scrollPane/scrlpane.res @@ -0,0 +1,5 @@ +icon16 icon scrlpane.bmp +name text "ScrollPane" +author text "DVX Project" +description text "Scrollable content container" +version text "1.0" diff --git a/widgets/widgetScrollPane.c b/widgets/scrollPane/widgetScrollPane.c similarity index 99% rename from widgets/widgetScrollPane.c rename to widgets/scrollPane/widgetScrollPane.c index 245e710..e11c78b 100644 --- a/widgets/widgetScrollPane.c +++ b/widgets/scrollPane/widgetScrollPane.c @@ -860,13 +860,15 @@ static const struct { }; static const WgtIfaceT sIface = { - .basName = "ScrollPane", - .props = NULL, - .propCount = 0, - .methods = NULL, - .methodCount = 0, - .events = NULL, - .eventCount = 0 + .basName = "ScrollPane", + .props = NULL, + .propCount = 0, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0, + .createSig = WGT_CREATE_PARENT, + .isContainer = true }; void wgtRegister(void) { diff --git a/widgets/separator/separatr.bmp b/widgets/separator/separatr.bmp new file mode 100644 index 0000000..1c76fdc --- /dev/null +++ b/widgets/separator/separatr.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2b8cb05edc0006df3c39b4d2697520e17d7816ffd82894a34e9c50a57b7e1d6 +size 822 diff --git a/widgets/separator/separatr.res b/widgets/separator/separatr.res new file mode 100644 index 0000000..ec90d00 --- /dev/null +++ b/widgets/separator/separatr.res @@ -0,0 +1,5 @@ +icon16 icon separatr.bmp +name text "Separator" +author text "DVX Project" +description text "Horizontal or vertical divider line" +version text "1.0" diff --git a/widgets/widgetSeparator.c b/widgets/separator/widgetSeparator.c similarity index 95% rename from widgets/widgetSeparator.c rename to widgets/separator/widgetSeparator.c index 00e8920..bbefcf4 100644 --- a/widgets/widgetSeparator.c +++ b/widgets/separator/widgetSeparator.c @@ -143,13 +143,14 @@ static const struct { }; static const WgtIfaceT sIface = { - .basName = "Line", - .props = NULL, - .propCount = 0, - .methods = NULL, - .methodCount = 0, - .events = NULL, - .eventCount = 0 + .basName = "Line", + .props = NULL, + .propCount = 0, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0, + .createSig = WGT_CREATE_PARENT }; void wgtRegister(void) { diff --git a/widgets/slider/slider.bmp b/widgets/slider/slider.bmp new file mode 100644 index 0000000..79e8ef1 --- /dev/null +++ b/widgets/slider/slider.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86146635941c5b737ffedb9fc296ad88b4d8d63acf36c06b95275f8a711d4323 +size 822 diff --git a/widgets/slider/slider.res b/widgets/slider/slider.res new file mode 100644 index 0000000..f7c804a --- /dev/null +++ b/widgets/slider/slider.res @@ -0,0 +1,5 @@ +icon16 icon slider.bmp +name text "Slider" +author text "DVX Project" +description text "Value slider (scrollbar)" +version text "1.0" diff --git a/widgets/widgetSlider.c b/widgets/slider/widgetSlider.c similarity index 97% rename from widgets/widgetSlider.c rename to widgets/slider/widgetSlider.c index f45013d..2b94b76 100644 --- a/widgets/widgetSlider.c +++ b/widgets/slider/widgetSlider.c @@ -398,13 +398,17 @@ static const WgtPropDescT sProps[] = { }; static const WgtIfaceT sIface = { - .basName = "HScrollBar", - .props = sProps, - .propCount = 1, - .methods = NULL, - .methodCount = 0, - .events = NULL, - .eventCount = 0 + .basName = "HScrollBar", + .props = sProps, + .propCount = 1, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0, + .createSig = WGT_CREATE_PARENT_INT_INT, + .createArgs = { 0, 100 }, + .defaultEvent = "Change", + .namePrefix = "HScroll" }; void wgtRegister(void) { diff --git a/widgets/spacer/spacer.bmp b/widgets/spacer/spacer.bmp new file mode 100644 index 0000000..b750265 --- /dev/null +++ b/widgets/spacer/spacer.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f36497d5c39f83727d439f13f1d516d77bc8645eebf979675a4bbead4ce1887 +size 822 diff --git a/widgets/spacer/spacer.res b/widgets/spacer/spacer.res new file mode 100644 index 0000000..faaa3ec --- /dev/null +++ b/widgets/spacer/spacer.res @@ -0,0 +1,5 @@ +icon16 icon spacer.bmp +name text "Spacer" +author text "DVX Project" +description text "Invisible spacing widget" +version text "1.0" diff --git a/widgets/widgetSpacer.c b/widgets/spacer/widgetSpacer.c similarity index 91% rename from widgets/widgetSpacer.c rename to widgets/spacer/widgetSpacer.c index 2ad7463..f4e6c5c 100644 --- a/widgets/widgetSpacer.c +++ b/widgets/spacer/widgetSpacer.c @@ -70,13 +70,14 @@ static const struct { }; static const WgtIfaceT sIface = { - .basName = "Spacer", - .props = NULL, - .propCount = 0, - .methods = NULL, - .methodCount = 0, - .events = NULL, - .eventCount = 0 + .basName = "Spacer", + .props = NULL, + .propCount = 0, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0, + .createSig = WGT_CREATE_PARENT }; void wgtRegister(void) { diff --git a/widgets/spinner/spinner.bmp b/widgets/spinner/spinner.bmp new file mode 100644 index 0000000..8c30e9c --- /dev/null +++ b/widgets/spinner/spinner.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b1fa0c7e6086b7f6dcf19221d75e341532763d23299313422d42ea5f7b4477a5 +size 822 diff --git a/widgets/spinner/spinner.res b/widgets/spinner/spinner.res new file mode 100644 index 0000000..c9d2e85 --- /dev/null +++ b/widgets/spinner/spinner.res @@ -0,0 +1,5 @@ +icon16 icon spinner.bmp +name text "Spinner" +author text "DVX Project" +description text "Numeric up/down spinner" +version text "1.0" diff --git a/widgets/widgetSpinner.c b/widgets/spinner/widgetSpinner.c similarity index 97% rename from widgets/widgetSpinner.c rename to widgets/spinner/widgetSpinner.c index 232d2d2..693a5de 100644 --- a/widgets/widgetSpinner.c +++ b/widgets/spinner/widgetSpinner.c @@ -534,7 +534,12 @@ WidgetT *wgtSpinner(WidgetT *parent, int32_t minVal, int32_t maxVal, int32_t ste int32_t wgtSpinnerGetValue(const WidgetT *w) { VALIDATE_WIDGET(w, sTypeId, 0); - const SpinnerDataT *d = (const SpinnerDataT *)w->data; + SpinnerDataT *d = (SpinnerDataT *)w->data; + + // Commit any in-progress edit so the returned value is current + if (d->editing) { + spinnerCommitEdit(d); + } return d->value; } @@ -598,13 +603,16 @@ static const WgtMethodDescT sMethods[] = { }; static const WgtIfaceT sIface = { - .basName = "SpinButton", - .props = sProps, - .propCount = 1, - .methods = sMethods, - .methodCount = 2, - .events = NULL, - .eventCount = 0 + .basName = "SpinButton", + .props = sProps, + .propCount = 1, + .methods = sMethods, + .methodCount = 2, + .events = NULL, + .eventCount = 0, + .createSig = WGT_CREATE_PARENT_INT_INT_INT, + .createArgs = { 0, 100, 1 }, + .defaultEvent = "Change" }; void wgtRegister(void) { diff --git a/widgets/splitter/splitter.bmp b/widgets/splitter/splitter.bmp new file mode 100644 index 0000000..461217f --- /dev/null +++ b/widgets/splitter/splitter.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de07fdf203c05f93a8e41aa2d1517d66893778d5013491aba5e34c33c0ebefaf +size 822 diff --git a/widgets/splitter/splitter.res b/widgets/splitter/splitter.res new file mode 100644 index 0000000..cba68ba --- /dev/null +++ b/widgets/splitter/splitter.res @@ -0,0 +1,5 @@ +icon16 icon splitter.bmp +name text "Splitter" +author text "DVX Project" +description text "Draggable pane divider" +version text "1.0" diff --git a/widgets/widgetSplitter.c b/widgets/splitter/widgetSplitter.c similarity index 98% rename from widgets/widgetSplitter.c rename to widgets/splitter/widgetSplitter.c index 7a9b74e..41cae69 100644 --- a/widgets/widgetSplitter.c +++ b/widgets/splitter/widgetSplitter.c @@ -529,13 +529,16 @@ static const WgtPropDescT sProps[] = { }; static const WgtIfaceT sIface = { - .basName = "Splitter", - .props = sProps, - .propCount = 1, - .methods = NULL, - .methodCount = 0, - .events = NULL, - .eventCount = 0 + .basName = "Splitter", + .props = sProps, + .propCount = 1, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0, + .createSig = WGT_CREATE_PARENT_BOOL, + .createArgs = { 1 }, + .isContainer = true }; void wgtRegister(void) { diff --git a/widgets/statusBar/statbar.bmp b/widgets/statusBar/statbar.bmp new file mode 100644 index 0000000..63003e1 --- /dev/null +++ b/widgets/statusBar/statbar.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67578e247a041d76d6c99d0c62c22c4c1abccdcc776daf612363c88f837135c5 +size 822 diff --git a/widgets/statusBar/statbar.res b/widgets/statusBar/statbar.res new file mode 100644 index 0000000..fb2c966 --- /dev/null +++ b/widgets/statusBar/statbar.res @@ -0,0 +1,5 @@ +icon16 icon statbar.bmp +name text "StatusBar" +author text "DVX Project" +description text "Window status bar container" +version text "1.0" diff --git a/widgets/widgetStatusBar.c b/widgets/statusBar/widgetStatusBar.c similarity index 94% rename from widgets/widgetStatusBar.c rename to widgets/statusBar/widgetStatusBar.c index ef13d31..c17847d 100644 --- a/widgets/widgetStatusBar.c +++ b/widgets/statusBar/widgetStatusBar.c @@ -108,13 +108,14 @@ static const struct { }; static const WgtIfaceT sIface = { - .basName = "StatusBar", - .props = NULL, - .propCount = 0, - .methods = NULL, - .methodCount = 0, - .events = NULL, - .eventCount = 0 + .basName = "StatusBar", + .props = NULL, + .propCount = 0, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0, + .createSig = WGT_CREATE_PARENT }; void wgtRegister(void) { diff --git a/widgets/tabControl/tabctrl.bmp b/widgets/tabControl/tabctrl.bmp new file mode 100644 index 0000000..28af4d7 --- /dev/null +++ b/widgets/tabControl/tabctrl.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b59d67b9744d50c58ec96881790ebe7ac734f3e8fe5646ed7f5fcacfa6b61797 +size 822 diff --git a/widgets/tabControl/tabctrl.res b/widgets/tabControl/tabctrl.res new file mode 100644 index 0000000..68c7cb9 --- /dev/null +++ b/widgets/tabControl/tabctrl.res @@ -0,0 +1,5 @@ +icon16 icon tabctrl.bmp +name text "TabControl" +author text "DVX Project" +description text "Tabbed page container" +version text "1.0" diff --git a/widgets/widgetTabControl.c b/widgets/tabControl/widgetTabControl.c similarity index 98% rename from widgets/widgetTabControl.c rename to widgets/tabControl/widgetTabControl.c index 24a6383..175309e 100644 --- a/widgets/widgetTabControl.c +++ b/widgets/tabControl/widgetTabControl.c @@ -681,13 +681,16 @@ static const WgtMethodDescT sMethods[] = { }; static const WgtIfaceT sIface = { - .basName = "TabStrip", - .props = sProps, - .propCount = 1, - .methods = sMethods, - .methodCount = 1, - .events = NULL, - .eventCount = 0 + .basName = "TabStrip", + .props = sProps, + .propCount = 1, + .methods = sMethods, + .methodCount = 1, + .events = NULL, + .eventCount = 0, + .createSig = WGT_CREATE_PARENT, + .isContainer = true, + .defaultEvent = "Click" }; void wgtRegister(void) { diff --git a/widgets/textInput/textinpt.bmp b/widgets/textInput/textinpt.bmp new file mode 100644 index 0000000..52f0c2f --- /dev/null +++ b/widgets/textInput/textinpt.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6767c1516ecfc49f104e6840fa939472c55779c098780f764703919ef1e7c2a5 +size 822 diff --git a/widgets/textInput/textinpt.res b/widgets/textInput/textinpt.res new file mode 100644 index 0000000..e4ab045 --- /dev/null +++ b/widgets/textInput/textinpt.res @@ -0,0 +1,11 @@ +# textinpt.res -- Resources for TextBox and TextArea widgets +# +# TextBox (first registered, uses unsuffixed names) +icon16 icon textinpt.bmp +name text "TextBox" +# TextArea (second registered) +icon16-2 icon textinpt.bmp +name-2 text "TextArea" +author text "DVX Project" +description text "Single-line and multi-line text editor" +version text "1.0" diff --git a/widgets/widgetTextInput.c b/widgets/textInput/widgetTextInput.c similarity index 98% rename from widgets/widgetTextInput.c rename to widgets/textInput/widgetTextInput.c index e722473..af82317 100644 --- a/widgets/widgetTextInput.c +++ b/widgets/textInput/widgetTextInput.c @@ -2612,19 +2612,47 @@ static const struct { .setShowLineNumbers = wgtTextAreaSetShowLineNumbers }; -static const WgtIfaceT sIface = { - .basName = "TextBox", - .props = NULL, - .propCount = 0, - .methods = NULL, - .methodCount = 0, - .events = NULL, - .eventCount = 0 +// Per-type APIs for the designer +static const struct { WidgetT *(*create)(WidgetT *, int32_t); } sTextInputApi = { .create = wgtTextInput }; +static const struct { WidgetT *(*create)(WidgetT *, int32_t); } sTextAreaApi = { .create = wgtTextArea }; + +static const WgtIfaceT sIfaceTextInput = { + .basName = "TextBox", + .props = NULL, + .propCount = 0, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0, + .createSig = WGT_CREATE_PARENT_INT, + .createArgs = { 256 }, + .defaultEvent = "Change" +}; + +static const WgtIfaceT sIfaceTextArea = { + .basName = "TextArea", + .props = NULL, + .propCount = 0, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0, + .createSig = WGT_CREATE_PARENT_INT, + .createArgs = { 4096 }, + .defaultEvent = "Change" }; void wgtRegister(void) { sTextInputTypeId = wgtRegisterClass(&sClassTextInput); sTextAreaTypeId = wgtRegisterClass(&sClassTextArea); + + // Original combined API (for widgetTextInput.h compatibility) wgtRegisterApi("textinput", &sApi); - wgtRegisterIface("textinput", &sIface); + + // Per-type APIs and ifaces (for designer/toolbox) + wgtRegisterApi("textbox", &sTextInputApi); + wgtRegisterIface("textbox", &sIfaceTextInput); + + wgtRegisterApi("textarea", &sTextAreaApi); + wgtRegisterIface("textarea", &sIfaceTextArea); } diff --git a/widgets/timer/timer.bmp b/widgets/timer/timer.bmp new file mode 100644 index 0000000..cf92932 --- /dev/null +++ b/widgets/timer/timer.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84c9b1aa14d420c1941901bcbd7dffa49b1473dc795ab37959abd98eb658dc61 +size 822 diff --git a/widgets/timer/timer.res b/widgets/timer/timer.res new file mode 100644 index 0000000..d15dd0e --- /dev/null +++ b/widgets/timer/timer.res @@ -0,0 +1,5 @@ +icon16 icon timer.bmp +name text "Timer" +author text "DVX Project" +description text "Periodic event timer" +version text "1.0" diff --git a/widgets/widgetTimer.c b/widgets/timer/widgetTimer.c similarity index 95% rename from widgets/widgetTimer.c rename to widgets/timer/widgetTimer.c index ddc5038..efa23d1 100644 --- a/widgets/widgetTimer.c +++ b/widgets/timer/widgetTimer.c @@ -234,13 +234,16 @@ static const WgtEventDescT sEvents[] = { }; static const WgtIfaceT sIface = { - .basName = "Timer", - .props = sProps, - .propCount = 2, - .methods = sMethods, - .methodCount = 2, - .events = sEvents, - .eventCount = 1 + .basName = "Timer", + .props = sProps, + .propCount = 2, + .methods = sMethods, + .methodCount = 2, + .events = sEvents, + .eventCount = 1, + .createSig = WGT_CREATE_PARENT_INT_BOOL, + .createArgs = { 1000, 1 }, + .defaultEvent = "Timer" }; void wgtRegister(void) { diff --git a/widgets/toolbar/toolbar.bmp b/widgets/toolbar/toolbar.bmp new file mode 100644 index 0000000..474ab68 --- /dev/null +++ b/widgets/toolbar/toolbar.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e31039ce3aed3021046e28f9051dae1456cb1a94fb07f372a8de371f69c787e +size 822 diff --git a/widgets/toolbar/toolbar.res b/widgets/toolbar/toolbar.res new file mode 100644 index 0000000..99fc484 --- /dev/null +++ b/widgets/toolbar/toolbar.res @@ -0,0 +1,5 @@ +icon16 icon toolbar.bmp +name text "Toolbar" +author text "DVX Project" +description text "Button toolbar container" +version text "1.0" diff --git a/widgets/widgetToolbar.c b/widgets/toolbar/widgetToolbar.c similarity index 93% rename from widgets/widgetToolbar.c rename to widgets/toolbar/widgetToolbar.c index c45601e..9612243 100644 --- a/widgets/widgetToolbar.c +++ b/widgets/toolbar/widgetToolbar.c @@ -101,13 +101,15 @@ static const struct { }; static const WgtIfaceT sIface = { - .basName = "Toolbar", - .props = NULL, - .propCount = 0, - .methods = NULL, - .methodCount = 0, - .events = NULL, - .eventCount = 0 + .basName = "Toolbar", + .props = NULL, + .propCount = 0, + .methods = NULL, + .methodCount = 0, + .events = NULL, + .eventCount = 0, + .createSig = WGT_CREATE_PARENT, + .isContainer = true }; void wgtRegister(void) { diff --git a/widgets/treeView/treeview.bmp b/widgets/treeView/treeview.bmp new file mode 100644 index 0000000..37148a0 --- /dev/null +++ b/widgets/treeView/treeview.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:754772b927c3d1ca251c8d833c0cb92a462c83173892b7c29a2a965ca46e2a03 +size 822 diff --git a/widgets/treeView/treeview.res b/widgets/treeView/treeview.res new file mode 100644 index 0000000..1ea5ee5 --- /dev/null +++ b/widgets/treeView/treeview.res @@ -0,0 +1,5 @@ +icon16 icon treeview.bmp +name text "TreeView" +author text "DVX Project" +description text "Hierarchical tree with expand/collapse" +version text "1.0" diff --git a/widgets/widgetTreeView.c b/widgets/treeView/widgetTreeView.c similarity index 94% rename from widgets/widgetTreeView.c rename to widgets/treeView/widgetTreeView.c index a1aee21..983245e 100644 --- a/widgets/widgetTreeView.c +++ b/widgets/treeView/widgetTreeView.c @@ -69,6 +69,7 @@ typedef struct { WidgetT *dragItem; WidgetT *dropTarget; bool dropAfter; + bool dropInto; int32_t cachedTotalHeight; int32_t cachedMaxWidth; bool dimsValid; @@ -312,15 +313,26 @@ static void paintReorderIndicator(WidgetT *w, DisplayT *d, const BlitOpsT *ops, return; } - int32_t lineY = w->y + TREE_BORDER - tv->scrollPos + dropY; - - if (tv->dropAfter) { - lineY += font->charHeight; - } - + int32_t baseY = w->y + TREE_BORDER - tv->scrollPos + dropY; int32_t lineX = w->x + TREE_BORDER; - drawHLine(d, ops, lineX, lineY, innerW, colors->contentFg); - drawHLine(d, ops, lineX, lineY + 1, innerW, colors->contentFg); + + if (tv->dropInto) { + // Draw a rectangle around the target to indicate reparenting + drawHLine(d, ops, lineX, baseY, innerW, colors->contentFg); + drawHLine(d, ops, lineX, baseY + font->charHeight - 1, innerW, colors->contentFg); + drawVLine(d, ops, lineX, baseY, font->charHeight, colors->contentFg); + drawVLine(d, ops, lineX + innerW - 1, baseY, font->charHeight, colors->contentFg); + } else { + // Draw a 2px insertion line + int32_t lineY = baseY; + + if (tv->dropAfter) { + lineY += font->charHeight; + } + + drawHLine(d, ops, lineX, lineY, innerW, colors->contentFg); + drawHLine(d, ops, lineX, lineY + 1, innerW, colors->contentFg); + } } @@ -1395,9 +1407,21 @@ static void widgetTreeViewReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, in if (target) { tv->dropTarget = target; - // Drop after if mouse is in the bottom half of the item + // Three-zone drop: top third = before, middle third = into, bottom third = after int32_t itemY = treeItemYPos(w, target, font); - tv->dropAfter = (relY > itemY + font->charHeight / 2); + int32_t third = font->charHeight / 3; + int32_t offset = relY - itemY; + + if (offset < third) { + tv->dropAfter = false; + tv->dropInto = false; + } else if (offset < third * 2) { + tv->dropAfter = false; + tv->dropInto = true; + } else { + tv->dropAfter = true; + tv->dropInto = false; + } } else { // Below all items -- drop after the last item tv->dropTarget = w->lastChild; @@ -1432,48 +1456,63 @@ static void widgetTreeViewReorderDrop(WidgetT *w) { // Remove dragged item from its current parent WidgetT *oldParent = dragItem->parent; + bool dropInto = tv->dropInto; + tv->dropInto = false; if (oldParent) { widgetRemoveChild(oldParent, dragItem); } - // Insert at the drop position - WidgetT *newParent = dropTarget->parent; + if (dropInto) { + // Reparent: make dragItem the last child of dropTarget + dragItem->nextSibling = NULL; + dragItem->parent = dropTarget; - if (!newParent) { - return; - } + if (dropTarget->lastChild) { + dropTarget->lastChild->nextSibling = dragItem; + } else { + dropTarget->firstChild = dragItem; + } - // Re-insert: walk newParent's children to find dropTarget, insert before/after - dragItem->nextSibling = NULL; - dragItem->parent = newParent; + dropTarget->lastChild = dragItem; + } else { + // Insert at the drop position (sibling reorder) + WidgetT *newParent = dropTarget->parent; - if (!dropAfter) { - // Insert before dropTarget - WidgetT *prev = NULL; + if (!newParent) { + return; + } - for (WidgetT *c = newParent->firstChild; c; c = c->nextSibling) { - if (c == dropTarget) { - dragItem->nextSibling = dropTarget; + dragItem->nextSibling = NULL; + dragItem->parent = newParent; - if (prev) { - prev->nextSibling = dragItem; - } else { - newParent->firstChild = dragItem; + if (!dropAfter) { + // Insert before dropTarget + WidgetT *prev = NULL; + + for (WidgetT *c = newParent->firstChild; c; c = c->nextSibling) { + if (c == dropTarget) { + dragItem->nextSibling = dropTarget; + + if (prev) { + prev->nextSibling = dragItem; + } else { + newParent->firstChild = dragItem; + } + + break; } - break; + prev = c; } + } else { + // Insert after dropTarget + dragItem->nextSibling = dropTarget->nextSibling; + dropTarget->nextSibling = dragItem; - prev = c; - } - } else { - // Insert after dropTarget - dragItem->nextSibling = dropTarget->nextSibling; - dropTarget->nextSibling = dragItem; - - if (newParent->lastChild == dropTarget) { - newParent->lastChild = dragItem; + if (newParent->lastChild == dropTarget) { + newParent->lastChild = dragItem; + } } } @@ -1728,13 +1767,15 @@ static const WgtMethodDescT sMethods[] = { }; static const WgtIfaceT sIface = { - .basName = "TreeView", - .props = NULL, - .propCount = 0, - .methods = sMethods, - .methodCount = 2, - .events = NULL, - .eventCount = 0 + .basName = "TreeView", + .props = NULL, + .propCount = 0, + .methods = sMethods, + .methodCount = 2, + .events = NULL, + .eventCount = 0, + .createSig = WGT_CREATE_PARENT, + .defaultEvent = "Click" }; void wgtRegister(void) {