From b1d74d7e3c90341b00d29324a5105fa71a5d2f47 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Thu, 19 Mar 2026 01:12:15 -0500 Subject: [PATCH] Start of preferences system. --- Makefile | 4 +- dvx.ini | 20 +++ dvx/Makefile | 23 ++- dvx/dvxApp.c | 26 +++- dvx/dvxApp.h | 8 + dvx/dvxPrefs.c | 101 +++++++++++++ dvx/dvxPrefs.h | 37 +++++ dvx/platform/dvxPlatform.h | 15 ++ dvx/platform/dvxPlatformDos.c | 62 +++++++- dvx/thirdparty/ini/LICENSE | 20 +++ dvx/thirdparty/ini/README.md | 63 ++++++++ dvx/thirdparty/ini/src/ini.c | 274 ++++++++++++++++++++++++++++++++++ dvx/thirdparty/ini/src/ini.h | 20 +++ dvx/widgets/widgetCore.c | 1 + dvx/widgets/widgetInternal.h | 1 + dvx/widgets/widgetTextInput.c | 2 +- dvxshell/Makefile | 16 +- dvxshell/shellApp.c | 17 +-- dvxshell/shellExport.c | 7 + dvxshell/shellMain.c | 55 ++++++- 20 files changed, 742 insertions(+), 30 deletions(-) create mode 100644 dvx.ini create mode 100644 dvx/dvxPrefs.c create mode 100644 dvx/dvxPrefs.h create mode 100644 dvx/thirdparty/ini/LICENSE create mode 100644 dvx/thirdparty/ini/README.md create mode 100644 dvx/thirdparty/ini/src/ini.c create mode 100644 dvx/thirdparty/ini/src/ini.h diff --git a/Makefile b/Makefile index 09811c3..b9af839 100644 --- a/Makefile +++ b/Makefile @@ -23,5 +23,5 @@ clean: $(MAKE) -C tasks clean $(MAKE) -C dvxshell clean $(MAKE) -C apps clean - -rmdir obj/dvx/widgets obj/dvx/platform obj/dvx obj/tasks obj/dvxshell obj/apps obj 2>/dev/null - -rmdir bin/apps/progman bin/apps/notepad bin/apps/clock bin/apps/dvxdemo bin/apps bin lib 2>/dev/null + -rmdir obj/dvx/widgets obj/dvx/platform obj/dvx/thirdparty obj/dvx obj/tasks obj/dvxshell obj/apps obj 2>/dev/null + -rmdir bin/config bin/apps/progman bin/apps/notepad bin/apps/clock bin/apps/dvxdemo bin/apps bin lib 2>/dev/null diff --git a/dvx.ini b/dvx.ini new file mode 100644 index 0000000..0a47a5e --- /dev/null +++ b/dvx.ini @@ -0,0 +1,20 @@ +; DVX Configuration +; +; Video mode selection. The system picks the closest available VESA mode. +; Common resolutions: 640x480, 800x600, 1024x768 +; Supported color depths: 8, 15, 16, 24, 32 + +[video] +width = 640 +height = 480 +bpp = 16 + +; Mouse settings. +; wheel: normal or reversed +; doubleclick: double-click speed in milliseconds (200-900, default 500) +; acceleration: off, low, medium, high (default medium) + +[mouse] +wheel = normal +doubleclick = 500 +acceleration = medium diff --git a/dvx/Makefile b/dvx/Makefile index b8a28e7..544eb7a 100644 --- a/dvx/Makefile +++ b/dvx/Makefile @@ -10,12 +10,15 @@ CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 OBJDIR = ../obj/dvx WOBJDIR = ../obj/dvx/widgets POBJDIR = ../obj/dvx/platform +TOBJDIR = ../obj/dvx/thirdparty LIBDIR = ../lib -SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxIcon.c dvxImageWrite.c dvxApp.c dvxDialog.c +SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxIcon.c dvxImageWrite.c dvxApp.c dvxDialog.c dvxPrefs.c PSRCS = platform/dvxPlatformDos.c +TSRCS = thirdparty/ini/src/ini.c + WSRCS = widgets/widgetAnsiTerm.c \ widgets/widgetClass.c \ widgets/widgetCore.c \ @@ -51,14 +54,15 @@ WSRCS = widgets/widgetAnsiTerm.c \ OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS)) POBJS = $(patsubst platform/%.c,$(POBJDIR)/%.o,$(PSRCS)) WOBJS = $(patsubst widgets/%.c,$(WOBJDIR)/%.o,$(WSRCS)) +TOBJS = $(TOBJDIR)/ini.o TARGET = $(LIBDIR)/libdvx.a .PHONY: all clean all: $(TARGET) -$(TARGET): $(OBJS) $(POBJS) $(WOBJS) | $(LIBDIR) - $(AR) rcs $@ $(OBJS) $(POBJS) $(WOBJS) +$(TARGET): $(OBJS) $(POBJS) $(WOBJS) $(TOBJS) | $(LIBDIR) + $(AR) rcs $@ $(OBJS) $(POBJS) $(WOBJS) $(TOBJS) $(RANLIB) $@ $(OBJDIR)/%.o: %.c | $(OBJDIR) @@ -70,6 +74,9 @@ $(POBJDIR)/%.o: platform/%.c | $(POBJDIR) $(WOBJDIR)/%.o: widgets/%.c | $(WOBJDIR) $(CC) $(CFLAGS) -c -o $@ $< +$(TOBJDIR)/ini.o: thirdparty/ini/src/ini.c | $(TOBJDIR) + $(CC) $(CFLAGS) -c -o $@ $< + $(OBJDIR): mkdir -p $(OBJDIR) @@ -79,6 +86,9 @@ $(POBJDIR): $(WOBJDIR): mkdir -p $(WOBJDIR) +$(TOBJDIR): + mkdir -p $(TOBJDIR) + $(LIBDIR): mkdir -p $(LIBDIR) @@ -92,9 +102,14 @@ $(OBJDIR)/dvxImageWrite.o: dvxImageWrite.c thirdparty/stb_image_write.h $(OBJDIR)/dvxApp.o: dvxApp.c dvxApp.h platform/dvxPlatform.h dvxTypes.h dvxVideo.h dvxDraw.h dvxComp.h dvxWm.h dvxFont.h dvxCursor.h $(OBJDIR)/dvxDialog.o: dvxDialog.c dvxDialog.h platform/dvxPlatform.h dvxApp.h dvxWidget.h widgets/widgetInternal.h dvxTypes.h dvxDraw.h +$(OBJDIR)/dvxPrefs.o: dvxPrefs.c dvxPrefs.h thirdparty/ini/src/ini.h + # Platform file dependencies $(POBJDIR)/dvxPlatformDos.o: platform/dvxPlatformDos.c platform/dvxPlatform.h dvxTypes.h dvxPalette.h +# Thirdparty file dependencies +$(TOBJDIR)/ini.o: thirdparty/ini/src/ini.c thirdparty/ini/src/ini.h + # Widget file dependencies WIDGET_DEPS = widgets/widgetInternal.h dvxWidget.h dvxTypes.h dvxApp.h dvxDraw.h dvxWm.h dvxVideo.h $(WOBJDIR)/widgetClass.o: widgets/widgetClass.c $(WIDGET_DEPS) @@ -130,4 +145,4 @@ $(WOBJDIR)/widgetScrollbar.o: widgets/widgetScrollbar.c $(WIDGET_DEPS) $(WOBJDIR)/widgetTreeView.o: widgets/widgetTreeView.c $(WIDGET_DEPS) clean: - rm -f $(OBJS) $(POBJS) $(WOBJS) $(TARGET) + rm -f $(OBJS) $(POBJS) $(WOBJS) $(TOBJS) $(TARGET) diff --git a/dvx/dvxApp.c b/dvx/dvxApp.c index 2da43c0..b705cea 100644 --- a/dvx/dvxApp.c +++ b/dvx/dvxApp.c @@ -1114,7 +1114,7 @@ static void dispatchEvents(AppContextT *ctx) { if (sb) { int32_t oldValue = sb->value; - sb->value += ctx->mouseWheel * MOUSE_WHEEL_STEP; + sb->value += ctx->mouseWheel * ctx->wheelDirection * MOUSE_WHEEL_STEP; if (sb->value < sb->min) { sb->value = sb->min; @@ -1603,6 +1603,9 @@ int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_ ctx->lastCloseClickTime = 0; ctx->lastTitleClickId = -1; ctx->lastTitleClickTime = 0; + ctx->wheelDirection = 1; + ctx->dblClickTicks = DBLCLICK_THRESHOLD; + sDblClickTicks = DBLCLICK_THRESHOLD; // Pre-compute fixed-point 16.16 reciprocal of character height so // popup menu item index calculation can use multiply+shift instead @@ -1884,6 +1887,21 @@ int32_t dvxScreenshot(AppContextT *ctx, const char *path) { } +// ============================================================ +// dvxSetMouseConfig +// ============================================================ + +void dvxSetMouseConfig(AppContextT *ctx, int32_t wheelDir, int32_t dblClickMs, int32_t accelThreshold) { + ctx->wheelDirection = (wheelDir < 0) ? -1 : 1; + ctx->dblClickTicks = (clock_t)dblClickMs * CLOCKS_PER_SEC / 1000; + sDblClickTicks = ctx->dblClickTicks; + + if (accelThreshold > 0) { + platformMouseSetAccel(accelThreshold); + } +} + + // ============================================================ // dvxShutdown // ============================================================ @@ -2239,7 +2257,7 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t clock_t now = clock(); if (ctx->lastIconClickId == iconWin->id && - (now - ctx->lastIconClickTime) < DBLCLICK_THRESHOLD) { + (now - ctx->lastIconClickTime) < ctx->dblClickTicks) { // Double-click: restore minimized window // Dirty the entire icon strip area dirtyListAdd(&ctx->dirty, 0, @@ -2288,7 +2306,7 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t if (win->resizable && ctx->lastTitleClickId == win->id && - (now - ctx->lastTitleClickTime) < DBLCLICK_THRESHOLD) { + (now - ctx->lastTitleClickTime) < ctx->dblClickTicks) { // Double-click: toggle maximize/restore ctx->lastTitleClickId = -1; @@ -2310,7 +2328,7 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t clock_t now = clock(); if (ctx->lastCloseClickId == win->id && - (now - ctx->lastCloseClickTime) < DBLCLICK_THRESHOLD) { + (now - ctx->lastCloseClickTime) < ctx->dblClickTicks) { ctx->lastCloseClickId = -1; closeSysMenu(ctx); diff --git a/dvx/dvxApp.h b/dvx/dvxApp.h index 61bb186..8efb386 100644 --- a/dvx/dvxApp.h +++ b/dvx/dvxApp.h @@ -92,6 +92,9 @@ typedef struct AppContextT { // terminal/text widgets) becomes a multiply+shift instead of an // integer divide, which is very slow on 486 (40+ cycles per DIV). uint32_t charHeightRecip; // fixed-point 16.16 reciprocal of font.charHeight + // Mouse configuration (loaded from preferences) + int32_t wheelDirection; // 1 = normal, -1 = reversed + clock_t dblClickTicks; // double-click speed in clock() ticks } AppContextT; // Initialize the entire GUI stack: video mode, input devices, font, @@ -99,6 +102,11 @@ typedef struct AppContextT { // entry point for starting a DVX application. Returns 0 on success. int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_t preferredBpp); +// Configure mouse behaviour. wheelDir: 1 = normal, -1 = reversed. +// dblClickMs: double-click speed in milliseconds (e.g. 500). +// accelThreshold: double-speed threshold in mickeys/sec (0 = don't change). +void dvxSetMouseConfig(AppContextT *ctx, int32_t wheelDir, int32_t dblClickMs, int32_t accelThreshold); + // Tear down the GUI stack in reverse order: destroy all windows, restore // text mode, release input devices. Safe to call after a failed dvxInit(). void dvxShutdown(AppContextT *ctx); diff --git a/dvx/dvxPrefs.c b/dvx/dvxPrefs.c new file mode 100644 index 0000000..28cbdb4 --- /dev/null +++ b/dvx/dvxPrefs.c @@ -0,0 +1,101 @@ +// dvxPrefs.c — INI-based preferences system +// +// Thin wrapper around rxi/ini that adds typed accessors with defaults. + +#include +#include +#include + +#include "dvxPrefs.h" +#include "thirdparty/ini/src/ini.h" + + +static ini_t *sIni = NULL; + + +// ============================================================ +// prefsFree +// ============================================================ + +void prefsFree(void) { + if (sIni) { + ini_free(sIni); + sIni = NULL; + } +} + + +// ============================================================ +// prefsGetBool +// ============================================================ + +bool prefsGetBool(const char *section, const char *key, bool defaultVal) { + const char *val = prefsGetString(section, key, NULL); + + if (!val) { + return defaultVal; + } + + // Case-insensitive first character check covers true/yes/1 and false/no/0 + char c = (char)tolower((unsigned char)val[0]); + + if (c == 't' || c == 'y' || c == '1') { + return true; + } + + if (c == 'f' || c == 'n' || c == '0') { + return false; + } + + return defaultVal; +} + + +// ============================================================ +// prefsGetInt +// ============================================================ + +int32_t prefsGetInt(const char *section, const char *key, int32_t defaultVal) { + const char *val = prefsGetString(section, key, NULL); + + if (!val) { + return defaultVal; + } + + char *end = NULL; + long n = strtol(val, &end, 10); + + if (end == val) { + return defaultVal; + } + + return (int32_t)n; +} + + +// ============================================================ +// prefsGetString +// ============================================================ + +const char *prefsGetString(const char *section, const char *key, const char *defaultVal) { + if (!sIni) { + return defaultVal; + } + + const char *val = ini_get(sIni, section, key); + + return val ? val : defaultVal; +} + + +// ============================================================ +// prefsLoad +// ============================================================ + +bool prefsLoad(const char *filename) { + prefsFree(); + + sIni = ini_load(filename); + + return sIni != NULL; +} diff --git a/dvx/dvxPrefs.h b/dvx/dvxPrefs.h new file mode 100644 index 0000000..c0ba614 --- /dev/null +++ b/dvx/dvxPrefs.h @@ -0,0 +1,37 @@ +// dvxPrefs.h — INI-based preferences system +// +// Loads a configuration file at startup and provides typed accessors +// with caller-supplied defaults. The INI file is read-only at runtime; +// values are queried by section + key. If the file is missing or a key +// is absent, the default is returned silently. +// +// The underlying parser is rxi/ini (thirdparty/ini/src). + +#ifndef DVX_PREFS_H +#define DVX_PREFS_H + +#include +#include + +// Load an INI file into memory. Returns true on success, false if the +// file could not be opened (all getters will return their defaults). +// Only one file may be loaded at a time; calling again frees the previous. +bool prefsLoad(const char *filename); + +// Release all memory held by the loaded INI file. +void prefsFree(void); + +// Retrieve a string value. Returns defaultVal if the section/key pair +// is not present. The returned pointer is valid until prefsFree(). +const char *prefsGetString(const char *section, const char *key, const char *defaultVal); + +// Retrieve an integer value. Returns defaultVal if the section/key pair +// is not present or cannot be parsed. +int32_t prefsGetInt(const char *section, const char *key, int32_t defaultVal); + +// Retrieve a boolean value. Recognises "true", "yes", "1" as true and +// "false", "no", "0" as false (case-insensitive). Returns defaultVal +// for anything else or if the key is missing. +bool prefsGetBool(const char *section, const char *key, bool defaultVal); + +#endif diff --git a/dvx/platform/dvxPlatform.h b/dvx/platform/dvxPlatform.h index 69a2de6..24425a8 100644 --- a/dvx/platform/dvxPlatform.h +++ b/dvx/platform/dvxPlatform.h @@ -139,6 +139,11 @@ bool platformMouseWheelInit(void); // driver between polls). int32_t platformMouseWheelPoll(void); +// Set the double-speed threshold in mickeys/second. When the mouse +// moves faster than this, cursor movement is doubled by the driver. +// A very high value (e.g. 10000) effectively disables acceleration. +void platformMouseSetAccel(int32_t threshold); + // Move the mouse cursor to an absolute screen position. Uses INT 33h // function 04h on DOS, SDL_WarpMouseInWindow on Linux. Used to clamp // the cursor to window edges during resize operations. @@ -195,4 +200,14 @@ const char *platformGetSystemInfo(const DisplayT *display); // describing why it's invalid. Used by the file dialog's save-as validation. const char *platformValidateFilename(const char *name); +// Change the working directory, including drive letter on DOS. Standard +// chdir() does not switch drives under DJGPP; this wrapper calls setdisk() +// first when the path contains a drive prefix (e.g. "A:\DVX"). +void platformChdir(const char *path); + +// Return a pointer to the last directory separator in path, or NULL if +// none is found. On DOS this checks both '/' and '\\' since DJGPP +// accepts either. On other platforms only '/' is recognised. +char *platformPathDirEnd(const char *path); + #endif // DVX_PLATFORM_H diff --git a/dvx/platform/dvxPlatformDos.c b/dvx/platform/dvxPlatformDos.c index 2e8dcd8..58ec7d4 100644 --- a/dvx/platform/dvxPlatformDos.c +++ b/dvx/platform/dvxPlatformDos.c @@ -29,12 +29,14 @@ #include "dvxPlatform.h" #include "../dvxPalette.h" +#include +#include +#include #include #include #include #include -#include -#include +#include // DJGPP-specific headers — this is the ONLY file that includes these #include @@ -475,6 +477,24 @@ char platformAltScanToChar(int32_t scancode) { } +// ============================================================ +// platformChdir +// ============================================================ +// +// Changes the working directory, switching the active DOS drive first +// when the path contains a drive letter (e.g. "A:\DVX"). Standard +// chdir() under DJGPP only changes the directory on the current drive. + +void platformChdir(const char *path) { + if (path[0] && path[1] == ':') { + int drive = toupper((unsigned char)path[0]) - 'A'; + setdisk(drive); + } + + chdir(path); +} + + // ============================================================ // platformFlushRect // ============================================================ @@ -1130,6 +1150,25 @@ void platformMouseInit(int32_t screenW, int32_t screenH) { } +// ============================================================ +// platformMouseSetAccel +// ============================================================ +// +// Sets the double-speed threshold via INT 33h function 13h. When the +// mouse moves faster than this many mickeys per second, cursor movement +// is doubled. A very high value (e.g. 10000) effectively disables +// acceleration. Typical driver defaults are around 64 mickeys/sec. + +void platformMouseSetAccel(int32_t threshold) { + __dpmi_regs r; + + memset(&r, 0, sizeof(r)); + r.x.ax = 0x0013; + r.x.dx = threshold; + __dpmi_int(0x33, &r); +} + + // ============================================================ // platformMousePoll // ============================================================ @@ -1218,6 +1257,25 @@ void platformMouseWarp(int32_t x, int32_t y) { } +// ============================================================ +// platformPathDirEnd +// ============================================================ +// +// Returns a pointer to the last directory separator in path. DOS +// accepts both '/' and '\\', so we check for whichever appears last. + +char *platformPathDirEnd(const char *path) { + char *fwd = strrchr(path, '/'); + char *back = strrchr(path, '\\'); + + if (back > fwd) { + return back; + } + + return fwd; +} + + // ============================================================ // platformSpanCopy8 // ============================================================ diff --git a/dvx/thirdparty/ini/LICENSE b/dvx/thirdparty/ini/LICENSE new file mode 100644 index 0000000..5818e8d --- /dev/null +++ b/dvx/thirdparty/ini/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2016 rxi + + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/dvx/thirdparty/ini/README.md b/dvx/thirdparty/ini/README.md new file mode 100644 index 0000000..e0f330d --- /dev/null +++ b/dvx/thirdparty/ini/README.md @@ -0,0 +1,63 @@ + +# ini +A *tiny* ANSI C library for loading .ini config files + +## Usage +The files **[ini.c](src/ini.c?raw=1)** and **[ini.h](src/ini.h?raw=1)** should +be dropped into an existing project. + +The library has support for sections, comment lines and quoted string values +(with escapes). Unquoted values and keys are trimmed of whitespace when loaded. + +```ini +; last modified 1 April 2001 by John Doe +[owner] +name = John Doe +organization = Acme Widgets Inc. + +[database] +; use IP address in case network name resolution is not working +server = 192.0.2.62 +port = 143 +file = "payroll.dat" +``` + +An ini file can be loaded into memory by using the `ini_load()` function. +`NULL` is returned if the file cannot be loaded. +```c +ini_t *config = ini_load("config.ini"); +``` + +The library provides two functions for retrieving values: the first is +`ini_get()`. Given a section and a key the corresponding value is returned if +it exists. If the `section` argument is `NULL` then all sections are searched. +```c +const char *name = ini_get(config, "owner", "name"); +if (name) { + printf("name: %s\n", name); +} +``` + +The second, `ini_sget()`, takes the same arguments as `ini_get()` with the +addition of a scanf format string and a pointer for where to store the value. +```c +const char *server = "default"; +int port = 80; + +ini_sget(config, "database", "server", NULL, &server); +ini_sget(config, "database", "port", "%d", &port); + +printf("server: %s:%d\n", server, port); +``` + +The `ini_free()` function is used to free the memory used by the `ini_t*` +object when we are done with it. Calling this function invalidates all string +pointers returned by the library. +```c +ini_free(config); +``` + + +## License +This library is free software; you can redistribute it and/or modify it under +the terms of the MIT license. See [LICENSE](LICENSE) for details. diff --git a/dvx/thirdparty/ini/src/ini.c b/dvx/thirdparty/ini/src/ini.c new file mode 100644 index 0000000..ab5f11d --- /dev/null +++ b/dvx/thirdparty/ini/src/ini.c @@ -0,0 +1,274 @@ +/** + * Copyright (c) 2016 rxi + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include +#include +#include +#include + +#include "ini.h" + +struct ini_t { + char *data; + char *end; +}; + + +/* Case insensitive string compare */ +static int strcmpci(const char *a, const char *b) { + for (;;) { + int d = tolower(*a) - tolower(*b); + if (d != 0 || !*a) { + return d; + } + a++, b++; + } +} + +/* Returns the next string in the split data */ +static char* next(ini_t *ini, char *p) { + p += strlen(p); + while (p < ini->end && *p == '\0') { + p++; + } + return p; +} + +static void trim_back(ini_t *ini, char *p) { + while (p >= ini->data && (*p == ' ' || *p == '\t' || *p == '\r')) { + *p-- = '\0'; + } +} + +static char* discard_line(ini_t *ini, char *p) { + while (p < ini->end && *p != '\n') { + *p++ = '\0'; + } + return p; +} + + +static char *unescape_quoted_value(ini_t *ini, char *p) { + /* Use `q` as write-head and `p` as read-head, `p` is always ahead of `q` + * as escape sequences are always larger than their resultant data */ + char *q = p; + p++; + while (p < ini->end && *p != '"' && *p != '\r' && *p != '\n') { + if (*p == '\\') { + /* Handle escaped char */ + p++; + switch (*p) { + default : *q = *p; break; + case 'r' : *q = '\r'; break; + case 'n' : *q = '\n'; break; + case 't' : *q = '\t'; break; + case '\r' : + case '\n' : + case '\0' : goto end; + } + + } else { + /* Handle normal char */ + *q = *p; + } + q++, p++; + } +end: + return q; +} + + +/* Splits data in place into strings containing section-headers, keys and + * values using one or more '\0' as a delimiter. Unescapes quoted values */ +static void split_data(ini_t *ini) { + char *value_start, *line_start; + char *p = ini->data; + + while (p < ini->end) { + switch (*p) { + case '\r': + case '\n': + case '\t': + case ' ': + *p = '\0'; + /* Fall through */ + + case '\0': + p++; + break; + + case '[': + p += strcspn(p, "]\n"); + *p = '\0'; + break; + + case ';': + p = discard_line(ini, p); + break; + + default: + line_start = p; + p += strcspn(p, "=\n"); + + /* Is line missing a '='? */ + if (*p != '=') { + p = discard_line(ini, line_start); + break; + } + trim_back(ini, p - 1); + + /* Replace '=' and whitespace after it with '\0' */ + do { + *p++ = '\0'; + } while (*p == ' ' || *p == '\r' || *p == '\t'); + + /* Is a value after '=' missing? */ + if (*p == '\n' || *p == '\0') { + p = discard_line(ini, line_start); + break; + } + + if (*p == '"') { + /* Handle quoted string value */ + value_start = p; + p = unescape_quoted_value(ini, p); + + /* Was the string empty? */ + if (p == value_start) { + p = discard_line(ini, line_start); + break; + } + + /* Discard the rest of the line after the string value */ + p = discard_line(ini, p); + + } else { + /* Handle normal value */ + p += strcspn(p, "\n"); + trim_back(ini, p - 1); + } + break; + } + } +} + + + +ini_t* ini_load(const char *filename) { + ini_t *ini = NULL; + FILE *fp = NULL; + int n, sz; + + /* Init ini struct */ + ini = malloc(sizeof(*ini)); + if (!ini) { + goto fail; + } + memset(ini, 0, sizeof(*ini)); + + /* Open file */ + fp = fopen(filename, "rb"); + if (!fp) { + goto fail; + } + + /* Get file size */ + fseek(fp, 0, SEEK_END); + sz = ftell(fp); + rewind(fp); + + /* Load file content into memory, null terminate, init end var */ + ini->data = malloc(sz + 1); + ini->data[sz] = '\0'; + ini->end = ini->data + sz; + n = fread(ini->data, 1, sz, fp); + if (n != sz) { + goto fail; + } + + /* Prepare data */ + split_data(ini); + + /* Clean up and return */ + fclose(fp); + return ini; + +fail: + if (fp) fclose(fp); + if (ini) ini_free(ini); + return NULL; +} + + +void ini_free(ini_t *ini) { + free(ini->data); + free(ini); +} + + +const char* ini_get(ini_t *ini, const char *section, const char *key) { + char *current_section = ""; + char *val; + char *p = ini->data; + + if (*p == '\0') { + p = next(ini, p); + } + + while (p < ini->end) { + if (*p == '[') { + /* Handle section */ + current_section = p + 1; + + } else { + /* Handle key */ + val = next(ini, p); + if (!section || !strcmpci(section, current_section)) { + if (!strcmpci(p, key)) { + return val; + } + } + p = val; + } + + p = next(ini, p); + } + + return NULL; +} + + +int ini_sget( + ini_t *ini, const char *section, const char *key, + const char *scanfmt, void *dst +) { + const char *val = ini_get(ini, section, key); + if (!val) { + return 0; + } + if (scanfmt) { + sscanf(val, scanfmt, dst); + } else { + *((const char**) dst) = val; + } + return 1; +} diff --git a/dvx/thirdparty/ini/src/ini.h b/dvx/thirdparty/ini/src/ini.h new file mode 100644 index 0000000..cd6af9f --- /dev/null +++ b/dvx/thirdparty/ini/src/ini.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2016 rxi + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the MIT license. See `ini.c` for details. + */ + +#ifndef INI_H +#define INI_H + +#define INI_VERSION "0.1.1" + +typedef struct ini_t ini_t; + +ini_t* ini_load(const char *filename); +void ini_free(ini_t *ini); +const char* ini_get(ini_t *ini, const char *section, const char *key); +int ini_sget(ini_t *ini, const char *section, const char *key, const char *scanfmt, void *dst); + +#endif diff --git a/dvx/widgets/widgetCore.c b/dvx/widgets/widgetCore.c index 053c781..438370f 100644 --- a/dvx/widgets/widgetCore.c +++ b/dvx/widgets/widgetCore.c @@ -37,6 +37,7 @@ // otherwise dangling pointers would cause crashes. widgetDestroyChildren() // and wgtDestroy() handle this cleanup. +clock_t sDblClickTicks = 0; // set from ctx->dblClickTicks during first paint bool sDebugLayout = false; WidgetT *sFocusedWidget = NULL; // currently focused widget (O(1) access, avoids tree walk) WidgetT *sOpenPopup = NULL; // dropdown/combobox with open popup list diff --git a/dvx/widgets/widgetInternal.h b/dvx/widgets/widgetInternal.h index b4a8e56..299820f 100644 --- a/dvx/widgets/widgetInternal.h +++ b/dvx/widgets/widgetInternal.h @@ -174,6 +174,7 @@ static inline void drawTextAccelEmbossed(DisplayT *d, const BlitOpsT *ops, const // and only one mouse can exist, this is safe. extern bool sCursorBlinkOn; // text cursor blink phase (toggled by wgtUpdateCursorBlink) +extern clock_t sDblClickTicks; // double-click threshold (set from AppContextT.dblClickTicks) extern bool sDebugLayout; extern WidgetT *sClosedPopup; // popup that was just closed (prevents immediate reopen) extern WidgetT *sFocusedWidget; // currently focused widget across all windows diff --git a/dvx/widgets/widgetTextInput.c b/dvx/widgets/widgetTextInput.c index 91c99f7..39c4837 100644 --- a/dvx/widgets/widgetTextInput.c +++ b/dvx/widgets/widgetTextInput.c @@ -321,7 +321,7 @@ static int32_t sClickCount = 0; int32_t multiClickDetect(int32_t vx, int32_t vy) { clock_t now = clock(); - if ((now - sLastClickTime) < DBLCLICK_TICKS && + if ((now - sLastClickTime) < sDblClickTicks && abs(vx - sLastClickX) < 4 && abs(vy - sLastClickY) < 4) { sClickCount++; } else { diff --git a/dvxshell/Makefile b/dvxshell/Makefile index b3c5017..e0e2c05 100644 --- a/dvxshell/Makefile +++ b/dvxshell/Makefile @@ -8,9 +8,10 @@ CWSDSTUB = $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/CWSDSTUB.EXE CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I../dvx -I../tasks LDFLAGS = -L../lib -ldvx -ltasks -lm -OBJDIR = ../obj/dvxshell -BINDIR = ../bin -LIBDIR = ../lib +OBJDIR = ../obj/dvxshell +BINDIR = ../bin +CONFIGDIR = ../bin/config +LIBDIR = ../lib SRCS = shellMain.c shellApp.c shellExport.c shellInfo.c OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS)) @@ -18,7 +19,7 @@ TARGET = $(BINDIR)/dvx.exe .PHONY: all clean libs -all: libs $(TARGET) +all: libs $(TARGET) $(CONFIGDIR)/dvx.ini libs: $(MAKE) -C ../dvx @@ -30,6 +31,9 @@ $(TARGET): $(OBJS) $(LIBDIR)/libdvx.a $(LIBDIR)/libtasks.a | $(BINDIR) cat $(CWSDSTUB) $(BINDIR)/dvx > $@ rm -f $(BINDIR)/dvx +$(CONFIGDIR)/dvx.ini: ../dvx.ini | $(CONFIGDIR) + cp $< $@ + $(OBJDIR)/%.o: %.c | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< @@ -39,6 +43,9 @@ $(OBJDIR): $(BINDIR): mkdir -p $(BINDIR) +$(CONFIGDIR): + mkdir -p $(CONFIGDIR) + # Dependencies $(OBJDIR)/shellMain.o: shellMain.c shellApp.h ../dvx/dvxApp.h ../dvx/dvxDialog.h ../tasks/taskswitch.h $(OBJDIR)/shellApp.o: shellApp.c shellApp.h ../dvx/dvxApp.h ../dvx/dvxDialog.h ../tasks/taskswitch.h @@ -46,3 +53,4 @@ $(OBJDIR)/shellExport.o: shellExport.c shellApp.h shellInfo.h ../dvx/dvxApp.h . $(OBJDIR)/shellInfo.o: shellInfo.c shellInfo.h shellApp.h ../dvx/dvxApp.h ../dvx/platform/dvxPlatform.h clean: rm -f $(OBJS) $(TARGET) $(BINDIR)/dvx.map $(BINDIR)/dvx.log + rm -rf $(CONFIGDIR) diff --git a/dvxshell/shellApp.c b/dvxshell/shellApp.c index 53cb0f3..acdfbf6 100644 --- a/dvxshell/shellApp.c +++ b/dvxshell/shellApp.c @@ -5,6 +5,7 @@ #include "shellApp.h" #include "dvxDialog.h" +#include "platform/dvxPlatform.h" #include #include @@ -363,21 +364,15 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) { app->dxeCtx.shellCtx = ctx; app->dxeCtx.appId = id; - // Derive app directory from path (everything up to last '/' or '\'). + // Derive app directory from path (everything up to the last separator). // This lets apps load resources relative to their own location rather - // than the shell's working directory. Handles both Unix and DOS path - // separators because DJGPP accepts either. + // than the shell's working directory. snprintf(app->dxeCtx.appDir, sizeof(app->dxeCtx.appDir), "%s", path); - char *lastSlash = strrchr(app->dxeCtx.appDir, '/'); - char *lastBack = strrchr(app->dxeCtx.appDir, '\\'); + char *sep = platformPathDirEnd(app->dxeCtx.appDir); - if (lastBack > lastSlash) { - lastSlash = lastBack; - } - - if (lastSlash) { - *lastSlash = '\0'; + if (sep) { + *sep = '\0'; } else { app->dxeCtx.appDir[0] = '.'; app->dxeCtx.appDir[1] = '\0'; diff --git a/dvxshell/shellExport.c b/dvxshell/shellExport.c index 3235f6a..783389e 100644 --- a/dvxshell/shellExport.c +++ b/dvxshell/shellExport.c @@ -33,6 +33,7 @@ #include "dvxDialog.h" #include "dvxWidget.h" #include "dvxDraw.h" +#include "dvxPrefs.h" #include "dvxVideo.h" #include "dvxWm.h" #include "taskswitch.h" @@ -143,8 +144,14 @@ DXE_EXPORT_TABLE(shellExportTable) { "_dvxCreateWindow", (void *)shellWrapCreateWindow }, { "_dvxDestroyWindow", (void *)shellWrapDestroyWindow }, + // dvxPrefs.h — preferences + DXE_EXPORT(prefsGetBool) + DXE_EXPORT(prefsGetInt) + DXE_EXPORT(prefsGetString) + // dvxApp.h — direct exports DXE_EXPORT(dvxInit) + DXE_EXPORT(dvxSetMouseConfig) DXE_EXPORT(dvxShutdown) DXE_EXPORT(dvxUpdate) { "_dvxCreateWindowCentered", (void *)shellWrapCreateWindowCentered }, diff --git a/dvxshell/shellMain.c b/dvxshell/shellMain.c index acffe09..a2a9003 100644 --- a/dvxshell/shellMain.c +++ b/dvxshell/shellMain.c @@ -27,6 +27,7 @@ #include "shellApp.h" #include "shellInfo.h" #include "dvxDialog.h" +#include "dvxPrefs.h" #include "platform/dvxPlatform.h" #include @@ -220,12 +221,61 @@ void shellRegisterDesktopUpdate(void (*updateFn)(void)) { // main // ============================================================ -int main(void) { +int main(int argc, char *argv[]) { + (void)argc; + + // Change to the directory containing the executable so that relative + // paths (CONFIG/, APPS/, etc.) resolve correctly regardless of where + // the user launched from. + char exeDir[260]; + strncpy(exeDir, argv[0], sizeof(exeDir) - 1); + exeDir[sizeof(exeDir) - 1] = '\0'; + + char *sep = platformPathDirEnd(exeDir); + + if (sep) { + *sep = '\0'; + platformChdir(exeDir); + } + sLogFile = fopen("dvx.log", "w"); shellLog("DVX Shell starting..."); + // Load preferences (missing file or keys silently use defaults) + prefsLoad("CONFIG/DVX.INI"); + + int32_t videoW = prefsGetInt("video", "width", 640); + int32_t videoH = prefsGetInt("video", "height", 480); + int32_t videoBpp = prefsGetInt("video", "bpp", 32); + shellLog("Preferences: video %ldx%ld %ldbpp", (long)videoW, (long)videoH, (long)videoBpp); + // Initialize GUI - int32_t result = dvxInit(&sCtx, 640, 480, 32); + int32_t result = dvxInit(&sCtx, videoW, videoH, videoBpp); + + if (result == 0) { + // Apply mouse preferences + const char *wheelStr = prefsGetString("mouse", "wheel", "normal"); + int32_t wheelDir = (strcmp(wheelStr, "reversed") == 0) ? -1 : 1; + int32_t dblClick = prefsGetInt("mouse", "doubleclick", 500); + + // Map acceleration name to double-speed threshold (mickeys/sec). + // "off" sets a very high threshold so acceleration never triggers. + const char *accelStr = prefsGetString("mouse", "acceleration", "medium"); + int32_t accelVal = 0; + + if (strcmp(accelStr, "off") == 0) { + accelVal = 10000; + } else if (strcmp(accelStr, "low") == 0) { + accelVal = 100; + } else if (strcmp(accelStr, "medium") == 0) { + accelVal = 64; + } else if (strcmp(accelStr, "high") == 0) { + accelVal = 32; + } + + dvxSetMouseConfig(&sCtx, wheelDir, dblClick, accelVal); + shellLog("Preferences: mouse wheel=%s doubleclick=%ldms accel=%s", wheelStr, (long)dblClick, accelStr); + } if (result != 0) { shellLog("Failed to initialize DVX GUI (error %ld)", (long)result); @@ -359,6 +409,7 @@ int main(void) { tsShutdown(); dvxShutdown(&sCtx); + prefsFree(); shellLog("DVX Shell exited.");