Files
classicnotepad/ClasicNotepad.c
T
2025-12-03 13:24:05 -05:00

594 lines
16 KiB
C

#define UNICODE
#define _UNICODE
#define __STDC_WANT_LIB_EXT1__ 1
#include <windows.h>
#include <commdlg.h>
#include <stdbool.h>
#include <stdio.h>
#include <wchar.h>
#ifndef swprintf_s
int swprintf_s(wchar_t *buffer, size_t sizeOfBuffer, const wchar_t *format, ...);
#endif
#define IDI_APP 101
#define IDM_FILE_NEW 1001
#define IDM_FILE_OPEN 1002
#define IDM_FILE_SAVE 1003
#define IDM_FILE_SAVE_AS 1004
#define IDM_FILE_EXIT 1005
#define IDM_EDIT_UNDO 2001
#define IDM_EDIT_CUT 2002
#define IDM_EDIT_COPY 2003
#define IDM_EDIT_PASTE 2004
#define IDM_EDIT_DELETE 2005
#define IDM_EDIT_SELECT 2006
#define IDM_EDIT_TIME 2007
#define IDM_FORMAT_WRAP 3001
#define IDM_FORMAT_FONT 3002
#define IDM_HELP_ABOUT 4001
static HWND g_hEdit = NULL;
static HFONT g_hFont = NULL;
static HMENU g_hMenu = NULL;
static HACCEL g_hAccel = NULL;
static bool g_wordWrap = false;
static WCHAR g_filePath[MAX_PATH] = L"";
static LOGFONTW g_logFont = {0};
#ifndef ARRAYSIZE
#define ARRAYSIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
#endif
int swprintf_s(wchar_t *buffer, size_t sizeOfBuffer, const wchar_t *format, ...);
static void CopyStringSafe(WCHAR *dest, size_t destCount, const WCHAR *src)
{
if (!dest || destCount == 0)
{
return;
}
if (!src)
{
dest[0] = L'\0';
return;
}
wcsncpy(dest, src, destCount - 1);
dest[destCount - 1] = L'\0';
}
static void UpdateTitle(HWND hwnd)
{
const WCHAR *name = g_filePath;
const WCHAR *lastSlash = wcsrchr(g_filePath, L'\\');
if (lastSlash && *(lastSlash + 1) != L'\0')
{
name = lastSlash + 1;
}
if (name[0] == L'\0')
{
name = L"Untitled";
}
WCHAR title[512];
swprintf_s(title, ARRAYSIZE(title), L"%s - ClasicNotepad", name);
SetWindowTextW(hwnd, title);
}
static void SetDefaultFont(HWND hwnd)
{
if (!g_hFont)
{
if (!SystemParametersInfoW(SPI_GETICONTITLELOGFONT, sizeof(g_logFont), &g_logFont, 0))
{
ZeroMemory(&g_logFont, sizeof(g_logFont));
g_logFont.lfHeight = -11;
wcscpy(g_logFont.lfFaceName, L"Segoe UI");
}
g_hFont = CreateFontIndirectW(&g_logFont);
}
SendMessage(hwnd, WM_SETFONT, (WPARAM)g_hFont, TRUE);
}
static void ResizeEdit(HWND hwndParent)
{
RECT rc;
GetClientRect(hwndParent, &rc);
MoveWindow(g_hEdit, 0, 0, rc.right - rc.left, rc.bottom - rc.top, TRUE);
}
static void RecreateEdit(HWND hwndParent)
{
int len = GetWindowTextLengthW(g_hEdit);
WCHAR *buffer = (WCHAR *)HeapAlloc(GetProcessHeap(), 0, (len + 1) * sizeof(WCHAR));
if (buffer)
{
GetWindowTextW(g_hEdit, buffer, len + 1);
}
DestroyWindow(g_hEdit);
DWORD styles = WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_TABSTOP | ES_LEFT | ES_MULTILINE | ES_AUTOVSCROLL | ES_WANTRETURN;
if (!g_wordWrap)
{
styles |= WS_HSCROLL | ES_AUTOHSCROLL;
}
g_hEdit = CreateWindowExW(0, L"EDIT", NULL, styles, 0, 0, 0, 0, hwndParent, (HMENU)1, GetModuleHandle(NULL), NULL);
SetDefaultFont(g_hEdit);
if (buffer)
{
SetWindowTextW(g_hEdit, buffer);
SendMessageW(g_hEdit, EM_SETSEL, (WPARAM)len, (LPARAM)len);
HeapFree(GetProcessHeap(), 0, buffer);
}
ResizeEdit(hwndParent);
}
static HMENU BuildMenu(void)
{
HMENU hMenu = CreateMenu();
HMENU hFile = CreatePopupMenu();
AppendMenuW(hFile, MF_STRING, IDM_FILE_NEW, L"&New\tCtrl+N");
AppendMenuW(hFile, MF_STRING, IDM_FILE_OPEN, L"&Open...\tCtrl+O");
AppendMenuW(hFile, MF_STRING, IDM_FILE_SAVE, L"&Save\tCtrl+S");
AppendMenuW(hFile, MF_STRING, IDM_FILE_SAVE_AS, L"Save &As...");
AppendMenuW(hFile, MF_SEPARATOR, 0, NULL);
AppendMenuW(hFile, MF_STRING, IDM_FILE_EXIT, L"E&xit");
HMENU hEdit = CreatePopupMenu();
AppendMenuW(hEdit, MF_STRING, IDM_EDIT_UNDO, L"&Undo\tCtrl+Z");
AppendMenuW(hEdit, MF_SEPARATOR, 0, NULL);
AppendMenuW(hEdit, MF_STRING, IDM_EDIT_CUT, L"Cu&t\tCtrl+X");
AppendMenuW(hEdit, MF_STRING, IDM_EDIT_COPY, L"&Copy\tCtrl+C");
AppendMenuW(hEdit, MF_STRING, IDM_EDIT_PASTE, L"&Paste\tCtrl+V");
AppendMenuW(hEdit, MF_STRING, IDM_EDIT_DELETE, L"&Delete\tDel");
AppendMenuW(hEdit, MF_SEPARATOR, 0, NULL);
AppendMenuW(hEdit, MF_STRING, IDM_EDIT_SELECT, L"Select &All\tCtrl+A");
AppendMenuW(hEdit, MF_STRING, IDM_EDIT_TIME, L"Time/&Date\tF5");
HMENU hFormat = CreatePopupMenu();
AppendMenuW(hFormat, MF_STRING | (g_wordWrap ? MF_CHECKED : 0), IDM_FORMAT_WRAP, L"&Word Wrap");
AppendMenuW(hFormat, MF_STRING, IDM_FORMAT_FONT, L"&Font...");
HMENU hHelp = CreatePopupMenu();
AppendMenuW(hHelp, MF_STRING, IDM_HELP_ABOUT, L"&About ClasicNotepad");
AppendMenuW(hMenu, MF_POPUP, (UINT_PTR)hFile, L"&File");
AppendMenuW(hMenu, MF_POPUP, (UINT_PTR)hEdit, L"&Edit");
AppendMenuW(hMenu, MF_POPUP, (UINT_PTR)hFormat, L"F&ormat");
AppendMenuW(hMenu, MF_POPUP, (UINT_PTR)hHelp, L"&Help");
return hMenu;
}
static void UpdateWrapMenu(void)
{
HMENU hFormat = GetSubMenu(g_hMenu, 2);
CheckMenuItem(hFormat, IDM_FORMAT_WRAP, MF_BYCOMMAND | (g_wordWrap ? MF_CHECKED : MF_UNCHECKED));
}
static bool ShowOpenDialog(HWND hwnd, WCHAR *pathBuffer, DWORD cchBuffer)
{
pathBuffer[0] = L'\0';
OPENFILENAMEW ofn = {0};
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = hwnd;
ofn.lpstrFilter = L"Text Files (*.txt)\0*.txt\0All Files (*.*)\0*.*\0";
ofn.lpstrFile = pathBuffer;
ofn.nMaxFile = cchBuffer;
ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_EXPLORER;
ofn.lpstrDefExt = L"txt";
return GetOpenFileNameW(&ofn);
}
static bool ShowSaveDialog(HWND hwnd)
{
OPENFILENAMEW ofn = {0};
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = hwnd;
ofn.lpstrFilter = L"Text Files (*.txt)\0*.txt\0All Files (*.*)\0*.*\0";
ofn.lpstrFile = g_filePath;
ofn.nMaxFile = MAX_PATH;
ofn.Flags = OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST | OFN_EXPLORER;
ofn.lpstrDefExt = L"txt";
return GetSaveFileNameW(&ofn);
}
static bool LoadFileToEdit(HWND hwnd, const WCHAR *path)
{
HANDLE hFile = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
MessageBoxW(hwnd, L"Unable to open the file.", L"Error", MB_ICONERROR | MB_OK);
return false;
}
LARGE_INTEGER size;
if (!GetFileSizeEx(hFile, &size) || size.QuadPart > 20 * 1024 * 1024)
{
CloseHandle(hFile);
MessageBoxW(hwnd, L"File is too large to open.", L"Error", MB_ICONERROR | MB_OK);
return false;
}
DWORD bytesToRead = (DWORD)size.QuadPart;
BYTE *buffer = (BYTE *)HeapAlloc(GetProcessHeap(), 0, bytesToRead + 3);
DWORD bytesRead = 0;
bool success = false;
if (buffer && ReadFile(hFile, buffer, bytesToRead, &bytesRead, NULL))
{
int skip = 0;
UINT codePage = CP_UTF8;
if (bytesRead >= 3 && buffer[0] == 0xEF && buffer[1] == 0xBB && buffer[2] == 0xBF)
{
skip = 3;
}
else if (bytesRead >= 2 && buffer[0] == 0xFF && buffer[1] == 0xFE)
{
codePage = 1200; // UTF-16 LE
}
else if (bytesRead >= 2 && buffer[0] == 0xFE && buffer[1] == 0xFF)
{
codePage = 1201; // UTF-16 BE
}
else
{
codePage = CP_ACP;
}
int wideLen = MultiByteToWideChar(codePage, 0, (LPCCH)(buffer + skip), bytesRead - skip, NULL, 0);
WCHAR *wbuf = (WCHAR *)HeapAlloc(GetProcessHeap(), 0, (wideLen + 1) * sizeof(WCHAR));
if (wbuf)
{
MultiByteToWideChar(codePage, 0, (LPCCH)(buffer + skip), bytesRead - skip, wbuf, wideLen);
wbuf[wideLen] = L'\0';
SetWindowTextW(g_hEdit, wbuf);
HeapFree(GetProcessHeap(), 0, wbuf);
success = true;
}
}
if (buffer)
{
HeapFree(GetProcessHeap(), 0, buffer);
}
CloseHandle(hFile);
if (!success)
{
MessageBoxW(hwnd, L"Unable to read the file.", L"Error", MB_ICONERROR | MB_OK);
}
return success;
}
static bool SaveFileFromEdit(HWND hwnd, const WCHAR *path)
{
int len = GetWindowTextLengthW(g_hEdit);
WCHAR *wbuf = (WCHAR *)HeapAlloc(GetProcessHeap(), 0, (len + 1) * sizeof(WCHAR));
if (!wbuf)
{
MessageBoxW(hwnd, L"Not enough memory to save.", L"Error", MB_ICONERROR | MB_OK);
return false;
}
GetWindowTextW(g_hEdit, wbuf, len + 1);
HANDLE hFile = CreateFileW(path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
HeapFree(GetProcessHeap(), 0, wbuf);
MessageBoxW(hwnd, L"Unable to create the file.", L"Error", MB_ICONERROR | MB_OK);
return false;
}
// Save as UTF-8 with BOM for reliable re-open.
BYTE bom[] = {0xEF, 0xBB, 0xBF};
DWORD written = 0;
bool success = false;
if (!WriteFile(hFile, bom, sizeof(bom), &written, NULL))
{
MessageBoxW(hwnd, L"Failed to save the file.", L"Error", MB_ICONERROR | MB_OK);
HeapFree(GetProcessHeap(), 0, wbuf);
CloseHandle(hFile);
return false;
}
int utf8Len = WideCharToMultiByte(CP_UTF8, 0, wbuf, len, NULL, 0, NULL, NULL);
if (utf8Len > 0)
{
BYTE *mbuf = (BYTE *)HeapAlloc(GetProcessHeap(), 0, utf8Len);
if (mbuf)
{
WideCharToMultiByte(CP_UTF8, 0, wbuf, len, (LPSTR)mbuf, utf8Len, NULL, NULL);
if (WriteFile(hFile, mbuf, utf8Len, &written, NULL))
{
success = true;
}
HeapFree(GetProcessHeap(), 0, mbuf);
}
}
else
{
success = true; // Empty file is fine.
}
HeapFree(GetProcessHeap(), 0, wbuf);
CloseHandle(hFile);
if (!success)
{
MessageBoxW(hwnd, L"Failed to save the file.", L"Error", MB_ICONERROR | MB_OK);
}
return success;
}
static void DoFileNew(HWND hwnd)
{
g_filePath[0] = L'\0';
SetWindowTextW(g_hEdit, L"");
UpdateTitle(hwnd);
}
static void DoFileOpen(HWND hwnd)
{
WCHAR original[MAX_PATH];
CopyStringSafe(original, ARRAYSIZE(original), g_filePath);
if (!ShowOpenDialog(hwnd, g_filePath, MAX_PATH))
{
CopyStringSafe(g_filePath, ARRAYSIZE(g_filePath), original);
return;
}
if (LoadFileToEdit(hwnd, g_filePath))
{
UpdateTitle(hwnd);
}
else
{
CopyStringSafe(g_filePath, ARRAYSIZE(g_filePath), original);
}
}
static void DoFileSave(HWND hwnd)
{
if (g_filePath[0] == L'\0')
{
if (!ShowSaveDialog(hwnd))
{
return;
}
}
if (SaveFileFromEdit(hwnd, g_filePath))
{
UpdateTitle(hwnd);
}
}
static void DoFileSaveAs(HWND hwnd)
{
WCHAR backup[MAX_PATH];
CopyStringSafe(backup, ARRAYSIZE(backup), g_filePath);
if (ShowSaveDialog(hwnd))
{
if (SaveFileFromEdit(hwnd, g_filePath))
{
UpdateTitle(hwnd);
}
else
{
CopyStringSafe(g_filePath, ARRAYSIZE(g_filePath), backup);
}
}
}
static void InsertTimeDate(void)
{
SYSTEMTIME st;
GetLocalTime(&st);
WCHAR text[64];
swprintf_s(text, ARRAYSIZE(text), L"%02d/%02d/%04d %02d:%02d", st.wMonth, st.wDay, st.wYear, st.wHour, st.wMinute);
SendMessageW(g_hEdit, EM_REPLACESEL, TRUE, (LPARAM)text);
}
static void ToggleWordWrap(HWND hwnd)
{
g_wordWrap = !g_wordWrap;
RecreateEdit(hwnd);
UpdateWrapMenu();
}
static void ChooseFontAndApply(HWND hwnd)
{
CHOOSEFONTW cf = {0};
cf.lStructSize = sizeof(cf);
cf.hwndOwner = hwnd;
cf.lpLogFont = &g_logFont;
cf.Flags = CF_SCREENFONTS | CF_INITTOLOGFONTSTRUCT | CF_EFFECTS;
if (ChooseFontW(&cf))
{
if (g_hFont)
{
DeleteObject(g_hFont);
g_hFont = NULL;
}
g_hFont = CreateFontIndirectW(&g_logFont);
SetDefaultFont(g_hEdit);
}
}
static void ShowAbout(HWND hwnd)
{
MessageBoxW(hwnd, L"ClasicNotepad\nBuilt with Win32 API in C.", L"About", MB_OK | MB_ICONINFORMATION);
}
static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg)
{
case WM_CREATE:
g_hMenu = BuildMenu();
SetMenu(hwnd, g_hMenu);
RecreateEdit(hwnd);
UpdateTitle(hwnd);
return 0;
case WM_SIZE:
ResizeEdit(hwnd);
return 0;
case WM_COMMAND:
switch (LOWORD(wParam))
{
case IDM_FILE_NEW:
DoFileNew(hwnd);
break;
case IDM_FILE_OPEN:
DoFileOpen(hwnd);
break;
case IDM_FILE_SAVE:
DoFileSave(hwnd);
break;
case IDM_FILE_SAVE_AS:
DoFileSaveAs(hwnd);
break;
case IDM_FILE_EXIT:
PostMessageW(hwnd, WM_CLOSE, 0, 0);
break;
case IDM_EDIT_UNDO:
SendMessageW(g_hEdit, WM_UNDO, 0, 0);
break;
case IDM_EDIT_CUT:
SendMessageW(g_hEdit, WM_CUT, 0, 0);
break;
case IDM_EDIT_COPY:
SendMessageW(g_hEdit, WM_COPY, 0, 0);
break;
case IDM_EDIT_PASTE:
SendMessageW(g_hEdit, WM_PASTE, 0, 0);
break;
case IDM_EDIT_DELETE:
SendMessageW(g_hEdit, WM_CLEAR, 0, 0);
break;
case IDM_EDIT_SELECT:
SendMessageW(g_hEdit, EM_SETSEL, 0, -1);
break;
case IDM_EDIT_TIME:
InsertTimeDate();
break;
case IDM_FORMAT_WRAP:
ToggleWordWrap(hwnd);
break;
case IDM_FORMAT_FONT:
ChooseFontAndApply(hwnd);
break;
case IDM_HELP_ABOUT:
ShowAbout(hwnd);
break;
}
return 0;
case WM_SETFOCUS:
SetFocus(g_hEdit);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProcW(hwnd, msg, wParam, lParam);
}
int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow)
{
(void)hPrevInstance;
(void)lpCmdLine;
{
HMODULE hUser = GetModuleHandleW(L"user32.dll");
if (hUser)
{
BOOL(WINAPI *pSetProcessDPIAware)(void) = (BOOL(WINAPI *)(void))GetProcAddress(hUser, "SetProcessDPIAware");
if (pSetProcessDPIAware)
{
pSetProcessDPIAware();
}
}
}
ACCEL accels[] = {
{FVIRTKEY | FCONTROL, 'N', IDM_FILE_NEW},
{FVIRTKEY | FCONTROL, 'O', IDM_FILE_OPEN},
{FVIRTKEY | FCONTROL, 'S', IDM_FILE_SAVE},
{FVIRTKEY | FCONTROL, 'A', IDM_EDIT_SELECT},
{FVIRTKEY | FCONTROL, 'Z', IDM_EDIT_UNDO},
{FVIRTKEY | FCONTROL, 'X', IDM_EDIT_CUT},
{FVIRTKEY | FCONTROL, 'C', IDM_EDIT_COPY},
{FVIRTKEY | FCONTROL, 'V', IDM_EDIT_PASTE},
{FVIRTKEY, VK_F5, IDM_EDIT_TIME},
};
g_hAccel = CreateAcceleratorTableW(accels, sizeof(accels) / sizeof(accels[0]));
WNDCLASSEXW wc = {0};
wc.cbSize = sizeof(wc);
wc.lpfnWndProc = WndProc;
wc.hInstance = hInstance;
HICON hIconLarge = LoadIconW(GetModuleHandleW(NULL), MAKEINTRESOURCEW(IDI_APP));
HICON hIconSmall = LoadIconW(GetModuleHandleW(NULL), MAKEINTRESOURCEW(IDI_APP));
if (!hIconLarge)
{
hIconLarge = LoadIcon(NULL, IDI_APPLICATION);
}
if (!hIconSmall)
{
hIconSmall = LoadIcon(NULL, IDI_APPLICATION);
}
wc.hIcon = hIconLarge;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wc.lpszClassName = L"ClasicNotepadClass";
wc.hIconSm = hIconSmall;
if (!RegisterClassExW(&wc))
{
MessageBoxW(NULL, L"Failed to register window class.", L"Error", MB_ICONERROR | MB_OK);
return -1;
}
HWND hwnd = CreateWindowExW(0, wc.lpszClassName, L"ClasicNotepad", WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, NULL, NULL, hInstance, NULL);
if (!hwnd)
{
MessageBoxW(NULL, L"Failed to create window.", L"Error", MB_ICONERROR | MB_OK);
return -1;
}
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
MSG msg;
while (GetMessageW(&msg, NULL, 0, 0) > 0)
{
if (!TranslateAcceleratorW(hwnd, g_hAccel, &msg))
{
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
return (int)msg.wParam;
}
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
(void)lpCmdLine;
return wWinMain(hInstance, hPrevInstance, GetCommandLineW(), nCmdShow);
}