diff options
Diffstat (limited to 'wx+')
-rwxr-xr-x | wx+/app_main.h | 88 | ||||
-rwxr-xr-x | wx+/async_task.h | 286 | ||||
-rwxr-xr-x | wx+/bitmap_button.h | 176 | ||||
-rwxr-xr-x | wx+/choice_enum.h | 230 | ||||
-rwxr-xr-x | wx+/context_menu.h | 204 | ||||
-rwxr-xr-x | wx+/dc.h | 274 | ||||
-rwxr-xr-x | wx+/file_drop.cpp | 112 | ||||
-rwxr-xr-x | wx+/file_drop.h | 130 | ||||
-rwxr-xr-x | wx+/font_size.h | 102 | ||||
-rwxr-xr-x | wx+/graph.cpp | 1694 | ||||
-rwxr-xr-x | wx+/graph.h | 708 | ||||
-rwxr-xr-x | wx+/grid.cpp | 4440 | ||||
-rwxr-xr-x | wx+/grid.h | 726 | ||||
-rwxr-xr-x | wx+/http.cpp | 534 | ||||
-rwxr-xr-x | wx+/http.h | 98 | ||||
-rwxr-xr-x | wx+/image_resources.cpp | 336 | ||||
-rwxr-xr-x | wx+/image_resources.h | 46 | ||||
-rwxr-xr-x | wx+/image_tools.cpp | 466 | ||||
-rwxr-xr-x | wx+/image_tools.h | 516 | ||||
-rwxr-xr-x | wx+/no_flicker.h | 78 | ||||
-rwxr-xr-x | wx+/popup_dlg.cpp | 604 | ||||
-rwxr-xr-x | wx+/popup_dlg.h | 188 | ||||
-rwxr-xr-x | wx+/popup_dlg_generated.cpp | 194 | ||||
-rwxr-xr-x | wx+/popup_dlg_generated.h | 142 | ||||
-rwxr-xr-x | wx+/rtl.h | 190 | ||||
-rwxr-xr-x | wx+/std_button_layout.h | 234 | ||||
-rwxr-xr-x | wx+/toggle_button.h | 134 | ||||
-rwxr-xr-x | wx+/tooltip.cpp | 194 | ||||
-rwxr-xr-x | wx+/tooltip.h | 64 | ||||
-rwxr-xr-x | wx+/zlib_wrap.cpp | 100 | ||||
-rwxr-xr-x | wx+/zlib_wrap.h | 228 |
31 files changed, 6758 insertions, 6758 deletions
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 <wx/window.h>
-#include <wx/app.h>
-
-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 <wx/window.h> +#include <wx/app.h> + +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 <functional>
-#include <zen/thread.h>
-#include <zen/scope_guard.h>
-#include <zen/stl_tools.h>
-#include <wx/timer.h>
-
-
-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 ResultType, class Fun>
-class ConcreteTask : public Task
-{
-public:
- template <class Fun2>
- ConcreteTask(std::future<ResultType>&& asyncResult, Fun2&& evalOnGui) : asyncResult_(std::move(asyncResult)), evalOnGui_(std::forward<Fun2>(evalOnGui)) {}
-
- bool resultReady () const override { return isReady(asyncResult_); }
- void evaluateResult() override
- {
- evalResult(IsSameType<ResultType, void>());
- }
-
-private:
- void evalResult(FalseType /*void result type*/) { evalOnGui_(asyncResult_.get()); }
- void evalResult(TrueType /*void result type*/) { asyncResult_.get(); evalOnGui_(); }
-
- std::future<ResultType> asyncResult_;
- Fun evalOnGui_; //keep "evalOnGui" strictly separated from async thread: in particular do not copy in thread!
-};
-
-
-class AsyncTasks
-{
-public:
- AsyncTasks() {}
-
- template <class Fun, class Fun2>
- void add(Fun&& evalAsync, Fun2&& evalOnGui)
- {
- using ResultType = decltype(evalAsync());
- tasks_.push_back(std::make_unique<ConcreteTask<ResultType, Fun2>>(zen::runAsync(std::forward<Fun>(evalAsync)), std::forward<Fun2>(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<std::unique_ptr<Task>> readyTasks; //Reentrancy; access to AsyncTasks::add is not protected! => evaluate outside erase_if
-
- erase_if(tasks_, [&](std::unique_ptr<Task>& 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<std::unique_ptr<Task>> tasks_;
-};
-}
-
-
-class AsyncGuiQueue : private wxEvtHandler
-{
-public:
- AsyncGuiQueue() { timer_.Connect(wxEVT_TIMER, wxEventHandler(AsyncGuiQueue::onTimerEvent), nullptr, this); }
-
- template <class Fun, class Fun2>
- void processAsync(Fun&& evalAsync, Fun2&& evalOnGui)
- {
- asyncTasks_.add(std::forward<Fun >(evalAsync),
- std::forward<Fun2>(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 <functional> +#include <zen/thread.h> +#include <zen/scope_guard.h> +#include <zen/stl_tools.h> +#include <wx/timer.h> + + +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 ResultType, class Fun> +class ConcreteTask : public Task +{ +public: + template <class Fun2> + ConcreteTask(std::future<ResultType>&& asyncResult, Fun2&& evalOnGui) : asyncResult_(std::move(asyncResult)), evalOnGui_(std::forward<Fun2>(evalOnGui)) {} + + bool resultReady () const override { return isReady(asyncResult_); } + void evaluateResult() override + { + evalResult(IsSameType<ResultType, void>()); + } + +private: + void evalResult(FalseType /*void result type*/) { evalOnGui_(asyncResult_.get()); } + void evalResult(TrueType /*void result type*/) { asyncResult_.get(); evalOnGui_(); } + + std::future<ResultType> asyncResult_; + Fun evalOnGui_; //keep "evalOnGui" strictly separated from async thread: in particular do not copy in thread! +}; + + +class AsyncTasks +{ +public: + AsyncTasks() {} + + template <class Fun, class Fun2> + void add(Fun&& evalAsync, Fun2&& evalOnGui) + { + using ResultType = decltype(evalAsync()); + tasks_.push_back(std::make_unique<ConcreteTask<ResultType, Fun2>>(zen::runAsync(std::forward<Fun>(evalAsync)), std::forward<Fun2>(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<std::unique_ptr<Task>> readyTasks; //Reentrancy; access to AsyncTasks::add is not protected! => evaluate outside erase_if + + erase_if(tasks_, [&](std::unique_ptr<Task>& 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<std::unique_ptr<Task>> tasks_; +}; +} + + +class AsyncGuiQueue : private wxEvtHandler +{ +public: + AsyncGuiQueue() { timer_.Connect(wxEVT_TIMER, wxEventHandler(AsyncGuiQueue::onTimerEvent), nullptr, this); } + + template <class Fun, class Fun2> + void processAsync(Fun&& evalAsync, Fun2&& evalOnGui) + { + asyncTasks_.add(std::forward<Fun >(evalAsync), + std::forward<Fun2>(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 <wx/bmpbuttn.h>
-#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 <wx/bmpbuttn.h> +#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 <vector>
-#include <wx/choice.h>
-
-//handle mapping of enum values to wxChoice controls
-/*
-Example:
-
-Member variable:
- zen::EnumDescrList<EnumOnError> 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 <class Enum>
-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<std::pair<Enum, std::pair<wxString, wxString>>>;
- DescrList descrList;
-};
-template <class Enum> void setEnumVal(const EnumDescrList<Enum>& mapping, wxChoice& ctrl, Enum value);
-template <class Enum> Enum getEnumVal(const EnumDescrList<Enum>& mapping, const wxChoice& ctrl);
-template <class Enum> void updateTooltipEnumVal(const EnumDescrList<Enum>& mapping, wxChoice& ctrl);
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-//--------------- impelementation -------------------------------------------
-template <class Enum>
-void setEnumVal(const EnumDescrList<Enum>& 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 <class Enum>
-Enum getEnumVal(const EnumDescrList<Enum>& mapping, const wxChoice& ctrl)
-{
- const int selectedPos = ctrl.GetSelection();
-
- if (0 <= selectedPos && selectedPos < static_cast<int>(mapping.descrList.size()))
- return mapping.descrList[selectedPos].first;
- else
- {
- assert(false);
- return Enum(0);
- }
-}
-
-template <class Enum> void updateTooltipEnumVal(const EnumDescrList<Enum>& 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 <vector> +#include <wx/choice.h> + +//handle mapping of enum values to wxChoice controls +/* +Example: + +Member variable: + zen::EnumDescrList<EnumOnError> 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 <class Enum> +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<std::pair<Enum, std::pair<wxString, wxString>>>; + DescrList descrList; +}; +template <class Enum> void setEnumVal(const EnumDescrList<Enum>& mapping, wxChoice& ctrl, Enum value); +template <class Enum> Enum getEnumVal(const EnumDescrList<Enum>& mapping, const wxChoice& ctrl); +template <class Enum> void updateTooltipEnumVal(const EnumDescrList<Enum>& mapping, wxChoice& ctrl); + + + + + + + + + + + + + + +//--------------- impelementation ------------------------------------------- +template <class Enum> +void setEnumVal(const EnumDescrList<Enum>& 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 <class Enum> +Enum getEnumVal(const EnumDescrList<Enum>& mapping, const wxChoice& ctrl) +{ + const int selectedPos = ctrl.GetSelection(); + + if (0 <= selectedPos && selectedPos < static_cast<int>(mapping.descrList.size())) + return mapping.descrList[selectedPos].first; + else + { + assert(false); + return Enum(0); + } +} + +template <class Enum> void updateTooltipEnumVal(const EnumDescrList<Enum>& 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 <map>
-#include <vector>
-#include <functional>
-#include <wx/menu.h>
-#include <wx/app.h>
-
-/*
-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<wxMenu>()) {}
-
- void addItem(const wxString& label, const std::function<void()>& 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<void()>& 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<void()>& 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<GenericCommand*>(event.m_callbackUserData))
- (cmd->fun_)();
- }
-
- struct GenericCommand : public wxObject
- {
- GenericCommand(const std::function<void()>& fun) : fun_(fun) {}
- std::function<void()> fun_;
- };
-
- std::unique_ptr<wxMenu> menu;
- std::map<int, std::function<void()>> 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 <map> +#include <vector> +#include <functional> +#include <wx/menu.h> +#include <wx/app.h> + +/* +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<wxMenu>()) {} + + void addItem(const wxString& label, const std::function<void()>& 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<void()>& 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<void()>& 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<GenericCommand*>(event.m_callbackUserData)) + (cmd->fun_)(); + } + + struct GenericCommand : public wxObject + { + GenericCommand(const std::function<void()>& fun) : fun_(fun) {} + std::function<void()> fun_; + }; + + std::unique_ptr<wxMenu> menu; + std::map<int, std::function<void()>> commandList; //(item id, command) +}; +} + +#endif //CONTEXT_MENU_H_18047302153418174632141234 @@ -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 <unordered_map>
-#include <zen/optional.h>
-#include <wx/dcbuffer.h> //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<wxBitmap>& 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<wxDC*, wxRect>& refDcToAreaMap() { static std::unordered_map<wxDC*, wxRect> clippingAreas; return clippingAreas; }
-
- Opt<wxRect> 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<wxBitmap>& buffer) : wxPaintDC(&wnd) {} };
-
-#else
-class BufferedPaintDC : public wxMemoryDC
-{
-public:
- BufferedPaintDC(wxWindow& wnd, Opt<wxBitmap>& 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<wxBitmap>& 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 <unordered_map> +#include <zen/optional.h> +#include <wx/dcbuffer.h> //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<wxBitmap>& 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<wxDC*, wxRect>& refDcToAreaMap() { static std::unordered_map<wxDC*, wxRect> clippingAreas; return clippingAreas; } + + Opt<wxRect> 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<wxBitmap>& buffer) : wxPaintDC(&wnd) {} }; + +#else +class BufferedPaintDC : public wxMemoryDC +{ +public: + BufferedPaintDC(wxWindow& wnd, Opt<wxBitmap>& 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<wxBitmap>& 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 <wx/dnd.h>
-#include <zen/utf.h>
-
-
-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<Zstring> filePaths;
- for (const wxString& file : fileArray)
- filePaths.push_back(utfTo<Zstring>(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 <wx/dnd.h> +#include <zen/utf.h> + + +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<Zstring> filePaths; + for (const wxString& file : fileArray) + filePaths.push_back(utfTo<Zstring>(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 <vector>
-#include <functional>
-#include <zen/zstring.h>
-#include <wx/window.h>
-#include <wx/event.h>
-
-
-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<Zstring>& droppedPaths) : wxCommandEvent(EVENT_DROP_FILE), droppedPaths_(droppedPaths) {}
-
- const std::vector<Zstring>& getPaths() const { return droppedPaths_; }
-
-private:
- wxEvent* Clone() const override { return new FileDropEvent(*this); }
-
- const std::vector<Zstring> 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 <vector> +#include <functional> +#include <zen/zstring.h> +#include <wx/window.h> +#include <wx/event.h> + + +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<Zstring>& droppedPaths) : wxCommandEvent(EVENT_DROP_FILE), droppedPaths_(droppedPaths) {} + + const std::vector<Zstring>& getPaths() const { return droppedPaths_; } + +private: + wxEvent* Clone() const override { return new FileDropEvent(*this); } + + const std::vector<Zstring> 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 <zen/basic_math.h>
-#include <wx/window.h>
-#include <zen/scope_guard.h>
-
-
-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 <zen/basic_math.h> +#include <wx/window.h> +#include <zen/scope_guard.h> + + +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 <cassert>
-#include <algorithm>
-#include <numeric>
-#include <zen/basic_math.h>
-#include <zen/scope_guard.h>
-#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 <class Function, class Function2>
-void cutPoints(std::vector<CurvePoint>& curvePoints, std::vector<char>& 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<CurvePoint> curvePointsTmp;
- std::vector<char> 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<CurvePoint>& curvePoints, std::vector<char>& 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<CurvePoint>& curvePoints, std::vector<char>& 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<CurvePoint> ContinuousCurveData::getPoints(double minX, double maxX, int pixelWidth) const
-{
- std::vector<CurvePoint> points;
-
- if (pixelWidth <= 1) return points;
- const ConvertCoord cvrtX(minX, maxX, pixelWidth - 1); //map [minX, maxX] to [0, pixelWidth - 1]
-
- const std::pair<double, double> 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<CurvePoint> SparseCurveData::getPoints(double minX, double maxX, int pixelWidth) const
-{
- std::vector<CurvePoint> points;
- if (pixelWidth <= 1) return points;
- const ConvertCoord cvrtX(minX, maxX, pixelWidth - 1); //map [minX, maxX] to [0, pixelWidth - 1]
- const std::pair<double, double> 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<CurvePoint> ptLe = getLessEq(x);
- Opt<CurvePoint> 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<MouseSelection>(*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<CurveData>& data, const CurveAttributes& ca)
-{
- curves_.clear();
- addCurve(data, ca);
-}
-
-
-void Graph2D::addCurve(const std::shared_ptr<CurveData>& 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<double>::infinity() : attr.minX; //automatic: ensure values are initialized by first curve
- double maxX = attr.maxXauto ? -std::numeric_limits<double>::infinity() : attr.maxX; //
- for (auto it = curves_.begin(); it != curves_.end(); ++it)
- if (const CurveData* curve = it->first.get())
- {
- const std::pair<double, double> 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<double>::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<double>::infinity() : attr.minY; //automatic: ensure values are initialized by first curve
- double maxY = attr.maxYauto ? -std::numeric_limits<double>::infinity() : attr.maxY; //
-
- std::vector<std::vector<CurvePoint>> curvePoints(curves_.size());
- std::vector<std::vector<char>> oobMarker (curves_.size()); //effectively a std::vector<bool> 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<CurvePoint>& 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<std::vector<wxPoint>> 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<wxPoint>& 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<int>(points.size()), &points[0]);
- }
- }
-
- //2. draw all currently set mouse selections (including active selection)
- std::vector<SelectionBlock> 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<wxPoint>& 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<int>(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 <cassert> +#include <algorithm> +#include <numeric> +#include <zen/basic_math.h> +#include <zen/scope_guard.h> +#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 <class Function, class Function2> +void cutPoints(std::vector<CurvePoint>& curvePoints, std::vector<char>& 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<CurvePoint> curvePointsTmp; + std::vector<char> 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<CurvePoint>& curvePoints, std::vector<char>& 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<CurvePoint>& curvePoints, std::vector<char>& 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<CurvePoint> ContinuousCurveData::getPoints(double minX, double maxX, int pixelWidth) const +{ + std::vector<CurvePoint> points; + + if (pixelWidth <= 1) return points; + const ConvertCoord cvrtX(minX, maxX, pixelWidth - 1); //map [minX, maxX] to [0, pixelWidth - 1] + + const std::pair<double, double> 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<CurvePoint> SparseCurveData::getPoints(double minX, double maxX, int pixelWidth) const +{ + std::vector<CurvePoint> points; + if (pixelWidth <= 1) return points; + const ConvertCoord cvrtX(minX, maxX, pixelWidth - 1); //map [minX, maxX] to [0, pixelWidth - 1] + const std::pair<double, double> 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<CurvePoint> ptLe = getLessEq(x); + Opt<CurvePoint> 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<MouseSelection>(*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<CurveData>& data, const CurveAttributes& ca) +{ + curves_.clear(); + addCurve(data, ca); +} + + +void Graph2D::addCurve(const std::shared_ptr<CurveData>& 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<double>::infinity() : attr.minX; //automatic: ensure values are initialized by first curve + double maxX = attr.maxXauto ? -std::numeric_limits<double>::infinity() : attr.maxX; // + for (auto it = curves_.begin(); it != curves_.end(); ++it) + if (const CurveData* curve = it->first.get()) + { + const std::pair<double, double> 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<double>::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<double>::infinity() : attr.minY; //automatic: ensure values are initialized by first curve + double maxY = attr.maxYauto ? -std::numeric_limits<double>::infinity() : attr.maxY; // + + std::vector<std::vector<CurvePoint>> curvePoints(curves_.size()); + std::vector<std::vector<char>> oobMarker (curves_.size()); //effectively a std::vector<bool> 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<CurvePoint>& 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<std::vector<wxPoint>> 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<wxPoint>& 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<int>(points.size()), &points[0]); + } + } + + //2. draw all currently set mouse selections (including active selection) + std::vector<SelectionBlock> 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<wxPoint>& 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<int>(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 <map>
-#include <vector>
-#include <memory>
-#include <wx/panel.h>
-#include <wx/settings.h>
-#include <wx/bitmap.h>
-#include <zen/string_tools.h>
-#include <zen/optional.h>
-
-//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<LabelFormatterTimeElapsed>()).
- setLabelY(Graph2D::LABEL_Y_RIGHT, 60, std::make_shared<LabelFormatterBytes>()));
- //set graph data
- std::shared_ptr<CurveData> 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<double, double> getRangeX() const = 0;
- virtual std::vector<CurvePoint> 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<CurvePoint> 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<CurvePoint> getLessEq (double x) const = 0;
- virtual Opt<CurvePoint> getGreaterEq(double x) const = 0;
-
-private:
- std::vector<CurvePoint> 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<double, double> getRangeX() const override { const size_t sz = getSize(); return std::make_pair(0.0, sz == 0 ? 0.0 : sz - 1.0); }
-
- Opt<CurvePoint> getLessEq(double x) const override
- {
- const size_t sz = getSize();
- const size_t pos = std::min<ptrdiff_t>(std::floor(x), sz - 1); //[!] expect unsigned underflow if empty!
- if (pos < sz)
- return CurvePoint(pos, getValue(pos));
- return NoValue();
- }
-
- Opt<CurvePoint> getGreaterEq(double x) const override
- {
- const size_t pos = std::max<ptrdiff_t>(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<double>& 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<double> 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<wxString>(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<int>(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<CurveData>& data, const CurveAttributes& ca = CurveAttributes());
- void addCurve(const std::shared_ptr<CurveData>& 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<LabelFormatter> newLabelFmt = std::make_shared<DecimalNumberFormatter>())
- {
- labelposX = posX;
- xLabelHeight = static_cast<int>(height);
- labelFmtX = newLabelFmt;
- return *this;
- }
- MainAttributes& setLabelY(PosLabelY posY, size_t width = 60, std::shared_ptr<LabelFormatter> newLabelFmt = std::make_shared<DecimalNumberFormatter>())
- {
- labelposY = posY;
- yLabelWidth = static_cast<int>(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<LabelFormatter> labelFmtX = std::make_shared<DecimalNumberFormatter>();
-
- PosLabelY labelposY = LABEL_Y_LEFT;
- int yLabelWidth = 60;
- std::shared_ptr<LabelFormatter> labelFmtY = std::make_shared<DecimalNumberFormatter>();
-
- std::map<PosCorner, wxString> 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<SelectionBlock> getSelections() const { return oldSel; }
- void setSelections(const std::vector<SelectionBlock>& 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<SelectionBlock> oldSel; //applied selections
- std::shared_ptr<MouseSelection> activeSel; //set during mouse selection
-
- MainAttributes attr; //global attributes
-
- Opt<wxBitmap> doubleBuffer;
-
- using CurveList = std::vector<std::pair<std::shared_ptr<CurveData>, 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 <map> +#include <vector> +#include <memory> +#include <wx/panel.h> +#include <wx/settings.h> +#include <wx/bitmap.h> +#include <zen/string_tools.h> +#include <zen/optional.h> + +//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<LabelFormatterTimeElapsed>()). + setLabelY(Graph2D::LABEL_Y_RIGHT, 60, std::make_shared<LabelFormatterBytes>())); + //set graph data + std::shared_ptr<CurveData> 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<double, double> getRangeX() const = 0; + virtual std::vector<CurvePoint> 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<CurvePoint> 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<CurvePoint> getLessEq (double x) const = 0; + virtual Opt<CurvePoint> getGreaterEq(double x) const = 0; + +private: + std::vector<CurvePoint> 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<double, double> getRangeX() const override { const size_t sz = getSize(); return std::make_pair(0.0, sz == 0 ? 0.0 : sz - 1.0); } + + Opt<CurvePoint> getLessEq(double x) const override + { + const size_t sz = getSize(); + const size_t pos = std::min<ptrdiff_t>(std::floor(x), sz - 1); //[!] expect unsigned underflow if empty! + if (pos < sz) + return CurvePoint(pos, getValue(pos)); + return NoValue(); + } + + Opt<CurvePoint> getGreaterEq(double x) const override + { + const size_t pos = std::max<ptrdiff_t>(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<double>& 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<double> 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<wxString>(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<int>(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<CurveData>& data, const CurveAttributes& ca = CurveAttributes()); + void addCurve(const std::shared_ptr<CurveData>& 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<LabelFormatter> newLabelFmt = std::make_shared<DecimalNumberFormatter>()) + { + labelposX = posX; + xLabelHeight = static_cast<int>(height); + labelFmtX = newLabelFmt; + return *this; + } + MainAttributes& setLabelY(PosLabelY posY, size_t width = 60, std::shared_ptr<LabelFormatter> newLabelFmt = std::make_shared<DecimalNumberFormatter>()) + { + labelposY = posY; + yLabelWidth = static_cast<int>(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<LabelFormatter> labelFmtX = std::make_shared<DecimalNumberFormatter>(); + + PosLabelY labelposY = LABEL_Y_LEFT; + int yLabelWidth = 60; + std::shared_ptr<LabelFormatter> labelFmtY = std::make_shared<DecimalNumberFormatter>(); + + std::map<PosCorner, wxString> 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<SelectionBlock> getSelections() const { return oldSel; } + void setSelections(const std::vector<SelectionBlock>& 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<SelectionBlock> oldSel; //applied selections + std::shared_ptr<MouseSelection> activeSel; //set during mouse selection + + MainAttributes attr; //global attributes + + Opt<wxBitmap> doubleBuffer; + + using CurveList = std::vector<std::pair<std::shared_ptr<CurveData>, 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 <cassert>
-#include <set>
-#include <chrono>
-#include <wx/settings.h>
-#include <wx/listbox.h>
-#include <wx/tooltip.h>
-#include <wx/timer.h>
-#include <wx/utils.h>
-//#include <zen/tick_count.h>
-#include <zen/string_tools.h>
-#include <zen/scope_guard.h>
-#include <zen/utf.h>
-#include <zen/format_unit.h>
-#include "dc.h"
-
- #include <gtk/gtk.h>
-
-using namespace zen;
-
-
-wxColor Grid::getColorSelectionGradientFrom() { return { 137, 172, 255 }; } //blue: HSL: 158, 255, 196 HSV: 222, 0.46, 1
-wxColor Grid::getColorSelectionGradientTo () { return { 225, 234, 255 }; } // HSL: 158, 255, 240 HSV: 222, 0.12, 1
-
-const int GridData::COLUMN_GAP_LEFT = 4;
-
-
-void zen::clearArea(wxDC& dc, const wxRect& rect, const wxColor& col)
-{
- wxDCPenChanger dummy (dc, col);
- wxDCBrushChanger dummy2(dc, col);
- dc.DrawRectangle(rect);
-}
-
-
-namespace
-{
-//let's NOT create wxWidgets objects statically:
-//------------------------------ Grid Parameters --------------------------------
-inline wxColor getColorLabelText() { return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); }
-inline wxColor getColorGridLine() { return { 192, 192, 192 }; } //light grey
-
-inline wxColor getColorLabelGradientFrom() { return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); }
-inline wxColor getColorLabelGradientTo () { return { 200, 200, 200 }; } //light grey
-
-inline wxColor getColorLabelGradientFocusFrom() { return getColorLabelGradientFrom(); }
-inline wxColor getColorLabelGradientFocusTo () { return Grid::getColorSelectionGradientFrom(); }
-
-const double MOUSE_DRAG_ACCELERATION = 1.5; //unit: [rows / (pixel * sec)] -> same value like Explorer!
-const int DEFAULT_COL_LABEL_BORDER = 6; //top + bottom border in addition to label height
-const int COLUMN_MOVE_DELAY = 5; //unit: [pixel] (from Explorer)
-const int COLUMN_MIN_WIDTH = 40; //only honored when resizing manually!
-const int ROW_LABEL_BORDER = 3;
-const int COLUMN_RESIZE_TOLERANCE = 6; //unit [pixel]
-const int COLUMN_FILL_GAP_TOLERANCE = 10; //enlarge column to fill full width when resizing
-
-const bool fillGapAfterColumns = true; //draw rows/column label to fill full window width; may become an instance variable some time?
-}
-
-//----------------------------------------------------------------------------------------------------------------
-const wxEventType zen::EVENT_GRID_MOUSE_LEFT_DOUBLE = wxNewEventType();
-const wxEventType zen::EVENT_GRID_MOUSE_LEFT_DOWN = wxNewEventType();
-const wxEventType zen::EVENT_GRID_MOUSE_LEFT_UP = wxNewEventType();
-const wxEventType zen::EVENT_GRID_MOUSE_RIGHT_DOWN = wxNewEventType();
-const wxEventType zen::EVENT_GRID_MOUSE_RIGHT_UP = wxNewEventType();
-const wxEventType zen::EVENT_GRID_SELECT_RANGE = wxNewEventType();
-const wxEventType zen::EVENT_GRID_COL_LABEL_MOUSE_LEFT = wxNewEventType();
-const wxEventType zen::EVENT_GRID_COL_LABEL_MOUSE_RIGHT = wxNewEventType();
-const wxEventType zen::EVENT_GRID_COL_RESIZE = wxNewEventType();
-//----------------------------------------------------------------------------------------------------------------
-
-void GridData::renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected)
-{
- drawCellBackground(dc, rect, enabled, selected, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
-}
-
-
-void GridData::renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover)
-{
- wxRect rectTmp = drawCellBorder(dc, rect);
-
- rectTmp.x += COLUMN_GAP_LEFT;
- rectTmp.width -= COLUMN_GAP_LEFT;
- drawCellText(dc, rectTmp, getValue(row, colType));
-}
-
-
-int GridData::getBestSize(wxDC& dc, size_t row, ColumnType colType)
-{
- return dc.GetTextExtent(getValue(row, colType)).GetWidth() + 2 * COLUMN_GAP_LEFT + 1; //gap on left and right side + border
-}
-
-
-wxRect GridData::drawCellBorder(wxDC& dc, const wxRect& rect) //returns remaining rectangle
-{
- wxDCPenChanger dummy2(dc, getColorGridLine());
- dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight());
- dc.DrawLine(rect.GetBottomRight(), rect.GetTopRight() + wxPoint(0, -1));
-
- return wxRect(rect.GetTopLeft(), wxSize(rect.width - 1, rect.height - 1));
-}
-
-
-void GridData::drawCellBackground(wxDC& dc, const wxRect& rect, bool enabled, bool selected, const wxColor& backgroundColor)
-{
- if (enabled)
- {
- if (selected)
- dc.GradientFillLinear(rect, Grid::getColorSelectionGradientFrom(), Grid::getColorSelectionGradientTo(), wxEAST);
- else
- clearArea(dc, rect, backgroundColor);
- }
- else
- clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE));
-}
-
-
-wxSize GridData::drawCellText(wxDC& dc, const wxRect& rect, const std::wstring& text, int alignment)
-{
- /*
- performance notes (Windows):
- - wxDC::GetTextExtent() is by far the most expensive call (20x more expensive than wxDC::DrawText())
- - wxDC::DrawLabel() is inefficiently implemented; internally calls: wxDC::GetMultiLineTextExtent(), wxDC::GetTextExtent(), wxDC::DrawText()
- - wxDC::GetMultiLineTextExtent() calls wxDC::GetTextExtent()
- - wxDC::DrawText also calls wxDC::GetTextExtent()!!
- => wxDC::DrawLabel() boils down to 3(!) calls to wxDC::GetTextExtent()!!!
- - wxDC::DrawLabel results in GetTextExtent() call even for empty strings!!!
- => skip the wxDC::DrawLabel() cruft and directly call wxDC::DrawText!
- */
-
- //truncate large texts and add ellipsis
- assert(!contains(text, L"\n"));
- const wchar_t ELLIPSIS = L'\u2026'; //"..."
-
- std::wstring textTrunc = text;
- wxSize extentTrunc = dc.GetTextExtent(textTrunc);
- if (extentTrunc.GetWidth() > rect.width)
- {
- //unlike Windows 7 Explorer, we truncate UTF-16 correctly: e.g. CJK-Ideogramm encodes to TWO wchar_t: utfTo<std::wstring>("\xf0\xa4\xbd\x9c");
- size_t low = 0; //number of unicode chars!
- size_t high = unicodeLength(text); //
- if (high > 1)
- for (;;)
- {
- const size_t middle = (low + high) / 2; //=> never 0 when "high - low > 1"
- if (high - low <= 1)
- {
- if (low == 0)
- {
- textTrunc = ELLIPSIS;
- extentTrunc = dc.GetTextExtent(ELLIPSIS);
- }
- break;
- }
-
- const std::wstring& candidate = 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 <class T>
- bool sendEventNow(T&& event) //take both "rvalue + lvalues", return "true" if a suitable event handler function was found and executed, and the function did not call wxEvent::Skip.
- {
- if (wxEvtHandler* evtHandler = parent_.GetEventHandler())
- return evtHandler->ProcessEvent(event);
- return false;
- }
-
-protected:
- void setToolTip(const std::wstring& text) //proper fix for wxWindow
- {
- wxToolTip* tt = GetToolTip();
-
- const wxString oldText = tt ? tt->GetTip() : wxString();
- if (text != oldText)
- {
- if (text.empty())
- SetToolTip(nullptr); //wxGTK doesn't allow wxToolTip with empty text!
- else
- {
- //wxWidgets bug: tooltip multiline property is defined by first tooltip text containing newlines or not (same is true for maximum width)
- if (!tt)
- SetToolTip(new wxToolTip(L"a b\n\
- a b")); //ugly, but working (on Windows)
- tt = GetToolTip(); //should be bound by now
- assert(tt);
- if (tt)
- tt->SetTip(text);
- }
- }
- }
-
-private:
- virtual void render(wxDC& dc, const wxRect& rect) = 0;
-
- virtual void onFocus(wxFocusEvent& event) { event.Skip(); }
- virtual void onChildFocus(wxEvent& event) {} //wxGTK::wxScrolledWindow automatically scrolls to child window when child gets focus -> prevent!
-
- virtual void onMouseLeftDown (wxMouseEvent& event) { event.Skip(); }
- virtual void onMouseLeftUp (wxMouseEvent& event) { event.Skip(); }
- virtual void onMouseLeftDouble(wxMouseEvent& event) { event.Skip(); }
- virtual void onMouseRightDown (wxMouseEvent& event) { event.Skip(); }
- virtual void onMouseRightUp (wxMouseEvent& event) { event.Skip(); }
- virtual void onMouseMovement (wxMouseEvent& event) { event.Skip(); }
- virtual void onLeaveWindow (wxMouseEvent& event) { event.Skip(); }
- virtual void onMouseCaptureLost(wxMouseCaptureLostEvent& event) { event.Skip(); }
-
- void onKeyDown(wxKeyEvent& event)
- {
- if (!sendEventNow(event)) //let parent collect all key events
- event.Skip();
- }
-
- void onMouseWheel(wxMouseEvent& event)
- {
- /*
- MSDN, WM_MOUSEWHEEL: "Sent to the focus window when the mouse wheel is rotated.
- The DefWindowProc function propagates the message to the window's parent.
- There should be no internal forwarding of the message, since DefWindowProc propagates
- it up the parent chain until it finds a window that processes it."
-
- On OS X there is no such propagation! => we need a redirection (the same wxGrid implements)
- */
-
- //new wxWidgets 3.0 screw-up for GTK2: wxScrollHelperEvtHandler::ProcessEvent() ignores wxEVT_MOUSEWHEEL events
- //thereby breaking the scenario of redirection to parent we need here (but also breaking their very own wxGrid sample)
- //=> call wxScrolledWindow mouse wheel handler directly
- parent_.HandleOnMouseWheel(event);
-
- //if (!sendEventNow(event))
- // event.Skip();
- }
-
- void onPaintEvent(wxPaintEvent& event)
- {
- //wxAutoBufferedPaintDC dc(this); -> this one happily fucks up for RTL layout by not drawing the first column (x = 0)!
- BufferedPaintDC dc(*this, doubleBuffer_);
-
- assert(GetSize() == GetClientSize());
-
- const wxRegion& updateReg = GetUpdateRegion();
- for (wxRegionIterator it = updateReg; it; ++it)
- render(dc, it.GetRect());
- }
-
- void onSizeEvent(wxSizeEvent& event)
- {
- Refresh();
- event.Skip();
- }
-
- void onEraseBackGround(wxEraseEvent& event) {}
-
- Grid& parent_;
- Opt<wxBitmap> doubleBuffer_;
-};
-
-//----------------------------------------------------------------------------------------------------------------
-//----------------------------------------------------------------------------------------------------------------
-
-class Grid::CornerWin : public SubWindow
-{
-public:
- CornerWin(Grid& parent) : SubWindow(parent) {}
-
-private:
- bool AcceptsFocus() const override { return false; }
-
- void render(wxDC& dc, const wxRect& rect) override
- {
- const wxRect& clientRect = GetClientRect();
-
- dc.GradientFillLinear(clientRect, getColorLabelGradientFrom(), getColorLabelGradientTo(), wxSOUTH);
-
- dc.SetPen(wxSystemSettings::GetColour(wxSYS_COLOUR_3DSHADOW));
-
- {
- wxDCPenChanger dummy(dc, getColorLabelGradientFrom());
- dc.DrawLine(clientRect.GetTopLeft(), clientRect.GetTopRight());
- }
-
- dc.GradientFillLinear(wxRect(clientRect.GetBottomLeft (), clientRect.GetTopLeft ()), getColorLabelGradientFrom(), dc.GetPen().GetColour(), wxSOUTH);
- dc.GradientFillLinear(wxRect(clientRect.GetBottomRight(), clientRect.GetTopRight()), getColorLabelGradientFrom(), dc.GetPen().GetColour(), wxSOUTH);
-
- dc.DrawLine(clientRect.GetBottomLeft(), clientRect.GetBottomRight());
-
- wxRect rectShrinked = clientRect;
- rectShrinked.Deflate(1);
- dc.SetPen(*wxWHITE_PEN);
-
- //dc.DrawLine(clientRect.GetTopLeft(), clientRect.GetTopRight() + wxPoint(1, 0));
- dc.DrawLine(rectShrinked.GetTopLeft(), rectShrinked.GetBottomLeft() + wxPoint(0, 1));
- }
-};
-
-//----------------------------------------------------------------------------------------------------------------
-//----------------------------------------------------------------------------------------------------------------
-
-class Grid::RowLabelWin : public SubWindow
-{
-public:
- RowLabelWin(Grid& parent) :
- SubWindow(parent),
- rowHeight_(parent.GetCharHeight() + 2 + 1) {} //default height; don't call any functions on "parent" other than those from wxWindow during construction!
- //2 for some more space, 1 for bottom border (gives 15 + 2 + 1 on Windows, 17 + 2 + 1 on Ubuntu)
-
- int getBestWidth(ptrdiff_t rowFrom, ptrdiff_t rowTo)
- {
- wxClientDC dc(this);
-
- wxFont labelFont = GetFont();
- //labelFont.SetWeight(wxFONTWEIGHT_BOLD);
- dc.SetFont(labelFont); //harmonize with RowLabelWin::render()!
-
- int bestWidth = 0;
- for (ptrdiff_t i = rowFrom; i <= rowTo; ++i)
- bestWidth = std::max(bestWidth, dc.GetTextExtent(formatRow(i)).GetWidth() + 2 * ROW_LABEL_BORDER);
- return bestWidth;
- }
-
- size_t getLogicalHeight() const { return refParent().getRowCount() * rowHeight_; }
-
- ptrdiff_t getRowAtPos(ptrdiff_t posY) const //returns < 0 on invalid input, else row number within: [0, rowCount]; rowCount if out of range
- {
- if (posY >= 0 && rowHeight_ > 0)
- {
- const size_t row = posY / rowHeight_;
- return std::min(row, refParent().getRowCount());
- }
- return -1;
- }
-
- int getRowHeight() const { return rowHeight_; } //guarantees to return size >= 1 !
- void setRowHeight(int height) { assert(height > 0); rowHeight_ = std::max(1, height); }
-
- wxRect getRowLabelArea(size_t row) const //returns empty rect if row not found
- {
- assert(GetClientAreaOrigin() == wxPoint());
- if (row < refParent().getRowCount())
- return wxRect(wxPoint(0, rowHeight_ * row),
- wxSize(GetClientSize().GetWidth(), rowHeight_));
- return wxRect();
- }
-
- std::pair<ptrdiff_t, ptrdiff_t> getRowsOnClient(const wxRect& clientRect) const //returns range [begin, end)
- {
- const int yFrom = refParent().CalcUnscrolledPosition(clientRect.GetTopLeft ()).y;
- const int yTo = refParent().CalcUnscrolledPosition(clientRect.GetBottomRight()).y;
-
- return std::make_pair(std::max(yFrom / rowHeight_, 0),
- std::min<ptrdiff_t>((yTo / rowHeight_) + 1, refParent().getRowCount()));
- }
-
-private:
- static std::wstring formatRow(size_t row) { return toGuiString(row + 1); } //convert number to std::wstring including thousands separator
-
- bool AcceptsFocus() const override { return false; }
-
- void render(wxDC& dc, const wxRect& rect) override
- {
- /*
- IsEnabled() vs IsThisEnabled() since wxWidgets 2.9.5:
-
- void wxWindowBase::NotifyWindowOnEnableChange(), called from bool wxWindowBase::Enable(), fails to refresh
- child elements when disabling a IsTopLevel() dialog, e.g. when showing a modal dialog.
- The unfortunate effect on XP for using IsEnabled() when rendering the grid is that the user can move the modal dialog
- and *draw* with it on the background while the grid refreshes as disabled incrementally!
-
- => Don't use IsEnabled() since it considers the top level window. The brittle wxWidgets implementation is right in their intention,
- but wrong when not refreshing child-windows: the control designer decides how his control should be rendered!
-
- => IsThisEnabled() OTOH is too shallow and does not consider parent windows which are not top level.
-
- The perfect solution would be a bool ShouldBeDrawnActive() { return "IsEnabled() but ignore effects of showing a modal dialog"; }
-
- However "IsThisEnabled()" is good enough (same like the old IsEnabled() on wxWidgets 2.8.12) and it avoids this pathetic behavior on XP.
- (Similar problem on Win 7: e.g. directly click sync button without comparing first)
- */
- if (IsThisEnabled())
- clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
- else
- clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE));
-
- wxFont labelFont = GetFont();
- //labelFont.SetWeight(wxFONTWEIGHT_BOLD);
- dc.SetFont(labelFont); //harmonize with RowLabelWin::getBestWidth()!
-
- auto rowRange = getRowsOnClient(rect); //returns range [begin, end)
- for (auto row = rowRange.first; row < rowRange.second; ++row)
- {
- wxRect singleLabelArea = getRowLabelArea(row); //returns empty rect if row not found
- if (singleLabelArea.height > 0)
- {
- singleLabelArea.y = refParent().CalcScrolledPosition(singleLabelArea.GetTopLeft()).y;
- drawRowLabel(dc, singleLabelArea, row);
- }
- }
- }
-
- void drawRowLabel(wxDC& dc, const wxRect& rect, size_t row)
- {
- //clearArea(dc, rect, getColorRowLabel());
- dc.GradientFillLinear(rect, getColorLabelGradientFrom(), getColorLabelGradientTo(), wxEAST); //clear overlapping cells
- wxDCTextColourChanger dummy3(dc, getColorLabelText()); //accessibility: always set both foreground AND background colors!
-
- //label text
- wxRect textRect = rect;
- textRect.Deflate(1);
-
- GridData::drawCellText(dc, textRect, formatRow(row), wxALIGN_CENTRE);
-
- //border lines
- {
- wxDCPenChanger dummy(dc, *wxWHITE_PEN);
- dc.DrawLine(rect.GetTopLeft(), rect.GetTopRight());
- }
- {
- wxDCPenChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_3DSHADOW));
- dc.DrawLine(rect.GetTopLeft(), rect.GetBottomLeft());
- dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight());
- dc.DrawLine(rect.GetBottomRight(), rect.GetTopRight() + wxPoint(0, -1));
- }
- }
-
- void onMouseLeftDown(wxMouseEvent& event) override { refParent().redirectRowLabelEvent(event); }
- void onMouseMovement(wxMouseEvent& event) override { refParent().redirectRowLabelEvent(event); }
- void onMouseLeftUp (wxMouseEvent& event) override { refParent().redirectRowLabelEvent(event); }
-
- int rowHeight_;
-};
-
-
-namespace
-{
-class ColumnResizing
-{
-public:
- ColumnResizing(wxWindow& wnd, size_t col, int startWidth, int clientPosX) :
- wnd_(wnd), col_(col), startWidth_(startWidth), clientPosX_(clientPosX)
- {
- wnd_.CaptureMouse();
- }
- ~ColumnResizing()
- {
- if (wnd_.HasCapture())
- wnd_.ReleaseMouse();
- }
-
- size_t getColumn () const { return col_; }
- int getStartWidth () const { return startWidth_; }
- int getStartPosX () const { return clientPosX_; }
-
-private:
- wxWindow& wnd_;
- const size_t col_;
- const int startWidth_;
- const int clientPosX_;
-};
-
-
-class ColumnMove
-{
-public:
- ColumnMove(wxWindow& wnd, size_t colFrom, int clientPosX) :
- wnd_(wnd),
- colFrom_(colFrom),
- colTo_(colFrom),
- clientPosX_(clientPosX) { wnd_.CaptureMouse(); }
- ~ColumnMove() { if (wnd_.HasCapture()) wnd_.ReleaseMouse(); }
-
- size_t getColumnFrom() const { return colFrom_; }
- size_t& refColumnTo() { return colTo_; }
- int getStartPosX () const { return clientPosX_; }
-
- bool isRealMove() const { return !singleClick_; }
- void setRealMove() { singleClick_ = false; }
-
-private:
- wxWindow& wnd_;
- const size_t colFrom_;
- size_t colTo_;
- const int clientPosX_;
- bool singleClick_ = true;
-};
-}
-
-//----------------------------------------------------------------------------------------------------------------
-
-class Grid::ColLabelWin : public SubWindow
-{
-public:
- ColLabelWin(Grid& parent) : SubWindow(parent) {}
-
-private:
- bool AcceptsFocus() const override { return false; }
-
- void render(wxDC& dc, const wxRect& rect) override
- {
- if (IsThisEnabled())
- clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
- else
- clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE));
-
- //coordinate with "colLabelHeight" in Grid constructor:
- wxFont labelFont = GetFont();
- labelFont.SetWeight(wxFONTWEIGHT_BOLD);
- dc.SetFont(labelFont);
-
- wxDCTextColourChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels
-
- const int colLabelHeight = refParent().colLabelHeight_;
-
- wxPoint labelAreaTL(refParent().CalcScrolledPosition(wxPoint(0, 0)).x, 0); //client coordinates
-
- std::vector<ColumnWidth> absWidths = refParent().getColWidths(); //resolve stretched widths
- for (auto it = absWidths.begin(); it != absWidths.end(); ++it)
- {
- const size_t col = it - absWidths.begin();
- const int width = it->width_; //don't use unsigned for calculations!
-
- if (labelAreaTL.x > rect.GetRight())
- return; //done, rect is fully covered
- if (labelAreaTL.x + width > rect.x)
- drawColumnLabel(dc, wxRect(labelAreaTL, wxSize(width, colLabelHeight)), col, it->type_);
- labelAreaTL.x += width;
- }
- if (labelAreaTL.x > rect.GetRight())
- return; //done, rect is fully covered
-
- //fill gap after columns and cover full width
- if (fillGapAfterColumns)
- {
- int totalWidth = 0;
- for (const ColumnWidth& cw : absWidths)
- totalWidth += cw.width_;
- const int clientWidth = GetClientSize().GetWidth(); //need reliable, stable width in contrast to rect.width
-
- if (totalWidth < clientWidth)
- drawColumnLabel(dc, wxRect(labelAreaTL, wxSize(clientWidth - totalWidth, colLabelHeight)), absWidths.size(), ColumnType::NONE);
- }
- }
-
- void drawColumnLabel(wxDC& dc, const wxRect& rect, size_t col, ColumnType colType)
- {
- if (auto dataView = refParent().getDataProvider())
- {
- const bool isHighlighted = activeResizing_ ? col == activeResizing_ ->getColumn () : //highlight_ column on mouse-over
- activeClickOrMove_ ? col == activeClickOrMove_->getColumnFrom() :
- highlightCol_ ? col == *highlightCol_ :
- false;
-
- RecursiveDcClipper clip(dc, rect);
- dataView->renderColumnLabel(refParent(), dc, rect, colType, isHighlighted);
-
- //draw move target location
- if (refParent().allowColumnMove_)
- if (activeClickOrMove_ && activeClickOrMove_->isRealMove())
- {
- if (col + 1 == activeClickOrMove_->refColumnTo()) //handle pos 1, 2, .. up to "at end" position
- dc.GradientFillLinear(wxRect(rect.GetTopRight(), rect.GetBottomRight() + wxPoint(-2, 0)), getColorLabelGradientFrom(), *wxBLUE, wxSOUTH);
- else if (col == activeClickOrMove_->refColumnTo() && col == 0) //pos 0
- dc.GradientFillLinear(wxRect(rect.GetTopLeft(), rect.GetBottomLeft() + wxPoint(2, 0)), getColorLabelGradientFrom(), *wxBLUE, wxSOUTH);
- }
- }
- }
-
- void onMouseLeftDown(wxMouseEvent& event) override
- {
- if (FindFocus() != &refParent().getMainWin())
- refParent().getMainWin().SetFocus();
-
- activeResizing_.reset();
- activeClickOrMove_.reset();
-
- if (Opt<ColAction> action = refParent().clientPosToColumnAction(event.GetPosition()))
- {
- if (action->wantResize)
- {
- if (!event.LeftDClick()) //double-clicks never seem to arrive here; why is this checked at all???
- if (Opt<int> colWidth = refParent().getColWidth(action->col))
- activeResizing_ = std::make_unique<ColumnResizing>(*this, action->col, *colWidth, event.GetPosition().x);
- }
- else //a move or single click
- activeClickOrMove_ = std::make_unique<ColumnMove>(*this, action->col, event.GetPosition().x);
- }
- event.Skip();
- }
-
- void onMouseLeftUp(wxMouseEvent& event) override
- {
- activeResizing_.reset(); //nothing else to do, actual work done by onMouseMovement()
-
- if (activeClickOrMove_)
- {
- if (activeClickOrMove_->isRealMove())
- {
- if (refParent().allowColumnMove_)
- {
- const size_t colFrom = activeClickOrMove_->getColumnFrom();
- size_t colTo = activeClickOrMove_->refColumnTo();
-
- if (colTo > colFrom) //simulate "colFrom" deletion
- --colTo;
-
- refParent().moveColumn(colFrom, colTo);
- }
- }
- else //notify single label click
- {
- if (const Opt<ColumnType> colType = refParent().colToType(activeClickOrMove_->getColumnFrom()))
- sendEventNow(GridLabelClickEvent(EVENT_GRID_COL_LABEL_MOUSE_LEFT, event, *colType));
- }
- activeClickOrMove_.reset();
- }
-
- refParent().updateWindowSizes(); //looks strange if done during onMouseMovement()
- refParent().Refresh();
- event.Skip();
- }
-
- void onMouseCaptureLost(wxMouseCaptureLostEvent& event) override
- {
- activeResizing_.reset();
- activeClickOrMove_.reset();
- Refresh();
- //event.Skip(); -> we DID handle it!
- }
-
- void onMouseLeftDouble(wxMouseEvent& event) override
- {
- if (Opt<ColAction> action = refParent().clientPosToColumnAction(event.GetPosition()))
- if (action->wantResize)
- {
- //auto-size visible range on double-click
- const int bestWidth = refParent().getBestColumnSize(action->col); //return -1 on error
- if (bestWidth >= 0)
- {
- refParent().setColumnWidth(bestWidth, action->col, ALLOW_GRID_EVENT);
- refParent().Refresh(); //refresh main grid as well!
- }
- }
- event.Skip();
- }
-
- void onMouseMovement(wxMouseEvent& event) override
- {
- if (activeResizing_)
- {
- const auto col = activeResizing_->getColumn();
- const int newWidth = activeResizing_->getStartWidth() + event.GetPosition().x - activeResizing_->getStartPosX();
-
- //set width tentatively
- refParent().setColumnWidth(newWidth, col, ALLOW_GRID_EVENT);
-
- //check if there's a small gap after last column, if yes, fill it
- const int gapWidth = GetClientSize().GetWidth() - refParent().getColWidthsSum(GetClientSize().GetWidth());
- if (std::abs(gapWidth) < COLUMN_FILL_GAP_TOLERANCE)
- refParent().setColumnWidth(newWidth + gapWidth, col, ALLOW_GRID_EVENT);
-
- refParent().Refresh(); //refresh columns on main grid as well!
- }
- else if (activeClickOrMove_)
- {
- const int clientPosX = event.GetPosition().x;
- if (std::abs(clientPosX - activeClickOrMove_->getStartPosX()) > COLUMN_MOVE_DELAY) //real move (not a single click)
- {
- activeClickOrMove_->setRealMove();
-
- const ptrdiff_t col = refParent().clientPosToMoveTargetColumn(event.GetPosition());
- if (col >= 0)
- activeClickOrMove_->refColumnTo() = col;
- }
- }
- else
- {
- if (const Opt<ColAction> action = refParent().clientPosToColumnAction(event.GetPosition()))
- {
- highlightCol_ = action->col;
-
- if (action->wantResize)
- SetCursor(wxCURSOR_SIZEWE); //set window-local only! :)
- else
- SetCursor(*wxSTANDARD_CURSOR);
- }
- else
- {
- highlightCol_ = NoValue();
- SetCursor(*wxSTANDARD_CURSOR);
- }
- }
-
- const std::wstring toolTip = [&]
- {
- const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition());
- const ColumnType colType = refParent().getColumnAtPos(absPos.x).colType; //returns ColumnType::NONE if no column at x position!
- if (colType != ColumnType::NONE)
- if (auto prov = refParent().getDataProvider())
- return prov->getToolTip(colType);
- return std::wstring();
- }();
- setToolTip(toolTip);
-
- Refresh();
- event.Skip();
- }
-
- void onLeaveWindow(wxMouseEvent& event) override
- {
- highlightCol_ = NoValue(); //wxEVT_LEAVE_WINDOW does not respect mouse capture! -> however highlight_ is drawn unconditionally during move/resize!
- Refresh();
- event.Skip();
- }
-
- void onMouseRightDown(wxMouseEvent& event) override
- {
- if (const Opt<ColAction> action = refParent().clientPosToColumnAction(event.GetPosition()))
- {
- if (const Opt<ColumnType> colType = refParent().colToType(action->col))
- sendEventNow(GridLabelClickEvent(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, event, *colType)); //notify right click
- else assert(false);
- }
- else
- //notify right click (on free space after last column)
- if (fillGapAfterColumns)
- sendEventNow(GridLabelClickEvent(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, event, ColumnType::NONE));
-
- event.Skip();
- }
-
- std::unique_ptr<ColumnResizing> activeResizing_;
- std::unique_ptr<ColumnMove> activeClickOrMove_;
- Opt<size_t> highlightCol_; //column during mouse-over
-};
-
-//----------------------------------------------------------------------------------------------------------------
-namespace
-{
-const wxEventType EVENT_GRID_HAS_SCROLLED = wxNewEventType(); //internal to Grid::MainWin::ScrollWindow()
-}
-//----------------------------------------------------------------------------------------------------------------
-
-class Grid::MainWin : public SubWindow
-{
-public:
- MainWin(Grid& parent,
- RowLabelWin& rowLabelWin,
- ColLabelWin& colLabelWin) : SubWindow(parent),
- rowLabelWin_(rowLabelWin),
- colLabelWin_(colLabelWin)
- {
- Connect(EVENT_GRID_HAS_SCROLLED, wxEventHandler(MainWin::onRequestWindowUpdate), nullptr, this);
- }
-
- ~MainWin() { assert(!gridUpdatePending_); }
-
- size_t getCursor() const { return cursorRow_; }
- size_t getAnchor() const { return selectionAnchor_; }
-
- void setCursor(size_t newCursorRow, size_t newAnchorRow)
- {
- cursorRow_ = newCursorRow;
- selectionAnchor_ = newAnchorRow;
- activeSelection_.reset(); //e.g. user might search with F3 while holding down left mouse button
- }
-
-private:
- void render(wxDC& dc, const wxRect& rect) override
- {
- if (IsThisEnabled())
- clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
- else
- clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE));
-
- dc.SetFont(GetFont()); //harmonize with Grid::getBestColumnSize()
-
- wxDCTextColourChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels
-
- std::vector<ColumnWidth> absWidths = refParent().getColWidths(); //resolve stretched widths
- {
- int totalRowWidth = 0;
- for (const ColumnWidth& cw : absWidths)
- totalRowWidth += cw.width_;
-
- //fill gap after columns and cover full width
- if (fillGapAfterColumns)
- totalRowWidth = std::max(totalRowWidth, GetClientSize().GetWidth());
-
- if (auto prov = refParent().getDataProvider())
- {
- RecursiveDcClipper dummy2(dc, rect); //do NOT draw background on cells outside of invalidated rect invalidating foreground text!
-
- wxPoint cellAreaTL(refParent().CalcScrolledPosition(wxPoint(0, 0))); //client coordinates
- const int rowHeight = rowLabelWin_.getRowHeight();
- const auto rowRange = rowLabelWin_.getRowsOnClient(rect); //returns range [begin, end)
-
- //draw background lines
- for (auto row = rowRange.first; row < rowRange.second; ++row)
- {
- const wxRect rowRect(cellAreaTL + wxPoint(0, row * rowHeight), wxSize(totalRowWidth, rowHeight));
- RecursiveDcClipper dummy3(dc, rowRect);
- prov->renderRowBackgound(dc, rowRect, row, refParent().IsThisEnabled(), drawAsSelected(row));
- }
-
- //draw single cells, column by column
- for (const ColumnWidth& cw : absWidths)
- {
- if (cellAreaTL.x > rect.GetRight())
- return; //done
-
- if (cellAreaTL.x + cw.width_ > rect.x)
- for (auto row = rowRange.first; row < rowRange.second; ++row)
- {
- const wxRect cellRect(cellAreaTL.x, cellAreaTL.y + row * rowHeight, cw.width_, rowHeight);
- RecursiveDcClipper dummy3(dc, cellRect);
- prov->renderCell(dc, cellRect, row, cw.type_, refParent().IsThisEnabled(), drawAsSelected(row), getRowHoverToDraw(row));
- }
- cellAreaTL.x += cw.width_;
- }
- }
- }
- }
-
- HoverArea getRowHoverToDraw(ptrdiff_t row) const
- {
- if (activeSelection_)
- {
- if (activeSelection_->getFirstClick().row_ == row)
- return activeSelection_->getFirstClick().hoverArea_;
- }
- else if (highlight_.row == row)
- return highlight_.rowHover;
- return HoverArea::NONE;
- }
-
- bool drawAsSelected(size_t row) const
- {
- if (activeSelection_) //check if user is currently selecting with mouse
- {
- const size_t rowFrom = std::min(activeSelection_->getStartRow(), activeSelection_->getCurrentRow());
- const size_t rowTo = std::max(activeSelection_->getStartRow(), activeSelection_->getCurrentRow());
-
- if (rowFrom <= row && row <= rowTo)
- return activeSelection_->isPositiveSelect(); //overwrite default
- }
- return refParent().isSelected(row);
- }
-
- void onMouseLeftDown (wxMouseEvent& event) override { onMouseDown(event); }
- void onMouseLeftUp (wxMouseEvent& event) override { onMouseUp (event); }
- void onMouseRightDown(wxMouseEvent& event) override { onMouseDown(event); }
- void onMouseRightUp (wxMouseEvent& event) override { onMouseUp (event); }
-
- void onMouseLeftDouble(wxMouseEvent& event) override
- {
- if (auto prov = refParent().getDataProvider())
- {
- const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition());
- const ptrdiff_t row = rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range
- const ColumnPosInfo cpi = refParent().getColumnAtPos(absPos.x); //returns ColumnType::NONE if no column at x position!
- const HoverArea rowHover = prov->getRowMouseHover(row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth);
- //client is interested in all double-clicks, even those outside of the grid!
- sendEventNow(GridClickEvent(EVENT_GRID_MOUSE_LEFT_DOUBLE, event, row, rowHover));
- }
- event.Skip();
- }
-
- void onMouseDown(wxMouseEvent& event) //handle left and right mouse button clicks (almost) the same
- {
- if (wxWindow::FindFocus() != this) //doesn't seem to happen automatically for right mouse button
- SetFocus();
-
- if (auto prov = refParent().getDataProvider())
- {
- const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition());
- const ptrdiff_t row = rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range
- const ColumnPosInfo cpi = refParent().getColumnAtPos(absPos.x); //returns ColumnType::NONE if no column at x position!
- const HoverArea rowHover = prov->getRowMouseHover(row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth);
- //row < 0 possible!!! Pressing "Menu key" simulates Mouse Right Down + Up at position 0xffff/0xffff!
-
- GridClickEvent mouseEvent(event.RightDown() ? EVENT_GRID_MOUSE_RIGHT_DOWN : EVENT_GRID_MOUSE_LEFT_DOWN, event, row, rowHover);
-
- if (row >= 0)
- if (!event.RightDown() || !refParent().isSelected(row)) //do NOT start a new selection if user right-clicks on a selected area!
- {
- if (event.ControlDown())
- activeSelection_ = std::make_unique<MouseSelection>(*this, row, !refParent().isSelected(row), mouseEvent);
- else if (event.ShiftDown())
- {
- activeSelection_ = std::make_unique<MouseSelection>(*this, selectionAnchor_, true, mouseEvent);
- refParent().clearSelection(ALLOW_GRID_EVENT);
- }
- else
- {
- activeSelection_ = std::make_unique<MouseSelection>(*this, row, true, mouseEvent);
- refParent().clearSelection(ALLOW_GRID_EVENT);
- }
- }
- //notify event *after* potential "clearSelection(true)" above: a client should first receive a GridRangeSelectEvent for clearing the grid, if necessary,
- //then GridClickEvent and the associated GridRangeSelectEvent one after the other
- sendEventNow(mouseEvent);
-
- Refresh();
- }
- event.Skip(); //allow changing focus
- }
-
- void onMouseUp(wxMouseEvent& event)
- {
- if (activeSelection_)
- {
- const size_t rowCount = refParent().getRowCount();
- if (rowCount > 0)
- {
- if (activeSelection_->getCurrentRow() < rowCount)
- {
- cursorRow_ = activeSelection_->getCurrentRow();
- selectionAnchor_ = activeSelection_->getStartRow(); //allowed to be "out of range"
- }
- else if (activeSelection_->getStartRow() < rowCount) //don't change cursor if "to" and "from" are out of range
- {
- cursorRow_ = rowCount - 1;
- selectionAnchor_ = activeSelection_->getStartRow(); //allowed to be "out of range"
- }
- else //total selection "out of range"
- selectionAnchor_ = cursorRow_;
- }
- //slight deviation from Explorer: change cursor while dragging mouse! -> unify behavior with shift + direction keys
-
- refParent().selectRangeAndNotify(activeSelection_->getStartRow (), //from
- activeSelection_->getCurrentRow(), //to
- activeSelection_->isPositiveSelect(),
- &activeSelection_->getFirstClick());
- activeSelection_.reset();
- }
-
- if (auto prov = refParent().getDataProvider())
- {
- //this one may point to row which is not in visible area!
- const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition());
- const ptrdiff_t row = rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range
- const ColumnPosInfo cpi = refParent().getColumnAtPos(absPos.x); //returns ColumnType::NONE if no column at x position!
- const HoverArea rowHover = prov->getRowMouseHover(row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth);
- //notify click event after the range selection! e.g. this makes sure the selection is applied before showing a context menu
- sendEventNow(GridClickEvent(event.RightUp() ? EVENT_GRID_MOUSE_RIGHT_UP : EVENT_GRID_MOUSE_LEFT_UP, event, row, rowHover));
- }
-
- //update highlight_ and tooltip: on OS X no mouse movement event is generated after a mouse button click (unlike on Windows)
- event.SetPosition(ScreenToClient(wxGetMousePosition())); //mouse position may have changed within above callbacks (e.g. context menu was shown)!
- onMouseMovement(event);
-
- Refresh();
- event.Skip(); //allow changing focus
- }
-
- void onMouseCaptureLost(wxMouseCaptureLostEvent& event) override
- {
- activeSelection_.reset();
- highlight_.row = -1;
- Refresh();
- //event.Skip(); -> we DID handle it!
- }
-
- void onMouseMovement(wxMouseEvent& event) override
- {
- if (auto prov = refParent().getDataProvider())
- {
- const ptrdiff_t rowCount = refParent().getRowCount();
- const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition());
- const ptrdiff_t row = rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range
- const ColumnPosInfo cpi = refParent().getColumnAtPos(absPos.x); //returns ColumnType::NONE if no column at x position!
- const HoverArea rowHover = prov->getRowMouseHover(row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth);
-
- const std::wstring toolTip = [&]
- {
- if (cpi.colType != ColumnType::NONE && 0 <= row && row < rowCount)
- return prov->getToolTip(row, cpi.colType);
- return std::wstring();
- }();
- setToolTip(toolTip); //show even during mouse selection!
-
- if (activeSelection_)
- activeSelection_->evalMousePos(); //call on both mouse movement + timer event!
- else
- {
- refreshHighlight(highlight_);
- highlight_.row = row;
- highlight_.rowHover = rowHover;
- refreshHighlight(highlight_); //multiple Refresh() calls are condensed into single one!
- }
- }
- event.Skip();
- }
-
- void onLeaveWindow(wxMouseEvent& event) override //wxEVT_LEAVE_WINDOW does not respect mouse capture!
- {
- if (!activeSelection_)
- {
- refreshHighlight(highlight_);
- highlight_.row = -1;
- }
-
- event.Skip();
- }
-
-
- void onFocus(wxFocusEvent& event) override { Refresh(); event.Skip(); }
-
- class MouseSelection : private wxEvtHandler
- {
- public:
- MouseSelection(MainWin& wnd, size_t rowStart, bool positiveSelect, const GridClickEvent& firstClick) :
- wnd_(wnd), rowStart_(rowStart), rowCurrent_(rowStart), positiveSelect_(positiveSelect), firstClick_(firstClick)
- {
- wnd_.CaptureMouse();
- timer_.Connect(wxEVT_TIMER, wxEventHandler(MouseSelection::onTimer), nullptr, this);
- timer_.Start(100); //timer interval in ms
- evalMousePos();
- }
- ~MouseSelection() { if (wnd_.HasCapture()) wnd_.ReleaseMouse(); }
-
- size_t getStartRow () const { return rowStart_; }
- size_t getCurrentRow () const { return rowCurrent_; }
- bool isPositiveSelect() const { return positiveSelect_; } //are we selecting or unselecting?
- const GridClickEvent& getFirstClick() const { return firstClick_; }
-
- void evalMousePos()
- {
- const auto now = std::chrono::steady_clock::now();
- const double deltaSecs = std::chrono::duration<double>(now - lastEvalTime_).count(); //unit: [sec]
- lastEvalTime_ = now;
-
- const wxPoint clientPos = wnd_.ScreenToClient(wxGetMousePosition());
- const wxSize clientSize = wnd_.GetClientSize();
- assert(wnd_.GetClientAreaOrigin() == wxPoint());
-
- //scroll while dragging mouse
- const int overlapPixY = clientPos.y < 0 ? clientPos.y :
- clientPos.y >= clientSize.GetHeight() ? clientPos.y - (clientSize.GetHeight() - 1) : 0;
- const int overlapPixX = clientPos.x < 0 ? clientPos.x :
- clientPos.x >= clientSize.GetWidth() ? clientPos.x - (clientSize.GetWidth() - 1) : 0;
-
- int pixelsPerUnitY = 0;
- wnd_.refParent().GetScrollPixelsPerUnit(nullptr, &pixelsPerUnitY);
- if (pixelsPerUnitY <= 0) return;
-
- const double mouseDragSpeedIncScrollU = pixelsPerUnitY > 0 ? MOUSE_DRAG_ACCELERATION * wnd_.rowLabelWin_.getRowHeight() / pixelsPerUnitY : 0; //unit: [scroll units / (pixel * sec)]
-
- auto autoScroll = [&](int overlapPix, double& toScroll)
- {
- if (overlapPix != 0)
- {
- const double scrollSpeed = overlapPix * mouseDragSpeedIncScrollU; //unit: [scroll units / sec]
- toScroll += scrollSpeed * deltaSecs;
- }
- else
- toScroll = 0;
- };
-
- autoScroll(overlapPixX, toScrollX_);
- autoScroll(overlapPixY, toScrollY_);
-
- if (static_cast<int>(toScrollX_) != 0 || static_cast<int>(toScrollY_) != 0)
- {
- wnd_.refParent().scrollDelta(static_cast<int>(toScrollX_), static_cast<int>(toScrollY_)); //
- toScrollX_ -= static_cast<int>(toScrollX_); //rounds down for positive numbers, up for negative,
- toScrollY_ -= static_cast<int>(toScrollY_); //exactly what we want
- }
-
- //select current row *after* scrolling
- wxPoint clientPosTrimmed = clientPos;
- numeric::clamp(clientPosTrimmed.y, 0, clientSize.GetHeight() - 1); //do not select row outside client window!
-
- const wxPoint absPos = wnd_.refParent().CalcUnscrolledPosition(clientPosTrimmed);
- const ptrdiff_t newRow = wnd_.rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range
- if (newRow >= 0)
- if (rowCurrent_ != newRow)
- {
- rowCurrent_ = newRow;
- wnd_.Refresh();
- }
- }
-
- private:
- void onTimer(wxEvent& event) { evalMousePos(); }
-
- MainWin& wnd_;
- const size_t rowStart_;
- ptrdiff_t rowCurrent_;
- const bool positiveSelect_;
- const GridClickEvent firstClick_;
- wxTimer timer_;
- double toScrollX_ = 0; //count outstanding scroll unit fractions while dragging mouse
- double toScrollY_ = 0; //
- std::chrono::steady_clock::time_point lastEvalTime_ = std::chrono::steady_clock::now();
- };
-
- struct MouseHighlight
- {
- ptrdiff_t row = -1;
- HoverArea rowHover = HoverArea::NONE;
- };
-
- void ScrollWindow(int dx, int dy, const wxRect* rect) override
- {
- wxWindow::ScrollWindow(dx, dy, rect);
- rowLabelWin_.ScrollWindow(0, dy, rect);
- colLabelWin_.ScrollWindow(dx, 0, rect);
-
- //attention, wxGTK call sequence: wxScrolledWindow::Scroll() -> wxScrolledHelperNative::Scroll() -> wxScrolledHelperNative::DoScroll()
- //which *first* calls us, MainWin::ScrollWindow(), and *then* internally updates m_yScrollPosition
- //=> we cannot use CalcUnscrolledPosition() here which gives the wrong/outdated value!!!
- //=> we need to update asynchronously:
- //=> don't send async event repeatedly => severe performance issues on wxGTK!
- //=> can't use idle event neither: too few idle events on Windows, e.g. NO idle events while mouse drag-scrolling!
- //=> solution: send single async event at most!
- if (!gridUpdatePending_) //without guarding, the number of outstanding async events can become very high during scrolling!! test case: Ubuntu: 170; Windows: 20
- {
- gridUpdatePending_ = true;
- wxCommandEvent scrollEvent(EVENT_GRID_HAS_SCROLLED);
- AddPendingEvent(scrollEvent); //asynchronously call updateAfterScroll()
- }
- }
-
- void onRequestWindowUpdate(wxEvent& event)
- {
- assert(gridUpdatePending_);
- ZEN_ON_SCOPE_EXIT(gridUpdatePending_ = false);
-
- refParent().updateWindowSizes(false); //row label width has changed -> do *not* update scrollbars: recursion on wxGTK! -> still a problem, now that we're called async??
- rowLabelWin_.Update(); //update while dragging scroll thumb
- }
-
- void refreshRow(size_t row)
- {
- const wxRect& rowArea = rowLabelWin_.getRowLabelArea(row); //returns empty rect if row not found
- const wxPoint topLeft = refParent().CalcScrolledPosition(wxPoint(0, rowArea.y)); //absolute -> client coordinates
- wxRect cellArea(topLeft, wxSize(refParent().getColWidthsSum(GetClientSize().GetWidth()), rowArea.height));
- RefreshRect(cellArea, false);
- }
-
- void refreshHighlight(const MouseHighlight& hl)
- {
- const ptrdiff_t rowCount = refParent().getRowCount();
- if (0 <= hl.row && hl.row < rowCount && hl.rowHover != HoverArea::NONE) //no highlight_? => NOP!
- refreshRow(hl.row);
- }
-
- RowLabelWin& rowLabelWin_;
- ColLabelWin& colLabelWin_;
-
- std::unique_ptr<MouseSelection> activeSelection_; //bound while user is selecting with mouse
- MouseHighlight highlight_; //current mouse highlight_ (superseeded by activeSelection_ if available)
-
- ptrdiff_t cursorRow_ = 0;
- size_t selectionAnchor_ = 0;
- bool gridUpdatePending_ = false;
-};
-
-//----------------------------------------------------------------------------------------------------------------
-//----------------------------------------------------------------------------------------------------------------
-
-Grid::Grid(wxWindow* parent,
- wxWindowID id,
- const wxPoint& pos,
- const wxSize& size,
- long style,
- const wxString& name) : wxScrolledWindow(parent, id, pos, size, style | wxWANTS_CHARS, name)
-{
- cornerWin_ = new CornerWin (*this); //
- rowLabelWin_ = new RowLabelWin(*this); //owership handled by "this"
- colLabelWin_ = new ColLabelWin(*this); //
- mainWin_ = new MainWin (*this, *rowLabelWin_, *colLabelWin_); //
-
- colLabelHeight_ = 2 * DEFAULT_COL_LABEL_BORDER + [&]() -> int
- {
- //coordinate with ColLabelWin::render():
- wxFont labelFont = colLabelWin_->GetFont();
- labelFont.SetWeight(wxFONTWEIGHT_BOLD);
- return labelFont.GetPixelSize().GetHeight();
- }();
-
- SetTargetWindow(mainWin_);
-
- SetInitialSize(size); //"Most controls will use this to set their initial size" -> why not
-
- assert(GetClientSize() == GetSize()); //borders are NOT allowed for Grid
- //reason: updateWindowSizes() wants to use "GetSize()" as a "GetClientSize()" including scrollbars
-
- Connect(wxEVT_PAINT, wxPaintEventHandler(Grid::onPaintEvent ), nullptr, this);
- Connect(wxEVT_ERASE_BACKGROUND, wxEraseEventHandler(Grid::onEraseBackGround), nullptr, this);
- Connect(wxEVT_SIZE, wxSizeEventHandler (Grid::onSizeEvent ), nullptr, this);
-
- Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(Grid::onKeyDown), nullptr, this);
-}
-
-
-void Grid::updateWindowSizes(bool updateScrollbar)
-{
- /* We have to deal with TWO nasty circular dependencies:
- 1.
- rowLabelWidth
- /|\
- mainWin::client width
- /|\
- SetScrollbars -> show/hide horizontal scrollbar depending on client width
- /|\
- mainWin::client height -> possibly trimmed by horizontal scrollbars
- /|\
- rowLabelWidth
-
- 2.
- mainWin_->GetClientSize()
- /|\
- SetScrollbars -> show/hide scrollbars depending on whether client size is big enough
- /|\
- GetClientSize(); -> possibly trimmed by scrollbars
- /|\
- mainWin_->GetClientSize() -> also trimmed, since it's a sub-window!
- */
-
- //break this vicious circle:
-
- //harmonize with Grid::GetSizeAvailableForScrollTarget()!
-
- //1. calculate row label width independent from scrollbars
- const int mainWinHeightGross = std::max(GetSize().GetHeight() - colLabelHeight_, 0); //independent from client sizes and scrollbars!
- const ptrdiff_t logicalHeight = rowLabelWin_->getLogicalHeight(); //
-
- int rowLabelWidth = 0;
- if (drawRowLabel_ && logicalHeight > 0)
- {
- ptrdiff_t yFrom = CalcUnscrolledPosition(wxPoint(0, 0)).y;
- ptrdiff_t yTo = CalcUnscrolledPosition(wxPoint(0, mainWinHeightGross - 1)).y ;
- numeric::clamp<ptrdiff_t>(yFrom, 0, logicalHeight - 1);
- numeric::clamp<ptrdiff_t>(yTo, 0, logicalHeight - 1);
-
- const ptrdiff_t rowFrom = rowLabelWin_->getRowAtPos(yFrom);
- const ptrdiff_t rowTo = rowLabelWin_->getRowAtPos(yTo);
- if (rowFrom >= 0 && rowTo >= 0)
- rowLabelWidth = rowLabelWin_->getBestWidth(rowFrom, rowTo);
- }
-
- auto getMainWinSize = [&](const wxSize& clientSize) { return wxSize(std::max(0, clientSize.GetWidth() - rowLabelWidth), std::max(0, clientSize.GetHeight() - colLabelHeight_)); };
-
- auto setScrollbars2 = [&](int logWidth, int logHeight) //replace SetScrollbars, which loses precision of pixelsPerUnitX for some brain-dead reason
- {
- mainWin_->SetVirtualSize(logWidth, logHeight); //set before calling SetScrollRate():
- //else SetScrollRate() would fail to preserve scroll position when "new virtual pixel-pos > old virtual height"
-
- int ppsuX = 0; //pixel per scroll unit
- int ppsuY = 0;
- GetScrollPixelsPerUnit(&ppsuX, &ppsuY);
-
- const int ppsuNew = rowLabelWin_->getRowHeight();
- if (ppsuX != ppsuNew || ppsuY != ppsuNew) //support polling!
- SetScrollRate(ppsuNew, ppsuNew); //internally calls AdjustScrollbars() and GetVirtualSize()!
-
- AdjustScrollbars(); //lousy wxWidgets design decision: internally calls mainWin_->GetClientSize() without considering impact of scrollbars!
- //Attention: setting scrollbars triggers *synchronous* resize event if scrollbars are shown or hidden! => updateWindowSizes() recursion! (Windows)
- };
-
- //2. update managed windows' sizes: just assume scrollbars are already set correctly, even if they may not be (yet)!
- //this ensures mainWin_->SetVirtualSize() and AdjustScrollbars() are working with the correct main window size, unless sb change later, which triggers a recalculation anyway!
- const wxSize mainWinSize = getMainWinSize(GetClientSize());
-
- cornerWin_ ->SetSize(0, 0, rowLabelWidth, colLabelHeight_);
- rowLabelWin_->SetSize(0, colLabelHeight_, rowLabelWidth, mainWinSize.GetHeight());
- colLabelWin_->SetSize(rowLabelWidth, 0, mainWinSize.GetWidth(), colLabelHeight_);
- mainWin_ ->SetSize(rowLabelWidth, colLabelHeight_, mainWinSize.GetWidth(), mainWinSize.GetHeight());
-
- //avoid flicker in wxWindowMSW::HandleSize() when calling ::EndDeferWindowPos() where the sub-windows are moved only although they need to be redrawn!
- colLabelWin_->Refresh();
- mainWin_ ->Refresh();
-
- //3. update scrollbars: "guide wxScrolledHelper to not screw up too much"
- if (updateScrollbar)
- {
- const int mainWinWidthGross = getMainWinSize(GetSize()).GetWidth();
-
- if (logicalHeight <= mainWinHeightGross &&
- getColWidthsSum(mainWinWidthGross) <= mainWinWidthGross &&
- //this special case needs to be considered *only* when both scrollbars are flexible:
- showScrollbarX_ == SB_SHOW_AUTOMATIC &&
- showScrollbarY_ == SB_SHOW_AUTOMATIC)
- setScrollbars2(0, 0); //no scrollbars required at all! -> wxScrolledWindow requires active help to detect this special case!
- else
- {
- const int logicalWidthTmp = getColWidthsSum(mainWinSize.GetWidth()); //assuming vertical scrollbar stays as it is...
- setScrollbars2(logicalWidthTmp, logicalHeight); //if scrollbars are shown or hidden a new resize event recurses into updateWindowSizes()
- /*
- is there a risk of endless recursion? No, 2-level recursion at most, consider the following 6 cases:
-
- <----------gw---------->
- <----------nw------>
- ------------------------ /|\ /|\
- | | | | |
- | main window | | nh |
- | | | | gh
- ------------------------ \|/ |
- | | | |
- ------------------------ \|/
- gw := gross width
- nw := net width := gross width - sb size
- gh := gross height
- nh := net height := gross height - sb size
-
- There are 6 cases that can occur:
- ---------------------------------
- lw := logical width
- lh := logical height
-
- 1. lw <= gw && lh <= gh => no scrollbars needed
-
- 2. lw > gw && lh > gh => need both scrollbars
-
- 3. lh > gh
- 4.1 lw <= nw => need vertical scrollbar only
- 4.2 nw < lw <= gw => need both scrollbars
-
- 4. lw > gw
- 3.1 lh <= nh => need horizontal scrollbar only
- 3.2 nh < lh <= gh => need both scrollbars
- */
- }
- }
-}
-
-
-wxSize Grid::GetSizeAvailableForScrollTarget(const wxSize& size)
-{
- //harmonize with Grid::updateWindowSizes()!
-
- //1. calculate row label width independent from scrollbars
- const int mainWinHeightGross = std::max(size.GetHeight() - colLabelHeight_, 0); //independent from client sizes and scrollbars!
- const ptrdiff_t logicalHeight = rowLabelWin_->getLogicalHeight(); //
-
- int rowLabelWidth = 0;
- if (drawRowLabel_ && logicalHeight > 0)
- {
- ptrdiff_t yFrom = CalcUnscrolledPosition(wxPoint(0, 0)).y;
- ptrdiff_t yTo = CalcUnscrolledPosition(wxPoint(0, mainWinHeightGross - 1)).y ;
- numeric::clamp<ptrdiff_t>(yFrom, 0, logicalHeight - 1);
- numeric::clamp<ptrdiff_t>(yTo, 0, logicalHeight - 1);
-
- const ptrdiff_t rowFrom = rowLabelWin_->getRowAtPos(yFrom);
- const ptrdiff_t rowTo = rowLabelWin_->getRowAtPos(yTo);
- if (rowFrom >= 0 && rowTo >= 0)
- rowLabelWidth = rowLabelWin_->getBestWidth(rowFrom, rowTo);
- }
-
- return size - wxSize(rowLabelWidth, colLabelHeight_);
-}
-
-
-void Grid::onPaintEvent(wxPaintEvent& event) { wxPaintDC dc(this); }
-
-
-void Grid::onKeyDown(wxKeyEvent& event)
-{
- int keyCode = event.GetKeyCode();
- if (GetLayoutDirection() == wxLayout_RightToLeft)
- {
- if (keyCode == WXK_LEFT || keyCode == WXK_NUMPAD_LEFT)
- keyCode = WXK_RIGHT;
- else if (keyCode == WXK_RIGHT || keyCode == WXK_NUMPAD_RIGHT)
- keyCode = WXK_LEFT;
- }
-
- const ptrdiff_t rowCount = getRowCount();
- const ptrdiff_t cursorRow = mainWin_->getCursor();
-
- auto moveCursorTo = [&](ptrdiff_t row)
- {
- if (rowCount > 0)
- {
- numeric::clamp<ptrdiff_t>(row, 0, rowCount - 1);
- setGridCursor(row);
- }
- };
-
- auto selectWithCursorTo = [&](ptrdiff_t row)
- {
- if (rowCount > 0)
- {
- numeric::clamp<ptrdiff_t>(row, 0, rowCount - 1);
- selectWithCursor(row);
- }
- };
-
- switch (keyCode)
- {
- //case WXK_TAB:
- // if (Navigate(event.ShiftDown() ? wxNavigationKeyEvent::IsBackward : wxNavigationKeyEvent::IsForward))
- // return;
- // break;
-
- case WXK_UP:
- case WXK_NUMPAD_UP:
- if (event.ShiftDown())
- selectWithCursorTo(cursorRow - 1);
- else if (event.ControlDown())
- scrollDelta(0, -1);
- else
- moveCursorTo(cursorRow - 1);
- return; //swallow event: wxScrolledWindow, wxWidgets 2.9.3 on Kubuntu x64 processes arrow keys: prevent this!
-
- case WXK_DOWN:
- case WXK_NUMPAD_DOWN:
- if (event.ShiftDown())
- selectWithCursorTo(cursorRow + 1);
- else if (event.ControlDown())
- scrollDelta(0, 1);
- else
- moveCursorTo(cursorRow + 1);
- return; //swallow event
-
- case WXK_LEFT:
- case WXK_NUMPAD_LEFT:
- if (event.ControlDown())
- scrollDelta(-1, 0);
- else if (event.ShiftDown())
- ;
- else
- moveCursorTo(cursorRow);
- return;
-
- case WXK_RIGHT:
- case WXK_NUMPAD_RIGHT:
- if (event.ControlDown())
- scrollDelta(1, 0);
- else if (event.ShiftDown())
- ;
- else
- moveCursorTo(cursorRow);
- return;
-
- case WXK_HOME:
- case WXK_NUMPAD_HOME:
- if (event.ShiftDown())
- selectWithCursorTo(0);
- //else if (event.ControlDown())
- // ;
- else
- moveCursorTo(0);
- return;
-
- case WXK_END:
- case WXK_NUMPAD_END:
- if (event.ShiftDown())
- selectWithCursorTo(rowCount - 1);
- //else if (event.ControlDown())
- // ;
- else
- moveCursorTo(rowCount - 1);
- return;
-
- case WXK_PAGEUP:
- case WXK_NUMPAD_PAGEUP:
- if (event.ShiftDown())
- selectWithCursorTo(cursorRow - GetClientSize().GetHeight() / rowLabelWin_->getRowHeight());
- //else if (event.ControlDown())
- // ;
- else
- moveCursorTo(cursorRow - GetClientSize().GetHeight() / rowLabelWin_->getRowHeight());
- return;
-
- case WXK_PAGEDOWN:
- case WXK_NUMPAD_PAGEDOWN:
- if (event.ShiftDown())
- selectWithCursorTo(cursorRow + GetClientSize().GetHeight() / rowLabelWin_->getRowHeight());
- //else if (event.ControlDown())
- // ;
- else
- moveCursorTo(cursorRow + GetClientSize().GetHeight() / rowLabelWin_->getRowHeight());
- return;
-
- case 'A': //Ctrl + A - select all
- if (event.ControlDown())
- selectRangeAndNotify(0, rowCount, true /*positive*/, nullptr /*mouseInitiated*/);
- break;
-
- case WXK_NUMPAD_ADD: //CTRL + '+' - auto-size all
- if (event.ControlDown())
- autoSizeColumns(ALLOW_GRID_EVENT);
- return;
- }
-
- event.Skip();
-}
-
-
-void Grid::setColumnLabelHeight(int height)
-{
- colLabelHeight_ = std::max(0, height);
- updateWindowSizes();
-}
-
-
-void Grid::showRowLabel(bool show)
-{
- drawRowLabel_ = show;
- updateWindowSizes();
-}
-
-
-void Grid::selectAllRows(GridEventPolicy rangeEventPolicy)
-{
- selection_.selectAll();
- mainWin_->Refresh();
-
- if (rangeEventPolicy == ALLOW_GRID_EVENT) //notify event, even if we're not triggered by user interaction
- {
- GridRangeSelectEvent selEvent(0, getRowCount(), true, nullptr);
- if (wxEvtHandler* evtHandler = GetEventHandler())
- evtHandler->ProcessEvent(selEvent);
- }
-}
-
-
-void Grid::clearSelection(GridEventPolicy rangeEventPolicy)
-{
- selection_.clear();
- mainWin_->Refresh();
-
- if (rangeEventPolicy == ALLOW_GRID_EVENT) //notify event, even if we're not triggered by user interaction
- {
- GridRangeSelectEvent unselectionEvent(0, getRowCount(), false, nullptr);
- if (wxEvtHandler* evtHandler = GetEventHandler())
- evtHandler->ProcessEvent(unselectionEvent);
- }
-}
-
-
-void Grid::scrollDelta(int deltaX, int deltaY)
-{
- int scrollPosX = 0;
- int scrollPosY = 0;
- GetViewStart(&scrollPosX, &scrollPosY);
-
- scrollPosX += deltaX;
- scrollPosY += deltaY;
-
- scrollPosX = std::max(0, scrollPosX); //wxScrollHelper::Scroll() will exit prematurely if input happens to be "-1"!
- scrollPosY = std::max(0, scrollPosY); //
-
- Scroll(scrollPosX, scrollPosY); //internally calls wxWindows::Update()!
- updateWindowSizes(); //may show horizontal scroll bar
-}
-
-
-void Grid::redirectRowLabelEvent(wxMouseEvent& event)
-{
- event.m_x = 0;
- if (wxEvtHandler* evtHandler = mainWin_->GetEventHandler())
- evtHandler->ProcessEvent(event);
-
- if (event.ButtonDown() && wxWindow::FindFocus() != mainWin_)
- mainWin_->SetFocus();
-}
-
-
-size_t Grid::getRowCount() const
-{
- return dataView_ ? dataView_->getRowCount() : 0;
-}
-
-
-void Grid::Refresh(bool eraseBackground, const wxRect* rect)
-{
- const size_t rowCountNew = getRowCount();
- if (rowCountOld_ != rowCountNew)
- {
- rowCountOld_ = rowCountNew;
- updateWindowSizes();
- }
-
- if (selection_.maxSize() != rowCountNew) //clear selection only when needed (consider setSelectedRows())
- selection_.init(rowCountNew);
-
- wxScrolledWindow::Refresh(eraseBackground, rect);
-}
-
-
-void Grid::setRowHeight(int height)
-{
- rowLabelWin_->setRowHeight(height);
- updateWindowSizes();
- Refresh();
-}
-
-
-void Grid::setColumnConfig(const std::vector<Grid::ColumnAttribute>& attr)
-{
- //hold ownership of non-visible columns
- oldColAttributes_ = attr;
-
- std::vector<VisibleColumn> visCols;
- for (const ColumnAttribute& ca : attr)
- {
- assert(ca.type_ != ColumnType::NONE);
- if (ca.visible_)
- visCols.emplace_back(ca.type_, ca.offset_, ca.stretch_);
- }
-
- //"ownership" of visible columns is now within Grid
- visibleCols_ = visCols;
-
- updateWindowSizes();
- Refresh();
-}
-
-
-std::vector<Grid::ColumnAttribute> Grid::getColumnConfig() const
-{
- //get non-visible columns (+ outdated visible ones)
- std::vector<ColumnAttribute> output = oldColAttributes_;
-
- auto iterVcols = visibleCols_.begin();
- auto iterVcolsend = visibleCols_.end();
-
- //update visible columns but keep order of non-visible ones!
- for (ColumnAttribute& ca : output)
- if (ca.visible_)
- {
- if (iterVcols != iterVcolsend)
- {
- ca.type_ = iterVcols->type_;
- ca.stretch_ = iterVcols->stretch_;
- ca.offset_ = iterVcols->offset_;
- ++iterVcols;
- }
- else
- assert(false);
- }
- assert(iterVcols == iterVcolsend);
-
- return output;
-}
-
-
-void Grid::showScrollBars(Grid::ScrollBarStatus horizontal, Grid::ScrollBarStatus vertical)
-{
- if (showScrollbarX_ == horizontal &&
- showScrollbarY_ == vertical) return; //support polling!
-
- showScrollbarX_ = horizontal;
- showScrollbarY_ = vertical;
-
- //the following wxGTK approach is pretty much identical to wxWidgets 2.9 ShowScrollbars() code!
-
- auto mapStatus = [](ScrollBarStatus sbStatus) -> GtkPolicyType
- {
- switch (sbStatus)
- {
- case SB_SHOW_AUTOMATIC:
- return GTK_POLICY_AUTOMATIC;
- case SB_SHOW_ALWAYS:
- return GTK_POLICY_ALWAYS;
- case SB_SHOW_NEVER:
- return GTK_POLICY_NEVER;
- }
- assert(false);
- return GTK_POLICY_AUTOMATIC;
- };
-
- GtkWidget* gridWidget = wxWindow::m_widget;
- GtkScrolledWindow* scrolledWindow = GTK_SCROLLED_WINDOW(gridWidget);
- ::gtk_scrolled_window_set_policy(scrolledWindow,
- mapStatus(horizontal),
- mapStatus(vertical));
-
- updateWindowSizes();
-}
-
-
-
-wxWindow& Grid::getCornerWin () { return *cornerWin_; }
-wxWindow& Grid::getRowLabelWin() { return *rowLabelWin_; }
-wxWindow& Grid::getColLabelWin() { return *colLabelWin_; }
-wxWindow& Grid::getMainWin () { return *mainWin_; }
-const wxWindow& Grid::getMainWin() const { return *mainWin_; }
-
-
-Opt<Grid::ColAction> Grid::clientPosToColumnAction(const wxPoint& pos) const
-{
- const int absPosX = CalcUnscrolledPosition(pos).x;
- if (absPosX >= 0)
- {
- const int resizeTolerance = allowColumnResize_ ? COLUMN_RESIZE_TOLERANCE : 0;
- std::vector<ColumnWidth> absWidths = getColWidths(); //resolve stretched widths
-
- int accuWidth = 0;
- for (size_t col = 0; col < absWidths.size(); ++col)
- {
- accuWidth += absWidths[col].width_;
- if (std::abs(absPosX - accuWidth) < resizeTolerance)
- {
- ColAction out;
- out.wantResize = true;
- out.col = col;
- return out;
- }
- else if (absPosX < accuWidth)
- {
- ColAction out;
- out.wantResize = false;
- out.col = col;
- return out;
- }
- }
- }
- return NoValue();
-}
-
-
-void Grid::moveColumn(size_t colFrom, size_t colTo)
-{
- if (colFrom < visibleCols_.size() &&
- colTo < visibleCols_.size() &&
- colTo != colFrom)
- {
- const VisibleColumn colAtt = visibleCols_[colFrom];
- visibleCols_.erase (visibleCols_.begin() + colFrom);
- visibleCols_.insert(visibleCols_.begin() + colTo, colAtt);
- }
-}
-
-
-ptrdiff_t Grid::clientPosToMoveTargetColumn(const wxPoint& pos) const
-{
-
- const int absPosX = CalcUnscrolledPosition(pos).x;
-
- int accWidth = 0;
- std::vector<ColumnWidth> absWidths = getColWidths(); //resolve negative/stretched widths
- for (auto itCol = absWidths.begin(); itCol != absWidths.end(); ++itCol)
- {
- const int width = itCol->width_; //beware dreaded unsigned conversions!
- accWidth += width;
-
- if (absPosX < accWidth - width / 2)
- return itCol - absWidths.begin();
- }
- return absWidths.size();
-}
-
-
-ColumnType Grid::colToType(size_t col) const
-{
- if (col < visibleCols_.size())
- return visibleCols_[col].type_;
- return ColumnType::NONE;
-}
-
-
-ptrdiff_t Grid::getRowAtPos(int posY) const { return rowLabelWin_->getRowAtPos(posY); }
-
-
-Grid::ColumnPosInfo Grid::getColumnAtPos(int posX) const
-{
- if (posX >= 0)
- {
- int accWidth = 0;
- for (const ColumnWidth& cw : getColWidths())
- {
- accWidth += cw.width_;
- if (posX < accWidth)
- return { cw.type_, posX + cw.width_ - accWidth, cw.width_ };
- }
- }
- return { ColumnType::NONE, 0, 0 };
-}
-
-
-wxRect Grid::getColumnLabelArea(ColumnType colType) const
-{
- std::vector<ColumnWidth> absWidths = getColWidths(); //resolve negative/stretched widths
-
- //colType is not unique in general, but *this* function expects it!
- assert(std::count_if(absWidths.begin(), absWidths.end(), [&](const ColumnWidth& cw) { return cw.type_ == colType; }) <= 1);
-
- auto itCol = std::find_if(absWidths.begin(), absWidths.end(), [&](const ColumnWidth& cw) { return cw.type_ == colType; });
- if (itCol != absWidths.end())
- {
- ptrdiff_t posX = 0;
- for (auto it = absWidths.begin(); it != itCol; ++it)
- posX += it->width_;
-
- return wxRect(wxPoint(posX, 0), wxSize(itCol->width_, colLabelHeight_));
- }
- return wxRect();
-}
-
-
-void Grid::refreshCell(size_t row, ColumnType colType)
-{
- const wxRect& colArea = getColumnLabelArea(colType); //returns empty rect if column not found
- const wxRect& rowArea = rowLabelWin_->getRowLabelArea(row); //returns empty rect if row not found
- if (colArea.height > 0 && rowArea.height > 0)
- {
- const wxPoint topLeft = CalcScrolledPosition(wxPoint(colArea.x, rowArea.y)); //absolute -> client coordinates
- const wxRect cellArea(topLeft, wxSize(colArea.width, rowArea.height));
-
- getMainWin().RefreshRect(cellArea, false);
- }
-}
-
-
-void Grid::setGridCursor(size_t row)
-{
- mainWin_->setCursor(row, row);
- makeRowVisible(row);
-
- selection_.clear(); //clear selection, do NOT fire event
- selectRangeAndNotify(row, row, true /*positive*/, nullptr /*mouseInitiated*/); //set new selection + fire event
-
- mainWin_->Refresh();
- rowLabelWin_->Refresh(); //row labels! (Kubuntu)
-}
-
-
-void Grid::selectWithCursor(ptrdiff_t row)
-{
- const size_t anchorRow = mainWin_->getAnchor();
-
- mainWin_->setCursor(row, anchorRow);
- makeRowVisible(row);
-
- selection_.clear(); //clear selection, do NOT fire event
- selectRangeAndNotify(anchorRow, row, true /*positive*/, nullptr /*mouseInitiated*/); //set new selection + fire event
-
- mainWin_->Refresh();
- rowLabelWin_->Refresh();
-}
-
-
-void Grid::makeRowVisible(size_t row)
-{
- const wxRect labelRect = rowLabelWin_->getRowLabelArea(row); //returns empty rect if row not found
- if (labelRect.height > 0)
- {
- int scrollPosX = 0;
- GetViewStart(&scrollPosX, nullptr);
-
- int pixelsPerUnitY = 0;
- GetScrollPixelsPerUnit(nullptr, &pixelsPerUnitY);
- if (pixelsPerUnitY <= 0) return;
-
- const int clientPosY = CalcScrolledPosition(labelRect.GetTopLeft()).y;
- if (clientPosY < 0)
- {
- const int scrollPosY = labelRect.y / pixelsPerUnitY;
- Scroll(scrollPosX, scrollPosY); //internally calls wxWindows::Update()!
- updateWindowSizes(); //may show horizontal scroll bar
- }
- else if (clientPosY + labelRect.height > rowLabelWin_->GetClientSize().GetHeight())
- {
- auto execScroll = [&](int clientHeight)
- {
- const int scrollPosY = std::ceil((labelRect.y - clientHeight +
- labelRect.height) / static_cast<double>(pixelsPerUnitY));
- Scroll(scrollPosX, scrollPosY);
- updateWindowSizes(); //may show horizontal scroll bar
- };
-
- const int clientHeightBefore = rowLabelWin_->GetClientSize().GetHeight();
- execScroll(clientHeightBefore);
-
- //client height may decrease after scroll due to a new horizontal scrollbar, resulting in a partially visible last row
- const int clientHeightAfter = rowLabelWin_->GetClientSize().GetHeight();
- if (clientHeightAfter < clientHeightBefore)
- execScroll(clientHeightAfter);
- }
- }
-}
-
-
-void Grid::selectRangeAndNotify(ptrdiff_t rowFrom, ptrdiff_t rowTo, bool positive, const GridClickEvent* mouseInitiated)
-{
- //sort + convert to half-open range
- auto rowFirst = std::min(rowFrom, rowTo);
- auto rowLast = std::max(rowFrom, rowTo) + 1;
-
- const size_t rowCount = getRowCount();
- numeric::clamp<ptrdiff_t>(rowFirst, 0, rowCount);
- numeric::clamp<ptrdiff_t>(rowLast, 0, rowCount);
-
- selection_.selectRange(rowFirst, rowLast, positive);
-
- //notify event
- GridRangeSelectEvent selectionEvent(rowFirst, rowLast, positive, mouseInitiated);
- if (wxEvtHandler* evtHandler = GetEventHandler())
- evtHandler->ProcessEvent(selectionEvent);
-
- mainWin_->Refresh();
-}
-
-
-void Grid::scrollTo(size_t row)
-{
- const wxRect labelRect = rowLabelWin_->getRowLabelArea(row); //returns empty rect if row not found
- if (labelRect.height > 0)
- {
- int pixelsPerUnitY = 0;
- GetScrollPixelsPerUnit(nullptr, &pixelsPerUnitY);
- if (pixelsPerUnitY > 0)
- {
- const int scrollPosYNew = labelRect.y / pixelsPerUnitY;
- int scrollPosXOld = 0;
- int scrollPosYOld = 0;
- GetViewStart(&scrollPosXOld, &scrollPosYOld);
-
- if (scrollPosYOld != scrollPosYNew) //support polling
- {
- Scroll(scrollPosXOld, scrollPosYNew); //internally calls wxWindows::Update()!
- updateWindowSizes(); //may show horizontal scroll bar
- Refresh();
- }
- }
- }
-}
-
-
-bool Grid::Enable(bool enable)
-{
- Refresh();
- return wxScrolledWindow::Enable(enable);
-}
-
-
-size_t Grid::getGridCursor() const
-{
- return mainWin_->getCursor();
-}
-
-
-int Grid::getBestColumnSize(size_t col) const
-{
- if (dataView_ && col < visibleCols_.size())
- {
- const ColumnType type = visibleCols_[col].type_;
-
- wxClientDC dc(mainWin_);
- dc.SetFont(mainWin_->GetFont()); //harmonize with MainWin::render()
-
- int maxSize = 0;
-
- auto rowRange = rowLabelWin_->getRowsOnClient(mainWin_->GetClientRect()); //returns range [begin, end)
- for (auto row = rowRange.first; row < rowRange.second; ++row)
- maxSize = std::max(maxSize, dataView_->getBestSize(dc, row, type));
-
- return maxSize;
- }
- return -1;
-}
-
-
-void Grid::setColumnWidth(int width, size_t col, GridEventPolicy columnResizeEventPolicy, bool notifyAsync)
-{
- if (col < visibleCols_.size())
- {
- VisibleColumn& vcRs = visibleCols_[col];
-
- const std::vector<int> stretchedWidths = getColStretchedWidths(mainWin_->GetClientSize().GetWidth());
- if (stretchedWidths.size() != visibleCols_.size())
- {
- assert(false);
- return;
- }
- //CAVEATS:
- //I. fixed-size columns: normalize offset so that resulting width is at least COLUMN_MIN_WIDTH: this is NOT enforced by getColWidths()!
- //II. stretched columns: do not allow user to set offsets so small that they result in negative (non-normalized) widths: this gives an
- //unusual delay when enlarging the column again later
- width = std::max(width, COLUMN_MIN_WIDTH);
-
- vcRs.offset_ = width - stretchedWidths[col]; //width := stretchedWidth + offset
-
- //III. resizing any column should normalize *all* other stretched columns' offsets considering current mainWinWidth!
- // test case:
- //1. have columns, both fixed-size and stretched, fit whole window width
- //2. shrink main window width so that horizontal scrollbars are shown despite the streched column
- //3. shrink a fixed-size column so that the scrollbars vanish and columns cover full width again
- //4. now verify that the stretched column is resizing immediately if main window is enlarged again
- for (size_t col2 = 0; col2 < visibleCols_.size(); ++col2)
- if (visibleCols_[col2].stretch_ > 0) //normalize stretched columns only
- visibleCols_[col2].offset_ = std::max(visibleCols_[col2].offset_, COLUMN_MIN_WIDTH - stretchedWidths[col2]);
-
- if (columnResizeEventPolicy == ALLOW_GRID_EVENT)
- {
- GridColumnResizeEvent sizeEvent(vcRs.offset_, vcRs.type_);
- if (wxEvtHandler* evtHandler = GetEventHandler())
- {
- if (notifyAsync)
- evtHandler->AddPendingEvent(sizeEvent);
- else
- evtHandler->ProcessEvent(sizeEvent);
- }
- }
- }
- else
- assert(false);
-}
-
-
-void Grid::autoSizeColumns(GridEventPolicy columnResizeEventPolicy)
-{
- if (allowColumnResize_)
- {
- for (size_t col = 0; col < visibleCols_.size(); ++col)
- {
- const int bestWidth = getBestColumnSize(col); //return -1 on error
- if (bestWidth >= 0)
- setColumnWidth(bestWidth, col, columnResizeEventPolicy, true);
- }
- updateWindowSizes();
- Refresh();
- }
-}
-
-
-std::vector<int> Grid::getColStretchedWidths(int clientWidth) const //final width = (normalized) (stretchedWidth + offset)
-{
- assert(clientWidth >= 0);
- clientWidth = std::max(clientWidth, 0);
- int stretchTotal = 0;
- for (const VisibleColumn& vc : visibleCols_)
- {
- assert(vc.stretch_ >= 0);
- stretchTotal += vc.stretch_;
- }
-
- int remainingWidth = clientWidth;
-
- std::vector<int> output;
-
- if (stretchTotal <= 0)
- output.resize(visibleCols_.size()); //fill with zeros
- else
- {
- for (const VisibleColumn& vc : visibleCols_)
- {
- const int width = clientWidth * vc.stretch_ / stretchTotal; //rounds down!
- output.push_back(width);
- remainingWidth -= width;
- }
-
- //distribute *all* of clientWidth: should suffice to enlarge the first few stretched columns; no need to minimize total absolute error of distribution
- if (remainingWidth > 0)
- for (size_t col2 = 0; col2 < visibleCols_.size(); ++col2)
- if (visibleCols_[col2].stretch_ > 0)
- {
- ++output[col2];
- if (--remainingWidth == 0)
- break;
- }
- assert(remainingWidth == 0);
- }
- return output;
-}
-
-
-std::vector<Grid::ColumnWidth> Grid::getColWidths() const
-{
- return getColWidths(mainWin_->GetClientSize().GetWidth());
-}
-
-
-std::vector<Grid::ColumnWidth> Grid::getColWidths(int mainWinWidth) const //evaluate stretched columns
-{
- const std::vector<int> stretchedWidths = getColStretchedWidths(mainWinWidth);
- assert(stretchedWidths.size() == visibleCols_.size());
-
- std::vector<ColumnWidth> output;
- for (size_t col2 = 0; col2 < visibleCols_.size(); ++col2)
- {
- const auto& vc = visibleCols_[col2];
- int width = stretchedWidths[col2] + vc.offset_;
-
- if (vc.stretch_ > 0)
- width = std::max(width, COLUMN_MIN_WIDTH); //normalization really needed here: e.g. smaller main window would result in negative width
- else
- width = std::max(width, 0); //support smaller width than COLUMN_MIN_WIDTH if set via configuration
-
- output.emplace_back(vc.type_, width);
- }
- return output;
-}
-
-
-int Grid::getColWidthsSum(int mainWinWidth) const
-{
- int sum = 0;
- for (const ColumnWidth& cw : getColWidths(mainWinWidth))
- sum += cw.width_;
- return sum;
-}
+// ***************************************************************************** +// * This file is part of the FreeFileSync project. It is distributed under * +// * GNU General Public License: http://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * +// ***************************************************************************** + +#include "grid.h" +#include <cassert> +#include <set> +#include <chrono> +#include <wx/settings.h> +#include <wx/listbox.h> +#include <wx/tooltip.h> +#include <wx/timer.h> +#include <wx/utils.h> +//#include <zen/tick_count.h> +#include <zen/string_tools.h> +#include <zen/scope_guard.h> +#include <zen/utf.h> +#include <zen/format_unit.h> +#include "dc.h" + + #include <gtk/gtk.h> + +using namespace zen; + + +wxColor Grid::getColorSelectionGradientFrom() { return { 137, 172, 255 }; } //blue: HSL: 158, 255, 196 HSV: 222, 0.46, 1 +wxColor Grid::getColorSelectionGradientTo () { return { 225, 234, 255 }; } // HSL: 158, 255, 240 HSV: 222, 0.12, 1 + +const int GridData::COLUMN_GAP_LEFT = 4; + + +void zen::clearArea(wxDC& dc, const wxRect& rect, const wxColor& col) +{ + wxDCPenChanger dummy (dc, col); + wxDCBrushChanger dummy2(dc, col); + dc.DrawRectangle(rect); +} + + +namespace +{ +//let's NOT create wxWidgets objects statically: +//------------------------------ Grid Parameters -------------------------------- +inline wxColor getColorLabelText() { return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); } +inline wxColor getColorGridLine() { return { 192, 192, 192 }; } //light grey + +inline wxColor getColorLabelGradientFrom() { return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); } +inline wxColor getColorLabelGradientTo () { return { 200, 200, 200 }; } //light grey + +inline wxColor getColorLabelGradientFocusFrom() { return getColorLabelGradientFrom(); } +inline wxColor getColorLabelGradientFocusTo () { return Grid::getColorSelectionGradientFrom(); } + +const double MOUSE_DRAG_ACCELERATION = 1.5; //unit: [rows / (pixel * sec)] -> same value like Explorer! +const int DEFAULT_COL_LABEL_BORDER = 6; //top + bottom border in addition to label height +const int COLUMN_MOVE_DELAY = 5; //unit: [pixel] (from Explorer) +const int COLUMN_MIN_WIDTH = 40; //only honored when resizing manually! +const int ROW_LABEL_BORDER = 3; +const int COLUMN_RESIZE_TOLERANCE = 6; //unit [pixel] +const int COLUMN_FILL_GAP_TOLERANCE = 10; //enlarge column to fill full width when resizing + +const bool fillGapAfterColumns = true; //draw rows/column label to fill full window width; may become an instance variable some time? +} + +//---------------------------------------------------------------------------------------------------------------- +const wxEventType zen::EVENT_GRID_MOUSE_LEFT_DOUBLE = wxNewEventType(); +const wxEventType zen::EVENT_GRID_MOUSE_LEFT_DOWN = wxNewEventType(); +const wxEventType zen::EVENT_GRID_MOUSE_LEFT_UP = wxNewEventType(); +const wxEventType zen::EVENT_GRID_MOUSE_RIGHT_DOWN = wxNewEventType(); +const wxEventType zen::EVENT_GRID_MOUSE_RIGHT_UP = wxNewEventType(); +const wxEventType zen::EVENT_GRID_SELECT_RANGE = wxNewEventType(); +const wxEventType zen::EVENT_GRID_COL_LABEL_MOUSE_LEFT = wxNewEventType(); +const wxEventType zen::EVENT_GRID_COL_LABEL_MOUSE_RIGHT = wxNewEventType(); +const wxEventType zen::EVENT_GRID_COL_RESIZE = wxNewEventType(); +//---------------------------------------------------------------------------------------------------------------- + +void GridData::renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected) +{ + drawCellBackground(dc, rect, enabled, selected, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); +} + + +void GridData::renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover) +{ + wxRect rectTmp = drawCellBorder(dc, rect); + + rectTmp.x += COLUMN_GAP_LEFT; + rectTmp.width -= COLUMN_GAP_LEFT; + drawCellText(dc, rectTmp, getValue(row, colType)); +} + + +int GridData::getBestSize(wxDC& dc, size_t row, ColumnType colType) +{ + return dc.GetTextExtent(getValue(row, colType)).GetWidth() + 2 * COLUMN_GAP_LEFT + 1; //gap on left and right side + border +} + + +wxRect GridData::drawCellBorder(wxDC& dc, const wxRect& rect) //returns remaining rectangle +{ + wxDCPenChanger dummy2(dc, getColorGridLine()); + dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight()); + dc.DrawLine(rect.GetBottomRight(), rect.GetTopRight() + wxPoint(0, -1)); + + return wxRect(rect.GetTopLeft(), wxSize(rect.width - 1, rect.height - 1)); +} + + +void GridData::drawCellBackground(wxDC& dc, const wxRect& rect, bool enabled, bool selected, const wxColor& backgroundColor) +{ + if (enabled) + { + if (selected) + dc.GradientFillLinear(rect, Grid::getColorSelectionGradientFrom(), Grid::getColorSelectionGradientTo(), wxEAST); + else + clearArea(dc, rect, backgroundColor); + } + else + clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE)); +} + + +wxSize GridData::drawCellText(wxDC& dc, const wxRect& rect, const std::wstring& text, int alignment) +{ + /* + performance notes (Windows): + - wxDC::GetTextExtent() is by far the most expensive call (20x more expensive than wxDC::DrawText()) + - wxDC::DrawLabel() is inefficiently implemented; internally calls: wxDC::GetMultiLineTextExtent(), wxDC::GetTextExtent(), wxDC::DrawText() + - wxDC::GetMultiLineTextExtent() calls wxDC::GetTextExtent() + - wxDC::DrawText also calls wxDC::GetTextExtent()!! + => wxDC::DrawLabel() boils down to 3(!) calls to wxDC::GetTextExtent()!!! + - wxDC::DrawLabel results in GetTextExtent() call even for empty strings!!! + => skip the wxDC::DrawLabel() cruft and directly call wxDC::DrawText! + */ + + //truncate large texts and add ellipsis + assert(!contains(text, L"\n")); + const wchar_t ELLIPSIS = L'\u2026'; //"..." + + std::wstring textTrunc = text; + wxSize extentTrunc = dc.GetTextExtent(textTrunc); + if (extentTrunc.GetWidth() > rect.width) + { + //unlike Windows 7 Explorer, we truncate UTF-16 correctly: e.g. CJK-Ideogramm encodes to TWO wchar_t: utfTo<std::wstring>("\xf0\xa4\xbd\x9c"); + size_t low = 0; //number of unicode chars! + size_t high = unicodeLength(text); // + if (high > 1) + for (;;) + { + const size_t middle = (low + high) / 2; //=> never 0 when "high - low > 1" + if (high - low <= 1) + { + if (low == 0) + { + textTrunc = ELLIPSIS; + extentTrunc = dc.GetTextExtent(ELLIPSIS); + } + break; + } + + const std::wstring& candidate = 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 <class T> + bool sendEventNow(T&& event) //take both "rvalue + lvalues", return "true" if a suitable event handler function was found and executed, and the function did not call wxEvent::Skip. + { + if (wxEvtHandler* evtHandler = parent_.GetEventHandler()) + return evtHandler->ProcessEvent(event); + return false; + } + +protected: + void setToolTip(const std::wstring& text) //proper fix for wxWindow + { + wxToolTip* tt = GetToolTip(); + + const wxString oldText = tt ? tt->GetTip() : wxString(); + if (text != oldText) + { + if (text.empty()) + SetToolTip(nullptr); //wxGTK doesn't allow wxToolTip with empty text! + else + { + //wxWidgets bug: tooltip multiline property is defined by first tooltip text containing newlines or not (same is true for maximum width) + if (!tt) + SetToolTip(new wxToolTip(L"a b\n\ + a b")); //ugly, but working (on Windows) + tt = GetToolTip(); //should be bound by now + assert(tt); + if (tt) + tt->SetTip(text); + } + } + } + +private: + virtual void render(wxDC& dc, const wxRect& rect) = 0; + + virtual void onFocus(wxFocusEvent& event) { event.Skip(); } + virtual void onChildFocus(wxEvent& event) {} //wxGTK::wxScrolledWindow automatically scrolls to child window when child gets focus -> prevent! + + virtual void onMouseLeftDown (wxMouseEvent& event) { event.Skip(); } + virtual void onMouseLeftUp (wxMouseEvent& event) { event.Skip(); } + virtual void onMouseLeftDouble(wxMouseEvent& event) { event.Skip(); } + virtual void onMouseRightDown (wxMouseEvent& event) { event.Skip(); } + virtual void onMouseRightUp (wxMouseEvent& event) { event.Skip(); } + virtual void onMouseMovement (wxMouseEvent& event) { event.Skip(); } + virtual void onLeaveWindow (wxMouseEvent& event) { event.Skip(); } + virtual void onMouseCaptureLost(wxMouseCaptureLostEvent& event) { event.Skip(); } + + void onKeyDown(wxKeyEvent& event) + { + if (!sendEventNow(event)) //let parent collect all key events + event.Skip(); + } + + void onMouseWheel(wxMouseEvent& event) + { + /* + MSDN, WM_MOUSEWHEEL: "Sent to the focus window when the mouse wheel is rotated. + The DefWindowProc function propagates the message to the window's parent. + There should be no internal forwarding of the message, since DefWindowProc propagates + it up the parent chain until it finds a window that processes it." + + On OS X there is no such propagation! => we need a redirection (the same wxGrid implements) + */ + + //new wxWidgets 3.0 screw-up for GTK2: wxScrollHelperEvtHandler::ProcessEvent() ignores wxEVT_MOUSEWHEEL events + //thereby breaking the scenario of redirection to parent we need here (but also breaking their very own wxGrid sample) + //=> call wxScrolledWindow mouse wheel handler directly + parent_.HandleOnMouseWheel(event); + + //if (!sendEventNow(event)) + // event.Skip(); + } + + void onPaintEvent(wxPaintEvent& event) + { + //wxAutoBufferedPaintDC dc(this); -> this one happily fucks up for RTL layout by not drawing the first column (x = 0)! + BufferedPaintDC dc(*this, doubleBuffer_); + + assert(GetSize() == GetClientSize()); + + const wxRegion& updateReg = GetUpdateRegion(); + for (wxRegionIterator it = updateReg; it; ++it) + render(dc, it.GetRect()); + } + + void onSizeEvent(wxSizeEvent& event) + { + Refresh(); + event.Skip(); + } + + void onEraseBackGround(wxEraseEvent& event) {} + + Grid& parent_; + Opt<wxBitmap> doubleBuffer_; +}; + +//---------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------- + +class Grid::CornerWin : public SubWindow +{ +public: + CornerWin(Grid& parent) : SubWindow(parent) {} + +private: + bool AcceptsFocus() const override { return false; } + + void render(wxDC& dc, const wxRect& rect) override + { + const wxRect& clientRect = GetClientRect(); + + dc.GradientFillLinear(clientRect, getColorLabelGradientFrom(), getColorLabelGradientTo(), wxSOUTH); + + dc.SetPen(wxSystemSettings::GetColour(wxSYS_COLOUR_3DSHADOW)); + + { + wxDCPenChanger dummy(dc, getColorLabelGradientFrom()); + dc.DrawLine(clientRect.GetTopLeft(), clientRect.GetTopRight()); + } + + dc.GradientFillLinear(wxRect(clientRect.GetBottomLeft (), clientRect.GetTopLeft ()), getColorLabelGradientFrom(), dc.GetPen().GetColour(), wxSOUTH); + dc.GradientFillLinear(wxRect(clientRect.GetBottomRight(), clientRect.GetTopRight()), getColorLabelGradientFrom(), dc.GetPen().GetColour(), wxSOUTH); + + dc.DrawLine(clientRect.GetBottomLeft(), clientRect.GetBottomRight()); + + wxRect rectShrinked = clientRect; + rectShrinked.Deflate(1); + dc.SetPen(*wxWHITE_PEN); + + //dc.DrawLine(clientRect.GetTopLeft(), clientRect.GetTopRight() + wxPoint(1, 0)); + dc.DrawLine(rectShrinked.GetTopLeft(), rectShrinked.GetBottomLeft() + wxPoint(0, 1)); + } +}; + +//---------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------- + +class Grid::RowLabelWin : public SubWindow +{ +public: + RowLabelWin(Grid& parent) : + SubWindow(parent), + rowHeight_(parent.GetCharHeight() + 2 + 1) {} //default height; don't call any functions on "parent" other than those from wxWindow during construction! + //2 for some more space, 1 for bottom border (gives 15 + 2 + 1 on Windows, 17 + 2 + 1 on Ubuntu) + + int getBestWidth(ptrdiff_t rowFrom, ptrdiff_t rowTo) + { + wxClientDC dc(this); + + wxFont labelFont = GetFont(); + //labelFont.SetWeight(wxFONTWEIGHT_BOLD); + dc.SetFont(labelFont); //harmonize with RowLabelWin::render()! + + int bestWidth = 0; + for (ptrdiff_t i = rowFrom; i <= rowTo; ++i) + bestWidth = std::max(bestWidth, dc.GetTextExtent(formatRow(i)).GetWidth() + 2 * ROW_LABEL_BORDER); + return bestWidth; + } + + size_t getLogicalHeight() const { return refParent().getRowCount() * rowHeight_; } + + ptrdiff_t getRowAtPos(ptrdiff_t posY) const //returns < 0 on invalid input, else row number within: [0, rowCount]; rowCount if out of range + { + if (posY >= 0 && rowHeight_ > 0) + { + const size_t row = posY / rowHeight_; + return std::min(row, refParent().getRowCount()); + } + return -1; + } + + int getRowHeight() const { return rowHeight_; } //guarantees to return size >= 1 ! + void setRowHeight(int height) { assert(height > 0); rowHeight_ = std::max(1, height); } + + wxRect getRowLabelArea(size_t row) const //returns empty rect if row not found + { + assert(GetClientAreaOrigin() == wxPoint()); + if (row < refParent().getRowCount()) + return wxRect(wxPoint(0, rowHeight_ * row), + wxSize(GetClientSize().GetWidth(), rowHeight_)); + return wxRect(); + } + + std::pair<ptrdiff_t, ptrdiff_t> getRowsOnClient(const wxRect& clientRect) const //returns range [begin, end) + { + const int yFrom = refParent().CalcUnscrolledPosition(clientRect.GetTopLeft ()).y; + const int yTo = refParent().CalcUnscrolledPosition(clientRect.GetBottomRight()).y; + + return std::make_pair(std::max(yFrom / rowHeight_, 0), + std::min<ptrdiff_t>((yTo / rowHeight_) + 1, refParent().getRowCount())); + } + +private: + static std::wstring formatRow(size_t row) { return toGuiString(row + 1); } //convert number to std::wstring including thousands separator + + bool AcceptsFocus() const override { return false; } + + void render(wxDC& dc, const wxRect& rect) override + { + /* + IsEnabled() vs IsThisEnabled() since wxWidgets 2.9.5: + + void wxWindowBase::NotifyWindowOnEnableChange(), called from bool wxWindowBase::Enable(), fails to refresh + child elements when disabling a IsTopLevel() dialog, e.g. when showing a modal dialog. + The unfortunate effect on XP for using IsEnabled() when rendering the grid is that the user can move the modal dialog + and *draw* with it on the background while the grid refreshes as disabled incrementally! + + => Don't use IsEnabled() since it considers the top level window. The brittle wxWidgets implementation is right in their intention, + but wrong when not refreshing child-windows: the control designer decides how his control should be rendered! + + => IsThisEnabled() OTOH is too shallow and does not consider parent windows which are not top level. + + The perfect solution would be a bool ShouldBeDrawnActive() { return "IsEnabled() but ignore effects of showing a modal dialog"; } + + However "IsThisEnabled()" is good enough (same like the old IsEnabled() on wxWidgets 2.8.12) and it avoids this pathetic behavior on XP. + (Similar problem on Win 7: e.g. directly click sync button without comparing first) + */ + if (IsThisEnabled()) + clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + else + clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE)); + + wxFont labelFont = GetFont(); + //labelFont.SetWeight(wxFONTWEIGHT_BOLD); + dc.SetFont(labelFont); //harmonize with RowLabelWin::getBestWidth()! + + auto rowRange = getRowsOnClient(rect); //returns range [begin, end) + for (auto row = rowRange.first; row < rowRange.second; ++row) + { + wxRect singleLabelArea = getRowLabelArea(row); //returns empty rect if row not found + if (singleLabelArea.height > 0) + { + singleLabelArea.y = refParent().CalcScrolledPosition(singleLabelArea.GetTopLeft()).y; + drawRowLabel(dc, singleLabelArea, row); + } + } + } + + void drawRowLabel(wxDC& dc, const wxRect& rect, size_t row) + { + //clearArea(dc, rect, getColorRowLabel()); + dc.GradientFillLinear(rect, getColorLabelGradientFrom(), getColorLabelGradientTo(), wxEAST); //clear overlapping cells + wxDCTextColourChanger dummy3(dc, getColorLabelText()); //accessibility: always set both foreground AND background colors! + + //label text + wxRect textRect = rect; + textRect.Deflate(1); + + GridData::drawCellText(dc, textRect, formatRow(row), wxALIGN_CENTRE); + + //border lines + { + wxDCPenChanger dummy(dc, *wxWHITE_PEN); + dc.DrawLine(rect.GetTopLeft(), rect.GetTopRight()); + } + { + wxDCPenChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_3DSHADOW)); + dc.DrawLine(rect.GetTopLeft(), rect.GetBottomLeft()); + dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight()); + dc.DrawLine(rect.GetBottomRight(), rect.GetTopRight() + wxPoint(0, -1)); + } + } + + void onMouseLeftDown(wxMouseEvent& event) override { refParent().redirectRowLabelEvent(event); } + void onMouseMovement(wxMouseEvent& event) override { refParent().redirectRowLabelEvent(event); } + void onMouseLeftUp (wxMouseEvent& event) override { refParent().redirectRowLabelEvent(event); } + + int rowHeight_; +}; + + +namespace +{ +class ColumnResizing +{ +public: + ColumnResizing(wxWindow& wnd, size_t col, int startWidth, int clientPosX) : + wnd_(wnd), col_(col), startWidth_(startWidth), clientPosX_(clientPosX) + { + wnd_.CaptureMouse(); + } + ~ColumnResizing() + { + if (wnd_.HasCapture()) + wnd_.ReleaseMouse(); + } + + size_t getColumn () const { return col_; } + int getStartWidth () const { return startWidth_; } + int getStartPosX () const { return clientPosX_; } + +private: + wxWindow& wnd_; + const size_t col_; + const int startWidth_; + const int clientPosX_; +}; + + +class ColumnMove +{ +public: + ColumnMove(wxWindow& wnd, size_t colFrom, int clientPosX) : + wnd_(wnd), + colFrom_(colFrom), + colTo_(colFrom), + clientPosX_(clientPosX) { wnd_.CaptureMouse(); } + ~ColumnMove() { if (wnd_.HasCapture()) wnd_.ReleaseMouse(); } + + size_t getColumnFrom() const { return colFrom_; } + size_t& refColumnTo() { return colTo_; } + int getStartPosX () const { return clientPosX_; } + + bool isRealMove() const { return !singleClick_; } + void setRealMove() { singleClick_ = false; } + +private: + wxWindow& wnd_; + const size_t colFrom_; + size_t colTo_; + const int clientPosX_; + bool singleClick_ = true; +}; +} + +//---------------------------------------------------------------------------------------------------------------- + +class Grid::ColLabelWin : public SubWindow +{ +public: + ColLabelWin(Grid& parent) : SubWindow(parent) {} + +private: + bool AcceptsFocus() const override { return false; } + + void render(wxDC& dc, const wxRect& rect) override + { + if (IsThisEnabled()) + clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + else + clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE)); + + //coordinate with "colLabelHeight" in Grid constructor: + wxFont labelFont = GetFont(); + labelFont.SetWeight(wxFONTWEIGHT_BOLD); + dc.SetFont(labelFont); + + wxDCTextColourChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels + + const int colLabelHeight = refParent().colLabelHeight_; + + wxPoint labelAreaTL(refParent().CalcScrolledPosition(wxPoint(0, 0)).x, 0); //client coordinates + + std::vector<ColumnWidth> absWidths = refParent().getColWidths(); //resolve stretched widths + for (auto it = absWidths.begin(); it != absWidths.end(); ++it) + { + const size_t col = it - absWidths.begin(); + const int width = it->width_; //don't use unsigned for calculations! + + if (labelAreaTL.x > rect.GetRight()) + return; //done, rect is fully covered + if (labelAreaTL.x + width > rect.x) + drawColumnLabel(dc, wxRect(labelAreaTL, wxSize(width, colLabelHeight)), col, it->type_); + labelAreaTL.x += width; + } + if (labelAreaTL.x > rect.GetRight()) + return; //done, rect is fully covered + + //fill gap after columns and cover full width + if (fillGapAfterColumns) + { + int totalWidth = 0; + for (const ColumnWidth& cw : absWidths) + totalWidth += cw.width_; + const int clientWidth = GetClientSize().GetWidth(); //need reliable, stable width in contrast to rect.width + + if (totalWidth < clientWidth) + drawColumnLabel(dc, wxRect(labelAreaTL, wxSize(clientWidth - totalWidth, colLabelHeight)), absWidths.size(), ColumnType::NONE); + } + } + + void drawColumnLabel(wxDC& dc, const wxRect& rect, size_t col, ColumnType colType) + { + if (auto dataView = refParent().getDataProvider()) + { + const bool isHighlighted = activeResizing_ ? col == activeResizing_ ->getColumn () : //highlight_ column on mouse-over + activeClickOrMove_ ? col == activeClickOrMove_->getColumnFrom() : + highlightCol_ ? col == *highlightCol_ : + false; + + RecursiveDcClipper clip(dc, rect); + dataView->renderColumnLabel(refParent(), dc, rect, colType, isHighlighted); + + //draw move target location + if (refParent().allowColumnMove_) + if (activeClickOrMove_ && activeClickOrMove_->isRealMove()) + { + if (col + 1 == activeClickOrMove_->refColumnTo()) //handle pos 1, 2, .. up to "at end" position + dc.GradientFillLinear(wxRect(rect.GetTopRight(), rect.GetBottomRight() + wxPoint(-2, 0)), getColorLabelGradientFrom(), *wxBLUE, wxSOUTH); + else if (col == activeClickOrMove_->refColumnTo() && col == 0) //pos 0 + dc.GradientFillLinear(wxRect(rect.GetTopLeft(), rect.GetBottomLeft() + wxPoint(2, 0)), getColorLabelGradientFrom(), *wxBLUE, wxSOUTH); + } + } + } + + void onMouseLeftDown(wxMouseEvent& event) override + { + if (FindFocus() != &refParent().getMainWin()) + refParent().getMainWin().SetFocus(); + + activeResizing_.reset(); + activeClickOrMove_.reset(); + + if (Opt<ColAction> action = refParent().clientPosToColumnAction(event.GetPosition())) + { + if (action->wantResize) + { + if (!event.LeftDClick()) //double-clicks never seem to arrive here; why is this checked at all??? + if (Opt<int> colWidth = refParent().getColWidth(action->col)) + activeResizing_ = std::make_unique<ColumnResizing>(*this, action->col, *colWidth, event.GetPosition().x); + } + else //a move or single click + activeClickOrMove_ = std::make_unique<ColumnMove>(*this, action->col, event.GetPosition().x); + } + event.Skip(); + } + + void onMouseLeftUp(wxMouseEvent& event) override + { + activeResizing_.reset(); //nothing else to do, actual work done by onMouseMovement() + + if (activeClickOrMove_) + { + if (activeClickOrMove_->isRealMove()) + { + if (refParent().allowColumnMove_) + { + const size_t colFrom = activeClickOrMove_->getColumnFrom(); + size_t colTo = activeClickOrMove_->refColumnTo(); + + if (colTo > colFrom) //simulate "colFrom" deletion + --colTo; + + refParent().moveColumn(colFrom, colTo); + } + } + else //notify single label click + { + if (const Opt<ColumnType> colType = refParent().colToType(activeClickOrMove_->getColumnFrom())) + sendEventNow(GridLabelClickEvent(EVENT_GRID_COL_LABEL_MOUSE_LEFT, event, *colType)); + } + activeClickOrMove_.reset(); + } + + refParent().updateWindowSizes(); //looks strange if done during onMouseMovement() + refParent().Refresh(); + event.Skip(); + } + + void onMouseCaptureLost(wxMouseCaptureLostEvent& event) override + { + activeResizing_.reset(); + activeClickOrMove_.reset(); + Refresh(); + //event.Skip(); -> we DID handle it! + } + + void onMouseLeftDouble(wxMouseEvent& event) override + { + if (Opt<ColAction> action = refParent().clientPosToColumnAction(event.GetPosition())) + if (action->wantResize) + { + //auto-size visible range on double-click + const int bestWidth = refParent().getBestColumnSize(action->col); //return -1 on error + if (bestWidth >= 0) + { + refParent().setColumnWidth(bestWidth, action->col, ALLOW_GRID_EVENT); + refParent().Refresh(); //refresh main grid as well! + } + } + event.Skip(); + } + + void onMouseMovement(wxMouseEvent& event) override + { + if (activeResizing_) + { + const auto col = activeResizing_->getColumn(); + const int newWidth = activeResizing_->getStartWidth() + event.GetPosition().x - activeResizing_->getStartPosX(); + + //set width tentatively + refParent().setColumnWidth(newWidth, col, ALLOW_GRID_EVENT); + + //check if there's a small gap after last column, if yes, fill it + const int gapWidth = GetClientSize().GetWidth() - refParent().getColWidthsSum(GetClientSize().GetWidth()); + if (std::abs(gapWidth) < COLUMN_FILL_GAP_TOLERANCE) + refParent().setColumnWidth(newWidth + gapWidth, col, ALLOW_GRID_EVENT); + + refParent().Refresh(); //refresh columns on main grid as well! + } + else if (activeClickOrMove_) + { + const int clientPosX = event.GetPosition().x; + if (std::abs(clientPosX - activeClickOrMove_->getStartPosX()) > COLUMN_MOVE_DELAY) //real move (not a single click) + { + activeClickOrMove_->setRealMove(); + + const ptrdiff_t col = refParent().clientPosToMoveTargetColumn(event.GetPosition()); + if (col >= 0) + activeClickOrMove_->refColumnTo() = col; + } + } + else + { + if (const Opt<ColAction> action = refParent().clientPosToColumnAction(event.GetPosition())) + { + highlightCol_ = action->col; + + if (action->wantResize) + SetCursor(wxCURSOR_SIZEWE); //set window-local only! :) + else + SetCursor(*wxSTANDARD_CURSOR); + } + else + { + highlightCol_ = NoValue(); + SetCursor(*wxSTANDARD_CURSOR); + } + } + + const std::wstring toolTip = [&] + { + const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition()); + const ColumnType colType = refParent().getColumnAtPos(absPos.x).colType; //returns ColumnType::NONE if no column at x position! + if (colType != ColumnType::NONE) + if (auto prov = refParent().getDataProvider()) + return prov->getToolTip(colType); + return std::wstring(); + }(); + setToolTip(toolTip); + + Refresh(); + event.Skip(); + } + + void onLeaveWindow(wxMouseEvent& event) override + { + highlightCol_ = NoValue(); //wxEVT_LEAVE_WINDOW does not respect mouse capture! -> however highlight_ is drawn unconditionally during move/resize! + Refresh(); + event.Skip(); + } + + void onMouseRightDown(wxMouseEvent& event) override + { + if (const Opt<ColAction> action = refParent().clientPosToColumnAction(event.GetPosition())) + { + if (const Opt<ColumnType> colType = refParent().colToType(action->col)) + sendEventNow(GridLabelClickEvent(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, event, *colType)); //notify right click + else assert(false); + } + else + //notify right click (on free space after last column) + if (fillGapAfterColumns) + sendEventNow(GridLabelClickEvent(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, event, ColumnType::NONE)); + + event.Skip(); + } + + std::unique_ptr<ColumnResizing> activeResizing_; + std::unique_ptr<ColumnMove> activeClickOrMove_; + Opt<size_t> highlightCol_; //column during mouse-over +}; + +//---------------------------------------------------------------------------------------------------------------- +namespace +{ +const wxEventType EVENT_GRID_HAS_SCROLLED = wxNewEventType(); //internal to Grid::MainWin::ScrollWindow() +} +//---------------------------------------------------------------------------------------------------------------- + +class Grid::MainWin : public SubWindow +{ +public: + MainWin(Grid& parent, + RowLabelWin& rowLabelWin, + ColLabelWin& colLabelWin) : SubWindow(parent), + rowLabelWin_(rowLabelWin), + colLabelWin_(colLabelWin) + { + Connect(EVENT_GRID_HAS_SCROLLED, wxEventHandler(MainWin::onRequestWindowUpdate), nullptr, this); + } + + ~MainWin() { assert(!gridUpdatePending_); } + + size_t getCursor() const { return cursorRow_; } + size_t getAnchor() const { return selectionAnchor_; } + + void setCursor(size_t newCursorRow, size_t newAnchorRow) + { + cursorRow_ = newCursorRow; + selectionAnchor_ = newAnchorRow; + activeSelection_.reset(); //e.g. user might search with F3 while holding down left mouse button + } + +private: + void render(wxDC& dc, const wxRect& rect) override + { + if (IsThisEnabled()) + clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + else + clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE)); + + dc.SetFont(GetFont()); //harmonize with Grid::getBestColumnSize() + + wxDCTextColourChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels + + std::vector<ColumnWidth> absWidths = refParent().getColWidths(); //resolve stretched widths + { + int totalRowWidth = 0; + for (const ColumnWidth& cw : absWidths) + totalRowWidth += cw.width_; + + //fill gap after columns and cover full width + if (fillGapAfterColumns) + totalRowWidth = std::max(totalRowWidth, GetClientSize().GetWidth()); + + if (auto prov = refParent().getDataProvider()) + { + RecursiveDcClipper dummy2(dc, rect); //do NOT draw background on cells outside of invalidated rect invalidating foreground text! + + wxPoint cellAreaTL(refParent().CalcScrolledPosition(wxPoint(0, 0))); //client coordinates + const int rowHeight = rowLabelWin_.getRowHeight(); + const auto rowRange = rowLabelWin_.getRowsOnClient(rect); //returns range [begin, end) + + //draw background lines + for (auto row = rowRange.first; row < rowRange.second; ++row) + { + const wxRect rowRect(cellAreaTL + wxPoint(0, row * rowHeight), wxSize(totalRowWidth, rowHeight)); + RecursiveDcClipper dummy3(dc, rowRect); + prov->renderRowBackgound(dc, rowRect, row, refParent().IsThisEnabled(), drawAsSelected(row)); + } + + //draw single cells, column by column + for (const ColumnWidth& cw : absWidths) + { + if (cellAreaTL.x > rect.GetRight()) + return; //done + + if (cellAreaTL.x + cw.width_ > rect.x) + for (auto row = rowRange.first; row < rowRange.second; ++row) + { + const wxRect cellRect(cellAreaTL.x, cellAreaTL.y + row * rowHeight, cw.width_, rowHeight); + RecursiveDcClipper dummy3(dc, cellRect); + prov->renderCell(dc, cellRect, row, cw.type_, refParent().IsThisEnabled(), drawAsSelected(row), getRowHoverToDraw(row)); + } + cellAreaTL.x += cw.width_; + } + } + } + } + + HoverArea getRowHoverToDraw(ptrdiff_t row) const + { + if (activeSelection_) + { + if (activeSelection_->getFirstClick().row_ == row) + return activeSelection_->getFirstClick().hoverArea_; + } + else if (highlight_.row == row) + return highlight_.rowHover; + return HoverArea::NONE; + } + + bool drawAsSelected(size_t row) const + { + if (activeSelection_) //check if user is currently selecting with mouse + { + const size_t rowFrom = std::min(activeSelection_->getStartRow(), activeSelection_->getCurrentRow()); + const size_t rowTo = std::max(activeSelection_->getStartRow(), activeSelection_->getCurrentRow()); + + if (rowFrom <= row && row <= rowTo) + return activeSelection_->isPositiveSelect(); //overwrite default + } + return refParent().isSelected(row); + } + + void onMouseLeftDown (wxMouseEvent& event) override { onMouseDown(event); } + void onMouseLeftUp (wxMouseEvent& event) override { onMouseUp (event); } + void onMouseRightDown(wxMouseEvent& event) override { onMouseDown(event); } + void onMouseRightUp (wxMouseEvent& event) override { onMouseUp (event); } + + void onMouseLeftDouble(wxMouseEvent& event) override + { + if (auto prov = refParent().getDataProvider()) + { + const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition()); + const ptrdiff_t row = rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range + const ColumnPosInfo cpi = refParent().getColumnAtPos(absPos.x); //returns ColumnType::NONE if no column at x position! + const HoverArea rowHover = prov->getRowMouseHover(row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth); + //client is interested in all double-clicks, even those outside of the grid! + sendEventNow(GridClickEvent(EVENT_GRID_MOUSE_LEFT_DOUBLE, event, row, rowHover)); + } + event.Skip(); + } + + void onMouseDown(wxMouseEvent& event) //handle left and right mouse button clicks (almost) the same + { + if (wxWindow::FindFocus() != this) //doesn't seem to happen automatically for right mouse button + SetFocus(); + + if (auto prov = refParent().getDataProvider()) + { + const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition()); + const ptrdiff_t row = rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range + const ColumnPosInfo cpi = refParent().getColumnAtPos(absPos.x); //returns ColumnType::NONE if no column at x position! + const HoverArea rowHover = prov->getRowMouseHover(row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth); + //row < 0 possible!!! Pressing "Menu key" simulates Mouse Right Down + Up at position 0xffff/0xffff! + + GridClickEvent mouseEvent(event.RightDown() ? EVENT_GRID_MOUSE_RIGHT_DOWN : EVENT_GRID_MOUSE_LEFT_DOWN, event, row, rowHover); + + if (row >= 0) + if (!event.RightDown() || !refParent().isSelected(row)) //do NOT start a new selection if user right-clicks on a selected area! + { + if (event.ControlDown()) + activeSelection_ = std::make_unique<MouseSelection>(*this, row, !refParent().isSelected(row), mouseEvent); + else if (event.ShiftDown()) + { + activeSelection_ = std::make_unique<MouseSelection>(*this, selectionAnchor_, true, mouseEvent); + refParent().clearSelection(ALLOW_GRID_EVENT); + } + else + { + activeSelection_ = std::make_unique<MouseSelection>(*this, row, true, mouseEvent); + refParent().clearSelection(ALLOW_GRID_EVENT); + } + } + //notify event *after* potential "clearSelection(true)" above: a client should first receive a GridRangeSelectEvent for clearing the grid, if necessary, + //then GridClickEvent and the associated GridRangeSelectEvent one after the other + sendEventNow(mouseEvent); + + Refresh(); + } + event.Skip(); //allow changing focus + } + + void onMouseUp(wxMouseEvent& event) + { + if (activeSelection_) + { + const size_t rowCount = refParent().getRowCount(); + if (rowCount > 0) + { + if (activeSelection_->getCurrentRow() < rowCount) + { + cursorRow_ = activeSelection_->getCurrentRow(); + selectionAnchor_ = activeSelection_->getStartRow(); //allowed to be "out of range" + } + else if (activeSelection_->getStartRow() < rowCount) //don't change cursor if "to" and "from" are out of range + { + cursorRow_ = rowCount - 1; + selectionAnchor_ = activeSelection_->getStartRow(); //allowed to be "out of range" + } + else //total selection "out of range" + selectionAnchor_ = cursorRow_; + } + //slight deviation from Explorer: change cursor while dragging mouse! -> unify behavior with shift + direction keys + + refParent().selectRangeAndNotify(activeSelection_->getStartRow (), //from + activeSelection_->getCurrentRow(), //to + activeSelection_->isPositiveSelect(), + &activeSelection_->getFirstClick()); + activeSelection_.reset(); + } + + if (auto prov = refParent().getDataProvider()) + { + //this one may point to row which is not in visible area! + const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition()); + const ptrdiff_t row = rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range + const ColumnPosInfo cpi = refParent().getColumnAtPos(absPos.x); //returns ColumnType::NONE if no column at x position! + const HoverArea rowHover = prov->getRowMouseHover(row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth); + //notify click event after the range selection! e.g. this makes sure the selection is applied before showing a context menu + sendEventNow(GridClickEvent(event.RightUp() ? EVENT_GRID_MOUSE_RIGHT_UP : EVENT_GRID_MOUSE_LEFT_UP, event, row, rowHover)); + } + + //update highlight_ and tooltip: on OS X no mouse movement event is generated after a mouse button click (unlike on Windows) + event.SetPosition(ScreenToClient(wxGetMousePosition())); //mouse position may have changed within above callbacks (e.g. context menu was shown)! + onMouseMovement(event); + + Refresh(); + event.Skip(); //allow changing focus + } + + void onMouseCaptureLost(wxMouseCaptureLostEvent& event) override + { + activeSelection_.reset(); + highlight_.row = -1; + Refresh(); + //event.Skip(); -> we DID handle it! + } + + void onMouseMovement(wxMouseEvent& event) override + { + if (auto prov = refParent().getDataProvider()) + { + const ptrdiff_t rowCount = refParent().getRowCount(); + const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition()); + const ptrdiff_t row = rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range + const ColumnPosInfo cpi = refParent().getColumnAtPos(absPos.x); //returns ColumnType::NONE if no column at x position! + const HoverArea rowHover = prov->getRowMouseHover(row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth); + + const std::wstring toolTip = [&] + { + if (cpi.colType != ColumnType::NONE && 0 <= row && row < rowCount) + return prov->getToolTip(row, cpi.colType); + return std::wstring(); + }(); + setToolTip(toolTip); //show even during mouse selection! + + if (activeSelection_) + activeSelection_->evalMousePos(); //call on both mouse movement + timer event! + else + { + refreshHighlight(highlight_); + highlight_.row = row; + highlight_.rowHover = rowHover; + refreshHighlight(highlight_); //multiple Refresh() calls are condensed into single one! + } + } + event.Skip(); + } + + void onLeaveWindow(wxMouseEvent& event) override //wxEVT_LEAVE_WINDOW does not respect mouse capture! + { + if (!activeSelection_) + { + refreshHighlight(highlight_); + highlight_.row = -1; + } + + event.Skip(); + } + + + void onFocus(wxFocusEvent& event) override { Refresh(); event.Skip(); } + + class MouseSelection : private wxEvtHandler + { + public: + MouseSelection(MainWin& wnd, size_t rowStart, bool positiveSelect, const GridClickEvent& firstClick) : + wnd_(wnd), rowStart_(rowStart), rowCurrent_(rowStart), positiveSelect_(positiveSelect), firstClick_(firstClick) + { + wnd_.CaptureMouse(); + timer_.Connect(wxEVT_TIMER, wxEventHandler(MouseSelection::onTimer), nullptr, this); + timer_.Start(100); //timer interval in ms + evalMousePos(); + } + ~MouseSelection() { if (wnd_.HasCapture()) wnd_.ReleaseMouse(); } + + size_t getStartRow () const { return rowStart_; } + size_t getCurrentRow () const { return rowCurrent_; } + bool isPositiveSelect() const { return positiveSelect_; } //are we selecting or unselecting? + const GridClickEvent& getFirstClick() const { return firstClick_; } + + void evalMousePos() + { + const auto now = std::chrono::steady_clock::now(); + const double deltaSecs = std::chrono::duration<double>(now - lastEvalTime_).count(); //unit: [sec] + lastEvalTime_ = now; + + const wxPoint clientPos = wnd_.ScreenToClient(wxGetMousePosition()); + const wxSize clientSize = wnd_.GetClientSize(); + assert(wnd_.GetClientAreaOrigin() == wxPoint()); + + //scroll while dragging mouse + const int overlapPixY = clientPos.y < 0 ? clientPos.y : + clientPos.y >= clientSize.GetHeight() ? clientPos.y - (clientSize.GetHeight() - 1) : 0; + const int overlapPixX = clientPos.x < 0 ? clientPos.x : + clientPos.x >= clientSize.GetWidth() ? clientPos.x - (clientSize.GetWidth() - 1) : 0; + + int pixelsPerUnitY = 0; + wnd_.refParent().GetScrollPixelsPerUnit(nullptr, &pixelsPerUnitY); + if (pixelsPerUnitY <= 0) return; + + const double mouseDragSpeedIncScrollU = pixelsPerUnitY > 0 ? MOUSE_DRAG_ACCELERATION * wnd_.rowLabelWin_.getRowHeight() / pixelsPerUnitY : 0; //unit: [scroll units / (pixel * sec)] + + auto autoScroll = [&](int overlapPix, double& toScroll) + { + if (overlapPix != 0) + { + const double scrollSpeed = overlapPix * mouseDragSpeedIncScrollU; //unit: [scroll units / sec] + toScroll += scrollSpeed * deltaSecs; + } + else + toScroll = 0; + }; + + autoScroll(overlapPixX, toScrollX_); + autoScroll(overlapPixY, toScrollY_); + + if (static_cast<int>(toScrollX_) != 0 || static_cast<int>(toScrollY_) != 0) + { + wnd_.refParent().scrollDelta(static_cast<int>(toScrollX_), static_cast<int>(toScrollY_)); // + toScrollX_ -= static_cast<int>(toScrollX_); //rounds down for positive numbers, up for negative, + toScrollY_ -= static_cast<int>(toScrollY_); //exactly what we want + } + + //select current row *after* scrolling + wxPoint clientPosTrimmed = clientPos; + numeric::clamp(clientPosTrimmed.y, 0, clientSize.GetHeight() - 1); //do not select row outside client window! + + const wxPoint absPos = wnd_.refParent().CalcUnscrolledPosition(clientPosTrimmed); + const ptrdiff_t newRow = wnd_.rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range + if (newRow >= 0) + if (rowCurrent_ != newRow) + { + rowCurrent_ = newRow; + wnd_.Refresh(); + } + } + + private: + void onTimer(wxEvent& event) { evalMousePos(); } + + MainWin& wnd_; + const size_t rowStart_; + ptrdiff_t rowCurrent_; + const bool positiveSelect_; + const GridClickEvent firstClick_; + wxTimer timer_; + double toScrollX_ = 0; //count outstanding scroll unit fractions while dragging mouse + double toScrollY_ = 0; // + std::chrono::steady_clock::time_point lastEvalTime_ = std::chrono::steady_clock::now(); + }; + + struct MouseHighlight + { + ptrdiff_t row = -1; + HoverArea rowHover = HoverArea::NONE; + }; + + void ScrollWindow(int dx, int dy, const wxRect* rect) override + { + wxWindow::ScrollWindow(dx, dy, rect); + rowLabelWin_.ScrollWindow(0, dy, rect); + colLabelWin_.ScrollWindow(dx, 0, rect); + + //attention, wxGTK call sequence: wxScrolledWindow::Scroll() -> wxScrolledHelperNative::Scroll() -> wxScrolledHelperNative::DoScroll() + //which *first* calls us, MainWin::ScrollWindow(), and *then* internally updates m_yScrollPosition + //=> we cannot use CalcUnscrolledPosition() here which gives the wrong/outdated value!!! + //=> we need to update asynchronously: + //=> don't send async event repeatedly => severe performance issues on wxGTK! + //=> can't use idle event neither: too few idle events on Windows, e.g. NO idle events while mouse drag-scrolling! + //=> solution: send single async event at most! + if (!gridUpdatePending_) //without guarding, the number of outstanding async events can become very high during scrolling!! test case: Ubuntu: 170; Windows: 20 + { + gridUpdatePending_ = true; + wxCommandEvent scrollEvent(EVENT_GRID_HAS_SCROLLED); + AddPendingEvent(scrollEvent); //asynchronously call updateAfterScroll() + } + } + + void onRequestWindowUpdate(wxEvent& event) + { + assert(gridUpdatePending_); + ZEN_ON_SCOPE_EXIT(gridUpdatePending_ = false); + + refParent().updateWindowSizes(false); //row label width has changed -> do *not* update scrollbars: recursion on wxGTK! -> still a problem, now that we're called async?? + rowLabelWin_.Update(); //update while dragging scroll thumb + } + + void refreshRow(size_t row) + { + const wxRect& rowArea = rowLabelWin_.getRowLabelArea(row); //returns empty rect if row not found + const wxPoint topLeft = refParent().CalcScrolledPosition(wxPoint(0, rowArea.y)); //absolute -> client coordinates + wxRect cellArea(topLeft, wxSize(refParent().getColWidthsSum(GetClientSize().GetWidth()), rowArea.height)); + RefreshRect(cellArea, false); + } + + void refreshHighlight(const MouseHighlight& hl) + { + const ptrdiff_t rowCount = refParent().getRowCount(); + if (0 <= hl.row && hl.row < rowCount && hl.rowHover != HoverArea::NONE) //no highlight_? => NOP! + refreshRow(hl.row); + } + + RowLabelWin& rowLabelWin_; + ColLabelWin& colLabelWin_; + + std::unique_ptr<MouseSelection> activeSelection_; //bound while user is selecting with mouse + MouseHighlight highlight_; //current mouse highlight_ (superseeded by activeSelection_ if available) + + ptrdiff_t cursorRow_ = 0; + size_t selectionAnchor_ = 0; + bool gridUpdatePending_ = false; +}; + +//---------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------- + +Grid::Grid(wxWindow* parent, + wxWindowID id, + const wxPoint& pos, + const wxSize& size, + long style, + const wxString& name) : wxScrolledWindow(parent, id, pos, size, style | wxWANTS_CHARS, name) +{ + cornerWin_ = new CornerWin (*this); // + rowLabelWin_ = new RowLabelWin(*this); //owership handled by "this" + colLabelWin_ = new ColLabelWin(*this); // + mainWin_ = new MainWin (*this, *rowLabelWin_, *colLabelWin_); // + + colLabelHeight_ = 2 * DEFAULT_COL_LABEL_BORDER + [&]() -> int + { + //coordinate with ColLabelWin::render(): + wxFont labelFont = colLabelWin_->GetFont(); + labelFont.SetWeight(wxFONTWEIGHT_BOLD); + return labelFont.GetPixelSize().GetHeight(); + }(); + + SetTargetWindow(mainWin_); + + SetInitialSize(size); //"Most controls will use this to set their initial size" -> why not + + assert(GetClientSize() == GetSize()); //borders are NOT allowed for Grid + //reason: updateWindowSizes() wants to use "GetSize()" as a "GetClientSize()" including scrollbars + + Connect(wxEVT_PAINT, wxPaintEventHandler(Grid::onPaintEvent ), nullptr, this); + Connect(wxEVT_ERASE_BACKGROUND, wxEraseEventHandler(Grid::onEraseBackGround), nullptr, this); + Connect(wxEVT_SIZE, wxSizeEventHandler (Grid::onSizeEvent ), nullptr, this); + + Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(Grid::onKeyDown), nullptr, this); +} + + +void Grid::updateWindowSizes(bool updateScrollbar) +{ + /* We have to deal with TWO nasty circular dependencies: + 1. + rowLabelWidth + /|\ + mainWin::client width + /|\ + SetScrollbars -> show/hide horizontal scrollbar depending on client width + /|\ + mainWin::client height -> possibly trimmed by horizontal scrollbars + /|\ + rowLabelWidth + + 2. + mainWin_->GetClientSize() + /|\ + SetScrollbars -> show/hide scrollbars depending on whether client size is big enough + /|\ + GetClientSize(); -> possibly trimmed by scrollbars + /|\ + mainWin_->GetClientSize() -> also trimmed, since it's a sub-window! + */ + + //break this vicious circle: + + //harmonize with Grid::GetSizeAvailableForScrollTarget()! + + //1. calculate row label width independent from scrollbars + const int mainWinHeightGross = std::max(GetSize().GetHeight() - colLabelHeight_, 0); //independent from client sizes and scrollbars! + const ptrdiff_t logicalHeight = rowLabelWin_->getLogicalHeight(); // + + int rowLabelWidth = 0; + if (drawRowLabel_ && logicalHeight > 0) + { + ptrdiff_t yFrom = CalcUnscrolledPosition(wxPoint(0, 0)).y; + ptrdiff_t yTo = CalcUnscrolledPosition(wxPoint(0, mainWinHeightGross - 1)).y ; + numeric::clamp<ptrdiff_t>(yFrom, 0, logicalHeight - 1); + numeric::clamp<ptrdiff_t>(yTo, 0, logicalHeight - 1); + + const ptrdiff_t rowFrom = rowLabelWin_->getRowAtPos(yFrom); + const ptrdiff_t rowTo = rowLabelWin_->getRowAtPos(yTo); + if (rowFrom >= 0 && rowTo >= 0) + rowLabelWidth = rowLabelWin_->getBestWidth(rowFrom, rowTo); + } + + auto getMainWinSize = [&](const wxSize& clientSize) { return wxSize(std::max(0, clientSize.GetWidth() - rowLabelWidth), std::max(0, clientSize.GetHeight() - colLabelHeight_)); }; + + auto setScrollbars2 = [&](int logWidth, int logHeight) //replace SetScrollbars, which loses precision of pixelsPerUnitX for some brain-dead reason + { + mainWin_->SetVirtualSize(logWidth, logHeight); //set before calling SetScrollRate(): + //else SetScrollRate() would fail to preserve scroll position when "new virtual pixel-pos > old virtual height" + + int ppsuX = 0; //pixel per scroll unit + int ppsuY = 0; + GetScrollPixelsPerUnit(&ppsuX, &ppsuY); + + const int ppsuNew = rowLabelWin_->getRowHeight(); + if (ppsuX != ppsuNew || ppsuY != ppsuNew) //support polling! + SetScrollRate(ppsuNew, ppsuNew); //internally calls AdjustScrollbars() and GetVirtualSize()! + + AdjustScrollbars(); //lousy wxWidgets design decision: internally calls mainWin_->GetClientSize() without considering impact of scrollbars! + //Attention: setting scrollbars triggers *synchronous* resize event if scrollbars are shown or hidden! => updateWindowSizes() recursion! (Windows) + }; + + //2. update managed windows' sizes: just assume scrollbars are already set correctly, even if they may not be (yet)! + //this ensures mainWin_->SetVirtualSize() and AdjustScrollbars() are working with the correct main window size, unless sb change later, which triggers a recalculation anyway! + const wxSize mainWinSize = getMainWinSize(GetClientSize()); + + cornerWin_ ->SetSize(0, 0, rowLabelWidth, colLabelHeight_); + rowLabelWin_->SetSize(0, colLabelHeight_, rowLabelWidth, mainWinSize.GetHeight()); + colLabelWin_->SetSize(rowLabelWidth, 0, mainWinSize.GetWidth(), colLabelHeight_); + mainWin_ ->SetSize(rowLabelWidth, colLabelHeight_, mainWinSize.GetWidth(), mainWinSize.GetHeight()); + + //avoid flicker in wxWindowMSW::HandleSize() when calling ::EndDeferWindowPos() where the sub-windows are moved only although they need to be redrawn! + colLabelWin_->Refresh(); + mainWin_ ->Refresh(); + + //3. update scrollbars: "guide wxScrolledHelper to not screw up too much" + if (updateScrollbar) + { + const int mainWinWidthGross = getMainWinSize(GetSize()).GetWidth(); + + if (logicalHeight <= mainWinHeightGross && + getColWidthsSum(mainWinWidthGross) <= mainWinWidthGross && + //this special case needs to be considered *only* when both scrollbars are flexible: + showScrollbarX_ == SB_SHOW_AUTOMATIC && + showScrollbarY_ == SB_SHOW_AUTOMATIC) + setScrollbars2(0, 0); //no scrollbars required at all! -> wxScrolledWindow requires active help to detect this special case! + else + { + const int logicalWidthTmp = getColWidthsSum(mainWinSize.GetWidth()); //assuming vertical scrollbar stays as it is... + setScrollbars2(logicalWidthTmp, logicalHeight); //if scrollbars are shown or hidden a new resize event recurses into updateWindowSizes() + /* + is there a risk of endless recursion? No, 2-level recursion at most, consider the following 6 cases: + + <----------gw----------> + <----------nw------> + ------------------------ /|\ /|\ + | | | | | + | main window | | nh | + | | | | gh + ------------------------ \|/ | + | | | | + ------------------------ \|/ + gw := gross width + nw := net width := gross width - sb size + gh := gross height + nh := net height := gross height - sb size + + There are 6 cases that can occur: + --------------------------------- + lw := logical width + lh := logical height + + 1. lw <= gw && lh <= gh => no scrollbars needed + + 2. lw > gw && lh > gh => need both scrollbars + + 3. lh > gh + 4.1 lw <= nw => need vertical scrollbar only + 4.2 nw < lw <= gw => need both scrollbars + + 4. lw > gw + 3.1 lh <= nh => need horizontal scrollbar only + 3.2 nh < lh <= gh => need both scrollbars + */ + } + } +} + + +wxSize Grid::GetSizeAvailableForScrollTarget(const wxSize& size) +{ + //harmonize with Grid::updateWindowSizes()! + + //1. calculate row label width independent from scrollbars + const int mainWinHeightGross = std::max(size.GetHeight() - colLabelHeight_, 0); //independent from client sizes and scrollbars! + const ptrdiff_t logicalHeight = rowLabelWin_->getLogicalHeight(); // + + int rowLabelWidth = 0; + if (drawRowLabel_ && logicalHeight > 0) + { + ptrdiff_t yFrom = CalcUnscrolledPosition(wxPoint(0, 0)).y; + ptrdiff_t yTo = CalcUnscrolledPosition(wxPoint(0, mainWinHeightGross - 1)).y ; + numeric::clamp<ptrdiff_t>(yFrom, 0, logicalHeight - 1); + numeric::clamp<ptrdiff_t>(yTo, 0, logicalHeight - 1); + + const ptrdiff_t rowFrom = rowLabelWin_->getRowAtPos(yFrom); + const ptrdiff_t rowTo = rowLabelWin_->getRowAtPos(yTo); + if (rowFrom >= 0 && rowTo >= 0) + rowLabelWidth = rowLabelWin_->getBestWidth(rowFrom, rowTo); + } + + return size - wxSize(rowLabelWidth, colLabelHeight_); +} + + +void Grid::onPaintEvent(wxPaintEvent& event) { wxPaintDC dc(this); } + + +void Grid::onKeyDown(wxKeyEvent& event) +{ + int keyCode = event.GetKeyCode(); + if (GetLayoutDirection() == wxLayout_RightToLeft) + { + if (keyCode == WXK_LEFT || keyCode == WXK_NUMPAD_LEFT) + keyCode = WXK_RIGHT; + else if (keyCode == WXK_RIGHT || keyCode == WXK_NUMPAD_RIGHT) + keyCode = WXK_LEFT; + } + + const ptrdiff_t rowCount = getRowCount(); + const ptrdiff_t cursorRow = mainWin_->getCursor(); + + auto moveCursorTo = [&](ptrdiff_t row) + { + if (rowCount > 0) + { + numeric::clamp<ptrdiff_t>(row, 0, rowCount - 1); + setGridCursor(row); + } + }; + + auto selectWithCursorTo = [&](ptrdiff_t row) + { + if (rowCount > 0) + { + numeric::clamp<ptrdiff_t>(row, 0, rowCount - 1); + selectWithCursor(row); + } + }; + + switch (keyCode) + { + //case WXK_TAB: + // if (Navigate(event.ShiftDown() ? wxNavigationKeyEvent::IsBackward : wxNavigationKeyEvent::IsForward)) + // return; + // break; + + case WXK_UP: + case WXK_NUMPAD_UP: + if (event.ShiftDown()) + selectWithCursorTo(cursorRow - 1); + else if (event.ControlDown()) + scrollDelta(0, -1); + else + moveCursorTo(cursorRow - 1); + return; //swallow event: wxScrolledWindow, wxWidgets 2.9.3 on Kubuntu x64 processes arrow keys: prevent this! + + case WXK_DOWN: + case WXK_NUMPAD_DOWN: + if (event.ShiftDown()) + selectWithCursorTo(cursorRow + 1); + else if (event.ControlDown()) + scrollDelta(0, 1); + else + moveCursorTo(cursorRow + 1); + return; //swallow event + + case WXK_LEFT: + case WXK_NUMPAD_LEFT: + if (event.ControlDown()) + scrollDelta(-1, 0); + else if (event.ShiftDown()) + ; + else + moveCursorTo(cursorRow); + return; + + case WXK_RIGHT: + case WXK_NUMPAD_RIGHT: + if (event.ControlDown()) + scrollDelta(1, 0); + else if (event.ShiftDown()) + ; + else + moveCursorTo(cursorRow); + return; + + case WXK_HOME: + case WXK_NUMPAD_HOME: + if (event.ShiftDown()) + selectWithCursorTo(0); + //else if (event.ControlDown()) + // ; + else + moveCursorTo(0); + return; + + case WXK_END: + case WXK_NUMPAD_END: + if (event.ShiftDown()) + selectWithCursorTo(rowCount - 1); + //else if (event.ControlDown()) + // ; + else + moveCursorTo(rowCount - 1); + return; + + case WXK_PAGEUP: + case WXK_NUMPAD_PAGEUP: + if (event.ShiftDown()) + selectWithCursorTo(cursorRow - GetClientSize().GetHeight() / rowLabelWin_->getRowHeight()); + //else if (event.ControlDown()) + // ; + else + moveCursorTo(cursorRow - GetClientSize().GetHeight() / rowLabelWin_->getRowHeight()); + return; + + case WXK_PAGEDOWN: + case WXK_NUMPAD_PAGEDOWN: + if (event.ShiftDown()) + selectWithCursorTo(cursorRow + GetClientSize().GetHeight() / rowLabelWin_->getRowHeight()); + //else if (event.ControlDown()) + // ; + else + moveCursorTo(cursorRow + GetClientSize().GetHeight() / rowLabelWin_->getRowHeight()); + return; + + case 'A': //Ctrl + A - select all + if (event.ControlDown()) + selectRangeAndNotify(0, rowCount, true /*positive*/, nullptr /*mouseInitiated*/); + break; + + case WXK_NUMPAD_ADD: //CTRL + '+' - auto-size all + if (event.ControlDown()) + autoSizeColumns(ALLOW_GRID_EVENT); + return; + } + + event.Skip(); +} + + +void Grid::setColumnLabelHeight(int height) +{ + colLabelHeight_ = std::max(0, height); + updateWindowSizes(); +} + + +void Grid::showRowLabel(bool show) +{ + drawRowLabel_ = show; + updateWindowSizes(); +} + + +void Grid::selectAllRows(GridEventPolicy rangeEventPolicy) +{ + selection_.selectAll(); + mainWin_->Refresh(); + + if (rangeEventPolicy == ALLOW_GRID_EVENT) //notify event, even if we're not triggered by user interaction + { + GridRangeSelectEvent selEvent(0, getRowCount(), true, nullptr); + if (wxEvtHandler* evtHandler = GetEventHandler()) + evtHandler->ProcessEvent(selEvent); + } +} + + +void Grid::clearSelection(GridEventPolicy rangeEventPolicy) +{ + selection_.clear(); + mainWin_->Refresh(); + + if (rangeEventPolicy == ALLOW_GRID_EVENT) //notify event, even if we're not triggered by user interaction + { + GridRangeSelectEvent unselectionEvent(0, getRowCount(), false, nullptr); + if (wxEvtHandler* evtHandler = GetEventHandler()) + evtHandler->ProcessEvent(unselectionEvent); + } +} + + +void Grid::scrollDelta(int deltaX, int deltaY) +{ + int scrollPosX = 0; + int scrollPosY = 0; + GetViewStart(&scrollPosX, &scrollPosY); + + scrollPosX += deltaX; + scrollPosY += deltaY; + + scrollPosX = std::max(0, scrollPosX); //wxScrollHelper::Scroll() will exit prematurely if input happens to be "-1"! + scrollPosY = std::max(0, scrollPosY); // + + Scroll(scrollPosX, scrollPosY); //internally calls wxWindows::Update()! + updateWindowSizes(); //may show horizontal scroll bar +} + + +void Grid::redirectRowLabelEvent(wxMouseEvent& event) +{ + event.m_x = 0; + if (wxEvtHandler* evtHandler = mainWin_->GetEventHandler()) + evtHandler->ProcessEvent(event); + + if (event.ButtonDown() && wxWindow::FindFocus() != mainWin_) + mainWin_->SetFocus(); +} + + +size_t Grid::getRowCount() const +{ + return dataView_ ? dataView_->getRowCount() : 0; +} + + +void Grid::Refresh(bool eraseBackground, const wxRect* rect) +{ + const size_t rowCountNew = getRowCount(); + if (rowCountOld_ != rowCountNew) + { + rowCountOld_ = rowCountNew; + updateWindowSizes(); + } + + if (selection_.maxSize() != rowCountNew) //clear selection only when needed (consider setSelectedRows()) + selection_.init(rowCountNew); + + wxScrolledWindow::Refresh(eraseBackground, rect); +} + + +void Grid::setRowHeight(int height) +{ + rowLabelWin_->setRowHeight(height); + updateWindowSizes(); + Refresh(); +} + + +void Grid::setColumnConfig(const std::vector<Grid::ColumnAttribute>& attr) +{ + //hold ownership of non-visible columns + oldColAttributes_ = attr; + + std::vector<VisibleColumn> visCols; + for (const ColumnAttribute& ca : attr) + { + assert(ca.type_ != ColumnType::NONE); + if (ca.visible_) + visCols.emplace_back(ca.type_, ca.offset_, ca.stretch_); + } + + //"ownership" of visible columns is now within Grid + visibleCols_ = visCols; + + updateWindowSizes(); + Refresh(); +} + + +std::vector<Grid::ColumnAttribute> Grid::getColumnConfig() const +{ + //get non-visible columns (+ outdated visible ones) + std::vector<ColumnAttribute> output = oldColAttributes_; + + auto iterVcols = visibleCols_.begin(); + auto iterVcolsend = visibleCols_.end(); + + //update visible columns but keep order of non-visible ones! + for (ColumnAttribute& ca : output) + if (ca.visible_) + { + if (iterVcols != iterVcolsend) + { + ca.type_ = iterVcols->type_; + ca.stretch_ = iterVcols->stretch_; + ca.offset_ = iterVcols->offset_; + ++iterVcols; + } + else + assert(false); + } + assert(iterVcols == iterVcolsend); + + return output; +} + + +void Grid::showScrollBars(Grid::ScrollBarStatus horizontal, Grid::ScrollBarStatus vertical) +{ + if (showScrollbarX_ == horizontal && + showScrollbarY_ == vertical) return; //support polling! + + showScrollbarX_ = horizontal; + showScrollbarY_ = vertical; + + //the following wxGTK approach is pretty much identical to wxWidgets 2.9 ShowScrollbars() code! + + auto mapStatus = [](ScrollBarStatus sbStatus) -> GtkPolicyType + { + switch (sbStatus) + { + case SB_SHOW_AUTOMATIC: + return GTK_POLICY_AUTOMATIC; + case SB_SHOW_ALWAYS: + return GTK_POLICY_ALWAYS; + case SB_SHOW_NEVER: + return GTK_POLICY_NEVER; + } + assert(false); + return GTK_POLICY_AUTOMATIC; + }; + + GtkWidget* gridWidget = wxWindow::m_widget; + GtkScrolledWindow* scrolledWindow = GTK_SCROLLED_WINDOW(gridWidget); + ::gtk_scrolled_window_set_policy(scrolledWindow, + mapStatus(horizontal), + mapStatus(vertical)); + + updateWindowSizes(); +} + + + +wxWindow& Grid::getCornerWin () { return *cornerWin_; } +wxWindow& Grid::getRowLabelWin() { return *rowLabelWin_; } +wxWindow& Grid::getColLabelWin() { return *colLabelWin_; } +wxWindow& Grid::getMainWin () { return *mainWin_; } +const wxWindow& Grid::getMainWin() const { return *mainWin_; } + + +Opt<Grid::ColAction> Grid::clientPosToColumnAction(const wxPoint& pos) const +{ + const int absPosX = CalcUnscrolledPosition(pos).x; + if (absPosX >= 0) + { + const int resizeTolerance = allowColumnResize_ ? COLUMN_RESIZE_TOLERANCE : 0; + std::vector<ColumnWidth> absWidths = getColWidths(); //resolve stretched widths + + int accuWidth = 0; + for (size_t col = 0; col < absWidths.size(); ++col) + { + accuWidth += absWidths[col].width_; + if (std::abs(absPosX - accuWidth) < resizeTolerance) + { + ColAction out; + out.wantResize = true; + out.col = col; + return out; + } + else if (absPosX < accuWidth) + { + ColAction out; + out.wantResize = false; + out.col = col; + return out; + } + } + } + return NoValue(); +} + + +void Grid::moveColumn(size_t colFrom, size_t colTo) +{ + if (colFrom < visibleCols_.size() && + colTo < visibleCols_.size() && + colTo != colFrom) + { + const VisibleColumn colAtt = visibleCols_[colFrom]; + visibleCols_.erase (visibleCols_.begin() + colFrom); + visibleCols_.insert(visibleCols_.begin() + colTo, colAtt); + } +} + + +ptrdiff_t Grid::clientPosToMoveTargetColumn(const wxPoint& pos) const +{ + + const int absPosX = CalcUnscrolledPosition(pos).x; + + int accWidth = 0; + std::vector<ColumnWidth> absWidths = getColWidths(); //resolve negative/stretched widths + for (auto itCol = absWidths.begin(); itCol != absWidths.end(); ++itCol) + { + const int width = itCol->width_; //beware dreaded unsigned conversions! + accWidth += width; + + if (absPosX < accWidth - width / 2) + return itCol - absWidths.begin(); + } + return absWidths.size(); +} + + +ColumnType Grid::colToType(size_t col) const +{ + if (col < visibleCols_.size()) + return visibleCols_[col].type_; + return ColumnType::NONE; +} + + +ptrdiff_t Grid::getRowAtPos(int posY) const { return rowLabelWin_->getRowAtPos(posY); } + + +Grid::ColumnPosInfo Grid::getColumnAtPos(int posX) const +{ + if (posX >= 0) + { + int accWidth = 0; + for (const ColumnWidth& cw : getColWidths()) + { + accWidth += cw.width_; + if (posX < accWidth) + return { cw.type_, posX + cw.width_ - accWidth, cw.width_ }; + } + } + return { ColumnType::NONE, 0, 0 }; +} + + +wxRect Grid::getColumnLabelArea(ColumnType colType) const +{ + std::vector<ColumnWidth> absWidths = getColWidths(); //resolve negative/stretched widths + + //colType is not unique in general, but *this* function expects it! + assert(std::count_if(absWidths.begin(), absWidths.end(), [&](const ColumnWidth& cw) { return cw.type_ == colType; }) <= 1); + + auto itCol = std::find_if(absWidths.begin(), absWidths.end(), [&](const ColumnWidth& cw) { return cw.type_ == colType; }); + if (itCol != absWidths.end()) + { + ptrdiff_t posX = 0; + for (auto it = absWidths.begin(); it != itCol; ++it) + posX += it->width_; + + return wxRect(wxPoint(posX, 0), wxSize(itCol->width_, colLabelHeight_)); + } + return wxRect(); +} + + +void Grid::refreshCell(size_t row, ColumnType colType) +{ + const wxRect& colArea = getColumnLabelArea(colType); //returns empty rect if column not found + const wxRect& rowArea = rowLabelWin_->getRowLabelArea(row); //returns empty rect if row not found + if (colArea.height > 0 && rowArea.height > 0) + { + const wxPoint topLeft = CalcScrolledPosition(wxPoint(colArea.x, rowArea.y)); //absolute -> client coordinates + const wxRect cellArea(topLeft, wxSize(colArea.width, rowArea.height)); + + getMainWin().RefreshRect(cellArea, false); + } +} + + +void Grid::setGridCursor(size_t row) +{ + mainWin_->setCursor(row, row); + makeRowVisible(row); + + selection_.clear(); //clear selection, do NOT fire event + selectRangeAndNotify(row, row, true /*positive*/, nullptr /*mouseInitiated*/); //set new selection + fire event + + mainWin_->Refresh(); + rowLabelWin_->Refresh(); //row labels! (Kubuntu) +} + + +void Grid::selectWithCursor(ptrdiff_t row) +{ + const size_t anchorRow = mainWin_->getAnchor(); + + mainWin_->setCursor(row, anchorRow); + makeRowVisible(row); + + selection_.clear(); //clear selection, do NOT fire event + selectRangeAndNotify(anchorRow, row, true /*positive*/, nullptr /*mouseInitiated*/); //set new selection + fire event + + mainWin_->Refresh(); + rowLabelWin_->Refresh(); +} + + +void Grid::makeRowVisible(size_t row) +{ + const wxRect labelRect = rowLabelWin_->getRowLabelArea(row); //returns empty rect if row not found + if (labelRect.height > 0) + { + int scrollPosX = 0; + GetViewStart(&scrollPosX, nullptr); + + int pixelsPerUnitY = 0; + GetScrollPixelsPerUnit(nullptr, &pixelsPerUnitY); + if (pixelsPerUnitY <= 0) return; + + const int clientPosY = CalcScrolledPosition(labelRect.GetTopLeft()).y; + if (clientPosY < 0) + { + const int scrollPosY = labelRect.y / pixelsPerUnitY; + Scroll(scrollPosX, scrollPosY); //internally calls wxWindows::Update()! + updateWindowSizes(); //may show horizontal scroll bar + } + else if (clientPosY + labelRect.height > rowLabelWin_->GetClientSize().GetHeight()) + { + auto execScroll = [&](int clientHeight) + { + const int scrollPosY = std::ceil((labelRect.y - clientHeight + + labelRect.height) / static_cast<double>(pixelsPerUnitY)); + Scroll(scrollPosX, scrollPosY); + updateWindowSizes(); //may show horizontal scroll bar + }; + + const int clientHeightBefore = rowLabelWin_->GetClientSize().GetHeight(); + execScroll(clientHeightBefore); + + //client height may decrease after scroll due to a new horizontal scrollbar, resulting in a partially visible last row + const int clientHeightAfter = rowLabelWin_->GetClientSize().GetHeight(); + if (clientHeightAfter < clientHeightBefore) + execScroll(clientHeightAfter); + } + } +} + + +void Grid::selectRangeAndNotify(ptrdiff_t rowFrom, ptrdiff_t rowTo, bool positive, const GridClickEvent* mouseInitiated) +{ + //sort + convert to half-open range + auto rowFirst = std::min(rowFrom, rowTo); + auto rowLast = std::max(rowFrom, rowTo) + 1; + + const size_t rowCount = getRowCount(); + numeric::clamp<ptrdiff_t>(rowFirst, 0, rowCount); + numeric::clamp<ptrdiff_t>(rowLast, 0, rowCount); + + selection_.selectRange(rowFirst, rowLast, positive); + + //notify event + GridRangeSelectEvent selectionEvent(rowFirst, rowLast, positive, mouseInitiated); + if (wxEvtHandler* evtHandler = GetEventHandler()) + evtHandler->ProcessEvent(selectionEvent); + + mainWin_->Refresh(); +} + + +void Grid::scrollTo(size_t row) +{ + const wxRect labelRect = rowLabelWin_->getRowLabelArea(row); //returns empty rect if row not found + if (labelRect.height > 0) + { + int pixelsPerUnitY = 0; + GetScrollPixelsPerUnit(nullptr, &pixelsPerUnitY); + if (pixelsPerUnitY > 0) + { + const int scrollPosYNew = labelRect.y / pixelsPerUnitY; + int scrollPosXOld = 0; + int scrollPosYOld = 0; + GetViewStart(&scrollPosXOld, &scrollPosYOld); + + if (scrollPosYOld != scrollPosYNew) //support polling + { + Scroll(scrollPosXOld, scrollPosYNew); //internally calls wxWindows::Update()! + updateWindowSizes(); //may show horizontal scroll bar + Refresh(); + } + } + } +} + + +bool Grid::Enable(bool enable) +{ + Refresh(); + return wxScrolledWindow::Enable(enable); +} + + +size_t Grid::getGridCursor() const +{ + return mainWin_->getCursor(); +} + + +int Grid::getBestColumnSize(size_t col) const +{ + if (dataView_ && col < visibleCols_.size()) + { + const ColumnType type = visibleCols_[col].type_; + + wxClientDC dc(mainWin_); + dc.SetFont(mainWin_->GetFont()); //harmonize with MainWin::render() + + int maxSize = 0; + + auto rowRange = rowLabelWin_->getRowsOnClient(mainWin_->GetClientRect()); //returns range [begin, end) + for (auto row = rowRange.first; row < rowRange.second; ++row) + maxSize = std::max(maxSize, dataView_->getBestSize(dc, row, type)); + + return maxSize; + } + return -1; +} + + +void Grid::setColumnWidth(int width, size_t col, GridEventPolicy columnResizeEventPolicy, bool notifyAsync) +{ + if (col < visibleCols_.size()) + { + VisibleColumn& vcRs = visibleCols_[col]; + + const std::vector<int> stretchedWidths = getColStretchedWidths(mainWin_->GetClientSize().GetWidth()); + if (stretchedWidths.size() != visibleCols_.size()) + { + assert(false); + return; + } + //CAVEATS: + //I. fixed-size columns: normalize offset so that resulting width is at least COLUMN_MIN_WIDTH: this is NOT enforced by getColWidths()! + //II. stretched columns: do not allow user to set offsets so small that they result in negative (non-normalized) widths: this gives an + //unusual delay when enlarging the column again later + width = std::max(width, COLUMN_MIN_WIDTH); + + vcRs.offset_ = width - stretchedWidths[col]; //width := stretchedWidth + offset + + //III. resizing any column should normalize *all* other stretched columns' offsets considering current mainWinWidth! + // test case: + //1. have columns, both fixed-size and stretched, fit whole window width + //2. shrink main window width so that horizontal scrollbars are shown despite the streched column + //3. shrink a fixed-size column so that the scrollbars vanish and columns cover full width again + //4. now verify that the stretched column is resizing immediately if main window is enlarged again + for (size_t col2 = 0; col2 < visibleCols_.size(); ++col2) + if (visibleCols_[col2].stretch_ > 0) //normalize stretched columns only + visibleCols_[col2].offset_ = std::max(visibleCols_[col2].offset_, COLUMN_MIN_WIDTH - stretchedWidths[col2]); + + if (columnResizeEventPolicy == ALLOW_GRID_EVENT) + { + GridColumnResizeEvent sizeEvent(vcRs.offset_, vcRs.type_); + if (wxEvtHandler* evtHandler = GetEventHandler()) + { + if (notifyAsync) + evtHandler->AddPendingEvent(sizeEvent); + else + evtHandler->ProcessEvent(sizeEvent); + } + } + } + else + assert(false); +} + + +void Grid::autoSizeColumns(GridEventPolicy columnResizeEventPolicy) +{ + if (allowColumnResize_) + { + for (size_t col = 0; col < visibleCols_.size(); ++col) + { + const int bestWidth = getBestColumnSize(col); //return -1 on error + if (bestWidth >= 0) + setColumnWidth(bestWidth, col, columnResizeEventPolicy, true); + } + updateWindowSizes(); + Refresh(); + } +} + + +std::vector<int> Grid::getColStretchedWidths(int clientWidth) const //final width = (normalized) (stretchedWidth + offset) +{ + assert(clientWidth >= 0); + clientWidth = std::max(clientWidth, 0); + int stretchTotal = 0; + for (const VisibleColumn& vc : visibleCols_) + { + assert(vc.stretch_ >= 0); + stretchTotal += vc.stretch_; + } + + int remainingWidth = clientWidth; + + std::vector<int> output; + + if (stretchTotal <= 0) + output.resize(visibleCols_.size()); //fill with zeros + else + { + for (const VisibleColumn& vc : visibleCols_) + { + const int width = clientWidth * vc.stretch_ / stretchTotal; //rounds down! + output.push_back(width); + remainingWidth -= width; + } + + //distribute *all* of clientWidth: should suffice to enlarge the first few stretched columns; no need to minimize total absolute error of distribution + if (remainingWidth > 0) + for (size_t col2 = 0; col2 < visibleCols_.size(); ++col2) + if (visibleCols_[col2].stretch_ > 0) + { + ++output[col2]; + if (--remainingWidth == 0) + break; + } + assert(remainingWidth == 0); + } + return output; +} + + +std::vector<Grid::ColumnWidth> Grid::getColWidths() const +{ + return getColWidths(mainWin_->GetClientSize().GetWidth()); +} + + +std::vector<Grid::ColumnWidth> Grid::getColWidths(int mainWinWidth) const //evaluate stretched columns +{ + const std::vector<int> stretchedWidths = getColStretchedWidths(mainWinWidth); + assert(stretchedWidths.size() == visibleCols_.size()); + + std::vector<ColumnWidth> output; + for (size_t col2 = 0; col2 < visibleCols_.size(); ++col2) + { + const auto& vc = visibleCols_[col2]; + int width = stretchedWidths[col2] + vc.offset_; + + if (vc.stretch_ > 0) + width = std::max(width, COLUMN_MIN_WIDTH); //normalization really needed here: e.g. smaller main window would result in negative width + else + width = std::max(width, 0); //support smaller width than COLUMN_MIN_WIDTH if set via configuration + + output.emplace_back(vc.type_, width); + } + return output; +} + + +int Grid::getColWidthsSum(int mainWinWidth) const +{ + int sum = 0; + for (const ColumnWidth& cw : getColWidths(mainWinWidth)) + sum += cw.width_; + return sum; +} @@ -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 <memory>
-#include <numeric>
-#include <vector>
-#include <wx/scrolwin.h>
-#include <zen/basic_math.h>
-#include <zen/optional.h>
-
-//a user-friendly, extensible and high-performance grid control
-
-namespace zen
-{
-enum class ColumnType { NONE = -1 }; //user-defiend column type
-enum class HoverArea { NONE = -1 }; //user-defined area for mouse selections for a given row (may span multiple columns or split a single column into multiple areas)
-
-//------------------------ events ------------------------------------------------
-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<GridClickEvent>()) { 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<GridClickEvent> 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<ColumnAttribute>& attr); //set column count + widths
- std::vector<ColumnAttribute> getColumnConfig() const;
-
- void setDataProvider(const std::shared_ptr<GridData>& dataView) { dataView_ = dataView; }
- /**/ GridData* getDataProvider() { return dataView_.get(); }
- const GridData* getDataProvider() const { return dataView_.get(); }
- //-----------------------------------------------------------------------------
-
- void setColumnLabelHeight(int height);
- void showRowLabel(bool visible);
-
- enum ScrollBarStatus
- {
- SB_SHOW_AUTOMATIC,
- SB_SHOW_ALWAYS,
- SB_SHOW_NEVER,
- };
- //alternative until wxScrollHelper::ShowScrollbars() becomes available in wxWidgets 2.9
- void showScrollBars(ScrollBarStatus horizontal, ScrollBarStatus vertical);
-
- std::vector<size_t> getSelectedRows() const { return selection_.get(); }
- void 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<size_t> get() const
- {
- std::vector<size_t> 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<size_t>(rowFirst, 0, rowSelectionValue.size());
- numeric::clamp<size_t>(rowLast, 0, rowSelectionValue.size());
-
- std::fill(rowSelectionValue.begin() + rowFirst, rowSelectionValue.begin() + rowLast, positive);
- }
- else assert(false);
- }
-
- private:
- std::vector<char> rowSelectionValue; //effectively a vector<bool> 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<ColumnWidth> getColWidths() const; //
- std::vector<ColumnWidth> getColWidths(int mainWinWidth) const; //evaluate stretched columns
- int getColWidthsSum(int mainWinWidth) const;
- std::vector<int> getColStretchedWidths(int clientWidth) const; //final width = (normalized) (stretchedWidth + offset)
-
- Opt<int> 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<ColAction> 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<GridData> dataView_;
- Selection selection_;
- bool allowColumnMove_ = true;
- bool allowColumnResize_ = true;
-
- std::vector<VisibleColumn> visibleCols_; //individual widths, type and total column count
- std::vector<ColumnAttribute> 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 <memory> +#include <numeric> +#include <vector> +#include <wx/scrolwin.h> +#include <zen/basic_math.h> +#include <zen/optional.h> + +//a user-friendly, extensible and high-performance grid control + +namespace zen +{ +enum class ColumnType { NONE = -1 }; //user-defiend column type +enum class HoverArea { NONE = -1 }; //user-defined area for mouse selections for a given row (may span multiple columns or split a single column into multiple areas) + +//------------------------ events ------------------------------------------------ +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<GridClickEvent>()) { 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<GridClickEvent> 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<ColumnAttribute>& attr); //set column count + widths + std::vector<ColumnAttribute> getColumnConfig() const; + + void setDataProvider(const std::shared_ptr<GridData>& dataView) { dataView_ = dataView; } + /**/ GridData* getDataProvider() { return dataView_.get(); } + const GridData* getDataProvider() const { return dataView_.get(); } + //----------------------------------------------------------------------------- + + void setColumnLabelHeight(int height); + void showRowLabel(bool visible); + + enum ScrollBarStatus + { + SB_SHOW_AUTOMATIC, + SB_SHOW_ALWAYS, + SB_SHOW_NEVER, + }; + //alternative until wxScrollHelper::ShowScrollbars() becomes available in wxWidgets 2.9 + void showScrollBars(ScrollBarStatus horizontal, ScrollBarStatus vertical); + + std::vector<size_t> getSelectedRows() const { return selection_.get(); } + void 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<size_t> get() const + { + std::vector<size_t> 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<size_t>(rowFirst, 0, rowSelectionValue.size()); + numeric::clamp<size_t>(rowLast, 0, rowSelectionValue.size()); + + std::fill(rowSelectionValue.begin() + rowFirst, rowSelectionValue.begin() + rowLast, positive); + } + else assert(false); + } + + private: + std::vector<char> rowSelectionValue; //effectively a vector<bool> 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<ColumnWidth> getColWidths() const; // + std::vector<ColumnWidth> getColWidths(int mainWinWidth) const; //evaluate stretched columns + int getColWidthsSum(int mainWinWidth) const; + std::vector<int> getColStretchedWidths(int clientWidth) const; //final width = (normalized) (stretchedWidth + offset) + + Opt<int> 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<ColAction> 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<GridData> dataView_; + Selection selection_; + bool allowColumnMove_ = true; + bool allowColumnResize_ = true; + + std::vector<VisibleColumn> visibleCols_; //individual widths, type and total column count + std::vector<ColumnAttribute> 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 <wx/app.h>
- #include <zen/thread.h> //std::thread::id
- #include <wx/protocol/http.h>
-
-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<wxString>(*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<std::wstring>(L"HTTP status code %x.", L"%x", numberTo<std::wstring>(sc)));
-
- if (!httpStream_ || webAccess_.GetError() != wxPROTO_NOERR)
- throw SysError(L"wxHTTP::GetError (" + numberTo<std::wstring>(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<char*>(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<std::string>(__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<std::wstring>(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<wxInputStream> httpStream_; //must be deleted BEFORE webAccess is closed
-
- std::vector<char> memBuf_;
- const IOCallback notifyUnbufferedIO_; //throw X
-};
-
-
-HttpInputStream::HttpInputStream(std::unique_ptr<Impl>&& 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<std::string>(*pimpl_); } //throw SysError, X;
-
-namespace
-{
-std::unique_ptr<HttpInputStream::Impl> 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<HttpInputStream::Impl>(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<char, char> 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<std::pair<std::string, std::string>>& 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<std::pair<std::string, std::string>> zen::xWwwFormUrlDecode(const std::string& str)
-{
- std::vector<std::pair<std::string, std::string>> 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<std::pair<std::string, std::string>>& 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<wxInputStream> 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 <wx/app.h> + #include <zen/thread.h> //std::thread::id + #include <wx/protocol/http.h> + +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<wxString>(*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<std::wstring>(L"HTTP status code %x.", L"%x", numberTo<std::wstring>(sc))); + + if (!httpStream_ || webAccess_.GetError() != wxPROTO_NOERR) + throw SysError(L"wxHTTP::GetError (" + numberTo<std::wstring>(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<char*>(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<std::string>(__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<std::wstring>(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<wxInputStream> httpStream_; //must be deleted BEFORE webAccess is closed + + std::vector<char> memBuf_; + const IOCallback notifyUnbufferedIO_; //throw X +}; + + +HttpInputStream::HttpInputStream(std::unique_ptr<Impl>&& 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<std::string>(*pimpl_); } //throw SysError, X; + +namespace +{ +std::unique_ptr<HttpInputStream::Impl> 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<HttpInputStream::Impl>(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<char, char> 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<std::pair<std::string, std::string>>& 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<std::pair<std::string, std::string>> zen::xWwwFormUrlDecode(const std::string& str) +{ + std::vector<std::pair<std::string, std::string>> 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<std::pair<std::string, std::string>>& 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<wxInputStream> 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! +} @@ -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 <zen/file_error.h>
-#include <zen/serialize.h>
-
-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<Impl>&& pimpl);
- HttpInputStream(HttpInputStream&&) = default;
- ~HttpInputStream();
-
-private:
- std::unique_ptr<Impl> 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<std::pair<std::string, std::string>>& postParams); //throw SysError
-bool internetIsAlive(); //noexcept
-
-std::string xWwwFormUrlEncode(const std::vector<std::pair<std::string, std::string>>& paramPairs);
-std::vector<std::pair<std::string, std::string>> 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 <zen/file_error.h> +#include <zen/serialize.h> + +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<Impl>&& pimpl); + HttpInputStream(HttpInputStream&&) = default; + ~HttpInputStream(); + +private: + std::unique_ptr<Impl> 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<std::pair<std::string, std::string>>& postParams); //throw SysError +bool internetIsAlive(); //noexcept + +std::string xWwwFormUrlEncode(const std::vector<std::pair<std::string, std::string>>& paramPairs); +std::vector<std::pair<std::string, std::string>> 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 <memory>
-#include <map>
-#include <zen/utf.h>
-#include <zen/globals.h>
-#include <zen/thread.h>
-#include <wx/wfstream.h>
-#include <wx/zipstrm.h>
-#include <wx/image.h>
-#include <wx/mstream.h>
-#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<char> 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<GlobalBitmaps> instance()
- {
- static Global<GlobalBitmaps> inst(std::make_unique<GlobalBitmaps>());
- 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<wxString, wxBitmap> bitmaps;
- std::map<wxString, wxAnimation> anims;
-};
-
-
-void GlobalBitmaps::init(const Zstring& filepath)
-{
- assert(bitmaps.empty() && anims.empty());
-
- wxFFileInputStream input(utfTo<wxString>(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<wxZipEntry> 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<GlobalBitmaps> inst = GlobalBitmaps::instance())
- inst->init(filepath);
- else
- assert(false);
-}
-
-
-void zen::cleanupResourceImages()
-{
- if (std::shared_ptr<GlobalBitmaps> inst = GlobalBitmaps::instance())
- inst->cleanup();
- else
- assert(false);
-}
-
-
-const wxBitmap& zen::getResourceImage(const wxString& name)
-{
- if (std::shared_ptr<GlobalBitmaps> inst = GlobalBitmaps::instance())
- return inst->getImage(name);
- assert(false);
- return wxNullBitmap;
-}
-
-
-const wxAnimation& zen::getResourceAnimation(const wxString& name)
-{
- if (std::shared_ptr<GlobalBitmaps> 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 <memory> +#include <map> +#include <zen/utf.h> +#include <zen/globals.h> +#include <zen/thread.h> +#include <wx/wfstream.h> +#include <wx/zipstrm.h> +#include <wx/image.h> +#include <wx/mstream.h> +#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<char> 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<GlobalBitmaps> instance() + { + static Global<GlobalBitmaps> inst(std::make_unique<GlobalBitmaps>()); + 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<wxString, wxBitmap> bitmaps; + std::map<wxString, wxAnimation> anims; +}; + + +void GlobalBitmaps::init(const Zstring& filepath) +{ + assert(bitmaps.empty() && anims.empty()); + + wxFFileInputStream input(utfTo<wxString>(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<wxZipEntry> 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<GlobalBitmaps> inst = GlobalBitmaps::instance()) + inst->init(filepath); + else + assert(false); +} + + +void zen::cleanupResourceImages() +{ + if (std::shared_ptr<GlobalBitmaps> inst = GlobalBitmaps::instance()) + inst->cleanup(); + else + assert(false); +} + + +const wxBitmap& zen::getResourceImage(const wxString& name) +{ + if (std::shared_ptr<GlobalBitmaps> inst = GlobalBitmaps::instance()) + return inst->getImage(name); + assert(false); + return wxNullBitmap; +} + + +const wxAnimation& zen::getResourceAnimation(const wxString& name) +{ + if (std::shared_ptr<GlobalBitmaps> 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 <wx/bitmap.h>
-#include <wx/animate.h>
-#include <zen/zstring.h>
-
-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 <wx/bitmap.h> +#include <wx/animate.h> +#include <zen/zstring.h> + +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 <zen/string_tools.h>
-#include <wx/app.h>
-
-
-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<unsigned char>((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 <zen/string_tools.h> +#include <wx/app.h> + + +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<unsigned char>((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 <numeric>
-#include <wx/bitmap.h>
-#include <wx/image.h>
-#include <wx/dcmemory.h>
-#include <zen/basic_math.h>
-
-
-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<double>(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<unsigned char>(std::min(255, c + level)); });
- else
- std::for_each(pixBegin, pixEnd, [&](unsigned char& c) { c = static_cast<unsigned char>(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<int>(h / 360) * 360;
- else if (h < 0)
- h -= static_cast<int>(h / 360) * 360 - 360;
- numeric::confine<double>(s, 0, 1);
- numeric::confine<double>(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<unsigned char>(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 <numeric> +#include <wx/bitmap.h> +#include <wx/image.h> +#include <wx/dcmemory.h> +#include <zen/basic_math.h> + + +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<double>(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<unsigned char>(std::min(255, c + level)); }); + else + std::for_each(pixBegin, pixEnd, [&](unsigned char& c) { c = static_cast<unsigned char>(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<int>(h / 360) * 360; + else if (h < 0) + h -= static_cast<int>(h / 360) * 360 - 360; + numeric::confine<double>(s, 0, 1); + numeric::confine<double>(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<unsigned char>(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 <wx/textctrl.h>
-#include <wx/stattext.h>
-
-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 <wx/textctrl.h> +#include <wx/stattext.h> + +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 <wx/app.h>
-#include <wx/display.h>
-#include <wx+/std_button_layout.h>
-#include <wx+/font_size.h>
-#include <wx+/image_resources.h>
-#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<int>(ConfirmationButton3::CANCEL)); }
- void OnCancel(wxCommandEvent& event) override { EndModal(static_cast<int>(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<int>(ConfirmationButton3::CANCEL));
- return;
- }
- event.Skip();
- }
-
- void OnButtonAffirmative(wxCommandEvent& event) override
- {
- if (checkBoxValue_)
- *checkBoxValue_ = m_checkBoxCustom->GetValue();
- EndModal(static_cast<int>(ConfirmationButton3::DO_IT));
- }
-
- void OnButtonNegative(wxCommandEvent& event) override
- {
- if (checkBoxValue_)
- *checkBoxValue_ = m_checkBoxCustom->GetValue();
- EndModal(static_cast<int>(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<ConfirmationButton>(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<ConfirmationButton3>(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 <wx/app.h> +#include <wx/display.h> +#include <wx+/std_button_layout.h> +#include <wx+/font_size.h> +#include <wx+/image_resources.h> +#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<int>(ConfirmationButton3::CANCEL)); } + void OnCancel(wxCommandEvent& event) override { EndModal(static_cast<int>(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<int>(ConfirmationButton3::CANCEL)); + return; + } + event.Skip(); + } + + void OnButtonAffirmative(wxCommandEvent& event) override + { + if (checkBoxValue_) + *checkBoxValue_ = m_checkBoxCustom->GetValue(); + EndModal(static_cast<int>(ConfirmationButton3::DO_IT)); + } + + void OnButtonNegative(wxCommandEvent& event) override + { + if (checkBoxValue_) + *checkBoxValue_ = m_checkBoxCustom->GetValue(); + EndModal(static_cast<int>(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<ConfirmationButton>(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<ConfirmationButton3>(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 <wx/window.h>
-#include <wx/bitmap.h>
-#include <wx/string.h>
-
-
-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 <wx+/image_resources.h>
-
-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<int>(ConfirmationButton3::DO_IT ), //[!]
- CANCEL = static_cast<int>(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 <wx/window.h> +#include <wx/bitmap.h> +#include <wx/string.h> + + +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 <wx+/image_resources.h> + +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<int>(ConfirmationButton3::DO_IT ), //[!] + CANCEL = static_cast<int>(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 <wx/artprov.h>
-#include <wx/xrc/xmlres.h>
-#include <wx/intl.h>
-#include <wx/bitmap.h>
-#include <wx/image.h>
-#include <wx/icon.h>
-#include <wx/statbmp.h>
-#include <wx/gdicmn.h>
-#include <wx/font.h>
-#include <wx/colour.h>
-#include <wx/settings.h>
-#include <wx/string.h>
-#include <wx/stattext.h>
-#include <wx/textctrl.h>
-#include <wx/sizer.h>
-#include <wx/panel.h>
-#include <wx/statline.h>
-#include <wx/checkbox.h>
-#include <wx/button.h>
-#include <wx/dialog.h>
-
-#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 <wx/artprov.h> +#include <wx/xrc/xmlres.h> +#include <wx/intl.h> +#include <wx/bitmap.h> +#include <wx/image.h> +#include <wx/icon.h> +#include <wx/statbmp.h> +#include <wx/gdicmn.h> +#include <wx/font.h> +#include <wx/colour.h> +#include <wx/settings.h> +#include <wx/string.h> +#include <wx/stattext.h> +#include <wx/textctrl.h> +#include <wx/sizer.h> +#include <wx/panel.h> +#include <wx/statline.h> +#include <wx/checkbox.h> +#include <wx/button.h> +#include <wx/dialog.h> + +#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__ @@ -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 <zen/optional.h>
-#include <wx/dcmemory.h>
-#include <wx/image.h>
-#include <wx/app.h>
-
-namespace zen
-{
-//functions supporting right-to-left GUI layout
-void drawBitmapRtlMirror (wxDC& dc, const wxBitmap& image, const wxRect& rect, int alignment, Opt<wxBitmap>& 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<wxBitmap>& 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 <zen/optional.h> +#include <wx/dcmemory.h> +#include <wx/image.h> +#include <wx/app.h> + +namespace zen +{ +//functions supporting right-to-left GUI layout +void drawBitmapRtlMirror (wxDC& dc, const wxBitmap& image, const wxRect& rect, int alignment, Opt<wxBitmap>& 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<wxBitmap>& 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 <algorithm>
-#include <wx/sizer.h>
-#include <wx/button.h>
-
-
-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<size_t>(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<size_t>(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 <algorithm> +#include <wx/sizer.h> +#include <wx/button.h> + + +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<size_t>(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<size_t>(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 <wx/bmpbuttn.h>
-#include <wx+/bitmap_button.h>
-
-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 <wx/bmpbuttn.h> +#include <wx+/bitmap_button.h> + +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 <wx/dialog.h>
-#include <wx/stattext.h>
-#include <wx/sizer.h>
-#include <wx/statbmp.h>
-#include <wx/settings.h>
-#include <wx/app.h>
-#include <wx+/image_tools.h>
-
-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 <wx/dialog.h> +#include <wx/stattext.h> +#include <wx/sizer.h> +#include <wx/statbmp.h> +#include <wx/settings.h> +#include <wx/app.h> +#include <wx+/image_tools.h> + +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 <wx/window.h>
-
-
-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 <wx/window.h> + + +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 <zlib.h> //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<uLong>(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<uLong>(trgLen);
- const int rv = ::compress2(static_cast<Bytef*>(trg), //Bytef* dest,
- &bufferSize, //uLongf* destLen,
- static_cast<const Bytef*>(src), //const Bytef* source,
- static_cast<uLong>(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<uLong>(trgLen);
- const int rv = ::uncompress(static_cast<Bytef*>(trg), //Bytef* dest,
- &bufferSize, //uLongf* destLen,
- static_cast<const Bytef*>(src), //const Bytef* source,
- static_cast<uLong>(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 <zlib.h> //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<uLong>(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<uLong>(trgLen); + const int rv = ::compress2(static_cast<Bytef*>(trg), //Bytef* dest, + &bufferSize, //uLongf* destLen, + static_cast<const Bytef*>(src), //const Bytef* source, + static_cast<uLong>(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<uLong>(trgLen); + const int rv = ::uncompress(static_cast<Bytef*>(trg), //Bytef* dest, + &bufferSize, //uLongf* destLen, + static_cast<const Bytef*>(src), //const Bytef* source, + static_cast<uLong>(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 <zen/serialize.h>
-
-
-namespace zen
-{
-class ZlibInternalError {};
-
-// compression level must be between 0 and 9:
-// 0: no compression
-// 9: best compression
-template <class BinContainer> //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 <class BinContainer>
-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 <class BinContainer>
-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<const char*>(&uncompressedSize),
- reinterpret_cast<const char*>(&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 <class BinContainer>
-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<char*>(&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<size_t>(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<size_t>(uncompressedSize)); //throw ZlibInternalError
- if (bytesWritten != static_cast<size_t>(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 <zen/serialize.h> + + +namespace zen +{ +class ZlibInternalError {}; + +// compression level must be between 0 and 9: +// 0: no compression +// 9: best compression +template <class BinContainer> //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 <class BinContainer> +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 <class BinContainer> +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<const char*>(&uncompressedSize), + reinterpret_cast<const char*>(&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 <class BinContainer> +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<char*>(&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<size_t>(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<size_t>(uncompressedSize)); //throw ZlibInternalError + if (bytesWritten != static_cast<size_t>(uncompressedSize)) + throw ZlibInternalError(); + } + return contOut; +} +} + +#endif //ZLIB_WRAP_H_428597064566 |