OSDN Git Service

Set the exit status of the process to 0 if the comparison result is identical, 1...
[winmerge-jp/winmerge-jp.git] / Src / Merge.cpp
1 /////////////////////////////////////////////////////////////////////////////
2 //    WinMerge:  an interactive diff/merge utility
3 //    Copyright (C) 1997-2000  Thingamahoochie Software
4 //    Author: Dean Grimm
5 //    SPDX-License-Identifier: GPL-2.0-or-later
6 /////////////////////////////////////////////////////////////////////////////
7 /**
8  * @file  Merge.cpp
9  *
10  * @brief Defines the class behaviors for the application.
11  *
12  */
13
14 #include "stdafx.h"
15 #include "Merge.h"
16 #include "Constants.h"
17 #include "UnicodeString.h"
18 #include "unicoder.h"
19 #include "Environment.h"
20 #include "OptionsMgr.h"
21 #include "OptionsInit.h"
22 #include "RegOptionsMgr.h"
23 #include "IniOptionsMgr.h"
24 #include "OpenDoc.h"
25 #include "OpenFrm.h"
26 #include "OpenView.h"
27 #include "HexMergeDoc.h"
28 #include "HexMergeFrm.h"
29 #include "HexMergeView.h"
30 #include "AboutDlg.h"
31 #include "MainFrm.h"
32 #include "MergeEditFrm.h"
33 #include "DirFrame.h"
34 #include "MergeDoc.h"
35 #include "DirDoc.h"
36 #include "DirView.h"
37 #include "PropBackups.h"
38 #include "FileOrFolderSelect.h"
39 #include "FileFilterHelper.h"
40 #include "LineFiltersList.h"
41 #include "SubstitutionFiltersList.h"
42 #include "SyntaxColors.h"
43 #include "CCrystalTextMarkers.h"
44 #include "OptionsSyntaxColors.h"
45 #include "Plugins.h"
46 #include "ProjectFile.h"
47 #include "MergeEditSplitterView.h"
48 #include "LanguageSelect.h"
49 #include "OptionsDef.h"
50 #include "MergeCmdLineInfo.h"
51 #include "ConflictFileParser.h"
52 #include "JumpList.h"
53 #include "stringdiffs.h"
54 #include "TFile.h"
55 #include "paths.h"
56 #include "Shell.h"
57 #include "CompareStats.h"
58 #include "TestMain.h"
59 #include "charsets.h" // For shutdown cleanup
60
61 #ifdef _DEBUG
62 #define new DEBUG_NEW
63 #endif
64
65 /** @brief Location for command line help to open. */
66 static const TCHAR CommandLineHelpLocation[] = _T("::/htmlhelp/Command_line.html");
67
68 /** @brief Backup file extension. */
69 static const TCHAR BACKUP_FILE_EXT[] = _T("bak");
70
71 /////////////////////////////////////////////////////////////////////////////
72 // CMergeApp
73
74 BEGIN_MESSAGE_MAP(CMergeApp, CWinApp)
75         //{{AFX_MSG_MAP(CMergeApp)
76         ON_COMMAND(ID_APP_ABOUT, OnAppAbout)
77         ON_COMMAND(ID_HELP, OnHelp)
78         ON_COMMAND_EX_RANGE(ID_FILE_PROJECT_MRU_FIRST, ID_FILE_PROJECT_MRU_LAST, OnOpenRecentFile)
79         ON_UPDATE_COMMAND_UI(ID_FILE_PROJECT_MRU_FIRST, CWinApp::OnUpdateRecentFileMenu)
80         ON_COMMAND(ID_FILE_MERGINGMODE, OnMergingMode)
81         ON_UPDATE_COMMAND_UI(ID_FILE_MERGINGMODE, OnUpdateMergingMode)
82         ON_UPDATE_COMMAND_UI(ID_STATUS_MERGINGMODE, OnUpdateMergingStatus)
83         ON_COMMAND(ID_FILE_PRINT_SETUP, CWinApp::OnFilePrintSetup)
84         //}}AFX_MSG_MAP
85         // Standard file based document commands
86         //ON_COMMAND(ID_FILE_NEW, CWinApp::OnFileNew)
87         //ON_COMMAND(ID_FILE_OPEN, CWinApp::OnFileOpen)
88         // Standard print setup command
89 END_MESSAGE_MAP()
90
91 /////////////////////////////////////////////////////////////////////////////
92 // CMergeApp construction
93
94 CMergeApp::CMergeApp() :
95   m_bNeedIdleTimer(false)
96 , m_pOpenTemplate(nullptr)
97 , m_pDiffTemplate(nullptr)
98 , m_pHexMergeTemplate(nullptr)
99 , m_pDirTemplate(nullptr)
100 , m_mainThreadScripts(nullptr)
101 , m_nLastCompareResult(-1)
102 , m_bNonInteractive(false)
103 , m_pOptions(nullptr)
104 , m_pGlobalFileFilter(nullptr)
105 , m_nActiveOperations(0)
106 , m_pLangDlg(new CLanguageSelect())
107 , m_bEscShutdown(false)
108 , m_bExitIfNoDiff(MergeCmdLineInfo::ExitNoDiff::Disabled)
109 , m_pLineFilters(new LineFiltersList())
110 , m_pSubstitutionFiltersList(new SubstitutionFiltersList())
111 , m_pSyntaxColors(new SyntaxColors())
112 , m_pMarkers(new CCrystalTextMarkers())
113 , m_bMergingMode(false)
114 {
115         // add construction code here,
116         // Place all significant initialization in InitInstance
117 }
118
119 /**
120  * @brief Chose which options manager should be initialized.
121  * @return IniOptionsMgr if initial config file exists,
122  *   CRegOptionsMgr otherwise.
123  */
124 static COptionsMgr *CreateOptionManager(const MergeCmdLineInfo& cmdInfo)
125 {
126         String iniFilePath = cmdInfo.m_sIniFilepath;
127         if (!iniFilePath.empty())
128         {
129                 iniFilePath = paths::GetLongPath(iniFilePath);
130                 if (paths::CreateIfNeeded(paths::GetParentPath(iniFilePath)))
131                         return new CIniOptionsMgr(iniFilePath);
132         }
133         iniFilePath = paths::ConcatPath(env::GetProgPath(), _T("winmerge.ini"));
134         if (paths::DoesPathExist(iniFilePath) == paths::IS_EXISTING_FILE)
135                 return new CIniOptionsMgr(iniFilePath);
136         return new CRegOptionsMgr();
137 }
138
139 static HANDLE CreateMutexHandle()
140 {
141         // Create exclusion mutex name
142         TCHAR szDesktopName[MAX_PATH] = _T("Win9xDesktop");
143         DWORD dwLengthNeeded;
144         GetUserObjectInformation(GetThreadDesktop(GetCurrentThreadId()), UOI_NAME,
145                 szDesktopName, sizeof(szDesktopName), &dwLengthNeeded);
146         TCHAR szMutexName[MAX_PATH + 40];
147         // Combine window class name and desktop name to form a unique mutex name.
148         // As the window class name is decorated to distinguish between ANSI and
149         // UNICODE build, so will be the mutex name.
150         wsprintf(szMutexName, _T("%s-%s"), CMainFrame::szClassName, szDesktopName);
151         return CreateMutex(nullptr, FALSE, szMutexName);
152 }
153
154 static HWND ActivatePreviousInstanceAndSendCommandline(LPTSTR cmdLine)
155 {
156         HWND hWnd = FindWindow(CMainFrame::szClassName, nullptr);
157         if (hWnd == nullptr)
158                 return nullptr;
159         if (IsIconic(hWnd))
160                 ShowWindow(hWnd, SW_RESTORE);
161         SetForegroundWindow(GetLastActivePopup(hWnd));
162         COPYDATASTRUCT data = { 0, (lstrlen(cmdLine) + 1) * sizeof(TCHAR), cmdLine };
163         if (!SendMessage(hWnd, WM_COPYDATA, NULL, (LPARAM)&data))
164                 return nullptr;
165         return hWnd;
166 }
167
168 static void WaitForExitPreviousInstance(HWND hWnd)
169 {
170         DWORD dwProcessId = 0;
171         GetWindowThreadProcessId(hWnd, &dwProcessId);
172         HANDLE hProcess = OpenProcess(SYNCHRONIZE, FALSE, dwProcessId);
173         if (hProcess)
174                 WaitForSingleObject(hProcess, INFINITE);
175 }
176
177 static int ConvertLastCompareResultToExitCode(int nLastCompareResult)
178 {
179         if (nLastCompareResult == 0)
180                 return 0;
181         else if (nLastCompareResult > 0)
182                 return 1;
183         return 2;
184 }
185
186 CMergeApp::~CMergeApp()
187 {
188         strdiff::Close();
189 }
190 /////////////////////////////////////////////////////////////////////////////
191 // The one and only CMergeApp object
192
193 CMergeApp theApp;
194
195 /////////////////////////////////////////////////////////////////////////////
196 // CMergeApp initialization
197
198 /**
199  * @brief Initialize WinMerge application instance.
200  * @return TRUE if application initialization succeeds (and we'll run it),
201  *   FALSE if something failed and we exit the instance.
202  * @todo We could handle these failure situations more gratefully, i.e. show
203  *  at least some error message to the user..
204  */
205 BOOL CMergeApp::InitInstance()
206 {
207         // Prevents DLL hijacking
208         HMODULE hLibrary = GetModuleHandle(_T("kernel32.dll"));
209         BOOL (WINAPI *pfnSetSearchPathMode)(DWORD) = (BOOL (WINAPI *)(DWORD))GetProcAddress(hLibrary, "SetSearchPathMode");
210         if (pfnSetSearchPathMode != nullptr)
211                 pfnSetSearchPathMode(0x00000001L /*BASE_SEARCH_PATH_ENABLE_SAFE_SEARCHMODE*/ | 0x00008000L /*BASE_SEARCH_PATH_PERMANENT*/);
212         BOOL (WINAPI *pfnSetDllDirectoryA)(LPCSTR) = (BOOL (WINAPI *)(LPCSTR))GetProcAddress(hLibrary, "SetDllDirectoryA");
213         if (pfnSetDllDirectoryA != nullptr)
214                 pfnSetDllDirectoryA("");
215
216         JumpList::SetCurrentProcessExplicitAppUserModelID(L"Thingamahoochie.WinMerge");
217
218         InitCommonControls();    // initialize common control library
219         CWinApp::InitInstance(); // call parent class method
220
221         m_imageForInitializingGdiplus.Load((IStream*)nullptr); // initialize GDI+
222
223         // Runtime switch so programmer may set this in interactive debugger
224         int dbgmem = 0;
225         if (dbgmem)
226         {
227                 // get current setting
228                 int tmpFlag = _CrtSetDbgFlag( _CRTDBG_REPORT_FLAG );
229                 // Keep freed memory blocks in the heap's linked list and mark them as freed
230                 tmpFlag |= _CRTDBG_DELAY_FREE_MEM_DF;
231                 // Call _CrtCheckMemory at every allocation and deallocation request.
232                 // WARNING: This slows down WinMerge *A LOT*
233                 tmpFlag |= _CRTDBG_CHECK_ALWAYS_DF;
234                 // Set the new state for the flag
235                 _CrtSetDbgFlag( tmpFlag );
236         }
237
238         // CCrystalEdit Drag and Drop functionality needs AfxOleInit.
239         if(!AfxOleInit())
240         {
241                 TRACE(_T("AfxOleInitFailed. OLE functionality disabled"));
242         }
243
244         // Standard initialization
245         // If you are not using these features and wish to reduce the size
246         //  of your final executable, you should remove from the following
247         //  the specific initialization routines you do not need.
248
249         // Revoke the standard OLE Message Filter to avoid drawing frame while loading files.
250         COleMessageFilter* pOldFilter = AfxOleGetMessageFilter();
251         pOldFilter->Revoke();
252
253         // Load registry keys from WinMerge.reg if existing WinMerge.reg
254         env::LoadRegistryFromFile(paths::ConcatPath(env::GetProgPath(), _T("WinMerge.reg")));
255
256         // Parse command-line arguments.
257 #ifdef TEST_WINMERGE
258         MergeCmdLineInfo cmdInfo(_T(""));
259 #else
260         MergeCmdLineInfo cmdInfo(GetCommandLine());
261 #endif
262         m_pOptions.reset(CreateOptionManager(cmdInfo));
263         if (cmdInfo.m_bNoPrefs)
264                 m_pOptions->SetSerializing(false); // Turn off serializing to registry.
265
266         if (dynamic_cast<CRegOptionsMgr*>(m_pOptions.get()) != nullptr)
267                 Options::CopyHKLMValues();
268
269         Options::Init(m_pOptions.get()); // Implementation in OptionsInit.cpp
270         ApplyCommandLineConfigOptions(cmdInfo);
271         if (cmdInfo.m_sErrorMessages.size() > 0)
272         {
273                 if (AttachConsole(ATTACH_PARENT_PROCESS))
274                 {
275                         DWORD dwWritten;
276                         for (auto& msg : cmdInfo.m_sErrorMessages)
277                         {
278                                 String line = _T("WinMerge: ") + msg + _T("\n");
279                                 WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), line.c_str(), static_cast<DWORD>(line.length()), &dwWritten, nullptr);
280                         }
281                         FreeConsole();
282                 }
283         }
284
285         // Initialize temp folder
286         SetupTempPath();
287
288         // If paths were given to commandline we consider this being an invoke from
289         // commandline (from other application, shellextension etc).
290         bool bCommandLineInvoke = cmdInfo.m_Files.GetSize() > 0;
291
292         // WinMerge registry settings are stored under HKEY_CURRENT_USER/Software/Thingamahoochie
293         // This is the name of the company of the original author (Dean Grimm)
294         SetRegistryKey(_T("Thingamahoochie"));
295
296         int nSingleInstance = cmdInfo.m_nSingleInstance.has_value() ?
297                 *cmdInfo.m_nSingleInstance : GetOptionsMgr()->GetInt(OPT_SINGLE_INSTANCE);
298
299         HANDLE hMutex = CreateMutexHandle();
300         if (hMutex != nullptr)
301                 WaitForSingleObject(hMutex, INFINITE);
302         if (nSingleInstance != 0 && GetLastError() == ERROR_ALREADY_EXISTS)
303         {
304                 // Activate previous instance and send commandline to it
305                 HWND hWnd = ActivatePreviousInstanceAndSendCommandline(GetCommandLine());
306                 if (hWnd != nullptr)
307                 {
308                         ReleaseMutex(hMutex);
309                         CloseHandle(hMutex);
310                         if (nSingleInstance != 1)
311                                 WaitForExitPreviousInstance(hWnd);
312                         return FALSE;
313                 }
314         }
315
316         LoadStdProfileSettings(GetOptionsMgr()->GetInt(OPT_MRU_MAX));  // Load standard INI file options (including MRU)
317
318         charsets_init();
319         UpdateCodepageModule();
320
321         FileTransform::AutoUnpacking = GetOptionsMgr()->GetBool(OPT_PLUGINS_UNPACKER_MODE);
322         FileTransform::AutoPrediffing = GetOptionsMgr()->GetBool(OPT_PLUGINS_PREDIFFER_MODE);
323
324         NONCLIENTMETRICS ncm = { sizeof NONCLIENTMETRICS };
325         if (SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof NONCLIENTMETRICS, &ncm, 0))
326         {
327                 const int lfHeight = -MulDiv(9, CClientDC(CWnd::GetDesktopWindow()).GetDeviceCaps(LOGPIXELSY), 72);
328                 if (abs(ncm.lfMenuFont.lfHeight) > abs(lfHeight))
329                         ncm.lfMenuFont.lfHeight = lfHeight;
330                 if (wcscmp(ncm.lfMenuFont.lfFaceName, L"Meiryo") == 0 || wcscmp(ncm.lfMenuFont.lfFaceName, L"\U000030e1\U000030a4\U000030ea\U000030aa"/* "Meiryo" in Japanese */) == 0)
331                         wcscpy_s(ncm.lfMenuFont.lfFaceName, L"Meiryo UI");
332                 m_fontGUI.CreateFontIndirect(&ncm.lfMenuFont);
333         }
334
335         if (m_pSyntaxColors != nullptr)
336                 Options::SyntaxColors::Init(GetOptionsMgr(), m_pSyntaxColors.get());
337
338         if (m_pMarkers != nullptr)
339                 m_pMarkers->LoadFromRegistry();
340
341         CCrystalTextView::SetRenderingModeDefault(static_cast<CCrystalTextView::RENDERING_MODE>(GetOptionsMgr()->GetInt(OPT_RENDERING_MODE)));
342
343         if (m_pLineFilters != nullptr)
344                 m_pLineFilters->Initialize(GetOptionsMgr());
345
346         // If there are no filters loaded, and there is filter string in previous
347         // option string, import old filters to new place.
348         if (m_pLineFilters->GetCount() == 0)
349         {
350                 String oldFilter = theApp.GetProfileString(_T("Settings"), _T("RegExps"));
351                 if (!oldFilter.empty())
352                         m_pLineFilters->Import(oldFilter);
353         }
354
355         if (m_pSubstitutionFiltersList != nullptr)
356                 m_pSubstitutionFiltersList->Initialize(GetOptionsMgr());
357
358         // Check if filter folder is set, and create it if not
359         String pathMyFolders = GetOptionsMgr()->GetString(OPT_FILTER_USERPATH);
360         if (pathMyFolders.empty())
361         {
362                 // No filter path, set it to default and make sure it exists.
363                 pathMyFolders = GetOptionsMgr()->GetDefault<String>(OPT_FILTER_USERPATH);
364                 GetOptionsMgr()->SaveOption(OPT_FILTER_USERPATH, pathMyFolders);
365                 theApp.GetGlobalFileFilter()->SetUserFilterPath(pathMyFolders);
366         }
367         if (!paths::CreateIfNeeded(pathMyFolders))
368         {
369                 // Failed to create a folder, check it didn't already
370                 // exist.
371                 DWORD errCode = GetLastError();
372                 if (errCode != ERROR_ALREADY_EXISTS)
373                 {
374                         // Failed to create a folder for filters, fallback to
375                         // "My Documents"-folder. It is not worth the trouble to
376                         // bother user about this or user more clever solutions.
377                         GetOptionsMgr()->SaveOption(OPT_FILTER_USERPATH, env::GetMyDocuments());
378                 }
379         }
380
381         strdiff::Init(); // String diff init
382         strdiff::SetBreakChars(GetOptionsMgr()->GetString(OPT_BREAK_SEPARATORS).c_str());
383
384         m_bMergingMode = GetOptionsMgr()->GetBool(OPT_MERGE_MODE);
385
386         // Initialize i18n (multiple language) support
387
388         m_pLangDlg->InitializeLanguage((WORD)GetOptionsMgr()->GetInt(OPT_SELECTED_LANGUAGE));
389
390         m_mainThreadScripts = new CAssureScriptsForThread;
391
392         // Register the application's document templates.  Document templates
393         //  serve as the connection between documents, frame windows and views.
394
395         // Open view
396         m_pOpenTemplate = new CMultiDocTemplate(
397                 IDR_MAINFRAME,
398                 RUNTIME_CLASS(COpenDoc),
399                 RUNTIME_CLASS(COpenFrame), // custom MDI child frame
400                 RUNTIME_CLASS(COpenView));
401         AddDocTemplate(m_pOpenTemplate);
402
403         // Merge Edit view
404         m_pDiffTemplate = new CMultiDocTemplate(
405                 IDR_MERGEDOCTYPE,
406                 RUNTIME_CLASS(CMergeDoc),
407                 RUNTIME_CLASS(CMergeEditFrame), // custom MDI child frame
408                 RUNTIME_CLASS(CMergeEditSplitterView));
409         AddDocTemplate(m_pDiffTemplate);
410
411         // Merge Edit view
412         m_pHexMergeTemplate = new CMultiDocTemplate(
413                 IDR_MERGEDOCTYPE,
414                 RUNTIME_CLASS(CHexMergeDoc),
415                 RUNTIME_CLASS(CHexMergeFrame), // custom MDI child frame
416                 RUNTIME_CLASS(CHexMergeView));
417         AddDocTemplate(m_pHexMergeTemplate);
418
419         // Directory view
420         m_pDirTemplate = new CMultiDocTemplate(
421                 IDR_DIRDOCTYPE,
422                 RUNTIME_CLASS(CDirDoc),
423                 RUNTIME_CLASS(CDirFrame), // custom MDI child frame
424                 RUNTIME_CLASS(CDirView));
425         AddDocTemplate(m_pDirTemplate);
426
427         // create main MDI Frame window
428         CMainFrame* pMainFrame = new CMainFrame;
429         if (!pMainFrame->LoadFrame(IDR_MAINFRAME))
430         {
431                 if (hMutex != nullptr)
432                 {
433                         ReleaseMutex(hMutex);
434                         CloseHandle(hMutex);
435                 }
436                 return FALSE;
437         }
438         m_pMainWnd = pMainFrame;
439
440         // Init menus -- hMenuDefault is for MainFrame
441         pMainFrame->m_hMenuDefault = pMainFrame->NewDefaultMenu();
442
443         // Set the menu
444         // Note : for Windows98 compatibility, use FromHandle and not Attach/Detach
445         CMenu * pNewMenu = CMenu::FromHandle(pMainFrame->m_hMenuDefault);
446         pMainFrame->MDISetMenu(pNewMenu, nullptr);
447
448         // The main window has been initialized, so activate it.
449         pMainFrame->ActivateFrame(cmdInfo.m_nCmdShow);
450
451         // Since this function actually opens paths for compare it must be
452         // called after initializing CMainFrame!
453         bool bContinue = true;
454         if (!ParseArgsAndDoOpen(cmdInfo, pMainFrame) && bCommandLineInvoke)
455                 bContinue = false;
456
457         if (hMutex != nullptr)
458                 ReleaseMutex(hMutex);
459
460         // If user wants to cancel the compare, close WinMerge
461         if (!bContinue)
462         {
463                 pMainFrame->PostMessage(WM_CLOSE, 0, 0);
464         }
465
466 #ifdef TEST_WINMERGE
467         WinMergeTest::TestAll();
468 #endif
469
470         return bContinue;
471 }
472
473 static void OpenContributersFile(int&)
474 {
475         CMergeApp::OpenFileToExternalEditor(paths::ConcatPath(env::GetProgPath(), ContributorsPath));
476 }
477
478 static void OpenUrl(int&)
479 {
480         shell::Open(WinMergeURL);
481 }
482
483 // App command to run the dialog
484 void CMergeApp::OnAppAbout()
485 {
486         CAboutDlg aboutDlg;
487         aboutDlg.m_onclick_contributers += Poco::delegate(OpenContributersFile);
488         aboutDlg.m_onclick_url += Poco::delegate(OpenUrl);
489         aboutDlg.DoModal();
490         aboutDlg.m_onclick_contributers.clear();
491         aboutDlg.m_onclick_url.clear();
492 }
493
494 /////////////////////////////////////////////////////////////////////////////
495 // CMergeApp commands
496
497 /**
498  * @brief Called when application is about to exit.
499  * This functions is called when application is exiting, so this is
500  * good place to do cleanups.
501  * @return Application's exit value (returned from WinMain()).
502  */
503 int CMergeApp::ExitInstance()
504 {
505         charsets_cleanup();
506
507         //  Save registry keys if existing WinMerge.reg
508         env::SaveRegistryToFile(paths::ConcatPath(env::GetProgPath(), _T("WinMerge.reg")), RegDir);
509
510         // Remove tempfolder
511         const String temp = env::GetTemporaryPath();
512         ClearTempfolder(temp);
513
514         // Cleanup left over tempfiles from previous instances.
515         // Normally this should not need to do anything - but if for some reason
516         // WinMerge did not delete temp files this makes sure they are removed.
517         CleanupWMtemp();
518
519         delete m_mainThreadScripts;
520         CWinApp::ExitInstance();
521         
522 #ifndef _DEBUG
523         // There is a problem that OleUninitialize() in mfc/oleinit.cpp, which is called just before the process exits,
524         // hangs in rare cases.
525         // To deal with this problem, force the process to exit
526         // if the process does not exit within 2 seconds after the call to CMergeApp::ExitInstance().
527         _beginthreadex(0, 0,
528                 [](void*) -> unsigned int {
529                         Sleep(2000);
530                         ExitProcess(0);
531                 }, nullptr, 0, nullptr);
532 #endif
533
534         return ConvertLastCompareResultToExitCode(m_nLastCompareResult);
535 }
536
537 int CMergeApp::DoMessageBox(LPCTSTR lpszPrompt, UINT nType, UINT nIDPrompt)
538 {
539         // This is a convenient point for breakpointing !!!
540
541         // Create a handle to store the parent window of the message box.
542         CWnd* pParentWnd = CWnd::GetActiveWindow();
543
544         // Check whether an active window was retrieved successfully.
545         if (pParentWnd == nullptr)
546         {
547                 // Try to retrieve a handle to the last active popup.
548                 CWnd * mainwnd = GetMainWnd();
549                 if (mainwnd != nullptr)
550                         pParentWnd = mainwnd->GetLastActivePopup();
551         }
552
553         // Use our own message box implementation, which adds the
554         // do not show again checkbox, and implements it on subsequent calls
555         // (if caller set the style)
556
557         if (m_bNonInteractive)
558         {
559                 if (AttachConsole(ATTACH_PARENT_PROCESS))
560                 {
561                         DWORD dwWritten;
562                         String line = _T("WinMerge: ") + String(lpszPrompt) + _T("\n");
563                         WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), line.c_str(), static_cast<DWORD>(line.length()), &dwWritten, nullptr);
564                         FreeConsole();
565                 }
566                 return IDCANCEL;
567         }
568
569         // Create the message box dialog.
570         CMessageBoxDialog dlgMessage(pParentWnd, lpszPrompt, _T(""), nType | MB_RIGHT_ALIGN,
571                 nIDPrompt);
572
573         if (m_pMainWnd->IsIconic())
574                 m_pMainWnd->ShowWindow(SW_RESTORE);
575
576         // Display the message box dialog and return the result.
577         return static_cast<int>(dlgMessage.DoModal());
578 }
579
580 bool CMergeApp::IsReallyIdle() const
581 {
582         bool idle = true;
583         POSITION pos = m_pDirTemplate->GetFirstDocPosition();
584         while (pos != nullptr)
585         {
586                 CDirDoc *pDirDoc = static_cast<CDirDoc *>(m_pDirTemplate->GetNextDoc(pos));
587                 if (const CompareStats *pCompareStats = pDirDoc->GetCompareStats())
588                 {
589                         if (!pCompareStats->IsCompareDone() || pDirDoc->GetGeneratingReport())
590                                 idle = false;
591                 }
592         }
593     return idle;
594 }
595
596 BOOL CMergeApp::OnIdle(LONG lCount)
597 {
598         if (CWinApp::OnIdle(lCount))
599                 return TRUE;
600
601         // If anyone has requested notification when next idle occurs, send it
602         if (m_bNeedIdleTimer)
603         {
604                 m_bNeedIdleTimer = false;
605                 m_pMainWnd->SendMessageToDescendants(WM_TIMER, IDLE_TIMER, lCount, TRUE, FALSE);
606         }
607
608         if (m_bNonInteractive && IsReallyIdle())
609                 m_pMainWnd->PostMessage(WM_CLOSE, 0, 0);
610
611         if (typeid(*GetOptionsMgr()) == typeid(CRegOptionsMgr))
612         {
613                 static_cast<CRegOptionsMgr*>(GetOptionsMgr())->CloseKeys();
614         }
615
616         return FALSE;
617 }
618
619 /**
620  * @brief Load any known file filters.
621  *
622  * This function loads filter files from paths we know contain them.
623  * @note User's filter location may not be set yet.
624  */
625 void CMergeApp::InitializeFileFilters()
626 {
627         assert(m_pGlobalFileFilter != nullptr);
628         String filterPath = GetOptionsMgr()->GetString(OPT_FILTER_USERPATH);
629
630         if (!filterPath.empty())
631         {
632                 m_pGlobalFileFilter->SetUserFilterPath(filterPath);
633         }
634         m_pGlobalFileFilter->LoadAllFileFilters();
635 }
636
637 void CMergeApp::ApplyCommandLineConfigOptions(MergeCmdLineInfo& cmdInfo)
638 {
639         if (cmdInfo.m_bNoPrefs)
640                 m_pOptions->SetSerializing(false); // Turn off serializing to registry.
641
642         for (const auto& it : cmdInfo.m_Options)
643         {
644                 if (m_pOptions->Set(it.first, it.second) == COption::OPT_NOTFOUND)
645                 {
646                         String longname = m_pOptions->ExpandShortName(it.first);
647                         if (!longname.empty())
648                         {
649                                 m_pOptions->Set(longname, it.second);
650                         }
651                         else
652                         {
653                                 cmdInfo.m_sErrorMessages.push_back(strutils::format_string1(_T("Invalid key '%1' specified in /config option"), it.first));
654                         }
655                 }
656         }
657 }
658
659 /** @brief Read command line arguments and open files for comparison.
660  *
661  * The name of the function is a legacy code from the time that this function
662  * actually parsed the command line. Today the parsing is done using the
663  * MergeCmdLineInfo class.
664  * @param [in] cmdInfo Commandline parameters info.
665  * @param [in] pMainFrame Pointer to application main frame.
666  * @return `true` if we opened the compare, `false` if the compare was canceled.
667  */
668 bool CMergeApp::ParseArgsAndDoOpen(MergeCmdLineInfo& cmdInfo, CMainFrame* pMainFrame)
669 {
670         bool bCompared = false;
671         String strDesc[3];
672         std::unique_ptr<PackingInfo> infoUnpacker;
673         std::unique_ptr<PrediffingInfo> infoPrediffer;
674         unsigned nID = cmdInfo.m_nWindowType == MergeCmdLineInfo::AUTOMATIC ?
675                 0 : static_cast<unsigned>(cmdInfo.m_nWindowType) + ID_MERGE_COMPARE_TEXT - 1;
676
677         m_bNonInteractive = cmdInfo.m_bNonInteractive;
678
679         if (!cmdInfo.m_sUnpacker.empty())
680                 infoUnpacker.reset(new PackingInfo(cmdInfo.m_sUnpacker));
681
682         if (!cmdInfo.m_sPreDiffer.empty())
683                 infoPrediffer.reset(new PrediffingInfo(cmdInfo.m_sPreDiffer));
684
685         // Set the global file filter.
686         if (!cmdInfo.m_sFileFilter.empty())
687         {
688                 GetGlobalFileFilter()->SetFilter(cmdInfo.m_sFileFilter);
689         }
690
691         // Set codepage.
692         if (cmdInfo.m_nCodepage)
693         {
694                 UpdateDefaultCodepage(2,cmdInfo.m_nCodepage);
695         }
696
697         // Set compare method
698         if (cmdInfo.m_nCompMethod.has_value())
699                 GetOptionsMgr()->Set(OPT_CMP_METHOD, *cmdInfo.m_nCompMethod);
700
701         // Unless the user has requested to see WinMerge's usage open files for
702         // comparison.
703         if (cmdInfo.m_bShowUsage)
704         {
705                 ShowHelp(CommandLineHelpLocation);
706         }
707         else
708         {
709                 // Set the required information we need from the command line:
710
711                 m_bExitIfNoDiff = cmdInfo.m_bExitIfNoDiff;
712                 m_bEscShutdown = cmdInfo.m_bEscShutdown;
713
714                 m_strSaveAsPath = cmdInfo.m_sOutputpath;
715
716                 strDesc[0] = cmdInfo.m_sLeftDesc;
717                 if (cmdInfo.m_Files.GetSize() < 3)
718                 {
719                         strDesc[1] = cmdInfo.m_sRightDesc;
720                 }
721                 else
722                 {
723                         strDesc[1] = cmdInfo.m_sMiddleDesc;
724                         strDesc[2] = cmdInfo.m_sRightDesc;
725                 }
726
727                 std::unique_ptr<CMainFrame::OpenFileParams> pOpenParams;
728                 if (cmdInfo.m_nWindowType == MergeCmdLineInfo::TEXT)
729                         pOpenParams.reset(new CMainFrame::OpenTextFileParams());
730                 else if (cmdInfo.m_nWindowType == MergeCmdLineInfo::TABLE)
731                         pOpenParams.reset(new CMainFrame::OpenTableFileParams());
732                 else
733                         pOpenParams.reset(static_cast<CMainFrame::OpenTableFileParams *>(new CMainFrame::OpenAutoFileParams()));
734                 if (auto* pOpenTextFileParams = dynamic_cast<CMainFrame::OpenTextFileParams*>(pOpenParams.get()))
735                 {
736                         pOpenTextFileParams->m_line = cmdInfo.m_nLineIndex;
737                         pOpenTextFileParams->m_char = cmdInfo.m_nCharIndex;
738                         pOpenTextFileParams->m_fileExt = cmdInfo.m_sFileExt;
739                 }
740                 if (auto* pOpenTableFileParams = dynamic_cast<CMainFrame::OpenTableFileParams*>(pOpenParams.get()))
741                 {
742                         pOpenTableFileParams->m_tableDelimiter = cmdInfo.m_cTableDelimiter;
743                         pOpenTableFileParams->m_tableQuote = cmdInfo.m_cTableQuote;
744                         pOpenTableFileParams->m_tableAllowNewlinesInQuotes = cmdInfo.m_bTableAllowNewlinesInQuotes;
745                 }
746                 if (cmdInfo.m_Files.GetSize() > 2)
747                 {
748                         cmdInfo.m_dwLeftFlags |= FFILEOPEN_CMDLINE;
749                         cmdInfo.m_dwMiddleFlags |= FFILEOPEN_CMDLINE;
750                         cmdInfo.m_dwRightFlags |= FFILEOPEN_CMDLINE;
751                         DWORD dwFlags[3] = {cmdInfo.m_dwLeftFlags, cmdInfo.m_dwMiddleFlags, cmdInfo.m_dwRightFlags};
752                         bCompared = pMainFrame->DoFileOrFolderOpen(&cmdInfo.m_Files,
753                                 dwFlags, strDesc, cmdInfo.m_sReportFile, cmdInfo.m_bRecurse, nullptr,
754                                 infoUnpacker.get(), infoPrediffer.get(), nID, pOpenParams.get());
755                 }
756                 else if (cmdInfo.m_Files.GetSize() > 1)
757                 {
758                         DWORD dwFlags[3] = {cmdInfo.m_dwLeftFlags, cmdInfo.m_dwRightFlags, FFILEOPEN_NONE};
759                         bCompared = pMainFrame->DoFileOrFolderOpen(&cmdInfo.m_Files,
760                                 dwFlags, strDesc, cmdInfo.m_sReportFile, cmdInfo.m_bRecurse, nullptr,
761                                 infoUnpacker.get(), infoPrediffer.get(), nID, pOpenParams.get());
762                 }
763                 else if (cmdInfo.m_Files.GetSize() == 1)
764                 {
765                         String sFilepath = cmdInfo.m_Files[0];
766                         if (cmdInfo.m_bSelfCompare)
767                         {
768                                 strDesc[0] = cmdInfo.m_sLeftDesc;
769                                 strDesc[1] = cmdInfo.m_sRightDesc;
770                                 bCompared = pMainFrame->DoSelfCompare(nID, sFilepath, strDesc,
771                                         infoUnpacker.get(), infoPrediffer.get(), pOpenParams.get());
772                         }
773                         else if (IsProjectFile(sFilepath))
774                         {
775                                 bCompared = LoadAndOpenProjectFile(sFilepath);
776                         }
777                         else if (IsConflictFile(sFilepath))
778                         {
779                                 //For a conflict file, load the descriptions in their respective positions:  (they will be reordered as needed)
780                                 strDesc[0] = cmdInfo.m_sLeftDesc;
781                                 strDesc[1] = cmdInfo.m_sMiddleDesc;
782                                 strDesc[2] = cmdInfo.m_sRightDesc;
783                                 bCompared = pMainFrame->DoOpenConflict(sFilepath, strDesc);
784                         }
785                         else
786                         {
787                                 DWORD dwFlags[3] = {cmdInfo.m_dwLeftFlags, cmdInfo.m_dwRightFlags, FFILEOPEN_NONE};
788                                 bCompared = pMainFrame->DoFileOrFolderOpen(&cmdInfo.m_Files,
789                                         dwFlags, strDesc, cmdInfo.m_sReportFile, cmdInfo.m_bRecurse, nullptr,
790                                         infoUnpacker.get(), infoPrediffer.get(), nID, pOpenParams.get());
791                         }
792                 }
793                 else if (cmdInfo.m_Files.GetSize() == 0) // if there are no input args, we can check the display file dialog flag
794                 {
795                         if (!cmdInfo.m_bNewCompare)
796                         {
797                                 bool showFiles = m_pOptions->GetBool(OPT_SHOW_SELECT_FILES_AT_STARTUP);
798                                 if (showFiles)
799                                         pMainFrame->DoFileOrFolderOpen();
800                         }
801                         else
802                         {
803                                 bCompared = pMainFrame->DoFileNew(nID, 2, strDesc, infoPrediffer.get(), pOpenParams.get());
804                         }
805                 }
806         }
807         return bCompared;
808 }
809
810 void CMergeApp::UpdateDefaultCodepage(int cpDefaultMode, int cpCustomCodepage)
811 {
812         int wLangId;
813
814         switch (cpDefaultMode)
815         {
816                 case 0:
817                         ucr::setDefaultCodepage(GetACP());
818                         break;
819                 case 1:
820                         TCHAR buff[32];
821                         wLangId = GetLangId();
822                         if (GetLocaleInfo(wLangId, LOCALE_IDEFAULTANSICODEPAGE, buff, sizeof(buff)/sizeof(buff[0])))
823                                 ucr::setDefaultCodepage(_ttol(buff));
824                         else
825                                 ucr::setDefaultCodepage(GetACP());
826                         break;
827                 case 2:
828                         ucr::setDefaultCodepage(cpCustomCodepage);
829                         break;
830                 default:
831                         // no other valid option
832                         assert (false);
833                         ucr::setDefaultCodepage(GetACP());
834         }
835 }
836
837 /**
838  * @brief Send current option settings into codepage module
839  */
840 void CMergeApp::UpdateCodepageModule()
841 {
842         // Get current codepage settings from the options module
843         // and push them into the codepage module
844         UpdateDefaultCodepage(GetOptionsMgr()->GetInt(OPT_CP_DEFAULT_MODE), GetOptionsMgr()->GetInt(OPT_CP_DEFAULT_CUSTOM));
845 }
846
847 /** @brief Open help from mainframe when user presses F1*/
848 void CMergeApp::OnHelp()
849 {
850         ShowHelp();
851 }
852
853 /**
854  * @brief Open given file to external editor specified in options.
855  * @param [in] file Full path to file to open.
856  *
857  * Opens file to defined (in Options/system), Notepad by default,
858  * external editor. Path is decorated with quotation marks if needed
859  * (contains spaces). Also '$file' in editor path is replaced by
860  * filename to open.
861  * @param [in] file Full path to file to open.
862  * @param [in] nLineNumber Line number to go to.
863  */
864 void CMergeApp::OpenFileToExternalEditor(const String& file, int nLineNumber/* = 1*/)
865 {
866         String sCmd = env::ExpandEnvironmentVariables(GetOptionsMgr()->GetString(OPT_EXT_EDITOR_CMD));
867         String sFile(file);
868         strutils::replace(sCmd, _T("$linenum"), strutils::to_str(nLineNumber));
869
870         size_t nIndex = sCmd.find(_T("$file"));
871         if (nIndex != String::npos)
872         {
873                 sFile.insert(0, _T("\""));
874                 strutils::replace(sCmd, _T("$file"), sFile);
875                 nIndex = sCmd.find(' ', nIndex + sFile.length());
876                 if (nIndex != String::npos)
877                         sCmd.insert(nIndex, _T("\""));
878                 else
879                         sCmd += '"';
880         }
881         else
882         {
883                 sCmd += _T(" \"");
884                 sCmd += sFile;
885                 sCmd += _T("\"");
886         }
887
888         bool retVal = false;
889         STARTUPINFO stInfo = { sizeof STARTUPINFO };
890         PROCESS_INFORMATION processInfo;
891
892         retVal = !!CreateProcess(nullptr, (LPTSTR)sCmd.c_str(),
893                 nullptr, nullptr, FALSE, CREATE_DEFAULT_ERROR_MODE, nullptr, nullptr,
894                 &stInfo, &processInfo);
895
896         if (!retVal)
897         {
898                 // Error invoking external editor
899                 String msg = strutils::format_string1(_("Failed to execute external editor: %1"), sCmd);
900                 AfxMessageBox(msg.c_str(), MB_ICONSTOP);
901         }
902         else
903         {
904                 CloseHandle(processInfo.hThread);
905                 CloseHandle(processInfo.hProcess);
906         }
907 }
908
909 /** @brief Returns pointer to global file filter */
910 FileFilterHelper* CMergeApp::GetGlobalFileFilter()
911 {
912         if (!m_pGlobalFileFilter)
913         {
914                 m_pGlobalFileFilter.reset(new FileFilterHelper());
915
916                 InitializeFileFilters();
917
918                 // Read last used filter from registry
919                 // If filter fails to set, reset to default
920                 const String filterString = m_pOptions->GetString(OPT_FILEFILTER_CURRENT);
921                 bool bFilterSet = m_pGlobalFileFilter->SetFilter(filterString);
922                 if (!bFilterSet)
923                 {
924                         String filter = m_pGlobalFileFilter->GetFilterNameOrMask();
925                         m_pOptions->SaveOption(OPT_FILEFILTER_CURRENT, filter);
926                 }
927         }
928
929         return m_pGlobalFileFilter.get();
930 }
931
932 /**
933  * @brief Show Help - this is for opening help from outside mainframe.
934  * @param [in] helpLocation Location inside help, if `nullptr` main help is opened.
935  */
936 void CMergeApp::ShowHelp(LPCTSTR helpLocation /*= nullptr*/)
937 {
938         String sPath = paths::ConcatPath(env::GetProgPath(), strutils::format(DocsPath, GetLangName()));
939         if (paths::DoesPathExist(sPath) != paths::IS_EXISTING_FILE)
940                 sPath = paths::ConcatPath(env::GetProgPath(), strutils::format(DocsPath, _T("")));
941         if (helpLocation == nullptr)
942         {
943                 if (paths::DoesPathExist(sPath) == paths::IS_EXISTING_FILE)
944                         ::HtmlHelp(nullptr, sPath.c_str(), HH_DISPLAY_TOC, NULL);
945                 else
946                         shell::Open(DocsURL);
947         }
948         else
949         {
950                 if (paths::DoesPathExist(sPath) == paths::IS_EXISTING_FILE)
951                 {
952                         sPath += helpLocation;
953                         ::HtmlHelp(nullptr, sPath.c_str(), HH_DISPLAY_TOPIC, NULL);
954                 }
955         }
956 }
957
958 /**
959  * @brief Creates backup before file is saved or copied over.
960  * This function handles formatting correct path and filename for
961  * backup file. Formatting is done based on several options available
962  * for users in Options/Backups dialog. After path is formatted, file
963  * is simply just copied. Not much error checking, just if copying
964  * succeeded or failed.
965  * @param [in] bFolder Are we creating backup in folder compare?
966  * @param [in] pszPath Full path to file to backup.
967  * @return `true` if backup succeeds, or isn't just done.
968  */
969 bool CMergeApp::CreateBackup(bool bFolder, const String& pszPath)
970 {
971         // If user doesn't want to backups in folder compare, return
972         // success so operations don't abort.
973         if (bFolder && !(GetOptionsMgr()->GetBool(OPT_BACKUP_FOLDERCMP)))
974                 return true;
975         // Likewise if user doesn't want backups in file compare
976         else if (!bFolder && !(GetOptionsMgr()->GetBool(OPT_BACKUP_FILECMP)))
977                 return true;
978
979         // create backup copy of file if destination file exists
980         if (paths::DoesPathExist(pszPath) == paths::IS_EXISTING_FILE)
981         {
982                 String bakPath;
983                 String path;
984                 String filename;
985                 String ext;
986
987                 paths::SplitFilename(paths::GetLongPath(pszPath), &path, &filename, &ext);
988
989                 // Determine backup folder
990                 if (GetOptionsMgr()->GetInt(OPT_BACKUP_LOCATION) ==
991                         PropBackups::FOLDER_ORIGINAL)
992                 {
993                         // Put backups to same folder than original file
994                         bakPath = path;
995                 }
996                 else if (GetOptionsMgr()->GetInt(OPT_BACKUP_LOCATION) ==
997                         PropBackups::FOLDER_GLOBAL)
998                 {
999                         // Put backups to global folder defined in options
1000                         bakPath = GetOptionsMgr()->GetString(OPT_BACKUP_GLOBALFOLDER);
1001                         if (bakPath.empty())
1002                                 bakPath = path;
1003                         else
1004                                 bakPath = paths::GetLongPath(bakPath);
1005                 }
1006                 else
1007                 {
1008                         _RPTF0(_CRT_ERROR, "Unknown backup location!");
1009                 }
1010
1011                 bool success = false;
1012                 if (GetOptionsMgr()->GetBool(OPT_BACKUP_ADD_BAK))
1013                 {
1014                         // Don't add dot if there is no existing extension
1015                         if (ext.size() > 0)
1016                                 ext += _T(".");
1017                         ext += BACKUP_FILE_EXT;
1018                 }
1019
1020                 // Append time to filename if wanted so
1021                 // NOTE just adds timestamp at the moment as I couldn't figure out
1022                 // nice way to add a real time (invalid chars etc).
1023                 if (GetOptionsMgr()->GetBool(OPT_BACKUP_ADD_TIME))
1024                 {
1025                         struct tm tm;
1026                         time_t curtime = 0;
1027                         time(&curtime);
1028                         ::localtime_s(&tm, &curtime);
1029                         CString timestr;
1030                         timestr.Format(_T("%04d%02d%02d%02d%02d%02d"), tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
1031                         filename += _T("-");
1032                         filename += timestr;
1033                 }
1034
1035                 // Append filename and extension (+ optional .bak) to path
1036                 if ((bakPath.length() + filename.length() + ext.length())
1037                         < MAX_PATH_FULL)
1038                 {
1039                         success = true;
1040                         bakPath = paths::ConcatPath(bakPath, filename);
1041                         bakPath += _T(".");
1042                         bakPath += ext;
1043                 }
1044
1045                 if (success)
1046                 {
1047                         success = !!CopyFileW(TFile(pszPath).wpath().c_str(), TFile(bakPath).wpath().c_str(), FALSE);
1048                 }
1049
1050                 if (!success)
1051                 {
1052                         String msg = strutils::format_string1(
1053                                 _("Unable to backup original file:\n%1\n\nContinue anyway?"),
1054                                 pszPath);
1055                         if (AfxMessageBox(msg.c_str(), MB_YESNO | MB_ICONWARNING | MB_DONT_ASK_AGAIN, IDS_BACKUP_FAILED_PROMPT) != IDYES)
1056                                 return false;
1057                 }
1058                 return true;
1059         }
1060
1061         // we got here because we're either not backing up of there was nothing to backup
1062         return true;
1063 }
1064
1065 /**
1066  * @brief Checks if path (file/folder) is read-only and asks overwriting it.
1067  *
1068  * @param strSavePath [in,out] Path where to save (file or folder)
1069  * @param bMultiFile [in] Single file or multiple files/folder
1070  * @param bApplyToAll [in,out] Apply last user selection for all items?
1071  * @return Users selection:
1072  * - IDOK: Item was not readonly, no actions
1073  * - IDYES/IDYESTOALL: Overwrite readonly item
1074  * - IDNO: User selected new filename (single file) or user wants to skip
1075  * - IDCANCEL: Cancel operation
1076  * @sa CMainFrame::SyncFileToVCS()
1077  * @sa CMergeDoc::DoSave()
1078  */
1079 int CMergeApp::HandleReadonlySave(String& strSavePath, bool bMultiFile,
1080                 bool &bApplyToAll)
1081 {
1082         CFileStatus status;
1083         int nRetVal = IDOK;
1084         bool bFileRO = false;
1085         bool bFileExists = false;
1086         String s;
1087         String str;
1088         CString title;
1089
1090         if (!strSavePath.empty())
1091         {
1092                 try
1093                 {
1094                         TFile file(strSavePath);
1095                         bFileExists = file.exists();
1096                         if (bFileExists)
1097                                 bFileRO = !file.canWrite();
1098                 }
1099                 catch (...)
1100                 {
1101                 }
1102         }
1103
1104         if (bFileExists && bFileRO)
1105         {
1106                 UINT userChoice = 0;
1107
1108                 // Don't ask again if its already asked
1109                 if (bApplyToAll)
1110                         userChoice = IDYES;
1111                 else
1112                 {
1113                         // Prompt for user choice
1114                         if (bMultiFile)
1115                         {
1116                                 // Multiple files or folder
1117                                 str = strutils::format_string1(_("%1\nis marked read-only. Would you like to override the read-only item?"), strSavePath);
1118                                 userChoice = AfxMessageBox(str.c_str(), MB_YESNOCANCEL |
1119                                                 MB_ICONWARNING | MB_DEFBUTTON3 | MB_DONT_ASK_AGAIN |
1120                                                 MB_YES_TO_ALL, IDS_SAVEREADONLY_MULTI);
1121                         }
1122                         else
1123                         {
1124                                 // Single file
1125                                 str = strutils::format_string1(_("%1 is marked read-only. Would you like to override the read-only file? (No to save as new filename.)"), strSavePath);
1126                                 userChoice = AfxMessageBox(str.c_str(), MB_YESNOCANCEL |
1127                                                 MB_ICONWARNING | MB_DEFBUTTON2 | MB_DONT_ASK_AGAIN,
1128                                                 IDS_SAVEREADONLY_FMT);
1129                         }
1130                 }
1131                 switch (userChoice)
1132                 {
1133                 // Overwrite read-only file
1134                 case IDYESTOALL:
1135                         bApplyToAll = true;  // Don't ask again (no break here)
1136                         [[fallthrough]];
1137                 case IDYES:
1138                         CFile::GetStatus(strSavePath.c_str(), status);
1139                         status.m_mtime = 0;             // Avoid unwanted changes
1140                         status.m_attribute &= ~CFile::readOnly;
1141                         CFile::SetStatus(strSavePath.c_str(), status);
1142                         nRetVal = IDYES;
1143                         break;
1144
1145                 // Save to new filename (single) /skip this item (multiple)
1146                 case IDNO:
1147                         if (!bMultiFile)
1148                         {
1149                                 if (SelectFile(AfxGetMainWnd()->GetSafeHwnd(), s, false, strSavePath.c_str()))
1150                                 {
1151                                         strSavePath = s;
1152                                         nRetVal = IDNO;
1153                                 }
1154                                 else
1155                                         nRetVal = IDCANCEL;
1156                         }
1157                         else
1158                                 nRetVal = IDNO;
1159                         break;
1160
1161                 // Cancel saving
1162                 case IDCANCEL:
1163                         nRetVal = IDCANCEL;
1164                         break;
1165                 }
1166         }
1167         return nRetVal;
1168 }
1169
1170 String CMergeApp::GetPackingErrorMessage(int pane, int paneCount, const String& path, const PackingInfo& plugin)
1171 {
1172         String pluginName = plugin.GetPluginPipeline();
1173         return strutils::format_string2(
1174                 pane == 0 ? 
1175                         _("Plugin '%2' cannot pack your changes to the left file back into '%1'.\n\nThe original file will not be changed.\n\nDo you want to save the unpacked version to another file?")
1176                         : (pane == paneCount - 1) ? 
1177                                 _("Plugin '%2' cannot pack your changes to the right file back into '%1'.\n\nThe original file will not be changed.\n\nDo you want to save the unpacked version to another file?")
1178                                 : _("Plugin '%2' cannot pack your changes to the middle file back into '%1'.\n\nThe original file will not be changed.\n\nDo you want to save the unpacked version to another file?"),
1179                 path, pluginName);
1180 }
1181
1182 /**
1183  * @brief Is specified file a project file?
1184  * @param [in] filepath Full path to file to check.
1185  * @return true if file is a projectfile.
1186  */
1187 bool CMergeApp::IsProjectFile(const String& filepath) const
1188 {
1189         String ext;
1190         paths::SplitFilename(filepath, nullptr, nullptr, &ext);
1191         if (strutils::compare_nocase(ext, ProjectFile::PROJECTFILE_EXT) == 0)
1192                 return true;
1193         else
1194                 return false;
1195 }
1196
1197 bool CMergeApp::LoadProjectFile(const String& sProject, ProjectFile &project)
1198 {
1199         if (sProject.empty())
1200                 return false;
1201
1202         try
1203         {
1204         project.Read(sProject);
1205         }
1206         catch (Poco::Exception& e)
1207         {
1208                 String sErr = _("Unknown error attempting to open project file.");
1209                 sErr += ucr::toTString(e.displayText());
1210                 String msg = strutils::format_string2(_("Cannot open file\n%1\n\n%2"), sProject, sErr);
1211                 AfxMessageBox(msg.c_str(), MB_ICONSTOP);
1212                 return false;
1213         }
1214
1215         return true;
1216 }
1217
1218 bool CMergeApp::SaveProjectFile(const String& sProject, const ProjectFile &project)
1219 {
1220         try
1221         {
1222                 project.Save(sProject);
1223         }
1224         catch (Poco::Exception& e)
1225         {
1226                 String sErr = _("Unknown error attempting to save project file.");
1227                 sErr += ucr::toTString(e.displayText());
1228                 String msg = strutils::format_string2(_("Cannot open file\n%1\n\n%2"), sProject, sErr);
1229                 AfxMessageBox(msg.c_str(), MB_ICONSTOP);
1230                 return false;
1231         }
1232
1233         return true;
1234 }
1235
1236 /**
1237  * @brief Read project and perform comparison specified
1238  * @param [in] sProject Full path to project file.
1239  * @return `true` if loading project file and starting compare succeeded.
1240  */
1241 bool CMergeApp::LoadAndOpenProjectFile(const String& sProject, const String& sReportFile)
1242 {
1243         ProjectFile project;
1244         if (!LoadProjectFile(sProject, project))
1245                 return false;
1246
1247         bool rtn = true;
1248         for (auto& projItem : project.Items())
1249         {
1250                 std::unique_ptr<PrediffingInfo> pInfoPrediffer;
1251                 std::unique_ptr<PackingInfo> pInfoUnpacker;
1252                 PathContext tFiles;
1253                 bool bRecursive = false;
1254                 projItem.GetPaths(tFiles, bRecursive);
1255                 for (int i = 0; i < tFiles.GetSize(); ++i)
1256                 {
1257                         if (!paths::IsPathAbsolute(tFiles[i]))
1258                         {
1259                                 String sProjectDir = paths::GetParentPath(sProject);
1260                                 if (tFiles[i].substr(0, 1) == _T("\\"))
1261                                 {
1262                                         if (sProjectDir.length() > 1 && sProjectDir[1] == ':')
1263                                                 tFiles[i] = paths::ConcatPath(sProjectDir.substr(0, 2), tFiles[i]);
1264                                 }
1265                                 else
1266                                         tFiles[i] = paths::ConcatPath(sProjectDir, tFiles[i]);
1267                         }
1268                 }
1269                 bool bLeftReadOnly = projItem.GetLeftReadOnly();
1270                 bool bMiddleReadOnly = projItem.GetMiddleReadOnly();
1271                 bool bRightReadOnly = projItem.GetRightReadOnly();
1272                 if (projItem.HasFilter())
1273                 {
1274                         String filter = projItem.GetFilter();
1275                         filter = strutils::trim_ws(filter);
1276                         GetGlobalFileFilter()->SetFilter(filter);
1277                 }
1278                 if (projItem.HasSubfolders())
1279                         bRecursive = projItem.GetSubfolders() > 0;
1280                 if (projItem.HasUnpacker())
1281                         pInfoUnpacker.reset(new PackingInfo(projItem.GetUnpacker()));
1282                 if (projItem.HasPrediffer())
1283                         pInfoPrediffer.reset(new PrediffingInfo(projItem.GetPrediffer()));
1284
1285                 DWORD dwFlags[3] = {
1286                         static_cast<DWORD>(tFiles.GetPath(0).empty() ? FFILEOPEN_NONE : FFILEOPEN_PROJECT),
1287                         static_cast<DWORD>(tFiles.GetPath(1).empty() ? FFILEOPEN_NONE : FFILEOPEN_PROJECT),
1288                         static_cast<DWORD>(tFiles.GetPath(2).empty() ? FFILEOPEN_NONE : FFILEOPEN_PROJECT)
1289                 };
1290                 if (bLeftReadOnly)
1291                         dwFlags[0] |= FFILEOPEN_READONLY;
1292                 if (tFiles.GetSize() == 2)
1293                 {
1294                         if (bRightReadOnly)
1295                                 dwFlags[1] |= FFILEOPEN_READONLY;
1296                 }
1297                 else
1298                 {
1299                         if (bMiddleReadOnly)
1300                                 dwFlags[1] |= FFILEOPEN_READONLY;
1301                         if (bRightReadOnly)
1302                                 dwFlags[2] |= FFILEOPEN_READONLY;
1303                 }
1304
1305                 GetOptionsMgr()->SaveOption(OPT_CMP_INCLUDE_SUBDIRS, bRecursive);
1306
1307                 if (projItem.HasIgnoreWhite())
1308                         GetOptionsMgr()->SaveOption(OPT_CMP_IGNORE_WHITESPACE, projItem.GetIgnoreWhite());
1309                 if (projItem.HasIgnoreBlankLines())
1310                         GetOptionsMgr()->SaveOption(OPT_CMP_IGNORE_BLANKLINES, projItem.GetIgnoreBlankLines());
1311                 if (projItem.HasIgnoreCase())
1312                         GetOptionsMgr()->SaveOption(OPT_CMP_IGNORE_CASE, projItem.GetIgnoreCase());
1313                 if (projItem.HasIgnoreEol())
1314                         GetOptionsMgr()->SaveOption(OPT_CMP_IGNORE_EOL, projItem.GetIgnoreEol());
1315                 if (projItem.HasIgnoreCodepage())
1316                         GetOptionsMgr()->SaveOption(OPT_CMP_IGNORE_CODEPAGE, projItem.GetIgnoreCodepage());
1317                 if (projItem.HasFilterCommentsLines())
1318                         GetOptionsMgr()->SaveOption(OPT_CMP_FILTER_COMMENTLINES, projItem.GetFilterCommentsLines());
1319                 if (projItem.HasCompareMethod())
1320                         GetOptionsMgr()->SaveOption(OPT_CMP_METHOD, projItem.GetCompareMethod());
1321
1322                 rtn &= GetMainFrame()->DoFileOrFolderOpen(&tFiles, dwFlags, nullptr, sReportFile, bRecursive,
1323                         nullptr, pInfoUnpacker.get(), pInfoPrediffer.get());
1324         }
1325
1326         AddToRecentProjectsMRU(sProject.c_str());
1327         return rtn;
1328 }
1329
1330 /**
1331  * @brief Return windows language ID of current WinMerge GUI language
1332  */
1333 WORD CMergeApp::GetLangId() const
1334 {
1335         return m_pLangDlg->GetLangId();
1336 }
1337
1338 String CMergeApp::GetLangName() const
1339 {
1340         String name, ext;
1341         paths::SplitFilename(theApp.m_pLangDlg->GetFileName(theApp.GetLangId()), nullptr, &name, &ext);
1342         return name;
1343 }
1344
1345 /**
1346  * @brief Lang aware version of CStatusBar::SetIndicators()
1347  */
1348 void CMergeApp::SetIndicators(CStatusBar &sb, const UINT *rgid, int n) const
1349 {
1350         m_pLangDlg->SetIndicators(sb, rgid, n);
1351 }
1352
1353 /**
1354  * @brief Translate menu to current WinMerge GUI language
1355  */
1356 void CMergeApp::TranslateMenu(HMENU h) const
1357 {
1358         m_pLangDlg->TranslateMenu(h);
1359 }
1360
1361 /**
1362  * @brief Translate dialog to current WinMerge GUI language
1363  */
1364 void CMergeApp::TranslateDialog(HWND h) const
1365 {
1366         CWnd *pWnd = CWnd::FromHandle(h);
1367         pWnd->SetFont(const_cast<CFont *>(&m_fontGUI));
1368         pWnd->SendMessageToDescendants(WM_SETFONT, (WPARAM)m_fontGUI.m_hObject, MAKELPARAM(FALSE, 0), TRUE);
1369
1370         m_pLangDlg->TranslateDialog(h);
1371 }
1372
1373 /**
1374  * @brief Load string and translate to current WinMerge GUI language
1375  */
1376 String CMergeApp::LoadString(UINT id) const
1377 {
1378         return m_pLangDlg->LoadString(id);
1379 }
1380
1381 bool CMergeApp::TranslateString(const std::string& str, String& translated_str) const
1382 {
1383         return m_pLangDlg->TranslateString(str, translated_str);
1384 }
1385
1386 /**
1387  * @brief Load dialog caption and translate to current WinMerge GUI language
1388  */
1389 std::wstring CMergeApp::LoadDialogCaption(LPCTSTR lpDialogTemplateID) const
1390 {
1391         return m_pLangDlg->LoadDialogCaption(lpDialogTemplateID);
1392 }
1393
1394 /**
1395  * @brief Adds specified file to the recent projects list.
1396  * @param [in] sPathName Path to project file
1397  */
1398 void CMergeApp::AddToRecentProjectsMRU(LPCTSTR sPathName)
1399 {
1400         // sPathName will be added to the top of the MRU list.
1401         // If sPathName already exists in the MRU list, it will be moved to the top
1402         if (m_pRecentFileList != nullptr)    {
1403                 m_pRecentFileList->Add(sPathName);
1404                 m_pRecentFileList->WriteList();
1405         }
1406 }
1407
1408 void CMergeApp::SetupTempPath()
1409 {
1410         String instTemp = env::GetPerInstanceString(TempFolderPrefix);
1411         if (GetOptionsMgr()->GetBool(OPT_USE_SYSTEM_TEMP_PATH))
1412                 env::SetTemporaryPath(paths::ConcatPath(env::GetSystemTempPath(), instTemp));
1413         else
1414                 env::SetTemporaryPath(paths::ConcatPath(GetOptionsMgr()->GetString(OPT_CUSTOM_TEMP_PATH), instTemp));
1415 }
1416
1417 /**
1418  * @brief Handles menu selection from recent projects list
1419  * @param [in] nID Menu ID of the selected item
1420  */
1421 BOOL CMergeApp::OnOpenRecentFile(UINT nID)
1422 {
1423         return LoadAndOpenProjectFile(static_cast<const TCHAR *>(m_pRecentFileList->m_arrNames[nID-ID_FILE_PROJECT_MRU_FIRST]));
1424 }
1425
1426 /**
1427  * @brief Return if doc is in Merging/Editing mode
1428  */
1429 bool CMergeApp::GetMergingMode() const
1430 {
1431         return m_bMergingMode;
1432 }
1433
1434 /**
1435  * @brief Set doc to Merging/Editing mode
1436  */
1437 void CMergeApp::SetMergingMode(bool bMergingMode)
1438 {
1439         m_bMergingMode = bMergingMode;
1440         GetOptionsMgr()->SaveOption(OPT_MERGE_MODE, m_bMergingMode);
1441 }
1442
1443 /**
1444  * @brief Switch Merging/Editing mode and update
1445  * buffer read-only states accordingly
1446  */
1447 void CMergeApp::OnMergingMode()
1448 {
1449         bool bMergingMode = GetMergingMode();
1450
1451         if (!bMergingMode)
1452                 LangMessageBox(IDS_MERGE_MODE, MB_ICONINFORMATION | MB_DONT_DISPLAY_AGAIN, IDS_MERGE_MODE);
1453         SetMergingMode(!bMergingMode);
1454 }
1455
1456 /**
1457  * @brief Update Menuitem for Merging Mode
1458  */
1459 void CMergeApp::OnUpdateMergingMode(CCmdUI* pCmdUI)
1460 {
1461         pCmdUI->Enable(true);
1462         pCmdUI->SetCheck(GetMergingMode());
1463 }
1464
1465 /**
1466  * @brief Update MergingMode UI in statusbar
1467  */
1468 void CMergeApp::OnUpdateMergingStatus(CCmdUI *pCmdUI)
1469 {
1470         String text = theApp.LoadString(IDS_MERGEMODE_MERGING);
1471         pCmdUI->SetText(text.c_str());
1472         pCmdUI->Enable(GetMergingMode());
1473 }
1474
1475 UINT CMergeApp::GetProfileInt(LPCTSTR lpszSection, LPCTSTR lpszEntry, int nDefault)
1476 {
1477         COptionsMgr *pOptions = GetOptionsMgr();
1478         String name = strutils::format(_T("%s/%s"), lpszSection, lpszEntry);
1479         if (!pOptions->Get(name).IsInt())
1480                 pOptions->InitOption(name, nDefault);
1481         return pOptions->GetInt(name);
1482 }
1483
1484 BOOL CMergeApp::WriteProfileInt(LPCTSTR lpszSection, LPCTSTR lpszEntry, int nValue)
1485 {
1486         COptionsMgr *pOptions = GetOptionsMgr();
1487         String name = strutils::format(_T("%s/%s"), lpszSection, lpszEntry);
1488         if (!pOptions->Get(name).IsInt())
1489                 pOptions->InitOption(name, nValue);
1490         return pOptions->SaveOption(name, nValue) == COption::OPT_OK;
1491 }
1492
1493 CString CMergeApp::GetProfileString(LPCTSTR lpszSection, LPCTSTR lpszEntry, LPCTSTR lpszDefault)
1494 {
1495         COptionsMgr *pOptions = GetOptionsMgr();
1496         String name = strutils::format(_T("%s/%s"), lpszSection, lpszEntry);
1497         if (!pOptions->Get(name).IsString())
1498                 pOptions->InitOption(name, lpszDefault ? lpszDefault : _T(""));
1499         return pOptions->GetString(name).c_str();
1500 }
1501
1502 BOOL CMergeApp::WriteProfileString(LPCTSTR lpszSection, LPCTSTR lpszEntry, LPCTSTR lpszValue)
1503 {
1504         COptionsMgr *pOptions = GetOptionsMgr();
1505         if (lpszEntry != nullptr)
1506         {
1507                 String name = strutils::format(_T("%s/%s"), lpszSection, lpszEntry);
1508                 if (!pOptions->Get(name).IsString())
1509                         pOptions->InitOption(name, lpszValue ? lpszValue : _T(""));
1510                 return pOptions->SaveOption(name, lpszValue ? lpszValue : _T("")) == COption::OPT_OK;
1511         }
1512         else
1513         {
1514                 String name = strutils::format(_T("%s/"), lpszSection);
1515                 pOptions->RemoveOption(name);
1516         }
1517         return TRUE;
1518 }