OSDN Git Service

Fix issue #1326: Backup files: Misleading error message when file cannot be written
[winmerge-jp/winmerge-jp.git] / Src / Merge.cpp
index 22256e7..9356899 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 "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 "stringdiffs.h"
 #include "TFile.h"
 #include "paths.h"
+#include "Shell.h"
 #include "CompareStats.h"
 #include "TestMain.h"
 #include "charsets.h" // For shutdown cleanup
+#include "OptionsProject.h"
 
 #ifdef _DEBUG
 #define new DEBUG_NEW
@@ -93,12 +81,12 @@ BEGIN_MESSAGE_MAP(CMergeApp, CWinApp)
        ON_COMMAND(ID_FILE_MERGINGMODE, OnMergingMode)
        ON_UPDATE_COMMAND_UI(ID_FILE_MERGINGMODE, OnUpdateMergingMode)
        ON_UPDATE_COMMAND_UI(ID_STATUS_MERGINGMODE, OnUpdateMergingStatus)
+       ON_COMMAND(ID_FILE_PRINT_SETUP, CWinApp::OnFilePrintSetup)
        //}}AFX_MSG_MAP
        // Standard file based document commands
        //ON_COMMAND(ID_FILE_NEW, CWinApp::OnFileNew)
        //ON_COMMAND(ID_FILE_OPEN, CWinApp::OnFileOpen)
        // Standard print setup command
-       ON_COMMAND(ID_FILE_PRINT_SETUP, CWinApp::OnFilePrintSetup)
 END_MESSAGE_MAP()
 
 /////////////////////////////////////////////////////////////////////////////
@@ -111,24 +99,92 @@ CMergeApp::CMergeApp() :
 , m_pHexMergeTemplate(nullptr)
 , m_pDirTemplate(nullptr)
 , m_mainThreadScripts(nullptr)
-, m_nLastCompareResult(0)
+, m_nLastCompareResult(-1)
 , m_bNonInteractive(false)
-, m_pOptions(new CRegOptionsMgr())
-, m_pGlobalFileFilter(new FileFilterHelper())
+, m_pOptions(nullptr)
+, m_pGlobalFileFilter(nullptr)
 , m_nActiveOperations(0)
 , m_pLangDlg(new CLanguageSelect())
 , m_bEscShutdown(false)
-, m_bExitIfNoDiff(MergeCmdLineInfo::Disabled)
+, 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_bMergingMode(false)
+, m_bEnableExitCode(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.
+ */
+static COptionsMgr *CreateOptionManager(const MergeCmdLineInfo& cmdInfo)
+{
+       String iniFilePath = cmdInfo.m_sIniFilepath;
+       if (!iniFilePath.empty())
+       {
+               iniFilePath = paths::GetLongPath(iniFilePath);
+               if (paths::CreateIfNeeded(paths::GetParentPath(iniFilePath)))
+                       return new CIniOptionsMgr(iniFilePath);
+       }
+       iniFilePath = paths::ConcatPath(env::GetProgPath(), _T("winmerge.ini"));
+       if (paths::DoesPathExist(iniFilePath) == paths::IS_EXISTING_FILE)
+               return new CIniOptionsMgr(iniFilePath);
+       return new CRegOptionsMgr();
+}
+
+static HANDLE CreateMutexHandle()
+{
+       // Create exclusion mutex name
+       TCHAR szDesktopName[MAX_PATH] = _T("Win9xDesktop");
+       DWORD dwLengthNeeded;
+       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);
+       return CreateMutex(nullptr, FALSE, szMutexName);
+}
+
+static HWND ActivatePreviousInstanceAndSendCommandline(LPTSTR cmdLine)
+{
+       HWND hWnd = FindWindow(CMainFrame::szClassName, nullptr);
+       if (hWnd == nullptr)
+               return nullptr;
+       if (IsIconic(hWnd))
+               ShowWindow(hWnd, SW_RESTORE);
+       SetForegroundWindow(GetLastActivePopup(hWnd));
+       COPYDATASTRUCT data = { 0, (lstrlen(cmdLine) + 1) * sizeof(TCHAR), cmdLine };
+       if (!SendMessage(hWnd, WM_COPYDATA, NULL, (LPARAM)&data))
+               return nullptr;
+       return hWnd;
+}
+
+static void WaitForExitPreviousInstance(HWND hWnd)
+{
+       DWORD dwProcessId = 0;
+       GetWindowThreadProcessId(hWnd, &dwProcessId);
+       HANDLE hProcess = OpenProcess(SYNCHRONIZE, FALSE, dwProcessId);
+       if (hProcess)
+               WaitForSingleObject(hProcess, INFINITE);
+}
+
+static int ConvertLastCompareResultToExitCode(int nLastCompareResult)
+{
+       if (nLastCompareResult == 0)
+               return 0;
+       else if (nLastCompareResult > 0)
+               return 1;
+       return 2;
+}
+
 CMergeApp::~CMergeApp()
 {
        strdiff::Close();
@@ -164,6 +220,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)
@@ -198,10 +256,18 @@ BOOL CMergeApp::InitInstance()
        env::LoadRegistryFromFile(paths::ConcatPath(env::GetProgPath(), _T("WinMerge.reg")));
 
        // Parse command-line arguments.
