OSDN Git Service

Leave the class name as CIniOptionsMgr, but rename the filename to IniOptionsMgr.*
[winmerge-jp/winmerge-jp.git] / Src / Merge.cpp
index 6c90aee..bbda58a 100644 (file)
@@ -2,23 +2,9 @@
 //    WinMerge:  an interactive diff/merge utility
 //    Copyright (C) 1997-2000  Thingamahoochie Software
 //    Author: Dean Grimm
-//
-//    This program is free software; you can redistribute it and/or modify
-//    it under the terms of the GNU General Public License as published by
-//    the Free Software Foundation; either version 2 of the License, or
-//    (at your option) any later version.
-//
-//    This program is distributed in the hope that it will be useful,
-//    but WITHOUT ANY WARRANTY; without even the implied warranty of
-//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-//    GNU General Public License for more details.
-//
-//    You should have received a copy of the GNU General Public License
-//    along with this program; if not, write to the Free Software
-//    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
-//
+//    SPDX-License-Identifier: GPL-2.0-or-later
 /////////////////////////////////////////////////////////////////////////////
-/** 
+/**
  * @file  Merge.cpp
  *
  * @brief Defines the class behaviors for the application.
@@ -34,6 +20,7 @@
 #include "OptionsMgr.h"
 #include "OptionsInit.h"
 #include "RegOptionsMgr.h"
+#include "IniOptionsMgr.h"
 #include "OpenDoc.h"
 #include "OpenFrm.h"
 #include "OpenView.h"
 #include "HexMergeView.h"
 #include "AboutDlg.h"
 #include "MainFrm.h"
-#include "ChildFrm.h"
+#include "MergeEditFrm.h"
 #include "DirFrame.h"
 #include "MergeDoc.h"
 #include "DirDoc.h"
 #include "DirView.h"
 #include "PropBackups.h"
 #include "FileOrFolderSelect.h"
-#include "paths.h"
 #include "FileFilterHelper.h"
 #include "LineFiltersList.h"
-#include "FilterCommentsManager.h"
+#include "SubstitutionFiltersList.h"
 #include "SyntaxColors.h"
 #include "CCrystalTextMarkers.h"
 #include "OptionsSyntaxColors.h"
 #include "Plugins.h"
 #include "ProjectFile.h"
-#include "MergeEditView.h"
+#include "MergeEditSplitterView.h"
 #include "LanguageSelect.h"
 #include "OptionsDef.h"
 #include "MergeCmdLineInfo.h"
 #include "JumpList.h"
 #include "stringdiffs.h"
 #include "TFile.h"
-#include "SourceControl.h"
 #include "paths.h"
+#include "Shell.h"
+#include "CompareStats.h"
 #include "TestMain.h"
-
-// For shutdown cleanup
-#include "charsets.h"
+#include "charsets.h" // For shutdown cleanup
 
 #ifdef _DEBUG
 #define new DEBUG_NEW
 #endif
 
-
-
 /** @brief Location for command line help to open. */
-static TCHAR CommandLineHelpLocation[] = _T("::/htmlhelp/Command_line.html");
-
-// registry dir to WinMerge
-static String f_RegDir = _T("Software\\Thingamahoochie\\WinMerge");
+static const TCHAR CommandLineHelpLocation[] = _T("::/htmlhelp/Command_line.html");
 
 /** @brief Backup file extension. */
 static const TCHAR BACKUP_FILE_EXT[] = _T("bak");
 
-#ifndef WIN64
-/**
- * @brief Turn STL exceptions into MFC exceptions.
- * Based on the article "Visual C++ Exception-Handling Instrumentation"
- * by Eugene Gershnik, published at http://www.drdobbs.com/184416600.
- * Rethrow fix inspired by http://www.spinics.net/lists/wine/msg05996.html.
- */
-/*
-namespace Turn_STL_exceptions_into_MFC_exceptions
-{
-#      ifndef _STATIC_CPPLIB
-#      error This hack only works with _STATIC_CPPLIB defined.
-#      endif
-
-       class CDisguisedSTLException : public CException
-       {
-       private:
-               std::exception *m_pSTLException;
-       public:
-               CDisguisedSTLException(std::exception *pSTLException)
-               : m_pSTLException(pSTLException)
-               {
-               }
-               virtual BOOL GetErrorMessage(LPTSTR lpszError, UINT nMaxError, PUINT)
-               {
-                       StringCchPrintf(lpszError, nMaxError, _T("%hs"), m_pSTLException->what());
-                       return TRUE;
-               }
-       };
-
-       const DWORD CPP_EXCEPTION = 0xE06D7363;
-       const DWORD MS_MAGIC = 0x19930520;
-
-       extern "C" void __stdcall _CxxThrowException(void *pObject, _s__ThrowInfo const *pObjectInfo)
-       {
-               __declspec(thread) static ULONG_PTR args[3] = { MS_MAGIC, 0, 0 };
-               if (pObject == NULL)
-               {
-                       pObject = reinterpret_cast<void *>(args[1]);
-                       pObjectInfo = reinterpret_cast<_s__ThrowInfo const *>(args[2]);
-               }
-               else
-               {
-                       args[1] = (ULONG_PTR)pObject;
-                       args[2] = (ULONG_PTR)pObjectInfo;
-               }
-               int i;
-               if (pObjectInfo->pCatchableTypeArray && (i = pObjectInfo->pCatchableTypeArray->nCatchableTypes))
-               {
-                       const char *name = typeid(std::exception).raw_name();
-                       if (pObjectInfo->pCatchableTypeArray->arrayOfCatchableTypes[i - 1]->pType->name == name)
-                       {
-                               throw new CDisguisedSTLException(static_cast<std::exception *>(pObject));
-                       }
-               }
-               RaiseException(CPP_EXCEPTION, EXCEPTION_NONCONTINUABLE, sizeof(args)/sizeof(args[0]), args);
-       }
-}
-*/
-#endif
-
 /////////////////////////////////////////////////////////////////////////////
 // CMergeApp
 
@@ -157,7 +76,7 @@ BEGIN_MESSAGE_MAP(CMergeApp, CWinApp)
        ON_COMMAND(ID_APP_ABOUT, OnAppAbout)
        ON_COMMAND(ID_HELP, OnHelp)
        ON_COMMAND_EX_RANGE(ID_FILE_PROJECT_MRU_FIRST, ID_FILE_PROJECT_MRU_LAST, OnOpenRecentFile)
-       ON_UPDATE_COMMAND_UI(ID_FILE_PROJECT_MRU_FIRST, &CWinApp::OnUpdateRecentFileMenu)
+       ON_UPDATE_COMMAND_UI(ID_FILE_PROJECT_MRU_FIRST, CWinApp::OnUpdateRecentFileMenu)
        ON_COMMAND(ID_FILE_MERGINGMODE, OnMergingMode)
        ON_UPDATE_COMMAND_UI(ID_FILE_MERGINGMODE, OnUpdateMergingMode)
        ON_UPDATE_COMMAND_UI(ID_STATUS_MERGINGMODE, OnUpdateMergingStatus)
@@ -169,53 +88,51 @@ BEGIN_MESSAGE_MAP(CMergeApp, CWinApp)
        ON_COMMAND(ID_FILE_PRINT_SETUP, CWinApp::OnFilePrintSetup)
 END_MESSAGE_MAP()
 
