OSDN Git Service

Fix an issue where items with different case are not displayed correctly in the folde...
[winmerge-jp/winmerge-jp.git] / Src / DirDoc.cpp
1 /////////////////////////////////////////////////////////////////////////////
2 //    WinMerge:  an interactive diff/merge utility
3 //    Copyright (C) 1997-2000  Thingamahoochie Software
4 //    Author: Dean Grimm
5 //    SPDX-License-Identifier: GPL-2.0-or-later
6 /////////////////////////////////////////////////////////////////////////////
7 /** 
8  * @file  DirDoc.cpp
9  *
10  * @brief Implementation file for CDirDoc
11  *
12  */
13
14 #include "StdAfx.h"
15 #include "DirDoc.h"
16 #include <Poco/StringTokenizer.h>
17 #include <boost/range/mfc.hpp>
18 #include "Merge.h"
19 #include "IMergeDoc.h"
20 #include "CompareOptions.h"
21 #include "UnicodeString.h"
22 #include "CompareStats.h"
23 #include "FilterList.h"
24 #include "SubstitutionList.h"
25 #include "DirView.h"
26 #include "DirFrame.h"
27 #include "MainFrm.h"
28 #include "paths.h"
29 #include "7zCommon.h"
30 #include "OptionsDef.h"
31 #include "OptionsMgr.h"
32 #include "OptionsDiffOptions.h"
33 #include "LineFiltersList.h"
34 #include "SubstitutionFiltersList.h"
35 #include "FileFilterHelper.h"
36 #include "unicoder.h"
37 #include "DirActions.h"
38 #include "DirScan.h"
39 #include "MessageBoxDialog.h"
40 #include "DirCmpReport.h"
41 #include "DiffWrapper.h"
42 #include "FolderCmp.h"
43 #include "DirViewColItems.h"
44 #include <Poco/Semaphore.h>
45
46 #ifdef _DEBUG
47 #define new DEBUG_NEW
48 #endif
49
50 using Poco::StringTokenizer;
51 using boost::begin;
52 using boost::end;
53
54 int CDirDoc::m_nDirsTemp = 2;
55
56 /////////////////////////////////////////////////////////////////////////////
57 // CDirDoc
58
59 IMPLEMENT_DYNCREATE(CDirDoc, CDocument)
60
61 /**
62  * @brief Constructor.
63  */
64 CDirDoc::CDirDoc()
65 : m_pCtxt(nullptr)
66 , m_pDirView(nullptr)
67 , m_pCompareStats(nullptr)
68 , m_bMarkedRescan(false)
69 , m_pTempPathContext(nullptr)
70 , m_bGeneratingReport(false)
71 , m_pReport(nullptr)
72 {
73         m_nDirs = m_nDirsTemp;
74
75         m_bRO[0] = false;
76         m_bRO[1] = false;
77         m_bRO[2] = false;
78 }
79
80 /**
81  * @brief Destructor.
82  *
83  * Clears document list and deleted possible archive-temp files.
84  */
85 CDirDoc::~CDirDoc()
86 {
87         // Inform all of our merge docs that we're closing
88         for (auto pMergeDoc : m_MergeDocs)
89                 pMergeDoc->DirDocClosing(this);
90         // Delete all temporary folders belonging to this document
91         while (m_pTempPathContext != nullptr)
92         {
93                 m_pTempPathContext = m_pTempPathContext->DeleteHead();
94         }
95 }
96
97 /**
98  * @brief Called when new dirdoc is created.
99  */
100 BOOL CDirDoc::OnNewDocument()
101 {
102         if (!CDocument::OnNewDocument())
103                 return FALSE;
104
105         return TRUE;
106 }
107
108
109 BEGIN_MESSAGE_MAP(CDirDoc, CDocument)
110         //{{AFX_MSG_MAP(CDirDoc)
111                 // NOTE - the ClassWizard will add and remove mapping macros here.
112         // Progress dialog
113         ON_BN_CLICKED(IDC_COMPARISON_STOP, OnBnClickedComparisonStop)
114         ON_BN_CLICKED(IDC_COMPARISON_PAUSE, OnBnClickedComparisonPause)
115         ON_BN_CLICKED(IDC_COMPARISON_CONTINUE, OnBnClickedComparisonContinue)
116         //}}AFX_MSG_MAP
117 END_MESSAGE_MAP()
118
119
120 /////////////////////////////////////////////////////////////////////////////
121 // CDirDoc serialization
122
123 void CDirDoc::Serialize(CArchive& ar)
124 {
125         if (ar.IsStoring())
126         {
127                 // TODO: add storing code here
128         }
129         else
130         {
131                 // TODO: add loading code here
132         }
133 }
134
135 /////////////////////////////////////////////////////////////////////////////
136 // CDirDoc commands
137
138 /**
139  * @brief Initialise directory compare for given paths.
140  *
141  * Initialises directory compare with paths given and recursive choice.
142  * Previous compare context is first free'd.
143  * @param [in] paths Paths to compare
144  * @param [in] bRecursive If `true` subdirectories are included to compare.
145  */
146 void CDirDoc::InitCompare(const PathContext & paths, bool bRecursive, CTempPathContext *pTempPathContext)
147 {
148         // Abort previous comparing
149         while (m_diffThread.GetThreadState() == CDiffThread::THREAD_COMPARING)
150         {
151                 m_diffThread.Abort();
152                 Sleep(50);
153         }
154
155         m_pDirView->DeleteAllDisplayItems();
156         // Anything that can go wrong here will yield an exception.
157         // Default implementation of operator new() never returns `nullptr`.
158         
159         if (m_pCompareStats == nullptr)
160                 m_pCompareStats.reset(new CompareStats(m_nDirs));
161
162         m_pCtxt.reset(new CDiffContext(paths,
163                         GetOptionsMgr()->GetInt(OPT_CMP_METHOD)));
164         m_pCtxt->m_bRecursive = bRecursive;
165
166         if (pTempPathContext != nullptr)
167         {
168                 int nIndex;
169                 for (nIndex = 0; nIndex < m_nDirs; nIndex++)
170                         ApplyDisplayRoot(nIndex, pTempPathContext->m_strDisplayRoot[nIndex]);
171                 pTempPathContext->m_pParent = m_pTempPathContext;
172                 m_pTempPathContext = pTempPathContext;
173                 for (nIndex = 0; nIndex < m_nDirs; nIndex++)
174                         m_pTempPathContext->m_strRoot[nIndex] = m_pCtxt->GetNormalizedPath(nIndex);
175         }
176 }
177
178
179 /**
180  * @brief Load line filters to the compare context.
181  * Loads linefilters, converts them to UTF-8 and sets them for compare context.
182  */
183 void CDirDoc::LoadLineFilterList(CDiffContext *pCtxt)
184 {
185         ASSERT(pCtxt != nullptr);
186         
187         bool bFilters = GetOptionsMgr()->GetBool(OPT_LINEFILTER_ENABLED);
188         String filters = theApp.m_pLineFilters->GetAsString();
189         if (!bFilters || filters.empty())
190         {
191                 pCtxt->m_pFilterList.reset();
192                 return;
193         }
194
195         if (pCtxt->m_pFilterList)
196                 pCtxt->m_pFilterList->RemoveAllFilters();
197         else
198                 pCtxt->m_pFilterList.reset(new FilterList());
199
200         std::string regexp_str = ucr::toUTF8(filters);
201
202         // Add every "line" of regexps to regexp list
203         StringTokenizer tokens(regexp_str, "\r\n");
204         for (StringTokenizer::Iterator it = tokens.begin(); it != tokens.end(); ++it)
205                 pCtxt->m_pFilterList->AddRegExp(*it);
206 }
207
208 void CDirDoc::LoadSubstitutionFiltersList(CDiffContext* pCtxt)
209 {
210         ASSERT(pCtxt != nullptr);
211
212         bool SubstitutionFiltersEnabled = theApp.m_pSubstitutionFiltersList->GetEnabled();
213         if (!SubstitutionFiltersEnabled || theApp.m_pSubstitutionFiltersList->GetCount() == 0)
214         {
215                 pCtxt->m_pSubstitutionList.reset();
216                 return;
217         }
218
219         pCtxt->m_pSubstitutionList = theApp.m_pSubstitutionFiltersList->MakeSubstitutionList();
220 }
221
222 void CDirDoc::DiffThreadCallback(int& state)
223 {
224         PostMessage(m_pDirView->GetSafeHwnd(), MSG_UI_UPDATE, state, false);
225 }
226
227 void CDirDoc::InitDiffContext(CDiffContext *pCtxt)
228 {
229         LoadLineFilterList(pCtxt);
230         LoadSubstitutionFiltersList(pCtxt);
231
232         DIFFOPTIONS options = {0};
233         Options::DiffOptions::Load(GetOptionsMgr(), options);
234
235         pCtxt->CreateCompareOptions(GetOptionsMgr()->GetInt(OPT_CMP_METHOD), options);
236
237         pCtxt->m_iGuessEncodingType = GetOptionsMgr()->GetInt(OPT_CP_DETECT);
238         if ((pCtxt->m_iGuessEncodingType >> 16) == 0)
239                 pCtxt->m_iGuessEncodingType |= 50001 << 16;
240         pCtxt->m_bIgnoreSmallTimeDiff = GetOptionsMgr()->GetBool(OPT_IGNORE_SMALL_FILETIME);
241         pCtxt->m_bStopAfterFirstDiff = GetOptionsMgr()->GetBool(OPT_CMP_STOP_AFTER_FIRST);
242         pCtxt->m_nQuickCompareLimit = GetOptionsMgr()->GetInt(OPT_CMP_QUICK_LIMIT);
243         pCtxt->m_nBinaryCompareLimit = GetOptionsMgr()->GetInt(OPT_CMP_BINARY_LIMIT);
244         pCtxt->m_bPluginsEnabled = GetOptionsMgr()->GetBool(OPT_PLUGINS_ENABLED);
245         pCtxt->m_bWalkUniques = GetOptionsMgr()->GetBool(OPT_CMP_WALK_UNIQUE_DIRS);
246         pCtxt->m_bIgnoreReparsePoints = GetOptionsMgr()->GetBool(OPT_CMP_IGNORE_REPARSE_POINTS);
247         pCtxt->m_bIgnoreCodepage = GetOptionsMgr()->GetBool(OPT_CMP_IGNORE_CODEPAGE);
248         pCtxt->m_bEnableImageCompare = GetOptionsMgr()->GetBool(OPT_CMP_ENABLE_IMGCMP_IN_DIRCMP);
249         pCtxt->m_dColorDistanceThreshold = GetOptionsMgr()->GetInt(OPT_CMP_IMG_THRESHOLD) / 1000.0;
250         if (m_pDirView)
251                 pCtxt->m_pPropertySystem.reset(new PropertySystem(m_pDirView->GetDirViewColItems()->GetAdditionalPropertyNames()));
252
253         m_imgfileFilter.UseMask(true);
254         m_imgfileFilter.SetMask(GetOptionsMgr()->GetString(OPT_CMP_IMG_FILEPATTERNS));
255         pCtxt->m_pImgfileFilter = &m_imgfileFilter;
256
257         pCtxt->m_pCompareStats = m_pCompareStats.get();
258
259         // Make sure filters are up-to-date
260         auto* pGlobalFileFilter = theApp.GetGlobalFileFilter();
261         pGlobalFileFilter->ReloadUpdatedFilters();
262         m_fileHelper.CloneFrom(pGlobalFileFilter);
263         pCtxt->m_piFilterGlobal = &m_fileHelper;
264         
265         // All plugin management is done by our plugin manager
266         pCtxt->m_piPluginInfos = GetOptionsMgr()->GetBool(OPT_PLUGINS_ENABLED) ? &m_pluginman : nullptr;
267 }
268
269 /**
270  * @brief Perform directory comparison again from scratch
271  */
272 void CDirDoc::Rescan()
273 {
274         if (m_pCtxt == nullptr)
275                 return;
276
277         CDirFrame *pf = m_pDirView->GetParentFrame();
278
279         // If we're already doing a rescan, bail out
280         UINT threadState = m_diffThread.GetThreadState();
281         if (threadState == CDiffThread::THREAD_COMPARING)
282                 return;
283
284         if (!m_bGeneratingReport)
285                 m_pCompareStats->Reset();
286         m_pDirView->StartCompare(m_pCompareStats.get());
287
288         if (m_pCmpProgressBar == nullptr)
289                 m_pCmpProgressBar.reset(new DirCompProgressBar());
290
291         if (!::IsWindow(m_pCmpProgressBar->GetSafeHwnd()))
292                 m_pCmpProgressBar->Create(m_pDirView->GetParentFrame());
293
294         m_pCmpProgressBar->SetCompareStat(m_pCompareStats.get());
295         m_pCmpProgressBar->StartUpdating();
296
297         m_pDirView->GetParentFrame()->ShowControlBar(m_pCmpProgressBar.get(), TRUE, FALSE);
298
299         if (!m_bGeneratingReport)
300                 m_pDirView->DeleteAllDisplayItems();
301         // Don't clear if only scanning selected items
302         if (!m_bMarkedRescan && !m_bGeneratingReport)
303         {
304                 m_pCtxt->RemoveAll();
305                 m_pCtxt->InitDiffItemList();
306         }
307
308         InitDiffContext(m_pCtxt.get());
309
310         pf->GetHeaderInterface()->SetPaneCount(m_nDirs);
311         pf->GetHeaderInterface()->SetOnSetFocusCallback([&](int pane) {
312                 m_pDirView->SetActivePane(pane);
313                 GetOptionsMgr()->SaveOption(OPT_ACTIVE_PANE, pane);
314         });
315         for (int nIndex = 0; nIndex < m_nDirs; nIndex++)
316         {
317                 UpdateHeaderPath(nIndex);
318                 // draw the headers as inactive ones
319                 pf->GetHeaderInterface()->SetActive(nIndex, false);
320         }
321         pf->GetHeaderInterface()->Resize();
322         int nPane = GetOptionsMgr()->GetInt(OPT_ACTIVE_PANE);
323         m_pDirView->SetActivePane((nPane >= 0 && nPane < m_nDirs) ? nPane : 0);
324
325         // Show current compare method name and active filter name in statusbar
326         pf->SetFilterStatusDisplay(theApp.GetGlobalFileFilter()->GetFilterNameOrMask().c_str());
327         pf->SetCompareMethodStatusDisplay(m_pCtxt->GetCompareMethod());
328
329         // Folder names to compare are in the compare context
330         m_diffThread.SetContext(m_pCtxt.get());
331         m_diffThread.RemoveListener(this, &CDirDoc::DiffThreadCallback);
332         m_diffThread.AddListener(this, &CDirDoc::DiffThreadCallback);
333         if (m_bGeneratingReport)
334         {
335                 m_diffThread.SetCollectFunction([&](DiffFuncStruct* myStruct) {
336                         int m = 0;
337                         if (m_pReport->GetCopyToClipboard())
338                         {
339                                 ++m;
340                                 if (m_pReport->GetReportType() == REPORT_TYPE_SIMPLEHTML)
341                                         ++m;
342                         }
343                         if (!m_pReport->GetReportFile().empty())
344                                 ++m;
345                         myStruct->context->m_pCompareStats->IncreaseTotalItems(
346                                 (m_pDirView->GetListCtrl().GetItemCount() - (myStruct->context->m_bRecursive ? 0 : 1)) * m);
347                 });
348                 m_diffThread.SetCompareFunction([&](DiffFuncStruct* myStruct) {
349                         m_pReport->SetDiffFuncStruct(myStruct);
350                         myStruct->pSemaphore->wait();
351                         String errStr;
352                         if (m_pReport->GenerateReport(errStr))
353                         {
354                                 if (errStr.empty())
355                                 {
356                                         if (GetReportFile().empty())
357                                                 LangMessageBox(IDS_REPORT_SUCCESS, MB_OK | MB_ICONINFORMATION);
358                                 }
359                                 else
360                                 {
361                                         String msg = strutils::format_string1(
362                                                 _("Error creating the report:\n%1"),
363                                                 errStr);
364                                         AfxMessageBox(msg.c_str(), MB_OK | MB_ICONSTOP);
365                                 }
366                         }
367                         SetGeneratingReport(false);
368                         SetReport(nullptr);
369                 });
370                 m_diffThread.SetMarkedRescan(false);
371         }
372         else if (m_bMarkedRescan)
373         {
374                 m_diffThread.SetCollectFunction([](DiffFuncStruct* myStruct) {
375                         int nItems = DirScan_UpdateMarkedItems(myStruct, nullptr);
376                         myStruct->context->m_pCompareStats->IncreaseTotalItems(nItems);
377                         });
378                 m_diffThread.SetCompareFunction([](DiffFuncStruct* myStruct) {
379                         DirScan_CompareRequestedItems(myStruct, nullptr);
380                         });
381                 m_diffThread.SetMarkedRescan(true);
382         }
383         else
384         {
385                 m_diffThread.SetCollectFunction([](DiffFuncStruct* myStruct) {
386                         bool casesensitive = false;
387                         int depth = myStruct->context->m_bRecursive ? -1 : 0;
388                         PathContext paths = myStruct->context->GetNormalizedPaths();
389                         String subdir[3] = {_T(""), _T(""), _T("")}; // blank to start at roots specified in diff context
390                         // Build results list (except delaying file comparisons until below)
391                         DirScan_GetItems(paths, subdir, myStruct,
392                                         casesensitive, depth, nullptr, myStruct->context->m_bWalkUniques);
393                 });
394                 m_diffThread.SetCompareFunction([](DiffFuncStruct* myStruct) {
395                         DirScan_CompareItems(myStruct, nullptr);
396                 });
397                 m_diffThread.SetMarkedRescan(false);
398         }
399         m_diffThread.CompareDirectories();
400         m_bMarkedRescan = false;
401 }
402
403 /**
404  * @brief Empty & reload listview (of files & columns) with comparison results
405  * @todo Better solution for special items ("..")?
406  */
407 void CDirDoc::Redisplay()
408 {
409         if (m_pDirView == nullptr)
410                 return;
411
412         // Do not redisplay an empty CDirView
413         // Not only does it not have results, but AddSpecialItems will crash
414         // trying to dereference null context pointer to get to paths
415         if (!HasDiffs())
416                 return;
417
418         m_pDirView->Redisplay();
419 }
420
421 CDirView * CDirDoc::GetMainView() const
422 {
423         CDirView *pView = nullptr;
424         if (POSITION pos = GetFirstViewPosition())
425         {
426                 pView = static_cast<CDirView*>(GetNextView(pos));
427                 ASSERT_KINDOF(CDirView, pView);
428         }
429         return pView;
430 }
431
432 /**
433  * @brief Update in-memory diffitem status from disk and update view.
434  * @param [in] diffPos POSITION of item in UI list.
435  * @param [in] idx index to reload.
436  * @note Do not call this function from DirView code! This function
437  * calls slow DirView functions to get item position and to update GUI.
438  * Use UpdateStatusFromDisk() function instead.
439  */
440 void CDirDoc::ReloadItemStatus(DIFFITEM *diffPos, int idx)
441 {
442         // in case just copied (into existence) or modified
443         m_pCtxt->UpdateStatusFromDisk(diffPos, idx);
444
445         int nIdx = m_pDirView->GetItemIndex(diffPos);
446         if (nIdx != -1)
447         {
448                 // Update view
449                 m_pDirView->UpdateDiffItemStatus(nIdx);
450         }
451 }
452
453 void CDirDoc::InitStatusStrings()
454 {
455
456 }
457
458 /**
459  * @brief Update any resources necessary after a GUI language change
460  */
461 void CDirDoc::UpdateResources()
462 {
463         if (m_pDirView != nullptr)
464                 m_pDirView->UpdateResources();
465
466         SetTitle(nullptr);
467
468         Redisplay();
469 }
470
471 /**
472  * @brief Stash away our view pointer.
473  */
474 void CDirDoc::SetDirView(CDirView * newView)
475 {
476         m_pDirView = newView;
477         // MFC has a view list for us, so lets check against it
478         POSITION pos = GetFirstViewPosition();
479         CDirView * temp = static_cast<CDirView *>(GetNextView(pos));
480         ASSERT(temp == m_pDirView); // verify that our stashed pointer is the same as MFC's
481 }
482
483 /**
484  * @brief A new MergeDoc has been opened.
485  */
486 void CDirDoc::AddMergeDoc(IMergeDoc * pMergeDoc)
487 {
488         ASSERT(pMergeDoc != nullptr);
489         m_MergeDocs.AddTail(pMergeDoc);
490 }
491
492 /**
493  * @brief MergeDoc informs us it is closing.
494  */
495 void CDirDoc::MergeDocClosing(IMergeDoc * pMergeDoc)
496 {
497         ASSERT(pMergeDoc != nullptr);
498         if (POSITION pos = m_MergeDocs.CPtrList::Find(pMergeDoc))
499                 m_MergeDocs.RemoveAt(pos);
500         else
501                 ASSERT(false);
502
503         // If dir compare is empty (no compare results) and we are not closing
504         // because of reuse close also dir compare
505         if (m_pDirView != nullptr)
506         {
507                 if (m_pCtxt == nullptr)
508                         m_pDirView->PostMessage(WM_COMMAND, ID_FILE_CLOSE);
509         }
510         else if (m_MergeDocs.GetCount() == 0)
511         {
512                 delete this;
513         }
514 }
515
516 /**
517  * @brief Close MergeDocs opened from DirDoc.
518  *
519  * Asks confirmation for docs containing unsaved data and then
520  * closes MergeDocs.
521  * @return `true` if success, `false` if user canceled or closing failed
522  */
523 bool CDirDoc::CloseMergeDocs()
524 {
525         while (!m_MergeDocs.IsEmpty())
526                 if (!m_MergeDocs.GetTail()->CloseNow())
527                         return false;
528         return true;
529 }
530
531 /**
532  * @brief Update changed item's compare status
533  * @param [in] paths Paths for files we update
534  * @param [in] nDiffs Total amount of differences
535  * @param [in] nTrivialDiffs Amount of ignored differences
536  * @param [in] bIdentical `true` if files became identical, `false` otherwise.
537  */
538 void CDirDoc::UpdateChangedItem(const PathContext &paths,
539         UINT nDiffs, UINT nTrivialDiffs, bool bIdentical)
540 {
541         DIFFITEM *pos = FindItemFromPaths(*m_pCtxt, paths);
542         // If we failed files could have been swapped so lets try again
543         if (!pos)
544         {
545                 PathContext pathsSwapped(paths);
546                 std::swap(pathsSwapped[0], pathsSwapped[pathsSwapped.GetSize() - 1]);
547                 pos = FindItemFromPaths(*m_pCtxt, pathsSwapped);
548                 if (!pos && paths.GetSize() > 2)
549                 {
550                         pathsSwapped = paths;
551                         std::swap(pathsSwapped[0], pathsSwapped[1]);
552                         pos = FindItemFromPaths(*m_pCtxt, pathsSwapped);
553                         if (!pos && paths.GetSize() > 2)
554                         {
555                                 pathsSwapped = paths;
556                                 std::swap(pathsSwapped[1], pathsSwapped[2]);
557                                 pos = FindItemFromPaths(*m_pCtxt, pathsSwapped);
558                         }
559                 }
560         }
561         
562         // Update status if paths were found for items.
563         // Fail means we had unique items compared as 'renamed' items
564         // so there really is not status to update.
565         if (pos > 0)
566         {
567                 // Figure out new status code
568                 UINT diffcode = (bIdentical ? DIFFCODE::SAME : DIFFCODE::DIFF);
569
570                 // Update both views and diff context memory
571                 m_pCtxt->SetDiffStatusCode(pos, diffcode, DIFFCODE::COMPAREFLAGS);
572
573                 if (nDiffs != -1 && nTrivialDiffs != -1)
574                         m_pCtxt->SetDiffCounts(pos, nDiffs, nTrivialDiffs);
575                 for (int i = 0; i < m_pCtxt->GetCompareDirs(); ++i)
576                         ReloadItemStatus(pos, i);
577         }
578 }
579
580 /**
581  * @brief Cleans up after directory compare
582  */
583 void CDirDoc::CompareReady()
584 {
585         // Close and destroy the dialog after compare
586         if (m_pCmpProgressBar != nullptr)
587                 m_pDirView->GetParentFrame()->ShowControlBar(m_pCmpProgressBar.get(), FALSE, FALSE);
588         m_pCmpProgressBar.reset();
589 }
590
591 /**
592  * @brief Refresh cached options.
593  *
594  * For compare speed, we have to cache some frequently needed options,
595  * instead of getting option value every time from OptionsMgr. This
596  * function must be called every time options are changed to OptionsMgr.
597  */
598 void CDirDoc::RefreshOptions()
599 {
600         if (m_pCtxt != nullptr)
601                 m_pCtxt->m_bRecursive = GetOptionsMgr()->GetBool(OPT_CMP_INCLUDE_SUBDIRS);
602         if (m_pDirView != nullptr)
603                 m_pDirView->RefreshOptions();
604 }
605
606 /**
607  * @brief Write path and filename to headerbar
608  * @note SetText() does not repaint unchanged text
609  */
610 void CDirDoc::UpdateHeaderPath(int nIndex)
611 {
612         CDirFrame *pf = m_pDirView->GetParentFrame();
613         ASSERT(pf != nullptr);
614         String sText;
615
616         if (!m_strDesc[nIndex].empty())
617                 sText = m_strDesc[nIndex];
618         else
619         {
620                 sText = m_pCtxt->GetPath(nIndex);
621                 ApplyDisplayRoot(nIndex, sText);
622         }
623
624         pf->GetHeaderInterface()->SetText(nIndex, sText);
625 }
626
627 /**
628  * @brief virtual override called just before document is saved and closed
629  */
630 BOOL CDirDoc::SaveModified() 
631 {
632         // Do not allow closing if there is a thread running
633         if (m_diffThread.GetThreadState() == CDiffThread::THREAD_COMPARING)
634         {
635                 int ans = LangMessageBox(IDS_CONFIRM_CLOSE_WINDOW, MB_YESNO | MB_ICONWARNING);
636                 if (ans == IDNO)
637                         return FALSE;
638                 m_diffThread.Abort();
639                 while (m_diffThread.GetThreadState() == CDiffThread::THREAD_COMPARING)
640                         Sleep(50);
641                 CompareReady();
642         }
643         
644         return CDocument::SaveModified();
645 }
646
647 /**
648  * @brief Send signal to thread to stop current scan
649  *
650  * @sa CDirCompStateBar::OnStop()
651  */
652 void CDirDoc::AbortCurrentScan()
653 {
654         m_diffThread.Abort();
655 }
656
657 /**
658  * @brief Send signal to thread to pause current scan
659  */
660 void CDirDoc::PauseCurrentScan()
661 {
662         m_diffThread.Pause();
663 }
664
665 /**
666  * @brief Send signal to thread to continue current scan
667  */
668 void CDirDoc::ContinueCurrentScan()
669 {
670         m_diffThread.Continue();
671 }
672
673 /**
674  * @brief Returns true if there is an active scan that hasn't been aborted.
675  */
676 bool CDirDoc::IsCurrentScanAbortable() const
677 {
678         return (m_diffThread.GetThreadState() == CDiffThread::THREAD_COMPARING 
679                 && !m_diffThread.IsAborting());
680 }
681
682 /**
683  * @brief Set directory description texts shown in headerbar
684  */
685 void CDirDoc::SetDescriptions(const String strDesc[])
686 {
687         if (strDesc != nullptr)
688                 std::copy_n(strDesc, m_nDirs, m_strDesc);
689 }
690
691 /**
692  * @brief Replace internal root by display root
693  * When we have a archive file open, this function converts physical folder
694  * (that is in the temp folder where archive was extracted) to the virtual
695  * path for showing. The virtual path is path to the archive file, archive
696  * file name and folder inside the archive.
697  * @param [in, out] sText Path to convert.
698  */
699 void CDirDoc::ApplyDisplayRoot(int nIndex, String &sText)
700 {
701         if (m_pTempPathContext != nullptr)
702         {
703                 if (sText.find(m_pTempPathContext->m_strRoot[nIndex]) == String::npos)
704                 {
705                         for (int pane = 0; pane < m_nDirs; ++pane)
706                         {
707                                 if (sText.find(m_pTempPathContext->m_strRoot[pane]) != String::npos)
708                                 {
709                                         nIndex = pane;
710                                         break;
711                                 }
712                         }
713                 }
714                 sText.erase(0, m_pTempPathContext->m_strRoot[nIndex].length());
715                 sText.insert(0, m_pTempPathContext->m_strDisplayRoot[nIndex]);
716         }
717 }
718
719 /**
720  * @brief Set document title to given string or items compared.
721  * 
722  * Formats and sets caption for directory compare window. Caption
723  * has left- and right-side paths separated with '-'.
724  *
725  * @param [in] lpszTitle New title for window. If this parameter
726  * is not `nullptr` we use this string, otherwise format caption from
727  * actual paths.
728  */
729 void CDirDoc::SetTitle(LPCTSTR lpszTitle)
730 {
731         if (m_pDirView == nullptr)
732                 return;
733
734         if (lpszTitle != nullptr)
735                 CDocument::SetTitle(lpszTitle);
736         else if (m_pCtxt == nullptr || m_pCtxt->GetLeftPath().empty() ||
737                 m_pCtxt->GetRightPath().empty() || 
738                 (m_nDirs > 2 && m_pCtxt->GetMiddlePath().empty()))
739         {
740                 String title = _("Folder Comparison Results");
741                 CDocument::SetTitle(title.c_str());
742         }
743         else
744         {
745                 String sTitle;
746                 String sDirName[3];
747                 for (int index = 0; index < m_nDirs; index++)
748                 {
749                         String strPath = m_pCtxt->GetPath(index);
750                         ApplyDisplayRoot(index, strPath);
751                         sDirName[index] = paths::FindFileName(strPath);
752                 }
753                 if (std::count(&sDirName[0], &sDirName[0] + m_nDirs, sDirName[0]) == m_nDirs)
754                         sTitle = sDirName[0] + strutils::format(_T(" x %d"), m_nDirs);
755                 else
756                         sTitle = strutils::join(&sDirName[0], &sDirName[0] + m_nDirs, _T(" - "));
757                 CDocument::SetTitle(sTitle.c_str());
758         }       
759 }
760
761 /**
762  * @brief A string to display as a tooltip for MDITabbar
763  */
764 CString CDirDoc::GetTooltipString() const
765 {
766         return CMergeFrameCommon::GetTooltipString(m_pCtxt->GetNormalizedPaths(), m_strDesc, nullptr, nullptr).c_str();
767 }
768
769 /**
770  * @brief Checks if current folders are opened from archive file.
771  * @return true if we are inside archive, false otherwise.
772  */
773 bool CDirDoc::IsArchiveFolders() const
774 {
775         if (m_pTempPathContext != nullptr)
776                 return true;
777         else
778                 return false;
779 }
780
781 void CDirDoc::Swap(int idx1, int idx2)
782 {
783         if (m_diffThread.GetThreadState() != CDiffThread::THREAD_COMPLETED)
784                 return;
785         std::swap(m_bRO[idx1], m_bRO[idx2]);
786         std::swap(m_strDesc[idx1], m_strDesc[idx2]);
787         if (m_pTempPathContext != nullptr)
788                 m_pTempPathContext->Swap(idx1, idx2);
789         m_pCtxt->Swap(idx1, idx2);
790         m_pCompareStats->Swap(idx1, idx2);
791         for (int nIndex = 0; nIndex < m_nDirs; nIndex++)
792                 UpdateHeaderPath(nIndex);
793         SetTitle(nullptr);
794 }
795
796 bool CDirDoc::MoveableToNextDiff()
797 {
798         if (m_pDirView == nullptr)
799                 return false;
800         CMessageBoxDialog dlg(nullptr, _("Do you want to move to the next file?").c_str());
801         const int nFormerResult = dlg.GetFormerResult();
802         if (nFormerResult != -1 && nFormerResult == IDNO)
803                 return false;
804         return m_pDirView->HasNextDiff();
805 }
806
807 bool CDirDoc::MoveableToPrevDiff()
808 {
809         if (m_pDirView == nullptr)
810                 return false;
811         CMessageBoxDialog dlg(nullptr, _("Do you want to move to the previous file?").c_str());
812         const int nFormerResult = dlg.GetFormerResult();
813         if (nFormerResult != -1 && nFormerResult == IDNO)
814                 return false;
815         return m_pDirView->HasPrevDiff();
816 }
817
818 void CDirDoc::MoveToNextDiff(IMergeDoc *pMergeDoc)
819 {
820         if (m_pDirView == nullptr)
821                 return;
822         if (AfxMessageBox(_("Do you want to move to the next file?").c_str(), MB_YESNO | MB_DONT_ASK_AGAIN, IDS_MOVE_TO_NEXTFILE) == IDYES)
823         {
824                 pMergeDoc->CloseNow();
825                 m_pDirView->OpenNextDiff();
826                 GetMainFrame()->OnUpdateFrameTitle(FALSE);
827         }
828 }
829
830 void CDirDoc::MoveToPrevDiff(IMergeDoc *pMergeDoc)
831 {
832         if (m_pDirView == nullptr)
833                 return;
834         if (AfxMessageBox(_("Do you want to move to the previous file?").c_str(), MB_YESNO | MB_DONT_ASK_AGAIN, IDS_MOVE_TO_PREVFILE) == IDYES)
835         {
836                 pMergeDoc->CloseNow();
837                 m_pDirView->OpenPrevDiff();
838                 GetMainFrame()->OnUpdateFrameTitle(FALSE);
839         }
840 }
841
842 void CDirDoc::MoveToFirstFile(IMergeDoc* pMergeDoc)
843 {
844         if (m_pDirView == nullptr)
845                 return;
846         if (AfxMessageBox(_("Do you want to move to the first file?").c_str(), MB_YESNO | MB_DONT_ASK_AGAIN, IDS_MOVE_TO_FIRSTFILE) == IDYES)
847         {
848                 pMergeDoc->CloseNow();
849                 m_pDirView->OpenFirstFile();
850                 GetMainFrame()->OnUpdateFrameTitle(FALSE);
851         }
852 }
853
854 void CDirDoc::MoveToNextFile(IMergeDoc* pMergeDoc)
855 {
856         if (m_pDirView == nullptr)
857                 return;
858         if (AfxMessageBox(_("Do you want to move to the next file?").c_str(), MB_YESNO | MB_DONT_ASK_AGAIN, IDS_MOVE_TO_NEXTFILE) == IDYES)
859         {
860                 pMergeDoc->CloseNow();
861                 m_pDirView->OpenNextFile();
862                 GetMainFrame()->OnUpdateFrameTitle(FALSE);
863         }
864 }
865
866 void CDirDoc::MoveToPrevFile(IMergeDoc* pMergeDoc)
867 {
868         if (m_pDirView == nullptr)
869                 return;
870         if (AfxMessageBox(_("Do you want to move to the previous file?").c_str(), MB_YESNO | MB_DONT_ASK_AGAIN, IDS_MOVE_TO_PREVFILE) == IDYES)
871         {
872                 pMergeDoc->CloseNow();
873                 m_pDirView->OpenPrevFile();
874                 GetMainFrame()->OnUpdateFrameTitle(FALSE);
875         }
876 }
877
878 void CDirDoc::MoveToLastFile(IMergeDoc* pMergeDoc)
879 {
880         if (m_pDirView == nullptr)
881                 return;
882         if (AfxMessageBox(_("Do you want to move to the last file?").c_str(), MB_YESNO | MB_DONT_ASK_AGAIN, IDS_MOVE_TO_LASTFILE) == IDYES)
883         {
884                 pMergeDoc->CloseNow();
885                 m_pDirView->OpenLastFile();
886                 GetMainFrame()->OnUpdateFrameTitle(FALSE);
887         }
888 }
889
890 bool CDirDoc::IsFirstFile()
891 {
892         if (m_pDirView == nullptr)
893                 return true;
894         return m_pDirView->IsFirstFile();
895 }
896
897 bool CDirDoc::IsLastFile()
898 {
899         if (m_pDirView == nullptr)
900                 return true;
901         return m_pDirView->IsLastFile();
902 }
903
904 bool CDirDoc::CompareFilesIfFilesAreLarge(int nFiles, const FileLocation ifileloc[])
905 {
906         DIFFITEM di;
907         bool bLargeFile = false;
908         for (int i = 0; i < nFiles; ++i)
909         {
910                 di.diffFileInfo[i].SetFile(paths::FindFileName(ifileloc[i].filepath));
911                 if (di.diffFileInfo[i].Update(ifileloc[i].filepath))
912                         di.diffcode.setSideFlag(i);
913         }
914         if (nFiles == 3)
915                 di.diffcode.diffcode |= DIFFCODE::THREEWAY;
916
917         size_t fileSizeThreshold = GetOptionsMgr()->GetInt(OPT_FILE_SIZE_THRESHOLD);
918         for (int i = 0; i < nFiles; ++i)
919         {
920                 if (di.diffFileInfo[i].size != DirItem::FILE_SIZE_NONE && di.diffFileInfo[i].size > fileSizeThreshold)
921                         bLargeFile = true;
922         }
923         if (!bLargeFile)
924                 return false;
925
926         PathContext paths;
927         for (int i = 0; i < nFiles; ++i)
928                 paths.SetPath(i, ifileloc[i].filepath.empty() ? _T("NUL") : paths::GetParentPath(ifileloc[i].filepath));
929         CDiffContext ctxt(paths, CMP_QUICK_CONTENT);
930         DirViewColItems ci(nFiles, std::vector<String>{});
931         String msg = LoadResString(IDS_COMPARE_LARGE_FILES);
932         if (nFiles < 3)
933         {
934                 String sidestr[] = { _("Left:"), _("Right:") };
935                 for (int i = 0; i < nFiles; ++i)
936                 {
937                         if (ifileloc[i].filepath.empty())
938                         {
939                                 msg += strutils::format(_T("%s %s\n\n"), sidestr[i], _("None"));
940                         }
941                         else
942                         {
943                                 msg += strutils::format(_T("%s %s\n %s:  %s\n %s: %s (%s)\n\n"),
944                                         sidestr[i].c_str(), ifileloc[i].filepath.c_str(),
945                                         ci.GetColDisplayName(3 + i).c_str(), ci.ColGetTextToDisplay(&ctxt, 3 + i, di).c_str(),
946                                         ci.GetColDisplayName(8 + i).c_str(), ci.ColGetTextToDisplay(&ctxt, 8 + i, di).c_str(), ci.ColGetTextToDisplay(&ctxt, 10 + i, di).c_str());
947                         }
948                 }
949         }
950         else
951         {
952                 String sidestr[] = { _("Left:"), _("Middle:"), _("Right:") };
953                 for (int i = 0; i < nFiles; ++i)
954                 {
955                         if (ifileloc[i].filepath.empty())
956                         {
957                                 msg += strutils::format(_T("%s %s\n\n"), sidestr[i], _("None"));
958                         }
959                         else
960                         {
961                                 msg += strutils::format(_T("%s %s\n %s:  %s\n %s: %s (%s)\n\n"),
962                                         sidestr[i].c_str(), ifileloc[i].filepath.c_str(),
963                                         ci.GetColDisplayName(3 + i).c_str(), ci.ColGetTextToDisplay(&ctxt, 3 + i, di).c_str(),
964                                         ci.GetColDisplayName(10 + i).c_str(), ci.ColGetTextToDisplay(&ctxt, 10 + i, di).c_str(), ci.ColGetTextToDisplay(&ctxt, 13 + i, di).c_str());
965                         }
966                 }
967         }
968         CMessageBoxDialog dlg(
969                 m_pDirView ? m_pDirView->GetParentFrame() : nullptr,
970                 msg.c_str(), _T(""),
971                 MB_YESNOCANCEL | MB_ICONQUESTION | MB_DONT_ASK_AGAIN, 0U,
972                 _T("CompareLargeFiles"));
973         INT_PTR ans = dlg.DoModal();
974         if (ans == IDCANCEL)
975                 return true;
976         else if (ans == IDNO)
977                 return false;
978
979         InitDiffContext(&ctxt);
980         FolderCmp cmp(&ctxt);
981         CWaitCursor waitstatus;
982         di.diffcode.diffcode |= cmp.prepAndCompareFiles(di);
983         if (di.diffcode.isResultSame())
984         {
985                 ctxt.GetComparePaths(di, paths);
986                 CMergeFrameCommon::ShowIdenticalMessage(paths, true,
987                         [](LPCTSTR msg, UINT flags, UINT id) -> int { return AfxMessageBox(msg, flags, id); });
988         }
989         else
990         {
991                 AfxMessageBox(ci.ColGetTextToDisplay(&ctxt, 2, di).c_str(), MB_OK | MB_ICONINFORMATION);
992         }
993         theApp.SetLastCompareResult(di.diffcode.isResultDiff() ? 1 : 0);
994         return true;
995 }
996
997 void CDirDoc::OnBnClickedComparisonStop()
998 {
999         if (m_pCmpProgressBar != nullptr)
1000                 m_pCmpProgressBar->EndUpdating();
1001         AbortCurrentScan();
1002 }
1003
1004 void CDirDoc::OnBnClickedComparisonPause()
1005 {
1006         if (m_pCmpProgressBar != nullptr)
1007                 m_pCmpProgressBar->SetPaused(true);
1008         PauseCurrentScan();
1009 }
1010
1011 void CDirDoc::OnBnClickedComparisonContinue()
1012 {
1013         if (m_pCmpProgressBar != nullptr)
1014                 m_pCmpProgressBar->SetPaused(false);
1015         ContinueCurrentScan();
1016 }
1017