+#ifdef TEST_WINMERGE
+       MergeCmdLineInfo cmdInfo(_T(""));
+#else
        MergeCmdLineInfo cmdInfo(GetCommandLine());
+#endif
+       m_pOptions.reset(CreateOptionManager(cmdInfo));
        if (cmdInfo.m_bNoPrefs)
                m_pOptions->SetSerializing(false); // Turn off serializing to registry.
 
+       if (dynamic_cast<CRegOptionsMgr*>(m_pOptions.get()) != nullptr)
+               Options::CopyHKLMValues();
+
        Options::Init(m_pOptions.get()); // Implementation in OptionsInit.cpp
        ApplyCommandLineConfigOptions(cmdInfo);
        if (cmdInfo.m_sErrorMessages.size() > 0)
@@ -229,61 +295,33 @@ BOOL CMergeApp::InitInstance()
        // 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, 
-               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(nullptr, FALSE, szMutexName);
+       HANDLE hMutex = CreateMutexHandle();
        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, nullptr);
+               HWND hWnd = ActivatePreviousInstanceAndSendCommandline(GetCommandLine());
                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))
-                       {
-                               ReleaseMutex(hMutex);
-                               CloseHandle(hMutex);
-                               return FALSE;
-                       }
+                       ReleaseMutex(hMutex);
+                       CloseHandle(hMutex);
+                       if (nSingleInstance != 1)
+                               WaitForExitPreviousInstance(hWnd);
+                       return FALSE;
                }
        }
 
        LoadStdProfileSettings(GetOptionsMgr()->GetInt(OPT_MRU_MAX));  // Load standard INI file options (including MRU)
 
-       InitializeFileFilters();
-
-       // 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);
-       if (!bFilterSet)
-       {
-               String filter = m_pGlobalFileFilter->GetFilterNameOrMask();
-               m_pOptions->SaveOption(OPT_FILEFILTER_CURRENT, filter);
-       }
-
        charsets_init();
        UpdateCodepageModule();
 
-       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));
+       FileTransform::AutoUnpacking = GetOptionsMgr()->GetBool(OPT_PLUGINS_UNPACKER_MODE);
+       FileTransform::AutoPrediffing = GetOptionsMgr()->GetBool(OPT_PLUGINS_PREDIFFER_MODE);
 
        NONCLIENTMETRICS ncm = { sizeof NONCLIENTMETRICS };
        if (SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof NONCLIENTMETRICS, &ncm, 0))
@@ -297,11 +335,13 @@ BOOL CMergeApp::InitInstance()
        }
 
        if (m_pSyntaxColors != nullptr)
-               Options::SyntaxColors::Load(GetOptionsMgr(), m_pSyntaxColors.get());
+               Options::SyntaxColors::Init(GetOptionsMgr(), m_pSyntaxColors.get());
 
        if (m_pMarkers != nullptr)
                m_pMarkers->LoadFromRegistry();
 