-/**
-* @brief Mapping from command line argument name (eg, ignorews) to WinMerge
-* option name (eg, Settings/IgnoreSpace).
-*
-* These arguments take an optional colon and number, like so:
-*
-*  "/ignoreblanklines"  (makes WinMerge ignore blank lines)
-*  "/ignoreblanklines:1"  (makes WinMerge ignore blank lines)
-*  "/ignoreblanklines:0"  (makes WinMerge not ignore blank lines)
-*/
-struct ArgSetting
-{
-       LPCTSTR CmdArgName;
-       LPCTSTR WinMergeOptionName;
-};
-
-
 /////////////////////////////////////////////////////////////////////////////
 // CMergeApp construction
 
 CMergeApp::CMergeApp() :
-  m_bNeedIdleTimer(FALSE)
-, m_pOpenTemplate(0)
-, m_pDiffTemplate(0)
-, m_pHexMergeTemplate(0)
-, m_pDirTemplate(0)
-, m_mainThreadScripts(NULL)
+  m_bNeedIdleTimer(false)
+, m_pOpenTemplate(nullptr)
+, m_pDiffTemplate(nullptr)
+, m_pHexMergeTemplate(nullptr)
+, m_pDirTemplate(nullptr)
+, m_mainThreadScripts(nullptr)
 , m_nLastCompareResult(0)
 , m_bNonInteractive(false)
-, m_pOptions(new CRegOptionsMgr())
+, m_pOptions(CreateOptionManager())
 , m_pGlobalFileFilter(new FileFilterHelper())
 , m_nActiveOperations(0)
 , m_pLangDlg(new CLanguageSelect())
-, m_bEscShutdown(FALSE)
-, m_bClearCaseTool(FALSE)
-, m_bExitIfNoDiff(MergeCmdLineInfo::Disabled)
+, m_bEscShutdown(false)
+, m_bExitIfNoDiff(MergeCmdLineInfo::ExitNoDiff::Disabled)
 , m_pLineFilters(new LineFiltersList())
-, m_pFilterCommentsManager(new FilterCommentsManager())
+, m_pSubstitutionFiltersList(new SubstitutionFiltersList())
 , m_pSyntaxColors(new SyntaxColors())
 , m_pMarkers(new CCrystalTextMarkers())
-, m_pSourceControl(new SourceControl())
-, m_bMergingMode(FALSE)
+, m_bMergingMode(false)
 {
        // add construction code here,
        // Place all significant initialization in InitInstance
 }
 
+/**
+ * @brief Chose which options manager should be initialized.
+ * @return IniOptionsMgr if initial config file exists,
+ *   CRegOptionsMgr otherwise.
+ */
+COptionsMgr *CreateOptionManager()
+{
+       if (CIniOptionsMgr::CheckIfIniFileExist())
+       {
+               return new CIniOptionsMgr();
+       }
+       else
+       {
+               return new CRegOptionsMgr();
+       }
+}
+
 CMergeApp::~CMergeApp()
 {
        strdiff::Close();
@@ -240,10 +157,10 @@ BOOL CMergeApp::InitInstance()
        // Prevents DLL hijacking
        HMODULE hLibrary = GetModuleHandle(_T("kernel32.dll"));
        BOOL (WINAPI *pfnSetSearchPathMode)(DWORD) = (BOOL (WINAPI *)(DWORD))GetProcAddress(hLibrary, "SetSearchPathMode");
-       if (pfnSetSearchPathMode)
+       if (pfnSetSearchPathMode != nullptr)
                pfnSetSearchPathMode(0x00000001L /*BASE_SEARCH_PATH_ENABLE_SAFE_SEARCHMODE*/ | 0x00008000L /*BASE_SEARCH_PATH_PERMANENT*/);
        BOOL (WINAPI *pfnSetDllDirectoryA)(LPCSTR) = (BOOL (WINAPI *)(LPCSTR))GetProcAddress(hLibrary, "SetDllDirectoryA");
-       if (pfnSetDllDirectoryA)
+       if (pfnSetDllDirectoryA != nullptr)
                pfnSetDllDirectoryA("");
 
        JumpList::SetCurrentProcessExplicitAppUserModelID(L"Thingamahoochie.WinMerge");
@@ -251,6 +168,8 @@ BOOL CMergeApp::InitInstance()
        InitCommonControls();    // initialize common control library
        CWinApp::InitInstance(); // call parent class method
 
+       m_imageForInitializingGdiplus.Load((IStream*)nullptr); // initialize GDI+
+
        // Runtime switch so programmer may set this in interactive debugger
        int dbgmem = 0;
        if (dbgmem)
@@ -284,53 +203,82 @@ BOOL CMergeApp::InitInstance()
        // Load registry keys from WinMerge.reg if existing WinMerge.reg
        env::LoadRegistryFromFile(paths::ConcatPath(env::GetProgPath(), _T("WinMerge.reg")));
 
+       // Parse command-line arguments.
+#ifdef TEST_WINMERGE
+       MergeCmdLineInfo cmdInfo(_T(""));
+#else
+       MergeCmdLineInfo cmdInfo(GetCommandLine());
+#endif
+       if (cmdInfo.m_bNoPrefs)
+               m_pOptions->SetSerializing(false); // Turn off serializing to registry.
+
+       Options::CopyHKLMValues();
        Options::Init(m_pOptions.get()); // Implementation in OptionsInit.cpp
+       ApplyCommandLineConfigOptions(cmdInfo);
+       if (cmdInfo.m_sErrorMessages.size() > 0)
+       {
+               if (AttachConsole(ATTACH_PARENT_PROCESS))
+               {
+                       DWORD dwWritten;
+                       for (auto& msg : cmdInfo.m_sErrorMessages)
+                       {
+                               String line = _T("WinMerge: ") + msg + _T("\n");
+                               WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), line.c_str(), static_cast<DWORD>(line.length()), &dwWritten, nullptr);
+                       }
+                       FreeConsole();
+               }
+       }
 
        // Initialize temp folder
        SetupTempPath();
 
-       // Parse command-line arguments.
-       MergeCmdLineInfo cmdInfo(GetCommandLine());
-
        // If paths were given to commandline we consider this being an invoke from
        // commandline (from other application, shellextension etc).
-       BOOL bCommandLineInvoke = cmdInfo.m_Files.GetSize() > 0;
+       bool bCommandLineInvoke = cmdInfo.m_Files.GetSize() > 0;
 
        // WinMerge registry settings are stored under HKEY_CURRENT_USER/Software/Thingamahoochie
        // This is the name of the company of the original author (Dean Grimm)
        SetRegistryKey(_T("Thingamahoochie"));
 
-       BOOL bSingleInstance = GetOptionsMgr()->GetBool(OPT_SINGLE_INSTANCE) ||
-               (true == cmdInfo.m_bSingleInstance);
+       int nSingleInstance = cmdInfo.m_nSingleInstance.has_value() ?
+               *cmdInfo.m_nSingleInstance : GetOptionsMgr()->GetInt(OPT_SINGLE_INSTANCE);
 
        // Create exclusion mutex name
        TCHAR szDesktopName[MAX_PATH] = _T("Win9xDesktop");
        DWORD dwLengthNeeded;
-       GetUserObjectInformation(GetThreadDesktop(GetCurrentThreadId()), UOI_NAME, 
+       GetUserObjectInformation(GetThreadDesktop(GetCurrentThreadId()), UOI_NAME,
                szDesktopName, sizeof(szDesktopName), &dwLengthNeeded);
        TCHAR szMutexName[MAX_PATH + 40];
        // Combine window class name and desktop name to form a unique mutex name.
        // As the window class name is decorated to distinguish between ANSI and
        // UNICODE build, so will be the mutex name.
        wsprintf(szMutexName, _T("%s-%s"), CMainFrame::szClassName, szDesktopName);
-       HANDLE hMutex = CreateMutex(NULL, FALSE, szMutexName);
-       if (hMutex)
+       HANDLE hMutex = CreateMutex(nullptr, FALSE, szMutexName);
+       if (hMutex != nullptr)
                WaitForSingleObject(hMutex, INFINITE);
-       if (bSingleInstance && GetLastError() == ERROR_ALREADY_EXISTS)
+       if (nSingleInstance != 0 && GetLastError() == ERROR_ALREADY_EXISTS)
        {
                // Activate previous instance and send commandline to it
-               HWND hWnd = FindWindow(CMainFrame::szClassName, NULL);
-               if (hWnd)
+               HWND hWnd = FindWindow(CMainFrame::szClassName, nullptr);
+               if (hWnd != nullptr)
                {
                        if (IsIconic(hWnd))
                                ShowWindow(hWnd, SW_RESTORE);
                        SetForegroundWindow(GetLastActivePopup(hWnd));
                        LPTSTR cmdLine = GetCommandLine();
                        COPYDATASTRUCT data = { 0, (lstrlen(cmdLine) + 1) * sizeof(TCHAR), cmdLine};
-                       if (SendMessage(hWnd, WM_COPYDATA, NULL, (LPARAM)&data))
+                       if (::SendMessage(hWnd, WM_COPYDATA, NULL, (LPARAM)&data))
                        {
                                ReleaseMutex(hMutex);
                                CloseHandle(hMutex);
+                               if (nSingleInstance > 1)
+                               {
+                                       DWORD dwProcessId = 0;
+                                       GetWindowThreadProcessId(hWnd, &dwProcessId);
+                                       HANDLE hProcess = OpenProcess(SYNCHRONIZE, FALSE, dwProcessId);
+                                       if (hProcess)
+                                               WaitForSingleObject(hProcess, INFINITE);
+                               }
                                return FALSE;
                        }
                }
@@ -343,7 +291,7 @@ BOOL CMergeApp::InitInstance()
        // Read last used filter from registry
        // If filter fails to set, reset to default
        const String filterString = m_pOptions->GetString(OPT_FILEFILTER_CURRENT);
-       BOOL bFilterSet = m_pGlobalFileFilter->SetFilter(filterString);
+       bool bFilterSet = m_pGlobalFileFilter->SetFilter(filterString);
        if (!bFilterSet)
        {
                String filter = m_pGlobalFileFilter->GetFilterNameOrMask();
@@ -353,11 +301,8 @@ BOOL CMergeApp::InitInstance()
        charsets_init();
        UpdateCodepageModule();
 
-       if (m_pSourceControl)
-               m_pSourceControl->InitializeSourceControlMembers();
-
-       FileTransform::g_bUnpackerMode = theApp.GetProfileInt(_T("Settings"), _T("UnpackerMode"), PLUGIN_MANUAL);
-       FileTransform::g_bPredifferMode = theApp.GetProfileInt(_T("Settings"), _T("PredifferMode"), PLUGIN_MANUAL);
+       FileTransform::g_UnpackerMode = static_cast<PLUGIN_MODE>(GetOptionsMgr()->GetInt(OPT_PLUGINS_UNPACKER_MODE));
+       FileTransform::g_PredifferMode = static_cast<PLUGIN_MODE>(GetOptionsMgr()->GetInt(OPT_PLUGINS_PREDIFFER_MODE));
 
        NONCLIENTMETRICS ncm = { sizeof NONCLIENTMETRICS };
        if (SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof NONCLIENTMETRICS, &ncm, 0))
@@ -370,13 +315,15 @@ BOOL CMergeApp::InitInstance()
                m_fontGUI.CreateFontIndirect(&ncm.lfMenuFont);
        }
 
-       if (m_pSyntaxColors)
-               Options::SyntaxColors::Load(GetOptionsMgr(), m_pSyntaxColors.get());
+       if (m_pSyntaxColors != nullptr)
+               Options::SyntaxColors::Init(GetOptionsMgr(), m_pSyntaxColors.get());
 
-       if (m_pMarkers)
+       if (m_pMarkers != nullptr)
                m_pMarkers->LoadFromRegistry();
 
-       if (m_pLineFilters)
+       CCrystalTextView::SetRenderingModeDefault(static_cast<CCrystalTextView::RENDERING_MODE>(GetOptionsMgr()->GetInt(OPT_RENDERING_MODE)));
+
+       if (m_pLineFilters != nullptr)
                m_pLineFilters->Initialize(GetOptionsMgr());
 
        // If there are no filters loaded, and there is filter string in previous
@@ -388,6 +335,9 @@ BOOL CMergeApp::InitInstance()
                        m_pLineFilters->Import(oldFilter);
        }
 
+       if (m_pSubstitutionFiltersList != nullptr)
+               m_pSubstitutionFiltersList->Initialize(GetOptionsMgr());
+
        // Check if filter folder is set, and create it if not
        String pathMyFolders = GetOptionsMgr()->GetString(OPT_FILTER_USERPATH);
        if (pathMyFolders.empty())
@@ -437,8 +387,8 @@ BOOL CMergeApp::InitInstance()
        m_pDiffTemplate = new CMultiDocTemplate(
                IDR_MERGEDOCTYPE,
                RUNTIME_CLASS(CMergeDoc),
-               RUNTIME_CLASS(CChildFrame), // custom MDI child frame
-               RUNTIME_CLASS(CMergeEditView));
+               RUNTIME_CLASS(CMergeEditFrame), // custom MDI child frame
+               RUNTIME_CLASS(CMergeEditSplitterView));
        AddDocTemplate(m_pDiffTemplate);
 
        // Merge Edit view
