Bug and performance fixes. Lots of crap I forgot.
This commit is contained in:
parent
454a3620f7
commit
5f305dd14c
59 changed files with 1815 additions and 307 deletions
|
|
@ -29,28 +29,28 @@ dvxdemo: $(BINDIR)/kpunch/dvxdemo/dvxdemo.app
|
|||
dvxhelp: $(BINDIR)/kpunch/dvxhelp/dvxhelp.app
|
||||
|
||||
$(BINDIR)/kpunch/cpanel/cpanel.app: $(OBJDIR)/cpanel.o cpanel/cpanel.res cpanel/icon32.bmp | $(BINDIR)/kpunch/cpanel
|
||||
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $<
|
||||
$(DXE3GEN) -o $@ -U $<
|
||||
$(DVXRES) build $@ cpanel/cpanel.res
|
||||
|
||||
$(BINDIR)/kpunch/imgview/imgview.app: $(OBJDIR)/imgview.o imgview/imgview.res imgview/icon32.bmp | $(BINDIR)/kpunch/imgview
|
||||
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $<
|
||||
$(DXE3GEN) -o $@ -U $<
|
||||
$(DVXRES) build $@ imgview/imgview.res
|
||||
|
||||
$(BINDIR)/kpunch/progman/progman.app: $(OBJDIR)/progman.o | $(BINDIR)/kpunch/progman
|
||||
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $<
|
||||
$(DXE3GEN) -o $@ -U $<
|
||||
|
||||
$(BINDIR)/kpunch/notepad/notepad.app: $(OBJDIR)/notepad.o notepad/notepad.res notepad/icon32.bmp | $(BINDIR)/kpunch/notepad
|
||||
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $<
|
||||
$(DXE3GEN) -o $@ -U $<
|
||||
$(DVXRES) build $@ notepad/notepad.res
|
||||
|
||||
$(BINDIR)/kpunch/clock/clock.app: $(OBJDIR)/clock.o clock/clock.res clock/icon32.bmp | $(BINDIR)/kpunch/clock
|
||||
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -E _appShutdown -U $<
|
||||
$(DXE3GEN) -o $@ -U $<
|
||||
$(DVXRES) build $@ clock/clock.res
|
||||
|
||||
DVXDEMO_BMPS = logo.bmp new.bmp open.bmp sample.bmp save.bmp
|
||||
|
||||
$(BINDIR)/kpunch/dvxdemo/dvxdemo.app: $(OBJDIR)/dvxdemo.o $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) dvxdemo/dvxdemo.res dvxdemo/icon32.bmp | $(BINDIR)/kpunch/dvxdemo
|
||||
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $<
|
||||
$(DXE3GEN) -o $@ -U $<
|
||||
$(DVXRES) build $@ dvxdemo/dvxdemo.res
|
||||
cp $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) $(BINDIR)/kpunch/dvxdemo/
|
||||
|
||||
|
|
@ -73,7 +73,7 @@ $(OBJDIR)/dvxdemo.o: dvxdemo/dvxdemo.c | $(OBJDIR)
|
|||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(BINDIR)/kpunch/dvxhelp/dvxhelp.app: $(OBJDIR)/dvxhelp.o dvxhelp/dvxhelp.res dvxhelp/icon32.bmp | $(BINDIR)/kpunch/dvxhelp
|
||||
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $<
|
||||
$(DXE3GEN) -o $@ -U $<
|
||||
$(DVXRES) build $@ dvxhelp/dvxhelp.res
|
||||
|
||||
$(OBJDIR)/dvxhelp.o: dvxhelp/dvxhelp.c dvxhelp/hlpformat.h | $(OBJDIR)
|
||||
|
|
|
|||
|
|
@ -71,9 +71,7 @@ $(TEST_QUICK): $(TEST_QUICK_SRCS) | $(BINDIR)
|
|||
|
||||
# Runtime library DXE (exports symbols via dlregsym constructor)
|
||||
$(RT_TARGET): $(RT_OBJS) | $(RT_TARGETDIR)
|
||||
$(DXE3GEN) -o $(RT_TARGETDIR)/basrt.dxe \
|
||||
-E _bas -E _BAS \
|
||||
-U $(RT_OBJS)
|
||||
$(DXE3GEN) -o $(RT_TARGETDIR)/basrt.dxe -U $(RT_OBJS)
|
||||
mv $(RT_TARGETDIR)/basrt.dxe $@
|
||||
|
||||
$(RT_TARGETDIR)/basrt.dep: ../../config/basrt.dep | $(RT_TARGETDIR)
|
||||
|
|
@ -81,7 +79,7 @@ $(RT_TARGETDIR)/basrt.dep: ../../config/basrt.dep | $(RT_TARGETDIR)
|
|||
|
||||
# IDE app DXE (compiler linked in, runtime from basrt.lib)
|
||||
$(APP_TARGET): $(COMP_OBJS) $(APP_OBJS) dvxbasic.res | $(APPDIR)
|
||||
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $(COMP_OBJS) $(APP_OBJS)
|
||||
$(DXE3GEN) -o $@ -U $(COMP_OBJS) $(APP_OBJS)
|
||||
$(DVXRES) build $@ dvxbasic.res
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,11 @@ static const KeywordEntryT sKeywords[] = {
|
|||
{ "BYVAL", TOK_BYVAL },
|
||||
{ "CALL", TOK_CALL },
|
||||
{ "CASE", TOK_CASE },
|
||||
{ "CHDIR", TOK_CHDIR },
|
||||
{ "CHDRIVE", TOK_CHDRIVE },
|
||||
{ "CLOSE", TOK_CLOSE },
|
||||
{ "CURDIR", TOK_CURDIR },
|
||||
{ "CURDIR$", TOK_CURDIR },
|
||||
{ "CONST", TOK_CONST },
|
||||
{ "DATA", TOK_DATA },
|
||||
{ "DECLARE", TOK_DECLARE },
|
||||
|
|
@ -43,6 +47,8 @@ static const KeywordEntryT sKeywords[] = {
|
|||
{ "DEFSNG", TOK_DEFSNG },
|
||||
{ "DEFSTR", TOK_DEFSTR },
|
||||
{ "DIM", TOK_DIM },
|
||||
{ "DIR", TOK_DIR },
|
||||
{ "DIR$", TOK_DIR },
|
||||
{ "DO", TOK_DO },
|
||||
{ "DOEVENTS", TOK_DOEVENTS },
|
||||
{ "DOUBLE", TOK_DOUBLE },
|
||||
|
|
@ -57,9 +63,12 @@ static const KeywordEntryT sKeywords[] = {
|
|||
{ "EXPLICIT", TOK_EXPLICIT },
|
||||
{ "EXIT", TOK_EXIT },
|
||||
{ "FALSE", TOK_FALSE_KW },
|
||||
{ "FILECOPY", TOK_FILECOPY },
|
||||
{ "FILELEN", TOK_FILELEN },
|
||||
{ "FOR", TOK_FOR },
|
||||
{ "FUNCTION", TOK_FUNCTION },
|
||||
{ "GET", TOK_GET },
|
||||
{ "GETATTR", TOK_GETATTR },
|
||||
{ "GOSUB", TOK_GOSUB },
|
||||
{ "GOTO", TOK_GOTO },
|
||||
{ "HIDE", TOK_HIDE },
|
||||
|
|
@ -70,6 +79,7 @@ static const KeywordEntryT sKeywords[] = {
|
|||
{ "INPUT", TOK_INPUT },
|
||||
{ "INTEGER", TOK_INTEGER },
|
||||
{ "IS", TOK_IS },
|
||||
{ "KILL", TOK_KILL },
|
||||
{ "LBOUND", TOK_LBOUND },
|
||||
{ "LET", TOK_LET },
|
||||
{ "LINE", TOK_LINE },
|
||||
|
|
@ -77,10 +87,12 @@ static const KeywordEntryT sKeywords[] = {
|
|||
{ "LONG", TOK_LONG },
|
||||
{ "LOOP", TOK_LOOP },
|
||||
{ "ME", TOK_ME },
|
||||
{ "MKDIR", TOK_MKDIR },
|
||||
{ "MOD", TOK_MOD },
|
||||
{ "INPUTBOX", TOK_INPUTBOX },
|
||||
{ "INPUTBOX$", TOK_INPUTBOX },
|
||||
{ "MSGBOX", TOK_MSGBOX },
|
||||
{ "NAME", TOK_NAME },
|
||||
{ "NEXT", TOK_NEXT },
|
||||
{ "NOT", TOK_NOT },
|
||||
{ "ON", TOK_ON },
|
||||
|
|
@ -99,9 +111,11 @@ static const KeywordEntryT sKeywords[] = {
|
|||
{ "RESTORE", TOK_RESTORE },
|
||||
{ "RESUME", TOK_RESUME },
|
||||
{ "RETURN", TOK_RETURN },
|
||||
{ "RMDIR", TOK_RMDIR },
|
||||
{ "SEEK", TOK_SEEK },
|
||||
{ "SELECT", TOK_SELECT },
|
||||
{ "SET", TOK_SET },
|
||||
{ "SETATTR", TOK_SETATTR },
|
||||
{ "SHARED", TOK_SHARED },
|
||||
{ "SHELL", TOK_SHELL },
|
||||
{ "SHOW", TOK_SHOW },
|
||||
|
|
|
|||
|
|
@ -169,6 +169,20 @@ typedef enum {
|
|||
TOK_WRITE,
|
||||
TOK_XOR,
|
||||
|
||||
// Filesystem keywords
|
||||
TOK_CHDIR,
|
||||
TOK_CHDRIVE,
|
||||
TOK_CURDIR,
|
||||
TOK_DIR,
|
||||
TOK_FILECOPY,
|
||||
TOK_FILELEN,
|
||||
TOK_GETATTR,
|
||||
TOK_KILL,
|
||||
TOK_MKDIR,
|
||||
TOK_NAME,
|
||||
TOK_RMDIR,
|
||||
TOK_SETATTR,
|
||||
|
||||
// File modes
|
||||
TOK_APPEND,
|
||||
TOK_BINARY,
|
||||
|
|
|
|||
|
|
@ -342,8 +342,23 @@
|
|||
#define OP_INI_READ 0xE0 // pop default, pop key, pop section, pop file, push string
|
||||
#define OP_INI_WRITE 0xE1 // pop value, pop key, pop section, pop file
|
||||
|
||||
// Filesystem operations
|
||||
#define OP_FS_KILL 0xE2 // pop filename, delete file
|
||||
#define OP_FS_NAME 0xE3 // pop newname, pop oldname, rename
|
||||
#define OP_FS_FILECOPY 0xE4 // pop dst, pop src, copy file
|
||||
#define OP_FS_MKDIR 0xE5 // pop path, create directory
|
||||
#define OP_FS_RMDIR 0xE6 // pop path, remove directory
|
||||
#define OP_FS_CHDIR 0xE7 // pop path, change directory
|
||||
#define OP_FS_CHDRIVE 0xE8 // pop drive, change drive
|
||||
#define OP_FS_CURDIR 0xE9 // push current directory string
|
||||
#define OP_FS_DIR 0xEA // pop pattern, push first matching filename
|
||||
#define OP_FS_DIR_NEXT 0xEB // push next matching filename (no args)
|
||||
#define OP_FS_FILELEN 0xEC // pop filename, push file length
|
||||
#define OP_FS_GETATTR 0xED // pop filename, push attributes integer
|
||||
#define OP_FS_SETATTR 0xEE // pop attrs, pop filename, set attributes
|
||||
|
||||
// Debug
|
||||
#define OP_LINE 0xE2 // [uint16 lineNum] set current source line for debugger
|
||||
#define OP_LINE 0xEF // [uint16 lineNum] set current source line for debugger
|
||||
|
||||
// ============================================================
|
||||
// Halt
|
||||
|
|
|
|||
|
|
@ -141,6 +141,8 @@ static void parsePrimary(BasParserT *p);
|
|||
|
||||
static void parseAssignOrCall(BasParserT *p);
|
||||
static void parseBeginForm(BasParserT *p);
|
||||
static void parseChDir(BasParserT *p);
|
||||
static void parseChDrive(BasParserT *p);
|
||||
static void parseClose(BasParserT *p);
|
||||
static void parseConst(BasParserT *p);
|
||||
static void parseData(BasParserT *p);
|
||||
|
|
@ -154,6 +156,7 @@ static void parseEnd(BasParserT *p);
|
|||
static void parseEndForm(BasParserT *p);
|
||||
static void parseErase(BasParserT *p);
|
||||
static void parseExit(BasParserT *p);
|
||||
static void parseFileCopy(BasParserT *p);
|
||||
static void parseFor(BasParserT *p);
|
||||
static void parseFunction(BasParserT *p);
|
||||
static void parseGet(BasParserT *p);
|
||||
|
|
@ -161,8 +164,11 @@ static void parseGosub(BasParserT *p);
|
|||
static void parseGoto(BasParserT *p);
|
||||
static void parseIf(BasParserT *p);
|
||||
static void parseInput(BasParserT *p);
|
||||
static void parseKill(BasParserT *p);
|
||||
static void parseLineInput(BasParserT *p);
|
||||
static void parseMkDir(BasParserT *p);
|
||||
static void parseModule(BasParserT *p);
|
||||
static void parseName(BasParserT *p);
|
||||
static void parseOn(BasParserT *p);
|
||||
static void parseOnError(BasParserT *p);
|
||||
static void parseOpen(BasParserT *p);
|
||||
|
|
@ -173,7 +179,9 @@ static void parseRead(BasParserT *p);
|
|||
static void parseRedim(BasParserT *p);
|
||||
static void parseRestore(BasParserT *p);
|
||||
static void parseResume(BasParserT *p);
|
||||
static void parseRmDir(BasParserT *p);
|
||||
static void parseSeek(BasParserT *p);
|
||||
static void parseSetAttr(BasParserT *p);
|
||||
static void parseSelectCase(BasParserT *p);
|
||||
static void parseShell(BasParserT *p);
|
||||
static void parseSleep(BasParserT *p);
|
||||
|
|
@ -250,6 +258,14 @@ static void addPredefConsts(BasParserT *p) {
|
|||
|
||||
// Show mode flags
|
||||
addPredefConst(p, "vbModal", 1);
|
||||
|
||||
// File attribute constants
|
||||
addPredefConst(p, "vbNormal", 0);
|
||||
addPredefConst(p, "vbReadOnly", 1);
|
||||
addPredefConst(p, "vbHidden", 2);
|
||||
addPredefConst(p, "vbSystem", 4);
|
||||
addPredefConst(p, "vbDirectory", 16);
|
||||
addPredefConst(p, "vbArchive", 32);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1304,6 +1320,59 @@ static void parsePrimary(BasParserT *p) {
|
|||
return;
|
||||
}
|
||||
|
||||
// CurDir$ -- current directory (no args)
|
||||
if (tt == TOK_CURDIR) {
|
||||
advance(p);
|
||||
if (check(p, TOK_LPAREN)) {
|
||||
expect(p, TOK_LPAREN);
|
||||
expect(p, TOK_RPAREN);
|
||||
}
|
||||
basEmit8(&p->cg, OP_FS_CURDIR);
|
||||
return;
|
||||
}
|
||||
|
||||
// Dir$(pattern) or Dir$() for next match
|
||||
if (tt == TOK_DIR) {
|
||||
advance(p);
|
||||
if (check(p, TOK_LPAREN)) {
|
||||
expect(p, TOK_LPAREN);
|
||||
if (check(p, TOK_RPAREN)) {
|
||||
// Dir$() -- no args, get next match
|
||||
expect(p, TOK_RPAREN);
|
||||
basEmit8(&p->cg, OP_FS_DIR_NEXT);
|
||||
} else {
|
||||
// Dir$(pattern)
|
||||
parseExpression(p);
|
||||
expect(p, TOK_RPAREN);
|
||||
basEmit8(&p->cg, OP_FS_DIR);
|
||||
}
|
||||
} else {
|
||||
// Dir with no parens -- next match
|
||||
basEmit8(&p->cg, OP_FS_DIR_NEXT);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// FileLen(filename) -- file size without opening
|
||||
if (tt == TOK_FILELEN) {
|
||||
advance(p);
|
||||
expect(p, TOK_LPAREN);
|
||||
parseExpression(p);
|
||||
expect(p, TOK_RPAREN);
|
||||
basEmit8(&p->cg, OP_FS_FILELEN);
|
||||
return;
|
||||
}
|
||||
|
||||
// GetAttr(filename) -- file attributes
|
||||
if (tt == TOK_GETATTR) {
|
||||
advance(p);
|
||||
expect(p, TOK_LPAREN);
|
||||
parseExpression(p);
|
||||
expect(p, TOK_RPAREN);
|
||||
basEmit8(&p->cg, OP_FS_GETATTR);
|
||||
return;
|
||||
}
|
||||
|
||||
// InputBox$(prompt [, title [, default]])
|
||||
if (tt == TOK_INPUTBOX) {
|
||||
advance(p);
|
||||
|
|
@ -4563,6 +4632,76 @@ static void parseSelectCase(BasParserT *p) {
|
|||
}
|
||||
|
||||
|
||||
static void parseChDir(BasParserT *p) {
|
||||
// CHDIR path
|
||||
advance(p);
|
||||
parseExpression(p);
|
||||
basEmit8(&p->cg, OP_FS_CHDIR);
|
||||
}
|
||||
|
||||
|
||||
static void parseChDrive(BasParserT *p) {
|
||||
// CHDRIVE drive
|
||||
advance(p);
|
||||
parseExpression(p);
|
||||
basEmit8(&p->cg, OP_FS_CHDRIVE);
|
||||
}
|
||||
|
||||
|
||||
static void parseFileCopy(BasParserT *p) {
|
||||
// FILECOPY source, dest
|
||||
advance(p);
|
||||
parseExpression(p);
|
||||
expect(p, TOK_COMMA);
|
||||
parseExpression(p);
|
||||
basEmit8(&p->cg, OP_FS_FILECOPY);
|
||||
}
|
||||
|
||||
|
||||
static void parseKill(BasParserT *p) {
|
||||
// KILL filename
|
||||
advance(p);
|
||||
parseExpression(p);
|
||||
basEmit8(&p->cg, OP_FS_KILL);
|
||||
}
|
||||
|
||||
|
||||
static void parseMkDir(BasParserT *p) {
|
||||
// MKDIR path
|
||||
advance(p);
|
||||
parseExpression(p);
|
||||
basEmit8(&p->cg, OP_FS_MKDIR);
|
||||
}
|
||||
|
||||
|
||||
static void parseName(BasParserT *p) {
|
||||
// NAME oldname AS newname
|
||||
advance(p);
|
||||
parseExpression(p);
|
||||
expect(p, TOK_AS);
|
||||
parseExpression(p);
|
||||
basEmit8(&p->cg, OP_FS_NAME);
|
||||
}
|
||||
|
||||
|
||||
static void parseRmDir(BasParserT *p) {
|
||||
// RMDIR path
|
||||
advance(p);
|
||||
parseExpression(p);
|
||||
basEmit8(&p->cg, OP_FS_RMDIR);
|
||||
}
|
||||
|
||||
|
||||
static void parseSetAttr(BasParserT *p) {
|
||||
// SETATTR filename, attributes
|
||||
advance(p);
|
||||
parseExpression(p);
|
||||
expect(p, TOK_COMMA);
|
||||
parseExpression(p);
|
||||
basEmit8(&p->cg, OP_FS_SETATTR);
|
||||
}
|
||||
|
||||
|
||||
static void parseShell(BasParserT *p) {
|
||||
// SHELL "command" -- execute an OS command (discard return value)
|
||||
// SHELL -- no argument, no-op in embedded context
|
||||
|
|
@ -4793,14 +4932,38 @@ static void parseStatement(BasParserT *p) {
|
|||
parseRedim(p);
|
||||
break;
|
||||
|
||||
case TOK_FILECOPY:
|
||||
parseFileCopy(p);
|
||||
break;
|
||||
|
||||
case TOK_INPUT:
|
||||
parseInput(p);
|
||||
break;
|
||||
|
||||
case TOK_KILL:
|
||||
parseKill(p);
|
||||
break;
|
||||
|
||||
case TOK_MKDIR:
|
||||
parseMkDir(p);
|
||||
break;
|
||||
|
||||
case TOK_NAME:
|
||||
parseName(p);
|
||||
break;
|
||||
|
||||
case TOK_OPEN:
|
||||
parseOpen(p);
|
||||
break;
|
||||
|
||||
case TOK_CHDIR:
|
||||
parseChDir(p);
|
||||
break;
|
||||
|
||||
case TOK_CHDRIVE:
|
||||
parseChDrive(p);
|
||||
break;
|
||||
|
||||
case TOK_CLOSE:
|
||||
parseClose(p);
|
||||
break;
|
||||
|
|
@ -4849,6 +5012,14 @@ static void parseStatement(BasParserT *p) {
|
|||
parseResume(p);
|
||||
break;
|
||||
|
||||
case TOK_RMDIR:
|
||||
parseRmDir(p);
|
||||
break;
|
||||
|
||||
case TOK_SETATTR:
|
||||
parseSetAttr(p);
|
||||
break;
|
||||
|
||||
case TOK_RETURN:
|
||||
advance(p);
|
||||
if (p->sym.inLocalScope) {
|
||||
|
|
|
|||
|
|
@ -624,6 +624,11 @@ BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName) {
|
|||
return basValStringFromC(text ? text : "");
|
||||
}
|
||||
|
||||
// Help topic
|
||||
if (strcasecmp(propName, "HelpTopic") == 0) {
|
||||
return basValStringFromC(ctrl->helpTopic);
|
||||
}
|
||||
|
||||
// Data binding properties
|
||||
if (strcasecmp(propName, "DataSource") == 0) {
|
||||
return basValStringFromC(ctrl->dataSource);
|
||||
|
|
@ -1071,6 +1076,16 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
|
|||
continue;
|
||||
}
|
||||
|
||||
// HelpTopic is stored on BasControlT, not on the widget
|
||||
if (strcasecmp(key, "HelpTopic") == 0) {
|
||||
char *text = value;
|
||||
if (text[0] == '"') { text++; }
|
||||
int32_t tlen = (int32_t)strlen(text);
|
||||
if (tlen > 0 && text[tlen - 1] == '"') { text[tlen - 1] = '\0'; }
|
||||
snprintf(current->helpTopic, sizeof(current->helpTopic), "%s", text);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Layout property on a container: replace the parentStack entry
|
||||
// with a layout box inside the container widget.
|
||||
// NOTE: only do this for non-default layouts (HBox, WrapBox).
|
||||
|
|
@ -1134,6 +1149,8 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
|
|||
form->frmAutoSize = (strcasecmp(text, "True") == 0);
|
||||
} else if (strcasecmp(key, "Layout") == 0) {
|
||||
snprintf(form->frmLayout, sizeof(form->frmLayout), "%s", text);
|
||||
} else if (strcasecmp(key, "HelpTopic") == 0) {
|
||||
snprintf(form->helpTopic, sizeof(form->helpTopic), "%s", text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1453,6 +1470,14 @@ void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT
|
|||
return;
|
||||
}
|
||||
|
||||
// Help topic
|
||||
if (strcasecmp(propName, "HelpTopic") == 0) {
|
||||
BasStringT *s = basValFormatString(value);
|
||||
snprintf(ctrl->helpTopic, BAS_MAX_CTRL_NAME, "%s", s->data);
|
||||
basStringUnref(s);
|
||||
return;
|
||||
}
|
||||
|
||||
// Data binding properties (stored on BasControlT, not on the widget)
|
||||
if (strcasecmp(propName, "DataSource") == 0) {
|
||||
BasStringT *s = basValFormatString(value);
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ typedef struct BasControlT {
|
|||
char textBuf[BAS_MAX_TEXT_BUF]; // persistent text for Caption/Text
|
||||
char dataSource[BAS_MAX_CTRL_NAME]; // name of Data control for binding
|
||||
char dataField[BAS_MAX_CTRL_NAME]; // column name for binding
|
||||
char helpTopic[BAS_MAX_CTRL_NAME]; // help topic ID for F1
|
||||
int32_t menuId; // WM menu item ID (>0 for menu items, 0 for controls)
|
||||
} BasControlT;
|
||||
|
||||
|
|
@ -81,6 +82,7 @@ typedef struct BasFormT {
|
|||
bool frmCentered;
|
||||
bool frmAutoSize;
|
||||
char frmLayout[32]; // "VBox", "HBox", or "WrapBox"
|
||||
char helpTopic[BAS_MAX_CTRL_NAME]; // form-level help topic
|
||||
// Per-form variable storage (allocated at load, freed at unload)
|
||||
BasValueT *formVars;
|
||||
int32_t formVarCount;
|
||||
|
|
@ -110,6 +112,7 @@ typedef struct {
|
|||
BasFormT **forms; // stb_ds array of heap-allocated pointers
|
||||
int32_t formCount;
|
||||
BasFormT *currentForm; // form currently dispatching events
|
||||
char helpFile[256]; // project help file path (for F1)
|
||||
BasFrmCacheT *frmCache; // stb_ds array of cached .frm sources
|
||||
int32_t frmCacheCount;
|
||||
} BasFormRtT;
|
||||
|
|
|
|||
|
|
@ -749,8 +749,9 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
|
|||
else if (strcasecmp(key, "MaxWidth") == 0) { curCtrl->maxWidth = atoi(val); }
|
||||
else if (strcasecmp(key, "MaxHeight") == 0) { curCtrl->maxHeight = atoi(val); }
|
||||
else if (strcasecmp(key, "Weight") == 0) { curCtrl->weight = atoi(val); }
|
||||
else if (strcasecmp(key, "Index") == 0) { curCtrl->index = atoi(val); }
|
||||
else if (strcasecmp(key, "TabIndex") == 0) { /* ignored -- DVX has no tab order */ }
|
||||
else if (strcasecmp(key, "Index") == 0) { curCtrl->index = atoi(val); }
|
||||
else if (strcasecmp(key, "HelpTopic") == 0) { snprintf(curCtrl->helpTopic, DSGN_MAX_NAME, "%s", 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); }
|
||||
|
|
@ -762,6 +763,7 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
|
|||
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; }
|
||||
else if (strcasecmp(key, "HelpTopic") == 0) { snprintf(form->helpTopic, DSGN_MAX_NAME, "%s", val); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1140,6 +1142,10 @@ static int32_t saveControls(const DsgnFormT *form, char *buf, int32_t bufSize, i
|
|||
pos += snprintf(buf + pos, bufSize - pos, "%s Weight = %d\n", pad, (int)ctrl->weight);
|
||||
}
|
||||
|
||||
if (ctrl->helpTopic[0]) {
|
||||
pos += snprintf(buf + pos, bufSize - pos, "%s HelpTopic = \"%s\"\n", pad, ctrl->helpTopic);
|
||||
}
|
||||
|
||||
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; }
|
||||
|
|
@ -1236,6 +1242,10 @@ int32_t dsgnSaveFrm(const DsgnStateT *ds, char *buf, int32_t bufSize) {
|
|||
pos += snprintf(buf + pos, bufSize - pos, " Height = %d\n", (int)ds->form->height);
|
||||
}
|
||||
|
||||
if (ds->form->helpTopic[0]) {
|
||||
pos += snprintf(buf + pos, bufSize - pos, " HelpTopic = \"%s\"\n", ds->form->helpTopic);
|
||||
}
|
||||
|
||||
// Output menu items as nested Begin Menu blocks
|
||||
{
|
||||
int32_t menuCount = (int32_t)arrlen(ds->form->menuItems);
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ typedef struct {
|
|||
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)
|
||||
char helpTopic[DSGN_MAX_NAME]; // help topic ID for F1
|
||||
DsgnPropT props[DSGN_MAX_PROPS];
|
||||
int32_t propCount;
|
||||
WidgetT *widget; // live widget (created at design time for WYSIWYG)
|
||||
|
|
@ -86,6 +87,7 @@ typedef struct {
|
|||
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
|
||||
char helpTopic[DSGN_MAX_NAME]; // form-level help topic
|
||||
DsgnControlT **controls; // stb_ds array of heap-allocated pointers
|
||||
DsgnMenuItemT *menuItems; // stb_ds dynamic array (NULL if no menus)
|
||||
bool dirty;
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@
|
|||
#include "radio/radio.h"
|
||||
#include "textInput/textInpt.h"
|
||||
#include "dropdown/dropdown.h"
|
||||
#include "canvas/canvas.h"
|
||||
#include "listBox/listBox.h"
|
||||
#include "slider/slider.h"
|
||||
#include "tabControl/tabCtrl.h"
|
||||
#include "button/button.h"
|
||||
#include "splitter/splitter.h"
|
||||
#include "statusBar/statBar.h"
|
||||
|
|
@ -470,6 +474,7 @@ static void helpBuildCtrlTopic(const char *typeName, char *buf, int32_t bufSize)
|
|||
// ============================================================
|
||||
|
||||
static DxeAppContextT *sCtx = NULL;
|
||||
static char sIdeHelpFile[DVX_MAX_PATH]; // IDE help file (restored after program run)
|
||||
static AppContextT *sAc = NULL;
|
||||
static PrefsHandleT *sPrefs = NULL;
|
||||
static WindowT *sWin = NULL; // Main toolbar window
|
||||
|
|
@ -794,6 +799,7 @@ int32_t appMain(DxeAppContextT *ctx) {
|
|||
|
||||
// Set help file and context-sensitive F1 handler
|
||||
snprintf(ctx->helpFile, sizeof(ctx->helpFile), "%s%c%s", ctx->appDir, DVX_PATH_SEP, "dvxbasic.hlp");
|
||||
snprintf(sIdeHelpFile, sizeof(sIdeHelpFile), "%s", ctx->helpFile);
|
||||
ctx->onHelpQuery = helpQueryHandler;
|
||||
ctx->helpQueryCtx = NULL;
|
||||
|
||||
|
|
@ -2171,6 +2177,12 @@ static void runModule(BasModuleT *mod) {
|
|||
sDbgFormRt = formRt;
|
||||
sDbgModule = mod;
|
||||
sDbgState = DBG_RUNNING;
|
||||
|
||||
// Set project help file on form runtime for F1 context help
|
||||
if (sProject.helpFile[0]) {
|
||||
snprintf(formRt->helpFile, sizeof(formRt->helpFile), "%s%c%s",
|
||||
sProject.projectDir, DVX_PATH_SEP, sProject.helpFile);
|
||||
}
|
||||
updateProjectMenuState();
|
||||
|
||||
// Set breakpoints BEFORE loading forms so breakpoints in form
|
||||
|
|
@ -2206,7 +2218,6 @@ static void runModule(BasModuleT *mod) {
|
|||
// Run in slices of 10000 steps, yielding to DVX between slices
|
||||
basVmSetStepLimit(vm, IDE_STEP_SLICE);
|
||||
|
||||
int32_t totalSteps = 0;
|
||||
BasVmResultE result;
|
||||
sStopRequested = false;
|
||||
|
||||
|
|
@ -2228,7 +2239,6 @@ static void runModule(BasModuleT *mod) {
|
|||
}
|
||||
|
||||
result = basVmRun(vm);
|
||||
totalSteps += vm->stepCount;
|
||||
|
||||
if (result == BAS_VM_BREAKPOINT) {
|
||||
sDbgState = DBG_PAUSED;
|
||||
|
|
@ -2278,8 +2288,6 @@ static void runModule(BasModuleT *mod) {
|
|||
sStopRequested = false;
|
||||
|
||||
while (sWin && sAc->running && formRt->formCount > 0 && !sStopRequested && !vm->ended) {
|
||||
totalSteps += vm->stepCount;
|
||||
vm->stepCount = 0;
|
||||
|
||||
if (sDbgState == DBG_PAUSED) {
|
||||
// Paused inside an event handler
|
||||
|
|
@ -2321,9 +2329,7 @@ static void runModule(BasModuleT *mod) {
|
|||
updateProjectMenuState();
|
||||
setOutputText(sOutputBuf);
|
||||
|
||||
static char statusBuf[128];
|
||||
snprintf(statusBuf, sizeof(statusBuf), "Done. %ld instructions executed.", (long)totalSteps);
|
||||
setStatus(statusBuf);
|
||||
setStatus("Done.");
|
||||
|
||||
// Restore IDE windows
|
||||
if (hadFormWin && sFormWin) { dvxShowWindow(sAc, sFormWin); }
|
||||
|
|
@ -4691,9 +4697,34 @@ static void handleViewCmd(int32_t cmd) {
|
|||
// Preferences dialog
|
||||
// ============================================================
|
||||
|
||||
// Color entry names for the Colors tab (matches SYNTAX_* indices)
|
||||
static const char *sSyntaxColorNames[] = {
|
||||
"Default Text", // 0 = SYNTAX_DEFAULT
|
||||
"Keywords", // 1 = SYNTAX_KEYWORD
|
||||
"Strings", // 2 = SYNTAX_STRING
|
||||
"Comments", // 3 = SYNTAX_COMMENT
|
||||
"Numbers", // 4 = SYNTAX_NUMBER
|
||||
"Operators", // 5 = SYNTAX_OPERATOR
|
||||
"Types", // 6 = SYNTAX_TYPE
|
||||
};
|
||||
|
||||
#define SYNTAX_COLOR_COUNT 7
|
||||
|
||||
// Default syntax colors (0x00RRGGBB; 0 = use widget default)
|
||||
static const uint32_t sDefaultSyntaxColors[SYNTAX_COLOR_COUNT] = {
|
||||
0x00000000, // default -- not used (widget fg)
|
||||
0x00000080, // keyword -- dark blue
|
||||
0x00800000, // string -- dark red
|
||||
0x00008000, // comment -- dark green
|
||||
0x00800080, // number -- purple
|
||||
0x00808000, // operator -- dark yellow
|
||||
0x00008080, // type -- teal
|
||||
};
|
||||
|
||||
static struct {
|
||||
bool done;
|
||||
bool accepted;
|
||||
// General tab
|
||||
WidgetT *renameSkipComments;
|
||||
WidgetT *optionExplicit;
|
||||
WidgetT *tabWidthInput;
|
||||
|
|
@ -4703,6 +4734,16 @@ static struct {
|
|||
WidgetT *defVersion;
|
||||
WidgetT *defCopyright;
|
||||
WidgetT *defDescription;
|
||||
// Colors tab
|
||||
WidgetT *colorList;
|
||||
WidgetT *sliderR;
|
||||
WidgetT *sliderG;
|
||||
WidgetT *sliderB;
|
||||
WidgetT *lblR;
|
||||
WidgetT *lblG;
|
||||
WidgetT *lblB;
|
||||
WidgetT *colorSwatch;
|
||||
uint32_t syntaxColors[SYNTAX_COLOR_COUNT];
|
||||
} sPrefsDlg;
|
||||
|
||||
|
||||
|
|
@ -4719,10 +4760,97 @@ static void onPrefsCancel(WidgetT *w) {
|
|||
}
|
||||
|
||||
|
||||
static void prefsUpdateSwatch(void) {
|
||||
if (!sPrefsDlg.colorSwatch) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t r = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderR);
|
||||
uint8_t g = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderG);
|
||||
uint8_t b = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderB);
|
||||
|
||||
uint32_t color = packColor(&sAc->display, r, g, b);
|
||||
wgtCanvasClear(sPrefsDlg.colorSwatch, color);
|
||||
}
|
||||
|
||||
|
||||
static void prefsUpdateColorSliders(void) {
|
||||
int32_t idx = wgtListBoxGetSelected(sPrefsDlg.colorList);
|
||||
|
||||
if (idx < 0 || idx >= SYNTAX_COLOR_COUNT) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t c = sPrefsDlg.syntaxColors[idx];
|
||||
uint8_t r = (c >> 16) & 0xFF;
|
||||
uint8_t g = (c >> 8) & 0xFF;
|
||||
uint8_t b = c & 0xFF;
|
||||
|
||||
wgtSliderSetValue(sPrefsDlg.sliderR, r);
|
||||
wgtSliderSetValue(sPrefsDlg.sliderG, g);
|
||||
wgtSliderSetValue(sPrefsDlg.sliderB, b);
|
||||
|
||||
static char rBuf[8];
|
||||
static char gBuf[8];
|
||||
static char bBuf[8];
|
||||
snprintf(rBuf, sizeof(rBuf), "%d", (int)r);
|
||||
snprintf(gBuf, sizeof(gBuf), "%d", (int)g);
|
||||
snprintf(bBuf, sizeof(bBuf), "%d", (int)b);
|
||||
wgtSetText(sPrefsDlg.lblR, rBuf);
|
||||
wgtSetText(sPrefsDlg.lblG, gBuf);
|
||||
wgtSetText(sPrefsDlg.lblB, bBuf);
|
||||
|
||||
prefsUpdateSwatch();
|
||||
}
|
||||
|
||||
|
||||
static void onColorListChange(WidgetT *w) {
|
||||
(void)w;
|
||||
prefsUpdateColorSliders();
|
||||
}
|
||||
|
||||
|
||||
static void onColorSliderChange(WidgetT *w) {
|
||||
(void)w;
|
||||
|
||||
int32_t idx = wgtListBoxGetSelected(sPrefsDlg.colorList);
|
||||
|
||||
if (idx < 0 || idx >= SYNTAX_COLOR_COUNT) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t r = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderR);
|
||||
uint8_t g = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderG);
|
||||
uint8_t b = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderB);
|
||||
|
||||
static char rBuf[8];
|
||||
static char gBuf[8];
|
||||
static char bBuf[8];
|
||||
snprintf(rBuf, sizeof(rBuf), "%d", (int)r);
|
||||
snprintf(gBuf, sizeof(gBuf), "%d", (int)g);
|
||||
snprintf(bBuf, sizeof(bBuf), "%d", (int)b);
|
||||
wgtSetText(sPrefsDlg.lblR, rBuf);
|
||||
wgtSetText(sPrefsDlg.lblG, gBuf);
|
||||
wgtSetText(sPrefsDlg.lblB, bBuf);
|
||||
|
||||
sPrefsDlg.syntaxColors[idx] = ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b;
|
||||
prefsUpdateSwatch();
|
||||
}
|
||||
|
||||
|
||||
static void applySyntaxColors(void) {
|
||||
if (!sEditor) {
|
||||
return;
|
||||
}
|
||||
|
||||
wgtTextAreaSetSyntaxColors(sEditor, sPrefsDlg.syntaxColors, SYNTAX_COLOR_COUNT);
|
||||
}
|
||||
|
||||
|
||||
static void showPreferencesDialog(void) {
|
||||
memset(&sPrefsDlg, 0, sizeof(sPrefsDlg));
|
||||
|
||||
WindowT *win = dvxCreateWindowCentered(sAc, "Preferences", 400, 440, false);
|
||||
WindowT *win = dvxCreateWindowCentered(sAc, "Preferences", 420, 440, false);
|
||||
|
||||
if (!win) {
|
||||
return;
|
||||
|
|
@ -4734,8 +4862,16 @@ static void showPreferencesDialog(void) {
|
|||
WidgetT *root = wgtInitWindow(sAc, win);
|
||||
root->spacing = wgtPixels(4);
|
||||
|
||||
// ---- Editor section ----
|
||||
WidgetT *edFrame = wgtFrame(root, "Editor");
|
||||
// ---- Tab control ----
|
||||
WidgetT *tabs = wgtTabControl(root);
|
||||
tabs->weight = 100;
|
||||
|
||||
// ======== General tab ========
|
||||
WidgetT *generalPage = wgtTabPage(tabs, "General");
|
||||
generalPage->spacing = wgtPixels(4);
|
||||
|
||||
// Editor section
|
||||
WidgetT *edFrame = wgtFrame(generalPage, "Editor");
|
||||
edFrame->spacing = wgtPixels(2);
|
||||
|
||||
sPrefsDlg.renameSkipComments = wgtCheckbox(edFrame, "Skip comments/strings when renaming");
|
||||
|
|
@ -4757,9 +4893,10 @@ static void showPreferencesDialog(void) {
|
|||
sPrefsDlg.useSpaces = wgtCheckbox(edFrame, "Insert spaces instead of tabs");
|
||||
wgtCheckboxSetChecked(sPrefsDlg.useSpaces, prefsGetBool(sPrefs, "editor", "useSpaces", true));
|
||||
|
||||
// ---- Project Defaults section ----
|
||||
WidgetT *prjFrame = wgtFrame(root, "New Project Defaults");
|
||||
// Project Defaults section
|
||||
WidgetT *prjFrame = wgtFrame(generalPage, "New Project Defaults");
|
||||
prjFrame->spacing = wgtPixels(2);
|
||||
prjFrame->weight = 100;
|
||||
|
||||
WidgetT *r1 = wgtHBox(prjFrame);
|
||||
r1->spacing = wgtPixels(4);
|
||||
|
|
@ -4799,6 +4936,60 @@ static void showPreferencesDialog(void) {
|
|||
sPrefsDlg.defDescription->minH = wgtPixels(48);
|
||||
wgtSetText(sPrefsDlg.defDescription, prefsGetString(sPrefs, "defaults", "description", ""));
|
||||
|
||||
// ======== Colors tab ========
|
||||
WidgetT *colorsPage = wgtTabPage(tabs, "Colors");
|
||||
colorsPage->spacing = wgtPixels(4);
|
||||
|
||||
// Load current colors from prefs (or defaults)
|
||||
for (int32_t i = 0; i < SYNTAX_COLOR_COUNT; i++) {
|
||||
char key[32];
|
||||
snprintf(key, sizeof(key), "color%d", (int)i);
|
||||
sPrefsDlg.syntaxColors[i] = (uint32_t)prefsGetInt(sPrefs, "syntax", key, (int32_t)sDefaultSyntaxColors[i]);
|
||||
}
|
||||
|
||||
WidgetT *colorsHBox = wgtHBox(colorsPage);
|
||||
colorsHBox->spacing = wgtPixels(8);
|
||||
colorsHBox->weight = 100;
|
||||
|
||||
// Left: color list
|
||||
sPrefsDlg.colorList = wgtListBox(colorsHBox);
|
||||
sPrefsDlg.colorList->weight = 100;
|
||||
sPrefsDlg.colorList->onChange = onColorListChange;
|
||||
|
||||
wgtListBoxSetItems(sPrefsDlg.colorList, sSyntaxColorNames, SYNTAX_COLOR_COUNT);
|
||||
|
||||
// Right: RGB sliders + value labels + swatch preview
|
||||
WidgetT *sliderBox = wgtVBox(colorsHBox);
|
||||
sliderBox->spacing = wgtPixels(2);
|
||||
sliderBox->weight = 100;
|
||||
|
||||
wgtLabel(sliderBox, "Red:");
|
||||
sPrefsDlg.sliderR = wgtSlider(sliderBox, 0, 255);
|
||||
sPrefsDlg.sliderR->onChange = onColorSliderChange;
|
||||
sPrefsDlg.lblR = wgtLabel(sliderBox, "0");
|
||||
wgtLabelSetAlign(sPrefsDlg.lblR, AlignEndE);
|
||||
|
||||
wgtLabel(sliderBox, "Green:");
|
||||
sPrefsDlg.sliderG = wgtSlider(sliderBox, 0, 255);
|
||||
sPrefsDlg.sliderG->onChange = onColorSliderChange;
|
||||
sPrefsDlg.lblG = wgtLabel(sliderBox, "0");
|
||||
wgtLabelSetAlign(sPrefsDlg.lblG, AlignEndE);
|
||||
|
||||
wgtLabel(sliderBox, "Blue:");
|
||||
sPrefsDlg.sliderB = wgtSlider(sliderBox, 0, 255);
|
||||
sPrefsDlg.sliderB->onChange = onColorSliderChange;
|
||||
sPrefsDlg.lblB = wgtLabel(sliderBox, "0");
|
||||
wgtLabelSetAlign(sPrefsDlg.lblB, AlignEndE);
|
||||
|
||||
wgtLabel(sliderBox, "Preview:");
|
||||
sPrefsDlg.colorSwatch = wgtCanvas(sliderBox, 64, 24);
|
||||
|
||||
// Select first color entry and load sliders
|
||||
wgtListBoxSetSelected(sPrefsDlg.colorList, 1);
|
||||
prefsUpdateColorSliders();
|
||||
|
||||
wgtTabControlSetActive(tabs, 0);
|
||||
|
||||
// ---- OK / Cancel ----
|
||||
WidgetT *btnRow = wgtHBox(root);
|
||||
btnRow->spacing = wgtPixels(8);
|
||||
|
|
@ -4822,6 +5013,7 @@ static void showPreferencesDialog(void) {
|
|||
}
|
||||
|
||||
if (sPrefsDlg.accepted) {
|
||||
// General tab
|
||||
prefsSetBool(sPrefs, "editor", "renameSkipComments", wgtCheckboxIsChecked(sPrefsDlg.renameSkipComments));
|
||||
prefsSetBool(sPrefs, "editor", "optionExplicit", wgtCheckboxIsChecked(sPrefsDlg.optionExplicit));
|
||||
prefsSetBool(sPrefs, "editor", "useSpaces", wgtCheckboxIsChecked(sPrefsDlg.useSpaces));
|
||||
|
|
@ -4856,6 +5048,14 @@ static void showPreferencesDialog(void) {
|
|||
val = wgtGetText(sPrefsDlg.defDescription);
|
||||
prefsSetString(sPrefs, "defaults", "description", val ? val : "");
|
||||
|
||||
// Colors tab
|
||||
for (int32_t i = 0; i < SYNTAX_COLOR_COUNT; i++) {
|
||||
char key[32];
|
||||
snprintf(key, sizeof(key), "color%d", (int)i);
|
||||
prefsSetInt(sPrefs, "syntax", key, (int32_t)sPrefsDlg.syntaxColors[i]);
|
||||
}
|
||||
|
||||
applySyntaxColors();
|
||||
prefsSave(sPrefs);
|
||||
}
|
||||
|
||||
|
|
@ -4959,12 +5159,48 @@ static void helpQueryHandler(void *ctx) {
|
|||
|
||||
sCtx->helpTopic[0] = '\0';
|
||||
|
||||
// Restore IDE help file (may have been swapped for a BASIC program's)
|
||||
snprintf(sCtx->helpFile, sizeof(sCtx->helpFile), "%s", sIdeHelpFile);
|
||||
|
||||
// Determine which window is focused
|
||||
WindowT *focusWin = NULL;
|
||||
if (sAc->stack.focusedIdx >= 0 && sAc->stack.focusedIdx < sAc->stack.count) {
|
||||
focusWin = sAc->stack.windows[sAc->stack.focusedIdx];
|
||||
}
|
||||
|
||||
// Running BASIC program: check if focused window belongs to a form
|
||||
if (sDbgFormRt && sDbgFormRt->helpFile[0] && focusWin) {
|
||||
for (int32_t i = 0; i < sDbgFormRt->formCount; i++) {
|
||||
BasFormT *form = sDbgFormRt->forms[i];
|
||||
|
||||
if (form->window == focusWin) {
|
||||
// Swap to the project's help file
|
||||
snprintf(sCtx->helpFile, sizeof(sCtx->helpFile), "%s", sDbgFormRt->helpFile);
|
||||
|
||||
// Find the focused widget's HelpTopic
|
||||
WidgetT *focused = wgtGetFocused();
|
||||
|
||||
if (focused && focused->userData) {
|
||||
BasControlT *ctrl = (BasControlT *)focused->userData;
|
||||
|
||||
if (ctrl->helpTopic[0]) {
|
||||
snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "%s", ctrl->helpTopic);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to form-level HelpTopic
|
||||
if (form->helpTopic[0]) {
|
||||
snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "%s", form->helpTopic);
|
||||
return;
|
||||
}
|
||||
|
||||
// No topic set -- open help at default
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Code editor: look up the word under the cursor
|
||||
if (focusWin == sCodeWin && sEditor) {
|
||||
char word[128];
|
||||
|
|
@ -6893,6 +7129,20 @@ static void showCodeWindow(void) {
|
|||
sEditor = wgtTextArea(codeRoot, IDE_MAX_SOURCE);
|
||||
sEditor->weight = 100;
|
||||
wgtTextAreaSetColorize(sEditor, basicColorize, NULL);
|
||||
|
||||
// Apply saved syntax colors
|
||||
{
|
||||
uint32_t initColors[SYNTAX_COLOR_COUNT];
|
||||
|
||||
for (int32_t i = 0; i < SYNTAX_COLOR_COUNT; i++) {
|
||||
char key[32];
|
||||
snprintf(key, sizeof(key), "color%d", (int)i);
|
||||
initColors[i] = (uint32_t)prefsGetInt(sPrefs, "syntax", key, (int32_t)sDefaultSyntaxColors[i]);
|
||||
}
|
||||
|
||||
wgtTextAreaSetSyntaxColors(sEditor, initColors, SYNTAX_COLOR_COUNT);
|
||||
}
|
||||
|
||||
wgtTextAreaSetLineDecorator(sEditor, debugLineDecorator, sAc);
|
||||
wgtTextAreaSetGutterClick(sEditor, onGutterClick);
|
||||
wgtTextAreaSetShowLineNumbers(sEditor, true);
|
||||
|
|
|
|||
|
|
@ -148,6 +148,9 @@ bool prjLoad(PrjStateT *prj, const char *dbpPath) {
|
|||
val = prefsGetString(h, "Project", "Icon", NULL);
|
||||
if (val) { snprintf(prj->iconPath, sizeof(prj->iconPath), "%s", val); }
|
||||
|
||||
val = prefsGetString(h, "Project", "HelpFile", NULL);
|
||||
if (val) { snprintf(prj->helpFile, sizeof(prj->helpFile), "%s", val); }
|
||||
|
||||
// [Modules] section -- File0, File1, ...
|
||||
for (int32_t i = 0; i < PRJ_MAX_FILES; i++) {
|
||||
char key[16];
|
||||
|
|
@ -208,6 +211,7 @@ bool prjSave(const PrjStateT *prj) {
|
|||
if (prj->copyright[0]) { prefsSetString(h, "Project", "Copyright", prj->copyright); }
|
||||
if (prj->description[0]) { prefsSetString(h, "Project", "Description", prj->description); }
|
||||
if (prj->iconPath[0]) { prefsSetString(h, "Project", "Icon", prj->iconPath); }
|
||||
if (prj->helpFile[0]) { prefsSetString(h, "Project", "HelpFile", prj->helpFile); }
|
||||
|
||||
// [Modules] section
|
||||
int32_t modIdx = 0;
|
||||
|
|
@ -620,6 +624,7 @@ static struct {
|
|||
WidgetT *description;
|
||||
WidgetT *startupForm;
|
||||
const char **formNames; // stb_ds array of form name strings for startup dropdown
|
||||
WidgetT *helpFileInput;
|
||||
WidgetT *iconPreview;
|
||||
char iconPath[DVX_MAX_PATH];
|
||||
const char *appPath;
|
||||
|
|
@ -801,6 +806,33 @@ static void ppdOnBrowseIcon(WidgetT *w) {
|
|||
}
|
||||
|
||||
|
||||
static void ppdOnBrowseHelp(WidgetT *w) {
|
||||
(void)w;
|
||||
|
||||
FileFilterT filters[] = {
|
||||
{ "Help Files (*.hlp)", "*.hlp" },
|
||||
{ "All Files (*.*)", "*.*" }
|
||||
};
|
||||
|
||||
char path[DVX_MAX_PATH];
|
||||
|
||||
if (!dvxFileDialog(sPpd.ctx, "Select Help File", FD_SAVE, NULL, filters, 2, path, sizeof(path))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert to project-relative path
|
||||
const char *relPath = path;
|
||||
int32_t dirLen = (int32_t)strlen(sPpd.prj->projectDir);
|
||||
|
||||
if (strncasecmp(path, sPpd.prj->projectDir, dirLen) == 0 &&
|
||||
(path[dirLen] == '/' || path[dirLen] == '\\')) {
|
||||
relPath = path + dirLen + 1;
|
||||
}
|
||||
|
||||
wgtSetText(sPpd.helpFileInput, relPath);
|
||||
}
|
||||
|
||||
|
||||
static WidgetT *ppdAddRow(WidgetT *parent, const char *labelText, const char *value, int32_t maxLen) {
|
||||
WidgetT *row = wgtHBox(parent);
|
||||
row->spacing = wgtPixels(4);
|
||||
|
|
@ -927,6 +959,22 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
|
|||
ppdLoadIconPreview();
|
||||
}
|
||||
|
||||
// Help file row
|
||||
{
|
||||
WidgetT *hlpRow = wgtHBox(root);
|
||||
hlpRow->spacing = wgtPixels(4);
|
||||
|
||||
WidgetT *hlpLbl = wgtLabel(hlpRow, "Help File:");
|
||||
hlpLbl->minW = wgtPixels(PPD_LABEL_W);
|
||||
|
||||
sPpd.helpFileInput = wgtTextInput(hlpRow, DVX_MAX_PATH);
|
||||
sPpd.helpFileInput->weight = 100;
|
||||
wgtSetText(sPpd.helpFileInput, prj->helpFile);
|
||||
|
||||
WidgetT *hlpBrowse = wgtButton(hlpRow, "Browse...");
|
||||
hlpBrowse->onClick = ppdOnBrowseHelp;
|
||||
}
|
||||
|
||||
// Description: label above, textarea below (matches Preferences layout)
|
||||
wgtLabel(root, "Description:");
|
||||
sPpd.description = wgtTextArea(root, PRJ_MAX_DESC);
|
||||
|
|
@ -980,6 +1028,9 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
|
|||
|
||||
snprintf(prj->iconPath, sizeof(prj->iconPath), "%s", sPpd.iconPath);
|
||||
|
||||
s = wgtGetText(sPpd.helpFileInput);
|
||||
if (s) { snprintf(prj->helpFile, sizeof(prj->helpFile), "%s", s); }
|
||||
|
||||
// Read startup form from dropdown
|
||||
if (sPpd.startupForm && sPpd.formNames) {
|
||||
int32_t sfIdx = wgtDropdownGetSelected(sPpd.startupForm);
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ typedef struct {
|
|||
char copyright[PRJ_MAX_STRING];
|
||||
char description[PRJ_MAX_DESC];
|
||||
char iconPath[DVX_MAX_PATH]; // relative path to icon BMP
|
||||
char helpFile[DVX_MAX_PATH]; // relative path to .hlp file
|
||||
PrjFileT *files; // stb_ds dynamic array
|
||||
int32_t fileCount;
|
||||
PrjSourceMapT *sourceMap; // stb_ds dynamic array
|
||||
|
|
|
|||
|
|
@ -319,6 +319,7 @@ static uint8_t getPropType(const char *propName, const char *typeName) {
|
|||
if (strcasecmp(propName, "Visible") == 0) { return PROP_TYPE_BOOL; }
|
||||
if (strcasecmp(propName, "Enabled") == 0) { return PROP_TYPE_BOOL; }
|
||||
if (strcasecmp(propName, "Layout") == 0) { return PROP_TYPE_LAYOUT; }
|
||||
if (strcasecmp(propName, "HelpTopic") == 0) { return PROP_TYPE_STRING; }
|
||||
if (strcasecmp(propName, "DataSource") == 0) { return PROP_TYPE_DATASOURCE; }
|
||||
if (strcasecmp(propName, "DataField") == 0) { return PROP_TYPE_DATAFIELD; }
|
||||
if (strcasecmp(propName, "RecordSource") == 0) { return PROP_TYPE_RECORDSRC; }
|
||||
|
|
@ -828,6 +829,8 @@ static void onPropDblClick(WidgetT *w) {
|
|||
bool vis = ctrl->widget ? ctrl->widget->visible : true;
|
||||
cascadeToChildren(sDs, ctrl->name, vis, val);
|
||||
}
|
||||
} else if (strcasecmp(propName, "HelpTopic") == 0) {
|
||||
snprintf(ctrl->helpTopic, DSGN_MAX_NAME, "%s", newValue);
|
||||
} else {
|
||||
// Try widget iface setter first
|
||||
bool ifaceHandled = false;
|
||||
|
|
@ -976,6 +979,8 @@ static void onPropDblClick(WidgetT *w) {
|
|||
} else if (strcasecmp(propName, "Height") == 0) {
|
||||
sDs->form->height = atoi(newValue);
|
||||
sDs->form->autoSize = false;
|
||||
} else if (strcasecmp(propName, "HelpTopic") == 0) {
|
||||
snprintf(sDs->form->helpTopic, DSGN_MAX_NAME, "%s", newValue);
|
||||
}
|
||||
|
||||
sDs->form->dirty = true;
|
||||
|
|
@ -1437,6 +1442,7 @@ void prpRefresh(DsgnStateT *ds) {
|
|||
|
||||
addPropRow("Visible", ctrl->widget && ctrl->widget->visible ? "True" : "False");
|
||||
addPropRow("Enabled", ctrl->widget && ctrl->widget->enabled ? "True" : "False");
|
||||
addPropRow("HelpTopic", ctrl->helpTopic);
|
||||
|
||||
for (int32_t i = 0; i < ctrl->propCount; i++) {
|
||||
addPropRow(ctrl->props[i].name, ctrl->props[i].value);
|
||||
|
|
@ -1521,6 +1527,8 @@ void prpRefresh(DsgnStateT *ds) {
|
|||
|
||||
snprintf(buf, sizeof(buf), "%d", (int)ds->form->height);
|
||||
addPropRow("Height", buf);
|
||||
|
||||
addPropRow("HelpTopic", ds->form->helpTopic);
|
||||
}
|
||||
|
||||
wgtListViewSetData(sPropList, (const char **)sCellData, sCellRows);
|
||||
|
|
|
|||
|
|
@ -9,10 +9,13 @@
|
|||
#include "../compiler/opcodes.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <dirent.h>
|
||||
#include <fnmatch.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
|
|
@ -29,7 +32,9 @@ static BasCallFrameT *currentFrame(BasVmT *vm);
|
|||
static void defaultPrint(void *ctx, const char *text, bool newline);
|
||||
static BasVmResultE execArith(BasVmT *vm, uint8_t op);
|
||||
static BasVmResultE execCompare(BasVmT *vm, uint8_t op);
|
||||
static void dirClose(void);
|
||||
static BasVmResultE execFileOp(BasVmT *vm, uint8_t op);
|
||||
static BasVmResultE execFsOp(BasVmT *vm, uint8_t op);
|
||||
static BasVmResultE execLogical(BasVmT *vm, uint8_t op);
|
||||
static BasVmResultE execMath(BasVmT *vm, uint8_t op);
|
||||
static BasVmResultE execPrint(BasVmT *vm);
|
||||
|
|
@ -318,6 +323,9 @@ void basVmDestroy(BasVmT *vm) {
|
|||
}
|
||||
}
|
||||
|
||||
// Close Dir$ iterator
|
||||
dirClose();
|
||||
|
||||
basStringSystemShutdown();
|
||||
free(vm);
|
||||
}
|
||||
|
|
@ -1868,6 +1876,21 @@ BasVmResultE basVmStep(BasVmT *vm) {
|
|||
case OP_FILE_INPUT_N:
|
||||
return execFileOp(vm, op);
|
||||
|
||||
case OP_FS_KILL:
|
||||
case OP_FS_NAME:
|
||||
case OP_FS_FILECOPY:
|
||||
case OP_FS_MKDIR:
|
||||
case OP_FS_RMDIR:
|
||||
case OP_FS_CHDIR:
|
||||
case OP_FS_CHDRIVE:
|
||||
case OP_FS_CURDIR:
|
||||
case OP_FS_DIR:
|
||||
case OP_FS_DIR_NEXT:
|
||||
case OP_FS_FILELEN:
|
||||
case OP_FS_GETATTR:
|
||||
case OP_FS_SETATTR:
|
||||
return execFsOp(vm, op);
|
||||
|
||||
// ============================================================
|
||||
// DoEvents
|
||||
// ============================================================
|
||||
|
|
@ -4443,6 +4466,426 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
|
|||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// execFsOp -- filesystem operations (Kill, Name, MkDir, etc.)
|
||||
// ============================================================
|
||||
|
||||
// Dir$ state -- persists across Dir$() calls within a VM session
|
||||
static DIR *sDirHandle = NULL;
|
||||
static char sDirPattern[260];
|
||||
static char sDirPath[260];
|
||||
|
||||
static void dirClose(void) {
|
||||
if (sDirHandle) {
|
||||
closedir(sDirHandle);
|
||||
sDirHandle = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static const char *dirNext(void) {
|
||||
if (!sDirHandle) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct dirent *ent;
|
||||
|
||||
while ((ent = readdir(sDirHandle)) != NULL) {
|
||||
if (fnmatch(sDirPattern, ent->d_name, FNM_CASEFOLD) == 0) {
|
||||
return ent->d_name;
|
||||
}
|
||||
}
|
||||
|
||||
dirClose();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) {
|
||||
switch (op) {
|
||||
case OP_FS_KILL: {
|
||||
BasValueT fnVal;
|
||||
|
||||
if (!pop(vm, &fnVal)) {
|
||||
return BAS_VM_STACK_UNDERFLOW;
|
||||
}
|
||||
|
||||
BasValueT fnStr = basValToString(fnVal);
|
||||
basValRelease(&fnVal);
|
||||
|
||||
if (remove(fnStr.strVal->data) != 0) {
|
||||
basValRelease(&fnStr);
|
||||
runtimeError(vm, 53, "File not found");
|
||||
return BAS_VM_FILE_ERROR;
|
||||
}
|
||||
|
||||
basValRelease(&fnStr);
|
||||
break;
|
||||
}
|
||||
|
||||
case OP_FS_NAME: {
|
||||
BasValueT newVal;
|
||||
BasValueT oldVal;
|
||||
|
||||
if (!pop(vm, &newVal) || !pop(vm, &oldVal)) {
|
||||
return BAS_VM_STACK_UNDERFLOW;
|
||||
}
|
||||
|
||||
BasValueT newStr = basValToString(newVal);
|
||||
BasValueT oldStr = basValToString(oldVal);
|
||||
basValRelease(&newVal);
|
||||
basValRelease(&oldVal);
|
||||
|
||||
if (rename(oldStr.strVal->data, newStr.strVal->data) != 0) {
|
||||
basValRelease(&oldStr);
|
||||
basValRelease(&newStr);
|
||||
runtimeError(vm, 58, "File already exists or rename failed");
|
||||
return BAS_VM_FILE_ERROR;
|
||||
}
|
||||
|
||||
basValRelease(&oldStr);
|
||||
basValRelease(&newStr);
|
||||
break;
|
||||
}
|
||||
|
||||
case OP_FS_FILECOPY: {
|
||||
BasValueT dstVal;
|
||||
BasValueT srcVal;
|
||||
|
||||
if (!pop(vm, &dstVal) || !pop(vm, &srcVal)) {
|
||||
return BAS_VM_STACK_UNDERFLOW;
|
||||
}
|
||||
|
||||
BasValueT dstStr = basValToString(dstVal);
|
||||
BasValueT srcStr = basValToString(srcVal);
|
||||
basValRelease(&dstVal);
|
||||
basValRelease(&srcVal);
|
||||
|
||||
FILE *fin = fopen(srcStr.strVal->data, "rb");
|
||||
FILE *fout = NULL;
|
||||
|
||||
if (fin) {
|
||||
fout = fopen(dstStr.strVal->data, "wb");
|
||||
}
|
||||
|
||||
basValRelease(&srcStr);
|
||||
basValRelease(&dstStr);
|
||||
|
||||
if (!fin || !fout) {
|
||||
if (fin) {
|
||||
fclose(fin);
|
||||
}
|
||||
|
||||
if (fout) {
|
||||
fclose(fout);
|
||||
}
|
||||
|
||||
runtimeError(vm, 53, "File not found or cannot create destination");
|
||||
return BAS_VM_FILE_ERROR;
|
||||
}
|
||||
|
||||
char buf[4096];
|
||||
size_t n;
|
||||
|
||||
while ((n = fread(buf, 1, sizeof(buf), fin)) > 0) {
|
||||
fwrite(buf, 1, n, fout);
|
||||
}
|
||||
|
||||
fclose(fin);
|
||||
fclose(fout);
|
||||
break;
|
||||
}
|
||||
|
||||
case OP_FS_MKDIR: {
|
||||
BasValueT pathVal;
|
||||
|
||||
if (!pop(vm, &pathVal)) {
|
||||
return BAS_VM_STACK_UNDERFLOW;
|
||||
}
|
||||
|
||||
BasValueT pathStr = basValToString(pathVal);
|
||||
basValRelease(&pathVal);
|
||||
|
||||
#ifdef _WIN32
|
||||
int rc = mkdir(pathStr.strVal->data);
|
||||
#else
|
||||
int rc = mkdir(pathStr.strVal->data, 0755);
|
||||
#endif
|
||||
|
||||
if (rc != 0) {
|
||||
basValRelease(&pathStr);
|
||||
runtimeError(vm, 75, "Path/File access error");
|
||||
return BAS_VM_FILE_ERROR;
|
||||
}
|
||||
|
||||
basValRelease(&pathStr);
|
||||
break;
|
||||
}
|
||||
|
||||
case OP_FS_RMDIR: {
|
||||
BasValueT pathVal;
|
||||
|
||||
if (!pop(vm, &pathVal)) {
|
||||
return BAS_VM_STACK_UNDERFLOW;
|
||||
}
|
||||
|
||||
BasValueT pathStr = basValToString(pathVal);
|
||||
basValRelease(&pathVal);
|
||||
|
||||
if (rmdir(pathStr.strVal->data) != 0) {
|
||||
basValRelease(&pathStr);
|
||||
runtimeError(vm, 75, "Path/File access error");
|
||||
return BAS_VM_FILE_ERROR;
|
||||
}
|
||||
|
||||
basValRelease(&pathStr);
|
||||
break;
|
||||
}
|
||||
|
||||
case OP_FS_CHDIR: {
|
||||
BasValueT pathVal;
|
||||
|
||||
if (!pop(vm, &pathVal)) {
|
||||
return BAS_VM_STACK_UNDERFLOW;
|
||||
}
|
||||
|
||||
BasValueT pathStr = basValToString(pathVal);
|
||||
basValRelease(&pathVal);
|
||||
|
||||
if (chdir(pathStr.strVal->data) != 0) {
|
||||
basValRelease(&pathStr);
|
||||
runtimeError(vm, 76, "Path not found");
|
||||
return BAS_VM_FILE_ERROR;
|
||||
}
|
||||
|
||||
basValRelease(&pathStr);
|
||||
break;
|
||||
}
|
||||
|
||||
case OP_FS_CHDRIVE: {
|
||||
BasValueT driveVal;
|
||||
|
||||
if (!pop(vm, &driveVal)) {
|
||||
return BAS_VM_STACK_UNDERFLOW;
|
||||
}
|
||||
|
||||
BasValueT driveStr = basValToString(driveVal);
|
||||
basValRelease(&driveVal);
|
||||
|
||||
// On DOS, change to the drive's root directory
|
||||
if (driveStr.strVal->data[0]) {
|
||||
char drivePath[4];
|
||||
drivePath[0] = driveStr.strVal->data[0];
|
||||
drivePath[1] = ':';
|
||||
drivePath[2] = '\0';
|
||||
chdir(drivePath);
|
||||
}
|
||||
|
||||
basValRelease(&driveStr);
|
||||
break;
|
||||
}
|
||||
|
||||
case OP_FS_CURDIR: {
|
||||
char cwd[260];
|
||||
|
||||
if (!getcwd(cwd, sizeof(cwd))) {
|
||||
cwd[0] = '\0';
|
||||
}
|
||||
|
||||
BasStringT *s = basStringNew(cwd, (int32_t)strlen(cwd));
|
||||
BasValueT result;
|
||||
result.type = BAS_TYPE_STRING;
|
||||
result.strVal = s;
|
||||
|
||||
if (!push(vm, result)) {
|
||||
basStringUnref(s);
|
||||
return BAS_VM_STACK_OVERFLOW;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case OP_FS_DIR: {
|
||||
BasValueT patVal;
|
||||
|
||||
if (!pop(vm, &patVal)) {
|
||||
return BAS_VM_STACK_UNDERFLOW;
|
||||
}
|
||||
|
||||
BasValueT patStr = basValToString(patVal);
|
||||
basValRelease(&patVal);
|
||||
|
||||
dirClose();
|
||||
|
||||
// Split pattern into directory + filename pattern
|
||||
const char *pat = patStr.strVal->data;
|
||||
const char *lastSep = NULL;
|
||||
|
||||
for (const char *c = pat; *c; c++) {
|
||||
if (*c == '/' || *c == '\\') {
|
||||
lastSep = c;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastSep) {
|
||||
int32_t dirLen = (int32_t)(lastSep - pat);
|
||||
|
||||
if (dirLen >= (int32_t)sizeof(sDirPath)) {
|
||||
dirLen = (int32_t)sizeof(sDirPath) - 1;
|
||||
}
|
||||
|
||||
memcpy(sDirPath, pat, dirLen);
|
||||
sDirPath[dirLen] = '\0';
|
||||
snprintf(sDirPattern, sizeof(sDirPattern), "%s", lastSep + 1);
|
||||
} else {
|
||||
snprintf(sDirPath, sizeof(sDirPath), ".");
|
||||
snprintf(sDirPattern, sizeof(sDirPattern), "%s", pat);
|
||||
}
|
||||
|
||||
basValRelease(&patStr);
|
||||
|
||||
if (sDirPattern[0] == '\0') {
|
||||
snprintf(sDirPattern, sizeof(sDirPattern), "*");
|
||||
}
|
||||
|
||||
sDirHandle = opendir(sDirPath);
|
||||
|
||||
const char *match = dirNext();
|
||||
const char *text = match ? match : "";
|
||||
BasStringT *s = basStringNew(text, (int32_t)strlen(text));
|
||||
BasValueT result;
|
||||
result.type = BAS_TYPE_STRING;
|
||||
result.strVal = s;
|
||||
|
||||
if (!push(vm, result)) {
|
||||
basStringUnref(s);
|
||||
return BAS_VM_STACK_OVERFLOW;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case OP_FS_DIR_NEXT: {
|
||||
const char *match = dirNext();
|
||||
const char *text = match ? match : "";
|
||||
BasStringT *s = basStringNew(text, (int32_t)strlen(text));
|
||||
BasValueT result;
|
||||
result.type = BAS_TYPE_STRING;
|
||||
result.strVal = s;
|
||||
|
||||
if (!push(vm, result)) {
|
||||
basStringUnref(s);
|
||||
return BAS_VM_STACK_OVERFLOW;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case OP_FS_FILELEN: {
|
||||
BasValueT fnVal;
|
||||
|
||||
if (!pop(vm, &fnVal)) {
|
||||
return BAS_VM_STACK_UNDERFLOW;
|
||||
}
|
||||
|
||||
BasValueT fnStr = basValToString(fnVal);
|
||||
basValRelease(&fnVal);
|
||||
|
||||
struct stat st;
|
||||
int32_t size = 0;
|
||||
|
||||
if (stat(fnStr.strVal->data, &st) == 0) {
|
||||
size = (int32_t)st.st_size;
|
||||
}
|
||||
|
||||
basValRelease(&fnStr);
|
||||
|
||||
BasValueT result;
|
||||
result.type = BAS_TYPE_LONG;
|
||||
result.longVal = size;
|
||||
|
||||
if (!push(vm, result)) {
|
||||
return BAS_VM_STACK_OVERFLOW;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case OP_FS_GETATTR: {
|
||||
BasValueT fnVal;
|
||||
|
||||
if (!pop(vm, &fnVal)) {
|
||||
return BAS_VM_STACK_UNDERFLOW;
|
||||
}
|
||||
|
||||
BasValueT fnStr = basValToString(fnVal);
|
||||
basValRelease(&fnVal);
|
||||
|
||||
struct stat st;
|
||||
int32_t attrs = 0;
|
||||
|
||||
if (stat(fnStr.strVal->data, &st) == 0) {
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
attrs |= 16; // vbDirectory
|
||||
}
|
||||
|
||||
if (!(st.st_mode & S_IWUSR)) {
|
||||
attrs |= 1; // vbReadOnly
|
||||
}
|
||||
}
|
||||
|
||||
basValRelease(&fnStr);
|
||||
|
||||
BasValueT result;
|
||||
result.type = BAS_TYPE_INTEGER;
|
||||
result.intVal = (int16_t)attrs;
|
||||
|
||||
if (!push(vm, result)) {
|
||||
return BAS_VM_STACK_OVERFLOW;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case OP_FS_SETATTR: {
|
||||
BasValueT attrVal;
|
||||
BasValueT fnVal;
|
||||
|
||||
if (!pop(vm, &attrVal) || !pop(vm, &fnVal)) {
|
||||
return BAS_VM_STACK_UNDERFLOW;
|
||||
}
|
||||
|
||||
BasValueT fnStr = basValToString(fnVal);
|
||||
int32_t attrs = (int32_t)basValToNumber(attrVal);
|
||||
basValRelease(&fnVal);
|
||||
basValRelease(&attrVal);
|
||||
|
||||
struct stat st;
|
||||
|
||||
if (stat(fnStr.strVal->data, &st) == 0) {
|
||||
mode_t mode = st.st_mode;
|
||||
|
||||
if (attrs & 1) {
|
||||
mode &= ~(S_IWUSR | S_IWGRP | S_IWOTH);
|
||||
} else {
|
||||
mode |= S_IWUSR;
|
||||
}
|
||||
|
||||
chmod(fnStr.strVal->data, mode);
|
||||
}
|
||||
|
||||
basValRelease(&fnStr);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
return BAS_VM_BAD_OPCODE;
|
||||
}
|
||||
|
||||
return BAS_VM_OK;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// execLogical
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -411,6 +411,9 @@ static void scanAppsDir(void) {
|
|||
// object that the shell can dlopen(). We skip progman.app to avoid listing
|
||||
// ourselves in the launcher grid.
|
||||
static void scanAppsDirRecurse(const char *dirPath) {
|
||||
// Collect all entries first, close the handle, then process.
|
||||
// DOS has limited file handles; keeping a DIR open while
|
||||
// opening .app files for resource loading causes failures.
|
||||
DIR *dir = opendir(dirPath);
|
||||
|
||||
if (!dir) {
|
||||
|
|
@ -421,44 +424,37 @@ static void scanAppsDirRecurse(const char *dirPath) {
|
|||
return;
|
||||
}
|
||||
|
||||
char **names = NULL;
|
||||
struct dirent *ent;
|
||||
|
||||
while ((ent = readdir(dir)) != NULL) {
|
||||
// Skip . and ..
|
||||
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) {
|
||||
if (ent->d_name[0] == '.' && (ent->d_name[1] == '\0' || (ent->d_name[1] == '.' && ent->d_name[2] == '\0'))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Copy d_name before recursion — readdir may use a shared buffer
|
||||
char name[MAX_PATH_LEN];
|
||||
snprintf(name, sizeof(name), "%s", ent->d_name);
|
||||
arrput(names, strdup(ent->d_name));
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
|
||||
int32_t nEntries = (int32_t)arrlen(names);
|
||||
|
||||
for (int32_t i = 0; i < nEntries; i++) {
|
||||
char fullPath[MAX_PATH_LEN];
|
||||
snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPath, DVX_PATH_SEP, name);
|
||||
snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPath, DVX_PATH_SEP, names[i]);
|
||||
|
||||
// Check if this is a directory -- recurse into it
|
||||
struct stat st;
|
||||
|
||||
if (stat(fullPath, &st) == 0 && S_ISDIR(st.st_mode)) {
|
||||
scanAppsDirRecurse(fullPath);
|
||||
free(names[i]);
|
||||
continue;
|
||||
}
|
||||
|
||||
int32_t len = strlen(name);
|
||||
int32_t len = (int32_t)strlen(names[i]);
|
||||
|
||||
if (len < 5) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for .app extension (case-insensitive)
|
||||
const char *ext = name + len - 4;
|
||||
|
||||
if (strcasecmp(ext, ".app") != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip ourselves
|
||||
if (strcasecmp(name, "progman.app") == 0) {
|
||||
if (len < 5 || strcasecmp(names[i] + len - 4, ".app") != 0 || strcasecmp(names[i], "progman.app") == 0) {
|
||||
free(names[i]);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -473,7 +469,7 @@ static void scanAppsDirRecurse(const char *dirPath) {
|
|||
nameLen = SHELL_APP_NAME_MAX - 1;
|
||||
}
|
||||
|
||||
memcpy(newEntry.name, name, nameLen);
|
||||
memcpy(newEntry.name, names[i], nameLen);
|
||||
newEntry.name[nameLen] = '\0';
|
||||
|
||||
if (newEntry.name[0] >= 'a' && newEntry.name[0] <= 'z') {
|
||||
|
|
@ -484,20 +480,21 @@ static void scanAppsDirRecurse(const char *dirPath) {
|
|||
newEntry.iconData = dvxResLoadIcon(sAc, fullPath, "icon32", &newEntry.iconW, &newEntry.iconH, &newEntry.iconPitch);
|
||||
|
||||
if (!newEntry.iconData) {
|
||||
dvxLog("Progman: no icon32 resource in %s", name);
|
||||
dvxLog("Progman: no icon32 resource in %s", names[i]);
|
||||
}
|
||||
|
||||
dvxResLoadText(fullPath, "name", newEntry.name, SHELL_APP_NAME_MAX);
|
||||
dvxResLoadText(fullPath, "description", newEntry.tooltip, sizeof(newEntry.tooltip));
|
||||
|
||||
dvxLog("Progman: found %s (%s) icon=%s", newEntry.name, name, newEntry.iconData ? "yes" : "no");
|
||||
dvxLog("Progman: found %s (%s) icon=%s", newEntry.name, names[i], newEntry.iconData ? "yes" : "no");
|
||||
arrput(sAppFiles, newEntry);
|
||||
sAppCount = (int32_t)arrlen(sAppFiles);
|
||||
|
||||
free(names[i]);
|
||||
dvxUpdate(sAc);
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
arrfree(names);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -592,3 +589,8 @@ int32_t appMain(DxeAppContextT *ctx) {
|
|||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
void appShutdown(void) {
|
||||
shellUnregisterDesktopUpdate(desktopUpdate);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,15 +22,6 @@ OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
|
|||
TARGETDIR = $(LIBSDIR)/kpunch/libdvx
|
||||
TARGET = $(TARGETDIR)/libdvx.lib
|
||||
|
||||
# libdvx.lib export prefixes
|
||||
DVX_EXPORTS = -E _dvx -E _wgt -E _wm -E _prefs -E _rect -E _draw -E _pack -E _unpack -E _text \
|
||||
-E _setClip -E _resetClip -E _stbi_ -E _stbi_write -E _dirtyList \
|
||||
-E _widget \
|
||||
-E _sCursor -E _sDbl -E _sDebug -E _sClosed -E _sFocused -E _sKey \
|
||||
-E _sOpen -E _sPressed -E _sDrag -E _sDrawing -E _sResize \
|
||||
-E _sListView -E _sSplitter -E _sTreeView \
|
||||
-E _accelParse -E _clipboard -E _multiClick
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET) $(TARGETDIR)/libdvx.dep
|
||||
|
|
@ -39,7 +30,7 @@ $(TARGETDIR)/libdvx.dep: ../config/libdvx.dep | $(TARGETDIR)
|
|||
sed 's/$$/\r/' $< > $@
|
||||
|
||||
$(TARGET): $(OBJS) | $(TARGETDIR)
|
||||
$(DXE3GEN) -o $(TARGETDIR)/libdvx.dxe $(DVX_EXPORTS) -U $(OBJS)
|
||||
$(DXE3GEN) -o $(TARGETDIR)/libdvx.dxe -U $(OBJS)
|
||||
mv $(TARGETDIR)/libdvx.dxe $@
|
||||
|
||||
$(OBJDIR)/%.o: %.c | $(OBJDIR)
|
||||
|
|
|
|||
|
|
@ -706,8 +706,8 @@ Each DXE module is compiled to an object file with GCC, then linked with dxe3gen
|
|||
# Compile
|
||||
i586-pc-msdosdjgpp-gcc -O2 -march=i486 -mtune=i586 -c -o widget.o widget.c
|
||||
|
||||
# Link as DXE with exported symbols
|
||||
dxe3gen -o widget.wgt -E _wgtRegister -U widget.o
|
||||
# Link as DXE (exports all non-static symbols)
|
||||
dxe3gen -o widget.wgt -U widget.o
|
||||
|
||||
# Optionally append resources
|
||||
dvxres build widget.wgt widget.res
|
||||
|
|
|
|||
108
core/dvxApp.c
108
core/dvxApp.c
|
|
@ -144,7 +144,6 @@ static void openSysMenu(AppContextT *ctx, WindowT *win);
|
|||
static void pollKeyboard(AppContextT *ctx);
|
||||
static void pollMouse(AppContextT *ctx);
|
||||
static void pollWidgets(AppContextT *ctx);
|
||||
static void pollWidgetsWalk(AppContextT *ctx, WidgetT *w, WindowT *win);
|
||||
static void refreshMinimizedIcons(AppContextT *ctx);
|
||||
static void repositionWindow(AppContextT *ctx, WindowT *win, int32_t x, int32_t y, int32_t w, int32_t h);
|
||||
static void updateCursorShape(AppContextT *ctx);
|
||||
|
|
@ -950,16 +949,26 @@ static bool dispatchAccelKey(AppContextT *ctx, char key) {
|
|||
WidgetT *next = widgetFindNextFocusable(win->widgetRoot, target);
|
||||
|
||||
if (next) {
|
||||
WidgetT *prev = sFocusedWidget;
|
||||
sFocusedWidget = next;
|
||||
|
||||
wclsOnAccelActivate(next, win->widgetRoot);
|
||||
if (prev) {
|
||||
wgtInvalidatePaint(prev);
|
||||
}
|
||||
|
||||
wgtInvalidate(win->widgetRoot);
|
||||
wclsOnAccelActivate(next, win->widgetRoot);
|
||||
wgtInvalidatePaint(next);
|
||||
}
|
||||
} else if (wclsHas(target, WGT_METHOD_ON_ACCEL_ACTIVATE)) {
|
||||
WidgetT *prev = sFocusedWidget;
|
||||
sFocusedWidget = target;
|
||||
|
||||
if (prev && prev != target) {
|
||||
wgtInvalidatePaint(prev);
|
||||
}
|
||||
|
||||
wclsOnAccelActivate(target, win->widgetRoot);
|
||||
wgtInvalidate(win->widgetRoot);
|
||||
wgtInvalidatePaint(target);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
@ -2880,7 +2889,7 @@ static void pollKeyboard(AppContextT *ctx) {
|
|||
|
||||
wclsClosePopup(closing);
|
||||
|
||||
wgtInvalidate(closing);
|
||||
wgtInvalidatePaint(closing);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -2951,8 +2960,26 @@ static void pollKeyboard(AppContextT *ctx) {
|
|||
|
||||
if (next) {
|
||||
sOpenPopup = NULL;
|
||||
WidgetT *prev = sFocusedWidget;
|
||||
|
||||
// Switch focus BEFORE invalidating so paint sees
|
||||
// the correct focused state for both widgets.
|
||||
sFocusedWidget = next;
|
||||
|
||||
if (prev) {
|
||||
wgtInvalidatePaint(prev);
|
||||
|
||||
if (prev->onBlur) {
|
||||
prev->onBlur(prev);
|
||||
}
|
||||
}
|
||||
|
||||
wgtInvalidatePaint(next);
|
||||
|
||||
if (next->onFocus) {
|
||||
next->onFocus(next);
|
||||
}
|
||||
|
||||
// Scroll the widget into view if needed
|
||||
int32_t scrollX = win->hScroll ? win->hScroll->value : 0;
|
||||
int32_t scrollY = win->vScroll ? win->vScroll->value : 0;
|
||||
|
|
@ -2987,8 +3014,6 @@ static void pollKeyboard(AppContextT *ctx) {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
wgtInvalidate(win->widgetRoot);
|
||||
}
|
||||
|
||||
arrfree(fstack);
|
||||
|
|
@ -3056,48 +3081,40 @@ static void pollMouse(AppContextT *ctx) {
|
|||
// rect generation for efficient repainting.
|
||||
|
||||
static void pollWidgets(AppContextT *ctx) {
|
||||
for (int32_t i = 0; i < ctx->stack.count; i++) {
|
||||
WindowT *win = ctx->stack.windows[i];
|
||||
for (int32_t i = 0; i < sPollWidgetCount; i++) {
|
||||
WidgetT *w = sPollWidgets[i];
|
||||
WindowT *win = w->window;
|
||||
|
||||
if (win->widgetRoot) {
|
||||
pollWidgetsWalk(ctx, win->widgetRoot, win);
|
||||
if (!win) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// pollWidgetsWalk -- recursive helper
|
||||
// ============================================================
|
||||
|
||||
static void pollWidgetsWalk(AppContextT *ctx, WidgetT *w, WindowT *win) {
|
||||
if (w->wclass && (w->wclass->flags & WCLASS_NEEDS_POLL) && wclsHas(w, WGT_METHOD_POLL)) {
|
||||
wclsPoll(w, win);
|
||||
|
||||
// If the poll dirtied internal state and the widget supports
|
||||
// quickRepaint, render the dirty rows directly into the window's
|
||||
// content buffer and add the affected area to the global dirty list.
|
||||
if (wclsHas(w, WGT_METHOD_QUICK_REPAINT)) {
|
||||
int32_t dirtyY = 0;
|
||||
int32_t dirtyH = 0;
|
||||
if (!wclsHas(w, WGT_METHOD_QUICK_REPAINT)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (wclsQuickRepaint(w, &dirtyY, &dirtyH) > 0) {
|
||||
int32_t dirtyY = 0;
|
||||
int32_t dirtyH = 0;
|
||||
|
||||
if (wclsQuickRepaint(w, &dirtyY, &dirtyH) > 0) {
|
||||
win->contentDirty = true;
|
||||
|
||||
if (!win->minimized) {
|
||||
int32_t scrollY = win->vScroll ? win->vScroll->value : 0;
|
||||
int32_t rectX = win->x + win->contentX;
|
||||
int32_t rectY = win->y + win->contentY + dirtyY - scrollY;
|
||||
int32_t rectW = win->contentW;
|
||||
win->contentDirty = true;
|
||||
dirtyListAdd(&ctx->dirty, rectX, rectY, rectW, dirtyH);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (WidgetT *child = w->firstChild; child; child = child->nextSibling) {
|
||||
pollWidgetsWalk(ctx, child, win);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// ============================================================
|
||||
// refreshMinimizedIcons
|
||||
// ============================================================
|
||||
|
|
@ -3219,6 +3236,12 @@ static void updateCursorShape(AppContextT *ctx) {
|
|||
int32_t mx = ctx->mouseX;
|
||||
int32_t my = ctx->mouseY;
|
||||
|
||||
// Popup menus override all cursor logic -- always arrow
|
||||
if (ctx->popup.active || ctx->sysMenu.active) {
|
||||
ctx->cursorId = CURSOR_ARROW;
|
||||
return;
|
||||
}
|
||||
|
||||
// During active resize, keep the resize cursor
|
||||
if (ctx->stack.resizeWindow >= 0) {
|
||||
int32_t edge = ctx->stack.resizeEdge;
|
||||
|
|
@ -5063,14 +5086,23 @@ 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.
|
||||
// Flush deferred widget paints. wgtInvalidatePaint sets
|
||||
// widgetPaintPending instead of calling dvxInvalidateWindow inline,
|
||||
// so multiple invalidations per frame are batched into one tree walk.
|
||||
// Also handles first-paint (fullRepaint) for newly created windows.
|
||||
for (int32_t i = 0; i < ctx->stack.count; i++) {
|
||||
WindowT *win = ctx->stack.windows[i];
|
||||
|
||||
if (win->fullRepaint && win->onPaint) {
|
||||
dvxInvalidateWindow(ctx, win);
|
||||
if ((win->widgetPaintPending || win->fullRepaint) && win->onPaint) {
|
||||
win->widgetPaintPending = false;
|
||||
RectT fullRect = {0, 0, win->contentW, win->contentH};
|
||||
WIN_CALLBACK(ctx, win, win->onPaint(win, &fullRect));
|
||||
// fullRepaint is cleared by widgetOnPaint for widget windows.
|
||||
// For raw-paint windows, clear it here so they don't repaint
|
||||
// every frame forever.
|
||||
win->fullRepaint = false;
|
||||
win->contentDirty = true;
|
||||
dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -5095,7 +5127,7 @@ bool dvxUpdate(AppContextT *ctx) {
|
|||
sKeyPressedBtn->y + sKeyPressedBtn->h / 2);
|
||||
}
|
||||
|
||||
wgtInvalidate(sKeyPressedBtn);
|
||||
wgtInvalidatePaint(sKeyPressedBtn);
|
||||
sKeyPressedBtn = NULL;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -506,8 +506,9 @@ typedef struct WindowT {
|
|||
bool maximized;
|
||||
bool resizable;
|
||||
bool modal;
|
||||
bool contentDirty; // true when contentBuf has changed since last icon refresh
|
||||
bool fullRepaint; // true = clear + repaint all widgets; false = only dirty ones
|
||||
bool contentDirty; // true when contentBuf has changed since last icon refresh
|
||||
bool fullRepaint; // true = clear + repaint all widgets; false = only dirty ones
|
||||
bool widgetPaintPending; // deferred widget paint (set by wgtInvalidatePaint)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -249,6 +249,7 @@ typedef struct WidgetT {
|
|||
bool readOnly;
|
||||
bool swallowTab; // Tab key goes to widget, not focus nav
|
||||
bool paintDirty; // needs repaint (set by wgtInvalidatePaint)
|
||||
bool childDirty; // a descendant needs repaint (for WCLASS_PAINTS_CHILDREN)
|
||||
char accelKey; // lowercase accelerator character, 0 if none
|
||||
|
||||
// Content offset: mouse event coordinates are adjusted by this amount
|
||||
|
|
|
|||
|
|
@ -104,6 +104,8 @@ extern WidgetT *sFocusedWidget;
|
|||
extern WidgetT *sKeyPressedBtn;
|
||||
extern WidgetT *sOpenPopup;
|
||||
extern WidgetT *sDragWidget; // widget being dragged (any drag type)
|
||||
extern int32_t sPollWidgetCount;
|
||||
extern WidgetT **sPollWidgets; // stb_ds dynamic array
|
||||
extern void (*sCursorBlinkFn)(void);
|
||||
|
||||
// ============================================================
|
||||
|
|
@ -172,6 +174,8 @@ void widgetLayoutChildren(WidgetT *w, const BitmapFontT *font);
|
|||
// ============================================================
|
||||
|
||||
void widgetManageScrollbars(WindowT *win, AppContextT *ctx);
|
||||
void widgetOnBlur(WindowT *win);
|
||||
void widgetOnFocus(WindowT *win);
|
||||
void widgetOnKey(WindowT *win, int32_t key, int32_t mod);
|
||||
void widgetOnKeyUp(WindowT *win, int32_t scancode, int32_t mod);
|
||||
void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons);
|
||||
|
|
|
|||
54
core/dvxWm.c
54
core/dvxWm.c
|
|
@ -2845,6 +2845,60 @@ MenuT *wmCreateMenu(void) {
|
|||
// Unlike freeMenuRecursive (which only frees submenu children because the
|
||||
// top-level struct is embedded), this also frees the root MenuT itself.
|
||||
|
||||
// ============================================================
|
||||
// wmDrawVScrollbarAt
|
||||
// ============================================================
|
||||
|
||||
void wmDrawVScrollbarAt(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t h, int32_t scrollPos, int32_t visibleItems, int32_t totalItems) {
|
||||
int32_t sbW = SCROLLBAR_WIDTH;
|
||||
|
||||
// Trough
|
||||
rectFill(d, ops, x, y, sbW, h, colors->scrollbarTrough);
|
||||
|
||||
BevelStyleT troughBevel;
|
||||
troughBevel.highlight = colors->windowShadow;
|
||||
troughBevel.shadow = colors->windowHighlight;
|
||||
troughBevel.face = 0;
|
||||
troughBevel.width = 1;
|
||||
drawBevel(d, ops, x, y, sbW, h, &troughBevel);
|
||||
|
||||
BevelStyleT btnBevel;
|
||||
btnBevel.highlight = colors->windowHighlight;
|
||||
btnBevel.shadow = colors->windowShadow;
|
||||
btnBevel.face = colors->scrollbarBg;
|
||||
btnBevel.width = 1;
|
||||
|
||||
// Up arrow
|
||||
drawBevel(d, ops, x, y, sbW, sbW, &btnBevel);
|
||||
drawScrollbarArrow(d, ops, colors, x, y, sbW, 0);
|
||||
|
||||
// Down arrow
|
||||
int32_t downY = y + h - sbW;
|
||||
drawBevel(d, ops, x, downY, sbW, sbW, &btnBevel);
|
||||
drawScrollbarArrow(d, ops, colors, x, downY, sbW, 1);
|
||||
|
||||
// Thumb
|
||||
int32_t trackLen = h - sbW * 2;
|
||||
|
||||
if (trackLen > 0 && totalItems > visibleItems) {
|
||||
int32_t maxScroll = totalItems - visibleItems;
|
||||
int32_t thumbSize = (int32_t)(((int64_t)visibleItems * trackLen) / totalItems);
|
||||
|
||||
if (thumbSize < sbW) {
|
||||
thumbSize = sbW;
|
||||
}
|
||||
|
||||
int32_t thumbPos = 0;
|
||||
|
||||
if (maxScroll > 0) {
|
||||
thumbPos = (int32_t)(((int64_t)scrollPos * (trackLen - thumbSize)) / maxScroll);
|
||||
}
|
||||
|
||||
drawBevel(d, ops, x, y + sbW + thumbPos, sbW, thumbSize, &btnBevel);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void wmFreeMenu(MenuT *menu) {
|
||||
if (!menu) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -227,4 +227,10 @@ MenuT *wmCreateMenu(void);
|
|||
// heap-allocated submenu children recursively.
|
||||
void wmFreeMenu(MenuT *menu);
|
||||
|
||||
// Draw a standalone vertical scrollbar at the given screen coordinates.
|
||||
// Used by popup lists (dropdown, combobox) that need a scrollbar without
|
||||
// a full ScrollbarT struct. scrollPos/visibleItems/totalItems define the
|
||||
// scroll state; the function computes thumb position/size internally.
|
||||
void wmDrawVScrollbarAt(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t h, int32_t scrollPos, int32_t visibleItems, int32_t totalItems);
|
||||
|
||||
#endif // DVX_WM_H
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@
|
|||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <fcntl.h>
|
||||
#include <fnmatch.h>
|
||||
#include <sys/dxe.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/time.h>
|
||||
|
|
@ -2551,6 +2552,8 @@ DXE_EXPORT_TABLE(sDxeExportTable)
|
|||
// --- filesystem ---
|
||||
DXE_EXPORT(access)
|
||||
DXE_EXPORT(chdir)
|
||||
DXE_EXPORT(chmod)
|
||||
DXE_EXPORT(fnmatch)
|
||||
DXE_EXPORT(getcwd)
|
||||
DXE_EXPORT(realpath)
|
||||
DXE_EXPORT(stat)
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ bool sDebugLayout = false;
|
|||
WidgetT *sFocusedWidget = NULL; // currently focused widget (O(1) access, avoids tree walk)
|
||||
WidgetT *sOpenPopup = NULL; // dropdown/combobox with open popup list
|
||||
WidgetT *sDragWidget = NULL; // widget being dragged (any drag type)
|
||||
int32_t sPollWidgetCount = 0;
|
||||
WidgetT **sPollWidgets = NULL; // stb_ds dynamic array
|
||||
|
||||
// Shared clipboard -- process-wide, not per-widget.
|
||||
static char *sClipboard = NULL;
|
||||
|
|
@ -196,6 +198,11 @@ WidgetT *widgetAlloc(WidgetT *parent, int32_t type) {
|
|||
w->visible = true;
|
||||
w->enabled = true;
|
||||
|
||||
if (w->wclass->flags & WCLASS_NEEDS_POLL) {
|
||||
arrput(sPollWidgets, w);
|
||||
sPollWidgetCount = (int32_t)arrlen(sPollWidgets);
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
w->window = parent->window;
|
||||
widgetAddChild(parent, w);
|
||||
|
|
|
|||
|
|
@ -335,11 +335,11 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
|
|||
wclsGetPopupRect(sOpenPopup, font, win->contentH, &popX, &popY, &popW, &popH);
|
||||
|
||||
if (x >= popX && x < popX + popW && y >= popY && y < popY + popH) {
|
||||
// Click on popup item -- dispatch to widget's onMouse
|
||||
// Click on popup area -- dispatch to widget's onMouse.
|
||||
// The widget may keep the popup open (e.g. scrollbar click)
|
||||
// or close it (item selection sets sOpenPopup = NULL).
|
||||
wclsOnMouse(sOpenPopup, root, x, y);
|
||||
|
||||
sOpenPopup = NULL;
|
||||
wgtInvalidate(root);
|
||||
wgtInvalidatePaint(root);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -368,14 +368,14 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
|
|||
return;
|
||||
}
|
||||
|
||||
// Clear focus from the previously focused widget. This is done via
|
||||
// the cached sFocusedWidget pointer rather than walking the tree to
|
||||
// find the focused widget -- an O(1) operation vs O(n).
|
||||
// Clear focus from the previously focused widget. Must set
|
||||
// sFocusedWidget to NULL BEFORE invalidating so the inline paint
|
||||
// sees the widget as unfocused and erases its highlight.
|
||||
WidgetT *prevFocus = sFocusedWidget;
|
||||
|
||||
if (sFocusedWidget) {
|
||||
wgtInvalidatePaint(sFocusedWidget);
|
||||
sFocusedWidget = NULL;
|
||||
wgtInvalidatePaint(prevFocus);
|
||||
}
|
||||
|
||||
// Dispatch to the hit widget's mouse handler via vtable. The handler
|
||||
|
|
@ -465,8 +465,40 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
|
|||
if (sFocusedWidget && sFocusedWidget != prevFocus && sFocusedWidget->onFocus) {
|
||||
sFocusedWidget->onFocus(sFocusedWidget);
|
||||
}
|
||||
}
|
||||
|
||||
wgtInvalidate(root);
|
||||
|
||||
// ============================================================
|
||||
// widgetOnBlur -- window lost focus
|
||||
// ============================================================
|
||||
//
|
||||
// When a widget-managed window loses WM focus (user clicked another
|
||||
// window's title bar, or the window was minimized), clear the widget
|
||||
// focus so the cursor/highlight is removed. Without this, the text
|
||||
// cursor stays visible in the unfocused window.
|
||||
|
||||
void widgetOnBlur(WindowT *win) {
|
||||
if (sFocusedWidget && sFocusedWidget->window == win) {
|
||||
WidgetT *prev = sFocusedWidget;
|
||||
sFocusedWidget = NULL;
|
||||
wgtInvalidatePaint(prev);
|
||||
|
||||
if (prev->onBlur) {
|
||||
prev->onBlur(prev);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetOnFocus -- window gained focus
|
||||
// ============================================================
|
||||
//
|
||||
// Mark the window's content dirty so the compositor knows to
|
||||
// refresh its minimized icon thumbnail if needed.
|
||||
|
||||
void widgetOnFocus(WindowT *win) {
|
||||
win->contentDirty = true;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
#include "dvxWgtP.h"
|
||||
#include "dvxPlat.h"
|
||||
#include "stb_ds_wrap.h"
|
||||
#include "../widgets/box/box.h"
|
||||
|
||||
static bool sFullRepaint = false;
|
||||
|
|
@ -81,14 +82,38 @@ void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFo
|
|||
// The window's fullRepaint flag is stored in sFullRepaint for the
|
||||
// duration of the paint walk.
|
||||
bool dirty = w->paintDirty || sFullRepaint;
|
||||
w->paintDirty = false;
|
||||
|
||||
if (dirty) {
|
||||
// For WCLASS_PAINTS_CHILDREN widgets (TabControl, TreeView, ScrollPane,
|
||||
// Splitter): the generic child recursion below can't reach their
|
||||
// children, so these widgets must handle child painting themselves.
|
||||
// Only call their paint when something actually needs drawing.
|
||||
bool paintsChildren = w->wclass && (w->wclass->flags & WCLASS_PAINTS_CHILDREN);
|
||||
|
||||
if (paintsChildren) {
|
||||
// On full repaint, ensure paintDirty is set so the paint function
|
||||
// redraws its chrome (the window background was cleared).
|
||||
if (sFullRepaint) {
|
||||
w->paintDirty = true;
|
||||
}
|
||||
|
||||
// Skip entirely if nothing needs painting in this subtree
|
||||
if (!w->paintDirty && !w->childDirty) {
|
||||
return;
|
||||
}
|
||||
|
||||
// When this widget itself is dirty (will clear its background),
|
||||
// all descendants must repaint on the fresh background.
|
||||
bool savedFull = sFullRepaint;
|
||||
|
||||
if (w->paintDirty) {
|
||||
sFullRepaint = true;
|
||||
}
|
||||
|
||||
wclsPaint(w, d, ops, font, colors);
|
||||
}
|
||||
sFullRepaint = savedFull;
|
||||
w->paintDirty = false;
|
||||
w->childDirty = false;
|
||||
|
||||
// Widgets that paint their own children return early
|
||||
if (w->wclass && (w->wclass->flags & WCLASS_PAINTS_CHILDREN)) {
|
||||
if (sDebugLayout && dirty) {
|
||||
debugContainerBorder(w, d, ops);
|
||||
}
|
||||
|
|
@ -96,6 +121,12 @@ void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFo
|
|||
return;
|
||||
}
|
||||
|
||||
w->paintDirty = false;
|
||||
|
||||
if (dirty) {
|
||||
wclsPaint(w, d, ops, font, colors);
|
||||
}
|
||||
|
||||
// Always recurse into children — a clean parent may have dirty children
|
||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||
widgetPaintOne(c, d, ops, font, colors);
|
||||
|
|
@ -192,6 +223,16 @@ void wgtDestroy(WidgetT *w) {
|
|||
sDragWidget = NULL;
|
||||
}
|
||||
|
||||
if (w->wclass && (w->wclass->flags & WCLASS_NEEDS_POLL)) {
|
||||
for (int32_t i = 0; i < sPollWidgetCount; i++) {
|
||||
if (sPollWidgets[i] == w) {
|
||||
arrdel(sPollWidgets, i);
|
||||
sPollWidgetCount = (int32_t)arrlen(sPollWidgets);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If this is the root, clear the window's reference
|
||||
if (w->window && w->window->widgetRoot == w) {
|
||||
w->window->widgetRoot = NULL;
|
||||
|
|
@ -318,6 +359,8 @@ WidgetT *wgtInitWindow(AppContextT *ctx, WindowT *win) {
|
|||
win->onKey = widgetOnKey;
|
||||
win->onKeyUp = widgetOnKeyUp;
|
||||
win->onResize = widgetOnResize;
|
||||
win->onBlur = widgetOnBlur;
|
||||
win->onFocus = widgetOnFocus;
|
||||
|
||||
return root;
|
||||
}
|
||||
|
|
@ -386,20 +429,22 @@ void wgtInvalidatePaint(WidgetT *w) {
|
|||
// Mark only this widget as needing repaint
|
||||
w->paintDirty = true;
|
||||
|
||||
// Propagate childDirty up through WCLASS_PAINTS_CHILDREN ancestors
|
||||
// so they know to recurse into children during partial repaints.
|
||||
WidgetT *root = w;
|
||||
|
||||
while (root->parent) {
|
||||
root = root->parent;
|
||||
|
||||
if (root->wclass && (root->wclass->flags & WCLASS_PAINTS_CHILDREN)) {
|
||||
root->childDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
AppContextT *ctx = (AppContextT *)root->userData;
|
||||
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Partial repaint — only dirty widgets will be repainted
|
||||
dvxInvalidateWindow(ctx, w->window);
|
||||
// Defer the actual paint — it will happen once in the main loop
|
||||
// before compositing, batching multiple invalidations into one
|
||||
// tree walk instead of one per call.
|
||||
w->window->widgetPaintPending = true;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1186,8 +1186,8 @@ img { max-width: 100%; }
|
|||
<pre><code> # Compile
|
||||
i586-pc-msdosdjgpp-gcc -O2 -march=i486 -mtune=i586 -c -o widget.o widget.c
|
||||
|
||||
# Link as DXE with exported symbols
|
||||
dxe3gen -o widget.wgt -E _wgtRegister -U widget.o
|
||||
# Link as DXE (exports all non-static symbols)
|
||||
dxe3gen -o widget.wgt -U widget.o
|
||||
|
||||
# Optionally append resources
|
||||
dvxres build widget.wgt widget.res</code></pre>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ $(TARGETDIR)/listhelp.dep: ../config/listhelp.dep | $(TARGETDIR)
|
|||
sed 's/$$/\r/' $< > $@
|
||||
|
||||
$(TARGET): $(OBJS) | $(TARGETDIR)
|
||||
$(DXE3GEN) -o $(TARGETDIR)/listhelp.dxe -E _widgetDraw -E _widgetDropdown -E _widgetMax -E _widgetNavigate -E _widgetPaint -U $(OBJS)
|
||||
$(DXE3GEN) -o $(TARGETDIR)/listhelp.dxe -U $(OBJS)
|
||||
mv $(TARGETDIR)/listhelp.dxe $@
|
||||
|
||||
$(OBJDIR)/%.o: %.c | $(OBJDIR)
|
||||
|
|
|
|||
|
|
@ -108,6 +108,35 @@ int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t
|
|||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetTypeAheadSearch
|
||||
// ============================================================
|
||||
|
||||
int32_t widgetTypeAheadSearch(char ch, const char **items, int32_t itemCount, int32_t currentIdx) {
|
||||
if (itemCount <= 0 || !items) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
char upper = (ch >= 'a' && ch <= 'z') ? ch - 32 : ch;
|
||||
|
||||
// Search forward from currentIdx+1, wrapping around
|
||||
for (int32_t i = 1; i <= itemCount; i++) {
|
||||
int32_t idx = (currentIdx + i) % itemCount;
|
||||
char first = items[idx][0];
|
||||
|
||||
if (first >= 'a' && first <= 'z') {
|
||||
first -= 32;
|
||||
}
|
||||
|
||||
if (first == upper) {
|
||||
return idx;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// widgetPaintPopupList
|
||||
// ============================================================
|
||||
|
|
@ -115,19 +144,22 @@ int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t
|
|||
// Shared popup list painting for Dropdown and ComboBox.
|
||||
|
||||
void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t popX, int32_t popY, int32_t popW, int32_t popH, const char **items, int32_t itemCount, int32_t hoverIdx, int32_t scrollPos) {
|
||||
// Draw popup border
|
||||
bool hasScrollbar = (itemCount > DROPDOWN_MAX_VISIBLE);
|
||||
int32_t visibleItems = popH / font->charHeight;
|
||||
int32_t listW = hasScrollbar ? popW - POPUP_SCROLLBAR_W : popW;
|
||||
|
||||
// Draw popup border (covers item area only, not scrollbar)
|
||||
BevelStyleT bevel;
|
||||
bevel.highlight = colors->windowHighlight;
|
||||
bevel.shadow = colors->windowShadow;
|
||||
bevel.face = colors->contentBg;
|
||||
bevel.width = 2;
|
||||
drawBevel(d, ops, popX, popY, popW, popH, &bevel);
|
||||
drawBevel(d, ops, popX, popY, listW, popH, &bevel);
|
||||
|
||||
// Draw items
|
||||
int32_t visibleItems = popH / font->charHeight;
|
||||
int32_t textX = popX + TEXT_INPUT_PAD;
|
||||
int32_t textY = popY + 2;
|
||||
int32_t textW = popW - TEXT_INPUT_PAD * 2 - 4;
|
||||
int32_t textX = popX + TEXT_INPUT_PAD;
|
||||
int32_t textY = popY + 2;
|
||||
int32_t textW = listW - TEXT_INPUT_PAD * 2 - 4;
|
||||
|
||||
for (int32_t i = 0; i < visibleItems && (scrollPos + i) < itemCount; i++) {
|
||||
int32_t idx = scrollPos + i;
|
||||
|
|
@ -144,29 +176,77 @@ void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *f
|
|||
drawText(d, ops, font, textX, iy, items[idx], ifg, ibg, false);
|
||||
}
|
||||
|
||||
// Draw scroll indicators if the list extends beyond visible area
|
||||
if (itemCount > visibleItems) {
|
||||
int32_t cx = popX + popW / 2;
|
||||
uint32_t arrowC = colors->menuHighlightBg;
|
||||
// Draw scrollbar
|
||||
if (hasScrollbar) {
|
||||
wmDrawVScrollbarAt(d, ops, colors, popX + listW, popY, popH, scrollPos, visibleItems, itemCount);
|
||||
}
|
||||
}
|
||||
|
||||
// Up triangle (point at top, wide at bottom)
|
||||
if (scrollPos > 0) {
|
||||
int32_t ty = popY + 2;
|
||||
|
||||
for (int32_t i = 0; i < 3; i++) {
|
||||
drawHLine(d, ops, cx - i, ty + i, 1 + i * 2, arrowC);
|
||||
}
|
||||
// ============================================================
|
||||
// widgetPopupScrollbarClick
|
||||
// ============================================================
|
||||
|
||||
bool widgetPopupScrollbarClick(int32_t x, int32_t y, int32_t popX, int32_t popY, int32_t popW, int32_t popH, int32_t itemCount, int32_t visibleItems, int32_t *scrollPos) {
|
||||
if (itemCount <= DROPDOWN_MAX_VISIBLE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int32_t listW = popW - POPUP_SCROLLBAR_W;
|
||||
int32_t sbX = popX + listW;
|
||||
|
||||
if (x < sbX || x >= sbX + POPUP_SCROLLBAR_W) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int32_t maxScroll = itemCount - visibleItems;
|
||||
int32_t relY = y - popY;
|
||||
|
||||
if (relY < POPUP_SCROLLBAR_W) {
|
||||
// Up arrow
|
||||
if (*scrollPos > 0) {
|
||||
(*scrollPos)--;
|
||||
}
|
||||
} else if (relY >= popH - POPUP_SCROLLBAR_W) {
|
||||
// Down arrow
|
||||
if (*scrollPos < maxScroll) {
|
||||
(*scrollPos)++;
|
||||
}
|
||||
} else {
|
||||
// Trough — page up/down based on which side of thumb
|
||||
int32_t trackLen = popH - POPUP_SCROLLBAR_W * 2;
|
||||
int32_t thumbSize = (int32_t)(((int64_t)visibleItems * trackLen) / itemCount);
|
||||
|
||||
if (thumbSize < POPUP_SCROLLBAR_W) {
|
||||
thumbSize = POPUP_SCROLLBAR_W;
|
||||
}
|
||||
|
||||
// Down triangle (wide at top, point at bottom)
|
||||
if (scrollPos + visibleItems < itemCount) {
|
||||
int32_t by = popY + popH - 4;
|
||||
int32_t thumbPos = 0;
|
||||
|
||||
for (int32_t i = 0; i < 3; i++) {
|
||||
drawHLine(d, ops, cx - i, by - i, 1 + i * 2, arrowC);
|
||||
if (maxScroll > 0) {
|
||||
thumbPos = (int32_t)(((int64_t)(*scrollPos) * (trackLen - thumbSize)) / maxScroll);
|
||||
}
|
||||
|
||||
int32_t clickInTrack = relY - POPUP_SCROLLBAR_W;
|
||||
|
||||
if (clickInTrack < thumbPos) {
|
||||
// Page up
|
||||
*scrollPos -= visibleItems;
|
||||
|
||||
if (*scrollPos < 0) {
|
||||
*scrollPos = 0;
|
||||
}
|
||||
} else if (clickInTrack >= thumbPos + thumbSize) {
|
||||
// Page down
|
||||
*scrollPos += visibleItems;
|
||||
|
||||
if (*scrollPos > maxScroll) {
|
||||
*scrollPos = maxScroll;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -193,6 +273,11 @@ void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t conten
|
|||
*popW = w->w;
|
||||
*popH = visibleItems * font->charHeight + 4;
|
||||
|
||||
// Add scrollbar width when list exceeds visible area
|
||||
if (itemCount > DROPDOWN_MAX_VISIBLE) {
|
||||
*popW += POPUP_SCROLLBAR_W;
|
||||
}
|
||||
|
||||
if (w->y + w->h + *popH <= contentH) {
|
||||
*popY = w->y + w->h;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
#define DROPDOWN_BTN_WIDTH 16
|
||||
#define DROPDOWN_MAX_VISIBLE 8
|
||||
#define POPUP_SCROLLBAR_W SCROLLBAR_WIDTH
|
||||
|
||||
// ============================================================
|
||||
// Dropdown arrow glyph
|
||||
|
|
@ -42,4 +43,21 @@ void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t conten
|
|||
|
||||
void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t popX, int32_t popY, int32_t popW, int32_t popH, const char **items, int32_t itemCount, int32_t hoverIdx, int32_t scrollPos);
|
||||
|
||||
// ============================================================
|
||||
// Type-ahead search
|
||||
// ============================================================
|
||||
|
||||
// Search items[] for the next entry starting with ch (case-insensitive),
|
||||
// starting after currentIdx and wrapping around. Returns the matching
|
||||
// index, or -1 if no match found.
|
||||
int32_t widgetTypeAheadSearch(char ch, const char **items, int32_t itemCount, int32_t currentIdx);
|
||||
|
||||
// ============================================================
|
||||
// Popup scrollbar hit testing
|
||||
// ============================================================
|
||||
|
||||
// Returns true if the click at (x, y) is on the popup scrollbar.
|
||||
// Updates *scrollPos in place (clamped to valid range).
|
||||
bool widgetPopupScrollbarClick(int32_t x, int32_t y, int32_t popX, int32_t popY, int32_t popW, int32_t popH, int32_t itemCount, int32_t visibleItems, int32_t *scrollPos);
|
||||
|
||||
#endif // LIST_HELP_H
|
||||
|
|
|
|||
|
|
@ -227,49 +227,55 @@ static void readDeps(ModuleT *mod) {
|
|||
// ============================================================
|
||||
|
||||
static void scanDir(const char *dirPath, const char *ext, ModuleT **mods) {
|
||||
// Collect all entries first, close the handle, then process.
|
||||
// DOS has limited file handles; keeping a DIR open during
|
||||
// recursion or stat() causes intermittent failures.
|
||||
DIR *dir = opendir(dirPath);
|
||||
|
||||
if (!dir) {
|
||||
return;
|
||||
}
|
||||
|
||||
char **names = NULL;
|
||||
struct dirent *ent;
|
||||
|
||||
while ((ent = readdir(dir)) != NULL) {
|
||||
// Copy d_name — readdir may use a shared buffer across recursion
|
||||
char name[DVX_MAX_PATH];
|
||||
snprintf(name, sizeof(name), "%s", ent->d_name);
|
||||
|
||||
// Skip . and ..
|
||||
if (name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0'))) {
|
||||
if (ent->d_name[0] == '.' && (ent->d_name[1] == '\0' || (ent->d_name[1] == '.' && ent->d_name[2] == '\0'))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
arrput(names, strdup(ent->d_name));
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
|
||||
int32_t count = (int32_t)arrlen(names);
|
||||
int32_t extLen = (int32_t)strlen(ext);
|
||||
|
||||
for (int32_t i = 0; i < count; i++) {
|
||||
char path[DVX_MAX_PATH];
|
||||
snprintf(path, sizeof(path), "%s%c%s", dirPath, DVX_PATH_SEP, name);
|
||||
snprintf(path, sizeof(path), "%s%c%s", dirPath, DVX_PATH_SEP, names[i]);
|
||||
|
||||
// Check for matching extension
|
||||
int32_t nameLen = strlen(name);
|
||||
int32_t extLen = strlen(ext);
|
||||
int32_t nameLen = (int32_t)strlen(names[i]);
|
||||
|
||||
if (nameLen > extLen && strcasecmp(name + nameLen - extLen, ext) == 0) {
|
||||
if (nameLen > extLen && strcasecmp(names[i] + nameLen - extLen, ext) == 0) {
|
||||
ModuleT mod;
|
||||
memset(&mod, 0, sizeof(mod));
|
||||
snprintf(mod.path, sizeof(mod.path), "%s", path);
|
||||
extractBaseName(path, ext, mod.baseName, sizeof(mod.baseName));
|
||||
arrput(*mods, mod);
|
||||
continue;
|
||||
} else {
|
||||
struct stat st;
|
||||
|
||||
if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
|
||||
scanDir(path, ext, mods);
|
||||
}
|
||||
}
|
||||
|
||||
// Recurse into subdirectories
|
||||
struct stat st;
|
||||
|
||||
if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
|
||||
scanDir(path, ext, mods);
|
||||
}
|
||||
free(names[i]);
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
arrfree(names);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -500,13 +506,13 @@ bool platformGlobMatch(const char *pattern, const char *name) {
|
|||
|
||||
// Recursively count .hcf files under the given directory
|
||||
static int32_t countHcfFilesRecurse(const char *dirPath) {
|
||||
int32_t count = 0;
|
||||
DIR *dir = opendir(dirPath);
|
||||
|
||||
if (!dir) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
char **names = NULL;
|
||||
struct dirent *ent;
|
||||
|
||||
while ((ent = readdir(dir)) != NULL) {
|
||||
|
|
@ -514,31 +520,36 @@ static int32_t countHcfFilesRecurse(const char *dirPath) {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Copy d_name before any recursion — readdir may use a shared buffer
|
||||
char name[DVX_MAX_PATH];
|
||||
snprintf(name, sizeof(name), "%s", ent->d_name);
|
||||
|
||||
char fullPath[DVX_MAX_PATH];
|
||||
snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPath, DVX_PATH_SEP, name);
|
||||
|
||||
struct stat st;
|
||||
|
||||
if (stat(fullPath, &st) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
count += countHcfFilesRecurse(fullPath);
|
||||
} else {
|
||||
int32_t nameLen = (int32_t)strlen(name);
|
||||
|
||||
if (nameLen > 4 && strcasecmp(name + nameLen - 4, ".hcf") == 0) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
arrput(names, strdup(ent->d_name));
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
|
||||
int32_t count = 0;
|
||||
int32_t nEntries = (int32_t)arrlen(names);
|
||||
|
||||
for (int32_t i = 0; i < nEntries; i++) {
|
||||
char fullPath[DVX_MAX_PATH];
|
||||
snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPath, DVX_PATH_SEP, names[i]);
|
||||
|
||||
struct stat st;
|
||||
|
||||
if (stat(fullPath, &st) == 0) {
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
count += countHcfFilesRecurse(fullPath);
|
||||
} else {
|
||||
int32_t nameLen = (int32_t)strlen(names[i]);
|
||||
|
||||
if (nameLen > 4 && strcasecmp(names[i] + nameLen - 4, ".hcf") == 0) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
free(names[i]);
|
||||
}
|
||||
|
||||
arrfree(names);
|
||||
return count;
|
||||
}
|
||||
|
||||
|
|
@ -580,6 +591,7 @@ static void writeGlobToResp(FILE *resp, const char *pattern, const char *exclude
|
|||
return;
|
||||
}
|
||||
|
||||
char **names = NULL;
|
||||
struct dirent *ent;
|
||||
|
||||
while ((ent = readdir(d)) != NULL) {
|
||||
|
|
@ -587,32 +599,35 @@ static void writeGlobToResp(FILE *resp, const char *pattern, const char *exclude
|
|||
continue;
|
||||
}
|
||||
|
||||
char name[DVX_MAX_PATH];
|
||||
snprintf(name, sizeof(name), "%s", ent->d_name);
|
||||
|
||||
char fullPath[DVX_MAX_PATH];
|
||||
snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPart, DVX_PATH_SEP, name);
|
||||
|
||||
struct stat st;
|
||||
|
||||
if (stat(fullPath, &st) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
char subPattern[DVX_MAX_PATH];
|
||||
snprintf(subPattern, sizeof(subPattern), "%s%c%s", fullPath, DVX_PATH_SEP, globPart);
|
||||
writeGlobToResp(resp, subPattern, excludePattern);
|
||||
} else if (platformGlobMatch(globPart, name)) {
|
||||
if (excludePattern && platformGlobMatch(excludePattern, name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
fprintf(resp, "%s\n", fullPath);
|
||||
}
|
||||
arrput(names, strdup(ent->d_name));
|
||||
}
|
||||
|
||||
closedir(d);
|
||||
|
||||
int32_t nEntries = (int32_t)arrlen(names);
|
||||
|
||||
for (int32_t i = 0; i < nEntries; i++) {
|
||||
char fullPath[DVX_MAX_PATH];
|
||||
snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPart, DVX_PATH_SEP, names[i]);
|
||||
|
||||
struct stat st;
|
||||
|
||||
if (stat(fullPath, &st) == 0) {
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
char subPattern[DVX_MAX_PATH];
|
||||
snprintf(subPattern, sizeof(subPattern), "%s%c%s", fullPath, DVX_PATH_SEP, globPart);
|
||||
writeGlobToResp(resp, subPattern, excludePattern);
|
||||
} else if (platformGlobMatch(globPart, names[i])) {
|
||||
if (!excludePattern || !platformGlobMatch(excludePattern, names[i])) {
|
||||
fprintf(resp, "%s\n", fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
free(names[i]);
|
||||
}
|
||||
|
||||
arrfree(names);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -728,6 +743,7 @@ static void processHcfDir(const char *dirPath) {
|
|||
return;
|
||||
}
|
||||
|
||||
char **names = NULL;
|
||||
struct dirent *ent;
|
||||
|
||||
while ((ent = readdir(dir)) != NULL) {
|
||||
|
|
@ -735,33 +751,37 @@ static void processHcfDir(const char *dirPath) {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Copy d_name before any recursion — readdir may use a shared buffer
|
||||
char name[DVX_MAX_PATH];
|
||||
snprintf(name, sizeof(name), "%s", ent->d_name);
|
||||
|
||||
char fullPath[DVX_MAX_PATH];
|
||||
snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPath, DVX_PATH_SEP, name);
|
||||
|
||||
struct stat st;
|
||||
|
||||
if (stat(fullPath, &st) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
processHcfDir(fullPath);
|
||||
} else {
|
||||
int32_t nameLen = (int32_t)strlen(name);
|
||||
|
||||
if (nameLen > 4 && strcasecmp(name + nameLen - 4, ".hcf") == 0) {
|
||||
processHcf(fullPath, dirPath);
|
||||
sSplashLoaded++;
|
||||
splashUpdateProgress();
|
||||
}
|
||||
}
|
||||
arrput(names, strdup(ent->d_name));
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
|
||||
int32_t nEntries = (int32_t)arrlen(names);
|
||||
|
||||
for (int32_t i = 0; i < nEntries; i++) {
|
||||
char fullPath[DVX_MAX_PATH];
|
||||
snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPath, DVX_PATH_SEP, names[i]);
|
||||
|
||||
struct stat st;
|
||||
|
||||
if (stat(fullPath, &st) == 0) {
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
processHcfDir(fullPath);
|
||||
} else {
|
||||
int32_t nameLen = (int32_t)strlen(names[i]);
|
||||
|
||||
if (nameLen > 4 && strcasecmp(names[i] + nameLen - 4, ".hcf") == 0) {
|
||||
processHcf(fullPath, dirPath);
|
||||
sSplashLoaded++;
|
||||
splashUpdateProgress();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
free(names[i]);
|
||||
}
|
||||
|
||||
arrfree(names);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
4
run.sh
4
run.sh
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
|
||||
#flatpak run com.dosbox_x.DOSBox-X -conf dosbox-x-overrides.conf
|
||||
flatpak run com.dosbox_x.DOSBox-X -conf dosbox-x-overrides.conf
|
||||
|
||||
SDL_VIDEO_X11_VISUALID= ~/bin/dosbox-staging/dosbox -conf dosbox-staging-overrides.conf
|
||||
#SDL_VIDEO_X11_VISUALID= ~/bin/dosbox-staging/dosbox -conf dosbox-staging-overrides.conf
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ Building an Application
|
|||
|
||||
1. Write your app with appDescriptor and appMain exports
|
||||
2. Compile: gcc -c -o myapp.o myapp.c -Isdk/include/core ...
|
||||
3. Link: dxe3gen -o myapp.app -E _appDescriptor -E _appMain -U myapp.o
|
||||
3. Link: dxe3gen -o myapp.app -U myapp.o
|
||||
4. Optionally create a .res file and build resources with dvxres
|
||||
|
||||
See samples/hello/ for a complete example.
|
||||
|
|
@ -43,7 +43,7 @@ Building a Widget
|
|||
|
||||
1. Write your widget with wgtRegister export
|
||||
2. Compile: gcc -c -o mywgt.o mywgt.c -Isdk/include/core
|
||||
3. Link: dxe3gen -o mywgt.wgt -E _wgtRegister -U mywgt.o
|
||||
3. Link: dxe3gen -o mywgt.wgt -U mywgt.o
|
||||
|
||||
See samples/widget/ for a complete example.
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ CFLAGS = -O2 -Wall -Wextra -Werror -march=i486 -mtune=i586 \
|
|||
all: hello.app
|
||||
|
||||
hello.app: hello.o hello.res icon32.bmp
|
||||
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U hello.o
|
||||
$(DXE3GEN) -o $@ -U hello.o
|
||||
$(DVXRES) build $@ hello.res
|
||||
|
||||
hello.o: hello.c
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ CFLAGS = -O2 -Wall -Wextra -Werror -march=i486 -mtune=i586
|
|||
all: mylib.lib
|
||||
|
||||
mylib.lib: mylib.o
|
||||
$(DXE3GEN) -o mylib.dxe -E _myLibAdd -E _myLibMul -E _myLibVersion -U $<
|
||||
$(DXE3GEN) -o mylib.dxe -U $<
|
||||
mv mylib.dxe $@
|
||||
|
||||
mylib.o: mylib.c mylib.h
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
//
|
||||
// Build:
|
||||
// i586-pc-msdosdjgpp-gcc -O2 -Wall -c -o mylib.o mylib.c
|
||||
// dxe3gen -o mylib.lib -E _myLibAdd -E _myLibMul -E _myLibVersion -U mylib.o
|
||||
// dxe3gen -o mylib.lib -U mylib.o
|
||||
//
|
||||
// Deploy: copy mylib.lib to LIBS/<vendor>/MYLIB/ on the target.
|
||||
// Add a mylib.dep file if this library depends on others.
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ CFLAGS = -O2 -Wall -Wextra -Werror -march=i486 -mtune=i586 \
|
|||
all: mywgt.wgt
|
||||
|
||||
mywgt.wgt: mywgt.o mywgt.res icon24.bmp
|
||||
$(DXE3GEN) -o mywgt.dxe -E _wgtRegister -U mywgt.o
|
||||
$(DXE3GEN) -o mywgt.dxe -U mywgt.o
|
||||
mv mywgt.dxe $@
|
||||
$(DVXRES) build $@ mywgt.res
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
//
|
||||
// Build:
|
||||
// i586-pc-msdosdjgpp-gcc -O2 -Wall -I../../include/core -c -o mywgt.o mywgt.c
|
||||
// dxe3gen -o mywgt.wgt -E _wgtRegister -U mywgt.o
|
||||
// dxe3gen -o mywgt.wgt -U mywgt.o
|
||||
//
|
||||
// Deploy: copy mywgt.wgt to WIDGETS/<vendor>/MYWGT/ on the target.
|
||||
// Optionally include MYWGT.DHS (C API docs) and MYWGT.BHS (BASIC docs).
|
||||
|
|
|
|||
|
|
@ -24,9 +24,7 @@ $(TARGETDIR)/serial.dep: ../config/serial.dep | $(TARGETDIR)
|
|||
sed 's/$$/\r/' $< > $@
|
||||
|
||||
$(TARGET): $(OBJS) | $(TARGETDIR)
|
||||
$(DXE3GEN) -o $(TARGETDIR)/serial.dxe \
|
||||
-E _rs232 -E _pkt -E _secLink -E _secDh -E _secCipher -E _secRng \
|
||||
-U $(OBJS)
|
||||
$(DXE3GEN) -o $(TARGETDIR)/serial.dxe -U $(OBJS)
|
||||
mv $(TARGETDIR)/serial.dxe $@
|
||||
|
||||
$(OBJDIR)/rs232.o: ../rs232/rs232.c | $(OBJDIR)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ $(TARGETDIR)/dvxshell.dep: ../config/dvxshell.dep | $(TARGETDIR)
|
|||
sed 's/$$/\r/' $< > $@
|
||||
|
||||
$(TARGET): $(OBJS) | $(TARGETDIR)
|
||||
$(DXE3GEN) -o $(TARGETDIR)/dvxshell.dxe -E _shell -U $(OBJS)
|
||||
$(DXE3GEN) -o $(TARGETDIR)/dvxshell.dxe -U $(OBJS)
|
||||
mv $(TARGETDIR)/dvxshell.dxe $@
|
||||
|
||||
$(CONFIGDIR)/dvx.ini: ../config/dvx.ini | $(CONFIGDIR)
|
||||
|
|
|
|||
|
|
@ -99,9 +99,7 @@ $(OBJDIR)/sqlite_opcodes.o: $(GEN_OPCODES_C) | $(OBJDIR)
|
|||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(TARGET): $(OBJS) | $(TARGETDIR)
|
||||
$(DXE3GEN) -o $(TARGETDIR)/dvxsql.dxe \
|
||||
-E _dvxSql -E _sqlite3 \
|
||||
-U $(OBJS)
|
||||
$(DXE3GEN) -o $(TARGETDIR)/dvxsql.dxe -U $(OBJS)
|
||||
mv $(TARGETDIR)/dvxsql.dxe $@
|
||||
|
||||
$(OBJDIR):
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ TARGET = $(TARGETDIR)/libtasks.lib
|
|||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) | $(TARGETDIR)
|
||||
$(DXE3GEN) -o $(TARGETDIR)/libtasks.dxe -E _ts -U $(OBJS)
|
||||
$(DXE3GEN) -o $(TARGETDIR)/libtasks.dxe -U $(OBJS)
|
||||
mv $(TARGETDIR)/libtasks.dxe $@
|
||||
|
||||
$(OBJDIR)/%.o: %.c | $(OBJDIR)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ $(TARGETDIR)/texthelp.dep: ../config/texthelp.dep | $(TARGETDIR)
|
|||
sed 's/$$/\r/' $< > $@
|
||||
|
||||
$(TARGET): $(OBJS) | $(TARGETDIR)
|
||||
$(DXE3GEN) -o $(TARGETDIR)/texthelp.dxe -E _clipboard -E _clearOther -E _isWordChar -E _multiClick -E _widgetText -E _wordStart -E _wordEnd -U $(OBJS)
|
||||
$(DXE3GEN) -o $(TARGETDIR)/texthelp.dxe -U $(OBJS)
|
||||
mv $(TARGETDIR)/texthelp.dxe $@
|
||||
|
||||
$(OBJDIR)/%.o: %.c | $(OBJDIR)
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ WGT_MODS = $(foreach n,$(WGT_NAMES),$(WGTDIR)/$(n)/$(n).wgt)
|
|||
OBJS = $(foreach w,$(WIDGETS),$(OBJDIR)/$(word 3,$(subst :, ,$w)).o)
|
||||
|
||||
# Per-widget extra DXE3GEN flags (e.g. additional -E exports for dlsym)
|
||||
EXTRA_DXE_FLAGS_datactrl = -E _dataCtrl
|
||||
|
||||
DEPFILES = textinpt combobox spinner terminal listbox dropdown listview treeview
|
||||
WGT_DEPS = $(foreach d,$(DEPFILES),$(WGTDIR)/$(d)/$(d).dep)
|
||||
|
|
@ -75,7 +74,7 @@ $(OBJDIR)/$(word 3,$(subst :, ,$1)).o: $(word 2,$(subst :, ,$1))/$(word 3,$(subs
|
|||
$$(CC) $$(CFLAGS) -c -o $$@ $$<
|
||||
|
||||
$(WGTDIR)/$(word 1,$(subst :, ,$1))/$(word 1,$(subst :, ,$1)).wgt: $(OBJDIR)/$(word 3,$(subst :, ,$1)).o | $(WGTDIR)/$(word 1,$(subst :, ,$1))
|
||||
$$(DXE3GEN) -o $(WGTDIR)/$(word 1,$(subst :, ,$1))/$(word 1,$(subst :, ,$1)).dxe -E _wgtRegister $$(EXTRA_DXE_FLAGS_$(word 1,$(subst :, ,$1))) -U $$<
|
||||
$$(DXE3GEN) -o $(WGTDIR)/$(word 1,$(subst :, ,$1))/$(word 1,$(subst :, ,$1)).dxe -U $$<
|
||||
mv $(WGTDIR)/$(word 1,$(subst :, ,$1))/$(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; \
|
||||
|
|
|
|||
|
|
@ -695,23 +695,27 @@ void widgetCanvasPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma
|
|||
return;
|
||||
}
|
||||
|
||||
// Draw a sunken bevel border around the canvas
|
||||
// Bevel wraps tightly around the bitmap, not the full widget.
|
||||
// If the layout stretched the widget beyond the bitmap, fill
|
||||
// the excess area with content background.
|
||||
int32_t bevelW = cd->canvasW + CANVAS_BORDER * 2;
|
||||
int32_t bevelH = cd->canvasH + CANVAS_BORDER * 2;
|
||||
|
||||
if (w->w > bevelW || w->h > bevelH) {
|
||||
rectFill(d, ops, w->x, w->y, w->w, w->h, colors->contentBg);
|
||||
}
|
||||
|
||||
BevelStyleT sunken;
|
||||
sunken.highlight = colors->windowShadow;
|
||||
sunken.shadow = colors->windowHighlight;
|
||||
sunken.face = 0;
|
||||
sunken.width = CANVAS_BORDER;
|
||||
drawBevel(d, ops, w->x, w->y, w->w, w->h, &sunken);
|
||||
drawBevel(d, ops, w->x, w->y, bevelW, bevelH, &sunken);
|
||||
|
||||
// Blit the canvas data inside the border
|
||||
int32_t imgW = cd->canvasW;
|
||||
int32_t imgH = cd->canvasH;
|
||||
int32_t dx = w->x + CANVAS_BORDER;
|
||||
int32_t dy = w->y + CANVAS_BORDER;
|
||||
|
||||
rectCopy(d, ops, dx, dy,
|
||||
rectCopy(d, ops, w->x + CANVAS_BORDER, w->y + CANVAS_BORDER,
|
||||
cd->pixelData, cd->canvasPitch,
|
||||
0, 0, imgW, imgH);
|
||||
0, 0, cd->canvasW, cd->canvasH);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -129,6 +129,9 @@ void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
|
|||
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
||||
int32_t boxY = w->y + (w->h - CHECKBOX_BOX_SIZE) / 2;
|
||||
|
||||
// Clear full widget area so old focus rect is erased
|
||||
rectFill(d, ops, w->x, w->y, w->w, w->h, bg);
|
||||
|
||||
// Draw checkbox box
|
||||
BevelStyleT bevel;
|
||||
bevel.highlight = colors->windowShadow;
|
||||
|
|
@ -166,7 +169,7 @@ void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
|
|||
}
|
||||
|
||||
if (w == sFocusedWidget) {
|
||||
drawFocusRect(d, ops, labelX - 1, labelY - 1, labelW + 2, font->charHeight + 2, fg);
|
||||
drawFocusRect(d, ops, labelX - 1, w->y, labelW + 2, w->h, fg);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -166,6 +166,24 @@ void widgetComboBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
|||
wgtInvalidatePaint(w);
|
||||
return;
|
||||
}
|
||||
|
||||
// Type-ahead in open popup
|
||||
if (key >= 0x20 && key < 0x7F) {
|
||||
int32_t found = widgetTypeAheadSearch((char)key, d->items, d->itemCount, d->hoverIdx);
|
||||
|
||||
if (found >= 0) {
|
||||
d->hoverIdx = found;
|
||||
|
||||
if (d->hoverIdx < d->listScrollPos) {
|
||||
d->listScrollPos = d->hoverIdx;
|
||||
} else if (d->hoverIdx >= d->listScrollPos + DROPDOWN_MAX_VISIBLE) {
|
||||
d->listScrollPos = d->hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
|
||||
}
|
||||
|
||||
wgtInvalidatePaint(w);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Down arrow on closed combobox opens the popup
|
||||
|
|
@ -211,7 +229,7 @@ void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
|||
sFocusedWidget = w;
|
||||
ComboBoxDataT *d = (ComboBoxDataT *)w->data;
|
||||
|
||||
// If popup is open, this click is on a popup item -- select it
|
||||
// If popup is open, this click is on a popup item or scrollbar
|
||||
if (d->open) {
|
||||
AppContextT *ctx = wgtGetContext(w);
|
||||
const BitmapFontT *font = &ctx->font;
|
||||
|
|
@ -222,6 +240,14 @@ void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
|||
|
||||
widgetDropdownPopupRect(w, font, w->window->contentH, d->itemCount, &popX, &popY, &popW, &popH);
|
||||
|
||||
int32_t visibleItems = popH / font->charHeight;
|
||||
|
||||
// Check scrollbar click first
|
||||
if (widgetPopupScrollbarClick(vx, vy, popX, popY, popW, popH, d->itemCount, visibleItems, &d->listScrollPos)) {
|
||||
wgtInvalidatePaint(w);
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t itemIdx = d->listScrollPos + (vy - popY - 2) / font->charHeight;
|
||||
|
||||
if (itemIdx >= 0 && itemIdx < d->itemCount) {
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
|||
DropdownDataT *d = (DropdownDataT *)w->data;
|
||||
|
||||
if (d->open) {
|
||||
// Popup is open -- navigate items
|
||||
// Popup is open -- navigate items
|
||||
if (key == (0x48 | 0x100)) {
|
||||
if (d->hoverIdx > 0) {
|
||||
d->hoverIdx--;
|
||||
|
|
@ -135,6 +135,18 @@ void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
|||
if (w->onChange) {
|
||||
w->onChange(w);
|
||||
}
|
||||
} else if (key >= 0x20 && key < 0x7F) {
|
||||
int32_t found = widgetTypeAheadSearch((char)key, d->items, d->itemCount, d->hoverIdx);
|
||||
|
||||
if (found >= 0) {
|
||||
d->hoverIdx = found;
|
||||
|
||||
if (d->hoverIdx < d->scrollPos) {
|
||||
d->scrollPos = d->hoverIdx;
|
||||
} else if (d->hoverIdx >= d->scrollPos + DROPDOWN_MAX_VISIBLE) {
|
||||
d->scrollPos = d->hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Popup is closed
|
||||
|
|
@ -163,6 +175,17 @@ void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
|||
if (d->selectedIdx > 0) {
|
||||
d->selectedIdx--;
|
||||
|
||||
if (w->onChange) {
|
||||
w->onChange(w);
|
||||
}
|
||||
}
|
||||
} else if (key >= 0x21 && key < 0x7F) {
|
||||
// Type-ahead when closed (skip space — it opens the popup)
|
||||
int32_t found = widgetTypeAheadSearch((char)key, d->items, d->itemCount, d->selectedIdx);
|
||||
|
||||
if (found >= 0) {
|
||||
d->selectedIdx = found;
|
||||
|
||||
if (w->onChange) {
|
||||
w->onChange(w);
|
||||
}
|
||||
|
|
@ -190,7 +213,7 @@ void widgetDropdownOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
|||
sFocusedWidget = w;
|
||||
DropdownDataT *d = (DropdownDataT *)w->data;
|
||||
|
||||
// If popup is open, this click is on a popup item -- select it
|
||||
// If popup is open, this click is on a popup item or scrollbar
|
||||
if (d->open) {
|
||||
AppContextT *ctx = wgtGetContext(w);
|
||||
const BitmapFontT *font = &ctx->font;
|
||||
|
|
@ -201,6 +224,14 @@ void widgetDropdownOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
|||
|
||||
widgetDropdownPopupRect(w, font, w->window->contentH, d->itemCount, &popX, &popY, &popW, &popH);
|
||||
|
||||
int32_t visibleItems = popH / font->charHeight;
|
||||
|
||||
// Check scrollbar click first
|
||||
if (widgetPopupScrollbarClick(vx, vy, popX, popY, popW, popH, d->itemCount, visibleItems, &d->scrollPos)) {
|
||||
wgtInvalidatePaint(w);
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t itemIdx = d->scrollPos + (vy - popY - 2) / font->charHeight;
|
||||
|
||||
if (itemIdx >= 0 && itemIdx < d->itemCount) {
|
||||
|
|
|
|||
|
|
@ -233,6 +233,11 @@ void widgetListBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
|||
|
||||
int32_t newSel = widgetNavigateIndex(key, sel, d->itemCount, visibleRows);
|
||||
|
||||
// Type-ahead: printable character searches for next matching item
|
||||
if (newSel < 0 && key >= 0x20 && key < 0x7F) {
|
||||
newSel = widgetTypeAheadSearch((char)key, d->items, d->itemCount, sel);
|
||||
}
|
||||
|
||||
if (newSel < 0) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -483,6 +483,34 @@ void widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
|||
|
||||
int32_t newDisplaySel = widgetNavigateIndex(key, displaySel, rowCount, visibleRows);
|
||||
|
||||
// Type-ahead: search first column for next matching row
|
||||
if (newDisplaySel < 0 && key >= 0x20 && key < 0x7F && lv->cellData && lv->colCount > 0) {
|
||||
char upper = (char)key;
|
||||
|
||||
if (upper >= 'a' && upper <= 'z') {
|
||||
upper -= 32;
|
||||
}
|
||||
|
||||
for (int32_t i = 1; i <= rowCount; i++) {
|
||||
int32_t dispRow = (displaySel + i) % rowCount;
|
||||
int32_t dataRow = sortIdx ? sortIdx[dispRow] : dispRow;
|
||||
const char *cell = lv->cellData[dataRow * lv->colCount];
|
||||
|
||||
if (cell && cell[0]) {
|
||||
char first = cell[0];
|
||||
|
||||
if (first >= 'a' && first <= 'z') {
|
||||
first -= 32;
|
||||
}
|
||||
|
||||
if (first == upper) {
|
||||
newDisplaySel = dispRow;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newDisplaySel < 0) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -230,6 +230,9 @@ void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap
|
|||
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
||||
int32_t boxY = w->y + (w->h - CHECKBOX_BOX_SIZE) / 2;
|
||||
|
||||
// Clear full widget area so old focus rect is erased
|
||||
rectFill(d, ops, w->x, w->y, w->w, w->h, bg);
|
||||
|
||||
// Draw diamond-shaped radio box
|
||||
int32_t bx = w->x;
|
||||
int32_t mid = CHECKBOX_BOX_SIZE / 2;
|
||||
|
|
@ -289,7 +292,7 @@ void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap
|
|||
}
|
||||
|
||||
if (w == sFocusedWidget) {
|
||||
drawFocusRect(d, ops, labelX - 1, labelY - 1, labelW + 2, font->charHeight + 2, fg);
|
||||
drawFocusRect(d, ops, labelX - 1, w->y, labelW + 2, w->h, fg);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -791,19 +791,22 @@ void widgetScrollPanePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B
|
|||
sp->scrollPosV = clampInt(sp->scrollPosV, 0, maxScrollV);
|
||||
sp->scrollPosH = clampInt(sp->scrollPosH, 0, maxScrollH);
|
||||
|
||||
// Sunken border
|
||||
BevelStyleT bevel = BEVEL_SUNKEN(colors, bg, spBorder(w));
|
||||
drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel);
|
||||
bool selfDirty = w->paintDirty;
|
||||
|
||||
// Clip to content area and paint children
|
||||
int32_t oldClipX = d->clipX;
|
||||
int32_t oldClipY = d->clipY;
|
||||
int32_t oldClipW = d->clipW;
|
||||
int32_t oldClipH = d->clipH;
|
||||
setClipRect(d, w->x + spBorder(w), w->y + spBorder(w), innerW, innerH);
|
||||
|
||||
// Fill background
|
||||
rectFill(d, ops, w->x + spBorder(w), w->y + spBorder(w), innerW, innerH, bg);
|
||||
if (selfDirty) {
|
||||
// Full redraw: border + background + scrollbars
|
||||
BevelStyleT bevel = BEVEL_SUNKEN(colors, bg, spBorder(w));
|
||||
drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel);
|
||||
rectFill(d, ops, w->x + spBorder(w), w->y + spBorder(w), innerW, innerH, bg);
|
||||
}
|
||||
|
||||
setClipRect(d, w->x + spBorder(w), w->y + spBorder(w), innerW, innerH);
|
||||
|
||||
// Paint children (already positioned by layout with scroll offset)
|
||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||
|
|
@ -812,6 +815,10 @@ void widgetScrollPanePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B
|
|||
|
||||
setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH);
|
||||
|
||||
if (!selfDirty) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw scrollbars
|
||||
if (needVSb) {
|
||||
int32_t sbX = w->x + w->w - spBorder(w) - SP_SB_W;
|
||||
|
|
|
|||
|
|
@ -202,9 +202,13 @@ void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma
|
|||
(void)font;
|
||||
SliderDataT *sd = (SliderDataT *)w->data;
|
||||
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
||||
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
||||
uint32_t tickFg = w->enabled ? fg : colors->windowShadow;
|
||||
uint32_t thumbFg = w->enabled ? colors->buttonFace : colors->scrollbarTrough;
|
||||
|
||||
// Clear full widget area so old thumb position is erased
|
||||
rectFill(d, ops, w->x, w->y, w->w, w->h, bg);
|
||||
|
||||
int32_t range = sd->maxValue - sd->minValue;
|
||||
|
||||
if (range <= 0) {
|
||||
|
|
|
|||
|
|
@ -352,6 +352,8 @@ void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy
|
|||
if (hit->onChange) {
|
||||
hit->onChange(hit);
|
||||
}
|
||||
|
||||
wgtInvalidate(hit);
|
||||
}
|
||||
|
||||
break;
|
||||
|
|
@ -386,6 +388,11 @@ void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B
|
|||
TabControlDataT *td = (TabControlDataT *)w->data;
|
||||
int32_t tabH = font->charHeight + TAB_PAD_V * 2;
|
||||
bool scroll = tabNeedScroll(w, font);
|
||||
bool selfDirty = w->paintDirty;
|
||||
|
||||
if (!selfDirty) {
|
||||
goto paintChildren;
|
||||
}
|
||||
|
||||
// Content panel
|
||||
BevelStyleT panelBevel;
|
||||
|
|
@ -505,15 +512,17 @@ void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B
|
|||
|
||||
setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH);
|
||||
|
||||
paintChildren:
|
||||
;
|
||||
// Paint only active tab page's children
|
||||
tabIdx = 0;
|
||||
int32_t activeIdx = 0;
|
||||
|
||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||
if (c->type != sTabPageTypeId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tabIdx == td->activeTab) {
|
||||
if (activeIdx == td->activeTab) {
|
||||
for (WidgetT *gc = c->firstChild; gc; gc = gc->nextSibling) {
|
||||
widgetPaintOne(gc, d, ops, font, colors);
|
||||
}
|
||||
|
|
@ -521,7 +530,7 @@ void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B
|
|||
break;
|
||||
}
|
||||
|
||||
tabIdx++;
|
||||
activeIdx++;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ typedef struct {
|
|||
int32_t (*getCursorLine)(const WidgetT *w);
|
||||
void (*setGutterClick)(WidgetT *w, void (*fn)(WidgetT *, int32_t));
|
||||
int32_t (*getWordAtCursor)(const WidgetT *w, char *buf, int32_t bufSize);
|
||||
void (*setSyntaxColors)(WidgetT *w, const uint32_t *colors, int32_t count);
|
||||
} TextInputApiT;
|
||||
|
||||
static inline const TextInputApiT *dvxTextInputApi(void) {
|
||||
|
|
@ -56,5 +57,6 @@ static inline const TextInputApiT *dvxTextInputApi(void) {
|
|||
#define wgtTextAreaGetCursorLine(w) dvxTextInputApi()->getCursorLine(w)
|
||||
#define wgtTextAreaSetGutterClick(w, fn) dvxTextInputApi()->setGutterClick(w, fn)
|
||||
#define wgtTextAreaGetWordAtCursor(w, buf, sz) dvxTextInputApi()->getWordAtCursor(w, buf, sz)
|
||||
#define wgtTextAreaSetSyntaxColors(w, colors, count) dvxTextInputApi()->setSyntaxColors(w, colors, count)
|
||||
|
||||
#endif // TEXTINPT_H
|
||||
|
|
|
|||
|
|
@ -61,6 +61,16 @@
|
|||
static int32_t sTextInputTypeId = -1;
|
||||
static int32_t sTextAreaTypeId = -1;
|
||||
|
||||
// Syntax color indices (returned by colorize callback)
|
||||
#define SYNTAX_DEFAULT 0
|
||||
#define SYNTAX_KEYWORD 1
|
||||
#define SYNTAX_STRING 2
|
||||
#define SYNTAX_COMMENT 3
|
||||
#define SYNTAX_NUMBER 4
|
||||
#define SYNTAX_OPERATOR 5
|
||||
#define SYNTAX_TYPE 6
|
||||
#define SYNTAX_MAX 7
|
||||
|
||||
typedef enum {
|
||||
InputNormalE,
|
||||
InputPasswordE,
|
||||
|
|
@ -137,6 +147,9 @@ typedef struct {
|
|||
// Gutter click callback (optional). Fired when user clicks in the gutter.
|
||||
void (*onGutterClick)(WidgetT *w, int32_t lineNum);
|
||||
|
||||
// Custom syntax colors (0x00RRGGBB; 0 = use default hardcoded color)
|
||||
uint32_t customSyntaxColors[SYNTAX_MAX];
|
||||
|
||||
// Pre-allocated paint buffers (avoid 3KB stack alloc per visible line per frame)
|
||||
uint8_t *rawSyntax; // syntax color buffer (MAX_COLORIZE_LEN)
|
||||
char *expandBuf; // tab-expanded text (MAX_COLORIZE_LEN)
|
||||
|
|
@ -160,22 +173,12 @@ typedef struct {
|
|||
// Match the ANSI terminal cursor blink rate (CURSOR_MS in widgetAnsiTerm.c)
|
||||
#define CURSOR_BLINK_MS 250
|
||||
|
||||
// Syntax color indices (returned by colorize callback)
|
||||
#define SYNTAX_DEFAULT 0
|
||||
#define SYNTAX_KEYWORD 1
|
||||
#define SYNTAX_STRING 2
|
||||
#define SYNTAX_COMMENT 3
|
||||
#define SYNTAX_NUMBER 4
|
||||
#define SYNTAX_OPERATOR 5
|
||||
#define SYNTAX_TYPE 6
|
||||
#define SYNTAX_MAX 7
|
||||
|
||||
// ============================================================
|
||||
// Prototypes
|
||||
// ============================================================
|
||||
|
||||
static void drawColorizedText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t len, const uint8_t *syntaxColors, int32_t textOff, uint32_t defaultFg, uint32_t bg, const ColorSchemeT *colors);
|
||||
static uint32_t syntaxColor(const DisplayT *d, uint8_t idx, uint32_t defaultFg, const ColorSchemeT *colors);
|
||||
static void drawColorizedText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t len, const uint8_t *syntaxColors, int32_t textOff, uint32_t defaultFg, uint32_t bg, const uint32_t *customColors);
|
||||
static uint32_t syntaxColor(const DisplayT *d, uint8_t idx, uint32_t defaultFg, const uint32_t *custom);
|
||||
static bool maskCharValid(char slot, char ch);
|
||||
static int32_t maskFirstSlot(const char *mask);
|
||||
static bool maskIsSlot(char ch);
|
||||
|
|
@ -2046,16 +2049,19 @@ void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
|||
// syntaxColor
|
||||
// ============================================================
|
||||
|
||||
static uint32_t syntaxColor(const DisplayT *d, uint8_t idx, uint32_t defaultFg, const ColorSchemeT *colors) {
|
||||
(void)colors;
|
||||
static uint32_t syntaxColor(const DisplayT *d, uint8_t idx, uint32_t defaultFg, const uint32_t *custom) {
|
||||
if (idx > 0 && idx < SYNTAX_MAX && custom && custom[idx]) {
|
||||
uint32_t c = custom[idx];
|
||||
return packColor(d, (c >> 16) & 0xFF, (c >> 8) & 0xFF, c & 0xFF);
|
||||
}
|
||||
|
||||
switch (idx) {
|
||||
case SYNTAX_KEYWORD: return packColor(d, 0, 0, 128); // dark blue
|
||||
case SYNTAX_STRING: return packColor(d, 128, 0, 0); // dark red
|
||||
case SYNTAX_COMMENT: return packColor(d, 0, 128, 0); // dark green
|
||||
case SYNTAX_NUMBER: return packColor(d, 128, 0, 128); // purple
|
||||
case SYNTAX_OPERATOR: return packColor(d, 128, 128, 0); // dark yellow
|
||||
case SYNTAX_TYPE: return packColor(d, 0, 128, 128); // teal
|
||||
case SYNTAX_KEYWORD: return packColor(d, 0, 0, 128);
|
||||
case SYNTAX_STRING: return packColor(d, 128, 0, 0);
|
||||
case SYNTAX_COMMENT: return packColor(d, 0, 128, 0);
|
||||
case SYNTAX_NUMBER: return packColor(d, 128, 0, 128);
|
||||
case SYNTAX_OPERATOR: return packColor(d, 128, 128, 0);
|
||||
case SYNTAX_TYPE: return packColor(d, 0, 128, 128);
|
||||
default: return defaultFg;
|
||||
}
|
||||
}
|
||||
|
|
@ -2068,7 +2074,7 @@ static uint32_t syntaxColor(const DisplayT *d, uint8_t idx, uint32_t defaultFg,
|
|||
// Draw text with per-character syntax coloring. Batches consecutive
|
||||
// characters of the same color into single drawTextN calls.
|
||||
|
||||
static void drawColorizedText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t len, const uint8_t *syntaxColors, int32_t textOff, uint32_t defaultFg, uint32_t bg, const ColorSchemeT *colors) {
|
||||
static void drawColorizedText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t len, const uint8_t *syntaxColors, int32_t textOff, uint32_t defaultFg, uint32_t bg, const uint32_t *customColors) {
|
||||
int32_t runStart = 0;
|
||||
|
||||
while (runStart < len) {
|
||||
|
|
@ -2079,7 +2085,7 @@ static void drawColorizedText(DisplayT *d, const BlitOpsT *ops, const BitmapFont
|
|||
runEnd++;
|
||||
}
|
||||
|
||||
uint32_t fg = syntaxColor(d, curColor, defaultFg, colors);
|
||||
uint32_t fg = syntaxColor(d, curColor, defaultFg, customColors);
|
||||
drawTextN(d, ops, font, x + runStart * font->charWidth, y, text + runStart, runEnd - runStart, fg, bg, true);
|
||||
runStart = runEnd;
|
||||
}
|
||||
|
|
@ -2300,7 +2306,7 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
|
|||
// Before selection
|
||||
if (drawStart < vSelLo) {
|
||||
if (hasSyntax) {
|
||||
drawColorizedText(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, expandBuf + drawStart, vSelLo - drawStart, syntaxBuf, drawStart, fg, bg, colors);
|
||||
drawColorizedText(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, expandBuf + drawStart, vSelLo - drawStart, syntaxBuf, drawStart, fg, bg, ta->customSyntaxColors);
|
||||
} else {
|
||||
drawTextN(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, expandBuf + drawStart, vSelLo - drawStart, fg, bg, true);
|
||||
}
|
||||
|
|
@ -2314,7 +2320,7 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
|
|||
// After selection
|
||||
if (vSelHi < drawEnd) {
|
||||
if (hasSyntax) {
|
||||
drawColorizedText(d, ops, font, textX + (vSelHi - scrollCol) * font->charWidth, drawY, expandBuf + vSelHi, drawEnd - vSelHi, syntaxBuf, vSelHi, fg, bg, colors);
|
||||
drawColorizedText(d, ops, font, textX + (vSelHi - scrollCol) * font->charWidth, drawY, expandBuf + vSelHi, drawEnd - vSelHi, syntaxBuf, vSelHi, fg, bg, ta->customSyntaxColors);
|
||||
} else {
|
||||
drawTextN(d, ops, font, textX + (vSelHi - scrollCol) * font->charWidth, drawY, expandBuf + vSelHi, drawEnd - vSelHi, fg, bg, true);
|
||||
}
|
||||
|
|
@ -2338,7 +2344,7 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
|
|||
// No selection on this line
|
||||
if (drawStart < drawEnd) {
|
||||
if (hasSyntax) {
|
||||
drawColorizedText(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, expandBuf + drawStart, drawEnd - drawStart, syntaxBuf, drawStart, fg, bg, colors);
|
||||
drawColorizedText(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, expandBuf + drawStart, drawEnd - drawStart, syntaxBuf, drawStart, fg, bg, ta->customSyntaxColors);
|
||||
} else {
|
||||
drawTextN(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, expandBuf + drawStart, drawEnd - drawStart, fg, bg, true);
|
||||
}
|
||||
|
|
@ -3121,6 +3127,26 @@ void wgtTextAreaSetGutterClickCallback(WidgetT *w, void (*fn)(WidgetT *, int32_t
|
|||
}
|
||||
|
||||
|
||||
void wgtTextAreaSetSyntaxColors(WidgetT *w, const uint32_t *colors, int32_t count) {
|
||||
if (!w || w->type != sTextAreaTypeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
TextAreaDataT *ta = (TextAreaDataT *)w->data;
|
||||
memset(ta->customSyntaxColors, 0, sizeof(ta->customSyntaxColors));
|
||||
|
||||
if (colors && count > 0) {
|
||||
if (count > SYNTAX_MAX) {
|
||||
count = SYNTAX_MAX;
|
||||
}
|
||||
|
||||
memcpy(ta->customSyntaxColors, colors, count * sizeof(uint32_t));
|
||||
}
|
||||
|
||||
wgtInvalidatePaint(w);
|
||||
}
|
||||
|
||||
|
||||
int32_t wgtTextAreaGetCursorLine(const WidgetT *w) {
|
||||
if (!w || w->type != sTextAreaTypeId) {
|
||||
return 1;
|
||||
|
|
@ -3395,6 +3421,7 @@ static const struct {
|
|||
int32_t (*getCursorLine)(const WidgetT *w);
|
||||
void (*setGutterClick)(WidgetT *w, void (*fn)(WidgetT *, int32_t));
|
||||
int32_t (*getWordAtCursor)(const WidgetT *w, char *buf, int32_t bufSize);
|
||||
void (*setSyntaxColors)(WidgetT *w, const uint32_t *colors, int32_t count);
|
||||
} sApi = {
|
||||
.create = wgtTextInput,
|
||||
.password = wgtPasswordInput,
|
||||
|
|
@ -3412,7 +3439,8 @@ static const struct {
|
|||
.setLineDecorator = wgtTextAreaSetLineDecorator,
|
||||
.getCursorLine = wgtTextAreaGetCursorLine,
|
||||
.setGutterClick = wgtTextAreaSetGutterClickCallback,
|
||||
.getWordAtCursor = wgtTextAreaGetWordAtCursor
|
||||
.getWordAtCursor = wgtTextAreaGetWordAtCursor,
|
||||
.setSyntaxColors = wgtTextAreaSetSyntaxColors
|
||||
};
|
||||
|
||||
// Per-type APIs for the designer
|
||||
|
|
|
|||
|
|
@ -891,6 +891,63 @@ void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
|||
selTi->selected = !selTi->selected;
|
||||
tv->anchorItem = sel;
|
||||
}
|
||||
} else if (key >= 0x20 && key < 0x7F) {
|
||||
// Type-ahead: search visible items for next match
|
||||
char upper = (char)key;
|
||||
|
||||
if (upper >= 'a' && upper <= 'z') {
|
||||
upper -= 32;
|
||||
}
|
||||
|
||||
WidgetT *start = sel ? sel : firstVisibleItem(w);
|
||||
|
||||
if (start) {
|
||||
WidgetT *cur = nextVisibleItem(start, w);
|
||||
|
||||
if (!cur) {
|
||||
cur = firstVisibleItem(w);
|
||||
}
|
||||
|
||||
while (cur && cur != start) {
|
||||
TreeItemDataT *ti = (TreeItemDataT *)cur->data;
|
||||
|
||||
if (ti->text && ti->text[0]) {
|
||||
char first = ti->text[0];
|
||||
|
||||
if (first >= 'a' && first <= 'z') {
|
||||
first -= 32;
|
||||
}
|
||||
|
||||
if (first == upper) {
|
||||
setSelectedItem(w, cur);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
cur = nextVisibleItem(cur, w);
|
||||
|
||||
if (!cur) {
|
||||
cur = firstVisibleItem(w);
|
||||
}
|
||||
}
|
||||
|
||||
// Check the start item itself if we wrapped all the way around
|
||||
if (cur == start && sel) {
|
||||
TreeItemDataT *ti = (TreeItemDataT *)start->data;
|
||||
|
||||
if (ti->text && ti->text[0]) {
|
||||
char first = ti->text[0];
|
||||
|
||||
if (first >= 'a' && first <= 'z') {
|
||||
first -= 32;
|
||||
}
|
||||
|
||||
if (first == upper && cur != sel) {
|
||||
setSelectedItem(w, cur);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue