From 823740e1ffa2b3bd39f8dea8062f5c5a0d9c741b Mon Sep 17 00:00:00 2001 From: Daniel Wilhelm Date: Thu, 20 Apr 2017 16:55:28 -0600 Subject: normalize most lineendings --- wx+/app_main.h | 88 +- wx+/async_task.h | 286 +-- wx+/bitmap_button.h | 176 +- wx+/choice_enum.h | 230 +-- wx+/context_menu.h | 204 +- wx+/dc.h | 274 +-- wx+/file_drop.cpp | 112 +- wx+/file_drop.h | 130 +- wx+/font_size.h | 102 +- wx+/graph.cpp | 1694 ++++++++--------- wx+/graph.h | 708 +++---- wx+/grid.cpp | 4440 +++++++++++++++++++++---------------------- wx+/grid.h | 726 +++---- wx+/http.cpp | 534 +++--- wx+/http.h | 98 +- wx+/image_resources.cpp | 336 ++-- wx+/image_resources.h | 46 +- wx+/image_tools.cpp | 466 ++--- wx+/image_tools.h | 516 ++--- wx+/no_flicker.h | 78 +- wx+/popup_dlg.cpp | 604 +++--- wx+/popup_dlg.h | 188 +- wx+/popup_dlg_generated.cpp | 194 +- wx+/popup_dlg_generated.h | 142 +- wx+/rtl.h | 190 +- wx+/std_button_layout.h | 234 +-- wx+/toggle_button.h | 134 +- wx+/tooltip.cpp | 194 +- wx+/tooltip.h | 64 +- wx+/zlib_wrap.cpp | 100 +- wx+/zlib_wrap.h | 228 +-- 31 files changed, 6758 insertions(+), 6758 deletions(-) (limited to 'wx+') diff --git a/wx+/app_main.h b/wx+/app_main.h index 4b2d92a4..48148802 100755 --- a/wx+/app_main.h +++ b/wx+/app_main.h @@ -1,44 +1,44 @@ -// ***************************************************************************** -// * 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 * -// ***************************************************************************** - -#ifndef APP_MAIN_H_08215601837818347575856 -#define APP_MAIN_H_08215601837818347575856 - -#include -#include - -namespace zen -{ -//just some wrapper around a global variable representing the (logical) main application window -void setMainWindow(wxWindow* window); //set main window and enable "exit on frame delete" -bool mainWindowWasSet(); - - - - - - -//######################## implementation ######################## -inline -bool& refMainWndStatus() -{ - static bool status = false; //external linkage! - return status; -} - -inline -void setMainWindow(wxWindow* window) -{ - wxTheApp->SetTopWindow(window); - wxTheApp->SetExitOnFrameDelete(true); - - refMainWndStatus() = true; -} - -inline bool mainWindowWasSet() { return refMainWndStatus(); } -} - -#endif //APP_MAIN_H_08215601837818347575856 +// ***************************************************************************** +// * 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 * +// ***************************************************************************** + +#ifndef APP_MAIN_H_08215601837818347575856 +#define APP_MAIN_H_08215601837818347575856 + +#include +#include + +namespace zen +{ +//just some wrapper around a global variable representing the (logical) main application window +void setMainWindow(wxWindow* window); //set main window and enable "exit on frame delete" +bool mainWindowWasSet(); + + + + + + +//######################## implementation ######################## +inline +bool& refMainWndStatus() +{ + static bool status = false; //external linkage! + return status; +} + +inline +void setMainWindow(wxWindow* window) +{ + wxTheApp->SetTopWindow(window); + wxTheApp->SetExitOnFrameDelete(true); + + refMainWndStatus() = true; +} + +inline bool mainWindowWasSet() { return refMainWndStatus(); } +} + +#endif //APP_MAIN_H_08215601837818347575856 diff --git a/wx+/async_task.h b/wx+/async_task.h index 36d9068e..df688147 100755 --- a/wx+/async_task.h +++ b/wx+/async_task.h @@ -1,143 +1,143 @@ -// ***************************************************************************** -// * 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 * -// ***************************************************************************** - -#ifndef ASYNC_TASK_H_839147839170432143214321 -#define ASYNC_TASK_H_839147839170432143214321 - -#include -#include -#include -#include -#include - - -namespace zen -{ -/* -Run a task in an async thread, but process result in GUI event loop -------------------------------------------------------------------- -1. put AsyncGuiQueue instance inside a dialog: - AsyncGuiQueue guiQueue; - -2. schedule async task and synchronous continuation: - guiQueue.processAsync(evalAsync, evalOnGui); - -Alternative: use wxWidgets' inter-thread communication (wxEvtHandler::QueueEvent) https://wiki.wxwidgets.org/Inter-Thread_and_Inter-Process_communication - => don't bother, probably too many MT race conditions lurking around -*/ - -namespace impl -{ -struct Task -{ - virtual ~Task() {} - virtual bool resultReady () const = 0; - virtual void evaluateResult() = 0; -}; - - -template -class ConcreteTask : public Task -{ -public: - template - ConcreteTask(std::future&& asyncResult, Fun2&& evalOnGui) : asyncResult_(std::move(asyncResult)), evalOnGui_(std::forward(evalOnGui)) {} - - bool resultReady () const override { return isReady(asyncResult_); } - void evaluateResult() override - { - evalResult(IsSameType()); - } - -private: - void evalResult(FalseType /*void result type*/) { evalOnGui_(asyncResult_.get()); } - void evalResult(TrueType /*void result type*/) { asyncResult_.get(); evalOnGui_(); } - - std::future asyncResult_; - Fun evalOnGui_; //keep "evalOnGui" strictly separated from async thread: in particular do not copy in thread! -}; - - -class AsyncTasks -{ -public: - AsyncTasks() {} - - template - void add(Fun&& evalAsync, Fun2&& evalOnGui) - { - using ResultType = decltype(evalAsync()); - tasks_.push_back(std::make_unique>(zen::runAsync(std::forward(evalAsync)), std::forward(evalOnGui))); - } - //equivalent to "evalOnGui(evalAsync())" - // -> evalAsync: the usual thread-safety requirements apply! - // -> evalOnGui: no thread-safety concerns, but must only reference variables with greater-equal lifetime than the AsyncTask instance! - - void evalResults() //call from gui thread repreatedly - { - if (!inRecursion_) //prevent implicit recursion, e.g. if we're called from an idle event and spawn another one within the callback below - { - inRecursion_ = true; - ZEN_ON_SCOPE_EXIT(inRecursion_ = false); - - std::vector> readyTasks; //Reentrancy; access to AsyncTasks::add is not protected! => evaluate outside erase_if - - erase_if(tasks_, [&](std::unique_ptr& task) - { - if (task->resultReady()) - { - readyTasks.push_back(std::move(task)); - return true; - } - return false; - }); - - for (auto& task : readyTasks) - task->evaluateResult(); - } - } - - bool empty() const { return tasks_.empty(); } - -private: - AsyncTasks (const AsyncTasks&) = delete; - AsyncTasks& operator=(const AsyncTasks&) = delete; - - bool inRecursion_ = false; - std::vector> tasks_; -}; -} - - -class AsyncGuiQueue : private wxEvtHandler -{ -public: - AsyncGuiQueue() { timer_.Connect(wxEVT_TIMER, wxEventHandler(AsyncGuiQueue::onTimerEvent), nullptr, this); } - - template - void processAsync(Fun&& evalAsync, Fun2&& evalOnGui) - { - asyncTasks_.add(std::forward(evalAsync), - std::forward(evalOnGui)); - if (!timer_.IsRunning()) - timer_.Start(50 /*unit: [ms]*/); - } - -private: - void onTimerEvent(wxEvent& event) //schedule and run long-running tasks asynchronously - { - asyncTasks_.evalResults(); //process results on GUI queue - if (asyncTasks_.empty()) - timer_.Stop(); - } - - impl::AsyncTasks asyncTasks_; - wxTimer timer_; //don't use wxWidgets' idle handling => repeated idle requests/consumption hogs 100% cpu! -}; - -} - -#endif //ASYNC_TASK_H_839147839170432143214321 +// ***************************************************************************** +// * 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 * +// ***************************************************************************** + +#ifndef ASYNC_TASK_H_839147839170432143214321 +#define ASYNC_TASK_H_839147839170432143214321 + +#include +#include +#include +#include +#include + + +namespace zen +{ +/* +Run a task in an async thread, but process result in GUI event loop +------------------------------------------------------------------- +1. put AsyncGuiQueue instance inside a dialog: + AsyncGuiQueue guiQueue; + +2. schedule async task and synchronous continuation: + guiQueue.processAsync(evalAsync, evalOnGui); + +Alternative: use wxWidgets' inter-thread communication (wxEvtHandler::QueueEvent) https://wiki.wxwidgets.org/Inter-Thread_and_Inter-Process_communication + => don't bother, probably too many MT race conditions lurking around +*/ + +namespace impl +{ +struct Task +{ + virtual ~Task() {} + virtual bool resultReady () const = 0; + virtual void evaluateResult() = 0; +}; + + +template +class ConcreteTask : public Task +{ +public: + template + ConcreteTask(std::future&& asyncResult, Fun2&& evalOnGui) : asyncResult_(std::move(asyncResult)), evalOnGui_(std::forward(evalOnGui)) {} + + bool resultReady () const override { return isReady(asyncResult_); } + void evaluateResult() override + { + evalResult(IsSameType()); + } + +private: + void evalResult(FalseType /*void result type*/) { evalOnGui_(asyncResult_.get()); } + void evalResult(TrueType /*void result type*/) { asyncResult_.get(); evalOnGui_(); } + + std::future asyncResult_; + Fun evalOnGui_; //keep "evalOnGui" strictly separated from async thread: in particular do not copy in thread! +}; + + +class AsyncTasks +{ +public: + AsyncTasks() {} + + template + void add(Fun&& evalAsync, Fun2&& evalOnGui) + { + using ResultType = decltype(evalAsync()); + tasks_.push_back(std::make_unique>(zen::runAsync(std::forward(evalAsync)), std::forward(evalOnGui))); + } + //equivalent to "evalOnGui(evalAsync())" + // -> evalAsync: the usual thread-safety requirements apply! + // -> evalOnGui: no thread-safety concerns, but must only reference variables with greater-equal lifetime than the AsyncTask instance! + + void evalResults() //call from gui thread repreatedly + { + if (!inRecursion_) //prevent implicit recursion, e.g. if we're called from an idle event and spawn another one within the callback below + { + inRecursion_ = true; + ZEN_ON_SCOPE_EXIT(inRecursion_ = false); + + std::vector> readyTasks; //Reentrancy; access to AsyncTasks::add is not protected! => evaluate outside erase_if + + erase_if(tasks_, [&](std::unique_ptr& task) + { + if (task->resultReady()) + { + readyTasks.push_back(std::move(task)); + return true; + } + return false; + }); + + for (auto& task : readyTasks) + task->evaluateResult(); + } + } + + bool empty() const { return tasks_.empty(); } + +private: + AsyncTasks (const AsyncTasks&) = delete; + AsyncTasks& operator=(const AsyncTasks&) = delete; + + bool inRecursion_ = false; + std::vector> tasks_; +}; +} + + +class AsyncGuiQueue : private wxEvtHandler +{ +public: + AsyncGuiQueue() { timer_.Connect(wxEVT_TIMER, wxEventHandler(AsyncGuiQueue::onTimerEvent), nullptr, this); } + + template + void processAsync(Fun&& evalAsync, Fun2&& evalOnGui) + { + asyncTasks_.add(std::forward(evalAsync), + std::forward(evalOnGui)); + if (!timer_.IsRunning()) + timer_.Start(50 /*unit: [ms]*/); + } + +private: + void onTimerEvent(wxEvent& event) //schedule and run long-running tasks asynchronously + { + asyncTasks_.evalResults(); //process results on GUI queue + if (asyncTasks_.empty()) + timer_.Stop(); + } + + impl::AsyncTasks asyncTasks_; + wxTimer timer_; //don't use wxWidgets' idle handling => repeated idle requests/consumption hogs 100% cpu! +}; + +} + +#endif //ASYNC_TASK_H_839147839170432143214321 diff --git a/wx+/bitmap_button.h b/wx+/bitmap_button.h index 2ed8a278..18702dd3 100755 --- a/wx+/bitmap_button.h +++ b/wx+/bitmap_button.h @@ -1,88 +1,88 @@ -// ***************************************************************************** -// * 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 * -// ***************************************************************************** - -#ifndef BITMAP_BUTTON_H_83415718945878341563415 -#define BITMAP_BUTTON_H_83415718945878341563415 - -#include -#include "image_tools.h" - - -namespace zen -{ -//zen::BitmapTextButton is identical to wxBitmapButton, but preserves the label via SetLabel(), which wxFormbuilder would ditch! -class BitmapTextButton : public wxBitmapButton -{ -public: - BitmapTextButton(wxWindow* parent, - wxWindowID id, - const wxString& label, - const wxPoint& pos = wxDefaultPosition, - const wxSize& size = wxDefaultSize, - long style = 0, - const wxValidator& validator = wxDefaultValidator, - const wxString& name = wxButtonNameStr) : - wxBitmapButton(parent, id, wxNullBitmap, pos, size, style | wxBU_AUTODRAW, validator, name) { SetLabel(label); } -}; - -void setBitmapTextLabel(wxBitmapButton& btn, const wxImage& img, const wxString& text, int gap = 5, int border = 5); - -//set bitmap label flicker free: -void setImage(wxBitmapButton& button, const wxBitmap& bmp); - - - - - - - - - - - -//################################### implementation ################################### -inline -void setBitmapTextLabel(wxBitmapButton& btn, const wxImage& img, const wxString& text, int gap, int border) -{ - assert(gap >= 0 && border >= 0); - gap = std::max(0, gap); - border = std::max(0, border); - - wxImage dynImage = createImageFromText(text, btn.GetFont(), btn.GetForegroundColour()); - if (img.IsOk()) - { - if (btn.GetLayoutDirection() != wxLayout_RightToLeft) - dynImage = stackImages(img, dynImage, ImageStackLayout::HORIZONTAL, ImageStackAlignment::CENTER, gap); - else - dynImage = stackImages(dynImage, img, ImageStackLayout::HORIZONTAL, ImageStackAlignment::CENTER, gap); - } - - //SetMinSize() instead of SetSize() is needed here for wxWindows layout determination to work corretly - const int defaultHeight = wxButton::GetDefaultSize().GetHeight(); - btn.SetMinSize(wxSize(dynImage.GetWidth () + 2 * border, - std::max(dynImage.GetHeight() + 2 * border, defaultHeight))); - - btn.SetBitmapLabel(wxBitmap(dynImage)); - //SetLabel() calls confuse wxBitmapButton in the disabled state and it won't show the image! workaround: - btn.SetBitmapDisabled(wxBitmap(dynImage.ConvertToDisabled())); -} - - -inline -void setImage(wxBitmapButton& button, const wxBitmap& bmp) -{ - if (!isEqual(button.GetBitmapLabel(), bmp)) - { - button.SetBitmapLabel(bmp); - - //wxWidgets excels at screwing up consistently once again: - //the first call to SetBitmapLabel() *implicitly* sets the disabled bitmap, too, subsequent calls, DON'T! - button.SetBitmapDisabled(bmp.ConvertToDisabled()); //inefficiency: wxBitmap::ConvertToDisabled() implicitly converts to wxImage! - } -} -} - -#endif //BITMAP_BUTTON_H_83415718945878341563415 +// ***************************************************************************** +// * 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 * +// ***************************************************************************** + +#ifndef BITMAP_BUTTON_H_83415718945878341563415 +#define BITMAP_BUTTON_H_83415718945878341563415 + +#include +#include "image_tools.h" + + +namespace zen +{ +//zen::BitmapTextButton is identical to wxBitmapButton, but preserves the label via SetLabel(), which wxFormbuilder would ditch! +class BitmapTextButton : public wxBitmapButton +{ +public: + BitmapTextButton(wxWindow* parent, + wxWindowID id, + const wxString& label, + const wxPoint& pos = wxDefaultPosition, + const wxSize& size = wxDefaultSize, + long style = 0, + const wxValidator& validator = wxDefaultValidator, + const wxString& name = wxButtonNameStr) : + wxBitmapButton(parent, id, wxNullBitmap, pos, size, style | wxBU_AUTODRAW, validator, name) { SetLabel(label); } +}; + +void setBitmapTextLabel(wxBitmapButton& btn, const wxImage& img, const wxString& text, int gap = 5, int border = 5); + +//set bitmap label flicker free: +void setImage(wxBitmapButton& button, const wxBitmap& bmp); + + + + + + + + + + + +//################################### implementation ################################### +inline +void setBitmapTextLabel(wxBitmapButton& btn, const wxImage& img, const wxString& text, int gap, int border) +{ + assert(gap >= 0 && border >= 0); + gap = std::max(0, gap); + border = std::max(0, border); + + wxImage dynImage = createImageFromText(text, btn.GetFont(), btn.GetForegroundColour()); + if (img.IsOk()) + { + if (btn.GetLayoutDirection() != wxLayout_RightToLeft) + dynImage = stackImages(img, dynImage, ImageStackLayout::HORIZONTAL, ImageStackAlignment::CENTER, gap); + else + dynImage = stackImages(dynImage, img, ImageStackLayout::HORIZONTAL, ImageStackAlignment::CENTER, gap); + } + + //SetMinSize() instead of SetSize() is needed here for wxWindows layout determination to work corretly + const int defaultHeight = wxButton::GetDefaultSize().GetHeight(); + btn.SetMinSize(wxSize(dynImage.GetWidth () + 2 * border, + std::max(dynImage.GetHeight() + 2 * border, defaultHeight))); + + btn.SetBitmapLabel(wxBitmap(dynImage)); + //SetLabel() calls confuse wxBitmapButton in the disabled state and it won't show the image! workaround: + btn.SetBitmapDisabled(wxBitmap(dynImage.ConvertToDisabled())); +} + + +inline +void setImage(wxBitmapButton& button, const wxBitmap& bmp) +{ + if (!isEqual(button.GetBitmapLabel(), bmp)) + { + button.SetBitmapLabel(bmp); + + //wxWidgets excels at screwing up consistently once again: + //the first call to SetBitmapLabel() *implicitly* sets the disabled bitmap, too, subsequent calls, DON'T! + button.SetBitmapDisabled(bmp.ConvertToDisabled()); //inefficiency: wxBitmap::ConvertToDisabled() implicitly converts to wxImage! + } +} +} + +#endif //BITMAP_BUTTON_H_83415718945878341563415 diff --git a/wx+/choice_enum.h b/wx+/choice_enum.h index 9197db9a..d564fef8 100755 --- a/wx+/choice_enum.h +++ b/wx+/choice_enum.h @@ -1,115 +1,115 @@ -// ***************************************************************************** -// * 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 * -// ***************************************************************************** - -#ifndef CHOICE_ENUM_H_132413545345687 -#define CHOICE_ENUM_H_132413545345687 - -#include -#include - -//handle mapping of enum values to wxChoice controls -/* -Example: - -Member variable: - zen::EnumDescrList enumDescrMap; - -Constructor code: - enumDescrMap. - add(ON_ERROR_POPUP , "Show pop-up" , "Show pop-up on errors or warnings"). <- add localization - add(ON_ERROR_IGNORE, "Ignore errors" , "Hide all error and warning messages"). - add(ON_ERROR_EXIT , "Exit instantly", "Abort synchronization immediately"); - -Set enum value: - setEnumVal(enumDescrMap, *m_choiceHandleError, value); - -Get enum value: - value = getEnumVal(enumDescrMap, *m_choiceHandleError) - -Update enum tooltips (after user changed selection): - updateTooltipEnumVal(enumDescrMap, *m_choiceHandleError); -*/ - - -namespace zen -{ -template -struct EnumDescrList -{ - EnumDescrList& add(Enum value, const wxString& text, const wxString& tooltip = {}) - { - descrList.emplace_back(value, std::make_pair(text, tooltip)); - return *this; - } - using DescrList = std::vector>>; - DescrList descrList; -}; -template void setEnumVal(const EnumDescrList& mapping, wxChoice& ctrl, Enum value); -template Enum getEnumVal(const EnumDescrList& mapping, const wxChoice& ctrl); -template void updateTooltipEnumVal(const EnumDescrList& mapping, wxChoice& ctrl); - - - - - - - - - - - - - - -//--------------- impelementation ------------------------------------------- -template -void setEnumVal(const EnumDescrList& mapping, wxChoice& ctrl, Enum value) -{ - ctrl.Clear(); - - int selectedPos = 0; - for (auto it = mapping.descrList.begin(); it != mapping.descrList.end(); ++it) - { - ctrl.Append(it->second.first); - if (it->first == value) - { - selectedPos = it - mapping.descrList.begin(); - - if (!it->second.second.empty()) - ctrl.SetToolTip(it->second.second); - } - } - - ctrl.SetSelection(selectedPos); -} - -template -Enum getEnumVal(const EnumDescrList& mapping, const wxChoice& ctrl) -{ - const int selectedPos = ctrl.GetSelection(); - - if (0 <= selectedPos && selectedPos < static_cast(mapping.descrList.size())) - return mapping.descrList[selectedPos].first; - else - { - assert(false); - return Enum(0); - } -} - -template void updateTooltipEnumVal(const EnumDescrList& mapping, wxChoice& ctrl) -{ - const Enum value = getEnumVal(mapping, ctrl); - - for (const auto& item : mapping.descrList) - if (item.first == value) - ctrl.SetToolTip(item.second.second); -} - -} - - -#endif //CHOICE_ENUM_H_132413545345687 +// ***************************************************************************** +// * 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 * +// ***************************************************************************** + +#ifndef CHOICE_ENUM_H_132413545345687 +#define CHOICE_ENUM_H_132413545345687 + +#include +#include + +//handle mapping of enum values to wxChoice controls +/* +Example: + +Member variable: + zen::EnumDescrList enumDescrMap; + +Constructor code: + enumDescrMap. + add(ON_ERROR_POPUP , "Show pop-up" , "Show pop-up on errors or warnings"). <- add localization + add(ON_ERROR_IGNORE, "Ignore errors" , "Hide all error and warning messages"). + add(ON_ERROR_EXIT , "Exit instantly", "Abort synchronization immediately"); + +Set enum value: + setEnumVal(enumDescrMap, *m_choiceHandleError, value); + +Get enum value: + value = getEnumVal(enumDescrMap, *m_choiceHandleError) + +Update enum tooltips (after user changed selection): + updateTooltipEnumVal(enumDescrMap, *m_choiceHandleError); +*/ + + +namespace zen +{ +template +struct EnumDescrList +{ + EnumDescrList& add(Enum value, const wxString& text, const wxString& tooltip = {}) + { + descrList.emplace_back(value, std::make_pair(text, tooltip)); + return *this; + } + using DescrList = std::vector>>; + DescrList descrList; +}; +template void setEnumVal(const EnumDescrList& mapping, wxChoice& ctrl, Enum value); +template Enum getEnumVal(const EnumDescrList& mapping, const wxChoice& ctrl); +template void updateTooltipEnumVal(const EnumDescrList& mapping, wxChoice& ctrl); + + + + + + + + + + + + + + +//--------------- impelementation ------------------------------------------- +template +void setEnumVal(const EnumDescrList& mapping, wxChoice& ctrl, Enum value) +{ + ctrl.Clear(); + + int selectedPos = 0; + for (auto it = mapping.descrList.begin(); it != mapping.descrList.end(); ++it) + { + ctrl.Append(it->second.first); + if (it->first == value) + { + selectedPos = it - mapping.descrList.begin(); + + if (!it->second.second.empty()) + ctrl.SetToolTip(it->second.second); + } + } + + ctrl.SetSelection(selectedPos); +} + +template +Enum getEnumVal(const EnumDescrList& mapping, const wxChoice& ctrl) +{ + const int selectedPos = ctrl.GetSelection(); + + if (0 <= selectedPos && selectedPos < static_cast(mapping.descrList.size())) + return mapping.descrList[selectedPos].first; + else + { + assert(false); + return Enum(0); + } +} + +template void updateTooltipEnumVal(const EnumDescrList& mapping, wxChoice& ctrl) +{ + const Enum value = getEnumVal(mapping, ctrl); + + for (const auto& item : mapping.descrList) + if (item.first == value) + ctrl.SetToolTip(item.second.second); +} + +} + + +#endif //CHOICE_ENUM_H_132413545345687 diff --git a/wx+/context_menu.h b/wx+/context_menu.h index 9daba261..bd77100c 100755 --- a/wx+/context_menu.h +++ b/wx+/context_menu.h @@ -1,102 +1,102 @@ -// ***************************************************************************** -// * 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 * -// ***************************************************************************** - -#ifndef CONTEXT_MENU_H_18047302153418174632141234 -#define CONTEXT_MENU_H_18047302153418174632141234 - -#include -#include -#include -#include -#include - -/* -A context menu supporting C++11 lambda callbacks! - -Usage: - ContextMenu menu; - menu.addItem(L"Some Label", [&]{ ...do something... }); -> capture by reference is fine, as long as captured variables have at least scope of ContextMenu::popup()! - ... - menu.popup(wnd); -*/ - -namespace zen -{ -class ContextMenu : private wxEvtHandler -{ -public: - ContextMenu() : menu(std::make_unique()) {} - - void addItem(const wxString& label, const std::function& command, const wxBitmap* bmp = nullptr, bool enabled = true) - { - wxMenuItem* newItem = new wxMenuItem(menu.get(), wxID_ANY, label); //menu owns item! - if (bmp) newItem->SetBitmap(*bmp); //do not set AFTER appending item! wxWidgets screws up for yet another crappy reason - menu->Append(newItem); - if (!enabled) newItem->Enable(false); //do not enable BEFORE appending item! wxWidgets screws up for yet another crappy reason - commandList[newItem->GetId()] = command; //defer event connection, this may be a submenu only! - } - - void addCheckBox(const wxString& label, const std::function& command, bool checked, bool enabled = true) - { - wxMenuItem* newItem = menu->AppendCheckItem(wxID_ANY, label); - newItem->Check(checked); - if (!enabled) newItem->Enable(false); - commandList[newItem->GetId()] = command; - } - - void addRadio(const wxString& label, const std::function& command, bool selected, bool enabled = true) - { - wxMenuItem* newItem = menu->AppendRadioItem(wxID_ANY, label); - newItem->Check(selected); - if (!enabled) newItem->Enable(false); - commandList[newItem->GetId()] = command; - } - - void addSeparator() { menu->AppendSeparator(); } - - void addSubmenu(const wxString& label, ContextMenu& submenu, const wxBitmap* bmp = nullptr) //invalidates submenu! - { - //transfer submenu commands: - commandList.insert(submenu.commandList.begin(), submenu.commandList.end()); - submenu.commandList.clear(); - - submenu.menu->SetNextHandler(menu.get()); //on wxGTK submenu events are not propagated to their parent menu by default! - - wxMenuItem* newItem = new wxMenuItem(menu.get(), wxID_ANY, label, L"", wxITEM_NORMAL, submenu.menu.release()); //menu owns item, item owns submenu! - if (bmp) newItem->SetBitmap(*bmp); //do not set AFTER appending item! wxWidgets screws up for yet another crappy reason - menu->Append(newItem); - } - - void popup(wxWindow& wnd, const wxPoint& pos = wxDefaultPosition) //show popup menu + process lambdas - { - //eventually all events from submenu items will be received by this menu - for (const auto& item : commandList) - menu->Connect(item.first, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(ContextMenu::onSelection), new GenericCommand(item.second) /*pass ownership*/, this); - - wnd.PopupMenu(menu.get(), pos); - wxTheApp->ProcessPendingEvents(); //make sure lambdas are evaluated before going out of scope; - //although all events seem to be processed within wxWindows::PopupMenu, we shouldn't trust wxWidgets in this regard - } - -private: - void onSelection(wxCommandEvent& event) - { - if (auto cmd = dynamic_cast(event.m_callbackUserData)) - (cmd->fun_)(); - } - - struct GenericCommand : public wxObject - { - GenericCommand(const std::function& fun) : fun_(fun) {} - std::function fun_; - }; - - std::unique_ptr menu; - std::map> commandList; //(item id, command) -}; -} - -#endif //CONTEXT_MENU_H_18047302153418174632141234 +// ***************************************************************************** +// * 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 * +// ***************************************************************************** + +#ifndef CONTEXT_MENU_H_18047302153418174632141234 +#define CONTEXT_MENU_H_18047302153418174632141234 + +#include +#include +#include +#include +#include + +/* +A context menu supporting C++11 lambda callbacks! + +Usage: + ContextMenu menu; + menu.addItem(L"Some Label", [&]{ ...do something... }); -> capture by reference is fine, as long as captured variables have at least scope of ContextMenu::popup()! + ... + menu.popup(wnd); +*/ + +namespace zen +{ +class ContextMenu : private wxEvtHandler +{ +public: + ContextMenu() : menu(std::make_unique()) {} + + void addItem(const wxString& label, const std::function& command, const wxBitmap* bmp = nullptr, bool enabled = true) + { + wxMenuItem* newItem = new wxMenuItem(menu.get(), wxID_ANY, label); //menu owns item! + if (bmp) newItem->SetBitmap(*bmp); //do not set AFTER appending item! wxWidgets screws up for yet another crappy reason + menu->Append(newItem); + if (!enabled) newItem->Enable(false); //do not enable BEFORE appending item! wxWidgets screws up for yet another crappy reason + commandList[newItem->GetId()] = command; //defer event connection, this may be a submenu only! + } + + void addCheckBox(const wxString& label, const std::function& command, bool checked, bool enabled = true) + { + wxMenuItem* newItem = menu->AppendCheckItem(wxID_ANY, label); + newItem->Check(checked); + if (!enabled) newItem->Enable(false); + commandList[newItem->GetId()] = command; + } + + void addRadio(const wxString& label, const std::function& command, bool selected, bool enabled = true) + { + wxMenuItem* newItem = menu->AppendRadioItem(wxID_ANY, label); + newItem->Check(selected); + if (!enabled) newItem->Enable(false); + commandList[newItem->GetId()] = command; + } + + void addSeparator() { menu->AppendSeparator(); } + + void addSubmenu(const wxString& label, ContextMenu& submenu, const wxBitmap* bmp = nullptr) //invalidates submenu! + { + //transfer submenu commands: + commandList.insert(submenu.commandList.begin(), submenu.commandList.end()); + submenu.commandList.clear(); + + submenu.menu->SetNextHandler(menu.get()); //on wxGTK submenu events are not propagated to their parent menu by default! + + wxMenuItem* newItem = new wxMenuItem(menu.get(), wxID_ANY, label, L"", wxITEM_NORMAL, submenu.menu.release()); //menu owns item, item owns submenu! + if (bmp) newItem->SetBitmap(*bmp); //do not set AFTER appending item! wxWidgets screws up for yet another crappy reason + menu->Append(newItem); + } + + void popup(wxWindow& wnd, const wxPoint& pos = wxDefaultPosition) //show popup menu + process lambdas + { + //eventually all events from submenu items will be received by this menu + for (const auto& item : commandList) + menu->Connect(item.first, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(ContextMenu::onSelection), new GenericCommand(item.second) /*pass ownership*/, this); + + wnd.PopupMenu(menu.get(), pos); + wxTheApp->ProcessPendingEvents(); //make sure lambdas are evaluated before going out of scope; + //although all events seem to be processed within wxWindows::PopupMenu, we shouldn't trust wxWidgets in this regard + } + +private: + void onSelection(wxCommandEvent& event) + { + if (auto cmd = dynamic_cast(event.m_callbackUserData)) + (cmd->fun_)(); + } + + struct GenericCommand : public wxObject + { + GenericCommand(const std::function& fun) : fun_(fun) {} + std::function fun_; + }; + + std::unique_ptr menu; + std::map> commandList; //(item id, command) +}; +} + +#endif //CONTEXT_MENU_H_18047302153418174632141234 diff --git a/wx+/dc.h b/wx+/dc.h index 1d1e31e3..e5739053 100755 --- a/wx+/dc.h +++ b/wx+/dc.h @@ -1,137 +1,137 @@ -// ***************************************************************************** -// * 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 * -// ***************************************************************************** - -#ifndef DC_H_4987123956832143243214 -#define DC_H_4987123956832143243214 - -#include -#include -#include //for macro: wxALWAYS_NATIVE_DOUBLE_BUFFER - -namespace zen -{ -/* -1. wxDCClipper does *not* stack: another fix for yet another poor wxWidgets implementation - -class RecursiveDcClipper -{ - RecursiveDcClipper(wxDC& dc, const wxRect& r) : dc_(dc) -}; - ------------------------------------------------------------------------------------------------- - -2. wxAutoBufferedPaintDC skips one pixel on left side when RTL layout is active: a fix for a poor wxWidgets implementation -class BufferedPaintDC -{ - BufferedPaintDC(wxWindow& wnd, std::unique_ptr& buffer); -}; -*/ - - - - - - - - - - - -//---------------------- implementation ------------------------ -class RecursiveDcClipper -{ -public: - RecursiveDcClipper(wxDC& dc, const wxRect& r) : dc_(dc) - { - auto it = refDcToAreaMap().find(&dc); - if (it != refDcToAreaMap().end()) - { - oldRect = it->second; - - wxRect tmp = r; - tmp.Intersect(*oldRect); //better safe than sorry - dc_.SetClippingRegion(tmp); // - it->second = tmp; - } - else - { - dc_.SetClippingRegion(r); - refDcToAreaMap().emplace(&dc_, r); - } - } - - ~RecursiveDcClipper() - { - dc_.DestroyClippingRegion(); - if (oldRect) - { - dc_.SetClippingRegion(*oldRect); - refDcToAreaMap()[&dc_] = *oldRect; - } - else - refDcToAreaMap().erase(&dc_); - } - -private: - //associate "active" clipping area with each DC - static std::unordered_map& refDcToAreaMap() { static std::unordered_map clippingAreas; return clippingAreas; } - - Opt oldRect; - wxDC& dc_; -}; - - -#ifndef wxALWAYS_NATIVE_DOUBLE_BUFFER - #error we need this one! -#endif - -#if wxALWAYS_NATIVE_DOUBLE_BUFFER -struct BufferedPaintDC : public wxPaintDC { BufferedPaintDC(wxWindow& wnd, Opt& buffer) : wxPaintDC(&wnd) {} }; - -#else -class BufferedPaintDC : public wxMemoryDC -{ -public: - BufferedPaintDC(wxWindow& wnd, Opt& buffer) : buffer_(buffer), paintDc(&wnd) - { - const wxSize clientSize = wnd.GetClientSize(); - if (clientSize.GetWidth() > 0 && clientSize.GetHeight() > 0) //wxBitmap asserts this!! width may be 0; test case "Grid::CornerWin": compare both sides, then change config - { - if (!buffer_ || clientSize != wxSize(buffer->GetWidth(), buffer->GetHeight())) - buffer = wxBitmap(clientSize.GetWidth(), clientSize.GetHeight()); - - SelectObject(*buffer); - - if (paintDc.IsOk() && paintDc.GetLayoutDirection() == wxLayout_RightToLeft) - SetLayoutDirection(wxLayout_RightToLeft); - } - else - buffer = NoValue(); - } - - ~BufferedPaintDC() - { - if (buffer_) - { - if (GetLayoutDirection() == wxLayout_RightToLeft) - { - paintDc.SetLayoutDirection(wxLayout_LeftToRight); //workaround bug in wxDC::Blit() - SetLayoutDirection(wxLayout_LeftToRight); // - } - - const wxPoint origin = GetDeviceOrigin(); - paintDc.Blit(0, 0, buffer_->GetWidth(), buffer_->GetHeight(), this, -origin.x, -origin.y); - } - } - -private: - Opt& buffer_; - wxPaintDC paintDc; -}; -#endif -} - -#endif //DC_H_4987123956832143243214 +// ***************************************************************************** +// * 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 * +// ***************************************************************************** + +#ifndef DC_H_4987123956832143243214 +#define DC_H_4987123956832143243214 + +#include +#include +#include //for macro: wxALWAYS_NATIVE_DOUBLE_BUFFER + +namespace zen +{ +/* +1. wxDCClipper does *not* stack: another fix for yet another poor wxWidgets implementation + +class RecursiveDcClipper +{ + RecursiveDcClipper(wxDC& dc, const wxRect& r) : dc_(dc) +}; + +------------------------------------------------------------------------------------------------ + +2. wxAutoBufferedPaintDC skips one pixel on left side when RTL layout is active: a fix for a poor wxWidgets implementation +class BufferedPaintDC +{ + BufferedPaintDC(wxWindow& wnd, std::unique_ptr& buffer); +}; +*/ + + + + + + + + + + + +//---------------------- implementation ------------------------ +class RecursiveDcClipper +{ +public: + RecursiveDcClipper(wxDC& dc, const wxRect& r) : dc_(dc) + { + auto it = refDcToAreaMap().find(&dc); + if (it != refDcToAreaMap().end()) + { + oldRect = it->second; + + wxRect tmp = r; + tmp.Intersect(*oldRect); //better safe than sorry + dc_.SetClippingRegion(tmp); // + it->second = tmp; + } + else + { + dc_.SetClippingRegion(r); + refDcToAreaMap().emplace(&dc_, r); + } + } + + ~RecursiveDcClipper() + { + dc_.DestroyClippingRegion(); + if (oldRect) + { + dc_.SetClippingRegion(*oldRect); + refDcToAreaMap()[&dc_] = *oldRect; + } + else + refDcToAreaMap().erase(&dc_); + } + +private: + //associate "active" clipping area with each DC + static std::unordered_map& refDcToAreaMap() { static std::unordered_map clippingAreas; return clippingAreas; } + + Opt oldRect; + wxDC& dc_; +}; + + +#ifndef wxALWAYS_NATIVE_DOUBLE_BUFFER + #error we need this one! +#endif + +#if wxALWAYS_NATIVE_DOUBLE_BUFFER +struct BufferedPaintDC : public wxPaintDC { BufferedPaintDC(wxWindow& wnd, Opt& buffer) : wxPaintDC(&wnd) {} }; + +#else +class BufferedPaintDC : public wxMemoryDC +{ +public: + BufferedPaintDC(wxWindow& wnd, Opt& buffer) : buffer_(buffer), paintDc(&wnd) + { + const wxSize clientSize = wnd.GetClientSize(); + if (clientSize.GetWidth() > 0 && clientSize.GetHeight() > 0) //wxBitmap asserts this!! width may be 0; test case "Grid::CornerWin": compare both sides, then change config + { + if (!buffer_ || clientSize != wxSize(buffer->GetWidth(), buffer->GetHeight())) + buffer = wxBitmap(clientSize.GetWidth(), clientSize.GetHeight()); + + SelectObject(*buffer); + + if (paintDc.IsOk() && paintDc.GetLayoutDirection() == wxLayout_RightToLeft) + SetLayoutDirection(wxLayout_RightToLeft); + } + else + buffer = NoValue(); + } + + ~BufferedPaintDC() + { + if (buffer_) + { + if (GetLayoutDirection() == wxLayout_RightToLeft) + { + paintDc.SetLayoutDirection(wxLayout_LeftToRight); //workaround bug in wxDC::Blit() + SetLayoutDirection(wxLayout_LeftToRight); // + } + + const wxPoint origin = GetDeviceOrigin(); + paintDc.Blit(0, 0, buffer_->GetWidth(), buffer_->GetHeight(), this, -origin.x, -origin.y); + } + } + +private: + Opt& buffer_; + wxPaintDC paintDc; +}; +#endif +} + +#endif //DC_H_4987123956832143243214 diff --git a/wx+/file_drop.cpp b/wx+/file_drop.cpp index f951ca3c..6a02749a 100755 --- a/wx+/file_drop.cpp +++ b/wx+/file_drop.cpp @@ -1,56 +1,56 @@ -// ***************************************************************************** -// * 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 "file_drop.h" -#include -#include - - -using namespace zen; - - -const wxEventType zen::EVENT_DROP_FILE = wxNewEventType(); - - - - -namespace -{ -class WindowDropTarget : public wxFileDropTarget -{ -public: - WindowDropTarget(wxWindow& dropWindow) : dropWindow_(dropWindow) {} - -private: - bool OnDropFiles(wxCoord x, wxCoord y, const wxArrayString& fileArray) override - { - /*Linux, MTP: we get an empty file array - => switching to wxTextDropTarget won't help (much): we'd get the format - mtp://[usb:001,002]/Telefonspeicher/Folder/file.txt - instead of - /run/user/1000/gvfs/mtp:host=%5Busb%3A001%2C002%5D/Telefonspeicher/Folder/file.txt - */ - - //wxPoint clientDropPos(x, y) - std::vector filePaths; - for (const wxString& file : fileArray) - filePaths.push_back(utfTo(file)); - - //create a custom event on drop window: execute event after file dropping is completed! (after mouse is released) - if (wxEvtHandler* handler = dropWindow_.GetEventHandler()) - handler->AddPendingEvent(FileDropEvent(filePaths)); - return true; - } - - wxWindow& dropWindow_; -}; -} - - -void zen::setupFileDrop(wxWindow& wnd) -{ - wnd.SetDropTarget(new WindowDropTarget(wnd)); /*takes ownership*/ -} +// ***************************************************************************** +// * 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 "file_drop.h" +#include +#include + + +using namespace zen; + + +const wxEventType zen::EVENT_DROP_FILE = wxNewEventType(); + + + + +namespace +{ +class WindowDropTarget : public wxFileDropTarget +{ +public: + WindowDropTarget(wxWindow& dropWindow) : dropWindow_(dropWindow) {} + +private: + bool OnDropFiles(wxCoord x, wxCoord y, const wxArrayString& fileArray) override + { + /*Linux, MTP: we get an empty file array + => switching to wxTextDropTarget won't help (much): we'd get the format + mtp://[usb:001,002]/Telefonspeicher/Folder/file.txt + instead of + /run/user/1000/gvfs/mtp:host=%5Busb%3A001%2C002%5D/Telefonspeicher/Folder/file.txt + */ + + //wxPoint clientDropPos(x, y) + std::vector filePaths; + for (const wxString& file : fileArray) + filePaths.push_back(utfTo(file)); + + //create a custom event on drop window: execute event after file dropping is completed! (after mouse is released) + if (wxEvtHandler* handler = dropWindow_.GetEventHandler()) + handler->AddPendingEvent(FileDropEvent(filePaths)); + return true; + } + + wxWindow& dropWindow_; +}; +} + + +void zen::setupFileDrop(wxWindow& wnd) +{ + wnd.SetDropTarget(new WindowDropTarget(wnd)); /*takes ownership*/ +} diff --git a/wx+/file_drop.h b/wx+/file_drop.h index d90ffc20..35b7ca9e 100755 --- a/wx+/file_drop.h +++ b/wx+/file_drop.h @@ -1,65 +1,65 @@ -// ***************************************************************************** -// * 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 * -// ***************************************************************************** - -#ifndef FILE_DROP_H_09457802957842560325626 -#define FILE_DROP_H_09457802957842560325626 - -#include -#include -#include -#include -#include - - -namespace zen -{ -//register simple file drop event (without issue of freezing dialogs and without wxFileDropTarget overdesign) -//CAVEAT: a drop target window must not be directly or indirectly contained within a wxStaticBoxSizer until the following wxGTK bug -//is fixed. According to wxWidgets release cycles this is expected to be: never http://trac.wxwidgets.org/ticket/2763 - -/* -1. setup a window to emit EVENT_DROP_FILE: - - simple file system paths: setupFileDrop - - any shell paths with validation: setupShellItemDrop - -2. register events: -wnd.Connect (EVENT_DROP_FILE, FileDropEventHandler(MyDlg::OnFilesDropped), nullptr, this); -wnd.Disconnect(EVENT_DROP_FILE, FileDropEventHandler(MyDlg::OnFilesDropped), nullptr, this); - -3. do something: -void MyDlg::OnFilesDropped(FileDropEvent& event); -*/ - -extern const wxEventType EVENT_DROP_FILE; - - -class FileDropEvent : public wxCommandEvent -{ -public: - FileDropEvent(const std::vector& droppedPaths) : wxCommandEvent(EVENT_DROP_FILE), droppedPaths_(droppedPaths) {} - - const std::vector& getPaths() const { return droppedPaths_; } - -private: - wxEvent* Clone() const override { return new FileDropEvent(*this); } - - const std::vector droppedPaths_; -}; - - -using FileDropEventFunction = void (wxEvtHandler::*)(FileDropEvent&); - -#define FileDropEventHandler(func) \ - (wxObjectEventFunction)(wxEventFunction)wxStaticCastEvent(FileDropEventFunction, &func) - - - - - -void setupFileDrop(wxWindow& wnd); -} - -#endif //FILE_DROP_H_09457802957842560325626 +// ***************************************************************************** +// * 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 * +// ***************************************************************************** + +#ifndef FILE_DROP_H_09457802957842560325626 +#define FILE_DROP_H_09457802957842560325626 + +#include +#include +#include +#include +#include + + +namespace zen +{ +//register simple file drop event (without issue of freezing dialogs and without wxFileDropTarget overdesign) +//CAVEAT: a drop target window must not be directly or indirectly contained within a wxStaticBoxSizer until the following wxGTK bug +//is fixed. According to wxWidgets release cycles this is expected to be: never http://trac.wxwidgets.org/ticket/2763 + +/* +1. setup a window to emit EVENT_DROP_FILE: + - simple file system paths: setupFileDrop + - any shell paths with validation: setupShellItemDrop + +2. register events: +wnd.Connect (EVENT_DROP_FILE, FileDropEventHandler(MyDlg::OnFilesDropped), nullptr, this); +wnd.Disconnect(EVENT_DROP_FILE, FileDropEventHandler(MyDlg::OnFilesDropped), nullptr, this); + +3. do something: +void MyDlg::OnFilesDropped(FileDropEvent& event); +*/ + +extern const wxEventType EVENT_DROP_FILE; + + +class FileDropEvent : public wxCommandEvent +{ +public: + FileDropEvent(const std::vector& droppedPaths) : wxCommandEvent(EVENT_DROP_FILE), droppedPaths_(droppedPaths) {} + + const std::vector& getPaths() const { return droppedPaths_; } + +private: + wxEvent* Clone() const override { return new FileDropEvent(*this); } + + const std::vector droppedPaths_; +}; + + +using FileDropEventFunction = void (wxEvtHandler::*)(FileDropEvent&); + +#define FileDropEventHandler(func) \ + (wxObjectEventFunction)(wxEventFunction)wxStaticCastEvent(FileDropEventFunction, &func) + + + + + +void setupFileDrop(wxWindow& wnd); +} + +#endif //FILE_DROP_H_09457802957842560325626 diff --git a/wx+/font_size.h b/wx+/font_size.h index b117b09f..cd2b80f2 100755 --- a/wx+/font_size.h +++ b/wx+/font_size.h @@ -1,51 +1,51 @@ -// ***************************************************************************** -// * 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 * -// ***************************************************************************** - -#ifndef FONT_SIZE_H_23849632846734343234532 -#define FONT_SIZE_H_23849632846734343234532 - -#include -#include -#include - - -namespace zen -{ -//set portable font size in multiples of the operating system's default font size -void setRelativeFontSize(wxWindow& control, double factor); -void setMainInstructionFont(wxWindow& control); //following Windows/Gnome/OS X guidelines - - - - - - - - - - -//###################### implementation ##################### -inline -void setRelativeFontSize(wxWindow& control, double factor) -{ - wxFont font = control.GetFont(); - font.SetPointSize(numeric::round(wxNORMAL_FONT->GetPointSize() * factor)); - control.SetFont(font); -}; - - -inline -void setMainInstructionFont(wxWindow& control) -{ - wxFont font = control.GetFont(); - font.SetPointSize(numeric::round(wxNORMAL_FONT->GetPointSize() * 12.0 / 11)); - font.SetWeight(wxFONTWEIGHT_BOLD); - - control.SetFont(font); -}; -} - -#endif //FONT_SIZE_H_23849632846734343234532 +// ***************************************************************************** +// * 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 * +// ***************************************************************************** + +#ifndef FONT_SIZE_H_23849632846734343234532 +#define FONT_SIZE_H_23849632846734343234532 + +#include +#include +#include + + +namespace zen +{ +//set portable font size in multiples of the operating system's default font size +void setRelativeFontSize(wxWindow& control, double factor); +void setMainInstructionFont(wxWindow& control); //following Windows/Gnome/OS X guidelines + + + + + + + + + + +//###################### implementation ##################### +inline +void setRelativeFontSize(wxWindow& control, double factor) +{ + wxFont font = control.GetFont(); + font.SetPointSize(numeric::round(wxNORMAL_FONT->GetPointSize() * factor)); + control.SetFont(font); +}; + + +inline +void setMainInstructionFont(wxWindow& control) +{ + wxFont font = control.GetFont(); + font.SetPointSize(numeric::round(wxNORMAL_FONT->GetPointSize() * 12.0 / 11)); + font.SetWeight(wxFONTWEIGHT_BOLD); + + control.SetFont(font); +}; +} + +#endif //FONT_SIZE_H_23849632846734343234532 diff --git a/wx+/graph.cpp b/wx+/graph.cpp index 50c1daa6..d45a0e85 100755 --- a/wx+/graph.cpp +++ b/wx+/graph.cpp @@ -1,847 +1,847 @@ -// ***************************************************************************** -// * 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 "graph.h" -#include -#include -#include -#include -#include -#include "dc.h" - -using namespace zen; - - -//todo: support zoom via mouse wheel? - -const wxEventType zen::wxEVT_GRAPH_SELECTION = wxNewEventType(); - - -double zen::nextNiceNumber(double blockSize) //round to next number which is a convenient to read block size -{ - if (blockSize <= 0) - return 0; - - const double k = std::floor(std::log10(blockSize)); - const double e = std::pow(10, k); - if (numeric::isNull(e)) - return 0; - const double a = blockSize / e; //blockSize = a * 10^k with a in [1, 10) - assert(1 <= a && a < 10); - - //have a look at leading two digits: "nice" numbers start with 1, 2, 2.5 and 5 - const double steps[] = { 1, 2, 2.5, 5, 10 }; - return e * numeric::nearMatch(a, std::begin(steps), std::end(steps)); -} - - -namespace -{ -wxColor getDefaultColor(size_t pos) -{ - switch (pos % 10) - { - case 0: - return { 0, 69, 134 }; //blue - case 1: - return { 255, 66, 14 }; //red - case 2: - return { 255, 211, 32 }; //yellow - case 3: - return { 87, 157, 28 }; //green - case 4: - return { 126, 0, 33 }; //royal - case 5: - return { 131, 202, 255 }; //light blue - case 6: - return { 49, 64, 4 }; //dark green - case 7: - return { 174, 207, 0 }; //light green - case 8: - return { 75, 31, 111 }; //purple - case 9: - return { 255, 149, 14 }; //orange - } - assert(false); - return *wxBLACK; -} - - -class ConvertCoord //convert between screen and input data coordinates -{ -public: - ConvertCoord(double valMin, double valMax, size_t screenSize) : - min_(valMin), - scaleToReal(screenSize == 0 ? 0 : (valMax - valMin) / screenSize), - scaleToScr(numeric::isNull((valMax - valMin)) ? 0 : screenSize / (valMax - valMin)), - outOfBoundsLow (-1 * scaleToReal + valMin), - outOfBoundsHigh((screenSize + 1) * scaleToReal + valMin) { if (outOfBoundsLow > outOfBoundsHigh) std::swap(outOfBoundsLow, outOfBoundsHigh); } - - double screenToReal(double screenPos) const //map [0, screenSize] -> [valMin, valMax] - { - return screenPos * scaleToReal + min_; - } - double realToScreen(double realPos) const //return screen position in pixel (but with double precision!) - { - return (realPos - min_) * scaleToScr; - } - int realToScreenRound(double realPos) const //returns -1 and screenSize + 1 if out of bounds! - { - //catch large double values: if double is larger than what int can represent => undefined behavior! - numeric::clamp(realPos, outOfBoundsLow, outOfBoundsHigh); - return numeric::round(realToScreen(realPos)); - } - -private: - double min_; - double scaleToReal; - double scaleToScr; - - double outOfBoundsLow; - double outOfBoundsHigh; -}; - - -//enlarge value range to display to a multiple of a "useful" block size -//returns block cound -int widenRange(double& valMin, double& valMax, //in/out - int graphAreaSize, //in pixel - int optimalBlockSizePx, // - const LabelFormatter& labelFmt) -{ - if (graphAreaSize <= 0) return 0; - - const double minValRangePerBlock = (valMax - valMin) / graphAreaSize; - const double proposedValRangePerBlock = (valMax - valMin) * optimalBlockSizePx / graphAreaSize; - double valRangePerBlock = labelFmt.getOptimalBlockSize(proposedValRangePerBlock); - assert(numeric::isNull(proposedValRangePerBlock) || valRangePerBlock > minValRangePerBlock); - - if (numeric::isNull(valRangePerBlock)) //valMin == valMax or strange "optimal block size" - return 1; - - //don't allow sub-pixel blocks! => avoid erroneously high GDI render work load! - if (valRangePerBlock < minValRangePerBlock) - valRangePerBlock = std::ceil(minValRangePerBlock / valRangePerBlock) * valRangePerBlock; - - double blockMin = std::floor(valMin / valRangePerBlock); //store as double, not int: truncation possible, e.g. if valRangePerBlock == 1 - double blockMax = std::ceil (valMax / valRangePerBlock); // - int blockCount = numeric::round(blockMax - blockMin); - assert(blockCount >= 0); - - //handle valMin == valMax == integer - if (blockCount <= 0) - { - ++blockMax; - blockCount = 1; - } - - valMin = blockMin * valRangePerBlock; - valMax = blockMax * valRangePerBlock; - return blockCount; -} - - -void drawXLabel(wxDC& dc, double xMin, double xMax, int blockCount, const ConvertCoord& cvrtX, const wxRect& graphArea, const wxRect& labelArea, const LabelFormatter& labelFmt) -{ - assert(graphArea.width == labelArea.width && graphArea.x == labelArea.x); - if (blockCount <= 0) - return; - - wxDCPenChanger dummy(dc, wxColor(192, 192, 192)); //light grey => not accessible! but no big deal... - wxDCTextColourChanger dummy2(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels - - const double valRangePerBlock = (xMax - xMin) / blockCount; - - for (int i = 1; i < blockCount; ++i) - { - //draw grey vertical lines - const double valX = xMin + i * valRangePerBlock; //step over raw data, not graph area pixels, to not lose precision - const int x = graphArea.x + cvrtX.realToScreenRound(valX); - - if (graphArea.height > 0) - dc.DrawLine(wxPoint(x, graphArea.y), wxPoint(x, graphArea.y + graphArea.height)); //wxDC::DrawLine() doesn't draw last pixel - - //draw x axis labels - const wxString label = labelFmt.formatText(valX, valRangePerBlock); - const wxSize labelExtent = dc.GetMultiLineTextExtent(label); - dc.DrawText(label, wxPoint(x - labelExtent.GetWidth() / 2, labelArea.y + (labelArea.height - labelExtent.GetHeight()) / 2)); //center - } -} - - -void drawYLabel(wxDC& dc, double yMin, double yMax, int blockCount, const ConvertCoord& cvrtY, const wxRect& graphArea, const wxRect& labelArea, const LabelFormatter& labelFmt) -{ - assert(graphArea.height == labelArea.height && graphArea.y == labelArea.y); - if (blockCount <= 0) - return; - - wxDCPenChanger dummy(dc, wxColor(192, 192, 192)); //light grey => not accessible! but no big deal... - wxDCTextColourChanger dummy2(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels - - const double valRangePerBlock = (yMax - yMin) / blockCount; - - for (int i = 1; i < blockCount; ++i) - { - //draw grey horizontal lines - const double valY = yMin + i * valRangePerBlock; //step over raw data, not graph area pixels, to not lose precision - const int y = graphArea.y + cvrtY.realToScreenRound(valY); - - if (graphArea.width > 0) - dc.DrawLine(wxPoint(graphArea.x, y), wxPoint(graphArea.x + graphArea.width, y)); //wxDC::DrawLine() doesn't draw last pixel - - //draw y axis labels - const wxString label = labelFmt.formatText(valY, valRangePerBlock); - const wxSize labelExtent = dc.GetMultiLineTextExtent(label); - dc.DrawText(label, wxPoint(labelArea.x + (labelArea.width - labelExtent.GetWidth()) / 2, y - labelExtent.GetHeight() / 2)); //center - } -} - - -void drawCornerText(wxDC& dc, const wxRect& graphArea, const wxString& txt, Graph2D::PosCorner pos) -{ - if (txt.empty()) return; - const int borderX = 5; - const int borderY = 2; //it looks like wxDC::GetMultiLineTextExtent() precisely returns width, but too large a height: maybe they consider "text row height"? - - wxDCTextColourChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels - wxSize txtExtent = dc.GetMultiLineTextExtent(txt); - txtExtent.x += 2 * borderX; - txtExtent.y += 2 * borderY; - - wxPoint drawPos = graphArea.GetTopLeft(); - switch (pos) - { - case Graph2D::CORNER_TOP_LEFT: - break; - case Graph2D::CORNER_TOP_RIGHT: - drawPos.x += graphArea.width - txtExtent.GetWidth(); - break; - case Graph2D::CORNER_BOTTOM_LEFT: - drawPos.y += graphArea.height - txtExtent.GetHeight(); - break; - case Graph2D::CORNER_BOTTOM_RIGHT: - drawPos.x += graphArea.width - txtExtent.GetWidth(); - drawPos.y += graphArea.height - txtExtent.GetHeight(); - break; - } - dc.DrawText(txt, drawPos + wxPoint(borderX, borderY)); -} - - -//calculate intersection of polygon with half-plane -template -void cutPoints(std::vector& curvePoints, std::vector& oobMarker, Function isInside, Function2 getIntersection, bool doPolygonCut) -{ - assert(curvePoints.size() == oobMarker.size()); - - if (curvePoints.size() != oobMarker.size() || curvePoints.empty()) return; - - auto isMarkedOob = [&](size_t index) { return oobMarker[index] != 0; }; //test if point is start of an OOB line - - std::vector curvePointsTmp; - std::vector oobMarkerTmp; - curvePointsTmp.reserve(curvePoints.size()); //allocating memory for these containers is one - oobMarkerTmp .reserve(oobMarker .size()); //of the more expensive operations of Graph2D! - - auto savePoint = [&](const CurvePoint& pt, bool markedOob) { curvePointsTmp.push_back(pt); oobMarkerTmp.push_back(markedOob); }; - - bool pointInside = isInside(curvePoints[0]); - if (pointInside) - savePoint(curvePoints[0], isMarkedOob(0)); - - for (size_t index = 1; index < curvePoints.size(); ++index) - { - if (isInside(curvePoints[index]) != pointInside) - { - pointInside = !pointInside; - const CurvePoint is = getIntersection(curvePoints[index - 1], curvePoints[index]); //getIntersection returns "to" when delta is zero - savePoint(is, !pointInside || isMarkedOob(index - 1)); - } - if (pointInside) - savePoint(curvePoints[index], isMarkedOob(index)); - } - - //make sure the output polygon area is correctly shaped if either begin or end points are cut - if (doPolygonCut) //note: impacts min/max height-calculations! - if (curvePoints.size() >= 3) - if (isInside(curvePoints.front()) != pointInside) - { - assert(!oobMarkerTmp.empty()); - oobMarkerTmp.back() = true; - - const CurvePoint is = getIntersection(curvePoints.back(), curvePoints.front()); - savePoint(is, true); - } - - curvePointsTmp.swap(curvePoints); - oobMarkerTmp .swap(oobMarker); -} - - -struct GetIntersectionX -{ - GetIntersectionX(double x) : x_(x) {} - CurvePoint operator()(const CurvePoint& from, const CurvePoint& to) const - { - const double deltaX = to.x - from.x; - const double deltaY = to.y - from.y; - return numeric::isNull(deltaX) ? to : CurvePoint(x_, from.y + (x_ - from.x) / deltaX * deltaY); - } - -private: - const double x_; -}; - -struct GetIntersectionY -{ - GetIntersectionY(double y) : y_(y) {} - CurvePoint operator()(const CurvePoint& from, const CurvePoint& to) const - { - const double deltaX = to.x - from.x; - const double deltaY = to.y - from.y; - return numeric::isNull(deltaY) ? to : CurvePoint(from.x + (y_ - from.y) / deltaY * deltaX, y_); - } - -private: - const double y_; -}; - -void cutPointsOutsideX(std::vector& curvePoints, std::vector& oobMarker, double minX, double maxX, bool doPolygonCut) -{ - cutPoints(curvePoints, oobMarker, [&](const CurvePoint& pt) { return pt.x >= minX; }, GetIntersectionX(minX), doPolygonCut); - cutPoints(curvePoints, oobMarker, [&](const CurvePoint& pt) { return pt.x <= maxX; }, GetIntersectionX(maxX), doPolygonCut); -} - -void cutPointsOutsideY(std::vector& curvePoints, std::vector& oobMarker, double minY, double maxY, bool doPolygonCut) -{ - cutPoints(curvePoints, oobMarker, [&](const CurvePoint& pt) { return pt.y >= minY; }, GetIntersectionY(minY), doPolygonCut); - cutPoints(curvePoints, oobMarker, [&](const CurvePoint& pt) { return pt.y <= maxY; }, GetIntersectionY(maxY), doPolygonCut); -} -} - - -std::vector ContinuousCurveData::getPoints(double minX, double maxX, int pixelWidth) const -{ - std::vector points; - - if (pixelWidth <= 1) return points; - const ConvertCoord cvrtX(minX, maxX, pixelWidth - 1); //map [minX, maxX] to [0, pixelWidth - 1] - - const std::pair rangeX = getRangeX(); - - const double screenLow = cvrtX.realToScreen(std::max(rangeX.first, minX)); //=> xLow >= 0 - const double screenHigh = cvrtX.realToScreen(std::min(rangeX.second, maxX)); //=> xHigh <= pixelWidth - 1 - //if double is larger than what int can represent => undefined behavior! - //=> convert to int *after* checking value range! - if (screenLow <= screenHigh) - { - const int posFrom = std::ceil (screenLow ); //do not step outside [minX, maxX] in loop below! - const int posTo = std::floor(screenHigh); // - //conversion from std::floor/std::ceil double return value to int is loss-free for full value range of 32-bit int! tested successfully on MSVC - - for (int i = posFrom; i <= posTo; ++i) - { - const double x = cvrtX.screenToReal(i); - points.emplace_back(x, getValue(x)); - } - } - return points; -} - - -std::vector SparseCurveData::getPoints(double minX, double maxX, int pixelWidth) const -{ - std::vector points; - if (pixelWidth <= 1) return points; - const ConvertCoord cvrtX(minX, maxX, pixelWidth - 1); //map [minX, maxX] to [0, pixelWidth - 1] - const std::pair rangeX = getRangeX(); - - auto addPoint = [&](const CurvePoint& pt) - { - if (!points.empty()) - { - if (pt.x <= points.back().x) //allow ascending x-positions only! algorithm below may cause double-insertion after empty x-ranges! - return; - - if (addSteps_) - if (pt.y != points.back().y) - points.emplace_back(CurvePoint(pt.x, points.back().y)); //[!] aliasing parameter not yet supported via emplace_back: VS bug! => make copy - } - points.push_back(pt); - }; - - const int posFrom = cvrtX.realToScreenRound(std::max(rangeX.first, minX)); - const int posTo = cvrtX.realToScreenRound(std::min(rangeX.second, maxX)); - - for (int i = posFrom; i <= posTo; ++i) - { - const double x = cvrtX.screenToReal(i); - Opt ptLe = getLessEq(x); - Opt ptGe = getGreaterEq(x); - //both non-existent and invalid return values are mapped to out of expected range: => check on posLe/posGe NOT ptLe/ptGe in the following! - const int posLe = ptLe ? cvrtX.realToScreenRound(ptLe->x) : i + 1; - const int posGe = ptGe ? cvrtX.realToScreenRound(ptGe->x) : i - 1; - assert(!ptLe || posLe <= i); //check for invalid return values - assert(!ptGe || posGe >= i); // - /* - Breakdown of all combinations of posLe, posGe and expected action (n >= 1) - Note: For every empty x-range of at least one pixel, both next and previous points must be saved to keep the interpolating line stable!!! - - posLe | posGe | action - +-------+-------+-------- - | none | none | break - | i | none | save ptLe; break - | i - n | none | break; - +-------+-------+-------- - | none | i | save ptGe; continue - | i | i | save one of ptLe, ptGe; continue - | i - n | i | save ptGe; continue - +-------+-------+-------- - | none | i + n | save ptGe; jump to position posGe + 1 - | i | i + n | save ptLe; if n == 1: continue; else: save ptGe; jump to position posGe + 1 - | i - n | i + n | save ptLe, ptGe; jump to position posGe + 1 - +-------+-------+-------- - */ - if (posGe < i) - { - if (posLe == i) - addPoint(*ptLe); - break; - } - else if (posGe == i) //test if point would be mapped to pixel x-position i - { - if (posLe == i) // - addPoint(x - ptLe->x < ptGe->x - x ? *ptLe : *ptGe); - else - addPoint(*ptGe); - } - else - { - if (posLe <= i) - addPoint(*ptLe); - - if (posLe != i || posGe > i + 1) - { - addPoint(*ptGe); - i = posGe; //skip sparse area: +1 will be added by for-loop! - } - } - } - return points; -} - - -Graph2D::Graph2D(wxWindow* parent, - wxWindowID winid, - const wxPoint& pos, - const wxSize& size, - long style, - const wxString& name) : wxPanel(parent, winid, pos, size, style, name) -{ - Connect(wxEVT_PAINT, wxPaintEventHandler(Graph2D::onPaintEvent), nullptr, this); - Connect(wxEVT_SIZE, wxSizeEventHandler (Graph2D::onSizeEvent ), nullptr, this); - //http://wiki.wxwidgets.org/Flicker-Free_Drawing - Connect(wxEVT_ERASE_BACKGROUND, wxEraseEventHandler(Graph2D::onEraseBackGround), nullptr, this); - - //SetDoubleBuffered(true); slow as hell! - - SetBackgroundStyle(wxBG_STYLE_PAINT); - - Connect(wxEVT_LEFT_DOWN, wxMouseEventHandler(Graph2D::OnMouseLeftDown), nullptr, this); - Connect(wxEVT_MOTION, wxMouseEventHandler(Graph2D::OnMouseMovement), nullptr, this); - Connect(wxEVT_LEFT_UP, wxMouseEventHandler(Graph2D::OnMouseLeftUp), nullptr, this); - Connect(wxEVT_MOUSE_CAPTURE_LOST, wxMouseCaptureLostEventHandler(Graph2D::OnMouseCaptureLost), nullptr, this); -} - - -void Graph2D::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); - render(dc); -} - - -void Graph2D::OnMouseLeftDown(wxMouseEvent& event) -{ - activeSel = std::make_unique(*this, event.GetPosition()); - - if (!event.ControlDown()) - oldSel.clear(); - Refresh(); -} - - -void Graph2D::OnMouseMovement(wxMouseEvent& event) -{ - if (activeSel.get()) - { - activeSel->refCurrentPos() = event.GetPosition(); //corresponding activeSel->refSelection() is updated in Graph2D::render() - Refresh(); - } -} - - -void Graph2D::OnMouseLeftUp(wxMouseEvent& event) -{ - if (activeSel.get()) - { - if (activeSel->getStartPos() != activeSel->refCurrentPos()) //if it's just a single mouse click: discard selection - { - GraphSelectEvent selEvent(activeSel->refSelection()); //fire off GraphSelectEvent - if (wxEvtHandler* handler = GetEventHandler()) - handler->AddPendingEvent(selEvent); - - oldSel.push_back(activeSel->refSelection()); //commit selection - } - - activeSel.reset(); - Refresh(); - } -} - - -void Graph2D::OnMouseCaptureLost(wxMouseCaptureLostEvent& event) -{ - activeSel.reset(); - Refresh(); -} - - -void Graph2D::setCurve(const std::shared_ptr& data, const CurveAttributes& ca) -{ - curves_.clear(); - addCurve(data, ca); -} - - -void Graph2D::addCurve(const std::shared_ptr& data, const CurveAttributes& ca) -{ - CurveAttributes newAttr = ca; - if (newAttr.autoColor) - newAttr.setColor(getDefaultColor(curves_.size())); - curves_.emplace_back(data, newAttr); - Refresh(); -} - - -void Graph2D::render(wxDC& dc) const -{ - using namespace numeric; - - //set label font right at the start so that it is considered by wxDC::GetTextExtent() below! - dc.SetFont(labelFont); - - const wxRect clientRect = GetClientRect(); //DON'T use wxDC::GetSize()! DC may be larger than visible area! - { - //clear complete client area; set label background color - const wxColor backCol = GetBackgroundColour(); //user-configurable! - //wxPanel::GetClassDefaultAttributes().colBg : - //wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE); - wxDCPenChanger dummy (dc, backCol); - wxDCBrushChanger dummy2(dc, backCol); - dc.DrawRectangle(clientRect); - } - - /* - ----------------------- - | | x-label | - ----------------------- - |y-label | graph area | - |---------------------- - */ - wxRect graphArea = clientRect; - int xLabelPosY = clientRect.y; - int yLabelPosX = clientRect.x; - - switch (attr.labelposX) - { - case LABEL_X_TOP: - graphArea.y += attr.xLabelHeight; - graphArea.height -= attr.xLabelHeight; - break; - case LABEL_X_BOTTOM: - xLabelPosY += clientRect.height - attr.xLabelHeight; - graphArea.height -= attr.xLabelHeight; - break; - case LABEL_X_NONE: - break; - } - switch (attr.labelposY) - { - case LABEL_Y_LEFT: - graphArea.x += attr.yLabelWidth; - graphArea.width -= attr.yLabelWidth; - break; - case LABEL_Y_RIGHT: - yLabelPosX += clientRect.width - attr.yLabelWidth; - graphArea.width -= attr.yLabelWidth; - break; - case LABEL_Y_NONE: - break; - } - - { - //paint graph background (excluding label area) - wxDCPenChanger dummy (dc, getBorderColor()); - wxDCBrushChanger dummy2(dc, attr.backgroundColor); - //accessibility: consider system text and background colors; small drawback: color of graphs is NOT connected to the background! => responsibility of client to use correct colors - - dc.DrawRectangle(graphArea); - graphArea.Deflate(1, 1); //attention more wxWidgets design mistakes: behavior of wxRect::Deflate depends on object being const/non-const!!! - } - - //set label areas respecting graph area border! - const wxRect xLabelArea(graphArea.x, xLabelPosY, graphArea.width, attr.xLabelHeight); - const wxRect yLabelArea(yLabelPosX, graphArea.y, attr.yLabelWidth, graphArea.height); - const wxPoint graphAreaOrigin = graphArea.GetTopLeft(); - - //detect x value range - double minX = attr.minXauto ? std::numeric_limits::infinity() : attr.minX; //automatic: ensure values are initialized by first curve - double maxX = attr.maxXauto ? -std::numeric_limits::infinity() : attr.maxX; // - for (auto it = curves_.begin(); it != curves_.end(); ++it) - if (const CurveData* curve = it->first.get()) - { - const std::pair rangeX = curve->getRangeX(); - assert(rangeX.first <= rangeX.second + 1.0e-9); - //GCC fucks up badly when comparing two *binary identical* doubles and finds "begin > end" with diff of 1e-18 - - if (attr.minXauto) - minX = std::min(minX, rangeX.first); - if (attr.maxXauto) - maxX = std::max(maxX, rangeX.second); - } - - const wxSize minimalBlockSizePx = dc.GetTextExtent(L"00"); - - if (minX <= maxX && maxX - minX < std::numeric_limits::infinity()) //valid x-range - { - int blockCountX = 0; - //enlarge minX, maxX to a multiple of a "useful" block size - if (attr.labelposX != LABEL_X_NONE && attr.labelFmtX.get()) - blockCountX = widenRange(minX, maxX, //in/out - graphArea.width, - minimalBlockSizePx.GetWidth() * 7, - *attr.labelFmtX); - - //get raw values + detect y value range - double minY = attr.minYauto ? std::numeric_limits::infinity() : attr.minY; //automatic: ensure values are initialized by first curve - double maxY = attr.maxYauto ? -std::numeric_limits::infinity() : attr.maxY; // - - std::vector> curvePoints(curves_.size()); - std::vector> oobMarker (curves_.size()); //effectively a std::vector marking points that start an out-of-bounds line - - for (size_t index = 0; index < curves_.size(); ++index) - if (const CurveData* curve = curves_[index].first.get()) - { - std::vector& points = curvePoints[index]; - auto& marker = oobMarker [index]; - - points = curve->getPoints(minX, maxX, graphArea.width); - marker.resize(points.size()); //default value: false - if (!points.empty()) - { - //cut points outside visible x-range now in order to calculate height of visible line fragments only! - const bool doPolygonCut = curves_[index].second.fillMode == CurveAttributes::FILL_POLYGON; //impacts auto minY/maxY!! - cutPointsOutsideX(points, marker, minX, maxX, doPolygonCut); - - if (attr.minYauto || attr.maxYauto) - { - auto itPair = std::minmax_element(points.begin(), points.end(), [](const CurvePoint& lhs, const CurvePoint& rhs) { return lhs.y < rhs.y; }); - if (attr.minYauto) - minY = std::min(minY, itPair.first->y); - if (attr.maxYauto) - maxY = std::max(maxY, itPair.second->y); - } - } - } - - if (minY <= maxY) //valid y-range - { - int blockCountY = 0; - //enlarge minY, maxY to a multiple of a "useful" block size - if (attr.labelposY != LABEL_Y_NONE && attr.labelFmtY.get()) - blockCountY = widenRange(minY, maxY, //in/out - graphArea.height, - minimalBlockSizePx.GetHeight() * 3, - *attr.labelFmtY); - - if (graphArea.width <= 1 || graphArea.height <= 1) return; - const ConvertCoord cvrtX(minX, maxX, graphArea.width - 1); //map [minX, maxX] to [0, pixelWidth - 1] - const ConvertCoord cvrtY(maxY, minY, graphArea.height - 1); //map [minY, maxY] to [pixelHeight - 1, 0] - - //calculate curve coordinates on graph area - std::vector> drawPoints(curves_.size()); - - for (size_t index = 0; index < curves_.size(); ++index) - { - auto& cp = curvePoints[index]; - - //add two artificial points to fill the curve area towards x-axis => do this before cutPointsOutsideY() to handle curve leaving upper bound - if (curves_[index].second.fillMode == CurveAttributes::FILL_CURVE) - if (!cp.empty()) - { - cp.emplace_back(CurvePoint(cp.back ().x, minY)); //add lower right and left corners - cp.emplace_back(CurvePoint(cp.front().x, minY)); //[!] aliasing parameter not yet supported via emplace_back: VS bug! => make copy - oobMarker[index].back() = true; - oobMarker[index].push_back(true); - oobMarker[index].push_back(true); - } - - //cut points outside visible y-range before calculating pixels: - //1. realToScreenRound() deforms out-of-range values! - //2. pixels that are grossly out of range can be a severe performance problem when drawing on the DC (Windows) - const bool doPolygonCut = curves_[index].second.fillMode != CurveAttributes::FILL_NONE; - cutPointsOutsideY(cp, oobMarker[index], minY, maxY, doPolygonCut); - - auto& dp = drawPoints[index]; - for (const CurvePoint& pt : cp) - dp.push_back(wxPoint(cvrtX.realToScreenRound(pt.x), - cvrtY.realToScreenRound(pt.y)) + graphAreaOrigin); - } - - //update active mouse selection - if (activeSel.get() && graphArea.width > 0 && graphArea.height > 0) - { - auto widen = [](double* low, double* high) - { - if (*low > *high) - std::swap(low, high); - *low -= 0.5; - *high += 0.5; - }; - - const wxPoint screenStart = activeSel->getStartPos() - graphAreaOrigin; //make relative to graphArea - const wxPoint screenCurrent = activeSel->refCurrentPos() - graphAreaOrigin; - - //normalize positions: a mouse selection is symmetric and *not* an half-open range! - double screenFromX = clampCpy(screenStart .x, 0, graphArea.width - 1); - double screenFromY = clampCpy(screenStart .y, 0, graphArea.height - 1); - double screenToX = clampCpy(screenCurrent.x, 0, graphArea.width - 1); - double screenToY = clampCpy(screenCurrent.y, 0, graphArea.height - 1); - widen(&screenFromX, &screenToX); //use full pixel range for selection! - widen(&screenFromY, &screenToY); - - //save current selection as "double" coordinates - activeSel->refSelection().from = CurvePoint(cvrtX.screenToReal(screenFromX), - cvrtY.screenToReal(screenFromY)); - - activeSel->refSelection().to = CurvePoint(cvrtX.screenToReal(screenToX), - cvrtY.screenToReal(screenToY)); - } - - //#################### begin drawing #################### - //1. draw colored area under curves - for (auto it = curves_.begin(); it != curves_.end(); ++it) - if (it->second.fillMode != CurveAttributes::FILL_NONE) - { - const std::vector& points = drawPoints[it - curves_.begin()]; - if (points.size() >= 3) - { - wxDCBrushChanger dummy(dc, it->second.fillColor); - wxDCPenChanger dummy2(dc, it->second.fillColor); - dc.DrawPolygon(static_cast(points.size()), &points[0]); - } - } - - //2. draw all currently set mouse selections (including active selection) - std::vector allSelections = oldSel; - if (activeSel) - allSelections.push_back(activeSel->refSelection()); - { - //alpha channel not supported on wxMSW, so draw selection before curves - wxDCBrushChanger dummy(dc, wxColor(168, 202, 236)); //light blue - wxDCPenChanger dummy2(dc, wxColor( 51, 153, 255)); //dark blue - - auto shrink = [](double* low, double* high) - { - if (*low > *high) - std::swap(low, high); - *low += 0.5; - *high -= 0.5; - if (*low > *high) - *low = *high = (*low + *high) / 2; - }; - - for (const SelectionBlock& sel : allSelections) - { - //harmonize with active mouse selection above - double screenFromX = cvrtX.realToScreen(sel.from.x); - double screenFromY = cvrtY.realToScreen(sel.from.y); - double screenToX = cvrtX.realToScreen(sel.to.x); - double screenToY = cvrtY.realToScreen(sel.to.y); - shrink(&screenFromX, &screenToX); - shrink(&screenFromY, &screenToY); - - clamp(screenFromX, 0.0, graphArea.width - 1.0); - clamp(screenFromY, 0.0, graphArea.height - 1.0); - clamp(screenToX, 0.0, graphArea.width - 1.0); - clamp(screenToY, 0.0, graphArea.height - 1.0); - - const wxPoint pixelFrom = wxPoint(numeric::round(screenFromX), - numeric::round(screenFromY)) + graphAreaOrigin; - const wxPoint pixelTo = wxPoint(numeric::round(screenToX), - numeric::round(screenToY)) + graphAreaOrigin; - switch (attr.mouseSelMode) - { - case SELECT_NONE: - break; - case SELECT_RECTANGLE: - dc.DrawRectangle(wxRect(pixelFrom, pixelTo)); - break; - case SELECT_X_AXIS: - dc.DrawRectangle(wxRect(wxPoint(pixelFrom.x, graphArea.y), wxPoint(pixelTo.x, graphArea.y + graphArea.height - 1))); - break; - case SELECT_Y_AXIS: - dc.DrawRectangle(wxRect(wxPoint(graphArea.x, pixelFrom.y), wxPoint(graphArea.x + graphArea.width - 1, pixelTo.y))); - break; - } - } - } - - //3. draw labels and background grid - drawXLabel(dc, minX, maxX, blockCountX, cvrtX, graphArea, xLabelArea, *attr.labelFmtX); - drawYLabel(dc, minY, maxY, blockCountY, cvrtY, graphArea, yLabelArea, *attr.labelFmtY); - - //4. finally draw curves - { - dc.SetClippingRegion(graphArea); //prevent thick curves from drawing slightly outside - ZEN_ON_SCOPE_EXIT(dc.DestroyClippingRegion()); - - for (auto it = curves_.begin(); it != curves_.end(); ++it) - { - wxDCPenChanger dummy(dc, wxPen(it->second.color, it->second.lineWidth)); - - const size_t index = it - curves_.begin(); - const std::vector& points = drawPoints[index]; - const auto& marker = oobMarker [index]; - assert(points.size() == marker.size()); - - //draw all parts of the curve except for the out-of-bounds fragments - size_t drawIndexFirst = 0; - while (drawIndexFirst < points.size()) - { - size_t drawIndexLast = std::find(marker.begin() + drawIndexFirst, marker.end(), true) - marker.begin(); - if (drawIndexLast < points.size()) ++drawIndexLast; - - const int pointCount = static_cast(drawIndexLast - drawIndexFirst); - if (pointCount > 0) - { - if (pointCount >= 2) //on OS X wxWidgets has a nasty assert on this - dc.DrawLines(pointCount, &points[drawIndexFirst]); - dc.DrawPoint(points[drawIndexLast - 1]); //wxDC::DrawLines() doesn't draw last pixel - } - drawIndexFirst = std::find(marker.begin() + drawIndexLast, marker.end(), false) - marker.begin(); - } - } - } - - //5. draw corner texts - for (const auto& ct : attr.cornerTexts) - drawCornerText(dc, graphArea, ct.second, ct.first); - } - } -} +// ***************************************************************************** +// * 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 "graph.h" +#include +#include +#include +#include +#include +#include "dc.h" + +using namespace zen; + + +//todo: support zoom via mouse wheel? + +const wxEventType zen::wxEVT_GRAPH_SELECTION = wxNewEventType(); + + +double zen::nextNiceNumber(double blockSize) //round to next number which is a convenient to read block size +{ + if (blockSize <= 0) + return 0; + + const double k = std::floor(std::log10(blockSize)); + const double e = std::pow(10, k); + if (numeric::isNull(e)) + return 0; + const double a = blockSize / e; //blockSize = a * 10^k with a in [1, 10) + assert(1 <= a && a < 10); + + //have a look at leading two digits: "nice" numbers start with 1, 2, 2.5 and 5 + const double steps[] = { 1, 2, 2.5, 5, 10 }; + return e * numeric::nearMatch(a, std::begin(steps), std::end(steps)); +} + + +namespace +{ +wxColor getDefaultColor(size_t pos) +{ + switch (pos % 10) + { + case 0: + return { 0, 69, 134 }; //blue + case 1: + return { 255, 66, 14 }; //red + case 2: + return { 255, 211, 32 }; //yellow + case 3: + return { 87, 157, 28 }; //green + case 4: + return { 126, 0, 33 }; //royal + case 5: + return { 131, 202, 255 }; //light blue + case 6: + return { 49, 64, 4 }; //dark green + case 7: + return { 174, 207, 0 }; //light green + case 8: + return { 75, 31, 111 }; //purple + case 9: + return { 255, 149, 14 }; //orange + } + assert(false); + return *wxBLACK; +} + + +class ConvertCoord //convert between screen and input data coordinates +{ +public: + ConvertCoord(double valMin, double valMax, size_t screenSize) : + min_(valMin), + scaleToReal(screenSize == 0 ? 0 : (valMax - valMin) / screenSize), + scaleToScr(numeric::isNull((valMax - valMin)) ? 0 : screenSize / (valMax - valMin)), + outOfBoundsLow (-1 * scaleToReal + valMin), + outOfBoundsHigh((screenSize + 1) * scaleToReal + valMin) { if (outOfBoundsLow > outOfBoundsHigh) std::swap(outOfBoundsLow, outOfBoundsHigh); } + + double screenToReal(double screenPos) const //map [0, screenSize] -> [valMin, valMax] + { + return screenPos * scaleToReal + min_; + } + double realToScreen(double realPos) const //return screen position in pixel (but with double precision!) + { + return (realPos - min_) * scaleToScr; + } + int realToScreenRound(double realPos) const //returns -1 and screenSize + 1 if out of bounds! + { + //catch large double values: if double is larger than what int can represent => undefined behavior! + numeric::clamp(realPos, outOfBoundsLow, outOfBoundsHigh); + return numeric::round(realToScreen(realPos)); + } + +private: + double min_; + double scaleToReal; + double scaleToScr; + + double outOfBoundsLow; + double outOfBoundsHigh; +}; + + +//enlarge value range to display to a multiple of a "useful" block size +//returns block cound +int widenRange(double& valMin, double& valMax, //in/out + int graphAreaSize, //in pixel + int optimalBlockSizePx, // + const LabelFormatter& labelFmt) +{ + if (graphAreaSize <= 0) return 0; + + const double minValRangePerBlock = (valMax - valMin) / graphAreaSize; + const double proposedValRangePerBlock = (valMax - valMin) * optimalBlockSizePx / graphAreaSize; + double valRangePerBlock = labelFmt.getOptimalBlockSize(proposedValRangePerBlock); + assert(numeric::isNull(proposedValRangePerBlock) || valRangePerBlock > minValRangePerBlock); + + if (numeric::isNull(valRangePerBlock)) //valMin == valMax or strange "optimal block size" + return 1; + + //don't allow sub-pixel blocks! => avoid erroneously high GDI render work load! + if (valRangePerBlock < minValRangePerBlock) + valRangePerBlock = std::ceil(minValRangePerBlock / valRangePerBlock) * valRangePerBlock; + + double blockMin = std::floor(valMin / valRangePerBlock); //store as double, not int: truncation possible, e.g. if valRangePerBlock == 1 + double blockMax = std::ceil (valMax / valRangePerBlock); // + int blockCount = numeric::round(blockMax - blockMin); + assert(blockCount >= 0); + + //handle valMin == valMax == integer + if (blockCount <= 0) + { + ++blockMax; + blockCount = 1; + } + + valMin = blockMin * valRangePerBlock; + valMax = blockMax * valRangePerBlock; + return blockCount; +} + + +void drawXLabel(wxDC& dc, double xMin, double xMax, int blockCount, const ConvertCoord& cvrtX, const wxRect& graphArea, const wxRect& labelArea, const LabelFormatter& labelFmt) +{ + assert(graphArea.width == labelArea.width && graphArea.x == labelArea.x); + if (blockCount <= 0) + return; + + wxDCPenChanger dummy(dc, wxColor(192, 192, 192)); //light grey => not accessible! but no big deal... + wxDCTextColourChanger dummy2(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels + + const double valRangePerBlock = (xMax - xMin) / blockCount; + + for (int i = 1; i < blockCount; ++i) + { + //draw grey vertical lines + const double valX = xMin + i * valRangePerBlock; //step over raw data, not graph area pixels, to not lose precision + const int x = graphArea.x + cvrtX.realToScreenRound(valX); + + if (graphArea.height > 0) + dc.DrawLine(wxPoint(x, graphArea.y), wxPoint(x, graphArea.y + graphArea.height)); //wxDC::DrawLine() doesn't draw last pixel + + //draw x axis labels + const wxString label = labelFmt.formatText(valX, valRangePerBlock); + const wxSize labelExtent = dc.GetMultiLineTextExtent(label); + dc.DrawText(label, wxPoint(x - labelExtent.GetWidth() / 2, labelArea.y + (labelArea.height - labelExtent.GetHeight()) / 2)); //center + } +} + + +void drawYLabel(wxDC& dc, double yMin, double yMax, int blockCount, const ConvertCoord& cvrtY, const wxRect& graphArea, const wxRect& labelArea, const LabelFormatter& labelFmt) +{ + assert(graphArea.height == labelArea.height && graphArea.y == labelArea.y); + if (blockCount <= 0) + return; + + wxDCPenChanger dummy(dc, wxColor(192, 192, 192)); //light grey => not accessible! but no big deal... + wxDCTextColourChanger dummy2(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels + + const double valRangePerBlock = (yMax - yMin) / blockCount; + + for (int i = 1; i < blockCount; ++i) + { + //draw grey horizontal lines + const double valY = yMin + i * valRangePerBlock; //step over raw data, not graph area pixels, to not lose precision + const int y = graphArea.y + cvrtY.realToScreenRound(valY); + + if (graphArea.width > 0) + dc.DrawLine(wxPoint(graphArea.x, y), wxPoint(graphArea.x + graphArea.width, y)); //wxDC::DrawLine() doesn't draw last pixel + + //draw y axis labels + const wxString label = labelFmt.formatText(valY, valRangePerBlock); + const wxSize labelExtent = dc.GetMultiLineTextExtent(label); + dc.DrawText(label, wxPoint(labelArea.x + (labelArea.width - labelExtent.GetWidth()) / 2, y - labelExtent.GetHeight() / 2)); //center + } +} + + +void drawCornerText(wxDC& dc, const wxRect& graphArea, const wxString& txt, Graph2D::PosCorner pos) +{ + if (txt.empty()) return; + const int borderX = 5; + const int borderY = 2; //it looks like wxDC::GetMultiLineTextExtent() precisely returns width, but too large a height: maybe they consider "text row height"? + + wxDCTextColourChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels + wxSize txtExtent = dc.GetMultiLineTextExtent(txt); + txtExtent.x += 2 * borderX; + txtExtent.y += 2 * borderY; + + wxPoint drawPos = graphArea.GetTopLeft(); + switch (pos) + { + case Graph2D::CORNER_TOP_LEFT: + break; + case Graph2D::CORNER_TOP_RIGHT: + drawPos.x += graphArea.width - txtExtent.GetWidth(); + break; + case Graph2D::CORNER_BOTTOM_LEFT: + drawPos.y += graphArea.height - txtExtent.GetHeight(); + break; + case Graph2D::CORNER_BOTTOM_RIGHT: + drawPos.x += graphArea.width - txtExtent.GetWidth(); + drawPos.y += graphArea.height - txtExtent.GetHeight(); + break; + } + dc.DrawText(txt, drawPos + wxPoint(borderX, borderY)); +} + + +//calculate intersection of polygon with half-plane +template +void cutPoints(std::vector& curvePoints, std::vector& oobMarker, Function isInside, Function2 getIntersection, bool doPolygonCut) +{ + assert(curvePoints.size() == oobMarker.size()); + + if (curvePoints.size() != oobMarker.size() || curvePoints.empty()) return; + + auto isMarkedOob = [&](size_t index) { return oobMarker[index] != 0; }; //test if point is start of an OOB line + + std::vector curvePointsTmp; + std::vector oobMarkerTmp; + curvePointsTmp.reserve(curvePoints.size()); //allocating memory for these containers is one + oobMarkerTmp .reserve(oobMarker .size()); //of the more expensive operations of Graph2D! + + auto savePoint = [&](const CurvePoint& pt, bool markedOob) { curvePointsTmp.push_back(pt); oobMarkerTmp.push_back(markedOob); }; + + bool pointInside = isInside(curvePoints[0]); + if (pointInside) + savePoint(curvePoints[0], isMarkedOob(0)); + + for (size_t index = 1; index < curvePoints.size(); ++index) + { + if (isInside(curvePoints[index]) != pointInside) + { + pointInside = !pointInside; + const CurvePoint is = getIntersection(curvePoints[index - 1], curvePoints[index]); //getIntersection returns "to" when delta is zero + savePoint(is, !pointInside || isMarkedOob(index - 1)); + } + if (pointInside) + savePoint(curvePoints[index], isMarkedOob(index)); + } + + //make sure the output polygon area is correctly shaped if either begin or end points are cut + if (doPolygonCut) //note: impacts min/max height-calculations! + if (curvePoints.size() >= 3) + if (isInside(curvePoints.front()) != pointInside) + { + assert(!oobMarkerTmp.empty()); + oobMarkerTmp.back() = true; + + const CurvePoint is = getIntersection(curvePoints.back(), curvePoints.front()); + savePoint(is, true); + } + + curvePointsTmp.swap(curvePoints); + oobMarkerTmp .swap(oobMarker); +} + + +struct GetIntersectionX +{ + GetIntersectionX(double x) : x_(x) {} + CurvePoint operator()(const CurvePoint& from, const CurvePoint& to) const + { + const double deltaX = to.x - from.x; + const double deltaY = to.y - from.y; + return numeric::isNull(deltaX) ? to : CurvePoint(x_, from.y + (x_ - from.x) / deltaX * deltaY); + } + +private: + const double x_; +}; + +struct GetIntersectionY +{ + GetIntersectionY(double y) : y_(y) {} + CurvePoint operator()(const CurvePoint& from, const CurvePoint& to) const + { + const double deltaX = to.x - from.x; + const double deltaY = to.y - from.y; + return numeric::isNull(deltaY) ? to : CurvePoint(from.x + (y_ - from.y) / deltaY * deltaX, y_); + } + +private: + const double y_; +}; + +void cutPointsOutsideX(std::vector& curvePoints, std::vector& oobMarker, double minX, double maxX, bool doPolygonCut) +{ + cutPoints(curvePoints, oobMarker, [&](const CurvePoint& pt) { return pt.x >= minX; }, GetIntersectionX(minX), doPolygonCut); + cutPoints(curvePoints, oobMarker, [&](const CurvePoint& pt) { return pt.x <= maxX; }, GetIntersectionX(maxX), doPolygonCut); +} + +void cutPointsOutsideY(std::vector& curvePoints, std::vector& oobMarker, double minY, double maxY, bool doPolygonCut) +{ + cutPoints(curvePoints, oobMarker, [&](const CurvePoint& pt) { return pt.y >= minY; }, GetIntersectionY(minY), doPolygonCut); + cutPoints(curvePoints, oobMarker, [&](const CurvePoint& pt) { return pt.y <= maxY; }, GetIntersectionY(maxY), doPolygonCut); +} +} + + +std::vector ContinuousCurveData::getPoints(double minX, double maxX, int pixelWidth) const +{ + std::vector points; + + if (pixelWidth <= 1) return points; + const ConvertCoord cvrtX(minX, maxX, pixelWidth - 1); //map [minX, maxX] to [0, pixelWidth - 1] + + const std::pair rangeX = getRangeX(); + + const double screenLow = cvrtX.realToScreen(std::max(rangeX.first, minX)); //=> xLow >= 0 + const double screenHigh = cvrtX.realToScreen(std::min(rangeX.second, maxX)); //=> xHigh <= pixelWidth - 1 + //if double is larger than what int can represent => undefined behavior! + //=> convert to int *after* checking value range! + if (screenLow <= screenHigh) + { + const int posFrom = std::ceil (screenLow ); //do not step outside [minX, maxX] in loop below! + const int posTo = std::floor(screenHigh); // + //conversion from std::floor/std::ceil double return value to int is loss-free for full value range of 32-bit int! tested successfully on MSVC + + for (int i = posFrom; i <= posTo; ++i) + { + const double x = cvrtX.screenToReal(i); + points.emplace_back(x, getValue(x)); + } + } + return points; +} + + +std::vector SparseCurveData::getPoints(double minX, double maxX, int pixelWidth) const +{ + std::vector points; + if (pixelWidth <= 1) return points; + const ConvertCoord cvrtX(minX, maxX, pixelWidth - 1); //map [minX, maxX] to [0, pixelWidth - 1] + const std::pair rangeX = getRangeX(); + + auto addPoint = [&](const CurvePoint& pt) + { + if (!points.empty()) + { + if (pt.x <= points.back().x) //allow ascending x-positions only! algorithm below may cause double-insertion after empty x-ranges! + return; + + if (addSteps_) + if (pt.y != points.back().y) + points.emplace_back(CurvePoint(pt.x, points.back().y)); //[!] aliasing parameter not yet supported via emplace_back: VS bug! => make copy + } + points.push_back(pt); + }; + + const int posFrom = cvrtX.realToScreenRound(std::max(rangeX.first, minX)); + const int posTo = cvrtX.realToScreenRound(std::min(rangeX.second, maxX)); + + for (int i = posFrom; i <= posTo; ++i) + { + const double x = cvrtX.screenToReal(i); + Opt ptLe = getLessEq(x); + Opt ptGe = getGreaterEq(x); + //both non-existent and invalid return values are mapped to out of expected range: => check on posLe/posGe NOT ptLe/ptGe in the following! + const int posLe = ptLe ? cvrtX.realToScreenRound(ptLe->x) : i + 1; + const int posGe = ptGe ? cvrtX.realToScreenRound(ptGe->x) : i - 1; + assert(!ptLe || posLe <= i); //check for invalid return values + assert(!ptGe || posGe >= i); // + /* + Breakdown of all combinations of posLe, posGe and expected action (n >= 1) + Note: For every empty x-range of at least one pixel, both next and previous points must be saved to keep the interpolating line stable!!! + + posLe | posGe | action + +-------+-------+-------- + | none | none | break + | i | none | save ptLe; break + | i - n | none | break; + +-------+-------+-------- + | none | i | save ptGe; continue + | i | i | save one of ptLe, ptGe; continue + | i - n | i | save ptGe; continue + +-------+-------+-------- + | none | i + n | save ptGe; jump to position posGe + 1 + | i | i + n | save ptLe; if n == 1: continue; else: save ptGe; jump to position posGe + 1 + | i - n | i + n | save ptLe, ptGe; jump to position posGe + 1 + +-------+-------+-------- + */ + if (posGe < i) + { + if (posLe == i) + addPoint(*ptLe); + break; + } + else if (posGe == i) //test if point would be mapped to pixel x-position i + { + if (posLe == i) // + addPoint(x - ptLe->x < ptGe->x - x ? *ptLe : *ptGe); + else + addPoint(*ptGe); + } + else + { + if (posLe <= i) + addPoint(*ptLe); + + if (posLe != i || posGe > i + 1) + { + addPoint(*ptGe); + i = posGe; //skip sparse area: +1 will be added by for-loop! + } + } + } + return points; +} + + +Graph2D::Graph2D(wxWindow* parent, + wxWindowID winid, + const wxPoint& pos, + const wxSize& size, + long style, + const wxString& name) : wxPanel(parent, winid, pos, size, style, name) +{ + Connect(wxEVT_PAINT, wxPaintEventHandler(Graph2D::onPaintEvent), nullptr, this); + Connect(wxEVT_SIZE, wxSizeEventHandler (Graph2D::onSizeEvent ), nullptr, this); + //http://wiki.wxwidgets.org/Flicker-Free_Drawing + Connect(wxEVT_ERASE_BACKGROUND, wxEraseEventHandler(Graph2D::onEraseBackGround), nullptr, this); + + //SetDoubleBuffered(true); slow as hell! + + SetBackgroundStyle(wxBG_STYLE_PAINT); + + Connect(wxEVT_LEFT_DOWN, wxMouseEventHandler(Graph2D::OnMouseLeftDown), nullptr, this); + Connect(wxEVT_MOTION, wxMouseEventHandler(Graph2D::OnMouseMovement), nullptr, this); + Connect(wxEVT_LEFT_UP, wxMouseEventHandler(Graph2D::OnMouseLeftUp), nullptr, this); + Connect(wxEVT_MOUSE_CAPTURE_LOST, wxMouseCaptureLostEventHandler(Graph2D::OnMouseCaptureLost), nullptr, this); +} + + +void Graph2D::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); + render(dc); +} + + +void Graph2D::OnMouseLeftDown(wxMouseEvent& event) +{ + activeSel = std::make_unique(*this, event.GetPosition()); + + if (!event.ControlDown()) + oldSel.clear(); + Refresh(); +} + + +void Graph2D::OnMouseMovement(wxMouseEvent& event) +{ + if (activeSel.get()) + { + activeSel->refCurrentPos() = event.GetPosition(); //corresponding activeSel->refSelection() is updated in Graph2D::render() + Refresh(); + } +} + + +void Graph2D::OnMouseLeftUp(wxMouseEvent& event) +{ + if (activeSel.get()) + { + if (activeSel->getStartPos() != activeSel->refCurrentPos()) //if it's just a single mouse click: discard selection + { + GraphSelectEvent selEvent(activeSel->refSelection()); //fire off GraphSelectEvent + if (wxEvtHandler* handler = GetEventHandler()) + handler->AddPendingEvent(selEvent); + + oldSel.push_back(activeSel->refSelection()); //commit selection + } + + activeSel.reset(); + Refresh(); + } +} + + +void Graph2D::OnMouseCaptureLost(wxMouseCaptureLostEvent& event) +{ + activeSel.reset(); + Refresh(); +} + + +void Graph2D::setCurve(const std::shared_ptr& data, const CurveAttributes& ca) +{ + curves_.clear(); + addCurve(data, ca); +} + + +void Graph2D::addCurve(const std::shared_ptr& data, const CurveAttributes& ca) +{ + CurveAttributes newAttr = ca; + if (newAttr.autoColor) + newAttr.setColor(getDefaultColor(curves_.size())); + curves_.emplace_back(data, newAttr); + Refresh(); +} + + +void Graph2D::render(wxDC& dc) const +{ + using namespace numeric; + + //set label font right at the start so that it is considered by wxDC::GetTextExtent() below! + dc.SetFont(labelFont); + + const wxRect clientRect = GetClientRect(); //DON'T use wxDC::GetSize()! DC may be larger than visible area! + { + //clear complete client area; set label background color + const wxColor backCol = GetBackgroundColour(); //user-configurable! + //wxPanel::GetClassDefaultAttributes().colBg : + //wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE); + wxDCPenChanger dummy (dc, backCol); + wxDCBrushChanger dummy2(dc, backCol); + dc.DrawRectangle(clientRect); + } + + /* + ----------------------- + | | x-label | + ----------------------- + |y-label | graph area | + |---------------------- + */ + wxRect graphArea = clientRect; + int xLabelPosY = clientRect.y; + int yLabelPosX = clientRect.x; + + switch (attr.labelposX) + { + case LABEL_X_TOP: + graphArea.y += attr.xLabelHeight; + graphArea.height -= attr.xLabelHeight; + break; + case LABEL_X_BOTTOM: + xLabelPosY += clientRect.height - attr.xLabelHeight; + graphArea.height -= attr.xLabelHeight; + break; + case LABEL_X_NONE: + break; + } + switch (attr.labelposY) + { + case LABEL_Y_LEFT: + graphArea.x += attr.yLabelWidth; + graphArea.width -= attr.yLabelWidth; + break; + case LABEL_Y_RIGHT: + yLabelPosX += clientRect.width - attr.yLabelWidth; + graphArea.width -= attr.yLabelWidth; + break; + case LABEL_Y_NONE: + break; + } + + { + //paint graph background (excluding label area) + wxDCPenChanger dummy (dc, getBorderColor()); + wxDCBrushChanger dummy2(dc, attr.backgroundColor); + //accessibility: consider system text and background colors; small drawback: color of graphs is NOT connected to the background! => responsibility of client to use correct colors + + dc.DrawRectangle(graphArea); + graphArea.Deflate(1, 1); //attention more wxWidgets design mistakes: behavior of wxRect::Deflate depends on object being const/non-const!!! + } + + //set label areas respecting graph area border! + const wxRect xLabelArea(graphArea.x, xLabelPosY, graphArea.width, attr.xLabelHeight); + const wxRect yLabelArea(yLabelPosX, graphArea.y, attr.yLabelWidth, graphArea.height); + const wxPoint graphAreaOrigin = graphArea.GetTopLeft(); + + //detect x value range + double minX = attr.minXauto ? std::numeric_limits::infinity() : attr.minX; //automatic: ensure values are initialized by first curve + double maxX = attr.maxXauto ? -std::numeric_limits::infinity() : attr.maxX; // + for (auto it = curves_.begin(); it != curves_.end(); ++it) + if (const CurveData* curve = it->first.get()) + { + const std::pair rangeX = curve->getRangeX(); + assert(rangeX.first <= rangeX.second + 1.0e-9); + //GCC fucks up badly when comparing two *binary identical* doubles and finds "begin > end" with diff of 1e-18 + + if (attr.minXauto) + minX = std::min(minX, rangeX.first); + if (attr.maxXauto) + maxX = std::max(maxX, rangeX.second); + } + + const wxSize minimalBlockSizePx = dc.GetTextExtent(L"00"); + + if (minX <= maxX && maxX - minX < std::numeric_limits::infinity()) //valid x-range + { + int blockCountX = 0; + //enlarge minX, maxX to a multiple of a "useful" block size + if (attr.labelposX != LABEL_X_NONE && attr.labelFmtX.get()) + blockCountX = widenRange(minX, maxX, //in/out + graphArea.width, + minimalBlockSizePx.GetWidth() * 7, + *attr.labelFmtX); + + //get raw values + detect y value range + double minY = attr.minYauto ? std::numeric_limits::infinity() : attr.minY; //automatic: ensure values are initialized by first curve + double maxY = attr.maxYauto ? -std::numeric_limits::infinity() : attr.maxY; // + + std::vector> curvePoints(curves_.size()); + std::vector> oobMarker (curves_.size()); //effectively a std::vector marking points that start an out-of-bounds line + + for (size_t index = 0; index < curves_.size(); ++index) + if (const CurveData* curve = curves_[index].first.get()) + { + std::vector& points = curvePoints[index]; + auto& marker = oobMarker [index]; + + points = curve->getPoints(minX, maxX, graphArea.width); + marker.resize(points.size()); //default value: false + if (!points.empty()) + { + //cut points outside visible x-range now in order to calculate height of visible line fragments only! + const bool doPolygonCut = curves_[index].second.fillMode == CurveAttributes::FILL_POLYGON; //impacts auto minY/maxY!! + cutPointsOutsideX(points, marker, minX, maxX, doPolygonCut); + + if (attr.minYauto || attr.maxYauto) + { + auto itPair = std::minmax_element(points.begin(), points.end(), [](const CurvePoint& lhs, const CurvePoint& rhs) { return lhs.y < rhs.y; }); + if (attr.minYauto) + minY = std::min(minY, itPair.first->y); + if (attr.maxYauto) + maxY = std::max(maxY, itPair.second->y); + } + } + } + + if (minY <= maxY) //valid y-range + { + int blockCountY = 0; + //enlarge minY, maxY to a multiple of a "useful" block size + if (attr.labelposY != LABEL_Y_NONE && attr.labelFmtY.get()) + blockCountY = widenRange(minY, maxY, //in/out + graphArea.height, + minimalBlockSizePx.GetHeight() * 3, + *attr.labelFmtY); + + if (graphArea.width <= 1 || graphArea.height <= 1) return; + const ConvertCoord cvrtX(minX, maxX, graphArea.width - 1); //map [minX, maxX] to [0, pixelWidth - 1] + const ConvertCoord cvrtY(maxY, minY, graphArea.height - 1); //map [minY, maxY] to [pixelHeight - 1, 0] + + //calculate curve coordinates on graph area + std::vector> drawPoints(curves_.size()); + + for (size_t index = 0; index < curves_.size(); ++index) + { + auto& cp = curvePoints[index]; + + //add two artificial points to fill the curve area towards x-axis => do this before cutPointsOutsideY() to handle curve leaving upper bound + if (curves_[index].second.fillMode == CurveAttributes::FILL_CURVE) + if (!cp.empty()) + { + cp.emplace_back(CurvePoint(cp.back ().x, minY)); //add lower right and left corners + cp.emplace_back(CurvePoint(cp.front().x, minY)); //[!] aliasing parameter not yet supported via emplace_back: VS bug! => make copy + oobMarker[index].back() = true; + oobMarker[index].push_back(true); + oobMarker[index].push_back(true); + } + + //cut points outside visible y-range before calculating pixels: + //1. realToScreenRound() deforms out-of-range values! + //2. pixels that are grossly out of range can be a severe performance problem when drawing on the DC (Windows) + const bool doPolygonCut = curves_[index].second.fillMode != CurveAttributes::FILL_NONE; + cutPointsOutsideY(cp, oobMarker[index], minY, maxY, doPolygonCut); + + auto& dp = drawPoints[index]; + for (const CurvePoint& pt : cp) + dp.push_back(wxPoint(cvrtX.realToScreenRound(pt.x), + cvrtY.realToScreenRound(pt.y)) + graphAreaOrigin); + } + + //update active mouse selection + if (activeSel.get() && graphArea.width > 0 && graphArea.height > 0) + { + auto widen = [](double* low, double* high) + { + if (*low > *high) + std::swap(low, high); + *low -= 0.5; + *high += 0.5; + }; + + const wxPoint screenStart = activeSel->getStartPos() - graphAreaOrigin; //make relative to graphArea + const wxPoint screenCurrent = activeSel->refCurrentPos() - graphAreaOrigin; + + //normalize positions: a mouse selection is symmetric and *not* an half-open range! + double screenFromX = clampCpy(screenStart .x, 0, graphArea.width - 1); + double screenFromY = clampCpy(screenStart .y, 0, graphArea.height - 1); + double screenToX = clampCpy(screenCurrent.x, 0, graphArea.width - 1); + double screenToY = clampCpy(screenCurrent.y, 0, graphArea.height - 1); + widen(&screenFromX, &screenToX); //use full pixel range for selection! + widen(&screenFromY, &screenToY); + + //save current selection as "double" coordinates + activeSel->refSelection().from = CurvePoint(cvrtX.screenToReal(screenFromX), + cvrtY.screenToReal(screenFromY)); + + activeSel->refSelection().to = CurvePoint(cvrtX.screenToReal(screenToX), + cvrtY.screenToReal(screenToY)); + } + + //#################### begin drawing #################### + //1. draw colored area under curves + for (auto it = curves_.begin(); it != curves_.end(); ++it) + if (it->second.fillMode != CurveAttributes::FILL_NONE) + { + const std::vector& points = drawPoints[it - curves_.begin()]; + if (points.size() >= 3) + { + wxDCBrushChanger dummy(dc, it->second.fillColor); + wxDCPenChanger dummy2(dc, it->second.fillColor); + dc.DrawPolygon(static_cast(points.size()), &points[0]); + } + } + + //2. draw all currently set mouse selections (including active selection) + std::vector allSelections = oldSel; + if (activeSel) + allSelections.push_back(activeSel->refSelection()); + { + //alpha channel not supported on wxMSW, so draw selection before curves + wxDCBrushChanger dummy(dc, wxColor(168, 202, 236)); //light blue + wxDCPenChanger dummy2(dc, wxColor( 51, 153, 255)); //dark blue + + auto shrink = [](double* low, double* high) + { + if (*low > *high) + std::swap(low, high); + *low += 0.5; + *high -= 0.5; + if (*low > *high) + *low = *high = (*low + *high) / 2; + }; + + for (const SelectionBlock& sel : allSelections) + { + //harmonize with active mouse selection above + double screenFromX = cvrtX.realToScreen(sel.from.x); + double screenFromY = cvrtY.realToScreen(sel.from.y); + double screenToX = cvrtX.realToScreen(sel.to.x); + double screenToY = cvrtY.realToScreen(sel.to.y); + shrink(&screenFromX, &screenToX); + shrink(&screenFromY, &screenToY); + + clamp(screenFromX, 0.0, graphArea.width - 1.0); + clamp(screenFromY, 0.0, graphArea.height - 1.0); + clamp(screenToX, 0.0, graphArea.width - 1.0); + clamp(screenToY, 0.0, graphArea.height - 1.0); + + const wxPoint pixelFrom = wxPoint(numeric::round(screenFromX), + numeric::round(screenFromY)) + graphAreaOrigin; + const wxPoint pixelTo = wxPoint(numeric::round(screenToX), + numeric::round(screenToY)) + graphAreaOrigin; + switch (attr.mouseSelMode) + { + case SELECT_NONE: + break; + case SELECT_RECTANGLE: + dc.DrawRectangle(wxRect(pixelFrom, pixelTo)); + break; + case SELECT_X_AXIS: + dc.DrawRectangle(wxRect(wxPoint(pixelFrom.x, graphArea.y), wxPoint(pixelTo.x, graphArea.y + graphArea.height - 1))); + break; + case SELECT_Y_AXIS: + dc.DrawRectangle(wxRect(wxPoint(graphArea.x, pixelFrom.y), wxPoint(graphArea.x + graphArea.width - 1, pixelTo.y))); + break; + } + } + } + + //3. draw labels and background grid + drawXLabel(dc, minX, maxX, blockCountX, cvrtX, graphArea, xLabelArea, *attr.labelFmtX); + drawYLabel(dc, minY, maxY, blockCountY, cvrtY, graphArea, yLabelArea, *attr.labelFmtY); + + //4. finally draw curves + { + dc.SetClippingRegion(graphArea); //prevent thick curves from drawing slightly outside + ZEN_ON_SCOPE_EXIT(dc.DestroyClippingRegion()); + + for (auto it = curves_.begin(); it != curves_.end(); ++it) + { + wxDCPenChanger dummy(dc, wxPen(it->second.color, it->second.lineWidth)); + + const size_t index = it - curves_.begin(); + const std::vector& points = drawPoints[index]; + const auto& marker = oobMarker [index]; + assert(points.size() == marker.size()); + + //draw all parts of the curve except for the out-of-bounds fragments + size_t drawIndexFirst = 0; + while (drawIndexFirst < points.size()) + { + size_t drawIndexLast = std::find(marker.begin() + drawIndexFirst, marker.end(), true) - marker.begin(); + if (drawIndexLast < points.size()) ++drawIndexLast; + + const int pointCount = static_cast(drawIndexLast - drawIndexFirst); + if (pointCount > 0) + { + if (pointCount >= 2) //on OS X wxWidgets has a nasty assert on this + dc.DrawLines(pointCount, &points[drawIndexFirst]); + dc.DrawPoint(points[drawIndexLast - 1]); //wxDC::DrawLines() doesn't draw last pixel + } + drawIndexFirst = std::find(marker.begin() + drawIndexLast, marker.end(), false) - marker.begin(); + } + } + } + + //5. draw corner texts + for (const auto& ct : attr.cornerTexts) + drawCornerText(dc, graphArea, ct.second, ct.first); + } + } +} diff --git a/wx+/graph.h b/wx+/graph.h index 3b2d390d..84927b6d 100755 --- a/wx+/graph.h +++ b/wx+/graph.h @@ -1,354 +1,354 @@ -// ***************************************************************************** -// * 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 * -// ***************************************************************************** - -#ifndef GRAPH_H_234425245936567345799 -#define GRAPH_H_234425245936567345799 - -#include -#include -#include -#include -#include -#include -#include -#include - -//elegant 2D graph as wxPanel specialization - -namespace zen -{ -/* -Example: - //init graph (optional) - m_panelGraph->setAttributes(Graph2D::MainAttributes(). - setLabelX(Graph2D::LABEL_X_BOTTOM, 20, std::make_shared()). - setLabelY(Graph2D::LABEL_Y_RIGHT, 60, std::make_shared())); - //set graph data - std::shared_ptr curveDataBytes = ... - m_panelGraph->setCurve(curveDataBytes, Graph2D::CurveAttributes().setLineWidth(2).setColor(wxColor(0, 192, 0))); -*/ - -struct CurvePoint -{ - CurvePoint() {} - CurvePoint(double xVal, double yVal) : x(xVal), y(yVal) {} - double x = 0; - double y = 0; -}; -inline bool operator==(const CurvePoint& lhs, const CurvePoint& rhs) { return lhs.x == rhs.x && lhs.y == rhs.y; } -inline bool operator!=(const CurvePoint& lhs, const CurvePoint& rhs) { return !(lhs == rhs); } - - -struct CurveData -{ - virtual ~CurveData() {} - - virtual std::pair getRangeX() const = 0; - virtual std::vector getPoints(double minX, double maxX, int pixelWidth) const = 0; //points outside the draw area are automatically trimmed! -}; - -//special curve types: -struct ContinuousCurveData : public CurveData -{ - virtual double getValue(double x) const = 0; - -private: - std::vector getPoints(double minX, double maxX, int pixelWidth) const override; -}; - -struct SparseCurveData : public CurveData -{ - SparseCurveData(bool addSteps = false) : addSteps_(addSteps) {} //addSteps: add points to get a staircase effect or connect points via a direct line - - virtual Opt getLessEq (double x) const = 0; - virtual Opt getGreaterEq(double x) const = 0; - -private: - std::vector getPoints(double minX, double maxX, int pixelWidth) const override; - const bool addSteps_; -}; - - -struct ArrayCurveData : public SparseCurveData -{ - virtual double getValue(size_t pos) const = 0; - virtual size_t getSize () const = 0; - -private: - std::pair getRangeX() const override { const size_t sz = getSize(); return std::make_pair(0.0, sz == 0 ? 0.0 : sz - 1.0); } - - Opt getLessEq(double x) const override - { - const size_t sz = getSize(); - const size_t pos = std::min(std::floor(x), sz - 1); //[!] expect unsigned underflow if empty! - if (pos < sz) - return CurvePoint(pos, getValue(pos)); - return NoValue(); - } - - Opt getGreaterEq(double x) const override - { - const size_t pos = std::max(std::ceil(x), 0); //[!] use std::max with signed type! - if (pos < getSize()) - return CurvePoint(pos, getValue(pos)); - return NoValue(); - } -}; - - -struct VectorCurveData : public ArrayCurveData -{ - std::vector& refData() { return data; } -private: - double getValue(size_t pos) const override { return pos < data.size() ? data[pos] : 0; } - size_t getSize() const override { return data.size(); } - - std::vector data; -}; - -//------------------------------------------------------------------------------------------------------------ - -struct LabelFormatter -{ - virtual ~LabelFormatter() {} - - //determine convenient graph label block size in unit of data: usually some small deviation on "sizeProposed" - virtual double getOptimalBlockSize(double sizeProposed) const = 0; - - //create human-readable text for x or y-axis position - virtual wxString formatText(double value, double optimalBlockSize) const = 0; -}; - - -double nextNiceNumber(double blockSize); //round to next number which is convenient to read, e.g. 2.13 -> 2; 2.7 -> 2.5 - -struct DecimalNumberFormatter : public LabelFormatter -{ - double getOptimalBlockSize(double sizeProposed ) const override { return nextNiceNumber(sizeProposed); } - wxString formatText (double value, double optimalBlockSize) const override { return zen::numberTo(value); } -}; - -//------------------------------------------------------------------------------------------------------------ - -//emit data selection event -//Usage: wnd.Connect(wxEVT_GRAPH_SELECTION, GraphSelectEventHandler(MyDlg::OnGraphSelection), nullptr, this); -// void MyDlg::OnGraphSelection(GraphSelectEvent& event); - -extern const wxEventType wxEVT_GRAPH_SELECTION; - -struct SelectionBlock -{ - CurvePoint from; - CurvePoint to; -}; - -class GraphSelectEvent : public wxCommandEvent -{ -public: - GraphSelectEvent(const SelectionBlock& selBlock) : wxCommandEvent(wxEVT_GRAPH_SELECTION), selBlock_(selBlock) {} - wxEvent* Clone() const override { return new GraphSelectEvent(selBlock_); } - - SelectionBlock getSelection() { return selBlock_; } - -private: - SelectionBlock selBlock_; -}; - -using GraphSelectEventFunction = void (wxEvtHandler::*)(GraphSelectEvent&); - -#define GraphSelectEventHandler(func) \ - (wxObjectEventFunction)(wxEventFunction)wxStaticCastEvent(GraphSelectEventFunction, &func) - -//------------------------------------------------------------------------------------------------------------ - -class Graph2D : public wxPanel -{ -public: - Graph2D(wxWindow* parent, - wxWindowID winid = wxID_ANY, - const wxPoint& pos = wxDefaultPosition, - const wxSize& size = wxDefaultSize, - long style = wxTAB_TRAVERSAL | wxNO_BORDER, - const wxString& name = wxPanelNameStr); - - class CurveAttributes - { - public: - CurveAttributes() {} //required by GCC - CurveAttributes& setColor (const wxColor& col) { color = col; autoColor = false; return *this; } - CurveAttributes& fillCurveArea (const wxColor& col) { fillColor = col; fillMode = FILL_CURVE; return *this; } - CurveAttributes& fillPolygonArea(const wxColor& col) { fillColor = col; fillMode = FILL_POLYGON; return *this; } - CurveAttributes& setLineWidth(size_t width) { lineWidth = static_cast(width); return *this; } - - private: - friend class Graph2D; - - bool autoColor = true; - wxColor color; - - enum FillMode - { - FILL_NONE, - FILL_CURVE, - FILL_POLYGON - }; - - FillMode fillMode = FILL_NONE; - wxColor fillColor; - - int lineWidth = 2; - }; - - void setCurve(const std::shared_ptr& data, const CurveAttributes& ca = CurveAttributes()); - void addCurve(const std::shared_ptr& data, const CurveAttributes& ca = CurveAttributes()); - - static wxColor getBorderColor() { return { 130, 135, 144 }; } //medium grey, the same Win7 uses for other frame borders => not accessible! but no big deal... - - enum PosLabelY - { - LABEL_Y_LEFT, - LABEL_Y_RIGHT, - LABEL_Y_NONE - }; - - enum PosLabelX - { - LABEL_X_TOP, - LABEL_X_BOTTOM, - LABEL_X_NONE - }; - - enum PosCorner - { - CORNER_TOP_LEFT, - CORNER_TOP_RIGHT, - CORNER_BOTTOM_LEFT, - CORNER_BOTTOM_RIGHT, - }; - - enum SelMode - { - SELECT_NONE, - SELECT_RECTANGLE, - SELECT_X_AXIS, - SELECT_Y_AXIS, - }; - - class MainAttributes - { - public: - MainAttributes& setMinX(double newMinX) { minX = newMinX; minXauto = false; return *this; } - MainAttributes& setMaxX(double newMaxX) { maxX = newMaxX; maxXauto = false; return *this; } - - MainAttributes& setMinY(double newMinY) { minY = newMinY; minYauto = false; return *this; } - MainAttributes& setMaxY(double newMaxY) { maxY = newMaxY; maxYauto = false; return *this; } - - MainAttributes& setAutoSize() { minXauto = maxXauto = minYauto = maxYauto = true; return *this; } - - MainAttributes& setLabelX(PosLabelX posX, size_t height = 25, std::shared_ptr newLabelFmt = std::make_shared()) - { - labelposX = posX; - xLabelHeight = static_cast(height); - labelFmtX = newLabelFmt; - return *this; - } - MainAttributes& setLabelY(PosLabelY posY, size_t width = 60, std::shared_ptr newLabelFmt = std::make_shared()) - { - labelposY = posY; - yLabelWidth = static_cast(width); - labelFmtY = newLabelFmt; - return *this; - } - - MainAttributes& setCornerText(const wxString& txt, PosCorner pos) { cornerTexts[pos] = txt; return *this; } - - MainAttributes& setBackgroundColor(const wxColor& col) { backgroundColor = col; return *this; } - - MainAttributes& setSelectionMode(SelMode mode) { mouseSelMode = mode; return *this; } - - private: - friend class Graph2D; - - bool minXauto = true; //autodetect range for X value - bool maxXauto = true; - double minX = 0; //x-range to visualize - double maxX = 0; // - - bool minYauto = true; //autodetect range for Y value - bool maxYauto = true; - double minY = 0; //y-range to visualize - double maxY = 0; // - - PosLabelX labelposX = LABEL_X_BOTTOM; - int xLabelHeight = 25; - std::shared_ptr labelFmtX = std::make_shared(); - - PosLabelY labelposY = LABEL_Y_LEFT; - int yLabelWidth = 60; - std::shared_ptr labelFmtY = std::make_shared(); - - std::map cornerTexts; - - wxColor backgroundColor = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); - SelMode mouseSelMode = SELECT_RECTANGLE; - }; - void setAttributes(const MainAttributes& newAttr) { attr = newAttr; Refresh(); } - MainAttributes getAttributes() const { return attr; } - - std::vector getSelections() const { return oldSel; } - void setSelections(const std::vector& sel) - { - oldSel = sel; - activeSel.reset(); - Refresh(); - } - void clearSelection() { oldSel.clear(); Refresh(); } - -private: - void OnMouseLeftDown(wxMouseEvent& event); - void OnMouseMovement(wxMouseEvent& event); - void OnMouseLeftUp (wxMouseEvent& event); - void OnMouseCaptureLost(wxMouseCaptureLostEvent& event); - - void onPaintEvent(wxPaintEvent& event); - void onSizeEvent(wxSizeEvent& event) { Refresh(); event.Skip(); } - void onEraseBackGround(wxEraseEvent& event) {} - - void render(wxDC& dc) const; - - class MouseSelection - { - public: - MouseSelection(wxWindow& wnd, const wxPoint& posDragStart) : wnd_(wnd), posDragStart_(posDragStart), posDragCurrent(posDragStart) { wnd_.CaptureMouse(); } - ~MouseSelection() { if (wnd_.HasCapture()) wnd_.ReleaseMouse(); } - - wxPoint getStartPos() const { return posDragStart_; } - wxPoint& refCurrentPos() { return posDragCurrent; } - - SelectionBlock& refSelection() { return selBlock; } //updated in Graph2d::render(): this is fine, since only what's shown is selected! - - private: - wxWindow& wnd_; - const wxPoint posDragStart_; - wxPoint posDragCurrent; - SelectionBlock selBlock; - }; - std::vector oldSel; //applied selections - std::shared_ptr activeSel; //set during mouse selection - - MainAttributes attr; //global attributes - - Opt doubleBuffer; - - using CurveList = std::vector, CurveAttributes>>; - CurveList curves_; - - //perf!!! generating the font is *very* expensive! don't do this repeatedly in Graph2D::render()! - const wxFont labelFont { wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL, false, L"Arial" }; -}; -} - -#endif //GRAPH_H_234425245936567345799 +// ***************************************************************************** +// * 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 * +// ***************************************************************************** + +#ifndef GRAPH_H_234425245936567345799 +#define GRAPH_H_234425245936567345799 + +#include +#include +#include +#include +#include +#include +#include +#include + +//elegant 2D graph as wxPanel specialization + +namespace zen +{ +/* +Example: + //init graph (optional) + m_panelGraph->setAttributes(Graph2D::MainAttributes(). + setLabelX(Graph2D::LABEL_X_BOTTOM, 20, std::make_shared()). + setLabelY(Graph2D::LABEL_Y_RIGHT, 60, std::make_shared())); + //set graph data + std::shared_ptr curveDataBytes = ... + m_panelGraph->setCurve(curveDataBytes, Graph2D::CurveAttributes().setLineWidth(2).setColor(wxColor(0, 192, 0))); +*/ + +struct CurvePoint +{ + CurvePoint() {} + CurvePoint(double xVal, double yVal) : x(xVal), y(yVal) {} + double x = 0; + double y = 0; +}; +inline bool operator==(const CurvePoint& lhs, const CurvePoint& rhs) { return lhs.x == rhs.x && lhs.y == rhs.y; } +inline bool operator!=(const CurvePoint& lhs, const CurvePoint& rhs) { return !(lhs == rhs); } + + +struct CurveData +{ + virtual ~CurveData() {} + + virtual std::pair getRangeX() const = 0; + virtual std::vector getPoints(double minX, double maxX, int pixelWidth) const = 0; //points outside the draw area are automatically trimmed! +}; + +//special curve types: +struct ContinuousCurveData : public CurveData +{ + virtual double getValue(double x) const = 0; + +private: + std::vector getPoints(double minX, double maxX, int pixelWidth) const override; +}; + +struct SparseCurveData : public CurveData +{ + SparseCurveData(bool addSteps = false) : addSteps_(addSteps) {} //addSteps: add points to get a staircase effect or connect points via a direct line + + virtual Opt getLessEq (double x) const = 0; + virtual Opt getGreaterEq(double x) const = 0; + +private: + std::vector getPoints(double minX, double maxX, int pixelWidth) const override; + const bool addSteps_; +}; + + +struct ArrayCurveData : public SparseCurveData +{ + virtual double getValue(size_t pos) const = 0; + virtual size_t getSize () const = 0; + +private: + std::pair getRangeX() const override { const size_t sz = getSize(); return std::make_pair(0.0, sz == 0 ? 0.0 : sz - 1.0); } + + Opt getLessEq(double x) const override + { + const size_t sz = getSize(); + const size_t pos = std::min(std::floor(x), sz - 1); //[!] expect unsigned underflow if empty! + if (pos < sz) + return CurvePoint(pos, getValue(pos)); + return NoValue(); + } + + Opt getGreaterEq(double x) const override + { + const size_t pos = std::max(std::ceil(x), 0); //[!] use std::max with signed type! + if (pos < getSize()) + return CurvePoint(pos, getValue(pos)); + return NoValue(); + } +}; + + +struct VectorCurveData : public ArrayCurveData +{ + std::vector& refData() { return data; } +private: + double getValue(size_t pos) const override { return pos < data.size() ? data[pos] : 0; } + size_t getSize() const override { return data.size(); } + + std::vector data; +}; + +//------------------------------------------------------------------------------------------------------------ + +struct LabelFormatter +{ + virtual ~LabelFormatter() {} + + //determine convenient graph label block size in unit of data: usually some small deviation on "sizeProposed" + virtual double getOptimalBlockSize(double sizeProposed) const = 0; + + //create human-readable text for x or y-axis position + virtual wxString formatText(double value, double optimalBlockSize) const = 0; +}; + + +double nextNiceNumber(double blockSize); //round to next number which is convenient to read, e.g. 2.13 -> 2; 2.7 -> 2.5 + +struct DecimalNumberFormatter : public LabelFormatter +{ + double getOptimalBlockSize(double sizeProposed ) const override { return nextNiceNumber(sizeProposed); } + wxString formatText (double value, double optimalBlockSize) const override { return zen::numberTo(value); } +}; + +//------------------------------------------------------------------------------------------------------------ + +//emit data selection event +//Usage: wnd.Connect(wxEVT_GRAPH_SELECTION, GraphSelectEventHandler(MyDlg::OnGraphSelection), nullptr, this); +// void MyDlg::OnGraphSelection(GraphSelectEvent& event); + +extern const wxEventType wxEVT_GRAPH_SELECTION; + +struct SelectionBlock +{ + CurvePoint from; + CurvePoint to; +}; + +class GraphSelectEvent : public wxCommandEvent +{ +public: + GraphSelectEvent(const SelectionBlock& selBlock) : wxCommandEvent(wxEVT_GRAPH_SELECTION), selBlock_(selBlock) {} + wxEvent* Clone() const override { return new GraphSelectEvent(selBlock_); } + + SelectionBlock getSelection() { return selBlock_; } + +private: + SelectionBlock selBlock_; +}; + +using GraphSelectEventFunction = void (wxEvtHandler::*)(GraphSelectEvent&); + +#define GraphSelectEventHandler(func) \ + (wxObjectEventFunction)(wxEventFunction)wxStaticCastEvent(GraphSelectEventFunction, &func) + +//------------------------------------------------------------------------------------------------------------ + +class Graph2D : public wxPanel +{ +public: + Graph2D(wxWindow* parent, + wxWindowID winid = wxID_ANY, + const wxPoint& pos = wxDefaultPosition, + const wxSize& size = wxDefaultSize, + long style = wxTAB_TRAVERSAL | wxNO_BORDER, + const wxString& name = wxPanelNameStr); + + class CurveAttributes + { + public: + CurveAttributes() {} //required by GCC + CurveAttributes& setColor (const wxColor& col) { color = col; autoColor = false; return *this; } + CurveAttributes& fillCurveArea (const wxColor& col) { fillColor = col; fillMode = FILL_CURVE; return *this; } + CurveAttributes& fillPolygonArea(const wxColor& col) { fillColor = col; fillMode = FILL_POLYGON; return *this; } + CurveAttributes& setLineWidth(size_t width) { lineWidth = static_cast(width); return *this; } + + private: + friend class Graph2D; + + bool autoColor = true; + wxColor color; + + enum FillMode + { + FILL_NONE, + FILL_CURVE, + FILL_POLYGON + }; + + FillMode fillMode = FILL_NONE; + wxColor fillColor; + + int lineWidth = 2; + }; + + void setCurve(const std::shared_ptr& data, const CurveAttributes& ca = CurveAttributes()); + void addCurve(const std::shared_ptr& data, const CurveAttributes& ca = CurveAttributes()); + + static wxColor getBorderColor() { return { 130, 135, 144 }; } //medium grey, the same Win7 uses for other frame borders => not accessible! but no big deal... + + enum PosLabelY + { + LABEL_Y_LEFT, + LABEL_Y_RIGHT, + LABEL_Y_NONE + }; + + enum PosLabelX + { + LABEL_X_TOP, + LABEL_X_BOTTOM, + LABEL_X_NONE + }; + + enum PosCorner + { + CORNER_TOP_LEFT, + CORNER_TOP_RIGHT, + CORNER_BOTTOM_LEFT, + CORNER_BOTTOM_RIGHT, + }; + + enum SelMode + { + SELECT_NONE, + SELECT_RECTANGLE, + SELECT_X_AXIS, + SELECT_Y_AXIS, + }; + + class MainAttributes + { + public: + MainAttributes& setMinX(double newMinX) { minX = newMinX; minXauto = false; return *this; } + MainAttributes& setMaxX(double newMaxX) { maxX = newMaxX; maxXauto = false; return *this; } + + MainAttributes& setMinY(double newMinY) { minY = newMinY; minYauto = false; return *this; } + MainAttributes& setMaxY(double newMaxY) { maxY = newMaxY; maxYauto = false; return *this; } + + MainAttributes& setAutoSize() { minXauto = maxXauto = minYauto = maxYauto = true; return *this; } + + MainAttributes& setLabelX(PosLabelX posX, size_t height = 25, std::shared_ptr newLabelFmt = std::make_shared()) + { + labelposX = posX; + xLabelHeight = static_cast(height); + labelFmtX = newLabelFmt; + return *this; + } + MainAttributes& setLabelY(PosLabelY posY, size_t width = 60, std::shared_ptr newLabelFmt = std::make_shared()) + { + labelposY = posY; + yLabelWidth = static_cast(width); + labelFmtY = newLabelFmt; + return *this; + } + + MainAttributes& setCornerText(const wxString& txt, PosCorner pos) { cornerTexts[pos] = txt; return *this; } + + MainAttributes& setBackgroundColor(const wxColor& col) { backgroundColor = col; return *this; } + + MainAttributes& setSelectionMode(SelMode mode) { mouseSelMode = mode; return *this; } + + private: + friend class Graph2D; + + bool minXauto = true; //autodetect range for X value + bool maxXauto = true; + double minX = 0; //x-range to visualize + double maxX = 0; // + + bool minYauto = true; //autodetect range for Y value + bool maxYauto = true; + double minY = 0; //y-range to visualize + double maxY = 0; // + + PosLabelX labelposX = LABEL_X_BOTTOM; + int xLabelHeight = 25; + std::shared_ptr labelFmtX = std::make_shared(); + + PosLabelY labelposY = LABEL_Y_LEFT; + int yLabelWidth = 60; + std::shared_ptr labelFmtY = std::make_shared(); + + std::map cornerTexts; + + wxColor backgroundColor = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); + SelMode mouseSelMode = SELECT_RECTANGLE; + }; + void setAttributes(const MainAttributes& newAttr) { attr = newAttr; Refresh(); } + MainAttributes getAttributes() const { return attr; } + + std::vector getSelections() const { return oldSel; } + void setSelections(const std::vector& sel) + { + oldSel = sel; + activeSel.reset(); + Refresh(); + } + void clearSelection() { oldSel.clear(); Refresh(); } + +private: + void OnMouseLeftDown(wxMouseEvent& event); + void OnMouseMovement(wxMouseEvent& event); + void OnMouseLeftUp (wxMouseEvent& event); + void OnMouseCaptureLost(wxMouseCaptureLostEvent& event); + + void onPaintEvent(wxPaintEvent& event); + void onSizeEvent(wxSizeEvent& event) { Refresh(); event.Skip(); } + void onEraseBackGround(wxEraseEvent& event) {} + + void render(wxDC& dc) const; + + class MouseSelection + { + public: + MouseSelection(wxWindow& wnd, const wxPoint& posDragStart) : wnd_(wnd), posDragStart_(posDragStart), posDragCurrent(posDragStart) { wnd_.CaptureMouse(); } + ~MouseSelection() { if (wnd_.HasCapture()) wnd_.ReleaseMouse(); } + + wxPoint getStartPos() const { return posDragStart_; } + wxPoint& refCurrentPos() { return posDragCurrent; } + + SelectionBlock& refSelection() { return selBlock; } //updated in Graph2d::render(): this is fine, since only what's shown is selected! + + private: + wxWindow& wnd_; + const wxPoint posDragStart_; + wxPoint posDragCurrent; + SelectionBlock selBlock; + }; + std::vector oldSel; //applied selections + std::shared_ptr activeSel; //set during mouse selection + + MainAttributes attr; //global attributes + + Opt doubleBuffer; + + using CurveList = std::vector, CurveAttributes>>; + CurveList curves_; + + //perf!!! generating the font is *very* expensive! don't do this repeatedly in Graph2D::render()! + const wxFont labelFont { wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL, false, L"Arial" }; +}; +} + +#endif //GRAPH_H_234425245936567345799 diff --git a/wx+/grid.cpp b/wx+/grid.cpp index d714ecf5..1d67d8ad 100755 --- a/wx+/grid.cpp +++ b/wx+/grid.cpp @@ -1,2220 +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" - - #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: utfTo("\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 = getUnicodeSubstring(text, 0, 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; -} +// ***************************************************************************** +// * 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: utfTo("\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 = getUnicodeSubstring(text, 0, 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; +} diff --git a/wx+/grid.h b/wx+/grid.h index ff1d4b87..68e1fe61 100755 --- a/wx+/grid.h +++ b/wx+/grid.h @@ -1,363 +1,363 @@ -// ***************************************************************************** -// * 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 * -// ***************************************************************************** - -#ifndef GRID_H_834702134831734869987 -#define GRID_H_834702134831734869987 - -#include -#include -#include -#include -#include -#include - -//a user-friendly, extensible and high-performance grid control - -namespace zen -{ -enum class ColumnType { NONE = -1 }; //user-defiend column type -enum class HoverArea { NONE = -1 }; //user-defined area for mouse selections for a given row (may span multiple columns or split a single column into multiple areas) - -//------------------------ events ------------------------------------------------ -extern const wxEventType EVENT_GRID_MOUSE_LEFT_DOUBLE; // -extern const wxEventType EVENT_GRID_MOUSE_LEFT_DOWN; // -extern const wxEventType EVENT_GRID_MOUSE_LEFT_UP; //generates: GridClickEvent -extern const wxEventType EVENT_GRID_MOUSE_RIGHT_DOWN; // -extern const wxEventType EVENT_GRID_MOUSE_RIGHT_UP; // - -extern const wxEventType EVENT_GRID_SELECT_RANGE; //generates: GridRangeSelectEvent -//NOTE: neither first nor second row need to match EVENT_GRID_MOUSE_LEFT_DOWN/EVENT_GRID_MOUSE_LEFT_UP: user holding SHIFT; moving out of window... - -extern const wxEventType EVENT_GRID_COL_LABEL_MOUSE_LEFT; //generates: GridLabelClickEvent -extern const wxEventType EVENT_GRID_COL_LABEL_MOUSE_RIGHT; // -extern const wxEventType EVENT_GRID_COL_RESIZE; //generates: GridColumnResizeEvent - -//example: wnd.Connect(EVENT_GRID_COL_LABEL_LEFT_CLICK, GridClickEventHandler(MyDlg::OnLeftClick), nullptr, this); - -struct GridClickEvent : public wxMouseEvent -{ - GridClickEvent(wxEventType et, const wxMouseEvent& me, ptrdiff_t row, HoverArea hoverArea) : - wxMouseEvent(me), row_(row), hoverArea_(hoverArea) { SetEventType(et); } - wxEvent* Clone() const override { return new GridClickEvent(*this); } - - const ptrdiff_t row_; //-1 for invalid position, >= rowCount if out of range - const HoverArea hoverArea_; //may be HoverArea::NONE -}; - -struct GridRangeSelectEvent : public wxCommandEvent -{ - GridRangeSelectEvent(size_t rowFirst, size_t rowLast, bool positive, const GridClickEvent* mouseInitiated) : - wxCommandEvent(EVENT_GRID_SELECT_RANGE), rowFirst_(rowFirst), rowLast_(rowLast), positive_(positive), - mouseInitiated_(mouseInitiated ? *mouseInitiated : Opt()) { assert(rowFirst <= rowLast); } - wxEvent* Clone() const override { return new GridRangeSelectEvent(*this); } - - const size_t rowFirst_; //selected range: [rowFirst_, rowLast_) - const size_t rowLast_; - const bool positive_; //"false" when clearing selection! - Opt mouseInitiated_; //filled unless selection was performed via keyboard shortcuts or is result of Grid::clearSelection() -}; - -struct GridLabelClickEvent : public wxMouseEvent -{ - GridLabelClickEvent(wxEventType et, const wxMouseEvent& me, ColumnType colType) : wxMouseEvent(me), colType_(colType) { SetEventType(et); } - wxEvent* Clone() const override { return new GridLabelClickEvent(*this); } - - const ColumnType colType_; //may be ColumnType::NONE -}; - - -struct GridColumnResizeEvent : public wxCommandEvent -{ - GridColumnResizeEvent(int offset, ColumnType colType) : wxCommandEvent(EVENT_GRID_COL_RESIZE), colType_(colType), offset_(offset) {} - wxEvent* Clone() const override { return new GridColumnResizeEvent(*this); } - - const ColumnType colType_; - const int offset_; -}; - -using GridClickEventFunction = void (wxEvtHandler::*)(GridClickEvent&); -using GridRangeSelectEventFunction = void (wxEvtHandler::*)(GridRangeSelectEvent&); -using GridLabelClickEventFunction = void (wxEvtHandler::*)(GridLabelClickEvent&); -using GridColumnResizeEventFunction = void (wxEvtHandler::*)(GridColumnResizeEvent&); - -#define GridClickEventHandler(func) (wxObjectEventFunction)(wxEventFunction)wxStaticCastEvent(GridClickEventFunction, &func) -#define GridRangeSelectEventHandler(func) (wxObjectEventFunction)(wxEventFunction)wxStaticCastEvent(GridRangeSelectEventFunction, &func) -#define GridLabelClickEventHandler(func) (wxObjectEventFunction)(wxEventFunction)wxStaticCastEvent(GridLabelClickEventFunction, &func) -#define GridColumnResizeEventHandler(func)(wxObjectEventFunction)(wxEventFunction)wxStaticCastEvent(GridColumnResizeEventFunction, &func) - -//------------------------------------------------------------------------------------------------------------ -class Grid; - -void clearArea(wxDC& dc, const wxRect& rect, const wxColor& col); - -class GridData -{ -public: - virtual ~GridData() {} - - virtual size_t getRowCount() const = 0; - - //cell area: - virtual std::wstring getValue(size_t row, ColumnType colType) const = 0; - virtual void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected); //default implementation - virtual void renderCell (wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover); - virtual int getBestSize (wxDC& dc, size_t row, ColumnType colType); //must correspond to renderCell()! - virtual std::wstring getToolTip (size_t row, ColumnType colType) const { return std::wstring(); } - virtual HoverArea getRowMouseHover(size_t row, ColumnType colType, int cellRelativePosX, int cellWidth) { return HoverArea::NONE; } - - //label area: - virtual std::wstring getColumnLabel(ColumnType colType) const = 0; - virtual void renderColumnLabel(Grid& grid, wxDC& dc, const wxRect& rect, ColumnType colType, bool highlighted); //default implementation - virtual std::wstring getToolTip(ColumnType colType) const { return std::wstring(); } - - static const int COLUMN_GAP_LEFT; //for left-aligned text - - //optional helper routines: - static wxSize drawCellText (wxDC& dc, const wxRect& rect, const std::wstring& text, int alignment = wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); //returns text extent - static wxRect drawCellBorder (wxDC& dc, const wxRect& rect); //returns inner rectangle - static void drawCellBackground(wxDC& dc, const wxRect& rect, bool enabled, bool selected, const wxColor& backgroundColor); - - static wxRect drawColumnLabelBorder (wxDC& dc, const wxRect& rect); //returns inner rectangle - static void drawColumnLabelBackground(wxDC& dc, const wxRect& rect, bool highlighted); - static void drawColumnLabelText (wxDC& dc, const wxRect& rect, const std::wstring& text); -}; - - -enum GridEventPolicy -{ - ALLOW_GRID_EVENT, - DENY_GRID_EVENT -}; - - -class Grid : public wxScrolledWindow -{ -public: - Grid(wxWindow* parent, - wxWindowID id = wxID_ANY, - const wxPoint& pos = wxDefaultPosition, - const wxSize& size = wxDefaultSize, - long style = wxTAB_TRAVERSAL | wxNO_BORDER, - const wxString& name = wxPanelNameStr); - - size_t getRowCount() const; - - void setRowHeight(int height); - - struct ColumnAttribute - { - ColumnAttribute(ColumnType type, int offset, int stretch, bool visible = true) : type_(type), visible_(visible), stretch_(std::max(stretch, 0)), offset_(offset) { assert(stretch >=0 ); } - ColumnType type_; - bool visible_; - //first, client width is partitioned according to all available stretch factors, then "offset_" is added - //universal model: a non-stretched column has stretch factor 0 with the "offset" becoming identical to final width! - int stretch_; //>= 0 - int offset_; - }; - - void setColumnConfig(const std::vector& attr); //set column count + widths - std::vector getColumnConfig() const; - - void setDataProvider(const std::shared_ptr& dataView) { dataView_ = dataView; } - /**/ GridData* getDataProvider() { return dataView_.get(); } - const GridData* getDataProvider() const { return dataView_.get(); } - //----------------------------------------------------------------------------- - - void setColumnLabelHeight(int height); - void showRowLabel(bool visible); - - enum ScrollBarStatus - { - SB_SHOW_AUTOMATIC, - SB_SHOW_ALWAYS, - SB_SHOW_NEVER, - }; - //alternative until wxScrollHelper::ShowScrollbars() becomes available in wxWidgets 2.9 - void showScrollBars(ScrollBarStatus horizontal, ScrollBarStatus vertical); - - std::vector getSelectedRows() const { return selection_.get(); } - void selectAllRows (GridEventPolicy rangeEventPolicy); - void clearSelection(GridEventPolicy rangeEventPolicy); //turn off range selection event when calling this function in an event handler to avoid recursion! - - void scrollDelta(int deltaX, int deltaY); //in scroll units - - wxWindow& getCornerWin (); - wxWindow& getRowLabelWin(); - wxWindow& getColLabelWin(); - wxWindow& getMainWin (); - const wxWindow& getMainWin() const; - - ptrdiff_t getRowAtPos(int posY) const; //return -1 for invalid position, >= rowCount if out of range; absolute coordinates! - - struct ColumnPosInfo - { - ColumnType colType; //ColumnType::NONE no column at x position! - int cellRelativePosX; - int colWidth; - }; - ColumnPosInfo getColumnAtPos(int posX) const; //absolute position! - - void refreshCell(size_t row, ColumnType colType); - - void enableColumnMove (bool value) { allowColumnMove_ = value; } - void enableColumnResize(bool value) { allowColumnResize_ = value; } - - void setGridCursor(size_t row); //set + show + select cursor (+ emit range selection event) - size_t getGridCursor() const; //returns row - - void scrollTo(size_t row); - - void Refresh(bool eraseBackground = true, const wxRect* rect = nullptr) override; - bool Enable(bool enable = true) override; - - //############################################################################################################ - - static wxColor getColorSelectionGradientFrom(); - static wxColor getColorSelectionGradientTo(); - -private: - void onPaintEvent(wxPaintEvent& event); - void onEraseBackGround(wxEraseEvent& event) {} //[!] - void onSizeEvent(wxSizeEvent& event) { updateWindowSizes(); event.Skip(); } - void onKeyDown(wxKeyEvent& event); - - void updateWindowSizes(bool updateScrollbar = true); - - void selectWithCursor(ptrdiff_t row); - void makeRowVisible(size_t row); - - void redirectRowLabelEvent(wxMouseEvent& event); - - wxSize GetSizeAvailableForScrollTarget(const wxSize& size) override; //required since wxWidgets 2.9 if SetTargetWindow() is used - - - int getBestColumnSize(size_t col) const; //return -1 on error - - void autoSizeColumns(GridEventPolicy columnResizeEventPolicy); - - friend class GridData; - class SubWindow; - class CornerWin; - class RowLabelWin; - class ColLabelWin; - class MainWin; - - class Selection - { - public: - void init(size_t rowCount) { rowSelectionValue.resize(rowCount); clear(); } - - size_t maxSize() const { return rowSelectionValue.size(); } - - std::vector get() const - { - std::vector result; - for (size_t row = 0; row < rowSelectionValue.size(); ++row) - if (rowSelectionValue[row] != 0) - result.push_back(row); - return result; - } - - void selectAll() { selectRange(0, rowSelectionValue.size(), true); } - void clear () { selectRange(0, rowSelectionValue.size(), false); } - - bool isSelected(size_t row) const { return row < rowSelectionValue.size() ? rowSelectionValue[row] != 0 : false; } - - void selectRange(size_t rowFirst, size_t rowLast, bool positive = true) //select [rowFirst, rowLast), trims if required! - { - if (rowFirst <= rowLast) - { - numeric::clamp(rowFirst, 0, rowSelectionValue.size()); - numeric::clamp(rowLast, 0, rowSelectionValue.size()); - - std::fill(rowSelectionValue.begin() + rowFirst, rowSelectionValue.begin() + rowLast, positive); - } - else assert(false); - } - - private: - std::vector rowSelectionValue; //effectively a vector of size "number of rows" - }; - - struct VisibleColumn - { - VisibleColumn(ColumnType type, int offset, int stretch) : type_(type), stretch_(stretch), offset_(offset) {} - ColumnType type_; - int stretch_; //>= 0 - int offset_; - }; - - struct ColumnWidth - { - ColumnWidth(ColumnType type, int width) : type_(type), width_(width) {} - ColumnType type_; - int width_; - }; - std::vector getColWidths() const; // - std::vector getColWidths(int mainWinWidth) const; //evaluate stretched columns - int getColWidthsSum(int mainWinWidth) const; - std::vector getColStretchedWidths(int clientWidth) const; //final width = (normalized) (stretchedWidth + offset) - - Opt getColWidth(size_t col) const - { - const auto& widths = getColWidths(); - if (col < widths.size()) - return widths[col].width_; - return NoValue(); - } - - void setColumnWidth(int width, size_t col, GridEventPolicy columnResizeEventPolicy, bool notifyAsync = false); - - wxRect getColumnLabelArea(ColumnType colType) const; //returns empty rect if column not found - - void selectRangeAndNotify(ptrdiff_t rowFrom, ptrdiff_t rowTo, bool positive, const GridClickEvent* mouseInitiated); //select inclusive range [rowFrom, rowTo] + notify event! - - bool isSelected(size_t row) const { return selection_.isSelected(row); } - - struct ColAction - { - bool wantResize = false; //"!wantResize" means "move" or "single click" - size_t col = 0; - }; - Opt clientPosToColumnAction(const wxPoint& pos) const; - void moveColumn(size_t colFrom, size_t colTo); - ptrdiff_t clientPosToMoveTargetColumn(const wxPoint& pos) const; //return < 0 on error - - ColumnType colToType(size_t col) const; //returns ColumnType::NONE on error - - /* - Visual layout: - -------------------------------- - |CornerWin | ColLabelWin | - |------------------------------| - |RowLabelWin | MainWin | - | | | - -------------------------------- - */ - CornerWin* cornerWin_; - RowLabelWin* rowLabelWin_; - ColLabelWin* colLabelWin_; - MainWin* mainWin_; - - ScrollBarStatus showScrollbarX_ = SB_SHOW_AUTOMATIC; - ScrollBarStatus showScrollbarY_ = SB_SHOW_AUTOMATIC; - - int colLabelHeight_ = 0; - bool drawRowLabel_ = true; - - std::shared_ptr dataView_; - Selection selection_; - bool allowColumnMove_ = true; - bool allowColumnResize_ = true; - - std::vector visibleCols_; //individual widths, type and total column count - std::vector oldColAttributes_; //visible + nonvisible columns; use for conversion in setColumnConfig()/getColumnConfig() *only*! - - size_t rowCountOld_ = 0; //at the time of last Grid::Refresh() -}; -} - -#endif //GRID_H_834702134831734869987 +// ***************************************************************************** +// * 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 * +// ***************************************************************************** + +#ifndef GRID_H_834702134831734869987 +#define GRID_H_834702134831734869987 + +#include +#include +#include +#include +#include +#include + +//a user-friendly, extensible and high-performance grid control + +namespace zen +{ +enum class ColumnType { NONE = -1 }; //user-defiend column type +enum class HoverArea { NONE = -1 }; //user-defined area for mouse selections for a given row (may span multiple columns or split a single column into multiple areas) + +//------------------------ events ------------------------------------------------ +extern const wxEventType EVENT_GRID_MOUSE_LEFT_DOUBLE; // +extern const wxEventType EVENT_GRID_MOUSE_LEFT_DOWN; // +extern const wxEventType EVENT_GRID_MOUSE_LEFT_UP; //generates: GridClickEvent +extern const wxEventType EVENT_GRID_MOUSE_RIGHT_DOWN; // +extern const wxEventType EVENT_GRID_MOUSE_RIGHT_UP; // + +extern const wxEventType EVENT_GRID_SELECT_RANGE; //generates: GridRangeSelectEvent +//NOTE: neither first nor second row need to match EVENT_GRID_MOUSE_LEFT_DOWN/EVENT_GRID_MOUSE_LEFT_UP: user holding SHIFT; moving out of window... + +extern const wxEventType EVENT_GRID_COL_LABEL_MOUSE_LEFT; //generates: GridLabelClickEvent +extern const wxEventType EVENT_GRID_COL_LABEL_MOUSE_RIGHT; // +extern const wxEventType EVENT_GRID_COL_RESIZE; //generates: GridColumnResizeEvent + +//example: wnd.Connect(EVENT_GRID_COL_LABEL_LEFT_CLICK, GridClickEventHandler(MyDlg::OnLeftClick), nullptr, this); + +struct GridClickEvent : public wxMouseEvent +{ + GridClickEvent(wxEventType et, const wxMouseEvent& me, ptrdiff_t row, HoverArea hoverArea) : + wxMouseEvent(me), row_(row), hoverArea_(hoverArea) { SetEventType(et); } + wxEvent* Clone() const override { return new GridClickEvent(*this); } + + const ptrdiff_t row_; //-1 for invalid position, >= rowCount if out of range + const HoverArea hoverArea_; //may be HoverArea::NONE +}; + +struct GridRangeSelectEvent : public wxCommandEvent +{ + GridRangeSelectEvent(size_t rowFirst, size_t rowLast, bool positive, const GridClickEvent* mouseInitiated) : + wxCommandEvent(EVENT_GRID_SELECT_RANGE), rowFirst_(rowFirst), rowLast_(rowLast), positive_(positive), + mouseInitiated_(mouseInitiated ? *mouseInitiated : Opt()) { assert(rowFirst <= rowLast); } + wxEvent* Clone() const override { return new GridRangeSelectEvent(*this); } + + const size_t rowFirst_; //selected range: [rowFirst_, rowLast_) + const size_t rowLast_; + const bool positive_; //"false" when clearing selection! + Opt mouseInitiated_; //filled unless selection was performed via keyboard shortcuts or is result of Grid::clearSelection() +}; + +struct GridLabelClickEvent : public wxMouseEvent +{ + GridLabelClickEvent(wxEventType et, const wxMouseEvent& me, ColumnType colType) : wxMouseEvent(me), colType_(colType) { SetEventType(et); } + wxEvent* Clone() const override { return new GridLabelClickEvent(*this); } + + const ColumnType colType_; //may be ColumnType::NONE +}; + + +struct GridColumnResizeEvent : public wxCommandEvent +{ + GridColumnResizeEvent(int offset, ColumnType colType) : wxCommandEvent(EVENT_GRID_COL_RESIZE), colType_(colType), offset_(offset) {} + wxEvent* Clone() const override { return new GridColumnResizeEvent(*this); } + + const ColumnType colType_; + const int offset_; +}; + +using GridClickEventFunction = void (wxEvtHandler::*)(GridClickEvent&); +using GridRangeSelectEventFunction = void (wxEvtHandler::*)(GridRangeSelectEvent&); +using GridLabelClickEventFunction = void (wxEvtHandler::*)(GridLabelClickEvent&); +using GridColumnResizeEventFunction = void (wxEvtHandler::*)(GridColumnResizeEvent&); + +#define GridClickEventHandler(func) (wxObjectEventFunction)(wxEventFunction)wxStaticCastEvent(GridClickEventFunction, &func) +#define GridRangeSelectEventHandler(func) (wxObjectEventFunction)(wxEventFunction)wxStaticCastEvent(GridRangeSelectEventFunction, &func) +#define GridLabelClickEventHandler(func) (wxObjectEventFunction)(wxEventFunction)wxStaticCastEvent(GridLabelClickEventFunction, &func) +#define GridColumnResizeEventHandler(func)(wxObjectEventFunction)(wxEventFunction)wxStaticCastEvent(GridColumnResizeEventFunction, &func) + +//------------------------------------------------------------------------------------------------------------ +class Grid; + +void clearArea(wxDC& dc, const wxRect& rect, const wxColor& col); + +class GridData +{ +public: + virtual ~GridData() {} + + virtual size_t getRowCount() const = 0; + + //cell area: + virtual std::wstring getValue(size_t row, ColumnType colType) const = 0; + virtual void renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected); //default implementation + virtual void renderCell (wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover); + virtual int getBestSize (wxDC& dc, size_t row, ColumnType colType); //must correspond to renderCell()! + virtual std::wstring getToolTip (size_t row, ColumnType colType) const { return std::wstring(); } + virtual HoverArea getRowMouseHover(size_t row, ColumnType colType, int cellRelativePosX, int cellWidth) { return HoverArea::NONE; } + + //label area: + virtual std::wstring getColumnLabel(ColumnType colType) const = 0; + virtual void renderColumnLabel(Grid& grid, wxDC& dc, const wxRect& rect, ColumnType colType, bool highlighted); //default implementation + virtual std::wstring getToolTip(ColumnType colType) const { return std::wstring(); } + + static const int COLUMN_GAP_LEFT; //for left-aligned text + + //optional helper routines: + static wxSize drawCellText (wxDC& dc, const wxRect& rect, const std::wstring& text, int alignment = wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); //returns text extent + static wxRect drawCellBorder (wxDC& dc, const wxRect& rect); //returns inner rectangle + static void drawCellBackground(wxDC& dc, const wxRect& rect, bool enabled, bool selected, const wxColor& backgroundColor); + + static wxRect drawColumnLabelBorder (wxDC& dc, const wxRect& rect); //returns inner rectangle + static void drawColumnLabelBackground(wxDC& dc, const wxRect& rect, bool highlighted); + static void drawColumnLabelText (wxDC& dc, const wxRect& rect, const std::wstring& text); +}; + + +enum GridEventPolicy +{ + ALLOW_GRID_EVENT, + DENY_GRID_EVENT +}; + + +class Grid : public wxScrolledWindow +{ +public: + Grid(wxWindow* parent, + wxWindowID id = wxID_ANY, + const wxPoint& pos = wxDefaultPosition, + const wxSize& size = wxDefaultSize, + long style = wxTAB_TRAVERSAL | wxNO_BORDER, + const wxString& name = wxPanelNameStr); + + size_t getRowCount() const; + + void setRowHeight(int height); + + struct ColumnAttribute + { + ColumnAttribute(ColumnType type, int offset, int stretch, bool visible = true) : type_(type), visible_(visible), stretch_(std::max(stretch, 0)), offset_(offset) { assert(stretch >=0 ); } + ColumnType type_; + bool visible_; + //first, client width is partitioned according to all available stretch factors, then "offset_" is added + //universal model: a non-stretched column has stretch factor 0 with the "offset" becoming identical to final width! + int stretch_; //>= 0 + int offset_; + }; + + void setColumnConfig(const std::vector& attr); //set column count + widths + std::vector getColumnConfig() const; + + void setDataProvider(const std::shared_ptr& dataView) { dataView_ = dataView; } + /**/ GridData* getDataProvider() { return dataView_.get(); } + const GridData* getDataProvider() const { return dataView_.get(); } + //----------------------------------------------------------------------------- + + void setColumnLabelHeight(int height); + void showRowLabel(bool visible); + + enum ScrollBarStatus + { + SB_SHOW_AUTOMATIC, + SB_SHOW_ALWAYS, + SB_SHOW_NEVER, + }; + //alternative until wxScrollHelper::ShowScrollbars() becomes available in wxWidgets 2.9 + void showScrollBars(ScrollBarStatus horizontal, ScrollBarStatus vertical); + + std::vector getSelectedRows() const { return selection_.get(); } + void selectAllRows (GridEventPolicy rangeEventPolicy); + void clearSelection(GridEventPolicy rangeEventPolicy); //turn off range selection event when calling this function in an event handler to avoid recursion! + + void scrollDelta(int deltaX, int deltaY); //in scroll units + + wxWindow& getCornerWin (); + wxWindow& getRowLabelWin(); + wxWindow& getColLabelWin(); + wxWindow& getMainWin (); + const wxWindow& getMainWin() const; + + ptrdiff_t getRowAtPos(int posY) const; //return -1 for invalid position, >= rowCount if out of range; absolute coordinates! + + struct ColumnPosInfo + { + ColumnType colType; //ColumnType::NONE no column at x position! + int cellRelativePosX; + int colWidth; + }; + ColumnPosInfo getColumnAtPos(int posX) const; //absolute position! + + void refreshCell(size_t row, ColumnType colType); + + void enableColumnMove (bool value) { allowColumnMove_ = value; } + void enableColumnResize(bool value) { allowColumnResize_ = value; } + + void setGridCursor(size_t row); //set + show + select cursor (+ emit range selection event) + size_t getGridCursor() const; //returns row + + void scrollTo(size_t row); + + void Refresh(bool eraseBackground = true, const wxRect* rect = nullptr) override; + bool Enable(bool enable = true) override; + + //############################################################################################################ + + static wxColor getColorSelectionGradientFrom(); + static wxColor getColorSelectionGradientTo(); + +private: + void onPaintEvent(wxPaintEvent& event); + void onEraseBackGround(wxEraseEvent& event) {} //[!] + void onSizeEvent(wxSizeEvent& event) { updateWindowSizes(); event.Skip(); } + void onKeyDown(wxKeyEvent& event); + + void updateWindowSizes(bool updateScrollbar = true); + + void selectWithCursor(ptrdiff_t row); + void makeRowVisible(size_t row); + + void redirectRowLabelEvent(wxMouseEvent& event); + + wxSize GetSizeAvailableForScrollTarget(const wxSize& size) override; //required since wxWidgets 2.9 if SetTargetWindow() is used + + + int getBestColumnSize(size_t col) const; //return -1 on error + + void autoSizeColumns(GridEventPolicy columnResizeEventPolicy); + + friend class GridData; + class SubWindow; + class CornerWin; + class RowLabelWin; + class ColLabelWin; + class MainWin; + + class Selection + { + public: + void init(size_t rowCount) { rowSelectionValue.resize(rowCount); clear(); } + + size_t maxSize() const { return rowSelectionValue.size(); } + + std::vector get() const + { + std::vector result; + for (size_t row = 0; row < rowSelectionValue.size(); ++row) + if (rowSelectionValue[row] != 0) + result.push_back(row); + return result; + } + + void selectAll() { selectRange(0, rowSelectionValue.size(), true); } + void clear () { selectRange(0, rowSelectionValue.size(), false); } + + bool isSelected(size_t row) const { return row < rowSelectionValue.size() ? rowSelectionValue[row] != 0 : false; } + + void selectRange(size_t rowFirst, size_t rowLast, bool positive = true) //select [rowFirst, rowLast), trims if required! + { + if (rowFirst <= rowLast) + { + numeric::clamp(rowFirst, 0, rowSelectionValue.size()); + numeric::clamp(rowLast, 0, rowSelectionValue.size()); + + std::fill(rowSelectionValue.begin() + rowFirst, rowSelectionValue.begin() + rowLast, positive); + } + else assert(false); + } + + private: + std::vector rowSelectionValue; //effectively a vector of size "number of rows" + }; + + struct VisibleColumn + { + VisibleColumn(ColumnType type, int offset, int stretch) : type_(type), stretch_(stretch), offset_(offset) {} + ColumnType type_; + int stretch_; //>= 0 + int offset_; + }; + + struct ColumnWidth + { + ColumnWidth(ColumnType type, int width) : type_(type), width_(width) {} + ColumnType type_; + int width_; + }; + std::vector getColWidths() const; // + std::vector getColWidths(int mainWinWidth) const; //evaluate stretched columns + int getColWidthsSum(int mainWinWidth) const; + std::vector getColStretchedWidths(int clientWidth) const; //final width = (normalized) (stretchedWidth + offset) + + Opt getColWidth(size_t col) const + { + const auto& widths = getColWidths(); + if (col < widths.size()) + return widths[col].width_; + return NoValue(); + } + + void setColumnWidth(int width, size_t col, GridEventPolicy columnResizeEventPolicy, bool notifyAsync = false); + + wxRect getColumnLabelArea(ColumnType colType) const; //returns empty rect if column not found + + void selectRangeAndNotify(ptrdiff_t rowFrom, ptrdiff_t rowTo, bool positive, const GridClickEvent* mouseInitiated); //select inclusive range [rowFrom, rowTo] + notify event! + + bool isSelected(size_t row) const { return selection_.isSelected(row); } + + struct ColAction + { + bool wantResize = false; //"!wantResize" means "move" or "single click" + size_t col = 0; + }; + Opt clientPosToColumnAction(const wxPoint& pos) const; + void moveColumn(size_t colFrom, size_t colTo); + ptrdiff_t clientPosToMoveTargetColumn(const wxPoint& pos) const; //return < 0 on error + + ColumnType colToType(size_t col) const; //returns ColumnType::NONE on error + + /* + Visual layout: + -------------------------------- + |CornerWin | ColLabelWin | + |------------------------------| + |RowLabelWin | MainWin | + | | | + -------------------------------- + */ + CornerWin* cornerWin_; + RowLabelWin* rowLabelWin_; + ColLabelWin* colLabelWin_; + MainWin* mainWin_; + + ScrollBarStatus showScrollbarX_ = SB_SHOW_AUTOMATIC; + ScrollBarStatus showScrollbarY_ = SB_SHOW_AUTOMATIC; + + int colLabelHeight_ = 0; + bool drawRowLabel_ = true; + + std::shared_ptr dataView_; + Selection selection_; + bool allowColumnMove_ = true; + bool allowColumnResize_ = true; + + std::vector visibleCols_; //individual widths, type and total column count + std::vector oldColAttributes_; //visible + nonvisible columns; use for conversion in setColumnConfig()/getColumnConfig() *only*! + + size_t rowCountOld_ = 0; //at the time of last Grid::Refresh() +}; +} + +#endif //GRID_H_834702134831734869987 diff --git a/wx+/http.cpp b/wx+/http.cpp index 944c771a..15e8427f 100755 --- a/wx+/http.cpp +++ b/wx+/http.cpp @@ -1,267 +1,267 @@ -// ***************************************************************************** -// * 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 "http.h" - - #include - #include //std::thread::id - #include - -using namespace zen; - - -namespace -{ - -struct UrlRedirectError -{ - UrlRedirectError(const std::wstring& url) : newUrl(url) {} - std::wstring newUrl; -}; -} - - -class HttpInputStream::Impl -{ -public: - Impl(const std::wstring& url, const std::wstring& userAgent, const IOCallback& notifyUnbufferedIO, //throw SysError, UrlRedirectError - const std::string* postParams) : //issue POST if bound, GET otherwise - notifyUnbufferedIO_(notifyUnbufferedIO) - { - ZEN_ON_SCOPE_FAIL( cleanup(); /*destructor call would lead to member double clean-up!!!*/ ); - - assert(!startsWith(url, L"https:", CmpAsciiNoCase())); //not supported by wxHTTP! - const std::wstring urlFmt = startsWith(url, L"http://", CmpAsciiNoCase()) || - startsWith(url, L"https://", CmpAsciiNoCase()) ? afterFirst(url, L"://", IF_MISSING_RETURN_NONE) : url; - const std::wstring server = beforeFirst(urlFmt, L'/', IF_MISSING_RETURN_ALL); - const std::wstring page = L'/' + afterFirst(urlFmt, L'/', IF_MISSING_RETURN_NONE); - - assert(std::this_thread::get_id() == mainThreadId); - assert(wxApp::IsMainLoopRunning()); - - webAccess_.SetHeader(L"User-Agent", userAgent); - webAccess_.SetTimeout(10 /*[s]*/); //default: 10 minutes: WTF are these wxWidgets people thinking??? - - if (!webAccess_.Connect(server)) //will *not* fail for non-reachable url here! - throw SysError(L"wxHTTP::Connect"); - - if (postParams) - if (!webAccess_.SetPostText(L"application/x-www-form-urlencoded", utfTo(*postParams))) - throw SysError(L"wxHTTP::SetPostText"); - - httpStream_.reset(webAccess_.GetInputStream(page)); //pass ownership - const int sc = webAccess_.GetResponse(); - - //http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_Redirection - if (sc / 100 == 3) //e.g. 301, 302, 303, 307... we're not too greedy since we check location, too! - { - const std::wstring newUrl(webAccess_.GetHeader(L"Location")); - if (newUrl.empty()) - throw SysError(L"Unresolvable redirect. Empty target Location."); - - throw UrlRedirectError(newUrl); - } - - if (sc != 200) //HTTP_STATUS_OK - throw SysError(replaceCpy(L"HTTP status code %x.", L"%x", numberTo(sc))); - - if (!httpStream_ || webAccess_.GetError() != wxPROTO_NOERR) - throw SysError(L"wxHTTP::GetError (" + numberTo(webAccess_.GetError()) + L")"); - } - - ~Impl() { cleanup(); } - - size_t read(void* buffer, size_t bytesToRead) //throw SysError, X; return "bytesToRead" bytes unless end of stream! - { - const size_t blockSize = getBlockSize(); - - while (memBuf_.size() < bytesToRead) - { - memBuf_.resize(memBuf_.size() + blockSize); - const size_t bytesRead = tryRead(&*(memBuf_.end() - blockSize), blockSize); //throw SysError; may return short, only 0 means EOF! => CONTRACT: bytesToRead > 0 - memBuf_.resize(memBuf_.size() - blockSize + bytesRead); //caveat: unsigned arithmetics - - if (notifyUnbufferedIO_) notifyUnbufferedIO_(bytesRead); //throw X - - if (bytesRead == 0) //end of file - bytesToRead = std::min(bytesToRead, memBuf_.size()); - } - - std::copy(memBuf_.begin(), memBuf_.begin() + bytesToRead, static_cast(buffer)); - memBuf_.erase(memBuf_.begin(), memBuf_.begin() + bytesToRead); - return bytesToRead; - } - - size_t getBlockSize() const { return 64 * 1024; } - -private: - size_t tryRead(void* buffer, size_t bytesToRead) //throw SysError; may return short, only 0 means EOF! - { - if (bytesToRead == 0) //"read() with a count of 0 returns zero" => indistinguishable from end of file! => check! - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ":" + numberTo(__LINE__)); - assert(bytesToRead == getBlockSize()); - - httpStream_->Read(buffer, bytesToRead); - - const wxStreamError ec = httpStream_->GetLastError(); - if (ec != wxSTREAM_NO_ERROR && ec != wxSTREAM_EOF) - throw SysError(L"wxInputStream::GetLastError (" + numberTo(httpStream_->GetLastError()) + L")"); - - const size_t bytesRead = httpStream_->LastRead(); - //"if there are not enough bytes in the stream right now, LastRead() value will be - // less than size but greater than 0. If it is 0, it means that EOF has been reached." - assert(bytesRead > 0 || ec == wxSTREAM_EOF); - if (bytesRead > bytesToRead) //better safe than sorry - throw SysError(L"InternetReadFile: buffer overflow."); - - return bytesRead; //"zero indicates end of file" - } - - Impl (const Impl&) = delete; - Impl& operator=(const Impl&) = delete; - - void cleanup() - { - } - - wxHTTP webAccess_; - std::unique_ptr httpStream_; //must be deleted BEFORE webAccess is closed - - std::vector memBuf_; - const IOCallback notifyUnbufferedIO_; //throw X -}; - - -HttpInputStream::HttpInputStream(std::unique_ptr&& pimpl) : pimpl_(std::move(pimpl)) {} - -HttpInputStream::~HttpInputStream() {} - -size_t HttpInputStream::read(void* buffer, size_t bytesToRead) { return pimpl_->read(buffer, bytesToRead); } //throw SysError, X; return "bytesToRead" bytes unless end of stream! - -size_t HttpInputStream::getBlockSize() const { return pimpl_->getBlockSize(); } - -std::string HttpInputStream::readAll() { return bufferedLoad(*pimpl_); } //throw SysError, X; - -namespace -{ -std::unique_ptr sendHttpRequestImpl(const std::wstring& url, const std::wstring& userAgent, const IOCallback& notifyUnbufferedIO, //throw SysError - const std::string* postParams) //issue POST if bound, GET otherwise -{ - std::wstring urlRed = url; - //"A user agent should not automatically redirect a request more than five times, since such redirections usually indicate an infinite loop." - for (int redirects = 0; redirects < 6; ++redirects) - try - { - return std::make_unique(urlRed, userAgent, notifyUnbufferedIO, postParams); //throw SysError, UrlRedirectError - } - catch (const UrlRedirectError& e) { urlRed = e.newUrl; } - throw SysError(L"Too many redirects."); -} - - -//encode into "application/x-www-form-urlencoded" -std::string urlencode(const std::string& str) -{ - std::string out; - for (const char c : str) //follow PHP spec: https://github.com/php/php-src/blob/master/ext/standard/url.c#L500 - if (c == ' ') - out += '+'; - else if (('0' <= c && c <= '9') || - ('A' <= c && c <= 'Z') || - ('a' <= c && c <= 'z') || - c == '-' || c == '.' || c == '_') //note: "~" is encoded by PHP! - out += c; - else - { - const std::pair hex = hexify(c); - - out += '%'; - out += hex.first; - out += hex.second; - } - return out; -} - - -std::string urldecode(const std::string& str) -{ - std::string out; - for (size_t i = 0; i < str.size(); ++i) - { - const char c = str[i]; - if (c == '+') - out += ' '; - else if (c == '%' && str.size() - i >= 3 && - isHexDigit(str[i + 1]) && - isHexDigit(str[i + 2])) - { - out += unhexify(str[i + 1], str[i + 2]); - i += 2; - } - else - out += c; - } - return out; -} -} - - -std::string zen::xWwwFormUrlEncode(const std::vector>& paramPairs) -{ - std::string output; - for (const auto& pair : paramPairs) - output += urlencode(pair.first) + '=' + urlencode(pair.second) + '&'; - //encode both key and value: https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1 - if (!output.empty()) - output.pop_back(); - return output; -} - - -std::vector> zen::xWwwFormUrlDecode(const std::string& str) -{ - std::vector> output; - - for (const std::string& nvPair : split(str, '&', SplitType::SKIP_EMPTY)) - output.emplace_back(urldecode(beforeFirst(nvPair, '=', IF_MISSING_RETURN_ALL)), - urldecode(afterFirst (nvPair, '=', IF_MISSING_RETURN_NONE))); - return output; -} - - -HttpInputStream zen::sendHttpPost(const std::wstring& url, const std::wstring& userAgent, const IOCallback& notifyUnbufferedIO, - const std::vector>& postParams) //throw SysError -{ - const std::string encodedParams = xWwwFormUrlEncode(postParams); - return sendHttpRequestImpl(url, userAgent, notifyUnbufferedIO, &encodedParams); //throw SysError -} - - -HttpInputStream zen::sendHttpGet(const std::wstring& url, const std::wstring& userAgent, const IOCallback& notifyUnbufferedIO) //throw SysError -{ - return sendHttpRequestImpl(url, userAgent, notifyUnbufferedIO, nullptr); //throw SysError -} - - -bool zen::internetIsAlive() //noexcept -{ - assert(std::this_thread::get_id() == mainThreadId); - - const wxString server = L"www.google.com"; - const wxString page = L"/"; - - wxHTTP webAccess; - webAccess.SetTimeout(10 /*[s]*/); //default: 10 minutes: WTF are these wxWidgets people thinking??? - - if (!webAccess.Connect(server)) //will *not* fail for non-reachable url here! - return false; - - std::unique_ptr httpStream(webAccess.GetInputStream(page)); //call before checking wxHTTP::GetResponse() - const int sc = webAccess.GetResponse(); - //attention: http://www.google.com/ might redirect to "https" => don't follow, just return "true"!!! - return sc / 100 == 2 || //e.g. 200 - sc / 100 == 3; //e.g. 301, 302, 303, 307... when in doubt, consider internet alive! -} +// ***************************************************************************** +// * 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 "http.h" + + #include + #include //std::thread::id + #include + +using namespace zen; + + +namespace +{ + +struct UrlRedirectError +{ + UrlRedirectError(const std::wstring& url) : newUrl(url) {} + std::wstring newUrl; +}; +} + + +class HttpInputStream::Impl +{ +public: + Impl(const std::wstring& url, const std::wstring& userAgent, const IOCallback& notifyUnbufferedIO, //throw SysError, UrlRedirectError + const std::string* postParams) : //issue POST if bound, GET otherwise + notifyUnbufferedIO_(notifyUnbufferedIO) + { + ZEN_ON_SCOPE_FAIL( cleanup(); /*destructor call would lead to member double clean-up!!!*/ ); + + assert(!startsWith(url, L"https:", CmpAsciiNoCase())); //not supported by wxHTTP! + const std::wstring urlFmt = startsWith(url, L"http://", CmpAsciiNoCase()) || + startsWith(url, L"https://", CmpAsciiNoCase()) ? afterFirst(url, L"://", IF_MISSING_RETURN_NONE) : url; + const std::wstring server = beforeFirst(urlFmt, L'/', IF_MISSING_RETURN_ALL); + const std::wstring page = L'/' + afterFirst(urlFmt, L'/', IF_MISSING_RETURN_NONE); + + assert(std::this_thread::get_id() == mainThreadId); + assert(wxApp::IsMainLoopRunning()); + + webAccess_.SetHeader(L"User-Agent", userAgent); + webAccess_.SetTimeout(10 /*[s]*/); //default: 10 minutes: WTF are these wxWidgets people thinking??? + + if (!webAccess_.Connect(server)) //will *not* fail for non-reachable url here! + throw SysError(L"wxHTTP::Connect"); + + if (postParams) + if (!webAccess_.SetPostText(L"application/x-www-form-urlencoded", utfTo(*postParams))) + throw SysError(L"wxHTTP::SetPostText"); + + httpStream_.reset(webAccess_.GetInputStream(page)); //pass ownership + const int sc = webAccess_.GetResponse(); + + //http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_Redirection + if (sc / 100 == 3) //e.g. 301, 302, 303, 307... we're not too greedy since we check location, too! + { + const std::wstring newUrl(webAccess_.GetHeader(L"Location")); + if (newUrl.empty()) + throw SysError(L"Unresolvable redirect. Empty target Location."); + + throw UrlRedirectError(newUrl); + } + + if (sc != 200) //HTTP_STATUS_OK + throw SysError(replaceCpy(L"HTTP status code %x.", L"%x", numberTo(sc))); + + if (!httpStream_ || webAccess_.GetError() != wxPROTO_NOERR) + throw SysError(L"wxHTTP::GetError (" + numberTo(webAccess_.GetError()) + L")"); + } + + ~Impl() { cleanup(); } + + size_t read(void* buffer, size_t bytesToRead) //throw SysError, X; return "bytesToRead" bytes unless end of stream! + { + const size_t blockSize = getBlockSize(); + + while (memBuf_.size() < bytesToRead) + { + memBuf_.resize(memBuf_.size() + blockSize); + const size_t bytesRead = tryRead(&*(memBuf_.end() - blockSize), blockSize); //throw SysError; may return short, only 0 means EOF! => CONTRACT: bytesToRead > 0 + memBuf_.resize(memBuf_.size() - blockSize + bytesRead); //caveat: unsigned arithmetics + + if (notifyUnbufferedIO_) notifyUnbufferedIO_(bytesRead); //throw X + + if (bytesRead == 0) //end of file + bytesToRead = std::min(bytesToRead, memBuf_.size()); + } + + std::copy(memBuf_.begin(), memBuf_.begin() + bytesToRead, static_cast(buffer)); + memBuf_.erase(memBuf_.begin(), memBuf_.begin() + bytesToRead); + return bytesToRead; + } + + size_t getBlockSize() const { return 64 * 1024; } + +private: + size_t tryRead(void* buffer, size_t bytesToRead) //throw SysError; may return short, only 0 means EOF! + { + if (bytesToRead == 0) //"read() with a count of 0 returns zero" => indistinguishable from end of file! => check! + throw std::logic_error("Contract violation! " + std::string(__FILE__) + ":" + numberTo(__LINE__)); + assert(bytesToRead == getBlockSize()); + + httpStream_->Read(buffer, bytesToRead); + + const wxStreamError ec = httpStream_->GetLastError(); + if (ec != wxSTREAM_NO_ERROR && ec != wxSTREAM_EOF) + throw SysError(L"wxInputStream::GetLastError (" + numberTo(httpStream_->GetLastError()) + L")"); + + const size_t bytesRead = httpStream_->LastRead(); + //"if there are not enough bytes in the stream right now, LastRead() value will be + // less than size but greater than 0. If it is 0, it means that EOF has been reached." + assert(bytesRead > 0 || ec == wxSTREAM_EOF); + if (bytesRead > bytesToRead) //better safe than sorry + throw SysError(L"InternetReadFile: buffer overflow."); + + return bytesRead; //"zero indicates end of file" + } + + Impl (const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + + void cleanup() + { + } + + wxHTTP webAccess_; + std::unique_ptr httpStream_; //must be deleted BEFORE webAccess is closed + + std::vector memBuf_; + const IOCallback notifyUnbufferedIO_; //throw X +}; + + +HttpInputStream::HttpInputStream(std::unique_ptr&& pimpl) : pimpl_(std::move(pimpl)) {} + +HttpInputStream::~HttpInputStream() {} + +size_t HttpInputStream::read(void* buffer, size_t bytesToRead) { return pimpl_->read(buffer, bytesToRead); } //throw SysError, X; return "bytesToRead" bytes unless end of stream! + +size_t HttpInputStream::getBlockSize() const { return pimpl_->getBlockSize(); } + +std::string HttpInputStream::readAll() { return bufferedLoad(*pimpl_); } //throw SysError, X; + +namespace +{ +std::unique_ptr sendHttpRequestImpl(const std::wstring& url, const std::wstring& userAgent, const IOCallback& notifyUnbufferedIO, //throw SysError + const std::string* postParams) //issue POST if bound, GET otherwise +{ + std::wstring urlRed = url; + //"A user agent should not automatically redirect a request more than five times, since such redirections usually indicate an infinite loop." + for (int redirects = 0; redirects < 6; ++redirects) + try + { + return std::make_unique(urlRed, userAgent, notifyUnbufferedIO, postParams); //throw SysError, UrlRedirectError + } + catch (const UrlRedirectError& e) { urlRed = e.newUrl; } + throw SysError(L"Too many redirects."); +} + + +//encode into "application/x-www-form-urlencoded" +std::string urlencode(const std::string& str) +{ + std::string out; + for (const char c : str) //follow PHP spec: https://github.com/php/php-src/blob/master/ext/standard/url.c#L500 + if (c == ' ') + out += '+'; + else if (('0' <= c && c <= '9') || + ('A' <= c && c <= 'Z') || + ('a' <= c && c <= 'z') || + c == '-' || c == '.' || c == '_') //note: "~" is encoded by PHP! + out += c; + else + { + const std::pair hex = hexify(c); + + out += '%'; + out += hex.first; + out += hex.second; + } + return out; +} + + +std::string urldecode(const std::string& str) +{ + std::string out; + for (size_t i = 0; i < str.size(); ++i) + { + const char c = str[i]; + if (c == '+') + out += ' '; + else if (c == '%' && str.size() - i >= 3 && + isHexDigit(str[i + 1]) && + isHexDigit(str[i + 2])) + { + out += unhexify(str[i + 1], str[i + 2]); + i += 2; + } + else + out += c; + } + return out; +} +} + + +std::string zen::xWwwFormUrlEncode(const std::vector>& paramPairs) +{ + std::string output; + for (const auto& pair : paramPairs) + output += urlencode(pair.first) + '=' + urlencode(pair.second) + '&'; + //encode both key and value: https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1 + if (!output.empty()) + output.pop_back(); + return output; +} + + +std::vector> zen::xWwwFormUrlDecode(const std::string& str) +{ + std::vector> output; + + for (const std::string& nvPair : split(str, '&', SplitType::SKIP_EMPTY)) + output.emplace_back(urldecode(beforeFirst(nvPair, '=', IF_MISSING_RETURN_ALL)), + urldecode(afterFirst (nvPair, '=', IF_MISSING_RETURN_NONE))); + return output; +} + + +HttpInputStream zen::sendHttpPost(const std::wstring& url, const std::wstring& userAgent, const IOCallback& notifyUnbufferedIO, + const std::vector>& postParams) //throw SysError +{ + const std::string encodedParams = xWwwFormUrlEncode(postParams); + return sendHttpRequestImpl(url, userAgent, notifyUnbufferedIO, &encodedParams); //throw SysError +} + + +HttpInputStream zen::sendHttpGet(const std::wstring& url, const std::wstring& userAgent, const IOCallback& notifyUnbufferedIO) //throw SysError +{ + return sendHttpRequestImpl(url, userAgent, notifyUnbufferedIO, nullptr); //throw SysError +} + + +bool zen::internetIsAlive() //noexcept +{ + assert(std::this_thread::get_id() == mainThreadId); + + const wxString server = L"www.google.com"; + const wxString page = L"/"; + + wxHTTP webAccess; + webAccess.SetTimeout(10 /*[s]*/); //default: 10 minutes: WTF are these wxWidgets people thinking??? + + if (!webAccess.Connect(server)) //will *not* fail for non-reachable url here! + return false; + + std::unique_ptr httpStream(webAccess.GetInputStream(page)); //call before checking wxHTTP::GetResponse() + const int sc = webAccess.GetResponse(); + //attention: http://www.google.com/ might redirect to "https" => don't follow, just return "true"!!! + return sc / 100 == 2 || //e.g. 200 + sc / 100 == 3; //e.g. 301, 302, 303, 307... when in doubt, consider internet alive! +} diff --git a/wx+/http.h b/wx+/http.h index 97341e18..d2a28f2c 100755 --- a/wx+/http.h +++ b/wx+/http.h @@ -1,49 +1,49 @@ -// ***************************************************************************** -// * 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 * -// ***************************************************************************** - -#ifndef HTTP_h_879083425703425702 -#define HTTP_h_879083425703425702 - -#include -#include - -namespace zen -{ -/* - TREAD-SAFETY - ------------ - Windows: WinInet-based => may be called from worker thread - Linux: wxWidgets-based => don't call from worker thread -*/ -class HttpInputStream -{ -public: - //support zen/serialize.h buffered input stream concept - size_t read(void* buffer, size_t bytesToRead); //throw SysError, X; return "bytesToRead" bytes unless end of stream! - std::string readAll(); //throw SysError, X - - size_t getBlockSize() const; - - class Impl; - HttpInputStream(std::unique_ptr&& pimpl); - HttpInputStream(HttpInputStream&&) = default; - ~HttpInputStream(); - -private: - std::unique_ptr pimpl_; -}; - - -HttpInputStream sendHttpGet (const std::wstring& url, const std::wstring& userAgent, const IOCallback& notifyUnbufferedIO); //throw SysError -HttpInputStream sendHttpPost(const std::wstring& url, const std::wstring& userAgent, const IOCallback& notifyUnbufferedIO, - const std::vector>& postParams); //throw SysError -bool internetIsAlive(); //noexcept - -std::string xWwwFormUrlEncode(const std::vector>& paramPairs); -std::vector> xWwwFormUrlDecode(const std::string& str); -} - -#endif //HTTP_h_879083425703425702 +// ***************************************************************************** +// * 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 * +// ***************************************************************************** + +#ifndef HTTP_h_879083425703425702 +#define HTTP_h_879083425703425702 + +#include +#include + +namespace zen +{ +/* + TREAD-SAFETY + ------------ + Windows: WinInet-based => may be called from worker thread + Linux: wxWidgets-based => don't call from worker thread +*/ +class HttpInputStream +{ +public: + //support zen/serialize.h buffered input stream concept + size_t read(void* buffer, size_t bytesToRead); //throw SysError, X; return "bytesToRead" bytes unless end of stream! + std::string readAll(); //throw SysError, X + + size_t getBlockSize() const; + + class Impl; + HttpInputStream(std::unique_ptr&& pimpl); + HttpInputStream(HttpInputStream&&) = default; + ~HttpInputStream(); + +private: + std::unique_ptr pimpl_; +}; + + +HttpInputStream sendHttpGet (const std::wstring& url, const std::wstring& userAgent, const IOCallback& notifyUnbufferedIO); //throw SysError +HttpInputStream sendHttpPost(const std::wstring& url, const std::wstring& userAgent, const IOCallback& notifyUnbufferedIO, + const std::vector>& postParams); //throw SysError +bool internetIsAlive(); //noexcept + +std::string xWwwFormUrlEncode(const std::vector>& paramPairs); +std::vector> xWwwFormUrlDecode(const std::string& str); +} + +#endif //HTTP_h_879083425703425702 diff --git a/wx+/image_resources.cpp b/wx+/image_resources.cpp index dd8299c2..b185771b 100755 --- a/wx+/image_resources.cpp +++ b/wx+/image_resources.cpp @@ -1,168 +1,168 @@ -// ***************************************************************************** -// * 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 "image_resources.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "image_tools.h" - -using namespace zen; - - -namespace -{ - -void loadAnimFromZip(wxZipInputStream& zipInput, wxAnimation& anim) -{ - //work around wxWidgets bug: - //construct seekable input stream (zip-input stream is not seekable) for wxAnimation::Load() - //luckily this method call is very fast: below measurement precision! - std::vector data; - data.reserve(10000); - - int newValue = 0; - while ((newValue = zipInput.GetC()) != wxEOF) - data.push_back(newValue); - - wxMemoryInputStream seekAbleStream(&data.front(), data.size()); //stream does not take ownership of data - - anim.Load(seekAbleStream, wxANIMATION_TYPE_GIF); -} - - -class GlobalBitmaps -{ -public: - static std::shared_ptr instance() - { - static Global inst(std::make_unique()); - assert(std::this_thread::get_id() == mainThreadId); //wxWidgets is not thread-safe! - return inst.get(); - } - - GlobalBitmaps() {} - ~GlobalBitmaps() { assert(bitmaps.empty() && anims.empty()); } //don't leave wxWidgets objects for static destruction! - - void init(const Zstring& filepath); - void cleanup() - { - bitmaps.clear(); - anims.clear(); - } - - const wxBitmap& getImage (const wxString& name) const; - const wxAnimation& getAnimation(const wxString& name) const; - -private: - GlobalBitmaps (const GlobalBitmaps&) = delete; - GlobalBitmaps& operator=(const GlobalBitmaps&) = delete; - - std::map bitmaps; - std::map anims; -}; - - -void GlobalBitmaps::init(const Zstring& filepath) -{ - assert(bitmaps.empty() && anims.empty()); - - wxFFileInputStream input(utfTo(filepath)); - if (input.IsOk()) //if not... we don't want to react too harsh here - { - //activate support for .png files - wxImage::AddHandler(new wxPNGHandler); //ownership passed - - wxZipInputStream streamIn(input, wxConvUTF8); - //do NOT rely on wxConvLocal! On failure shows unhelpful popup "Cannot convert from the charset 'Unknown encoding (-1)'!" - - for (;;) - { - std::unique_ptr entry(streamIn.GetNextEntry()); //take ownership! - if (!entry) - break; - - const wxString name = entry->GetName(); - - //generic image loading - if (endsWith(name, L".png")) - { - wxImage img(streamIn, wxBITMAP_TYPE_PNG); - - //end this alpha/no-alpha/mask/wxDC::DrawBitmap/RTL/high-contrast-scheme interoperability nightmare here and now!!!! - //=> there's only one type of png image: with alpha channel, no mask!!! - convertToVanillaImage(img); - - bitmaps.emplace(name, img); - } - else if (endsWith(name, L".gif")) - loadAnimFromZip(streamIn, anims[name]); - } - } -} - - -const wxBitmap& GlobalBitmaps::getImage(const wxString& name) const -{ - auto it = bitmaps.find(contains(name, L'.') ? name : name + L".png"); //assume .png ending if nothing else specified - if (it != bitmaps.end()) - return it->second; - assert(false); - return wxNullBitmap; -} - - -const wxAnimation& GlobalBitmaps::getAnimation(const wxString& name) const -{ - auto it = anims.find(contains(name, L'.') ? name : name + L".gif"); - if (it != anims.end()) - return it->second; - assert(false); - return wxNullAnimation; -} -} - - -void zen::initResourceImages(const Zstring& filepath) -{ - if (std::shared_ptr inst = GlobalBitmaps::instance()) - inst->init(filepath); - else - assert(false); -} - - -void zen::cleanupResourceImages() -{ - if (std::shared_ptr inst = GlobalBitmaps::instance()) - inst->cleanup(); - else - assert(false); -} - - -const wxBitmap& zen::getResourceImage(const wxString& name) -{ - if (std::shared_ptr inst = GlobalBitmaps::instance()) - return inst->getImage(name); - assert(false); - return wxNullBitmap; -} - - -const wxAnimation& zen::getResourceAnimation(const wxString& name) -{ - if (std::shared_ptr inst = GlobalBitmaps::instance()) - return inst->getAnimation(name); - assert(false); - return wxNullAnimation; -} +// ***************************************************************************** +// * 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 "image_resources.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "image_tools.h" + +using namespace zen; + + +namespace +{ + +void loadAnimFromZip(wxZipInputStream& zipInput, wxAnimation& anim) +{ + //work around wxWidgets bug: + //construct seekable input stream (zip-input stream is not seekable) for wxAnimation::Load() + //luckily this method call is very fast: below measurement precision! + std::vector data; + data.reserve(10000); + + int newValue = 0; + while ((newValue = zipInput.GetC()) != wxEOF) + data.push_back(newValue); + + wxMemoryInputStream seekAbleStream(&data.front(), data.size()); //stream does not take ownership of data + + anim.Load(seekAbleStream, wxANIMATION_TYPE_GIF); +} + + +class GlobalBitmaps +{ +public: + static std::shared_ptr instance() + { + static Global inst(std::make_unique()); + assert(std::this_thread::get_id() == mainThreadId); //wxWidgets is not thread-safe! + return inst.get(); + } + + GlobalBitmaps() {} + ~GlobalBitmaps() { assert(bitmaps.empty() && anims.empty()); } //don't leave wxWidgets objects for static destruction! + + void init(const Zstring& filepath); + void cleanup() + { + bitmaps.clear(); + anims.clear(); + } + + const wxBitmap& getImage (const wxString& name) const; + const wxAnimation& getAnimation(const wxString& name) const; + +private: + GlobalBitmaps (const GlobalBitmaps&) = delete; + GlobalBitmaps& operator=(const GlobalBitmaps&) = delete; + + std::map bitmaps; + std::map anims; +}; + + +void GlobalBitmaps::init(const Zstring& filepath) +{ + assert(bitmaps.empty() && anims.empty()); + + wxFFileInputStream input(utfTo(filepath)); + if (input.IsOk()) //if not... we don't want to react too harsh here + { + //activate support for .png files + wxImage::AddHandler(new wxPNGHandler); //ownership passed + + wxZipInputStream streamIn(input, wxConvUTF8); + //do NOT rely on wxConvLocal! On failure shows unhelpful popup "Cannot convert from the charset 'Unknown encoding (-1)'!" + + for (;;) + { + std::unique_ptr entry(streamIn.GetNextEntry()); //take ownership! + if (!entry) + break; + + const wxString name = entry->GetName(); + + //generic image loading + if (endsWith(name, L".png")) + { + wxImage img(streamIn, wxBITMAP_TYPE_PNG); + + //end this alpha/no-alpha/mask/wxDC::DrawBitmap/RTL/high-contrast-scheme interoperability nightmare here and now!!!! + //=> there's only one type of png image: with alpha channel, no mask!!! + convertToVanillaImage(img); + + bitmaps.emplace(name, img); + } + else if (endsWith(name, L".gif")) + loadAnimFromZip(streamIn, anims[name]); + } + } +} + + +const wxBitmap& GlobalBitmaps::getImage(const wxString& name) const +{ + auto it = bitmaps.find(contains(name, L'.') ? name : name + L".png"); //assume .png ending if nothing else specified + if (it != bitmaps.end()) + return it->second; + assert(false); + return wxNullBitmap; +} + + +const wxAnimation& GlobalBitmaps::getAnimation(const wxString& name) const +{ + auto it = anims.find(contains(name, L'.') ? name : name + L".gif"); + if (it != anims.end()) + return it->second; + assert(false); + return wxNullAnimation; +} +} + + +void zen::initResourceImages(const Zstring& filepath) +{ + if (std::shared_ptr inst = GlobalBitmaps::instance()) + inst->init(filepath); + else + assert(false); +} + + +void zen::cleanupResourceImages() +{ + if (std::shared_ptr inst = GlobalBitmaps::instance()) + inst->cleanup(); + else + assert(false); +} + + +const wxBitmap& zen::getResourceImage(const wxString& name) +{ + if (std::shared_ptr inst = GlobalBitmaps::instance()) + return inst->getImage(name); + assert(false); + return wxNullBitmap; +} + + +const wxAnimation& zen::getResourceAnimation(const wxString& name) +{ + if (std::shared_ptr inst = GlobalBitmaps::instance()) + return inst->getAnimation(name); + assert(false); + return wxNullAnimation; +} diff --git a/wx+/image_resources.h b/wx+/image_resources.h index 37b94fce..c29adb9f 100755 --- a/wx+/image_resources.h +++ b/wx+/image_resources.h @@ -1,23 +1,23 @@ -// ***************************************************************************** -// * 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 * -// ***************************************************************************** - -#ifndef IMAGE_RESOURCES_H_8740257825342532457 -#define IMAGE_RESOURCES_H_8740257825342532457 - -#include -#include -#include - -namespace zen -{ -void initResourceImages(const Zstring& filepath); //pass resources .zip file at application startup -void cleanupResourceImages(); - -const wxBitmap& getResourceImage (const wxString& name); -const wxAnimation& getResourceAnimation(const wxString& name); -} - -#endif //IMAGE_RESOURCES_H_8740257825342532457 +// ***************************************************************************** +// * 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 * +// ***************************************************************************** + +#ifndef IMAGE_RESOURCES_H_8740257825342532457 +#define IMAGE_RESOURCES_H_8740257825342532457 + +#include +#include +#include + +namespace zen +{ +void initResourceImages(const Zstring& filepath); //pass resources .zip file at application startup +void cleanupResourceImages(); + +const wxBitmap& getResourceImage (const wxString& name); +const wxAnimation& getResourceAnimation(const wxString& name); +} + +#endif //IMAGE_RESOURCES_H_8740257825342532457 diff --git a/wx+/image_tools.cpp b/wx+/image_tools.cpp index 4b0f324f..90945a44 100755 --- a/wx+/image_tools.cpp +++ b/wx+/image_tools.cpp @@ -1,233 +1,233 @@ -// ***************************************************************************** -// * 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 "image_tools.h" -#include -#include - - -using namespace zen; - -namespace -{ -void writeToImage(const wxImage& source, wxImage& target, const wxPoint& pos) -{ - const int srcWidth = source.GetWidth (); - const int srcHeight = source.GetHeight(); - const int trgWidth = target.GetWidth (); - - if (srcWidth > 0 && srcHeight > 0) - { - assert(0 <= pos.x && pos.x + srcWidth <= trgWidth ); //draw area must be a - assert(0 <= pos.y && pos.y + srcHeight <= target.GetHeight()); //subset of target image! - assert(target.HasAlpha()); - - { - const unsigned char* sourcePtr = source.GetData(); - unsigned char* targetPtr = target.GetData() + 3 * (pos.x + pos.y * trgWidth); - - for (int row = 0; row < srcHeight; ++row) - ::memcpy(targetPtr + 3 * row * trgWidth, sourcePtr + 3 * row * srcWidth, 3 * srcWidth); - } - - //handle alpha channel - { - unsigned char* targetPtr = target.GetAlpha() + pos.x + pos.y * trgWidth; - if (source.HasAlpha()) - { - const unsigned char* sourcePtr = source.GetAlpha(); - for (int row = 0; row < srcHeight; ++row) - ::memcpy(targetPtr + row * trgWidth, sourcePtr + row * srcWidth, srcWidth); - } - else - for (int row = 0; row < srcHeight; ++row) - ::memset(targetPtr + row * trgWidth, wxIMAGE_ALPHA_OPAQUE, srcWidth); - } - } -} -} - - -wxImage zen::stackImages(const wxImage& img1, const wxImage& img2, ImageStackLayout dir, ImageStackAlignment align, int gap) -{ - assert(gap >= 0); - gap = std::max(0, gap); - - const int img1Width = img1.GetWidth (); - const int img1Height = img1.GetHeight(); - const int img2Width = img2.GetWidth (); - const int img2Height = img2.GetHeight(); - - int width = std::max(img1Width, img2Width); - int height = std::max(img1Height, img2Height); - switch (dir) - { - case ImageStackLayout::HORIZONTAL: - width = img1Width + gap + img2Width; - break; - - case ImageStackLayout::VERTICAL: - height = img1Height + gap + img2Height; - break; - } - wxImage output(width, height); - output.SetAlpha(); - ::memset(output.GetAlpha(), wxIMAGE_ALPHA_TRANSPARENT, width * height); - - auto calcPos = [&](int imageExtent, int totalExtent) - { - switch (align) - { - case ImageStackAlignment::CENTER: - return (totalExtent - imageExtent) / 2; - case ImageStackAlignment::LEFT: - return 0; - case ImageStackAlignment::RIGHT: - return totalExtent - imageExtent; - } - assert(false); - return 0; - }; - - switch (dir) - { - case ImageStackLayout::HORIZONTAL: - writeToImage(img1, output, wxPoint(0, calcPos(img1Height, height))); - writeToImage(img2, output, wxPoint(img1Width + gap, calcPos(img2Height, height))); - break; - - case ImageStackLayout::VERTICAL: - writeToImage(img1, output, wxPoint(calcPos(img1Width, width), 0)); - writeToImage(img2, output, wxPoint(calcPos(img2Width, width), img1Height + gap)); - break; - } - return output; -} - - -namespace -{ -void calcAlphaForBlackWhiteImage(wxImage& image) //assume black text on white background -{ - assert(image.HasAlpha()); - if (unsigned char* alphaPtr = image.GetAlpha()) - { - const int pixelCount = image.GetWidth() * image.GetHeight(); - const unsigned char* dataPtr = image.GetData(); - for (int i = 0; i < pixelCount; ++ i) - { - const unsigned char r = *dataPtr++; - const unsigned char g = *dataPtr++; - const unsigned char b = *dataPtr++; - - //black(0,0,0) becomes fully opaque(255), while white(255,255,255) becomes transparent(0) - alphaPtr[i] = static_cast((255 - r + 255 - g + 255 - b) / 3); //mixed mode arithmetics! - } - } -} - - -wxSize getTextExtent(const wxString& text, const wxFont& font) -{ - wxMemoryDC dc; //the context used for bitmaps - dc.SetFont(font); //the font parameter of GetMultiLineTextExtent() is not evalated on OS X, wxWidgets 2.9.5, so apply it to the DC directly! - return dc.GetMultiLineTextExtent(replaceCpy(text, L"&", L"", false)); //remove accelerator -} -} - -wxImage zen::createImageFromText(const wxString& text, const wxFont& font, const wxColor& col) -{ - //wxDC::DrawLabel() doesn't respect alpha channel => calculate alpha values manually: - - if (text.empty()) - return wxImage(); - - wxBitmap newBitmap(getTextExtent(text, font)); //seems we don't need to pass 24-bit depth here even for high-contrast color schemes - { - wxMemoryDC dc(newBitmap); - dc.SetBackground(*wxWHITE_BRUSH); - dc.Clear(); - - dc.SetTextForeground(*wxBLACK); //for use in calcAlphaForBlackWhiteImage - dc.SetTextBackground(*wxWHITE); // - dc.SetFont(font); - - //assert(!contains(text, L"&")); //accelerator keys not supported here; see also getTextExtent() - wxString textFmt = replaceCpy(text, L"&", L"", false); - - //for some reason wxDC::DrawText messes up "weak" bidi characters even when wxLayout_RightToLeft is set! (--> arrows in hebrew/arabic) - //=> use mark characters instead: - const wchar_t rtlMark = L'\u200F'; //UTF-8: E2 80 8F - if (wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft) - textFmt = rtlMark + textFmt + rtlMark; - - dc.DrawText(textFmt, wxPoint()); - } - - wxImage output(newBitmap.ConvertToImage()); - output.SetAlpha(); - - //calculate alpha channel - calcAlphaForBlackWhiteImage(output); - - //apply actual text color - unsigned char* dataPtr = output.GetData(); - const int pixelCount = output.GetWidth() * output.GetHeight(); - for (int i = 0; i < pixelCount; ++ i) - { - *dataPtr++ = col.Red(); - *dataPtr++ = col.Green(); - *dataPtr++ = col.Blue(); - } - return output; -} - - -void zen::convertToVanillaImage(wxImage& img) -{ - if (!img.HasAlpha()) - { - const int width = img.GetWidth (); - const int height = img.GetHeight(); - if (width <= 0 || height <= 0) return; - - unsigned char mask_r = 0; - unsigned char mask_g = 0; - unsigned char mask_b = 0; - const bool haveMask = img.HasMask() && img.GetOrFindMaskColour(&mask_r, &mask_g, &mask_b); - //check for mask before calling wxImage::GetOrFindMaskColour() to skip needlessly searching for new mask color - - img.SetAlpha(); - ::memset(img.GetAlpha(), wxIMAGE_ALPHA_OPAQUE, width * height); - - //wxWidgets, as always, tries to be more clever than it really is and fucks up wxStaticBitmap if wxBitmap is fully opaque: - img.GetAlpha()[width * height - 1] = 254; - - if (haveMask) - { - img.SetMask(false); - unsigned char* alphaPtr = img.GetAlpha(); - const unsigned char* dataPtr = img.GetData(); - - const int pixelCount = width * height; - for (int i = 0; i < pixelCount; ++ i) - { - const unsigned char r = *dataPtr++; - const unsigned char g = *dataPtr++; - const unsigned char b = *dataPtr++; - - if (r == mask_r && - g == mask_g && - b == mask_b) - alphaPtr[i] = wxIMAGE_ALPHA_TRANSPARENT; - } - } - } - else - { - assert(!img.HasMask()); - } -} +// ***************************************************************************** +// * 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 "image_tools.h" +#include +#include + + +using namespace zen; + +namespace +{ +void writeToImage(const wxImage& source, wxImage& target, const wxPoint& pos) +{ + const int srcWidth = source.GetWidth (); + const int srcHeight = source.GetHeight(); + const int trgWidth = target.GetWidth (); + + if (srcWidth > 0 && srcHeight > 0) + { + assert(0 <= pos.x && pos.x + srcWidth <= trgWidth ); //draw area must be a + assert(0 <= pos.y && pos.y + srcHeight <= target.GetHeight()); //subset of target image! + assert(target.HasAlpha()); + + { + const unsigned char* sourcePtr = source.GetData(); + unsigned char* targetPtr = target.GetData() + 3 * (pos.x + pos.y * trgWidth); + + for (int row = 0; row < srcHeight; ++row) + ::memcpy(targetPtr + 3 * row * trgWidth, sourcePtr + 3 * row * srcWidth, 3 * srcWidth); + } + + //handle alpha channel + { + unsigned char* targetPtr = target.GetAlpha() + pos.x + pos.y * trgWidth; + if (source.HasAlpha()) + { + const unsigned char* sourcePtr = source.GetAlpha(); + for (int row = 0; row < srcHeight; ++row) + ::memcpy(targetPtr + row * trgWidth, sourcePtr + row * srcWidth, srcWidth); + } + else + for (int row = 0; row < srcHeight; ++row) + ::memset(targetPtr + row * trgWidth, wxIMAGE_ALPHA_OPAQUE, srcWidth); + } + } +} +} + + +wxImage zen::stackImages(const wxImage& img1, const wxImage& img2, ImageStackLayout dir, ImageStackAlignment align, int gap) +{ + assert(gap >= 0); + gap = std::max(0, gap); + + const int img1Width = img1.GetWidth (); + const int img1Height = img1.GetHeight(); + const int img2Width = img2.GetWidth (); + const int img2Height = img2.GetHeight(); + + int width = std::max(img1Width, img2Width); + int height = std::max(img1Height, img2Height); + switch (dir) + { + case ImageStackLayout::HORIZONTAL: + width = img1Width + gap + img2Width; + break; + + case ImageStackLayout::VERTICAL: + height = img1Height + gap + img2Height; + break; + } + wxImage output(width, height); + output.SetAlpha(); + ::memset(output.GetAlpha(), wxIMAGE_ALPHA_TRANSPARENT, width * height); + + auto calcPos = [&](int imageExtent, int totalExtent) + { + switch (align) + { + case ImageStackAlignment::CENTER: + return (totalExtent - imageExtent) / 2; + case ImageStackAlignment::LEFT: + return 0; + case ImageStackAlignment::RIGHT: + return totalExtent - imageExtent; + } + assert(false); + return 0; + }; + + switch (dir) + { + case ImageStackLayout::HORIZONTAL: + writeToImage(img1, output, wxPoint(0, calcPos(img1Height, height))); + writeToImage(img2, output, wxPoint(img1Width + gap, calcPos(img2Height, height))); + break; + + case ImageStackLayout::VERTICAL: + writeToImage(img1, output, wxPoint(calcPos(img1Width, width), 0)); + writeToImage(img2, output, wxPoint(calcPos(img2Width, width), img1Height + gap)); + break; + } + return output; +} + + +namespace +{ +void calcAlphaForBlackWhiteImage(wxImage& image) //assume black text on white background +{ + assert(image.HasAlpha()); + if (unsigned char* alphaPtr = image.GetAlpha()) + { + const int pixelCount = image.GetWidth() * image.GetHeight(); + const unsigned char* dataPtr = image.GetData(); + for (int i = 0; i < pixelCount; ++ i) + { + const unsigned char r = *dataPtr++; + const unsigned char g = *dataPtr++; + const unsigned char b = *dataPtr++; + + //black(0,0,0) becomes fully opaque(255), while white(255,255,255) becomes transparent(0) + alphaPtr[i] = static_cast((255 - r + 255 - g + 255 - b) / 3); //mixed mode arithmetics! + } + } +} + + +wxSize getTextExtent(const wxString& text, const wxFont& font) +{ + wxMemoryDC dc; //the context used for bitmaps + dc.SetFont(font); //the font parameter of GetMultiLineTextExtent() is not evalated on OS X, wxWidgets 2.9.5, so apply it to the DC directly! + return dc.GetMultiLineTextExtent(replaceCpy(text, L"&", L"", false)); //remove accelerator +} +} + +wxImage zen::createImageFromText(const wxString& text, const wxFont& font, const wxColor& col) +{ + //wxDC::DrawLabel() doesn't respect alpha channel => calculate alpha values manually: + + if (text.empty()) + return wxImage(); + + wxBitmap newBitmap(getTextExtent(text, font)); //seems we don't need to pass 24-bit depth here even for high-contrast color schemes + { + wxMemoryDC dc(newBitmap); + dc.SetBackground(*wxWHITE_BRUSH); + dc.Clear(); + + dc.SetTextForeground(*wxBLACK); //for use in calcAlphaForBlackWhiteImage + dc.SetTextBackground(*wxWHITE); // + dc.SetFont(font); + + //assert(!contains(text, L"&")); //accelerator keys not supported here; see also getTextExtent() + wxString textFmt = replaceCpy(text, L"&", L"", false); + + //for some reason wxDC::DrawText messes up "weak" bidi characters even when wxLayout_RightToLeft is set! (--> arrows in hebrew/arabic) + //=> use mark characters instead: + const wchar_t rtlMark = L'\u200F'; //UTF-8: E2 80 8F + if (wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft) + textFmt = rtlMark + textFmt + rtlMark; + + dc.DrawText(textFmt, wxPoint()); + } + + wxImage output(newBitmap.ConvertToImage()); + output.SetAlpha(); + + //calculate alpha channel + calcAlphaForBlackWhiteImage(output); + + //apply actual text color + unsigned char* dataPtr = output.GetData(); + const int pixelCount = output.GetWidth() * output.GetHeight(); + for (int i = 0; i < pixelCount; ++ i) + { + *dataPtr++ = col.Red(); + *dataPtr++ = col.Green(); + *dataPtr++ = col.Blue(); + } + return output; +} + + +void zen::convertToVanillaImage(wxImage& img) +{ + if (!img.HasAlpha()) + { + const int width = img.GetWidth (); + const int height = img.GetHeight(); + if (width <= 0 || height <= 0) return; + + unsigned char mask_r = 0; + unsigned char mask_g = 0; + unsigned char mask_b = 0; + const bool haveMask = img.HasMask() && img.GetOrFindMaskColour(&mask_r, &mask_g, &mask_b); + //check for mask before calling wxImage::GetOrFindMaskColour() to skip needlessly searching for new mask color + + img.SetAlpha(); + ::memset(img.GetAlpha(), wxIMAGE_ALPHA_OPAQUE, width * height); + + //wxWidgets, as always, tries to be more clever than it really is and fucks up wxStaticBitmap if wxBitmap is fully opaque: + img.GetAlpha()[width * height - 1] = 254; + + if (haveMask) + { + img.SetMask(false); + unsigned char* alphaPtr = img.GetAlpha(); + const unsigned char* dataPtr = img.GetData(); + + const int pixelCount = width * height; + for (int i = 0; i < pixelCount; ++ i) + { + const unsigned char r = *dataPtr++; + const unsigned char g = *dataPtr++; + const unsigned char b = *dataPtr++; + + if (r == mask_r && + g == mask_g && + b == mask_b) + alphaPtr[i] = wxIMAGE_ALPHA_TRANSPARENT; + } + } + } + else + { + assert(!img.HasMask()); + } +} diff --git a/wx+/image_tools.h b/wx+/image_tools.h index 6287b752..cd0e28f0 100755 --- a/wx+/image_tools.h +++ b/wx+/image_tools.h @@ -1,258 +1,258 @@ -// ***************************************************************************** -// * 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 * -// ***************************************************************************** - -#ifndef IMAGE_TOOLS_H_45782456427634254 -#define IMAGE_TOOLS_H_45782456427634254 - -#include -#include -#include -#include -#include - - -namespace zen -{ -enum class ImageStackLayout -{ - HORIZONTAL, - VERTICAL -}; - -enum class ImageStackAlignment -{ - CENTER, - LEFT, - RIGHT, - TOP = LEFT, - BOTTOM = RIGHT, -}; -wxImage stackImages(const wxImage& img1, const wxImage& img2, ImageStackLayout dir, ImageStackAlignment align, int gap = 0); - -wxImage createImageFromText(const wxString& text, const wxFont& font, const wxColor& col); - -wxBitmap layOver(const wxBitmap& background, const wxBitmap& foreground); //merge - -wxImage greyScale(const wxImage& img); //greyscale + brightness adaption -wxBitmap greyScale(const wxBitmap& bmp); // - -//void moveImage(wxImage& img, int right, int up); -void adjustBrightness(wxImage& img, int targetLevel); -double getAvgBrightness(const wxImage& img); //in [0, 255] -void brighten(wxImage& img, int level); //level: delta per channel in points - -bool isEqual(const wxBitmap& lhs, const wxBitmap& rhs); //pixel-wise equality (respecting alpha channel) - -void convertToVanillaImage(wxImage& img); //add alpha channel if missing + remove mask if existing - -//wxColor gradient(const wxColor& from, const wxColor& to, double fraction); //maps fraction within [0, 1] to an intermediate color - -//wxColor hsvColor(double h, double s, double v); //h within [0, 360), s, v within [0, 1] - - - - - - - - - - - - - - -//################################### implementation ################################### -/* -inline -void moveImage(wxImage& img, int right, int up) -{ - img = img.GetSubImage(wxRect(std::max(0, -right), std::max(0, up), img.GetWidth() - abs(right), img.GetHeight() - abs(up))); - img.Resize(wxSize(img.GetWidth() + abs(right), img.GetHeight() + abs(up)), wxPoint(std::max(0, right), std::max(0, -up))); -} -*/ - - -inline -wxImage greyScale(const wxImage& img) -{ - wxImage output = img.ConvertToGreyscale(1.0 / 3, 1.0 / 3, 1.0 / 3); //treat all channels equally! - //wxImage output = bmp.ConvertToImage().ConvertToGreyscale(); - adjustBrightness(output, 160); - return output; -} - - -inline -wxBitmap greyScale(const wxBitmap& bmp) -{ - assert(!bmp.GetMask()); //wxWidgets screws up for the gazillionth time applying a mask instead of alpha channel if the .png image has only 0 and 0xff opacity values!!! - return greyScale(bmp.ConvertToImage()); -} - - -inline -double getAvgBrightness(const wxImage& img) -{ - const int pixelCount = img.GetWidth() * img.GetHeight(); - auto pixBegin = img.GetData(); - - if (pixelCount > 0 && pixBegin) - { - auto pixEnd = pixBegin + 3 * pixelCount; //RGB - - if (img.HasAlpha()) - { - const unsigned char* alphaFirst = img.GetAlpha(); - - //calculate average weighted by alpha channel - double dividend = 0; - for (auto it = pixBegin; it != pixEnd; ++it) - dividend += *it * static_cast(alphaFirst[(it - pixBegin) / 3]); - - const double divisor = 3.0 * std::accumulate(alphaFirst, alphaFirst + pixelCount, 0.0); - - return numeric::isNull(divisor) ? 0 : dividend / divisor; - } - else - return std::accumulate(pixBegin, pixEnd, 0.0) / (3.0 * pixelCount); - } - return 0; -} - - -inline -void brighten(wxImage& img, int level) -{ - if (auto pixBegin = img.GetData()) - { - const int pixelCount = img.GetWidth() * img.GetHeight(); - auto pixEnd = pixBegin + 3 * pixelCount; //RGB - if (level > 0) - std::for_each(pixBegin, pixEnd, [&](unsigned char& c) { c = static_cast(std::min(255, c + level)); }); - else - std::for_each(pixBegin, pixEnd, [&](unsigned char& c) { c = static_cast(std::max(0, c + level)); }); - } -} - - -inline -void adjustBrightness(wxImage& img, int targetLevel) -{ - brighten(img, targetLevel - getAvgBrightness(img)); -} - - -inline -wxBitmap layOver(const wxBitmap& background, const wxBitmap& foreground) -{ - assert(foreground.HasAlpha() == background.HasAlpha()); //we don't support mixed-mode brittleness! - - wxBitmap output(background.ConvertToImage()); //attention: wxBitmap/wxImage use ref-counting without copy on write! - { - wxMemoryDC dc(output); - - const int offsetX = (background.GetWidth () - foreground.GetWidth ()) / 2; - const int offsetY = (background.GetHeight() - foreground.GetHeight()) / 2; - dc.DrawBitmap(foreground, offsetX, offsetY); - } - return output; -} - - -inline -bool isEqual(const wxBitmap& lhs, const wxBitmap& rhs) -{ - if (lhs.IsOk() != rhs.IsOk()) - return false; - if (!lhs.IsOk()) - return true; - - if (lhs.GetSize() != rhs.GetSize()) - return false; - - wxImage imLhs = lhs.ConvertToImage(); - wxImage imRhs = rhs.ConvertToImage(); - - if (imLhs.HasAlpha() != imRhs.HasAlpha()) - return false; - - const int pixelCount = lhs.GetWidth() * lhs.GetHeight(); - - if (!std::equal(imLhs.GetData(), imLhs.GetData() + pixelCount * 3, imRhs.GetData())) - return false; - - if (imLhs.HasAlpha()) - if (!std::equal(imLhs.GetAlpha(), imLhs.GetAlpha() + pixelCount, imRhs.GetAlpha())) - return false; - - return true; -} - -/* -inline -wxColor gradient(const wxColor& from, const wxColor& to, double fraction) -{ - fraction = std::max(0.0, fraction); - fraction = std::min(1.0, fraction); - return wxColor(from.Red () + (to.Red () - from.Red ()) * fraction, - from.Green() + (to.Green() - from.Green()) * fraction, - from.Blue () + (to.Blue () - from.Blue ()) * fraction, - from.Alpha() + (to.Alpha() - from.Alpha()) * fraction); -} -*/ - -/* -inline -wxColor hsvColor(double h, double s, double v) //h within [0, 360), s, v within [0, 1] -{ - //http://de.wikipedia.org/wiki/HSV-Farbraum - - //make input values fit into bounds - if (h > 360) - h -= static_cast(h / 360) * 360; - else if (h < 0) - h -= static_cast(h / 360) * 360 - 360; - numeric::confine(s, 0, 1); - numeric::confine(v, 0, 1); - //------------------------------------ - const int h_i = h / 60; - const float f = h / 60 - h_i; - - auto polish = [](double val) -> unsigned char - { - int result = numeric::round(val * 255); - numeric::confine(result, 0, 255); - return static_cast(result); - }; - - const unsigned char p = polish(v * (1 - s)); - const unsigned char q = polish(v * (1 - s * f)); - const unsigned char t = polish(v * (1 - s * (1 - f))); - const unsigned char vi = polish(v); - - switch (h_i) - { - case 0: - return wxColor(vi, t, p); - case 1: - return wxColor(q, vi, p); - case 2: - return wxColor(p, vi, t); - case 3: - return wxColor(p, q, vi); - case 4: - return wxColor(t, p, vi); - case 5: - return wxColor(vi, p, q); - } - assert(false); - return *wxBLACK; -} -*/ -} - -#endif //IMAGE_TOOLS_H_45782456427634254 +// ***************************************************************************** +// * 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 * +// ***************************************************************************** + +#ifndef IMAGE_TOOLS_H_45782456427634254 +#define IMAGE_TOOLS_H_45782456427634254 + +#include +#include +#include +#include +#include + + +namespace zen +{ +enum class ImageStackLayout +{ + HORIZONTAL, + VERTICAL +}; + +enum class ImageStackAlignment +{ + CENTER, + LEFT, + RIGHT, + TOP = LEFT, + BOTTOM = RIGHT, +}; +wxImage stackImages(const wxImage& img1, const wxImage& img2, ImageStackLayout dir, ImageStackAlignment align, int gap = 0); + +wxImage createImageFromText(const wxString& text, const wxFont& font, const wxColor& col); + +wxBitmap layOver(const wxBitmap& background, const wxBitmap& foreground); //merge + +wxImage greyScale(const wxImage& img); //greyscale + brightness adaption +wxBitmap greyScale(const wxBitmap& bmp); // + +//void moveImage(wxImage& img, int right, int up); +void adjustBrightness(wxImage& img, int targetLevel); +double getAvgBrightness(const wxImage& img); //in [0, 255] +void brighten(wxImage& img, int level); //level: delta per channel in points + +bool isEqual(const wxBitmap& lhs, const wxBitmap& rhs); //pixel-wise equality (respecting alpha channel) + +void convertToVanillaImage(wxImage& img); //add alpha channel if missing + remove mask if existing + +//wxColor gradient(const wxColor& from, const wxColor& to, double fraction); //maps fraction within [0, 1] to an intermediate color + +//wxColor hsvColor(double h, double s, double v); //h within [0, 360), s, v within [0, 1] + + + + + + + + + + + + + + +//################################### implementation ################################### +/* +inline +void moveImage(wxImage& img, int right, int up) +{ + img = img.GetSubImage(wxRect(std::max(0, -right), std::max(0, up), img.GetWidth() - abs(right), img.GetHeight() - abs(up))); + img.Resize(wxSize(img.GetWidth() + abs(right), img.GetHeight() + abs(up)), wxPoint(std::max(0, right), std::max(0, -up))); +} +*/ + + +inline +wxImage greyScale(const wxImage& img) +{ + wxImage output = img.ConvertToGreyscale(1.0 / 3, 1.0 / 3, 1.0 / 3); //treat all channels equally! + //wxImage output = bmp.ConvertToImage().ConvertToGreyscale(); + adjustBrightness(output, 160); + return output; +} + + +inline +wxBitmap greyScale(const wxBitmap& bmp) +{ + assert(!bmp.GetMask()); //wxWidgets screws up for the gazillionth time applying a mask instead of alpha channel if the .png image has only 0 and 0xff opacity values!!! + return greyScale(bmp.ConvertToImage()); +} + + +inline +double getAvgBrightness(const wxImage& img) +{ + const int pixelCount = img.GetWidth() * img.GetHeight(); + auto pixBegin = img.GetData(); + + if (pixelCount > 0 && pixBegin) + { + auto pixEnd = pixBegin + 3 * pixelCount; //RGB + + if (img.HasAlpha()) + { + const unsigned char* alphaFirst = img.GetAlpha(); + + //calculate average weighted by alpha channel + double dividend = 0; + for (auto it = pixBegin; it != pixEnd; ++it) + dividend += *it * static_cast(alphaFirst[(it - pixBegin) / 3]); + + const double divisor = 3.0 * std::accumulate(alphaFirst, alphaFirst + pixelCount, 0.0); + + return numeric::isNull(divisor) ? 0 : dividend / divisor; + } + else + return std::accumulate(pixBegin, pixEnd, 0.0) / (3.0 * pixelCount); + } + return 0; +} + + +inline +void brighten(wxImage& img, int level) +{ + if (auto pixBegin = img.GetData()) + { + const int pixelCount = img.GetWidth() * img.GetHeight(); + auto pixEnd = pixBegin + 3 * pixelCount; //RGB + if (level > 0) + std::for_each(pixBegin, pixEnd, [&](unsigned char& c) { c = static_cast(std::min(255, c + level)); }); + else + std::for_each(pixBegin, pixEnd, [&](unsigned char& c) { c = static_cast(std::max(0, c + level)); }); + } +} + + +inline +void adjustBrightness(wxImage& img, int targetLevel) +{ + brighten(img, targetLevel - getAvgBrightness(img)); +} + + +inline +wxBitmap layOver(const wxBitmap& background, const wxBitmap& foreground) +{ + assert(foreground.HasAlpha() == background.HasAlpha()); //we don't support mixed-mode brittleness! + + wxBitmap output(background.ConvertToImage()); //attention: wxBitmap/wxImage use ref-counting without copy on write! + { + wxMemoryDC dc(output); + + const int offsetX = (background.GetWidth () - foreground.GetWidth ()) / 2; + const int offsetY = (background.GetHeight() - foreground.GetHeight()) / 2; + dc.DrawBitmap(foreground, offsetX, offsetY); + } + return output; +} + + +inline +bool isEqual(const wxBitmap& lhs, const wxBitmap& rhs) +{ + if (lhs.IsOk() != rhs.IsOk()) + return false; + if (!lhs.IsOk()) + return true; + + if (lhs.GetSize() != rhs.GetSize()) + return false; + + wxImage imLhs = lhs.ConvertToImage(); + wxImage imRhs = rhs.ConvertToImage(); + + if (imLhs.HasAlpha() != imRhs.HasAlpha()) + return false; + + const int pixelCount = lhs.GetWidth() * lhs.GetHeight(); + + if (!std::equal(imLhs.GetData(), imLhs.GetData() + pixelCount * 3, imRhs.GetData())) + return false; + + if (imLhs.HasAlpha()) + if (!std::equal(imLhs.GetAlpha(), imLhs.GetAlpha() + pixelCount, imRhs.GetAlpha())) + return false; + + return true; +} + +/* +inline +wxColor gradient(const wxColor& from, const wxColor& to, double fraction) +{ + fraction = std::max(0.0, fraction); + fraction = std::min(1.0, fraction); + return wxColor(from.Red () + (to.Red () - from.Red ()) * fraction, + from.Green() + (to.Green() - from.Green()) * fraction, + from.Blue () + (to.Blue () - from.Blue ()) * fraction, + from.Alpha() + (to.Alpha() - from.Alpha()) * fraction); +} +*/ + +/* +inline +wxColor hsvColor(double h, double s, double v) //h within [0, 360), s, v within [0, 1] +{ + //http://de.wikipedia.org/wiki/HSV-Farbraum + + //make input values fit into bounds + if (h > 360) + h -= static_cast(h / 360) * 360; + else if (h < 0) + h -= static_cast(h / 360) * 360 - 360; + numeric::confine(s, 0, 1); + numeric::confine(v, 0, 1); + //------------------------------------ + const int h_i = h / 60; + const float f = h / 60 - h_i; + + auto polish = [](double val) -> unsigned char + { + int result = numeric::round(val * 255); + numeric::confine(result, 0, 255); + return static_cast(result); + }; + + const unsigned char p = polish(v * (1 - s)); + const unsigned char q = polish(v * (1 - s * f)); + const unsigned char t = polish(v * (1 - s * (1 - f))); + const unsigned char vi = polish(v); + + switch (h_i) + { + case 0: + return wxColor(vi, t, p); + case 1: + return wxColor(q, vi, p); + case 2: + return wxColor(p, vi, t); + case 3: + return wxColor(p, q, vi); + case 4: + return wxColor(t, p, vi); + case 5: + return wxColor(vi, p, q); + } + assert(false); + return *wxBLACK; +} +*/ +} + +#endif //IMAGE_TOOLS_H_45782456427634254 diff --git a/wx+/no_flicker.h b/wx+/no_flicker.h index 6794b8bd..11443a1d 100755 --- a/wx+/no_flicker.h +++ b/wx+/no_flicker.h @@ -1,39 +1,39 @@ -// ***************************************************************************** -// * 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 * -// ***************************************************************************** - -#ifndef NO_FLICKER_H_893421590321532 -#define NO_FLICKER_H_893421590321532 - -#include -#include - -namespace zen -{ -inline -void setText(wxTextCtrl& control, const wxString& newText, bool* additionalLayoutChange = nullptr) -{ - const wxString& label = control.GetValue(); //perf: don't call twice! - if (additionalLayoutChange && !*additionalLayoutChange) //never revert from true to false! - *additionalLayoutChange = label.length() != newText.length(); //avoid screen flicker: update layout only when necessary - - if (label != newText) - control.ChangeValue(newText); -} - -inline -void setText(wxStaticText& control, wxString newText, bool* additionalLayoutChange = nullptr) -{ - - const wxString& label = control.GetLabel(); //perf: don't call twice! - if (additionalLayoutChange && !*additionalLayoutChange) - *additionalLayoutChange = label.length() != newText.length(); //avoid screen flicker: update layout only when necessary - - if (label != newText) - control.SetLabel(newText); -} -} - -#endif //NO_FLICKER_H_893421590321532 +// ***************************************************************************** +// * 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 * +// ***************************************************************************** + +#ifndef NO_FLICKER_H_893421590321532 +#define NO_FLICKER_H_893421590321532 + +#include +#include + +namespace zen +{ +inline +void setText(wxTextCtrl& control, const wxString& newText, bool* additionalLayoutChange = nullptr) +{ + const wxString& label = control.GetValue(); //perf: don't call twice! + if (additionalLayoutChange && !*additionalLayoutChange) //never revert from true to false! + *additionalLayoutChange = label.length() != newText.length(); //avoid screen flicker: update layout only when necessary + + if (label != newText) + control.ChangeValue(newText); +} + +inline +void setText(wxStaticText& control, wxString newText, bool* additionalLayoutChange = nullptr) +{ + + const wxString& label = control.GetLabel(); //perf: don't call twice! + if (additionalLayoutChange && !*additionalLayoutChange) + *additionalLayoutChange = label.length() != newText.length(); //avoid screen flicker: update layout only when necessary + + if (label != newText) + control.SetLabel(newText); +} +} + +#endif //NO_FLICKER_H_893421590321532 diff --git a/wx+/popup_dlg.cpp b/wx+/popup_dlg.cpp index dca93658..120bf852 100755 --- a/wx+/popup_dlg.cpp +++ b/wx+/popup_dlg.cpp @@ -1,302 +1,302 @@ -// ***************************************************************************** -// * 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 "popup_dlg.h" -#include -#include -#include -#include -#include -#include "popup_dlg_generated.h" - - -using namespace zen; - -namespace -{ -void setAsStandard(wxButton& btn) -{ - btn.SetDefault(); - btn.SetFocus(); -} - - -void setBestInitialSize(wxTextCtrl& ctrl, const wxString& text, wxSize maxSize) -{ - const int scrollbarWidth = 30; - if (maxSize.x <= scrollbarWidth) //implicitly checks for non-zero, too! - return; - maxSize.x -= scrollbarWidth; - - int bestWidth = 0; - int rowCount = 0; - int rowHeight = 0; - - auto evalLineExtent = [&](const wxSize& sz) -> bool //return true when done - { - if (sz.x > bestWidth) - bestWidth = std::min(maxSize.x, sz.x); - - rowCount += (sz.x + maxSize.x - 1) / maxSize.x; //integer round up: consider line-wraps! - rowHeight = std::max(rowHeight, sz.y); //all rows *should* have same height - - return rowCount * rowHeight >= maxSize.y; - }; - - for (auto it = text.begin();;) - { - auto itEnd = std::find(it, text.end(), L'\n'); - wxString line(it, itEnd); - if (line.empty()) - line = L" "; //GetTextExtent() returns (0, 0) for empty strings! - - wxSize sz = ctrl.GetTextExtent(line); //exactly gives row height, but does *not* consider newlines - if (evalLineExtent(sz)) - break; - - if (itEnd == text.end()) - break; - it = itEnd + 1; - } - - const int rowGap = 0; - const wxSize bestSize(bestWidth + scrollbarWidth, std::min(rowCount * (rowHeight + rowGap), maxSize.y)); - ctrl.SetMinSize(bestSize); //alas, SetMinClientSize() is just not working! -} -} - - -class zen::StandardPopupDialog : public PopupDialogGenerated -{ -public: - StandardPopupDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg) : - PopupDialogGenerated(parent), - checkBoxValue_(cfg.checkBoxValue) - { - wxBitmap iconTmp; - wxString titleTmp; - switch (type) - { - case DialogInfoType::INFO: - //"Information" is meaningless as caption text! - //confirmation doesn't use info icon - //iconTmp = getResourceImage(L"msg_info"); - break; - case DialogInfoType::WARNING: - iconTmp = getResourceImage(L"msg_warning"); - titleTmp = _("Warning"); - break; - case DialogInfoType::ERROR2: - iconTmp = getResourceImage(L"msg_error"); - titleTmp = _("Error"); - break; - } - if (cfg.icon.IsOk()) - iconTmp = cfg.icon; - - if (!cfg.title.empty()) - titleTmp = cfg.title; - //----------------------------------------------- - m_bitmapMsgType->SetBitmap(iconTmp); - - if (titleTmp.empty()) - SetTitle(wxTheApp->GetAppDisplayName()); - else - { - if (parent && parent->IsShownOnScreen()) - SetTitle(titleTmp); - else - SetTitle(wxTheApp->GetAppDisplayName() + L" - " + titleTmp); - } - - int maxWidth = 500; - int maxHeight = 400; //try to determine better value based on actual display resolution: - - if (parent) - { - const int disPos = wxDisplay::GetFromWindow(parent); //window must be visible - if (disPos != wxNOT_FOUND) - maxHeight = wxDisplay(disPos).GetClientArea().GetHeight() * 2 / 3; - } - - assert(!cfg.textMain.empty() || !cfg.textDetail.empty()); - if (!cfg.textMain.empty()) - { - setMainInstructionFont(*m_staticTextMain); - m_staticTextMain->SetLabel(cfg.textMain); - m_staticTextMain->Wrap(maxWidth); //call *after* SetLabel() - } - else - m_staticTextMain->Hide(); - - if (!cfg.textDetail.empty()) - { - const wxString& text = L"\n" + trimCpy(cfg.textDetail) + L"\n"; //add empty top/bottom lines *instead* of using border space! - setBestInitialSize(*m_textCtrlTextDetail, text, wxSize(maxWidth, maxHeight)); - m_textCtrlTextDetail->ChangeValue(text); - } - else - m_textCtrlTextDetail->Hide(); - - if (checkBoxValue_) - { - assert(contains(cfg.checkBoxLabel, L"&")); - m_checkBoxCustom->SetLabel(cfg.checkBoxLabel); - m_checkBoxCustom->SetValue(*checkBoxValue_); - } - else - m_checkBoxCustom->Hide(); - - Connect(wxEVT_CHAR_HOOK, wxKeyEventHandler(StandardPopupDialog::OnKeyPressed), nullptr, this); //dialog-specific local key events - } - -private: - void OnClose (wxCloseEvent& event) override { EndModal(static_cast(ConfirmationButton3::CANCEL)); } - void OnCancel(wxCommandEvent& event) override { EndModal(static_cast(ConfirmationButton3::CANCEL)); } - - void OnKeyPressed(wxKeyEvent& event) - { - switch (event.GetKeyCode()) - { - case WXK_RETURN: - case WXK_NUMPAD_ENTER: - { - wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); - OnButtonAffirmative(dummy); - return; - } - - case WXK_ESCAPE: //handle case where cancel button is hidden! - EndModal(static_cast(ConfirmationButton3::CANCEL)); - return; - } - event.Skip(); - } - - void OnButtonAffirmative(wxCommandEvent& event) override - { - if (checkBoxValue_) - *checkBoxValue_ = m_checkBoxCustom->GetValue(); - EndModal(static_cast(ConfirmationButton3::DO_IT)); - } - - void OnButtonNegative(wxCommandEvent& event) override - { - if (checkBoxValue_) - *checkBoxValue_ = m_checkBoxCustom->GetValue(); - EndModal(static_cast(ConfirmationButton3::DONT_DO_IT)); - } - - bool* checkBoxValue_; -}; - - -namespace -{ -class NotificationDialog : public StandardPopupDialog -{ -public: - NotificationDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg) : - StandardPopupDialog(parent, type, cfg) - { - m_buttonAffirmative->SetLabel(_("Close")); //UX Guide: use "Close" for errors, warnings and windows in which users can't make changes (no ampersand!) - m_buttonNegative->Hide(); - m_buttonCancel->Hide(); - - //set std order after button visibility was set - setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonAffirmative)); - setAsStandard(*m_buttonAffirmative); - GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() - Center(); //needs to be re-applied after a dialog size change! - } -}; - - -class ConfirmationDialog : public StandardPopupDialog -{ -public: - ConfirmationDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg, const wxString& labelDoIt) : - StandardPopupDialog(parent, type, cfg) - { - assert(contains(labelDoIt, L"&")); - m_buttonAffirmative->SetLabel(labelDoIt); - m_buttonNegative->Hide(); - - //set std order after button visibility was set - setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonAffirmative).setCancel(m_buttonCancel)); - setAsStandard(*m_buttonAffirmative); - GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() - Center(); //needs to be re-applied after a dialog size change! - } -}; -} - -class zen::ConfirmationDialog3 : public StandardPopupDialog -{ -public: - ConfirmationDialog3(wxWindow* parent, DialogInfoType type, const PopupDialogCfg3& cfg, const wxString& labelDoIt, const wxString& labelDontDoIt) : - StandardPopupDialog(parent, type, cfg.pdCfg_), - buttonToDisableWhenChecked_(cfg.buttonToDisableWhenChecked_) - { - assert(contains(labelDoIt, L"&")); - assert(contains(labelDontDoIt, L"&")); - m_buttonAffirmative->SetLabel(labelDoIt); - m_buttonNegative ->SetLabel(labelDontDoIt); - - //m_buttonAffirmative->SetId(wxID_IGNORE); -> setting id after button creation breaks "mouse snap to" functionality - //m_buttonNegative ->SetId(wxID_RETRY); -> also wxWidgets docs seem to hide some info: "Normally, the identifier should be provided on creation and should not be modified subsequently." - - updateGui(); - - //set std order after button visibility was set - setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonAffirmative).setNegative(m_buttonNegative).setCancel(m_buttonCancel)); - setAsStandard(*m_buttonAffirmative); - GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() - Center(); //needs to be re-applied after a dialog size change! - } - -private: - void OnCheckBoxClick(wxCommandEvent& event) override { updateGui(); event.Skip(); } - - void updateGui() - { - switch (buttonToDisableWhenChecked_) - { - case ConfirmationButton3::DO_IT: - m_buttonAffirmative->Enable(!m_checkBoxCustom->GetValue()); - break; - case ConfirmationButton3::DONT_DO_IT: - m_buttonNegative->Enable(!m_checkBoxCustom->GetValue()); - break; - case ConfirmationButton3::CANCEL: - break; - } - } - - const ConfirmationButton3 buttonToDisableWhenChecked_; -}; - -//######################################################################################## - -void zen::showNotificationDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg) -{ - NotificationDialog dlg(parent, type, cfg); - dlg.ShowModal(); -} - - -ConfirmationButton zen::showConfirmationDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg, const wxString& labelDoIt) -{ - ConfirmationDialog dlg(parent, type, cfg, labelDoIt); - return static_cast(dlg.ShowModal()); -} - - -ConfirmationButton3 zen::showConfirmationDialog3(wxWindow* parent, DialogInfoType type, const PopupDialogCfg3& cfg, const wxString& labelDoIt, const wxString& labelDontDoIt) -{ - ConfirmationDialog3 dlg(parent, type, cfg, labelDoIt, labelDontDoIt); - return static_cast(dlg.ShowModal()); -} +// ***************************************************************************** +// * 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 "popup_dlg.h" +#include +#include +#include +#include +#include +#include "popup_dlg_generated.h" + + +using namespace zen; + +namespace +{ +void setAsStandard(wxButton& btn) +{ + btn.SetDefault(); + btn.SetFocus(); +} + + +void setBestInitialSize(wxTextCtrl& ctrl, const wxString& text, wxSize maxSize) +{ + const int scrollbarWidth = 30; + if (maxSize.x <= scrollbarWidth) //implicitly checks for non-zero, too! + return; + maxSize.x -= scrollbarWidth; + + int bestWidth = 0; + int rowCount = 0; + int rowHeight = 0; + + auto evalLineExtent = [&](const wxSize& sz) -> bool //return true when done + { + if (sz.x > bestWidth) + bestWidth = std::min(maxSize.x, sz.x); + + rowCount += (sz.x + maxSize.x - 1) / maxSize.x; //integer round up: consider line-wraps! + rowHeight = std::max(rowHeight, sz.y); //all rows *should* have same height + + return rowCount * rowHeight >= maxSize.y; + }; + + for (auto it = text.begin();;) + { + auto itEnd = std::find(it, text.end(), L'\n'); + wxString line(it, itEnd); + if (line.empty()) + line = L" "; //GetTextExtent() returns (0, 0) for empty strings! + + wxSize sz = ctrl.GetTextExtent(line); //exactly gives row height, but does *not* consider newlines + if (evalLineExtent(sz)) + break; + + if (itEnd == text.end()) + break; + it = itEnd + 1; + } + + const int rowGap = 0; + const wxSize bestSize(bestWidth + scrollbarWidth, std::min(rowCount * (rowHeight + rowGap), maxSize.y)); + ctrl.SetMinSize(bestSize); //alas, SetMinClientSize() is just not working! +} +} + + +class zen::StandardPopupDialog : public PopupDialogGenerated +{ +public: + StandardPopupDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg) : + PopupDialogGenerated(parent), + checkBoxValue_(cfg.checkBoxValue) + { + wxBitmap iconTmp; + wxString titleTmp; + switch (type) + { + case DialogInfoType::INFO: + //"Information" is meaningless as caption text! + //confirmation doesn't use info icon + //iconTmp = getResourceImage(L"msg_info"); + break; + case DialogInfoType::WARNING: + iconTmp = getResourceImage(L"msg_warning"); + titleTmp = _("Warning"); + break; + case DialogInfoType::ERROR2: + iconTmp = getResourceImage(L"msg_error"); + titleTmp = _("Error"); + break; + } + if (cfg.icon.IsOk()) + iconTmp = cfg.icon; + + if (!cfg.title.empty()) + titleTmp = cfg.title; + //----------------------------------------------- + m_bitmapMsgType->SetBitmap(iconTmp); + + if (titleTmp.empty()) + SetTitle(wxTheApp->GetAppDisplayName()); + else + { + if (parent && parent->IsShownOnScreen()) + SetTitle(titleTmp); + else + SetTitle(wxTheApp->GetAppDisplayName() + L" - " + titleTmp); + } + + int maxWidth = 500; + int maxHeight = 400; //try to determine better value based on actual display resolution: + + if (parent) + { + const int disPos = wxDisplay::GetFromWindow(parent); //window must be visible + if (disPos != wxNOT_FOUND) + maxHeight = wxDisplay(disPos).GetClientArea().GetHeight() * 2 / 3; + } + + assert(!cfg.textMain.empty() || !cfg.textDetail.empty()); + if (!cfg.textMain.empty()) + { + setMainInstructionFont(*m_staticTextMain); + m_staticTextMain->SetLabel(cfg.textMain); + m_staticTextMain->Wrap(maxWidth); //call *after* SetLabel() + } + else + m_staticTextMain->Hide(); + + if (!cfg.textDetail.empty()) + { + const wxString& text = L"\n" + trimCpy(cfg.textDetail) + L"\n"; //add empty top/bottom lines *instead* of using border space! + setBestInitialSize(*m_textCtrlTextDetail, text, wxSize(maxWidth, maxHeight)); + m_textCtrlTextDetail->ChangeValue(text); + } + else + m_textCtrlTextDetail->Hide(); + + if (checkBoxValue_) + { + assert(contains(cfg.checkBoxLabel, L"&")); + m_checkBoxCustom->SetLabel(cfg.checkBoxLabel); + m_checkBoxCustom->SetValue(*checkBoxValue_); + } + else + m_checkBoxCustom->Hide(); + + Connect(wxEVT_CHAR_HOOK, wxKeyEventHandler(StandardPopupDialog::OnKeyPressed), nullptr, this); //dialog-specific local key events + } + +private: + void OnClose (wxCloseEvent& event) override { EndModal(static_cast(ConfirmationButton3::CANCEL)); } + void OnCancel(wxCommandEvent& event) override { EndModal(static_cast(ConfirmationButton3::CANCEL)); } + + void OnKeyPressed(wxKeyEvent& event) + { + switch (event.GetKeyCode()) + { + case WXK_RETURN: + case WXK_NUMPAD_ENTER: + { + wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); + OnButtonAffirmative(dummy); + return; + } + + case WXK_ESCAPE: //handle case where cancel button is hidden! + EndModal(static_cast(ConfirmationButton3::CANCEL)); + return; + } + event.Skip(); + } + + void OnButtonAffirmative(wxCommandEvent& event) override + { + if (checkBoxValue_) + *checkBoxValue_ = m_checkBoxCustom->GetValue(); + EndModal(static_cast(ConfirmationButton3::DO_IT)); + } + + void OnButtonNegative(wxCommandEvent& event) override + { + if (checkBoxValue_) + *checkBoxValue_ = m_checkBoxCustom->GetValue(); + EndModal(static_cast(ConfirmationButton3::DONT_DO_IT)); + } + + bool* checkBoxValue_; +}; + + +namespace +{ +class NotificationDialog : public StandardPopupDialog +{ +public: + NotificationDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg) : + StandardPopupDialog(parent, type, cfg) + { + m_buttonAffirmative->SetLabel(_("Close")); //UX Guide: use "Close" for errors, warnings and windows in which users can't make changes (no ampersand!) + m_buttonNegative->Hide(); + m_buttonCancel->Hide(); + + //set std order after button visibility was set + setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonAffirmative)); + setAsStandard(*m_buttonAffirmative); + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() + Center(); //needs to be re-applied after a dialog size change! + } +}; + + +class ConfirmationDialog : public StandardPopupDialog +{ +public: + ConfirmationDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg, const wxString& labelDoIt) : + StandardPopupDialog(parent, type, cfg) + { + assert(contains(labelDoIt, L"&")); + m_buttonAffirmative->SetLabel(labelDoIt); + m_buttonNegative->Hide(); + + //set std order after button visibility was set + setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonAffirmative).setCancel(m_buttonCancel)); + setAsStandard(*m_buttonAffirmative); + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() + Center(); //needs to be re-applied after a dialog size change! + } +}; +} + +class zen::ConfirmationDialog3 : public StandardPopupDialog +{ +public: + ConfirmationDialog3(wxWindow* parent, DialogInfoType type, const PopupDialogCfg3& cfg, const wxString& labelDoIt, const wxString& labelDontDoIt) : + StandardPopupDialog(parent, type, cfg.pdCfg_), + buttonToDisableWhenChecked_(cfg.buttonToDisableWhenChecked_) + { + assert(contains(labelDoIt, L"&")); + assert(contains(labelDontDoIt, L"&")); + m_buttonAffirmative->SetLabel(labelDoIt); + m_buttonNegative ->SetLabel(labelDontDoIt); + + //m_buttonAffirmative->SetId(wxID_IGNORE); -> setting id after button creation breaks "mouse snap to" functionality + //m_buttonNegative ->SetId(wxID_RETRY); -> also wxWidgets docs seem to hide some info: "Normally, the identifier should be provided on creation and should not be modified subsequently." + + updateGui(); + + //set std order after button visibility was set + setStandardButtonLayout(*bSizerStdButtons, StdButtons().setAffirmative(m_buttonAffirmative).setNegative(m_buttonNegative).setCancel(m_buttonCancel)); + setAsStandard(*m_buttonAffirmative); + GetSizer()->SetSizeHints(this); //~=Fit() + SetMinSize() + Center(); //needs to be re-applied after a dialog size change! + } + +private: + void OnCheckBoxClick(wxCommandEvent& event) override { updateGui(); event.Skip(); } + + void updateGui() + { + switch (buttonToDisableWhenChecked_) + { + case ConfirmationButton3::DO_IT: + m_buttonAffirmative->Enable(!m_checkBoxCustom->GetValue()); + break; + case ConfirmationButton3::DONT_DO_IT: + m_buttonNegative->Enable(!m_checkBoxCustom->GetValue()); + break; + case ConfirmationButton3::CANCEL: + break; + } + } + + const ConfirmationButton3 buttonToDisableWhenChecked_; +}; + +//######################################################################################## + +void zen::showNotificationDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg) +{ + NotificationDialog dlg(parent, type, cfg); + dlg.ShowModal(); +} + + +ConfirmationButton zen::showConfirmationDialog(wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg, const wxString& labelDoIt) +{ + ConfirmationDialog dlg(parent, type, cfg, labelDoIt); + return static_cast(dlg.ShowModal()); +} + + +ConfirmationButton3 zen::showConfirmationDialog3(wxWindow* parent, DialogInfoType type, const PopupDialogCfg3& cfg, const wxString& labelDoIt, const wxString& labelDontDoIt) +{ + ConfirmationDialog3 dlg(parent, type, cfg, labelDoIt, labelDontDoIt); + return static_cast(dlg.ShowModal()); +} diff --git a/wx+/popup_dlg.h b/wx+/popup_dlg.h index 2f0adeff..cc6ffee8 100755 --- a/wx+/popup_dlg.h +++ b/wx+/popup_dlg.h @@ -1,94 +1,94 @@ -// ***************************************************************************** -// * 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 * -// ***************************************************************************** - -#ifndef POPUP_DLG_H_820780154723456 -#define POPUP_DLG_H_820780154723456 - -#include -#include -#include - - -namespace zen -{ -//parent window, optional: support correct dialog placement above parent on multiple monitor systems -//this module requires error, warning and info image files in resources.zip, see - -struct PopupDialogCfg; -struct PopupDialogCfg3; - -enum class DialogInfoType -{ - INFO, - WARNING, - ERROR2, //fuck the ERROR macro in WinGDI.h! -}; - -enum class ConfirmationButton3 -{ - DO_IT, - DONT_DO_IT, - CANCEL -}; - -enum class ConfirmationButton -{ - DO_IT = static_cast(ConfirmationButton3::DO_IT ), //[!] - CANCEL = static_cast(ConfirmationButton3::CANCEL), //Clang requires a "static_cast" -}; - -void showNotificationDialog (wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg); -ConfirmationButton showConfirmationDialog (wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg, const wxString& labelDoIt); -ConfirmationButton3 showConfirmationDialog3(wxWindow* parent, DialogInfoType type, const PopupDialogCfg3& cfg, const wxString& labelDoIt, const wxString& labelDontDoIt); - -//---------------------------------------------------------------------------------------------------------------- -class StandardPopupDialog; -class ConfirmationDialog3; - -struct PopupDialogCfg -{ - PopupDialogCfg& setIcon (const wxBitmap& bmp ) { icon = bmp; return *this; } - PopupDialogCfg& setTitle (const wxString& label) { title = label; return *this; } - PopupDialogCfg& setMainInstructions (const wxString& label) { textMain = label; return *this; } //set at least one of these! - PopupDialogCfg& setDetailInstructions(const wxString& label) { textDetail = label; return *this; } // - PopupDialogCfg& setCheckBox(bool& value, const wxString& label) { checkBoxValue = &value; checkBoxLabel = label; return *this; } - -private: - friend class StandardPopupDialog; - - wxBitmap icon; - wxString title; - wxString textMain; - wxString textDetail; - bool* checkBoxValue = nullptr; //in/out - wxString checkBoxLabel; -}; - - -struct PopupDialogCfg3 -{ - PopupDialogCfg3& setIcon (const wxBitmap& bmp ) { pdCfg_.setIcon (bmp); return *this; } - PopupDialogCfg3& setTitle (const wxString& label) { pdCfg_.setTitle (label); return *this; } - PopupDialogCfg3& setMainInstructions (const wxString& label) { pdCfg_.setMainInstructions (label); return *this; } //set at least one of these! - PopupDialogCfg3& setDetailInstructions(const wxString& label) { pdCfg_.setDetailInstructions(label); return *this; } // - PopupDialogCfg3& setCheckBox(bool& value, const wxString& label) { pdCfg_.setCheckBox(value, label); return *this; } - PopupDialogCfg3& setCheckBox(bool& value, const wxString& label, ConfirmationButton3 disableWhenChecked) - { - assert(disableWhenChecked != ConfirmationButton3::CANCEL); - setCheckBox(value, label); - buttonToDisableWhenChecked_ = disableWhenChecked; - return *this; - } - -private: - friend class ConfirmationDialog3; - - PopupDialogCfg pdCfg_; - ConfirmationButton3 buttonToDisableWhenChecked_ = ConfirmationButton3::CANCEL; -}; -} - -#endif //POPUP_DLG_H_820780154723456 +// ***************************************************************************** +// * 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 * +// ***************************************************************************** + +#ifndef POPUP_DLG_H_820780154723456 +#define POPUP_DLG_H_820780154723456 + +#include +#include +#include + + +namespace zen +{ +//parent window, optional: support correct dialog placement above parent on multiple monitor systems +//this module requires error, warning and info image files in resources.zip, see + +struct PopupDialogCfg; +struct PopupDialogCfg3; + +enum class DialogInfoType +{ + INFO, + WARNING, + ERROR2, //fuck the ERROR macro in WinGDI.h! +}; + +enum class ConfirmationButton3 +{ + DO_IT, + DONT_DO_IT, + CANCEL +}; + +enum class ConfirmationButton +{ + DO_IT = static_cast(ConfirmationButton3::DO_IT ), //[!] + CANCEL = static_cast(ConfirmationButton3::CANCEL), //Clang requires a "static_cast" +}; + +void showNotificationDialog (wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg); +ConfirmationButton showConfirmationDialog (wxWindow* parent, DialogInfoType type, const PopupDialogCfg& cfg, const wxString& labelDoIt); +ConfirmationButton3 showConfirmationDialog3(wxWindow* parent, DialogInfoType type, const PopupDialogCfg3& cfg, const wxString& labelDoIt, const wxString& labelDontDoIt); + +//---------------------------------------------------------------------------------------------------------------- +class StandardPopupDialog; +class ConfirmationDialog3; + +struct PopupDialogCfg +{ + PopupDialogCfg& setIcon (const wxBitmap& bmp ) { icon = bmp; return *this; } + PopupDialogCfg& setTitle (const wxString& label) { title = label; return *this; } + PopupDialogCfg& setMainInstructions (const wxString& label) { textMain = label; return *this; } //set at least one of these! + PopupDialogCfg& setDetailInstructions(const wxString& label) { textDetail = label; return *this; } // + PopupDialogCfg& setCheckBox(bool& value, const wxString& label) { checkBoxValue = &value; checkBoxLabel = label; return *this; } + +private: + friend class StandardPopupDialog; + + wxBitmap icon; + wxString title; + wxString textMain; + wxString textDetail; + bool* checkBoxValue = nullptr; //in/out + wxString checkBoxLabel; +}; + + +struct PopupDialogCfg3 +{ + PopupDialogCfg3& setIcon (const wxBitmap& bmp ) { pdCfg_.setIcon (bmp); return *this; } + PopupDialogCfg3& setTitle (const wxString& label) { pdCfg_.setTitle (label); return *this; } + PopupDialogCfg3& setMainInstructions (const wxString& label) { pdCfg_.setMainInstructions (label); return *this; } //set at least one of these! + PopupDialogCfg3& setDetailInstructions(const wxString& label) { pdCfg_.setDetailInstructions(label); return *this; } // + PopupDialogCfg3& setCheckBox(bool& value, const wxString& label) { pdCfg_.setCheckBox(value, label); return *this; } + PopupDialogCfg3& setCheckBox(bool& value, const wxString& label, ConfirmationButton3 disableWhenChecked) + { + assert(disableWhenChecked != ConfirmationButton3::CANCEL); + setCheckBox(value, label); + buttonToDisableWhenChecked_ = disableWhenChecked; + return *this; + } + +private: + friend class ConfirmationDialog3; + + PopupDialogCfg pdCfg_; + ConfirmationButton3 buttonToDisableWhenChecked_ = ConfirmationButton3::CANCEL; +}; +} + +#endif //POPUP_DLG_H_820780154723456 diff --git a/wx+/popup_dlg_generated.cpp b/wx+/popup_dlg_generated.cpp index 3b47573e..a79ffa52 100755 --- a/wx+/popup_dlg_generated.cpp +++ b/wx+/popup_dlg_generated.cpp @@ -1,97 +1,97 @@ -/////////////////////////////////////////////////////////////////////////// -// C++ code generated with wxFormBuilder (version Jun 17 2015) -// http://www.wxformbuilder.org/ -// -// PLEASE DO "NOT" EDIT THIS FILE! -/////////////////////////////////////////////////////////////////////////// - -#include "popup_dlg_generated.h" - -/////////////////////////////////////////////////////////////////////////// - -PopupDialogGenerated::PopupDialogGenerated( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxDialog( parent, id, title, pos, size, style ) -{ - this->SetSizeHints( wxSize( -1, -1 ), wxDefaultSize ); - this->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); - - wxBoxSizer* bSizer24; - bSizer24 = new wxBoxSizer( wxVERTICAL ); - - m_panel33 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); - m_panel33->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); - - wxBoxSizer* bSizer165; - bSizer165 = new wxBoxSizer( wxHORIZONTAL ); - - m_bitmapMsgType = new wxStaticBitmap( m_panel33, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), 0 ); - bSizer165->Add( m_bitmapMsgType, 0, wxALL, 10 ); - - wxBoxSizer* bSizer16; - bSizer16 = new wxBoxSizer( wxVERTICAL ); - - - bSizer16->Add( 0, 10, 0, 0, 5 ); - - m_staticTextMain = new wxStaticText( m_panel33, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); - m_staticTextMain->Wrap( -1 ); - bSizer16->Add( m_staticTextMain, 0, wxRIGHT, 10 ); - - - bSizer16->Add( 0, 5, 0, 0, 5 ); - - m_textCtrlTextDetail = new wxTextCtrl( m_panel33, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE|wxTE_READONLY|wxNO_BORDER ); - bSizer16->Add( m_textCtrlTextDetail, 1, wxEXPAND, 5 ); - - - bSizer165->Add( bSizer16, 1, wxEXPAND, 5 ); - - - m_panel33->SetSizer( bSizer165 ); - m_panel33->Layout(); - bSizer165->Fit( m_panel33 ); - bSizer24->Add( m_panel33, 1, wxEXPAND, 5 ); - - m_staticline6 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); - bSizer24->Add( m_staticline6, 0, wxEXPAND, 5 ); - - wxBoxSizer* bSizer25; - bSizer25 = new wxBoxSizer( wxVERTICAL ); - - m_checkBoxCustom = new wxCheckBox( this, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); - bSizer25->Add( m_checkBoxCustom, 0, wxALIGN_CENTER_HORIZONTAL|wxALL, 5 ); - - bSizerStdButtons = new wxBoxSizer( wxHORIZONTAL ); - - m_buttonAffirmative = new wxButton( this, wxID_YES, _("dummy"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); - bSizerStdButtons->Add( m_buttonAffirmative, 0, wxALIGN_CENTER_VERTICAL|wxBOTTOM|wxRIGHT|wxLEFT, 5 ); - - m_buttonNegative = new wxButton( this, wxID_NO, _("dummy"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); - bSizerStdButtons->Add( m_buttonNegative, 0, wxALIGN_CENTER_VERTICAL|wxBOTTOM|wxRIGHT, 5 ); - - m_buttonCancel = new wxButton( this, wxID_CANCEL, _("Cancel"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); - bSizerStdButtons->Add( m_buttonCancel, 0, wxALIGN_CENTER_VERTICAL|wxBOTTOM|wxRIGHT, 5 ); - - - bSizer25->Add( bSizerStdButtons, 0, wxALIGN_RIGHT, 5 ); - - - bSizer24->Add( bSizer25, 0, wxEXPAND, 5 ); - - - this->SetSizer( bSizer24 ); - this->Layout(); - bSizer24->Fit( this ); - - this->Centre( wxBOTH ); - - // Connect Events - this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( PopupDialogGenerated::OnClose ) ); - m_checkBoxCustom->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( PopupDialogGenerated::OnCheckBoxClick ), NULL, this ); - m_buttonAffirmative->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PopupDialogGenerated::OnButtonAffirmative ), NULL, this ); - m_buttonNegative->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PopupDialogGenerated::OnButtonNegative ), NULL, this ); - m_buttonCancel->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PopupDialogGenerated::OnCancel ), NULL, this ); -} - -PopupDialogGenerated::~PopupDialogGenerated() -{ -} +/////////////////////////////////////////////////////////////////////////// +// C++ code generated with wxFormBuilder (version Jun 17 2015) +// http://www.wxformbuilder.org/ +// +// PLEASE DO "NOT" EDIT THIS FILE! +/////////////////////////////////////////////////////////////////////////// + +#include "popup_dlg_generated.h" + +/////////////////////////////////////////////////////////////////////////// + +PopupDialogGenerated::PopupDialogGenerated( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxDialog( parent, id, title, pos, size, style ) +{ + this->SetSizeHints( wxSize( -1, -1 ), wxDefaultSize ); + this->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + + wxBoxSizer* bSizer24; + bSizer24 = new wxBoxSizer( wxVERTICAL ); + + m_panel33 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panel33->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer165; + bSizer165 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapMsgType = new wxStaticBitmap( m_panel33, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizer165->Add( m_bitmapMsgType, 0, wxALL, 10 ); + + wxBoxSizer* bSizer16; + bSizer16 = new wxBoxSizer( wxVERTICAL ); + + + bSizer16->Add( 0, 10, 0, 0, 5 ); + + m_staticTextMain = new wxStaticText( m_panel33, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextMain->Wrap( -1 ); + bSizer16->Add( m_staticTextMain, 0, wxRIGHT, 10 ); + + + bSizer16->Add( 0, 5, 0, 0, 5 ); + + m_textCtrlTextDetail = new wxTextCtrl( m_panel33, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE|wxTE_READONLY|wxNO_BORDER ); + bSizer16->Add( m_textCtrlTextDetail, 1, wxEXPAND, 5 ); + + + bSizer165->Add( bSizer16, 1, wxEXPAND, 5 ); + + + m_panel33->SetSizer( bSizer165 ); + m_panel33->Layout(); + bSizer165->Fit( m_panel33 ); + bSizer24->Add( m_panel33, 1, wxEXPAND, 5 ); + + m_staticline6 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer24->Add( m_staticline6, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer25; + bSizer25 = new wxBoxSizer( wxVERTICAL ); + + m_checkBoxCustom = new wxCheckBox( this, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); + bSizer25->Add( m_checkBoxCustom, 0, wxALIGN_CENTER_HORIZONTAL|wxALL, 5 ); + + bSizerStdButtons = new wxBoxSizer( wxHORIZONTAL ); + + m_buttonAffirmative = new wxButton( this, wxID_YES, _("dummy"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizerStdButtons->Add( m_buttonAffirmative, 0, wxALIGN_CENTER_VERTICAL|wxBOTTOM|wxRIGHT|wxLEFT, 5 ); + + m_buttonNegative = new wxButton( this, wxID_NO, _("dummy"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizerStdButtons->Add( m_buttonNegative, 0, wxALIGN_CENTER_VERTICAL|wxBOTTOM|wxRIGHT, 5 ); + + m_buttonCancel = new wxButton( this, wxID_CANCEL, _("Cancel"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + bSizerStdButtons->Add( m_buttonCancel, 0, wxALIGN_CENTER_VERTICAL|wxBOTTOM|wxRIGHT, 5 ); + + + bSizer25->Add( bSizerStdButtons, 0, wxALIGN_RIGHT, 5 ); + + + bSizer24->Add( bSizer25, 0, wxEXPAND, 5 ); + + + this->SetSizer( bSizer24 ); + this->Layout(); + bSizer24->Fit( this ); + + this->Centre( wxBOTH ); + + // Connect Events + this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( PopupDialogGenerated::OnClose ) ); + m_checkBoxCustom->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( PopupDialogGenerated::OnCheckBoxClick ), NULL, this ); + m_buttonAffirmative->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PopupDialogGenerated::OnButtonAffirmative ), NULL, this ); + m_buttonNegative->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PopupDialogGenerated::OnButtonNegative ), NULL, this ); + m_buttonCancel->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( PopupDialogGenerated::OnCancel ), NULL, this ); +} + +PopupDialogGenerated::~PopupDialogGenerated() +{ +} diff --git a/wx+/popup_dlg_generated.h b/wx+/popup_dlg_generated.h index 64e68c08..bef871c7 100755 --- a/wx+/popup_dlg_generated.h +++ b/wx+/popup_dlg_generated.h @@ -1,71 +1,71 @@ -/////////////////////////////////////////////////////////////////////////// -// C++ code generated with wxFormBuilder (version Jun 17 2015) -// http://www.wxformbuilder.org/ -// -// PLEASE DO "NOT" EDIT THIS FILE! -/////////////////////////////////////////////////////////////////////////// - -#ifndef __POPUP_DLG_GENERATED_H__ -#define __POPUP_DLG_GENERATED_H__ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "zen/i18n.h" - -/////////////////////////////////////////////////////////////////////////// - - -/////////////////////////////////////////////////////////////////////////////// -/// Class PopupDialogGenerated -/////////////////////////////////////////////////////////////////////////////// -class PopupDialogGenerated : public wxDialog -{ -private: - -protected: - wxPanel* m_panel33; - wxStaticBitmap* m_bitmapMsgType; - wxStaticText* m_staticTextMain; - wxTextCtrl* m_textCtrlTextDetail; - wxStaticLine* m_staticline6; - wxCheckBox* m_checkBoxCustom; - wxBoxSizer* bSizerStdButtons; - wxButton* m_buttonAffirmative; - wxButton* m_buttonNegative; - wxButton* m_buttonCancel; - - // Virtual event handlers, overide them in your derived class - virtual void OnClose( wxCloseEvent& event ) { event.Skip(); } - virtual void OnCheckBoxClick( wxCommandEvent& event ) { event.Skip(); } - virtual void OnButtonAffirmative( wxCommandEvent& event ) { event.Skip(); } - virtual void OnButtonNegative( wxCommandEvent& event ) { event.Skip(); } - virtual void OnCancel( wxCommandEvent& event ) { event.Skip(); } - - -public: - - PopupDialogGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = _("dummy"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER ); - ~PopupDialogGenerated(); - -}; - -#endif //__POPUP_DLG_GENERATED_H__ +/////////////////////////////////////////////////////////////////////////// +// C++ code generated with wxFormBuilder (version Jun 17 2015) +// http://www.wxformbuilder.org/ +// +// PLEASE DO "NOT" EDIT THIS FILE! +/////////////////////////////////////////////////////////////////////////// + +#ifndef __POPUP_DLG_GENERATED_H__ +#define __POPUP_DLG_GENERATED_H__ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "zen/i18n.h" + +/////////////////////////////////////////////////////////////////////////// + + +/////////////////////////////////////////////////////////////////////////////// +/// Class PopupDialogGenerated +/////////////////////////////////////////////////////////////////////////////// +class PopupDialogGenerated : public wxDialog +{ +private: + +protected: + wxPanel* m_panel33; + wxStaticBitmap* m_bitmapMsgType; + wxStaticText* m_staticTextMain; + wxTextCtrl* m_textCtrlTextDetail; + wxStaticLine* m_staticline6; + wxCheckBox* m_checkBoxCustom; + wxBoxSizer* bSizerStdButtons; + wxButton* m_buttonAffirmative; + wxButton* m_buttonNegative; + wxButton* m_buttonCancel; + + // Virtual event handlers, overide them in your derived class + virtual void OnClose( wxCloseEvent& event ) { event.Skip(); } + virtual void OnCheckBoxClick( wxCommandEvent& event ) { event.Skip(); } + virtual void OnButtonAffirmative( wxCommandEvent& event ) { event.Skip(); } + virtual void OnButtonNegative( wxCommandEvent& event ) { event.Skip(); } + virtual void OnCancel( wxCommandEvent& event ) { event.Skip(); } + + +public: + + PopupDialogGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = _("dummy"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER ); + ~PopupDialogGenerated(); + +}; + +#endif //__POPUP_DLG_GENERATED_H__ diff --git a/wx+/rtl.h b/wx+/rtl.h index f516ca29..206e8e6f 100755 --- a/wx+/rtl.h +++ b/wx+/rtl.h @@ -1,95 +1,95 @@ -// ***************************************************************************** -// * 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 * -// ***************************************************************************** - -#ifndef RTL_H_0183487180058718273432148 -#define RTL_H_0183487180058718273432148 - -#include -#include -#include -#include - -namespace zen -{ -//functions supporting right-to-left GUI layout -void drawBitmapRtlMirror (wxDC& dc, const wxBitmap& image, const wxRect& rect, int alignment, Opt& buffer); -void drawBitmapRtlNoMirror(wxDC& dc, const wxBitmap& image, const wxRect& rect, int alignment); -//wxDC::DrawIcon DOES mirror by default -> implement RTL support when needed - -wxBitmap mirrorIfRtl(const wxBitmap& bmp); - -//manual text flow correction: http://www.w3.org/International/articles/inline-bidi-markup/ - - - - - - - - -//---------------------- implementation ------------------------ -namespace impl -{ -//don't use wxDC::DrawLabel: it results in expensive GetTextExtent() call even when passing an empty string!!! -//also avoid wxDC::DrawLabel 1-off alignment bugs -inline -void drawBitmapAligned(wxDC& dc, const wxBitmap& image, const wxRect& rect, int alignment) -{ - wxPoint pt = rect.GetTopLeft(); - if (alignment & wxALIGN_RIGHT) //note: wxALIGN_LEFT == 0! - pt.x += rect.width - image.GetWidth(); - else if (alignment & wxALIGN_CENTER_HORIZONTAL) - pt.x += (rect.width - image.GetWidth()) / 2; - - if (alignment & wxALIGN_BOTTOM) //note: wxALIGN_TOP == 0! - pt.y += rect.height - image.GetHeight(); - else if (alignment & wxALIGN_CENTER_VERTICAL) - pt.y += (rect.height - image.GetHeight()) / 2; - - dc.DrawBitmap(image, pt); -} -} - - -inline -void drawBitmapRtlMirror(wxDC& dc, const wxBitmap& image, const wxRect& rect, int alignment, Opt& buffer) -{ - if (dc.GetLayoutDirection() == wxLayout_RightToLeft) - { - if (!buffer || buffer->GetWidth() != rect.width || buffer->GetHeight() < rect.height) //[!] since we do a mirror, width needs to match exactly! - buffer = wxBitmap(rect.width, rect.height); - - wxMemoryDC memDc(*buffer); - memDc.Blit(wxPoint(0, 0), rect.GetSize(), &dc, rect.GetTopLeft()); //blit in: background is mirrored due to memDc, dc having different layout direction! - - impl::drawBitmapAligned(memDc, image, wxRect(0, 0, rect.width, rect.height), alignment); - //note: we cannot simply use memDc.SetLayoutDirection(wxLayout_RightToLeft) due to some strange 1 pixel bug! - - dc.Blit(rect.GetTopLeft(), rect.GetSize(), &memDc, wxPoint(0, 0)); //blit out: mirror once again - } - else - impl::drawBitmapAligned(dc, image, rect, alignment); -} - - -inline -void drawBitmapRtlNoMirror(wxDC& dc, const wxBitmap& image, const wxRect& rect, int alignment) -{ - return impl::drawBitmapAligned(dc, image, rect, alignment); //wxDC::DrawBitmap does NOT mirror by default -} - - -inline -wxBitmap mirrorIfRtl(const wxBitmap& bmp) -{ - if (wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft) - return bmp.ConvertToImage().Mirror(); - else - return bmp; -} -} - -#endif //RTL_H_0183487180058718273432148 +// ***************************************************************************** +// * 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 * +// ***************************************************************************** + +#ifndef RTL_H_0183487180058718273432148 +#define RTL_H_0183487180058718273432148 + +#include +#include +#include +#include + +namespace zen +{ +//functions supporting right-to-left GUI layout +void drawBitmapRtlMirror (wxDC& dc, const wxBitmap& image, const wxRect& rect, int alignment, Opt& buffer); +void drawBitmapRtlNoMirror(wxDC& dc, const wxBitmap& image, const wxRect& rect, int alignment); +//wxDC::DrawIcon DOES mirror by default -> implement RTL support when needed + +wxBitmap mirrorIfRtl(const wxBitmap& bmp); + +//manual text flow correction: http://www.w3.org/International/articles/inline-bidi-markup/ + + + + + + + + +//---------------------- implementation ------------------------ +namespace impl +{ +//don't use wxDC::DrawLabel: it results in expensive GetTextExtent() call even when passing an empty string!!! +//also avoid wxDC::DrawLabel 1-off alignment bugs +inline +void drawBitmapAligned(wxDC& dc, const wxBitmap& image, const wxRect& rect, int alignment) +{ + wxPoint pt = rect.GetTopLeft(); + if (alignment & wxALIGN_RIGHT) //note: wxALIGN_LEFT == 0! + pt.x += rect.width - image.GetWidth(); + else if (alignment & wxALIGN_CENTER_HORIZONTAL) + pt.x += (rect.width - image.GetWidth()) / 2; + + if (alignment & wxALIGN_BOTTOM) //note: wxALIGN_TOP == 0! + pt.y += rect.height - image.GetHeight(); + else if (alignment & wxALIGN_CENTER_VERTICAL) + pt.y += (rect.height - image.GetHeight()) / 2; + + dc.DrawBitmap(image, pt); +} +} + + +inline +void drawBitmapRtlMirror(wxDC& dc, const wxBitmap& image, const wxRect& rect, int alignment, Opt& buffer) +{ + if (dc.GetLayoutDirection() == wxLayout_RightToLeft) + { + if (!buffer || buffer->GetWidth() != rect.width || buffer->GetHeight() < rect.height) //[!] since we do a mirror, width needs to match exactly! + buffer = wxBitmap(rect.width, rect.height); + + wxMemoryDC memDc(*buffer); + memDc.Blit(wxPoint(0, 0), rect.GetSize(), &dc, rect.GetTopLeft()); //blit in: background is mirrored due to memDc, dc having different layout direction! + + impl::drawBitmapAligned(memDc, image, wxRect(0, 0, rect.width, rect.height), alignment); + //note: we cannot simply use memDc.SetLayoutDirection(wxLayout_RightToLeft) due to some strange 1 pixel bug! + + dc.Blit(rect.GetTopLeft(), rect.GetSize(), &memDc, wxPoint(0, 0)); //blit out: mirror once again + } + else + impl::drawBitmapAligned(dc, image, rect, alignment); +} + + +inline +void drawBitmapRtlNoMirror(wxDC& dc, const wxBitmap& image, const wxRect& rect, int alignment) +{ + return impl::drawBitmapAligned(dc, image, rect, alignment); //wxDC::DrawBitmap does NOT mirror by default +} + + +inline +wxBitmap mirrorIfRtl(const wxBitmap& bmp) +{ + if (wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft) + return bmp.ConvertToImage().Mirror(); + else + return bmp; +} +} + +#endif //RTL_H_0183487180058718273432148 diff --git a/wx+/std_button_layout.h b/wx+/std_button_layout.h index ae9e9e1c..47eea3c6 100755 --- a/wx+/std_button_layout.h +++ b/wx+/std_button_layout.h @@ -1,117 +1,117 @@ -// ***************************************************************************** -// * 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 * -// ***************************************************************************** - -#ifndef STD_BUTTON_LAYOUT_H_183470321478317214 -#define STD_BUTTON_LAYOUT_H_183470321478317214 - -#include -#include -#include - - -namespace zen -{ -struct StdButtons -{ - StdButtons& setAffirmative (wxButton* btn) { btnYes = btn; return *this; } - StdButtons& setNegative (wxButton* btn) { btnNo = btn; return *this; } - StdButtons& setCancel (wxButton* btn) { btnCancel = btn; return *this; } - - wxButton* btnYes = nullptr; - wxButton* btnNo = nullptr; - wxButton* btnCancel = nullptr; -}; - -void setStandardButtonLayout(wxBoxSizer& sizer, const StdButtons& buttons = StdButtons()); -//sizer width will change! => call wxWindow::Fit and wxWindow::Layout - - - - - - - - - - - -//--------------- impelementation ------------------------------------------- -inline -void setStandardButtonLayout(wxBoxSizer& sizer, const StdButtons& buttons) -{ - assert(sizer.GetOrientation() == wxHORIZONTAL); - - StdButtons buttonsTmp = buttons; - - auto detach = [&](wxButton*& btn) - { - if (btn) - { - assert(btn->GetContainingSizer() == &sizer); - if (btn->IsShown()) - { - bool rv = sizer.Detach(btn); - assert(rv); - if (!rv) - btn = nullptr; - } - else - btn = nullptr; - } - }; - - detach(buttonsTmp.btnYes); - detach(buttonsTmp.btnNo); - detach(buttonsTmp.btnCancel); - - //GNOME Human Interface Guidelines: https://developer.gnome.org/hig-book/3.2/hig-book.html#alert-spacing - const int spaceH = 6; //OK - const int spaceRimH = 12; //OK - const int spaceRimV = 12; //OK - - bool settingFirstButton = true; - auto attach = [&](wxButton* btn) - { - if (btn) - { - assert(btn->GetMinSize().GetHeight() == -1); //let OS or this routine do the sizing! note: OS X does not allow changing the (visible!) button height! - const int defaultHeight = wxButton::GetDefaultSize().GetHeight(); //buffered by wxWidgets - btn->SetMinSize(wxSize(-1, std::max(defaultHeight, 30))); //default button height is much too small => increase! - - if (settingFirstButton) - settingFirstButton = false; - else - sizer.Add(spaceH, 0); - sizer.Add(btn, 0, wxTOP | wxBOTTOM | wxALIGN_CENTER_VERTICAL, spaceRimV); - } - }; - - //set border on left considering existing items - if (sizer.GetChildren().GetCount() > 0) //for yet another retarded reason wxWidgets will have wxSizer::GetItem(0) cause an assert rather than just return nullptr as documented - if (wxSizerItem* item = sizer.GetItem(static_cast(0))) - { - assert(item->GetBorder() <= spaceRimV); //pragmatic check: other controls in the sizer should not have a larger border - int flag = item->GetFlag(); - if (flag & wxLEFT) - { - flag &= ~wxLEFT; - item->SetFlag(flag); - } - sizer.Insert(static_cast(0), spaceRimH, 0); - } - - sizer.Add(spaceRimH, 0); - attach(buttonsTmp.btnNo); - attach(buttonsTmp.btnCancel); - attach(buttonsTmp.btnYes); - - sizer.Add(spaceRimH, 0); - - assert(buttonsTmp.btnCancel || buttonsTmp.btnYes); //OS X: there should be at least one button following the gap after the "dangerous" no-button -} -} - -#endif //STD_BUTTON_LAYOUT_H_183470321478317214 +// ***************************************************************************** +// * 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 * +// ***************************************************************************** + +#ifndef STD_BUTTON_LAYOUT_H_183470321478317214 +#define STD_BUTTON_LAYOUT_H_183470321478317214 + +#include +#include +#include + + +namespace zen +{ +struct StdButtons +{ + StdButtons& setAffirmative (wxButton* btn) { btnYes = btn; return *this; } + StdButtons& setNegative (wxButton* btn) { btnNo = btn; return *this; } + StdButtons& setCancel (wxButton* btn) { btnCancel = btn; return *this; } + + wxButton* btnYes = nullptr; + wxButton* btnNo = nullptr; + wxButton* btnCancel = nullptr; +}; + +void setStandardButtonLayout(wxBoxSizer& sizer, const StdButtons& buttons = StdButtons()); +//sizer width will change! => call wxWindow::Fit and wxWindow::Layout + + + + + + + + + + + +//--------------- impelementation ------------------------------------------- +inline +void setStandardButtonLayout(wxBoxSizer& sizer, const StdButtons& buttons) +{ + assert(sizer.GetOrientation() == wxHORIZONTAL); + + StdButtons buttonsTmp = buttons; + + auto detach = [&](wxButton*& btn) + { + if (btn) + { + assert(btn->GetContainingSizer() == &sizer); + if (btn->IsShown()) + { + bool rv = sizer.Detach(btn); + assert(rv); + if (!rv) + btn = nullptr; + } + else + btn = nullptr; + } + }; + + detach(buttonsTmp.btnYes); + detach(buttonsTmp.btnNo); + detach(buttonsTmp.btnCancel); + + //GNOME Human Interface Guidelines: https://developer.gnome.org/hig-book/3.2/hig-book.html#alert-spacing + const int spaceH = 6; //OK + const int spaceRimH = 12; //OK + const int spaceRimV = 12; //OK + + bool settingFirstButton = true; + auto attach = [&](wxButton* btn) + { + if (btn) + { + assert(btn->GetMinSize().GetHeight() == -1); //let OS or this routine do the sizing! note: OS X does not allow changing the (visible!) button height! + const int defaultHeight = wxButton::GetDefaultSize().GetHeight(); //buffered by wxWidgets + btn->SetMinSize(wxSize(-1, std::max(defaultHeight, 30))); //default button height is much too small => increase! + + if (settingFirstButton) + settingFirstButton = false; + else + sizer.Add(spaceH, 0); + sizer.Add(btn, 0, wxTOP | wxBOTTOM | wxALIGN_CENTER_VERTICAL, spaceRimV); + } + }; + + //set border on left considering existing items + if (sizer.GetChildren().GetCount() > 0) //for yet another retarded reason wxWidgets will have wxSizer::GetItem(0) cause an assert rather than just return nullptr as documented + if (wxSizerItem* item = sizer.GetItem(static_cast(0))) + { + assert(item->GetBorder() <= spaceRimV); //pragmatic check: other controls in the sizer should not have a larger border + int flag = item->GetFlag(); + if (flag & wxLEFT) + { + flag &= ~wxLEFT; + item->SetFlag(flag); + } + sizer.Insert(static_cast(0), spaceRimH, 0); + } + + sizer.Add(spaceRimH, 0); + attach(buttonsTmp.btnNo); + attach(buttonsTmp.btnCancel); + attach(buttonsTmp.btnYes); + + sizer.Add(spaceRimH, 0); + + assert(buttonsTmp.btnCancel || buttonsTmp.btnYes); //OS X: there should be at least one button following the gap after the "dangerous" no-button +} +} + +#endif //STD_BUTTON_LAYOUT_H_183470321478317214 diff --git a/wx+/toggle_button.h b/wx+/toggle_button.h index 5457d986..9c3cb3f1 100755 --- a/wx+/toggle_button.h +++ b/wx+/toggle_button.h @@ -1,67 +1,67 @@ -// ***************************************************************************** -// * 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 * -// ***************************************************************************** - -#ifndef TOGGLE_BUTTON_H_8173024810574556 -#define TOGGLE_BUTTON_H_8173024810574556 - -#include -#include - -class ToggleButton : public wxBitmapButton -{ -public: - ToggleButton(wxWindow* parent, - wxWindowID id, - const wxBitmap& bitmap, - const wxPoint& pos = wxDefaultPosition, - const wxSize& size = wxDefaultSize, - long style = 0, - const wxValidator& validator = wxDefaultValidator, - const wxString& name = wxButtonNameStr) : wxBitmapButton(parent, id, bitmap, pos, size, style, validator, name) - { - SetLayoutDirection(wxLayout_LeftToRight); //avoid mirroring RTL languages like Hebrew or Arabic - } - - void init(const wxBitmap& activeBmp, - const wxBitmap& inactiveBmp); - - void setActive(bool value); - bool isActive() const { return active; } - void toggle() { setActive(!active); } - -private: - bool active = false; - - wxBitmap activeBmp_; - wxBitmap inactiveBmp_; -}; - - - - - - - -//######################## implementation ######################## -inline -void ToggleButton::init(const wxBitmap& activeBmp, - const wxBitmap& inactiveBmp) -{ - activeBmp_ = activeBmp; - inactiveBmp_ = inactiveBmp; - - setActive(active); -} - - -inline -void ToggleButton::setActive(bool value) -{ - active = value; - zen::setImage(*this, active ? activeBmp_ : inactiveBmp_); -} - -#endif //TOGGLE_BUTTON_H_8173024810574556 +// ***************************************************************************** +// * 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 * +// ***************************************************************************** + +#ifndef TOGGLE_BUTTON_H_8173024810574556 +#define TOGGLE_BUTTON_H_8173024810574556 + +#include +#include + +class ToggleButton : public wxBitmapButton +{ +public: + ToggleButton(wxWindow* parent, + wxWindowID id, + const wxBitmap& bitmap, + const wxPoint& pos = wxDefaultPosition, + const wxSize& size = wxDefaultSize, + long style = 0, + const wxValidator& validator = wxDefaultValidator, + const wxString& name = wxButtonNameStr) : wxBitmapButton(parent, id, bitmap, pos, size, style, validator, name) + { + SetLayoutDirection(wxLayout_LeftToRight); //avoid mirroring RTL languages like Hebrew or Arabic + } + + void init(const wxBitmap& activeBmp, + const wxBitmap& inactiveBmp); + + void setActive(bool value); + bool isActive() const { return active; } + void toggle() { setActive(!active); } + +private: + bool active = false; + + wxBitmap activeBmp_; + wxBitmap inactiveBmp_; +}; + + + + + + + +//######################## implementation ######################## +inline +void ToggleButton::init(const wxBitmap& activeBmp, + const wxBitmap& inactiveBmp) +{ + activeBmp_ = activeBmp; + inactiveBmp_ = inactiveBmp; + + setActive(active); +} + + +inline +void ToggleButton::setActive(bool value) +{ + active = value; + zen::setImage(*this, active ? activeBmp_ : inactiveBmp_); +} + +#endif //TOGGLE_BUTTON_H_8173024810574556 diff --git a/wx+/tooltip.cpp b/wx+/tooltip.cpp index b2946187..053b0714 100755 --- a/wx+/tooltip.cpp +++ b/wx+/tooltip.cpp @@ -1,97 +1,97 @@ -// ***************************************************************************** -// * 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 "tooltip.h" -#include -#include -#include -#include -#include -#include -#include - -using namespace zen; - - -class Tooltip::TooltipDlgGenerated : public wxDialog -{ -public: - TooltipDlgGenerated(wxWindow* parent, - wxWindowID id = wxID_ANY, - const wxString& title = {}, - const wxPoint& pos = wxDefaultPosition, - const wxSize& size = wxDefaultSize, - long style = 0) : wxDialog(parent, id, title, pos, size, style) - { - //Suse Linux/X11: needs parent window, else there are z-order issues - - this->SetSizeHints(wxDefaultSize, wxDefaultSize); - this->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_INFOBK)); //both required: on Ubuntu background is black, foreground white! - this->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_INFOTEXT)); // - - wxBoxSizer* bSizer158 = new wxBoxSizer(wxHORIZONTAL); - bitmapLeft_ = new wxStaticBitmap(this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0); - bSizer158->Add(bitmapLeft_, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - - staticTextMain_ = new wxStaticText(this, wxID_ANY, wxString(), wxDefaultPosition, wxDefaultSize, 0); - bSizer158->Add(staticTextMain_, 0, wxALL | wxALIGN_CENTER_HORIZONTAL | wxALIGN_CENTER_VERTICAL, 5); - - this->SetSizer(bSizer158); - this->Layout(); - bSizer158->Fit(this); - - } - - wxStaticText* staticTextMain_; - wxStaticBitmap* bitmapLeft_; -}; - - -void Tooltip::show(const wxString& text, wxPoint mousePos, const wxBitmap* bmp) -{ - if (!tipWindow_) - tipWindow_ = new TooltipDlgGenerated(&parent_); //ownership passed to parent - - const wxBitmap& newBmp = bmp ? *bmp : wxNullBitmap; - - if (!isEqual(tipWindow_->bitmapLeft_->GetBitmap(), newBmp)) - { - tipWindow_->bitmapLeft_->SetBitmap(newBmp); - tipWindow_->Refresh(); //needed if bitmap size changed! - } - - if (text != tipWindow_->staticTextMain_->GetLabel()) - { - tipWindow_->staticTextMain_->SetLabel(text); - tipWindow_->staticTextMain_->Wrap(600); - } - - tipWindow_->GetSizer()->SetSizeHints(tipWindow_); //~=Fit() + SetMinSize() - //Linux: Fit() seems to be broken => this needs to be called EVERY time inside show, not only if text or bmp change - - const wxPoint newPos = wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft ? - mousePos - wxPoint(30 + tipWindow_->GetSize().GetWidth(), 0) : - mousePos + wxPoint(30, 0); - - if (newPos != tipWindow_->GetScreenPosition()) - tipWindow_->SetSize(newPos.x, newPos.y, wxDefaultCoord, wxDefaultCoord); - //attention!!! possible endless loop: mouse pointer must NOT be within tipWindow! - //else it will trigger a wxEVT_LEAVE_WINDOW on middle grid which will hide the window, causing the window to be shown again via this method, etc. - - if (!tipWindow_->IsShown()) - tipWindow_->Show(); -} - - -void Tooltip::hide() -{ - if (tipWindow_) - { - //on wxGTK the tooltip is sometimes not shown again after it was hidden: e.g. drag-selection on middle grid - tipWindow_->Destroy(); //apply brute force: - tipWindow_ = nullptr; // - } -} +// ***************************************************************************** +// * 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 "tooltip.h" +#include +#include +#include +#include +#include +#include +#include + +using namespace zen; + + +class Tooltip::TooltipDlgGenerated : public wxDialog +{ +public: + TooltipDlgGenerated(wxWindow* parent, + wxWindowID id = wxID_ANY, + const wxString& title = {}, + const wxPoint& pos = wxDefaultPosition, + const wxSize& size = wxDefaultSize, + long style = 0) : wxDialog(parent, id, title, pos, size, style) + { + //Suse Linux/X11: needs parent window, else there are z-order issues + + this->SetSizeHints(wxDefaultSize, wxDefaultSize); + this->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_INFOBK)); //both required: on Ubuntu background is black, foreground white! + this->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_INFOTEXT)); // + + wxBoxSizer* bSizer158 = new wxBoxSizer(wxHORIZONTAL); + bitmapLeft_ = new wxStaticBitmap(this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0); + bSizer158->Add(bitmapLeft_, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); + + staticTextMain_ = new wxStaticText(this, wxID_ANY, wxString(), wxDefaultPosition, wxDefaultSize, 0); + bSizer158->Add(staticTextMain_, 0, wxALL | wxALIGN_CENTER_HORIZONTAL | wxALIGN_CENTER_VERTICAL, 5); + + this->SetSizer(bSizer158); + this->Layout(); + bSizer158->Fit(this); + + } + + wxStaticText* staticTextMain_; + wxStaticBitmap* bitmapLeft_; +}; + + +void Tooltip::show(const wxString& text, wxPoint mousePos, const wxBitmap* bmp) +{ + if (!tipWindow_) + tipWindow_ = new TooltipDlgGenerated(&parent_); //ownership passed to parent + + const wxBitmap& newBmp = bmp ? *bmp : wxNullBitmap; + + if (!isEqual(tipWindow_->bitmapLeft_->GetBitmap(), newBmp)) + { + tipWindow_->bitmapLeft_->SetBitmap(newBmp); + tipWindow_->Refresh(); //needed if bitmap size changed! + } + + if (text != tipWindow_->staticTextMain_->GetLabel()) + { + tipWindow_->staticTextMain_->SetLabel(text); + tipWindow_->staticTextMain_->Wrap(600); + } + + tipWindow_->GetSizer()->SetSizeHints(tipWindow_); //~=Fit() + SetMinSize() + //Linux: Fit() seems to be broken => this needs to be called EVERY time inside show, not only if text or bmp change + + const wxPoint newPos = wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft ? + mousePos - wxPoint(30 + tipWindow_->GetSize().GetWidth(), 0) : + mousePos + wxPoint(30, 0); + + if (newPos != tipWindow_->GetScreenPosition()) + tipWindow_->SetSize(newPos.x, newPos.y, wxDefaultCoord, wxDefaultCoord); + //attention!!! possible endless loop: mouse pointer must NOT be within tipWindow! + //else it will trigger a wxEVT_LEAVE_WINDOW on middle grid which will hide the window, causing the window to be shown again via this method, etc. + + if (!tipWindow_->IsShown()) + tipWindow_->Show(); +} + + +void Tooltip::hide() +{ + if (tipWindow_) + { + //on wxGTK the tooltip is sometimes not shown again after it was hidden: e.g. drag-selection on middle grid + tipWindow_->Destroy(); //apply brute force: + tipWindow_ = nullptr; // + } +} diff --git a/wx+/tooltip.h b/wx+/tooltip.h index 1892ba23..f2a7043e 100755 --- a/wx+/tooltip.h +++ b/wx+/tooltip.h @@ -1,32 +1,32 @@ -// ***************************************************************************** -// * 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 * -// ***************************************************************************** - -#ifndef TOOLTIP_H_8912740832170515 -#define TOOLTIP_H_8912740832170515 - -#include - - -namespace zen -{ -class Tooltip -{ -public: - Tooltip(wxWindow& parent) : parent_(parent) {} //parent needs to live at least as long as this instance! - - void show(const wxString& text, - wxPoint mousePos, //absolute screen coordinates - const wxBitmap* bmp = nullptr); - void hide(); - -private: - class TooltipDlgGenerated; - TooltipDlgGenerated* tipWindow_ = nullptr; - wxWindow& parent_; -}; -} - -#endif //TOOLTIP_H_8912740832170515 +// ***************************************************************************** +// * 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 * +// ***************************************************************************** + +#ifndef TOOLTIP_H_8912740832170515 +#define TOOLTIP_H_8912740832170515 + +#include + + +namespace zen +{ +class Tooltip +{ +public: + Tooltip(wxWindow& parent) : parent_(parent) {} //parent needs to live at least as long as this instance! + + void show(const wxString& text, + wxPoint mousePos, //absolute screen coordinates + const wxBitmap* bmp = nullptr); + void hide(); + +private: + class TooltipDlgGenerated; + TooltipDlgGenerated* tipWindow_ = nullptr; + wxWindow& parent_; +}; +} + +#endif //TOOLTIP_H_8912740832170515 diff --git a/wx+/zlib_wrap.cpp b/wx+/zlib_wrap.cpp index 8b2e2976..32ed65e1 100755 --- a/wx+/zlib_wrap.cpp +++ b/wx+/zlib_wrap.cpp @@ -1,50 +1,50 @@ -// ***************************************************************************** -// * 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 "zlib_wrap.h" - #include //let's pray this is the same version wxWidgets is linking against! - -using namespace zen; - - -size_t zen::impl::zlib_compressBound(size_t len) -{ - return ::compressBound(static_cast(len)); //upper limit for buffer size, larger than input size!!! -} - - -size_t zen::impl::zlib_compress(const void* src, size_t srcLen, void* trg, size_t trgLen, int level) //throw ZlibInternalError -{ - uLongf bufferSize = static_cast(trgLen); - const int rv = ::compress2(static_cast(trg), //Bytef* dest, - &bufferSize, //uLongf* destLen, - static_cast(src), //const Bytef* source, - static_cast(srcLen), //uLong sourceLen, - level); //int level - // Z_OK: success - // Z_MEM_ERROR: not enough memory - // Z_BUF_ERROR: not enough room in the output buffer - if (rv != Z_OK || bufferSize > trgLen) - throw ZlibInternalError(); - return bufferSize; -} - - -size_t zen::impl::zlib_decompress(const void* src, size_t srcLen, void* trg, size_t trgLen) //throw ZlibInternalError -{ - uLongf bufferSize = static_cast(trgLen); - const int rv = ::uncompress(static_cast(trg), //Bytef* dest, - &bufferSize, //uLongf* destLen, - static_cast(src), //const Bytef* source, - static_cast(srcLen)); //uLong sourceLen - // Z_OK: success - // Z_MEM_ERROR: not enough memory - // Z_BUF_ERROR: not enough room in the output buffer - // Z_DATA_ERROR: input data was corrupted or incomplete - if (rv != Z_OK || bufferSize > trgLen) - throw ZlibInternalError(); - return bufferSize; -} +// ***************************************************************************** +// * 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 "zlib_wrap.h" + #include //let's pray this is the same version wxWidgets is linking against! + +using namespace zen; + + +size_t zen::impl::zlib_compressBound(size_t len) +{ + return ::compressBound(static_cast(len)); //upper limit for buffer size, larger than input size!!! +} + + +size_t zen::impl::zlib_compress(const void* src, size_t srcLen, void* trg, size_t trgLen, int level) //throw ZlibInternalError +{ + uLongf bufferSize = static_cast(trgLen); + const int rv = ::compress2(static_cast(trg), //Bytef* dest, + &bufferSize, //uLongf* destLen, + static_cast(src), //const Bytef* source, + static_cast(srcLen), //uLong sourceLen, + level); //int level + // Z_OK: success + // Z_MEM_ERROR: not enough memory + // Z_BUF_ERROR: not enough room in the output buffer + if (rv != Z_OK || bufferSize > trgLen) + throw ZlibInternalError(); + return bufferSize; +} + + +size_t zen::impl::zlib_decompress(const void* src, size_t srcLen, void* trg, size_t trgLen) //throw ZlibInternalError +{ + uLongf bufferSize = static_cast(trgLen); + const int rv = ::uncompress(static_cast(trg), //Bytef* dest, + &bufferSize, //uLongf* destLen, + static_cast(src), //const Bytef* source, + static_cast(srcLen)); //uLong sourceLen + // Z_OK: success + // Z_MEM_ERROR: not enough memory + // Z_BUF_ERROR: not enough room in the output buffer + // Z_DATA_ERROR: input data was corrupted or incomplete + if (rv != Z_OK || bufferSize > trgLen) + throw ZlibInternalError(); + return bufferSize; +} diff --git a/wx+/zlib_wrap.h b/wx+/zlib_wrap.h index 11d8227b..ae2e6d0e 100755 --- a/wx+/zlib_wrap.h +++ b/wx+/zlib_wrap.h @@ -1,114 +1,114 @@ -// ***************************************************************************** -// * 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 * -// ***************************************************************************** - -#ifndef ZLIB_WRAP_H_428597064566 -#define ZLIB_WRAP_H_428597064566 - -#include - - -namespace zen -{ -class ZlibInternalError {}; - -// compression level must be between 0 and 9: -// 0: no compression -// 9: best compression -template //as specified in serialize.h -BinContainer compress(const BinContainer& stream, int level); //throw ZlibInternalError -//caveat: output stream is physically larger than input! => strip additional reserved space if needed: "BinContainer(output.begin(), output.end())" - -template -BinContainer decompress(const BinContainer& stream); //throw ZlibInternalError - - - - - - - - - - - -//######################## implementation ########################## -namespace impl -{ -size_t zlib_compressBound(size_t len); -size_t zlib_compress (const void* src, size_t srcLen, void* trg, size_t trgLen, int level); //throw ZlibInternalError -size_t zlib_decompress(const void* src, size_t srcLen, void* trg, size_t trgLen); //throw ZlibInternalError -} - - -template -BinContainer compress(const BinContainer& stream, int level) //throw ZlibInternalError -{ - BinContainer contOut; - if (!stream.empty()) //don't dereference iterator into empty container! - { - //save uncompressed stream size for decompression - const uint64_t uncompressedSize = stream.size(); //use portable number type! - contOut.resize(sizeof(uncompressedSize)); - std::copy(reinterpret_cast(&uncompressedSize), - reinterpret_cast(&uncompressedSize) + sizeof(uncompressedSize), - &*contOut.begin()); - - const size_t bufferEstimate = impl::zlib_compressBound(stream.size()); //upper limit for buffer size, larger than input size!!! - - contOut.resize(contOut.size() + bufferEstimate); - - const size_t bytesWritten = impl::zlib_compress(&*stream.begin(), - stream.size(), - &*contOut.begin() + contOut.size() - bufferEstimate, - bufferEstimate, - level); //throw ZlibInternalError - if (bytesWritten < bufferEstimate) - contOut.resize(contOut.size() - (bufferEstimate - bytesWritten)); //caveat: unsigned arithmetics - //caveat: physical memory consumption still *unchanged*! - } - return contOut; -} - - -template -BinContainer decompress(const BinContainer& stream) //throw ZlibInternalError -{ - BinContainer contOut; - if (!stream.empty()) //don't dereference iterator into empty container! - { - //retrieve size of uncompressed data - uint64_t uncompressedSize = 0; //use portable number type! - if (stream.size() < sizeof(uncompressedSize)) - throw ZlibInternalError(); - std::copy(&*stream.begin(), - &*stream.begin() + sizeof(uncompressedSize), - reinterpret_cast(&uncompressedSize)); - //attention: contOut MUST NOT be empty! Else it will pass a nullptr to zlib_decompress() => Z_STREAM_ERROR although "uncompressedSize == 0"!!! - //secondary bug: don't dereference iterator into empty container! - if (uncompressedSize == 0) //cannot be 0: compress() directly maps empty -> empty container skipping zlib! - throw ZlibInternalError(); - - try - { - contOut.resize(static_cast(uncompressedSize)); //throw std::bad_alloc - } - catch (std::bad_alloc&) //most likely due to data corruption! - { - throw ZlibInternalError(); - } - - const size_t bytesWritten = impl::zlib_decompress(&*stream.begin() + sizeof(uncompressedSize), - stream.size() - sizeof(uncompressedSize), - &*contOut.begin(), - static_cast(uncompressedSize)); //throw ZlibInternalError - if (bytesWritten != static_cast(uncompressedSize)) - throw ZlibInternalError(); - } - return contOut; -} -} - -#endif //ZLIB_WRAP_H_428597064566 +// ***************************************************************************** +// * 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 * +// ***************************************************************************** + +#ifndef ZLIB_WRAP_H_428597064566 +#define ZLIB_WRAP_H_428597064566 + +#include + + +namespace zen +{ +class ZlibInternalError {}; + +// compression level must be between 0 and 9: +// 0: no compression +// 9: best compression +template //as specified in serialize.h +BinContainer compress(const BinContainer& stream, int level); //throw ZlibInternalError +//caveat: output stream is physically larger than input! => strip additional reserved space if needed: "BinContainer(output.begin(), output.end())" + +template +BinContainer decompress(const BinContainer& stream); //throw ZlibInternalError + + + + + + + + + + + +//######################## implementation ########################## +namespace impl +{ +size_t zlib_compressBound(size_t len); +size_t zlib_compress (const void* src, size_t srcLen, void* trg, size_t trgLen, int level); //throw ZlibInternalError +size_t zlib_decompress(const void* src, size_t srcLen, void* trg, size_t trgLen); //throw ZlibInternalError +} + + +template +BinContainer compress(const BinContainer& stream, int level) //throw ZlibInternalError +{ + BinContainer contOut; + if (!stream.empty()) //don't dereference iterator into empty container! + { + //save uncompressed stream size for decompression + const uint64_t uncompressedSize = stream.size(); //use portable number type! + contOut.resize(sizeof(uncompressedSize)); + std::copy(reinterpret_cast(&uncompressedSize), + reinterpret_cast(&uncompressedSize) + sizeof(uncompressedSize), + &*contOut.begin()); + + const size_t bufferEstimate = impl::zlib_compressBound(stream.size()); //upper limit for buffer size, larger than input size!!! + + contOut.resize(contOut.size() + bufferEstimate); + + const size_t bytesWritten = impl::zlib_compress(&*stream.begin(), + stream.size(), + &*contOut.begin() + contOut.size() - bufferEstimate, + bufferEstimate, + level); //throw ZlibInternalError + if (bytesWritten < bufferEstimate) + contOut.resize(contOut.size() - (bufferEstimate - bytesWritten)); //caveat: unsigned arithmetics + //caveat: physical memory consumption still *unchanged*! + } + return contOut; +} + + +template +BinContainer decompress(const BinContainer& stream) //throw ZlibInternalError +{ + BinContainer contOut; + if (!stream.empty()) //don't dereference iterator into empty container! + { + //retrieve size of uncompressed data + uint64_t uncompressedSize = 0; //use portable number type! + if (stream.size() < sizeof(uncompressedSize)) + throw ZlibInternalError(); + std::copy(&*stream.begin(), + &*stream.begin() + sizeof(uncompressedSize), + reinterpret_cast(&uncompressedSize)); + //attention: contOut MUST NOT be empty! Else it will pass a nullptr to zlib_decompress() => Z_STREAM_ERROR although "uncompressedSize == 0"!!! + //secondary bug: don't dereference iterator into empty container! + if (uncompressedSize == 0) //cannot be 0: compress() directly maps empty -> empty container skipping zlib! + throw ZlibInternalError(); + + try + { + contOut.resize(static_cast(uncompressedSize)); //throw std::bad_alloc + } + catch (std::bad_alloc&) //most likely due to data corruption! + { + throw ZlibInternalError(); + } + + const size_t bytesWritten = impl::zlib_decompress(&*stream.begin() + sizeof(uncompressedSize), + stream.size() - sizeof(uncompressedSize), + &*contOut.begin(), + static_cast(uncompressedSize)); //throw ZlibInternalError + if (bytesWritten != static_cast(uncompressedSize)) + throw ZlibInternalError(); + } + return contOut; +} +} + +#endif //ZLIB_WRAP_H_428597064566 -- cgit