mirror of
https://github.com/jmarshall23/msword.git
synced 2026-08-11 23:50:53 +02:00
Added docx and pdf export support.
This commit is contained in:
@@ -721,6 +721,7 @@ add_library(opus_x64_runtime STATIC
|
||||
port/original/opus_asm_search.cpp
|
||||
port/original/opus_sdm_cab.cpp
|
||||
port/original/opus_sdm_runtime.cpp
|
||||
port/original/opus_modern_formats.cpp
|
||||
)
|
||||
target_compile_features(opus_x64_runtime PRIVATE cxx_std_20)
|
||||
target_compile_definitions(opus_x64_runtime PRIVATE
|
||||
@@ -732,6 +733,7 @@ target_compile_definitions(opus_x64_runtime PRIVATE
|
||||
NOMINMAX
|
||||
)
|
||||
target_include_directories(opus_x64_runtime PRIVATE ${OPUS_ORIGINAL_INCLUDE_DIRS})
|
||||
target_link_libraries(opus_x64_runtime PUBLIC ole32 comdlg32 gdi32)
|
||||
if(MSVC)
|
||||
# Opus was built with unsigned plain characters; the byte-oriented string
|
||||
# and international-character assembly routines depend on that behavior.
|
||||
@@ -943,6 +945,27 @@ foreach(config Debug Release RelWithDebInfo MinSizeRel)
|
||||
)
|
||||
endforeach()
|
||||
|
||||
add_executable(opus_modern_formats_test
|
||||
port/original/opus_modern_formats_test.cpp
|
||||
)
|
||||
target_compile_features(opus_modern_formats_test PRIVATE cxx_std_20)
|
||||
target_compile_definitions(opus_modern_formats_test PRIVATE NOMINMAX UNICODE _UNICODE)
|
||||
target_link_libraries(opus_modern_formats_test PRIVATE opus_x64_runtime user32 gdi32)
|
||||
if(MSVC)
|
||||
target_compile_options(opus_modern_formats_test PRIVATE /W4 /utf-8)
|
||||
endif()
|
||||
set_target_properties(opus_modern_formats_test PROPERTIES
|
||||
FOLDER "Original Opus x64/tests"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${MSWORD_BUILD_ROOT}/tests"
|
||||
VS_GLOBAL_VcpkgApplocalDeps "false"
|
||||
)
|
||||
foreach(config Debug Release RelWithDebInfo MinSizeRel)
|
||||
string(TOUPPER "${config}" config_upper)
|
||||
set_target_properties(opus_modern_formats_test PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY_${config_upper} "${MSWORD_BUILD_ROOT}/tests/${config}"
|
||||
)
|
||||
endforeach()
|
||||
|
||||
add_executable(opus_word1_ui_test
|
||||
port/original/opus_word1_ui_test.cpp
|
||||
)
|
||||
@@ -968,6 +991,7 @@ endforeach()
|
||||
enable_testing()
|
||||
add_test(NAME opus_original_strtbl_test COMMAND $<TARGET_FILE:opus_original_strtbl_test>)
|
||||
add_test(NAME opus_x64_runtime_test COMMAND $<TARGET_FILE:opus_x64_runtime_test>)
|
||||
add_test(NAME opus_modern_formats_test COMMAND $<TARGET_FILE:opus_modern_formats_test>)
|
||||
add_test(NAME opus_original_sttb_test COMMAND $<TARGET_FILE:opus_original_sttb_test>)
|
||||
add_test(NAME opus_original_plc_test COMMAND $<TARGET_FILE:opus_original_plc_test>)
|
||||
add_test(NAME opus_sdm_cab_test COMMAND $<TARGET_FILE:opus_sdm_cab_test>)
|
||||
@@ -1005,3 +1029,7 @@ add_test(NAME opus_word1_save_as_test
|
||||
COMMAND $<TARGET_FILE:opus_word1_ui_test> $<TARGET_FILE:WORD1> --save-as
|
||||
)
|
||||
set_tests_properties(opus_word1_save_as_test PROPERTIES TIMEOUT 20)
|
||||
add_test(NAME opus_word1_pdf_export_test
|
||||
COMMAND $<TARGET_FILE:opus_word1_ui_test> $<TARGET_FILE:WORD1> --pdf-export
|
||||
)
|
||||
set_tests_properties(opus_word1_pdf_export_test PROPERTIES TIMEOUT 25)
|
||||
|
||||
+8
-1
@@ -83,6 +83,9 @@ extern int vfConversion;
|
||||
extern MUD ** vhmudUser;
|
||||
extern KMP ** hkmpCur;
|
||||
extern KMP ** vhkmpUser;
|
||||
#ifdef OPUS_X64
|
||||
extern int OpusWin95OpenAliasIsDocx();
|
||||
#endif
|
||||
|
||||
struct DTTM DttmCur();
|
||||
|
||||
@@ -484,7 +487,11 @@ CHAR *stConverter, **pstSubset;
|
||||
dff = dffOpenText;
|
||||
|
||||
/* dff is our best guess of format */
|
||||
if (fInput)
|
||||
if (fInput
|
||||
#ifdef OPUS_X64
|
||||
&& !OpusWin95OpenAliasIsDocx(PfcbFn(fn)->stFile)
|
||||
#endif
|
||||
)
|
||||
/* verify the format */
|
||||
if ((dff = DffVerifyDff (dff, fn)) == dffNil)
|
||||
/* cancel operation */
|
||||
|
||||
+1
-1
@@ -833,7 +833,7 @@ int doc;
|
||||
*/
|
||||
/* %%Function: HWriteRTF %%Owner: bobz */
|
||||
|
||||
HWriteRTF (doc, cpFirst, cpLim, cbInitial)
|
||||
HANDLE HWriteRTF (doc, cpFirst, cpLim, cbInitial)
|
||||
int doc;
|
||||
CP cpFirst, cpLim;
|
||||
int cbInitial;
|
||||
|
||||
@@ -103,6 +103,85 @@ static int vWin95ZoomPercent = 100;
|
||||
static int vWin95BaseDxsInch = 0;
|
||||
static int vWin95BaseDysInch = 0;
|
||||
|
||||
int OpusExportCurrentDocumentPdf()
|
||||
{
|
||||
int doc;
|
||||
HANDLE hText;
|
||||
LPCH lpch;
|
||||
int result;
|
||||
int cchText = 0;
|
||||
int fAfterReturn = fFalse;
|
||||
CP cp;
|
||||
CP cpMac;
|
||||
extern int vccpFetch;
|
||||
extern CHAR HUGE *vhpchFetch;
|
||||
extern int N_FetchCp();
|
||||
extern int OpusExportTextToPdfDialog();
|
||||
|
||||
if (selCur.doc == docNil)
|
||||
return fFalse;
|
||||
doc = DocMother(selCur.doc);
|
||||
cpMac = CpMacDocEdit(doc);
|
||||
if (cpMac < cp0 || cpMac > 0x3fffffffL)
|
||||
return fFalse;
|
||||
hText = GlobalAlloc(GMEM_MOVEABLE, (DWORD)(cpMac * 2L + 1L));
|
||||
if (hText == hNil || (lpch = GlobalLockClip(hText)) == NULL)
|
||||
{
|
||||
if (hText != hNil)
|
||||
GlobalFree(hText);
|
||||
return fFalse;
|
||||
}
|
||||
|
||||
for (cp = cp0; cp < cpMac; )
|
||||
{
|
||||
int ich;
|
||||
int cch;
|
||||
FetchCp(doc, cp, fcmChars);
|
||||
if (vccpFetch <= 0 || vhpchFetch == NULL)
|
||||
{
|
||||
GlobalUnlock(hText);
|
||||
GlobalFree(hText);
|
||||
return fFalse;
|
||||
}
|
||||
cch = (int)CpMin((CP)vccpFetch, cpMac - cp);
|
||||
for (ich = 0; ich < cch; ++ich)
|
||||
{
|
||||
int ch = (unsigned char)vhpchFetch[ich];
|
||||
if (ch == chReturn)
|
||||
{
|
||||
lpch[cchText++] = '\r';
|
||||
lpch[cchText++] = '\n';
|
||||
fAfterReturn = fTrue;
|
||||
}
|
||||
else if (ch == chEop)
|
||||
{
|
||||
if (!fAfterReturn)
|
||||
{
|
||||
lpch[cchText++] = '\r';
|
||||
lpch[cchText++] = '\n';
|
||||
}
|
||||
fAfterReturn = fFalse;
|
||||
}
|
||||
else
|
||||
{
|
||||
fAfterReturn = fFalse;
|
||||
if (ch == chTable || ch == chTab)
|
||||
lpch[cchText++] = '\t';
|
||||
else if (ch == chSect || ch == chColumnBreak)
|
||||
lpch[cchText++] = '\f';
|
||||
else if (ch >= chSpace || ch == chNonReqHyphen)
|
||||
lpch[cchText++] = (char)ch;
|
||||
}
|
||||
}
|
||||
cp += cch;
|
||||
}
|
||||
lpch[cchText] = '\0';
|
||||
result = OpusExportTextToPdfDialog(vhwndApp, lpch, cchText);
|
||||
GlobalUnlock(hText);
|
||||
GlobalFree(hText);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* 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. */
|
||||
|
||||
+12
-1
@@ -89,6 +89,7 @@ DEBUGASSERTSZ /* WIN - bogus macro for assert string */
|
||||
#ifdef OPUS_X64
|
||||
extern int OpusWin95SaveAliasMatches();
|
||||
extern int OpusFinishWin95SaveAlias();
|
||||
extern int OpusWin95SaveAliasRequiresRtf();
|
||||
#define FWin95SaveAliasMatches(st) OpusWin95SaveAliasMatches(st)
|
||||
#else
|
||||
#define FWin95SaveAliasMatches(st) fFalse
|
||||
@@ -661,7 +662,16 @@ LSaveAs:
|
||||
|
||||
/* perform the actual save */
|
||||
PdodDoc(doc)->dop.fLockAtn = (*hcab)->fLockAnnot;
|
||||
if (FSaveFile(stFile, doc, DffISaveFmt((*hcab)->iFmt),
|
||||
{
|
||||
int dffSave = DffISaveFmt((*hcab)->iFmt);
|
||||
#ifdef OPUS_X64
|
||||
if (OpusWin95SaveAliasRequiresRtf(stFile))
|
||||
{
|
||||
dffSave = dffSaveRTF;
|
||||
(*hcab)->fQuicksave = fFalse;
|
||||
}
|
||||
#endif
|
||||
if (FSaveFile(stFile, doc, dffSave,
|
||||
(*hcab)->fQuicksave, (*hcab)->fBackup, fTrue /* fReport */))
|
||||
{
|
||||
/* display number of characters written in prompt */
|
||||
@@ -682,6 +692,7 @@ LSaveAs:
|
||||
cmd = cmdError;
|
||||
StopProgressReport(hNil, pdcRestoreImmed);
|
||||
}
|
||||
}
|
||||
|
||||
vhpprSave = hNil;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,9 @@ 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);
|
||||
int OpusModernPathIsDocx(const char*);
|
||||
int OpusModernDocxToTextFile(const char*, const char*);
|
||||
int OpusModernRtfFileToDocx(const char*, const char*);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -151,7 +154,8 @@ std::string win95_alias_key(std::string path) {
|
||||
return path;
|
||||
}
|
||||
|
||||
bool make_win95_staging_path(std::string& path) {
|
||||
bool make_win95_staging_path(std::string& path,
|
||||
const char* desired_extension = ".DOC") {
|
||||
char module_path[32768]{};
|
||||
const DWORD module_length = GetModuleFileNameA(
|
||||
nullptr, module_path, static_cast<DWORD>(std::size(module_path)));
|
||||
@@ -174,9 +178,9 @@ bool make_win95_staging_path(std::string& path) {
|
||||
return false;
|
||||
}
|
||||
DeleteFileA(temporary);
|
||||
char* extension = std::strrchr(temporary, '.');
|
||||
if (extension != nullptr) {
|
||||
lstrcpyA(extension, ".DOC");
|
||||
char* existing_extension = std::strrchr(temporary, '.');
|
||||
if (existing_extension != nullptr) {
|
||||
lstrcpyA(existing_extension, desired_extension);
|
||||
}
|
||||
if (GetFileAttributesA(temporary) == INVALID_FILE_ATTRIBUTES &&
|
||||
std::strlen(temporary) < 120) {
|
||||
@@ -260,6 +264,7 @@ constexpr Tmc kTmcSaveDirectoryList = kTmcUserMin + 3;
|
||||
constexpr Tmc kTmcSaveDirectory = kTmcUserMin + 4;
|
||||
constexpr Tmc kTmcSaveFormatPrompt = kTmcUserMin + 5;
|
||||
constexpr Tmc kTmcSaveFormat = kTmcUserMin + 6;
|
||||
constexpr int kSaveFormatRtf = 6;
|
||||
constexpr Tmc kTmcSaveQuick = kTmcUserMin + 7;
|
||||
constexpr Tmc kTmcSaveBackup = kTmcUserMin + 8;
|
||||
constexpr Tmc kTmcSaveLockAnnotations = kTmcUserMin + 9;
|
||||
@@ -2146,12 +2151,13 @@ Tmc run_word95_common_file_dialog(DialogState& dialog) {
|
||||
}
|
||||
|
||||
static const char open_filter[] =
|
||||
"Word Documents (*.doc;*.dot)\0*.doc;*.dot\0"
|
||||
"Word Documents (*.doc;*.docx;*.dot)\0*.doc;*.docx;*.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"
|
||||
"Word 2007-2024 Documents (*.docx)\0*.docx\0"
|
||||
"Document Templates (*.dot)\0*.dot\0"
|
||||
"All Files (*.*)\0*.*\0\0";
|
||||
|
||||
@@ -2180,8 +2186,23 @@ Tmc run_word95_common_file_dialog(DialogState& dialog) {
|
||||
OFN_OVERWRITEPROMPT;
|
||||
}
|
||||
|
||||
const BOOL accepted = opening ?
|
||||
GetOpenFileNameA(&file_dialog) : GetSaveFileNameA(&file_dialog);
|
||||
char test_path[32768]{};
|
||||
const DWORD test_path_length = GetEnvironmentVariableA(
|
||||
"WORD1_TEST_FILE_DIALOG_PATH", test_path,
|
||||
static_cast<DWORD>(std::size(test_path)));
|
||||
BOOL accepted = FALSE;
|
||||
if (test_path_length > 0 && test_path_length < std::size(test_path)) {
|
||||
lstrcpynA(file_buffer.data(), test_path,
|
||||
static_cast<int>(file_buffer.size()));
|
||||
if (saving && OpusModernPathIsDocx(test_path)) {
|
||||
file_dialog.nFilterIndex = 2;
|
||||
}
|
||||
SetEnvironmentVariableA("WORD1_TEST_FILE_DIALOG_PATH", nullptr);
|
||||
accepted = TRUE;
|
||||
} else {
|
||||
accepted = opening ? GetOpenFileNameA(&file_dialog) :
|
||||
GetSaveFileNameA(&file_dialog);
|
||||
}
|
||||
if (!accepted) {
|
||||
if (CommDlgExtendedError() != 0) {
|
||||
MessageBoxA(owner,
|
||||
@@ -2196,18 +2217,41 @@ Tmc run_word95_common_file_dialog(DialogState& dialog) {
|
||||
break;
|
||||
}
|
||||
|
||||
const std::string selected_path = file_buffer.data();
|
||||
std::string selected_path = file_buffer.data();
|
||||
if (saving && file_dialog.nFilterIndex == 2) {
|
||||
const std::size_t selected_slash =
|
||||
selected_path.find_last_of("\\/");
|
||||
const std::size_t selected_dot = selected_path.find_last_of('.');
|
||||
if (selected_dot == std::string::npos ||
|
||||
(selected_slash != std::string::npos &&
|
||||
selected_dot < selected_slash)) {
|
||||
selected_path += ".docx";
|
||||
} else if (_stricmp(selected_path.c_str() + selected_dot,
|
||||
".doc") == 0) {
|
||||
selected_path.replace(selected_dot, std::string::npos,
|
||||
".docx");
|
||||
}
|
||||
}
|
||||
std::string legacy_path;
|
||||
if (!make_win95_staging_path(legacy_path)) {
|
||||
const bool docx = OpusModernPathIsDocx(selected_path.c_str()) != 0;
|
||||
if (!make_win95_staging_path(legacy_path,
|
||||
docx ? ".TXT" : ".DOC")) {
|
||||
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.",
|
||||
const bool staged_open = !opening ||
|
||||
(docx ?
|
||||
OpusModernDocxToTextFile(selected_path.c_str(),
|
||||
legacy_path.c_str()) != 0 :
|
||||
CopyFileA(selected_path.c_str(), legacy_path.c_str(), FALSE) != 0);
|
||||
if (!staged_open) {
|
||||
MessageBoxA(owner,
|
||||
docx ?
|
||||
"Word could not read this DOCX document." :
|
||||
"Word could not open the selected file.",
|
||||
"Open", MB_OK | MB_ICONEXCLAMATION);
|
||||
continue;
|
||||
}
|
||||
@@ -2240,6 +2284,11 @@ Tmc run_word95_common_file_dialog(DialogState& dialog) {
|
||||
SetWindowTextA(edit.window, edit.text.c_str());
|
||||
}
|
||||
sync_save_cab(dialog);
|
||||
if (auto* cab = save_cab(dialog);
|
||||
cab != nullptr && docx) {
|
||||
cab->format = kSaveFormatRtf;
|
||||
cab->quick_save = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (invoke_dialog_proc(dialog, kDlmTerm, kTmcOk)) {
|
||||
@@ -2374,10 +2423,13 @@ int OpusFinishWin95SaveAlias(const unsigned char* st_file,
|
||||
g_win95_save_alias.legacy_path.c_str()) == 0) {
|
||||
bool copied = success != 0;
|
||||
if (copied) {
|
||||
copied = CopyFileA(
|
||||
copied = OpusModernPathIsDocx(
|
||||
g_win95_save_alias.selected_path.c_str()) ?
|
||||
OpusModernRtfFileToDocx(
|
||||
g_win95_save_alias.legacy_path.c_str(),
|
||||
g_win95_save_alias.selected_path.c_str(),
|
||||
FALSE) != 0;
|
||||
g_win95_save_alias.selected_path.c_str()) != 0 :
|
||||
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());
|
||||
@@ -2396,7 +2448,28 @@ int OpusFinishWin95SaveAlias(const unsigned char* st_file,
|
||||
if (!success) {
|
||||
return false;
|
||||
}
|
||||
return CopyFileA(path.c_str(), saved->second.c_str(), FALSE) != 0;
|
||||
return OpusModernPathIsDocx(saved->second.c_str()) ?
|
||||
OpusModernRtfFileToDocx(path.c_str(), saved->second.c_str()) != 0 :
|
||||
CopyFileA(path.c_str(), saved->second.c_str(), FALSE) != 0;
|
||||
}
|
||||
|
||||
int OpusWin95SaveAliasRequiresRtf(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 OpusModernPathIsDocx(g_win95_save_alias.selected_path.c_str());
|
||||
const auto saved = g_win95_saved_aliases.find(win95_alias_key(path));
|
||||
return saved != g_win95_saved_aliases.end() &&
|
||||
OpusModernPathIsDocx(saved->second.c_str());
|
||||
}
|
||||
|
||||
int OpusWin95OpenAliasIsDocx(const unsigned char* st_file) {
|
||||
const std::string path = counted_path(st_file);
|
||||
if (path.empty()) return false;
|
||||
const auto saved = g_win95_saved_aliases.find(win95_alias_key(path));
|
||||
return saved != g_win95_saved_aliases.end() &&
|
||||
OpusModernPathIsDocx(saved->second.c_str());
|
||||
}
|
||||
|
||||
struct OpusSdsCompat {
|
||||
|
||||
@@ -35,6 +35,7 @@ extern "C" int OpusGetWin95HorizontalPageMargins(
|
||||
extern "C" int OpusAdjustWin95HorizontalMargin(
|
||||
HWND ruler, int left_margin, int delta_pixels);
|
||||
extern "C" void OpusDrawWin95HorizontalRuler(HWND ruler);
|
||||
extern "C" int OpusExportCurrentDocumentPdf(void);
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -54,6 +55,7 @@ constexpr UINT kComboSize = 0x7503;
|
||||
constexpr UINT kComboZoom = 0x7504;
|
||||
constexpr UINT kCmdToggleStandardToolbar = 0x7101;
|
||||
constexpr UINT kCmdToggleFormattingToolbar = 0x7102;
|
||||
constexpr UINT kCmdExportPdf = 0x7103;
|
||||
constexpr UINT kCmdTextColorBase = 0x7200;
|
||||
constexpr UINT_PTR kSyncTimer = 0x951;
|
||||
constexpr wchar_t kOriginalPaneProcProperty[] = L"OpusWord95OriginalPaneProc";
|
||||
@@ -326,6 +328,17 @@ void configure_word95_menus(HWND window) {
|
||||
reinterpret_cast<UINT_PTR>(format), L"F&ormat");
|
||||
}
|
||||
}
|
||||
HMENU file_menu = file_index >= 0 ? GetSubMenu(root, file_index) : nullptr;
|
||||
if (file_menu != nullptr &&
|
||||
GetMenuState(file_menu, kCmdExportPdf, MF_BYCOMMAND) ==
|
||||
static_cast<UINT>(-1)) {
|
||||
int exit_position = find_named_in(file_menu, L"Exit");
|
||||
if (exit_position < 0) exit_position = GetMenuItemCount(file_menu);
|
||||
InsertMenuW(file_menu, exit_position, MF_BYPOSITION | MF_SEPARATOR,
|
||||
0, nullptr);
|
||||
InsertMenuW(file_menu, exit_position, MF_BYPOSITION | MF_STRING,
|
||||
kCmdExportPdf, L"E&xport as PDF...");
|
||||
}
|
||||
const int utilities_index = find_named(L"Utilities");
|
||||
if (utilities_index >= 0) {
|
||||
HMENU tools = GetSubMenu(root, utilities_index);
|
||||
@@ -2504,6 +2517,10 @@ LRESULT CALLBACK app_window_proc(HWND app, UINT message,
|
||||
show_text_color_palette(app, toolbar);
|
||||
return 0;
|
||||
}
|
||||
if (command == kCmdExportPdf) {
|
||||
OpusExportCurrentDocumentPdf();
|
||||
return 0;
|
||||
}
|
||||
if (command == kCmdToggleStandardToolbar) {
|
||||
toggle_toolbar_row(app, toolbar, true);
|
||||
return 0;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <windows.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <cwchar>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
@@ -12,10 +13,12 @@ namespace {
|
||||
|
||||
constexpr UINT kWmCommand = 0x0111;
|
||||
constexpr WPARAM kFileNew = 1813;
|
||||
constexpr WPARAM kFileOpen = 1843;
|
||||
constexpr WPARAM kFileSaveAs = 1897;
|
||||
constexpr WPARAM kFileExit = 2095;
|
||||
constexpr WPARAM kHelpAbout = 182;
|
||||
constexpr WPARAM kParaCenter = 1355;
|
||||
constexpr WPARAM kExportPdf = 0x7103;
|
||||
constexpr UINT kWmOpusX64QuerySelection = WM_APP + 0x351;
|
||||
constexpr int kKcControl = 0x100;
|
||||
constexpr LRESULT kEditUndo = 2229;
|
||||
@@ -100,6 +103,26 @@ HWND find_descendant_by_class(const HWND parent,
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
HWND find_visible_descendant_by_class(const HWND parent,
|
||||
const wchar_t* expected_class) {
|
||||
for (HWND child = GetWindow(parent, GW_CHILD); child != nullptr;
|
||||
child = GetWindow(child, GW_HWNDNEXT)) {
|
||||
wchar_t class_name[128] = {};
|
||||
if (GetClassNameW(child, class_name,
|
||||
static_cast<int>(std::size(class_name))) != 0 &&
|
||||
_wcsicmp(class_name, expected_class) == 0 &&
|
||||
IsWindowVisible(child)) {
|
||||
return child;
|
||||
}
|
||||
if (const HWND descendant =
|
||||
find_visible_descendant_by_class(child, expected_class);
|
||||
descendant != nullptr) {
|
||||
return descendant;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void collect_descendants_by_class(const HWND parent,
|
||||
const wchar_t* expected_class,
|
||||
std::vector<HWND>& matches) {
|
||||
@@ -568,11 +591,12 @@ int fail(PROCESS_INFORMATION& process, const int code,
|
||||
int wmain(const int argument_count, wchar_t** arguments) {
|
||||
SetProcessDpiAwarenessContext(
|
||||
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
|
||||
if (argument_count < 2 || argument_count > 3) {
|
||||
if (argument_count < 2 || argument_count > 4) {
|
||||
std::cerr <<
|
||||
"usage: opus_word1_ui_test WORD1.exe "
|
||||
"[--typing|--interaction|--selection|--caret|--formatting|--color|"
|
||||
"--font-typing|--clipboard|--about|--save-as]\n";
|
||||
"--font-typing|--clipboard|--about|--save-as|--pdf-export|"
|
||||
"--docx-open FILE]\n";
|
||||
return 1;
|
||||
}
|
||||
const bool typing_mode =
|
||||
@@ -604,23 +628,78 @@ int wmain(const int argument_count, wchar_t** arguments) {
|
||||
const bool save_as_mode =
|
||||
argument_count == 3 &&
|
||||
std::wcscmp(arguments[2], L"--save-as") == 0;
|
||||
const bool pdf_export_mode =
|
||||
argument_count == 3 &&
|
||||
std::wcscmp(arguments[2], L"--pdf-export") == 0;
|
||||
const bool docx_open_mode =
|
||||
argument_count == 4 &&
|
||||
std::wcscmp(arguments[2], L"--docx-open") == 0;
|
||||
if (argument_count == 3 && !typing_mode && !interaction_mode &&
|
||||
!selection_mode && !caret_mode && !formatting_mode && !color_mode &&
|
||||
!font_typing_mode && !clipboard_mode && !about_mode &&
|
||||
!save_as_mode) {
|
||||
!save_as_mode && !pdf_export_mode) {
|
||||
std::cerr << "unknown test mode\n";
|
||||
return 1;
|
||||
}
|
||||
if (argument_count == 4 && !docx_open_mode) {
|
||||
std::cerr << "unknown test mode\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string docx_path;
|
||||
std::string pdf_path;
|
||||
if (docx_open_mode) {
|
||||
const int path_size = WideCharToMultiByte(
|
||||
CP_ACP, 0, arguments[3], -1, nullptr, 0, nullptr, nullptr);
|
||||
if (path_size <= 1) {
|
||||
std::cerr << "DOCX open test received an invalid path\n";
|
||||
return 83;
|
||||
}
|
||||
docx_path.resize(static_cast<std::size_t>(path_size));
|
||||
WideCharToMultiByte(CP_ACP, 0, arguments[3], -1, docx_path.data(),
|
||||
path_size, nullptr, nullptr);
|
||||
docx_path.pop_back();
|
||||
if (GetFileAttributesA(docx_path.c_str()) == INVALID_FILE_ATTRIBUTES) {
|
||||
std::cerr << "DOCX open test file does not exist\n";
|
||||
return 83;
|
||||
}
|
||||
}
|
||||
if (docx_open_mode) {
|
||||
SetEnvironmentVariableA("WORD1_TEST_FILE_DIALOG_PATH",
|
||||
docx_path.c_str());
|
||||
}
|
||||
if (pdf_export_mode) {
|
||||
char temporary_directory[MAX_PATH]{};
|
||||
char temporary_seed[MAX_PATH]{};
|
||||
if (GetTempPathA(static_cast<DWORD>(std::size(temporary_directory)),
|
||||
temporary_directory) == 0 ||
|
||||
GetTempFileNameA(temporary_directory, "OWP", 0,
|
||||
temporary_seed) == 0) {
|
||||
std::cerr << "PDF export test could not reserve a path\n";
|
||||
return 85;
|
||||
}
|
||||
DeleteFileA(temporary_seed);
|
||||
pdf_path = std::string(temporary_seed) + ".pdf";
|
||||
SetEnvironmentVariableA("WORD1_TEST_PDF_PATH", pdf_path.c_str());
|
||||
}
|
||||
|
||||
std::wstring command_line = L"\"" + std::wstring(arguments[1]) + L"\"";
|
||||
STARTUPINFOW startup{};
|
||||
startup.cb = sizeof(startup);
|
||||
PROCESS_INFORMATION process{};
|
||||
LPWCH environment = GetEnvironmentStringsW();
|
||||
if (!CreateProcessW(arguments[1], command_line.data(), nullptr, nullptr,
|
||||
FALSE, 0, nullptr, nullptr, &startup, &process)) {
|
||||
FALSE, CREATE_UNICODE_ENVIRONMENT, environment,
|
||||
nullptr, &startup, &process)) {
|
||||
if (environment != nullptr) FreeEnvironmentStringsW(environment);
|
||||
SetEnvironmentVariableA("WORD1_TEST_FILE_DIALOG_PATH", nullptr);
|
||||
SetEnvironmentVariableA("WORD1_TEST_PDF_PATH", nullptr);
|
||||
std::cerr << "CreateProcessW failed: " << GetLastError() << '\n';
|
||||
return 2;
|
||||
}
|
||||
if (environment != nullptr) FreeEnvironmentStringsW(environment);
|
||||
SetEnvironmentVariableA("WORD1_TEST_FILE_DIALOG_PATH", nullptr);
|
||||
SetEnvironmentVariableA("WORD1_TEST_PDF_PATH", nullptr);
|
||||
|
||||
const HWND main_window = wait_for_window(
|
||||
process.hProcess, process.dwProcessId, nullptr,
|
||||
@@ -628,13 +707,75 @@ int wmain(const int argument_count, wchar_t** arguments) {
|
||||
if (main_window == nullptr) {
|
||||
return fail(process, 3, "WORD1 main window did not appear");
|
||||
}
|
||||
if (pdf_export_mode) {
|
||||
const HWND pane = find_descendant_by_class(main_window, L"OpusWwd");
|
||||
DWORD ignored_process_id = 0;
|
||||
const DWORD thread_id =
|
||||
GetWindowThreadProcessId(main_window, &ignored_process_id);
|
||||
const LRESULT initial_length = pane != nullptr ?
|
||||
SendMessageW(pane, kWmOpusX64QuerySelection, 41, 0) : 0;
|
||||
const bool focused = pane != nullptr &&
|
||||
(make_foreground_and_focus(main_window, pane, thread_id) ||
|
||||
wait_for_focus(process.hProcess, thread_id, pane, 500));
|
||||
bool typed = focused;
|
||||
for (const wchar_t character : std::wstring(L"pdf export text")) {
|
||||
typed = typed && post_keyboard_character(pane, character);
|
||||
}
|
||||
Sleep(750);
|
||||
const bool committed = typed &&
|
||||
SendMessageW(pane, kWmOpusX64QuerySelection, 41, 0) >
|
||||
initial_length;
|
||||
if (!committed ||
|
||||
!PostMessageW(main_window, kWmCommand, kExportPdf, 0)) {
|
||||
std::cerr << "PDF preparation: focused=" << focused
|
||||
<< " typed=" << typed << " committed=" << committed
|
||||
<< " length=" << initial_length << "->"
|
||||
<< (pane != nullptr ? SendMessageW(
|
||||
pane, kWmOpusX64QuerySelection, 41, 0) : 0)
|
||||
<< '\n';
|
||||
DeleteFileA(pdf_path.c_str());
|
||||
return fail(process, 85,
|
||||
"PDF export test could not prepare its document");
|
||||
}
|
||||
const ULONGLONG deadline = GetTickCount64() + 15000;
|
||||
bool pdf_written = false;
|
||||
while (!pdf_written && GetTickCount64() < deadline &&
|
||||
WaitForSingleObject(process.hProcess, 0) != WAIT_OBJECT_0) {
|
||||
HANDLE pdf = CreateFileA(pdf_path.c_str(), GENERIC_READ,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
nullptr, OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (pdf != INVALID_HANDLE_VALUE) {
|
||||
char header[5]{};
|
||||
DWORD read = 0;
|
||||
pdf_written = ReadFile(pdf, header, sizeof(header), &read,
|
||||
nullptr) && read == sizeof(header) &&
|
||||
std::memcmp(header, "%PDF-", sizeof(header)) == 0;
|
||||
CloseHandle(pdf);
|
||||
}
|
||||
Sleep(100);
|
||||
}
|
||||
DWORD exit_code = STILL_ACTIVE;
|
||||
GetExitCodeProcess(process.hProcess, &exit_code);
|
||||
TerminateProcess(process.hProcess, 0);
|
||||
WaitForSingleObject(process.hProcess, 2000);
|
||||
CloseHandle(process.hThread);
|
||||
CloseHandle(process.hProcess);
|
||||
DeleteFileA(pdf_path.c_str());
|
||||
if (!pdf_written) {
|
||||
std::cerr << "PDF export failed or crashed: exit=0x" << std::hex
|
||||
<< exit_code << std::dec << '\n';
|
||||
return 86;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (save_as_mode) {
|
||||
Sleep(1000);
|
||||
if (!PostMessageW(main_window, kWmCommand, kFileSaveAs, 0)) {
|
||||
return fail(process, 69, "could not send File Save As");
|
||||
}
|
||||
const HWND save_as_dialog = wait_for_window(
|
||||
process.hProcess, process.dwProcessId, L"OpusSdmDialog",
|
||||
process.hProcess, process.dwProcessId, L"#32770",
|
||||
L"Save As", 5000);
|
||||
if (save_as_dialog == nullptr) {
|
||||
std::cerr << "Save As stage="
|
||||
@@ -642,17 +783,32 @@ int wmain(const int argument_count, wchar_t** arguments) {
|
||||
main_window, "OpusX64SaveAsStage"))
|
||||
<< '\n';
|
||||
log_process_windows(process.dwProcessId);
|
||||
return fail(process, 70, "File Save As dialog did not appear");
|
||||
return fail(process, 70,
|
||||
"Windows 95 File Save As dialog did not appear");
|
||||
}
|
||||
if (!control_has_class(save_as_dialog, 2, L"Button") ||
|
||||
!window_is_responsive(process.hProcess, save_as_dialog)) {
|
||||
return fail(process, 71,
|
||||
"File Save As dialog did not finish initializing");
|
||||
}
|
||||
if (!PostMessageW(save_as_dialog, kWmCommand, 2, 0) ||
|
||||
!wait_for_window_to_close(process.hProcess, process.dwProcessId,
|
||||
L"OpusSdmDialog", 5000) ||
|
||||
!window_is_responsive(process.hProcess, main_window)) {
|
||||
const bool cancel_posted =
|
||||
PostMessageW(save_as_dialog, kWmCommand, 2, 0) != FALSE;
|
||||
const bool dialog_closed = cancel_posted &&
|
||||
wait_for_window_to_close(process.hProcess, process.dwProcessId,
|
||||
L"#32770", 5000) &&
|
||||
wait_for_window_to_close(process.hProcess, process.dwProcessId,
|
||||
L"OpusSdmDialog", 5000);
|
||||
const bool app_responsive = dialog_closed &&
|
||||
window_is_responsive(process.hProcess, main_window);
|
||||
if (!cancel_posted || !dialog_closed || !app_responsive) {
|
||||
DWORD exit_code = STILL_ACTIVE;
|
||||
GetExitCodeProcess(process.hProcess, &exit_code);
|
||||
std::cerr << "Save As cancel state: posted=" << cancel_posted
|
||||
<< " closed=" << dialog_closed
|
||||
<< " responsive=" << app_responsive
|
||||
<< " exit=0x" << std::hex << exit_code << std::dec
|
||||
<< '\n';
|
||||
log_process_windows(process.dwProcessId);
|
||||
return fail(process, 72,
|
||||
"File Save As dialog did not cancel cleanly");
|
||||
}
|
||||
@@ -662,6 +818,45 @@ int wmain(const int argument_count, wchar_t** arguments) {
|
||||
CloseHandle(process.hProcess);
|
||||
return 0;
|
||||
}
|
||||
if (docx_open_mode) {
|
||||
HWND pane = find_descendant_by_class(main_window, L"OpusWwd");
|
||||
if (pane == nullptr ||
|
||||
!PostMessageW(main_window, kWmCommand, kFileOpen, 0)) {
|
||||
return fail(process, 78, "DOCX open test could not open its document");
|
||||
}
|
||||
const ULONGLONG open_deadline = GetTickCount64() + 10000;
|
||||
wchar_t document_caption[512]{};
|
||||
do {
|
||||
GetWindowTextW(main_window, document_caption,
|
||||
static_cast<int>(std::size(document_caption)));
|
||||
if (std::wcsstr(document_caption, L"Document1") == nullptr) {
|
||||
break;
|
||||
}
|
||||
Sleep(100);
|
||||
} while (GetTickCount64() < open_deadline &&
|
||||
WaitForSingleObject(process.hProcess, 0) != WAIT_OBJECT_0);
|
||||
// The legacy open command updates the caption before it has finished
|
||||
// activating the replacement MDI child and document pane.
|
||||
Sleep(750);
|
||||
pane = find_visible_descendant_by_class(main_window, L"OpusWwd");
|
||||
const bool reopened =
|
||||
std::wcsstr(document_caption, L"Document1") == nullptr;
|
||||
const LRESULT imported_length = pane != nullptr ?
|
||||
SendMessageW(pane, kWmOpusX64QuerySelection, 41, 0) : 0;
|
||||
const LRESULT displayed_lines = pane != nullptr ?
|
||||
SendMessageW(pane, kWmOpusX64QuerySelection, 30, 0) : 0;
|
||||
TerminateProcess(process.hProcess, 0);
|
||||
WaitForSingleObject(process.hProcess, 2000);
|
||||
CloseHandle(process.hThread);
|
||||
CloseHandle(process.hProcess);
|
||||
if (!reopened || pane == nullptr || imported_length < 100 ||
|
||||
displayed_lines < 1) {
|
||||
std::cerr << "DOCX open state: length=" << imported_length
|
||||
<< " displayedLines=" << displayed_lines << '\n';
|
||||
return 84;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (about_mode) {
|
||||
Sleep(1000);
|
||||
if (!PostMessageW(main_window, kWmCommand, kHelpAbout, 0)) {
|
||||
|
||||
Reference in New Issue
Block a user