OSDN Git Service

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