+       CCrystalTextView::SetRenderingModeDefault(static_cast<CCrystalTextView::RENDERING_MODE>(GetOptionsMgr()->GetInt(OPT_RENDERING_MODE)));
+
        if (m_pLineFilters != nullptr)
                m_pLineFilters->Initialize(GetOptionsMgr());
 
@@ -314,6 +354,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())
@@ -321,7 +364,7 @@ BOOL CMergeApp::InitInstance()
                // No filter path, set it to default and make sure it exists.
                pathMyFolders = GetOptionsMgr()->GetDefault<String>(OPT_FILTER_USERPATH);
                GetOptionsMgr()->SaveOption(OPT_FILTER_USERPATH, pathMyFolders);
-               theApp.m_pGlobalFileFilter->SetUserFilterPath(pathMyFolders);
+               theApp.GetGlobalFileFilter()->SetUserFilterPath(pathMyFolders);
        }
        if (!paths::CreateIfNeeded(pathMyFolders))
        {
@@ -404,9 +447,8 @@ BOOL CMergeApp::InitInstance()
        CMenu * pNewMenu = CMenu::FromHandle(pMainFrame->m_hMenuDefault);
        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!
@@ -432,7 +474,12 @@ BOOL CMergeApp::InitInstance()
 
 static void OpenContributersFile(int&)
 {
-       theApp.OpenFileToExternalEditor(paths::ConcatPath(env::GetProgPath(), ContributorsPath));
+       CMergeApp::OpenFileToExternalEditor(paths::ConcatPath(env::GetProgPath(), ContributorsPath));
+}
+
+static void OpenUrl(int&)
+{
+       shell::Open(WinMergeURL);
 }
 
 // App command to run the dialog
@@ -440,8 +487,10 @@ 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();
 }
 
 /////////////////////////////////////////////////////////////////////////////
@@ -453,7 +502,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();
 
@@ -465,13 +514,26 @@ 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();
 
        delete m_mainThreadScripts;
        CWinApp::ExitInstance();
-       return 0;
+       
+#ifndef _DEBUG
+       // There is a problem that OleUninitialize() in mfc/oleinit.cpp, which is called just before the process exits,
+       // hangs in rare cases.
+       // To deal with this problem, force the process to exit
+       // if the process does not exit within 2 seconds after the call to CMergeApp::ExitInstance().
+       _beginthreadex(0, 0,
+               [](void*) -> unsigned int {
+                       Sleep(2000);
+                       ExitProcess(0);
+               }, nullptr, 0, nullptr);
+#endif
+
+       return m_bEnableExitCode ? ConvertLastCompareResultToExitCode(m_nLastCompareResult) : 0;
 }
 
 int CMergeApp::DoMessageBox(LPCTSTR lpszPrompt, UINT nType, UINT nIDPrompt)
@@ -509,7 +571,7 @@ int CMergeApp::DoMessageBox(LPCTSTR lpszPrompt, UINT nType, UINT nIDPrompt)
        // Create the message box dialog.
        CMessageBoxDialog dlgMessage(pParentWnd, lpszPrompt, _T(""), nType | MB_RIGHT_ALIGN,
                nIDPrompt);
-       
+
        if (m_pMainWnd->IsIconic())
                m_pMainWnd->ShowWindow(SW_RESTORE);
 
@@ -517,15 +579,6 @@ 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()
-{
-       m_bNeedIdleTimer = true; 
-}
-
 bool CMergeApp::IsReallyIdle() const
 {
        bool idle = true;
@@ -542,7 +595,7 @@ bool CMergeApp::IsReallyIdle() const
     return idle;
 }
 
-BOOL CMergeApp::OnIdle(LONG lCount) 
+BOOL CMergeApp::OnIdle(LONG lCount)
 {
        if (CWinApp::OnIdle(lCount))
                return TRUE;
@@ -557,7 +610,10 @@ BOOL CMergeApp::OnIdle(LONG lCount)
        if (m_bNonInteractive && IsReallyIdle())
                m_pMainWnd->PostMessage(WM_CLOSE, 0, 0);
 
-       static_cast<CRegOptionsMgr *>(GetOptionsMgr())->CloseHandles();
+       if (typeid(*GetOptionsMgr()) == typeid(CRegOptionsMgr))
+       {
+               static_cast<CRegOptionsMgr*>(GetOptionsMgr())->CloseKeys();
+       }
 
        return FALSE;
 }
