// ************************************************************************** // * 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; 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 or... (cont.firstFile == nullptr && //single dir node... cont.subDirs.size() == 1 && // cont.subDirs[0].firstFile == nullptr && //...that is empty cont.subDirs[0].subDirs.empty())) // { cont.subDirs.clear(); cont.firstFile = nullptr; } } 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 = nullptr; 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(); compressNode(subDirView); } else cont.subDirs.pop_back(); }); } namespace { //generate nice percentage numbers which precisely sum up to 100 void calcPercentage(std::vector<std::pair<UInt64, int*>>& workList) { const UInt64 total = std::accumulate(workList.begin(), workList.end(), UInt64(), [](UInt64 sum, const std::pair<UInt64, int*>& pair) { return sum + 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; }); //find #remainingPercent items with largest absolute error remainingPercent = std::min(remainingPercent, static_cast<int>(workList.size())); if (remainingPercent > 0) { std::nth_element(workList.begin(), workList.begin() + remainingPercent - 1, workList.end(), [total](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 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 nullptr; }; 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 (folderCmp.size() == 1) //single folder pair case (empty pairs were already removed!) do NOT use folderCmpView for this check! { if (!folderCmpView.empty()) //it may really be! 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) { newView.push_back(TreeView::RootNodeImpl()); 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: //however we *cannot* reuse code here; this were only possible if we could replace "std::vector<RootNodeImpl>" by "Container"! if (root.firstFile || !root.subDirs.empty()) { root.baseMap = baseObj; compressNode(root); } else newView.pop_back(); }); 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()); } } ptrdiff_t TreeView::getParent(size_t row) const { if (row < flatTree.size()) { const size_t level = flatTree[row].level_; while (row-- > 0) if (flatTree[row].level_ < level) return row; } 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; //remove truly empty folder pairs as early as this: we want to distinguish single/multiple folder pair cases by looking at "folderCmp" vector_remove_if(folderCmp, [](const std::shared_ptr<BaseDirMapping>& baseObj) { return baseObj->getBaseDirPf<LEFT_SIDE >().empty() && baseObj->getBaseDirPf<RIGHT_SIDE>().empty(); }); } 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 nullptr; } //########################################################################################################## 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), nullptr, this); grid.Connect(EVENT_GRID_MOUSE_LEFT_DOWN, GridClickEventHandler(GridDataNavi::onMouseLeft ), nullptr, this); grid.Connect(EVENT_GRID_MOUSE_LEFT_DOUBLE, GridClickEventHandler(GridDataNavi::onMouseLeftDouble ), nullptr, this); grid.Connect(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, GridClickEventHandler(GridDataNavi::onGridLabelContext), nullptr, this ); grid.Connect(EVENT_GRID_COL_LABEL_MOUSE_LEFT, GridClickEventHandler(GridDataNavi::onGridLabelLeftClick ), nullptr, 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(size_t 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, size_t 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, size_t 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 += static_cast<int>(node->level_) * widthLevelStep; rectTmp.width -= static_cast<int>(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(numberTo<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, size_t 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 + static_cast<int>(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; } const size_t rowCount = grid_.getRowCount(); if (rowCount == 0) return; size_t row = grid_.getGridCursor().first; 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(rowCount - 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(size_t 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(size_t 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); }