@@ -461,7 +411,7 @@ BOOL CMergeApp::InitInstance()
        CMainFrame* pMainFrame = new CMainFrame;
        if (!pMainFrame->LoadFrame(IDR_MAINFRAME))
        {
-               if (hMutex)
+               if (hMutex != nullptr)
                {
                        ReleaseMutex(hMutex);
                        CloseHandle(hMutex);
@@ -476,28 +426,22 @@ BOOL CMergeApp::InitInstance()
        // Set the menu
        // Note : for Windows98 compatibility, use FromHandle and not Attach/Detach
        CMenu * pNewMenu = CMenu::FromHandle(pMainFrame->m_hMenuDefault);
-       pMainFrame->MDISetMenu(pNewMenu, NULL);
+       pMainFrame->MDISetMenu(pNewMenu, nullptr);
 
-       // The main window has been initialized, so activate and update it.
+       // The main window has been initialized, so activate it.
        pMainFrame->ActivateFrame(cmdInfo.m_nCmdShow);
-       pMainFrame->UpdateWindow();
 
        // Since this function actually opens paths for compare it must be
        // called after initializing CMainFrame!
-       BOOL bContinue = TRUE;
-       if (ParseArgsAndDoOpen(cmdInfo, pMainFrame) == FALSE && bCommandLineInvoke)
-               bContinue = FALSE;
+       bool bContinue = true;
+       if (!ParseArgsAndDoOpen(cmdInfo, pMainFrame) && bCommandLineInvoke)
+               bContinue = false;
 
-       if (hMutex)
+       if (hMutex != nullptr)
                ReleaseMutex(hMutex);
 
-       if (m_bNonInteractive)
-       {
-               bContinue = FALSE;
-       }
-
        // If user wants to cancel the compare, close WinMerge
-       if (bContinue == FALSE)
+       if (!bContinue)
        {
                pMainFrame->PostMessage(WM_CLOSE, 0, 0);
        }
@@ -514,13 +458,20 @@ static void OpenContributersFile(int&)
        theApp.OpenFileToExternalEditor(paths::ConcatPath(env::GetProgPath(), ContributorsPath));
 }
 
+static void OpenUrl(int&)
+{
+       shell::Open(WinMergeURL);
+}
+
 // App command to run the dialog
 void CMergeApp::OnAppAbout()
 {
        CAboutDlg aboutDlg;
        aboutDlg.m_onclick_contributers += Poco::delegate(OpenContributersFile);
+       aboutDlg.m_onclick_url += Poco::delegate(OpenUrl);
        aboutDlg.DoModal();
        aboutDlg.m_onclick_contributers.clear();
+       aboutDlg.m_onclick_url.clear();
 }
 
 /////////////////////////////////////////////////////////////////////////////
@@ -532,7 +483,7 @@ void CMergeApp::OnAppAbout()
  * good place to do cleanups.
  * @return Application's exit value (returned from WinMain()).
  */
-int CMergeApp::ExitInstance() 
+int CMergeApp::ExitInstance()
 {
        charsets_cleanup();
 
@@ -544,7 +495,7 @@ int CMergeApp::ExitInstance()
        ClearTempfolder(temp);
 
        // Cleanup left over tempfiles from previous instances.
-       // Normally this should not neet to do anything - but if for some reason
+       // Normally this should not need to do anything - but if for some reason
        // WinMerge did not delete temp files this makes sure they are removed.
        CleanupWMtemp();
 
@@ -553,19 +504,19 @@ int CMergeApp::ExitInstance()
        return 0;
 }
 
-int CMergeApp::DoMessageBox( LPCTSTR lpszPrompt, UINT nType, UINT nIDPrompt )
+int CMergeApp::DoMessageBox(LPCTSTR lpszPrompt, UINT nType, UINT nIDPrompt)
 {
        // This is a convenient point for breakpointing !!!
 
        // Create a handle to store the parent window of the message box.
        CWnd* pParentWnd = CWnd::GetActiveWindow();
-       
+
        // Check whether an active window was retrieved successfully.
-       if ( pParentWnd == NULL )
+       if (pParentWnd == nullptr)
        {
                // Try to retrieve a handle to the last active popup.
                CWnd * mainwnd = GetMainWnd();
-               if (mainwnd)
+               if (mainwnd != nullptr)
                        pParentWnd = mainwnd->GetLastActivePopup();
        }
 
@@ -574,12 +525,21 @@ int CMergeApp::DoMessageBox( LPCTSTR lpszPrompt, UINT nType, UINT nIDPrompt )
        // (if caller set the style)
 
        if (m_bNonInteractive)
+       {
+               if (AttachConsole(ATTACH_PARENT_PROCESS))
+               {
+                       DWORD dwWritten;
+                       String line = _T("WinMerge: ") + String(lpszPrompt) + _T("\n");
+                       WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), line.c_str(), static_cast<DWORD>(line.length()), &dwWritten, nullptr);
+                       FreeConsole();
+               }
                return IDCANCEL;
+       }
 
        // Create the message box dialog.
        CMessageBoxDialog dlgMessage(pParentWnd, lpszPrompt, _T(""), nType | MB_RIGHT_ALIGN,
                nIDPrompt);
-       
+
        if (m_pMainWnd->IsIconic())
                m_pMainWnd->ShowWindow(SW_RESTORE);
 
@@ -587,16 +547,23 @@ int CMergeApp::DoMessageBox( LPCTSTR lpszPrompt, UINT nType, UINT nIDPrompt )
        return static_cast<int>(dlgMessage.DoModal());
 }
 
-/** 
- * @brief Set flag so that application will broadcast notification at next
- * idle time (via WM_TIMER id=IDLE_TIMER)
- */
-void CMergeApp::SetNeedIdleTimer()
+bool CMergeApp::IsReallyIdle() const
 {
-       m_bNeedIdleTimer = TRUE; 
+       bool idle = true;
+       POSITION pos = m_pDirTemplate->GetFirstDocPosition();
+       while (pos != nullptr)
+       {
+               CDirDoc *pDirDoc = static_cast<CDirDoc *>(m_pDirTemplate->GetNextDoc(pos));
+               if (const CompareStats *pCompareStats = pDirDoc->GetCompareStats())
+               {
+                       if (!pCompareStats->IsCompareDone() || pDirDoc->GetGeneratingReport())
+                               idle = false;
+               }
+       }
+    return idle;
 }
 
-BOOL CMergeApp::OnIdle(LONG lCount) 
+BOOL CMergeApp::OnIdle(LONG lCount)
 {
        if (CWinApp::OnIdle(lCount))
                return TRUE;
@@ -604,9 +571,18 @@ BOOL CMergeApp::OnIdle(LONG lCount)
        // If anyone has requested notification when next idle occurs, send it
        if (m_bNeedIdleTimer)
        {
-               m_bNeedIdleTimer = FALSE;
+               m_bNeedIdleTimer = false;
                m_pMainWnd->SendMessageToDescendants(WM_TIMER, IDLE_TIMER, lCount, TRUE, FALSE);
        }
+
+       if (m_bNonInteractive && IsReallyIdle())
+               m_pMainWnd->PostMessage(WM_CLOSE, 0, 0);
+
+       if (typeid(*GetOptionsMgr()) == typeid(CRegOptionsMgr))
+       {
+               static_cast<CRegOptionsMgr*>(GetOptionsMgr())->CloseKeys();
+       }
+
        return FALSE;
 }
 
@@ -618,15 +594,37 @@ BOOL CMergeApp::OnIdle(LONG lCount)
  */
 void CMergeApp::InitializeFileFilters()
 {
-       CString filterPath = GetProfileString(_T("Settings"), _T("UserFilterPath"), _T(""));
+       String filterPath = GetOptionsMgr()->GetString(OPT_FILTER_USERPATH);
 
-       if (!filterPath.IsEmpty())
+       if (!filterPath.empty())
        {
-               m_pGlobalFileFilter->SetUserFilterPath((LPCTSTR)filterPath);
+               m_pGlobalFileFilter->SetUserFilterPath(filterPath);
        }
        m_pGlobalFileFilter->LoadAllFileFilters();
 }
 
+void CMergeApp::ApplyCommandLineConfigOptions(MergeCmdLineInfo& cmdInfo)
+{
+       if (cmdInfo.m_bNoPrefs)
+               m_pOptions->SetSerializing(false); // Turn off serializing to registry.
+
+       for (const auto& it : cmdInfo.m_Options)
+       {
+               if (m_pOptions->Set(it.first, it.second) == COption::OPT_NOTFOUND)
+               {
+                       String longname = m_pOptions->ExpandShortName(it.first);
+                       if (!longname.empty())
+                       {
+                               m_pOptions->Set(longname, it.second);
+                       }
+                       else
+                       {
+                               cmdInfo.m_sErrorMessages.push_back(strutils::format_string1(_T("Invalid key '%1' specified in /config option"), it.first));
+                       }
+               }
+       }
+}
+
 /** @brief Read command line arguments and open files for comparison.
  *
  * The name of the function is a legacy code from the time that this function
@@ -634,14 +632,22 @@ void CMergeApp::InitializeFileFilters()
  * MergeCmdLineInfo class.
  * @param [in] cmdInfo Commandline parameters info.
  * @param [in] pMainFrame Pointer to application main frame.
- * @return TRUE if we opened the compare, FALSE if the compare was canceled.
+ * @return `true` if we opened the compare, `false` if the compare was canceled.
  */
