From 27a2f60cd155173d0b48fdedbc3ec625e75cb6ec Mon Sep 17 00:00:00 2001 From: Justin Marshall Date: Thu, 6 Aug 2026 13:49:53 -0700 Subject: [PATCH] Full operating system font support. --- src/CMakeLists.txt | 4 + src/Opus/LOADFONT.C | 36 +- src/Opus/fontwin.h | 16 +- src/Opus/iconbar3.c | 5 +- src/Opus/rulrib.c | 15 + src/Opus/wordtech/fetch2.c | 76 ++ src/Opus/wordtech/insert.c | 13 + src/Opus/wordtech/inssubs.c | 6 + src/Opus/wordtech/prcsubs.c | 2 +- src/Opus/wordtech/word.h | 10 + src/Opus/wproc.c | 144 +++ src/port/original/char.sdm | 12 +- src/port/original/edstyle.sdm | 11 +- .../original/opus_original_startup_probe.cpp | 71 ++ src/port/original/opus_sdm_runtime.cpp | 852 +++++++++++++++++- src/port/original/style.sdm | 8 +- src/port/winword.manifest | 4 +- 17 files changed, 1219 insertions(+), 66 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bbb45b5..9c3d88e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -988,3 +988,7 @@ add_test(NAME opus_word1_selection_test COMMAND $ $ --selection ) set_tests_properties(opus_word1_selection_test PROPERTIES TIMEOUT 20) +add_test(NAME opus_word1_font_typing_test + COMMAND $ $ --font-typing +) +set_tests_properties(opus_word1_font_typing_test PROPERTIES TIMEOUT 20) diff --git a/src/Opus/LOADFONT.C b/src/Opus/LOADFONT.C index 834a7b8..53b1ce8 100644 --- a/src/Opus/LOADFONT.C +++ b/src/Opus/LOADFONT.C @@ -807,9 +807,17 @@ LOGFONT *plf; int fPrinterFont; { /* Translate an FCID into a windows logical font request structure */ int ps; + int ibstFont; struct FFN *pffn; SetBytes(plf, 0, sizeof(LOGFONT)); +#ifdef OPUS_X64 + /* Word's selection engine deliberately XOR-inverts rendered glyphs. + ClearType's per-channel subpixels turn into colored fringes under that + operation, so request grayscale antialiasing for screen fonts. */ + if (!fPrinterFont) + plf->lfQuality = 4; /* ANTIALIASED_QUALITY */ +#endif /* Scale the request into device units */ @@ -824,11 +832,33 @@ int fPrinterFont; screen fonts, use positive lfHeight, meaning choose the font by CELL height instead of CHARACTER height. ChrisLa says this fiasco is Aldus' fault. */ - if (fcid.ibstFont != ibstCourier || fPrinterFont) + ibstFont = (int)fcid.ibstFont; + if (ibstFont != ibstCourier || fPrinterFont) plf->lfHeight = -plf->lfHeight; - Assert( fcid.ibstFont < (*vhsttbFont)->ibstMac ); - pffn = PstFromSttb( vhsttbFont, fcid.ibstFont ); +#ifdef OPUS_X64 + /* A damaged or partially converted document font map must not turn a + failed STTB lookup into a null dereference in GDI setup. Use Word's + default master font entry as the recoverable display fallback. */ + if (vhsttbFont == hNil || *vhsttbFont == NULL) + { + plf->lfPitchAndFamily = fcid.prq; + plf->lfCharSet = DEFAULT_CHARSET; + return fTrue; + } + if (ibstFont < 0 || ibstFont >= (*vhsttbFont)->ibstMac) + ibstFont = ibstFontDefault; +#endif + Assert( ibstFont < (*vhsttbFont)->ibstMac ); + pffn = PstFromSttb( vhsttbFont, ibstFont ); +#ifdef OPUS_X64 + if (pffn == NULL) + { + plf->lfPitchAndFamily = fcid.prq; + plf->lfCharSet = DEFAULT_CHARSET; + return fTrue; + } +#endif /* bits 0-1: pitch request bits 4-6: font family */ plf->lfPitchAndFamily = (pffn->ffid & maskFfFfid) | fcid.prq; diff --git a/src/Opus/fontwin.h b/src/Opus/fontwin.h index 06b9be4..3917dcf 100644 --- a/src/Opus/fontwin.h +++ b/src/Opus/fontwin.h @@ -78,15 +78,15 @@ union FCID struct { /* wProps */ - int fBold: 1; /* bold, italics in same position as chp */ - int fItalic: 1; - int fStrike: 1; - int kul: 3; - int prq: 2; /* pitch request */ - int hps: 8; + unsigned int fBold: 1; /* bold, italics in same position as chp */ + unsigned int fItalic: 1; + unsigned int fStrike: 1; + unsigned int kul: 3; + unsigned int prq: 2; /* pitch request */ + unsigned int hps: 8; /* wExtra */ - int ibstFont: 8; /* index into master font table */ - int : 8; + unsigned int ibstFont: 8; /* index into master font table */ + unsigned int : 8; }; }; diff --git a/src/Opus/iconbar3.c b/src/Opus/iconbar3.c index 5b85f12..db36ce0 100644 --- a/src/Opus/iconbar3.c +++ b/src/Opus/iconbar3.c @@ -1140,7 +1140,10 @@ IBDlgLoop() KME * pkme; pkme = PkmeOfKcInChain(KcModified(vmsgLast.wParam & 0xff)); - if (pkme->kt == ktMacro) + /* A printable key need not be present in the command key map. + The 16-bit build happened to tolerate the resulting nil pointer; + Win64 does not. Only inspect the entry when one was found. */ + if (pkme != NULL && pkme->kt == ktMacro) { HDLG hdlgFocus = HdlgGetFocus(); HDLG hdlgOld = HdlgSetCurDlg(hdlgFocus); diff --git a/src/Opus/rulrib.c b/src/Opus/rulrib.c index b4e4d00..a598ed7 100644 --- a/src/Opus/rulrib.c +++ b/src/Opus/rulrib.c @@ -1652,6 +1652,9 @@ WORD iibb; WORD wParam; LONG lParam; { +#ifdef OPUS_X64 + extern void OpusX64TraceRibbon(); +#endif switch (ibm) { case ibmDblClk: @@ -1685,6 +1688,12 @@ LONG lParam; int ftc = ValGetIibbHwndIb(hwnd, IDLKSFONT); int hps = ValGetIibbHwndIb(hwnd, IDLKSPOINT); +#ifdef OPUS_X64 + OpusX64TraceRibbon("original-term", ibm, iibb, ftc, hps, + (long)selCur.cpFirst, (long)selCur.cpLim, + selCur.fIns); +#endif + Assert (vssc == sscNil); if (ftc >= 0) { @@ -1699,6 +1708,12 @@ LONG lParam; if (cch > 0) { ApplyGrpprlSelCur(grpprl, cch, fTrue); +#ifdef OPUS_X64 + OpusX64TraceRibbon("original-applied", ibm, cch, + selCur.chp.ftc, selCur.chp.hps, + (long)selCur.cpFirst, (long)selCur.cpLim, + selCur.fIns); +#endif if (!vmerr.fMemFail) FSetAgainGrpprl(grpprl, cch, bcmFormatting); diff --git a/src/Opus/wordtech/fetch2.c b/src/Opus/wordtech/fetch2.c index ae0972b..04b8c54 100644 --- a/src/Opus/wordtech/fetch2.c +++ b/src/Opus/wordtech/fetch2.c @@ -20,6 +20,9 @@ DEBUGASSERTSZ /* WIN - bogus macro for assert string */ #include "ruler.h" #include "pic.h" #include "error.h" +#ifdef OPUS_X64 +#include +#endif #ifdef MAC #include "mac.h" #include "toolbox.h" @@ -967,6 +970,79 @@ WORD wParam; return 0; } +#ifdef OPUS_X64 +/* Native SDM bridge. The historical parser expects a segmented SDM parse + object, which no longer exists on x64. Keep the actual Word font mapping + in this original module and expose flat-string adapters to the native + control layer. */ +EXPORT int OpusX64FtcFromFontName(sz) +const char *sz; +{ + int cch; + struct FFN *pffn; + char rgch[cbFfnLast]; + + pffn = (struct FFN *)rgch; + if (sz == NULL || *sz == '\0') + return wNinch; + if (CchSz(sz) > LF_FACESIZE - 1) + { + bltbyte(sz, pffn->szFfn, LF_FACESIZE - 1); + pffn->szFfn[LF_FACESIZE - 1] = '\0'; + cch = LF_FACESIZE; + } + else + cch = CchCopySz(sz, pffn->szFfn); + cch = CchStripString(pffn->szFfn, cch); + if (cch == 0) + return wNinch; + return FtcValidateFont(pffn); +} + +EXPORT int OpusX64HpsFromFontSize(sz) +const char *sz; +{ + char *pchEnd; + double points; + int hps; + + if (sz == NULL || *sz == '\0') + return wNinch; + points = strtod(sz, &pchEnd); + while (*pchEnd == ' ' || *pchEnd == '\t') + ++pchEnd; + if (*pchEnd != '\0') + return wError; + hps = (int)(points * 2.0 + 0.5); + /* CHP stores the size in one unsigned byte. The original parser's + 4..127 point range becomes 8..254 after conversion to half-points. */ + if (hps < 8 || hps > 254) + return wError; + return hps; +} + +EXPORT void OpusX64FontNameFromFtc(ftc, sz, cchMax) +int ftc; +char *sz; +int cchMax; +{ + int ibst; + char *szFont; + + if (sz == NULL || cchMax <= 0) + return; + *sz = '\0'; + if (ftc == wNinch || selCur.doc == docNil) + return; + ibst = IbstFontFromFtcDoc(ftc, selCur.doc); + if (ibst == iNil) + return; + szFont = ((struct FFN *)PstFromSttb(vhsttbFont, ibst))->szFfn; + bltbyte(szFont, sz, min(CchSz(szFont), cchMax)); + sz[cchMax - 1] = '\0'; +} +#endif + #endif /* WIN */ diff --git a/src/Opus/wordtech/insert.c b/src/Opus/wordtech/insert.c index df2e043..c94a768 100644 --- a/src/Opus/wordtech/insert.c +++ b/src/Opus/wordtech/insert.c @@ -208,6 +208,14 @@ TLV *ptlv; CP cpLim; CP dcp; BOOL fSkipInval = fFalse; +#ifdef OPUS_X64 + /* The fast insert path updates EDL character counts in place. That is + * insufficient when a newly committed character run has taller font + * metrics than the existing line: the old EDL height can survive until a + * later repaint and clip both this line and the lines below it. */ + CP cpFirstCommitted = cpInsert; + int cchCommitted = ichInsert; +#endif #ifdef WIN int cchInsert; #else /* MAC */ @@ -323,6 +331,11 @@ have to be updated */ #ifdef WIN LSkipInval: +#ifdef OPUS_X64 + if (fEnd && cchCommitted > 0 && cpFirstCommitted != cpNil) + InvalCp(PcaSetDcp(&caT, selCur.doc, cpFirstCommitted, + (CP)cchCommitted)); +#endif if (vfRecording && ptlv->fInsertLoop) RecordInsert(ichInsert, ptlv); if (vfRecordNext && (ptlv->fRecordBksp || fEnd)) diff --git a/src/Opus/wordtech/inssubs.c b/src/Opus/wordtech/inssubs.c index ad64e48..2be7301 100644 --- a/src/Opus/wordtech/inssubs.c +++ b/src/Opus/wordtech/inssubs.c @@ -754,7 +754,13 @@ get operate efficiently beyond the Eof */ #ifdef MAC *(hpbFirst + sizeof(FC) + crun) = (!fWord3) ? (bCur / sizeof(int)) : bCur; #else /* WIN */ + /* The FKP byte stores an offset in 16-bit words. That is part of + * the document/scratch-page format, not the host C int width. */ +#ifdef OPUS_X64 + *(hpbFirst + sizeof(FC) + crun) = bCur >> 1; +#else *(hpbFirst + sizeof(FC) + crun) = bCur / sizeof(int); +#endif #endif /* WIN */ *((FC HUGE *)hpbFirst) = fcLim; pfkpd->bFreeFirst += sizeof(FC) + 1; diff --git a/src/Opus/wordtech/prcsubs.c b/src/Opus/wordtech/prcsubs.c index c4d3b80..2cac758 100644 --- a/src/Opus/wordtech/prcsubs.c +++ b/src/Opus/wordtech/prcsubs.c @@ -2345,7 +2345,7 @@ int cb; /* given a prm return a pointer to the list of sprm (pgrpprl) and return the length of the grpprl */ /* %%Function:PgrpprlFromPrm %%Owner:davidlu */ -PgrpprlFromPrm(prm, pcb, grpprl) +char *PgrpprlFromPrm(prm, pcb, grpprl) struct PRM prm; int *pcb; char *grpprl; /* user provided buffer for unloading sprm from piece table */ diff --git a/src/Opus/wordtech/word.h b/src/Opus/wordtech/word.h index db5af8b..6e05203 100644 --- a/src/Opus/wordtech/word.h +++ b/src/Opus/wordtech/word.h @@ -64,7 +64,16 @@ typedef long FC; typedef unsigned PN; /* 512 byte page */ typedef unsigned PO; /* 128 byte page (Old page format) */ +#ifdef OPUS_X64 +/* The page-cache LRU clock was a wrapping Win16 word. Leaving TS as a + * native 32-bit unsigned value lets tsMruBps grow past tsMax (0xffff), after + * which no old cache page compares as eligible and the fallback can evict + * vibpProtect. That replaces the character page still referenced by + * vhpchFetch with an FKP formatting page during mixed-font line fetches. */ +typedef unsigned short TS; /* TS = wrapping 16-bit time stamp */ +#else typedef unsigned TS; /* TS = time stamp */ +#endif typedef unsigned uns; #ifndef OPUS_X64 @@ -828,6 +837,7 @@ CP CpFirstTap1(); CP CpTableFirst(); CP CpTableLim(); struct SEL *PselActive(); +char *PgrpprlFromPrm(); CP CpSkipFormula(); CP CpScaleThumb(); struct PLC **HplcedlWw(); diff --git a/src/Opus/wproc.c b/src/Opus/wproc.c index c033961..1d6ba0e 100644 --- a/src/Opus/wproc.c +++ b/src/Opus/wproc.c @@ -91,6 +91,11 @@ struct CA caPara; struct CA caPage; struct CA caSect; struct CA caTable; +#ifdef OPUS_X64 +extern struct FTI vfti; +extern char (**vhgrpchr)[]; +extern int vbchrMac; +#endif extern struct FCB **mpfnhfcb[]; extern int vfnPreload; extern int vcPreload; @@ -372,6 +377,10 @@ extern struct RF vrf; extern BOOL vfInCommit; extern int cbMemChunk; extern struct BPTB vbptbExt; +#ifdef OPUS_X64 +extern int vibp; +extern int vibpProtect; +#endif extern int vgrfMenuKeysAreDirty; extern BOOL vfEmptyMenu; @@ -1590,6 +1599,141 @@ LONG lParam; return LOWORD(wParam) == 47 ? (LRESULT) edl.dlk : (LRESULT) edl.fEnd; } + case 49: return (LRESULT) selCur.chp.ftc; + case 50: return (LRESULT) selCur.chp.hps; + case 51: + case 52: + { + CP cp = (CP) lParam; + if (cp < cp0 || cp >= CpMacDocEdit(selCur.doc)) + return (LRESULT) -1; + FetchCp(selCur.doc, cp, fcmProps); + return LOWORD(wParam) == 51 ? + (LRESULT) vchpFetch.ftc : (LRESULT) vchpFetch.hps; + } + case 53: + return (LRESULT) IbstFontFromFtcDoc(selCur.chp.ftc, + selCur.doc); + case 54: + { + union FCID fcid; + fcid.lFcid = 0L; + fcid.ibstFont = (int)lParam; + return (LRESULT) fcid.ibstFont; + } + case 58: + return (LRESULT) sizeof(union FCID); + case 55: + case 56: + case 57: + InvalFli(); + FormatLine(selCur.ww, selCur.doc, (CP)lParam); + if (LOWORD(wParam) == 55) + return (LRESULT) vfli.dypLine; + if (LOWORD(wParam) == 56) + return (LRESULT) vfti.fcid.hps; + return (LRESULT) (vfti.dypAscent + vfti.dypDescent); + case 59: + case 60: + case 61: + case 62: + { + int bchr = 0; + int cchp = 0; + int hpsChpMax = 0; + int hpsFcidMax = 0; + int ichMax = 0; + InvalFli(); + FormatLine(selCur.ww, selCur.doc, (CP)lParam); + while (bchr < vbchrMac) + { + struct CHR *pchr = + (struct CHR *)&(**vhgrpchr)[bchr]; + int cb = CbFromChrm(pchr->chrm); + if (cb <= 0 || cb > vbchrMac - bchr) + break; + if (pchr->chrm == chrmChp) + { + ++cchp; + hpsChpMax = max(hpsChpMax, + (int)pchr->chp.hps); + hpsFcidMax = max(hpsFcidMax, + (int)pchr->fcid.hps); + ichMax = max(ichMax, + (int)(unsigned char)pchr->ich); + } + bchr += cb; + } + if (LOWORD(wParam) == 59) return (LRESULT) hpsChpMax; + if (LOWORD(wParam) == 60) return (LRESULT) hpsFcidMax; + if (LOWORD(wParam) == 61) return (LRESULT) cchp; + return (LRESULT) ichMax; + } + case 63: + InvalFli(); + FormatLine(selCur.ww, selCur.doc, cp0); + if ((int)lParam < 0 || (int)lParam >= vfli.ichMac) + return (LRESULT) -1; + return (LRESULT) vfli.rgdxp[(int)lParam]; + case 64: + case 65: + case 66: + case 67: + { + struct DR *pdr = PdrGalley(PwwdWw(WwFromHwnd(hwnd))); + CP cp = CpPlc(pdr->hplcedl, 0); + InvalFli(); + FormatLineDr(selCur.ww, cp, pdr); + if (LOWORD(wParam) == 64) + { + if ((int)lParam < 0 || (int)lParam >= vfli.ichMac) + return (LRESULT) -1; + return (LRESULT) vfli.rgdxp[(int)lParam]; + } + if (LOWORD(wParam) == 65) return (LRESULT) vfli.dxsInch; + if (LOWORD(wParam) == 66) return (LRESULT) vfli.dxuInch; + return (LRESULT) vfti.dxpInch; + } + case 68: + { + int ich = (int)HIWORD(wParam); + InvalFli(); + FormatLine(selCur.ww, selCur.doc, (CP)lParam); + if (ich < 0 || ich >= vfli.ichMac) + return (LRESULT)-1; + return (LRESULT)(unsigned char)vfli.rgch[ich]; + } + case 69: + { + CP cp = (CP)lParam; + if (cp < cp0 || cp >= CpMacDoc(selCur.doc)) + return (LRESULT)-1; + FetchCp(selCur.doc, cp, fcmChars); + if (vccpFetch <= 0 || vhpchFetch == NULL) + return (LRESULT)-1; + return (LRESULT)(unsigned char)*vhpchFetch; + } + case 70: + { + CP cp = (CP)lParam; + if (cp < cp0 || cp >= CpMacDoc(selCur.doc)) + return (LRESULT)-1; + FetchCp(selCur.doc, cp, + fcmChars + fcmProps + fcmParseCaps); + if (vccpFetch <= 0 || vhpchFetch == NULL) + return (LRESULT)-1; + return (LRESULT)(unsigned char)*vhpchFetch; + } + case 71: + { + CP cp = (CP)lParam; + if (cp < cp0 || cp >= CpMacDoc(selCur.doc)) + return (LRESULT)-1; + FetchCp(selCur.doc, cp, + fcmChars + fcmProps + fcmParseCaps); + return (LRESULT)MAKELONG((WORD)vibpProtect, + (WORD)vibp); + } } return (LRESULT) -1; #endif diff --git a/src/port/original/char.sdm b/src/port/original/char.sdm index c28d996..4f62574 100644 --- a/src/port/original/char.sdm +++ b/src/port/original/char.sdm @@ -1,7 +1,9 @@ #pragma once -/* - * Compile-stage template boundary for Opus/dlg/char.des. The native SDM - * resource compiler/runtime will replace this descriptor during UI bring-up. - */ -DLG_CONST DLT dltCharacter = { 0 }; +/* Regenerated header from Opus/dlg/char.des. The x64 SDM runtime consumes + the header and materializes the archived controls as native Win32 items. */ +extern BOOL FDlgCharacter(DLM, TMC, WORD, WORD, WORD); +DLG_CONST DLT dltCharacter = { + { 34, 38, 180, 152 }, IDDCharacter, tmcCharName, FDlgCharacter, + 24, bdrSysMenu, { 0 } +}; diff --git a/src/port/original/edstyle.sdm b/src/port/original/edstyle.sdm index 8dfb313..0f9b6dc 100644 --- a/src/port/original/edstyle.sdm +++ b/src/port/original/edstyle.sdm @@ -1,5 +1,6 @@ -/* - * Compile-only boundary for Opus/dlg/edstyle.des. Regenerate the complete - * template with the ported historical dialog compiler before activation. - */ -DLG_CONST DLT dltDefineStyle = { 0 }; +/* Regenerated header from Opus/dlg/edstyle.des. */ +extern BOOL FDlgDefineStyle(DLM, TMC, WORD, WORD, WORD); +DLG_CONST DLT dltDefineStyle = { + { 34, 18, 188, 101 }, IDDDefineStyle, tmcDSStyle, FDlgDefineStyle, + 19, bdrSysMenu, { 0 } +}; diff --git a/src/port/original/opus_original_startup_probe.cpp b/src/port/original/opus_original_startup_probe.cpp index 07cb25b..f54e32c 100644 --- a/src/port/original/opus_original_startup_probe.cpp +++ b/src/port/original/opus_original_startup_probe.cpp @@ -12,6 +12,30 @@ extern "C" int WINAPI OpusOriginalWinMain(HINSTANCE instance, HINSTANCE previous, LPSTR command_line, int show_command); +using OpusOriginalListProc = unsigned short (*)( + unsigned short, char*, int, unsigned short, unsigned short, + unsigned short); +using OpusFontValueProc = int (*)(const char*); +using OpusFontNameFromValueProc = void (*)(int, char*, int); +extern "C" unsigned short WListFontName(unsigned short, char*, int, + unsigned short, unsigned short, + unsigned short); +extern "C" unsigned short WListFontSize(unsigned short, char*, int, + unsigned short, unsigned short, + unsigned short); +extern "C" unsigned short WListStyles(unsigned short, char*, int, + unsigned short, unsigned short, + unsigned short); +extern "C" unsigned short Look1WListEntbl(unsigned short, char*, int, + unsigned short, unsigned short, + unsigned short); +extern "C" int OpusX64FtcFromFontName(const char*); +extern "C" int OpusX64HpsFromFontSize(const char*); +extern "C" void OpusX64FontNameFromFtc(int, char*, int); +extern "C" void OpusRegisterOriginalDialogCallbacks( + OpusOriginalListProc, OpusOriginalListProc, OpusOriginalListProc, + OpusOriginalListProc, OpusFontValueProc, OpusFontValueProc, + OpusFontNameFromValueProc); namespace { @@ -36,6 +60,22 @@ void BuildDiagnosticPath(const char* file_name, char* path, size_t path_size) { file_name); } +void ResetRibbonTrace() { + char trace_path[MAX_PATH] = {}; + BuildDiagnosticPath("WORD1-ribbon.txt", trace_path, sizeof(trace_path)); + HANDLE file = CreateFileA(trace_path, GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (file == INVALID_HANDLE_VALUE) { + return; + } + char header[128] = {}; + std::snprintf(header, sizeof(header), "WORD1 ribbon trace pid=%lu\r\n", + GetCurrentProcessId()); + WriteCrashText(file, header); + CloseHandle(file); +} + void WriteCurrentStack(HANDLE file, unsigned frames_to_skip) { void* frames[64] = {}; const USHORT frame_count = @@ -334,6 +374,28 @@ LONG WINAPI ObserveVectoredException(EXCEPTION_POINTERS* exception) { } // namespace +extern "C" void OpusX64TraceRibbon(const char* stage, int message, int tmc, + int first_value, int second_value, + long cp_first, long cp_limit, + int insertion) { + char trace_path[MAX_PATH] = {}; + BuildDiagnosticPath("WORD1-ribbon.txt", trace_path, sizeof(trace_path)); + HANDLE file = CreateFileA(trace_path, FILE_APPEND_DATA, + FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, + OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (file == INVALID_HANDLE_VALUE) { + return; + } + char line[384] = {}; + std::snprintf(line, sizeof(line), + "%llu %s msg=%d tmc=%d a=%d b=%d sel=%ld,%ld ins=%d\r\n", + static_cast(GetTickCount64()), + stage != nullptr ? stage : "", message, tmc, first_value, + second_value, cp_first, cp_limit, insertion); + WriteCrashText(file, line); + CloseHandle(file); +} + int WINAPI wWinMain(HINSTANCE instance, HINSTANCE previous, PWSTR command_line, int show_command) { if ((command_line != nullptr && @@ -345,6 +407,15 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE previous, SetUnhandledExceptionFilter(WriteCrashStack); AddVectoredExceptionHandler(1, ObserveVectoredException); _RTC_SetErrorFuncW(WriteRtcFailure); + ResetRibbonTrace(); + + /* The native SDM shim owns the controls, while Microsoft's original + callbacks still own every font, point-size, color, and style list. */ + OpusRegisterOriginalDialogCallbacks(WListFontName, WListFontSize, + WListStyles, Look1WListEntbl, + OpusX64FtcFromFontName, + OpusX64HpsFromFontSize, + OpusX64FontNameFromFtc); char command_line_ansi[32768] = {}; if (command_line != nullptr) { diff --git a/src/port/original/opus_sdm_runtime.cpp b/src/port/original/opus_sdm_runtime.cpp index 3d9bbe5..18c32b7 100644 --- a/src/port/original/opus_sdm_runtime.cpp +++ b/src/port/original/opus_sdm_runtime.cpp @@ -5,6 +5,7 @@ extern "C" { } #include +#include #include #include #include @@ -18,6 +19,7 @@ extern std::uintptr_t wRefDlgCur; extern HWND vhWndMsgBoxParent; void GetCabSz(void**, char*, std::uint16_t, std::uint16_t); int FSetCabSz(void**, const char*, std::uint16_t); +void OpusX64TraceRibbon(const char*, int, int, int, int, long, long, int); } /* @@ -35,6 +37,9 @@ using Dword = std::uint32_t; using Hdlg = Word; using Tmc = Word; using Hcab = void**; +using OriginalListProc = Word (*)(Word, char*, int, Word, Word, Word); +using FontValueProc = int (*)(const char*); +using FontNameFromValueProc = void (*)(int, char*, int); struct Rec { int x; @@ -104,6 +109,13 @@ Hdlg g_current_dialog = 0; Hdlg g_focus_dialog = 0; bool g_initialized = false; bool g_noninteractive = false; +OriginalListProc g_list_font_name = nullptr; +OriginalListProc g_list_font_size = nullptr; +OriginalListProc g_list_styles = nullptr; +OriginalListProc g_list_character_color = nullptr; +FontValueProc g_font_name_to_value = nullptr; +FontValueProc g_font_size_to_value = nullptr; +FontNameFromValueProc g_font_name_from_value = nullptr; constexpr Word kDlmInit = 0x0001; constexpr Word kDlmTerm = 0x0003; @@ -111,10 +123,45 @@ constexpr Word kDlmExit = 0x0004; constexpr Word kDlmChange = 0x0005; constexpr Word kDlmClick = 0x0006; constexpr Word kDlmDblClk = 0x0007; +constexpr Word kDlmSetItemFocus = 0x000b; +constexpr Word kDlmKillItemFocus = 0x000c; +constexpr Word kDlmSetDialogFocus = 0x000d; +constexpr Word kDlmKillDialogFocus = 0x000e; +constexpr Word kDlmDialogClick = 0x0012; +constexpr UINT kWmCommitRibbonSelection = WM_APP + 0x352; constexpr Word kIddNewDoc = 2; constexpr Word kIddOpen = 3; +constexpr Word kIddCharacter = 16; +constexpr Word kIddApplyStyle = 23; +constexpr Word kIddDefineStyle = 24; +constexpr Word kCxtRibbonIconBar = 0x8005; +constexpr Word kCxtRulerIconBar = 0x8006; constexpr Tmc kTmcOk = 1; constexpr Tmc kTmcCancel = 2; +constexpr Tmc kTmcUserMin = 0x0400; +constexpr Tmc kTmcCharacterName = kTmcUserMin; +constexpr Tmc kTmcCharacterSize = kTmcUserMin + 2; +constexpr Tmc kTmcCharacterColor = kTmcUserMin + 4; +constexpr Tmc kTmcApplyStyle = kTmcUserMin; +constexpr Tmc kTmcApplyDefine = kTmcUserMin + 2; +constexpr Tmc kTmcApplyBanter = kTmcUserMin + 3; +constexpr Tmc kTmcDefineStyle = kTmcUserMin; +constexpr Tmc kTmcDefineChars = kTmcUserMin + 2; +constexpr Tmc kTmcDefineParas = kTmcUserMin + 3; +constexpr Tmc kTmcDefineTabs = kTmcUserMin + 4; +constexpr Tmc kTmcDefinePosition = kTmcUserMin + 5; +constexpr Tmc kTmcDefineOptions = kTmcUserMin + 6; +constexpr Tmc kTmcDefineBanter = kTmcUserMin + 7; +constexpr Tmc kTmcDefineBasedOn = kTmcUserMin + 8; +constexpr Tmc kTmcDefineNext = kTmcUserMin + 10; +constexpr Tmc kTmcDefineTemplate = kTmcUserMin + 12; +constexpr Tmc kTmcDefineCommit = kTmcUserMin + 13; +constexpr Tmc kTmcDefineDelete = kTmcUserMin + 14; +constexpr Tmc kTmcDefineRename = kTmcUserMin + 15; +constexpr Tmc kTmcDefineMerge = kTmcUserMin + 16; +constexpr Word kTmmCount = 2; +constexpr Word kTmmText = 3; +constexpr Word kUnknownListCount = 0xffff; constexpr Tmc kTmcSummary = 0x0400; constexpr Tmc kTmcNewDot = 0x0401; constexpr Tmc kTmcRNewDoc = 0x0402; @@ -198,7 +245,9 @@ ATOM ensure_native_dialog_class() { HWND create_dialog_host(const DialogState& dialog, const Dli* initializer) { if (dialog.modal && - (dialog.hid == kIddOpen || dialog.hid == kIddNewDoc)) { + (dialog.hid == kIddOpen || dialog.hid == kIddNewDoc || + dialog.hid == kIddCharacter || dialog.hid == kIddApplyStyle || + dialog.hid == kIddDefineStyle)) { if (ensure_native_dialog_class() == 0) { return nullptr; } @@ -237,8 +286,21 @@ HWND create_dialog_host(const DialogState& dialog, const Dli* initializer) { (anchor.right - anchor.left - width) / 2; const int y = anchor.top + (anchor.bottom - anchor.top - height) / 2; - const char* const caption = - dialog.hid == kIddNewDoc ? "New" : "Open"; + const char* caption = "Open"; + switch (dialog.hid) { + case kIddNewDoc: + caption = "New"; + break; + case kIddCharacter: + caption = "Character"; + break; + case kIddApplyStyle: + caption = "Apply Style"; + break; + case kIddDefineStyle: + caption = "Define Style"; + break; + } return CreateWindowExA( extended_style, "OpusSdmDialog", caption, style, x, y, width, height, owner, nullptr, GetModuleHandleW(nullptr), @@ -250,6 +312,9 @@ HWND create_dialog_host(const DialogState& dialog, const Dli* initializer) { !IsWindow(initializer->hwnd)) { return nullptr; } + if (ensure_native_dialog_class() == 0) { + return nullptr; + } int x = initializer->dx; int y = initializer->dy; @@ -270,9 +335,10 @@ HWND create_dialog_host(const DialogState& dialog, const Dli* initializer) { inherited without depending on that retired message. */ const DWORD style = WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS; - return CreateWindowExA(0, "STATIC", "", style, x, y, width, height, - initializer->hwnd, nullptr, - GetModuleHandleW(nullptr), nullptr); + return CreateWindowExA( + 0, "OpusSdmDialog", "", style, x, y, width, height, + initializer->hwnd, nullptr, GetModuleHandleW(nullptr), + reinterpret_cast(static_cast(dialog.handle))); } HWND create_native_control(DialogState& dialog, const Tmc tmc, @@ -339,6 +405,36 @@ void create_untracked_control(DialogState& dialog, const char* window_class, } } +bool window_is_class(const HWND window, const char* expected) { + if (window == nullptr || !IsWindow(window)) { + return false; + } + char actual[32] = {}; + return GetClassNameA(window, actual, static_cast(sizeof(actual))) != 0 && + _stricmp(actual, expected) == 0; +} + +void add_entry_to_native_control(const ControlState& state, + const std::string& entry) { + if (state.window == nullptr || !IsWindow(state.window)) { + return; + } + const UINT message = + window_is_class(state.window, "COMBOBOX") ? CB_ADDSTRING : LB_ADDSTRING; + SendMessageA(state.window, message, 0, + reinterpret_cast(entry.c_str())); +} + +void reset_native_list(ControlState& state) { + if (state.window == nullptr || !IsWindow(state.window)) { + return; + } + const UINT message = window_is_class(state.window, "COMBOBOX") + ? CB_RESETCONTENT + : LB_RESETCONTENT; + SendMessageA(state.window, message, 0, 0); +} + std::string join_path(const std::string& directory, const std::string& leaf) { if (directory.empty()) { @@ -352,10 +448,7 @@ void add_native_list_entry(DialogState& dialog, const Tmc tmc, const std::string& entry) { auto& state = dialog.controls[tmc]; state.entries.push_back(entry); - if (state.window != nullptr && IsWindow(state.window)) { - SendMessageA(state.window, LB_ADDSTRING, 0, - reinterpret_cast(entry.c_str())); - } + add_entry_to_native_control(state, entry); } void set_open_directory_label(DialogState& dialog) { @@ -690,42 +783,581 @@ void materialize_open_template(DialogState& dialog) { read_open_cab(dialog); } +bool is_font_name_control(const DialogState& dialog, const Tmc tmc) { + return (dialog.hid == kCxtRibbonIconBar && tmc == kTmcUserMin) || + (dialog.hid == kIddCharacter && tmc == kTmcCharacterName); +} + +bool is_font_size_control(const DialogState& dialog, const Tmc tmc) { + return (dialog.hid == kCxtRibbonIconBar && tmc == kTmcUserMin + 1) || + (dialog.hid == kIddCharacter && tmc == kTmcCharacterSize); +} + +void refresh_font_control_value(DialogState& dialog, const Tmc raw_tmc, + ControlState& state, + const bool read_native_text) { + const Tmc tmc = static_cast(raw_tmc & ~0x8000u); + if (read_native_text && state.window != nullptr && + IsWindow(state.window)) { + HWND text_window = state.window; + if (window_is_class(state.window, "COMBOBOX")) { + COMBOBOXINFO info{}; + info.cbSize = sizeof(info); + if (GetComboBoxInfo(state.window, &info) && + info.hwndItem != nullptr) { + text_window = info.hwndItem; + } + } + const int wide_length = GetWindowTextLengthW(text_window); + std::vector wide_text( + static_cast(wide_length) + 1); + GetWindowTextW(text_window, wide_text.data(), + static_cast(wide_text.size())); + const int byte_count = WideCharToMultiByte( + CP_ACP, 0, wide_text.data(), -1, nullptr, 0, nullptr, nullptr); + if (byte_count > 0) { + std::vector text(static_cast(byte_count)); + WideCharToMultiByte(CP_ACP, 0, wide_text.data(), -1, text.data(), + byte_count, nullptr, nullptr); + state.text = text.data(); + } else { + state.text.clear(); + } + } + + if (is_font_name_control(dialog, tmc) && + g_font_name_to_value != nullptr && !state.text.empty()) { + state.value = + static_cast(g_font_name_to_value(state.text.c_str())); + } else if (is_font_size_control(dialog, tmc) && + g_font_size_to_value != nullptr && !state.text.empty()) { + state.value = + static_cast(g_font_size_to_value(state.text.c_str())); + } +} + +int CALLBACK collect_system_font(const LOGFONTA* logical_font, + const TEXTMETRICA*, DWORD, LPARAM parameter) { + if (logical_font == nullptr || logical_font->lfFaceName[0] == '\0' || + logical_font->lfFaceName[0] == '@') { + return 1; + } + auto* fonts = reinterpret_cast*>(parameter); + fonts->emplace_back(logical_font->lfFaceName); + return 1; +} + +std::vector installed_windows_fonts() { + std::vector fonts; + const HDC dc = GetDC(nullptr); + if (dc != nullptr) { + LOGFONTA logical_font{}; + logical_font.lfCharSet = DEFAULT_CHARSET; + EnumFontFamiliesExA(dc, &logical_font, + reinterpret_cast(collect_system_font), + reinterpret_cast(&fonts), 0); + ReleaseDC(nullptr, dc); + } + std::sort(fonts.begin(), fonts.end(), + [](const std::string& left, const std::string& right) { + return _stricmp(left.c_str(), right.c_str()) < 0; + }); + fonts.erase(std::unique(fonts.begin(), fonts.end(), + [](const std::string& left, + const std::string& right) { + return _stricmp(left.c_str(), right.c_str()) == 0; + }), + fonts.end()); + return fonts; +} + +void replace_list_entries(DialogState& dialog, const Tmc tmc, + const std::vector& entries) { + auto& state = dialog.controls[tmc]; + if (state.window != nullptr && IsWindow(state.window)) { + const int length = GetWindowTextLengthA(state.window); + std::vector buffer(static_cast(length) + 1); + GetWindowTextA(state.window, buffer.data(), + static_cast(buffer.size())); + state.text = buffer.data(); + } + const std::string edit_text = state.text; + state.entries = entries; + reset_native_list(state); + for (const auto& entry : state.entries) { + add_entry_to_native_control(state, entry); + } + state.text = edit_text; + if (state.window != nullptr && IsWindow(state.window)) { + SetWindowTextA(state.window, state.text.c_str()); + } +} + +bool populate_windows_font_control(DialogState& dialog, const Tmc tmc) { + if (is_font_name_control(dialog, tmc)) { + replace_list_entries(dialog, tmc, installed_windows_fonts()); + return !dialog.controls[tmc].entries.empty(); + } + if (is_font_size_control(dialog, tmc)) { + static const std::vector sizes = { + "8", "9", "10", "11", "12", "14", "16", "18", + "20", "22", "24", "26", "28", "36", "48", "72"}; + replace_list_entries(dialog, tmc, sizes); + return true; + } + return false; +} + +struct OriginalListBinding { + OriginalListProc proc = nullptr; + Word parameter = 0; +}; + +OriginalListBinding original_list_binding(const DialogState& dialog, + const Tmc raw_tmc) { + const Tmc tmc = static_cast(raw_tmc & ~0x8000u); + if (dialog.hid == kCxtRibbonIconBar) { + if (tmc == kTmcUserMin) { + return {g_list_font_name, 0}; + } + if (tmc == kTmcUserMin + 1) { + return {g_list_font_size, kTmcUserMin}; + } + } else if (dialog.hid == kCxtRulerIconBar && tmc == kTmcUserMin) { + return {g_list_styles, 0}; + } else if (dialog.hid == kIddCharacter) { + if (tmc == kTmcCharacterName) { + return {g_list_font_name, 0}; + } + if (tmc == kTmcCharacterSize) { + return {g_list_font_size, kTmcCharacterName}; + } + if (tmc == kTmcCharacterColor) { + return {g_list_character_color, 0}; + } + } else if (dialog.hid == kIddApplyStyle && tmc == kTmcApplyStyle) { + return {g_list_styles, 0}; + } else if (dialog.hid == kIddDefineStyle && + (tmc == kTmcDefineStyle || tmc == kTmcDefineBasedOn || + tmc == kTmcDefineNext)) { + return {g_list_styles, 0}; + } + return {}; +} + +bool populate_original_list(DialogState& dialog, const Tmc raw_tmc) { + const Tmc tmc = static_cast(raw_tmc & ~0x8000u); + if (populate_windows_font_control(dialog, tmc)) { + return true; + } + const auto binding = original_list_binding(dialog, tmc); + if (binding.proc == nullptr) { + return false; + } + + auto& state = dialog.controls[tmc]; + if (state.window != nullptr && IsWindow(state.window)) { + const int length = GetWindowTextLengthA(state.window); + std::vector text_buffer(static_cast(length) + 1); + GetWindowTextA(state.window, text_buffer.data(), + static_cast(text_buffer.size())); + state.text = text_buffer.data(); + } + const std::string edit_text = state.text; + state.entries.clear(); + reset_native_list(state); + + const Hdlg previous_current = g_current_dialog; + g_current_dialog = dialog.handle; + sync_current_dialog_globals(); + + char buffer[512] = {}; + const Word reported_count = + binding.proc(kTmmCount, buffer, 0, 0, tmc, binding.parameter); + const unsigned limit = reported_count == kUnknownListCount + ? 4096u + : static_cast(reported_count); + for (unsigned index = 0; index < limit; ++index) { + buffer[0] = '\0'; + if (binding.proc(kTmmText, buffer, static_cast(index), 0, tmc, + binding.parameter) == 0) { + break; + } + state.entries.emplace_back(buffer); + add_entry_to_native_control(state, state.entries.back()); + } + + g_current_dialog = previous_current; + sync_current_dialog_globals(); + state.text = edit_text; + if (state.window != nullptr && IsWindow(state.window)) { + SetWindowTextA(state.window, state.text.c_str()); + } + return !state.entries.empty(); +} + +void read_style_cab(DialogState& dialog, const Tmc tmc) { + char style_name[256] = {}; + if (dialog.cab != nullptr) { + /* Pointer arguments begin at iag 1 in the native, aligned CAB layout + used by CABAPPLYSTYLE and CABDEFINESTYLE. */ + GetCabSz(dialog.cab, style_name, + static_cast(sizeof(style_name)), 1); + } + auto& state = dialog.controls[tmc]; + state.text = style_name; + if (state.window != nullptr) { + SetWindowTextA(state.window, state.text.c_str()); + } +} + +void sync_style_cab(DialogState& dialog, const Tmc tmc) { + auto found = dialog.controls.find(tmc); + if (found == dialog.controls.end()) { + return; + } + auto& state = found->second; + if (state.window != nullptr && IsWindow(state.window)) { + const int length = GetWindowTextLengthA(state.window); + std::vector buffer(static_cast(length) + 1); + GetWindowTextA(state.window, buffer.data(), + static_cast(buffer.size())); + state.text = buffer.data(); + } + if (dialog.cab != nullptr) { + FSetCabSz(dialog.cab, state.text.c_str(), 1); + } +} + +struct CabCharacterNative { + Word simple_words; + Word handle_words; + Word sab; + Word alignment; + int ftc; + int hps; + int color; + int bold; + int italic; + int small_caps; + int hidden; + int underline; + int word_underline; + int double_underline; + int position; + int position_amount; + int spacing; + int spacing_amount; +}; + +CabCharacterNative* character_cab(DialogState& dialog) { + if (dialog.cab == nullptr || *dialog.cab == nullptr || + OpusCbOfH(dialog.cab) < sizeof(CabCharacterNative)) { + return nullptr; + } + return static_cast(*dialog.cab); +} + +void set_native_check(DialogState& dialog, const Tmc tmc, const int value) { + auto& state = dialog.controls[tmc]; + state.value = static_cast(value); + if (state.window != nullptr) { + SendMessageA(state.window, BM_SETCHECK, + value != 0 ? BST_CHECKED : BST_UNCHECKED, 0); + } +} + +void read_character_cab(DialogState& dialog) { + auto* cab = character_cab(dialog); + if (cab == nullptr) { + return; + } + char font_name[LF_FACESIZE] = {}; + if (g_font_name_from_value != nullptr) { + g_font_name_from_value(cab->ftc, font_name, + static_cast(sizeof(font_name))); + } + auto& font = dialog.controls[kTmcCharacterName]; + font.text = font_name; + font.value = static_cast(cab->ftc); + if (font.window != nullptr) { + SetWindowTextA(font.window, font.text.c_str()); + } + + char size_text[32] = {}; + if (cab->hps >= 0 && cab->hps != 0x8001) { + if ((cab->hps & 1) == 0) { + std::snprintf(size_text, sizeof(size_text), "%d", cab->hps / 2); + } else { + std::snprintf(size_text, sizeof(size_text), "%d.5", cab->hps / 2); + } + } + auto& size = dialog.controls[kTmcCharacterSize]; + size.text = size_text; + size.value = static_cast(cab->hps); + if (size.window != nullptr) { + SetWindowTextA(size.window, size.text.c_str()); + } + + if (cab->color >= 0 && + static_cast(cab->color) < + dialog.controls[kTmcCharacterColor].entries.size()) { + SendMessageA(dialog.controls[kTmcCharacterColor].window, CB_SETCURSEL, + cab->color, 0); + dialog.controls[kTmcCharacterColor].value = + static_cast(cab->color); + } + set_native_check(dialog, kTmcUserMin + 5, cab->bold); + set_native_check(dialog, kTmcUserMin + 6, cab->italic); + set_native_check(dialog, kTmcUserMin + 7, cab->small_caps); + set_native_check(dialog, kTmcUserMin + 8, cab->hidden); + set_native_check(dialog, kTmcUserMin + 9, cab->underline); + set_native_check(dialog, kTmcUserMin + 10, cab->word_underline); + set_native_check(dialog, kTmcUserMin + 11, cab->double_underline); + const int position = cab->position >= 0 && cab->position <= 2 + ? cab->position + : 0; + set_native_check(dialog, static_cast(kTmcUserMin + 13 + position), 1); + const int spacing = cab->spacing >= 0 && cab->spacing <= 2 + ? cab->spacing + : 0; + set_native_check(dialog, static_cast(kTmcUserMin + 18 + spacing), 1); +} + +void sync_character_cab(DialogState& dialog) { + auto* cab = character_cab(dialog); + if (cab == nullptr) { + return; + } + const auto read_text = [&dialog](const Tmc tmc) { + auto& state = dialog.controls[tmc]; + if (state.window != nullptr) { + const int length = GetWindowTextLengthA(state.window); + std::vector buffer(static_cast(length) + 1); + GetWindowTextA(state.window, buffer.data(), + static_cast(buffer.size())); + state.text = buffer.data(); + } + return state.text; + }; + if (g_font_name_to_value != nullptr) { + const int ftc = + g_font_name_to_value(read_text(kTmcCharacterName).c_str()); + if (ftc >= 0) { + cab->ftc = ftc; + } + } + if (g_font_size_to_value != nullptr) { + const int hps = + g_font_size_to_value(read_text(kTmcCharacterSize).c_str()); + if (hps >= 0) { + cab->hps = hps; + } + } + const LRESULT color = SendMessageA( + dialog.controls[kTmcCharacterColor].window, CB_GETCURSEL, 0, 0); + if (color != CB_ERR) { + cab->color = static_cast(color); + } + const auto checked = [&dialog](const Tmc tmc) { + return SendMessageA(dialog.controls[tmc].window, BM_GETCHECK, 0, 0) == + BST_CHECKED; + }; + cab->bold = checked(kTmcUserMin + 5); + cab->italic = checked(kTmcUserMin + 6); + cab->small_caps = checked(kTmcUserMin + 7); + cab->hidden = checked(kTmcUserMin + 8); + cab->underline = checked(kTmcUserMin + 9); + cab->word_underline = checked(kTmcUserMin + 10); + cab->double_underline = checked(kTmcUserMin + 11); + for (int index = 0; index < 3; ++index) { + if (checked(static_cast(kTmcUserMin + 13 + index))) { + cab->position = index; + } + if (checked(static_cast(kTmcUserMin + 18 + index))) { + cab->spacing = index; + } + } +} + +void materialize_character_template(DialogState& dialog) { + if (dialog.hid != kIddCharacter || dialog.window == nullptr) { + return; + } + constexpr DWORD combo_style = + WS_TABSTOP | WS_VSCROLL | CBS_DROPDOWN | CBS_AUTOHSCROLL; + create_static_text(dialog, "Character", {4, 4, 44, 9}); + create_static_text(dialog, "&Font:", {4, 14, 35, 9}); + create_native_control(dialog, kTmcCharacterName, "COMBOBOX", "", + {4, 24, 80, 68}, combo_style); + create_static_text(dialog, "&Points:", {88, 14, 35, 9}); + create_native_control(dialog, kTmcCharacterSize, "COMBOBOX", "", + {88, 24, 40, 68}, combo_style); + create_static_text(dialog, "Co&lor:", {4, 38, 35, 9}); + create_native_control(dialog, kTmcCharacterColor, "COMBOBOX", "", + {4, 48, 44, 72}, + WS_TABSTOP | WS_VSCROLL | CBS_DROPDOWNLIST); + + const struct { + Tmc tmc; + const char* caption; + Rec rec; + } checks[] = { + {kTmcUserMin + 5, "&Bold", {5, 63, 26, 12}}, + {kTmcUserMin + 6, "&Italic", {5, 75, 34, 12}}, + {kTmcUserMin + 7, "Small &Caps", {5, 87, 50, 12}}, + {kTmcUserMin + 8, "&Hidden", {5, 99, 34, 12}}, + {kTmcUserMin + 9, "&Underline", {5, 111, 46, 12}}, + {kTmcUserMin + 10, "&Word underline", {5, 123, 66, 12}}, + {kTmcUserMin + 11, "&Double underline", {5, 135, 74, 12}}, + }; + for (const auto& check : checks) { + create_native_control(dialog, check.tmc, "BUTTON", check.caption, + check.rec, WS_TABSTOP | BS_AUTOCHECKBOX); + } + + create_untracked_control(dialog, "BUTTON", "Position", {84, 44, 89, 48}, + BS_GROUPBOX); + create_native_control(dialog, kTmcUserMin + 13, "BUTTON", "&Normal", + {87, 54, 45, 12}, + WS_TABSTOP | WS_GROUP | BS_AUTORADIOBUTTON); + create_native_control(dialog, kTmcUserMin + 14, "BUTTON", "&Superscript", + {87, 65, 54, 12}, + WS_TABSTOP | BS_AUTORADIOBUTTON); + create_native_control(dialog, kTmcUserMin + 15, "BUTTON", "Subsc&ript", + {87, 76, 50, 12}, + WS_TABSTOP | BS_AUTORADIOBUTTON); + create_static_text(dialog, "B&y:", {141, 67, 13, 9}); + create_native_control(dialog, kTmcUserMin + 16, "EDIT", "", + {141, 77, 30, 12}, + WS_TABSTOP | WS_BORDER | ES_AUTOHSCROLL); + + create_untracked_control(dialog, "BUTTON", "Character Spacing", + {84, 96, 89, 49}, BS_GROUPBOX); + create_native_control(dialog, kTmcUserMin + 18, "BUTTON", "N&ormal", + {87, 107, 45, 12}, + WS_TABSTOP | WS_GROUP | BS_AUTORADIOBUTTON); + create_native_control(dialog, kTmcUserMin + 19, "BUTTON", "&Expanded", + {87, 119, 46, 12}, + WS_TABSTOP | BS_AUTORADIOBUTTON); + create_native_control(dialog, kTmcUserMin + 20, "BUTTON", "&Condensed", + {87, 130, 50, 12}, + WS_TABSTOP | BS_AUTORADIOBUTTON); + create_static_text(dialog, "By&:", {141, 120, 12, 9}); + create_native_control(dialog, kTmcUserMin + 21, "EDIT", "", + {141, 130, 30, 12}, + WS_TABSTOP | WS_BORDER | ES_AUTOHSCROLL); + create_native_control(dialog, kTmcOk, "BUTTON", "OK", {140, 6, 34, 14}, + WS_TABSTOP | BS_DEFPUSHBUTTON); + create_native_control(dialog, kTmcCancel, "BUTTON", "Cancel", + {140, 23, 34, 14}, WS_TABSTOP | BS_PUSHBUTTON); + dialog.caption = "Character"; + dialog.native_modal = true; + populate_original_list(dialog, kTmcCharacterName); + populate_original_list(dialog, kTmcCharacterSize); + populate_original_list(dialog, kTmcCharacterColor); + read_character_cab(dialog); +} + +void materialize_apply_style_template(DialogState& dialog) { + if (dialog.hid != kIddApplyStyle || dialog.window == nullptr) { + return; + } + create_static_text(dialog, "&Style Name:", {4, 3, 55, 9}); + create_native_control(dialog, kTmcApplyStyle, "COMBOBOX", "", + {4, 13, 96, 60}, + WS_TABSTOP | WS_VSCROLL | CBS_DROPDOWN | + CBS_AUTOHSCROLL); + create_native_control(dialog, kTmcOk, "BUTTON", "OK", {104, 6, 40, 14}, + WS_TABSTOP | BS_DEFPUSHBUTTON); + create_native_control(dialog, kTmcCancel, "BUTTON", "Cancel", + {104, 23, 40, 14}, WS_TABSTOP | BS_PUSHBUTTON); + create_native_control(dialog, kTmcApplyDefine, "BUTTON", "&Define...", + {103, 39, 42, 14}, WS_TABSTOP | BS_PUSHBUTTON); + create_native_control(dialog, kTmcApplyBanter, "STATIC", "", + {4, 75, 141, 24}, SS_LEFT); + dialog.caption = "Apply Style"; + dialog.native_modal = true; + read_style_cab(dialog, kTmcApplyStyle); + populate_original_list(dialog, kTmcApplyStyle); +} + +void materialize_define_style_template(DialogState& dialog) { + if (dialog.hid != kIddDefineStyle || dialog.window == nullptr) { + return; + } + create_static_text(dialog, "Define &Style Name:", {4, 3, 78, 10}); + create_native_control(dialog, kTmcDefineStyle, "COMBOBOX", "", + {4, 14, 76, 60}, + WS_TABSTOP | WS_VSCROLL | CBS_DROPDOWN | + CBS_AUTOHSCROLL); + create_native_control(dialog, kTmcDefineChars, "BUTTON", "&Character...", + {85, 14, 52, 14}, WS_TABSTOP | BS_PUSHBUTTON); + create_native_control(dialog, kTmcDefineParas, "BUTTON", "&Paragraph...", + {85, 30, 52, 14}, WS_TABSTOP | BS_PUSHBUTTON); + create_native_control(dialog, kTmcDefineTabs, "BUTTON", "&Tabs...", + {85, 46, 52, 14}, WS_TABSTOP | BS_PUSHBUTTON); + create_native_control(dialog, kTmcDefinePosition, "BUTTON", "Pos&ition...", + {85, 62, 52, 14}, WS_TABSTOP | BS_PUSHBUTTON); + create_native_control(dialog, kTmcOk, "BUTTON", "OK", {140, 14, 45, 14}, + WS_TABSTOP | BS_DEFPUSHBUTTON); + create_native_control(dialog, kTmcCancel, "BUTTON", "Cancel", + {140, 30, 45, 14}, WS_TABSTOP | BS_PUSHBUTTON); + create_native_control(dialog, kTmcDefineOptions, "BUTTON", "&Options >>", + {140, 62, 45, 14}, WS_TABSTOP | BS_PUSHBUTTON); + create_native_control(dialog, kTmcDefineBanter, "STATIC", "", + {4, 76, 180, 24}, SS_LEFT); + create_native_control(dialog, kTmcDefineCommit, "BUTTON", "&Define", + {4, 129, 43, 14}, WS_TABSTOP | BS_PUSHBUTTON); + create_native_control(dialog, kTmcDefineDelete, "BUTTON", "De&lete", + {49, 129, 43, 14}, WS_TABSTOP | BS_PUSHBUTTON); + create_native_control(dialog, kTmcDefineRename, "BUTTON", "&Rename...", + {94, 129, 43, 14}, WS_TABSTOP | BS_PUSHBUTTON); + create_native_control(dialog, kTmcDefineMerge, "BUTTON", "&Merge...", + {139, 129, 43, 14}, WS_TABSTOP | BS_PUSHBUTTON); + dialog.caption = "Define Style"; + dialog.native_modal = true; + read_style_cab(dialog, kTmcDefineStyle); + populate_original_list(dialog, kTmcDefineStyle); +} + void materialize_icon_bar_template(DialogState& dialog) { - constexpr Word cxtRibbonIconBar = 0x8005; - constexpr Word cxtRulerIconBar = 0x8006; - constexpr Tmc tmcUserMin = 0x0400; constexpr DWORD combo_style = WS_TABSTOP | WS_VSCROLL | CBS_DROPDOWN | CBS_AUTOHSCROLL; - if (dialog.hid == cxtRibbonIconBar) { + if (dialog.hid == kCxtRibbonIconBar) { const bool win3 = (*dialog.template_handle)->rec.dx == 160; if (win3) { create_static_text(dialog, "Font:", {4, 3, 20, 8}); - create_native_control(dialog, tmcUserMin, "COMBOBOX", "", + create_native_control(dialog, kTmcUserMin, "COMBOBOX", "", {26, 1, 76, 67}, combo_style); create_static_text(dialog, "Pts:", {108, 3, 14, 8}); - create_native_control(dialog, tmcUserMin + 1, "COMBOBOX", "", + create_native_control(dialog, kTmcUserMin + 1, "COMBOBOX", "", {127, 1, 28, 67}, combo_style); } else { create_static_text(dialog, "Font:", {4, 3, 20, 8}); - create_native_control(dialog, tmcUserMin, "COMBOBOX", "", + create_native_control(dialog, kTmcUserMin, "COMBOBOX", "", {29, 1, 80, 68}, combo_style); create_static_text(dialog, "Pts:", {115, 3, 16, 8}); - create_native_control(dialog, tmcUserMin + 1, "COMBOBOX", "", + create_native_control(dialog, kTmcUserMin + 1, "COMBOBOX", "", {134, 1, 32, 68}, combo_style); } - } else if (dialog.hid == cxtRulerIconBar) { + } else if (dialog.hid == kCxtRulerIconBar) { const bool win3 = (*dialog.template_handle)->rec.dx == 102; if (win3) { create_static_text(dialog, "Style:", {4, 3, 20, 8}); - create_native_control(dialog, tmcUserMin, "COMBOBOX", "", + create_native_control(dialog, kTmcUserMin, "COMBOBOX", "", {26, 1, 76, 68}, combo_style); } else { create_static_text(dialog, "Style:", {4, 3, 24, 8}); - create_native_control(dialog, tmcUserMin, "COMBOBOX", "", + create_native_control(dialog, kTmcUserMin, "COMBOBOX", "", {29, 1, 80, 68}, combo_style); } } + if (dialog.hid == kCxtRibbonIconBar) { + populate_original_list(dialog, kTmcUserMin); + populate_original_list(dialog, kTmcUserMin + 1); + } } HWND ensure_control_window(const Tmc raw_tmc) { @@ -786,22 +1418,64 @@ bool invoke_dialog_proc(DialogState& dialog, const Word message, return proc(message, tmc, new_value, old_value, parameter) != 0; } +void ensure_icon_bar_commands_active(DialogState& dialog, const Tmc tmc) { + if (dialog.hid != kCxtRibbonIconBar || dialog.commands_active) { + return; + } + dialog.commands_active = true; + invoke_dialog_proc(dialog, kDlmSetDialogFocus, tmc); + invoke_dialog_proc(dialog, kDlmSetItemFocus, tmc); +} + +void commit_ribbon_list_selection(DialogState& dialog, const Tmc tmc) { + if (dialog.hid != kCxtRibbonIconBar || !dialog.commands_active) { + return; + } + /* The Win16 SDM/icon-bar loop applied a combo selection when its dialog + focus was terminated. A native Win32 drop-down keeps focus on the + combo after the list closes, so that termination never reliably + occurs. CBN_SELENDOK identifies the completed choice, after all + intermediate CBN_SELCHANGE notifications, and is the safe point to + send the same original callbacks. Typed edit text still commits on + CBN_KILLFOCUS. */ + OpusX64TraceRibbon("commit-begin", kDlmKillDialogFocus, tmc, + dialog.controls[tmc].value, dialog.commands_active, + 0, 0, 0); + const bool item_result = + invoke_dialog_proc(dialog, kDlmKillItemFocus, tmc); + const bool dialog_result = + invoke_dialog_proc(dialog, kDlmKillDialogFocus, tmc); + dialog.commands_active = false; + /* Win16 SDM returned focus to the document when an icon-bar session + ended. A native combo otherwise retains focus, forcing the user to + click the document; that mouse click reloads the properties at the + caret and discards the just-applied insertion font and size. Route + the focus return through Microsoft's original FDlgIb handler. */ + const bool focus_result = + invoke_dialog_proc(dialog, kDlmDialogClick, tmc); + OpusX64TraceRibbon("commit-end", kDlmKillDialogFocus, tmc, + item_result, dialog_result, focus_result, 0, 0); +} + std::string selected_list_text(const ControlState& control_state) { if (control_state.window == nullptr) { return {}; } - const LRESULT selection = - SendMessageA(control_state.window, LB_GETCURSEL, 0, 0); - if (selection == LB_ERR) { + const bool combo = window_is_class(control_state.window, "COMBOBOX"); + const LRESULT selection = SendMessageA( + control_state.window, combo ? CB_GETCURSEL : LB_GETCURSEL, 0, 0); + if (selection == (combo ? CB_ERR : LB_ERR)) { return {}; } - const LRESULT length = - SendMessageA(control_state.window, LB_GETTEXTLEN, selection, 0); - if (length == LB_ERR) { + const LRESULT length = SendMessageA( + control_state.window, combo ? CB_GETLBTEXTLEN : LB_GETTEXTLEN, + selection, 0); + if (length == (combo ? CB_ERR : LB_ERR)) { return {}; } std::vector text(static_cast(length) + 1); - SendMessageA(control_state.window, LB_GETTEXT, selection, + SendMessageA(control_state.window, combo ? CB_GETLBTEXT : LB_GETTEXT, + selection, reinterpret_cast(text.data())); return text.data(); } @@ -874,6 +1548,12 @@ void finish_native_dialog(DialogState& dialog, const Tmc result) { sync_open_cab(dialog); } else if (dialog.hid == kIddNewDoc) { sync_new_cab(dialog); + } else if (dialog.hid == kIddApplyStyle) { + sync_style_cab(dialog, kTmcApplyStyle); + } else if (dialog.hid == kIddDefineStyle) { + sync_style_cab(dialog, kTmcDefineStyle); + } else if (dialog.hid == kIddCharacter) { + sync_character_cab(dialog); } if (!invoke_dialog_proc(dialog, kDlmTerm, result)) { return; @@ -915,14 +1595,77 @@ void handle_dialog_command(const Hdlg handle, const WPARAM w_param, found->second.text = text.data(); } } - if (!dialog->commands_active) { - return; - } - g_current_dialog = handle; g_focus_dialog = handle; sync_current_dialog_globals(); + if (found != dialog->controls.end() && + window_is_class(found->second.window, "COMBOBOX")) { + OpusX64TraceRibbon("combo", notification, tmc, + found->second.value, dialog->commands_active, + 0, 0, 0); + if (notification == CBN_SETFOCUS) { + if (!dialog->commands_active) { + dialog->commands_active = true; + invoke_dialog_proc(*dialog, kDlmSetDialogFocus, tmc); + } + invoke_dialog_proc(*dialog, kDlmSetItemFocus, tmc); + return; + } + if (notification == CBN_KILLFOCUS) { + if (dialog->commands_active) { + invoke_dialog_proc(*dialog, kDlmKillItemFocus, tmc); + invoke_dialog_proc(*dialog, kDlmKillDialogFocus, tmc); + dialog->commands_active = false; + } + return; + } + if (notification == CBN_DROPDOWN) { + populate_original_list(*dialog, tmc); + return; + } + if (notification == CBN_EDITCHANGE) { + ensure_icon_bar_commands_active(*dialog, tmc); + refresh_font_control_value(*dialog, tmc, found->second, true); + OpusX64TraceRibbon("combo-edit", notification, tmc, + found->second.value, + dialog->commands_active, 0, 0, 0); + invoke_dialog_proc(*dialog, kDlmChange, tmc); + return; + } + if (notification == CBN_SELCHANGE) { + ensure_icon_bar_commands_active(*dialog, tmc); + const LRESULT selection = + SendMessageA(found->second.window, CB_GETCURSEL, 0, 0); + if (selection != CB_ERR) { + dialog->controls[static_cast(tmc + 1)].value = + static_cast(selection); + found->second.value = static_cast(selection); + found->second.text = selected_list_text(found->second); + refresh_font_control_value(*dialog, tmc, found->second, + false); + OpusX64TraceRibbon("combo-select", notification, tmc, + found->second.value, + static_cast(selection), 0, 0, 0); + SetWindowTextA(found->second.window, + found->second.text.c_str()); + invoke_dialog_proc(*dialog, kDlmClick, + static_cast(tmc + 1)); + invoke_dialog_proc(*dialog, kDlmChange, tmc); + } + return; + } + if (notification == CBN_SELENDOK) { + ensure_icon_bar_commands_active(*dialog, tmc); + PostMessageW(dialog->window, kWmCommitRibbonSelection, tmc, 0); + return; + } + } + + if (!dialog->commands_active) { + return; + } + if (dialog->hid == kIddNewDoc && tmc == kTmcNewType && notification == EN_CHANGE) { invoke_dialog_proc(*dialog, kDlmChange, tmc); @@ -1000,6 +1743,17 @@ LRESULT CALLBACK native_dialog_window_proc(const HWND window, case WM_COMMAND: handle_dialog_command(handle, w_param, l_param); return 0; + case kWmCommitRibbonSelection: + if (auto* dialog = find_dialog(handle); dialog != nullptr) { + g_current_dialog = handle; + g_focus_dialog = handle; + sync_current_dialog_globals(); + ensure_icon_bar_commands_active( + *dialog, static_cast(w_param)); + commit_ribbon_list_selection( + *dialog, static_cast(w_param)); + } + return 0; case WM_CLOSE: if (auto* dialog = find_dialog(handle); dialog != nullptr) { finish_native_dialog(*dialog, kTmcCancel); @@ -1028,6 +1782,20 @@ HWND vhWndMsgBoxParent = nullptr; extern void** hcabDlgCur; extern std::uintptr_t wRefDlgCur; +void OpusRegisterOriginalDialogCallbacks( + OriginalListProc list_font_name, OriginalListProc list_font_size, + OriginalListProc list_styles, OriginalListProc list_character_color, + FontValueProc font_name_to_value, FontValueProc font_size_to_value, + FontNameFromValueProc font_name_from_value) { + g_list_font_name = list_font_name; + g_list_font_size = list_font_size; + g_list_styles = list_styles; + g_list_character_color = list_character_color; + g_font_name_to_value = font_name_to_value; + g_font_size_to_value = font_size_to_value; + g_font_name_from_value = font_name_from_value; +} + int FInitSdm_sdm21(void*) { g_initialized = true; dac.dxBorder = GetSystemMetrics(SM_CXBORDER); @@ -1100,6 +1868,9 @@ Hdlg HdlgStartDlg(DltHeader** dialog_template, Hcab cab, Dli* initializer) { materialize_icon_bar_template(g_dialogs.at(handle)); materialize_new_template(g_dialogs.at(handle)); materialize_open_template(g_dialogs.at(handle)); + materialize_character_template(g_dialogs.at(handle)); + materialize_apply_style_template(g_dialogs.at(handle)); + materialize_define_style_template(g_dialogs.at(handle)); return handle; } @@ -1304,8 +2075,13 @@ void SetTmcVal_sdm21(Tmc tmc, Word value) { } } Word ValGetTmc(Tmc tmc) { - const auto* state = find_control(tmc); - return state == nullptr ? 0 : state->value; + const Tmc value_tmc = static_cast(tmc & ~0x8000u); + const auto found = g_dialog.controls.find(value_tmc); + if (found == g_dialog.controls.end()) { + return 0; + } + refresh_font_control_value(g_dialog, value_tmc, found->second, true); + return found->second.value; } void SetTmcText_sdm21(Tmc tmc, char* text) { @@ -1317,6 +2093,7 @@ void SetTmcText_sdm21(Tmc tmc, char* text) { if (state.window != nullptr && IsWindow(state.window)) { SetWindowTextA(state.window, state.text.c_str()); } + refresh_font_control_value(g_dialog, tmc, state, false); } void GetTmcText_sdm21(Tmc tmc, char* destination, Word capacity) { const auto* state = find_control(tmc); @@ -1382,7 +2159,7 @@ Word TmvGetTmc(Tmc tmc) { const auto* state = find_control(tmc); return state != nullptr && !state->text.empty() ? 3 : 1; } -void RedisplayTmc(Tmc) {} +void RedisplayTmc(Tmc tmc) { populate_original_list(g_dialog, tmc); } void EnableTmc_sdm21(Tmc tmc, int enabled) { auto& state = control(tmc); @@ -1421,10 +2198,7 @@ void CompleteComboTmc(Tmc) {} void AddListBoxEntry(Tmc tmc, char* entry) { auto& state = control(tmc); state.entries.emplace_back(counted_or_zero_terminated(entry)); - if (state.window != nullptr && IsWindow(state.window)) { - SendMessageA(state.window, CB_ADDSTRING, 0, - reinterpret_cast(state.entries.back().c_str())); - } + add_entry_to_native_control(state, state.entries.back()); } void InsertListBoxEntry(Tmc tmc, char* entry, Word index) { auto& state = control(tmc); diff --git a/src/port/original/style.sdm b/src/port/original/style.sdm index affe416..5444fee 100644 --- a/src/port/original/style.sdm +++ b/src/port/original/style.sdm @@ -1,2 +1,6 @@ -/* Template boundary for the original Opus/dlg/style.des description. */ -DLG_CONST DLT dltApplyStyle = { 0 }; +/* Regenerated header from Opus/dlg/style.des. */ +extern BOOL FDlgApplyStyle(DLM, TMC, WORD, WORD, WORD); +DLG_CONST DLT dltApplyStyle = { + { 32, 16, 150, 102 }, IDDApplyStyle, tmcASStyle, FDlgApplyStyle, + 6, bdrSysMenu, { 0 } +}; diff --git a/src/port/winword.manifest b/src/port/winword.manifest index d08c7f4..73395b4 100644 --- a/src/port/winword.manifest +++ b/src/port/winword.manifest @@ -17,9 +17,9 @@ - PerMonitorV2 + true + System true -