DVX_GUI/apps/notepad/notepad.c

383 lines
10 KiB
C

// notepad.c -- Simple text editor DXE application (callback-only)
//
// A callback-only DXE app (hasMainLoop = false) that provides basic text
// editing with file I/O. Demonstrates the standard DXE app pattern:
// 1. appMain creates window + widget tree, registers callbacks, returns 0
// 2. All editing happens through the TextArea widget's built-in behavior
// 3. File operations and dirty tracking are handled by menu callbacks
//
// The TextArea widget handles keyboard input, cursor movement, selection,
// scrolling, word wrap, and undo internally. Notepad only needs to wire
// up menus and file I/O around it.
#include "dvxApp.h"
#include "dvxDialog.h"
#include "dvxWidget.h"
#include "dvxWm.h"
#include "platform/dvxPlatform.h"
#include "shellApp.h"
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// ============================================================
// Constants
// ============================================================
// 32KB text buffer limit. Keeps memory usage bounded on DOS; larger files
// are silently truncated on load. The TextArea widget allocates this buffer
// internally when constructed.
#define TEXT_BUF_SIZE 32768
#define CMD_NEW 100
#define CMD_OPEN 101
#define CMD_SAVE 102
#define CMD_SAVEAS 103
#define CMD_EXIT 104
#define CMD_CUT 200
#define CMD_COPY 201
#define CMD_PASTE 202
#define CMD_SELALL 203
// ============================================================
// Module state
// ============================================================
static DxeAppContextT *sCtx = NULL;
static WindowT *sWin = NULL;
static WidgetT *sTextArea = NULL;
static char sFilePath[260] = "";
// Hash of text content at last save/open, used for cheap dirty detection
static uint32_t sCleanHash = 0;
// ============================================================
// Prototypes
// ============================================================
int32_t appMain(DxeAppContextT *ctx);
static bool askSaveChanges(void);
static void doNew(void);
static void doOpen(void);
static void doSave(void);
static void doSaveAs(void);
static uint32_t hashText(const char *text);
static bool isDirty(void);
static void markClean(void);
static void onClose(WindowT *win);
static void onMenu(WindowT *win, int32_t menuId);
// ============================================================
// App descriptor
// ============================================================
// Callback-only: the TextArea widget handles all interactive editing
// entirely within the shell's event dispatch, so no dedicated task needed.
AppDescriptorT appDescriptor = {
.name = "Notepad",
.hasMainLoop = false,
.multiInstance = true,
.stackSize = SHELL_STACK_DEFAULT,
.priority = TS_PRIORITY_NORMAL
};
// ============================================================
// Dirty tracking
// ============================================================
// djb2-xor hash for dirty detection. Not cryptographic -- just a fast way
// to detect changes without storing a full copy of the last-saved text.
// False negatives are theoretically possible but vanishingly unlikely for
// text edits. This avoids the memory cost of keeping a shadow buffer.
static uint32_t hashText(const char *text) {
if (!text) {
return 0;
}
uint32_t h = 5381;
while (*text) {
h = ((h << 5) + h) ^ (uint8_t)*text;
text++;
}
return h;
}
static bool isDirty(void) {
const char *text = wgtGetText(sTextArea);
return hashText(text) != sCleanHash;
}
static void markClean(void) {
const char *text = wgtGetText(sTextArea);
sCleanHash = hashText(text);
}
// Returns true if it's OK to proceed (saved, discarded, or not dirty).
// Returns false if the user cancelled.
static bool askSaveChanges(void) {
if (!isDirty()) {
return true;
}
int32_t result = dvxMessageBox(sCtx->shellCtx, "Notepad", "Save changes?", MB_YESNOCANCEL | MB_ICONQUESTION);
if (result == ID_YES) {
doSave();
return true;
}
if (result == ID_NO) {
return true;
}
return false;
}
// ============================================================
// File operations
// ============================================================
static void doNew(void) {
if (!askSaveChanges()) {
return;
}
wgtSetText(sTextArea, "");
sFilePath[0] = '\0';
markClean();
dvxSetTitle(sCtx->shellCtx, sWin, "Untitled - Notepad");
}
static void doOpen(void) {
if (!askSaveChanges()) {
return;
}
FileFilterT filters[] = {
{ "Text Files (*.txt)", "*.txt" },
{ "All Files (*.*)", "*.*" }
};
char path[260];
if (!dvxFileDialog(sCtx->shellCtx, "Open", FD_OPEN, NULL, filters, 2, path, sizeof(path))) {
return;
}
// Open in binary mode to avoid DJGPP's CR/LF translation; the TextArea
// widget handles line endings internally.
FILE *f = fopen(path, "rb");
if (!f) {
dvxMessageBox(sCtx->shellCtx, "Error", "Could not open file.", MB_OK | MB_ICONERROR);
return;
}
// Read entire file into a temporary buffer. Files larger than the
// TextArea's buffer are silently truncated -- this matches the behavior
// of Windows Notepad on large files.
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
if (size >= TEXT_BUF_SIZE - 1) {
size = TEXT_BUF_SIZE - 2;
}
char *buf = (char *)malloc(size + 1);
if (buf) {
fread(buf, 1, size, f);
buf[size] = '\0';
platformStripLineEndings(buf, (int32_t)size);
wgtSetText(sTextArea, buf);
free(buf);
}
fclose(f);
snprintf(sFilePath, sizeof(sFilePath), "%s", path);
markClean();
char title[300];
snprintf(title, sizeof(title), "%s - Notepad", sFilePath);
dvxSetTitle(sCtx->shellCtx, sWin, title);
}
static void doSave(void) {
if (sFilePath[0] == '\0') {
doSaveAs();
return;
}
const char *text = wgtGetText(sTextArea);
if (!text) {
return;
}
FILE *f = fopen(sFilePath, "wb");
if (!f) {
dvxMessageBox(sCtx->shellCtx, "Error", "Could not save file.", MB_OK | MB_ICONERROR);
return;
}
const char *eol = platformLineEnding();
for (const char *p = text; *p; p++) {
if (*p == '\n') {
fputs(eol, f);
} else {
fputc(*p, f);
}
}
fclose(f);
markClean();
}
static void doSaveAs(void) {
FileFilterT filters[] = {
{ "Text Files (*.txt)", "*.txt" },
{ "All Files (*.*)", "*.*" }
};
char path[260];
if (!dvxFileDialog(sCtx->shellCtx, "Save As", FD_SAVE, NULL, filters, 2, path, sizeof(path))) {
return;
}
snprintf(sFilePath, sizeof(sFilePath), "%s", path);
doSave();
char title[300];
snprintf(title, sizeof(title), "%s - Notepad", sFilePath);
dvxSetTitle(sCtx->shellCtx, sWin, title);
}
// ============================================================
// Callbacks
// ============================================================
// onClose is the only cleanup path for a callback-only app. We null our
// pointers after destroying the window; the shell will reap the DXE on the
// next frame when it detects no windows remain for this appId.
static void onClose(WindowT *win) {
if (!askSaveChanges()) {
return;
}
dvxDestroyWindow(sCtx->shellCtx, win);
sWin = NULL;
sTextArea = NULL;
}
static void onMenu(WindowT *win, int32_t menuId) {
(void)win;
switch (menuId) {
case CMD_NEW:
doNew();
break;
case CMD_OPEN:
doOpen();
break;
case CMD_SAVE:
doSave();
break;
case CMD_SAVEAS:
doSaveAs();
break;
case CMD_EXIT:
onClose(sWin);
break;
case CMD_CUT:
break;
case CMD_COPY:
break;
case CMD_PASTE:
break;
case CMD_SELALL:
break;
}
}
// ============================================================
// Entry point
// ============================================================
// Entry point. Offset +20 from center so multiple notepad instances cascade
// naturally rather than stacking exactly on top of each other.
int32_t appMain(DxeAppContextT *ctx) {
sCtx = ctx;
AppContextT *ac = ctx->shellCtx;
int32_t screenW = ac->display.width;
int32_t screenH = ac->display.height;
int32_t winW = 480;
int32_t winH = 360;
int32_t winX = (screenW - winW) / 2 + 20;
int32_t winY = (screenH - winH) / 3 + 20;
sWin = dvxCreateWindow(ac, "Untitled - Notepad", winX, winY, winW, winH, true);
if (!sWin) {
return -1;
}
sWin->onClose = onClose;
sWin->onMenu = onMenu;
// Menu bar
MenuBarT *menuBar = wmAddMenuBar(sWin);
MenuT *fileMenu = wmAddMenu(menuBar, "&File");
wmAddMenuItem(fileMenu, "&New", CMD_NEW);
wmAddMenuItem(fileMenu, "&Open...", CMD_OPEN);
wmAddMenuSeparator(fileMenu);
wmAddMenuItem(fileMenu, "&Save", CMD_SAVE);
wmAddMenuItem(fileMenu, "Save &As...", CMD_SAVEAS);
wmAddMenuSeparator(fileMenu);
wmAddMenuItem(fileMenu, "E&xit", CMD_EXIT);
MenuT *editMenu = wmAddMenu(menuBar, "&Edit");
wmAddMenuItem(editMenu, "Cu&t\tCtrl+X", CMD_CUT);
wmAddMenuItem(editMenu, "&Copy\tCtrl+C", CMD_COPY);
wmAddMenuItem(editMenu, "&Paste\tCtrl+V", CMD_PASTE);
wmAddMenuSeparator(editMenu);
wmAddMenuItem(editMenu, "Select &All\tCtrl+A", CMD_SELALL);
// The widget tree is minimal: just a TextArea filling the entire content
// area (weight=100). The TextArea widget provides editing, scrolling,
// selection, copy/paste, and undo -- all driven by keyboard events that
// the shell dispatches to the focused widget.
WidgetT *root = wgtInitWindow(ac, sWin);
sTextArea = wgtTextArea(root, TEXT_BUF_SIZE);
sTextArea->weight = 100;
sFilePath[0] = '\0';
markClean();
wgtInvalidate(root);
return 0;
}