OSDN Git Service

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