summaryrefslogtreecommitdiff
path: root/ui/tree_view.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'ui/tree_view.cpp')
-rw-r--r--ui/tree_view.cpp1176
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);
+}
bgstack15