-BOOL CMergeApp::ParseArgsAndDoOpen(MergeCmdLineInfo& cmdInfo, CMainFrame* pMainFrame)
+bool CMergeApp::ParseArgsAndDoOpen(MergeCmdLineInfo& cmdInfo, CMainFrame* pMainFrame)
 {
-       BOOL bCompared = FALSE;
+       bool bCompared = false;
        String strDesc[3];
+       std::unique_ptr<PackingInfo> infoUnpacker;
+
        m_bNonInteractive = cmdInfo.m_bNonInteractive;
 
+       if (!cmdInfo.m_sUnpacker.empty())
+       {
+               infoUnpacker.reset(new PackingInfo(PLUGIN_MODE::PLUGIN_MANUAL));
+               infoUnpacker->m_PluginName = cmdInfo.m_sUnpacker;
+       }
+
        // Set the global file filter.
        if (!cmdInfo.m_sFileFilter.empty())
        {
@@ -654,6 +660,10 @@ BOOL CMergeApp::ParseArgsAndDoOpen(MergeCmdLineInfo& cmdInfo, CMainFrame* pMainF
                UpdateDefaultCodepage(2,cmdInfo.m_nCodepage);
        }
 
+       // Set compare method
+       if (cmdInfo.m_nCompMethod.has_value())
+               GetOptionsMgr()->Set(OPT_CMP_METHOD, *cmdInfo.m_nCompMethod);
+
        // Unless the user has requested to see WinMerge's usage open files for
        // comparison.
        if (cmdInfo.m_bShowUsage)
@@ -664,7 +674,6 @@ BOOL CMergeApp::ParseArgsAndDoOpen(MergeCmdLineInfo& cmdInfo, CMainFrame* pMainF
        {
                // Set the required information we need from the command line:
 
-               m_bClearCaseTool = cmdInfo.m_bClearCaseTool;
                m_bExitIfNoDiff = cmdInfo.m_bExitIfNoDiff;
                m_bEscShutdown = cmdInfo.m_bEscShutdown;
 
@@ -688,20 +697,26 @@ BOOL CMergeApp::ParseArgsAndDoOpen(MergeCmdLineInfo& cmdInfo, CMainFrame* pMainF
                        cmdInfo.m_dwRightFlags |= FFILEOPEN_CMDLINE;
                        DWORD dwFlags[3] = {cmdInfo.m_dwLeftFlags, cmdInfo.m_dwMiddleFlags, cmdInfo.m_dwRightFlags};
                        bCompared = pMainFrame->DoFileOpen(&cmdInfo.m_Files,
-                               dwFlags, strDesc, cmdInfo.m_sReportFile, cmdInfo.m_bRecurse, NULL,
-                               cmdInfo.m_sPreDiffer);
+                               dwFlags, strDesc, cmdInfo.m_sReportFile, cmdInfo.m_bRecurse, nullptr,
+                               cmdInfo.m_sPreDiffer, infoUnpacker.get());
                }
                else if (cmdInfo.m_Files.GetSize() > 1)
                {
                        DWORD dwFlags[3] = {cmdInfo.m_dwLeftFlags, cmdInfo.m_dwRightFlags, FFILEOPEN_NONE};
                        bCompared = pMainFrame->DoFileOpen(&cmdInfo.m_Files,
-                               dwFlags, strDesc, cmdInfo.m_sReportFile, cmdInfo.m_bRecurse, NULL,
-                               cmdInfo.m_sPreDiffer);
+                               dwFlags, strDesc, cmdInfo.m_sReportFile, cmdInfo.m_bRecurse, nullptr,
+                               cmdInfo.m_sPreDiffer, infoUnpacker.get());
                }
                else if (cmdInfo.m_Files.GetSize() == 1)
                {
                        String sFilepath = cmdInfo.m_Files[0];
-                       if (IsProjectFile(sFilepath))
+                       if (cmdInfo.m_bSelfCompare)
+                       {
+                               strDesc[0] = cmdInfo.m_sLeftDesc;
+                               strDesc[1] = cmdInfo.m_sRightDesc;
+                               bCompared = pMainFrame->DoSelfCompare(IDOK, sFilepath, strDesc);
+                       }
+                       else if (IsProjectFile(sFilepath))
                        {
                                bCompared = LoadAndOpenProjectFile(sFilepath);
                        }
@@ -717,13 +732,13 @@ BOOL CMergeApp::ParseArgsAndDoOpen(MergeCmdLineInfo& cmdInfo, CMainFrame* pMainF
                        {
                                DWORD dwFlags[3] = {cmdInfo.m_dwLeftFlags, cmdInfo.m_dwRightFlags, FFILEOPEN_NONE};
                                bCompared = pMainFrame->DoFileOpen(&cmdInfo.m_Files,
-                                       dwFlags, strDesc, cmdInfo.m_sReportFile, cmdInfo.m_bRecurse, NULL, 
-                                       cmdInfo.m_sPreDiffer);
+                                       dwFlags, strDesc, cmdInfo.m_sReportFile, cmdInfo.m_bRecurse, nullptr,
+                                       cmdInfo.m_sPreDiffer, infoUnpacker.get());
                        }
                }
                else if (cmdInfo.m_Files.GetSize() == 0) // if there are no input args, we can check the display file dialog flag
                {
-                       BOOL showFiles = m_pOptions->GetBool(OPT_SHOW_SELECT_FILES_AT_STARTUP);
+                       bool showFiles = m_pOptions->GetBool(OPT_SHOW_SELECT_FILES_AT_STARTUP);
                        if (showFiles)
                                pMainFrame->DoFileOpen();
                }
@@ -753,7 +768,7 @@ void CMergeApp::UpdateDefaultCodepage(int cpDefaultMode, int cpCustomCodepage)
                        break;
                default:
                        // no other valid option
-                       assert (0);
+                       assert (false);
                        ucr::setDefaultCodepage(GetACP());
        }
 }
