Files
free-file-sync-mirror/FreeFileSync/Source/ui/abstract_folder_picker.cpp
2025-12-10 14:38:26 -08:00

393 lines
17 KiB
C++

// *****************************************************************************
// * This file is part of the FreeFileSync project. It is distributed under *
// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 *
// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
// *****************************************************************************
#include "abstract_folder_picker.h"
#include <wx+/async_task.h>
#include <wx+/image_resources.h>
#include <wx+/popup_dlg.h>
#include <wx+/image_tools.h>
#include "gui_generated.h"
#include "../icon_buffer.h"
using namespace zen;
using namespace fff;
using AFS = AbstractFileSystem;
namespace
{
enum class NodeLoadStatus
{
notLoaded,
loading,
loaded
};
struct AfsTreeItemData : public wxTreeItemData
{
AfsTreeItemData(const AbstractPath& path) : folderPath(path) {}
const AbstractPath folderPath;
std::wstring errorMsg; //optional
NodeLoadStatus loadStatus = NodeLoadStatus::notLoaded;
std::vector<std::function<void()>> onLoadCompleted; //bound!
};
wxString getNodeDisplayName(const AbstractPath& folderPath)
{
if (!AFS::getParentPath(folderPath)) //server root
return utfTo<wxString>(FILE_NAME_SEPARATOR);
return utfTo<wxString>(AFS::getItemName(folderPath));
}
class AbstractFolderPickerDlg : public AbstractFolderPickerGenerated
{
public:
AbstractFolderPickerDlg(wxWindow* parent, AbstractPath& folderPath);
private:
void onOkay (wxCommandEvent& event) override;
void onCancel(wxCommandEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); }
void onClose (wxCloseEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); }
void onLocalKeyEvent(wxKeyEvent& event);
void onExpandNode(wxTreeEvent& event) override;
void onItemTooltip(wxTreeEvent& event);
void populateNodeThen(const wxTreeItemId& itemId, const std::function<void()>& evalOnGui /*optional*/, bool popupErrors);
void findAndNavigateToExistingPath(const AbstractPath& folderPath);
void navigateToExistingPath(const wxTreeItemId& itemId, const std::vector<Zstring>& nodeRelPath, AFS::ItemType leafType);
enum class TreeNodeImage
{
root = 0, //used as zero-based wxImageList index!
folder,
folderSymlink,
error
};
AsyncGuiQueue guiQueue_{25 /*polling [ms]*/}; //schedule and run long-running tasks asynchronously, but process results on GUI queue
//output-only parameters:
AbstractPath& folderPathOut_;
};
AbstractFolderPickerDlg::AbstractFolderPickerDlg(wxWindow* parent, AbstractPath& folderPath) :
AbstractFolderPickerGenerated(parent),
folderPathOut_(folderPath)
{
setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonOK).setCancel(m_buttonCancel));
m_staticTextStatus->SetLabel(L"");
m_treeCtrlFileSystem->SetMinSize({dipToWxsize(350), dipToWxsize(400)});
const int iconSize = screenToWxsize(IconBuffer::getPixSize(IconBuffer::IconSize::small));
auto imgList = std::make_unique<wxImageList>(iconSize, iconSize);
//add images in same sequence like TreeNodeImage enum!!!
imgList->Add(toScaledBitmap(loadImage("server", wxsizeToScreen(iconSize))));
imgList->Add(toScaledBitmap( IconBuffer::genericDirIcon (IconBuffer::IconSize::small)));
imgList->Add(toScaledBitmap(layOver(IconBuffer::genericDirIcon (IconBuffer::IconSize::small),
IconBuffer::linkOverlayIcon(IconBuffer::IconSize::small))));
imgList->Add(toScaledBitmap(loadImage("msg_error", wxsizeToScreen(iconSize))));
assert(imgList->GetImageCount() == static_cast<int>(TreeNodeImage::error) + 1);
m_treeCtrlFileSystem->AssignImageList(imgList.release()); //pass ownership
const AbstractPath rootPath(folderPath.afsDevice, AfsPath());
const wxTreeItemId rootId = m_treeCtrlFileSystem->AddRoot(getNodeDisplayName(rootPath), static_cast<int>(TreeNodeImage::root), -1,
new AfsTreeItemData(rootPath));
m_treeCtrlFileSystem->SetItemHasChildren(rootId);
if (!AFS::getParentPath(folderPath)) //server root
populateNodeThen(rootId, [this, rootId] { m_treeCtrlFileSystem->Expand(rootId); }, true /*popupErrors*/);
else
try //folder picker has dual responsibility:
{
//1. test server connection:
const AFS::ItemType type = AFS::getItemType(folderPath); //throw FileError
//2. navigate + select path
navigateToExistingPath(rootId, splitCpy(folderPath.afsPath.value, FILE_NAME_SEPARATOR, SplitOnEmpty::skip), type);
}
catch (const FileError& e) //not existing or access error
{
findAndNavigateToExistingPath(*AFS::getParentPath(folderPath)); //let's run async while the error message is shown :)
showNotificationDialog(parent /*"this" not yet shown!*/, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString()));
}
//----------------------------------------------------------------------
GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize()
#ifdef __WXGTK3__
Show(); //GTK3 size calculation requires visible window: https://github.com/wxWidgets/wxWidgets/issues/16088
//Hide(); -> avoids old position flash before Center() on GNOME but causes hang on KDE? https://freefilesync.org/forum/viewtopic.php?t=10103#p42404
#endif
Center(); //apply *after* dialog size change!
Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& event) { onLocalKeyEvent(event); }); //dialog-specific local key events
Bind(wxEVT_TREE_ITEM_GETTOOLTIP, [this](wxTreeEvent& event) { onItemTooltip (event); });
m_treeCtrlFileSystem->SetFocus();
}
void AbstractFolderPickerDlg::onLocalKeyEvent(wxKeyEvent& event)
{
switch (event.GetKeyCode())
{
//wxTreeCtrl seems to eat up ENTER without adding any functionality; we can do better:
case WXK_RETURN:
case WXK_NUMPAD_ENTER:
if (event.ControlDown()) //Ctrl+Enter or on macOS: Command+Enter
{
wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED);
m_buttonOK->Command(dummy); //simulate click
return;
}
break;
}
event.Skip();
}
struct FlatTraverserCallback : public AFS::TraverserCallback
{
struct Result
{
std::unordered_map<Zstring, bool /*is symlink*/> folderNames;
std::wstring errorMsg;
};
const Result& getResult() { return result_; }
private:
void onFile (const AFS::FileInfo& fi) override {}
std::shared_ptr<TraverserCallback> onFolder (const AFS::FolderInfo& fi) override { result_.folderNames.emplace(fi.itemName, fi.isFollowedSymlink); return nullptr; }
HandleLink onSymlink(const AFS::SymlinkInfo& si) override { return HandleLink::follow; }
HandleError reportDirError (const ErrorInfo& errorInfo) override { logError(errorInfo.msg); return HandleError::ignore; }
HandleError reportItemError(const ErrorInfo& errorInfo, const Zstring& itemName) override { logError(errorInfo.msg); return HandleError::ignore; }
void logError(const std::wstring& msg)
{
if (result_.errorMsg.empty())
result_.errorMsg = msg;
}
Result result_;
};
void AbstractFolderPickerDlg::populateNodeThen(const wxTreeItemId& itemId, const std::function<void()>& evalOnGui, bool popupErrors)
{
if (auto itemData = dynamic_cast<AfsTreeItemData*>(m_treeCtrlFileSystem->GetItemData(itemId)))
{
switch (itemData->loadStatus)
{
case NodeLoadStatus::notLoaded:
{
if (evalOnGui)
itemData->onLoadCompleted.push_back(evalOnGui);
itemData->loadStatus = NodeLoadStatus::loading;
m_treeCtrlFileSystem->SetItemText(itemId, getNodeDisplayName(itemData->folderPath) + L" (" + _("Loading...") + L')');
guiQueue_.processAsync([folderPath = itemData->folderPath] //AbstractPath is thread-safe like an int!
{
auto ft = std::make_shared<FlatTraverserCallback>(); //noexcept, traverse directory one level deep
AFS::traverseFolderRecursive(folderPath.afsDevice, {{folderPath.afsPath, ft}}, 1 /*parallelOps*/);
return ft->getResult();
},
[this, itemId, popupErrors](const FlatTraverserCallback::Result& result)
{
if (auto itemData2 = dynamic_cast<AfsTreeItemData*>(m_treeCtrlFileSystem->GetItemData(itemId)))
{
m_treeCtrlFileSystem->SetItemText(itemId, getNodeDisplayName(itemData2->folderPath)); //remove "loading" phrase
if (result.folderNames.empty())
m_treeCtrlFileSystem->SetItemHasChildren(itemId, false);
else
{
//let's not use the wxTreeCtrl::OnCompareItems() abomination to implement sorting:
std::vector<std::pair<Zstring, bool /*is symlink*/>> folderNamesSorted(result.folderNames.begin(), result.folderNames.end());
std::sort(folderNamesSorted.begin(), folderNamesSorted.end(), [](const auto& lhs, const auto& rhs) { return LessNaturalSort()(lhs.first, rhs.first); });
for (const auto& [childName, isSymlink] : folderNamesSorted)
{
const AbstractPath childFolderPath = AFS::appendRelPath(itemData2->folderPath, childName);
wxTreeItemId childId = m_treeCtrlFileSystem->AppendItem(itemId, getNodeDisplayName(childFolderPath),
static_cast<int>(isSymlink ? TreeNodeImage::folderSymlink : TreeNodeImage::folder), -1,
new AfsTreeItemData(childFolderPath));
m_treeCtrlFileSystem->SetItemHasChildren(childId);
}
}
if (!result.errorMsg.empty())
{
m_treeCtrlFileSystem->SetItemImage(itemId, static_cast<int>(TreeNodeImage::error));
itemData2->errorMsg = result.errorMsg;
if (popupErrors)
showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(result.errorMsg));
}
itemData2->loadStatus = NodeLoadStatus::loaded; //set status *before* running callbacks
for (const auto& evalOnGui2 : itemData2->onLoadCompleted)
evalOnGui2();
}
});
}
break;
case NodeLoadStatus::loading:
if (evalOnGui) itemData->onLoadCompleted.push_back(evalOnGui);
break;
case NodeLoadStatus::loaded:
if (evalOnGui) evalOnGui();
break;
}
}
}
//1. find longest existing/accessible (parent) path
void AbstractFolderPickerDlg::findAndNavigateToExistingPath(const AbstractPath& folderPath)
{
if (!AFS::getParentPath(folderPath))
return m_staticTextStatus->SetLabel(L"");
m_staticTextStatus->SetLabelText(_("Scanning...") + L' ' + utfTo<std::wstring>(FILE_NAME_SEPARATOR + folderPath.afsPath.value)); //keep it short!
guiQueue_.processAsync([folderPath]() -> std::optional<AFS::ItemType>
{
try
{
return AFS::getItemType(folderPath); //throw FileError
}
catch (FileError&) { return std::nullopt; } //not existing or access error
},
[this, folderPath](std::optional<AFS::ItemType> type)
{
if (type)
{
m_staticTextStatus->SetLabel(L"");
navigateToExistingPath(m_treeCtrlFileSystem->GetRootItem(), splitCpy(folderPath.afsPath.value, FILE_NAME_SEPARATOR, SplitOnEmpty::skip), *type);
}
else //split into multiple small async tasks rather than a single large one!
findAndNavigateToExistingPath(*AFS::getParentPath(folderPath));
});
}
//2. navgiate while ignoring any intermediate (access) errors or problems with hidden folders
void AbstractFolderPickerDlg::navigateToExistingPath(const wxTreeItemId& itemId, const std::vector<Zstring>& nodeRelPath, AFS::ItemType leafType)
{
if (nodeRelPath.empty() ||
(nodeRelPath.size() == 1 && leafType == AFS::ItemType::file)) //let's be *uber* correct
{
m_treeCtrlFileSystem->SelectItem(itemId);
//m_treeCtrlFileSystem->EnsureVisible(itemId); -> not needed: maybe wxTreeCtrl::Expand() does this?
return;
}
populateNodeThen(itemId, [this, itemId, nodeRelPath, leafType]
{
const Zstring childFolderName = nodeRelPath.front();
const std::vector<Zstring> childFolderRelPath{nodeRelPath.begin() + 1, nodeRelPath.end()};
wxTreeItemId childIdMatch;
size_t insertPos = 0; //let's not use the wxTreeCtrl::OnCompareItems() abomination to implement sorting
wxTreeItemIdValue cookie = nullptr;
for (wxTreeItemId childId = m_treeCtrlFileSystem->GetFirstChild(itemId, cookie);
childId.IsOk();
childId = m_treeCtrlFileSystem->GetNextChild(itemId, cookie))
if (auto itemData = dynamic_cast<AfsTreeItemData*>(m_treeCtrlFileSystem->GetItemData(childId)))
{
const Zstring& itemName = AFS::getItemName(itemData->folderPath);
if (LessNaturalSort()(itemName, childFolderName))
++insertPos; //assume items are already naturally sorted, see populateNodeThen()
if (equalNoCase(itemName, childFolderName))
{
childIdMatch = childId;
if (itemName == childFolderName)
break; //exact match => no need to search further!
}
}
//we *know* that childFolder exists: Maybe it's just hidden during browsing: https://freefilesync.org/forum/viewtopic.php?t=3809
if (!childIdMatch.IsOk()) // or access to root folder is denied: https://freefilesync.org/forum/viewtopic.php?t=5999
if (auto itemData = dynamic_cast<AfsTreeItemData*>(m_treeCtrlFileSystem->GetItemData(itemId)))
{
m_treeCtrlFileSystem->SetItemHasChildren(itemId);
const AbstractPath childFolderPath = AFS::appendRelPath(itemData->folderPath, childFolderName);
childIdMatch = m_treeCtrlFileSystem->InsertItem(itemId, insertPos, getNodeDisplayName(childFolderPath),
static_cast<int>(childFolderRelPath.empty() && leafType == AFS::ItemType::symlink ?
TreeNodeImage::folderSymlink : TreeNodeImage::folder), -1,
new AfsTreeItemData(childFolderPath));
m_treeCtrlFileSystem->SetItemHasChildren(childIdMatch);
}
m_treeCtrlFileSystem->Expand(itemId); //wxTreeCtr::Expand emits wxTreeEvent!!!
navigateToExistingPath(childIdMatch, childFolderRelPath, leafType);
}, false /*popupErrors*/);
}
void AbstractFolderPickerDlg::onExpandNode(wxTreeEvent& event)
{
const wxTreeItemId itemId = event.GetItem();
if (auto itemData = dynamic_cast<AfsTreeItemData*>(m_treeCtrlFileSystem->GetItemData(itemId)))
if (itemData->loadStatus != NodeLoadStatus::loaded)
populateNodeThen(itemId, [this, itemId]() { m_treeCtrlFileSystem->Expand(itemId); }, true /*popupErrors*/); //wxTreeCtr::Expand emits wxTreeEvent!!! watch out for recursion!
}
void AbstractFolderPickerDlg::onItemTooltip(wxTreeEvent& event)
{
wxString tooltip;
if (auto itemData = dynamic_cast<AfsTreeItemData*>(m_treeCtrlFileSystem->GetItemData(event.GetItem())))
tooltip = itemData->errorMsg;
event.SetToolTip(tooltip);
}
void AbstractFolderPickerDlg::onOkay(wxCommandEvent& event)
{
const wxTreeItemId itemId = m_treeCtrlFileSystem->GetFocusedItem();
auto itemData = dynamic_cast<AfsTreeItemData*>(m_treeCtrlFileSystem->GetItemData(itemId));
assert(itemData);
if (itemData)
folderPathOut_ = itemData->folderPath;
EndModal(static_cast<int>(ConfirmationButton::accept));
}
}
ConfirmationButton fff::showAbstractFolderPicker(wxWindow* parent, AbstractPath& folderPath)
{
AbstractFolderPickerDlg pickerDlg(parent, folderPath);
return static_cast<ConfirmationButton>(pickerDlg.ShowModal());
}