diff options
Diffstat (limited to 'ui/tree_view.cpp')
-rw-r--r-- | ui/tree_view.cpp | 1176 |
1 files changed, 1176 insertions, 0 deletions
diff --git a/ui/tree_view.cpp b/ui/tree_view.cpp new file mode 100644 index 00000000..cd29938e --- /dev/null +++ b/ui/tree_view.cpp @@ -0,0 +1,1176 @@ +// ************************************************************************** +// * This file is part of the FreeFileSync project. It is distributed under * +// * GNU General Public License: http://www.gnu.org/licenses/gpl.html * +// * Copyright (C) ZenJu (zhnmju123 AT gmx DOT de) - All Rights Reserved * +// ************************************************************************** + +#include <set> +#include "tree_view.h" +#include <wx/settings.h> +#include <wx/menu.h> +#include <zen/i18n.h> +#include <zen/stl_tools.h> +#include <wx+/format_unit.h> +#include <wx+/rtl.h> +#include <wx+/context_menu.h> +#include "../lib/icon_buffer.h" +#include "../lib/resources.h" + +using namespace zen; + + +template <class Function> //(const FileSystemObject&) -> bool +void TreeView::extractVisibleSubtree(HierarchyObject& hierObj, //in + TreeView::Container& cont, //out + Function pred) +{ + auto getBytes = [](const FileMapping& fileObj) -> UInt64 //MSVC screws up miserably if we put this lambda into std::for_each + { + //give accumulated bytes the semantics of a sync preview! + if (fileObj.isActive()) + switch (fileObj.getSyncDir()) + { + case SYNC_DIR_LEFT: + return fileObj.getFileSize<RIGHT_SIDE>(); + case SYNC_DIR_RIGHT: + return fileObj.getFileSize<LEFT_SIDE>(); + case SYNC_DIR_NONE: + break; + } + return std::max(fileObj.getFileSize<LEFT_SIDE>(), fileObj.getFileSize<RIGHT_SIDE>()); + }; + + + cont.firstFile = NULL; + std::for_each(hierObj.refSubFiles().begin(), hierObj.refSubFiles().end(), + [&](FileMapping& fileObj) + { + if (pred(fileObj)) + { + cont.bytesNet += getBytes(fileObj); + + if (!cont.firstFile) + cont.firstFile = fileObj.getId(); + } + }); + cont.bytesGross += cont.bytesNet; + + if (!cont.firstFile) + std::find_if(hierObj.refSubLinks().begin(), hierObj.refSubLinks().end(), + [&](SymLinkMapping& linkObj) -> bool + { + if (pred(linkObj)) + { + cont.firstFile = linkObj.getId(); + return true; + } + return false; + }); + + cont.subDirs.reserve(hierObj.refSubDirs().size()); //avoid expensive reallocations! + + std::for_each(hierObj.refSubDirs().begin(), hierObj.refSubDirs().end(), + [&cont, pred](DirMapping& subDirObj) + { + cont.subDirs.push_back(TreeView::DirNodeImpl()); // + auto& subDirView = cont.subDirs.back(); + TreeView::extractVisibleSubtree(subDirObj, subDirView, pred); + cont.bytesGross += subDirView.bytesGross; + + if (pred(subDirObj) || subDirView.firstFile || !subDirView.subDirs.empty()) + { + subDirView.objId = subDirObj.getId(); + + //------------------- small hack -------------------------------------------- + //remove single-element sub-trees (*after* inclusion check!!!) + if (subDirView.subDirs.empty() || + (subDirView.firstFile == NULL && subDirView.subDirs.size() == 1 && subDirView.subDirs[0].subDirs.empty() && subDirView.subDirs[0].firstFile == NULL)) + { + subDirView.subDirs.clear(); + subDirView.firstFile = NULL; + } + } + else + cont.subDirs.pop_back(); + }); +} + + +namespace +{ +//generate nice percentage numbers which sum up to 100 +void calcPercentage(std::vector<std::pair<UInt64, int*>>& workList) +{ + const UInt64 total = std::accumulate(workList.begin(), workList.end(), UInt64(), + [](UInt64 val, const std::pair<UInt64, int*>& pair) { return val + pair.first; }); + + if (total == 0U) //this case doesn't work with the error minimizing algorithm below + { + std::for_each(workList.begin(), workList.end(), + [&](std::pair<UInt64, int*>& pair) { *pair.second = 0; }); + return; + } + + int remainingPercent = 100; + std::for_each(workList.begin(), workList.end(), + [&](std::pair<UInt64, int*>& pair) + { + *pair.second = to<double>(pair.first) * 100 / to<double>(total); //round down + remainingPercent -= *pair.second; + }); + + //sort descending by absolute error + std::sort(workList.begin(), workList.end(), + [&](const std::pair<UInt64, int*>& lhs, const std::pair<UInt64, int*>& rhs) + { + //return std::abs(*lhs.second - to<double>(lhs.first) * 100 / total) > std::abs(*rhs.second - to<double>(rhs.first) * 100 / total); + return (to<double>(lhs.first) - to<double>(rhs.first)) * 100 / to<double>(total) > *lhs.second - *rhs.second; + }); + + //distribute remaining percent so that overall error is minimized as much as possible + remainingPercent = std::min(std::max(0, remainingPercent), static_cast<int>(workList.size())); + std::for_each(workList.begin(), workList.begin() + remainingPercent, + [&](std::pair<UInt64, int*>& pair) { ++*pair.second; }); +} +} + + +template <bool ascending> +struct TreeView::LessShortName +{ + bool operator()(const TreeLine& lhs, const TreeLine& rhs) + { + //files last (irrespective of sort direction) + if (lhs.type_ == TreeView::TYPE_FILES) + return false; + else if (rhs.type_ == TreeView::TYPE_FILES) + return true; + + if (lhs.type_ != rhs.type_) // + return lhs.type_ < rhs.type_; //shouldn't happen! Root nodes are never sorted + + switch (lhs.type_) + { + case TreeView::TYPE_ROOT: + return false; + + case TreeView::TYPE_DIRECTORY: + { + const auto* dirObjL = dynamic_cast<const DirMapping*>(FileSystemObject::retrieve(static_cast<const TreeView::DirNodeImpl*>(lhs.node_)->objId)); + const auto* dirObjR = dynamic_cast<const DirMapping*>(FileSystemObject::retrieve(static_cast<const TreeView::DirNodeImpl*>(rhs.node_)->objId)); + + if (!dirObjL) //might be pathologic, but it's covered + return false; + else if (!dirObjR) + return true; + + return makeSortDirection(LessFilename(), Int2Type<ascending>())(dirObjL->getObjShortName(), dirObjR->getObjShortName()); + } + + case TreeView::TYPE_FILES: + break; + } + assert(false); + return false; //:= all equal + } +}; + + +template <bool ascending> +void TreeView::sortSingleLevel(std::vector<TreeLine>& items, ColumnTypeNavi columnType) +{ + auto getBytes = [](const TreeLine& line) -> UInt64 + { + switch (line.type_) + { + case TreeView::TYPE_ROOT: + case TreeView::TYPE_DIRECTORY: + return line.node_->bytesGross; + case TreeView::TYPE_FILES: + return line.node_->bytesNet; + } + assert(false); + return 0U; + }; + + const auto lessBytes = [&](const TreeLine& lhs, const TreeLine& rhs) { return getBytes(lhs) < getBytes(rhs); }; + + switch (columnType) + { + case COL_TYPE_NAVI_BYTES: + std::sort(items.begin(), items.end(), makeSortDirection(lessBytes, Int2Type<ascending>())); + break; + + case COL_TYPE_NAVI_DIRECTORY: + std::sort(items.begin(), items.end(), LessShortName<ascending>()); + break; + } +} + + +void TreeView::getChildren(const Container& cont, size_t level, std::vector<TreeLine>& output) +{ + output.clear(); + output.reserve(cont.subDirs.size() + 1); //keep pointers in "workList" valid + std::vector<std::pair<UInt64, int*>> workList; + + std::for_each(cont.subDirs.begin(), cont.subDirs.end(), + [&output, level, &workList](const DirNodeImpl& subDir) + { + output.push_back(TreeView::TreeLine(level, 0, &subDir, TreeView::TYPE_DIRECTORY)); + workList.push_back(std::make_pair(subDir.bytesGross, &output.back().percent_)); + }); + + if (cont.firstFile) + { + output.push_back(TreeLine(level, 0, &cont, TreeView::TYPE_FILES)); + workList.push_back(std::make_pair(cont.bytesNet, &output.back().percent_)); + } + calcPercentage(workList); + + if (sortAscending) + sortSingleLevel<true>(output, sortColumn); + else + sortSingleLevel<false>(output, sortColumn); +} + + +void TreeView::applySubView(std::vector<RootNodeImpl>&& newView) +{ + //preserve current node expansion status + auto getHierAlias = [](const TreeView::TreeLine& tl) -> const HierarchyObject* + { + switch (tl.type_) + { + case TreeView::TYPE_ROOT: + return static_cast<const RootNodeImpl*>(tl.node_)->baseMap.get(); + + case TreeView::TYPE_DIRECTORY: + if (auto dirObj = dynamic_cast<const DirMapping*>(FileSystemObject::retrieve(static_cast<const DirNodeImpl*>(tl.node_)->objId))) + return dirObj; + break; + + case TreeView::TYPE_FILES: + break; //none!!! + } + return NULL; + }; + + zen::hash_set<const HierarchyObject*> expandedNodes; + if (!flatTree.empty()) + { + auto iter = flatTree.begin(); + for (auto iterNext = flatTree.begin() + 1; iterNext != flatTree.end(); ++iterNext, ++iter) + if (iter->level_ < iterNext->level_) + if (auto hierObj = getHierAlias(*iter)) + expandedNodes.insert(hierObj); + } + + //update view on full data + folderCmpView.swap(newView); //newView may be an alias for folderCmpView! see sorting! + + //set default flat tree + flatTree.clear(); + + if (folderCmpView.size() == 1) + getChildren(folderCmpView[0], 0, flatTree); //do not show root + else + { + std::vector<std::pair<UInt64, int*>> workList; + flatTree.reserve(folderCmpView.size()); //keep pointers in "workList" valid + + std::for_each(folderCmpView.begin(), folderCmpView.end(), + [&](const RootNodeImpl& root) + { + flatTree.push_back(TreeView::TreeLine(0, 0, &root, TreeView::TYPE_ROOT)); + workList.push_back(std::make_pair(root.bytesGross, &flatTree.back().percent_)); + }); + + calcPercentage(workList); + } + + //restore node expansion status + for (size_t row = 0; row < flatTree.size(); ++row) //flatTree size changes within loop! + { + const TreeLine& line = flatTree[row]; + + if (auto hierObj = getHierAlias(line)) + if (expandedNodes.find(hierObj) != expandedNodes.end()) + { + 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! + + std::for_each(folderCmp.begin(), folderCmp.end(), + [&](const std::shared_ptr<BaseDirMapping>& baseObj) + { + if (!baseObj->getBaseDirPf<LEFT_SIDE>().empty() || !baseObj->getBaseDirPf<RIGHT_SIDE>().empty()) + { + newView.push_back(TreeView::RootNodeImpl()); + RootNodeImpl& root = newView.back(); + root.baseMap = baseObj; + this->extractVisibleSubtree(*baseObj, root, pred); //"this->" is bogus for a static method, but GCC screws this one up + } + }); + + applySubView(std::move(newView)); +} + + +void TreeView::setSortDirection(ColumnTypeNavi colType, bool ascending) //apply permanently! +{ + sortColumn = colType; + sortAscending = ascending; + + //reapply current view + applySubView(std::move(folderCmpView)); +} + + +bool TreeView::getDefaultSortDirection(ColumnTypeNavi colType) +{ + switch (colType) + { + case COL_TYPE_NAVI_BYTES: + return false; + case COL_TYPE_NAVI_DIRECTORY: + return true; + } + assert(false); + return true; +} + + +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 TreeView::STATUS_EXPANDED; + + //it's either reduced or empty + switch (flatTree[row].type_) + { + case TreeView::TYPE_DIRECTORY: + case TreeView::TYPE_ROOT: + return flatTree[row].node_->firstFile || !flatTree[row].node_->subDirs.empty() ? TreeView::STATUS_REDUCED : TreeView::STATUS_EMPTY; + + case TreeView::TYPE_FILES: + return TreeView::STATUS_EMPTY; + } + } + return TreeView::STATUS_EMPTY; +} + + +void TreeView::expandNode(size_t row) +{ + if (row < flatTree.size()) + { + std::vector<TreeLine> newLines; + + switch (flatTree[row].type_) + { + case TreeView::TYPE_ROOT: + case TreeView::TYPE_DIRECTORY: + getChildren(*flatTree[row].node_, flatTree[row].level_ + 1, newLines); + break; + case TreeView::TYPE_FILES: + break; + } + flatTree.insert(flatTree.begin() + row + 1, newLines.begin(), newLines.end()); + } +} + + +void TreeView::reduceNode(size_t row) +{ + if (row < flatTree.size()) + { + const size_t 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()); + } +} + + +int TreeView::getParent(size_t row) const +{ + if (row < flatTree.size()) + { + const size_t level = flatTree[row].level_; + + for (; row > 0; --row) + if (flatTree[row - 1].level_ < level) + return row - 1; + } + return -1; +} + + +void TreeView::updateCmpResult(bool hideFiltered, + bool leftOnlyFilesActive, + bool rightOnlyFilesActive, + bool leftNewerFilesActive, + bool rightNewerFilesActive, + bool differentFilesActive, + bool equalFilesActive, + bool conflictFilesActive) +{ + updateView([&](const FileSystemObject& fsObj) -> bool + { + if (hideFiltered && !fsObj.isActive()) + return false; + + switch (fsObj.getCategory()) + { + case FILE_LEFT_SIDE_ONLY: + return leftOnlyFilesActive; + case FILE_RIGHT_SIDE_ONLY: + return rightOnlyFilesActive; + case FILE_LEFT_NEWER: + return leftNewerFilesActive; + case FILE_RIGHT_NEWER: + return rightNewerFilesActive; + case FILE_DIFFERENT: + return differentFilesActive; + case FILE_EQUAL: + return equalFilesActive; + case FILE_CONFLICT: + case FILE_DIFFERENT_METADATA: + return conflictFilesActive; + } + assert(false); + return true; + }); +} + + +void TreeView::updateSyncPreview(bool hideFiltered, + bool syncCreateLeftActive, + bool syncCreateRightActive, + bool syncDeleteLeftActive, + bool syncDeleteRightActive, + bool syncDirOverwLeftActive, + bool syncDirOverwRightActive, + bool syncDirNoneActive, + bool syncEqualActive, + bool conflictFilesActive) +{ + updateView([&](const FileSystemObject& fsObj) -> bool + { + if (hideFiltered && !fsObj.isActive()) + return false; + + switch (fsObj.getSyncOperation()) + { + case SO_CREATE_NEW_LEFT: + case SO_MOVE_LEFT_TARGET: + return syncCreateLeftActive; + case SO_CREATE_NEW_RIGHT: + case SO_MOVE_RIGHT_TARGET: + return syncCreateRightActive; + case SO_DELETE_LEFT: + case SO_MOVE_LEFT_SOURCE: + return syncDeleteLeftActive; + case SO_DELETE_RIGHT: + case SO_MOVE_RIGHT_SOURCE: + return syncDeleteRightActive; + case SO_OVERWRITE_RIGHT: + case SO_COPY_METADATA_TO_RIGHT: + return syncDirOverwRightActive; + case SO_OVERWRITE_LEFT: + case SO_COPY_METADATA_TO_LEFT: + return syncDirOverwLeftActive; + case SO_DO_NOTHING: + return syncDirNoneActive; + case SO_EQUAL: + return syncEqualActive; + case SO_UNRESOLVED_CONFLICT: + return conflictFilesActive; + } + assert(false); + return true; + }); +} + + +void TreeView::setData(FolderComparison& newData) +{ + std::vector<TreeLine >().swap(flatTree); //free mem + std::vector<RootNodeImpl>().swap(folderCmpView); // + folderCmp = newData; +} + + +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 TreeView::TYPE_ROOT: + { + const auto* root = static_cast<const TreeView::RootNodeImpl*>(flatTree[row].node_); + return make_unique<TreeView::RootNode>(percent, getStatus(row), root->bytesGross, *(root->baseMap)); + } + break; + + case TreeView::TYPE_DIRECTORY: + { + const auto* dir = static_cast<const TreeView::DirNodeImpl*>(flatTree[row].node_); + if (auto dirObj = dynamic_cast<DirMapping*>(FileSystemObject::retrieve(dir->objId))) + return make_unique<TreeView::DirNode>(percent, level, getStatus(row), dir->bytesGross, *dirObj); + } + break; + + case TreeView::TYPE_FILES: + { + const auto* parentDir = flatTree[row].node_; + if (auto firstFile = FileSystemObject::retrieve(parentDir->firstFile)) + return make_unique<TreeView::FilesNode>(percent, level, parentDir->bytesNet, *firstFile); + } + break; + } + } + return NULL; +} + +//########################################################################################################## + +namespace +{ +const wxColour COLOR_LEVEL0(0xcc, 0xcc, 0xff); +const wxColour COLOR_LEVEL1(0xcc, 0xff, 0xcc); +const wxColour COLOR_LEVEL2(0xff, 0xff, 0x99); + +const wxColour COLOR_LEVEL3(0xcc, 0xff, 0xff); +const wxColour COLOR_LEVEL4(0xff, 0xcc, 0xff); +const wxColour COLOR_LEVEL5(0x99, 0xff, 0xcc); + +const wxColour COLOR_LEVEL6(0xcc, 0xcc, 0x99); +const wxColour COLOR_LEVEL7(0xff, 0xcc, 0xcc); +const wxColour COLOR_LEVEL8(0xcc, 0xff, 0x99); + +const wxColour COLOR_LEVEL9 (0xff, 0xff, 0xcc); +const wxColour COLOR_LEVEL10(0xcc, 0xcc, 0xcc); +const wxColour COLOR_LEVEL11(0xff, 0xcc, 0x99); + +const wxColour COLOR_PERCENTAGE_BORDER(198, 198, 198); +const wxColour COLOR_PERCENTAGE_BACKGROUND(0xf8, 0xf8, 0xf8); + +//const wxColor COLOR_TREE_SELECTION_GRADIENT_FROM = wxColor( 89, 255, 99); //green: H:88 S:255 V:172 +//const wxColor COLOR_TREE_SELECTION_GRADIENT_TO = wxColor(225, 255, 227); // H:88 S:255 V:240 +const wxColor COLOR_TREE_SELECTION_GRADIENT_FROM = getColorSelectionGradientFrom(); +const wxColor COLOR_TREE_SELECTION_GRADIENT_TO = getColorSelectionGradientTo (); + + +class GridDataNavi : private wxEvtHandler, public GridData +{ +public: + GridDataNavi(Grid& grid, const std::shared_ptr<TreeView>& treeDataView) : treeDataView_(treeDataView), + fileIcon(IconBuffer(IconBuffer::SIZE_SMALL).genericFileIcon()), + dirIcon (IconBuffer(IconBuffer::SIZE_SMALL).genericDirIcon ()), + rootBmp(GlobalResources::getImage(L"rootFolder").ConvertToImage().Scale(fileIcon.GetWidth(), fileIcon.GetHeight(), wxIMAGE_QUALITY_HIGH)), + widthNodeIcon(fileIcon.GetWidth()), + widthLevelStep(widthNodeIcon), + widthNodeStatus(GlobalResources::getImage(L"nodeExpanded").GetWidth()), + grid_(grid), + showPercentBar(true) + { + grid.getMainWin().Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(GridDataNavi::onKeyDown), NULL, this); + grid.Connect(EVENT_GRID_MOUSE_LEFT_DOWN, GridClickEventHandler(GridDataNavi::onMouseLeft ), NULL, this); + grid.Connect(EVENT_GRID_MOUSE_LEFT_DOUBLE, GridClickEventHandler(GridDataNavi::onMouseLeftDouble ), NULL, this); + grid.Connect(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, GridClickEventHandler(GridDataNavi::onGridLabelContext), NULL, this ); + grid.Connect(EVENT_GRID_COL_LABEL_MOUSE_LEFT, GridClickEventHandler(GridDataNavi::onGridLabelLeftClick ), NULL, this ); + } + + void setShowPercentage(bool value) { showPercentBar = value; grid_.Refresh(); } + bool getShowPercentage() const { return showPercentBar; } + +private: + virtual size_t getRowCount() const { return treeDataView_ ? treeDataView_->linesTotal() : 0; } + + virtual wxString getValue(int row, ColumnType colType) const + { + if (treeDataView_) + { + if (std::unique_ptr<TreeView::Node> node = treeDataView_->getLine(row)) + switch (static_cast<ColumnTypeNavi>(colType)) + { + case COL_TYPE_NAVI_BYTES: + return filesizeToShortString(to<Int64>(node->bytes_)); + + case COL_TYPE_NAVI_DIRECTORY: + if (const TreeView::RootNode* root = dynamic_cast<const TreeView::RootNode*>(node.get())) + { + const wxString dirLeft = utf8CvrtTo<wxString>(beforeLast(root->baseMap_.getBaseDirPf<LEFT_SIDE >(), FILE_NAME_SEPARATOR)); + const wxString dirRight = utf8CvrtTo<wxString>(beforeLast(root->baseMap_.getBaseDirPf<RIGHT_SIDE>(), FILE_NAME_SEPARATOR)); + + if (dirLeft.empty()) + return dirRight; + else if (dirRight.empty()) + return dirLeft; + else + return utf8CvrtTo<wxString>(dirLeft + L" \x2212 " + dirRight); //\x2212 = unicode minus + } + else if (const TreeView::DirNode* dir = dynamic_cast<const TreeView::DirNode*>(node.get())) + return utf8CvrtTo<wxString>(dir->dirObj_.getObjShortName()); + else if (dynamic_cast<const TreeView::FilesNode*>(node.get())) + return _("Files"); + break; + } + } + return wxEmptyString; + } + + virtual void renderColumnLabel(Grid& tree, wxDC& dc, const wxRect& rect, ColumnType colType, bool highlighted) + { + wxRect rectInside = drawColumnLabelBorder(dc, rect); + drawColumnLabelBackground(dc, rectInside, highlighted); + + const int COLUMN_BORDER_LEFT = 4; + + rectInside.x += COLUMN_BORDER_LEFT; + rectInside.width -= COLUMN_BORDER_LEFT; + drawColumnLabelText(dc, rectInside, getColumnLabel(colType)); + + if (treeDataView_) //draw sort marker + { + auto sortInfo = treeDataView_->getSortDirection(); + if (colType == static_cast<ColumnType>(sortInfo.first)) + { + const wxBitmap& marker = GlobalResources::getImage(sortInfo.second ? L"sortAscending" : L"sortDescending"); + wxPoint markerBegin = rectInside.GetTopLeft() + wxPoint((rectInside.width - marker.GetWidth()) / 2, 0); + dc.DrawBitmap(marker, markerBegin, true); //respect 2-pixel border + } + } + } + + static const int CELL_BORDER = 2; + + virtual void renderRowBackgound(wxDC& dc, const wxRect& rect, int row, bool enabled, bool selected, bool hasFocus) + { + if (enabled) + { + if (selected) + dc.GradientFillLinear(rect, COLOR_TREE_SELECTION_GRADIENT_FROM, COLOR_TREE_SELECTION_GRADIENT_TO, wxEAST); + //ignore focus + else + clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + } + else + clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE)); + } + + virtual void renderCell(Grid& grid, wxDC& dc, const wxRect& rect, int row, ColumnType colType) + { + //wxRect rectTmp= drawCellBorder(dc, rect); + wxRect rectTmp = rect; + + // Partitioning: + // ___________________________________________________________________________________________ + // | space | border | percentage bar | 2 x border | node status | border |icon | border | rest | + // -------------------------------------------------------------------------------------------- + // -> synchronize renderCell() <-> getBestSize() <-> onMouseLeft() + + if (static_cast<ColumnTypeNavi>(colType) == COL_TYPE_NAVI_DIRECTORY && treeDataView_) + { + if (std::unique_ptr<TreeView::Node> node = treeDataView_->getLine(row)) + { + ////clear first secion: + //clearArea(dc, wxRect(rect.GetTopLeft(), wxSize( + // node->level_ * widthLevelStep + CELL_BORDER + //width + // (showPercentBar ? widthPercentBar + 2 * CELL_BORDER : 0) + // + // widthNodeStatus + CELL_BORDER + widthNodeIcon + CELL_BORDER, // + // rect.height)), wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + + //consume space + rectTmp.x += node->level_ * widthLevelStep; + rectTmp.width -= node->level_ * widthLevelStep; + + rectTmp.x += CELL_BORDER; + rectTmp.width -= CELL_BORDER; + + if (rectTmp.width > 0) + { + //percentage bar + if (showPercentBar) + { + const wxColour brushCol = [&]() -> wxColour + { + switch (node->level_ % 12) + { + case 0: + return COLOR_LEVEL0; + case 1: + return COLOR_LEVEL1; + case 2: + return COLOR_LEVEL2; + case 3: + return COLOR_LEVEL3; + case 4: + return COLOR_LEVEL4; + case 5: + return COLOR_LEVEL5; + case 6: + return COLOR_LEVEL6; + case 7: + return COLOR_LEVEL7; + case 8: + return COLOR_LEVEL8; + case 9: + return COLOR_LEVEL9; + case 10: + return COLOR_LEVEL10; + default: + return COLOR_LEVEL11; + } + }(); + + const wxRect areaPerc(rectTmp.x, rectTmp.y + 2, widthPercentBar, rectTmp.height - 4); + { + //background + wxDCPenChanger dummy(dc, *wxTRANSPARENT_PEN); + wxDCBrushChanger dummy2(dc, COLOR_PERCENTAGE_BACKGROUND); + dc.DrawRectangle(areaPerc); + + //inner area + dc.SetBrush(brushCol); + + wxRect areaPercTmp = areaPerc; + areaPercTmp.width -= 2; //do not include left/right border + areaPercTmp.x += 1; // + areaPercTmp.width *= node->percent_ / 100.0; + dc.DrawRectangle(areaPercTmp); + + //outer border + dc.SetPen(COLOR_PERCENTAGE_BORDER); + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.DrawRectangle(areaPerc); + } + dc.DrawLabel(toString<wxString>(node->percent_) + L"%", areaPerc, wxALIGN_CENTER); + + rectTmp.x += widthPercentBar + 2 * CELL_BORDER; + rectTmp.width -= widthPercentBar + 2 * CELL_BORDER; + } + if (rectTmp.width > 0) + { + //node status + auto drawStatus = [&](const wchar_t* image) + { + const wxBitmap& bmp = GlobalResources::getImage(image); + + wxRect rectStat(rectTmp.GetTopLeft(), wxSize(bmp.GetWidth(), bmp.GetHeight())); + rectStat.y += (rectTmp.height - rectStat.height) / 2; + + clearArea(dc, rectStat, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + drawBitmapRtlMirror(dc, bmp, rectStat, wxALIGN_CENTER, buffer); + }; + + switch (node->status_) + { + case TreeView::STATUS_EXPANDED: + drawStatus(L"nodeExpanded"); + break; + case TreeView::STATUS_REDUCED: + drawStatus(L"nodeReduced"); + break; + case TreeView::STATUS_EMPTY: + break; + } + + rectTmp.x += widthNodeStatus + CELL_BORDER; + rectTmp.width -= widthNodeStatus + CELL_BORDER; + if (rectTmp.width > 0) + { + bool isActive = true; + //icon + if (dynamic_cast<const TreeView::RootNode*>(node.get())) + drawBitmapRtlNoMirror(dc, rootBmp, rectTmp, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, buffer); + else if (auto dir = dynamic_cast<const TreeView::DirNode*>(node.get())) + { + drawIconRtlNoMirror(dc, dirIcon, rectTmp.GetTopLeft() + wxPoint(0, (rectTmp.height - dirIcon.GetHeight()) / 2), buffer); + isActive = dir->dirObj_.isActive(); + } + else if (dynamic_cast<const TreeView::FilesNode*>(node.get())) + drawIconRtlNoMirror(dc, fileIcon, rectTmp.GetTopLeft() + wxPoint(0, (rectTmp.height - fileIcon.GetHeight()) / 2), buffer); + + //convert icon to greyscale if row is not active + if (!isActive) + { + wxBitmap bmp(widthNodeIcon, rectTmp.height); + wxMemoryDC memDc(bmp); + memDc.Blit(0, 0, widthNodeIcon, rectTmp.height, &dc, rectTmp.x, rectTmp.y); //blit in + + bmp = wxBitmap(bmp.ConvertToImage().ConvertToGreyscale(1.0/3, 1.0/3, 1.0/3)); //treat all channels equally! + memDc.SelectObject(bmp); + + dc.Blit(rectTmp.x, rectTmp.y, widthNodeIcon, rectTmp.height, &memDc, 0, 0); //blit out + } + + rectTmp.x += widthNodeIcon + CELL_BORDER; + rectTmp.width -= widthNodeIcon + CELL_BORDER; + + if (rectTmp.width > 0) + drawCellText(dc, rectTmp, getValue(row, colType), grid.IsEnabled(), wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); + } + } + } + } + } + else + { + int alignment = wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL; + + //have file size right-justified (but don't change for RTL languages) + if (static_cast<ColumnTypeNavi>(colType) == COL_TYPE_NAVI_BYTES && grid.GetLayoutDirection() != wxLayout_RightToLeft) + { + rectTmp.width -= 2 * CELL_BORDER; + alignment = wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL; + } + else //left-justified + { + rectTmp.x += 2 * CELL_BORDER; + rectTmp.width -= 2 * CELL_BORDER; + } + + drawCellText(dc, rectTmp, getValue(row, colType), grid.IsEnabled(), alignment); + } + } + + virtual size_t getBestSize(wxDC& dc, int row, ColumnType colType) + { + // -> synchronize renderCell() <-> getBestSize() <-> onMouseLeft() + + if (static_cast<ColumnTypeNavi>(colType) == COL_TYPE_NAVI_DIRECTORY && treeDataView_) + { + if (std::unique_ptr<TreeView::Node> node = treeDataView_->getLine(row)) + return node->level_ * widthLevelStep + CELL_BORDER + (showPercentBar ? widthPercentBar + 2 * CELL_BORDER : 0) + widthNodeStatus + CELL_BORDER + + widthNodeIcon + CELL_BORDER + dc.GetTextExtent(getValue(row, colType)).GetWidth() + + CELL_BORDER; //additional border from right + else + return 0; + } + else + return 2 * CELL_BORDER + dc.GetTextExtent(getValue(row, colType)).GetWidth() + + 2 * CELL_BORDER; //include border from right! + } + + virtual wxString getColumnLabel(ColumnType colType) const + { + switch (static_cast<ColumnTypeNavi>(colType)) + { + case COL_TYPE_NAVI_BYTES: + return _("Size"); + case COL_TYPE_NAVI_DIRECTORY: + return _("Name"); + } + return wxEmptyString; + } + + void onMouseLeft(GridClickEvent& event) + { + if (treeDataView_) + { + bool clickOnNodeStatus = false; + if (static_cast<ColumnTypeNavi>(event.colType_) == COL_TYPE_NAVI_DIRECTORY) + if (std::unique_ptr<TreeView::Node> node = treeDataView_->getLine(event.row_)) + { + const int absX = grid_.CalcUnscrolledPosition(event.GetPosition()).x; + const wxRect cellArea = grid_.getCellArea(event.row_, event.colType_); + if (cellArea.width > 0 && cellArea.height > 0) + { + const int tolerance = 1; + const int xNodeStatusFirst = -tolerance + cellArea.x + node->level_ * widthLevelStep + CELL_BORDER + (showPercentBar ? widthPercentBar + 2 * CELL_BORDER : 0); + const int xNodeStatusLast = xNodeStatusFirst + widthNodeStatus + 2 * tolerance; + // -> synchronize renderCell() <-> getBestSize() <-> onMouseLeft() + + if (xNodeStatusFirst <= absX && absX < xNodeStatusLast) + clickOnNodeStatus = true; + } + } + //-------------------------------------------------------------------------------------------------- + + if (clickOnNodeStatus && event.row_ >= 0) + switch (treeDataView_->getStatus(event.row_)) + { + case TreeView::STATUS_EXPANDED: + return reduceNode(event.row_); + case TreeView::STATUS_REDUCED: + return expandNode(event.row_); + case TreeView::STATUS_EMPTY: + break; + } + } + event.Skip(); + } + + void onMouseLeftDouble(GridClickEvent& event) + { + if (event.row_ >= 0 && treeDataView_) + switch (treeDataView_->getStatus(event.row_)) + { + case TreeView::STATUS_EXPANDED: + return reduceNode(event.row_); + case TreeView::STATUS_REDUCED: + return expandNode(event.row_); + case TreeView::STATUS_EMPTY: + break; + } + event.Skip(); + } + + void onKeyDown(wxKeyEvent& event) + { + int keyCode = event.GetKeyCode(); + if (wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft) + { + if (keyCode == WXK_LEFT) + keyCode = WXK_RIGHT; + else if (keyCode == WXK_RIGHT) + keyCode = WXK_LEFT; + else if (keyCode == WXK_NUMPAD_LEFT) + keyCode = WXK_NUMPAD_RIGHT; + else if (keyCode == WXK_NUMPAD_RIGHT) + keyCode = WXK_NUMPAD_LEFT; + } + + int row = grid_.getGridCursor().first; + if (row < 0) + { + row = 0; + grid_.setGridCursor(0); + } + else + { + if (event.ShiftDown()) + ; + else if (event.ControlDown()) + ; + else + switch (keyCode) + { + case WXK_LEFT: + case WXK_NUMPAD_LEFT: + if (treeDataView_) + switch (treeDataView_->getStatus(row)) + { + case TreeView::STATUS_EXPANDED: + return reduceNode(row); + case TreeView::STATUS_REDUCED: + case TreeView::STATUS_EMPTY: + + const int parentRow = treeDataView_->getParent(row); + if (parentRow >= 0) + grid_.setGridCursor(parentRow); + break; + } + return; //swallow event + + case WXK_RIGHT: + case WXK_NUMPAD_RIGHT: + if (treeDataView_) + switch (treeDataView_->getStatus(row)) + { + case TreeView::STATUS_EXPANDED: + grid_.setGridCursor(std::min(static_cast<int>(grid_.getRowCount()) - 1, row + 1)); + break; + case TreeView::STATUS_REDUCED: + return expandNode(row); + case TreeView::STATUS_EMPTY: + break; + } + return; //swallow event + } + } + + event.Skip(); + } + + void onGridLabelContext(GridClickEvent& event) + { + ContextMenu menu; + + //-------------------------------------------------------------------------------------------------------- + auto toggleColumn = [&](const Grid::ColumnAttribute& ca) + { + auto colAttr = grid_.getColumnConfig(); + + for (auto iter = colAttr.begin(); iter != colAttr.end(); ++iter) + if (iter->type_ == ca.type_) + { + iter->visible_ = !ca.visible_; + grid_.setColumnConfig(colAttr); + return; + } + }; + + const auto& colAttr = grid_.getColumnConfig(); + for (auto iter = colAttr.begin(); iter != colAttr.end(); ++iter) + { + const Grid::ColumnAttribute& ca = *iter; + + menu.addCheckBox(getColumnLabel(ca.type_), [ca, toggleColumn]() { toggleColumn(ca); }, + ca.visible_, ca.type_ != static_cast<ColumnType>(COL_TYPE_NAVI_DIRECTORY)); //do not allow user to hide file name column! + } + //-------------------------------------------------------------------------------------------------------- + menu.addCheckBox(_("Percentage"), [this] { setShowPercentage(!getShowPercentage()); }, getShowPercentage()); + //-------------------------------------------------------------------------------------------------------- + menu.addSeparator(); + + auto setDefaultColumns = [&] + { + setShowPercentage(defaultValueShowPercentage); + grid_.setColumnConfig(treeview::convertConfig(getDefaultColumnAttributesNavi())); + }; + menu.addItem(_("&Default"), setDefaultColumns); //'&' -> reuse text from "default" buttons elsewhere + + menu.popup(grid_); + + event.Skip(); + } + + void onGridLabelLeftClick(GridClickEvent& event) + { + if (treeDataView_) + { + const auto colTypeNavi = static_cast<ColumnTypeNavi>(event.colType_); + bool sortAscending = TreeView::getDefaultSortDirection(colTypeNavi); + + const auto sortInfo = treeDataView_->getSortDirection(); + if (sortInfo.first == colTypeNavi) + sortAscending = !sortInfo.second; + + treeDataView_->setSortDirection(colTypeNavi, sortAscending); + grid_.clearSelection(); + grid_.Refresh(); + } + } + + void expandNode(int row) + { + treeDataView_->expandNode(row); + grid_.Refresh(); //this one clears selection (changed row count) + grid_.setGridCursor(row); + //grid_.autoSizeColumns(); -> doesn't look as good as expected + } + + void reduceNode(int row) + { + treeDataView_->reduceNode(row); + grid_.Refresh(); //this one clears selection (changed row count) + grid_.setGridCursor(row); + //grid_.autoSizeColumns(); -> doesn't look as good as expected + } + + std::shared_ptr<TreeView> treeDataView_; + const wxIcon fileIcon; + const wxIcon dirIcon; + const wxBitmap rootBmp; + std::unique_ptr<wxBitmap> buffer; //avoid costs of recreating this temporal variable + const int widthNodeIcon; + const int widthLevelStep; + const int widthNodeStatus; + static const int widthPercentBar = 60; + Grid& grid_; + bool showPercentBar; +}; +} + + +void treeview::init(Grid& grid, const std::shared_ptr<TreeView>& treeDataView) +{ + grid.setDataProvider(std::make_shared<GridDataNavi>(grid, treeDataView)); + grid.showRowLabel(false); + grid.setRowHeight(IconBuffer(IconBuffer::SIZE_SMALL).getSize() + 2); //add some space +} + + +void treeview::setShowPercentage(Grid& grid, bool value) +{ + if (auto* prov = dynamic_cast<GridDataNavi*>(grid.getDataProvider())) + prov->setShowPercentage(value); + else + assert(false); +} + + +bool treeview::getShowPercentage(const Grid& grid) +{ + if (auto* prov = dynamic_cast<const GridDataNavi*>(grid.getDataProvider())) + return prov->getShowPercentage(); + assert(false); + return true; +} + + +namespace +{ +std::vector<ColumnAttributeNavi> makeConsistent(const std::vector<ColumnAttributeNavi>& attribs) +{ + std::set<ColumnTypeNavi> usedTypes; + + std::vector<ColumnAttributeNavi> output; + //remove duplicates + std::copy_if(attribs.begin(), attribs.end(), std::back_inserter(output), + [&](const ColumnAttributeNavi& a) { return usedTypes.insert(a.type_).second; }); + + //make sure each type is existing! + const auto& defAttr = getDefaultColumnAttributesNavi(); + std::copy_if(defAttr.begin(), defAttr.end(), std::back_inserter(output), + [&](const ColumnAttributeNavi& a) { return usedTypes.insert(a.type_).second; }); + + return output; +} +} + +std::vector<Grid::ColumnAttribute> treeview::convertConfig(const std::vector<ColumnAttributeNavi>& attribs) +{ + const auto& attribClean = makeConsistent(attribs); + + std::vector<Grid::ColumnAttribute> output; + std::transform(attribClean.begin(), attribClean.end(), std::back_inserter(output), + [&](const ColumnAttributeNavi& a) { return Grid::ColumnAttribute(static_cast<ColumnType>(a.type_), a.width_, a.visible_); }); + + return output; +} + + +std::vector<ColumnAttributeNavi> treeview::convertConfig(const std::vector<Grid::ColumnAttribute>& attribs) +{ + std::vector<ColumnAttributeNavi> output; + + std::transform(attribs.begin(), attribs.end(), std::back_inserter(output), + [&](const Grid::ColumnAttribute& ca) { return ColumnAttributeNavi(static_cast<ColumnTypeNavi>(ca.type_), ca.width_, ca.visible_); }); + + return makeConsistent(output); +} |