summaryrefslogtreecommitdiff
path: root/wx+/grid.h
blob: 57a8ffe35391cba072b95c93aca0a4e7777a3a9c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
// *****************************************************************************
// * 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 <numeric>
#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       (wxDC& dc, size_t row, ColumnType colType); //must correspond to renderCell()!
    virtual HoverArea    getMouseHover     (wxDC& 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
bgstack15