// ************************************************************************** // * 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 (zenju AT gmx DOT de) - All Rights Reserved * // ************************************************************************** #include #include "tree_view.h" #include #include #include #include #include #include #include #include #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 //(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(); case SYNC_DIR_RIGHT: return fileObj.getFileSize(); case SYNC_DIR_NONE: break; } return std::max(fileObj.getFileSize(), fileObj.getFileSize()); }; 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>& workList) { const UInt64 total = std::accumulate(workList.begin(), workList.end(), UInt64(), [](UInt64 sum, const std::pair& 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& pair) { *pair.second = 0; }); return; } int remainingPercent = 100; std::for_each(workList.begin(), workList.end(), [&](std::pair& pair) { *pair.second = to(pair.first) * 100 / to(total); //round down remainingPercent -= *pair.second; }); //find #remainingPercent items with largest absolute error remainingPercent = std::min(remainingPercent, static_cast(workList.size())); if (remainingPercent > 0) { std::nth_element(workList.begin(), workList.begin() + remainingPercent - 1, workList.end(), [total](const std::pair& lhs, const std::pair& rhs) { //return std::abs(*lhs.second - to(lhs.first) * 100 / total) > std::abs(*rhs.second - to(rhs.first) * 100 / total); return (to(lhs.first) - to(rhs.first)) * 100 / to(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& pair) { ++*pair.second; }); } } } template 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(FileSystemObject::retrieve(static_cast(lhs.node_)->objId)); const auto* dirObjR = dynamic_cast(FileSystemObject::retrieve(static_cast(rhs.node_)->objId)); if (!dirObjL) //might be pathologic, but it's covered return false; else if (!dirObjR) return true; return makeSortDirection(LessFilename(), Int2Type())(dirObjL->getObjShortName(), dirObjR->getObjShortName()); } case TreeView::TYPE_FILES: break; } assert(false); return false; //:= all equal } }; template void TreeView::sortSingleLevel(std::vector& 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())); break; case COL_TYPE_NAVI_DIRECTORY: std::sort(items.begin(), items.end(), LessShortName()); break; } } void TreeView::getChildren(const Container& cont, size_t level, std::vector& output) { output.clear(); output.reserve(cont.subDirs.size() + 1); //keep pointers in "workList" valid std::vector> 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(output, sortColumn); else sortSingleLevel(output, sortColumn); } void TreeView::applySubView(std::vector&& newView) { //preserve current node expansion status auto getHierAlias = [](const TreeView::TreeLine& tl) -> const HierarchyObject* { switch (tl.type_) { case TreeView::TYPE_ROOT: return static_cast(tl.node_)->baseMap.get(); case TreeView::TYPE_DIRECTORY: if (auto dirObj = dynamic_cast(FileSystemObject::retrieve(static_cast(tl.node_)->objId))) return dirObj; break; case TreeView::TYPE_FILES: break; //none!!! } return nullptr; }; zen::hash_set expandedNodes; if (!flatTree.empty()) { auto it = flatTree.begin(); for (auto iterNext = flatTree.begin() + 1; iterNext != flatTree.end(); ++iterNext, ++it) if (it->level_ < iterNext->level_) if (auto hierObj = getHierAlias(*it)) 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> 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 newLines; getChildren(*line.node_, line.level_ + 1, newLines); flatTree.insert(flatTree.begin() + row + 1, newLines.begin(), newLines.end()); } } } template void TreeView::updateView(Predicate pred) { //update view on full data std::vector newView; newView.reserve(folderCmp.size()); //avoid expensive reallocations! std::for_each(folderCmp.begin(), folderCmp.end(), [&](const std::shared_ptr& 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" by "Container"! if (root.firstFile || !root.subDirs.empty()) { root.baseMap = baseObj; this->compressNode(root); //"this->" required by two-pass lookup as enforced by GCC 4.7 } 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 (getStatus(row) != TreeView::STATUS_REDUCED) { assert(false); return; } if (row < flatTree.size()) { std::vector 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: case FILE_DIFFERENT_METADATA: //= sub-category of equal return equalFilesActive; case FILE_CONFLICT: 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().swap(flatTree); //free mem std::vector().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& baseObj) { return baseObj->getBaseDirPf().empty() && baseObj->getBaseDirPf().empty(); }); } std::unique_ptr 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(flatTree[row].node_); return make_unique(percent, getStatus(row), root->bytesGross, *(root->baseMap)); } break; case TreeView::TYPE_DIRECTORY: { const auto* dir = static_cast(flatTree[row].node_); if (auto dirObj = dynamic_cast(FileSystemObject::retrieve(dir->objId))) return make_unique(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(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: HSV: 88, 255, 172 //const wxColor COLOR_TREE_SELECTION_GRADIENT_TO = wxColor(225, 255, 227); // HSV: 88, 255, 240 const wxColor COLOR_TREE_SELECTION_GRADIENT_FROM = getColorSelectionGradientFrom(); const wxColor COLOR_TREE_SELECTION_GRADIENT_TO = getColorSelectionGradientTo (); const int iconSizeSmall = IconBuffer::getSize(IconBuffer::SIZE_SMALL); class GridDataNavi : private wxEvtHandler, public GridData { public: GridDataNavi(Grid& grid, const std::shared_ptr& treeDataView) : treeDataView_(treeDataView), fileIcon(IconBuffer(IconBuffer::SIZE_SMALL).genericFileIcon()), dirIcon (IconBuffer(IconBuffer::SIZE_SMALL).genericDirIcon ()), rootBmp(getResourceImage(L"rootFolder").ConvertToImage().Scale(iconSizeSmall, iconSizeSmall, wxIMAGE_QUALITY_HIGH)), widthNodeIcon(iconSizeSmall), widthLevelStep(widthNodeIcon), widthNodeStatus(getResourceImage(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 node = treeDataView_->getLine(row)) switch (static_cast(colType)) { case COL_TYPE_NAVI_BYTES: return filesizeToShortString(to(node->bytes_)); case COL_TYPE_NAVI_DIRECTORY: if (const TreeView::RootNode* root = dynamic_cast(node.get())) { const wxString dirLeft = utfCvrtTo(beforeLast(root->baseMap_.getBaseDirPf(), FILE_NAME_SEPARATOR)); const wxString dirRight = utfCvrtTo(beforeLast(root->baseMap_.getBaseDirPf(), FILE_NAME_SEPARATOR)); if (dirLeft.empty()) return dirRight; else if (dirRight.empty()) return dirLeft; else return utfCvrtTo(dirLeft + L" \x2212 " + dirRight); //\x2212 = unicode minus } else if (const TreeView::DirNode* dir = dynamic_cast(node.get())) return utfCvrtTo(dir->dirObj_.getObjShortName()); else if (dynamic_cast(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_GAP_LEFT = 4; rectInside.x += COLUMN_GAP_LEFT; rectInside.width -= COLUMN_GAP_LEFT; drawColumnLabelText(dc, rectInside, getColumnLabel(colType)); if (treeDataView_) //draw sort marker { auto sortInfo = treeDataView_->getSortDirection(); if (colType == static_cast(sortInfo.first)) { const wxBitmap& marker = getResourceImage(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 gap } } } static const int GAP_SIZE = 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 | gap | percentage bar | 2 x gap | node status | gap |icon | gap | rest | // -------------------------------------------------------------------------------- // -> synchronize renderCell() <-> getBestSize() <-> onMouseLeft() if (static_cast(colType) == COL_TYPE_NAVI_DIRECTORY && treeDataView_) { if (std::unique_ptr node = treeDataView_->getLine(row)) { ////clear first secion: //clearArea(dc, wxRect(rect.GetTopLeft(), wxSize( // node->level_ * widthLevelStep + GAP_SIZE + //width // (showPercentBar ? widthPercentBar + 2 * GAP_SIZE : 0) + // // widthNodeStatus + GAP_SIZE + widthNodeIcon + GAP_SIZE, // // rect.height)), wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); //consume space rectTmp.x += static_cast(node->level_) * widthLevelStep; rectTmp.width -= static_cast(node->level_) * widthLevelStep; rectTmp.x += GAP_SIZE; rectTmp.width -= GAP_SIZE; 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); { //clear background wxDCPenChanger dummy (dc, COLOR_PERCENTAGE_BORDER); wxDCBrushChanger dummy2(dc, COLOR_PERCENTAGE_BACKGROUND); dc.DrawRectangle(areaPerc); //inner area dc.SetPen (brushCol); dc.SetBrush(brushCol); wxRect areaPercTmp = areaPerc; areaPercTmp.Deflate(1); //do not include border areaPercTmp.width = numeric::round(areaPercTmp.width * node->percent_ / 100.0); dc.DrawRectangle(areaPercTmp); } wxDCTextColourChanger dummy3(dc, *wxBLACK); //accessibility: always set both foreground AND background colors! dc.DrawLabel(numberTo(node->percent_) + L"%", areaPerc, wxALIGN_CENTER); rectTmp.x += widthPercentBar + 2 * GAP_SIZE; rectTmp.width -= widthPercentBar + 2 * GAP_SIZE; } if (rectTmp.width > 0) { //node status auto drawStatus = [&](const wchar_t* image) { const wxBitmap& bmp = getResourceImage(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)); clearArea(dc, rectStat, *wxWHITE); //accessibility: always set both foreground AND background colors! 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 + GAP_SIZE; rectTmp.width -= widthNodeStatus + GAP_SIZE; if (rectTmp.width > 0) { wxBitmap nodeIcon; bool isActive = true; //icon if (dynamic_cast(node.get())) nodeIcon = rootBmp; else if (auto dir = dynamic_cast(node.get())) { nodeIcon = dirIcon; isActive = dir->dirObj_.isActive(); } else if (dynamic_cast(node.get())) nodeIcon = fileIcon; if (isActive) drawBitmapRtlNoMirror(dc, nodeIcon, rectTmp, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, buffer); else drawBitmapRtlNoMirror(dc, wxBitmap(nodeIcon.ConvertToImage().ConvertToGreyscale(1.0 / 3, 1.0 / 3, 1.0 / 3)), //treat all channels equally! rectTmp, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, buffer); rectTmp.x += widthNodeIcon + GAP_SIZE; rectTmp.width -= widthNodeIcon + GAP_SIZE; 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(colType) == COL_TYPE_NAVI_BYTES && grid.GetLayoutDirection() != wxLayout_RightToLeft) { rectTmp.width -= 2 * GAP_SIZE; alignment = wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL; } else //left-justified { rectTmp.x += 2 * GAP_SIZE; rectTmp.width -= 2 * GAP_SIZE; } 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(colType) == COL_TYPE_NAVI_DIRECTORY && treeDataView_) { if (std::unique_ptr node = treeDataView_->getLine(row)) return node->level_ * widthLevelStep + GAP_SIZE + (showPercentBar ? widthPercentBar + 2 * GAP_SIZE : 0) + widthNodeStatus + GAP_SIZE + widthNodeIcon + GAP_SIZE + dc.GetTextExtent(getValue(row, colType)).GetWidth() + GAP_SIZE; //additional gap from right else return 0; } else return 2 * GAP_SIZE + dc.GetTextExtent(getValue(row, colType)).GetWidth() + 2 * GAP_SIZE; //include gap from right! } virtual wxString getColumnLabel(ColumnType colType) const { switch (static_cast(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(event.colType_) == COL_TYPE_NAVI_DIRECTORY) if (std::unique_ptr 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(node->level_) * widthLevelStep + GAP_SIZE + (showPercentBar ? widthPercentBar + 2 * GAP_SIZE : 0); const int xNodeStatusLast = (xNodeStatusFirst + tolerance) + widthNodeStatus + tolerance; // -> synchronize renderCell() <-> getBestSize() <-> onMouseLeft() if (xNodeStatusFirst <= absX && absX < xNodeStatusLast) clickOnNodeStatus = true; } } //-------------------------------------------------------------------------------------------------- if (clickOnNodeStatus) 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 (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: case WXK_NUMPAD_SUBTRACT: //http://msdn.microsoft.com/en-us/library/ms971323.aspx#atg_keyboardshortcuts_windows_shortcut_keys 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: case WXK_NUMPAD_ADD: 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 it = colAttr.begin(); it != colAttr.end(); ++it) if (it->type_ == ca.type_) { it->visible_ = !ca.visible_; grid_.setColumnConfig(colAttr); return; } }; const auto& colAttr = grid_.getColumnConfig(); for (auto it = colAttr.begin(); it != colAttr.end(); ++it) { const Grid::ColumnAttribute& ca = *it; menu.addCheckBox(getColumnLabel(ca.type_), [ca, toggleColumn]() { toggleColumn(ca); }, ca.visible_, ca.type_ != static_cast(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(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(); //implicitly clears selection (changed row count after expand) grid_.setGridCursor(row); //grid_.autoSizeColumns(); -> doesn't look as good as expected } void reduceNode(size_t row) { treeDataView_->reduceNode(row); grid_.Refresh(); grid_.setGridCursor(row); } std::shared_ptr treeDataView_; const wxBitmap fileIcon; const wxBitmap dirIcon; const wxBitmap rootBmp; std::unique_ptr 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& treeDataView) { grid.setDataProvider(std::make_shared(grid, treeDataView)); grid.showRowLabel(false); const int rowHeight = std::max(IconBuffer::getSize(IconBuffer::SIZE_SMALL), grid.getMainWin().GetCharHeight()) + 2; //allow 1 pixel space on top and bottom; dearly needed on OS X! grid.setRowHeight(rowHeight); } void treeview::setShowPercentage(Grid& grid, bool value) { if (auto* prov = dynamic_cast(grid.getDataProvider())) prov->setShowPercentage(value); else assert(false); } bool treeview::getShowPercentage(const Grid& grid) { if (auto* prov = dynamic_cast(grid.getDataProvider())) return prov->getShowPercentage(); assert(false); return true; } namespace { std::vector makeConsistent(const std::vector& attribs) { std::set usedTypes; std::vector 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 treeview::convertConfig(const std::vector& attribs) { const auto& attribClean = makeConsistent(attribs); std::vector output; std::transform(attribClean.begin(), attribClean.end(), std::back_inserter(output), [&](const ColumnAttributeNavi& ca) { return Grid::ColumnAttribute(static_cast(ca.type_), ca.offset_, ca.stretch_, ca.visible_); }); return output; } std::vector treeview::convertConfig(const std::vector& attribs) { std::vector output; std::transform(attribs.begin(), attribs.end(), std::back_inserter(output), [&](const Grid::ColumnAttribute& ca) { return ColumnAttributeNavi(static_cast(ca.type_), ca.offset_, ca.stretch_, ca.visible_); }); return makeConsistent(output); }