383 lines
10 KiB
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;
|
|
}
|