1 #include "filezilla.h"
2 
3 #define FILELISTCTRL_INCLUDE_TEMPLATE_DEFINITION
4 
5 #include "view.h"
6 #include "LocalListView.h"
7 #include "queue.h"
8 #include "filezillaapp.h"
9 #include "filter_manager.h"
10 #include "file_utils.h"
11 #include "infotext.h"
12 #include "inputdialog.h"
13 #include <algorithm>
14 #include "dndobjects.h"
15 #include "Options.h"
16 #ifdef __WXMSW__
17 #include "lm.h"
18 #include <wx/msw/registry.h>
19 #include "volume_enumerator.h"
20 #endif
21 #include "dragdropmanager.h"
22 #include "drop_target_ex.h"
23 #include "edithandler.h"
24 #include "filelist_statusbar.h"
25 #include "graphics.h"
26 #include "local_recursive_operation.h"
27 #include "sizeformatting.h"
28 #include "timeformatting.h"
29 
30 #include <libfilezilla/local_filesys.hpp>
31 #include <libfilezilla/process.hpp>
32 #include <libfilezilla/recursive_remove.hpp>
33 
34 #include <wx/menu.h>
35 
36 class CLocalListViewDropTarget final : public CFileDropTarget<wxListCtrlEx>
37 {
38 public:
CLocalListViewDropTarget(CLocalListView * pLocalListView)39 	CLocalListViewDropTarget(CLocalListView* pLocalListView)
40 		: CFileDropTarget<wxListCtrlEx>(pLocalListView)
41 		, m_pLocalListView(pLocalListView)
42 	{
43 	}
44 
ClearDropHighlight()45 	void ClearDropHighlight()
46 	{
47 		const int dropTarget = m_pLocalListView->m_dropTarget;
48 		if (dropTarget != -1)
49 		{
50 			m_pLocalListView->m_dropTarget = -1;
51 #ifdef __WXMSW__
52 			m_pLocalListView->SetItemState(dropTarget, 0, wxLIST_STATE_DROPHILITED);
53 #else
54 			m_pLocalListView->RefreshItem(dropTarget);
55 #endif
56 		}
57 	}
58 
OnData(wxCoord x,wxCoord y,wxDragResult def)59 	virtual wxDragResult OnData(wxCoord x, wxCoord y, wxDragResult def)
60 	{
61 		def = CScrollableDropTarget<wxListCtrlEx>::FixupDragResult(def);
62 
63 		if (def == wxDragError ||
64 			def == wxDragNone ||
65 			def == wxDragCancel)
66 		{
67 			return def;
68 		}
69 
70 		if (m_pLocalListView->m_fileData.empty()) {
71 			return wxDragError;
72 		}
73 
74 		if (def != wxDragCopy && def != wxDragMove) {
75 			return wxDragError;
76 		}
77 
78 		CDragDropManager* pDragDropManager = CDragDropManager::Get();
79 		if (pDragDropManager) {
80 			pDragDropManager->pDropTarget = m_pLocalListView;
81 		}
82 
83 		std::wstring subdir;
84 		int flags;
85 		int hit = m_pLocalListView->HitTest(wxPoint(x, y), flags, 0);
86 		if (hit != -1 && (flags & wxLIST_HITTEST_ONITEM)) {
87 			const CLocalFileData* const data = m_pLocalListView->GetData(hit);
88 			if (data && data->dir) {
89 				subdir = data->name;
90 			}
91 		}
92 
93 		CLocalPath dir = m_pLocalListView->m_state.GetLocalDir();
94 		if (!subdir.empty()) {
95 			if (!dir.ChangePath(subdir)) {
96 				return wxDragError;
97 			}
98 		}
99 
100 		if (!dir.IsWriteable()) {
101 			return wxDragError;
102 		}
103 
104 		if (!GetData()) {
105 			return wxDragError;
106 		}
107 
108 		auto const format = m_pDataObject->GetReceivedFormat();
109 		if (format == m_pFileDataObject->GetFormat()) {
110 			m_pLocalListView->m_state.HandleDroppedFiles(m_pFileDataObject, dir, def == wxDragCopy);
111 		}
112 		else if (format == m_pLocalDataObject->GetFormat()) {
113 			m_pLocalListView->m_state.HandleDroppedFiles(m_pLocalDataObject, dir, def == wxDragCopy);
114 		}
115 		else {
116 			if (m_pRemoteDataObject->GetProcessId() != (int)wxGetProcessId()) {
117 				wxMessageBoxEx(_("Drag&drop between different instances of FileZilla has not been implemented yet."));
118 				return wxDragNone;
119 			}
120 
121 			if (!m_pLocalListView->m_state.GetSite() || m_pRemoteDataObject->GetSite().server != m_pLocalListView->m_state.GetSite().server) {
122 				wxMessageBoxEx(_("Drag&drop between different servers has not been implemented yet."));
123 				return wxDragNone;
124 			}
125 
126 			if (!m_pLocalListView->m_state.DownloadDroppedFiles(m_pRemoteDataObject, dir)) {
127 				return wxDragNone;
128 			}
129 		}
130 
131 		return def;
132 	}
133 
OnDrop(wxCoord x,wxCoord y)134 	virtual bool OnDrop(wxCoord x, wxCoord y)
135 	{
136 		CScrollableDropTarget<wxListCtrlEx>::OnDrop(x, y);
137 		ClearDropHighlight();
138 
139 		if (m_pLocalListView->m_fileData.empty()) {
140 			return false;
141 		}
142 
143 		return true;
144 	}
145 
DisplayDropHighlight(wxPoint const & point)146 	virtual int DisplayDropHighlight(wxPoint const& point) override
147 	{
148 		DoDisplayDropHighlight(point);
149 		return -1;
150 	}
151 
DoDisplayDropHighlight(wxPoint point)152 	virtual wxString DoDisplayDropHighlight(wxPoint point)
153 	{
154 		wxString subDir;
155 
156 		int flags;
157 		int hit = m_pLocalListView->HitTest(point, flags, 0);
158 		if (!(flags & wxLIST_HITTEST_ONITEM)) {
159 			hit = -1;
160 		}
161 
162 		if (hit != -1) {
163 			const CLocalFileData* const data = m_pLocalListView->GetData(hit);
164 			if (!data || !data->dir) {
165 				hit = -1;
166 			}
167 			else {
168 				const CDragDropManager* pDragDropManager = CDragDropManager::Get();
169 				if (pDragDropManager && pDragDropManager->pDragSource == m_pLocalListView) {
170 					if (m_pLocalListView->GetItemState(hit, wxLIST_STATE_SELECTED)) {
171 						hit = -1;
172 					}
173 					else {
174 						subDir = data->name;
175 					}
176 				}
177 				else {
178 					subDir = data->name;
179 				}
180 			}
181 		}
182 		if (hit != m_pLocalListView->m_dropTarget) {
183 			ClearDropHighlight();
184 			if (hit != -1) {
185 				m_pLocalListView->m_dropTarget = hit;
186 #ifdef __WXMSW__
187 				m_pLocalListView->SetItemState(hit, wxLIST_STATE_DROPHILITED, wxLIST_STATE_DROPHILITED);
188 #else
189 				m_pLocalListView->RefreshItem(hit);
190 #endif
191 			}
192 		}
193 
194 		return subDir;
195 	}
196 
OnDragOver(wxCoord x,wxCoord y,wxDragResult def)197 	virtual wxDragResult OnDragOver(wxCoord x, wxCoord y, wxDragResult def)
198 	{
199 		def = CScrollableDropTarget<wxListCtrlEx>::OnDragOver(x, y, def);
200 
201 		if (def == wxDragError ||
202 			def == wxDragNone ||
203 			def == wxDragCancel)
204 		{
205 			ClearDropHighlight();
206 			return def;
207 		}
208 
209 		if (m_pLocalListView->m_fileData.empty()) {
210 			ClearDropHighlight();
211 			return wxDragNone;
212 		}
213 
214 		std::wstring const subdir = DoDisplayDropHighlight(wxPoint(x, y)).ToStdWstring();
215 
216 		CLocalPath dir = m_pLocalListView->m_state.GetLocalDir();
217 		if (subdir.empty()) {
218 			const CDragDropManager* pDragDropManager = CDragDropManager::Get();
219 			if (pDragDropManager && pDragDropManager->localParent == m_pLocalListView->m_dir) {
220 				return wxDragNone;
221 			}
222 		}
223 		else {
224 			if (!dir.ChangePath(subdir)) {
225 				return wxDragNone;
226 			}
227 		}
228 
229 		if (!dir.IsWriteable()) {
230 			return wxDragNone;
231 		}
232 
233 		return def;
234 	}
235 
OnLeave()236 	virtual void OnLeave()
237 	{
238 		CScrollableDropTarget<wxListCtrlEx>::OnLeave();
239 		ClearDropHighlight();
240 	}
241 
OnEnter(wxCoord x,wxCoord y,wxDragResult def)242 	virtual wxDragResult OnEnter(wxCoord x, wxCoord y, wxDragResult def)
243 	{
244 		def = CScrollableDropTarget<wxListCtrlEx>::OnEnter(x, y, def);
245 		return OnDragOver(x, y, def);
246 	}
247 
248 protected:
249 	CLocalListView *m_pLocalListView{};
250 };
251 
BEGIN_EVENT_TABLE(CLocalListView,CFileListCtrl<CLocalFileData>)252 BEGIN_EVENT_TABLE(CLocalListView, CFileListCtrl<CLocalFileData>)
253 	EVT_LIST_ITEM_ACTIVATED(wxID_ANY, CLocalListView::OnItemActivated)
254 	EVT_CONTEXT_MENU(CLocalListView::OnContextMenu)
255 	// Map both ID_UPLOAD and ID_ADDTOQUEUE to OnMenuUpload, code is identical
256 	EVT_MENU(XRCID("ID_UPLOAD"), CLocalListView::OnMenuUpload)
257 	EVT_MENU(XRCID("ID_ADDTOQUEUE"), CLocalListView::OnMenuUpload)
258 	EVT_MENU(XRCID("ID_MKDIR"), CLocalListView::OnMenuMkdir)
259 	EVT_MENU(XRCID("ID_MKDIR_CHGDIR"), CLocalListView::OnMenuMkdirChgDir)
260 	EVT_MENU(XRCID("ID_DELETE"), CLocalListView::OnMenuDelete)
261 	EVT_MENU(XRCID("ID_RENAME"), CLocalListView::OnMenuRename)
262 	EVT_KEY_DOWN(CLocalListView::OnKeyDown)
263 	EVT_SIZE(CLocalListView::OnSize)
264 	EVT_LIST_BEGIN_DRAG(wxID_ANY, CLocalListView::OnBeginDrag)
265 	EVT_MENU(XRCID("ID_OPEN"), CLocalListView::OnMenuOpen)
266 	EVT_MENU(XRCID("ID_EDIT"), CLocalListView::OnMenuEdit)
267 	EVT_MENU(XRCID("ID_ENTER"), CLocalListView::OnMenuEnter)
268 #ifdef __WXMSW__
269 	EVT_COMMAND(-1, fzEVT_VOLUMESENUMERATED, CLocalListView::OnVolumesEnumerated)
270 	EVT_COMMAND(-1, fzEVT_VOLUMEENUMERATED, CLocalListView::OnVolumesEnumerated)
271 #endif
272 	EVT_MENU(XRCID("ID_CONTEXT_REFRESH"), CLocalListView::OnMenuRefresh)
273 END_EVENT_TABLE()
274 
275 CLocalListView::CLocalListView(CView* pParent, CState& state, CQueueView *pQueue)
276 	: CFileListCtrl<CLocalFileData>(pParent, pQueue),
277 	CStateEventHandler(state),
278 	m_parentView(pParent)
279 {
280 	wxGetApp().AddStartupProfileRecord("CLocalListView::CLocalListView");
281 	m_state.RegisterHandler(this, STATECHANGE_LOCAL_DIR);
282 	m_state.RegisterHandler(this, STATECHANGE_APPLYFILTER);
283 	m_state.RegisterHandler(this, STATECHANGE_LOCAL_REFRESH_FILE);
284 	m_state.RegisterHandler(this, STATECHANGE_SERVER);
285 
286 	const unsigned long widths[4] = { 170, 80, 120, 120 };
287 
288 	AddColumn(_("Filename"), wxLIST_FORMAT_LEFT, widths[0], true);
289 	AddColumn(_("Filesize"), wxLIST_FORMAT_RIGHT, widths[1]);
290 	AddColumn(_("Filetype"), wxLIST_FORMAT_LEFT, widths[2]);
291 	AddColumn(_("Last modified"), wxLIST_FORMAT_LEFT, widths[3]);
292 	LoadColumnSettings(OPTION_LOCALFILELIST_COLUMN_WIDTHS, OPTION_LOCALFILELIST_COLUMN_SHOWN, OPTION_LOCALFILELIST_COLUMN_ORDER);
293 
294 	SetImageList(GetSystemImageList(), wxIMAGE_LIST_SMALL);
295 
296 	InitHeaderSortImageList();
297 
298 	InitSort(OPTION_LOCALFILELIST_SORTORDER);
299 
300 	SetDropTarget(new CLocalListViewDropTarget(this));
301 
302 	EnablePrefixSearch(true);
303 
304 	m_windowTinter = std::make_unique<CWindowTinter>(*GetMainWindow());
305 
306 	m_pInfoText = new CInfoText(*this);
307 }
308 
~CLocalListView()309 CLocalListView::~CLocalListView()
310 {
311 	wxString str = wxString::Format(_T("%d %d"), m_sortDirection, m_sortColumn);
312 	COptions::Get()->set(OPTION_LOCALFILELIST_SORTORDER, str.ToStdWstring());
313 
314 #ifdef __WXMSW__
315 	volumeEnumeratorThread_.reset();
316 #endif
317 }
318 
DisplayDir(CLocalPath const & dirname)319 bool CLocalListView::DisplayDir(CLocalPath const& dirname)
320 {
321 	CancelLabelEdit();
322 
323 	std::wstring focused;
324 	int focusedItem = -1;
325 	std::vector<std::wstring> selectedNames;
326 	bool ensureVisible = false;
327 	if (m_dir != dirname) {
328 		ResetSearchPrefix();
329 
330 		if (IsComparing()) {
331 			ExitComparisonMode();
332 		}
333 
334 		ClearSelection();
335 		focused = m_state.GetPreviouslyVisitedLocalSubdir();
336 		ensureVisible = !focused.empty();
337 		if (focused.empty()) {
338 			focused = _T("..");
339 		}
340 
341 		if (GetItemCount()) {
342 			EnsureVisible(0);
343 		}
344 		m_dir = dirname;
345 	}
346 	else {
347 		// Remember which items were selected
348 		selectedNames = RememberSelectedItems(focused, focusedItem);
349 	}
350 
351 	if (m_pFilelistStatusBar) {
352 		m_pFilelistStatusBar->UnselectAll();
353 	}
354 
355 	const int oldItemCount = m_indexMapping.size();
356 
357 	m_fileData.clear();
358 	m_indexMapping.clear();
359 
360 	m_hasParent = m_dir.HasLogicalParent();
361 
362 	if (m_hasParent) {
363 		CLocalFileData data;
364 		data.dir = true;
365 		data.name = _T("..");
366 		data.size = -1;
367 		m_fileData.push_back(data);
368 		m_indexMapping.push_back(0);
369 	}
370 
371 #ifdef __WXMSW__
372 	if (m_dir.GetPath() == _T("\\")) {
373 		DisplayDrives();
374 	}
375 	else if (m_dir.GetPath().substr(0, 2) == _T("\\\\")) {
376 		auto pos = m_dir.GetPath().find('\\', 2);
377 		if (pos != std::wstring::npos && pos + 1 < m_dir.GetPath().size()) {
378 			goto regular_dir;
379 		}
380 
381 		// UNC path without shares
382 		DisplayShares(m_dir.GetPath());
383 	}
384 	else
385 #endif
386 	{
387 #ifdef __WXMSW__
388 regular_dir:
389 #endif
390 		CStateFilterManager const& filter = m_state.GetStateFilterManager();
391 		fz::local_filesys local_filesys;
392 
393 		auto result = local_filesys.begin_find_files(fz::to_native(m_dir.GetPath()), false);
394 		if (!result) {
395 
396 			if (result.error_ == fz::result::noperm) {
397 				SetInfoText(_("You do not have permission to list this directory"));
398 			}
399 			else {
400 				SetInfoText(_("Could not list directory contents"));
401 			}
402 
403 			SetItemCount(1);
404 			if (m_pFilelistStatusBar) {
405 				m_pFilelistStatusBar->SetDirectoryContents(0, 0, 0, 0, 0);
406 			}
407 
408 			return false;
409 		}
410 
411 		SetInfoText(wxString());
412 
413 		int64_t totalSize{};
414 		int unknown_sizes = 0;
415 		int totalFileCount = 0;
416 		int totalDirCount = 0;
417 		int hidden = 0;
418 
419 		int num = m_fileData.size();
420 		CLocalFileData data;
421 		bool wasLink{};
422 		fz::local_filesys::type t{};
423 		fz::native_string name;
424 		while (local_filesys.get_next_file(name, wasLink, t, &data.size, &data.time, &data.attributes)) {
425 			data.name = fz::to_wstring(name);
426 			data.dir = t == fz::local_filesys::dir;
427 			if (name.empty() || data.name.empty()) {
428 				wxGetApp().DisplayEncodingWarning();
429 				continue;
430 			}
431 
432 			m_fileData.push_back(data);
433 			if (!filter.FilenameFiltered(data.name, m_dir.GetPath(), data.dir, data.size, true, data.attributes, data.time)) {
434 				if (data.dir) {
435 					++totalDirCount;
436 				}
437 				else {
438 					if (data.size != -1) {
439 						totalSize += data.size;
440 					}
441 					else {
442 						++unknown_sizes;
443 					}
444 					++totalFileCount;
445 				}
446 				m_indexMapping.push_back(num);
447 			}
448 			else {
449 				++hidden;
450 			}
451 			++num;
452 		}
453 
454 		if (m_pFilelistStatusBar) {
455 			m_pFilelistStatusBar->SetDirectoryContents(totalFileCount, totalDirCount, totalSize, unknown_sizes, hidden);
456 		}
457 	}
458 
459 	if (m_dropTarget != -1) {
460 		CLocalFileData* data = GetData(m_dropTarget);
461 		if (!data || !data->dir) {
462 			SetItemState(m_dropTarget, 0, wxLIST_STATE_DROPHILITED);
463 			m_dropTarget = -1;
464 		}
465 	}
466 
467 	const int count = m_indexMapping.size();
468 	if (oldItemCount != count) {
469 		SetItemCount(count);
470 	}
471 
472 	SortList(-1, -1, false);
473 
474 	if (IsComparing()) {
475 		m_originalIndexMapping.clear();
476 		RefreshComparison();
477 	}
478 
479 	ReselectItems(selectedNames, focused, focusedItem, ensureVisible);
480 
481 	RefreshListOnly();
482 
483 	return true;
484 }
485 
486 // See comment to OnGetItemText
OnGetItemImage(long item) const487 int CLocalListView::OnGetItemImage(long item) const
488 {
489 	CLocalListView *pThis = const_cast<CLocalListView *>(this);
490 	CLocalFileData *data = pThis->GetData(item);
491 	if (!data) {
492 		return -1;
493 	}
494 	int &icon = data->icon;
495 
496 	if (icon == -2) {
497 		std::wstring path;
498 		if (data->name != L"..") {
499 #ifdef __WXMSW__
500 			if (m_dir.GetPath() == L"\\") {
501 				path = data->name + L"\\";
502 			}
503 			else
504 #endif
505 			{
506 				path = m_dir.GetPath() + data->name;
507 			}
508 		}
509 
510 		icon = pThis->GetIconIndex(data->dir ? iconType::dir : iconType::file, path);
511 	}
512 	return icon;
513 }
514 
OnItemActivated(wxListEvent & event)515 void CLocalListView::OnItemActivated(wxListEvent &event)
516 {
517 	int count = 0;
518 	bool back = false;
519 
520 	int item = -1;
521 	for (;;) {
522 		item = GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
523 		if (item == -1) {
524 			break;
525 		}
526 
527 		count++;
528 
529 		if (!item && m_hasParent) {
530 			back = true;
531 		}
532 	}
533 	if (count > 1) {
534 		if (back) {
535 			wxBell();
536 			return;
537 		}
538 
539 		wxCommandEvent cmdEvent;
540 		OnMenuUpload(cmdEvent);
541 		return;
542 	}
543 
544 	item = event.GetIndex();
545 
546 	CLocalFileData *data = GetData(item);
547 	if (!data) {
548 		return;
549 	}
550 
551 	if (data->dir) {
552 		const int action = COptions::Get()->get_int(OPTION_DOUBLECLICK_ACTION_DIRECTORY);
553 		if (action == 3) {
554 			// No action
555 			wxBell();
556 			return;
557 		}
558 
559 		if (!action || data->name == _T("..")) {
560 			// Enter action
561 
562 			std::wstring error;
563 			if (!m_state.SetLocalDir(data->name, &error)) {
564 				if (!error.empty()) {
565 					wxMessageBoxEx(error, _("Failed to change directory"), wxICON_INFORMATION);
566 				}
567 				else {
568 					wxBell();
569 				}
570 			}
571 			return;
572 		}
573 
574 		wxCommandEvent evt(0, action == 1 ? XRCID("ID_UPLOAD") : XRCID("ID_ADDTOQUEUE"));
575 		OnMenuUpload(evt);
576 		return;
577 	}
578 
579 	if (data->comparison_flags == fill) {
580 		wxBell();
581 		return;
582 	}
583 
584 	const int action = COptions::Get()->get_int(OPTION_DOUBLECLICK_ACTION_FILE);
585 	if (action == 3) {
586 		// No action
587 		wxBell();
588 		return;
589 	}
590 
591 	if (action == 2) {
592 		// View / Edit action
593 		wxCommandEvent evt;
594 		OnMenuEdit(evt);
595 		return;
596 	}
597 
598 	Site const& site = m_state.GetSite();
599 	if (!site) {
600 		wxBell();
601 		return;
602 	}
603 
604 	CServerPath path = m_state.GetRemotePath();
605 	if (path.empty()) {
606 		wxBell();
607 		return;
608 	}
609 
610 	const bool queue_only = action == 1;
611 
612 	m_pQueue->QueueFile(queue_only, false, data->name, wxEmptyString, m_dir, path, site, data->size);
613 	m_pQueue->QueueFile_Finish(true);
614 }
615 
OnMenuEnter(wxCommandEvent &)616 void CLocalListView::OnMenuEnter(wxCommandEvent &)
617 {
618 	int item = GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
619 	if (item == -1) {
620 		wxBell();
621 		return;
622 	}
623 
624 	if (GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED) != -1) {
625 		wxBell();
626 		return;
627 	}
628 
629 	CLocalFileData *data = GetData(item);
630 	if (!data || !data->dir) {
631 		wxBell();
632 		return;
633 	}
634 
635 	std::wstring error;
636 	if (!m_state.SetLocalDir(data->name, &error)) {
637 		if (!error.empty()) {
638 			wxMessageBoxEx(error, _("Failed to change directory"), wxICON_INFORMATION);
639 		}
640 		else {
641 			wxBell();
642 		}
643 	}
644 }
645 
646 #ifdef __WXMSW__
DisplayDrives()647 void CLocalListView::DisplayDrives()
648 {
649 	SetInfoText(wxString());
650 	int count = m_fileData.size();
651 
652 	std::vector<std::wstring> drives = CVolumeDescriptionEnumeratorThread::GetDrives();
653 	for (auto it = drives.begin(); it != drives.end(); ++it) {
654 		std::wstring drive = *it;
655 		if (!drive.empty() && drive.back() == '\\') {
656 			drive.pop_back();
657 		}
658 
659 		CLocalFileData data;
660 		data.name = drive;
661 		data.label = fz::sparse_optional<std::wstring>(data.name);
662 		data.dir = true;
663 		data.size = -1;
664 		data.icon = GetIconIndex(iconType::dir, std::wstring(), false);
665 
666 		m_fileData.push_back(data);
667 		m_indexMapping.push_back(count);
668 		++count;
669 	}
670 
671 	if (m_pFilelistStatusBar) {
672 		m_pFilelistStatusBar->SetDirectoryContents(0, drives.size(), 0, false, 0);
673 	}
674 
675 	if (!volumeEnumeratorThread_) {
676 		volumeEnumeratorThread_ = std::make_unique<CVolumeDescriptionEnumeratorThread>(this, m_state.pool_);
677 		if (volumeEnumeratorThread_->Failed()) {
678 			volumeEnumeratorThread_.reset();
679 		}
680 	}
681 }
682 
DisplayShares(wxString computer)683 void CLocalListView::DisplayShares(wxString computer)
684 {
685 	SetInfoText(wxString());
686 
687 	// Cast through a union to avoid warning about breaking strict aliasing rule
688 	union
689 	{
690 		SHARE_INFO_1* pShareInfo;
691 		LPBYTE pShareInfoBlob;
692 	} si;
693 
694 	DWORD read, total;
695 	DWORD resume_handle = 0;
696 
697 	if (!computer.empty() && computer.Last() == '\\') {
698 		computer.RemoveLast();
699 	}
700 
701 	int j = m_fileData.size();
702 	int share_count = 0;
703 	int res = 0;
704 	do {
705 		const wxWX2WCbuf buf = computer.wc_str(wxConvLocal);
706 		res = NetShareEnum((wchar_t*)(const wchar_t*)buf, 1, &si.pShareInfoBlob, MAX_PREFERRED_LENGTH, &read, &total, &resume_handle);
707 
708 		if (res != ERROR_SUCCESS && res != ERROR_MORE_DATA) {
709 			break;
710 		}
711 
712 		SHARE_INFO_1* p = si.pShareInfo;
713 		for (unsigned int i = 0; i < read; ++i, ++p) {
714 			if (p->shi1_type != STYPE_DISKTREE) {
715 				continue;
716 			}
717 
718 			CLocalFileData data;
719 			data.name = p->shi1_netname;
720 #ifdef __WXMSW__
721 			data.label = fz::sparse_optional<std::wstring>(data.name);
722 #endif
723 			data.dir = true;
724 			data.size = -1;
725 
726 			m_fileData.push_back(data);
727 			m_indexMapping.push_back(j++);
728 
729 			share_count++;
730 		}
731 
732 		NetApiBufferFree(si.pShareInfo);
733 	}
734 	while (res == ERROR_MORE_DATA);
735 
736 	if (m_pFilelistStatusBar) {
737 		m_pFilelistStatusBar->SetDirectoryContents(0, share_count, 0, false, 0);
738 	}
739 }
740 
741 #endif //__WXMSW__
742 
GetData(unsigned int item)743 CLocalFileData* CLocalListView::GetData(unsigned int item)
744 {
745 	if (!IsItemValid(item)) {
746 		return 0;
747 	}
748 
749 	return &m_fileData[m_indexMapping[item]];
750 }
751 
IsItemValid(unsigned int item) const752 bool CLocalListView::IsItemValid(unsigned int item) const
753 {
754 	if (item >= m_indexMapping.size()) {
755 		return false;
756 	}
757 
758 	unsigned int index = m_indexMapping[item];
759 	if (index >= m_fileData.size()) {
760 		return false;
761 	}
762 
763 	return true;
764 }
765 
GetSortComparisonObject()766 std::unique_ptr<CFileListCtrlSortBase> CLocalListView::GetSortComparisonObject()
767 {
768 	CFileListCtrlSortBase::DirSortMode dirSortMode = GetDirSortMode();
769 	NameSortMode nameSortMode = GetNameSortMode();
770 
771 	if (!m_sortDirection) {
772 		if (m_sortColumn == 1) {
773 			return std::make_unique<CFileListCtrlSortSize<std::vector<CLocalFileData>, CLocalFileData>>(m_fileData, m_fileData, dirSortMode, nameSortMode, this);
774 		}
775 		else if (m_sortColumn == 2) {
776 			return std::make_unique<CFileListCtrlSortType<std::vector<CLocalFileData>, CLocalFileData>>(m_fileData, m_fileData, dirSortMode, nameSortMode, this);
777 		}
778 		else if (m_sortColumn == 3) {
779 			return std::make_unique<CFileListCtrlSortTime<std::vector<CLocalFileData>, CLocalFileData>>(m_fileData, m_fileData, dirSortMode, nameSortMode, this);
780 		}
781 		else {
782 			return std::make_unique<CFileListCtrlSortName<std::vector<CLocalFileData>, CLocalFileData>>(m_fileData, m_fileData, dirSortMode, nameSortMode, this);
783 		}
784 	}
785 	else {
786 		if (m_sortColumn == 1) {
787 			return std::make_unique<CReverseSort<CFileListCtrlSortSize<std::vector<CLocalFileData>, CLocalFileData>, CLocalFileData>>(m_fileData, m_fileData, dirSortMode, nameSortMode, this);
788 		}
789 		else if (m_sortColumn == 2) {
790 			return std::make_unique<CReverseSort<CFileListCtrlSortType<std::vector<CLocalFileData>, CLocalFileData>, CLocalFileData>>(m_fileData, m_fileData, dirSortMode, nameSortMode, this);
791 		}
792 		else if (m_sortColumn == 3) {
793 			return std::make_unique<CReverseSort<CFileListCtrlSortTime<std::vector<CLocalFileData>, CLocalFileData>, CLocalFileData>>(m_fileData, m_fileData, dirSortMode, nameSortMode, this);
794 		}
795 		else {
796 			return std::make_unique<CReverseSort<CFileListCtrlSortName<std::vector<CLocalFileData>, CLocalFileData>, CLocalFileData>>(m_fileData, m_fileData, dirSortMode, nameSortMode, this);
797 		}
798 	}
799 }
800 
OnContextMenu(wxContextMenuEvent & event)801 void CLocalListView::OnContextMenu(wxContextMenuEvent& event)
802 {
803 	if (GetEditControl()) {
804 		event.Skip();
805 		return;
806 	}
807 
808 	wxMenu menu;
809 
810 	auto item = new wxMenuItem(&menu, XRCID("ID_UPLOAD"), _("&Upload"), _("Upload selected files and directories"));
811 	item->SetBitmap(wxArtProvider::GetBitmap(_T("ART_UPLOAD"), wxART_MENU));
812 	menu.Append(item);
813 	item = new wxMenuItem(&menu, XRCID("ID_ADDTOQUEUE"), _("&Add files to queue"), _("Add selected files and folders to the transfer queue"));
814 	item->SetBitmap(wxArtProvider::GetBitmap(_T("ART_UPLOADADD"), wxART_MENU));
815 	menu.Append(item);
816 	menu.Append(XRCID("ID_ENTER"), _("E&nter directory"), _("Enter selected directory"));
817 
818 	menu.AppendSeparator();
819 	menu.Append(XRCID("ID_OPEN"), _("&Open"), _("Open the file."));
820 	menu.Append(XRCID("ID_EDIT"), _("&Edit"), _("Edit the file with the configured editor and upload changes to the server."));
821 
822 	menu.AppendSeparator();
823 	menu.Append(XRCID("ID_MKDIR"), _("&Create directory"), _("Create a new subdirectory in the current directory"));
824 	menu.Append(XRCID("ID_MKDIR_CHGDIR"), _("Create director&y and enter it"), _("Create a new subdirectory in the current directory and change into it"));
825 	menu.Append(XRCID("ID_CONTEXT_REFRESH"), _("Re&fresh"));
826 
827 	menu.AppendSeparator();
828 	menu.Append(XRCID("ID_DELETE"),_("&Delete"), _("Delete selected files and directories"));
829 	menu.Append(XRCID("ID_RENAME"), _("&Rename"), _("Rename selected files and directories"));
830 
831 	const bool connected = m_state.IsRemoteConnected();
832 	if (!connected) {
833 		menu.Enable(XRCID("ID_EDIT"), COptions::Get()->get_int(OPTION_EDIT_TRACK_LOCAL) == 0);
834 		menu.Enable(XRCID("ID_UPLOAD"), false);
835 		menu.Enable(XRCID("ID_ADDTOQUEUE"), false);
836 	}
837 
838 	int index = GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
839 	int count = 0;
840 	int fillCount = 0;
841 	bool selectedDir = false;
842 	while (index != -1) {
843 		++count;
844 		const CLocalFileData* const data = GetData(index);
845 		if (!data || (!index && m_hasParent)) {
846 			menu.Enable(XRCID("ID_OPEN"), false);
847 			menu.Enable(XRCID("ID_RENAME"), false);
848 			menu.Enable(XRCID("ID_EDIT"), false);
849 		}
850 		if ((data && data->comparison_flags == fill) || (!index && m_hasParent)) {
851 			fillCount++;
852 		}
853 		if (data && data->dir) {
854 			selectedDir = true;
855 		}
856 		index = GetNextItem(index, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
857 	}
858 	if (!count || fillCount == count) {
859 		menu.Delete(XRCID("ID_ENTER"));
860 		menu.Enable(XRCID("ID_UPLOAD"), false);
861 		menu.Enable(XRCID("ID_ADDTOQUEUE"), false);
862 		menu.Enable(XRCID("ID_DELETE"), false);
863 		menu.Enable(XRCID("ID_RENAME"), false);
864 		menu.Enable(XRCID("ID_EDIT"), false);
865 	}
866 	else if (count > 1) {
867 		menu.Delete(XRCID("ID_ENTER"));
868 		menu.Enable(XRCID("ID_RENAME"), false);
869 	}
870 	else {
871 		// Exactly one item selected
872 		if (!selectedDir) {
873 			menu.Delete(XRCID("ID_ENTER"));
874 		}
875 	}
876 	if (selectedDir) {
877 		menu.Enable(XRCID("ID_EDIT"), false);
878 		if (m_state.GetLocalRecursiveOperation() && m_state.GetLocalRecursiveOperation()->IsActive()) {
879 			menu.Enable(XRCID("ID_UPLOAD"), false);
880 			menu.Enable(XRCID("ID_ADDTOQUEUE"), false);
881 		}
882 	}
883 
884 	PopupMenu(&menu);
885 }
886 
OnMenuUpload(wxCommandEvent & event)887 void CLocalListView::OnMenuUpload(wxCommandEvent& event)
888 {
889 	Site const& site = m_state.GetSite();
890 	if (!site) {
891 		wxBell();
892 		return;
893 	}
894 
895 	bool added = false;
896 
897 	bool queue_only = event.GetId() == XRCID("ID_ADDTOQUEUE");
898 
899 	auto recursiveOperation = m_state.GetLocalRecursiveOperation();
900 	if (!recursiveOperation || recursiveOperation->IsActive()) {
901 		wxBell();
902 		return;
903 	}
904 
905 	local_recursion_root root;
906 
907 	long item = -1;
908 	for (;;) {
909 		item = GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
910 		if (!item && m_hasParent) {
911 			continue;
912 		}
913 		if (item == -1) {
914 			break;
915 		}
916 
917 		const CLocalFileData *data = GetData(item);
918 		if (!data) {
919 			break;
920 		}
921 
922 		if (data->comparison_flags == fill) {
923 			continue;
924 		}
925 
926 		CServerPath remotePath = m_state.GetRemotePath();
927 		if (remotePath.empty()) {
928 			wxBell();
929 			break;
930 		}
931 
932 		if (data->dir) {
933 			CLocalPath localPath = m_dir;
934 			if (!localPath.ChangePath(data->name)) {
935 				continue;
936 			}
937 			if (!remotePath.ChangePath(data->name)) {
938 				continue;
939 			}
940 
941 			root.add_dir_to_visit(localPath, remotePath);
942 		}
943 		else {
944 			m_pQueue->QueueFile(queue_only, false, data->name, wxEmptyString, m_dir, remotePath, site, data->size);
945 			added = true;
946 		}
947 	}
948 
949 	if (added) {
950 		m_pQueue->QueueFile_Finish(!queue_only);
951 	}
952 
953 	if (!root.empty()) {
954 		recursiveOperation->AddRecursionRoot(std::move(root));
955 		CFilterManager filter;
956 		recursiveOperation->StartRecursiveOperation(recursive_operation::recursive_transfer, filter.GetActiveFilters(), !queue_only);
957 	}
958 }
959 
960 // Create a new Directory
OnMenuMkdir(wxCommandEvent &)961 void CLocalListView::OnMenuMkdir(wxCommandEvent&)
962 {
963 	wxString newdir = MenuMkdir();
964 	if (!newdir.empty()) {
965 		m_state.RefreshLocal();
966 	}
967 }
968 
969 // Create a new Directory and enter the new Directory
OnMenuMkdirChgDir(wxCommandEvent &)970 void CLocalListView::OnMenuMkdirChgDir(wxCommandEvent&)
971 {
972 	std::wstring newdir = MenuMkdir().ToStdWstring();
973 	if (newdir.empty()) {
974 		return;
975 	}
976 
977 	// OnMenuEnter
978 	std::wstring error;
979 	if (!m_state.SetLocalDir(newdir, &error)) {
980 		if (!error.empty()) {
981 			wxMessageBoxEx(error, _("Failed to change directory"), wxICON_INFORMATION);
982 		}
983 		else {
984 			wxBell();
985 		}
986 	}
987 }
988 
989 // Helper-Function to create a new Directory
990 // Returns the name of the new directory
MenuMkdir()991 wxString CLocalListView::MenuMkdir()
992 {
993 	CInputDialog dlg;
994 	if (!dlg.Create(this, _("Create directory"), _("Please enter the name of the directory which should be created:"))) {
995 		return wxString();
996 	}
997 
998 	if (dlg.ShowModal() != wxID_OK) {
999 		return wxString();
1000 	}
1001 
1002 	if (dlg.GetValue().empty()) {
1003 		wxBell();
1004 		return wxString();
1005 	}
1006 
1007 	wxFileName fn(dlg.GetValue(), wxString());
1008 	fn.Normalize(wxPATH_NORM_ALL, m_dir.GetPath());
1009 
1010 	bool res;
1011 
1012 	{
1013 		wxLogNull log;
1014 		res = fn.Mkdir(fn.GetPath(), 0777, wxPATH_MKDIR_FULL);
1015 	}
1016 
1017 	if (!res) {
1018 		wxBell();
1019 		return wxString();
1020 	}
1021 
1022 	// Return name of the New Directory
1023 	return fn.GetPath();
1024 }
1025 
OnMenuDelete(wxCommandEvent &)1026 void CLocalListView::OnMenuDelete(wxCommandEvent&)
1027 {
1028 	std::list<fz::native_string> pathsToDelete;
1029 	long item = -1;
1030 	while ((item = GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED)) != -1) {
1031 		if (!item && m_hasParent) {
1032 			continue;
1033 		}
1034 
1035 		CLocalFileData *data = GetData(item);
1036 		if (!data) {
1037 			continue;
1038 		}
1039 
1040 		if (data->comparison_flags == fill) {
1041 			continue;
1042 		}
1043 
1044 		pathsToDelete.push_back(fz::to_native(m_dir.GetPath() + data->name));
1045 	}
1046 	gui_recursive_remove rmd(this);
1047 	rmd.remove(pathsToDelete);
1048 
1049 	m_state.SetLocalDir(m_dir);
1050 }
1051 
OnMenuRename(wxCommandEvent &)1052 void CLocalListView::OnMenuRename(wxCommandEvent&)
1053 {
1054 	if (GetEditControl()) {
1055 		GetEditControl()->SetFocus();
1056 		return;
1057 	}
1058 
1059 	int item = GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
1060 	if (item < 0 || (!item && m_hasParent)) {
1061 		wxBell();
1062 		return;
1063 	}
1064 
1065 	if (GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED) != -1) {
1066 		wxBell();
1067 		return;
1068 	}
1069 
1070 	CLocalFileData *data = GetData(item);
1071 	if (!data || data->comparison_flags == fill) {
1072 		wxBell();
1073 		return;
1074 	}
1075 
1076 	EditLabel(item);
1077 }
1078 
OnKeyDown(wxKeyEvent & event)1079 void CLocalListView::OnKeyDown(wxKeyEvent& event)
1080 {
1081 #ifdef __WXMAC__
1082 #define CursorModifierKey wxMOD_CMD
1083 #else
1084 #define CursorModifierKey wxMOD_ALT
1085 #endif
1086 
1087 	const int code = event.GetKeyCode();
1088 	if (code == WXK_DELETE || code == WXK_NUMPAD_DELETE) {
1089 		if (GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED) == -1) {
1090 			wxBell();
1091 			return;
1092 		}
1093 
1094 		wxCommandEvent tmp;
1095 		OnMenuDelete(tmp);
1096 	}
1097 	else if (code == WXK_F2) {
1098 		wxCommandEvent tmp;
1099 		OnMenuRename(tmp);
1100 	}
1101 	else if (code == WXK_RIGHT && event.GetModifiers() == CursorModifierKey) {
1102 		wxListEvent evt;
1103 		evt.m_itemIndex = GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_FOCUSED);
1104 		OnItemActivated(evt);
1105 	}
1106 	else if (code == WXK_DOWN && event.GetModifiers() == CursorModifierKey) {
1107 		wxCommandEvent cmdEvent;
1108 		OnMenuUpload(cmdEvent);
1109 	}
1110 	else if (code == 'N' && event.GetModifiers() == (wxMOD_CONTROL | wxMOD_SHIFT)) {
1111 		wxCommandEvent cmdEvent;
1112 		OnMenuMkdir(cmdEvent);
1113 	}
1114 	else if (code == 'F' && event.GetModifiers() == wxMOD_CONTROL) {
1115 		if (m_parentView) {
1116 			m_parentView->ShowSearchPanel();
1117 		}
1118 	}
1119 	else {
1120 		event.Skip();
1121 	}
1122 }
1123 
OnBeginRename(const wxListEvent & event)1124 bool CLocalListView::OnBeginRename(const wxListEvent& event)
1125 {
1126 	if (!m_state.GetLocalDir().IsWriteable()) {
1127 		return false;
1128 	}
1129 
1130 	if (event.GetIndex() == 0 && m_hasParent) {
1131 		return false;
1132 	}
1133 
1134 	const CLocalFileData * const data = GetData(event.GetIndex());
1135 	if (!data || data->comparison_flags == fill) {
1136 		return false;
1137 	}
1138 
1139 	return true;
1140 }
1141 
OnAcceptRename(const wxListEvent & event)1142 bool CLocalListView::OnAcceptRename(const wxListEvent& event)
1143 {
1144 	const int index = event.GetIndex();
1145 	if (!index && m_hasParent) {
1146 		return false;
1147 	}
1148 
1149 	if (event.GetLabel().empty()) {
1150 		return false;
1151 	}
1152 
1153 	if (!m_state.GetLocalDir().IsWriteable()) {
1154 		return false;
1155 	}
1156 
1157 	CLocalFileData *const data = GetData(event.GetIndex());
1158 	if (!data || data->comparison_flags == fill) {
1159 		return false;
1160 	}
1161 
1162 	std::wstring newname = event.GetLabel().ToStdWstring();
1163 #ifdef __WXMSW__
1164 	newname = newname.substr(0, 255);
1165 #endif
1166 
1167 	if (newname == data->name) {
1168 		return false;
1169 	}
1170 
1171 	if (!RenameFile(this, m_dir.GetPath(), data->name, newname)) {
1172 		return false;
1173 	}
1174 
1175 	data->name = newname;
1176 #ifdef __WXMSW__
1177 	data->label.clear();
1178 #endif
1179 	CallAfter([&](){m_state.RefreshLocal();});
1180 
1181 	return true;
1182 }
1183 
ApplyCurrentFilter()1184 void CLocalListView::ApplyCurrentFilter()
1185 {
1186 	CStateFilterManager const& filter = m_state.GetStateFilterManager();
1187 
1188 	if (!filter.HasSameLocalAndRemoteFilters() && IsComparing()) {
1189 		ExitComparisonMode();
1190 	}
1191 
1192 	unsigned int min = m_hasParent ? 1 : 0;
1193 	if (m_fileData.size() <= min) {
1194 		return;
1195 	}
1196 
1197 	int focusedItem = -1;
1198 	std::wstring focused;
1199 	std::vector<std::wstring> const& selectedNames = RememberSelectedItems(focused, focusedItem);
1200 
1201 	if (m_pFilelistStatusBar) {
1202 		m_pFilelistStatusBar->UnselectAll();
1203 	}
1204 
1205 	int64_t totalSize{};
1206 	int unknown_sizes = 0;
1207 	int totalFileCount = 0;
1208 	int totalDirCount = 0;
1209 	int hidden = 0;
1210 
1211 	m_indexMapping.clear();
1212 	if (m_hasParent) {
1213 		m_indexMapping.push_back(0);
1214 	}
1215 	for (unsigned int i = min; i < m_fileData.size(); ++i) {
1216 		const CLocalFileData& data = m_fileData[i];
1217 		if (data.comparison_flags == fill) {
1218 			continue;
1219 		}
1220 		if (filter.FilenameFiltered(data.name, m_dir.GetPath(), data.dir, data.size, true, data.attributes, data.time)) {
1221 			++hidden;
1222 			continue;
1223 		}
1224 
1225 		if (data.dir) {
1226 			++totalDirCount;
1227 		}
1228 		else {
1229 			if (data.size != -1) {
1230 				totalSize += data.size;
1231 			}
1232 			else {
1233 				++unknown_sizes;
1234 			}
1235 			++totalFileCount;
1236 		}
1237 
1238 		m_indexMapping.push_back(i);
1239 	}
1240 	SetItemCount(m_indexMapping.size());
1241 
1242 	if (m_pFilelistStatusBar) {
1243 		m_pFilelistStatusBar->SetDirectoryContents(totalFileCount, totalDirCount, totalSize, unknown_sizes, hidden);
1244 	}
1245 
1246 	SortList(-1, -1, false);
1247 
1248 	if (IsComparing()) {
1249 		m_originalIndexMapping.clear();
1250 		RefreshComparison();
1251 	}
1252 
1253 	ReselectItems(selectedNames, focused, focusedItem);
1254 
1255 	if (!IsComparing()) {
1256 		RefreshListOnly();
1257 	}
1258 }
1259 
RememberSelectedItems(std::wstring & focused,int & focusedItem)1260 std::vector<std::wstring> CLocalListView::RememberSelectedItems(std::wstring& focused, int & focusedItem)
1261 {
1262 	std::vector<std::wstring> selectedNames;
1263 	// Remember which items were selected
1264 #ifndef __WXMSW__
1265 	// GetNextItem is O(n) if nothing is selected, GetSelectedItemCount() is O(1)
1266 	if (GetSelectedItemCount())
1267 #endif
1268 	{
1269 		int item = -1;
1270 		for (;;) {
1271 			item = GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
1272 			if (item < 0 || static_cast<size_t>(item) >= m_indexMapping.size()) {
1273 				break;
1274 			}
1275 			const CLocalFileData &data = m_fileData[m_indexMapping[item]];
1276 			if (data.comparison_flags != fill) {
1277 				if (data.dir) {
1278 					selectedNames.push_back(L"d" + data.name);
1279 				}
1280 				else {
1281 					selectedNames.push_back(L"-" + data.name);
1282 				}
1283 			}
1284 			SetSelection(item, false);
1285 		}
1286 	}
1287 
1288 	focusedItem = -1;
1289 	int item = GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_FOCUSED);
1290 	if (item >= 0 && static_cast<size_t>(item) < m_indexMapping.size()) {
1291 		const CLocalFileData &data = m_fileData[m_indexMapping[item]];
1292 		if (data.comparison_flags != fill) {
1293 			focused = data.name;
1294 		}
1295 		focusedItem = item;
1296 	}
1297 
1298 	return selectedNames;
1299 }
1300 
ReselectItems(const std::vector<std::wstring> & selectedNames,std::wstring focused,int focusedItem,bool ensureVisible)1301 void CLocalListView::ReselectItems(const std::vector<std::wstring>& selectedNames, std::wstring focused, int focusedItem, bool ensureVisible)
1302 {
1303 	if (!GetItemCount()) {
1304 		return;
1305 	}
1306 
1307 	// Reselect previous items if neccessary.
1308 	// Sorting direction did not change. We just have to scan through items once
1309 
1310 	if (selectedNames.empty()) {
1311 		if (focused.empty()) {
1312 			return;
1313 		}
1314 		for (unsigned int i = 0; i < m_indexMapping.size(); ++i) {
1315 			const CLocalFileData &data = m_fileData[m_indexMapping[i]];
1316 			if (data.name == focused) {
1317 				SetItemState(i, wxLIST_STATE_FOCUSED, wxLIST_STATE_FOCUSED);
1318 				if (ensureVisible) {
1319 					EnsureVisible(i);
1320 				}
1321 				return;
1322 			}
1323 		}
1324 
1325 		if (focusedItem != -1 && GetItemCount() != 0) {
1326 			if (focusedItem >= GetItemCount()) {
1327 				focusedItem = GetItemCount() - 1;
1328 			}
1329 			SetItemState(focusedItem, wxLIST_STATE_FOCUSED, wxLIST_STATE_FOCUSED);
1330 		}
1331 		return;
1332 	}
1333 
1334 	int firstSelected = -1;
1335 
1336 	int i = -1;
1337 	for (auto const& selectedName : selectedNames) {
1338 		while (++i < (int)m_indexMapping.size()) {
1339 			const CLocalFileData &data = m_fileData[m_indexMapping[i]];
1340 			if (data.name == focused) {
1341 				SetItemState(i, wxLIST_STATE_FOCUSED, wxLIST_STATE_FOCUSED);
1342 				if (ensureVisible) {
1343 					EnsureVisible(i);
1344 				}
1345 				focused.clear();
1346 				focusedItem = -1;
1347 			}
1348 			if (data.dir && selectedName == (_T("d") + data.name)) {
1349 				if (firstSelected == -1) {
1350 					firstSelected = i;
1351 				}
1352 				if (m_pFilelistStatusBar) {
1353 					m_pFilelistStatusBar->SelectDirectory();
1354 				}
1355 				SetSelection(i, true);
1356 				break;
1357 			}
1358 			else if (selectedName == (_T("-") + data.name)) {
1359 				if (firstSelected == -1) {
1360 					firstSelected = i;
1361 				}
1362 				if (m_pFilelistStatusBar) {
1363 					m_pFilelistStatusBar->SelectFile(data.size);
1364 				}
1365 				SetSelection(i, true);
1366 				break;
1367 			}
1368 		}
1369 		if (i == (int)m_indexMapping.size()) {
1370 			break;
1371 		}
1372 	}
1373 	if (!focused.empty()) {
1374 		if (firstSelected != -1) {
1375 			SetItemState(firstSelected, wxLIST_STATE_FOCUSED, wxLIST_STATE_FOCUSED);
1376 		}
1377 		else {
1378 			if (GetItemCount() != 0) {
1379 				if (focusedItem == -1) {
1380 					focusedItem = 0;
1381 				}
1382 				else if (focusedItem >= GetItemCount()) {
1383 					focusedItem = GetItemCount() - 1;
1384 				}
1385 				SetItemState(focusedItem, wxLIST_STATE_FOCUSED, wxLIST_STATE_FOCUSED);
1386 			}
1387 		}
1388 	}
1389 }
1390 
OnSize(wxSizeEvent & event)1391 void CLocalListView::OnSize(wxSizeEvent& event)
1392 {
1393 	event.Skip();
1394 	if (m_pInfoText) {
1395 		m_pInfoText->Reposition();
1396 	}
1397 }
1398 
OnStateChange(t_statechange_notifications notification,std::wstring const & data,const void *)1399 void CLocalListView::OnStateChange(t_statechange_notifications notification, std::wstring const& data, const void*)
1400 {
1401 	if (notification == STATECHANGE_LOCAL_DIR) {
1402 		DisplayDir(m_state.GetLocalDir());
1403 	}
1404 	else if (notification == STATECHANGE_APPLYFILTER) {
1405 		ApplyCurrentFilter();
1406 	}
1407 	else if (notification == STATECHANGE_SERVER) {
1408 		if (m_windowTinter) {
1409 			m_windowTinter->SetBackgroundTint(site_colour_to_wx(m_state.GetSite().m_colour));
1410 		}
1411 		if (m_pInfoText) {
1412 			m_pInfoText->SetBackgroundTint(site_colour_to_wx(m_state.GetSite().m_colour));
1413 		}
1414 	}
1415 	else {
1416 		wxASSERT(notification == STATECHANGE_LOCAL_REFRESH_FILE);
1417 		RefreshFile(data);
1418 	}
1419 }
1420 
SetInfoText(wxString const & text)1421 void CLocalListView::SetInfoText(wxString const& text)
1422 {
1423 	if (!m_pInfoText) {
1424 		return;
1425 	}
1426 
1427 	if (IsComparing() || text.empty()) {
1428 		m_pInfoText->Hide();
1429 	}
1430 	else {
1431 		m_pInfoText->SetText(text);
1432 		m_pInfoText->Reposition();
1433 		m_pInfoText->Show();
1434 	}
1435 }
1436 
OnBeginDrag(wxListEvent &)1437 void CLocalListView::OnBeginDrag(wxListEvent&)
1438 {
1439 	if (COptions::Get()->get_int(OPTION_DND_DISABLED) != 0) {
1440 		return;
1441 	}
1442 
1443 	long item = -1;
1444 	for (;;) {
1445 		item = GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
1446 		if (item == -1) {
1447 			break;
1448 		}
1449 
1450 		if (!item && m_hasParent) {
1451 			return;
1452 		}
1453 	}
1454 
1455 #ifdef __WXMAC__
1456 	// Don't use wxFileDataObject on Mac, crashes on Mojave, wx bug #18232
1457 	CLocalDataObject obj;
1458 #else
1459 	wxFileDataObject obj;
1460 #endif
1461 
1462 	CDragDropManager* pDragDropManager = CDragDropManager::Init();
1463 	pDragDropManager->pDragSource = this;
1464 	pDragDropManager->localParent = m_dir;
1465 
1466 	auto const path = m_dir.GetPath();
1467 
1468 	bool added = false;
1469 
1470 	item = -1;
1471 	for (;;) {
1472 		item = GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
1473 		if (item == -1) {
1474 			break;
1475 		}
1476 
1477 		CLocalFileData *data = GetData(item);
1478 		if (!data) {
1479 			continue;
1480 		}
1481 
1482 		if (data->comparison_flags == fill) {
1483 			continue;
1484 		}
1485 
1486 		std::wstring name = path + data->name;
1487 		obj.AddFile(name);
1488 		added = true;
1489 	}
1490 
1491 	if (!added) {
1492 		pDragDropManager->Release();
1493 		return;
1494 	}
1495 
1496 	CLabelEditBlocker b(*this);
1497 
1498 	wxDropSource source(this);
1499 	source.SetData(obj);
1500 	int res = source.DoDragDrop(wxDrag_AllowMove);
1501 
1502 	bool handled_internally = pDragDropManager->pDropTarget != 0;
1503 
1504 	pDragDropManager->Release();
1505 
1506 	if (!handled_internally && (res == wxDragCopy || res == wxDragMove)) {
1507 		// We only need to refresh local side if the operation got handled
1508 		// externally, the internal handlers do this for us already
1509 		m_state.RefreshLocal();
1510 	}
1511 }
1512 
RefreshFile(std::wstring const & file)1513 void CLocalListView::RefreshFile(std::wstring const& file)
1514 {
1515 	CLocalFileData data;
1516 
1517 	bool wasLink;
1518 	fz::local_filesys::type type = fz::local_filesys::get_file_info(fz::to_native(m_dir.GetPath() + file), wasLink, &data.size, &data.time, &data.attributes);
1519 	if (type == fz::local_filesys::unknown) {
1520 		return;
1521 	}
1522 
1523 	data.name = file;
1524 	data.dir = type == fz::local_filesys::dir;
1525 
1526 	CStateFilterManager const& filter = m_state.GetStateFilterManager();
1527 	if (filter.FilenameFiltered(data.name, m_dir.GetPath(), data.dir, data.size, true, data.attributes, data.time)) {
1528 		return;
1529 	}
1530 
1531 	CancelLabelEdit();
1532 
1533 	// Look if file data already exists
1534 	unsigned int i = 0;
1535 	for (auto iter = m_fileData.begin(); iter != m_fileData.end(); ++iter, ++i) {
1536 		const CLocalFileData& oldData = *iter;
1537 		if (oldData.name != file) {
1538 			continue;
1539 		}
1540 
1541 		// Update file list status bar
1542 		if (m_pFilelistStatusBar) {
1543 #ifndef __WXMSW__
1544 			// GetNextItem is O(n) if nothing is selected, GetSelectedItemCount() is O(1)
1545 			if (GetSelectedItemCount())
1546 #endif
1547 			{
1548 				int item = -1;
1549 				for (;;) {
1550 					item = GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
1551 					if (item == -1) {
1552 						break;
1553 					}
1554 					if (m_indexMapping[item] != i) {
1555 						continue;
1556 					}
1557 
1558 					if (oldData.dir) {
1559 						m_pFilelistStatusBar->UnselectDirectory();
1560 					}
1561 					else {
1562 						m_pFilelistStatusBar->UnselectFile(oldData.size);
1563 					}
1564 					if (data.dir) {
1565 						m_pFilelistStatusBar->SelectDirectory();
1566 					}
1567 					else {
1568 						m_pFilelistStatusBar->SelectFile(data.size);
1569 					}
1570 					break;
1571 				}
1572 			}
1573 
1574 			if (oldData.dir) {
1575 				m_pFilelistStatusBar->RemoveDirectory();
1576 			}
1577 			else {
1578 				m_pFilelistStatusBar->RemoveFile(oldData.size);
1579 			}
1580 			if (data.dir) {
1581 				m_pFilelistStatusBar->AddDirectory();
1582 			}
1583 			else {
1584 				m_pFilelistStatusBar->AddFile(data.size);
1585 			}
1586 		}
1587 
1588 		// Update the data
1589 		data.fileType = oldData.fileType;
1590 
1591 		*iter = data;
1592 		if (IsComparing()) {
1593 			// Sort order doesn't change
1594 			RefreshComparison();
1595 		}
1596 		else {
1597 			if (m_sortColumn) {
1598 				SortList();
1599 			}
1600 			RefreshListOnly(false);
1601 		}
1602 		return;
1603 	}
1604 
1605 	if (data.dir) {
1606 		m_pFilelistStatusBar->AddDirectory();
1607 	}
1608 	else {
1609 		m_pFilelistStatusBar->AddFile(data.size);
1610 	}
1611 
1612 	std::wstring focused;
1613 	int focusedItem = -1;
1614 	std::vector<std::wstring> selectedNames;
1615 	if (IsComparing()) {
1616 		selectedNames = RememberSelectedItems(focused, focusedItem);
1617 		if (!m_originalIndexMapping.empty()) {
1618 			m_indexMapping.clear();
1619 			m_originalIndexMapping.swap(m_indexMapping);
1620 		}
1621 	}
1622 
1623 	// Insert new entry
1624 	int index = m_fileData.size();
1625 	m_fileData.push_back(data);
1626 
1627 	// Find correct position in index mapping
1628 	std::vector<unsigned int>::iterator start = m_indexMapping.begin();
1629 	if (m_hasParent) {
1630 		++start;
1631 	}
1632 	std::unique_ptr<CFileListCtrlSortBase> compare = GetSortComparisonObject();
1633 	std::vector<unsigned int>::iterator insertPos = std::lower_bound(start, m_indexMapping.end(), index, SortPredicate(compare));
1634 
1635 	const int item = insertPos - m_indexMapping.begin();
1636 	m_indexMapping.insert(insertPos, index);
1637 
1638 	if (!IsComparing()) {
1639 		SetItemCount(m_indexMapping.size());
1640 
1641 		// Move selections
1642 		int prevState = 0;
1643 		for (unsigned int j = item; j < m_indexMapping.size(); ++j) {
1644 			int state = GetItemState(j, wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED);
1645 			if (state != prevState) {
1646 				SetItemState(j, prevState, wxLIST_STATE_FOCUSED);
1647 				SetSelection(j, (prevState & wxLIST_STATE_SELECTED) != 0);
1648 				prevState = state;
1649 			}
1650 		}
1651 		RefreshListOnly();
1652 	}
1653 	else {
1654 		RefreshComparison();
1655 		if (m_pFilelistStatusBar) {
1656 			m_pFilelistStatusBar->UnselectAll();
1657 		}
1658 		ReselectItems(selectedNames, focused, focusedItem);
1659 	}
1660 }
1661 
OnGetItemAttr(long item) const1662 wxListItemAttr* CLocalListView::OnGetItemAttr(long item) const
1663 {
1664 	CLocalListView *pThis = const_cast<CLocalListView *>(this);
1665 	const CLocalFileData* const data = pThis->GetData((unsigned int)item);
1666 
1667 	if (!data) {
1668 		return 0;
1669 	}
1670 
1671 #ifndef __WXMSW__
1672 	if (item == m_dropTarget) {
1673 		return &pThis->m_dropHighlightAttribute;
1674 	}
1675 #endif
1676 
1677 	switch (data->comparison_flags)
1678 	{
1679 	case different:
1680 		return &pThis->m_comparisonBackgrounds[0];
1681 	case lonely:
1682 		return &pThis->m_comparisonBackgrounds[1];
1683 	case newer:
1684 		return &pThis->m_comparisonBackgrounds[2];
1685 	default:
1686 		return 0;
1687 	}
1688 }
1689 
StartComparison()1690 void CLocalListView::StartComparison()
1691 {
1692 	if (m_sortDirection || m_sortColumn != 0) {
1693 		wxASSERT(m_originalIndexMapping.empty());
1694 		SortList(0, 0);
1695 	}
1696 
1697 	ComparisonRememberSelections();
1698 
1699 	if (m_originalIndexMapping.empty()) {
1700 		m_originalIndexMapping.swap(m_indexMapping);
1701 	}
1702 	else {
1703 		m_indexMapping.clear();
1704 	}
1705 
1706 	m_comparisonIndex = -1;
1707 
1708 	if (m_fileData.empty() || m_fileData.back().comparison_flags != fill) {
1709 		CLocalFileData data;
1710 		data.dir = false;
1711 		data.icon = -1;
1712 		data.size = -1;
1713 		data.comparison_flags = fill;
1714 		m_fileData.push_back(data);
1715 	}
1716 }
1717 
get_next_file(std::wstring_view & name,std::wstring &,bool & dir,int64_t & size,fz::datetime & date)1718 bool CLocalListView::get_next_file(std::wstring_view & name, std::wstring &, bool& dir, int64_t& size, fz::datetime& date)
1719 {
1720 	if (++m_comparisonIndex >= (int)m_originalIndexMapping.size()) {
1721 		return false;
1722 	}
1723 
1724 	const unsigned int index = m_originalIndexMapping[m_comparisonIndex];
1725 	if (index >= m_fileData.size()) {
1726 		return false;
1727 	}
1728 
1729 	CLocalFileData const& data = m_fileData[index];
1730 
1731 	name = data.name;
1732 	dir = data.dir;
1733 	size = data.size;
1734 	date = data.time;
1735 
1736 	return true;
1737 }
1738 
FinishComparison()1739 void CLocalListView::FinishComparison()
1740 {
1741 	SetInfoText(wxString());
1742 
1743 	SetItemCount(m_indexMapping.size());
1744 
1745 	ComparisonRestoreSelections();
1746 
1747 	RefreshListOnly();
1748 
1749 	CComparableListing* pOther = GetOther();
1750 	if (pOther) {
1751 		pOther->ScrollTopItem(GetTopItem());
1752 	}
1753 }
1754 
CanStartComparison()1755 bool CLocalListView::CanStartComparison()
1756 {
1757 	return true;
1758 }
1759 
GetItemText(int item,unsigned int column)1760 wxString CLocalListView::GetItemText(int item, unsigned int column)
1761 {
1762 	CLocalFileData *data = GetData(item);
1763 	if (!data) {
1764 		return wxString();
1765 	}
1766 
1767 	if (!column) {
1768 #ifdef __WXMSW__
1769 		return data->label ? *data->label : data->name;
1770 #else
1771 		return data->name;
1772 #endif
1773 	}
1774 	else if (column == 1) {
1775 		if (data->size < 0) {
1776 			return wxString();
1777 		}
1778 		else {
1779 			return CSizeFormat::Format(data->size);
1780 		}
1781 	}
1782 	else if (column == 2) {
1783 		if (!item && m_hasParent) {
1784 			return wxString();
1785 		}
1786 
1787 		if (data->comparison_flags == fill) {
1788 			return wxString();
1789 		}
1790 
1791 		if (data->fileType.empty()) {
1792 			data->fileType = GetType(data->name, data->dir, m_dir.GetPath());
1793 		}
1794 
1795 		return data->fileType;
1796 	}
1797 	else if (column == 3) {
1798 		return CTimeFormat::Format(data->time);
1799 	}
1800 	return wxString();
1801 }
1802 
OnMenuEdit(wxCommandEvent &)1803 void CLocalListView::OnMenuEdit(wxCommandEvent&)
1804 {
1805 	Site site;
1806 	CServerPath path;
1807 
1808 	if (!m_state.GetSite()) {
1809 		if (COptions::Get()->get_int(OPTION_EDIT_TRACK_LOCAL)) {
1810 			wxMessageBoxEx(_("Cannot edit file, not connected to any server."), _("Editing failed"), wxICON_EXCLAMATION);
1811 			return;
1812 		}
1813 	}
1814 	else {
1815 		site = m_state.GetSite();
1816 
1817 		path = m_state.GetRemotePath();
1818 		if (path.empty()) {
1819 			wxMessageBoxEx(_("Cannot edit file, remote path unknown."), _("Editing failed"), wxICON_EXCLAMATION);
1820 			return;
1821 		}
1822 	}
1823 
1824 	std::vector<CEditHandler::FileData> selected_item;
1825 
1826 	long item = -1;
1827 	while ((item = GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED)) != -1) {
1828 		if (!item && m_hasParent) {
1829 			wxBell();
1830 			return;
1831 		}
1832 
1833 		const CLocalFileData *data = GetData(item);
1834 		if (!data) {
1835 			continue;
1836 		}
1837 
1838 		if (data->dir) {
1839 			wxBell();
1840 			return;
1841 		}
1842 
1843 		if (data->comparison_flags == fill) {
1844 			continue;
1845 		}
1846 
1847 		selected_item.push_back({m_dir.GetPath() + data->name, data->size});
1848 	}
1849 
1850 	CEditHandler* pEditHandler = CEditHandler::Get();
1851 	pEditHandler->Edit(CEditHandler::local, selected_item, path, site, this);
1852 }
1853 
OnMenuOpen(wxCommandEvent &)1854 void CLocalListView::OnMenuOpen(wxCommandEvent&)
1855 {
1856 	long item = GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
1857 	if (item == -1) {
1858 		OpenInFileManager(m_dir.GetPath());
1859 		return;
1860 	}
1861 
1862 	std::list<CLocalFileData> selected_item_list;
1863 
1864 	item = -1;
1865 	while ((item = GetNextItem(item, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED)) != -1) {
1866 		if (!item && m_hasParent) {
1867 			wxBell();
1868 			return;
1869 		}
1870 
1871 		const CLocalFileData *data = GetData(item);
1872 		if (!data) {
1873 			continue;
1874 		}
1875 
1876 		if (data->comparison_flags == fill) {
1877 			continue;
1878 		}
1879 
1880 		selected_item_list.push_back(*data);
1881 	}
1882 
1883 	CEditHandler* pEditHandler = CEditHandler::Get();
1884 	if (!pEditHandler) {
1885 		wxBell();
1886 		return;
1887 	}
1888 
1889 	if (selected_item_list.empty()) {
1890 		wxBell();
1891 		return;
1892 	}
1893 
1894 	if (selected_item_list.size() > 10) {
1895 		CConditionalDialog dlg(this, CConditionalDialog::many_selected_for_edit, CConditionalDialog::yesno);
1896 		dlg.SetTitle(_("Confirmation needed"));
1897 		dlg.AddText(_("You have selected more than 10 files or directories to open, do you really want to continue?"));
1898 
1899 		if (!dlg.Run()) {
1900 			return;
1901 		}
1902 	}
1903 
1904 	for (auto const& data : selected_item_list) {
1905 		if (data.dir) {
1906 			CLocalPath path(m_dir);
1907 			if (!path.ChangePath(data.name)) {
1908 				wxBell();
1909 				continue;
1910 			}
1911 
1912 			OpenInFileManager(path.GetPath());
1913 			continue;
1914 		}
1915 
1916 
1917 		wxFileName fn(m_dir.GetPath(), data.name);
1918 		if (wxLaunchDefaultApplication(fn.GetFullPath(), 0)) {
1919 			continue;
1920 		}
1921 		auto cmd_with_args = GetSystemAssociation(fn.GetFullPath().ToStdWstring());
1922 		if (cmd_with_args.empty()) {
1923 			cmd_with_args = pEditHandler->GetAssociation(fn.GetFullPath().ToStdWstring());
1924 		}
1925 		if (cmd_with_args.empty()) {
1926 			wxMessageBoxEx(wxString::Format(_("The file '%s' could not be opened:\nNo program has been associated on your system with this file type."), fn.GetFullPath()), _("Opening failed"), wxICON_EXCLAMATION);
1927 			continue;
1928 		}
1929 		if (!ProgramExists(cmd_with_args.front())) {
1930 			wxString msg = wxString::Format(_("The file '%s' cannot be opened:\nThe associated program (%s) could not be found.\nPlease check your filetype associations."), fn.GetFullPath(), cmd_with_args.front());
1931 			wxMessageBoxEx(msg, _("Cannot edit file"), wxICON_EXCLAMATION);
1932 			continue;
1933 		}
1934 
1935 		if (fz::spawn_detached_process(AssociationToCommand(cmd_with_args, fn.GetFullPath().ToStdWstring()))) {
1936 			continue;
1937 		}
1938 		wxMessageBoxEx(wxString::Format(_("The file '%s' could not be opened:\nThe associated command failed"), fn.GetFullPath()), _("Opening failed"), wxICON_EXCLAMATION);
1939 
1940 	}
1941 }
1942 
ItemIsDir(int index) const1943 bool CLocalListView::ItemIsDir(int index) const
1944 {
1945 	return m_fileData[index].dir;
1946 }
1947 
ItemGetSize(int index) const1948 int64_t CLocalListView::ItemGetSize(int index) const
1949 {
1950 	return m_fileData[index].size;
1951 }
1952 
1953 #ifdef __WXMSW__
1954 
OnVolumesEnumerated(wxCommandEvent & event)1955 void CLocalListView::OnVolumesEnumerated(wxCommandEvent& event)
1956 {
1957 	if (!volumeEnumeratorThread_) {
1958 		return;
1959 	}
1960 
1961 	std::vector<CVolumeDescriptionEnumeratorThread::t_VolumeInfo> volumeInfo = volumeEnumeratorThread_->GetVolumes();
1962 
1963 	if (event.GetEventType() == fzEVT_VOLUMESENUMERATED) {
1964 		volumeEnumeratorThread_.reset();
1965 	}
1966 
1967 	if (m_dir.GetPath() != _T("\\")) {
1968 		return;
1969 	}
1970 
1971 	for (auto const& info : volumeInfo) {
1972 		std::wstring const& drive = info.volume;
1973 
1974 		unsigned int item, index;
1975 		for (item = m_hasParent ? 1 : 0; item < m_indexMapping.size(); ++item) {
1976 			index = m_indexMapping[item];
1977 			if (m_fileData[index].name == drive || m_fileData[index].name.substr(0, drive.size() + 1) == drive + _T(" ")) {
1978 				break;
1979 			}
1980 		}
1981 		if (item >= m_indexMapping.size()) {
1982 			continue;
1983 		}
1984 
1985 		if (!info.volumeName.empty()) {
1986 			m_fileData[index].label = fz::sparse_optional<std::wstring>(drive + _T(" (") + info.volumeName + _T(")"));
1987 		}
1988 		if (info.icon != -1) {
1989 			m_fileData[index].icon = info.icon;
1990 		}
1991 
1992 		RefreshItem(item);
1993 	}
1994 }
1995 
1996 #endif
1997 
OnMenuRefresh(wxCommandEvent &)1998 void CLocalListView::OnMenuRefresh(wxCommandEvent&)
1999 {
2000 	m_state.RefreshLocal();
2001 }
2002 
OnNavigationEvent(bool forward)2003 void CLocalListView::OnNavigationEvent(bool forward)
2004 {
2005 	if (!forward) {
2006 		if (!m_hasParent) {
2007 			wxBell();
2008 			return;
2009 		}
2010 
2011 		std::wstring error;
2012 		if (!m_state.SetLocalDir(_T(".."), &error)) {
2013 			if (!error.empty()) {
2014 				wxMessageBoxEx(error, _("Failed to change directory"), wxICON_INFORMATION);
2015 			}
2016 			else {
2017 				wxBell();
2018 			}
2019 		}
2020 	}
2021 }
2022