From 9d071d2a2cec9a7662a02669488569a017f0ea35 Mon Sep 17 00:00:00 2001 From: Daniel Wilhelm Date: Mon, 13 Feb 2017 21:25:04 -0700 Subject: 8.9 --- wx+/grid.cpp | 4494 +++++++++++++++++++++++++++++----------------------------- 1 file changed, 2220 insertions(+), 2274 deletions(-) mode change 100644 => 100755 wx+/grid.cpp (limited to 'wx+/grid.cpp') diff --git a/wx+/grid.cpp b/wx+/grid.cpp old mode 100644 new mode 100755 index 5d393f08..75abcd2f --- 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 -#include -#include -#include -#include -#include -#include -#include -//#include -#include -#include -#include -#include -#include "dc.h" - -#ifdef ZEN_LINUX - #include -#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("\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 - 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 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 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((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 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 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 colWidth = refParent().getColWidth(action->col)) - activeResizing_ = std::make_unique(*this, action->col, *colWidth, event.GetPosition().x); - } - else //a move or single click - activeClickOrMove_ = std::make_unique(*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 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 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 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 action = refParent().clientPosToColumnAction(event.GetPosition())) - { - if (const Opt 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 activeResizing_; - std::unique_ptr activeClickOrMove_; - Opt 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 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(*this, row, !refParent().isSelected(row), mouseEvent); - else if (event.ShiftDown()) - { - activeSelection_ = std::make_unique(*this, selectionAnchor_, true, mouseEvent); - refParent().clearSelection(ALLOW_GRID_EVENT); - } - else - { - activeSelection_ = std::make_unique(*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(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(toScrollX_) != 0 || static_cast(toScrollY_) != 0) - { - wnd_.refParent().scrollDelta(static_cast(toScrollX_), static_cast(toScrollY_)); // - toScrollX_ -= static_cast(toScrollX_); //rounds down for positive numbers, up for negative, - toScrollY_ -= static_cast(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 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(yFrom, 0, logicalHeight - 1); - numeric::clamp(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(yFrom, 0, logicalHeight - 1); - numeric::clamp(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(row, 0, rowCount - 1); - setGridCursor(row); - } - }; - - auto selectWithCursorTo = [&](ptrdiff_t row) - { - if (rowCount > 0) - { - numeric::clamp(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& attr) -{ - //hold ownership of non-visible columns - oldColAttributes_ = attr; - - std::vector 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::getColumnConfig() const -{ - //get non-visible columns (+ outdated visible ones) - std::vector 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::clientPosToColumnAction(const wxPoint& pos) const -{ - const int absPosX = CalcUnscrolledPosition(pos).x; - if (absPosX >= 0) - { - const int resizeTolerance = allowColumnResize_ ? COLUMN_RESIZE_TOLERANCE : 0; - std::vector 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 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 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(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(rowFirst, 0, rowCount); - numeric::clamp(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 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 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 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::getColWidths() const -{ - return getColWidths(mainWin_->GetClientSize().GetWidth()); -} - - -std::vector Grid::getColWidths(int mainWinWidth) const //evaluate stretched columns -{ - const std::vector stretchedWidths = getColStretchedWidths(mainWinWidth); - assert(stretchedWidths.size() == visibleCols_.size()); - - std::vector 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 +#include +#include +#include +#include +#include +#include +#include +//#include +#include +#include +#include +#include +#include "dc.h" + + #include + +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("\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 + 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 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 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((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 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 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 colWidth = refParent().getColWidth(action->col)) + activeResizing_ = std::make_unique(*this, action->col, *colWidth, event.GetPosition().x); + } + else //a move or single click + activeClickOrMove_ = std::make_unique(*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 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 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 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 action = refParent().clientPosToColumnAction(event.GetPosition())) + { + if (const Opt 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 activeResizing_; + std::unique_ptr activeClickOrMove_; + Opt 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 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(*this, row, !refParent().isSelected(row), mouseEvent); + else if (event.ShiftDown()) + { + activeSelection_ = std::make_unique(*this, selectionAnchor_, true, mouseEvent); + refParent().clearSelection(ALLOW_GRID_EVENT); + } + else + { + activeSelection_ = std::make_unique(*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(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(toScrollX_) != 0 || static_cast(toScrollY_) != 0) + { + wnd_.refParent().scrollDelta(static_cast(toScrollX_), static_cast(toScrollY_)); // + toScrollX_ -= static_cast(toScrollX_); //rounds down for positive numbers, up for negative, + toScrollY_ -= static_cast(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 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(yFrom, 0, logicalHeight - 1); + numeric::clamp(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(yFrom, 0, logicalHeight - 1); + numeric::clamp(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(row, 0, rowCount - 1); + setGridCursor(row); + } + }; + + auto selectWithCursorTo = [&](ptrdiff_t row) + { + if (rowCount > 0) + { + numeric::clamp(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& attr) +{ + //hold ownership of non-visible columns + oldColAttributes_ = attr; + + std::vector 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::getColumnConfig() const +{ + //get non-visible columns (+ outdated visible ones) + std::vector 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::clientPosToColumnAction(const wxPoint& pos) const +{ + const int absPosX = CalcUnscrolledPosition(pos).x; + if (absPosX >= 0) + { + const int resizeTolerance = allowColumnResize_ ? COLUMN_RESIZE_TOLERANCE : 0; + std::vector 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 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 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(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(rowFirst, 0, rowCount); + numeric::clamp(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 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 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 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::getColWidths() const +{ + return getColWidths(mainWin_->GetClientSize().GetWidth()); +} + + +std::vector Grid::getColWidths(int mainWinWidth) const //evaluate stretched columns +{ + const std::vector stretchedWidths = getColStretchedWidths(mainWinWidth); + assert(stretchedWidths.size() == visibleCols_.size()); + + std::vector 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; +} -- cgit