From 031b19a9c8135009100fc41fcd2257f6d9ae7b87 Mon Sep 17 00:00:00 2001 From: Justin Marshall Date: Thu, 6 Aug 2026 23:57:43 -0700 Subject: [PATCH] File Save/Open now works correctly. --- src/Opus/FILE2.C | 6 + src/Opus/create.c | 4 +- src/Opus/filewin.c | 2 +- src/Opus/message.h | 4 + src/Opus/prompt.h | 10 + src/Opus/rulerdrw.c | 18 ++ src/Opus/save.c | 34 +++ src/Opus/wordtech/file.h | 8 + src/Opus/wordtech/savefast.c | 2 +- src/Opus/wordtech/word.h | 9 + src/Opus/wproc.c | 2 + src/Opus/wwchange.c | 7 +- src/port/original/opus_asm_filewin.cpp | 41 +++ src/port/original/opus_sdm_runtime.cpp | 354 ++++++++++++++++++++++++ src/port/original/opus_win95_chrome.cpp | 148 +++++++++- src/port/original/opus_x64_compat.h | 14 + 16 files changed, 655 insertions(+), 8 deletions(-) diff --git a/src/Opus/FILE2.C b/src/Opus/FILE2.C index 274a3a6..b9c4ea0 100644 --- a/src/Opus/FILE2.C +++ b/src/Opus/FILE2.C @@ -473,7 +473,13 @@ int fDelete; /* delete the file */ /* the sad truth is that we couldn't care less whether the file is deleted; better to let the user see temp files than warn him the delete failed */ if (fDelete) + { +#ifdef OPUS_X64 + EcOsDelete(st, 0); /* volume is unused by the Windows implementation */ +#else EcOsDelete(st, vol); +#endif + } } /* end DeleteFn */ diff --git a/src/Opus/create.c b/src/Opus/create.c index 61903d1..a20e336 100644 --- a/src/Opus/create.c +++ b/src/Opus/create.c @@ -1692,7 +1692,7 @@ uns cbTotal; /* %%Function:HplReadPlf %%Owner:peterj */ /* H P L R E A D P L F */ -HplReadPlf(fn, fcFirst, cb) +struct PL **HplReadPlf(fn, fcFirst, cb) int fn; FC fcFirst; int cb; @@ -1714,7 +1714,7 @@ int cb; /* %%Function:HsttbReadSttbFromFile %%Owner:peterj */ /* H S T T B R E A D S T T B F F R O M F I L E */ -HsttbReadSttbfFromFile(fn, fc, fExternal, fStyleRules, cbExtra) +struct STTB **HsttbReadSttbfFromFile(fn, fc, fExternal, fStyleRules, cbExtra) int fn; FC fc; BOOL fExternal, fStyleRules; diff --git a/src/Opus/filewin.c b/src/Opus/filewin.c index 9e5bbd5..1c4a423 100644 --- a/src/Opus/filewin.c +++ b/src/Opus/filewin.c @@ -587,7 +587,7 @@ PN pn; Assert (vbptbExt.ibpMax < 32768 / 4); ispnMax = ((vbptbExt.ibpMax << 2) / vbptbExt.cqbpspn); - Debug(ispnOldest = ispnSecondOldest = -1); + ispnOldest = ispnSecondOldest = -1; tsOldest = tsSecondOldest = 0; for (ispn = 0; ispn < ispnMax; ispn++) { diff --git a/src/Opus/message.h b/src/Opus/message.h index 8d0f6cd..dda1f72 100644 --- a/src/Opus/message.h +++ b/src/Opus/message.h @@ -29,6 +29,10 @@ typedef struct } MST; #ifdef OPUS_X64 +struct PPR; +struct PPR **HpprStartProgressReport(MST mst, unsigned *rgw, int nIncr, + int fAbortCheck); + static __inline MST OpusMakeMst(cxt, st) int cxt; char *st; diff --git a/src/Opus/prompt.h b/src/Opus/prompt.h index 2703a69..7d78734 100644 --- a/src/Opus/prompt.h +++ b/src/Opus/prompt.h @@ -92,7 +92,17 @@ struct PPR #define MmoFromCmg(cmg) ((cmg) & mmoNewestCmg) +#ifndef OPUS_X64 struct PPR **HpprStartProgressReport(); +#endif +#ifdef OPUS_X64 +/* The AMD64 prompt implementation lives in opus_asm_prompt.cpp. A complete + prototype is required here: with an unprototyped call, a literal NULL in + the fifth argument only initializes part of the native pointer stack slot. */ +void ProgressReportPercent(struct PPR **hppr, long lLow, long lHigh, + long l, long *plNext); +void ChangeProgressReport(struct PPR **hppr, unsigned nNew); +#endif NATIVE FQueryAbortCheckProc(); EXPORT AdjustPrompt(); diff --git a/src/Opus/rulerdrw.c b/src/Opus/rulerdrw.c index c6638fb..b7c2d6d 100644 --- a/src/Opus/rulerdrw.c +++ b/src/Opus/rulerdrw.c @@ -103,6 +103,24 @@ static int vWin95ZoomPercent = 100; static int vWin95BaseDxsInch = 0; static int vWin95BaseDysInch = 0; +/* The Word 1.x Color command opens a keyboard prompt. The Win95 chrome + supplies a palette instead, but still applies the original character + property so document storage and formatting behavior remain unchanged. */ +int OpusApplyWin95TextColor(hwnd, ico) +HWND hwnd; +int ico; +{ + int ww = WwFromHwnd(hwnd); + + if (ww < wwDocMin || ww >= wwMac || selCur.ww != ww || + ico < icoAuto || ico >= icoMax) + return fFalse; + if (!FApplyOneProp(sprmCIco, ico)) + return fFalse; + selCur.fUpdateRuler = fTrue; + return fTrue; +} + /* Read-only layout metrics used by the Win95 vertical ruler overlay. */ int OpusGetWin95VerticalRulerMetrics(hwnd, pypPageTop, pypPageBottom, pdypTopMargin, pdypBottomMargin, pdypInch) diff --git a/src/Opus/save.c b/src/Opus/save.c index 76a7781..270d092 100644 --- a/src/Opus/save.c +++ b/src/Opus/save.c @@ -86,6 +86,14 @@ DEBUGASSERTSZ /* WIN - bogus macro for assert string */ #include "saveas.hs" #include "saveas.sdm" +#ifdef OPUS_X64 +extern int OpusWin95SaveAliasMatches(); +extern int OpusFinishWin95SaveAlias(); +#define FWin95SaveAliasMatches(st) OpusWin95SaveAliasMatches(st) +#else +#define FWin95SaveAliasMatches(st) fFalse +#endif + #ifdef PROTOTYPE #include "save.cpt" @@ -548,6 +556,13 @@ LSaveAs: AssertDo(FNormalizeStFile(stT, stFile, (*hcab)->iFmt == ISaveFmtDff(dffSaveDocType) ? nfoDot : nfoNormal)); + #ifdef OPUS_X64 + /* The restored SDM summary-properties dialog is not part of the + Win95 common-dialog path. Do not leave Save As waiting on that + hidden secondary modal; properties remain available from File. */ + fPromptSI = fFalse; + PdodDoc(doc)->fPromptSI = fFalse; + #endif } if (pcmb->fCheck) @@ -677,6 +692,14 @@ LSaveAs: #endif /* DCORELOAD */ LCleanup: + #ifdef OPUS_X64 + if (cmd == cmdOK && pcmb->fAction && + FWin95SaveAliasMatches(stFile) && PdodDoc(doc)->fn != fnNil) + FCloseFn(PdodDoc(doc)->fn); + if (!OpusFinishWin95SaveAlias(stFile, + cmd == cmdOK && pcmb->fAction) && cmd == cmdOK) + cmd = cmdError; + #endif if (cmd == cmdCancelled) fGuaranteeSave = fFalse; @@ -960,6 +983,7 @@ LStayIn: on the currently-on-line floppy */ if (FExistsStFile(stFile, fFalse /* fAnywhere */) && + !FWin95SaveAliasMatches(stFile) && ((fn = PdodDoc(pcesa->doc)->fn) == fnNil || FnFromOnlineSt(stFile) != fn)) { @@ -989,6 +1013,7 @@ LStayIn: } /* check for overwritting foreign file */ + if (!FWin95SaveAliasMatches(stFile)) switch (IdOverwriteForeign (pcesa->doc, stFile, (*hcab)->iFmt)) { case IDYES: @@ -2889,12 +2914,21 @@ CHAR *stFile; int doc; { +#ifdef OPUS_X64 + /* The original progress-report handle can be retired by nested prompt + activity while the save writer is still holding vhpprSave. Leave the + save progress field inactive on the flat port; the normal completion + prompt below still reports the saved document and character count. */ + vhpprSave = hNil; + return 0; +#else int nIncr = NIncrFromL(CpMacDoc(doc)); Assert(vhpprSave == hNil); if ((vhpprSave = HpprStartProgressReport (mst, &stFile, nIncr, fFalse)) == hNil) /* no biggie */ vmerr.mat = matNil; +#endif } diff --git a/src/Opus/wordtech/file.h b/src/Opus/wordtech/file.h index f841a53..b3da4a6 100644 --- a/src/Opus/wordtech/file.h +++ b/src/Opus/wordtech/file.h @@ -1023,6 +1023,14 @@ struct PLB int fNoTsNonDest; /* don't update tsMru on fns != fnDest */ }; +#ifdef OPUS_X64 +/* These readers return movable-heap handles. The original K&R sources + * relied on implicit-int declarations, which truncate the handles on AMD64. */ +struct STTB **HsttbReadSttbfFromFile(int fn, FC fc, int fExternal, + int fStyleRules, int cbExtra); +struct PL **HplReadPlf(int fn, FC fcFirst, int cb); +#endif + /* fast io block buffer */ #ifdef OPUS_X64 diff --git a/src/Opus/wordtech/savefast.c b/src/Opus/wordtech/savefast.c index 090b6e4..211574a 100644 --- a/src/Opus/wordtech/savefast.c +++ b/src/Opus/wordtech/savefast.c @@ -1369,7 +1369,7 @@ int *pfPieceTableItem; { vpqsib->ipcd = IInPlc(hplcpcd, vpqsib->cpPicFetch - 1); vpqsib->cpPicFetchLim = CpPlc(hplcpcd, vpqsib->ipcd + 1); - if (ipcd + 1 == IMacPlc(hplcpcd)) + if (vpqsib->ipcd + 1 == IMacPlc(hplcpcd)) vpqsib->cpPicFetchLim--; } } diff --git a/src/Opus/wordtech/word.h b/src/Opus/wordtech/word.h index 6e05203..5377458 100644 --- a/src/Opus/wordtech/word.h +++ b/src/Opus/wordtech/word.h @@ -601,8 +601,13 @@ rather than before the first char of line dl (same cp position!) */ struct MERR { +#ifdef OPUS_X64 + void **hrgwEmerg1; /* native movable handle; hNil means stage 1 emergency */ + void **hrgwEmerg2; /* native movable handle reserved for Save */ +#else int hrgwEmerg1; /* if hNil, we have stage 1 heap emergency */ int hrgwEmerg2; /* for use by Save */ +#endif int fDocFull; /* maximum number of docs has been allocated */ int fFnFull; /* maximum number of fns has been allocated */ int fWwFull; /* maximum number of wws has been allocated */ @@ -646,7 +651,11 @@ struct MERR int fPrintEmerg; /* Set if printer DC is unavailable */ int fDclFull; /* maximum number of dcls (dde channels) allocated */ #endif +#ifdef OPUS_X64 + void **hrgwEmerg3; /* native movable handle reserved for EmergSave */ +#else int hrgwEmerg3; /* for use by EmergSave */ +#endif int fWarnDocTooBig; /* set if some doc has grown beyond cpWarnTooBig */ #ifdef MAC long qqrgwEmergSave; /* Mac - to make sure we are able to save */ diff --git a/src/Opus/wproc.c b/src/Opus/wproc.c index e483160..343a24a 100644 --- a/src/Opus/wproc.c +++ b/src/Opus/wproc.c @@ -1753,6 +1753,8 @@ LONG lParam; return (LRESULT)PwwdWw(WwFromHwnd(hwnd))->rcePage.yeBottom; case 78: return (LRESULT)OpusGetWin95ZoomPercent(hwnd); + case 79: + return (LRESULT)selCur.chp.ico; } return (LRESULT) -1; #endif diff --git a/src/Opus/wwchange.c b/src/Opus/wwchange.c index d7986fd..4f9d711 100644 --- a/src/Opus/wwchange.c +++ b/src/Opus/wwchange.c @@ -113,6 +113,9 @@ extern int vfTableKeymap; extern HWND vhwndMsgBoxParent; extern struct UAB vuab; extern int dclMac; +#ifdef OPUS_X64 +extern int OpusWin95DisplayAlias(); +#endif extern CHAR szApp[]; extern HBRUSH vhbrGray; extern int mwMac; @@ -964,7 +967,6 @@ int nfo; * */ - /* %%Function:FNormalizeSzFile %%Owner:chic */ FNormalizeSzFile( szFile, szNormal, nfo ) CHAR *szFile; @@ -2015,6 +2017,9 @@ int rgmw[], imwMac; Assert(doc != docNil); GetDocSt(doc, st, gdsoRelative); +#ifdef OPUS_X64 + OpusWin95DisplayAlias(st); +#endif if (imwMac > 1) /* compute imw of mw and append : */ diff --git a/src/port/original/opus_asm_filewin.cpp b/src/port/original/opus_asm_filewin.cpp index 6fb38cb..7efb4ec 100644 --- a/src/port/original/opus_asm_filewin.cpp +++ b/src/port/original/opus_asm_filewin.cpp @@ -1,7 +1,12 @@ #include "opus_x64_compat.h" +#ifdef OpenFile +#undef OpenFile +#endif + #include #include +#include #include /* AMD64 translation of FILEWINN.ASM. Word continues to open files through @@ -46,6 +51,42 @@ extern "C" { extern NativeBptbPrefix vbptbExt; +int OpusOpenFile(const LPCSTR file_name, void* const legacy_ofs, + const UINT style) { + struct LegacyOfStruct { + int byte_count : 8; + int fixed_disk : 8; + WORD error_code; + BYTE reserved[4]; + CHAR path[120]; + }; + + if (legacy_ofs == nullptr) { + SetLastError(ERROR_INVALID_PARAMETER); + return HFILE_ERROR; + } + + auto* const legacy = static_cast(legacy_ofs); + OFSTRUCT native_ofs{}; + native_ofs.cBytes = sizeof(native_ofs); + native_ofs.fFixedDisk = legacy->fixed_disk; + native_ofs.nErrCode = legacy->error_code; + std::memcpy(&native_ofs.Reserved1, legacy->reserved, + sizeof(legacy->reserved)); + lstrcpynA(native_ofs.szPathName, legacy->path, + static_cast(std::size(native_ofs.szPathName))); + + const HFILE result = OpenFile(file_name, &native_ofs, style); + legacy->byte_count = native_ofs.cBytes; + legacy->fixed_disk = native_ofs.fFixedDisk; + legacy->error_code = native_ofs.nErrCode; + std::memcpy(legacy->reserved, &native_ofs.Reserved1, + sizeof(legacy->reserved)); + lstrcpynA(legacy->path, native_ofs.szPathName, + static_cast(std::size(legacy->path))); + return result; +} + int FCloseDoshnd(const HFILE file) { if (file == HFILE_ERROR) { return 0; diff --git a/src/port/original/opus_sdm_runtime.cpp b/src/port/original/opus_sdm_runtime.cpp index 9414765..4303572 100644 --- a/src/port/original/opus_sdm_runtime.cpp +++ b/src/port/original/opus_sdm_runtime.cpp @@ -1,10 +1,12 @@ #include "opus_x64_compat.h" #include "opus_x64_heap.h" +#include extern "C" { #include "dac.h" } #include +#include #include #include #include @@ -117,6 +119,82 @@ FontValueProc g_font_name_to_value = nullptr; FontValueProc g_font_size_to_value = nullptr; FontNameFromValueProc g_font_name_from_value = nullptr; +struct Win95SaveAlias { + bool active = false; + bool created = false; + std::string selected_path; + std::string legacy_path; +}; + +Win95SaveAlias g_win95_save_alias; +std::unordered_map g_win95_saved_aliases; + +struct Win95AliasCleanup { + ~Win95AliasCleanup() { + if (g_win95_save_alias.created && + !g_win95_save_alias.legacy_path.empty()) { + DeleteFileA(g_win95_save_alias.legacy_path.c_str()); + } + for (const auto& saved : g_win95_saved_aliases) { + DeleteFileA(saved.first.c_str()); + } + } +}; + +Win95AliasCleanup g_win95_alias_cleanup; + +std::string win95_alias_key(std::string path) { + std::transform(path.begin(), path.end(), path.begin(), + [](const unsigned char character) { + return static_cast(std::tolower(character)); + }); + return path; +} + +bool make_win95_staging_path(std::string& path) { + char module_path[32768]{}; + const DWORD module_length = GetModuleFileNameA( + nullptr, module_path, static_cast(std::size(module_path))); + if (module_length == 0 || module_length >= std::size(module_path)) { + return false; + } + char* separator = std::strrchr(module_path, '\\'); + if (separator == nullptr) { + return false; + } + *separator = '\0'; + std::string directory = std::string(module_path) + "\\W95TEMP"; + if (!CreateDirectoryA(directory.c_str(), nullptr) && + GetLastError() != ERROR_ALREADY_EXISTS) { + return false; + } + for (int attempt = 0; attempt < 32; ++attempt) { + char temporary[MAX_PATH]{}; + if (GetTempFileNameA(directory.c_str(), "W95", 0, temporary) == 0) { + return false; + } + DeleteFileA(temporary); + char* extension = std::strrchr(temporary, '.'); + if (extension != nullptr) { + lstrcpyA(extension, ".DOC"); + } + if (GetFileAttributesA(temporary) == INVALID_FILE_ATTRIBUTES && + std::strlen(temporary) < 120) { + path = temporary; + return true; + } + } + return false; +} + +std::string counted_path(const unsigned char* st_file) { + if (st_file == nullptr || st_file[0] == 0 || st_file[0] >= 120) { + return {}; + } + return std::string(reinterpret_cast(st_file + 1), + st_file[0]); +} + constexpr Word kDlmInit = 0x0001; constexpr Word kDlmTerm = 0x0003; constexpr Word kDlmExit = 0x0004; @@ -2022,6 +2100,181 @@ void handle_dialog_command(const Hdlg handle, const WPARAM w_param, } } +Tmc run_word95_common_file_dialog(DialogState& dialog) { + const bool opening = dialog.hid == kIddOpen; + const bool saving = dialog.hid == kIddSaveAs; + if (!opening && !saving) { + return static_cast(-1); + } + + invoke_dialog_proc(dialog, kDlmInit, dialog.focus); + dialog.commands_active = true; + if (opening) { + read_open_cab(dialog); + } else { + read_save_cab(dialog); + } + + HWND owner = dialog.window != nullptr ? + GetWindow(dialog.window, GW_OWNER) : nullptr; + if (owner == nullptr || !IsWindow(owner)) { + owner = vhWndMsgBoxParent; + } + + std::vector file_buffer(32768, '\0'); + std::string initial = opening ? + dialog.controls[kTmcOpenFileName].text : + dialog.controls[kTmcSaveFile].text; + const auto initial_alias = + g_win95_saved_aliases.find(win95_alias_key(initial)); + if (initial_alias != g_win95_saved_aliases.end()) { + initial = initial_alias->second; + } + establish_open_directory(dialog, initial.empty() ? "*.*" : initial); + if (initial.find_first_of("*?") == std::string::npos) { + lstrcpynA(file_buffer.data(), initial.c_str(), + static_cast(file_buffer.size())); + } + + std::string default_extension = "doc"; + const std::size_t slash = initial.find_last_of("\\/"); + const std::size_t dot = initial.find_last_of('.'); + if (dot != std::string::npos && + (slash == std::string::npos || dot > slash) && + dot + 1 < initial.size()) { + default_extension = initial.substr(dot + 1); + } + + static const char open_filter[] = + "Word Documents (*.doc;*.dot)\0*.doc;*.dot\0" + "Rich Text Format (*.rtf)\0*.rtf\0" + "Text Files (*.txt)\0*.txt\0" + "All Files (*.*)\0*.*\0\0"; + static const char save_filter[] = + "Word Documents (*.doc)\0*.doc\0" + "Document Templates (*.dot)\0*.dot\0" + "All Files (*.*)\0*.*\0\0"; + + Tmc result = kTmcCancel; + for (;;) { + OPENFILENAMEA file_dialog{}; + file_dialog.lStructSize = sizeof(file_dialog); + file_dialog.hwndOwner = owner; + file_dialog.lpstrFilter = opening ? open_filter : save_filter; + file_dialog.nFilterIndex = 1; + file_dialog.lpstrFile = file_buffer.data(); + file_dialog.nMaxFile = static_cast(file_buffer.size()); + file_dialog.lpstrInitialDir = dialog.current_directory.empty() ? + nullptr : dialog.current_directory.c_str(); + file_dialog.lpstrTitle = opening ? "Open" : "Save As"; + file_dialog.lpstrDefExt = default_extension.c_str(); + file_dialog.Flags = OFN_EXPLORER | OFN_ENABLESIZING | + OFN_LONGNAMES | OFN_NOCHANGEDIR | OFN_PATHMUSTEXIST; + if (opening) { + file_dialog.Flags |= OFN_FILEMUSTEXIST; + if (dialog.controls[kTmcOpenReadOnly].value != 0) { + file_dialog.Flags |= OFN_READONLY; + } + } else { + file_dialog.Flags |= OFN_NOREADONLYRETURN | + OFN_OVERWRITEPROMPT; + } + + const BOOL accepted = opening ? + GetOpenFileNameA(&file_dialog) : GetSaveFileNameA(&file_dialog); + if (!accepted) { + if (CommDlgExtendedError() != 0) { + MessageBoxA(owner, + "The Windows file dialog could not be opened.", + opening ? "Open" : "Save As", + MB_OK | MB_ICONEXCLAMATION); + result = static_cast(-1); + } else { + invoke_dialog_proc(dialog, kDlmTerm, kTmcCancel); + result = kTmcCancel; + } + break; + } + + const std::string selected_path = file_buffer.data(); + std::string legacy_path; + if (!make_win95_staging_path(legacy_path)) { + MessageBoxA(owner, + "Word could not prepare a temporary document file.", + opening ? "Open" : "Save As", + MB_OK | MB_ICONEXCLAMATION); + continue; + } + if (opening && + !CopyFileA(selected_path.c_str(), legacy_path.c_str(), FALSE)) { + MessageBoxA(owner, "Word could not open the selected file.", + "Open", MB_OK | MB_ICONEXCLAMATION); + continue; + } + + establish_open_directory(dialog, selected_path); + if (opening) { + g_win95_saved_aliases[win95_alias_key(legacy_path)] = + selected_path; + auto& edit = dialog.controls[kTmcOpenFileName]; + edit.text = legacy_path; + if (edit.window != nullptr) { + SetWindowTextA(edit.window, edit.text.c_str()); + } + auto& read_only = dialog.controls[kTmcOpenReadOnly]; + read_only.value = + (file_dialog.Flags & OFN_READONLY) != 0; + if (read_only.window != nullptr) { + SendMessageA(read_only.window, BM_SETCHECK, + read_only.value ? BST_CHECKED : BST_UNCHECKED, 0); + } + sync_open_cab(dialog); + } else { + g_win95_save_alias.active = true; + g_win95_save_alias.created = true; + g_win95_save_alias.selected_path = selected_path; + g_win95_save_alias.legacy_path = legacy_path; + auto& edit = dialog.controls[kTmcSaveFile]; + edit.text = legacy_path; + if (edit.window != nullptr) { + SetWindowTextA(edit.window, edit.text.c_str()); + } + sync_save_cab(dialog); + } + + if (invoke_dialog_proc(dialog, kDlmTerm, kTmcOk)) { + result = dialog.dying ? dialog.result_tmc : kTmcOk; + if (saving && result != kTmcOk) { + if (g_win95_save_alias.created) { + DeleteFileA(g_win95_save_alias.legacy_path.c_str()); + } + g_win95_save_alias = {}; + } + break; + } + if (dialog.dying) { + result = dialog.result_tmc; + if (saving) { + if (g_win95_save_alias.created) { + DeleteFileA(g_win95_save_alias.legacy_path.c_str()); + } + g_win95_save_alias = {}; + } + break; + } + if (saving) { + if (g_win95_save_alias.created) { + DeleteFileA(g_win95_save_alias.legacy_path.c_str()); + } + g_win95_save_alias = {}; + } + } + + dialog.commands_active = false; + invoke_dialog_proc(dialog, kDlmExit, result); + return result; +} + LRESULT CALLBACK native_dialog_window_proc(const HWND window, const UINT message, const WPARAM w_param, @@ -2062,6 +2315,90 @@ LRESULT CALLBACK native_dialog_window_proc(const HWND window, extern "C" { +int OpusWin95SaveAliasMatches(const unsigned char* st_file) { + const std::string path = counted_path(st_file); + if (path.empty()) { + return false; + } + if (g_win95_save_alias.active && + _stricmp(path.c_str(), + g_win95_save_alias.legacy_path.c_str()) == 0) { + return true; + } + return g_win95_saved_aliases.find(win95_alias_key(path)) != + g_win95_saved_aliases.end(); +} + +int OpusWin95DisplayAlias(unsigned char* const st_file) { + const std::string path = counted_path(st_file); + if (path.empty()) { + return false; + } + auto saved = g_win95_saved_aliases.find(win95_alias_key(path)); + if (saved == g_win95_saved_aliases.end()) { + char full_path[32768]{}; + const DWORD length = GetFullPathNameA( + path.c_str(), static_cast(std::size(full_path)), + full_path, nullptr); + if (length == 0 || length >= std::size(full_path)) { + return false; + } + saved = g_win95_saved_aliases.find(win95_alias_key(full_path)); + if (saved == g_win95_saved_aliases.end()) { + return false; + } + } + + const char* name = saved->second.c_str(); + if (const char* separator = std::strrchr(name, '\\'); + separator != nullptr) { + name = separator + 1; + } + const std::size_t length = (std::min)( + std::strlen(name), static_cast(119)); + st_file[0] = static_cast(length); + std::memcpy(st_file + 1, name, length); + return true; +} + +int OpusFinishWin95SaveAlias(const unsigned char* st_file, + const int success) { + const std::string path = counted_path(st_file); + if (path.empty()) { + return !g_win95_save_alias.active; + } + const std::string key = win95_alias_key(path); + + if (g_win95_save_alias.active && + _stricmp(path.c_str(), + g_win95_save_alias.legacy_path.c_str()) == 0) { + bool copied = success != 0; + if (copied) { + copied = CopyFileA( + g_win95_save_alias.legacy_path.c_str(), + g_win95_save_alias.selected_path.c_str(), + FALSE) != 0; + } + if (!success) { + DeleteFileA(g_win95_save_alias.legacy_path.c_str()); + } else if (copied) { + g_win95_saved_aliases[key] = + g_win95_save_alias.selected_path; + } + g_win95_save_alias = {}; + return copied; + } + + const auto saved = g_win95_saved_aliases.find(key); + if (saved == g_win95_saved_aliases.end()) { + return true; + } + if (!success) { + return false; + } + return CopyFileA(path.c_str(), saved->second.c_str(), FALSE) != 0; +} + struct OpusSdsCompat { std::uintptr_t current_segment; std::uintptr_t focus_segment; @@ -2182,6 +2519,23 @@ Tmc TmcDoDlgDli(DltHeader** dialog_template, Hcab cab, Dli* initializer) { return static_cast(-1); } + if (dialog->modal && + (dialog->hid == kIddOpen || dialog->hid == kIddSaveAs)) { + const Tmc common_result = run_word95_common_file_dialog(*dialog); + dialog = find_dialog(handle); + if (dialog != nullptr && dialog->window != nullptr && + IsWindow(dialog->window)) { + DestroyWindow(dialog->window); + } + g_dialogs.erase(handle); + g_current_dialog = find_dialog(previous_current) == nullptr ? + 0 : previous_current; + g_focus_dialog = find_dialog(previous_focus) == nullptr ? + 0 : previous_focus; + sync_current_dialog_globals(); + return common_result; + } + Tmc result = kTmcCancel; HWND owner = nullptr; bool restore_owner = false; diff --git a/src/port/original/opus_win95_chrome.cpp b/src/port/original/opus_win95_chrome.cpp index 44aea97..21c1e22 100644 --- a/src/port/original/opus_win95_chrome.cpp +++ b/src/port/original/opus_win95_chrome.cpp @@ -24,6 +24,7 @@ extern "C" int OpusAdjustWin95Zoom(HWND pane, int percent_delta); extern "C" int OpusRecenterWin95PageView(HWND pane); extern "C" int OpusSetWin95VerticalMargin( HWND pane, int top_margin, int margin_pixels); +extern "C" int OpusApplyWin95TextColor(HWND pane, int color_index); extern "C" int OpusGetWin95HorizontalRulerMetrics( HWND ruler, int* zero, int* active_left, int* active_right, int* pixels_per_inch, int* left_indent, int* first_indent, @@ -42,6 +43,7 @@ constexpr wchar_t kRulerOverlayClass[] = L"OpusWin95RulerOverlay"; constexpr int kToolbarBitmap = 201; constexpr int kSpriteCell = 20; constexpr COLORREF kButtonFace = RGB(192, 192, 192); +constexpr UINT_PTR kMenuRepaintTimer = 0x7f51; constexpr COLORREF kButtonShadow = RGB(128, 128, 128); constexpr COLORREF kButtonDarkShadow = RGB(0, 0, 0); constexpr COLORREF kButtonHighlight = RGB(255, 255, 255); @@ -52,6 +54,7 @@ constexpr UINT kComboSize = 0x7503; constexpr UINT kComboZoom = 0x7504; constexpr UINT kCmdToggleStandardToolbar = 0x7101; constexpr UINT kCmdToggleFormattingToolbar = 0x7102; +constexpr UINT kCmdTextColorBase = 0x7200; constexpr UINT_PTR kSyncTimer = 0x951; constexpr wchar_t kOriginalPaneProcProperty[] = L"OpusWord95OriginalPaneProc"; @@ -153,6 +156,8 @@ struct ToolbarState { bool formatting_visible = true; bool startup_page_view_requested = false; int ruler_refreshes_remaining = 0; + int text_color_index = 0; + COLORREF text_color = RGB(0, 0, 0); HitResult pressed{}; std::array standard_latched{}; std::array format_latched{}; @@ -244,6 +249,18 @@ void style_menu_tree(HMENU menu) { } } +BOOL CALLBACK repaint_menu_popup(HWND window, LPARAM) { + wchar_t class_name[32]{}; + GetClassNameW(window, class_name, + static_cast(std::size(class_name))); + if (lstrcmpW(class_name, L"#32768") == 0 && IsWindowVisible(window)) { + RedrawWindow(window, nullptr, nullptr, + RDW_ERASE | RDW_INVALIDATE | RDW_UPDATENOW | + RDW_ALLCHILDREN | RDW_FRAME); + } + return TRUE; +} + void configure_word95_menus(HWND window) { HMENU root = GetMenu(window); if (root == nullptr || GetMenuItemCount(root) < 7) { @@ -591,7 +608,8 @@ void draw_alignment(HDC dc, RECT area, FormatGlyph glyph) { } void draw_format_glyph(HWND toolbar, HDC dc, const FormatButton& spec, - RECT rect, bool sunken, HDC sprite_dc) { + RECT rect, bool sunken, HDC sprite_dc, + COLORREF text_color) { const int offset = sunken ? scale(toolbar, 1) : 0; RECT area{rect.left + scale(toolbar, 4) + offset, rect.top + scale(toolbar, 4) + offset, @@ -623,7 +641,7 @@ void draw_format_glyph(HWND toolbar, HDC dc, const FormatButton& spec, SelectObject(dc, old_font); DeleteObject(font); if (spec.glyph == FormatGlyph::color) { - HBRUSH swatch = CreateSolidBrush(RGB(255, 255, 0)); + HBRUSH swatch = CreateSolidBrush(text_color); RECT swatch_rect{area.left + 1, area.bottom - 3, area.right - 1, area.bottom}; FillRect(dc, &swatch_rect, swatch); @@ -740,7 +758,8 @@ void paint_toolbar(HWND toolbar, ToolbarState& state) { state.pressed.index == index) || state.format_latched[index]; draw_classic_edge(dc, rect, down); draw_format_glyph(toolbar, dc, kFormatButtons[index], rect, down, - state.sprite != nullptr ? sprite_dc : nullptr); + state.sprite != nullptr ? sprite_dc : nullptr, + state.text_color); } } if (old_bitmap != nullptr) { @@ -934,6 +953,116 @@ void restore_document_focus_from_root(HWND root) { } } +HBITMAP create_color_menu_swatch(HWND owner, COLORREF color, + bool automatic) { + HDC screen = GetDC(owner); + if (screen == nullptr) { + return nullptr; + } + const int size = scale(owner, 14); + HBITMAP bitmap = CreateCompatibleBitmap(screen, size, size); + HDC memory = CreateCompatibleDC(screen); + HBITMAP previous = bitmap != nullptr ? static_cast( + SelectObject(memory, bitmap)) : nullptr; + if (bitmap != nullptr) { + RECT bounds{0, 0, size, size}; + FillRect(memory, &bounds, GetSysColorBrush(COLOR_MENU)); + RECT swatch{scale(owner, 2), scale(owner, 2), + size - scale(owner, 1), size - scale(owner, 1)}; + HBRUSH fill = CreateSolidBrush(color); + FillRect(memory, &swatch, fill); + DeleteObject(fill); + FrameRect(memory, &swatch, + static_cast(GetStockObject(BLACK_BRUSH))); + if (automatic) { + HPEN pen = CreatePen(PS_SOLID, 1, RGB(255, 255, 255)); + HPEN old_pen = static_cast(SelectObject(memory, pen)); + MoveToEx(memory, swatch.left + 1, swatch.bottom - 2, nullptr); + LineTo(memory, swatch.right - 1, swatch.top + 1); + SelectObject(memory, old_pen); + DeleteObject(pen); + } + SelectObject(memory, previous); + } + DeleteDC(memory); + ReleaseDC(owner, screen); + return bitmap; +} + +void show_text_color_palette(HWND app, HWND toolbar) { + ToolbarState* state = toolbar_state(toolbar); + if (state == nullptr) { + return; + } + struct Choice { + int index; + COLORREF color; + const wchar_t* name; + }; + const std::array choices{{ + {0, RGB(0, 0, 0), L"Automatic"}, + {1, RGB(0, 0, 0), L"Black"}, + {2, RGB(0, 0, 255), L"Blue"}, + {3, RGB(0, 255, 255), L"Cyan"}, + {4, RGB(0, 128, 0), L"Green"}, + {5, RGB(255, 0, 255), L"Magenta"}, + {6, RGB(255, 0, 0), L"Red"}, + {7, RGB(255, 255, 0), L"Yellow"}, + {8, RGB(255, 255, 255), L"White"}, + }}; + + HMENU popup = CreatePopupMenu(); + if (popup == nullptr) { + return; + } + std::vector swatches; + swatches.reserve(choices.size()); + for (const auto& choice : choices) { + const UINT command = kCmdTextColorBase + choice.index; + AppendMenuW(popup, MF_STRING, command, choice.name); + HBITMAP swatch = create_color_menu_swatch( + toolbar, choice.color, choice.index == 0); + swatches.push_back(swatch); + if (swatch != nullptr) { + SetMenuItemBitmaps(popup, command, MF_BYCOMMAND, swatch, swatch); + } + if (choice.index == state->text_color_index) { + CheckMenuItem(popup, command, MF_BYCOMMAND | MF_CHECKED); + } + } + style_menu_tree(popup); + + RECT anchor = format_button_rect(toolbar, *state, 3); + POINT point{anchor.left, anchor.bottom}; + ClientToScreen(toolbar, &point); + const UINT selected = TrackPopupMenu( + popup, TPM_RETURNCMD | TPM_LEFTALIGN | TPM_TOPALIGN | TPM_RIGHTBUTTON, + point.x, point.y, 0, app, nullptr); + if (selected >= kCmdTextColorBase && + selected < kCmdTextColorBase + choices.size()) { + const int index = static_cast(selected - kCmdTextColorBase); + HWND pane = nullptr; + EnumChildWindows(app, find_document_pane, + reinterpret_cast(&pane)); + if (pane != nullptr && + OpusApplyWin95TextColor(pane, choices[index].index)) { + state->text_color_index = choices[index].index; + state->text_color = choices[index].color; + InvalidateRect(toolbar, nullptr, FALSE); + UpdateWindow(toolbar); + } else { + MessageBeep(MB_ICONEXCLAMATION); + } + } + DestroyMenu(popup); + for (HBITMAP swatch : swatches) { + if (swatch != nullptr) { + DeleteObject(swatch); + } + } + restore_document_focus_from_root(app); +} + void forward_combo(HWND mirror, HWND source, int notification, bool& edit_dirty) { if (mirror == nullptr || source == nullptr || !IsWindow(source)) { @@ -2371,6 +2500,10 @@ LRESULT CALLBACK app_window_proc(HWND app, UINT message, } g_word95_page_view_active = true; } + if (command == bcmColor) { + show_text_color_palette(app, toolbar); + return 0; + } if (command == kCmdToggleStandardToolbar) { toggle_toolbar_row(app, toolbar, true); return 0; @@ -2385,6 +2518,15 @@ LRESULT CALLBACK app_window_proc(HWND app, UINT message, if (state != nullptr) { update_toolbar_menu_checks(app, *state); } + /* On current Windows versions a popup using a classic background + brush can receive its initial erase before the menu items have + been invalidated. Repaint once the #32768 popup exists so the + complete File menu is visible without requiring mouse hover. */ + SetTimer(app, kMenuRepaintTimer, 15, nullptr); + } else if (message == WM_TIMER && w_param == kMenuRepaintTimer) { + KillTimer(app, kMenuRepaintTimer); + EnumThreadWindows(GetCurrentThreadId(), repaint_menu_popup, 0); + return 0; } else if (message == WM_NCDESTROY) { g_original_app_proc = nullptr; if (original != nullptr) { diff --git a/src/port/original/opus_x64_compat.h b/src/port/original/opus_x64_compat.h index 87a1e7c..45c8333 100644 --- a/src/port/original/opus_x64_compat.h +++ b/src/port/original/opus_x64_compat.h @@ -59,6 +59,20 @@ BOOL OpusChangeMenu(HMENU menu, UINT item, const void *new_text, #ifdef OPUS_X64 #define DeleteAtom OpusDeleteAtom void OpusDeleteAtom(ATOM atom); + +/* Win16 OFSTRUCT reserved 120 bytes for its path, while the current SDK's + * OFSTRUCT reserves 128. Passing Word's original structure directly to + * OpenFileA lets the API overwrite eight bytes beyond the caller's buffer. + * Marshal through a native OFSTRUCT so legacy file open/reopen behavior is + * preserved without corrupting the stack. */ +#ifdef __cplusplus +extern "C" { +#endif +HFILE OpusOpenFile(LPCSTR file_name, void *legacy_ofs, UINT style); +#ifdef __cplusplus +} +#endif +#define OpenFile OpusOpenFile #endif /* Several original translation units define NOGDI to trim the Win16 header,