summaryrefslogtreecommitdiff
path: root/wx+/grid.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'wx+/grid.cpp')
-rwxr-xr-x[-rw-r--r--]wx+/grid.cpp4494
1 files changed, 2220 insertions, 2274 deletions
diff --git a/wx+/grid.cpp b/wx+/grid.cpp
index 5d393f08..75abcd2f 100644..100755
--- a/wx+/grid.cpp
+++ b/wx+/grid.cpp
@@ -1,2274 +1,2220 @@
-// *****************************************************************************
-// * This file is part of the FreeFileSync project. It is distributed under *
-// * GNU General Public License: http://www.gnu.org/licenses/gpl-3.0 *
-// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
-// *****************************************************************************
-
-#include "grid.h"
-#include <cassert>
-#include <set>
-#include <chrono>
-#include <wx/settings.h>
-#include <wx/listbox.h>
-#include <wx/tooltip.h>
-#include <wx/timer.h>
-#include <wx/utils.h>
-//#include <zen/tick_count.h>
-#include <zen/string_tools.h>
-#include <zen/scope_guard.h>
-#include <zen/utf.h>
-#include <zen/format_unit.h>
-#include "dc.h"
-
-#ifdef ZEN_LINUX
- #include <gtk/gtk.h>
-#endif
-
-using namespace zen;
-
-
-wxColor Grid::getColorSelectionGradientFrom() { return { 137, 172, 255 }; } //blue: HSL: 158, 255, 196 HSV: 222, 0.46, 1
-wxColor Grid::getColorSelectionGradientTo () { return { 225, 234, 255 }; } // HSL: 158, 255, 240 HSV: 222, 0.12, 1
-
-const int GridData::COLUMN_GAP_LEFT = 4;
-
-
-void zen::clearArea(wxDC& dc, const wxRect& rect, const wxColor& col)
-{
- wxDCPenChanger dummy (dc, col);
- wxDCBrushChanger dummy2(dc, col);
- dc.DrawRectangle(rect);
-}
-
-
-namespace
-{
-//let's NOT create wxWidgets objects statically:
-//------------------------------ Grid Parameters --------------------------------
-inline wxColor getColorLabelText() { return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); }
-inline wxColor getColorGridLine() { return { 192, 192, 192 }; } //light grey
-
-inline wxColor getColorLabelGradientFrom() { return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); }
-inline wxColor getColorLabelGradientTo () { return { 200, 200, 200 }; } //light grey
-
-inline wxColor getColorLabelGradientFocusFrom() { return getColorLabelGradientFrom(); }
-inline wxColor getColorLabelGradientFocusTo () { return Grid::getColorSelectionGradientFrom(); }
-
-const double MOUSE_DRAG_ACCELERATION = 1.5; //unit: [rows / (pixel * sec)] -> same value like Explorer!
-const int DEFAULT_COL_LABEL_BORDER = 6; //top + bottom border in addition to label height
-const int COLUMN_MOVE_DELAY = 5; //unit: [pixel] (from Explorer)
-const int COLUMN_MIN_WIDTH = 40; //only honored when resizing manually!
-const int ROW_LABEL_BORDER = 3;
-const int COLUMN_RESIZE_TOLERANCE = 6; //unit [pixel]
-const int COLUMN_FILL_GAP_TOLERANCE = 10; //enlarge column to fill full width when resizing
-
-const bool fillGapAfterColumns = true; //draw rows/column label to fill full window width; may become an instance variable some time?
-}
-
-//----------------------------------------------------------------------------------------------------------------
-const wxEventType zen::EVENT_GRID_MOUSE_LEFT_DOUBLE = wxNewEventType();
-const wxEventType zen::EVENT_GRID_MOUSE_LEFT_DOWN = wxNewEventType();
-const wxEventType zen::EVENT_GRID_MOUSE_LEFT_UP = wxNewEventType();
-const wxEventType zen::EVENT_GRID_MOUSE_RIGHT_DOWN = wxNewEventType();
-const wxEventType zen::EVENT_GRID_MOUSE_RIGHT_UP = wxNewEventType();
-const wxEventType zen::EVENT_GRID_SELECT_RANGE = wxNewEventType();
-const wxEventType zen::EVENT_GRID_COL_LABEL_MOUSE_LEFT = wxNewEventType();
-const wxEventType zen::EVENT_GRID_COL_LABEL_MOUSE_RIGHT = wxNewEventType();
-const wxEventType zen::EVENT_GRID_COL_RESIZE = wxNewEventType();
-//----------------------------------------------------------------------------------------------------------------
-
-void GridData::renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected)
-{
- drawCellBackground(dc, rect, enabled, selected, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
-}
-
-
-void GridData::renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover)
-{
- wxRect rectTmp = drawCellBorder(dc, rect);
-
- rectTmp.x += COLUMN_GAP_LEFT;
- rectTmp.width -= COLUMN_GAP_LEFT;
- drawCellText(dc, rectTmp, getValue(row, colType));
-}
-
-
-int GridData::getBestSize(wxDC& dc, size_t row, ColumnType colType)
-{
- return dc.GetTextExtent(getValue(row, colType)).GetWidth() + 2 * COLUMN_GAP_LEFT + 1; //gap on left and right side + border
-}
-
-
-wxRect GridData::drawCellBorder(wxDC& dc, const wxRect& rect) //returns remaining rectangle
-{
- wxDCPenChanger dummy2(dc, getColorGridLine());
- dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight());
- dc.DrawLine(rect.GetBottomRight(), rect.GetTopRight() + wxPoint(0, -1));
-
- return wxRect(rect.GetTopLeft(), wxSize(rect.width - 1, rect.height - 1));
-}
-
-
-void GridData::drawCellBackground(wxDC& dc, const wxRect& rect, bool enabled, bool selected, const wxColor& backgroundColor)
-{
- if (enabled)
- {
- if (selected)
- dc.GradientFillLinear(rect, Grid::getColorSelectionGradientFrom(), Grid::getColorSelectionGradientTo(), wxEAST);
- else
- clearArea(dc, rect, backgroundColor);
- }
- else
- clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE));
-}
-
-
-wxSize GridData::drawCellText(wxDC& dc, const wxRect& rect, const std::wstring& text, int alignment)
-{
- /*
- performance notes (Windows):
- - wxDC::GetTextExtent() is by far the most expensive call (20x more expensive than wxDC::DrawText())
- - wxDC::DrawLabel() is inefficiently implemented; internally calls: wxDC::GetMultiLineTextExtent(), wxDC::GetTextExtent(), wxDC::DrawText()
- - wxDC::GetMultiLineTextExtent() calls wxDC::GetTextExtent()
- - wxDC::DrawText also calls wxDC::GetTextExtent()!!
- => wxDC::DrawLabel() boils down to 3(!) calls to wxDC::GetTextExtent()!!!
- - wxDC::DrawLabel results in GetTextExtent() call even for empty strings!!!
- => skip the wxDC::DrawLabel() cruft and directly call wxDC::DrawText!
- */
-
- //truncate large texts and add ellipsis
- assert(!contains(text, L"\n"));
- const wchar_t ELLIPSIS = L'\u2026'; //"..."
-
- std::wstring textTrunc = text;
- wxSize extentTrunc = dc.GetTextExtent(textTrunc);
- if (extentTrunc.GetWidth() > rect.width)
- {
- //unlike Windows 7 Explorer, we truncate UTF-16 correctly: e.g. CJK-Ideogramm encodes to TWO wchar_t: utfCvrtTo<std::wstring>("\xf0\xa4\xbd\x9c");
- size_t low = 0; //number of unicode chars!
- size_t high = unicodeLength(text); //
- if (high > 1)
- for (;;)
- {
- const size_t middle = (low + high) / 2; //=> never 0 when "high - low > 1"
- if (high - low <= 1)
- {
- if (low == 0)
- {
- textTrunc = ELLIPSIS;
- extentTrunc = dc.GetTextExtent(ELLIPSIS);
- }
- break;
- }
-
- const std::wstring& candidate = std::wstring(strBegin(text), findUnicodePos(text, middle)) + ELLIPSIS;
- const wxSize extentCand = dc.GetTextExtent(candidate); //perf: most expensive call of this routine!
-
- if (extentCand.GetWidth() <= rect.width)
- {
- low = middle;
- textTrunc = candidate;
- extentTrunc = extentCand;
- }
- else
- high = middle;
- }
- }
-
- wxPoint pt = rect.GetTopLeft();
- if (alignment & wxALIGN_RIGHT) //note: wxALIGN_LEFT == 0!
- pt.x += rect.width - extentTrunc.GetWidth();
- else if (alignment & wxALIGN_CENTER_HORIZONTAL)
- pt.x += (rect.width - extentTrunc.GetWidth()) / 2;
-
- if (alignment & wxALIGN_BOTTOM) //note: wxALIGN_TOP == 0!
- pt.y += rect.height - extentTrunc.GetHeight();
- else if (alignment & wxALIGN_CENTER_VERTICAL)
- pt.y += (rect.height - extentTrunc.GetHeight()) / 2;
-
- RecursiveDcClipper clip(dc, rect);
- dc.DrawText(textTrunc, pt);
- return extentTrunc;
-}
-
-
-void GridData::renderColumnLabel(Grid& grid, wxDC& dc, const wxRect& rect, ColumnType colType, bool highlighted)
-{
- wxRect rectTmp = drawColumnLabelBorder(dc, rect);
- drawColumnLabelBackground(dc, rectTmp, highlighted);
-
- rectTmp.x += COLUMN_GAP_LEFT;
- rectTmp.width -= COLUMN_GAP_LEFT;
- drawColumnLabelText(dc, rectTmp, getColumnLabel(colType));
-}
-
-
-wxRect GridData::drawColumnLabelBorder(wxDC& dc, const wxRect& rect) //returns remaining rectangle
-{
- //draw white line
- {
- wxDCPenChanger dummy(dc, *wxWHITE_PEN);
- dc.DrawLine(rect.GetTopLeft(), rect.GetBottomLeft());
- }
-
- //draw border (with gradient)
- {
- wxDCPenChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_3DSHADOW));
- dc.GradientFillLinear(wxRect(rect.GetTopRight(), rect.GetBottomRight()), getColorLabelGradientFrom(), dc.GetPen().GetColour(), wxSOUTH);
- dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight() + wxPoint(1, 0));
- }
-
- return wxRect(rect.x + 1, rect.y, rect.width - 2, rect.height - 1); //we really don't like wxRect::Deflate, do we?
-}
-
-
-void GridData::drawColumnLabelBackground(wxDC& dc, const wxRect& rect, bool highlighted)
-{
- if (highlighted)
- dc.GradientFillLinear(rect, getColorLabelGradientFocusFrom(), getColorLabelGradientFocusTo(), wxSOUTH);
- else //regular background gradient
- dc.GradientFillLinear(rect, getColorLabelGradientFrom(), getColorLabelGradientTo(), wxSOUTH); //clear overlapping cells
-}
-
-
-void GridData::drawColumnLabelText(wxDC& dc, const wxRect& rect, const std::wstring& text)
-{
- wxDCTextColourChanger dummy(dc, getColorLabelText()); //accessibility: always set both foreground AND background colors!
- drawCellText(dc, rect, text, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
-}
-
-//----------------------------------------------------------------------------------------------------------------
-/*
- SubWindow
- /|\
- |
- -----------------------------------
- | | | |
-CornerWin RowLabelWin ColLabelWin MainWin
-
-*/
-class Grid::SubWindow : public wxWindow
-{
-public:
- SubWindow(Grid& parent) :
- wxWindow(&parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxWANTS_CHARS | wxBORDER_NONE, wxPanelNameStr),
- parent_(parent)
- {
- Connect(wxEVT_PAINT, wxPaintEventHandler(SubWindow::onPaintEvent), nullptr, this);
- Connect(wxEVT_SIZE, wxSizeEventHandler (SubWindow::onSizeEvent), nullptr, this);
- //http://wiki.wxwidgets.org/Flicker-Free_Drawing
- Connect(wxEVT_ERASE_BACKGROUND, wxEraseEventHandler(SubWindow::onEraseBackGround), nullptr, this);
-
- //SetDoubleBuffered(true); slow as hell!
-
- SetBackgroundStyle(wxBG_STYLE_PAINT);
-
- Connect(wxEVT_SET_FOCUS, wxFocusEventHandler(SubWindow::onFocus), nullptr, this);
- Connect(wxEVT_KILL_FOCUS, wxFocusEventHandler(SubWindow::onFocus), nullptr, this);
- Connect(wxEVT_CHILD_FOCUS, wxEventHandler(SubWindow::onChildFocus), nullptr, this);
-
- Connect(wxEVT_LEFT_DOWN, wxMouseEventHandler(SubWindow::onMouseLeftDown ), nullptr, this);
- Connect(wxEVT_LEFT_UP, wxMouseEventHandler(SubWindow::onMouseLeftUp ), nullptr, this);
- Connect(wxEVT_LEFT_DCLICK, wxMouseEventHandler(SubWindow::onMouseLeftDouble), nullptr, this);
- Connect(wxEVT_RIGHT_DOWN, wxMouseEventHandler(SubWindow::onMouseRightDown ), nullptr, this);
- Connect(wxEVT_RIGHT_UP, wxMouseEventHandler(SubWindow::onMouseRightUp ), nullptr, this);
- Connect(wxEVT_MOTION, wxMouseEventHandler(SubWindow::onMouseMovement ), nullptr, this);
- Connect(wxEVT_LEAVE_WINDOW, wxMouseEventHandler(SubWindow::onLeaveWindow ), nullptr, this);
- Connect(wxEVT_MOUSEWHEEL, wxMouseEventHandler(SubWindow::onMouseWheel ), nullptr, this);
- Connect(wxEVT_MOUSE_CAPTURE_LOST, wxMouseCaptureLostEventHandler(SubWindow::onMouseCaptureLost), nullptr, this);
-
- Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(SubWindow::onKeyDown), nullptr, this);
-
- assert(GetClientAreaOrigin() == wxPoint()); //generally assumed when dealing with coordinates below
- }
- Grid& refParent() { return parent_; }
- const Grid& refParent() const { return parent_; }
-
- template <class T>
- bool sendEventNow(T&& event) //take both "rvalue + lvalues", return "true" if a suitable event handler function was found and executed, and the function did not call wxEvent::Skip.
- {
- if (wxEvtHandler* evtHandler = parent_.GetEventHandler())
- return evtHandler->ProcessEvent(event);
- return false;
- }
-
-protected:
- void setToolTip(const std::wstring& text) //proper fix for wxWindow
- {
- wxToolTip* tt = GetToolTip();
-
- const wxString oldText = tt ? tt->GetTip() : wxString();
- if (text != oldText)
- {
- if (text.empty())
- SetToolTip(nullptr); //wxGTK doesn't allow wxToolTip with empty text!
- else
- {
- //wxWidgets bug: tooltip multiline property is defined by first tooltip text containing newlines or not (same is true for maximum width)
- if (!tt)
- SetToolTip(new wxToolTip(L"a b\n\
- a b")); //ugly, but working (on Windows)
- tt = GetToolTip(); //should be bound by now
- assert(tt);
- if (tt)
- tt->SetTip(text);
- }
- }
- }
-
-private:
- virtual void render(wxDC& dc, const wxRect& rect) = 0;
-
- virtual void onFocus(wxFocusEvent& event) { event.Skip(); }
- virtual void onChildFocus(wxEvent& event) {} //wxGTK::wxScrolledWindow automatically scrolls to child window when child gets focus -> prevent!
-
- virtual void onMouseLeftDown (wxMouseEvent& event) { event.Skip(); }
- virtual void onMouseLeftUp (wxMouseEvent& event) { event.Skip(); }
- virtual void onMouseLeftDouble(wxMouseEvent& event) { event.Skip(); }
- virtual void onMouseRightDown (wxMouseEvent& event) { event.Skip(); }
- virtual void onMouseRightUp (wxMouseEvent& event) { event.Skip(); }
- virtual void onMouseMovement (wxMouseEvent& event) { event.Skip(); }
- virtual void onLeaveWindow (wxMouseEvent& event) { event.Skip(); }
- virtual void onMouseCaptureLost(wxMouseCaptureLostEvent& event) { event.Skip(); }
-
- void onKeyDown(wxKeyEvent& event)
- {
- if (!sendEventNow(event)) //let parent collect all key events
- event.Skip();
- }
-
- void onMouseWheel(wxMouseEvent& event)
- {
- /*
- MSDN, WM_MOUSEWHEEL: "Sent to the focus window when the mouse wheel is rotated.
- The DefWindowProc function propagates the message to the window's parent.
- There should be no internal forwarding of the message, since DefWindowProc propagates
- it up the parent chain until it finds a window that processes it."
-
- On OS X there is no such propagation! => we need a redirection (the same wxGrid implements)
- */
-
- //new wxWidgets 3.0 screw-up for GTK2: wxScrollHelperEvtHandler::ProcessEvent() ignores wxEVT_MOUSEWHEEL events
- //thereby breaking the scenario of redirection to parent we need here (but also breaking their very own wxGrid sample)
- //=> call wxScrolledWindow mouse wheel handler directly
- parent_.HandleOnMouseWheel(event);
-
- //if (!sendEventNow(event))
- // event.Skip();
- }
-
- void onPaintEvent(wxPaintEvent& event)
- {
-#ifndef NDEBUG
-#ifdef ZEN_WIN
- if (runningPaintEvent_ == true) //looks like showing the assert window here would quit the debug session
- __debugbreak();
-#else
- assert(runningPaintEvent_ == false); //catch unexpected recursion, e.g.: getIconByIndex() seems to run a message loop in rare cases!
-#endif
- runningPaintEvent_ = true;
- ZEN_ON_SCOPE_EXIT(runningPaintEvent_ = false);
-#endif
- //wxAutoBufferedPaintDC dc(this); -> this one happily fucks up for RTL layout by not drawing the first column (x = 0)!
- BufferedPaintDC dc(*this, doubleBuffer_);
-
- assert(GetSize() == GetClientSize());
-
- const wxRegion& updateReg = GetUpdateRegion();
- for (wxRegionIterator it = updateReg; it; ++it)
- render(dc, it.GetRect());
- }
-
- void onSizeEvent(wxSizeEvent& event)
- {
- Refresh();
- event.Skip();
- }
-
- void onEraseBackGround(wxEraseEvent& event) {}
-
- Grid& parent_;
- Opt<wxBitmap> doubleBuffer_;
-#ifndef NDEBUG
- bool runningPaintEvent_ = false;
-#endif
-};
-
-//----------------------------------------------------------------------------------------------------------------
-//----------------------------------------------------------------------------------------------------------------
-
-class Grid::CornerWin : public SubWindow
-{
-public:
- CornerWin(Grid& parent) : SubWindow(parent) {}
-
-private:
- bool AcceptsFocus() const override { return false; }
-
- void render(wxDC& dc, const wxRect& rect) override
- {
- const wxRect& clientRect = GetClientRect();
-
- dc.GradientFillLinear(clientRect, getColorLabelGradientFrom(), getColorLabelGradientTo(), wxSOUTH);
-
- dc.SetPen(wxSystemSettings::GetColour(wxSYS_COLOUR_3DSHADOW));
-
- {
- wxDCPenChanger dummy(dc, getColorLabelGradientFrom());
- dc.DrawLine(clientRect.GetTopLeft(), clientRect.GetTopRight());
- }
-
- dc.GradientFillLinear(wxRect(clientRect.GetBottomLeft (), clientRect.GetTopLeft ()), getColorLabelGradientFrom(), dc.GetPen().GetColour(), wxSOUTH);
- dc.GradientFillLinear(wxRect(clientRect.GetBottomRight(), clientRect.GetTopRight()), getColorLabelGradientFrom(), dc.GetPen().GetColour(), wxSOUTH);
-
- dc.DrawLine(clientRect.GetBottomLeft(), clientRect.GetBottomRight());
-
- wxRect rectShrinked = clientRect;
- rectShrinked.Deflate(1);
- dc.SetPen(*wxWHITE_PEN);
-
- //dc.DrawLine(clientRect.GetTopLeft(), clientRect.GetTopRight() + wxPoint(1, 0));
- dc.DrawLine(rectShrinked.GetTopLeft(), rectShrinked.GetBottomLeft() + wxPoint(0, 1));
- }
-};
-
-//----------------------------------------------------------------------------------------------------------------
-//----------------------------------------------------------------------------------------------------------------
-
-class Grid::RowLabelWin : public SubWindow
-{
-public:
- RowLabelWin(Grid& parent) :
- SubWindow(parent),
- rowHeight_(parent.GetCharHeight() + 2 + 1) {} //default height; don't call any functions on "parent" other than those from wxWindow during construction!
- //2 for some more space, 1 for bottom border (gives 15 + 2 + 1 on Windows, 17 + 2 + 1 on Ubuntu)
-
- int getBestWidth(ptrdiff_t rowFrom, ptrdiff_t rowTo)
- {
- wxClientDC dc(this);
-
- wxFont labelFont = GetFont();
- //labelFont.SetWeight(wxFONTWEIGHT_BOLD);
- dc.SetFont(labelFont); //harmonize with RowLabelWin::render()!
-
- int bestWidth = 0;
- for (ptrdiff_t i = rowFrom; i <= rowTo; ++i)
- bestWidth = std::max(bestWidth, dc.GetTextExtent(formatRow(i)).GetWidth() + 2 * ROW_LABEL_BORDER);
- return bestWidth;
- }
-
- size_t getLogicalHeight() const { return refParent().getRowCount() * rowHeight_; }
-
- ptrdiff_t getRowAtPos(ptrdiff_t posY) const //returns < 0 on invalid input, else row number within: [0, rowCount]; rowCount if out of range
- {
- if (posY >= 0 && rowHeight_ > 0)
- {
- const size_t row = posY / rowHeight_;
- return std::min(row, refParent().getRowCount());
- }
- return -1;
- }
-
- int getRowHeight() const { return rowHeight_; } //guarantees to return size >= 1 !
- void setRowHeight(int height) { assert(height > 0); rowHeight_ = std::max(1, height); }
-
- wxRect getRowLabelArea(size_t row) const //returns empty rect if row not found
- {
- assert(GetClientAreaOrigin() == wxPoint());
- if (row < refParent().getRowCount())
- return wxRect(wxPoint(0, rowHeight_ * row),
- wxSize(GetClientSize().GetWidth(), rowHeight_));
- return wxRect();
- }
-
- std::pair<ptrdiff_t, ptrdiff_t> getRowsOnClient(const wxRect& clientRect) const //returns range [begin, end)
- {
- const int yFrom = refParent().CalcUnscrolledPosition(clientRect.GetTopLeft ()).y;
- const int yTo = refParent().CalcUnscrolledPosition(clientRect.GetBottomRight()).y;
-
- return std::make_pair(std::max(yFrom / rowHeight_, 0),
- std::min<ptrdiff_t>((yTo / rowHeight_) + 1, refParent().getRowCount()));
- }
-
-private:
- static std::wstring formatRow(size_t row) { return toGuiString(row + 1); } //convert number to std::wstring including thousands separator
-
- bool AcceptsFocus() const override { return false; }
-
- void render(wxDC& dc, const wxRect& rect) override
- {
- /*
- IsEnabled() vs IsThisEnabled() since wxWidgets 2.9.5:
-
- void wxWindowBase::NotifyWindowOnEnableChange(), called from bool wxWindowBase::Enable(), fails to refresh
- child elements when disabling a IsTopLevel() dialog, e.g. when showing a modal dialog.
- The unfortunate effect on XP for using IsEnabled() when rendering the grid is that the user can move the modal dialog
- and *draw* with it on the background while the grid refreshes as disabled incrementally!
-
- => Don't use IsEnabled() since it considers the top level window. The brittle wxWidgets implementation is right in their intention,
- but wrong when not refreshing child-windows: the control designer decides how his control should be rendered!
-
- => IsThisEnabled() OTOH is too shallow and does not consider parent windows which are not top level.
-
- The perfect solution would be a bool ShouldBeDrawnActive() { return "IsEnabled() but ignore effects of showing a modal dialog"; }
-
- However "IsThisEnabled()" is good enough (same like the old IsEnabled() on wxWidgets 2.8.12) and it avoids this pathetic behavior on XP.
- (Similar problem on Win 7: e.g. directly click sync button without comparing first)
- */
- if (IsThisEnabled())
- clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
- else
- clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE));
-
- wxFont labelFont = GetFont();
- //labelFont.SetWeight(wxFONTWEIGHT_BOLD);
- dc.SetFont(labelFont); //harmonize with RowLabelWin::getBestWidth()!
-
- auto rowRange = getRowsOnClient(rect); //returns range [begin, end)
- for (auto row = rowRange.first; row < rowRange.second; ++row)
- {
- wxRect singleLabelArea = getRowLabelArea(row); //returns empty rect if row not found
- if (singleLabelArea.height > 0)
- {
- singleLabelArea.y = refParent().CalcScrolledPosition(singleLabelArea.GetTopLeft()).y;
- drawRowLabel(dc, singleLabelArea, row);
- }
- }
- }
-
- void drawRowLabel(wxDC& dc, const wxRect& rect, size_t row)
- {
- //clearArea(dc, rect, getColorRowLabel());
- dc.GradientFillLinear(rect, getColorLabelGradientFrom(), getColorLabelGradientTo(), wxEAST); //clear overlapping cells
- wxDCTextColourChanger dummy3(dc, getColorLabelText()); //accessibility: always set both foreground AND background colors!
-
- //label text
- wxRect textRect = rect;
- textRect.Deflate(1);
-
- GridData::drawCellText(dc, textRect, formatRow(row), wxALIGN_CENTRE);
-
- //border lines
- {
- wxDCPenChanger dummy(dc, *wxWHITE_PEN);
- dc.DrawLine(rect.GetTopLeft(), rect.GetTopRight());
- }
- {
- wxDCPenChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_3DSHADOW));
- dc.DrawLine(rect.GetTopLeft(), rect.GetBottomLeft());
- dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight());
- dc.DrawLine(rect.GetBottomRight(), rect.GetTopRight() + wxPoint(0, -1));
- }
- }
-
- void onMouseLeftDown(wxMouseEvent& event) override { refParent().redirectRowLabelEvent(event); }
- void onMouseMovement(wxMouseEvent& event) override { refParent().redirectRowLabelEvent(event); }
- void onMouseLeftUp (wxMouseEvent& event) override { refParent().redirectRowLabelEvent(event); }
-
- int rowHeight_;
-};
-
-
-namespace
-{
-class ColumnResizing
-{
-public:
- ColumnResizing(wxWindow& wnd, size_t col, int startWidth, int clientPosX) :
- wnd_(wnd), col_(col), startWidth_(startWidth), clientPosX_(clientPosX)
- {
- wnd_.CaptureMouse();
- }
- ~ColumnResizing()
- {
- if (wnd_.HasCapture())
- wnd_.ReleaseMouse();
- }
-
- size_t getColumn () const { return col_; }
- int getStartWidth () const { return startWidth_; }
- int getStartPosX () const { return clientPosX_; }
-
-private:
- wxWindow& wnd_;
- const size_t col_;
- const int startWidth_;
- const int clientPosX_;
-};
-
-
-class ColumnMove
-{
-public:
- ColumnMove(wxWindow& wnd, size_t colFrom, int clientPosX) :
- wnd_(wnd),
- colFrom_(colFrom),
- colTo_(colFrom),
- clientPosX_(clientPosX) { wnd_.CaptureMouse(); }
- ~ColumnMove() { if (wnd_.HasCapture()) wnd_.ReleaseMouse(); }
-
- size_t getColumnFrom() const { return colFrom_; }
- size_t& refColumnTo() { return colTo_; }
- int getStartPosX () const { return clientPosX_; }
-
- bool isRealMove() const { return !singleClick_; }
- void setRealMove() { singleClick_ = false; }
-
-private:
- wxWindow& wnd_;
- const size_t colFrom_;
- size_t colTo_;
- const int clientPosX_;
- bool singleClick_ = true;
-};
-}
-
-//----------------------------------------------------------------------------------------------------------------
-
-class Grid::ColLabelWin : public SubWindow
-{
-public:
- ColLabelWin(Grid& parent) : SubWindow(parent) {}
-
-private:
- bool AcceptsFocus() const override { return false; }
-
- void render(wxDC& dc, const wxRect& rect) override
- {
- if (IsThisEnabled())
- clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
- else
- clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE));
-
- //coordinate with "colLabelHeight" in Grid constructor:
- wxFont labelFont = GetFont();
- labelFont.SetWeight(wxFONTWEIGHT_BOLD);
- dc.SetFont(labelFont);
-
- wxDCTextColourChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels
-
- const int colLabelHeight = refParent().colLabelHeight_;
-
- wxPoint labelAreaTL(refParent().CalcScrolledPosition(wxPoint(0, 0)).x, 0); //client coordinates
-
- std::vector<ColumnWidth> absWidths = refParent().getColWidths(); //resolve stretched widths
- for (auto it = absWidths.begin(); it != absWidths.end(); ++it)
- {
- const size_t col = it - absWidths.begin();
- const int width = it->width_; //don't use unsigned for calculations!
-
- if (labelAreaTL.x > rect.GetRight())
- return; //done, rect is fully covered
- if (labelAreaTL.x + width > rect.x)
- drawColumnLabel(dc, wxRect(labelAreaTL, wxSize(width, colLabelHeight)), col, it->type_);
- labelAreaTL.x += width;
- }
- if (labelAreaTL.x > rect.GetRight())
- return; //done, rect is fully covered
-
- //fill gap after columns and cover full width
- if (fillGapAfterColumns)
- {
- int totalWidth = 0;
- for (const ColumnWidth& cw : absWidths)
- totalWidth += cw.width_;
- const int clientWidth = GetClientSize().GetWidth(); //need reliable, stable width in contrast to rect.width
-
- if (totalWidth < clientWidth)
- drawColumnLabel(dc, wxRect(labelAreaTL, wxSize(clientWidth - totalWidth, colLabelHeight)), absWidths.size(), ColumnType::NONE);
- }
- }
-
- void drawColumnLabel(wxDC& dc, const wxRect& rect, size_t col, ColumnType colType)
- {
- if (auto dataView = refParent().getDataProvider())
- {
- const bool isHighlighted = activeResizing_ ? col == activeResizing_ ->getColumn () : //highlight_ column on mouse-over
- activeClickOrMove_ ? col == activeClickOrMove_->getColumnFrom() :
- highlightCol_ ? col == *highlightCol_ :
- false;
-
- RecursiveDcClipper clip(dc, rect);
- dataView->renderColumnLabel(refParent(), dc, rect, colType, isHighlighted);
-
- //draw move target location
- if (refParent().allowColumnMove_)
- if (activeClickOrMove_ && activeClickOrMove_->isRealMove())
- {
- if (col + 1 == activeClickOrMove_->refColumnTo()) //handle pos 1, 2, .. up to "at end" position
- dc.GradientFillLinear(wxRect(rect.GetTopRight(), rect.GetBottomRight() + wxPoint(-2, 0)), getColorLabelGradientFrom(), *wxBLUE, wxSOUTH);
- else if (col == activeClickOrMove_->refColumnTo() && col == 0) //pos 0
- dc.GradientFillLinear(wxRect(rect.GetTopLeft(), rect.GetBottomLeft() + wxPoint(2, 0)), getColorLabelGradientFrom(), *wxBLUE, wxSOUTH);
- }
- }
- }
-
- void onMouseLeftDown(wxMouseEvent& event) override
- {
- if (FindFocus() != &refParent().getMainWin())
- refParent().getMainWin().SetFocus();
-
- activeResizing_.reset();
- activeClickOrMove_.reset();
-
- if (Opt<ColAction> action = refParent().clientPosToColumnAction(event.GetPosition()))
- {
- if (action->wantResize)
- {
- if (!event.LeftDClick()) //double-clicks never seem to arrive here; why is this checked at all???
- if (Opt<int> colWidth = refParent().getColWidth(action->col))
- activeResizing_ = std::make_unique<ColumnResizing>(*this, action->col, *colWidth, event.GetPosition().x);
- }
- else //a move or single click
- activeClickOrMove_ = std::make_unique<ColumnMove>(*this, action->col, event.GetPosition().x);
- }
- event.Skip();
- }
-
- void onMouseLeftUp(wxMouseEvent& event) override
- {
- activeResizing_.reset(); //nothing else to do, actual work done by onMouseMovement()
-
- if (activeClickOrMove_)
- {
- if (activeClickOrMove_->isRealMove())
- {
- if (refParent().allowColumnMove_)
- {
- const size_t colFrom = activeClickOrMove_->getColumnFrom();
- size_t colTo = activeClickOrMove_->refColumnTo();
-
- if (colTo > colFrom) //simulate "colFrom" deletion
- --colTo;
-
- refParent().moveColumn(colFrom, colTo);
- }
- }
- else //notify single label click
- {
- if (const Opt<ColumnType> colType = refParent().colToType(activeClickOrMove_->getColumnFrom()))
- sendEventNow(GridLabelClickEvent(EVENT_GRID_COL_LABEL_MOUSE_LEFT, event, *colType));
- }
- activeClickOrMove_.reset();
- }
-
- refParent().updateWindowSizes(); //looks strange if done during onMouseMovement()
- refParent().Refresh();
- event.Skip();
- }
-
- void onMouseCaptureLost(wxMouseCaptureLostEvent& event) override
- {
- activeResizing_.reset();
- activeClickOrMove_.reset();
- Refresh();
- //event.Skip(); -> we DID handle it!
- }
-
- void onMouseLeftDouble(wxMouseEvent& event) override
- {
- if (Opt<ColAction> action = refParent().clientPosToColumnAction(event.GetPosition()))
- if (action->wantResize)
- {
- //auto-size visible range on double-click
- const int bestWidth = refParent().getBestColumnSize(action->col); //return -1 on error
- if (bestWidth >= 0)
- {
- refParent().setColumnWidth(bestWidth, action->col, ALLOW_GRID_EVENT);
- refParent().Refresh(); //refresh main grid as well!
- }
- }
- event.Skip();
- }
-
- void onMouseMovement(wxMouseEvent& event) override
- {
- if (activeResizing_)
- {
- const auto col = activeResizing_->getColumn();
- const int newWidth = activeResizing_->getStartWidth() + event.GetPosition().x - activeResizing_->getStartPosX();
-
- //set width tentatively
- refParent().setColumnWidth(newWidth, col, ALLOW_GRID_EVENT);
-
- //check if there's a small gap after last column, if yes, fill it
- const int gapWidth = GetClientSize().GetWidth() - refParent().getColWidthsSum(GetClientSize().GetWidth());
- if (std::abs(gapWidth) < COLUMN_FILL_GAP_TOLERANCE)
- refParent().setColumnWidth(newWidth + gapWidth, col, ALLOW_GRID_EVENT);
-
- refParent().Refresh(); //refresh columns on main grid as well!
- }
- else if (activeClickOrMove_)
- {
- const int clientPosX = event.GetPosition().x;
- if (std::abs(clientPosX - activeClickOrMove_->getStartPosX()) > COLUMN_MOVE_DELAY) //real move (not a single click)
- {
- activeClickOrMove_->setRealMove();
-
- const ptrdiff_t col = refParent().clientPosToMoveTargetColumn(event.GetPosition());
- if (col >= 0)
- activeClickOrMove_->refColumnTo() = col;
- }
- }
- else
- {
- if (const Opt<ColAction> action = refParent().clientPosToColumnAction(event.GetPosition()))
- {
- highlightCol_ = action->col;
-
- if (action->wantResize)
- SetCursor(wxCURSOR_SIZEWE); //set window-local only! :)
- else
- SetCursor(*wxSTANDARD_CURSOR);
- }
- else
- {
- highlightCol_ = NoValue();
- SetCursor(*wxSTANDARD_CURSOR);
- }
- }
-
- const std::wstring toolTip = [&]
- {
- const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition());
- const ColumnType colType = refParent().getColumnAtPos(absPos.x).colType; //returns ColumnType::NONE if no column at x position!
- if (colType != ColumnType::NONE)
- if (auto prov = refParent().getDataProvider())
- return prov->getToolTip(colType);
- return std::wstring();
- }();
- setToolTip(toolTip);
-
- Refresh();
- event.Skip();
- }
-
- void onLeaveWindow(wxMouseEvent& event) override
- {
- highlightCol_ = NoValue(); //wxEVT_LEAVE_WINDOW does not respect mouse capture! -> however highlight_ is drawn unconditionally during move/resize!
- Refresh();
- event.Skip();
- }
-
- void onMouseRightDown(wxMouseEvent& event) override
- {
- if (const Opt<ColAction> action = refParent().clientPosToColumnAction(event.GetPosition()))
- {
- if (const Opt<ColumnType> colType = refParent().colToType(action->col))
- sendEventNow(GridLabelClickEvent(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, event, *colType)); //notify right click
- else assert(false);
- }
- else
- //notify right click (on free space after last column)
- if (fillGapAfterColumns)
- sendEventNow(GridLabelClickEvent(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, event, ColumnType::NONE));
-
- event.Skip();
- }
-
- std::unique_ptr<ColumnResizing> activeResizing_;
- std::unique_ptr<ColumnMove> activeClickOrMove_;
- Opt<size_t> highlightCol_; //column during mouse-over
-};
-
-//----------------------------------------------------------------------------------------------------------------
-namespace
-{
-const wxEventType EVENT_GRID_HAS_SCROLLED = wxNewEventType(); //internal to Grid::MainWin::ScrollWindow()
-}
-//----------------------------------------------------------------------------------------------------------------
-
-class Grid::MainWin : public SubWindow
-{
-public:
- MainWin(Grid& parent,
- RowLabelWin& rowLabelWin,
- ColLabelWin& colLabelWin) : SubWindow(parent),
- rowLabelWin_(rowLabelWin),
- colLabelWin_(colLabelWin)
- {
- Connect(EVENT_GRID_HAS_SCROLLED, wxEventHandler(MainWin::onRequestWindowUpdate), nullptr, this);
- }
-
- ~MainWin() { assert(!gridUpdatePending_); }
-
- size_t getCursor() const { return cursorRow_; }
- size_t getAnchor() const { return selectionAnchor_; }
-
- void setCursor(size_t newCursorRow, size_t newAnchorRow)
- {
- cursorRow_ = newCursorRow;
- selectionAnchor_ = newAnchorRow;
- activeSelection_.reset(); //e.g. user might search with F3 while holding down left mouse button
- }
-
-private:
- void render(wxDC& dc, const wxRect& rect) override
- {
- if (IsThisEnabled())
- clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
- else
- clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE));
-
- dc.SetFont(GetFont()); //harmonize with Grid::getBestColumnSize()
-
- wxDCTextColourChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels
-
- std::vector<ColumnWidth> absWidths = refParent().getColWidths(); //resolve stretched widths
- {
- int totalRowWidth = 0;
- for (const ColumnWidth& cw : absWidths)
- totalRowWidth += cw.width_;
-
- //fill gap after columns and cover full width
- if (fillGapAfterColumns)
- totalRowWidth = std::max(totalRowWidth, GetClientSize().GetWidth());
-
- if (auto prov = refParent().getDataProvider())
- {
- RecursiveDcClipper dummy2(dc, rect); //do NOT draw background on cells outside of invalidated rect invalidating foreground text!
-
- wxPoint cellAreaTL(refParent().CalcScrolledPosition(wxPoint(0, 0))); //client coordinates
- const int rowHeight = rowLabelWin_.getRowHeight();
- const auto rowRange = rowLabelWin_.getRowsOnClient(rect); //returns range [begin, end)
-
- //draw background lines
- for (auto row = rowRange.first; row < rowRange.second; ++row)
- {
- const wxRect rowRect(cellAreaTL + wxPoint(0, row * rowHeight), wxSize(totalRowWidth, rowHeight));
- RecursiveDcClipper dummy3(dc, rowRect);
- prov->renderRowBackgound(dc, rowRect, row, refParent().IsThisEnabled(), drawAsSelected(row));
- }
-
- //draw single cells, column by column
- for (const ColumnWidth& cw : absWidths)
- {
- if (cellAreaTL.x > rect.GetRight())
- return; //done
-
- if (cellAreaTL.x + cw.width_ > rect.x)
- for (auto row = rowRange.first; row < rowRange.second; ++row)
- {
- const wxRect cellRect(cellAreaTL.x, cellAreaTL.y + row * rowHeight, cw.width_, rowHeight);
- RecursiveDcClipper dummy3(dc, cellRect);
- prov->renderCell(dc, cellRect, row, cw.type_, refParent().IsThisEnabled(), drawAsSelected(row), getRowHoverToDraw(row));
- }
- cellAreaTL.x += cw.width_;
- }
- }
- }
- }
-
- HoverArea getRowHoverToDraw(ptrdiff_t row) const
- {
- if (activeSelection_)
- {
- if (activeSelection_->getFirstClick().row_ == row)
- return activeSelection_->getFirstClick().hoverArea_;
- }
- else if (highlight_.row == row)
- return highlight_.rowHover;
- return HoverArea::NONE;
- }
-
- bool drawAsSelected(size_t row) const
- {
- if (activeSelection_) //check if user is currently selecting with mouse
- {
- const size_t rowFrom = std::min(activeSelection_->getStartRow(), activeSelection_->getCurrentRow());
- const size_t rowTo = std::max(activeSelection_->getStartRow(), activeSelection_->getCurrentRow());
-
- if (rowFrom <= row && row <= rowTo)
- return activeSelection_->isPositiveSelect(); //overwrite default
- }
- return refParent().isSelected(row);
- }
-
- void onMouseLeftDown (wxMouseEvent& event) override { onMouseDown(event); }
- void onMouseLeftUp (wxMouseEvent& event) override { onMouseUp (event); }
- void onMouseRightDown(wxMouseEvent& event) override { onMouseDown(event); }
- void onMouseRightUp (wxMouseEvent& event) override { onMouseUp (event); }
-
- void onMouseLeftDouble(wxMouseEvent& event) override
- {
- if (auto prov = refParent().getDataProvider())
- {
- const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition());
- const ptrdiff_t row = rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range
- const ColumnPosInfo cpi = refParent().getColumnAtPos(absPos.x); //returns ColumnType::NONE if no column at x position!
- const HoverArea rowHover = prov->getRowMouseHover(row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth);
- //client is interested in all double-clicks, even those outside of the grid!
- sendEventNow(GridClickEvent(EVENT_GRID_MOUSE_LEFT_DOUBLE, event, row, rowHover));
- }
- event.Skip();
- }
-
- void onMouseDown(wxMouseEvent& event) //handle left and right mouse button clicks (almost) the same
- {
- if (wxWindow::FindFocus() != this) //doesn't seem to happen automatically for right mouse button
- SetFocus();
-
- if (auto prov = refParent().getDataProvider())
- {
- const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition());
- const ptrdiff_t row = rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range
- const ColumnPosInfo cpi = refParent().getColumnAtPos(absPos.x); //returns ColumnType::NONE if no column at x position!
- const HoverArea rowHover = prov->getRowMouseHover(row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth);
- //row < 0 possible!!! Pressing "Menu key" simulates Mouse Right Down + Up at position 0xffff/0xffff!
-
- GridClickEvent mouseEvent(event.RightDown() ? EVENT_GRID_MOUSE_RIGHT_DOWN : EVENT_GRID_MOUSE_LEFT_DOWN, event, row, rowHover);
-
- if (row >= 0)
- if (!event.RightDown() || !refParent().isSelected(row)) //do NOT start a new selection if user right-clicks on a selected area!
- {
- if (event.ControlDown())
- activeSelection_ = std::make_unique<MouseSelection>(*this, row, !refParent().isSelected(row), mouseEvent);
- else if (event.ShiftDown())
- {
- activeSelection_ = std::make_unique<MouseSelection>(*this, selectionAnchor_, true, mouseEvent);
- refParent().clearSelection(ALLOW_GRID_EVENT);
- }
- else
- {
- activeSelection_ = std::make_unique<MouseSelection>(*this, row, true, mouseEvent);
- refParent().clearSelection(ALLOW_GRID_EVENT);
- }
- }
- //notify event *after* potential "clearSelection(true)" above: a client should first receive a GridRangeSelectEvent for clearing the grid, if necessary,
- //then GridClickEvent and the associated GridRangeSelectEvent one after the other
- sendEventNow(mouseEvent);
-
- Refresh();
- }
- event.Skip(); //allow changing focus
- }
-
- void onMouseUp(wxMouseEvent& event)
- {
- if (activeSelection_)
- {
- const size_t rowCount = refParent().getRowCount();
- if (rowCount > 0)
- {
- if (activeSelection_->getCurrentRow() < rowCount)
- {
- cursorRow_ = activeSelection_->getCurrentRow();
- selectionAnchor_ = activeSelection_->getStartRow(); //allowed to be "out of range"
- }
- else if (activeSelection_->getStartRow() < rowCount) //don't change cursor if "to" and "from" are out of range
- {
- cursorRow_ = rowCount - 1;
- selectionAnchor_ = activeSelection_->getStartRow(); //allowed to be "out of range"
- }
- else //total selection "out of range"
- selectionAnchor_ = cursorRow_;
- }
- //slight deviation from Explorer: change cursor while dragging mouse! -> unify behavior with shift + direction keys
-
- refParent().selectRangeAndNotify(activeSelection_->getStartRow (), //from
- activeSelection_->getCurrentRow(), //to
- activeSelection_->isPositiveSelect(),
- &activeSelection_->getFirstClick());
- activeSelection_.reset();
- }
-
- if (auto prov = refParent().getDataProvider())
- {
- //this one may point to row which is not in visible area!
- const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition());
- const ptrdiff_t row = rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range
- const ColumnPosInfo cpi = refParent().getColumnAtPos(absPos.x); //returns ColumnType::NONE if no column at x position!
- const HoverArea rowHover = prov->getRowMouseHover(row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth);
- //notify click event after the range selection! e.g. this makes sure the selection is applied before showing a context menu
- sendEventNow(GridClickEvent(event.RightUp() ? EVENT_GRID_MOUSE_RIGHT_UP : EVENT_GRID_MOUSE_LEFT_UP, event, row, rowHover));
- }
-
- //update highlight_ and tooltip: on OS X no mouse movement event is generated after a mouse button click (unlike on Windows)
- event.SetPosition(ScreenToClient(wxGetMousePosition())); //mouse position may have changed within above callbacks (e.g. context menu was shown)!
- onMouseMovement(event);
-
- Refresh();
- event.Skip(); //allow changing focus
- }
-
- void onMouseCaptureLost(wxMouseCaptureLostEvent& event) override
- {
- activeSelection_.reset();
- highlight_.row = -1;
- Refresh();
- //event.Skip(); -> we DID handle it!
- }
-
- void onMouseMovement(wxMouseEvent& event) override
- {
- if (auto prov = refParent().getDataProvider())
- {
- const ptrdiff_t rowCount = refParent().getRowCount();
- const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition());
- const ptrdiff_t row = rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range
- const ColumnPosInfo cpi = refParent().getColumnAtPos(absPos.x); //returns ColumnType::NONE if no column at x position!
- const HoverArea rowHover = prov->getRowMouseHover(row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth);
-
- const std::wstring toolTip = [&]
- {
- if (cpi.colType != ColumnType::NONE && 0 <= row && row < rowCount)
- return prov->getToolTip(row, cpi.colType);
- return std::wstring();
- }();
- setToolTip(toolTip); //show even during mouse selection!
-
- if (activeSelection_)
- activeSelection_->evalMousePos(); //call on both mouse movement + timer event!
- else
- {
- refreshHighlight(highlight_);
- highlight_.row = row;
- highlight_.rowHover = rowHover;
- refreshHighlight(highlight_); //multiple Refresh() calls are condensed into single one!
- }
- }
- event.Skip();
- }
-
- void onLeaveWindow(wxMouseEvent& event) override //wxEVT_LEAVE_WINDOW does not respect mouse capture!
- {
- if (!activeSelection_)
- {
- refreshHighlight(highlight_);
- highlight_.row = -1;
- }
-
- event.Skip();
- }
-
-
- void onFocus(wxFocusEvent& event) override { Refresh(); event.Skip(); }
-
- class MouseSelection : private wxEvtHandler
- {
- public:
- MouseSelection(MainWin& wnd, size_t rowStart, bool positiveSelect, const GridClickEvent& firstClick) :
- wnd_(wnd), rowStart_(rowStart), rowCurrent_(rowStart), positiveSelect_(positiveSelect), firstClick_(firstClick)
- {
- wnd_.CaptureMouse();
- timer_.Connect(wxEVT_TIMER, wxEventHandler(MouseSelection::onTimer), nullptr, this);
- timer_.Start(100); //timer interval in ms
- evalMousePos();
- }
- ~MouseSelection() { if (wnd_.HasCapture()) wnd_.ReleaseMouse(); }
-
- size_t getStartRow () const { return rowStart_; }
- size_t getCurrentRow () const { return rowCurrent_; }
- bool isPositiveSelect() const { return positiveSelect_; } //are we selecting or unselecting?
- const GridClickEvent& getFirstClick() const { return firstClick_; }
-
- void evalMousePos()
- {
- const auto now = std::chrono::steady_clock::now();
- const double deltaSecs = std::chrono::duration<double>(now - lastEvalTime_).count(); //unit: [sec]
- lastEvalTime_ = now;
-
- const wxPoint clientPos = wnd_.ScreenToClient(wxGetMousePosition());
- const wxSize clientSize = wnd_.GetClientSize();
- assert(wnd_.GetClientAreaOrigin() == wxPoint());
-
- //scroll while dragging mouse
- const int overlapPixY = clientPos.y < 0 ? clientPos.y :
- clientPos.y >= clientSize.GetHeight() ? clientPos.y - (clientSize.GetHeight() - 1) : 0;
- const int overlapPixX = clientPos.x < 0 ? clientPos.x :
- clientPos.x >= clientSize.GetWidth() ? clientPos.x - (clientSize.GetWidth() - 1) : 0;
-
- int pixelsPerUnitY = 0;
- wnd_.refParent().GetScrollPixelsPerUnit(nullptr, &pixelsPerUnitY);
- if (pixelsPerUnitY <= 0) return;
-
- const double mouseDragSpeedIncScrollU = pixelsPerUnitY > 0 ? MOUSE_DRAG_ACCELERATION * wnd_.rowLabelWin_.getRowHeight() / pixelsPerUnitY : 0; //unit: [scroll units / (pixel * sec)]
-
- auto autoScroll = [&](int overlapPix, double& toScroll)
- {
- if (overlapPix != 0)
- {
- const double scrollSpeed = overlapPix * mouseDragSpeedIncScrollU; //unit: [scroll units / sec]
- toScroll += scrollSpeed * deltaSecs;
- }
- else
- toScroll = 0;
- };
-
- autoScroll(overlapPixX, toScrollX_);
- autoScroll(overlapPixY, toScrollY_);
-
- if (static_cast<int>(toScrollX_) != 0 || static_cast<int>(toScrollY_) != 0)
- {
- wnd_.refParent().scrollDelta(static_cast<int>(toScrollX_), static_cast<int>(toScrollY_)); //
- toScrollX_ -= static_cast<int>(toScrollX_); //rounds down for positive numbers, up for negative,
- toScrollY_ -= static_cast<int>(toScrollY_); //exactly what we want
- }
-
- //select current row *after* scrolling
- wxPoint clientPosTrimmed = clientPos;
- numeric::clamp(clientPosTrimmed.y, 0, clientSize.GetHeight() - 1); //do not select row outside client window!
-
- const wxPoint absPos = wnd_.refParent().CalcUnscrolledPosition(clientPosTrimmed);
- const ptrdiff_t newRow = wnd_.rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range
- if (newRow >= 0)
- if (rowCurrent_ != newRow)
- {
- rowCurrent_ = newRow;
- wnd_.Refresh();
- }
- }
-
- private:
- void onTimer(wxEvent& event) { evalMousePos(); }
-
- MainWin& wnd_;
- const size_t rowStart_;
- ptrdiff_t rowCurrent_;
- const bool positiveSelect_;
- const GridClickEvent firstClick_;
- wxTimer timer_;
- double toScrollX_ = 0; //count outstanding scroll unit fractions while dragging mouse
- double toScrollY_ = 0; //
- std::chrono::steady_clock::time_point lastEvalTime_ = std::chrono::steady_clock::now();
- };
-
- struct MouseHighlight
- {
- ptrdiff_t row = -1;
- HoverArea rowHover = HoverArea::NONE;
- };
-
- void ScrollWindow(int dx, int dy, const wxRect* rect) override
- {
- wxWindow::ScrollWindow(dx, dy, rect);
- rowLabelWin_.ScrollWindow(0, dy, rect);
- colLabelWin_.ScrollWindow(dx, 0, rect);
-
- //attention, wxGTK call sequence: wxScrolledWindow::Scroll() -> wxScrolledHelperNative::Scroll() -> wxScrolledHelperNative::DoScroll()
- //which *first* calls us, MainWin::ScrollWindow(), and *then* internally updates m_yScrollPosition
- //=> we cannot use CalcUnscrolledPosition() here which gives the wrong/outdated value!!!
- //=> we need to update asynchronously:
- //=> don't send async event repeatedly => severe performance issues on wxGTK!
- //=> can't use idle event neither: too few idle events on Windows, e.g. NO idle events while mouse drag-scrolling!
- //=> solution: send single async event at most!
- if (!gridUpdatePending_) //without guarding, the number of outstanding async events can become very high during scrolling!! test case: Ubuntu: 170; Windows: 20
- {
- gridUpdatePending_ = true;
- wxCommandEvent scrollEvent(EVENT_GRID_HAS_SCROLLED);
- AddPendingEvent(scrollEvent); //asynchronously call updateAfterScroll()
- }
- }
-
- void onRequestWindowUpdate(wxEvent& event)
- {
- assert(gridUpdatePending_);
- ZEN_ON_SCOPE_EXIT(gridUpdatePending_ = false);
-
- refParent().updateWindowSizes(false); //row label width has changed -> do *not* update scrollbars: recursion on wxGTK! -> still a problem, now that we're called async??
- rowLabelWin_.Update(); //update while dragging scroll thumb
- }
-
- void refreshRow(size_t row)
- {
- const wxRect& rowArea = rowLabelWin_.getRowLabelArea(row); //returns empty rect if row not found
- const wxPoint topLeft = refParent().CalcScrolledPosition(wxPoint(0, rowArea.y)); //absolute -> client coordinates
- wxRect cellArea(topLeft, wxSize(refParent().getColWidthsSum(GetClientSize().GetWidth()), rowArea.height));
- RefreshRect(cellArea, false);
- }
-
- void refreshHighlight(const MouseHighlight& hl)
- {
- const ptrdiff_t rowCount = refParent().getRowCount();
- if (0 <= hl.row && hl.row < rowCount && hl.rowHover != HoverArea::NONE) //no highlight_? => NOP!
- refreshRow(hl.row);
- }
-
- RowLabelWin& rowLabelWin_;
- ColLabelWin& colLabelWin_;
-
- std::unique_ptr<MouseSelection> activeSelection_; //bound while user is selecting with mouse
- MouseHighlight highlight_; //current mouse highlight_ (superseeded by activeSelection_ if available)
-
- ptrdiff_t cursorRow_ = 0;
- size_t selectionAnchor_ = 0;
- bool gridUpdatePending_ = false;
-};
-
-//----------------------------------------------------------------------------------------------------------------
-//----------------------------------------------------------------------------------------------------------------
-
-Grid::Grid(wxWindow* parent,
- wxWindowID id,
- const wxPoint& pos,
- const wxSize& size,
- long style,
- const wxString& name) : wxScrolledWindow(parent, id, pos, size, style | wxWANTS_CHARS, name)
-{
- cornerWin_ = new CornerWin (*this); //
- rowLabelWin_ = new RowLabelWin(*this); //owership handled by "this"
- colLabelWin_ = new ColLabelWin(*this); //
- mainWin_ = new MainWin (*this, *rowLabelWin_, *colLabelWin_); //
-
- colLabelHeight_ = 2 * DEFAULT_COL_LABEL_BORDER + [&]() -> int
- {
- //coordinate with ColLabelWin::render():
- wxFont labelFont = colLabelWin_->GetFont();
- labelFont.SetWeight(wxFONTWEIGHT_BOLD);
- return labelFont.GetPixelSize().GetHeight();
- }();
-
- SetTargetWindow(mainWin_);
-
- SetInitialSize(size); //"Most controls will use this to set their initial size" -> why not
-
- assert(GetClientSize() == GetSize()); //borders are NOT allowed for Grid
- //reason: updateWindowSizes() wants to use "GetSize()" as a "GetClientSize()" including scrollbars
-
- Connect(wxEVT_PAINT, wxPaintEventHandler(Grid::onPaintEvent ), nullptr, this);
- Connect(wxEVT_ERASE_BACKGROUND, wxEraseEventHandler(Grid::onEraseBackGround), nullptr, this);
- Connect(wxEVT_SIZE, wxSizeEventHandler (Grid::onSizeEvent ), nullptr, this);
-
- Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(Grid::onKeyDown), nullptr, this);
-}
-
-
-void Grid::updateWindowSizes(bool updateScrollbar)
-{
- /* We have to deal with TWO nasty circular dependencies:
- 1.
- rowLabelWidth
- /|\
- mainWin::client width
- /|\
- SetScrollbars -> show/hide horizontal scrollbar depending on client width
- /|\
- mainWin::client height -> possibly trimmed by horizontal scrollbars
- /|\
- rowLabelWidth
-
- 2.
- mainWin_->GetClientSize()
- /|\
- SetScrollbars -> show/hide scrollbars depending on whether client size is big enough
- /|\
- GetClientSize(); -> possibly trimmed by scrollbars
- /|\
- mainWin_->GetClientSize() -> also trimmed, since it's a sub-window!
- */
-
- //break this vicious circle:
-
- //harmonize with Grid::GetSizeAvailableForScrollTarget()!
-
- //1. calculate row label width independent from scrollbars
- const int mainWinHeightGross = std::max(GetSize().GetHeight() - colLabelHeight_, 0); //independent from client sizes and scrollbars!
- const ptrdiff_t logicalHeight = rowLabelWin_->getLogicalHeight(); //
-
- int rowLabelWidth = 0;
- if (drawRowLabel_ && logicalHeight > 0)
- {
- ptrdiff_t yFrom = CalcUnscrolledPosition(wxPoint(0, 0)).y;
- ptrdiff_t yTo = CalcUnscrolledPosition(wxPoint(0, mainWinHeightGross - 1)).y ;
- numeric::clamp<ptrdiff_t>(yFrom, 0, logicalHeight - 1);
- numeric::clamp<ptrdiff_t>(yTo, 0, logicalHeight - 1);
-
- const ptrdiff_t rowFrom = rowLabelWin_->getRowAtPos(yFrom);
- const ptrdiff_t rowTo = rowLabelWin_->getRowAtPos(yTo);
- if (rowFrom >= 0 && rowTo >= 0)
- rowLabelWidth = rowLabelWin_->getBestWidth(rowFrom, rowTo);
- }
-
- auto getMainWinSize = [&](const wxSize& clientSize) { return wxSize(std::max(0, clientSize.GetWidth() - rowLabelWidth), std::max(0, clientSize.GetHeight() - colLabelHeight_)); };
-
- auto setScrollbars2 = [&](int logWidth, int logHeight) //replace SetScrollbars, which loses precision of pixelsPerUnitX for some brain-dead reason
- {
- mainWin_->SetVirtualSize(logWidth, logHeight); //set before calling SetScrollRate():
- //else SetScrollRate() would fail to preserve scroll position when "new virtual pixel-pos > old virtual height"
-
- int ppsuX = 0; //pixel per scroll unit
- int ppsuY = 0;
- GetScrollPixelsPerUnit(&ppsuX, &ppsuY);
-
- const int ppsuNew = rowLabelWin_->getRowHeight();
- if (ppsuX != ppsuNew || ppsuY != ppsuNew) //support polling!
- SetScrollRate(ppsuNew, ppsuNew); //internally calls AdjustScrollbars() and GetVirtualSize()!
-
- AdjustScrollbars(); //lousy wxWidgets design decision: internally calls mainWin_->GetClientSize() without considering impact of scrollbars!
- //Attention: setting scrollbars triggers *synchronous* resize event if scrollbars are shown or hidden! => updateWindowSizes() recursion! (Windows)
- };
-
- //2. update managed windows' sizes: just assume scrollbars are already set correctly, even if they may not be (yet)!
- //this ensures mainWin_->SetVirtualSize() and AdjustScrollbars() are working with the correct main window size, unless sb change later, which triggers a recalculation anyway!
- const wxSize mainWinSize = getMainWinSize(GetClientSize());
-
- cornerWin_ ->SetSize(0, 0, rowLabelWidth, colLabelHeight_);
- rowLabelWin_->SetSize(0, colLabelHeight_, rowLabelWidth, mainWinSize.GetHeight());
- colLabelWin_->SetSize(rowLabelWidth, 0, mainWinSize.GetWidth(), colLabelHeight_);
- mainWin_ ->SetSize(rowLabelWidth, colLabelHeight_, mainWinSize.GetWidth(), mainWinSize.GetHeight());
-
- //avoid flicker in wxWindowMSW::HandleSize() when calling ::EndDeferWindowPos() where the sub-windows are moved only although they need to be redrawn!
- colLabelWin_->Refresh();
- mainWin_ ->Refresh();
-
- //3. update scrollbars: "guide wxScrolledHelper to not screw up too much"
- if (updateScrollbar)
- {
- const int mainWinWidthGross = getMainWinSize(GetSize()).GetWidth();
-
- if (logicalHeight <= mainWinHeightGross &&
- getColWidthsSum(mainWinWidthGross) <= mainWinWidthGross &&
- //this special case needs to be considered *only* when both scrollbars are flexible:
- showScrollbarX_ == SB_SHOW_AUTOMATIC &&
- showScrollbarY_ == SB_SHOW_AUTOMATIC)
- setScrollbars2(0, 0); //no scrollbars required at all! -> wxScrolledWindow requires active help to detect this special case!
- else
- {
- const int logicalWidthTmp = getColWidthsSum(mainWinSize.GetWidth()); //assuming vertical scrollbar stays as it is...
- setScrollbars2(logicalWidthTmp, logicalHeight); //if scrollbars are shown or hidden a new resize event recurses into updateWindowSizes()
- /*
- is there a risk of endless recursion? No, 2-level recursion at most, consider the following 6 cases:
-
- <----------gw---------->
- <----------nw------>
- ------------------------ /|\ /|\
- | | | | |
- | main window | | nh |
- | | | | gh
- ------------------------ \|/ |
- | | | |
- ------------------------ \|/
- gw := gross width
- nw := net width := gross width - sb size
- gh := gross height
- nh := net height := gross height - sb size
-
- There are 6 cases that can occur:
- ---------------------------------
- lw := logical width
- lh := logical height
-
- 1. lw <= gw && lh <= gh => no scrollbars needed
-
- 2. lw > gw && lh > gh => need both scrollbars
-
- 3. lh > gh
- 4.1 lw <= nw => need vertical scrollbar only
- 4.2 nw < lw <= gw => need both scrollbars
-
- 4. lw > gw
- 3.1 lh <= nh => need horizontal scrollbar only
- 3.2 nh < lh <= gh => need both scrollbars
- */
- }
- }
-}
-
-
-wxSize Grid::GetSizeAvailableForScrollTarget(const wxSize& size)
-{
- //harmonize with Grid::updateWindowSizes()!
-
- //1. calculate row label width independent from scrollbars
- const int mainWinHeightGross = std::max(size.GetHeight() - colLabelHeight_, 0); //independent from client sizes and scrollbars!
- const ptrdiff_t logicalHeight = rowLabelWin_->getLogicalHeight(); //
-
- int rowLabelWidth = 0;
- if (drawRowLabel_ && logicalHeight > 0)
- {
- ptrdiff_t yFrom = CalcUnscrolledPosition(wxPoint(0, 0)).y;
- ptrdiff_t yTo = CalcUnscrolledPosition(wxPoint(0, mainWinHeightGross - 1)).y ;
- numeric::clamp<ptrdiff_t>(yFrom, 0, logicalHeight - 1);
- numeric::clamp<ptrdiff_t>(yTo, 0, logicalHeight - 1);
-
- const ptrdiff_t rowFrom = rowLabelWin_->getRowAtPos(yFrom);
- const ptrdiff_t rowTo = rowLabelWin_->getRowAtPos(yTo);
- if (rowFrom >= 0 && rowTo >= 0)
- rowLabelWidth = rowLabelWin_->getBestWidth(rowFrom, rowTo);
- }
-
- return size - wxSize(rowLabelWidth, colLabelHeight_);
-}
-
-
-void Grid::onPaintEvent(wxPaintEvent& event) { wxPaintDC dc(this); }
-
-
-void Grid::onKeyDown(wxKeyEvent& event)
-{
- int keyCode = event.GetKeyCode();
- if (GetLayoutDirection() == wxLayout_RightToLeft)
- {
- if (keyCode == WXK_LEFT || keyCode == WXK_NUMPAD_LEFT)
- keyCode = WXK_RIGHT;
- else if (keyCode == WXK_RIGHT || keyCode == WXK_NUMPAD_RIGHT)
- keyCode = WXK_LEFT;
- }
-
- const ptrdiff_t rowCount = getRowCount();
- const ptrdiff_t cursorRow = mainWin_->getCursor();
-
- auto moveCursorTo = [&](ptrdiff_t row)
- {
- if (rowCount > 0)
- {
- numeric::clamp<ptrdiff_t>(row, 0, rowCount - 1);
- setGridCursor(row);
- }
- };
-
- auto selectWithCursorTo = [&](ptrdiff_t row)
- {
- if (rowCount > 0)
- {
- numeric::clamp<ptrdiff_t>(row, 0, rowCount - 1);
- selectWithCursor(row);
- }
- };
-
- switch (keyCode)
- {
- //case WXK_TAB:
- // if (Navigate(event.ShiftDown() ? wxNavigationKeyEvent::IsBackward : wxNavigationKeyEvent::IsForward))
- // return;
- // break;
-
- case WXK_UP:
- case WXK_NUMPAD_UP:
- if (event.ShiftDown())
- selectWithCursorTo(cursorRow - 1);
- else if (event.ControlDown())
- scrollDelta(0, -1);
- else
- moveCursorTo(cursorRow - 1);
- return; //swallow event: wxScrolledWindow, wxWidgets 2.9.3 on Kubuntu x64 processes arrow keys: prevent this!
-
- case WXK_DOWN:
- case WXK_NUMPAD_DOWN:
- if (event.ShiftDown())
- selectWithCursorTo(cursorRow + 1);
- else if (event.ControlDown())
- scrollDelta(0, 1);
- else
- moveCursorTo(cursorRow + 1);
- return; //swallow event
-
- case WXK_LEFT:
- case WXK_NUMPAD_LEFT:
- if (event.ControlDown())
- scrollDelta(-1, 0);
- else if (event.ShiftDown())
- ;
- else
- moveCursorTo(cursorRow);
- return;
-
- case WXK_RIGHT:
- case WXK_NUMPAD_RIGHT:
- if (event.ControlDown())
- scrollDelta(1, 0);
- else if (event.ShiftDown())
- ;
- else
- moveCursorTo(cursorRow);
- return;
-
- case WXK_HOME:
- case WXK_NUMPAD_HOME:
- if (event.ShiftDown())
- selectWithCursorTo(0);
- //else if (event.ControlDown())
- // ;
- else
- moveCursorTo(0);
- return;
-
- case WXK_END:
- case WXK_NUMPAD_END:
- if (event.ShiftDown())
- selectWithCursorTo(rowCount - 1);
- //else if (event.ControlDown())
- // ;
- else
- moveCursorTo(rowCount - 1);
- return;
-
- case WXK_PAGEUP:
- case WXK_NUMPAD_PAGEUP:
- if (event.ShiftDown())
- selectWithCursorTo(cursorRow - GetClientSize().GetHeight() / rowLabelWin_->getRowHeight());
- //else if (event.ControlDown())
- // ;
- else
- moveCursorTo(cursorRow - GetClientSize().GetHeight() / rowLabelWin_->getRowHeight());
- return;
-
- case WXK_PAGEDOWN:
- case WXK_NUMPAD_PAGEDOWN:
- if (event.ShiftDown())
- selectWithCursorTo(cursorRow + GetClientSize().GetHeight() / rowLabelWin_->getRowHeight());
- //else if (event.ControlDown())
- // ;
- else
- moveCursorTo(cursorRow + GetClientSize().GetHeight() / rowLabelWin_->getRowHeight());
- return;
-
- case 'A': //Ctrl + A - select all
- if (event.ControlDown())
- selectRangeAndNotify(0, rowCount, true /*positive*/, nullptr /*mouseInitiated*/);
- break;
-
- case WXK_NUMPAD_ADD: //CTRL + '+' - auto-size all
- if (event.ControlDown())
- autoSizeColumns(ALLOW_GRID_EVENT);
- return;
- }
-
- event.Skip();
-}
-
-
-void Grid::setColumnLabelHeight(int height)
-{
- colLabelHeight_ = std::max(0, height);
- updateWindowSizes();
-}
-
-
-void Grid::showRowLabel(bool show)
-{
- drawRowLabel_ = show;
- updateWindowSizes();
-}
-
-
-void Grid::selectAllRows(GridEventPolicy rangeEventPolicy)
-{
- selection_.selectAll();
- mainWin_->Refresh();
-
- if (rangeEventPolicy == ALLOW_GRID_EVENT) //notify event, even if we're not triggered by user interaction
- {
- GridRangeSelectEvent selEvent(0, getRowCount(), true, nullptr);
- if (wxEvtHandler* evtHandler = GetEventHandler())
- evtHandler->ProcessEvent(selEvent);
- }
-}
-
-
-void Grid::clearSelection(GridEventPolicy rangeEventPolicy)
-{
- selection_.clear();
- mainWin_->Refresh();
-
- if (rangeEventPolicy == ALLOW_GRID_EVENT) //notify event, even if we're not triggered by user interaction
- {
- GridRangeSelectEvent unselectionEvent(0, getRowCount(), false, nullptr);
- if (wxEvtHandler* evtHandler = GetEventHandler())
- evtHandler->ProcessEvent(unselectionEvent);
- }
-}
-
-
-void Grid::scrollDelta(int deltaX, int deltaY)
-{
- int scrollPosX = 0;
- int scrollPosY = 0;
- GetViewStart(&scrollPosX, &scrollPosY);
-
- scrollPosX += deltaX;
- scrollPosY += deltaY;
-
- scrollPosX = std::max(0, scrollPosX); //wxScrollHelper::Scroll() will exit prematurely if input happens to be "-1"!
- scrollPosY = std::max(0, scrollPosY); //
-
- Scroll(scrollPosX, scrollPosY); //internally calls wxWindows::Update()!
- updateWindowSizes(); //may show horizontal scroll bar
-}
-
-
-void Grid::redirectRowLabelEvent(wxMouseEvent& event)
-{
- event.m_x = 0;
- if (wxEvtHandler* evtHandler = mainWin_->GetEventHandler())
- evtHandler->ProcessEvent(event);
-
- if (event.ButtonDown() && wxWindow::FindFocus() != mainWin_)
- mainWin_->SetFocus();
-}
-
-
-size_t Grid::getRowCount() const
-{
- return dataView_ ? dataView_->getRowCount() : 0;
-}
-
-
-void Grid::Refresh(bool eraseBackground, const wxRect* rect)
-{
- const size_t rowCountNew = getRowCount();
- if (rowCountOld_ != rowCountNew)
- {
- rowCountOld_ = rowCountNew;
- updateWindowSizes();
- }
-
- if (selection_.maxSize() != rowCountNew) //clear selection only when needed (consider setSelectedRows())
- selection_.init(rowCountNew);
-
- wxScrolledWindow::Refresh(eraseBackground, rect);
-}
-
-
-void Grid::setRowHeight(int height)
-{
- rowLabelWin_->setRowHeight(height);
- updateWindowSizes();
- Refresh();
-}
-
-
-void Grid::setColumnConfig(const std::vector<Grid::ColumnAttribute>& attr)
-{
- //hold ownership of non-visible columns
- oldColAttributes_ = attr;
-
- std::vector<VisibleColumn> visCols;
- for (const ColumnAttribute& ca : attr)
- {
- assert(ca.type_ != ColumnType::NONE);
- if (ca.visible_)
- visCols.emplace_back(ca.type_, ca.offset_, ca.stretch_);
- }
-
- //"ownership" of visible columns is now within Grid
- visibleCols_ = visCols;
-
- updateWindowSizes();
- Refresh();
-}
-
-
-std::vector<Grid::ColumnAttribute> Grid::getColumnConfig() const
-{
- //get non-visible columns (+ outdated visible ones)
- std::vector<ColumnAttribute> output = oldColAttributes_;
-
- auto iterVcols = visibleCols_.begin();
- auto iterVcolsend = visibleCols_.end();
-
- //update visible columns but keep order of non-visible ones!
- for (ColumnAttribute& ca : output)
- if (ca.visible_)
- {
- if (iterVcols != iterVcolsend)
- {
- ca.type_ = iterVcols->type_;
- ca.stretch_ = iterVcols->stretch_;
- ca.offset_ = iterVcols->offset_;
- ++iterVcols;
- }
- else
- assert(false);
- }
- assert(iterVcols == iterVcolsend);
-
- return output;
-}
-
-
-void Grid::showScrollBars(Grid::ScrollBarStatus horizontal, Grid::ScrollBarStatus vertical)
-{
- if (showScrollbarX_ == horizontal &&
- showScrollbarY_ == vertical) return; //support polling!
-
- showScrollbarX_ = horizontal;
- showScrollbarY_ = vertical;
-
-#if defined ZEN_WIN || defined ZEN_MAC
- //handled by Grid::SetScrollbar
-#elif defined ZEN_LINUX //get rid of scrollbars, but preserve scrolling behavior!
- //the following wxGTK approach is pretty much identical to wxWidgets 2.9 ShowScrollbars() code!
-
- auto mapStatus = [](ScrollBarStatus sbStatus) -> GtkPolicyType
- {
- switch (sbStatus)
- {
- case SB_SHOW_AUTOMATIC:
- return GTK_POLICY_AUTOMATIC;
- case SB_SHOW_ALWAYS:
- return GTK_POLICY_ALWAYS;
- case SB_SHOW_NEVER:
- return GTK_POLICY_NEVER;
- }
- assert(false);
- return GTK_POLICY_AUTOMATIC;
- };
-
- GtkWidget* gridWidget = wxWindow::m_widget;
- GtkScrolledWindow* scrolledWindow = GTK_SCROLLED_WINDOW(gridWidget);
- ::gtk_scrolled_window_set_policy(scrolledWindow,
- mapStatus(horizontal),
- mapStatus(vertical));
-#endif
-
- updateWindowSizes();
-}
-
-#if defined ZEN_WIN || defined ZEN_MAC
-void Grid::SetScrollbar(int orientation, int position, int thumbSize, int range, bool refresh)
-{
- /*
- wxWidgets >= 2.9 ShowScrollbars() is next to useless since it doesn't
- honor wxSHOW_SB_ALWAYS on OS X, so let's ditch it and avoid more non-portability surprises
- */
-
- ScrollBarStatus sbStatus = SB_SHOW_AUTOMATIC;
- if (orientation == wxHORIZONTAL)
- sbStatus = showScrollbarX_;
- else if (orientation == wxVERTICAL)
- sbStatus = showScrollbarY_;
- else
- assert(false);
-
- switch (sbStatus)
- {
- case SB_SHOW_AUTOMATIC:
- wxScrolledWindow::SetScrollbar(orientation, position, thumbSize, range, refresh);
- break;
-
- case SB_SHOW_ALWAYS:
- if (range <= 1) //scrollbars would be hidden for range == 0 or 1!
- wxScrolledWindow::SetScrollbar(orientation, 0, 199999, 200000, refresh);
- else
- wxScrolledWindow::SetScrollbar(orientation, position, thumbSize, range, refresh);
- break;
-
- case SB_SHOW_NEVER:
- wxScrolledWindow::SetScrollbar(orientation, 0, 0, 0, refresh);
- break;
- }
-}
-#endif
-
-
-wxWindow& Grid::getCornerWin () { return *cornerWin_; }
-wxWindow& Grid::getRowLabelWin() { return *rowLabelWin_; }
-wxWindow& Grid::getColLabelWin() { return *colLabelWin_; }
-wxWindow& Grid::getMainWin () { return *mainWin_; }
-const wxWindow& Grid::getMainWin() const { return *mainWin_; }
-
-
-Opt<Grid::ColAction> Grid::clientPosToColumnAction(const wxPoint& pos) const
-{
- const int absPosX = CalcUnscrolledPosition(pos).x;
- if (absPosX >= 0)
- {
- const int resizeTolerance = allowColumnResize_ ? COLUMN_RESIZE_TOLERANCE : 0;
- std::vector<ColumnWidth> absWidths = getColWidths(); //resolve stretched widths
-
- int accuWidth = 0;
- for (size_t col = 0; col < absWidths.size(); ++col)
- {
- accuWidth += absWidths[col].width_;
- if (std::abs(absPosX - accuWidth) < resizeTolerance)
- {
- ColAction out;
- out.wantResize = true;
- out.col = col;
- return out;
- }
- else if (absPosX < accuWidth)
- {
- ColAction out;
- out.wantResize = false;
- out.col = col;
- return out;
- }
- }
- }
- return NoValue();
-}
-
-
-void Grid::moveColumn(size_t colFrom, size_t colTo)
-{
- if (colFrom < visibleCols_.size() &&
- colTo < visibleCols_.size() &&
- colTo != colFrom)
- {
- const VisibleColumn colAtt = visibleCols_[colFrom];
- visibleCols_.erase (visibleCols_.begin() + colFrom);
- visibleCols_.insert(visibleCols_.begin() + colTo, colAtt);
- }
-}
-
-
-ptrdiff_t Grid::clientPosToMoveTargetColumn(const wxPoint& pos) const
-{
-
- const int absPosX = CalcUnscrolledPosition(pos).x;
-
- int accWidth = 0;
- std::vector<ColumnWidth> absWidths = getColWidths(); //resolve negative/stretched widths
- for (auto itCol = absWidths.begin(); itCol != absWidths.end(); ++itCol)
- {
- const int width = itCol->width_; //beware dreaded unsigned conversions!
- accWidth += width;
-
- if (absPosX < accWidth - width / 2)
- return itCol - absWidths.begin();
- }
- return absWidths.size();
-}
-
-
-ColumnType Grid::colToType(size_t col) const
-{
- if (col < visibleCols_.size())
- return visibleCols_[col].type_;
- return ColumnType::NONE;
-}
-
-
-ptrdiff_t Grid::getRowAtPos(int posY) const { return rowLabelWin_->getRowAtPos(posY); }
-
-
-Grid::ColumnPosInfo Grid::getColumnAtPos(int posX) const
-{
- if (posX >= 0)
- {
- int accWidth = 0;
- for (const ColumnWidth& cw : getColWidths())
- {
- accWidth += cw.width_;
- if (posX < accWidth)
- return { cw.type_, posX + cw.width_ - accWidth, cw.width_ };
- }
- }
- return { ColumnType::NONE, 0, 0 };
-}
-
-
-wxRect Grid::getColumnLabelArea(ColumnType colType) const
-{
- std::vector<ColumnWidth> absWidths = getColWidths(); //resolve negative/stretched widths
-
- //colType is not unique in general, but *this* function expects it!
- assert(std::count_if(absWidths.begin(), absWidths.end(), [&](const ColumnWidth& cw) { return cw.type_ == colType; }) <= 1);
-
- auto itCol = std::find_if(absWidths.begin(), absWidths.end(), [&](const ColumnWidth& cw) { return cw.type_ == colType; });
- if (itCol != absWidths.end())
- {
- ptrdiff_t posX = 0;
- for (auto it = absWidths.begin(); it != itCol; ++it)
- posX += it->width_;
-
- return wxRect(wxPoint(posX, 0), wxSize(itCol->width_, colLabelHeight_));
- }
- return wxRect();
-}
-
-
-void Grid::refreshCell(size_t row, ColumnType colType)
-{
- const wxRect& colArea = getColumnLabelArea(colType); //returns empty rect if column not found
- const wxRect& rowArea = rowLabelWin_->getRowLabelArea(row); //returns empty rect if row not found
- if (colArea.height > 0 && rowArea.height > 0)
- {
- const wxPoint topLeft = CalcScrolledPosition(wxPoint(colArea.x, rowArea.y)); //absolute -> client coordinates
- const wxRect cellArea(topLeft, wxSize(colArea.width, rowArea.height));
-
- getMainWin().RefreshRect(cellArea, false);
- }
-}
-
-
-void Grid::setGridCursor(size_t row)
-{
- mainWin_->setCursor(row, row);
- makeRowVisible(row);
-
- selection_.clear(); //clear selection, do NOT fire event
- selectRangeAndNotify(row, row, true /*positive*/, nullptr /*mouseInitiated*/); //set new selection + fire event
-
- mainWin_->Refresh();
- rowLabelWin_->Refresh(); //row labels! (Kubuntu)
-}
-
-
-void Grid::selectWithCursor(ptrdiff_t row)
-{
- const size_t anchorRow = mainWin_->getAnchor();
-
- mainWin_->setCursor(row, anchorRow);
- makeRowVisible(row);
-
- selection_.clear(); //clear selection, do NOT fire event
- selectRangeAndNotify(anchorRow, row, true /*positive*/, nullptr /*mouseInitiated*/); //set new selection + fire event
-
- mainWin_->Refresh();
- rowLabelWin_->Refresh();
-}
-
-
-void Grid::makeRowVisible(size_t row)
-{
- const wxRect labelRect = rowLabelWin_->getRowLabelArea(row); //returns empty rect if row not found
- if (labelRect.height > 0)
- {
- int scrollPosX = 0;
- GetViewStart(&scrollPosX, nullptr);
-
- int pixelsPerUnitY = 0;
- GetScrollPixelsPerUnit(nullptr, &pixelsPerUnitY);
- if (pixelsPerUnitY <= 0) return;
-
- const int clientPosY = CalcScrolledPosition(labelRect.GetTopLeft()).y;
- if (clientPosY < 0)
- {
- const int scrollPosY = labelRect.y / pixelsPerUnitY;
- Scroll(scrollPosX, scrollPosY); //internally calls wxWindows::Update()!
- updateWindowSizes(); //may show horizontal scroll bar
- }
- else if (clientPosY + labelRect.height > rowLabelWin_->GetClientSize().GetHeight())
- {
- auto execScroll = [&](int clientHeight)
- {
- const int scrollPosY = std::ceil((labelRect.y - clientHeight +
- labelRect.height) / static_cast<double>(pixelsPerUnitY));
- Scroll(scrollPosX, scrollPosY);
- updateWindowSizes(); //may show horizontal scroll bar
- };
-
- const int clientHeightBefore = rowLabelWin_->GetClientSize().GetHeight();
- execScroll(clientHeightBefore);
-
- //client height may decrease after scroll due to a new horizontal scrollbar, resulting in a partially visible last row
- const int clientHeightAfter = rowLabelWin_->GetClientSize().GetHeight();
- if (clientHeightAfter < clientHeightBefore)
- execScroll(clientHeightAfter);
- }
- }
-}
-
-
-void Grid::selectRangeAndNotify(ptrdiff_t rowFrom, ptrdiff_t rowTo, bool positive, const GridClickEvent* mouseInitiated)
-{
- //sort + convert to half-open range
- auto rowFirst = std::min(rowFrom, rowTo);
- auto rowLast = std::max(rowFrom, rowTo) + 1;
-
- const size_t rowCount = getRowCount();
- numeric::clamp<ptrdiff_t>(rowFirst, 0, rowCount);
- numeric::clamp<ptrdiff_t>(rowLast, 0, rowCount);
-
- selection_.selectRange(rowFirst, rowLast, positive);
-
- //notify event
- GridRangeSelectEvent selectionEvent(rowFirst, rowLast, positive, mouseInitiated);
- if (wxEvtHandler* evtHandler = GetEventHandler())
- evtHandler->ProcessEvent(selectionEvent);
-
- mainWin_->Refresh();
-}
-
-
-void Grid::scrollTo(size_t row)
-{
- const wxRect labelRect = rowLabelWin_->getRowLabelArea(row); //returns empty rect if row not found
- if (labelRect.height > 0)
- {
- int pixelsPerUnitY = 0;
- GetScrollPixelsPerUnit(nullptr, &pixelsPerUnitY);
- if (pixelsPerUnitY > 0)
- {
- const int scrollPosYNew = labelRect.y / pixelsPerUnitY;
- int scrollPosXOld = 0;
- int scrollPosYOld = 0;
- GetViewStart(&scrollPosXOld, &scrollPosYOld);
-
- if (scrollPosYOld != scrollPosYNew) //support polling
- {
- Scroll(scrollPosXOld, scrollPosYNew); //internally calls wxWindows::Update()!
- updateWindowSizes(); //may show horizontal scroll bar
- Refresh();
- }
- }
- }
-}
-
-
-bool Grid::Enable(bool enable)
-{
- Refresh();
- return wxScrolledWindow::Enable(enable);
-}
-
-
-size_t Grid::getGridCursor() const
-{
- return mainWin_->getCursor();
-}
-
-
-int Grid::getBestColumnSize(size_t col) const
-{
- if (dataView_ && col < visibleCols_.size())
- {
- const ColumnType type = visibleCols_[col].type_;
-
- wxClientDC dc(mainWin_);
- dc.SetFont(mainWin_->GetFont()); //harmonize with MainWin::render()
-
- int maxSize = 0;
-
- auto rowRange = rowLabelWin_->getRowsOnClient(mainWin_->GetClientRect()); //returns range [begin, end)
- for (auto row = rowRange.first; row < rowRange.second; ++row)
- maxSize = std::max(maxSize, dataView_->getBestSize(dc, row, type));
-
- return maxSize;
- }
- return -1;
-}
-
-
-void Grid::setColumnWidth(int width, size_t col, GridEventPolicy columnResizeEventPolicy, bool notifyAsync)
-{
- if (col < visibleCols_.size())
- {
- VisibleColumn& vcRs = visibleCols_[col];
-
- const std::vector<int> stretchedWidths = getColStretchedWidths(mainWin_->GetClientSize().GetWidth());
- if (stretchedWidths.size() != visibleCols_.size())
- {
- assert(false);
- return;
- }
- //CAVEATS:
- //I. fixed-size columns: normalize offset so that resulting width is at least COLUMN_MIN_WIDTH: this is NOT enforced by getColWidths()!
- //II. stretched columns: do not allow user to set offsets so small that they result in negative (non-normalized) widths: this gives an
- //unusual delay when enlarging the column again later
- width = std::max(width, COLUMN_MIN_WIDTH);
-
- vcRs.offset_ = width - stretchedWidths[col]; //width := stretchedWidth + offset
-
- //III. resizing any column should normalize *all* other stretched columns' offsets considering current mainWinWidth!
- // test case:
- //1. have columns, both fixed-size and stretched, fit whole window width
- //2. shrink main window width so that horizontal scrollbars are shown despite the streched column
- //3. shrink a fixed-size column so that the scrollbars vanish and columns cover full width again
- //4. now verify that the stretched column is resizing immediately if main window is enlarged again
- for (size_t col2 = 0; col2 < visibleCols_.size(); ++col2)
- if (visibleCols_[col2].stretch_ > 0) //normalize stretched columns only
- visibleCols_[col2].offset_ = std::max(visibleCols_[col2].offset_, COLUMN_MIN_WIDTH - stretchedWidths[col2]);
-
- if (columnResizeEventPolicy == ALLOW_GRID_EVENT)
- {
- GridColumnResizeEvent sizeEvent(vcRs.offset_, vcRs.type_);
- if (wxEvtHandler* evtHandler = GetEventHandler())
- {
- if (notifyAsync)
- evtHandler->AddPendingEvent(sizeEvent);
- else
- evtHandler->ProcessEvent(sizeEvent);
- }
- }
- }
- else
- assert(false);
-}
-
-
-void Grid::autoSizeColumns(GridEventPolicy columnResizeEventPolicy)
-{
- if (allowColumnResize_)
- {
- for (size_t col = 0; col < visibleCols_.size(); ++col)
- {
- const int bestWidth = getBestColumnSize(col); //return -1 on error
- if (bestWidth >= 0)
- setColumnWidth(bestWidth, col, columnResizeEventPolicy, true);
- }
- updateWindowSizes();
- Refresh();
- }
-}
-
-
-std::vector<int> Grid::getColStretchedWidths(int clientWidth) const //final width = (normalized) (stretchedWidth + offset)
-{
- assert(clientWidth >= 0);
- clientWidth = std::max(clientWidth, 0);
- int stretchTotal = 0;
- for (const VisibleColumn& vc : visibleCols_)
- {
- assert(vc.stretch_ >= 0);
- stretchTotal += vc.stretch_;
- }
-
- int remainingWidth = clientWidth;
-
- std::vector<int> output;
-
- if (stretchTotal <= 0)
- output.resize(visibleCols_.size()); //fill with zeros
- else
- {
- for (const VisibleColumn& vc : visibleCols_)
- {
- const int width = clientWidth * vc.stretch_ / stretchTotal; //rounds down!
- output.push_back(width);
- remainingWidth -= width;
- }
-
- //distribute *all* of clientWidth: should suffice to enlarge the first few stretched columns; no need to minimize total absolute error of distribution
- if (remainingWidth > 0)
- for (size_t col2 = 0; col2 < visibleCols_.size(); ++col2)
- if (visibleCols_[col2].stretch_ > 0)
- {
- ++output[col2];
- if (--remainingWidth == 0)
- break;
- }
- assert(remainingWidth == 0);
- }
- return output;
-}
-
-
-std::vector<Grid::ColumnWidth> Grid::getColWidths() const
-{
- return getColWidths(mainWin_->GetClientSize().GetWidth());
-}
-
-
-std::vector<Grid::ColumnWidth> Grid::getColWidths(int mainWinWidth) const //evaluate stretched columns
-{
- const std::vector<int> stretchedWidths = getColStretchedWidths(mainWinWidth);
- assert(stretchedWidths.size() == visibleCols_.size());
-
- std::vector<ColumnWidth> output;
- for (size_t col2 = 0; col2 < visibleCols_.size(); ++col2)
- {
- const auto& vc = visibleCols_[col2];
- int width = stretchedWidths[col2] + vc.offset_;
-
- if (vc.stretch_ > 0)
- width = std::max(width, COLUMN_MIN_WIDTH); //normalization really needed here: e.g. smaller main window would result in negative width
- else
- width = std::max(width, 0); //support smaller width than COLUMN_MIN_WIDTH if set via configuration
-
- output.emplace_back(vc.type_, width);
- }
- return output;
-}
-
-
-int Grid::getColWidthsSum(int mainWinWidth) const
-{
- int sum = 0;
- for (const ColumnWidth& cw : getColWidths(mainWinWidth))
- sum += cw.width_;
- return sum;
-};
+// *****************************************************************************
+// * This file is part of the FreeFileSync project. It is distributed under *
+// * GNU General Public License: http://www.gnu.org/licenses/gpl-3.0 *
+// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
+// *****************************************************************************
+
+#include "grid.h"
+#include <cassert>
+#include <set>
+#include <chrono>
+#include <wx/settings.h>
+#include <wx/listbox.h>
+#include <wx/tooltip.h>
+#include <wx/timer.h>
+#include <wx/utils.h>
+//#include <zen/tick_count.h>
+#include <zen/string_tools.h>
+#include <zen/scope_guard.h>
+#include <zen/utf.h>
+#include <zen/format_unit.h>
+#include "dc.h"
+
+ #include <gtk/gtk.h>
+
+using namespace zen;
+
+
+wxColor Grid::getColorSelectionGradientFrom() { return { 137, 172, 255 }; } //blue: HSL: 158, 255, 196 HSV: 222, 0.46, 1
+wxColor Grid::getColorSelectionGradientTo () { return { 225, 234, 255 }; } // HSL: 158, 255, 240 HSV: 222, 0.12, 1
+
+const int GridData::COLUMN_GAP_LEFT = 4;
+
+
+void zen::clearArea(wxDC& dc, const wxRect& rect, const wxColor& col)
+{
+ wxDCPenChanger dummy (dc, col);
+ wxDCBrushChanger dummy2(dc, col);
+ dc.DrawRectangle(rect);
+}
+
+
+namespace
+{
+//let's NOT create wxWidgets objects statically:
+//------------------------------ Grid Parameters --------------------------------
+inline wxColor getColorLabelText() { return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); }
+inline wxColor getColorGridLine() { return { 192, 192, 192 }; } //light grey
+
+inline wxColor getColorLabelGradientFrom() { return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); }
+inline wxColor getColorLabelGradientTo () { return { 200, 200, 200 }; } //light grey
+
+inline wxColor getColorLabelGradientFocusFrom() { return getColorLabelGradientFrom(); }
+inline wxColor getColorLabelGradientFocusTo () { return Grid::getColorSelectionGradientFrom(); }
+
+const double MOUSE_DRAG_ACCELERATION = 1.5; //unit: [rows / (pixel * sec)] -> same value like Explorer!
+const int DEFAULT_COL_LABEL_BORDER = 6; //top + bottom border in addition to label height
+const int COLUMN_MOVE_DELAY = 5; //unit: [pixel] (from Explorer)
+const int COLUMN_MIN_WIDTH = 40; //only honored when resizing manually!
+const int ROW_LABEL_BORDER = 3;
+const int COLUMN_RESIZE_TOLERANCE = 6; //unit [pixel]
+const int COLUMN_FILL_GAP_TOLERANCE = 10; //enlarge column to fill full width when resizing
+
+const bool fillGapAfterColumns = true; //draw rows/column label to fill full window width; may become an instance variable some time?
+}
+
+//----------------------------------------------------------------------------------------------------------------
+const wxEventType zen::EVENT_GRID_MOUSE_LEFT_DOUBLE = wxNewEventType();
+const wxEventType zen::EVENT_GRID_MOUSE_LEFT_DOWN = wxNewEventType();
+const wxEventType zen::EVENT_GRID_MOUSE_LEFT_UP = wxNewEventType();
+const wxEventType zen::EVENT_GRID_MOUSE_RIGHT_DOWN = wxNewEventType();
+const wxEventType zen::EVENT_GRID_MOUSE_RIGHT_UP = wxNewEventType();
+const wxEventType zen::EVENT_GRID_SELECT_RANGE = wxNewEventType();
+const wxEventType zen::EVENT_GRID_COL_LABEL_MOUSE_LEFT = wxNewEventType();
+const wxEventType zen::EVENT_GRID_COL_LABEL_MOUSE_RIGHT = wxNewEventType();
+const wxEventType zen::EVENT_GRID_COL_RESIZE = wxNewEventType();
+//----------------------------------------------------------------------------------------------------------------
+
+void GridData::renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected)
+{
+ drawCellBackground(dc, rect, enabled, selected, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
+}
+
+
+void GridData::renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover)
+{
+ wxRect rectTmp = drawCellBorder(dc, rect);
+
+ rectTmp.x += COLUMN_GAP_LEFT;
+ rectTmp.width -= COLUMN_GAP_LEFT;
+ drawCellText(dc, rectTmp, getValue(row, colType));
+}
+
+
+int GridData::getBestSize(wxDC& dc, size_t row, ColumnType colType)
+{
+ return dc.GetTextExtent(getValue(row, colType)).GetWidth() + 2 * COLUMN_GAP_LEFT + 1; //gap on left and right side + border
+}
+
+
+wxRect GridData::drawCellBorder(wxDC& dc, const wxRect& rect) //returns remaining rectangle
+{
+ wxDCPenChanger dummy2(dc, getColorGridLine());
+ dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight());
+ dc.DrawLine(rect.GetBottomRight(), rect.GetTopRight() + wxPoint(0, -1));
+
+ return wxRect(rect.GetTopLeft(), wxSize(rect.width - 1, rect.height - 1));
+}
+
+
+void GridData::drawCellBackground(wxDC& dc, const wxRect& rect, bool enabled, bool selected, const wxColor& backgroundColor)
+{
+ if (enabled)
+ {
+ if (selected)
+ dc.GradientFillLinear(rect, Grid::getColorSelectionGradientFrom(), Grid::getColorSelectionGradientTo(), wxEAST);
+ else
+ clearArea(dc, rect, backgroundColor);
+ }
+ else
+ clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE));
+}
+
+
+wxSize GridData::drawCellText(wxDC& dc, const wxRect& rect, const std::wstring& text, int alignment)
+{
+ /*
+ performance notes (Windows):
+ - wxDC::GetTextExtent() is by far the most expensive call (20x more expensive than wxDC::DrawText())
+ - wxDC::DrawLabel() is inefficiently implemented; internally calls: wxDC::GetMultiLineTextExtent(), wxDC::GetTextExtent(), wxDC::DrawText()
+ - wxDC::GetMultiLineTextExtent() calls wxDC::GetTextExtent()
+ - wxDC::DrawText also calls wxDC::GetTextExtent()!!
+ => wxDC::DrawLabel() boils down to 3(!) calls to wxDC::GetTextExtent()!!!
+ - wxDC::DrawLabel results in GetTextExtent() call even for empty strings!!!
+ => skip the wxDC::DrawLabel() cruft and directly call wxDC::DrawText!
+ */
+
+ //truncate large texts and add ellipsis
+ assert(!contains(text, L"\n"));
+ const wchar_t ELLIPSIS = L'\u2026'; //"..."
+
+ std::wstring textTrunc = text;
+ wxSize extentTrunc = dc.GetTextExtent(textTrunc);
+ if (extentTrunc.GetWidth() > rect.width)
+ {
+ //unlike Windows 7 Explorer, we truncate UTF-16 correctly: e.g. CJK-Ideogramm encodes to TWO wchar_t: utfCvrtTo<std::wstring>("\xf0\xa4\xbd\x9c");
+ size_t low = 0; //number of unicode chars!
+ size_t high = unicodeLength(text); //
+ if (high > 1)
+ for (;;)
+ {
+ const size_t middle = (low + high) / 2; //=> never 0 when "high - low > 1"
+ if (high - low <= 1)
+ {
+ if (low == 0)
+ {
+ textTrunc = ELLIPSIS;
+ extentTrunc = dc.GetTextExtent(ELLIPSIS);
+ }
+ break;
+ }
+
+ const std::wstring& candidate = std::wstring(strBegin(text), findUnicodePos(text, middle)) + ELLIPSIS;
+ const wxSize extentCand = dc.GetTextExtent(candidate); //perf: most expensive call of this routine!
+
+ if (extentCand.GetWidth() <= rect.width)
+ {
+ low = middle;
+ textTrunc = candidate;
+ extentTrunc = extentCand;
+ }
+ else
+ high = middle;
+ }
+ }
+
+ wxPoint pt = rect.GetTopLeft();
+ if (alignment & wxALIGN_RIGHT) //note: wxALIGN_LEFT == 0!
+ pt.x += rect.width - extentTrunc.GetWidth();
+ else if (alignment & wxALIGN_CENTER_HORIZONTAL)
+ pt.x += (rect.width - extentTrunc.GetWidth()) / 2;
+
+ if (alignment & wxALIGN_BOTTOM) //note: wxALIGN_TOP == 0!
+ pt.y += rect.height - extentTrunc.GetHeight();
+ else if (alignment & wxALIGN_CENTER_VERTICAL)
+ pt.y += (rect.height - extentTrunc.GetHeight()) / 2;
+
+ RecursiveDcClipper clip(dc, rect);
+ dc.DrawText(textTrunc, pt);
+ return extentTrunc;
+}
+
+
+void GridData::renderColumnLabel(Grid& grid, wxDC& dc, const wxRect& rect, ColumnType colType, bool highlighted)
+{
+ wxRect rectTmp = drawColumnLabelBorder(dc, rect);
+ drawColumnLabelBackground(dc, rectTmp, highlighted);
+
+ rectTmp.x += COLUMN_GAP_LEFT;
+ rectTmp.width -= COLUMN_GAP_LEFT;
+ drawColumnLabelText(dc, rectTmp, getColumnLabel(colType));
+}
+
+
+wxRect GridData::drawColumnLabelBorder(wxDC& dc, const wxRect& rect) //returns remaining rectangle
+{
+ //draw white line
+ {
+ wxDCPenChanger dummy(dc, *wxWHITE_PEN);
+ dc.DrawLine(rect.GetTopLeft(), rect.GetBottomLeft());
+ }
+
+ //draw border (with gradient)
+ {
+ wxDCPenChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_3DSHADOW));
+ dc.GradientFillLinear(wxRect(rect.GetTopRight(), rect.GetBottomRight()), getColorLabelGradientFrom(), dc.GetPen().GetColour(), wxSOUTH);
+ dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight() + wxPoint(1, 0));
+ }
+
+ return wxRect(rect.x + 1, rect.y, rect.width - 2, rect.height - 1); //we really don't like wxRect::Deflate, do we?
+}
+
+
+void GridData::drawColumnLabelBackground(wxDC& dc, const wxRect& rect, bool highlighted)
+{
+ if (highlighted)
+ dc.GradientFillLinear(rect, getColorLabelGradientFocusFrom(), getColorLabelGradientFocusTo(), wxSOUTH);
+ else //regular background gradient
+ dc.GradientFillLinear(rect, getColorLabelGradientFrom(), getColorLabelGradientTo(), wxSOUTH); //clear overlapping cells
+}
+
+
+void GridData::drawColumnLabelText(wxDC& dc, const wxRect& rect, const std::wstring& text)
+{
+ wxDCTextColourChanger dummy(dc, getColorLabelText()); //accessibility: always set both foreground AND background colors!
+ drawCellText(dc, rect, text, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
+}
+
+//----------------------------------------------------------------------------------------------------------------
+/*
+ SubWindow
+ /|\
+ |
+ -----------------------------------
+ | | | |
+CornerWin RowLabelWin ColLabelWin MainWin
+
+*/
+class Grid::SubWindow : public wxWindow
+{
+public:
+ SubWindow(Grid& parent) :
+ wxWindow(&parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxWANTS_CHARS | wxBORDER_NONE, wxPanelNameStr),
+ parent_(parent)
+ {
+ Connect(wxEVT_PAINT, wxPaintEventHandler(SubWindow::onPaintEvent), nullptr, this);
+ Connect(wxEVT_SIZE, wxSizeEventHandler (SubWindow::onSizeEvent), nullptr, this);
+ //http://wiki.wxwidgets.org/Flicker-Free_Drawing
+ Connect(wxEVT_ERASE_BACKGROUND, wxEraseEventHandler(SubWindow::onEraseBackGround), nullptr, this);
+
+ //SetDoubleBuffered(true); slow as hell!
+
+ SetBackgroundStyle(wxBG_STYLE_PAINT);
+
+ Connect(wxEVT_SET_FOCUS, wxFocusEventHandler(SubWindow::onFocus), nullptr, this);
+ Connect(wxEVT_KILL_FOCUS, wxFocusEventHandler(SubWindow::onFocus), nullptr, this);
+ Connect(wxEVT_CHILD_FOCUS, wxEventHandler(SubWindow::onChildFocus), nullptr, this);
+
+ Connect(wxEVT_LEFT_DOWN, wxMouseEventHandler(SubWindow::onMouseLeftDown ), nullptr, this);
+ Connect(wxEVT_LEFT_UP, wxMouseEventHandler(SubWindow::onMouseLeftUp ), nullptr, this);
+ Connect(wxEVT_LEFT_DCLICK, wxMouseEventHandler(SubWindow::onMouseLeftDouble), nullptr, this);
+ Connect(wxEVT_RIGHT_DOWN, wxMouseEventHandler(SubWindow::onMouseRightDown ), nullptr, this);
+ Connect(wxEVT_RIGHT_UP, wxMouseEventHandler(SubWindow::onMouseRightUp ), nullptr, this);
+ Connect(wxEVT_MOTION, wxMouseEventHandler(SubWindow::onMouseMovement ), nullptr, this);
+ Connect(wxEVT_LEAVE_WINDOW, wxMouseEventHandler(SubWindow::onLeaveWindow ), nullptr, this);
+ Connect(wxEVT_MOUSEWHEEL, wxMouseEventHandler(SubWindow::onMouseWheel ), nullptr, this);
+ Connect(wxEVT_MOUSE_CAPTURE_LOST, wxMouseCaptureLostEventHandler(SubWindow::onMouseCaptureLost), nullptr, this);
+
+ Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(SubWindow::onKeyDown), nullptr, this);
+
+ assert(GetClientAreaOrigin() == wxPoint()); //generally assumed when dealing with coordinates below
+ }
+ Grid& refParent() { return parent_; }
+ const Grid& refParent() const { return parent_; }
+
+ template <class T>
+ bool sendEventNow(T&& event) //take both "rvalue + lvalues", return "true" if a suitable event handler function was found and executed, and the function did not call wxEvent::Skip.
+ {
+ if (wxEvtHandler* evtHandler = parent_.GetEventHandler())
+ return evtHandler->ProcessEvent(event);
+ return false;
+ }
+
+protected:
+ void setToolTip(const std::wstring& text) //proper fix for wxWindow
+ {
+ wxToolTip* tt = GetToolTip();
+
+ const wxString oldText = tt ? tt->GetTip() : wxString();
+ if (text != oldText)
+ {
+ if (text.empty())
+ SetToolTip(nullptr); //wxGTK doesn't allow wxToolTip with empty text!
+ else
+ {
+ //wxWidgets bug: tooltip multiline property is defined by first tooltip text containing newlines or not (same is true for maximum width)
+ if (!tt)
+ SetToolTip(new wxToolTip(L"a b\n\
+ a b")); //ugly, but working (on Windows)
+ tt = GetToolTip(); //should be bound by now
+ assert(tt);
+ if (tt)
+ tt->SetTip(text);
+ }
+ }
+ }
+
+private:
+ virtual void render(wxDC& dc, const wxRect& rect) = 0;
+
+ virtual void onFocus(wxFocusEvent& event) { event.Skip(); }
+ virtual void onChildFocus(wxEvent& event) {} //wxGTK::wxScrolledWindow automatically scrolls to child window when child gets focus -> prevent!
+
+ virtual void onMouseLeftDown (wxMouseEvent& event) { event.Skip(); }
+ virtual void onMouseLeftUp (wxMouseEvent& event) { event.Skip(); }
+ virtual void onMouseLeftDouble(wxMouseEvent& event) { event.Skip(); }
+ virtual void onMouseRightDown (wxMouseEvent& event) { event.Skip(); }
+ virtual void onMouseRightUp (wxMouseEvent& event) { event.Skip(); }
+ virtual void onMouseMovement (wxMouseEvent& event) { event.Skip(); }
+ virtual void onLeaveWindow (wxMouseEvent& event) { event.Skip(); }
+ virtual void onMouseCaptureLost(wxMouseCaptureLostEvent& event) { event.Skip(); }
+
+ void onKeyDown(wxKeyEvent& event)
+ {
+ if (!sendEventNow(event)) //let parent collect all key events
+ event.Skip();
+ }
+
+ void onMouseWheel(wxMouseEvent& event)
+ {
+ /*
+ MSDN, WM_MOUSEWHEEL: "Sent to the focus window when the mouse wheel is rotated.
+ The DefWindowProc function propagates the message to the window's parent.
+ There should be no internal forwarding of the message, since DefWindowProc propagates
+ it up the parent chain until it finds a window that processes it."
+
+ On OS X there is no such propagation! => we need a redirection (the same wxGrid implements)
+ */
+
+ //new wxWidgets 3.0 screw-up for GTK2: wxScrollHelperEvtHandler::ProcessEvent() ignores wxEVT_MOUSEWHEEL events
+ //thereby breaking the scenario of redirection to parent we need here (but also breaking their very own wxGrid sample)
+ //=> call wxScrolledWindow mouse wheel handler directly
+ parent_.HandleOnMouseWheel(event);
+
+ //if (!sendEventNow(event))
+ // event.Skip();
+ }
+
+ void onPaintEvent(wxPaintEvent& event)
+ {
+ //wxAutoBufferedPaintDC dc(this); -> this one happily fucks up for RTL layout by not drawing the first column (x = 0)!
+ BufferedPaintDC dc(*this, doubleBuffer_);
+
+ assert(GetSize() == GetClientSize());
+
+ const wxRegion& updateReg = GetUpdateRegion();
+ for (wxRegionIterator it = updateReg; it; ++it)
+ render(dc, it.GetRect());
+ }
+
+ void onSizeEvent(wxSizeEvent& event)
+ {
+ Refresh();
+ event.Skip();
+ }
+
+ void onEraseBackGround(wxEraseEvent& event) {}
+
+ Grid& parent_;
+ Opt<wxBitmap> doubleBuffer_;
+};
+
+//----------------------------------------------------------------------------------------------------------------
+//----------------------------------------------------------------------------------------------------------------
+
+class Grid::CornerWin : public SubWindow
+{
+public:
+ CornerWin(Grid& parent) : SubWindow(parent) {}
+
+private:
+ bool AcceptsFocus() const override { return false; }
+
+ void render(wxDC& dc, const wxRect& rect) override
+ {
+ const wxRect& clientRect = GetClientRect();
+
+ dc.GradientFillLinear(clientRect, getColorLabelGradientFrom(), getColorLabelGradientTo(), wxSOUTH);
+
+ dc.SetPen(wxSystemSettings::GetColour(wxSYS_COLOUR_3DSHADOW));
+
+ {
+ wxDCPenChanger dummy(dc, getColorLabelGradientFrom());
+ dc.DrawLine(clientRect.GetTopLeft(), clientRect.GetTopRight());
+ }
+
+ dc.GradientFillLinear(wxRect(clientRect.GetBottomLeft (), clientRect.GetTopLeft ()), getColorLabelGradientFrom(), dc.GetPen().GetColour(), wxSOUTH);
+ dc.GradientFillLinear(wxRect(clientRect.GetBottomRight(), clientRect.GetTopRight()), getColorLabelGradientFrom(), dc.GetPen().GetColour(), wxSOUTH);
+
+ dc.DrawLine(clientRect.GetBottomLeft(), clientRect.GetBottomRight());
+
+ wxRect rectShrinked = clientRect;
+ rectShrinked.Deflate(1);
+ dc.SetPen(*wxWHITE_PEN);
+
+ //dc.DrawLine(clientRect.GetTopLeft(), clientRect.GetTopRight() + wxPoint(1, 0));
+ dc.DrawLine(rectShrinked.GetTopLeft(), rectShrinked.GetBottomLeft() + wxPoint(0, 1));
+ }
+};
+
+//----------------------------------------------------------------------------------------------------------------
+//----------------------------------------------------------------------------------------------------------------
+
+class Grid::RowLabelWin : public SubWindow
+{
+public:
+ RowLabelWin(Grid& parent) :
+ SubWindow(parent),
+ rowHeight_(parent.GetCharHeight() + 2 + 1) {} //default height; don't call any functions on "parent" other than those from wxWindow during construction!
+ //2 for some more space, 1 for bottom border (gives 15 + 2 + 1 on Windows, 17 + 2 + 1 on Ubuntu)
+
+ int getBestWidth(ptrdiff_t rowFrom, ptrdiff_t rowTo)
+ {
+ wxClientDC dc(this);
+
+ wxFont labelFont = GetFont();
+ //labelFont.SetWeight(wxFONTWEIGHT_BOLD);
+ dc.SetFont(labelFont); //harmonize with RowLabelWin::render()!
+
+ int bestWidth = 0;
+ for (ptrdiff_t i = rowFrom; i <= rowTo; ++i)
+ bestWidth = std::max(bestWidth, dc.GetTextExtent(formatRow(i)).GetWidth() + 2 * ROW_LABEL_BORDER);
+ return bestWidth;
+ }
+
+ size_t getLogicalHeight() const { return refParent().getRowCount() * rowHeight_; }
+
+ ptrdiff_t getRowAtPos(ptrdiff_t posY) const //returns < 0 on invalid input, else row number within: [0, rowCount]; rowCount if out of range
+ {
+ if (posY >= 0 && rowHeight_ > 0)
+ {
+ const size_t row = posY / rowHeight_;
+ return std::min(row, refParent().getRowCount());
+ }
+ return -1;
+ }
+
+ int getRowHeight() const { return rowHeight_; } //guarantees to return size >= 1 !
+ void setRowHeight(int height) { assert(height > 0); rowHeight_ = std::max(1, height); }
+
+ wxRect getRowLabelArea(size_t row) const //returns empty rect if row not found
+ {
+ assert(GetClientAreaOrigin() == wxPoint());
+ if (row < refParent().getRowCount())
+ return wxRect(wxPoint(0, rowHeight_ * row),
+ wxSize(GetClientSize().GetWidth(), rowHeight_));
+ return wxRect();
+ }
+
+ std::pair<ptrdiff_t, ptrdiff_t> getRowsOnClient(const wxRect& clientRect) const //returns range [begin, end)
+ {
+ const int yFrom = refParent().CalcUnscrolledPosition(clientRect.GetTopLeft ()).y;
+ const int yTo = refParent().CalcUnscrolledPosition(clientRect.GetBottomRight()).y;
+
+ return std::make_pair(std::max(yFrom / rowHeight_, 0),
+ std::min<ptrdiff_t>((yTo / rowHeight_) + 1, refParent().getRowCount()));
+ }
+
+private:
+ static std::wstring formatRow(size_t row) { return toGuiString(row + 1); } //convert number to std::wstring including thousands separator
+
+ bool AcceptsFocus() const override { return false; }
+
+ void render(wxDC& dc, const wxRect& rect) override
+ {
+ /*
+ IsEnabled() vs IsThisEnabled() since wxWidgets 2.9.5:
+
+ void wxWindowBase::NotifyWindowOnEnableChange(), called from bool wxWindowBase::Enable(), fails to refresh
+ child elements when disabling a IsTopLevel() dialog, e.g. when showing a modal dialog.
+ The unfortunate effect on XP for using IsEnabled() when rendering the grid is that the user can move the modal dialog
+ and *draw* with it on the background while the grid refreshes as disabled incrementally!
+
+ => Don't use IsEnabled() since it considers the top level window. The brittle wxWidgets implementation is right in their intention,
+ but wrong when not refreshing child-windows: the control designer decides how his control should be rendered!
+
+ => IsThisEnabled() OTOH is too shallow and does not consider parent windows which are not top level.
+
+ The perfect solution would be a bool ShouldBeDrawnActive() { return "IsEnabled() but ignore effects of showing a modal dialog"; }
+
+ However "IsThisEnabled()" is good enough (same like the old IsEnabled() on wxWidgets 2.8.12) and it avoids this pathetic behavior on XP.
+ (Similar problem on Win 7: e.g. directly click sync button without comparing first)
+ */
+ if (IsThisEnabled())
+ clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
+ else
+ clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE));
+
+ wxFont labelFont = GetFont();
+ //labelFont.SetWeight(wxFONTWEIGHT_BOLD);
+ dc.SetFont(labelFont); //harmonize with RowLabelWin::getBestWidth()!
+
+ auto rowRange = getRowsOnClient(rect); //returns range [begin, end)
+ for (auto row = rowRange.first; row < rowRange.second; ++row)
+ {
+ wxRect singleLabelArea = getRowLabelArea(row); //returns empty rect if row not found
+ if (singleLabelArea.height > 0)
+ {
+ singleLabelArea.y = refParent().CalcScrolledPosition(singleLabelArea.GetTopLeft()).y;
+ drawRowLabel(dc, singleLabelArea, row);
+ }
+ }
+ }
+
+ void drawRowLabel(wxDC& dc, const wxRect& rect, size_t row)
+ {
+ //clearArea(dc, rect, getColorRowLabel());
+ dc.GradientFillLinear(rect, getColorLabelGradientFrom(), getColorLabelGradientTo(), wxEAST); //clear overlapping cells
+ wxDCTextColourChanger dummy3(dc, getColorLabelText()); //accessibility: always set both foreground AND background colors!
+
+ //label text
+ wxRect textRect = rect;
+ textRect.Deflate(1);
+
+ GridData::drawCellText(dc, textRect, formatRow(row), wxALIGN_CENTRE);
+
+ //border lines
+ {
+ wxDCPenChanger dummy(dc, *wxWHITE_PEN);
+ dc.DrawLine(rect.GetTopLeft(), rect.GetTopRight());
+ }
+ {
+ wxDCPenChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_3DSHADOW));
+ dc.DrawLine(rect.GetTopLeft(), rect.GetBottomLeft());
+ dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight());
+ dc.DrawLine(rect.GetBottomRight(), rect.GetTopRight() + wxPoint(0, -1));
+ }
+ }
+
+ void onMouseLeftDown(wxMouseEvent& event) override { refParent().redirectRowLabelEvent(event); }
+ void onMouseMovement(wxMouseEvent& event) override { refParent().redirectRowLabelEvent(event); }
+ void onMouseLeftUp (wxMouseEvent& event) override { refParent().redirectRowLabelEvent(event); }
+
+ int rowHeight_;
+};
+
+
+namespace
+{
+class ColumnResizing
+{
+public:
+ ColumnResizing(wxWindow& wnd, size_t col, int startWidth, int clientPosX) :
+ wnd_(wnd), col_(col), startWidth_(startWidth), clientPosX_(clientPosX)
+ {
+ wnd_.CaptureMouse();
+ }
+ ~ColumnResizing()
+ {
+ if (wnd_.HasCapture())
+ wnd_.ReleaseMouse();
+ }
+
+ size_t getColumn () const { return col_; }
+ int getStartWidth () const { return startWidth_; }
+ int getStartPosX () const { return clientPosX_; }
+
+private:
+ wxWindow& wnd_;
+ const size_t col_;
+ const int startWidth_;
+ const int clientPosX_;
+};
+
+
+class ColumnMove
+{
+public:
+ ColumnMove(wxWindow& wnd, size_t colFrom, int clientPosX) :
+ wnd_(wnd),
+ colFrom_(colFrom),
+ colTo_(colFrom),
+ clientPosX_(clientPosX) { wnd_.CaptureMouse(); }
+ ~ColumnMove() { if (wnd_.HasCapture()) wnd_.ReleaseMouse(); }
+
+ size_t getColumnFrom() const { return colFrom_; }
+ size_t& refColumnTo() { return colTo_; }
+ int getStartPosX () const { return clientPosX_; }
+
+ bool isRealMove() const { return !singleClick_; }
+ void setRealMove() { singleClick_ = false; }
+
+private:
+ wxWindow& wnd_;
+ const size_t colFrom_;
+ size_t colTo_;
+ const int clientPosX_;
+ bool singleClick_ = true;
+};
+}
+
+//----------------------------------------------------------------------------------------------------------------
+
+class Grid::ColLabelWin : public SubWindow
+{
+public:
+ ColLabelWin(Grid& parent) : SubWindow(parent) {}
+
+private:
+ bool AcceptsFocus() const override { return false; }
+
+ void render(wxDC& dc, const wxRect& rect) override
+ {
+ if (IsThisEnabled())
+ clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
+ else
+ clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE));
+
+ //coordinate with "colLabelHeight" in Grid constructor:
+ wxFont labelFont = GetFont();
+ labelFont.SetWeight(wxFONTWEIGHT_BOLD);
+ dc.SetFont(labelFont);
+
+ wxDCTextColourChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels
+
+ const int colLabelHeight = refParent().colLabelHeight_;
+
+ wxPoint labelAreaTL(refParent().CalcScrolledPosition(wxPoint(0, 0)).x, 0); //client coordinates
+
+ std::vector<ColumnWidth> absWidths = refParent().getColWidths(); //resolve stretched widths
+ for (auto it = absWidths.begin(); it != absWidths.end(); ++it)
+ {
+ const size_t col = it - absWidths.begin();
+ const int width = it->width_; //don't use unsigned for calculations!
+
+ if (labelAreaTL.x > rect.GetRight())
+ return; //done, rect is fully covered
+ if (labelAreaTL.x + width > rect.x)
+ drawColumnLabel(dc, wxRect(labelAreaTL, wxSize(width, colLabelHeight)), col, it->type_);
+ labelAreaTL.x += width;
+ }
+ if (labelAreaTL.x > rect.GetRight())
+ return; //done, rect is fully covered
+
+ //fill gap after columns and cover full width
+ if (fillGapAfterColumns)
+ {
+ int totalWidth = 0;
+ for (const ColumnWidth& cw : absWidths)
+ totalWidth += cw.width_;
+ const int clientWidth = GetClientSize().GetWidth(); //need reliable, stable width in contrast to rect.width
+
+ if (totalWidth < clientWidth)
+ drawColumnLabel(dc, wxRect(labelAreaTL, wxSize(clientWidth - totalWidth, colLabelHeight)), absWidths.size(), ColumnType::NONE);
+ }
+ }
+
+ void drawColumnLabel(wxDC& dc, const wxRect& rect, size_t col, ColumnType colType)
+ {
+ if (auto dataView = refParent().getDataProvider())
+ {
+ const bool isHighlighted = activeResizing_ ? col == activeResizing_ ->getColumn () : //highlight_ column on mouse-over
+ activeClickOrMove_ ? col == activeClickOrMove_->getColumnFrom() :
+ highlightCol_ ? col == *highlightCol_ :
+ false;
+
+ RecursiveDcClipper clip(dc, rect);
+ dataView->renderColumnLabel(refParent(), dc, rect, colType, isHighlighted);
+
+ //draw move target location
+ if (refParent().allowColumnMove_)
+ if (activeClickOrMove_ && activeClickOrMove_->isRealMove())
+ {
+ if (col + 1 == activeClickOrMove_->refColumnTo()) //handle pos 1, 2, .. up to "at end" position
+ dc.GradientFillLinear(wxRect(rect.GetTopRight(), rect.GetBottomRight() + wxPoint(-2, 0)), getColorLabelGradientFrom(), *wxBLUE, wxSOUTH);
+ else if (col == activeClickOrMove_->refColumnTo() && col == 0) //pos 0
+ dc.GradientFillLinear(wxRect(rect.GetTopLeft(), rect.GetBottomLeft() + wxPoint(2, 0)), getColorLabelGradientFrom(), *wxBLUE, wxSOUTH);
+ }
+ }
+ }
+
+ void onMouseLeftDown(wxMouseEvent& event) override
+ {
+ if (FindFocus() != &refParent().getMainWin())
+ refParent().getMainWin().SetFocus();
+
+ activeResizing_.reset();
+ activeClickOrMove_.reset();
+
+ if (Opt<ColAction> action = refParent().clientPosToColumnAction(event.GetPosition()))
+ {
+ if (action->wantResize)
+ {
+ if (!event.LeftDClick()) //double-clicks never seem to arrive here; why is this checked at all???
+ if (Opt<int> colWidth = refParent().getColWidth(action->col))
+ activeResizing_ = std::make_unique<ColumnResizing>(*this, action->col, *colWidth, event.GetPosition().x);
+ }
+ else //a move or single click
+ activeClickOrMove_ = std::make_unique<ColumnMove>(*this, action->col, event.GetPosition().x);
+ }
+ event.Skip();
+ }
+
+ void onMouseLeftUp(wxMouseEvent& event) override
+ {
+ activeResizing_.reset(); //nothing else to do, actual work done by onMouseMovement()
+
+ if (activeClickOrMove_)
+ {
+ if (activeClickOrMove_->isRealMove())
+ {
+ if (refParent().allowColumnMove_)
+ {
+ const size_t colFrom = activeClickOrMove_->getColumnFrom();
+ size_t colTo = activeClickOrMove_->refColumnTo();
+
+ if (colTo > colFrom) //simulate "colFrom" deletion
+ --colTo;
+
+ refParent().moveColumn(colFrom, colTo);
+ }
+ }
+ else //notify single label click
+ {
+ if (const Opt<ColumnType> colType = refParent().colToType(activeClickOrMove_->getColumnFrom()))
+ sendEventNow(GridLabelClickEvent(EVENT_GRID_COL_LABEL_MOUSE_LEFT, event, *colType));
+ }
+ activeClickOrMove_.reset();
+ }
+
+ refParent().updateWindowSizes(); //looks strange if done during onMouseMovement()
+ refParent().Refresh();
+ event.Skip();
+ }
+
+ void onMouseCaptureLost(wxMouseCaptureLostEvent& event) override
+ {
+ activeResizing_.reset();
+ activeClickOrMove_.reset();
+ Refresh();
+ //event.Skip(); -> we DID handle it!
+ }
+
+ void onMouseLeftDouble(wxMouseEvent& event) override
+ {
+ if (Opt<ColAction> action = refParent().clientPosToColumnAction(event.GetPosition()))
+ if (action->wantResize)
+ {
+ //auto-size visible range on double-click
+ const int bestWidth = refParent().getBestColumnSize(action->col); //return -1 on error
+ if (bestWidth >= 0)
+ {
+ refParent().setColumnWidth(bestWidth, action->col, ALLOW_GRID_EVENT);
+ refParent().Refresh(); //refresh main grid as well!
+ }
+ }
+ event.Skip();
+ }
+
+ void onMouseMovement(wxMouseEvent& event) override
+ {
+ if (activeResizing_)
+ {
+ const auto col = activeResizing_->getColumn();
+ const int newWidth = activeResizing_->getStartWidth() + event.GetPosition().x - activeResizing_->getStartPosX();
+
+ //set width tentatively
+ refParent().setColumnWidth(newWidth, col, ALLOW_GRID_EVENT);
+
+ //check if there's a small gap after last column, if yes, fill it
+ const int gapWidth = GetClientSize().GetWidth() - refParent().getColWidthsSum(GetClientSize().GetWidth());
+ if (std::abs(gapWidth) < COLUMN_FILL_GAP_TOLERANCE)
+ refParent().setColumnWidth(newWidth + gapWidth, col, ALLOW_GRID_EVENT);
+
+ refParent().Refresh(); //refresh columns on main grid as well!
+ }
+ else if (activeClickOrMove_)
+ {
+ const int clientPosX = event.GetPosition().x;
+ if (std::abs(clientPosX - activeClickOrMove_->getStartPosX()) > COLUMN_MOVE_DELAY) //real move (not a single click)
+ {
+ activeClickOrMove_->setRealMove();
+
+ const ptrdiff_t col = refParent().clientPosToMoveTargetColumn(event.GetPosition());
+ if (col >= 0)
+ activeClickOrMove_->refColumnTo() = col;
+ }
+ }
+ else
+ {
+ if (const Opt<ColAction> action = refParent().clientPosToColumnAction(event.GetPosition()))
+ {
+ highlightCol_ = action->col;
+
+ if (action->wantResize)
+ SetCursor(wxCURSOR_SIZEWE); //set window-local only! :)
+ else
+ SetCursor(*wxSTANDARD_CURSOR);
+ }
+ else
+ {
+ highlightCol_ = NoValue();
+ SetCursor(*wxSTANDARD_CURSOR);
+ }
+ }
+
+ const std::wstring toolTip = [&]
+ {
+ const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition());
+ const ColumnType colType = refParent().getColumnAtPos(absPos.x).colType; //returns ColumnType::NONE if no column at x position!
+ if (colType != ColumnType::NONE)
+ if (auto prov = refParent().getDataProvider())
+ return prov->getToolTip(colType);
+ return std::wstring();
+ }();
+ setToolTip(toolTip);
+
+ Refresh();
+ event.Skip();
+ }
+
+ void onLeaveWindow(wxMouseEvent& event) override
+ {
+ highlightCol_ = NoValue(); //wxEVT_LEAVE_WINDOW does not respect mouse capture! -> however highlight_ is drawn unconditionally during move/resize!
+ Refresh();
+ event.Skip();
+ }
+
+ void onMouseRightDown(wxMouseEvent& event) override
+ {
+ if (const Opt<ColAction> action = refParent().clientPosToColumnAction(event.GetPosition()))
+ {
+ if (const Opt<ColumnType> colType = refParent().colToType(action->col))
+ sendEventNow(GridLabelClickEvent(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, event, *colType)); //notify right click
+ else assert(false);
+ }
+ else
+ //notify right click (on free space after last column)
+ if (fillGapAfterColumns)
+ sendEventNow(GridLabelClickEvent(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, event, ColumnType::NONE));
+
+ event.Skip();
+ }
+
+ std::unique_ptr<ColumnResizing> activeResizing_;
+ std::unique_ptr<ColumnMove> activeClickOrMove_;
+ Opt<size_t> highlightCol_; //column during mouse-over
+};
+
+//----------------------------------------------------------------------------------------------------------------
+namespace
+{
+const wxEventType EVENT_GRID_HAS_SCROLLED = wxNewEventType(); //internal to Grid::MainWin::ScrollWindow()
+}
+//----------------------------------------------------------------------------------------------------------------
+
+class Grid::MainWin : public SubWindow
+{
+public:
+ MainWin(Grid& parent,
+ RowLabelWin& rowLabelWin,
+ ColLabelWin& colLabelWin) : SubWindow(parent),
+ rowLabelWin_(rowLabelWin),
+ colLabelWin_(colLabelWin)
+ {
+ Connect(EVENT_GRID_HAS_SCROLLED, wxEventHandler(MainWin::onRequestWindowUpdate), nullptr, this);
+ }
+
+ ~MainWin() { assert(!gridUpdatePending_); }
+
+ size_t getCursor() const { return cursorRow_; }
+ size_t getAnchor() const { return selectionAnchor_; }
+
+ void setCursor(size_t newCursorRow, size_t newAnchorRow)
+ {
+ cursorRow_ = newCursorRow;
+ selectionAnchor_ = newAnchorRow;
+ activeSelection_.reset(); //e.g. user might search with F3 while holding down left mouse button
+ }
+
+private:
+ void render(wxDC& dc, const wxRect& rect) override
+ {
+ if (IsThisEnabled())
+ clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
+ else
+ clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE));
+
+ dc.SetFont(GetFont()); //harmonize with Grid::getBestColumnSize()
+
+ wxDCTextColourChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels
+
+ std::vector<ColumnWidth> absWidths = refParent().getColWidths(); //resolve stretched widths
+ {
+ int totalRowWidth = 0;
+ for (const ColumnWidth& cw : absWidths)
+ totalRowWidth += cw.width_;
+
+ //fill gap after columns and cover full width
+ if (fillGapAfterColumns)
+ totalRowWidth = std::max(totalRowWidth, GetClientSize().GetWidth());
+
+ if (auto prov = refParent().getDataProvider())
+ {
+ RecursiveDcClipper dummy2(dc, rect); //do NOT draw background on cells outside of invalidated rect invalidating foreground text!
+
+ wxPoint cellAreaTL(refParent().CalcScrolledPosition(wxPoint(0, 0))); //client coordinates
+ const int rowHeight = rowLabelWin_.getRowHeight();
+ const auto rowRange = rowLabelWin_.getRowsOnClient(rect); //returns range [begin, end)
+
+ //draw background lines
+ for (auto row = rowRange.first; row < rowRange.second; ++row)
+ {
+ const wxRect rowRect(cellAreaTL + wxPoint(0, row * rowHeight), wxSize(totalRowWidth, rowHeight));
+ RecursiveDcClipper dummy3(dc, rowRect);
+ prov->renderRowBackgound(dc, rowRect, row, refParent().IsThisEnabled(), drawAsSelected(row));
+ }
+
+ //draw single cells, column by column
+ for (const ColumnWidth& cw : absWidths)
+ {
+ if (cellAreaTL.x > rect.GetRight())
+ return; //done
+
+ if (cellAreaTL.x + cw.width_ > rect.x)
+ for (auto row = rowRange.first; row < rowRange.second; ++row)
+ {
+ const wxRect cellRect(cellAreaTL.x, cellAreaTL.y + row * rowHeight, cw.width_, rowHeight);
+ RecursiveDcClipper dummy3(dc, cellRect);
+ prov->renderCell(dc, cellRect, row, cw.type_, refParent().IsThisEnabled(), drawAsSelected(row), getRowHoverToDraw(row));
+ }
+ cellAreaTL.x += cw.width_;
+ }
+ }
+ }
+ }
+
+ HoverArea getRowHoverToDraw(ptrdiff_t row) const
+ {
+ if (activeSelection_)
+ {
+ if (activeSelection_->getFirstClick().row_ == row)
+ return activeSelection_->getFirstClick().hoverArea_;
+ }
+ else if (highlight_.row == row)
+ return highlight_.rowHover;
+ return HoverArea::NONE;
+ }
+
+ bool drawAsSelected(size_t row) const
+ {
+ if (activeSelection_) //check if user is currently selecting with mouse
+ {
+ const size_t rowFrom = std::min(activeSelection_->getStartRow(), activeSelection_->getCurrentRow());
+ const size_t rowTo = std::max(activeSelection_->getStartRow(), activeSelection_->getCurrentRow());
+
+ if (rowFrom <= row && row <= rowTo)
+ return activeSelection_->isPositiveSelect(); //overwrite default
+ }
+ return refParent().isSelected(row);
+ }
+
+ void onMouseLeftDown (wxMouseEvent& event) override { onMouseDown(event); }
+ void onMouseLeftUp (wxMouseEvent& event) override { onMouseUp (event); }
+ void onMouseRightDown(wxMouseEvent& event) override { onMouseDown(event); }
+ void onMouseRightUp (wxMouseEvent& event) override { onMouseUp (event); }
+
+ void onMouseLeftDouble(wxMouseEvent& event) override
+ {
+ if (auto prov = refParent().getDataProvider())
+ {
+ const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition());
+ const ptrdiff_t row = rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range
+ const ColumnPosInfo cpi = refParent().getColumnAtPos(absPos.x); //returns ColumnType::NONE if no column at x position!
+ const HoverArea rowHover = prov->getRowMouseHover(row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth);
+ //client is interested in all double-clicks, even those outside of the grid!
+ sendEventNow(GridClickEvent(EVENT_GRID_MOUSE_LEFT_DOUBLE, event, row, rowHover));
+ }
+ event.Skip();
+ }
+
+ void onMouseDown(wxMouseEvent& event) //handle left and right mouse button clicks (almost) the same
+ {
+ if (wxWindow::FindFocus() != this) //doesn't seem to happen automatically for right mouse button
+ SetFocus();
+
+ if (auto prov = refParent().getDataProvider())
+ {
+ const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition());
+ const ptrdiff_t row = rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range
+ const ColumnPosInfo cpi = refParent().getColumnAtPos(absPos.x); //returns ColumnType::NONE if no column at x position!
+ const HoverArea rowHover = prov->getRowMouseHover(row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth);
+ //row < 0 possible!!! Pressing "Menu key" simulates Mouse Right Down + Up at position 0xffff/0xffff!
+
+ GridClickEvent mouseEvent(event.RightDown() ? EVENT_GRID_MOUSE_RIGHT_DOWN : EVENT_GRID_MOUSE_LEFT_DOWN, event, row, rowHover);
+
+ if (row >= 0)
+ if (!event.RightDown() || !refParent().isSelected(row)) //do NOT start a new selection if user right-clicks on a selected area!
+ {
+ if (event.ControlDown())
+ activeSelection_ = std::make_unique<MouseSelection>(*this, row, !refParent().isSelected(row), mouseEvent);
+ else if (event.ShiftDown())
+ {
+ activeSelection_ = std::make_unique<MouseSelection>(*this, selectionAnchor_, true, mouseEvent);
+ refParent().clearSelection(ALLOW_GRID_EVENT);
+ }
+ else
+ {
+ activeSelection_ = std::make_unique<MouseSelection>(*this, row, true, mouseEvent);
+ refParent().clearSelection(ALLOW_GRID_EVENT);
+ }
+ }
+ //notify event *after* potential "clearSelection(true)" above: a client should first receive a GridRangeSelectEvent for clearing the grid, if necessary,
+ //then GridClickEvent and the associated GridRangeSelectEvent one after the other
+ sendEventNow(mouseEvent);
+
+ Refresh();
+ }
+ event.Skip(); //allow changing focus
+ }
+
+ void onMouseUp(wxMouseEvent& event)
+ {
+ if (activeSelection_)
+ {
+ const size_t rowCount = refParent().getRowCount();
+ if (rowCount > 0)
+ {
+ if (activeSelection_->getCurrentRow() < rowCount)
+ {
+ cursorRow_ = activeSelection_->getCurrentRow();
+ selectionAnchor_ = activeSelection_->getStartRow(); //allowed to be "out of range"
+ }
+ else if (activeSelection_->getStartRow() < rowCount) //don't change cursor if "to" and "from" are out of range
+ {
+ cursorRow_ = rowCount - 1;
+ selectionAnchor_ = activeSelection_->getStartRow(); //allowed to be "out of range"
+ }
+ else //total selection "out of range"
+ selectionAnchor_ = cursorRow_;
+ }
+ //slight deviation from Explorer: change cursor while dragging mouse! -> unify behavior with shift + direction keys
+
+ refParent().selectRangeAndNotify(activeSelection_->getStartRow (), //from
+ activeSelection_->getCurrentRow(), //to
+ activeSelection_->isPositiveSelect(),
+ &activeSelection_->getFirstClick());
+ activeSelection_.reset();
+ }
+
+ if (auto prov = refParent().getDataProvider())
+ {
+ //this one may point to row which is not in visible area!
+ const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition());
+ const ptrdiff_t row = rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range
+ const ColumnPosInfo cpi = refParent().getColumnAtPos(absPos.x); //returns ColumnType::NONE if no column at x position!
+ const HoverArea rowHover = prov->getRowMouseHover(row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth);
+ //notify click event after the range selection! e.g. this makes sure the selection is applied before showing a context menu
+ sendEventNow(GridClickEvent(event.RightUp() ? EVENT_GRID_MOUSE_RIGHT_UP : EVENT_GRID_MOUSE_LEFT_UP, event, row, rowHover));
+ }
+
+ //update highlight_ and tooltip: on OS X no mouse movement event is generated after a mouse button click (unlike on Windows)
+ event.SetPosition(ScreenToClient(wxGetMousePosition())); //mouse position may have changed within above callbacks (e.g. context menu was shown)!
+ onMouseMovement(event);
+
+ Refresh();
+ event.Skip(); //allow changing focus
+ }
+
+ void onMouseCaptureLost(wxMouseCaptureLostEvent& event) override
+ {
+ activeSelection_.reset();
+ highlight_.row = -1;
+ Refresh();
+ //event.Skip(); -> we DID handle it!
+ }
+
+ void onMouseMovement(wxMouseEvent& event) override
+ {
+ if (auto prov = refParent().getDataProvider())
+ {
+ const ptrdiff_t rowCount = refParent().getRowCount();
+ const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition());
+ const ptrdiff_t row = rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range
+ const ColumnPosInfo cpi = refParent().getColumnAtPos(absPos.x); //returns ColumnType::NONE if no column at x position!
+ const HoverArea rowHover = prov->getRowMouseHover(row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth);
+
+ const std::wstring toolTip = [&]
+ {
+ if (cpi.colType != ColumnType::NONE && 0 <= row && row < rowCount)
+ return prov->getToolTip(row, cpi.colType);
+ return std::wstring();
+ }();
+ setToolTip(toolTip); //show even during mouse selection!
+
+ if (activeSelection_)
+ activeSelection_->evalMousePos(); //call on both mouse movement + timer event!
+ else
+ {
+ refreshHighlight(highlight_);
+ highlight_.row = row;
+ highlight_.rowHover = rowHover;
+ refreshHighlight(highlight_); //multiple Refresh() calls are condensed into single one!
+ }
+ }
+ event.Skip();
+ }
+
+ void onLeaveWindow(wxMouseEvent& event) override //wxEVT_LEAVE_WINDOW does not respect mouse capture!
+ {
+ if (!activeSelection_)
+ {
+ refreshHighlight(highlight_);
+ highlight_.row = -1;
+ }
+
+ event.Skip();
+ }
+
+
+ void onFocus(wxFocusEvent& event) override { Refresh(); event.Skip(); }
+
+ class MouseSelection : private wxEvtHandler
+ {
+ public:
+ MouseSelection(MainWin& wnd, size_t rowStart, bool positiveSelect, const GridClickEvent& firstClick) :
+ wnd_(wnd), rowStart_(rowStart), rowCurrent_(rowStart), positiveSelect_(positiveSelect), firstClick_(firstClick)
+ {
+ wnd_.CaptureMouse();
+ timer_.Connect(wxEVT_TIMER, wxEventHandler(MouseSelection::onTimer), nullptr, this);
+ timer_.Start(100); //timer interval in ms
+ evalMousePos();
+ }
+ ~MouseSelection() { if (wnd_.HasCapture()) wnd_.ReleaseMouse(); }
+
+ size_t getStartRow () const { return rowStart_; }
+ size_t getCurrentRow () const { return rowCurrent_; }
+ bool isPositiveSelect() const { return positiveSelect_; } //are we selecting or unselecting?
+ const GridClickEvent& getFirstClick() const { return firstClick_; }
+
+ void evalMousePos()
+ {
+ const auto now = std::chrono::steady_clock::now();
+ const double deltaSecs = std::chrono::duration<double>(now - lastEvalTime_).count(); //unit: [sec]
+ lastEvalTime_ = now;
+
+ const wxPoint clientPos = wnd_.ScreenToClient(wxGetMousePosition());
+ const wxSize clientSize = wnd_.GetClientSize();
+ assert(wnd_.GetClientAreaOrigin() == wxPoint());
+
+ //scroll while dragging mouse
+ const int overlapPixY = clientPos.y < 0 ? clientPos.y :
+ clientPos.y >= clientSize.GetHeight() ? clientPos.y - (clientSize.GetHeight() - 1) : 0;
+ const int overlapPixX = clientPos.x < 0 ? clientPos.x :
+ clientPos.x >= clientSize.GetWidth() ? clientPos.x - (clientSize.GetWidth() - 1) : 0;
+
+ int pixelsPerUnitY = 0;
+ wnd_.refParent().GetScrollPixelsPerUnit(nullptr, &pixelsPerUnitY);
+ if (pixelsPerUnitY <= 0) return;
+
+ const double mouseDragSpeedIncScrollU = pixelsPerUnitY > 0 ? MOUSE_DRAG_ACCELERATION * wnd_.rowLabelWin_.getRowHeight() / pixelsPerUnitY : 0; //unit: [scroll units / (pixel * sec)]
+
+ auto autoScroll = [&](int overlapPix, double& toScroll)
+ {
+ if (overlapPix != 0)
+ {
+ const double scrollSpeed = overlapPix * mouseDragSpeedIncScrollU; //unit: [scroll units / sec]
+ toScroll += scrollSpeed * deltaSecs;
+ }
+ else
+ toScroll = 0;
+ };
+
+ autoScroll(overlapPixX, toScrollX_);
+ autoScroll(overlapPixY, toScrollY_);
+
+ if (static_cast<int>(toScrollX_) != 0 || static_cast<int>(toScrollY_) != 0)
+ {
+ wnd_.refParent().scrollDelta(static_cast<int>(toScrollX_), static_cast<int>(toScrollY_)); //
+ toScrollX_ -= static_cast<int>(toScrollX_); //rounds down for positive numbers, up for negative,
+ toScrollY_ -= static_cast<int>(toScrollY_); //exactly what we want
+ }
+
+ //select current row *after* scrolling
+ wxPoint clientPosTrimmed = clientPos;
+ numeric::clamp(clientPosTrimmed.y, 0, clientSize.GetHeight() - 1); //do not select row outside client window!
+
+ const wxPoint absPos = wnd_.refParent().CalcUnscrolledPosition(clientPosTrimmed);
+ const ptrdiff_t newRow = wnd_.rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range
+ if (newRow >= 0)
+ if (rowCurrent_ != newRow)
+ {
+ rowCurrent_ = newRow;
+ wnd_.Refresh();
+ }
+ }
+
+ private:
+ void onTimer(wxEvent& event) { evalMousePos(); }
+
+ MainWin& wnd_;
+ const size_t rowStart_;
+ ptrdiff_t rowCurrent_;
+ const bool positiveSelect_;
+ const GridClickEvent firstClick_;
+ wxTimer timer_;
+ double toScrollX_ = 0; //count outstanding scroll unit fractions while dragging mouse
+ double toScrollY_ = 0; //
+ std::chrono::steady_clock::time_point lastEvalTime_ = std::chrono::steady_clock::now();
+ };
+
+ struct MouseHighlight
+ {
+ ptrdiff_t row = -1;
+ HoverArea rowHover = HoverArea::NONE;
+ };
+
+ void ScrollWindow(int dx, int dy, const wxRect* rect) override
+ {
+ wxWindow::ScrollWindow(dx, dy, rect);
+ rowLabelWin_.ScrollWindow(0, dy, rect);
+ colLabelWin_.ScrollWindow(dx, 0, rect);
+
+ //attention, wxGTK call sequence: wxScrolledWindow::Scroll() -> wxScrolledHelperNative::Scroll() -> wxScrolledHelperNative::DoScroll()
+ //which *first* calls us, MainWin::ScrollWindow(), and *then* internally updates m_yScrollPosition
+ //=> we cannot use CalcUnscrolledPosition() here which gives the wrong/outdated value!!!
+ //=> we need to update asynchronously:
+ //=> don't send async event repeatedly => severe performance issues on wxGTK!
+ //=> can't use idle event neither: too few idle events on Windows, e.g. NO idle events while mouse drag-scrolling!
+ //=> solution: send single async event at most!
+ if (!gridUpdatePending_) //without guarding, the number of outstanding async events can become very high during scrolling!! test case: Ubuntu: 170; Windows: 20
+ {
+ gridUpdatePending_ = true;
+ wxCommandEvent scrollEvent(EVENT_GRID_HAS_SCROLLED);
+ AddPendingEvent(scrollEvent); //asynchronously call updateAfterScroll()
+ }
+ }
+
+ void onRequestWindowUpdate(wxEvent& event)
+ {
+ assert(gridUpdatePending_);
+ ZEN_ON_SCOPE_EXIT(gridUpdatePending_ = false);
+
+ refParent().updateWindowSizes(false); //row label width has changed -> do *not* update scrollbars: recursion on wxGTK! -> still a problem, now that we're called async??
+ rowLabelWin_.Update(); //update while dragging scroll thumb
+ }
+
+ void refreshRow(size_t row)
+ {
+ const wxRect& rowArea = rowLabelWin_.getRowLabelArea(row); //returns empty rect if row not found
+ const wxPoint topLeft = refParent().CalcScrolledPosition(wxPoint(0, rowArea.y)); //absolute -> client coordinates
+ wxRect cellArea(topLeft, wxSize(refParent().getColWidthsSum(GetClientSize().GetWidth()), rowArea.height));
+ RefreshRect(cellArea, false);
+ }
+
+ void refreshHighlight(const MouseHighlight& hl)
+ {
+ const ptrdiff_t rowCount = refParent().getRowCount();
+ if (0 <= hl.row && hl.row < rowCount && hl.rowHover != HoverArea::NONE) //no highlight_? => NOP!
+ refreshRow(hl.row);
+ }
+
+ RowLabelWin& rowLabelWin_;
+ ColLabelWin& colLabelWin_;
+
+ std::unique_ptr<MouseSelection> activeSelection_; //bound while user is selecting with mouse
+ MouseHighlight highlight_; //current mouse highlight_ (superseeded by activeSelection_ if available)
+
+ ptrdiff_t cursorRow_ = 0;
+ size_t selectionAnchor_ = 0;
+ bool gridUpdatePending_ = false;
+};
+
+//----------------------------------------------------------------------------------------------------------------
+//----------------------------------------------------------------------------------------------------------------
+
+Grid::Grid(wxWindow* parent,
+ wxWindowID id,
+ const wxPoint& pos,
+ const wxSize& size,
+ long style,
+ const wxString& name) : wxScrolledWindow(parent, id, pos, size, style | wxWANTS_CHARS, name)
+{
+ cornerWin_ = new CornerWin (*this); //
+ rowLabelWin_ = new RowLabelWin(*this); //owership handled by "this"
+ colLabelWin_ = new ColLabelWin(*this); //
+ mainWin_ = new MainWin (*this, *rowLabelWin_, *colLabelWin_); //
+
+ colLabelHeight_ = 2 * DEFAULT_COL_LABEL_BORDER + [&]() -> int
+ {
+ //coordinate with ColLabelWin::render():
+ wxFont labelFont = colLabelWin_->GetFont();
+ labelFont.SetWeight(wxFONTWEIGHT_BOLD);
+ return labelFont.GetPixelSize().GetHeight();
+ }();
+
+ SetTargetWindow(mainWin_);
+
+ SetInitialSize(size); //"Most controls will use this to set their initial size" -> why not
+
+ assert(GetClientSize() == GetSize()); //borders are NOT allowed for Grid
+ //reason: updateWindowSizes() wants to use "GetSize()" as a "GetClientSize()" including scrollbars
+
+ Connect(wxEVT_PAINT, wxPaintEventHandler(Grid::onPaintEvent ), nullptr, this);
+ Connect(wxEVT_ERASE_BACKGROUND, wxEraseEventHandler(Grid::onEraseBackGround), nullptr, this);
+ Connect(wxEVT_SIZE, wxSizeEventHandler (Grid::onSizeEvent ), nullptr, this);
+
+ Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(Grid::onKeyDown), nullptr, this);
+}
+
+
+void Grid::updateWindowSizes(bool updateScrollbar)
+{
+ /* We have to deal with TWO nasty circular dependencies:
+ 1.
+ rowLabelWidth
+ /|\
+ mainWin::client width
+ /|\
+ SetScrollbars -> show/hide horizontal scrollbar depending on client width
+ /|\
+ mainWin::client height -> possibly trimmed by horizontal scrollbars
+ /|\
+ rowLabelWidth
+
+ 2.
+ mainWin_->GetClientSize()
+ /|\
+ SetScrollbars -> show/hide scrollbars depending on whether client size is big enough
+ /|\
+ GetClientSize(); -> possibly trimmed by scrollbars
+ /|\
+ mainWin_->GetClientSize() -> also trimmed, since it's a sub-window!
+ */
+
+ //break this vicious circle:
+
+ //harmonize with Grid::GetSizeAvailableForScrollTarget()!
+
+ //1. calculate row label width independent from scrollbars
+ const int mainWinHeightGross = std::max(GetSize().GetHeight() - colLabelHeight_, 0); //independent from client sizes and scrollbars!
+ const ptrdiff_t logicalHeight = rowLabelWin_->getLogicalHeight(); //
+
+ int rowLabelWidth = 0;
+ if (drawRowLabel_ && logicalHeight > 0)
+ {
+ ptrdiff_t yFrom = CalcUnscrolledPosition(wxPoint(0, 0)).y;
+ ptrdiff_t yTo = CalcUnscrolledPosition(wxPoint(0, mainWinHeightGross - 1)).y ;
+ numeric::clamp<ptrdiff_t>(yFrom, 0, logicalHeight - 1);
+ numeric::clamp<ptrdiff_t>(yTo, 0, logicalHeight - 1);
+
+ const ptrdiff_t rowFrom = rowLabelWin_->getRowAtPos(yFrom);
+ const ptrdiff_t rowTo = rowLabelWin_->getRowAtPos(yTo);
+ if (rowFrom >= 0 && rowTo >= 0)
+ rowLabelWidth = rowLabelWin_->getBestWidth(rowFrom, rowTo);
+ }
+
+ auto getMainWinSize = [&](const wxSize& clientSize) { return wxSize(std::max(0, clientSize.GetWidth() - rowLabelWidth), std::max(0, clientSize.GetHeight() - colLabelHeight_)); };
+
+ auto setScrollbars2 = [&](int logWidth, int logHeight) //replace SetScrollbars, which loses precision of pixelsPerUnitX for some brain-dead reason
+ {
+ mainWin_->SetVirtualSize(logWidth, logHeight); //set before calling SetScrollRate():
+ //else SetScrollRate() would fail to preserve scroll position when "new virtual pixel-pos > old virtual height"
+
+ int ppsuX = 0; //pixel per scroll unit
+ int ppsuY = 0;
+ GetScrollPixelsPerUnit(&ppsuX, &ppsuY);
+
+ const int ppsuNew = rowLabelWin_->getRowHeight();
+ if (ppsuX != ppsuNew || ppsuY != ppsuNew) //support polling!
+ SetScrollRate(ppsuNew, ppsuNew); //internally calls AdjustScrollbars() and GetVirtualSize()!
+
+ AdjustScrollbars(); //lousy wxWidgets design decision: internally calls mainWin_->GetClientSize() without considering impact of scrollbars!
+ //Attention: setting scrollbars triggers *synchronous* resize event if scrollbars are shown or hidden! => updateWindowSizes() recursion! (Windows)
+ };
+
+ //2. update managed windows' sizes: just assume scrollbars are already set correctly, even if they may not be (yet)!
+ //this ensures mainWin_->SetVirtualSize() and AdjustScrollbars() are working with the correct main window size, unless sb change later, which triggers a recalculation anyway!
+ const wxSize mainWinSize = getMainWinSize(GetClientSize());
+
+ cornerWin_ ->SetSize(0, 0, rowLabelWidth, colLabelHeight_);
+ rowLabelWin_->SetSize(0, colLabelHeight_, rowLabelWidth, mainWinSize.GetHeight());
+ colLabelWin_->SetSize(rowLabelWidth, 0, mainWinSize.GetWidth(), colLabelHeight_);
+ mainWin_ ->SetSize(rowLabelWidth, colLabelHeight_, mainWinSize.GetWidth(), mainWinSize.GetHeight());
+
+ //avoid flicker in wxWindowMSW::HandleSize() when calling ::EndDeferWindowPos() where the sub-windows are moved only although they need to be redrawn!
+ colLabelWin_->Refresh();
+ mainWin_ ->Refresh();
+
+ //3. update scrollbars: "guide wxScrolledHelper to not screw up too much"
+ if (updateScrollbar)
+ {
+ const int mainWinWidthGross = getMainWinSize(GetSize()).GetWidth();
+
+ if (logicalHeight <= mainWinHeightGross &&
+ getColWidthsSum(mainWinWidthGross) <= mainWinWidthGross &&
+ //this special case needs to be considered *only* when both scrollbars are flexible:
+ showScrollbarX_ == SB_SHOW_AUTOMATIC &&
+ showScrollbarY_ == SB_SHOW_AUTOMATIC)
+ setScrollbars2(0, 0); //no scrollbars required at all! -> wxScrolledWindow requires active help to detect this special case!
+ else
+ {
+ const int logicalWidthTmp = getColWidthsSum(mainWinSize.GetWidth()); //assuming vertical scrollbar stays as it is...
+ setScrollbars2(logicalWidthTmp, logicalHeight); //if scrollbars are shown or hidden a new resize event recurses into updateWindowSizes()
+ /*
+ is there a risk of endless recursion? No, 2-level recursion at most, consider the following 6 cases:
+
+ <----------gw---------->
+ <----------nw------>
+ ------------------------ /|\ /|\
+ | | | | |
+ | main window | | nh |
+ | | | | gh
+ ------------------------ \|/ |
+ | | | |
+ ------------------------ \|/
+ gw := gross width
+ nw := net width := gross width - sb size
+ gh := gross height
+ nh := net height := gross height - sb size
+
+ There are 6 cases that can occur:
+ ---------------------------------
+ lw := logical width
+ lh := logical height
+
+ 1. lw <= gw && lh <= gh => no scrollbars needed
+
+ 2. lw > gw && lh > gh => need both scrollbars
+
+ 3. lh > gh
+ 4.1 lw <= nw => need vertical scrollbar only
+ 4.2 nw < lw <= gw => need both scrollbars
+
+ 4. lw > gw
+ 3.1 lh <= nh => need horizontal scrollbar only
+ 3.2 nh < lh <= gh => need both scrollbars
+ */
+ }
+ }
+}
+
+
+wxSize Grid::GetSizeAvailableForScrollTarget(const wxSize& size)
+{
+ //harmonize with Grid::updateWindowSizes()!
+
+ //1. calculate row label width independent from scrollbars
+ const int mainWinHeightGross = std::max(size.GetHeight() - colLabelHeight_, 0); //independent from client sizes and scrollbars!
+ const ptrdiff_t logicalHeight = rowLabelWin_->getLogicalHeight(); //
+
+ int rowLabelWidth = 0;
+ if (drawRowLabel_ && logicalHeight > 0)
+ {
+ ptrdiff_t yFrom = CalcUnscrolledPosition(wxPoint(0, 0)).y;
+ ptrdiff_t yTo = CalcUnscrolledPosition(wxPoint(0, mainWinHeightGross - 1)).y ;
+ numeric::clamp<ptrdiff_t>(yFrom, 0, logicalHeight - 1);
+ numeric::clamp<ptrdiff_t>(yTo, 0, logicalHeight - 1);
+
+ const ptrdiff_t rowFrom = rowLabelWin_->getRowAtPos(yFrom);
+ const ptrdiff_t rowTo = rowLabelWin_->getRowAtPos(yTo);
+ if (rowFrom >= 0 && rowTo >= 0)
+ rowLabelWidth = rowLabelWin_->getBestWidth(rowFrom, rowTo);
+ }
+
+ return size - wxSize(rowLabelWidth, colLabelHeight_);
+}
+
+
+void Grid::onPaintEvent(wxPaintEvent& event) { wxPaintDC dc(this); }
+
+
+void Grid::onKeyDown(wxKeyEvent& event)
+{
+ int keyCode = event.GetKeyCode();
+ if (GetLayoutDirection() == wxLayout_RightToLeft)
+ {
+ if (keyCode == WXK_LEFT || keyCode == WXK_NUMPAD_LEFT)
+ keyCode = WXK_RIGHT;
+ else if (keyCode == WXK_RIGHT || keyCode == WXK_NUMPAD_RIGHT)
+ keyCode = WXK_LEFT;
+ }
+
+ const ptrdiff_t rowCount = getRowCount();
+ const ptrdiff_t cursorRow = mainWin_->getCursor();
+
+ auto moveCursorTo = [&](ptrdiff_t row)
+ {
+ if (rowCount > 0)
+ {
+ numeric::clamp<ptrdiff_t>(row, 0, rowCount - 1);
+ setGridCursor(row);
+ }
+ };
+
+ auto selectWithCursorTo = [&](ptrdiff_t row)
+ {
+ if (rowCount > 0)
+ {
+ numeric::clamp<ptrdiff_t>(row, 0, rowCount - 1);
+ selectWithCursor(row);
+ }
+ };
+
+ switch (keyCode)
+ {
+ //case WXK_TAB:
+ // if (Navigate(event.ShiftDown() ? wxNavigationKeyEvent::IsBackward : wxNavigationKeyEvent::IsForward))
+ // return;
+ // break;
+
+ case WXK_UP:
+ case WXK_NUMPAD_UP:
+ if (event.ShiftDown())
+ selectWithCursorTo(cursorRow - 1);
+ else if (event.ControlDown())
+ scrollDelta(0, -1);
+ else
+ moveCursorTo(cursorRow - 1);
+ return; //swallow event: wxScrolledWindow, wxWidgets 2.9.3 on Kubuntu x64 processes arrow keys: prevent this!
+
+ case WXK_DOWN:
+ case WXK_NUMPAD_DOWN:
+ if (event.ShiftDown())
+ selectWithCursorTo(cursorRow + 1);
+ else if (event.ControlDown())
+ scrollDelta(0, 1);
+ else
+ moveCursorTo(cursorRow + 1);
+ return; //swallow event
+
+ case WXK_LEFT:
+ case WXK_NUMPAD_LEFT:
+ if (event.ControlDown())
+ scrollDelta(-1, 0);
+ else if (event.ShiftDown())
+ ;
+ else
+ moveCursorTo(cursorRow);
+ return;
+
+ case WXK_RIGHT:
+ case WXK_NUMPAD_RIGHT:
+ if (event.ControlDown())
+ scrollDelta(1, 0);
+ else if (event.ShiftDown())
+ ;
+ else
+ moveCursorTo(cursorRow);
+ return;
+
+ case WXK_HOME:
+ case WXK_NUMPAD_HOME:
+ if (event.ShiftDown())
+ selectWithCursorTo(0);
+ //else if (event.ControlDown())
+ // ;
+ else
+ moveCursorTo(0);
+ return;
+
+ case WXK_END:
+ case WXK_NUMPAD_END:
+ if (event.ShiftDown())
+ selectWithCursorTo(rowCount - 1);
+ //else if (event.ControlDown())
+ // ;
+ else
+ moveCursorTo(rowCount - 1);
+ return;
+
+ case WXK_PAGEUP:
+ case WXK_NUMPAD_PAGEUP:
+ if (event.ShiftDown())
+ selectWithCursorTo(cursorRow - GetClientSize().GetHeight() / rowLabelWin_->getRowHeight());
+ //else if (event.ControlDown())
+ // ;
+ else
+ moveCursorTo(cursorRow - GetClientSize().GetHeight() / rowLabelWin_->getRowHeight());
+ return;
+
+ case WXK_PAGEDOWN:
+ case WXK_NUMPAD_PAGEDOWN:
+ if (event.ShiftDown())
+ selectWithCursorTo(cursorRow + GetClientSize().GetHeight() / rowLabelWin_->getRowHeight());
+ //else if (event.ControlDown())
+ // ;
+ else
+ moveCursorTo(cursorRow + GetClientSize().GetHeight() / rowLabelWin_->getRowHeight());
+ return;
+
+ case 'A': //Ctrl + A - select all
+ if (event.ControlDown())
+ selectRangeAndNotify(0, rowCount, true /*positive*/, nullptr /*mouseInitiated*/);
+ break;
+
+ case WXK_NUMPAD_ADD: //CTRL + '+' - auto-size all
+ if (event.ControlDown())
+ autoSizeColumns(ALLOW_GRID_EVENT);
+ return;
+ }
+
+ event.Skip();
+}
+
+
+void Grid::setColumnLabelHeight(int height)
+{
+ colLabelHeight_ = std::max(0, height);
+ updateWindowSizes();
+}
+
+
+void Grid::showRowLabel(bool show)
+{
+ drawRowLabel_ = show;
+ updateWindowSizes();
+}
+
+
+void Grid::selectAllRows(GridEventPolicy rangeEventPolicy)
+{
+ selection_.selectAll();
+ mainWin_->Refresh();
+
+ if (rangeEventPolicy == ALLOW_GRID_EVENT) //notify event, even if we're not triggered by user interaction
+ {
+ GridRangeSelectEvent selEvent(0, getRowCount(), true, nullptr);
+ if (wxEvtHandler* evtHandler = GetEventHandler())
+ evtHandler->ProcessEvent(selEvent);
+ }
+}
+
+
+void Grid::clearSelection(GridEventPolicy rangeEventPolicy)
+{
+ selection_.clear();
+ mainWin_->Refresh();
+
+ if (rangeEventPolicy == ALLOW_GRID_EVENT) //notify event, even if we're not triggered by user interaction
+ {
+ GridRangeSelectEvent unselectionEvent(0, getRowCount(), false, nullptr);
+ if (wxEvtHandler* evtHandler = GetEventHandler())
+ evtHandler->ProcessEvent(unselectionEvent);
+ }
+}
+
+
+void Grid::scrollDelta(int deltaX, int deltaY)
+{
+ int scrollPosX = 0;
+ int scrollPosY = 0;
+ GetViewStart(&scrollPosX, &scrollPosY);
+
+ scrollPosX += deltaX;
+ scrollPosY += deltaY;
+
+ scrollPosX = std::max(0, scrollPosX); //wxScrollHelper::Scroll() will exit prematurely if input happens to be "-1"!
+ scrollPosY = std::max(0, scrollPosY); //
+
+ Scroll(scrollPosX, scrollPosY); //internally calls wxWindows::Update()!
+ updateWindowSizes(); //may show horizontal scroll bar
+}
+
+
+void Grid::redirectRowLabelEvent(wxMouseEvent& event)
+{
+ event.m_x = 0;
+ if (wxEvtHandler* evtHandler = mainWin_->GetEventHandler())
+ evtHandler->ProcessEvent(event);
+
+ if (event.ButtonDown() && wxWindow::FindFocus() != mainWin_)
+ mainWin_->SetFocus();
+}
+
+
+size_t Grid::getRowCount() const
+{
+ return dataView_ ? dataView_->getRowCount() : 0;
+}
+
+
+void Grid::Refresh(bool eraseBackground, const wxRect* rect)
+{
+ const size_t rowCountNew = getRowCount();
+ if (rowCountOld_ != rowCountNew)
+ {
+ rowCountOld_ = rowCountNew;
+ updateWindowSizes();
+ }
+
+ if (selection_.maxSize() != rowCountNew) //clear selection only when needed (consider setSelectedRows())
+ selection_.init(rowCountNew);
+
+ wxScrolledWindow::Refresh(eraseBackground, rect);
+}
+
+
+void Grid::setRowHeight(int height)
+{
+ rowLabelWin_->setRowHeight(height);
+ updateWindowSizes();
+ Refresh();
+}
+
+
+void Grid::setColumnConfig(const std::vector<Grid::ColumnAttribute>& attr)
+{
+ //hold ownership of non-visible columns
+ oldColAttributes_ = attr;
+
+ std::vector<VisibleColumn> visCols;
+ for (const ColumnAttribute& ca : attr)
+ {
+ assert(ca.type_ != ColumnType::NONE);
+ if (ca.visible_)
+ visCols.emplace_back(ca.type_, ca.offset_, ca.stretch_);
+ }
+
+ //"ownership" of visible columns is now within Grid
+ visibleCols_ = visCols;
+
+ updateWindowSizes();
+ Refresh();
+}
+
+
+std::vector<Grid::ColumnAttribute> Grid::getColumnConfig() const
+{
+ //get non-visible columns (+ outdated visible ones)
+ std::vector<ColumnAttribute> output = oldColAttributes_;
+
+ auto iterVcols = visibleCols_.begin();
+ auto iterVcolsend = visibleCols_.end();
+
+ //update visible columns but keep order of non-visible ones!
+ for (ColumnAttribute& ca : output)
+ if (ca.visible_)
+ {
+ if (iterVcols != iterVcolsend)
+ {
+ ca.type_ = iterVcols->type_;
+ ca.stretch_ = iterVcols->stretch_;
+ ca.offset_ = iterVcols->offset_;
+ ++iterVcols;
+ }
+ else
+ assert(false);
+ }
+ assert(iterVcols == iterVcolsend);
+
+ return output;
+}
+
+
+void Grid::showScrollBars(Grid::ScrollBarStatus horizontal, Grid::ScrollBarStatus vertical)
+{
+ if (showScrollbarX_ == horizontal &&
+ showScrollbarY_ == vertical) return; //support polling!
+
+ showScrollbarX_ = horizontal;
+ showScrollbarY_ = vertical;
+
+ //the following wxGTK approach is pretty much identical to wxWidgets 2.9 ShowScrollbars() code!
+
+ auto mapStatus = [](ScrollBarStatus sbStatus) -> GtkPolicyType
+ {
+ switch (sbStatus)
+ {
+ case SB_SHOW_AUTOMATIC:
+ return GTK_POLICY_AUTOMATIC;
+ case SB_SHOW_ALWAYS:
+ return GTK_POLICY_ALWAYS;
+ case SB_SHOW_NEVER:
+ return GTK_POLICY_NEVER;
+ }
+ assert(false);
+ return GTK_POLICY_AUTOMATIC;
+ };
+
+ GtkWidget* gridWidget = wxWindow::m_widget;
+ GtkScrolledWindow* scrolledWindow = GTK_SCROLLED_WINDOW(gridWidget);
+ ::gtk_scrolled_window_set_policy(scrolledWindow,
+ mapStatus(horizontal),
+ mapStatus(vertical));
+
+ updateWindowSizes();
+}
+
+
+
+wxWindow& Grid::getCornerWin () { return *cornerWin_; }
+wxWindow& Grid::getRowLabelWin() { return *rowLabelWin_; }
+wxWindow& Grid::getColLabelWin() { return *colLabelWin_; }
+wxWindow& Grid::getMainWin () { return *mainWin_; }
+const wxWindow& Grid::getMainWin() const { return *mainWin_; }
+
+
+Opt<Grid::ColAction> Grid::clientPosToColumnAction(const wxPoint& pos) const
+{
+ const int absPosX = CalcUnscrolledPosition(pos).x;
+ if (absPosX >= 0)
+ {
+ const int resizeTolerance = allowColumnResize_ ? COLUMN_RESIZE_TOLERANCE : 0;
+ std::vector<ColumnWidth> absWidths = getColWidths(); //resolve stretched widths
+
+ int accuWidth = 0;
+ for (size_t col = 0; col < absWidths.size(); ++col)
+ {
+ accuWidth += absWidths[col].width_;
+ if (std::abs(absPosX - accuWidth) < resizeTolerance)
+ {
+ ColAction out;
+ out.wantResize = true;
+ out.col = col;
+ return out;
+ }
+ else if (absPosX < accuWidth)
+ {
+ ColAction out;
+ out.wantResize = false;
+ out.col = col;
+ return out;
+ }
+ }
+ }
+ return NoValue();
+}
+
+
+void Grid::moveColumn(size_t colFrom, size_t colTo)
+{
+ if (colFrom < visibleCols_.size() &&
+ colTo < visibleCols_.size() &&
+ colTo != colFrom)
+ {
+ const VisibleColumn colAtt = visibleCols_[colFrom];
+ visibleCols_.erase (visibleCols_.begin() + colFrom);
+ visibleCols_.insert(visibleCols_.begin() + colTo, colAtt);
+ }
+}
+
+
+ptrdiff_t Grid::clientPosToMoveTargetColumn(const wxPoint& pos) const
+{
+
+ const int absPosX = CalcUnscrolledPosition(pos).x;
+
+ int accWidth = 0;
+ std::vector<ColumnWidth> absWidths = getColWidths(); //resolve negative/stretched widths
+ for (auto itCol = absWidths.begin(); itCol != absWidths.end(); ++itCol)
+ {
+ const int width = itCol->width_; //beware dreaded unsigned conversions!
+ accWidth += width;
+
+ if (absPosX < accWidth - width / 2)
+ return itCol - absWidths.begin();
+ }
+ return absWidths.size();
+}
+
+
+ColumnType Grid::colToType(size_t col) const
+{
+ if (col < visibleCols_.size())
+ return visibleCols_[col].type_;
+ return ColumnType::NONE;
+}
+
+
+ptrdiff_t Grid::getRowAtPos(int posY) const { return rowLabelWin_->getRowAtPos(posY); }
+
+
+Grid::ColumnPosInfo Grid::getColumnAtPos(int posX) const
+{
+ if (posX >= 0)
+ {
+ int accWidth = 0;
+ for (const ColumnWidth& cw : getColWidths())
+ {
+ accWidth += cw.width_;
+ if (posX < accWidth)
+ return { cw.type_, posX + cw.width_ - accWidth, cw.width_ };
+ }
+ }
+ return { ColumnType::NONE, 0, 0 };
+}
+
+
+wxRect Grid::getColumnLabelArea(ColumnType colType) const
+{
+ std::vector<ColumnWidth> absWidths = getColWidths(); //resolve negative/stretched widths
+
+ //colType is not unique in general, but *this* function expects it!
+ assert(std::count_if(absWidths.begin(), absWidths.end(), [&](const ColumnWidth& cw) { return cw.type_ == colType; }) <= 1);
+
+ auto itCol = std::find_if(absWidths.begin(), absWidths.end(), [&](const ColumnWidth& cw) { return cw.type_ == colType; });
+ if (itCol != absWidths.end())
+ {
+ ptrdiff_t posX = 0;
+ for (auto it = absWidths.begin(); it != itCol; ++it)
+ posX += it->width_;
+
+ return wxRect(wxPoint(posX, 0), wxSize(itCol->width_, colLabelHeight_));
+ }
+ return wxRect();
+}
+
+
+void Grid::refreshCell(size_t row, ColumnType colType)
+{
+ const wxRect& colArea = getColumnLabelArea(colType); //returns empty rect if column not found
+ const wxRect& rowArea = rowLabelWin_->getRowLabelArea(row); //returns empty rect if row not found
+ if (colArea.height > 0 && rowArea.height > 0)
+ {
+ const wxPoint topLeft = CalcScrolledPosition(wxPoint(colArea.x, rowArea.y)); //absolute -> client coordinates
+ const wxRect cellArea(topLeft, wxSize(colArea.width, rowArea.height));
+
+ getMainWin().RefreshRect(cellArea, false);
+ }
+}
+
+
+void Grid::setGridCursor(size_t row)
+{
+ mainWin_->setCursor(row, row);
+ makeRowVisible(row);
+
+ selection_.clear(); //clear selection, do NOT fire event
+ selectRangeAndNotify(row, row, true /*positive*/, nullptr /*mouseInitiated*/); //set new selection + fire event
+
+ mainWin_->Refresh();
+ rowLabelWin_->Refresh(); //row labels! (Kubuntu)
+}
+
+
+void Grid::selectWithCursor(ptrdiff_t row)
+{
+ const size_t anchorRow = mainWin_->getAnchor();
+
+ mainWin_->setCursor(row, anchorRow);
+ makeRowVisible(row);
+
+ selection_.clear(); //clear selection, do NOT fire event
+ selectRangeAndNotify(anchorRow, row, true /*positive*/, nullptr /*mouseInitiated*/); //set new selection + fire event
+
+ mainWin_->Refresh();
+ rowLabelWin_->Refresh();
+}
+
+
+void Grid::makeRowVisible(size_t row)
+{
+ const wxRect labelRect = rowLabelWin_->getRowLabelArea(row); //returns empty rect if row not found
+ if (labelRect.height > 0)
+ {
+ int scrollPosX = 0;
+ GetViewStart(&scrollPosX, nullptr);
+
+ int pixelsPerUnitY = 0;
+ GetScrollPixelsPerUnit(nullptr, &pixelsPerUnitY);
+ if (pixelsPerUnitY <= 0) return;
+
+ const int clientPosY = CalcScrolledPosition(labelRect.GetTopLeft()).y;
+ if (clientPosY < 0)
+ {
+ const int scrollPosY = labelRect.y / pixelsPerUnitY;
+ Scroll(scrollPosX, scrollPosY); //internally calls wxWindows::Update()!
+ updateWindowSizes(); //may show horizontal scroll bar
+ }
+ else if (clientPosY + labelRect.height > rowLabelWin_->GetClientSize().GetHeight())
+ {
+ auto execScroll = [&](int clientHeight)
+ {
+ const int scrollPosY = std::ceil((labelRect.y - clientHeight +
+ labelRect.height) / static_cast<double>(pixelsPerUnitY));
+ Scroll(scrollPosX, scrollPosY);
+ updateWindowSizes(); //may show horizontal scroll bar
+ };
+
+ const int clientHeightBefore = rowLabelWin_->GetClientSize().GetHeight();
+ execScroll(clientHeightBefore);
+
+ //client height may decrease after scroll due to a new horizontal scrollbar, resulting in a partially visible last row
+ const int clientHeightAfter = rowLabelWin_->GetClientSize().GetHeight();
+ if (clientHeightAfter < clientHeightBefore)
+ execScroll(clientHeightAfter);
+ }
+ }
+}
+
+
+void Grid::selectRangeAndNotify(ptrdiff_t rowFrom, ptrdiff_t rowTo, bool positive, const GridClickEvent* mouseInitiated)
+{
+ //sort + convert to half-open range
+ auto rowFirst = std::min(rowFrom, rowTo);
+ auto rowLast = std::max(rowFrom, rowTo) + 1;
+
+ const size_t rowCount = getRowCount();
+ numeric::clamp<ptrdiff_t>(rowFirst, 0, rowCount);
+ numeric::clamp<ptrdiff_t>(rowLast, 0, rowCount);
+
+ selection_.selectRange(rowFirst, rowLast, positive);
+
+ //notify event
+ GridRangeSelectEvent selectionEvent(rowFirst, rowLast, positive, mouseInitiated);
+ if (wxEvtHandler* evtHandler = GetEventHandler())
+ evtHandler->ProcessEvent(selectionEvent);
+
+ mainWin_->Refresh();
+}
+
+
+void Grid::scrollTo(size_t row)
+{
+ const wxRect labelRect = rowLabelWin_->getRowLabelArea(row); //returns empty rect if row not found
+ if (labelRect.height > 0)
+ {
+ int pixelsPerUnitY = 0;
+ GetScrollPixelsPerUnit(nullptr, &pixelsPerUnitY);
+ if (pixelsPerUnitY > 0)
+ {
+ const int scrollPosYNew = labelRect.y / pixelsPerUnitY;
+ int scrollPosXOld = 0;
+ int scrollPosYOld = 0;
+ GetViewStart(&scrollPosXOld, &scrollPosYOld);
+
+ if (scrollPosYOld != scrollPosYNew) //support polling
+ {
+ Scroll(scrollPosXOld, scrollPosYNew); //internally calls wxWindows::Update()!
+ updateWindowSizes(); //may show horizontal scroll bar
+ Refresh();
+ }
+ }
+ }
+}
+
+
+bool Grid::Enable(bool enable)
+{
+ Refresh();
+ return wxScrolledWindow::Enable(enable);
+}
+
+
+size_t Grid::getGridCursor() const
+{
+ return mainWin_->getCursor();
+}
+
+
+int Grid::getBestColumnSize(size_t col) const
+{
+ if (dataView_ && col < visibleCols_.size())
+ {
+ const ColumnType type = visibleCols_[col].type_;
+
+ wxClientDC dc(mainWin_);
+ dc.SetFont(mainWin_->GetFont()); //harmonize with MainWin::render()
+
+ int maxSize = 0;
+
+ auto rowRange = rowLabelWin_->getRowsOnClient(mainWin_->GetClientRect()); //returns range [begin, end)
+ for (auto row = rowRange.first; row < rowRange.second; ++row)
+ maxSize = std::max(maxSize, dataView_->getBestSize(dc, row, type));
+
+ return maxSize;
+ }
+ return -1;
+}
+
+
+void Grid::setColumnWidth(int width, size_t col, GridEventPolicy columnResizeEventPolicy, bool notifyAsync)
+{
+ if (col < visibleCols_.size())
+ {
+ VisibleColumn& vcRs = visibleCols_[col];
+
+ const std::vector<int> stretchedWidths = getColStretchedWidths(mainWin_->GetClientSize().GetWidth());
+ if (stretchedWidths.size() != visibleCols_.size())
+ {
+ assert(false);
+ return;
+ }
+ //CAVEATS:
+ //I. fixed-size columns: normalize offset so that resulting width is at least COLUMN_MIN_WIDTH: this is NOT enforced by getColWidths()!
+ //II. stretched columns: do not allow user to set offsets so small that they result in negative (non-normalized) widths: this gives an
+ //unusual delay when enlarging the column again later
+ width = std::max(width, COLUMN_MIN_WIDTH);
+
+ vcRs.offset_ = width - stretchedWidths[col]; //width := stretchedWidth + offset
+
+ //III. resizing any column should normalize *all* other stretched columns' offsets considering current mainWinWidth!
+ // test case:
+ //1. have columns, both fixed-size and stretched, fit whole window width
+ //2. shrink main window width so that horizontal scrollbars are shown despite the streched column
+ //3. shrink a fixed-size column so that the scrollbars vanish and columns cover full width again
+ //4. now verify that the stretched column is resizing immediately if main window is enlarged again
+ for (size_t col2 = 0; col2 < visibleCols_.size(); ++col2)
+ if (visibleCols_[col2].stretch_ > 0) //normalize stretched columns only
+ visibleCols_[col2].offset_ = std::max(visibleCols_[col2].offset_, COLUMN_MIN_WIDTH - stretchedWidths[col2]);
+
+ if (columnResizeEventPolicy == ALLOW_GRID_EVENT)
+ {
+ GridColumnResizeEvent sizeEvent(vcRs.offset_, vcRs.type_);
+ if (wxEvtHandler* evtHandler = GetEventHandler())
+ {
+ if (notifyAsync)
+ evtHandler->AddPendingEvent(sizeEvent);
+ else
+ evtHandler->ProcessEvent(sizeEvent);
+ }
+ }
+ }
+ else
+ assert(false);
+}
+
+
+void Grid::autoSizeColumns(GridEventPolicy columnResizeEventPolicy)
+{
+ if (allowColumnResize_)
+ {
+ for (size_t col = 0; col < visibleCols_.size(); ++col)
+ {
+ const int bestWidth = getBestColumnSize(col); //return -1 on error
+ if (bestWidth >= 0)
+ setColumnWidth(bestWidth, col, columnResizeEventPolicy, true);
+ }
+ updateWindowSizes();
+ Refresh();
+ }
+}
+
+
+std::vector<int> Grid::getColStretchedWidths(int clientWidth) const //final width = (normalized) (stretchedWidth + offset)
+{
+ assert(clientWidth >= 0);
+ clientWidth = std::max(clientWidth, 0);
+ int stretchTotal = 0;
+ for (const VisibleColumn& vc : visibleCols_)
+ {
+ assert(vc.stretch_ >= 0);
+ stretchTotal += vc.stretch_;
+ }
+
+ int remainingWidth = clientWidth;
+
+ std::vector<int> output;
+
+ if (stretchTotal <= 0)
+ output.resize(visibleCols_.size()); //fill with zeros
+ else
+ {
+ for (const VisibleColumn& vc : visibleCols_)
+ {
+ const int width = clientWidth * vc.stretch_ / stretchTotal; //rounds down!
+ output.push_back(width);
+ remainingWidth -= width;
+ }
+
+ //distribute *all* of clientWidth: should suffice to enlarge the first few stretched columns; no need to minimize total absolute error of distribution
+ if (remainingWidth > 0)
+ for (size_t col2 = 0; col2 < visibleCols_.size(); ++col2)
+ if (visibleCols_[col2].stretch_ > 0)
+ {
+ ++output[col2];
+ if (--remainingWidth == 0)
+ break;
+ }
+ assert(remainingWidth == 0);
+ }
+ return output;
+}
+
+
+std::vector<Grid::ColumnWidth> Grid::getColWidths() const
+{
+ return getColWidths(mainWin_->GetClientSize().GetWidth());
+}
+
+
+std::vector<Grid::ColumnWidth> Grid::getColWidths(int mainWinWidth) const //evaluate stretched columns
+{
+ const std::vector<int> stretchedWidths = getColStretchedWidths(mainWinWidth);
+ assert(stretchedWidths.size() == visibleCols_.size());
+
+ std::vector<ColumnWidth> output;
+ for (size_t col2 = 0; col2 < visibleCols_.size(); ++col2)
+ {
+ const auto& vc = visibleCols_[col2];
+ int width = stretchedWidths[col2] + vc.offset_;
+
+ if (vc.stretch_ > 0)
+ width = std::max(width, COLUMN_MIN_WIDTH); //normalization really needed here: e.g. smaller main window would result in negative width
+ else
+ width = std::max(width, 0); //support smaller width than COLUMN_MIN_WIDTH if set via configuration
+
+ output.emplace_back(vc.type_, width);
+ }
+ return output;
+}
+
+
+int Grid::getColWidthsSum(int mainWinWidth) const
+{
+ int sum = 0;
+ for (const ColumnWidth& cw : getColWidths(mainWinWidth))
+ sum += cw.width_;
+ return sum;
+}
bgstack15