// ***************************************************************************** // * This file is part of the FreeFileSync project. It is distributed under * // * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * // * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * // ***************************************************************************** #ifndef GRID_H_834702134831734869987 #define GRID_H_834702134831734869987 #include #include #include #include #include #include #include //a user-friendly, extensible and high-performance grid control namespace zen { enum class ColumnType { NONE = -1 }; //user-defiend column type enum class HoverArea { NONE = -1 }; //user-defined area for mouse selections for a given row (may span multiple columns or split a single column into multiple areas) //------------------------ events ------------------------------------------------ extern const wxEventType EVENT_GRID_MOUSE_LEFT_DOUBLE; // extern const wxEventType EVENT_GRID_MOUSE_LEFT_DOWN; // extern const wxEventType EVENT_GRID_MOUSE_LEFT_UP; //generates: GridClickEvent extern const wxEventType EVENT_GRID_MOUSE_RIGHT_DOWN; // extern const wxEventType EVENT_GRID_MOUSE_RIGHT_UP; // extern const wxEventType EVENT_GRID_SELECT_RANGE; //generates: GridSelectEvent //NOTE: neither first nor second row need to match EVENT_GRID_MOUSE_LEFT_DOWN/EVENT_GRID_MOUSE_LEFT_UP: user holding SHIFT; moving out of window... extern const wxEventType EVENT_GRID_COL_LABEL_MOUSE_LEFT; //generates: GridLabelClickEvent extern const wxEventType EVENT_GRID_COL_LABEL_MOUSE_RIGHT; // extern const wxEventType EVENT_GRID_COL_RESIZE; //generates: GridColumnResizeEvent //example: wnd.Connect(EVENT_GRID_COL_LABEL_LEFT_CLICK, GridClickEventHandler(MyDlg::OnLeftClick), nullptr, this); struct GridClickEvent : public wxMouseEvent { GridClickEvent(wxEventType et, const wxMouseEvent& me, ptrdiff_t row, HoverArea hoverArea) : wxMouseEvent(me), row_(row), hoverArea_(hoverArea) { SetEventType(et); } GridClickEvent* Clone() const override { return new GridClickEvent(*this); } const ptrdiff_t row_; //-1 for invalid position, >= rowCount if out of range const HoverArea hoverArea_; //may be HoverArea::NONE }; struct GridSelectEvent : public wxCommandEvent { GridSelectEvent(size_t rowFirst, size_t rowLast, bool positive, const GridClickEvent* mouseClick) : wxCommandEvent(EVENT_GRID_SELECT_RANGE), rowFirst_(rowFirst), rowLast_(rowLast), positive_(positive), mouseClick_(mouseClick ? *mouseClick : std::optional()) { assert(rowFirst <= rowLast); } GridSelectEvent* Clone() const override { return new GridSelectEvent(*this); } const size_t rowFirst_; //selected range: [rowFirst_, rowLast_) const size_t rowLast_; const bool positive_; //"false" when clearing selection! const std::optional mouseClick_; //filled unless selection was performed via keyboard shortcuts }; struct GridLabelClickEvent : public wxMouseEvent { GridLabelClickEvent(wxEventType et, const wxMouseEvent& me, ColumnType colType) : wxMouseEvent(me), colType_(colType) { SetEventType(et); } GridLabelClickEvent* Clone() const override { return new GridLabelClickEvent(*this); } const ColumnType colType_; //may be ColumnType::NONE }; struct GridColumnResizeEvent : public wxCommandEvent { GridColumnResizeEvent(int offset, ColumnType colType) : wxCommandEvent(EVENT_GRID_COL_RESIZE), colType_(colType), offset_(offset) {} GridColumnResizeEvent* Clone() const override { return new GridColumnResizeEvent(*this); } const ColumnType colType_; const int offset_; }; using GridClickEventFunction = void (wxEvtHandler::*)(GridClickEvent&); using GridSelectEventFunction = void (wxEvtHandler::*)(GridSelectEvent&); using GridLabelClickEventFunction = void (wxEvtHandler::*)(GridLabelClickEvent&); using GridColumnResizeEventFunction = void (wxEvtHandler::*)(GridColumnResizeEvent&); #define GridClickEventHandler(func) (wxObjectEventFunction)(wxEventFunction)wxStaticCastEvent(GridClickEventFunction, &func) #define GridSelectEventHandler(func) (wxObjectEventFunction)(wxEventFunction)wxStaticCastEvent(GridSelectEventFunction, &func) #define GridLabelClickEventHandler(func) (wxObjectEventFunction)(wxEventFunction)wxStaticCastEvent(GridLabelClickEventFunction, &func) #define GridColumnResizeEventHandler(func)(wxObjectEventFunction)(wxEventFunction)wxStaticCastEvent(GridColumnResizeEventFunction, &func) //------------------------------------------------------------------------------------------------------------ class Grid; class GridData { public: virtual ~GridData() {} virtual size_t getRowCount() const = 0; //cell area: virtual std::wstring getValue(size_t row, ColumnType colType) const = 0; virtual void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected); //default implementation virtual void renderCell (wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover); virtual int getBestSize (wxDC& dc, size_t row, ColumnType colType); //must correspond to renderCell()! virtual std::wstring getToolTip (size_t row, ColumnType colType) const { return std::wstring(); } virtual HoverArea getRowMouseHover(size_t row, ColumnType colType, int cellRelativePosX, int cellWidth) { return HoverArea::NONE; } //label area: virtual std::wstring getColumnLabel(ColumnType colType) const = 0; virtual void renderColumnLabel(Grid& grid, wxDC& dc, const wxRect& rect, ColumnType colType, bool highlighted); //default implementation virtual std::wstring getToolTip(ColumnType colType) const { return std::wstring(); } static int getColumnGapLeft(); //for left-aligned text static wxColor getColorSelectionGradientFrom(); static wxColor getColorSelectionGradientTo(); //optional helper routines: static wxSize drawCellText (wxDC& dc, const wxRect& rect, const std::wstring& text, int alignment = wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); //returns text extent static wxRect drawCellBorder (wxDC& dc, const wxRect& rect); //returns inner rectangle static void drawCellBackground(wxDC& dc, const wxRect& rect, bool enabled, bool selected, const wxColor& backgroundColor); static wxRect drawColumnLabelBackground(wxDC& dc, const wxRect& rect, bool highlighted); //returns inner rectangle static void drawColumnLabelText (wxDC& dc, const wxRect& rect, const std::wstring& text); }; enum class GridEventPolicy { ALLOW, DENY }; class Grid : public wxScrolledWindow { public: Grid(wxWindow* parent, wxWindowID id = wxID_ANY, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = wxTAB_TRAVERSAL | wxNO_BORDER, const wxString& name = wxPanelNameStr); size_t getRowCount() const; void setRowHeight(int height); struct ColAttributes { ColumnType type = ColumnType::NONE; //first, client width is partitioned according to all available stretch factors, then "offset_" is added //universal model: a non-stretched column has stretch factor 0 with the "offset" becoming identical to final width! int offset = 0; int stretch = 0; //>= 0 bool visible = false; }; void setColumnConfig(const std::vector& attr); //set column count + widths std::vector getColumnConfig() const; void setDataProvider(const std::shared_ptr& dataView) { dataView_ = dataView; } /**/ GridData* getDataProvider() { return dataView_.get(); } const GridData* getDataProvider() const { return dataView_.get(); } //----------------------------------------------------------------------------- void setColumnLabelHeight(int height); void showRowLabel(bool visible); enum ScrollBarStatus { SB_SHOW_AUTOMATIC, SB_SHOW_ALWAYS, SB_SHOW_NEVER, }; //alternative until wxScrollHelper::ShowScrollbars() becomes available in wxWidgets 2.9 void showScrollBars(ScrollBarStatus horizontal, ScrollBarStatus vertical); std::vector getSelectedRows() const { return selection_.get(); } void selectRow(size_t row, GridEventPolicy rangeEventPolicy); void selectAllRows (GridEventPolicy rangeEventPolicy); //turn off range selection event when calling this function in an event handler to avoid recursion! void clearSelection(GridEventPolicy rangeEventPolicy); // void scrollDelta(int deltaX, int deltaY); //in scroll units wxWindow& getCornerWin (); wxWindow& getRowLabelWin(); wxWindow& getColLabelWin(); wxWindow& getMainWin (); const wxWindow& getMainWin() const; ptrdiff_t getRowAtPos(int posY) const; //return -1 for invalid position, >= rowCount if out of range; absolute coordinates! struct ColumnPosInfo { ColumnType colType = ColumnType::NONE; //ColumnType::NONE no column at x position! int cellRelativePosX = 0; int colWidth = 0; }; ColumnPosInfo getColumnAtPos(int posX) const; //absolute position! void refreshCell(size_t row, ColumnType colType); void enableColumnMove (bool value) { allowColumnMove_ = value; } void enableColumnResize(bool value) { allowColumnResize_ = value; } void setGridCursor(size_t row, GridEventPolicy rangeEventPolicy); //set + show + select cursor size_t getGridCursor() const; //returns row void scrollTo(size_t row); size_t getTopRow() const; void makeRowVisible(size_t row); void Refresh(bool eraseBackground = true, const wxRect* rect = nullptr) override; bool Enable(bool enable = true) override; //############################################################################################################ private: void onPaintEvent(wxPaintEvent& event); void onSizeEvent(wxSizeEvent& event) { updateWindowSizes(); event.Skip(); } void onKeyDown(wxKeyEvent& event); void updateWindowSizes(bool updateScrollbar = true); void selectWithCursor(ptrdiff_t row); //emits GridSelectEvent void redirectRowLabelEvent(wxMouseEvent& event); wxSize GetSizeAvailableForScrollTarget(const wxSize& size) override; //required since wxWidgets 2.9 if SetTargetWindow() is used int getBestColumnSize(size_t col) const; //return -1 on error void autoSizeColumns(GridEventPolicy columnResizeEventPolicy); friend class GridData; class SubWindow; class CornerWin; class RowLabelWin; class ColLabelWin; class MainWin; class Selection { public: void init(size_t rowCount) { selected_.resize(rowCount); clear(); } size_t maxSize() const { return selected_.size(); } std::vector get() const { std::vector result; for (size_t row = 0; row < selected_.size(); ++row) if (selected_[row] != 0) result.push_back(row); return result; } void selectRow(size_t row) { selectRange(row, row + 1, true); } void selectAll () { selectRange(0, selected_.size(), true); } void clear () { selectRange(0, selected_.size(), false); } bool isSelected(size_t row) const { return row < selected_.size() ? selected_[row] != 0 : false; } void selectRange(size_t rowFirst, size_t rowLast, bool positive = true) //select [rowFirst, rowLast), trims if required! { if (rowFirst <= rowLast) { numeric::clamp(rowFirst, 0, selected_.size()); numeric::clamp(rowLast, 0, selected_.size()); std::fill(selected_.begin() + rowFirst, selected_.begin() + rowLast, positive); } else assert(false); } private: std::vector selected_; //effectively a vector of size "number of rows" }; struct VisibleColumn { ColumnType type = ColumnType::NONE; int offset = 0; int stretch = 0; //>= 0 }; struct ColumnWidth { ColumnType type = ColumnType::NONE; int width = 0; }; std::vector getColWidths() const; // std::vector getColWidths(int mainWinWidth) const; //evaluate stretched columns int getColWidthsSum(int mainWinWidth) const; std::vector getColStretchedWidths(int clientWidth) const; //final width = (normalized) (stretchedWidth + offset) std::optional getColWidth(size_t col) const { const auto& widths = getColWidths(); if (col < widths.size()) return widths[col].width; return {}; } void setColumnWidth(int width, size_t col, GridEventPolicy columnResizeEventPolicy, bool notifyAsync = false); wxRect getColumnLabelArea(ColumnType colType) const; //returns empty rect if column not found //select inclusive range [rowFrom, rowTo] void selectRange(ptrdiff_t rowFrom, ptrdiff_t rowTo, bool positive, const GridClickEvent* mouseClick, GridEventPolicy rangeEventPolicy); bool isSelected(size_t row) const { return selection_.isSelected(row); } struct ColAction { bool wantResize = false; //"!wantResize" means "move" or "single click" size_t col = 0; }; std::optional clientPosToColumnAction(const wxPoint& pos) const; void moveColumn(size_t colFrom, size_t colTo); ptrdiff_t clientPosToMoveTargetColumn(const wxPoint& pos) const; //return < 0 on error ColumnType colToType(size_t col) const; //returns ColumnType::NONE on error /* Visual layout: -------------------------------- |CornerWin | ColLabelWin | |------------------------------| |RowLabelWin | MainWin | | | | -------------------------------- */ CornerWin* cornerWin_; RowLabelWin* rowLabelWin_; ColLabelWin* colLabelWin_; MainWin* mainWin_; ScrollBarStatus showScrollbarX_ = SB_SHOW_AUTOMATIC; ScrollBarStatus showScrollbarY_ = SB_SHOW_AUTOMATIC; int colLabelHeight_ = 0; bool drawRowLabel_ = true; std::shared_ptr dataView_; Selection selection_; bool allowColumnMove_ = true; bool allowColumnResize_ = true; std::vector visibleCols_; //individual widths, type and total column count std::vector oldColAttributes_; //visible + nonvisible columns; use for conversion in setColumnConfig()/getColumnConfig() *only*! size_t rowCountOld_ = 0; //at the time of last Grid::Refresh() }; //------------------------------------------------------------------------------------------------------------ template std::vector makeConsistent(const std::vector& attribs, const std::vector& defaults) { using ColTypeReal = decltype(ColAttrReal().type); std::vector output; std::set usedTypes; //remove duplicates auto appendUnique = [&](const std::vector& attr) { std::copy_if(attr.begin(), attr.end(), std::back_inserter(output), [&](const ColAttrReal& a) { return usedTypes.insert(a.type).second; }); }; appendUnique(attribs); appendUnique(defaults); //make sure each type is existing! return output; } template std::vector convertColAttributes(const std::vector& attribs, const std::vector& defaults) { std::vector output; for (const ColAttrReal& ca : makeConsistent(attribs, defaults)) output.push_back({ static_cast(ca.type), ca.offset, ca.stretch, ca.visible }); return output; } template std::vector convertColAttributes(const std::vector& attribs) { using ColTypeReal = decltype(ColAttrReal().type); std::vector output; for (const Grid::ColAttributes& ca : attribs) output.push_back({ static_cast(ca.type), ca.offset, ca.stretch, ca.visible }); return output; } } #endif //GRID_H_834702134831734869987