1 /////////////////////////////////////////////////////////////////////////////
2 // WinMerge: an interactive diff/merge utility
3 // Copyright (C) 1997-2000 Thingamahoochie Software
6 // This program is free software; you can redistribute it and/or modify
7 // it under the terms of the GNU General Public License as published by
8 // the Free Software Foundation; either version 2 of the License, or
9 // (at your option) any later version.
11 // This program is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 // GNU General Public License for more details.
16 // You should have received a copy of the GNU General Public License
17 // along with this program; if not, write to the Free Software
18 // Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20 /////////////////////////////////////////////////////////////////////////////
24 * @brief Defines the class behaviors for the application.
30 #include "Constants.h"
31 #include "UnicodeString.h"
33 #include "Environment.h"
34 #include "OptionsMgr.h"
35 #include "OptionsInit.h"
36 #include "RegOptionsMgr.h"
40 #include "HexMergeDoc.h"
41 #include "HexMergeFrm.h"
42 #include "HexMergeView.h"
45 #include "MergeEditFrm.h"
50 #include "PropBackups.h"
51 #include "FileOrFolderSelect.h"
53 #include "FileFilterHelper.h"
54 #include "LineFiltersList.h"
55 #include "FilterCommentsManager.h"
56 #include "SyntaxColors.h"
57 #include "CCrystalTextMarkers.h"
58 #include "OptionsSyntaxColors.h"
60 #include "ProjectFile.h"
61 #include "MergeEditSplitterView.h"
62 #include "LanguageSelect.h"
63 #include "OptionsDef.h"
64 #include "MergeCmdLineInfo.h"
65 #include "ConflictFileParser.h"
67 #include "stringdiffs.h"
70 #include "CompareStats.h"
72 #include "charsets.h" // For shutdown cleanup
78 /** @brief Location for command line help to open. */
79 static const TCHAR CommandLineHelpLocation[] = _T("::/htmlhelp/Command_line.html");
81 /** @brief Backup file extension. */
82 static const TCHAR BACKUP_FILE_EXT[] = _T("bak");
84 /////////////////////////////////////////////////////////////////////////////
87 BEGIN_MESSAGE_MAP(CMergeApp, CWinApp)
88 //{{AFX_MSG_MAP(CMergeApp)
89 ON_COMMAND(ID_APP_ABOUT, OnAppAbout)
90 ON_COMMAND(ID_HELP, OnHelp)
91 ON_COMMAND_EX_RANGE(ID_FILE_PROJECT_MRU_FIRST, ID_FILE_PROJECT_MRU_LAST, OnOpenRecentFile)
92 ON_UPDATE_COMMAND_UI(ID_FILE_PROJECT_MRU_FIRST, CWinApp::OnUpdateRecentFileMenu)
93 ON_COMMAND(ID_FILE_MERGINGMODE, OnMergingMode)
94 ON_UPDATE_COMMAND_UI(ID_FILE_MERGINGMODE, OnUpdateMergingMode)
95 ON_UPDATE_COMMAND_UI(ID_STATUS_MERGINGMODE, OnUpdateMergingStatus)
97 // Standard file based document commands
98 //ON_COMMAND(ID_FILE_NEW, CWinApp::OnFileNew)
99 //ON_COMMAND(ID_FILE_OPEN, CWinApp::OnFileOpen)
100 // Standard print setup command
101 ON_COMMAND(ID_FILE_PRINT_SETUP, CWinApp::OnFilePrintSetup)
104 /////////////////////////////////////////////////////////////////////////////
105 // CMergeApp construction
107 CMergeApp::CMergeApp() :
108 m_bNeedIdleTimer(false)
109 , m_pOpenTemplate(nullptr)
110 , m_pDiffTemplate(nullptr)
111 , m_pHexMergeTemplate(nullptr)
112 , m_pDirTemplate(nullptr)
113 , m_mainThreadScripts(nullptr)
114 , m_nLastCompareResult(0)
115 , m_bNonInteractive(false)
116 , m_pOptions(new CRegOptionsMgr())
117 , m_pGlobalFileFilter(new FileFilterHelper())
118 , m_nActiveOperations(0)
119 , m_pLangDlg(new CLanguageSelect())
120 , m_bEscShutdown(false)
121 , m_bExitIfNoDiff(MergeCmdLineInfo::Disabled)
122 , m_pLineFilters(new LineFiltersList())
123 , m_pFilterCommentsManager(new FilterCommentsManager())
124 , m_pSyntaxColors(new SyntaxColors())
125 , m_pMarkers(new CCrystalTextMarkers())
126 , m_bMergingMode(false)
128 // add construction code here,
129 // Place all significant initialization in InitInstance
132 CMergeApp::~CMergeApp()
136 /////////////////////////////////////////////////////////////////////////////
137 // The one and only CMergeApp object
141 /////////////////////////////////////////////////////////////////////////////
142 // CMergeApp initialization
145 * @brief Initialize WinMerge application instance.
146 * @return TRUE if application initialization succeeds (and we'll run it),
147 * FALSE if something failed and we exit the instance.
148 * @todo We could handle these failure situations more gratefully, i.e. show
149 * at least some error message to the user..
151 BOOL CMergeApp::InitInstance()
153 // Prevents DLL hijacking
154 HMODULE hLibrary = GetModuleHandle(_T("kernel32.dll"));
155 BOOL (WINAPI *pfnSetSearchPathMode)(DWORD) = (BOOL (WINAPI *)(DWORD))GetProcAddress(hLibrary, "SetSearchPathMode");
156 if (pfnSetSearchPathMode != nullptr)
157 pfnSetSearchPathMode(0x00000001L /*BASE_SEARCH_PATH_ENABLE_SAFE_SEARCHMODE*/ | 0x00008000L /*BASE_SEARCH_PATH_PERMANENT*/);
158 BOOL (WINAPI *pfnSetDllDirectoryA)(LPCSTR) = (BOOL (WINAPI *)(LPCSTR))GetProcAddress(hLibrary, "SetDllDirectoryA");
159 if (pfnSetDllDirectoryA != nullptr)
160 pfnSetDllDirectoryA("");
162 JumpList::SetCurrentProcessExplicitAppUserModelID(L"Thingamahoochie.WinMerge");
164 InitCommonControls(); // initialize common control library
165 CWinApp::InitInstance(); // call parent class method
167 // Runtime switch so programmer may set this in interactive debugger
171 // get current setting
172 int tmpFlag = _CrtSetDbgFlag( _CRTDBG_REPORT_FLAG );
173 // Keep freed memory blocks in the heap's linked list and mark them as freed
174 tmpFlag |= _CRTDBG_DELAY_FREE_MEM_DF;
175 // Call _CrtCheckMemory at every allocation and deallocation request.
176 // WARNING: This slows down WinMerge *A LOT*
177 tmpFlag |= _CRTDBG_CHECK_ALWAYS_DF;
178 // Set the new state for the flag
179 _CrtSetDbgFlag( tmpFlag );
182 // CCrystalEdit Drag and Drop functionality needs AfxOleInit.
185 TRACE(_T("AfxOleInitFailed. OLE functionality disabled"));
188 // Standard initialization
189 // If you are not using these features and wish to reduce the size
190 // of your final executable, you should remove from the following
191 // the specific initialization routines you do not need.
193 // Revoke the standard OLE Message Filter to avoid drawing frame while loading files.
194 COleMessageFilter* pOldFilter = AfxOleGetMessageFilter();
195 pOldFilter->Revoke();
197 // Load registry keys from WinMerge.reg if existing WinMerge.reg
198 env::LoadRegistryFromFile(paths::ConcatPath(env::GetProgPath(), _T("WinMerge.reg")));
200 // Parse command-line arguments.
201 MergeCmdLineInfo cmdInfo(GetCommandLine());
202 if (cmdInfo.m_bNoPrefs)
203 m_pOptions->SetSerializing(false); // Turn off serializing to registry.
205 Options::Init(m_pOptions.get()); // Implementation in OptionsInit.cpp
206 ApplyCommandLineConfigOptions(cmdInfo);
207 if (cmdInfo.m_sErrorMessages.size() > 0)
209 if (AttachConsole(ATTACH_PARENT_PROCESS))
212 for (auto& msg : cmdInfo.m_sErrorMessages)
214 String line = _T("WinMerge: ") + msg + _T("\n");
215 WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), line.c_str(), static_cast<DWORD>(line.length()), &dwWritten, nullptr);
221 // Initialize temp folder
224 // If paths were given to commandline we consider this being an invoke from
225 // commandline (from other application, shellextension etc).
226 bool bCommandLineInvoke = cmdInfo.m_Files.GetSize() > 0;
228 // WinMerge registry settings are stored under HKEY_CURRENT_USER/Software/Thingamahoochie
229 // This is the name of the company of the original author (Dean Grimm)
230 SetRegistryKey(_T("Thingamahoochie"));
232 bool bSingleInstance = GetOptionsMgr()->GetBool(OPT_SINGLE_INSTANCE) ||
233 (true == cmdInfo.m_bSingleInstance);
235 // Create exclusion mutex name
236 TCHAR szDesktopName[MAX_PATH] = _T("Win9xDesktop");
237 DWORD dwLengthNeeded;
238 GetUserObjectInformation(GetThreadDesktop(GetCurrentThreadId()), UOI_NAME,
239 szDesktopName, sizeof(szDesktopName), &dwLengthNeeded);
240 TCHAR szMutexName[MAX_PATH + 40];
241 // Combine window class name and desktop name to form a unique mutex name.
242 // As the window class name is decorated to distinguish between ANSI and
243 // UNICODE build, so will be the mutex name.
244 wsprintf(szMutexName, _T("%s-%s"), CMainFrame::szClassName, szDesktopName);
245 HANDLE hMutex = CreateMutex(nullptr, FALSE, szMutexName);
246 if (hMutex != nullptr)
247 WaitForSingleObject(hMutex, INFINITE);
248 if (bSingleInstance && GetLastError() == ERROR_ALREADY_EXISTS)
250 // Activate previous instance and send commandline to it
251 HWND hWnd = FindWindow(CMainFrame::szClassName, nullptr);
255 ShowWindow(hWnd, SW_RESTORE);
256 SetForegroundWindow(GetLastActivePopup(hWnd));
257 LPTSTR cmdLine = GetCommandLine();
258 COPYDATASTRUCT data = { 0, (lstrlen(cmdLine) + 1) * sizeof(TCHAR), cmdLine};
259 if (::SendMessage(hWnd, WM_COPYDATA, NULL, (LPARAM)&data))
261 ReleaseMutex(hMutex);
268 LoadStdProfileSettings(GetOptionsMgr()->GetInt(OPT_MRU_MAX)); // Load standard INI file options (including MRU)
270 InitializeFileFilters();
272 // Read last used filter from registry
273 // If filter fails to set, reset to default
274 const String filterString = m_pOptions->GetString(OPT_FILEFILTER_CURRENT);
275 bool bFilterSet = m_pGlobalFileFilter->SetFilter(filterString);
278 String filter = m_pGlobalFileFilter->GetFilterNameOrMask();
279 m_pOptions->SaveOption(OPT_FILEFILTER_CURRENT, filter);
283 UpdateCodepageModule();
285 FileTransform::g_UnpackerMode = static_cast<PLUGIN_MODE>(GetOptionsMgr()->GetInt(OPT_PLUGINS_UNPACKER_MODE));
286 FileTransform::g_PredifferMode = static_cast<PLUGIN_MODE>(GetOptionsMgr()->GetInt(OPT_PLUGINS_PREDIFFER_MODE));
288 NONCLIENTMETRICS ncm = { sizeof NONCLIENTMETRICS };
289 if (SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof NONCLIENTMETRICS, &ncm, 0))
291 const int lfHeight = -MulDiv(9, CClientDC(CWnd::GetDesktopWindow()).GetDeviceCaps(LOGPIXELSY), 72);
292 if (abs(ncm.lfMenuFont.lfHeight) > abs(lfHeight))
293 ncm.lfMenuFont.lfHeight = lfHeight;
294 if (wcscmp(ncm.lfMenuFont.lfFaceName, L"Meiryo") == 0 || wcscmp(ncm.lfMenuFont.lfFaceName, L"\U000030e1\U000030a4\U000030ea\U000030aa"/* "Meiryo" in Japanese */) == 0)
295 wcscpy_s(ncm.lfMenuFont.lfFaceName, L"Meiryo UI");
296 m_fontGUI.CreateFontIndirect(&ncm.lfMenuFont);
299 if (m_pSyntaxColors != nullptr)
300 Options::SyntaxColors::Load(GetOptionsMgr(), m_pSyntaxColors.get());
302 if (m_pMarkers != nullptr)
303 m_pMarkers->LoadFromRegistry();
305 CCrystalTextView::SetRenderingModeDefault(static_cast<CCrystalTextView::RENDERING_MODE>(GetOptionsMgr()->GetInt(OPT_RENDERING_MODE)));
307 if (m_pLineFilters != nullptr)
308 m_pLineFilters->Initialize(GetOptionsMgr());
310 // If there are no filters loaded, and there is filter string in previous
311 // option string, import old filters to new place.
312 if (m_pLineFilters->GetCount() == 0)
314 String oldFilter = theApp.GetProfileString(_T("Settings"), _T("RegExps"));
315 if (!oldFilter.empty())
316 m_pLineFilters->Import(oldFilter);
319 // Check if filter folder is set, and create it if not
320 String pathMyFolders = GetOptionsMgr()->GetString(OPT_FILTER_USERPATH);
321 if (pathMyFolders.empty())
323 // No filter path, set it to default and make sure it exists.
324 pathMyFolders = GetOptionsMgr()->GetDefault<String>(OPT_FILTER_USERPATH);
325 GetOptionsMgr()->SaveOption(OPT_FILTER_USERPATH, pathMyFolders);
326 theApp.m_pGlobalFileFilter->SetUserFilterPath(pathMyFolders);
328 if (!paths::CreateIfNeeded(pathMyFolders))
330 // Failed to create a folder, check it didn't already
332 DWORD errCode = GetLastError();
333 if (errCode != ERROR_ALREADY_EXISTS)
335 // Failed to create a folder for filters, fallback to
336 // "My Documents"-folder. It is not worth the trouble to
337 // bother user about this or user more clever solutions.
338 GetOptionsMgr()->SaveOption(OPT_FILTER_USERPATH, env::GetMyDocuments());
342 strdiff::Init(); // String diff init
343 strdiff::SetBreakChars(GetOptionsMgr()->GetString(OPT_BREAK_SEPARATORS).c_str());
345 m_bMergingMode = GetOptionsMgr()->GetBool(OPT_MERGE_MODE);
347 // Initialize i18n (multiple language) support
349 m_pLangDlg->InitializeLanguage((WORD)GetOptionsMgr()->GetInt(OPT_SELECTED_LANGUAGE));
351 m_mainThreadScripts = new CAssureScriptsForThread;
353 // Register the application's document templates. Document templates
354 // serve as the connection between documents, frame windows and views.
357 m_pOpenTemplate = new CMultiDocTemplate(
359 RUNTIME_CLASS(COpenDoc),
360 RUNTIME_CLASS(COpenFrame), // custom MDI child frame
361 RUNTIME_CLASS(COpenView));
362 AddDocTemplate(m_pOpenTemplate);
365 m_pDiffTemplate = new CMultiDocTemplate(
367 RUNTIME_CLASS(CMergeDoc),
368 RUNTIME_CLASS(CMergeEditFrame), // custom MDI child frame
369 RUNTIME_CLASS(CMergeEditSplitterView));
370 AddDocTemplate(m_pDiffTemplate);
373 m_pHexMergeTemplate = new CMultiDocTemplate(
375 RUNTIME_CLASS(CHexMergeDoc),
376 RUNTIME_CLASS(CHexMergeFrame), // custom MDI child frame
377 RUNTIME_CLASS(CHexMergeView));
378 AddDocTemplate(m_pHexMergeTemplate);
381 m_pDirTemplate = new CMultiDocTemplate(
383 RUNTIME_CLASS(CDirDoc),
384 RUNTIME_CLASS(CDirFrame), // custom MDI child frame
385 RUNTIME_CLASS(CDirView));
386 AddDocTemplate(m_pDirTemplate);
388 // create main MDI Frame window
389 CMainFrame* pMainFrame = new CMainFrame;
390 if (!pMainFrame->LoadFrame(IDR_MAINFRAME))
392 if (hMutex != nullptr)
394 ReleaseMutex(hMutex);
399 m_pMainWnd = pMainFrame;
401 // Init menus -- hMenuDefault is for MainFrame
402 pMainFrame->m_hMenuDefault = pMainFrame->NewDefaultMenu();
405 // Note : for Windows98 compatibility, use FromHandle and not Attach/Detach
406 CMenu * pNewMenu = CMenu::FromHandle(pMainFrame->m_hMenuDefault);
407 pMainFrame->MDISetMenu(pNewMenu, nullptr);
409 // The main window has been initialized, so activate and update it.
410 pMainFrame->ActivateFrame(cmdInfo.m_nCmdShow);
411 pMainFrame->UpdateWindow();
413 // Since this function actually opens paths for compare it must be
414 // called after initializing CMainFrame!
415 bool bContinue = true;
416 if (!ParseArgsAndDoOpen(cmdInfo, pMainFrame) && bCommandLineInvoke)
419 if (hMutex != nullptr)
420 ReleaseMutex(hMutex);
422 // If user wants to cancel the compare, close WinMerge
425 pMainFrame->PostMessage(WM_CLOSE, 0, 0);
429 WinMergeTest::TestAll();
435 static void OpenContributersFile(int&)
437 theApp.OpenFileToExternalEditor(paths::ConcatPath(env::GetProgPath(), ContributorsPath));
440 // App command to run the dialog
441 void CMergeApp::OnAppAbout()
444 aboutDlg.m_onclick_contributers += Poco::delegate(OpenContributersFile);
446 aboutDlg.m_onclick_contributers.clear();
449 /////////////////////////////////////////////////////////////////////////////
450 // CMergeApp commands
453 * @brief Called when application is about to exit.
454 * This functions is called when application is exiting, so this is
455 * good place to do cleanups.
456 * @return Application's exit value (returned from WinMain()).
458 int CMergeApp::ExitInstance()
462 // Save registry keys if existing WinMerge.reg
463 env::SaveRegistryToFile(paths::ConcatPath(env::GetProgPath(), _T("WinMerge.reg")), RegDir);
466 const String temp = env::GetTemporaryPath();
467 ClearTempfolder(temp);
469 // Cleanup left over tempfiles from previous instances.
470 // Normally this should not neet to do anything - but if for some reason
471 // WinMerge did not delete temp files this makes sure they are removed.
474 delete m_mainThreadScripts;
475 CWinApp::ExitInstance();
479 int CMergeApp::DoMessageBox(LPCTSTR lpszPrompt, UINT nType, UINT nIDPrompt)
481 // This is a convenient point for breakpointing !!!
483 // Create a handle to store the parent window of the message box.
484 CWnd* pParentWnd = CWnd::GetActiveWindow();
486 // Check whether an active window was retrieved successfully.
487 if (pParentWnd == nullptr)
489 // Try to retrieve a handle to the last active popup.
490 CWnd * mainwnd = GetMainWnd();
491 if (mainwnd != nullptr)
492 pParentWnd = mainwnd->GetLastActivePopup();
495 // Use our own message box implementation, which adds the
496 // do not show again checkbox, and implements it on subsequent calls
497 // (if caller set the style)
499 if (m_bNonInteractive)
501 if (AttachConsole(ATTACH_PARENT_PROCESS))
504 String line = _T("WinMerge: ") + String(lpszPrompt) + _T("\n");
505 WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), line.c_str(), static_cast<DWORD>(line.length()), &dwWritten, nullptr);
511 // Create the message box dialog.
512 CMessageBoxDialog dlgMessage(pParentWnd, lpszPrompt, _T(""), nType | MB_RIGHT_ALIGN,
515 if (m_pMainWnd->IsIconic())
516 m_pMainWnd->ShowWindow(SW_RESTORE);
518 // Display the message box dialog and return the result.
519 return static_cast<int>(dlgMessage.DoModal());
522 bool CMergeApp::IsReallyIdle() const
525 POSITION pos = m_pDirTemplate->GetFirstDocPosition();
526 while (pos != nullptr)
528 CDirDoc *pDirDoc = static_cast<CDirDoc *>(m_pDirTemplate->GetNextDoc(pos));
529 if (const CompareStats *pCompareStats = pDirDoc->GetCompareStats())
531 if (!pCompareStats->IsCompareDone() || pDirDoc->GetGeneratingReport())
538 BOOL CMergeApp::OnIdle(LONG lCount)
540 if (CWinApp::OnIdle(lCount))
543 // If anyone has requested notification when next idle occurs, send it
544 if (m_bNeedIdleTimer)
546 m_bNeedIdleTimer = false;
547 m_pMainWnd->SendMessageToDescendants(WM_TIMER, IDLE_TIMER, lCount, TRUE, FALSE);
550 if (m_bNonInteractive && IsReallyIdle())
551 m_pMainWnd->PostMessage(WM_CLOSE, 0, 0);
553 static_cast<CRegOptionsMgr *>(GetOptionsMgr())->CloseKeys();
559 * @brief Load any known file filters.
561 * This function loads filter files from paths we know contain them.
562 * @note User's filter location may not be set yet.
564 void CMergeApp::InitializeFileFilters()
566 String filterPath = GetOptionsMgr()->GetString(OPT_FILTER_USERPATH);
568 if (!filterPath.empty())
570 m_pGlobalFileFilter->SetUserFilterPath(filterPath);
572 m_pGlobalFileFilter->LoadAllFileFilters();
575 void CMergeApp::ApplyCommandLineConfigOptions(MergeCmdLineInfo& cmdInfo)
577 if (cmdInfo.m_bNoPrefs)
578 m_pOptions->SetSerializing(false); // Turn off serializing to registry.
580 for (const auto& it : cmdInfo.m_Options)
582 if (m_pOptions->Set(it.first, it.second) == COption::OPT_NOTFOUND)
584 String longname = m_pOptions->ExpandShortName(it.first);
585 if (!longname.empty())
587 m_pOptions->Set(longname, it.second);
591 cmdInfo.m_sErrorMessages.push_back(strutils::format_string1(_T("Invalid key '%1' specified in /config option"), it.first));
597 /** @brief Read command line arguments and open files for comparison.
599 * The name of the function is a legacy code from the time that this function
600 * actually parsed the command line. Today the parsing is done using the
601 * MergeCmdLineInfo class.
602 * @param [in] cmdInfo Commandline parameters info.
603 * @param [in] pMainFrame Pointer to application main frame.
604 * @return `true` if we opened the compare, `false` if the compare was canceled.
606 bool CMergeApp::ParseArgsAndDoOpen(MergeCmdLineInfo& cmdInfo, CMainFrame* pMainFrame)
608 bool bCompared = false;
610 m_bNonInteractive = cmdInfo.m_bNonInteractive;
612 // Set the global file filter.
613 if (!cmdInfo.m_sFileFilter.empty())
615 m_pGlobalFileFilter->SetFilter(cmdInfo.m_sFileFilter);
619 if (cmdInfo.m_nCodepage)
621 UpdateDefaultCodepage(2,cmdInfo.m_nCodepage);
624 // Unless the user has requested to see WinMerge's usage open files for
626 if (cmdInfo.m_bShowUsage)
628 ShowHelp(CommandLineHelpLocation);
632 // Set the required information we need from the command line:
634 m_bExitIfNoDiff = cmdInfo.m_bExitIfNoDiff;
635 m_bEscShutdown = cmdInfo.m_bEscShutdown;
637 m_strSaveAsPath = cmdInfo.m_sOutputpath;
639 strDesc[0] = cmdInfo.m_sLeftDesc;
640 if (cmdInfo.m_Files.GetSize() < 3)
642 strDesc[1] = cmdInfo.m_sRightDesc;
646 strDesc[1] = cmdInfo.m_sMiddleDesc;
647 strDesc[2] = cmdInfo.m_sRightDesc;
650 if (cmdInfo.m_Files.GetSize() > 2)
652 cmdInfo.m_dwLeftFlags |= FFILEOPEN_CMDLINE;
653 cmdInfo.m_dwMiddleFlags |= FFILEOPEN_CMDLINE;
654 cmdInfo.m_dwRightFlags |= FFILEOPEN_CMDLINE;
655 DWORD dwFlags[3] = {cmdInfo.m_dwLeftFlags, cmdInfo.m_dwMiddleFlags, cmdInfo.m_dwRightFlags};
656 bCompared = pMainFrame->DoFileOpen(&cmdInfo.m_Files,
657 dwFlags, strDesc, cmdInfo.m_sReportFile, cmdInfo.m_bRecurse, nullptr,
658 cmdInfo.m_sPreDiffer);
660 else if (cmdInfo.m_Files.GetSize() > 1)
662 DWORD dwFlags[3] = {cmdInfo.m_dwLeftFlags, cmdInfo.m_dwRightFlags, FFILEOPEN_NONE};
663 bCompared = pMainFrame->DoFileOpen(&cmdInfo.m_Files,
664 dwFlags, strDesc, cmdInfo.m_sReportFile, cmdInfo.m_bRecurse, nullptr,
665 cmdInfo.m_sPreDiffer);
667 else if (cmdInfo.m_Files.GetSize() == 1)
669 String sFilepath = cmdInfo.m_Files[0];
670 if (IsProjectFile(sFilepath))
672 bCompared = LoadAndOpenProjectFile(sFilepath);
674 else if (IsConflictFile(sFilepath))
676 //For a conflict file, load the descriptions in their respective positions: (they will be reordered as needed)
677 strDesc[0] = cmdInfo.m_sLeftDesc;
678 strDesc[1] = cmdInfo.m_sMiddleDesc;
679 strDesc[2] = cmdInfo.m_sRightDesc;
680 bCompared = pMainFrame->DoOpenConflict(sFilepath, strDesc);
684 DWORD dwFlags[3] = {cmdInfo.m_dwLeftFlags, cmdInfo.m_dwRightFlags, FFILEOPEN_NONE};
685 bCompared = pMainFrame->DoFileOpen(&cmdInfo.m_Files,
686 dwFlags, strDesc, cmdInfo.m_sReportFile, cmdInfo.m_bRecurse, nullptr,
687 cmdInfo.m_sPreDiffer);
690 else if (cmdInfo.m_Files.GetSize() == 0) // if there are no input args, we can check the display file dialog flag
692 bool showFiles = m_pOptions->GetBool(OPT_SHOW_SELECT_FILES_AT_STARTUP);
694 pMainFrame->DoFileOpen();
700 void CMergeApp::UpdateDefaultCodepage(int cpDefaultMode, int cpCustomCodepage)
704 switch (cpDefaultMode)
707 ucr::setDefaultCodepage(GetACP());
711 wLangId = GetLangId();
712 if (GetLocaleInfo(wLangId, LOCALE_IDEFAULTANSICODEPAGE, buff, sizeof(buff)/sizeof(buff[0])))
713 ucr::setDefaultCodepage(_ttol(buff));
715 ucr::setDefaultCodepage(GetACP());
718 ucr::setDefaultCodepage(cpCustomCodepage);
721 // no other valid option
723 ucr::setDefaultCodepage(GetACP());
728 * @brief Send current option settings into codepage module
730 void CMergeApp::UpdateCodepageModule()
732 // Get current codepage settings from the options module
733 // and push them into the codepage module
734 UpdateDefaultCodepage(GetOptionsMgr()->GetInt(OPT_CP_DEFAULT_MODE), GetOptionsMgr()->GetInt(OPT_CP_DEFAULT_CUSTOM));
737 /** @brief Open help from mainframe when user presses F1*/
738 void CMergeApp::OnHelp()
744 * @brief Open given file to external editor specified in options.
745 * @param [in] file Full path to file to open.
747 * Opens file to defined (in Options/system), Notepad by default,
748 * external editor. Path is decorated with quotation marks if needed
749 * (contains spaces). Also '$file' in editor path is replaced by
751 * @param [in] file Full path to file to open.
752 * @param [in] nLineNumber Line number to go to.
754 void CMergeApp::OpenFileToExternalEditor(const String& file, int nLineNumber/* = 1*/)
756 String sCmd = GetOptionsMgr()->GetString(OPT_EXT_EDITOR_CMD);
758 strutils::replace(sCmd, _T("$linenum"), strutils::to_str(nLineNumber));
760 size_t nIndex = sCmd.find(_T("$file"));
761 if (nIndex != String::npos)
763 sFile.insert(0, _T("\""));
764 strutils::replace(sCmd, _T("$file"), sFile);
765 nIndex = sCmd.find(' ', nIndex + sFile.length());
766 if (nIndex != String::npos)
767 sCmd.insert(nIndex, _T("\""));
779 STARTUPINFO stInfo = { sizeof STARTUPINFO };
780 PROCESS_INFORMATION processInfo;
782 retVal = !!CreateProcess(nullptr, (LPTSTR)sCmd.c_str(),
783 nullptr, nullptr, FALSE, CREATE_DEFAULT_ERROR_MODE, nullptr, nullptr,
784 &stInfo, &processInfo);
788 // Error invoking external editor
789 String msg = strutils::format_string1(_("Failed to execute external editor: %1"), sCmd);
790 AfxMessageBox(msg.c_str(), MB_ICONSTOP);
794 CloseHandle(processInfo.hThread);
795 CloseHandle(processInfo.hProcess);
800 * @brief Open file, if it exists, else open url
802 void CMergeApp::OpenFileOrUrl(LPCTSTR szFile, LPCTSTR szUrl)
804 if (paths::DoesPathExist(szFile) == paths::IS_EXISTING_FILE)
805 ShellExecute(nullptr, _T("open"), _T("notepad.exe"), szFile, nullptr, SW_SHOWNORMAL);
807 ShellExecute(nullptr, _T("open"), szUrl, nullptr, nullptr, SW_SHOWNORMAL);
811 * @brief Show Help - this is for opening help from outside mainframe.
812 * @param [in] helpLocation Location inside help, if `nullptr` main help is opened.
814 void CMergeApp::ShowHelp(LPCTSTR helpLocation /*= nullptr*/)
817 LANGID LangId = GetLangId();
818 paths::SplitFilename(m_pLangDlg->GetFileName(LangId), nullptr, &name, &ext);
819 String sPath = paths::ConcatPath(env::GetProgPath(), strutils::format(DocsPath, name.c_str()));
820 if (paths::DoesPathExist(sPath) != paths::IS_EXISTING_FILE)
821 sPath = paths::ConcatPath(env::GetProgPath(), strutils::format(DocsPath, _T("")));
822 if (helpLocation == nullptr)
824 if (paths::DoesPathExist(sPath) == paths::IS_EXISTING_FILE)
825 ::HtmlHelp(nullptr, sPath.c_str(), HH_DISPLAY_TOC, NULL);
827 ShellExecute(nullptr, _T("open"), DocsURL, nullptr, nullptr, SW_SHOWNORMAL);
831 if (paths::DoesPathExist(sPath) == paths::IS_EXISTING_FILE)
833 sPath += helpLocation;
834 ::HtmlHelp(nullptr, sPath.c_str(), HH_DISPLAY_TOPIC, NULL);
840 * @brief Creates backup before file is saved or copied over.
841 * This function handles formatting correct path and filename for
842 * backup file. Formatting is done based on several options available
843 * for users in Options/Backups dialog. After path is formatted, file
844 * is simply just copied. Not much error checking, just if copying
845 * succeeded or failed.
846 * @param [in] bFolder Are we creating backup in folder compare?
847 * @param [in] pszPath Full path to file to backup.
848 * @return `true` if backup succeeds, or isn't just done.
850 bool CMergeApp::CreateBackup(bool bFolder, const String& pszPath)
852 // If user doesn't want to backups in folder compare, return
853 // success so operations don't abort.
854 if (bFolder && !(GetOptionsMgr()->GetBool(OPT_BACKUP_FOLDERCMP)))
856 // Likewise if user doesn't want backups in file compare
857 else if (!bFolder && !(GetOptionsMgr()->GetBool(OPT_BACKUP_FILECMP)))
860 // create backup copy of file if destination file exists
861 if (paths::DoesPathExist(pszPath) == paths::IS_EXISTING_FILE)
868 paths::SplitFilename(paths::GetLongPath(pszPath), &path, &filename, &ext);
870 // Determine backup folder
871 if (GetOptionsMgr()->GetInt(OPT_BACKUP_LOCATION) ==
872 PropBackups::FOLDER_ORIGINAL)
874 // Put backups to same folder than original file
877 else if (GetOptionsMgr()->GetInt(OPT_BACKUP_LOCATION) ==
878 PropBackups::FOLDER_GLOBAL)
880 // Put backups to global folder defined in options
881 bakPath = GetOptionsMgr()->GetString(OPT_BACKUP_GLOBALFOLDER);
885 bakPath = paths::GetLongPath(bakPath);
889 _RPTF0(_CRT_ERROR, "Unknown backup location!");
892 bool success = false;
893 if (GetOptionsMgr()->GetBool(OPT_BACKUP_ADD_BAK))
895 // Don't add dot if there is no existing extension
898 ext += BACKUP_FILE_EXT;
901 // Append time to filename if wanted so
902 // NOTE just adds timestamp at the moment as I couldn't figure out
903 // nice way to add a real time (invalid chars etc).
904 if (GetOptionsMgr()->GetBool(OPT_BACKUP_ADD_TIME))
909 ::localtime_s(&tm, &curtime);
911 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);
916 // Append filename and extension (+ optional .bak) to path
917 if ((bakPath.length() + filename.length() + ext.length())
921 bakPath = paths::ConcatPath(bakPath, filename);
928 success = !!CopyFileW(TFile(pszPath).wpath().c_str(), TFile(bakPath).wpath().c_str(), FALSE);
933 String msg = strutils::format_string1(
934 _("Unable to backup original file:\n%1\n\nContinue anyway?"),
936 if (AfxMessageBox(msg.c_str(), MB_YESNO | MB_ICONWARNING | MB_DONT_ASK_AGAIN) != IDYES)
942 // we got here because we're either not backing up of there was nothing to backup
947 * @brief Checks if path (file/folder) is read-only and asks overwriting it.
949 * @param strSavePath [in,out] Path where to save (file or folder)
950 * @param bMultiFile [in] Single file or multiple files/folder
951 * @param bApplyToAll [in,out] Apply last user selection for all items?
952 * @return Users selection:
953 * - IDOK: Item was not readonly, no actions
954 * - IDYES/IDYESTOALL: Overwrite readonly item
955 * - IDNO: User selected new filename (single file) or user wants to skip
956 * - IDCANCEL: Cancel operation
957 * @sa CMainFrame::SyncFileToVCS()
958 * @sa CMergeDoc::DoSave()
960 int CMergeApp::HandleReadonlySave(String& strSavePath, bool bMultiFile,
965 bool bFileRO = false;
966 bool bFileExists = false;
971 if (!strSavePath.empty())
975 TFile file(strSavePath);
976 bFileExists = file.exists();
978 bFileRO = !file.canWrite();
985 if (bFileExists && bFileRO)
989 // Don't ask again if its already asked
994 // Prompt for user choice
997 // Multiple files or folder
998 str = strutils::format_string1(_("%1\nis marked read-only. Would you like to override the read-only item?"), strSavePath);
999 userChoice = AfxMessageBox(str.c_str(), MB_YESNOCANCEL |
1000 MB_ICONWARNING | MB_DEFBUTTON3 | MB_DONT_ASK_AGAIN |
1001 MB_YES_TO_ALL, IDS_SAVEREADONLY_MULTI);
1006 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);
1007 userChoice = AfxMessageBox(str.c_str(), MB_YESNOCANCEL |
1008 MB_ICONWARNING | MB_DEFBUTTON2 | MB_DONT_ASK_AGAIN,
1009 IDS_SAVEREADONLY_FMT);
1014 // Overwrite read-only file
1016 bApplyToAll = true; // Don't ask again (no break here)
1018 CFile::GetStatus(strSavePath.c_str(), status);
1019 status.m_mtime = 0; // Avoid unwanted changes
1020 status.m_attribute &= ~CFile::readOnly;
1021 CFile::SetStatus(strSavePath.c_str(), status);
1025 // Save to new filename (single) /skip this item (multiple)
1029 if (SelectFile(AfxGetMainWnd()->GetSafeHwnd(), s, false, strSavePath.c_str()))
1051 * @brief Is specified file a project file?
1052 * @param [in] filepath Full path to file to check.
1053 * @return true if file is a projectfile.
1055 bool CMergeApp::IsProjectFile(const String& filepath) const
1058 paths::SplitFilename(filepath, nullptr, nullptr, &ext);
1059 if (strutils::compare_nocase(ext, ProjectFile::PROJECTFILE_EXT) == 0)
1065 bool CMergeApp::LoadProjectFile(const String& sProject, ProjectFile &project)
1067 if (sProject.empty())
1072 project.Read(sProject);
1074 catch (Poco::Exception& e)
1076 String sErr = _("Unknown error attempting to open project file");
1077 sErr += ucr::toTString(e.displayText());
1078 String msg = strutils::format_string2(_("Cannot open file\n%1\n\n%2"), sProject, sErr);
1079 AfxMessageBox(msg.c_str(), MB_ICONSTOP);
1086 bool CMergeApp::SaveProjectFile(const String& sProject, const ProjectFile &project)
1090 project.Save(sProject);
1092 catch (Poco::Exception& e)
1094 String sErr = _("Unknown error attempting to save project file");
1095 sErr += ucr::toTString(e.displayText());
1096 String msg = strutils::format_string2(_("Cannot open file\n%1\n\n%2"), sProject, sErr);
1097 AfxMessageBox(msg.c_str(), MB_ICONSTOP);
1105 * @brief Read project and perform comparison specified
1106 * @param [in] sProject Full path to project file.
1107 * @return `true` if loading project file and starting compare succeeded.
1109 bool CMergeApp::LoadAndOpenProjectFile(const String& sProject, const String& sReportFile)
1111 ProjectFile project;
1112 if (!LoadProjectFile(sProject, project))
1116 for (auto& projItem : project.Items())
1119 bool bRecursive = false;
1120 projItem.GetPaths(tFiles, bRecursive);
1121 for (int i = 0; i < tFiles.GetSize(); ++i)
1123 if (!paths::IsPathAbsolute(tFiles[i]))
1125 String sProjectDir = paths::GetParentPath(sProject);
1126 if (tFiles[i].substr(0, 1) == _T("\\"))
1128 if (sProjectDir.length() > 1 && sProjectDir[1] == ':')
1129 tFiles[i] = paths::ConcatPath(sProjectDir.substr(0, 2), tFiles[i]);
1132 tFiles[i] = paths::ConcatPath(sProjectDir, tFiles[i]);
1135 bool bLeftReadOnly = projItem.GetLeftReadOnly();
1136 bool bMiddleReadOnly = projItem.GetMiddleReadOnly();
1137 bool bRightReadOnly = projItem.GetRightReadOnly();
1138 if (projItem.HasFilter())
1140 String filter = projItem.GetFilter();
1141 filter = strutils::trim_ws(filter);
1142 m_pGlobalFileFilter->SetFilter(filter);
1144 if (projItem.HasSubfolders())
1145 bRecursive = projItem.GetSubfolders() > 0;
1147 DWORD dwFlags[3] = {
1148 static_cast<DWORD>(tFiles.GetPath(0).empty() ? FFILEOPEN_NONE : FFILEOPEN_PROJECT),
1149 static_cast<DWORD>(tFiles.GetPath(1).empty() ? FFILEOPEN_NONE : FFILEOPEN_PROJECT),
1150 static_cast<DWORD>(tFiles.GetPath(2).empty() ? FFILEOPEN_NONE : FFILEOPEN_PROJECT)
1153 dwFlags[0] |= FFILEOPEN_READONLY;
1154 if (tFiles.GetSize() == 2)
1157 dwFlags[1] |= FFILEOPEN_READONLY;
1161 if (bMiddleReadOnly)
1162 dwFlags[1] |= FFILEOPEN_READONLY;
1164 dwFlags[2] |= FFILEOPEN_READONLY;
1167 GetOptionsMgr()->SaveOption(OPT_CMP_INCLUDE_SUBDIRS, bRecursive);
1169 rtn &= GetMainFrame()->DoFileOpen(&tFiles, dwFlags, nullptr, sReportFile, bRecursive);
1172 AddToRecentProjectsMRU(sProject.c_str());
1177 * @brief Return windows language ID of current WinMerge GUI language
1179 WORD CMergeApp::GetLangId() const
1181 return m_pLangDlg->GetLangId();
1185 * @brief Lang aware version of CStatusBar::SetIndicators()
1187 void CMergeApp::SetIndicators(CStatusBar &sb, const UINT *rgid, int n) const
1189 m_pLangDlg->SetIndicators(sb, rgid, n);
1193 * @brief Translate menu to current WinMerge GUI language
1195 void CMergeApp::TranslateMenu(HMENU h) const
1197 m_pLangDlg->TranslateMenu(h);
1201 * @brief Translate dialog to current WinMerge GUI language
1203 void CMergeApp::TranslateDialog(HWND h) const
1205 CWnd *pWnd = CWnd::FromHandle(h);
1206 pWnd->SetFont(const_cast<CFont *>(&m_fontGUI));
1207 pWnd->SendMessageToDescendants(WM_SETFONT, (WPARAM)m_fontGUI.m_hObject, MAKELPARAM(FALSE, 0), TRUE);
1209 m_pLangDlg->TranslateDialog(h);
1213 * @brief Load string and translate to current WinMerge GUI language
1215 String CMergeApp::LoadString(UINT id) const
1217 return m_pLangDlg->LoadString(id);
1220 bool CMergeApp::TranslateString(const std::string& str, String& translated_str) const
1222 return m_pLangDlg->TranslateString(str, translated_str);
1226 * @brief Load dialog caption and translate to current WinMerge GUI language
1228 std::wstring CMergeApp::LoadDialogCaption(LPCTSTR lpDialogTemplateID) const
1230 return m_pLangDlg->LoadDialogCaption(lpDialogTemplateID);
1234 * @brief Adds specified file to the recent projects list.
1235 * @param [in] sPathName Path to project file
1237 void CMergeApp::AddToRecentProjectsMRU(LPCTSTR sPathName)
1239 // sPathName will be added to the top of the MRU list.
1240 // If sPathName already exists in the MRU list, it will be moved to the top
1241 if (m_pRecentFileList != nullptr) {
1242 m_pRecentFileList->Add(sPathName);
1243 m_pRecentFileList->WriteList();
1247 void CMergeApp::SetupTempPath()
1249 String instTemp = env::GetPerInstanceString(TempFolderPrefix);
1250 if (GetOptionsMgr()->GetBool(OPT_USE_SYSTEM_TEMP_PATH))
1251 env::SetTemporaryPath(paths::ConcatPath(env::GetSystemTempPath(), instTemp));
1253 env::SetTemporaryPath(paths::ConcatPath(GetOptionsMgr()->GetString(OPT_CUSTOM_TEMP_PATH), instTemp));
1257 * @brief Handles menu selection from recent projects list
1258 * @param [in] nID Menu ID of the selected item
1260 BOOL CMergeApp::OnOpenRecentFile(UINT nID)
1262 return LoadAndOpenProjectFile(static_cast<const TCHAR *>(m_pRecentFileList->m_arrNames[nID-ID_FILE_PROJECT_MRU_FIRST]));
1266 * @brief Return if doc is in Merging/Editing mode
1268 bool CMergeApp::GetMergingMode() const
1270 return m_bMergingMode;
1274 * @brief Set doc to Merging/Editing mode
1276 void CMergeApp::SetMergingMode(bool bMergingMode)
1278 m_bMergingMode = bMergingMode;
1279 GetOptionsMgr()->SaveOption(OPT_MERGE_MODE, m_bMergingMode);
1283 * @brief Switch Merging/Editing mode and update
1284 * buffer read-only states accordingly
1286 void CMergeApp::OnMergingMode()
1288 bool bMergingMode = GetMergingMode();
1291 LangMessageBox(IDS_MERGE_MODE, MB_ICONINFORMATION | MB_DONT_DISPLAY_AGAIN);
1292 SetMergingMode(!bMergingMode);
1296 * @brief Update Menuitem for Merging Mode
1298 void CMergeApp::OnUpdateMergingMode(CCmdUI* pCmdUI)
1300 pCmdUI->Enable(true);
1301 pCmdUI->SetCheck(GetMergingMode());
1305 * @brief Update MergingMode UI in statusbar
1307 void CMergeApp::OnUpdateMergingStatus(CCmdUI *pCmdUI)
1309 String text = theApp.LoadString(IDS_MERGEMODE_MERGING);
1310 pCmdUI->SetText(text.c_str());
1311 pCmdUI->Enable(GetMergingMode());
1314 UINT CMergeApp::GetProfileInt(LPCTSTR lpszSection, LPCTSTR lpszEntry, int nDefault)
1316 COptionsMgr *pOptions = GetOptionsMgr();
1317 String name = strutils::format(_T("%s/%s"), lpszSection, lpszEntry);
1318 if (!pOptions->Get(name).IsInt())
1319 pOptions->InitOption(name, nDefault);
1320 return pOptions->GetInt(name);
1323 BOOL CMergeApp::WriteProfileInt(LPCTSTR lpszSection, LPCTSTR lpszEntry, int nValue)
1325 COptionsMgr *pOptions = GetOptionsMgr();
1326 String name = strutils::format(_T("%s/%s"), lpszSection, lpszEntry);
1327 if (!pOptions->Get(name).IsInt())
1328 pOptions->InitOption(name, nValue);
1329 return pOptions->SaveOption(name, nValue) == COption::OPT_OK;
1332 CString CMergeApp::GetProfileString(LPCTSTR lpszSection, LPCTSTR lpszEntry, LPCTSTR lpszDefault)
1334 COptionsMgr *pOptions = GetOptionsMgr();
1335 String name = strutils::format(_T("%s/%s"), lpszSection, lpszEntry);
1336 if (!pOptions->Get(name).IsString())
1337 pOptions->InitOption(name, lpszDefault ? lpszDefault : _T(""));
1338 return pOptions->GetString(name).c_str();
1341 BOOL CMergeApp::WriteProfileString(LPCTSTR lpszSection, LPCTSTR lpszEntry, LPCTSTR lpszValue)
1343 COptionsMgr *pOptions = GetOptionsMgr();
1344 if (lpszEntry != nullptr)
1346 String name = strutils::format(_T("%s/%s"), lpszSection, lpszEntry);
1347 if (!pOptions->Get(name).IsString())
1348 pOptions->InitOption(name, lpszValue ? lpszValue : _T(""));
1349 return pOptions->SaveOption(name, lpszValue ? lpszValue : _T("")) == COption::OPT_OK;
1353 for (auto& name : pOptions->GetNameList())
1355 if (name.find(lpszSection) == 0)
1356 pOptions->RemoveOption(name);