@@ -809,12 +824,12 @@ void CMergeApp::OpenFileToExternalEditor(const String& file, int nLineNumber/* =
                sCmd += _T("\"");
        }
 
-       BOOL retVal = FALSE;
+       bool retVal = false;
        STARTUPINFO stInfo = { sizeof STARTUPINFO };
        PROCESS_INFORMATION processInfo;
 
-       retVal = CreateProcess(NULL, (LPTSTR)sCmd.c_str(),
-               NULL, NULL, FALSE, CREATE_DEFAULT_ERROR_MODE, NULL, NULL,
+       retVal = !!CreateProcess(nullptr, (LPTSTR)sCmd.c_str(),
+               nullptr, nullptr, FALSE, CREATE_DEFAULT_ERROR_MODE, nullptr, nullptr,
                &stInfo, &processInfo);
 
        if (!retVal)
@@ -831,41 +846,30 @@ void CMergeApp::OpenFileToExternalEditor(const String& file, int nLineNumber/* =
 }
 
 /**
- * @brief Open file, if it exists, else open url
- */
-void CMergeApp::OpenFileOrUrl(LPCTSTR szFile, LPCTSTR szUrl)
-{
-       if (paths::DoesPathExist(szFile) == paths::IS_EXISTING_FILE)
-               ShellExecute(NULL, _T("open"), _T("notepad.exe"), szFile, NULL, SW_SHOWNORMAL);
-       else
-               ShellExecute(NULL, _T("open"), szUrl, NULL, NULL, SW_SHOWNORMAL);
-}
-
-/**
  * @brief Show Help - this is for opening help from outside mainframe.
- * @param [in] helpLocation Location inside help, if NULL main help is opened.
+ * @param [in] helpLocation Location inside help, if `nullptr` main help is opened.
  */
-void CMergeApp::ShowHelp(LPCTSTR helpLocation /*= NULL*/)
+void CMergeApp::ShowHelp(LPCTSTR helpLocation /*= nullptr*/)
 {
-       String sPath = env::GetProgPath();
+       String name, ext;
        LANGID LangId = GetLangId();
-       if (PRIMARYLANGID(LangId) == LANG_JAPANESE)
-               sPath = paths::ConcatPath(sPath, DocsPath_ja);
-       else
-               sPath = paths::ConcatPath(sPath, DocsPath);
-       if (helpLocation == NULL)
+       paths::SplitFilename(m_pLangDlg->GetFileName(LangId), nullptr, &name, &ext);
+       String sPath = paths::ConcatPath(env::GetProgPath(), strutils::format(DocsPath, name.c_str()));
+       if (paths::DoesPathExist(sPath) != paths::IS_EXISTING_FILE)
+               sPath = paths::ConcatPath(env::GetProgPath(), strutils::format(DocsPath, _T("")));
+       if (helpLocation == nullptr)
        {
                if (paths::DoesPathExist(sPath) == paths::IS_EXISTING_FILE)
-                       ::HtmlHelp(NULL, sPath.c_str(), HH_DISPLAY_TOC, NULL);
+                       ::HtmlHelp(nullptr, sPath.c_str(), HH_DISPLAY_TOC, NULL);
                else
-                       ShellExecute(NULL, _T("open"), DocsURL, NULL, NULL, SW_SHOWNORMAL);
+                       shell::Open(DocsURL);
        }
        else
        {
                if (paths::DoesPathExist(sPath) == paths::IS_EXISTING_FILE)
                {
                        sPath += helpLocation;
-                       ::HtmlHelp(NULL, sPath.c_str(), HH_DISPLAY_TOPIC, NULL);
+                       ::HtmlHelp(nullptr, sPath.c_str(), HH_DISPLAY_TOPIC, NULL);
                }
        }
 }
@@ -879,17 +883,17 @@ void CMergeApp::ShowHelp(LPCTSTR helpLocation /*= NULL*/)
  * succeeded or failed.
  * @param [in] bFolder Are we creating backup in folder compare?
  * @param [in] pszPath Full path to file to backup.
- * @return TRUE if backup succeeds, or isn't just done.
+ * @return `true` if backup succeeds, or isn't just done.
  */
-BOOL CMergeApp::CreateBackup(BOOL bFolder, const String& pszPath)
+bool CMergeApp::CreateBackup(bool bFolder, const String& pszPath)
 {
        // If user doesn't want to backups in folder compare, return
        // success so operations don't abort.
        if (bFolder && !(GetOptionsMgr()->GetBool(OPT_BACKUP_FOLDERCMP)))
-               return TRUE;
+               return true;
        // Likewise if user doesn't want backups in file compare
        else if (!bFolder && !(GetOptionsMgr()->GetBool(OPT_BACKUP_FILECMP)))
-               return TRUE;
+               return true;
 
        // create backup copy of file if destination file exists
        if (paths::DoesPathExist(pszPath) == paths::IS_EXISTING_FILE)
@@ -898,7 +902,7 @@ BOOL CMergeApp::CreateBackup(BOOL bFolder, const String& pszPath)
                String path;
                String filename;
                String ext;
-       
+
                paths::SplitFilename(paths::GetLongPath(pszPath), &path, &filename, &ext);
 
                // Determine backup folder
@@ -923,7 +927,7 @@ BOOL CMergeApp::CreateBackup(BOOL bFolder, const String& pszPath)
                        _RPTF0(_CRT_ERROR, "Unknown backup location!");
                }
 
-               BOOL success = FALSE;
+               bool success = false;
                if (GetOptionsMgr()->GetBool(OPT_BACKUP_ADD_BAK))
                {
                        // Don't add dot if there is no existing extension
@@ -937,73 +941,44 @@ BOOL CMergeApp::CreateBackup(BOOL bFolder, const String& pszPath)
                // nice way to add a real time (invalid chars etc).
                if (GetOptionsMgr()->GetBool(OPT_BACKUP_ADD_TIME))
                {
-                       struct tm *tm;
+                       struct tm tm;
                        time_t curtime = 0;
                        time(&curtime);
-                       tm = localtime(&curtime);
+                       ::localtime_s(&tm, &curtime);
                        CString timestr;
-                       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);
+                       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);
                        filename += _T("-");
                        filename += timestr;
                }
 
                // Append filename and extension (+ optional .bak) to path
                if ((bakPath.length() + filename.length() + ext.length())
-                       < MAX_PATH)
+                       < MAX_PATH_FULL)
                {
-                       success = TRUE;
+                       success = true;
                        bakPath = paths::ConcatPath(bakPath, filename);
                        bakPath += _T(".");
                        bakPath += ext;
                }
 
                if (success)
-                       success = CopyFile(pszPath.c_str(), bakPath.c_str(), FALSE);
-               
+               {
+                       success = !!CopyFileW(TFile(pszPath).wpath().c_str(), TFile(bakPath).wpath().c_str(), FALSE);
+               }
+
                if (!success)
                {
                        String msg = strutils::format_string1(
                                _("Unable to backup original file:\n%1\n\nContinue anyway?"),
                                pszPath);
                        if (AfxMessageBox(msg.c_str(), MB_YESNO | MB_ICONWARNING | MB_DONT_ASK_AGAIN) != IDYES)
-                               return FALSE;
+                               return false;
                }
-               return TRUE;
+               return true;
        }
 
        // we got here because we're either not backing up of there was nothing to backup
-       return TRUE;
-}
-
-/**
- * @brief Sync file to Version Control System
- * @param pszDest [in] Where to copy (incl. filename)
- * @param bApplyToAll [in,out] Apply user selection to all items
- * @param psError [out] Error string that can be shown to user in caller func
- * @return User selection or -1 if error happened
- * @sa CMainFrame::HandleReadonlySave()
- * @sa CDirView::PerformActionList()
- */
-int CMergeApp::SyncFileToVCS(const String& pszDest, BOOL &bApplyToAll,
-       String& sError)
-{
-       String sActionError;
-       String strSavePath(pszDest);
-       int nVerSys = 0;
-
-       nVerSys = GetOptionsMgr()->GetInt(OPT_VCS_SYSTEM);
-       
-       int nRetVal = HandleReadonlySave(strSavePath, TRUE, bApplyToAll);
-       if (nRetVal == IDCANCEL || nRetVal == IDNO)
-               return nRetVal;
-       
-       // If VC project opened from VSS sync and version control used
-       if ((nVerSys == SourceControl::VCS_VSS4 || nVerSys == SourceControl::VCS_VSS5) && m_pSourceControl->m_bVCProjSync)
-       {
-               if (!m_pSourceControl->m_vssHelper.ReLinkVCProj(strSavePath, sError))
-                       nRetVal = -1;
-       }
-       return nRetVal;
+       return true;
 }
 
 /**
@@ -1020,17 +995,16 @@ int CMergeApp::SyncFileToVCS(const String& pszDest, BOOL &bApplyToAll,
  * @sa CMainFrame::SyncFileToVCS()
  * @sa CMergeDoc::DoSave()
  */
