OSDN Git Service

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