OSDN Git Service

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