Files
2025-12-10 14:38:26 -08:00

405 lines
16 KiB
C++

// *****************************************************************************
// * 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 <memory>
#include <optional>
#include <vector>
#include <zen/stl_tools.h>
#include <wx/scrolwin.h>
//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 ------------------------------------------------
//example: wnd.Bind(EVENT_GRID_COL_LABEL_LEFT_CLICK, [this](GridClickEvent& event) { onGridLeftClick(event); });
struct GridClickEvent;
struct GridSelectEvent;
struct GridLabelClickEvent;
struct GridColumnResizeEvent;
struct GridContextMenuEvent;
wxDECLARE_EVENT(EVENT_GRID_MOUSE_LEFT_DOUBLE, GridClickEvent);
wxDECLARE_EVENT(EVENT_GRID_MOUSE_LEFT_DOWN, GridClickEvent);
wxDECLARE_EVENT(EVENT_GRID_MOUSE_RIGHT_DOWN, GridClickEvent);
wxDECLARE_EVENT(EVENT_GRID_SELECT_RANGE, 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...
wxDECLARE_EVENT(EVENT_GRID_COL_LABEL_MOUSE_LEFT, GridLabelClickEvent);
wxDECLARE_EVENT(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, GridLabelClickEvent);
wxDECLARE_EVENT(EVENT_GRID_COL_RESIZE, GridColumnResizeEvent);
//wxContextMenuEvent? => generated by wxWidgets when right mouse down/up is not handled; even OS-dependent in which case event is generated
//=> inappropriate! we know better when to show context!
wxDECLARE_EVENT(EVENT_GRID_CONTEXT_MENU, GridContextMenuEvent);
struct GridClickEvent : public wxEvent
{
GridClickEvent(wxEventType et, ptrdiff_t row, HoverArea hoverArea, const wxPoint& mousePos) :
wxEvent(0 /*winid*/, et), row_(row), hoverArea_(hoverArea), mousePos_(mousePos) {}
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
const wxPoint mousePos_; //Grid-relative coordinates
};
struct GridSelectEvent : public wxEvent
{
GridSelectEvent(size_t rowFirst, size_t rowLast, bool positive, const GridClickEvent* mouseClick) :
wxEvent(0 /*winid*/, EVENT_GRID_SELECT_RANGE), rowFirst_(rowFirst), rowLast_(rowLast), positive_(positive),
mouseClick_(mouseClick ? *mouseClick : std::optional<GridClickEvent>()) { 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<GridClickEvent> mouseClick_; //filled unless selection was performed via keyboard shortcuts
};
struct GridLabelClickEvent : public wxEvent
{
GridLabelClickEvent(wxEventType et, ColumnType colType, const wxPoint& mousePos) : wxEvent(0 /*winid*/, et), colType_(colType), mousePos_(mousePos) {}
GridLabelClickEvent* Clone() const override { return new GridLabelClickEvent(*this); }
const ColumnType colType_; //may be ColumnType::none
const wxPoint mousePos_; //Grid-relative coordinates
};
struct GridColumnResizeEvent : public wxEvent
{
GridColumnResizeEvent(int offset, ColumnType colType) : wxEvent(0 /*winid*/, EVENT_GRID_COL_RESIZE), colType_(colType), offset_(offset) {}
GridColumnResizeEvent* Clone() const override { return new GridColumnResizeEvent(*this); }
const ColumnType colType_;
const int offset_;
};
struct GridContextMenuEvent : public wxEvent
{
GridContextMenuEvent(const wxPoint& mousePos) : wxEvent(0 /*winid*/, EVENT_GRID_CONTEXT_MENU), mousePos_(mousePos) {}
GridContextMenuEvent* Clone() const override { return new GridContextMenuEvent(*this); }
const wxPoint mousePos_; //Grid-relative coordinates
};
//------------------------------------------------------------------------------------------------------------
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, HoverArea rowHover); //default implementation
virtual void renderCell (wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover);
virtual int getBestSize (const wxReadOnlyDC& dc, size_t row, ColumnType colType); //must correspond to renderCell()!
virtual HoverArea getMouseHover(const wxReadOnlyDC& dc, size_t row, ColumnType colType, int cellRelativePosX, int cellWidth) { return HoverArea::none; }
virtual std::wstring getToolTip (size_t row, ColumnType colType, HoverArea rowHover) { return std::wstring(); }
//label area:
virtual std::wstring getColumnLabel(ColumnType colType) const = 0;
virtual void renderColumnLabel(wxDC& dc, const wxRect& rect, ColumnType colType, bool enabled, bool highlighted); //default implementation
virtual std::wstring getToolTip(ColumnType colType) const { return std::wstring(); }
//optional helper routines:
static int getColumnGapLeft(); //for left-aligned text
static wxColor getColorSelectionGradientFrom();
static wxColor getColorSelectionGradientTo();
static void drawCellText(wxDC& dc, const wxRect& rect, const std::wstring_view text,
int alignment = wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, const wxSize* textExtentHint = nullptr);
static wxRect drawCellBorder(wxDC& dc, const wxRect& rect); //returns inner rectangle
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, bool enabled);
};
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 = wxASCII_STR(wxPanelNameStr));
size_t getRowCount() const;
void setRowHeight(int height);
int getRowHeight() const;
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<ColAttributes>& attr); //set column count + widths
std::vector<ColAttributes> getColumnConfig() const;
void setDataProvider(const std::shared_ptr<GridData>& dataView) { dataView_ = dataView; }
/**/ GridData* getDataProvider() { return dataView_.get(); }
const GridData* getDataProvider() const { return dataView_.get(); }
//-----------------------------------------------------------------------------
void setColumnLabelHeight(int height);
int getColumnLabelHeight() const;
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<size_t> 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 selectRange(size_t rowFirst, size_t rowLast, bool positive, GridEventPolicy rangeEventPolicy); //select [rowFirst, rowLast)
void scrollDelta(int deltaX, int deltaY); //in scroll units
wxWindow& getCornerWin ();
wxWindow& getRowLabelWin();
wxWindow& getColLabelWin();
wxWindow& getMainWin ();
const wxWindow& getMainWin() const;
struct ColumnPosInfo
{
ColumnType colType = ColumnType::none; //ColumnType::none => no column at posX!
int cellRelativePosX = 0;
int colWidth = 0;
};
ColumnPosInfo getColumnAtWinPos(int posX) const;
ptrdiff_t getRowAtWinPos(int posY) const; //return -1 for invalid position, >= rowCount if out of range
std::pair<ptrdiff_t, ptrdiff_t> getVisibleRows(const wxRect& clientRect) const; //returns range [begin, end)
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);
void makeRowVisible(size_t row);
void Refresh(bool eraseBackground = true, const wxRect* rect = nullptr) override;
bool Enable(bool enable = true) override;
//############################################################################################################
private:
void onKeyDown(wxKeyEvent& event);
void updateWindowSizes(bool updateScrollbar = true);
void selectWithCursor(ptrdiff_t row); //emits GridSelectEvent
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 resize(size_t rowCount) { selected_.resize(rowCount, false); }
size_t gridSize() const { return selected_.size(); }
std::vector<size_t> get() const
{
std::vector<size_t> result;
for (size_t row = 0; row < selected_.size(); ++row)
if (selected_[row] != 0)
result.push_back(row);
return result;
}
bool isSelected(size_t row) const { return row < selected_.size() ? selected_[row] != 0 : false; }
bool matchesRange(size_t rowFirst, size_t rowLast, bool positive)
{
if (rowFirst <= rowLast && rowLast <= selected_.size())
{
const auto rangeEnd = selected_.begin() + rowLast;
return std::find(selected_.begin() + rowFirst, rangeEnd, static_cast<unsigned char>(!positive)) == rangeEnd;
}
else
{
assert(false);
return false;
}
}
void clear() { selectRange(0, selected_.size(), false); }
void selectRange(size_t rowFirst, size_t rowLast, bool positive = true) //select [rowFirst, rowLast), trims if required!
{
assert(rowFirst <= rowLast && rowLast <= selected_.size());
if (rowFirst < rowLast)
std::fill(selected_.begin() + std::min(rowFirst, selected_.size()),
selected_.begin() + std::min(rowLast, selected_.size()), positive);
}
private:
std::vector<unsigned char> selected_; //effectively a vector<bool> 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<ColumnWidth> getColWidths() const; //
std::vector<ColumnWidth> getColWidths(int mainWinWidth) const; //evaluate stretched columns
int getColWidthsSum(int mainWinWidth) const;
std::vector<int> getColStretchedWidths(int clientWidth) const; //final width = (normalized) (stretchedWidth + offset)
std::optional<int> 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 selectRange2(size_t rowFirst, size_t rowLast, 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;
};
void moveColumn(size_t colFrom, size_t colTo);
ColumnType colToType(size_t col) const; //returns ColumnType::none on error
/* Grid window layout:
_______________________________
| CornerWin | ColLabelWin |
|_____________|_______________|
| RowLabelWin | MainWin |
| | |
|_____________|_______________| */
CornerWin* cornerWin_;
RowLabelWin* rowLabelWin_;
ColLabelWin* colLabelWin_;
MainWin* mainWin_;
ScrollBarStatus showScrollbarH_ = SB_SHOW_AUTOMATIC;
ScrollBarStatus showScrollbarV_ = SB_SHOW_AUTOMATIC;
bool drawRowLabel_ = true;
std::shared_ptr<GridData> dataView_;
Selection selection_;
bool allowColumnMove_ = true;
bool allowColumnResize_ = true;
std::vector<VisibleColumn> visibleCols_; //individual widths, type and total column count
std::vector<ColAttributes> oldColAttributes_; //visible + nonvisible columns; use for conversion in setColumnConfig()/getColumnConfig() *only*!
size_t rowCountOld_ = 0; //at the time of last Grid::Refresh()
int scrollBarHeightH_ = 0; //optional: may not be known (yet)
int scrollBarWidthV_ = 0; //
};
//------------------------------------------------------------------------------------------------------------
template <class ColAttrReal>
std::vector<ColAttrReal> makeConsistent(const std::vector<ColAttrReal>& attribs, const std::vector<ColAttrReal>& defaults)
{
std::vector<ColAttrReal> output = attribs;
append(output, defaults); //make sure each type is existing!
removeDuplicatesStable(output, [](const ColAttrReal& lhs, const ColAttrReal& rhs) { return lhs.type < rhs.type; });
return output;
}
template <class ColAttrReal>
std::vector<Grid::ColAttributes> convertColAttributes(const std::vector<ColAttrReal>& attribs, const std::vector<ColAttrReal>& defaults)
{
std::vector<Grid::ColAttributes> output;
for (const ColAttrReal& ca : makeConsistent(attribs, defaults))
output.push_back({static_cast<ColumnType>(ca.type), ca.offset, ca.stretch, ca.visible});
return output;
}
template <class ColAttrReal>
std::vector<ColAttrReal> convertColAttributes(const std::vector<Grid::ColAttributes>& attribs)
{
using ColTypeReal = decltype(ColAttrReal().type);
std::vector<ColAttrReal> output;
for (const Grid::ColAttributes& ca : attribs)
output.push_back({static_cast<ColTypeReal>(ca.type), ca.offset, ca.stretch, ca.visible});
return output;
}
}
#endif //GRID_H_834702134831734869987