1 /////////////////////////////////////////////////////////////////////////////
2 // WinMerge: an interactive diff/merge utility
3 // Copyright (C) 1997-2000 Thingamahoochie Software
5 // SPDX-License-Identifier: GPL-2.0-or-later
6 /////////////////////////////////////////////////////////////////////////////
10 * @brief Defines the class behaviors for the application.
16 #include "Constants.h"
17 #include "UnicodeString.h"
19 #include "Environment.h"
20 #include "OptionsMgr.h"
21 #include "OptionsInit.h"
22 #include "RegOptionsMgr.h"
23 #include "IniOptionsMgr.h"
27 #include "HexMergeDoc.h"
28 #include "HexMergeFrm.h"
29 #include "HexMergeView.h"
31 #include "PreferencesDlg.h"
32 #include "SelectPluginDlg.h"
34 #include "MergeEditFrm.h"
39 #include "PropBackups.h"
40 #include "FileOrFolderSelect.h"
41 #include "FileFilterHelper.h"
42 #include "LineFiltersList.h"
43 #include "SubstitutionFiltersList.h"
44 #include "SyntaxColors.h"
45 #include "CCrystalTextMarkers.h"
46 #include "OptionsSyntaxColors.h"
48 #include "ProjectFile.h"
49 #include "MergeEditSplitterView.h"
50 #include "LanguageSelect.h"
51 #include "OptionsDef.h"
52 #include "MergeCmdLineInfo.h"
53 #include "ConflictFileParser.h"
55 #include "stringdiffs.h"
59 #include "CompareStats.h"
61 #include "charsets.h" // For shutdown cleanup
62 #include "OptionsProject.h"
63 #include "MergeAppCOMClass.h"
70 /** @brief Backup file extension. */
71 static const tchar_t BACKUP_FILE_EXT[] = _T("bak");
73 /////////////////////////////////////////////////////////////////////////////
76 BEGIN_MESSAGE_MAP(CMergeApp, CWinApp)
77 //{{AFX_MSG_MAP(CMergeApp)
78 ON_COMMAND(ID_APP_ABOUT, OnAppAbout)
79 ON_COMMAND(ID_HELP, OnHelp)
80 ON_COMMAND_EX_RANGE(ID_FILE_PROJECT_MRU_FIRST, ID_FILE_PROJECT_MRU_LAST, OnOpenRecentFile)
81 ON_UPDATE_COMMAND_UI(ID_FILE_PROJECT_MRU_FIRST, CWinApp::OnUpdateRecentFileMenu)
82 ON_COMMAND(ID_FILE_MERGINGMODE, OnMergingMode)
83 ON_UPDATE_COMMAND_UI(ID_FILE_MERGINGMODE, OnUpdateMergingMode)
84 ON_UPDATE_COMMAND_UI(ID_STATUS_MERGINGMODE, OnUpdateMergingStatus)
85 ON_COMMAND(ID_FILE_PRINT_SETUP, CWinApp::OnFilePrintSetup)
87 // Standard file based document commands
88 //ON_COMMAND(ID_FILE_NEW, CWinApp::OnFileNew)
89 //ON_COMMAND(ID_FILE_OPEN, CWinApp::OnFileOpen)
90 // Standard print setup command
93 /////////////////////////////////////////////////////////////////////////////
94 // CMergeApp construction
96 CMergeApp::CMergeApp() :
97 m_bNeedIdleTimer(false)
98 , m_pOpenTemplate(nullptr)
99 , m_pDiffTemplate(nullptr)
100 , m_pHexMergeTemplate(nullptr)
101 , m_pDirTemplate(nullptr)
102 , m_mainThreadScripts(nullptr)
103 , m_nLastCompareResult(-1)
104 , m_bNonInteractive(false)
105 , m_pOptions(nullptr)
106 , m_pGlobalFileFilter(nullptr)
107 , m_nActiveOperations(0)
108 , m_pLangDlg(new CLanguageSelect())
109 , m_bEscShutdown(false)
110 , m_bExitIfNoDiff(MergeCmdLineInfo::ExitNoDiff::Disabled)
111 , m_pLineFilters(new LineFiltersList())
112 , m_pSubstitutionFiltersList(new SubstitutionFiltersList())
113 , m_pSyntaxColors(new SyntaxColors())
114 , m_pMarkers(new CCrystalTextMarkers())
115 , m_bMergingMode(false)
116 , m_bEnableExitCode(false)
118 // add construction code here,
119 // Place all significant initialization in InitInstance
123 * @brief Chose which options manager should be initialized.
124 * @return IniOptionsMgr if initial config file exists,
125 * CRegOptionsMgr otherwise.
127 static COptionsMgr *CreateOptionManager(const MergeCmdLineInfo& cmdInfo)
129 String iniFilePath = cmdInfo.m_sIniFilepath;
130 if (!iniFilePath.empty())
132 iniFilePath = paths::GetLongPath(iniFilePath);
133 if (paths::CreateIfNeeded(paths::GetParentPath(iniFilePath)))
134 return new CIniOptionsMgr(iniFilePath);
136 iniFilePath = paths::ConcatPath(env::GetProgPath(), _T("winmerge.ini"));
137 if (paths::DoesPathExist(iniFilePath) == paths::IS_EXISTING_FILE)
138 return new CIniOptionsMgr(iniFilePath);
139 return new CRegOptionsMgr();
142 static HANDLE CreateMutexHandle()
144 // Create exclusion mutex name
145 tchar_t szDesktopName[MAX_PATH] = _T("Win9xDesktop");
146 DWORD dwLengthNeeded;
147 GetUserObjectInformation(GetThreadDesktop(GetCurrentThreadId()), UOI_NAME,
148 szDesktopName, sizeof(szDesktopName), &dwLengthNeeded);
149 tchar_t szMutexName[MAX_PATH + 40];
150 // Combine window class name and desktop name to form a unique mutex name.
151 // As the window class name is decorated to distinguish between ANSI and
152 // UNICODE build, so will be the mutex name.
153 wsprintf(szMutexName, _T("%s-%s"), CMainFrame::szClassName, szDesktopName);
154 return CreateMutex(nullptr, FALSE, szMutexName);
157 static HWND ActivatePreviousInstanceAndSendCommandline(tchar_t* cmdLine)
159 HWND hWnd = FindWindow(CMainFrame::szClassName, nullptr);
163 ShowWindow(hWnd, SW_RESTORE);
164 SetForegroundWindow(GetLastActivePopup(hWnd));
165 COPYDATASTRUCT data = { 0, (lstrlen(cmdLine) + 1) * sizeof(tchar_t), cmdLine };
166 if (!SendMessage(hWnd, WM_COPYDATA, NULL, (LPARAM)&data))
171 static void WaitForExitPreviousInstance(HWND hWnd)
173 DWORD dwProcessId = 0;
174 GetWindowThreadProcessId(hWnd, &dwProcessId);
175 HANDLE hProcess = OpenProcess(SYNCHRONIZE, FALSE, dwProcessId);
177 WaitForSingleObject(hProcess, INFINITE);
180 static int ConvertLastCompareResultToExitCode(int nLastCompareResult)
182 if (nLastCompareResult == 0)
184 else if (nLastCompareResult > 0)
189 std::vector<JumpList::Item> CMergeApp::CreateUserTasks(MergeCmdLineInfo::usertasksflags_t flags)
191 std::vector<JumpList::Item> items;
192 if (flags & MergeCmdLineInfo::NEW_TEXT_COMPARE)
193 items.emplace_back(_(""), _T("/new /t text"), _("New Text Compare"), _T(""), _T(""), 0);
194 if (flags & MergeCmdLineInfo::NEW_TABLE_COMPARE)
195 items.emplace_back(_(""), _T("/new /t table"), _("New Table Compare"), _T(""), _T(""), 0);
196 if (flags & MergeCmdLineInfo::NEW_BINARY_COMPARE)
197 items.emplace_back(_(""), _T("/new /t binary"), _("New Binary Compare"), _T(""), _T(""), 0);
198 if (flags & MergeCmdLineInfo::NEW_IMAGE_COMPARE)
199 items.emplace_back(_(""), _T("/new /t image"), _("New Image Compare"), _T(""), _T(""), 0);
200 if (flags & MergeCmdLineInfo::NEW_WEBPAGE_COMPARE)
201 items.emplace_back(_(""), _T("/new /t webpage"), _("New Webpage Compare"), _T(""), _T(""), 0);
202 if (flags & MergeCmdLineInfo::CLIPBOARD_COMPARE)
203 items.emplace_back(_(""), _T("/clipboard-compare"), _("Clipboard Compare"), _T(""), _T(""), 0);
204 if (flags & MergeCmdLineInfo::SHOW_OPTIONS_DIALOG)
205 items.emplace_back(_(""), _T("/show-dialog options"), _("Options"), _T(""), _T(""), 0);
209 CMergeApp::~CMergeApp()
213 /////////////////////////////////////////////////////////////////////////////
214 // The one and only CMergeApp object
218 /////////////////////////////////////////////////////////////////////////////
219 // CMergeApp initialization
222 * @brief Initialize WinMerge application instance.
223 * @return TRUE if application initialization succeeds (and we'll run it),
224 * FALSE if something failed and we exit the instance.
225 * @todo We could handle these failure situations more gratefully, i.e. show
226 * at least some error message to the user..
228 BOOL CMergeApp::InitInstance()
230 // Prevents DLL hijacking
232 ::SetSearchPathMode(BASE_SEARCH_PATH_ENABLE_SAFE_SEARCHMODE | BASE_SEARCH_PATH_PERMANENT);
233 ::SetDllDirectory(_T(""));
235 HMODULE hLibrary = GetModuleHandle(_T("kernel32.dll"));
236 BOOL (WINAPI *pfnSetSearchPathMode)(DWORD) = (BOOL (WINAPI *)(DWORD))GetProcAddress(hLibrary, "SetSearchPathMode");
237 if (pfnSetSearchPathMode != nullptr)
238 pfnSetSearchPathMode(0x00000001L /*BASE_SEARCH_PATH_ENABLE_SAFE_SEARCHMODE*/ | 0x00008000L /*BASE_SEARCH_PATH_PERMANENT*/);
239 BOOL (WINAPI *pfnSetDllDirectoryA)(LPCSTR) = (BOOL (WINAPI *)(LPCSTR))GetProcAddress(hLibrary, "SetDllDirectoryA");
240 if (pfnSetDllDirectoryA != nullptr)
241 pfnSetDllDirectoryA("");
244 JumpList::SetCurrentProcessExplicitAppUserModelID(L"Thingamahoochie.WinMerge");
246 InitCommonControls(); // initialize common control library
247 CWinApp::InitInstance(); // call parent class method
249 m_imageForInitializingGdiplus.Load((IStream*)nullptr); // initialize GDI+
251 // Runtime switch so programmer may set this in interactive debugger
255 // get current setting
256 int tmpFlag = _CrtSetDbgFlag( _CRTDBG_REPORT_FLAG );
257 // Keep freed memory blocks in the heap's linked list and mark them as freed
258 tmpFlag |= _CRTDBG_DELAY_FREE_MEM_DF;
259 // Call _CrtCheckMemory at every allocation and deallocation request.
260 // WARNING: This slows down WinMerge *A LOT*
261 tmpFlag |= _CRTDBG_CHECK_ALWAYS_DF;
262 // Set the new state for the flag
263 _CrtSetDbgFlag( tmpFlag );
266 // CCrystalEdit Drag and Drop functionality needs AfxOleInit.
269 TRACE(_T("AfxOleInitFailed. OLE functionality disabled"));
272 // Standard initialization
273 // If you are not using these features and wish to reduce the size
274 // of your final executable, you should remove from the following
275 // the specific initialization routines you do not need.
277 // Revoke the standard OLE Message Filter to avoid drawing frame while loading files.
278 COleMessageFilter* pOldFilter = AfxOleGetMessageFilter();
279 pOldFilter->Revoke();
281 // Load registry keys from WinMerge.reg if existing WinMerge.reg
282 env::LoadRegistryFromFile(paths::ConcatPath(env::GetProgPath(), _T("WinMerge.reg")));
284 // Parse command-line arguments.
286 MergeCmdLineInfo cmdInfo(_T(""));
288 MergeCmdLineInfo cmdInfo(GetCommandLine());
290 m_pOptions.reset(CreateOptionManager(cmdInfo));
291 if (cmdInfo.m_bNoPrefs)
292 m_pOptions->SetSerializing(false); // Turn off serializing to registry.
294 if (dynamic_cast<CRegOptionsMgr*>(m_pOptions.get()) != nullptr)
295 Options::CopyHKLMValues();
297 Options::Init(m_pOptions.get()); // Implementation in OptionsInit.cpp
298 ApplyCommandLineConfigOptions(cmdInfo);
299 if (cmdInfo.m_sErrorMessages.size() > 0)
301 if (AttachConsole(ATTACH_PARENT_PROCESS))
304 for (auto& msg : cmdInfo.m_sErrorMessages)
306 String line = _T("WinMerge: ") + msg + _T("\n");
307 WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), line.c_str(), static_cast<DWORD>(line.length()), &dwWritten, nullptr);
313 // Initialize temp folder
316 // If paths were given to commandline we consider this being an invoke from
317 // commandline (from other application, shellextension etc).
318 bool bCommandLineInvoke = cmdInfo.m_Files.GetSize() > 0;
320 // WinMerge registry settings are stored under HKEY_CURRENT_USER/Software/Thingamahoochie
321 // This is the name of the company of the original author (Dean Grimm)
322 SetRegistryKey(_T("Thingamahoochie"));
324 int nSingleInstance = cmdInfo.m_nSingleInstance.has_value() ?
325 *cmdInfo.m_nSingleInstance : GetOptionsMgr()->GetInt(OPT_SINGLE_INSTANCE);
327 HANDLE hMutex = CreateMutexHandle();
328 if (hMutex != nullptr)
329 WaitForSingleObject(hMutex, INFINITE);
330 if (nSingleInstance != 0 && GetLastError() == ERROR_ALREADY_EXISTS)
332 // Activate previous instance and send commandline to it
333 HWND hWnd = ActivatePreviousInstanceAndSendCommandline(GetCommandLine());
336 ReleaseMutex(hMutex);
338 if (nSingleInstance != 1)
339 WaitForExitPreviousInstance(hWnd);
344 LoadStdProfileSettings(GetOptionsMgr()->GetInt(OPT_MRU_MAX)); // Load standard INI file options (including MRU)
346 // Initialize i18n (multiple language) support
347 m_pLangDlg->InitializeLanguage((WORD)GetOptionsMgr()->GetInt(OPT_SELECTED_LANGUAGE));
350 UpdateCodepageModule();
352 FileTransform::AutoUnpacking = GetOptionsMgr()->GetBool(OPT_PLUGINS_UNPACKER_MODE);
353 FileTransform::AutoPrediffing = GetOptionsMgr()->GetBool(OPT_PLUGINS_PREDIFFER_MODE);
355 NONCLIENTMETRICS ncm = { sizeof NONCLIENTMETRICS };
356 if (SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof NONCLIENTMETRICS, &ncm, 0))
358 const int lfHeight = -MulDiv(9, CClientDC(CWnd::GetDesktopWindow()).GetDeviceCaps(LOGPIXELSY), 72);
359 if (abs(ncm.lfMenuFont.lfHeight) > abs(lfHeight))
360 ncm.lfMenuFont.lfHeight = lfHeight;
361 if (wcscmp(ncm.lfMenuFont.lfFaceName, L"Meiryo") == 0 || wcscmp(ncm.lfMenuFont.lfFaceName, L"\U000030e1\U000030a4\U000030ea\U000030aa"/* "Meiryo" in Japanese */) == 0)
362 wcscpy_s(ncm.lfMenuFont.lfFaceName, L"Meiryo UI");
363 m_fontGUI.CreateFontIndirect(&ncm.lfMenuFont);
366 if (m_pSyntaxColors != nullptr)
367 Options::SyntaxColors::Init(GetOptionsMgr(), m_pSyntaxColors.get());
369 if (m_pMarkers != nullptr)
370 m_pMarkers->LoadFromRegistry();
372 CCrystalTextView::SetRenderingModeDefault(static_cast<CCrystalTextView::RENDERING_MODE>(GetOptionsMgr()->GetInt(OPT_RENDERING_MODE)));
374 if (m_pLineFilters != nullptr)
375 m_pLineFilters->Initialize(GetOptionsMgr());
377 if (m_pSubstitutionFiltersList != nullptr)
378 m_pSubstitutionFiltersList->Initialize(GetOptionsMgr());
380 // Check if filter folder is set, and create it if not
381 String pathMyFolders = GetOptionsMgr()->GetString(OPT_FILTER_USERPATH);
382 if (pathMyFolders.empty())
384 // No filter path, set it to default and make sure it exists.
385 pathMyFolders = GetOptionsMgr()->GetDefault<String>(OPT_FILTER_USERPATH);
386 GetOptionsMgr()->SaveOption(OPT_FILTER_USERPATH, pathMyFolders);
387 theApp.GetGlobalFileFilter()->SetUserFilterPath(pathMyFolders);
389 if (!paths::CreateIfNeeded(pathMyFolders))
391 // Failed to create a folder, check it didn't already
393 DWORD errCode = GetLastError();
394 if (errCode != ERROR_ALREADY_EXISTS)
396 // Failed to create a folder for filters, fallback to
397 // "My Documents"-folder. It is not worth the trouble to
398 // bother user about this or user more clever solutions.
399 GetOptionsMgr()->SaveOption(OPT_FILTER_USERPATH, env::GetMyDocuments());
403 strdiff::Init(); // String diff init
404 strdiff::SetBreakChars(GetOptionsMgr()->GetString(OPT_BREAK_SEPARATORS).c_str());
406 m_bMergingMode = GetOptionsMgr()->GetBool(OPT_MERGE_MODE);
408 m_mainThreadScripts = new CAssureScriptsForThread(new MergeAppCOMClass());
410 if (cmdInfo.m_nDialogType != MergeCmdLineInfo::NO_DIALOG)
412 ShowDialog(cmdInfo.m_nDialogType);
415 if (cmdInfo.m_bShowCompareAsMenu)
417 cmdInfo.m_bShowCompareAsMenu = false;
418 if (!ShowCompareAsMenu(cmdInfo))
422 // Register the application's document templates. Document templates
423 // serve as the connection between documents, frame windows and views.
426 m_pOpenTemplate = new CMultiDocTemplate(
428 RUNTIME_CLASS(COpenDoc),
429 RUNTIME_CLASS(COpenFrame), // custom MDI child frame
430 RUNTIME_CLASS(COpenView));
431 AddDocTemplate(m_pOpenTemplate);
434 m_pDiffTemplate = new CMultiDocTemplate(
436 RUNTIME_CLASS(CMergeDoc),
437 RUNTIME_CLASS(CMergeEditFrame), // custom MDI child frame
438 RUNTIME_CLASS(CMergeEditSplitterView));
439 AddDocTemplate(m_pDiffTemplate);
442 m_pHexMergeTemplate = new CMultiDocTemplate(
444 RUNTIME_CLASS(CHexMergeDoc),
445 RUNTIME_CLASS(CHexMergeFrame), // custom MDI child frame
446 RUNTIME_CLASS(CHexMergeView));
447 AddDocTemplate(m_pHexMergeTemplate);
450 m_pDirTemplate = new CMultiDocTemplate(
452 RUNTIME_CLASS(CDirDoc),
453 RUNTIME_CLASS(CDirFrame), // custom MDI child frame
454 RUNTIME_CLASS(CDirView));
455 AddDocTemplate(m_pDirTemplate);
457 // create main MDI Frame window
458 CMainFrame* pMainFrame = new CMainFrame;
459 if (!pMainFrame->LoadFrame(IDR_MAINFRAME))
461 if (hMutex != nullptr)
463 ReleaseMutex(hMutex);
468 m_pMainWnd = pMainFrame;
470 // Init menus -- hMenuDefault is for MainFrame
471 pMainFrame->m_hMenuDefault = pMainFrame->NewDefaultMenu();
474 // Note : for Windows98 compatibility, use FromHandle and not Attach/Detach
475 CMenu * pNewMenu = CMenu::FromHandle(pMainFrame->m_hMenuDefault);
476 pMainFrame->MDISetMenu(pNewMenu, nullptr);
478 // The main window has been initialized, so activate it.
479 pMainFrame->ActivateFrame(cmdInfo.m_nCmdShow);
481 // Since this function actually opens paths for compare it must be
482 // called after initializing CMainFrame!
483 bool bContinue = true;
484 if (!ParseArgsAndDoOpen(cmdInfo, pMainFrame) && bCommandLineInvoke)
487 if (hMutex != nullptr)
488 ReleaseMutex(hMutex);
490 // If user wants to cancel the compare, close WinMerge
493 pMainFrame->PostMessage(WM_CLOSE, 0, 0);
497 WinMergeTest::TestAll();
503 static void OpenContributersFile(int&)
505 CMergeApp::OpenFileToExternalEditor(paths::ConcatPath(env::GetProgPath(), ContributorsPath));
508 static void OpenUrl(int&)
510 shell::Open(WinMergeURL);
513 // App command to run the dialog
514 void CMergeApp::OnAppAbout()
517 aboutDlg.m_onclick_contributers += Poco::delegate(OpenContributersFile);
518 aboutDlg.m_onclick_url += Poco::delegate(OpenUrl);
520 aboutDlg.m_onclick_contributers.clear();
521 aboutDlg.m_onclick_url.clear();
524 /////////////////////////////////////////////////////////////////////////////
525 // CMergeApp commands
528 * @brief Called when application is about to exit.
529 * This functions is called when application is exiting, so this is
530 * good place to do cleanups.
531 * @return Application's exit value (returned from WinMain()).
533 int CMergeApp::ExitInstance()
537 // Save registry keys if existing WinMerge.reg
538 env::SaveRegistryToFile(paths::ConcatPath(env::GetProgPath(), _T("WinMerge.reg")), RegDir);
541 const String temp = env::GetTemporaryPath();
542 ClearTempfolder(temp);
544 // Cleanup left over tempfiles from previous instances.
545 // Normally this should not need to do anything - but if for some reason
546 // WinMerge did not delete temp files this makes sure they are removed.
549 delete m_mainThreadScripts;
550 CWinApp::ExitInstance();
553 // There is a problem that OleUninitialize() in mfc/oleinit.cpp, which is called just before the process exits,
554 // hangs in rare cases.
555 // To deal with this problem, force the process to exit
556 // if the process does not exit within 2 seconds after the call to CMergeApp::ExitInstance().
558 [](void*) -> unsigned int {
561 }, nullptr, 0, nullptr);
564 return m_bEnableExitCode ? ConvertLastCompareResultToExitCode(m_nLastCompareResult) : 0;
567 int CMergeApp::DoMessageBox(const tchar_t* lpszPrompt, UINT nType, UINT nIDPrompt)
569 // This is a convenient point for breakpointing !!!
571 // Create a handle to store the parent window of the message box.
572 CWnd* pParentWnd = CWnd::GetActiveWindow();
574 // Check whether an active window was retrieved successfully.
575 if (pParentWnd == nullptr)
577 // Try to retrieve a handle to the last active popup.
578 CWnd * mainwnd = GetMainWnd();
579 if (mainwnd != nullptr)
580 pParentWnd = mainwnd->GetLastActivePopup();
583 // Use our own message box implementation, which adds the
584 // do not show again checkbox, and implements it on subsequent calls
585 // (if caller set the style)
587 if (m_bNonInteractive)
589 if (AttachConsole(ATTACH_PARENT_PROCESS))
592 String line = _T("WinMerge: ") + String(lpszPrompt) + _T("\n");
593 WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), line.c_str(), static_cast<DWORD>(line.length()), &dwWritten, nullptr);
599 // Create the message box dialog.
600 CMessageBoxDialog dlgMessage(pParentWnd, lpszPrompt, _T(""), nType | MB_RIGHT_ALIGN,
603 if (m_pMainWnd->IsIconic())
604 m_pMainWnd->ShowWindow(SW_RESTORE);
606 // Display the message box dialog and return the result.
607 return static_cast<int>(dlgMessage.DoModal());
610 bool CMergeApp::IsReallyIdle() const
613 POSITION pos = m_pDirTemplate->GetFirstDocPosition();
614 while (pos != nullptr)
616 CDirDoc *pDirDoc = static_cast<CDirDoc *>(m_pDirTemplate->GetNextDoc(pos));
617 if (const CompareStats *pCompareStats = pDirDoc->GetCompareStats())
619 if (!pCompareStats->IsCompareDone() || pDirDoc->GetGeneratingReport())
626 BOOL CMergeApp::OnIdle(LONG lCount)
628 if (CWinApp::OnIdle(lCount))
631 // If anyone has requested notification when next idle occurs, send it
632 if (m_bNeedIdleTimer)
634 m_bNeedIdleTimer = false;
635 m_pMainWnd->SendMessageToDescendants(WM_TIMER, IDLE_TIMER, lCount, TRUE, FALSE);
638 if (m_bNonInteractive && IsReallyIdle())
639 m_pMainWnd->PostMessage(WM_CLOSE, 0, 0);
641 if (typeid(*GetOptionsMgr()) == typeid(CRegOptionsMgr))
643 static_cast<CRegOptionsMgr*>(GetOptionsMgr())->CloseKeys();
646 static int count = 0;
650 while (!m_idleFuncs.empty())
652 m_idleFuncs.front()();
653 m_idleFuncs.pop_front();
661 * @brief Load any known file filters.
663 * This function loads filter files from paths we know contain them.
664 * @note User's filter location may not be set yet.
666 void CMergeApp::InitializeFileFilters()
668 assert(m_pGlobalFileFilter != nullptr);
669 const String& filterPath = GetOptionsMgr()->GetString(OPT_FILTER_USERPATH);
671 if (!filterPath.empty())
673 m_pGlobalFileFilter->SetUserFilterPath(filterPath);
675 m_pGlobalFileFilter->LoadAllFileFilters();
678 void CMergeApp::ApplyCommandLineConfigOptions(MergeCmdLineInfo& cmdInfo)
680 if (cmdInfo.m_bNoPrefs)
681 m_pOptions->SetSerializing(false); // Turn off serializing to registry.
683 for (const auto& it : cmdInfo.m_Options)
685 if (m_pOptions->Set(it.first, it.second) == COption::OPT_NOTFOUND)
687 String longname = m_pOptions->ExpandShortName(it.first);
688 if (!longname.empty())
690 m_pOptions->Set(longname, it.second);
694 cmdInfo.m_sErrorMessages.push_back(strutils::format_string1(_T("Invalid key '%1' specified in /config option"), it.first));
700 bool CMergeApp::ShowCompareAsMenu(MergeCmdLineInfo& cmdInfo)
703 VERIFY(menu.LoadMenu(IDR_POPUP_COMPARE));
704 theApp.TranslateMenu(menu.m_hMenu);
705 CMenu* pPopup = menu.GetSubMenu(0);
708 String filteredFilenames = strutils::join(cmdInfo.m_Files.begin(), cmdInfo.m_Files.end(), _T("|"));
709 CMainFrame::AppendPluginMenus(pPopup, filteredFilenames, FileTransform::UnpackerEventNames, true, ID_UNPACKERS_FIRST);
712 GetCursorPos(&point);
715 RECT rc{point.x, point.y, point.x, point.y};
716 wnd.CreateEx(0, _T("static"), _T(""), WS_POPUP, rc, nullptr, 0);
717 wnd.ShowWindow(SW_SHOW);
719 int nID = pPopup->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON | TPM_NONOTIFY | TPM_RETURNCMD, point.x, point.y, &wnd);
722 case ID_MERGE_COMPARE_TEXT:
723 cmdInfo.m_nWindowType = MergeCmdLineInfo::TEXT;
725 case ID_MERGE_COMPARE_TABLE:
726 cmdInfo.m_nWindowType = MergeCmdLineInfo::TABLE;
728 case ID_MERGE_COMPARE_HEX:
729 cmdInfo.m_nWindowType = MergeCmdLineInfo::BINARY;
731 case ID_MERGE_COMPARE_IMAGE:
732 cmdInfo.m_nWindowType = MergeCmdLineInfo::IMAGE;
734 case ID_MERGE_COMPARE_WEBPAGE:
735 cmdInfo.m_nWindowType = MergeCmdLineInfo::WEBPAGE;
738 if (nID == ID_OPEN_WITH_UNPACKER)
740 CSelectPluginDlg dlg(cmdInfo.m_sUnpacker, cmdInfo.m_Files.GetSize() > 0 ? cmdInfo.m_Files[0] : _T(""),
741 CSelectPluginDlg::PluginType::Unpacker, false, AfxGetMainWnd());
742 if (dlg.DoModal() == IDOK)
743 cmdInfo.m_sUnpacker = dlg.GetPluginPipeline();
745 else if(nID >= ID_UNPACKERS_FIRST && nID <= ID_UNPACKERS_LAST)
747 cmdInfo.m_sUnpacker = CMainFrame::GetPluginPipelineByMenuId(nID, FileTransform::UnpackerEventNames, ID_UNPACKERS_FIRST);
758 void CMergeApp::ShowDialog(MergeCmdLineInfo::DialogType type)
762 case MergeCmdLineInfo::OPTIONS_DIALOG:
764 CPreferencesDlg dlg(GetOptionsMgr(), GetMainSyntaxColors());
768 case MergeCmdLineInfo::ABOUT_DIALOG:
778 /** @brief Read command line arguments and open files for comparison.
780 * The name of the function is a legacy code from the time that this function
781 * actually parsed the command line. Today the parsing is done using the
782 * MergeCmdLineInfo class.
783 * @param [in] cmdInfo Commandline parameters info.
784 * @param [in] pMainFrame Pointer to application main frame.
785 * @return `true` if we opened the compare, `false` if the compare was canceled.
787 bool CMergeApp::ParseArgsAndDoOpen(MergeCmdLineInfo& cmdInfo, CMainFrame* pMainFrame)
789 bool bCompared = false;
791 std::unique_ptr<PackingInfo> infoUnpacker;
792 std::unique_ptr<PrediffingInfo> infoPrediffer;
793 unsigned nID = cmdInfo.m_nWindowType == MergeCmdLineInfo::AUTOMATIC ?
794 0 : static_cast<unsigned>(cmdInfo.m_nWindowType) + ID_MERGE_COMPARE_TEXT - 1;
796 m_bNonInteractive = cmdInfo.m_bNonInteractive;
797 m_bEnableExitCode = cmdInfo.m_bEnableExitCode;
799 if (!cmdInfo.m_sUnpacker.empty())
800 infoUnpacker.reset(new PackingInfo(cmdInfo.m_sUnpacker));
802 if (!cmdInfo.m_sPreDiffer.empty())
803 infoPrediffer.reset(new PrediffingInfo(cmdInfo.m_sPreDiffer));
805 if (cmdInfo.m_nDialogType != MergeCmdLineInfo::NO_DIALOG)
807 ShowDialog(cmdInfo.m_nDialogType);
811 if (cmdInfo.m_bShowCompareAsMenu)
813 cmdInfo.m_bShowCompareAsMenu = false;
814 if (!ShowCompareAsMenu(cmdInfo))
818 if (cmdInfo.m_dwUserTasksFlags.has_value())
820 JumpList::AddUserTasks(CreateUserTasks(*cmdInfo.m_dwUserTasksFlags));
822 if (ERROR_SUCCESS == reg.Open(HKEY_CURRENT_USER, RegDir))
823 reg.WriteDword(_T("UserTasksFlags"), *cmdInfo.m_dwUserTasksFlags);
826 // Set the global file filter.
827 if (!cmdInfo.m_sFileFilter.empty())
829 GetGlobalFileFilter()->SetFilter(cmdInfo.m_sFileFilter);
833 if (cmdInfo.m_nCodepage)
835 UpdateDefaultCodepage(2,cmdInfo.m_nCodepage);
838 // Set compare method
839 if (cmdInfo.m_nCompMethod.has_value())
840 GetOptionsMgr()->Set(OPT_CMP_METHOD, *cmdInfo.m_nCompMethod);
842 // Unless the user has requested to see WinMerge's usage open files for
844 if (cmdInfo.m_bShowUsage)
846 ShowHelp(CommandLineHelpLocation);
850 // Set the required information we need from the command line:
852 m_bExitIfNoDiff = cmdInfo.m_bExitIfNoDiff;
853 m_bEscShutdown = cmdInfo.m_bEscShutdown;
855 m_strSaveAsPath = cmdInfo.m_sOutputpath;
857 strDesc[0] = cmdInfo.m_sLeftDesc;
858 if (cmdInfo.m_Files.GetSize() < 3)
860 strDesc[1] = cmdInfo.m_sRightDesc;
864 strDesc[1] = cmdInfo.m_sMiddleDesc;
865 strDesc[2] = cmdInfo.m_sRightDesc;
868 std::unique_ptr<CMainFrame::OpenFileParams> pOpenParams;
869 if (cmdInfo.m_nWindowType == MergeCmdLineInfo::TEXT)
870 pOpenParams.reset(new CMainFrame::OpenTextFileParams());
871 else if (cmdInfo.m_nWindowType == MergeCmdLineInfo::TABLE)
872 pOpenParams.reset(new CMainFrame::OpenTableFileParams());
874 pOpenParams.reset(static_cast<CMainFrame::OpenTableFileParams *>(new CMainFrame::OpenAutoFileParams()));
875 if (auto* pOpenTextFileParams = dynamic_cast<CMainFrame::OpenTextFileParams*>(pOpenParams.get()))
877 pOpenTextFileParams->m_line = cmdInfo.m_nLineIndex;
878 pOpenTextFileParams->m_char = cmdInfo.m_nCharIndex;
879 pOpenTextFileParams->m_fileExt = cmdInfo.m_sFileExt;
881 if (auto* pOpenTableFileParams = dynamic_cast<CMainFrame::OpenTableFileParams*>(pOpenParams.get()))
883 pOpenTableFileParams->m_tableDelimiter = cmdInfo.m_cTableDelimiter;
884 pOpenTableFileParams->m_tableQuote = cmdInfo.m_cTableQuote;
885 pOpenTableFileParams->m_tableAllowNewlinesInQuotes = cmdInfo.m_bTableAllowNewlinesInQuotes;
887 if (cmdInfo.m_Files.GetSize() > 2)
889 cmdInfo.m_dwLeftFlags |= FFILEOPEN_CMDLINE;
890 cmdInfo.m_dwMiddleFlags |= FFILEOPEN_CMDLINE;
891 cmdInfo.m_dwRightFlags |= FFILEOPEN_CMDLINE;
892 fileopenflags_t dwFlags[3] = {cmdInfo.m_dwLeftFlags, cmdInfo.m_dwMiddleFlags, cmdInfo.m_dwRightFlags};
893 bCompared = pMainFrame->DoFileOrFolderOpen(&cmdInfo.m_Files,
894 dwFlags, strDesc, cmdInfo.m_sReportFile, cmdInfo.m_bRecurse, nullptr,
895 infoUnpacker.get(), infoPrediffer.get(), nID, pOpenParams.get());
897 else if (cmdInfo.m_Files.GetSize() > 1)
899 fileopenflags_t dwFlags[3] = {cmdInfo.m_dwLeftFlags, cmdInfo.m_dwRightFlags, FFILEOPEN_NONE};
900 bCompared = pMainFrame->DoFileOrFolderOpen(&cmdInfo.m_Files,
901 dwFlags, strDesc, cmdInfo.m_sReportFile, cmdInfo.m_bRecurse, nullptr,
902 infoUnpacker.get(), infoPrediffer.get(), nID, pOpenParams.get());
904 else if (cmdInfo.m_Files.GetSize() == 1)
906 String sFilepath = cmdInfo.m_Files[0];
907 if (cmdInfo.m_bSelfCompare)
909 strDesc[0] = cmdInfo.m_sLeftDesc;
910 strDesc[1] = cmdInfo.m_sRightDesc;
911 bCompared = pMainFrame->DoSelfCompare(nID, sFilepath, strDesc,
912 infoUnpacker.get(), infoPrediffer.get(), pOpenParams.get());
914 else if (IsProjectFile(sFilepath))
916 bCompared = LoadAndOpenProjectFile(sFilepath);
918 else if (ConflictFileParser::IsConflictFile(sFilepath))
920 //For a conflict file, load the descriptions in their respective positions: (they will be reordered as needed)
921 strDesc[0] = cmdInfo.m_sLeftDesc;
922 strDesc[1] = cmdInfo.m_sMiddleDesc;
923 strDesc[2] = cmdInfo.m_sRightDesc;
924 bCompared = pMainFrame->DoOpenConflict(sFilepath, strDesc);
928 fileopenflags_t dwFlags[3] = {cmdInfo.m_dwLeftFlags, cmdInfo.m_dwRightFlags, FFILEOPEN_NONE};
929 bCompared = pMainFrame->DoFileOrFolderOpen(&cmdInfo.m_Files,
930 dwFlags, strDesc, cmdInfo.m_sReportFile, cmdInfo.m_bRecurse, nullptr,
931 infoUnpacker.get(), infoPrediffer.get(), nID, pOpenParams.get());
934 else if (cmdInfo.m_Files.GetSize() == 0) // if there are no input args, we can check the display file dialog flag
936 if (cmdInfo.m_bNewCompare)
938 fileopenflags_t dwFlags[3] = {cmdInfo.m_dwLeftFlags, cmdInfo.m_dwRightFlags, FFILEOPEN_NONE};
939 bCompared = pMainFrame->DoFileNew(nID, 2, dwFlags, strDesc, infoPrediffer.get(), pOpenParams.get());
941 else if (cmdInfo.m_bClipboardCompare)
943 fileopenflags_t dwFlags[3] = {cmdInfo.m_dwLeftFlags, cmdInfo.m_dwRightFlags, FFILEOPEN_NONE};
944 bCompared = pMainFrame->DoOpenClipboard(nID, 2, dwFlags, strDesc, infoUnpacker.get(), infoPrediffer.get(), pOpenParams.get());
948 bool showFiles = m_pOptions->GetBool(OPT_SHOW_SELECT_FILES_AT_STARTUP);
950 pMainFrame->DoFileOrFolderOpen();
957 void CMergeApp::UpdateDefaultCodepage(int cpDefaultMode, int cpCustomCodepage)
961 switch (cpDefaultMode)
964 ucr::setDefaultCodepage(GetACP());
968 wLangId = GetLangId();
969 if (GetLocaleInfo(wLangId, LOCALE_IDEFAULTANSICODEPAGE, buff, sizeof(buff)/sizeof(buff[0])))
970 ucr::setDefaultCodepage(tc::ttol(buff));
972 ucr::setDefaultCodepage(GetACP());
975 ucr::setDefaultCodepage(cpCustomCodepage);
978 // no other valid option
980 ucr::setDefaultCodepage(GetACP());
985 * @brief Send current option settings into codepage module
987 void CMergeApp::UpdateCodepageModule()
989 // Get current codepage settings from the options module
990 // and push them into the codepage module
991 UpdateDefaultCodepage(GetOptionsMgr()->GetInt(OPT_CP_DEFAULT_MODE), GetOptionsMgr()->GetInt(OPT_CP_DEFAULT_CUSTOM));
994 /** @brief Open help from mainframe when user presses F1*/
995 void CMergeApp::OnHelp()
1001 * @brief Open given file to external editor specified in options.
1002 * @param [in] file Full path to file to open.
1004 * Opens file to defined (in Options/system), Notepad by default,
1005 * external editor. Path is decorated with quotation marks if needed
1006 * (contains spaces). Also '$file' in editor path is replaced by
1008 * @param [in] file Full path to file to open.
1009 * @param [in] nLineNumber Line number to go to.
1011 void CMergeApp::OpenFileToExternalEditor(const String& file, int nLineNumber/* = 1*/)
1013 String sCmd = env::ExpandEnvironmentVariables(GetOptionsMgr()->GetString(OPT_EXT_EDITOR_CMD));
1015 strutils::replace(sCmd, _T("$linenum"), strutils::to_str(nLineNumber));
1017 size_t nIndex = sCmd.find(_T("$file"));
1018 if (nIndex != String::npos)
1020 sFile.insert(0, _T("\""));
1021 strutils::replace(sCmd, _T("$file"), sFile);
1022 nIndex = sCmd.find(' ', nIndex + sFile.length());
1023 if (nIndex != String::npos)
1024 sCmd.insert(nIndex, _T("\""));
1035 bool retVal = false;
1036 STARTUPINFO stInfo = { sizeof STARTUPINFO };
1037 PROCESS_INFORMATION processInfo;
1039 retVal = !!CreateProcess(nullptr, (tchar_t*)sCmd.c_str(),
1040 nullptr, nullptr, FALSE, CREATE_DEFAULT_ERROR_MODE, nullptr, nullptr,
1041 &stInfo, &processInfo);
1045 // Error invoking external editor
1046 String msg = strutils::format_string1(_("Failed to execute external editor: %1"), sCmd);
1047 AfxMessageBox(msg.c_str(), MB_ICONSTOP);
1051 CloseHandle(processInfo.hThread);
1052 CloseHandle(processInfo.hProcess);
1056 /** @brief Returns pointer to global file filter */
1057 FileFilterHelper* CMergeApp::GetGlobalFileFilter()
1059 if (!m_pGlobalFileFilter)
1061 m_pGlobalFileFilter.reset(new FileFilterHelper());
1063 InitializeFileFilters();
1065 // Read last used filter from registry
1066 // If filter fails to set, reset to default
1067 const String filterString = m_pOptions->GetString(OPT_FILEFILTER_CURRENT);
1068 bool bFilterSet = m_pGlobalFileFilter->SetFilter(filterString);
1071 String filter = m_pGlobalFileFilter->GetFilterNameOrMask();
1072 m_pOptions->SaveOption(OPT_FILEFILTER_CURRENT, filter);
1076 return m_pGlobalFileFilter.get();
1080 * @brief Show Help - this is for opening help from outside mainframe.
1081 * @param [in] helpLocation Location inside help, if `nullptr` main help is opened.
1083 void CMergeApp::ShowHelp(const tchar_t* helpLocation /*= nullptr*/)
1085 String sPath = paths::ConcatPath(env::GetProgPath(), strutils::format(DocsPath, GetLangName()));
1086 if (paths::DoesPathExist(sPath) != paths::IS_EXISTING_FILE)
1087 sPath = paths::ConcatPath(env::GetProgPath(), strutils::format(DocsPath, _T("")));
1088 if (helpLocation == nullptr)
1090 if (paths::DoesPathExist(sPath) == paths::IS_EXISTING_FILE)
1091 ::HtmlHelp(nullptr, sPath.c_str(), HH_DISPLAY_TOC, NULL);
1093 shell::Open(DocsURL);
1097 if (paths::DoesPathExist(sPath) == paths::IS_EXISTING_FILE)
1099 sPath += helpLocation;
1100 ::HtmlHelp(nullptr, sPath.c_str(), HH_DISPLAY_TOPIC, NULL);
1106 * @brief Creates backup before file is saved or copied over.
1107 * This function handles formatting correct path and filename for
1108 * backup file. Formatting is done based on several options available
1109 * for users in Options/Backups dialog. After path is formatted, file
1110 * is simply just copied. Not much error checking, just if copying
1111 * succeeded or failed.
1112 * @param [in] bFolder Are we creating backup in folder compare?
1113 * @param [in] pszPath Full path to file to backup.
1114 * @return `true` if backup succeeds, or isn't just done.
1116 bool CMergeApp::CreateBackup(bool bFolder, const String& pszPath)
1118 // If user doesn't want to backups in folder compare, return
1119 // success so operations don't abort.
1120 if (bFolder && !(GetOptionsMgr()->GetBool(OPT_BACKUP_FOLDERCMP)))
1122 // Likewise if user doesn't want backups in file compare
1123 else if (!bFolder && !(GetOptionsMgr()->GetBool(OPT_BACKUP_FILECMP)))
1126 // create backup copy of file if destination file exists
1127 if (paths::DoesPathExist(pszPath) == paths::IS_EXISTING_FILE)
1134 paths::SplitFilename(paths::GetLongPath(pszPath), &path, &filename, &ext);
1136 // Determine backup folder
1137 if (GetOptionsMgr()->GetInt(OPT_BACKUP_LOCATION) ==
1138 PropBackups::FOLDER_ORIGINAL)
1140 // Put backups to same folder than original file
1141 bakPath = std::move(path);
1143 else if (GetOptionsMgr()->GetInt(OPT_BACKUP_LOCATION) ==
1144 PropBackups::FOLDER_GLOBAL)
1146 // Put backups to global folder defined in options
1147 bakPath = GetOptionsMgr()->GetString(OPT_BACKUP_GLOBALFOLDER);
1148 if (bakPath.empty())
1149 bakPath = std::move(path);
1151 bakPath = paths::GetLongPath(bakPath);
1153 paths::CreateIfNeeded(bakPath);
1157 _RPTF0(_CRT_ERROR, "Unknown backup location!");
1160 bool success = false;
1161 if (GetOptionsMgr()->GetBool(OPT_BACKUP_ADD_BAK))
1163 // Don't add dot if there is no existing extension
1166 ext += BACKUP_FILE_EXT;
1169 // Append time to filename if wanted so
1170 // NOTE just adds timestamp at the moment as I couldn't figure out
1171 // nice way to add a real time (invalid chars etc).
1172 if (GetOptionsMgr()->GetBool(OPT_BACKUP_ADD_TIME))
1177 ::localtime_s(&tm, &curtime);
1179 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);
1180 filename += _T("-");
1181 filename += timestr;
1184 // Append filename and extension (+ optional .bak) to path
1185 if ((bakPath.length() + filename.length() + ext.length())
1189 bakPath = paths::ConcatPath(bakPath, filename);
1196 success = !!CopyFileW(TFile(pszPath).wpath().c_str(), TFile(bakPath).wpath().c_str(), FALSE);
1201 String msg = strutils::format_string1(
1202 _("Unable to backup original file:\n%1\n\nContinue anyway?"),
1203 pszPath + _T("\n(\u2192 ") + bakPath + _T(")"));
1204 if (AfxMessageBox(msg.c_str(), MB_YESNO | MB_ICONWARNING | MB_DONT_ASK_AGAIN, IDS_BACKUP_FAILED_PROMPT) != IDYES)
1210 // we got here because we're either not backing up of there was nothing to backup
1215 * @brief Checks if path (file/folder) is read-only and asks overwriting it.
1217 * @param strSavePath [in,out] Path where to save (file or folder)
1218 * @param bMultiFile [in] Single file or multiple files/folder
1219 * @param bApplyToAll [in,out] Apply last user selection for all items?
1220 * @return Users selection:
1221 * - IDOK: Item was not readonly, no actions
1222 * - IDYES/IDYESTOALL: Overwrite readonly item
1223 * - IDNO: User selected new filename (single file) or user wants to skip
1224 * - IDCANCEL: Cancel operation
1225 * @sa CMainFrame::SyncFileToVCS()
1226 * @sa CMergeDoc::DoSave()
1228 int CMergeApp::HandleReadonlySave(String& strSavePath, bool bMultiFile,
1233 bool bFileRO = false;
1234 bool bFileExists = false;
1238 if (!strSavePath.empty())
1242 TFile file(strSavePath);
1243 bFileExists = file.exists();
1245 bFileRO = !file.canWrite();
1252 if (bFileExists && bFileRO)
1254 UINT userChoice = 0;
1256 // Don't ask again if its already asked
1261 // Prompt for user choice
1264 // Multiple files or folder
1265 str = strutils::format_string1(_("%1\nis marked read-only. Would you like to override the read-only item?"), strSavePath);
1266 userChoice = AfxMessageBox(str.c_str(), MB_YESNOCANCEL |
1267 MB_ICONWARNING | MB_DEFBUTTON3 | MB_DONT_ASK_AGAIN |
1268 MB_YES_TO_ALL, IDS_SAVEREADONLY_MULTI);
1273 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);
1274 userChoice = AfxMessageBox(str.c_str(), MB_YESNOCANCEL |
1275 MB_ICONWARNING | MB_DEFBUTTON2 | MB_DONT_ASK_AGAIN,
1276 IDS_SAVEREADONLY_FMT);
1281 // Overwrite read-only file
1283 bApplyToAll = true; // Don't ask again (no break here)
1286 CFile::GetStatus(strSavePath.c_str(), status);
1287 status.m_mtime = 0; // Avoid unwanted changes
1288 status.m_attribute &= ~CFile::readOnly;
1289 CFile::SetStatus(strSavePath.c_str(), status);
1293 // Save to new filename (single) /skip this item (multiple)
1297 if (SelectFile(AfxGetMainWnd()->GetSafeHwnd(), s, false, strSavePath.c_str()))
1318 String CMergeApp::GetPackingErrorMessage(int pane, int paneCount, const String& path, const PackingInfo& plugin)
1320 String pluginName = plugin.GetPluginPipeline();
1321 return strutils::format_string2(
1323 _("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?")
1324 : (pane == paneCount - 1) ?
1325 _("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?")
1326 : _("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?"),
1331 * @brief Is specified file a project file?
1332 * @param [in] filepath Full path to file to check.
1333 * @return true if file is a projectfile.
1335 bool CMergeApp::IsProjectFile(const String& filepath) const
1338 paths::SplitFilename(filepath, nullptr, nullptr, &ext);
1339 if (strutils::compare_nocase(ext, ProjectFile::PROJECTFILE_EXT) == 0)
1345 bool CMergeApp::LoadProjectFile(const String& sProject, ProjectFile &project)
1347 if (sProject.empty())
1352 project.Read(sProject);
1354 catch (Poco::Exception& e)
1356 String sErr = _("Unknown error attempting to open project file.");
1357 sErr += ucr::toTString(e.displayText());
1358 String msg = strutils::format_string2(_("Cannot open file\n%1\n\n%2"), sProject, sErr);
1359 AfxMessageBox(msg.c_str(), MB_ICONSTOP);
1366 bool CMergeApp::SaveProjectFile(const String& sProject, const ProjectFile &project)
1370 project.Save(sProject);
1372 catch (Poco::Exception& e)
1374 String sErr = _("Unknown error attempting to save project file.");
1375 sErr += ucr::toTString(e.displayText());
1376 String msg = strutils::format_string2(_("Cannot open file\n%1\n\n%2"), sProject, sErr);
1377 AfxMessageBox(msg.c_str(), MB_ICONSTOP);
1385 * @brief Read project and perform comparison specified
1386 * @param [in] sProject Full path to project file.
1387 * @return `true` if loading project file and starting compare succeeded.
1389 bool CMergeApp::LoadAndOpenProjectFile(const String& sProject, const String& sReportFile)
1391 ProjectFile project;
1392 if (!LoadProjectFile(sProject, project))
1396 for (auto& projItem : project.Items())
1398 std::unique_ptr<PrediffingInfo> pInfoPrediffer;
1399 std::unique_ptr<PackingInfo> pInfoUnpacker;
1401 bool bDummy = false;
1402 projItem.GetPaths(tFiles, bDummy);
1403 for (int i = 0; i < tFiles.GetSize(); ++i)
1405 tFiles[i] = env::ExpandEnvironmentVariables(tFiles[i]);
1406 if (!paths::IsPathAbsolute(tFiles[i]) && !paths::IsURL(tFiles[i]))
1408 String sProjectDir = paths::GetParentPath(sProject);
1409 if (tFiles[i].substr(0, 1) == _T("\\"))
1411 if (sProjectDir.length() > 1 && sProjectDir[1] == ':')
1412 tFiles[i] = paths::ConcatPath(sProjectDir.substr(0, 2), tFiles[i]);
1415 tFiles[i] = paths::ConcatPath(sProjectDir, tFiles[i]);
1418 bool bLeftReadOnly = projItem.GetLeftReadOnly();
1419 bool bMiddleReadOnly = projItem.GetMiddleReadOnly();
1420 bool bRightReadOnly = projItem.GetRightReadOnly();
1421 if (Options::Project::Get(GetOptionsMgr(), Options::Project::Operation::Open, Options::Project::Item::FileFilter) && projItem.HasFilter())
1423 String filter = projItem.GetFilter();
1424 filter = strutils::trim_ws(filter);
1425 GetGlobalFileFilter()->SetFilter(filter);
1427 bool bRecursive = GetOptionsMgr()->GetBool(OPT_CMP_INCLUDE_SUBDIRS);
1428 if (Options::Project::Get(GetOptionsMgr(), Options::Project::Operation::Open, Options::Project::Item::IncludeSubfolders) && projItem.HasSubfolders())
1429 bRecursive = projItem.GetSubfolders() > 0;
1430 if (Options::Project::Get(GetOptionsMgr(), Options::Project::Operation::Open, Options::Project::Item::Plugin))
1432 if (projItem.HasUnpacker())
1433 pInfoUnpacker.reset(new PackingInfo(projItem.GetUnpacker()));
1434 if (projItem.HasPrediffer())
1435 pInfoPrediffer.reset(new PrediffingInfo(projItem.GetPrediffer()));
1438 if (projItem.HasWindowType())
1439 nID = ID_MERGE_COMPARE_TEXT + projItem.GetWindowType() - 1;
1440 std::unique_ptr<CMainFrame::OpenTableFileParams> pOpenTableFileParams;
1441 if (nID == ID_MERGE_COMPARE_TABLE)
1443 pOpenTableFileParams = std::make_unique<CMainFrame::OpenTableFileParams>();
1444 pOpenTableFileParams->m_tableDelimiter = projItem.GetTableDelimiter();
1445 pOpenTableFileParams->m_tableQuote = projItem.GetTableQuote();
1446 pOpenTableFileParams->m_tableAllowNewlinesInQuotes = projItem.GetTableAllowNewLinesInQuotes();
1450 fileopenflags_t dwFlags[3] = {
1451 static_cast<fileopenflags_t>(tFiles.GetPath(0).empty() ? FFILEOPEN_NONE : FFILEOPEN_PROJECT),
1452 static_cast<fileopenflags_t>(tFiles.GetPath(1).empty() ? FFILEOPEN_NONE : FFILEOPEN_PROJECT),
1453 static_cast<fileopenflags_t>(tFiles.GetPath(2).empty() ? FFILEOPEN_NONE : FFILEOPEN_PROJECT)
1456 dwFlags[0] |= FFILEOPEN_READONLY;
1457 if (projItem.HasLeftDesc())
1458 strDesc[0] = projItem.GetLeftDesc();
1459 if (tFiles.GetSize() == 2)
1462 dwFlags[1] |= FFILEOPEN_READONLY;
1463 if (projItem.HasRightDesc())
1464 strDesc[1] = projItem.GetRightDesc();
1468 if (bMiddleReadOnly)
1469 dwFlags[1] |= FFILEOPEN_READONLY;
1471 dwFlags[2] |= FFILEOPEN_READONLY;
1472 if (projItem.HasMiddleDesc())
1473 strDesc[1] = projItem.GetMiddleDesc();
1474 if (projItem.HasRightDesc())
1475 strDesc[2] = projItem.GetRightDesc();
1478 GetOptionsMgr()->Set(OPT_CMP_INCLUDE_SUBDIRS, bRecursive);
1480 if (Options::Project::Get(GetOptionsMgr(), Options::Project::Operation::Open, Options::Project::Item::CompareOptions))
1482 if (projItem.HasIgnoreWhite())
1483 GetOptionsMgr()->Set(OPT_CMP_IGNORE_WHITESPACE, projItem.GetIgnoreWhite());
1484 if (projItem.HasIgnoreBlankLines())
1485 GetOptionsMgr()->Set(OPT_CMP_IGNORE_BLANKLINES, projItem.GetIgnoreBlankLines());
1486 if (projItem.HasIgnoreCase())
1487 GetOptionsMgr()->Set(OPT_CMP_IGNORE_CASE, projItem.GetIgnoreCase());
1488 if (projItem.HasIgnoreEol())
1489 GetOptionsMgr()->Set(OPT_CMP_IGNORE_EOL, projItem.GetIgnoreEol());
1490 if (projItem.HasIgnoreNumbers())
1491 GetOptionsMgr()->Set(OPT_CMP_IGNORE_NUMBERS, projItem.GetIgnoreNumbers());
1492 if (projItem.HasIgnoreCodepage())
1493 GetOptionsMgr()->Set(OPT_CMP_IGNORE_CODEPAGE, projItem.GetIgnoreCodepage());
1494 if (projItem.HasFilterCommentsLines())
1495 GetOptionsMgr()->Set(OPT_CMP_FILTER_COMMENTLINES, projItem.GetFilterCommentsLines());
1496 if (projItem.HasCompareMethod())
1497 GetOptionsMgr()->Set(OPT_CMP_METHOD, projItem.GetCompareMethod());
1500 std::unique_ptr<CMainFrame::OpenFolderParams> pOpenFolderParams;
1501 if ((Options::Project::Get(GetOptionsMgr(), Options::Project::Operation::Open, Options::Project::Item::HiddenItems)) && projItem.HasHiddenItems())
1503 pOpenFolderParams = std::make_unique<CMainFrame::OpenFolderParams>();
1504 pOpenFolderParams->m_hiddenItems = projItem.GetHiddenItems();
1507 rtn &= GetMainFrame()->DoFileOrFolderOpen(&tFiles, dwFlags, strDesc, sReportFile, bRecursive,
1508 nullptr, pInfoUnpacker.get(), pInfoPrediffer.get(), nID,
1509 nID == ID_MERGE_COMPARE_TABLE ?
1510 static_cast<CMainFrame::OpenFileParams*>(pOpenTableFileParams.get()) :
1511 static_cast<CMainFrame::OpenFileParams*>(pOpenFolderParams.get()));
1514 AddToRecentProjectsMRU(sProject.c_str());
1519 * @brief Return windows language ID of current WinMerge GUI language
1521 WORD CMergeApp::GetLangId() const
1523 return m_pLangDlg->GetLangId();
1526 String CMergeApp::GetLangName() const
1529 paths::SplitFilename(theApp.m_pLangDlg->GetFileName(theApp.GetLangId()), nullptr, &name, &ext);
1534 * @brief Lang aware version of CStatusBar::SetIndicators()
1536 void CMergeApp::SetIndicators(CStatusBar &sb, const UINT *rgid, int n) const
1538 m_pLangDlg->SetIndicators(sb, rgid, n);
1542 * @brief Translate menu to current WinMerge GUI language
1544 void CMergeApp::TranslateMenu(HMENU h) const
1546 m_pLangDlg->TranslateMenu(h);
1550 * @brief Translate dialog to current WinMerge GUI language
1552 void CMergeApp::TranslateDialog(HWND h) const
1554 CWnd *pWnd = CWnd::FromHandle(h);
1555 pWnd->SetFont(const_cast<CFont *>(&m_fontGUI));
1556 pWnd->SendMessageToDescendants(WM_SETFONT, (WPARAM)m_fontGUI.m_hObject, MAKELPARAM(FALSE, 0), TRUE);
1558 m_pLangDlg->TranslateDialog(h);
1562 * @brief Load string and translate to current WinMerge GUI language
1564 String CMergeApp::LoadString(UINT id) const
1566 return m_pLangDlg->LoadString(id);
1569 bool CMergeApp::TranslateString(const std::string& str, String& translated_str) const
1571 return m_pLangDlg->TranslateString(str, translated_str);
1574 bool CMergeApp::TranslateString(const std::wstring& str, String& translated_str) const
1576 return m_pLangDlg->TranslateString(str, translated_str);
1580 * @brief Load dialog caption and translate to current WinMerge GUI language
1582 std::wstring CMergeApp::LoadDialogCaption(const tchar_t* lpDialogTemplateID) const
1584 return m_pLangDlg->LoadDialogCaption(lpDialogTemplateID);
1588 * @brief Adds specified file to the recent projects list.
1589 * @param [in] sPathName Path to project file
1591 void CMergeApp::AddToRecentProjectsMRU(const tchar_t* sPathName)
1593 // sPathName will be added to the top of the MRU list.
1594 // If sPathName already exists in the MRU list, it will be moved to the top
1595 if (m_pRecentFileList != nullptr) {
1596 m_pRecentFileList->Add(sPathName);
1597 m_pRecentFileList->WriteList();
1601 void CMergeApp::SetupTempPath()
1603 String instTemp = env::GetPerInstanceString(TempFolderPrefix);
1604 if (GetOptionsMgr()->GetBool(OPT_USE_SYSTEM_TEMP_PATH))
1605 env::SetTemporaryPath(paths::ConcatPath(env::GetSystemTempPath(), instTemp));
1607 env::SetTemporaryPath(paths::ConcatPath(GetOptionsMgr()->GetString(OPT_CUSTOM_TEMP_PATH), instTemp));
1611 * @brief Handles menu selection from recent projects list
1612 * @param [in] nID Menu ID of the selected item
1614 BOOL CMergeApp::OnOpenRecentFile(UINT nID)
1616 return LoadAndOpenProjectFile(static_cast<const tchar_t *>(m_pRecentFileList->m_arrNames[nID-ID_FILE_PROJECT_MRU_FIRST]));
1620 * @brief Return if doc is in Merging/Editing mode
1622 bool CMergeApp::GetMergingMode() const
1624 return m_bMergingMode;
1628 * @brief Set doc to Merging/Editing mode
1630 void CMergeApp::SetMergingMode(bool bMergingMode)
1632 m_bMergingMode = bMergingMode;
1633 GetOptionsMgr()->SaveOption(OPT_MERGE_MODE, m_bMergingMode);
1637 * @brief Switch Merging/Editing mode and update
1638 * buffer read-only states accordingly
1640 void CMergeApp::OnMergingMode()
1642 bool bMergingMode = GetMergingMode();
1645 LangMessageBox(IDS_MERGE_MODE, MB_ICONINFORMATION | MB_DONT_DISPLAY_AGAIN, IDS_MERGE_MODE);
1646 SetMergingMode(!bMergingMode);
1650 * @brief Update Menuitem for Merging Mode
1652 void CMergeApp::OnUpdateMergingMode(CCmdUI* pCmdUI)
1654 pCmdUI->Enable(true);
1655 pCmdUI->SetCheck(GetMergingMode());
1659 * @brief Update MergingMode UI in statusbar
1661 void CMergeApp::OnUpdateMergingStatus(CCmdUI *pCmdUI)
1663 String text = theApp.LoadString(IDS_MERGEMODE_MERGING);
1664 pCmdUI->SetText(text.c_str());
1665 pCmdUI->Enable(GetMergingMode());
1668 UINT CMergeApp::GetProfileInt(const tchar_t* lpszSection, const tchar_t* lpszEntry, int nDefault)
1670 COptionsMgr *pOptions = GetOptionsMgr();
1671 String name = strutils::format(_T("%s/%s"), lpszSection, lpszEntry);
1672 if (!pOptions->Get(name).IsInt())
1673 pOptions->InitOption(name, nDefault);
1674 return pOptions->GetInt(name);
1677 BOOL CMergeApp::WriteProfileInt(const tchar_t* lpszSection, const tchar_t* lpszEntry, int nValue)
1679 COptionsMgr *pOptions = GetOptionsMgr();
1680 String name = strutils::format(_T("%s/%s"), lpszSection, lpszEntry);
1681 if (!pOptions->Get(name).IsInt())
1682 pOptions->InitOption(name, nValue);
1683 return pOptions->SaveOption(name, nValue) == COption::OPT_OK;
1686 CString CMergeApp::GetProfileString(const tchar_t* lpszSection, const tchar_t* lpszEntry, const tchar_t* lpszDefault)
1688 COptionsMgr *pOptions = GetOptionsMgr();
1689 String name = strutils::format(_T("%s/%s"), lpszSection, lpszEntry);
1690 if (!pOptions->Get(name).IsString())
1691 pOptions->InitOption(name, lpszDefault ? lpszDefault : _T(""));
1692 return pOptions->GetString(name).c_str();
1695 BOOL CMergeApp::WriteProfileString(const tchar_t* lpszSection, const tchar_t* lpszEntry, const tchar_t* lpszValue)
1697 COptionsMgr *pOptions = GetOptionsMgr();
1698 if (lpszEntry != nullptr)
1700 String name = strutils::format(_T("%s/%s"), lpszSection, lpszEntry);
1701 if (!pOptions->Get(name).IsString())
1702 pOptions->InitOption(name, lpszValue ? lpszValue : _T(""));
1703 return pOptions->SaveOption(name, lpszValue ? lpszValue : _T("")) == COption::OPT_OK;
1707 String name = strutils::format(_T("%s/"), lpszSection);
1708 pOptions->RemoveOption(name);