1219 lines
46 KiB
C++
1219 lines
46 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 "tree_grid.h"
|
|
#include <wx/settings.h>
|
|
#include <zen/i18n.h>
|
|
#include <zen/utf.h>
|
|
#include <zen/stl_tools.h>
|
|
#include <zen/format_unit.h>
|
|
#include <wx+/rtl.h>
|
|
#include <wx+/color_tools.h>
|
|
#include <wx+/dc.h>
|
|
#include <wx+/context_menu.h>
|
|
#include <wx+/image_resources.h>
|
|
#include "../icon_buffer.h"
|
|
|
|
using namespace zen;
|
|
using namespace fff;
|
|
|
|
|
|
namespace
|
|
{
|
|
//let's NOT create wxWidgets objects statically:
|
|
const int PERCENTAGE_BAR_WIDTH_DIP = 60;
|
|
const int TREE_GRID_GAP_SIZE_DIP = 4;
|
|
|
|
inline wxColor getColorPercentBorder () { return {198, 198, 198}; }
|
|
inline wxColor getColorPercentBackground() { return {0xf8, 0xf8, 0xf8}; }
|
|
|
|
|
|
Zstring getFolderPairName(const FolderPair& folder)
|
|
{
|
|
if (folder.hasEquivalentItemNames())
|
|
return folder.getItemName<SelectSide::left>();
|
|
else
|
|
return folder.getItemName<SelectSide::left >() + Zstr(" | ") +
|
|
folder.getItemName<SelectSide::right>();
|
|
}
|
|
}
|
|
|
|
|
|
TreeView::TreeView(FolderComparison& folderCmp, const SortInfo& si) : currentSort_(si)
|
|
{
|
|
for (SharedRef<BaseFolderPair>& baseObj : folderCmp)
|
|
//remove truly empty folder pairs as early as this: we want to distinguish single/multiple folder pair cases by looking at "folderCmp"
|
|
if (!AFS::isNullPath(baseObj.ref().getAbstractPath<SelectSide::left >()) ||
|
|
!AFS::isNullPath(baseObj.ref().getAbstractPath<SelectSide::right>()))
|
|
folderCmp_.push_back(baseObj.ptr());
|
|
}
|
|
|
|
|
|
inline
|
|
void TreeView::compressNode(Container& cont) //remove single-element sub-trees -> gain clarity + usability (call *after* inclusion check!!!)
|
|
{
|
|
if (cont.subDirs.empty()) //single files node
|
|
cont.showFilesNode = false;
|
|
|
|
#if 0 //let's not go overboard: empty folders should not be condensed => used for file exclusion filter; user expects to see them
|
|
if (!cont.showFilesNode && //single dir node...
|
|
cont.subDirs.size() == 1 && //
|
|
!cont.subDirs[0].showFilesNode && //...that is empty
|
|
cont.subDirs[0].subDirs.empty()) //
|
|
cont.subDirs.clear();
|
|
#endif
|
|
}
|
|
|
|
|
|
template <class Function> //(const FileSystemObject&) -> bool
|
|
void TreeView::extractVisibleSubtree(ContainerObject& conObj, //in
|
|
TreeView::Container& cont, //out
|
|
Function pred)
|
|
{
|
|
auto getBytes = [](const FilePair& file) //MSVC screws up miserably if we put this lambda into std::for_each
|
|
{
|
|
#if 0 //give accumulated bytes the semantics of a sync preview?
|
|
switch (getEffectiveSyncDir(file.getSyncOperation()))
|
|
{
|
|
case SyncDirection::none: break;
|
|
case SyncDirection::left: return file.getFileSize<SelectSide::right>();
|
|
case SyncDirection::right: return file.getFileSize<SelectSide::left>();
|
|
}
|
|
#endif
|
|
//prefer file-browser semantics over sync preview (=> always show useful numbers, even for SyncDirection::none)
|
|
//discussion: https://freefilesync.org/forum/viewtopic.php?t=1595
|
|
return std::max(file.isEmpty<SelectSide::left >() ? 0 : file.getFileSize<SelectSide::left>(),
|
|
file.isEmpty<SelectSide::right>() ? 0 : file.getFileSize<SelectSide::right>());
|
|
};
|
|
|
|
for (FilePair& file : conObj.files())
|
|
if (pred(file))
|
|
{
|
|
cont.bytesNet += getBytes(file);
|
|
++cont.itemCountNet;
|
|
}
|
|
|
|
for (SymlinkPair& symlink : conObj.symlinks())
|
|
if (pred(symlink))
|
|
++cont.itemCountNet;
|
|
|
|
cont.showFilesNode = cont.itemCountNet > 0;
|
|
|
|
cont.bytesGross += cont.bytesNet;
|
|
cont.itemCountGross += cont.itemCountNet;
|
|
|
|
cont.subDirs.reserve(conObj.subfolders().size()); //avoid expensive reallocations!
|
|
|
|
for (FolderPair& folder : conObj.subfolders())
|
|
{
|
|
const bool included = pred(folder);
|
|
|
|
cont.subDirs.emplace_back(); //
|
|
auto& subDirCont = cont.subDirs.back();
|
|
TreeView::extractVisibleSubtree(folder, subDirCont, pred);
|
|
if (included)
|
|
++subDirCont.itemCountGross;
|
|
|
|
cont.bytesGross += subDirCont.bytesGross;
|
|
cont.itemCountGross += subDirCont.itemCountGross;
|
|
|
|
if (!included && subDirCont.subDirs.empty() && !subDirCont.showFilesNode)
|
|
cont.subDirs.pop_back();
|
|
else
|
|
{
|
|
subDirCont.containerRef = std::static_pointer_cast<FolderPair>(folder.shared_from_this());
|
|
compressNode(subDirCont);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
namespace
|
|
{
|
|
//generate "nice" percentage numbers which precisely add up to 100
|
|
void calcPercentage(std::vector<std::pair<uint64_t, int*>>& workList)
|
|
{
|
|
uint64_t bytesTotal = 0;
|
|
for (const auto& [bytes, percentOut] : workList)
|
|
bytesTotal += bytes;
|
|
|
|
if (bytesTotal == 0U) //this case doesn't work with the error minimizing algorithm below
|
|
{
|
|
for (auto& [bytes, percentOut] : workList)
|
|
*percentOut = 0;
|
|
return;
|
|
}
|
|
|
|
int remainingPercent = 100;
|
|
for (auto& [bytes, percentOut] : workList)
|
|
{
|
|
*percentOut = static_cast<int>(bytes * 100U / bytesTotal); //round down
|
|
remainingPercent -= *percentOut;
|
|
}
|
|
assert(remainingPercent >= 0);
|
|
assert(remainingPercent < std::ssize(workList));
|
|
|
|
//distribute remaining percent so that overall error is minimized as much as possible:
|
|
remainingPercent = std::min(remainingPercent, static_cast<int>(workList.size()));
|
|
if (remainingPercent > 0)
|
|
{
|
|
std::nth_element(workList.begin(), workList.begin() + remainingPercent - 1, workList.end(),
|
|
[bytesTotal](const std::pair<uint64_t, int*>& lhs, const std::pair<uint64_t, int*>& rhs)
|
|
{
|
|
return lhs.first * 100U % bytesTotal > rhs.first * 100U % bytesTotal;
|
|
});
|
|
|
|
std::for_each(workList.begin(), workList.begin() + remainingPercent, [&](std::pair<uint64_t, int*>& pair) { ++*pair.second; });
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
template <bool ascending>
|
|
struct TreeView::LessShortName
|
|
{
|
|
bool operator()(const TreeLine& lhs, const TreeLine& rhs) const
|
|
{
|
|
//files last (irrespective of sort direction)
|
|
if (lhs.type == NodeType::files)
|
|
return false;
|
|
else if (rhs.type == NodeType::files)
|
|
return true;
|
|
|
|
if (lhs.type != rhs.type) //
|
|
return lhs.type < rhs.type; //shouldn't happen! root nodes not mixed with files or directories
|
|
|
|
switch (lhs.type)
|
|
{
|
|
case NodeType::root:
|
|
return makeSortDirection(LessNaturalSort() /*even on Linux*/,
|
|
std::bool_constant<ascending>())(utfTo<Zstring>(static_cast<const RootNodeImpl*>(lhs.node)->displayName),
|
|
utfTo<Zstring>(static_cast<const RootNodeImpl*>(rhs.node)->displayName));
|
|
case NodeType::folder:
|
|
{
|
|
const auto* folderL = static_cast<FolderPair*>(lhs.node->containerRef.lock().get());
|
|
const auto* folderR = static_cast<FolderPair*>(rhs.node->containerRef.lock().get());
|
|
|
|
if (!folderL)
|
|
return false;
|
|
else if (!folderR)
|
|
return true;
|
|
|
|
return makeSortDirection(LessNaturalSort(), std::bool_constant<ascending>())(getFolderPairName(*folderL), getFolderPairName(*folderR));
|
|
}
|
|
|
|
case NodeType::files:
|
|
break;
|
|
}
|
|
assert(false);
|
|
return false; //:= all equal
|
|
}
|
|
};
|
|
|
|
|
|
template <bool ascending>
|
|
void TreeView::sortSingleLevel(std::vector<TreeLine>& items, ColumnTypeOverview columnType)
|
|
{
|
|
auto getBytes = [](const TreeLine& line) -> uint64_t
|
|
{
|
|
switch (line.type)
|
|
{
|
|
case NodeType::root:
|
|
case NodeType::folder:
|
|
return line.node->bytesGross;
|
|
case NodeType::files:
|
|
return line.node->bytesNet;
|
|
}
|
|
assert(false);
|
|
return 0U;
|
|
};
|
|
|
|
auto getCount = [](const TreeLine& line) -> int
|
|
{
|
|
switch (line.type)
|
|
{
|
|
case NodeType::root:
|
|
case NodeType::folder:
|
|
return line.node->itemCountGross;
|
|
|
|
case NodeType::files:
|
|
return line.node->itemCountNet;
|
|
}
|
|
assert(false);
|
|
return 0;
|
|
};
|
|
|
|
const auto lessBytes = [&](const TreeLine& lhs, const TreeLine& rhs) { return getBytes(lhs) < getBytes(rhs); };
|
|
const auto lessCount = [&](const TreeLine& lhs, const TreeLine& rhs) { return getCount(lhs) < getCount(rhs); };
|
|
|
|
switch (columnType)
|
|
{
|
|
case ColumnTypeOverview::folder:
|
|
std::sort(items.begin(), items.end(), LessShortName<ascending>());
|
|
break;
|
|
case ColumnTypeOverview::itemCount:
|
|
std::sort(items.begin(), items.end(), makeSortDirection(lessCount, std::bool_constant<ascending>()));
|
|
break;
|
|
case ColumnTypeOverview::bytes:
|
|
std::sort(items.begin(), items.end(), makeSortDirection(lessBytes, std::bool_constant<ascending>()));
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
void TreeView::getChildren(const Container& cont, unsigned int level, std::vector<TreeLine>& output)
|
|
{
|
|
output.clear();
|
|
output.reserve(cont.subDirs.size() + 1); //keep pointers in "workList" valid
|
|
std::vector<std::pair<uint64_t, int*>> workList;
|
|
|
|
for (const Container& subDir : cont.subDirs)
|
|
{
|
|
output.push_back({level, 0, &subDir, NodeType::folder});
|
|
workList.emplace_back(subDir.bytesGross, &output.back().percent);
|
|
}
|
|
|
|
if (cont.showFilesNode)
|
|
{
|
|
output.push_back({level, 0, &cont, NodeType::files});
|
|
workList.emplace_back(cont.bytesNet, &output.back().percent);
|
|
}
|
|
calcPercentage(workList);
|
|
|
|
if (currentSort_.ascending)
|
|
sortSingleLevel<true>(output, currentSort_.sortCol);
|
|
else
|
|
sortSingleLevel<false>(output, currentSort_.sortCol);
|
|
}
|
|
|
|
|
|
void TreeView::applySubView(std::vector<RootNodeImpl>&& newView)
|
|
{
|
|
//preserve current node expansion status
|
|
auto getContainer = [](const TreeView::TreeLine& tl) -> const ContainerObject*
|
|
{
|
|
switch (tl.type)
|
|
{
|
|
case NodeType::root:
|
|
case NodeType::folder:
|
|
return tl.node->containerRef.lock().get();
|
|
|
|
case NodeType::files:
|
|
break; //none!!!
|
|
}
|
|
return nullptr;
|
|
};
|
|
|
|
std::unordered_set<const ContainerObject*> expandedNodes;
|
|
if (!flatTree_.empty())
|
|
{
|
|
auto it = flatTree_.begin();
|
|
for (auto itNext = flatTree_.begin() + 1; itNext != flatTree_.end(); ++itNext, ++it)
|
|
if (it->level < itNext->level)
|
|
if (auto conObj = getContainer(*it))
|
|
expandedNodes.insert(conObj);
|
|
}
|
|
|
|
//update view on full data
|
|
folderCmpView_.swap(newView); //newView may be an alias for folderCmpView! see sorting!
|
|
|
|
//set default flat tree
|
|
flatTree_.clear();
|
|
|
|
if (folderCmp_.size() == 1) //single folder pair case (empty pairs were already removed!) do NOT use folderCmpView for this check!
|
|
{
|
|
if (!folderCmpView_.empty()) //possibly empty!
|
|
getChildren(folderCmpView_[0], 0, flatTree_); //do not show root
|
|
}
|
|
else
|
|
{
|
|
//following is almost identical with TreeView::getChildren():
|
|
// however we *cannot* reuse code here since "std::vector<RootNodeImpl>" is not a "Container"!
|
|
|
|
flatTree_.reserve(folderCmpView_.size()); //keep pointers in "workList" valid
|
|
std::vector<std::pair<uint64_t, int*>> workList;
|
|
|
|
for (const RootNodeImpl& root : folderCmpView_)
|
|
{
|
|
flatTree_.push_back({0, 0, &root, NodeType::root});
|
|
workList.emplace_back(root.bytesGross, &flatTree_.back().percent);
|
|
}
|
|
|
|
calcPercentage(workList);
|
|
|
|
if (currentSort_.ascending)
|
|
sortSingleLevel<true>(flatTree_, currentSort_.sortCol);
|
|
else
|
|
sortSingleLevel<false>(flatTree_, currentSort_.sortCol);
|
|
}
|
|
|
|
//restore node expansion status
|
|
for (size_t row = 0; row < flatTree_.size(); ++row) //flatTree size changes during loop!
|
|
{
|
|
const TreeLine& line = flatTree_[row];
|
|
|
|
if (auto conObj = getContainer(line))
|
|
if (expandedNodes.contains(conObj))
|
|
{
|
|
std::vector<TreeLine> newLines;
|
|
getChildren(*line.node, line.level + 1, newLines);
|
|
|
|
flatTree_.insert(flatTree_.begin() + row + 1, newLines.begin(), newLines.end());
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
template <class Predicate>
|
|
void TreeView::updateView(Predicate pred)
|
|
{
|
|
//update view on full data
|
|
std::vector<RootNodeImpl> newView;
|
|
newView.reserve(folderCmp_.size()); //avoid expensive reallocations!
|
|
|
|
for (const std::weak_ptr<BaseFolderPair>& baseObjRef : folderCmp_)
|
|
if (BaseFolderPair* baseObj = baseObjRef.lock().get())
|
|
{
|
|
newView.emplace_back();
|
|
RootNodeImpl& root = newView.back();
|
|
this->extractVisibleSubtree(*baseObj, root, pred); //"this->" is bogus for a static method, but GCC screws this one up
|
|
|
|
//warning: the following lines are almost 1:1 copy from extractVisibleSubtree, adapted for BaseFolderPair:
|
|
if (root.subDirs.empty() && !root.showFilesNode)
|
|
newView.pop_back();
|
|
else
|
|
{
|
|
root.containerRef = baseObjRef;
|
|
root.displayName = getShortDisplayNameForFolderPair(baseObj->getAbstractPath<SelectSide::left >(),
|
|
baseObj->getAbstractPath<SelectSide::right>());
|
|
|
|
this->compressNode(root); //"this->" required by two-pass lookup as enforced by GCC 4.7
|
|
}
|
|
}
|
|
|
|
lastViewFilterPred_ = pred;
|
|
applySubView(std::move(newView));
|
|
}
|
|
|
|
|
|
void TreeView::setSortDirection(ColumnTypeOverview colType, bool ascending) //apply permanently!
|
|
{
|
|
currentSort_ = SortInfo{colType, ascending};
|
|
|
|
//reapply current view
|
|
applySubView(std::move(folderCmpView_));
|
|
}
|
|
|
|
|
|
TreeView::NodeStatus TreeView::getStatus(size_t row) const
|
|
{
|
|
if (row < flatTree_.size())
|
|
{
|
|
if (row + 1 < flatTree_.size() && flatTree_[row + 1].level > flatTree_[row].level)
|
|
return NodeStatus::expanded;
|
|
|
|
//it's either reduced or empty
|
|
switch (flatTree_[row].type)
|
|
{
|
|
case NodeType::root:
|
|
case NodeType::folder:
|
|
return flatTree_[row].node->showFilesNode || !flatTree_[row].node->subDirs.empty() ? NodeStatus::reduced : NodeStatus::empty;
|
|
|
|
case NodeType::files:
|
|
return NodeStatus::empty;
|
|
}
|
|
}
|
|
return NodeStatus::empty;
|
|
}
|
|
|
|
|
|
void TreeView::expandNode(size_t row)
|
|
{
|
|
if (getStatus(row) != NodeStatus::reduced)
|
|
{
|
|
assert(false);
|
|
return;
|
|
}
|
|
|
|
if (row < flatTree_.size())
|
|
{
|
|
std::vector<TreeLine> newLines;
|
|
|
|
switch (flatTree_[row].type)
|
|
{
|
|
case NodeType::root:
|
|
case NodeType::folder:
|
|
getChildren(*flatTree_[row].node, flatTree_[row].level + 1, newLines);
|
|
break;
|
|
case NodeType::files:
|
|
break;
|
|
}
|
|
flatTree_.insert(flatTree_.begin() + row + 1, newLines.begin(), newLines.end());
|
|
}
|
|
}
|
|
|
|
|
|
void TreeView::reduceNode(size_t row)
|
|
{
|
|
if (row < flatTree_.size())
|
|
{
|
|
const unsigned int parentLevel = flatTree_[row].level;
|
|
|
|
bool done = false;
|
|
flatTree_.erase(std::remove_if(flatTree_.begin() + row + 1, flatTree_.end(),
|
|
[&](const TreeLine& line) -> bool
|
|
{
|
|
if (done)
|
|
return false;
|
|
if (line.level > parentLevel)
|
|
return true;
|
|
else
|
|
{
|
|
done = true;
|
|
return false;
|
|
}
|
|
}), flatTree_.end());
|
|
}
|
|
}
|
|
|
|
|
|
ptrdiff_t TreeView::getParent(size_t row) const
|
|
{
|
|
if (row < flatTree_.size())
|
|
{
|
|
const auto level = flatTree_[row].level;
|
|
|
|
while (row-- > 0)
|
|
if (flatTree_[row].level < level)
|
|
return row;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
|
|
void TreeView::applyDifferenceFilter(bool showExcluded,
|
|
bool leftOnlyFilesActive,
|
|
bool rightOnlyFilesActive,
|
|
bool leftNewerFilesActive,
|
|
bool rightNewerFilesActive,
|
|
bool differentFilesActive,
|
|
bool equalFilesActive,
|
|
bool conflictFilesActive)
|
|
{
|
|
updateView([showExcluded, //make sure the predicate can be stored safely!
|
|
leftOnlyFilesActive,
|
|
rightOnlyFilesActive,
|
|
leftNewerFilesActive,
|
|
rightNewerFilesActive,
|
|
differentFilesActive,
|
|
equalFilesActive,
|
|
conflictFilesActive](const FileSystemObject& fsObj) -> bool
|
|
{
|
|
if (!fsObj.isActive() && !showExcluded)
|
|
return false;
|
|
|
|
switch (fsObj.getCategory())
|
|
{
|
|
case FILE_LEFT_ONLY:
|
|
return leftOnlyFilesActive;
|
|
case FILE_RIGHT_ONLY:
|
|
return rightOnlyFilesActive;
|
|
case FILE_LEFT_NEWER:
|
|
return leftNewerFilesActive;
|
|
case FILE_RIGHT_NEWER:
|
|
return rightNewerFilesActive;
|
|
case FILE_DIFFERENT_CONTENT:
|
|
return differentFilesActive;
|
|
case FILE_EQUAL:
|
|
return equalFilesActive;
|
|
case FILE_RENAMED:
|
|
case FILE_CONFLICT:
|
|
case FILE_TIME_INVALID:
|
|
return conflictFilesActive;
|
|
}
|
|
assert(false);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
|
|
void TreeView::applyActionFilter(bool showExcluded,
|
|
bool syncCreateLeftActive,
|
|
bool syncCreateRightActive,
|
|
bool syncDeleteLeftActive,
|
|
bool syncDeleteRightActive,
|
|
bool syncDirOverwLeftActive,
|
|
bool syncDirOverwRightActive,
|
|
bool syncDirNoneActive,
|
|
bool syncEqualActive,
|
|
bool conflictFilesActive)
|
|
{
|
|
updateView([showExcluded, //make sure the predicate can be stored safely!
|
|
syncCreateLeftActive,
|
|
syncCreateRightActive,
|
|
syncDeleteLeftActive,
|
|
syncDeleteRightActive,
|
|
syncDirOverwLeftActive,
|
|
syncDirOverwRightActive,
|
|
syncDirNoneActive,
|
|
syncEqualActive,
|
|
conflictFilesActive](const FileSystemObject& fsObj) -> bool
|
|
{
|
|
if (!fsObj.isActive() && !showExcluded)
|
|
return false;
|
|
|
|
switch (fsObj.getSyncOperation())
|
|
{
|
|
case SO_CREATE_LEFT:
|
|
return syncCreateLeftActive;
|
|
case SO_CREATE_RIGHT:
|
|
return syncCreateRightActive;
|
|
case SO_DELETE_LEFT:
|
|
return syncDeleteLeftActive;
|
|
case SO_DELETE_RIGHT:
|
|
return syncDeleteRightActive;
|
|
case SO_OVERWRITE_RIGHT:
|
|
case SO_RENAME_RIGHT:
|
|
case SO_MOVE_RIGHT_FROM:
|
|
case SO_MOVE_RIGHT_TO:
|
|
return syncDirOverwRightActive;
|
|
case SO_OVERWRITE_LEFT:
|
|
case SO_RENAME_LEFT:
|
|
case SO_MOVE_LEFT_FROM:
|
|
case SO_MOVE_LEFT_TO:
|
|
return syncDirOverwLeftActive;
|
|
case SO_DO_NOTHING:
|
|
return syncDirNoneActive;
|
|
case SO_EQUAL:
|
|
return syncEqualActive;
|
|
case SO_UNRESOLVED_CONFLICT:
|
|
return conflictFilesActive;
|
|
}
|
|
assert(false);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
|
|
std::unique_ptr<TreeView::Node> TreeView::getLine(size_t row) const
|
|
{
|
|
if (row < flatTree_.size())
|
|
{
|
|
const auto level = flatTree_[row].level;
|
|
const int percent = flatTree_[row].percent;
|
|
|
|
switch (flatTree_[row].type)
|
|
{
|
|
case NodeType::root:
|
|
{
|
|
const auto& root = *static_cast<const TreeView::RootNodeImpl*>(flatTree_[row].node);
|
|
|
|
if (BaseFolderPair* baseFolder = static_cast<BaseFolderPair*>(root.containerRef.lock().get()))
|
|
return std::make_unique<TreeView::RootNode>(percent, root.bytesGross, root.itemCountGross, getStatus(row), *baseFolder, root.displayName);
|
|
}
|
|
break;
|
|
|
|
case NodeType::folder:
|
|
{
|
|
const Container& contObj = *flatTree_[row].node;
|
|
|
|
if (FolderPair* folder = static_cast<FolderPair*>(contObj.containerRef.lock().get()))
|
|
return std::make_unique<TreeView::DirNode>(percent, contObj.bytesGross, contObj.itemCountGross, level, getStatus(row), *folder);
|
|
}
|
|
break;
|
|
|
|
case NodeType::files:
|
|
{
|
|
const auto& parentFolder = *flatTree_[row].node;
|
|
|
|
if (ContainerObject* conObj = parentFolder.containerRef.lock().get())
|
|
{
|
|
std::vector<FileSystemObject*> filesAndLinks;
|
|
|
|
//lazy evaluation: recheck "lastViewFilterPred" again rather than buffer and bloat "lastViewFilterPred"
|
|
for (FileSystemObject& fsObj : conObj->files())
|
|
if (lastViewFilterPred_(fsObj))
|
|
filesAndLinks.push_back(&fsObj);
|
|
|
|
for (FileSystemObject& fsObj : conObj->symlinks())
|
|
if (lastViewFilterPred_(fsObj))
|
|
filesAndLinks.push_back(&fsObj);
|
|
|
|
return std::make_unique<TreeView::FilesNode>(percent, parentFolder.bytesNet, parentFolder.itemCountNet, level, std::move(filesAndLinks));
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
//##########################################################################################################
|
|
|
|
namespace
|
|
{
|
|
wxColor getColorForLevel(size_t level)
|
|
{
|
|
switch (level % 12)
|
|
{
|
|
case 0: return {0xcc, 0xcc, 0xff};
|
|
case 1: return {0xcc, 0xff, 0xcc};
|
|
case 2: return {0xff, 0xff, 0x99};
|
|
case 3: return {0xdd, 0xdd, 0xdd};
|
|
case 4: return {0xff, 0xcc, 0xff};
|
|
case 5: return {0x99, 0xff, 0xcc};
|
|
case 6: return {0xcc, 0xcc, 0x99};
|
|
case 7: return {0xff, 0xcc, 0xcc};
|
|
case 8: return {0xcc, 0xff, 0x99};
|
|
case 9: return {0xff, 0xff, 0xcc};
|
|
case 10: return {0xcc, 0xff, 0xff};
|
|
case 11: return {0xff, 0xcc, 0x99};
|
|
}
|
|
assert(false);
|
|
return *wxBLACK;
|
|
}
|
|
|
|
|
|
class GridDataTree : private wxEvtHandler, public GridData
|
|
{
|
|
public:
|
|
GridDataTree(Grid& grid) :
|
|
widthNodeIcon_(screenToWxsize(IconBuffer::getPixSize(IconBuffer::IconSize::small))),
|
|
widthLevelStep_(widthNodeIcon_),
|
|
widthNodeStatus_(screenToWxsize(loadImage("node_expanded").GetWidth())),
|
|
rootIcon_(loadImage("root_folder", wxsizeToScreen(widthNodeIcon_))),
|
|
grid_(grid)
|
|
{
|
|
grid.Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onKeyDown(event); });
|
|
grid.Bind(EVENT_GRID_MOUSE_LEFT_DOWN, [this](GridClickEvent& event) { onMouseLeft (event); });
|
|
grid.Bind(EVENT_GRID_MOUSE_LEFT_DOUBLE, [this](GridClickEvent& event) { onMouseLeftDouble(event); });
|
|
grid.Bind(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, [this](GridLabelClickEvent& event) { onGridLabelContext (event); });
|
|
grid.Bind(EVENT_GRID_COL_LABEL_MOUSE_LEFT, [this](GridLabelClickEvent& event) { onGridLabelLeftClick(event); });
|
|
}
|
|
|
|
void setData(FolderComparison& folderCmp)
|
|
{
|
|
const TreeView::SortInfo sortCfg = treeDataView_.ref().getSortConfig(); //preserve!
|
|
|
|
treeDataView_ = makeSharedRef<TreeView>(); //clear old data view first! avoid memory peaks!
|
|
treeDataView_ = makeSharedRef<TreeView>(folderCmp, sortCfg);
|
|
}
|
|
|
|
const TreeView& getDataView() const { return treeDataView_.ref(); }
|
|
/**/ TreeView& getDataView() { return treeDataView_.ref(); }
|
|
|
|
void setShowPercentage(bool value) { showPercentBar_ = value; grid_.Refresh(); }
|
|
bool getShowPercentage() const { return showPercentBar_; }
|
|
|
|
private:
|
|
size_t getRowCount() const override { return getDataView().rowsTotal(); }
|
|
|
|
std::wstring getToolTip(size_t row, ColumnType colType, HoverArea rowHover) override
|
|
{
|
|
switch (static_cast<ColumnTypeOverview>(colType))
|
|
{
|
|
case ColumnTypeOverview::folder:
|
|
if (std::unique_ptr<TreeView::Node> node = getDataView().getLine(row))
|
|
if (const TreeView::RootNode* root = dynamic_cast<const TreeView::RootNode*>(node.get()))
|
|
{
|
|
const std::wstring& dirLeft = AFS::getDisplayPath(root->baseFolder.getAbstractPath<SelectSide::left >());
|
|
const std::wstring& dirRight = AFS::getDisplayPath(root->baseFolder.getAbstractPath<SelectSide::right>());
|
|
if (dirLeft.empty())
|
|
return dirRight;
|
|
else if (dirRight.empty())
|
|
return dirLeft;
|
|
return dirLeft + /*L' ' + EM_DASH + */ L'\n' + dirRight;
|
|
}
|
|
break;
|
|
|
|
case ColumnTypeOverview::itemCount:
|
|
case ColumnTypeOverview::bytes:
|
|
break;
|
|
}
|
|
return std::wstring();
|
|
}
|
|
|
|
std::wstring getValue(size_t row, ColumnType colType) const override
|
|
{
|
|
if (std::unique_ptr<TreeView::Node> node = getDataView().getLine(row))
|
|
switch (static_cast<ColumnTypeOverview>(colType))
|
|
{
|
|
case ColumnTypeOverview::folder:
|
|
if (const TreeView::RootNode* root = dynamic_cast<const TreeView::RootNode*>(node.get()))
|
|
return root->displayName;
|
|
else if (const TreeView::DirNode* dir = dynamic_cast<const TreeView::DirNode*>(node.get()))
|
|
return utfTo<std::wstring>(getFolderPairName(dir->folder));
|
|
else if (dynamic_cast<const TreeView::FilesNode*>(node.get()))
|
|
return _("Files");
|
|
break;
|
|
|
|
case ColumnTypeOverview::itemCount:
|
|
return formatNumber(node->itemCount_);
|
|
|
|
case ColumnTypeOverview::bytes:
|
|
return formatFilesizeShort(node->bytes_);
|
|
}
|
|
|
|
return std::wstring();
|
|
}
|
|
|
|
void renderColumnLabel(wxDC& dc, const wxRect& rect, ColumnType colType, bool enabled, bool highlighted) override
|
|
{
|
|
const auto colTypeTree = static_cast<ColumnTypeOverview>(colType);
|
|
|
|
const wxRect rectInner = drawColumnLabelBackground(dc, rect, highlighted);
|
|
wxRect rectRemain = rectInner;
|
|
|
|
rectRemain.x += getColumnGapLeft();
|
|
rectRemain.width -= getColumnGapLeft();
|
|
drawColumnLabelText(dc, rectRemain, getColumnLabel(colType), enabled);
|
|
|
|
if (const auto [sortCol, ascending] = getDataView().getSortConfig();
|
|
colTypeTree == sortCol)
|
|
{
|
|
const wxImage sortMarker = loadImage(ascending ? "sort_ascending" : "sort_descending");
|
|
drawBitmapRtlNoMirror(dc, enabled ? sortMarker : sortMarker.ConvertToDisabled(), rectInner, wxALIGN_CENTER_HORIZONTAL);
|
|
}
|
|
}
|
|
|
|
|
|
void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected, HoverArea rowHover) override
|
|
{
|
|
if (enabled && selected)
|
|
GridData::renderRowBackgound(dc, rect, row, true /*enabled*/, true /*selected*/, rowHover);
|
|
else
|
|
; //clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); -> already the default
|
|
}
|
|
|
|
|
|
enum class HoverAreaTree
|
|
{
|
|
node,
|
|
item,
|
|
};
|
|
|
|
void renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover) override
|
|
{
|
|
wxDCTextColourChanger textColor(dc);
|
|
if (enabled && selected) //accessibility: always set *both* foreground AND background colors!
|
|
textColor.Set(*wxBLACK);
|
|
|
|
//wxRect rectTmp= drawCellBorder(dc, rect);
|
|
wxRect rectTmp = rect;
|
|
|
|
// Partitioning:
|
|
// ________________________________________________________________________________
|
|
// | space | gap | percentage bar | 2 x gap | node status | gap |icon | gap | rest |
|
|
// --------------------------------------------------------------------------------
|
|
// -> synchronize renderCell() <-> getBestSize() <-> getMouseHover()
|
|
|
|
if (static_cast<ColumnTypeOverview>(colType) == ColumnTypeOverview::folder)
|
|
{
|
|
if (std::unique_ptr<TreeView::Node> node = getDataView().getLine(row))
|
|
{
|
|
auto drawIcon = [&](wxImage icon, const wxRect& rectIcon, bool drawActive)
|
|
{
|
|
if (!drawActive)
|
|
icon = icon.ConvertToGreyscale(1.0 / 3, 1.0 / 3, 1.0 / 3); //treat all channels equally!
|
|
|
|
if (!enabled)
|
|
icon = icon.ConvertToDisabled();
|
|
|
|
drawBitmapRtlNoMirror(dc, icon, rectIcon, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
|
|
};
|
|
|
|
//consume space
|
|
rectTmp.x += static_cast<int>(node->level_) * widthLevelStep_;
|
|
rectTmp.width -= static_cast<int>(node->level_) * widthLevelStep_;
|
|
|
|
rectTmp.x += gapSize_;
|
|
rectTmp.width -= gapSize_;
|
|
|
|
if (rectTmp.width > 0)
|
|
{
|
|
//percentage bar
|
|
if (showPercentBar_)
|
|
{
|
|
wxRect areaPerc(rectTmp.x, rectTmp.y + dipToWxsize(2), percentageBarWidth_, rectTmp.height - dipToWxsize(4));
|
|
//clear background
|
|
drawFilledRectangle(dc, areaPerc, getColorPercentBackground(), getColorPercentBorder(), dipToWxsize(1));
|
|
areaPerc.Deflate(dipToWxsize(1));
|
|
|
|
//inner area
|
|
wxRect areaPercTmp = areaPerc;
|
|
areaPercTmp.width = numeric::intDivRound(areaPercTmp.width * node->percent_, 100);
|
|
clearArea(dc, areaPercTmp, getColorForLevel(node->level_));
|
|
|
|
wxDCTextColourChanger textColorPercent(dc, *wxBLACK); //accessibility: always set both foreground AND background colors!
|
|
drawCellText(dc, areaPerc, numberTo<std::wstring>(node->percent_) + L"%", wxALIGN_CENTER);
|
|
|
|
rectTmp.x += percentageBarWidth_ + 2 * gapSize_;
|
|
rectTmp.width -= percentageBarWidth_ + 2 * gapSize_;
|
|
}
|
|
if (rectTmp.width > 0)
|
|
{
|
|
//node status
|
|
const bool drawMouseHover = static_cast<HoverAreaTree>(rowHover) == HoverAreaTree::node;
|
|
switch (node->status_)
|
|
{
|
|
case TreeView::NodeStatus::expanded:
|
|
drawIcon(loadImage(drawMouseHover ? "node_expanded_hover" : "node_expanded"), rectTmp, true /*drawActive*/);
|
|
break;
|
|
case TreeView::NodeStatus::reduced:
|
|
drawIcon(loadImage(drawMouseHover ? "node_reduced_hover" : "node_reduced"), rectTmp, true /*drawActive*/);
|
|
break;
|
|
case TreeView::NodeStatus::empty:
|
|
break;
|
|
}
|
|
|
|
rectTmp.x += widthNodeStatus_ + gapSize_;
|
|
rectTmp.width -= widthNodeStatus_ + gapSize_;
|
|
if (rectTmp.width > 0)
|
|
{
|
|
wxImage nodeIcon;
|
|
bool isActive = true;
|
|
//icon
|
|
if (dynamic_cast<const TreeView::RootNode*>(node.get()))
|
|
nodeIcon = rootIcon_;
|
|
else if (auto dir = dynamic_cast<const TreeView::DirNode*>(node.get()))
|
|
{
|
|
nodeIcon = dirIcon_;
|
|
isActive = dir->folder.isActive();
|
|
}
|
|
else if (dynamic_cast<const TreeView::FilesNode*>(node.get()))
|
|
nodeIcon = fileIcon_;
|
|
|
|
drawIcon(nodeIcon, rectTmp, isActive);
|
|
|
|
if (static_cast<HoverAreaTree>(rowHover) == HoverAreaTree::item)
|
|
drawRectangleBorder(dc, rectTmp, mouseHighlightColor_, dipToWxsize(1));
|
|
|
|
rectTmp.x += widthNodeIcon_ + gapSize_;
|
|
rectTmp.width -= widthNodeIcon_ + gapSize_;
|
|
|
|
if (rectTmp.width > 0)
|
|
{
|
|
if (!isActive &&
|
|
(!enabled || !selected))
|
|
textColor.Set(wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT));
|
|
|
|
drawCellText(dc, rectTmp, getValue(row, colType), wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
int alignment = wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL;
|
|
|
|
//have file size and item count right-justified (but don't change for RTL languages)
|
|
if ((static_cast<ColumnTypeOverview>(colType) == ColumnTypeOverview::bytes ||
|
|
static_cast<ColumnTypeOverview>(colType) == ColumnTypeOverview::itemCount) && grid_.GetLayoutDirection() != wxLayout_RightToLeft)
|
|
{
|
|
rectTmp.width -= 2 * gapSize_;
|
|
alignment = wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL;
|
|
}
|
|
else //left-justified
|
|
{
|
|
rectTmp.x += 2 * gapSize_;
|
|
rectTmp.width -= 2 * gapSize_;
|
|
}
|
|
|
|
drawCellText(dc, rectTmp, getValue(row, colType), alignment);
|
|
}
|
|
}
|
|
|
|
int getBestSize(const wxReadOnlyDC& dc, size_t row, ColumnType colType) override
|
|
{
|
|
// -> synchronize renderCell() <-> getBestSize() <-> getMouseHover()
|
|
|
|
if (static_cast<ColumnTypeOverview>(colType) == ColumnTypeOverview::folder)
|
|
{
|
|
if (std::unique_ptr<TreeView::Node> node = getDataView().getLine(row))
|
|
return node->level_ * widthLevelStep_ + gapSize_ + (showPercentBar_ ? percentageBarWidth_ + 2 * gapSize_ : 0) + widthNodeStatus_ + gapSize_
|
|
+ widthNodeIcon_ + gapSize_ + dc.GetTextExtent(getValue(row, colType)).GetWidth() +
|
|
gapSize_; //additional gap from right
|
|
else
|
|
return 0;
|
|
}
|
|
else
|
|
return 2 * gapSize_ + dc.GetTextExtent(getValue(row, colType)).GetWidth() +
|
|
2 * gapSize_; //include gap from right!
|
|
}
|
|
|
|
HoverArea getMouseHover(const wxReadOnlyDC& dc, size_t row, ColumnType colType, int cellRelativePosX, int cellWidth) override
|
|
{
|
|
if (static_cast<ColumnTypeOverview>(colType) == ColumnTypeOverview::folder)
|
|
if (std::unique_ptr<TreeView::Node> node = getDataView().getLine(row))
|
|
{
|
|
const int nodeStatusXFirst = static_cast<int>(node->level_) * widthLevelStep_ + gapSize_ + (showPercentBar_ ? percentageBarWidth_ + 2 * gapSize_ : 0);
|
|
const int nodeStatusXLast = nodeStatusXFirst + widthNodeStatus_;
|
|
// -> synchronize renderCell() <-> getBestSize() <-> getMouseHover()
|
|
|
|
const int tolerance = dipToWxsize(5);
|
|
if (nodeStatusXFirst - tolerance <= cellRelativePosX && cellRelativePosX < nodeStatusXLast + tolerance)
|
|
return static_cast<HoverArea>(HoverAreaTree::node);
|
|
}
|
|
return static_cast<HoverArea>(HoverAreaTree::item);
|
|
}
|
|
|
|
std::wstring getColumnLabel(ColumnType colType) const override
|
|
{
|
|
switch (static_cast<ColumnTypeOverview>(colType))
|
|
{
|
|
case ColumnTypeOverview::folder:
|
|
return _("Folder");
|
|
case ColumnTypeOverview::itemCount:
|
|
return _("Items");
|
|
case ColumnTypeOverview::bytes:
|
|
return _("Size");
|
|
}
|
|
return std::wstring();
|
|
}
|
|
|
|
void onMouseLeft(GridClickEvent& event)
|
|
{
|
|
switch (static_cast<HoverAreaTree>(event.hoverArea_))
|
|
{
|
|
case HoverAreaTree::node:
|
|
switch (getDataView().getStatus(event.row_))
|
|
{
|
|
case TreeView::NodeStatus::expanded:
|
|
return reduceNode(event.row_);
|
|
case TreeView::NodeStatus::reduced:
|
|
return expandNode(event.row_);
|
|
case TreeView::NodeStatus::empty:
|
|
break;
|
|
}
|
|
break;
|
|
case HoverAreaTree::item:
|
|
break;
|
|
}
|
|
event.Skip();
|
|
}
|
|
|
|
void onMouseLeftDouble(GridClickEvent& event)
|
|
{
|
|
switch (getDataView().getStatus(event.row_))
|
|
{
|
|
case TreeView::NodeStatus::expanded:
|
|
return reduceNode(event.row_);
|
|
case TreeView::NodeStatus::reduced:
|
|
return expandNode(event.row_);
|
|
case TreeView::NodeStatus::empty:
|
|
break;
|
|
}
|
|
event.Skip();
|
|
}
|
|
|
|
void onKeyDown(wxKeyEvent& event)
|
|
{
|
|
int keyCode = event.GetKeyCode();
|
|
if (grid_.GetLayoutDirection() == wxLayout_RightToLeft)
|
|
{
|
|
if (keyCode == WXK_LEFT || keyCode == WXK_NUMPAD_LEFT)
|
|
keyCode = WXK_RIGHT;
|
|
else if (keyCode == WXK_RIGHT || keyCode == WXK_NUMPAD_RIGHT)
|
|
keyCode = WXK_LEFT;
|
|
}
|
|
|
|
const size_t rowCount = grid_.getRowCount();
|
|
if (rowCount == 0) return;
|
|
|
|
const size_t row = grid_.getGridCursor();
|
|
if (event.ShiftDown())
|
|
;
|
|
else if (event.ControlDown())
|
|
;
|
|
else
|
|
switch (keyCode)
|
|
{
|
|
case WXK_LEFT:
|
|
case WXK_NUMPAD_LEFT:
|
|
case WXK_NUMPAD_SUBTRACT: //https://docs.microsoft.com/en-us/previous-versions/windows/desktop/dnacc/guidelines-for-keyboard-user-interface-design#windows-shortcut-keys
|
|
switch (getDataView().getStatus(row))
|
|
{
|
|
case TreeView::NodeStatus::expanded:
|
|
return reduceNode(row);
|
|
case TreeView::NodeStatus::reduced:
|
|
case TreeView::NodeStatus::empty:
|
|
|
|
const int parentRow = getDataView().getParent(row);
|
|
if (parentRow >= 0)
|
|
grid_.setGridCursor(parentRow, GridEventPolicy::allow);
|
|
break;
|
|
}
|
|
return; //swallow event
|
|
|
|
case WXK_RIGHT:
|
|
case WXK_NUMPAD_RIGHT:
|
|
case WXK_NUMPAD_ADD:
|
|
switch (getDataView().getStatus(row))
|
|
{
|
|
case TreeView::NodeStatus::expanded:
|
|
grid_.setGridCursor(std::min(rowCount - 1, row + 1), GridEventPolicy::allow);
|
|
break;
|
|
case TreeView::NodeStatus::reduced:
|
|
return expandNode(row);
|
|
case TreeView::NodeStatus::empty:
|
|
break;
|
|
}
|
|
return; //swallow event
|
|
}
|
|
|
|
event.Skip();
|
|
}
|
|
|
|
void onGridLabelContext(GridLabelClickEvent& event)
|
|
{
|
|
ContextMenu menu;
|
|
//--------------------------------------------------------------------------------------------------------
|
|
menu.addCheckBox(_("Percentage"), [this] { setShowPercentage(!getShowPercentage()); }, getShowPercentage());
|
|
//--------------------------------------------------------------------------------------------------------
|
|
auto toggleColumn = [&](ColumnType ct)
|
|
{
|
|
auto colAttr = grid_.getColumnConfig();
|
|
|
|
Grid::ColAttributes* caFolderName = nullptr;
|
|
Grid::ColAttributes* caToggle = nullptr;
|
|
|
|
for (Grid::ColAttributes& ca : colAttr)
|
|
if (ca.type == static_cast<ColumnType>(ColumnTypeOverview::folder))
|
|
caFolderName = &ca;
|
|
else if (ca.type == ct)
|
|
caToggle = &ca;
|
|
|
|
assert(caFolderName && caFolderName->stretch > 0 && caFolderName->visible);
|
|
assert(caToggle && caToggle->stretch == 0);
|
|
|
|
if (caFolderName && caToggle)
|
|
{
|
|
caToggle->visible = !caToggle->visible;
|
|
|
|
//take width of newly visible column from stretched folder name column
|
|
caFolderName->offset -= caToggle->visible ? caToggle->offset : -caToggle->offset;
|
|
|
|
grid_.setColumnConfig(colAttr);
|
|
}
|
|
};
|
|
|
|
for (const Grid::ColAttributes& ca : grid_.getColumnConfig())
|
|
{
|
|
menu.addCheckBox(getColumnLabel(ca.type), [ct = ca.type, toggleColumn] { toggleColumn(ct); },
|
|
ca.visible, ca.type != static_cast<ColumnType>(ColumnTypeOverview::folder)); //do not allow user to hide file name column!
|
|
}
|
|
//--------------------------------------------------------------------------------------------------------
|
|
menu.addSeparator();
|
|
|
|
auto setDefaultColumns = [&]
|
|
{
|
|
setShowPercentage(overviewPanelShowPercentageDefault);
|
|
grid_.setColumnConfig(convertColAttributes(getOverviewDefaultColAttribs(), getOverviewDefaultColAttribs()));
|
|
};
|
|
menu.addItem(_("&Default"), setDefaultColumns, loadImage("reset_sicon")); //'&' -> reuse text from "default" buttons elsewhere
|
|
//--------------------------------------------------------------------------------------------------------
|
|
|
|
menu.popup(grid_, {event.mousePos_.x, grid_.getColumnLabelHeight()});
|
|
//event.Skip();
|
|
}
|
|
|
|
void onGridLabelLeftClick(GridLabelClickEvent& event)
|
|
{
|
|
const auto colTypeTree = static_cast<ColumnTypeOverview>(event.colType_);
|
|
|
|
bool sortAscending = getDefaultSortDirection(colTypeTree);
|
|
const auto [sortCol, ascending] = getDataView().getSortConfig();
|
|
if (sortCol == colTypeTree)
|
|
sortAscending = !ascending;
|
|
|
|
getDataView().setSortDirection(colTypeTree, sortAscending);
|
|
grid_.Refresh(); //just in case, but setSortDirection() should not change grid size
|
|
grid_.clearSelection(GridEventPolicy::allow);
|
|
}
|
|
|
|
void expandNode(size_t row)
|
|
{
|
|
getDataView().expandNode(row);
|
|
grid_.Refresh(); //implicitly clears selection (changed row count after expand)
|
|
grid_.setGridCursor(row, GridEventPolicy::allow);
|
|
//grid_.autoSizeColumns(); -> doesn't look as good as expected
|
|
}
|
|
|
|
void reduceNode(size_t row)
|
|
{
|
|
getDataView().reduceNode(row);
|
|
grid_.Refresh();
|
|
grid_.setGridCursor(row, GridEventPolicy::allow);
|
|
}
|
|
|
|
SharedRef<TreeView> treeDataView_ = makeSharedRef<TreeView>();
|
|
|
|
const int gapSize_ = dipToWxsize(TREE_GRID_GAP_SIZE_DIP);
|
|
const int percentageBarWidth_ = dipToWxsize(PERCENTAGE_BAR_WIDTH_DIP);
|
|
|
|
const wxImage fileIcon_ = IconBuffer::genericFileIcon(IconBuffer::IconSize::small);
|
|
const wxImage dirIcon_ = IconBuffer::genericDirIcon (IconBuffer::IconSize::small);
|
|
|
|
const int widthNodeIcon_;
|
|
const int widthLevelStep_;
|
|
const int widthNodeStatus_;
|
|
|
|
const wxImage rootIcon_;
|
|
const wxColor mouseHighlightColor_ = enhanceContrast(*wxBLUE, //primarily needed for dark mode!
|
|
wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW), 5 /*contrastRatioMin*/); //W3C recommends >= 4.5
|
|
|
|
Grid& grid_;
|
|
bool showPercentBar_ = true;
|
|
};
|
|
}
|
|
|
|
|
|
void treegrid::init(Grid& grid)
|
|
{
|
|
grid.setDataProvider(std::make_shared<GridDataTree>(grid));
|
|
grid.showRowLabel(false);
|
|
|
|
const int rowHeight = std::max(screenToWxsize(IconBuffer::getPixSize(IconBuffer::IconSize::small)) + dipToWxsize(2), //1 extra pixel on top/bottom; dearly needed on OS X!
|
|
grid.getMainWin().GetCharHeight()); //seems to already include 3 margin pixels on top/bottom (consider percentage area)
|
|
grid.setRowHeight(rowHeight);
|
|
}
|
|
|
|
|
|
void treegrid::setData(zen::Grid& grid, FolderComparison& folderCmp)
|
|
{
|
|
if (auto* prov = dynamic_cast<GridDataTree*>(grid.getDataProvider()))
|
|
return prov->setData(folderCmp);
|
|
throw std::runtime_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] treegrid was not initialized.");
|
|
}
|
|
|
|
|
|
TreeView& treegrid::getDataView(Grid& grid)
|
|
{
|
|
if (auto* prov = dynamic_cast<GridDataTree*>(grid.getDataProvider()))
|
|
return prov->getDataView();
|
|
throw std::runtime_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] treegrid was not initialized.");
|
|
}
|
|
|
|
|
|
void treegrid::setShowPercentage(Grid& grid, bool value)
|
|
{
|
|
if (auto* prov = dynamic_cast<GridDataTree*>(grid.getDataProvider()))
|
|
prov->setShowPercentage(value);
|
|
else
|
|
assert(false);
|
|
}
|
|
|
|
|
|
bool treegrid::getShowPercentage(const Grid& grid)
|
|
{
|
|
if (auto* prov = dynamic_cast<const GridDataTree*>(grid.getDataProvider()))
|
|
return prov->getShowPercentage();
|
|
assert(false);
|
|
return true;
|
|
}
|