@@ -570,7 +626,8 @@ BOOL CMergeApp::OnIdle(LONG lCount)
  */
 void CMergeApp::InitializeFileFilters()
 {
-       String filterPath = GetOptionsMgr()->GetString(OPT_FILTER_USERPATH);
+       assert(m_pGlobalFileFilter != nullptr);
+       const String& filterPath = GetOptionsMgr()->GetString(OPT_FILTER_USERPATH);
 
        if (!filterPath.empty())
        {
@@ -614,12 +671,24 @@ bool CMergeApp::ParseArgsAndDoOpen(MergeCmdLineInfo& cmdInfo, CMainFrame* pMainF
 {
        bool bCompared = false;
        String strDesc[3];
+       std::unique_ptr<PackingInfo> infoUnpacker;
+       std::unique_ptr<PrediffingInfo> infoPrediffer;
+       unsigned nID = cmdInfo.m_nWindowType == MergeCmdLineInfo::AUTOMATIC ?
+               0 : static_cast<unsigned>(cmdInfo.m_nWindowType) + ID_MERGE_COMPARE_TEXT - 1;
+
        m_bNonInteractive = cmdInfo.m_bNonInteractive;
+       m_bEnableExitCode = cmdInfo.m_bEnableExitCode;
+
+       if (!cmdInfo.m_sUnpacker.empty())
+               infoUnpacker.reset(new PackingInfo(cmdInfo.m_sUnpacker));
+
+       if (!cmdInfo.m_sPreDiffer.empty())
+               infoPrediffer.reset(new PrediffingInfo(cmdInfo.m_sPreDiffer));
 
        // Set the global file filter.
        if (!cmdInfo.m_sFileFilter.empty())
        {
-               m_pGlobalFileFilter->SetFilter(cmdInfo.m_sFileFilter);
+               GetGlobalFileFilter()->SetFilter(cmdInfo.m_sFileFilter);
        }
 
        // Set codepage.
@@ -628,6 +697,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)
@@ -654,31 +727,57 @@ bool CMergeApp::ParseArgsAndDoOpen(MergeCmdLineInfo& cmdInfo, CMainFrame* pMainF
                        strDesc[2] = cmdInfo.m_sRightDesc;
                }
 
+               std::unique_ptr<CMainFrame::OpenFileParams> pOpenParams;
+               if (cmdInfo.m_nWindowType == MergeCmdLineInfo::TEXT)
+                       pOpenParams.reset(new CMainFrame::OpenTextFileParams());
+               else if (cmdInfo.m_nWindowType == MergeCmdLineInfo::TABLE)
+                       pOpenParams.reset(new CMainFrame::OpenTableFileParams());
+               else
+                       pOpenParams.reset(static_cast<CMainFrame::OpenTableFileParams *>(new CMainFrame::OpenAutoFileParams()));
+               if (auto* pOpenTextFileParams = dynamic_cast<CMainFrame::OpenTextFileParams*>(pOpenParams.get()))
+               {
+                       pOpenTextFileParams->m_line = cmdInfo.m_nLineIndex;
+                       pOpenTextFileParams->m_char = cmdInfo.m_nCharIndex;
+                       pOpenTextFileParams->m_fileExt = cmdInfo.m_sFileExt;
+               }
+               if (auto* pOpenTableFileParams = dynamic_cast<CMainFrame::OpenTableFileParams*>(pOpenParams.get()))
+               {
+                       pOpenTableFileParams->m_tableDelimiter = cmdInfo.m_cTableDelimiter;
+                       pOpenTableFileParams->m_tableQuote = cmdInfo.m_cTableQuote;
+                       pOpenTableFileParams->m_tableAllowNewlinesInQuotes = cmdInfo.m_bTableAllowNewlinesInQuotes;
+               }
                if (cmdInfo.m_Files.GetSize() > 2)
                {
                        cmdInfo.m_dwLeftFlags |= FFILEOPEN_CMDLINE;
                        cmdInfo.m_dwMiddleFlags |= FFILEOPEN_CMDLINE;
                        cmdInfo.m_dwRightFlags |= FFILEOPEN_CMDLINE;
                        DWORD dwFlags[3] = {cmdInfo.m_dwLeftFlags, cmdInfo.m_dwMiddleFlags, cmdInfo.m_dwRightFlags};
-                       bCompared = pMainFrame->DoFileOpen(&cmdInfo.m_Files,
+                       bCompared = pMainFrame->DoFileOrFolderOpen(&cmdInfo.m_Files,
                                dwFlags, strDesc, cmdInfo.m_sReportFile, cmdInfo.m_bRecurse, nullptr,
-                               cmdInfo.m_sPreDiffer);
+                               infoUnpacker.get(), infoPrediffer.get(), nID, pOpenParams.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,
+                       bCompared = pMainFrame->DoFileOrFolderOpen(&cmdInfo.m_Files,
                                dwFlags, strDesc, cmdInfo.m_sReportFile, cmdInfo.m_bRecurse, nullptr,
-                               cmdInfo.m_sPreDiffer);
+                               infoUnpacker.get(), infoPrediffer.get(), nID, pOpenParams.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(nID, sFilepath, strDesc,
+                                       infoUnpacker.get(), infoPrediffer.get(), pOpenParams.get());
+                       }
+                       else if (IsProjectFile(sFilepath))
                        {
                                bCompared = LoadAndOpenProjectFile(sFilepath);
                        }
-                       else if (IsConflictFile(sFilepath))
+                       else if (ConflictFileParser::IsConflictFile(sFilepath))
                        {
                                //For a conflict file, load the descriptions in their respective positions:  (they will be reordered as needed)
                                strDesc[0] = cmdInfo.m_sLeftDesc;
@@ -689,16 +788,28 @@ bool CMergeApp::ParseArgsAndDoOpen(MergeCmdLineInfo& cmdInfo, CMainFrame* pMainF
                        else
                        {
                                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, nullptr, 
-                                       cmdInfo.m_sPreDiffer);
+                               bCompared = pMainFrame->DoFileOrFolderOpen(&cmdInfo.m_Files,
+                                       dwFlags, strDesc, cmdInfo.m_sReportFile, cmdInfo.m_bRecurse, nullptr,
+                                       infoUnpacker.get(), infoPrediffer.get(), nID, pOpenParams.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);
-                       if (showFiles)
-                               pMainFrame->DoFileOpen();
+                       if (cmdInfo.m_bNewCompare)
+                       {
+                               bCompared = pMainFrame->DoFileNew(nID, 2, strDesc, infoPrediffer.get(), pOpenParams.get());
+                       }
+                       else if (cmdInfo.m_bClipboardCompare)
+                       {
+                               DWORD dwFlags[3] = {cmdInfo.m_dwLeftFlags, cmdInfo.m_dwRightFlags, FFILEOPEN_NONE};
+                               bCompared = pMainFrame->DoOpenClipboard(nID, 2, dwFlags, strDesc, infoUnpacker.get(), infoPrediffer.get(), pOpenParams.get());
+                       }
+                       else
+                       {
+                               bool showFiles = m_pOptions->GetBool(OPT_SHOW_SELECT_FILES_AT_STARTUP);
+                               if (showFiles)
+                                       pMainFrame->DoFileOrFolderOpen();
+                       }
                }
        }
        return bCompared;
@@ -760,7 +871,7 @@ void CMergeApp::OnHelp()
  */
 void CMergeApp::OpenFileToExternalEditor(const String& file, int nLineNumber/* = 1*/)
 {
-       String sCmd = GetOptionsMgr()->GetString(OPT_EXT_EDITOR_CMD);
+       String sCmd = env::ExpandEnvironmentVariables(GetOptionsMgr()->GetString(OPT_EXT_EDITOR_CMD));
        String sFile(file);
        strutils::replace(sCmd, _T("$linenum"), strutils::to_str(nLineNumber));
 
@@ -803,15 +914,27 @@ void CMergeApp::OpenFileToExternalEditor(const String& file, int nLineNumber/* =
        }
 }
 
-/**
- * @brief Open file, if it exists, else open url
- */
-void CMergeApp::OpenFileOrUrl(LPCTSTR szFile, LPCTSTR szUrl)
+/** @brief Returns pointer to global file filter */
+FileFilterHelper* CMergeApp::GetGlobalFileFilter()
 {
-       if (paths::DoesPathExist(szFile) == paths::IS_EXISTING_FILE)
-               ShellExecute(nullptr, _T("open"), _T("notepad.exe"), szFile, nullptr, SW_SHOWNORMAL);
-       else
-               ShellExecute(nullptr, _T("open"), szUrl, nullptr, nullptr, SW_SHOWNORMAL);
+       if (!m_pGlobalFileFilter)
+       {
+               m_pGlobalFileFilter.reset(new FileFilterHelper());
+
+               InitializeFileFilters();
+
+               // 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);
+               if (!bFilterSet)
+               {
+                       String filter = m_pGlobalFileFilter->GetFilterNameOrMask();
+                       m_pOptions->SaveOption(OPT_FILEFILTER_CURRENT, filter);
+               }
+       }
+
+       return m_pGlobalFileFilter.get();
 }
 
 /**
@@ -820,10 +943,7 @@ void CMergeApp::OpenFileOrUrl(LPCTSTR szFile, LPCTSTR szUrl)
  */
 void CMergeApp::ShowHelp(LPCTSTR helpLocation /*= nullptr*/)
 {
-       String name, ext;
-       LANGID LangId = GetLangId();
-       paths::SplitFilename(m_pLangDlg->GetFileName(LangId), nullptr, &name, &ext);
-       String sPath = paths::ConcatPath(env::GetProgPath(), strutils::format(DocsPath, name.c_str()));
+       String sPath = paths::ConcatPath(env::GetProgPath(), strutils::format(DocsPath, GetLangName()));
        if (paths::DoesPathExist(sPath) != paths::IS_EXISTING_FILE)
                sPath = paths::ConcatPath(env::GetProgPath(), strutils::format(DocsPath, _T("")));
        if (helpLocation == nullptr)
@@ -831,7 +951,7 @@ void CMergeApp::ShowHelp(LPCTSTR helpLocation /*= nullptr*/)
                if (paths::DoesPathExist(sPath) == paths::IS_EXISTING_FILE)
                        ::HtmlHelp(nullptr, sPath.c_str(), HH_DISPLAY_TOC, NULL);
                else
-                       ShellExecute(nullptr, _T("open"), DocsURL, nullptr, nullptr, SW_SHOWNORMAL);
+                       shell::Open(DocsURL);
        }
        else
        {
@@ -871,7 +991,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
@@ -879,7 +999,7 @@ bool CMergeApp::CreateBackup(bool bFolder, const String& pszPath)
                        PropBackups::FOLDER_ORIGINAL)
                {
                        // Put backups to same folder than original file
-                       bakPath = path;
+                       bakPath = std::move(path);
                }
                else if (GetOptionsMgr()->GetInt(OPT_BACKUP_LOCATION) ==
                        PropBackups::FOLDER_GLOBAL)
@@ -887,7 +1007,7 @@ bool CMergeApp::CreateBackup(bool bFolder, const String& pszPath)
                        // Put backups to global folder defined in options
                        bakPath = GetOptionsMgr()->GetString(OPT_BACKUP_GLOBALFOLDER);
                        if (bakPath.empty())
-                               bakPath = path;
+                               bakPath = std::move(path);
                        else
                                bakPath = paths::GetLongPath(bakPath);
                }
@@ -934,13 +1054,13 @@ bool CMergeApp::CreateBackup(bool bFolder, const String& pszPath)
                {
                        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)
+                               pszPath + _T("\n(\u2192 ") + bakPath + _T(")"));
+                       if (AfxMessageBox(msg.c_str(), MB_YESNO | MB_ICONWARNING | MB_DONT_ASK_AGAIN, IDS_BACKUP_FAILED_PROMPT) != IDYES)
                                return false;
                }
                return true;
@@ -973,7 +1093,6 @@ int CMergeApp::HandleReadonlySave(String& strSavePath, bool bMultiFile,
        bool bFileExists = false;
        String s;
        String str;
-       CString title;
 
        if (!strSavePath.empty())
        {
@@ -992,7 +1111,7 @@ int CMergeApp::HandleReadonlySave(String& strSavePath, bool bMultiFile,
        if (bFileExists && bFileRO)
        {
                UINT userChoice = 0;
-               
+
                // Don't ask again if its already asked
                if (bApplyToAll)
                        userChoice = IDYES;
@@ -1010,7 +1129,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);
@@ -1021,6 +1140,7 @@ int CMergeApp::HandleReadonlySave(String& strSavePath, bool bMultiFile,
                // Overwrite read-only file
                case IDYESTOALL:
                        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
@@ -1028,7 +1148,7 @@ 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)
@@ -1054,6 +1174,18 @@ int CMergeApp::HandleReadonlySave(String& strSavePath, bool bMultiFile,
        return nRetVal;
 }
 
+String CMergeApp::GetPackingErrorMessage(int pane, int paneCount, const String& path, const PackingInfo& plugin)
+{
+       String pluginName = plugin.GetPluginPipeline();
+       return strutils::format_string2(
+               pane == 0 ? 
+                       _("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?")
+                       : (pane == paneCount - 1) ? 
+                               _("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?")
+                               : _("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?"),
+               path, pluginName);
+}
+
 /**
  * @brief Is specified file a project file?
  * @param [in] filepath Full path to file to check.
@@ -1080,7 +1212,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);
@@ -1098,7 +1230,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);
@@ -1108,7 +1240,7 @@ 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.
@@ -1118,48 +1250,91 @@ bool CMergeApp::LoadAndOpenProjectFile(const String& sProject, const String& sRe
        ProjectFile project;
        if (!LoadProjectFile(sProject, project))
                return false;
-       
-       PathContext tFiles;
-       bool bLeftReadOnly = false;
-       bool bMiddleReadOnly = false;
-       bool bRightReadOnly = false;
-       bool bRecursive = false;
-       project.GetPaths(tFiles, 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>(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
+
+       bool rtn = true;
+       for (auto& projItem : project.Items())
        {
-               if (bMiddleReadOnly)
-                       dwFlags[1] |= FFILEOPEN_READONLY;
-               if (bRightReadOnly)
-                       dwFlags[2] |= FFILEOPEN_READONLY;
-       }
+               std::unique_ptr<PrediffingInfo> pInfoPrediffer;
+               std::unique_ptr<PackingInfo> pInfoUnpacker;
+               PathContext tFiles;
+               bool bDummy = false;
+               projItem.GetPaths(tFiles, bDummy);
+               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 (Options::Project::Get(GetOptionsMgr(), Options::Project::Operation::Open, Options::Project::Item::FileFilter) && projItem.HasFilter())
+               {
+                       String filter = projItem.GetFilter();
+                       filter = strutils::trim_ws(filter);
+                       GetGlobalFileFilter()->SetFilter(filter);
+               }
+               bool bRecursive = GetOptionsMgr()->GetBool(OPT_CMP_INCLUDE_SUBDIRS);
+               if (Options::Project::Get(GetOptionsMgr(), Options::Project::Operation::Open, Options::Project::Item::IncludeSubfolders) && projItem.HasSubfolders())
+                       bRecursive = projItem.GetSubfolders() > 0;
+               if (Options::Project::Get(GetOptionsMgr(), Options::Project::Operation::Open, Options::Project::Item::UnpackerPlugin) && projItem.HasUnpacker())
+                       pInfoUnpacker.reset(new PackingInfo(projItem.GetUnpacker()));
+               if (projItem.HasPrediffer())
+                       pInfoPrediffer.reset(new PrediffingInfo(projItem.GetPrediffer()));
+
+               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);
-       
-       bool rtn = GetMainFrame()->DoFileOpen(&tFiles, dwFlags, nullptr, sReportFile, bRecursive);
+               GetOptionsMgr()->Set(OPT_CMP_INCLUDE_SUBDIRS, bRecursive);
+
+               if (Options::Project::Get(GetOptionsMgr(), Options::Project::Operation::Open, Options::Project::Item::CompareOptions))
+               {
+                       if (projItem.HasIgnoreWhite())
+                               GetOptionsMgr()->Set(OPT_CMP_IGNORE_WHITESPACE, projItem.GetIgnoreWhite());
+                       if (projItem.HasIgnoreBlankLines())
+                               GetOptionsMgr()->Set(OPT_CMP_IGNORE_BLANKLINES, projItem.GetIgnoreBlankLines());
+                       if (projItem.HasIgnoreCase())
+                               GetOptionsMgr()->Set(OPT_CMP_IGNORE_CASE, projItem.GetIgnoreCase());
+                       if (projItem.HasIgnoreEol())
+                               GetOptionsMgr()->Set(OPT_CMP_IGNORE_EOL, projItem.GetIgnoreEol());
+                       if (projItem.HasIgnoreNumbers())
+                               GetOptionsMgr()->Set(OPT_CMP_IGNORE_NUMBERS, projItem.GetIgnoreNumbers());
+                       if (projItem.HasIgnoreCodepage())
+                               GetOptionsMgr()->Set(OPT_CMP_IGNORE_CODEPAGE, projItem.GetIgnoreCodepage());
+                       if (projItem.HasFilterCommentsLines())
+                               GetOptionsMgr()->Set(OPT_CMP_FILTER_COMMENTLINES, projItem.GetFilterCommentsLines());
+                       if (projItem.HasCompareMethod())
+                               GetOptionsMgr()->Set(OPT_CMP_METHOD, projItem.GetCompareMethod());
+               }
+
+               rtn &= GetMainFrame()->DoFileOrFolderOpen(&tFiles, dwFlags, nullptr, sReportFile, bRecursive,
+                       nullptr, pInfoUnpacker.get(), pInfoPrediffer.get());
+       }
 
        AddToRecentProjectsMRU(sProject.c_str());
        return rtn;
@@ -1173,6 +1348,13 @@ WORD CMergeApp::GetLangId() const
        return m_pLangDlg->GetLangId();
 }
 
+String CMergeApp::GetLangName() const
+{
+       String name, ext;
+       paths::SplitFilename(theApp.m_pLangDlg->GetFileName(theApp.GetLangId()), nullptr, &name, &ext);
+       return name;
+}
+
 /**
  * @brief Lang aware version of CStatusBar::SetIndicators()
  */
@@ -1228,7 +1410,7 @@ 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 != nullptr)    {
                m_pRecentFileList->Add(sPathName);
@@ -1280,7 +1462,7 @@ void CMergeApp::OnMergingMode()
        bool bMergingMode = GetMergingMode();
 
        if (!bMergingMode)
-               LangMessageBox(IDS_MERGE_MODE, MB_ICONINFORMATION | MB_DONT_DISPLAY_AGAIN);
+               LangMessageBox(IDS_MERGE_MODE, MB_ICONINFORMATION | MB_DONT_DISPLAY_AGAIN, IDS_MERGE_MODE);
        SetMergingMode(!bMergingMode);
 }
 
@@ -1342,12 +1524,8 @@ BOOL CMergeApp::WriteProfileString(LPCTSTR lpszSection, LPCTSTR lpszEntry, LPCTS
        }
        else
        {
-               for (auto& name : pOptions->GetNameList())
-               {
-                       if (name.find(lpszSection) == 0)
-                               pOptions->RemoveOption(name);
-               }
-
+               String name = strutils::format(_T("%s/"), lpszSection);
+               pOptions->RemoveOption(name);
        }
        return TRUE;
 }