-int CMergeApp::HandleReadonlySave(String& strSavePath, BOOL bMultiFile,
-               BOOL &bApplyToAll)
+int CMergeApp::HandleReadonlySave(String& strSavePath, bool bMultiFile,
+               bool &bApplyToAll)
 {
        CFileStatus status;
        int nRetVal = IDOK;
-       BOOL bFileRO = FALSE;
-       BOOL bFileExists = FALSE;
+       bool bFileRO = false;
+       bool bFileExists = false;
        String s;
        String str;
        CString title;
-       int nVerSys = 0;
 
        if (!strSavePath.empty())
        {
@@ -1046,23 +1020,10 @@ int CMergeApp::HandleReadonlySave(String& strSavePath, BOOL bMultiFile,
                }
        }
 
-       nVerSys = GetOptionsMgr()->GetInt(OPT_VCS_SYSTEM);
-       
        if (bFileExists && bFileRO)
        {
                UINT userChoice = 0;
-               // Version control system used?
-               // Checkout file from VCS and modify, don't ask about overwriting
-               // RO files etc.
-               if (nVerSys != SourceControl::VCS_NONE)
-               {
-                       bool bRetVal = m_pSourceControl->SaveToVersionControl(strSavePath);
-                       if (bRetVal)
-                               return IDYES;
-                       else
-                               return IDCANCEL;
-               }
-               
+
                // Don't ask again if its already asked
                if (bApplyToAll)
                        userChoice = IDYES;
@@ -1080,7 +1041,7 @@ int CMergeApp::HandleReadonlySave(String& strSavePath, BOOL bMultiFile,
                        else
                        {
                                // Single file
-                               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);
+                               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);
                                userChoice = AfxMessageBox(str.c_str(), MB_YESNOCANCEL |
                                                MB_ICONWARNING | MB_DEFBUTTON2 | MB_DONT_ASK_AGAIN,
                                                IDS_SAVEREADONLY_FMT);
@@ -1090,7 +1051,8 @@ int CMergeApp::HandleReadonlySave(String& strSavePath, BOOL bMultiFile,
                {
                // Overwrite read-only file
                case IDYESTOALL:
-                       bApplyToAll = TRUE;  // Don't ask again (no break here)
+                       bApplyToAll = true;  // Don't ask again (no break here)
+                       [[fallthrough]];
                case IDYES:
                        CFile::GetStatus(strSavePath.c_str(), status);
                        status.m_mtime = 0;             // Avoid unwanted changes
@@ -1098,12 +1060,12 @@ int CMergeApp::HandleReadonlySave(String& strSavePath, BOOL bMultiFile,
                        CFile::SetStatus(strSavePath.c_str(), status);
                        nRetVal = IDYES;
                        break;
-               
+
                // Save to new filename (single) /skip this item (multiple)
                case IDNO:
                        if (!bMultiFile)
                        {
-                               if (SelectFile(AfxGetMainWnd()->GetSafeHwnd(), s, strSavePath.c_str(), _("Save As"), _T(""), FALSE))
+                               if (SelectFile(AfxGetMainWnd()->GetSafeHwnd(), s, false, strSavePath.c_str()))
                                {
                                        strSavePath = s;
                                        nRetVal = IDNO;
@@ -1125,37 +1087,6 @@ int CMergeApp::HandleReadonlySave(String& strSavePath, BOOL bMultiFile,
 }
 
 /**
- * @brief Shows VSS error from exception and writes log.
- */
-void CMergeApp::ShowVSSError(CException *e, const String& strItem)
-{
-       TCHAR errStr[1024] = {0};
-       if (e->GetErrorMessage(errStr, 1024))
-       {
-               String errMsg = theApp.LoadString(IDS_VSS_ERRORFROM);
-               String logMsg = errMsg;
-               errMsg += _T("\n");
-               errMsg += errStr;
-               logMsg += _T(" ");
-               logMsg += errStr;
-               if (!strItem.empty())
-               {
-                       errMsg += _T("\n\n");
-                       errMsg += strItem;
-                       logMsg += _T(": ");
-                       logMsg += strItem;
-               }
-               LogErrorString(logMsg);
-               AfxMessageBox(errMsg.c_str(), MB_ICONSTOP);
-       }
-       else
-       {
-               LogErrorString(_T("VSSError (unable to GetErrorMessage)"));
-               e->ReportError(MB_ICONSTOP, IDS_VSS_RUN_ERROR);
-       }
-}
-
-/**
  * @brief Is specified file a project file?
  * @param [in] filepath Full path to file to check.
  * @return true if file is a projectfile.
@@ -1163,7 +1094,7 @@ void CMergeApp::ShowVSSError(CException *e, const String& strItem)
 bool CMergeApp::IsProjectFile(const String& filepath) const
 {
        String ext;
-       paths::SplitFilename(filepath, NULL, NULL, &ext);
+       paths::SplitFilename(filepath, nullptr, nullptr, &ext);
        if (strutils::compare_nocase(ext, ProjectFile::PROJECTFILE_EXT) == 0)
                return true;
        else
@@ -1181,7 +1112,7 @@ bool CMergeApp::LoadProjectFile(const String& sProject, ProjectFile &project)
        }
        catch (Poco::Exception& e)
        {
-               String sErr = _("Unknown error attempting to open project file");
+               String sErr = _("Unknown error attempting to open project file.");
                sErr += ucr::toTString(e.displayText());
                String msg = strutils::format_string2(_("Cannot open file\n%1\n\n%2"), sProject, sErr);
                AfxMessageBox(msg.c_str(), MB_ICONSTOP);
@@ -1199,7 +1130,7 @@ bool CMergeApp::SaveProjectFile(const String& sProject, const ProjectFile &proje
        }
        catch (Poco::Exception& e)
        {
-               String sErr = _("Unknown error attempting to save project file");
+               String sErr = _("Unknown error attempting to save project file.");
                sErr += ucr::toTString(e.displayText());
                String msg = strutils::format_string2(_("Cannot open file\n%1\n\n%2"), sProject, sErr);
                AfxMessageBox(msg.c_str(), MB_ICONSTOP);
@@ -1209,61 +1140,76 @@ bool CMergeApp::SaveProjectFile(const String& sProject, const ProjectFile &proje
        return true;
 }
 
-/** 
+/**
  * @brief Read project and perform comparison specified
  * @param [in] sProject Full path to project file.
- * @return TRUE if loading project file and starting compare succeeded.
+ * @return `true` if loading project file and starting compare succeeded.
  */
 bool CMergeApp::LoadAndOpenProjectFile(const String& sProject, const String& sReportFile)
 {
        ProjectFile project;
        if (!LoadProjectFile(sProject, project))
                return false;
-       
-       PathContext files;
-       BOOL bLeftReadOnly = FALSE;
-       BOOL bMiddleReadOnly = FALSE;
-       BOOL bRightReadOnly = FALSE;
-       bool bRecursive = FALSE;
-       project.GetPaths(files, bRecursive);
-       bLeftReadOnly = project.GetLeftReadOnly();
-       bMiddleReadOnly = project.GetMiddleReadOnly();
-       bRightReadOnly = project.GetRightReadOnly();
-       if (project.HasFilter())
-       {
-               String filter = project.GetFilter();
-               filter = strutils::trim_ws(filter);
-               m_pGlobalFileFilter->SetFilter(filter);
-       }
-       if (project.HasSubfolders())
-               bRecursive = project.GetSubfolders() > 0;
-
-       DWORD dwFlags[3] = {
-               static_cast<DWORD>(files.GetPath(0).empty() ? FFILEOPEN_NONE : FFILEOPEN_PROJECT),
-               static_cast<DWORD>(files.GetPath(1).empty() ? FFILEOPEN_NONE : FFILEOPEN_PROJECT),
-               static_cast<DWORD>(files.GetPath(2).empty() ? FFILEOPEN_NONE : FFILEOPEN_PROJECT)
-       };
-       if (bLeftReadOnly)
-               dwFlags[0] |= FFILEOPEN_READONLY;
-       if (files.GetSize() == 2)
-       {
-               if (bRightReadOnly)
-                       dwFlags[1] |= FFILEOPEN_READONLY;
-       }
-       else
+
+       bool rtn = true;
+       for (auto& projItem : project.Items())
        {
-               if (bMiddleReadOnly)
-                       dwFlags[1] |= FFILEOPEN_READONLY;
-               if (bRightReadOnly)
-                       dwFlags[2] |= FFILEOPEN_READONLY;
-       }
+               PathContext tFiles;
+               bool bRecursive = false;
+               projItem.GetPaths(tFiles, bRecursive);
+               for (int i = 0; i < tFiles.GetSize(); ++i)
+               {
+                       if (!paths::IsPathAbsolute(tFiles[i]))
+                       {
+                               String sProjectDir = paths::GetParentPath(sProject);
+                               if (tFiles[i].substr(0, 1) == _T("\\"))
+                               {
+                                       if (sProjectDir.length() > 1 && sProjectDir[1] == ':')
+                                               tFiles[i] = paths::ConcatPath(sProjectDir.substr(0, 2), tFiles[i]);
+                               }
+                               else
+                                       tFiles[i] = paths::ConcatPath(sProjectDir, tFiles[i]);
+                       }
+               }
+               bool bLeftReadOnly = projItem.GetLeftReadOnly();
+               bool bMiddleReadOnly = projItem.GetMiddleReadOnly();
+               bool bRightReadOnly = projItem.GetRightReadOnly();
+               if (projItem.HasFilter())
+               {
+                       String filter = projItem.GetFilter();
+                       filter = strutils::trim_ws(filter);
+                       m_pGlobalFileFilter->SetFilter(filter);
+               }
+               if (projItem.HasSubfolders())
+                       bRecursive = projItem.GetSubfolders() > 0;
+
+               DWORD dwFlags[3] = {
+                       static_cast<DWORD>(tFiles.GetPath(0).empty() ? FFILEOPEN_NONE : FFILEOPEN_PROJECT),
+                       static_cast<DWORD>(tFiles.GetPath(1).empty() ? FFILEOPEN_NONE : FFILEOPEN_PROJECT),
+                       static_cast<DWORD>(tFiles.GetPath(2).empty() ? FFILEOPEN_NONE : FFILEOPEN_PROJECT)
+               };
+               if (bLeftReadOnly)
+                       dwFlags[0] |= FFILEOPEN_READONLY;
+               if (tFiles.GetSize() == 2)
+               {
+                       if (bRightReadOnly)
+                               dwFlags[1] |= FFILEOPEN_READONLY;
+               }
+               else
+               {
+                       if (bMiddleReadOnly)
+                               dwFlags[1] |= FFILEOPEN_READONLY;
+                       if (bRightReadOnly)
+                               dwFlags[2] |= FFILEOPEN_READONLY;
+               }
+
+               GetOptionsMgr()->SaveOption(OPT_CMP_INCLUDE_SUBDIRS, bRecursive);
 
-       GetOptionsMgr()->SaveOption(OPT_CMP_INCLUDE_SUBDIRS, bRecursive);
-       
-       BOOL rtn = GetMainFrame()->DoFileOpen(&files, dwFlags, NULL, sReportFile, bRecursive);
+               rtn &= GetMainFrame()->DoFileOpen(&tFiles, dwFlags, nullptr, sReportFile, bRecursive);
+       }
 
        AddToRecentProjectsMRU(sProject.c_str());
-       return !!rtn;
+       return rtn;
 }
 
 /**
@@ -1329,9 +1275,9 @@ std::wstring CMergeApp::LoadDialogCaption(LPCTSTR lpDialogTemplateID) const
  */
 void CMergeApp::AddToRecentProjectsMRU(LPCTSTR sPathName)
 {
-       // sPathName will be added to the top of the MRU list. 
+       // sPathName will be added to the top of the MRU list.
        // If sPathName already exists in the MRU list, it will be moved to the top
-       if (m_pRecentFileList != NULL)    {
+       if (m_pRecentFileList != nullptr)    {
                m_pRecentFileList->Add(sPathName);
                m_pRecentFileList->WriteList();
        }
@@ -1404,3 +1350,47 @@ void CMergeApp::OnUpdateMergingStatus(CCmdUI *pCmdUI)
        pCmdUI->Enable(GetMergingMode());
 }
 
+UINT CMergeApp::GetProfileInt(LPCTSTR lpszSection, LPCTSTR lpszEntry, int nDefault)
+{
+       COptionsMgr *pOptions = GetOptionsMgr();
+       String name = strutils::format(_T("%s/%s"), lpszSection, lpszEntry);
+       if (!pOptions->Get(name).IsInt())
+               pOptions->InitOption(name, nDefault);
+       return pOptions->GetInt(name);
+}
+
+BOOL CMergeApp::WriteProfileInt(LPCTSTR lpszSection, LPCTSTR lpszEntry, int nValue)
+{
+       COptionsMgr *pOptions = GetOptionsMgr();
+       String name = strutils::format(_T("%s/%s"), lpszSection, lpszEntry);
+       if (!pOptions->Get(name).IsInt())
+               pOptions->InitOption(name, nValue);
+       return pOptions->SaveOption(name, nValue) == COption::OPT_OK;
+}
+
+CString CMergeApp::GetProfileString(LPCTSTR lpszSection, LPCTSTR lpszEntry, LPCTSTR lpszDefault)
+{
+       COptionsMgr *pOptions = GetOptionsMgr();
+       String name = strutils::format(_T("%s/%s"), lpszSection, lpszEntry);
+       if (!pOptions->Get(name).IsString())
+               pOptions->InitOption(name, lpszDefault ? lpszDefault : _T(""));
+       return pOptions->GetString(name).c_str();
+}
+
+BOOL CMergeApp::WriteProfileString(LPCTSTR lpszSection, LPCTSTR lpszEntry, LPCTSTR lpszValue)
+{
+       COptionsMgr *pOptions = GetOptionsMgr();
+       if (lpszEntry != nullptr)
+       {
+               String name = strutils::format(_T("%s/%s"), lpszSection, lpszEntry);
+               if (!pOptions->Get(name).IsString())
+                       pOptions->InitOption(name, lpszValue ? lpszValue : _T(""));
+               return pOptions->SaveOption(name, lpszValue ? lpszValue : _T("")) == COption::OPT_OK;
+       }
+       else
+       {
+               String name = strutils::format(_T("%s/"), lpszSection);
+               pOptions->RemoveOption(name);
+       }
+       return TRUE;
+}