diff options
Diffstat (limited to 'wx+')
-rwxr-xr-x[-rw-r--r--] | wx+/app_main.h | 100 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/async_task.h | 293 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/bitmap_button.h | 176 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/choice_enum.h | 230 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/context_menu.h | 204 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/dc.h | 274 | ||||
-rwxr-xr-x | wx+/file_drop.cpp | 56 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/file_drop.h | 229 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/font_size.h | 132 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/graph.cpp | 1694 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/graph.h | 708 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/grid.cpp | 4494 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/grid.h | 729 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/http.cpp | 701 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/http.h | 97 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/image_resources.cpp | 339 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/image_resources.h | 46 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/image_tools.cpp | 466 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/image_tools.h | 516 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/no_flicker.h | 82 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/popup_dlg.cpp | 614 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/popup_dlg.h | 191 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/popup_dlg_generated.cpp | 194 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/popup_dlg_generated.h | 142 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/rtl.h | 190 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/std_button_layout.h | 263 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/string_conv.h | 56 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/timespan.h | 332 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/toggle_button.h | 134 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/tooltip.cpp | 201 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/tooltip.h | 64 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/zlib_wrap.cpp | 104 | ||||
-rwxr-xr-x[-rw-r--r--] | wx+/zlib_wrap.h | 228 |
33 files changed, 6959 insertions, 7320 deletions
diff --git a/wx+/app_main.h b/wx+/app_main.h index 02d08709..2202ebc5 100644..100755 --- a/wx+/app_main.h +++ b/wx+/app_main.h @@ -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 * -// ***************************************************************************** - -#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 7ac03949..36d9068e 100644..100755 --- a/wx+/async_task.h +++ b/wx+/async_task.h @@ -1,150 +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 - { -#ifndef NDEBUG - assert(resultReady() && !resultEvaluated_); - resultEvaluated_ = true; -#endif - 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! -#ifndef NDEBUG - bool resultEvaluated_ = false; -#endif -}; - - -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 18702dd3..2ed8a278 100644..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 d564fef8..9197db9a 100644..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 bd77100c..9daba261 100644..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 new file mode 100755 index 00000000..cab848b9 --- /dev/null +++ b/wx+/file_drop.cpp @@ -0,0 +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(utfCvrtTo<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 57880ce2..d90ffc20 100644..100755 --- a/wx+/file_drop.h +++ b/wx+/file_drop.h @@ -1,164 +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 <zen/utf.h> -#include <wx/window.h> -#include <wx/event.h> -#include <wx/dnd.h> - -#ifdef ZEN_WIN_VISTA_AND_LATER - #include <zen/win.h> -#endif - - -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); -*/ - - - - - - - - - -namespace impl -{ -inline -wxEventType getFileDropEventType() -{ - //inline functions have external linkage by default => this static is also extern, i.e. program wide unique! but defined in a header... ;) - static wxEventType dummy = wxNewEventType(); - return dummy; -} -} - -//define new event type -const wxEventType EVENT_DROP_FILE = impl::getFileDropEventType(); - -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) - - - -#ifdef ZEN_WIN_VISTA_AND_LATER -using DragDropValidator = bool (*)(const std::vector<Zstring>& shellItemPaths); //plain static function => no lifetime management needed! -using DragDropConsumer = std::function<void (const std::vector<Zstring>& shellItemPaths)>; - -namespace impl -{ -void registerDragDrop(HWND hwnd, const DragDropValidator& acceptDrop, const DragDropConsumer& onDrop); -void unregisterDragDrop(HWND hwnd); - -class DragDropCleanupWindow : private wxWindow -{ -public: - DragDropCleanupWindow(wxWindow& dropWindow) : wxWindow(&dropWindow, wxID_ANY), dropHwnd(dropWindow.GetHWND()) - { - Hide(); //this is just a dummy window so that its parent can have ownership - Disable(); - } - ~DragDropCleanupWindow() { impl::unregisterDragDrop(dropHwnd); } - -private: - const HWND dropHwnd; -}; -} - - -inline -void setupShellItemDrop(wxWindow& dropWindow, const DragDropValidator& acceptDrop) -{ - auto onDrop = [&dropWindow](const std::vector<Zstring>& shellItemPaths) - { - //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(shellItemPaths)); - }; - - impl::registerDragDrop(static_cast<HWND>(dropWindow.GetHWND()), acceptDrop, onDrop); - - //make sure clean-up is tied to dropWindow life-time: - new impl::DragDropCleanupWindow(dropWindow); //ownership passed to "dropWindow" -} -#endif - - -namespace impl -{ -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(utfCvrtTo<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_; -}; -} - - -inline -void setupFileDrop(wxWindow& wnd) { wnd.SetDropTarget(new impl::WindowDropTarget(wnd)); /*takes ownership*/ } -} - -#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 05055853..b117b09f 100644..100755 --- a/wx+/font_size.h +++ b/wx+/font_size.h @@ -1,81 +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> -#ifdef ZEN_WIN_VISTA_AND_LATER - #include <zen/win.h> - #include <Uxtheme.h> - #include <vsstyle.h> //TEXT_MAININSTRUCTION - #include <vssym32.h> //TMT_TEXTCOLOR -#endif - - -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(); -#ifdef ZEN_WIN //http://msdn.microsoft.com/de-DE/library/windows/desktop/aa974176#fonts - font.SetPointSize(wxNORMAL_FONT->GetPointSize() * 4 / 3); //integer round down - -#ifdef ZEN_WIN_VISTA_AND_LATER - //get main instruction color: don't hard-code, respect accessibility! - if (HTHEME hTheme = ::OpenThemeData(NULL, //__in HWND hwnd, - L"TEXTSTYLE")) //__in LPCWSTR pszClassList - { - ZEN_ON_SCOPE_EXIT(::CloseThemeData(hTheme)); - - COLORREF cr = {}; - if (::GetThemeColor(hTheme, //_In_ HTHEME hTheme, - TEXT_MAININSTRUCTION, // _In_ int iPartId, - 0, // _In_ int iStateId, - TMT_TEXTCOLOR, // _In_ int iPropId, - &cr) == S_OK) // _Out_ COLORREF *pColor - control.SetForegroundColour(wxColor(cr)); - } -#endif - -#elif defined ZEN_LINUX //https://developer.gnome.org/hig-book/3.2/hig-book.html#alert-text - font.SetPointSize(numeric::round(wxNORMAL_FONT->GetPointSize() * 12.0 / 11)); - font.SetWeight(wxFONTWEIGHT_BOLD); - -#elif defined ZEN_MAC //https://developer.apple.com/library/mac/documentation/UserExperience/Conceptual/AppleHIGuidelines/Windows/Windows.html#//apple_ref/doc/uid/20000961-TP10 - font.SetWeight(wxFONTWEIGHT_BOLD); -#endif - 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 d45a0e85..50c1daa6 100644..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 84927b6d..3b2d390d 100644..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 5d393f08..75abcd2f 100644..100755 --- a/wx+/grid.cpp +++ b/wx+/grid.cpp @@ -1,2274 +1,2220 @@ -// ***************************************************************************** -// * This file is part of the FreeFileSync project. It is distributed under * -// * GNU General Public License: http://www.gnu.org/licenses/gpl-3.0 * -// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * -// ***************************************************************************** - -#include "grid.h" -#include <cassert> -#include <set> -#include <chrono> -#include <wx/settings.h> -#include <wx/listbox.h> -#include <wx/tooltip.h> -#include <wx/timer.h> -#include <wx/utils.h> -//#include <zen/tick_count.h> -#include <zen/string_tools.h> -#include <zen/scope_guard.h> -#include <zen/utf.h> -#include <zen/format_unit.h> -#include "dc.h" - -#ifdef ZEN_LINUX - #include <gtk/gtk.h> -#endif - -using namespace zen; - - -wxColor Grid::getColorSelectionGradientFrom() { return { 137, 172, 255 }; } //blue: HSL: 158, 255, 196 HSV: 222, 0.46, 1 -wxColor Grid::getColorSelectionGradientTo () { return { 225, 234, 255 }; } // HSL: 158, 255, 240 HSV: 222, 0.12, 1 - -const int GridData::COLUMN_GAP_LEFT = 4; - - -void zen::clearArea(wxDC& dc, const wxRect& rect, const wxColor& col) -{ - wxDCPenChanger dummy (dc, col); - wxDCBrushChanger dummy2(dc, col); - dc.DrawRectangle(rect); -} - - -namespace -{ -//let's NOT create wxWidgets objects statically: -//------------------------------ Grid Parameters -------------------------------- -inline wxColor getColorLabelText() { return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); } -inline wxColor getColorGridLine() { return { 192, 192, 192 }; } //light grey - -inline wxColor getColorLabelGradientFrom() { return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); } -inline wxColor getColorLabelGradientTo () { return { 200, 200, 200 }; } //light grey - -inline wxColor getColorLabelGradientFocusFrom() { return getColorLabelGradientFrom(); } -inline wxColor getColorLabelGradientFocusTo () { return Grid::getColorSelectionGradientFrom(); } - -const double MOUSE_DRAG_ACCELERATION = 1.5; //unit: [rows / (pixel * sec)] -> same value like Explorer! -const int DEFAULT_COL_LABEL_BORDER = 6; //top + bottom border in addition to label height -const int COLUMN_MOVE_DELAY = 5; //unit: [pixel] (from Explorer) -const int COLUMN_MIN_WIDTH = 40; //only honored when resizing manually! -const int ROW_LABEL_BORDER = 3; -const int COLUMN_RESIZE_TOLERANCE = 6; //unit [pixel] -const int COLUMN_FILL_GAP_TOLERANCE = 10; //enlarge column to fill full width when resizing - -const bool fillGapAfterColumns = true; //draw rows/column label to fill full window width; may become an instance variable some time? -} - -//---------------------------------------------------------------------------------------------------------------- -const wxEventType zen::EVENT_GRID_MOUSE_LEFT_DOUBLE = wxNewEventType(); -const wxEventType zen::EVENT_GRID_MOUSE_LEFT_DOWN = wxNewEventType(); -const wxEventType zen::EVENT_GRID_MOUSE_LEFT_UP = wxNewEventType(); -const wxEventType zen::EVENT_GRID_MOUSE_RIGHT_DOWN = wxNewEventType(); -const wxEventType zen::EVENT_GRID_MOUSE_RIGHT_UP = wxNewEventType(); -const wxEventType zen::EVENT_GRID_SELECT_RANGE = wxNewEventType(); -const wxEventType zen::EVENT_GRID_COL_LABEL_MOUSE_LEFT = wxNewEventType(); -const wxEventType zen::EVENT_GRID_COL_LABEL_MOUSE_RIGHT = wxNewEventType(); -const wxEventType zen::EVENT_GRID_COL_RESIZE = wxNewEventType(); -//---------------------------------------------------------------------------------------------------------------- - -void GridData::renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected) -{ - drawCellBackground(dc, rect, enabled, selected, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); -} - - -void GridData::renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover) -{ - wxRect rectTmp = drawCellBorder(dc, rect); - - rectTmp.x += COLUMN_GAP_LEFT; - rectTmp.width -= COLUMN_GAP_LEFT; - drawCellText(dc, rectTmp, getValue(row, colType)); -} - - -int GridData::getBestSize(wxDC& dc, size_t row, ColumnType colType) -{ - return dc.GetTextExtent(getValue(row, colType)).GetWidth() + 2 * COLUMN_GAP_LEFT + 1; //gap on left and right side + border -} - - -wxRect GridData::drawCellBorder(wxDC& dc, const wxRect& rect) //returns remaining rectangle -{ - wxDCPenChanger dummy2(dc, getColorGridLine()); - dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight()); - dc.DrawLine(rect.GetBottomRight(), rect.GetTopRight() + wxPoint(0, -1)); - - return wxRect(rect.GetTopLeft(), wxSize(rect.width - 1, rect.height - 1)); -} - - -void GridData::drawCellBackground(wxDC& dc, const wxRect& rect, bool enabled, bool selected, const wxColor& backgroundColor) -{ - if (enabled) - { - if (selected) - dc.GradientFillLinear(rect, Grid::getColorSelectionGradientFrom(), Grid::getColorSelectionGradientTo(), wxEAST); - else - clearArea(dc, rect, backgroundColor); - } - else - clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE)); -} - - -wxSize GridData::drawCellText(wxDC& dc, const wxRect& rect, const std::wstring& text, int alignment) -{ - /* - performance notes (Windows): - - wxDC::GetTextExtent() is by far the most expensive call (20x more expensive than wxDC::DrawText()) - - wxDC::DrawLabel() is inefficiently implemented; internally calls: wxDC::GetMultiLineTextExtent(), wxDC::GetTextExtent(), wxDC::DrawText() - - wxDC::GetMultiLineTextExtent() calls wxDC::GetTextExtent() - - wxDC::DrawText also calls wxDC::GetTextExtent()!! - => wxDC::DrawLabel() boils down to 3(!) calls to wxDC::GetTextExtent()!!! - - wxDC::DrawLabel results in GetTextExtent() call even for empty strings!!! - => skip the wxDC::DrawLabel() cruft and directly call wxDC::DrawText! - */ - - //truncate large texts and add ellipsis - assert(!contains(text, L"\n")); - const wchar_t ELLIPSIS = L'\u2026'; //"..." - - std::wstring textTrunc = text; - wxSize extentTrunc = dc.GetTextExtent(textTrunc); - if (extentTrunc.GetWidth() > rect.width) - { - //unlike Windows 7 Explorer, we truncate UTF-16 correctly: e.g. CJK-Ideogramm encodes to TWO wchar_t: utfCvrtTo<std::wstring>("\xf0\xa4\xbd\x9c"); - size_t low = 0; //number of unicode chars! - size_t high = unicodeLength(text); // - if (high > 1) - for (;;) - { - const size_t middle = (low + high) / 2; //=> never 0 when "high - low > 1" - if (high - low <= 1) - { - if (low == 0) - { - textTrunc = ELLIPSIS; - extentTrunc = dc.GetTextExtent(ELLIPSIS); - } - break; - } - - const std::wstring& candidate = std::wstring(strBegin(text), findUnicodePos(text, middle)) + ELLIPSIS; - const wxSize extentCand = dc.GetTextExtent(candidate); //perf: most expensive call of this routine! - - if (extentCand.GetWidth() <= rect.width) - { - low = middle; - textTrunc = candidate; - extentTrunc = extentCand; - } - else - high = middle; - } - } - - wxPoint pt = rect.GetTopLeft(); - if (alignment & wxALIGN_RIGHT) //note: wxALIGN_LEFT == 0! - pt.x += rect.width - extentTrunc.GetWidth(); - else if (alignment & wxALIGN_CENTER_HORIZONTAL) - pt.x += (rect.width - extentTrunc.GetWidth()) / 2; - - if (alignment & wxALIGN_BOTTOM) //note: wxALIGN_TOP == 0! - pt.y += rect.height - extentTrunc.GetHeight(); - else if (alignment & wxALIGN_CENTER_VERTICAL) - pt.y += (rect.height - extentTrunc.GetHeight()) / 2; - - RecursiveDcClipper clip(dc, rect); - dc.DrawText(textTrunc, pt); - return extentTrunc; -} - - -void GridData::renderColumnLabel(Grid& grid, wxDC& dc, const wxRect& rect, ColumnType colType, bool highlighted) -{ - wxRect rectTmp = drawColumnLabelBorder(dc, rect); - drawColumnLabelBackground(dc, rectTmp, highlighted); - - rectTmp.x += COLUMN_GAP_LEFT; - rectTmp.width -= COLUMN_GAP_LEFT; - drawColumnLabelText(dc, rectTmp, getColumnLabel(colType)); -} - - -wxRect GridData::drawColumnLabelBorder(wxDC& dc, const wxRect& rect) //returns remaining rectangle -{ - //draw white line - { - wxDCPenChanger dummy(dc, *wxWHITE_PEN); - dc.DrawLine(rect.GetTopLeft(), rect.GetBottomLeft()); - } - - //draw border (with gradient) - { - wxDCPenChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_3DSHADOW)); - dc.GradientFillLinear(wxRect(rect.GetTopRight(), rect.GetBottomRight()), getColorLabelGradientFrom(), dc.GetPen().GetColour(), wxSOUTH); - dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight() + wxPoint(1, 0)); - } - - return wxRect(rect.x + 1, rect.y, rect.width - 2, rect.height - 1); //we really don't like wxRect::Deflate, do we? -} - - -void GridData::drawColumnLabelBackground(wxDC& dc, const wxRect& rect, bool highlighted) -{ - if (highlighted) - dc.GradientFillLinear(rect, getColorLabelGradientFocusFrom(), getColorLabelGradientFocusTo(), wxSOUTH); - else //regular background gradient - dc.GradientFillLinear(rect, getColorLabelGradientFrom(), getColorLabelGradientTo(), wxSOUTH); //clear overlapping cells -} - - -void GridData::drawColumnLabelText(wxDC& dc, const wxRect& rect, const std::wstring& text) -{ - wxDCTextColourChanger dummy(dc, getColorLabelText()); //accessibility: always set both foreground AND background colors! - drawCellText(dc, rect, text, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); -} - -//---------------------------------------------------------------------------------------------------------------- -/* - SubWindow - /|\ - | - ----------------------------------- - | | | | -CornerWin RowLabelWin ColLabelWin MainWin - -*/ -class Grid::SubWindow : public wxWindow -{ -public: - SubWindow(Grid& parent) : - wxWindow(&parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxWANTS_CHARS | wxBORDER_NONE, wxPanelNameStr), - parent_(parent) - { - Connect(wxEVT_PAINT, wxPaintEventHandler(SubWindow::onPaintEvent), nullptr, this); - Connect(wxEVT_SIZE, wxSizeEventHandler (SubWindow::onSizeEvent), nullptr, this); - //http://wiki.wxwidgets.org/Flicker-Free_Drawing - Connect(wxEVT_ERASE_BACKGROUND, wxEraseEventHandler(SubWindow::onEraseBackGround), nullptr, this); - - //SetDoubleBuffered(true); slow as hell! - - SetBackgroundStyle(wxBG_STYLE_PAINT); - - Connect(wxEVT_SET_FOCUS, wxFocusEventHandler(SubWindow::onFocus), nullptr, this); - Connect(wxEVT_KILL_FOCUS, wxFocusEventHandler(SubWindow::onFocus), nullptr, this); - Connect(wxEVT_CHILD_FOCUS, wxEventHandler(SubWindow::onChildFocus), nullptr, this); - - Connect(wxEVT_LEFT_DOWN, wxMouseEventHandler(SubWindow::onMouseLeftDown ), nullptr, this); - Connect(wxEVT_LEFT_UP, wxMouseEventHandler(SubWindow::onMouseLeftUp ), nullptr, this); - Connect(wxEVT_LEFT_DCLICK, wxMouseEventHandler(SubWindow::onMouseLeftDouble), nullptr, this); - Connect(wxEVT_RIGHT_DOWN, wxMouseEventHandler(SubWindow::onMouseRightDown ), nullptr, this); - Connect(wxEVT_RIGHT_UP, wxMouseEventHandler(SubWindow::onMouseRightUp ), nullptr, this); - Connect(wxEVT_MOTION, wxMouseEventHandler(SubWindow::onMouseMovement ), nullptr, this); - Connect(wxEVT_LEAVE_WINDOW, wxMouseEventHandler(SubWindow::onLeaveWindow ), nullptr, this); - Connect(wxEVT_MOUSEWHEEL, wxMouseEventHandler(SubWindow::onMouseWheel ), nullptr, this); - Connect(wxEVT_MOUSE_CAPTURE_LOST, wxMouseCaptureLostEventHandler(SubWindow::onMouseCaptureLost), nullptr, this); - - Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(SubWindow::onKeyDown), nullptr, this); - - assert(GetClientAreaOrigin() == wxPoint()); //generally assumed when dealing with coordinates below - } - Grid& refParent() { return parent_; } - const Grid& refParent() const { return parent_; } - - template <class T> - bool sendEventNow(T&& event) //take both "rvalue + lvalues", return "true" if a suitable event handler function was found and executed, and the function did not call wxEvent::Skip. - { - if (wxEvtHandler* evtHandler = parent_.GetEventHandler()) - return evtHandler->ProcessEvent(event); - return false; - } - -protected: - void setToolTip(const std::wstring& text) //proper fix for wxWindow - { - wxToolTip* tt = GetToolTip(); - - const wxString oldText = tt ? tt->GetTip() : wxString(); - if (text != oldText) - { - if (text.empty()) - SetToolTip(nullptr); //wxGTK doesn't allow wxToolTip with empty text! - else - { - //wxWidgets bug: tooltip multiline property is defined by first tooltip text containing newlines or not (same is true for maximum width) - if (!tt) - SetToolTip(new wxToolTip(L"a b\n\ - a b")); //ugly, but working (on Windows) - tt = GetToolTip(); //should be bound by now - assert(tt); - if (tt) - tt->SetTip(text); - } - } - } - -private: - virtual void render(wxDC& dc, const wxRect& rect) = 0; - - virtual void onFocus(wxFocusEvent& event) { event.Skip(); } - virtual void onChildFocus(wxEvent& event) {} //wxGTK::wxScrolledWindow automatically scrolls to child window when child gets focus -> prevent! - - virtual void onMouseLeftDown (wxMouseEvent& event) { event.Skip(); } - virtual void onMouseLeftUp (wxMouseEvent& event) { event.Skip(); } - virtual void onMouseLeftDouble(wxMouseEvent& event) { event.Skip(); } - virtual void onMouseRightDown (wxMouseEvent& event) { event.Skip(); } - virtual void onMouseRightUp (wxMouseEvent& event) { event.Skip(); } - virtual void onMouseMovement (wxMouseEvent& event) { event.Skip(); } - virtual void onLeaveWindow (wxMouseEvent& event) { event.Skip(); } - virtual void onMouseCaptureLost(wxMouseCaptureLostEvent& event) { event.Skip(); } - - void onKeyDown(wxKeyEvent& event) - { - if (!sendEventNow(event)) //let parent collect all key events - event.Skip(); - } - - void onMouseWheel(wxMouseEvent& event) - { - /* - MSDN, WM_MOUSEWHEEL: "Sent to the focus window when the mouse wheel is rotated. - The DefWindowProc function propagates the message to the window's parent. - There should be no internal forwarding of the message, since DefWindowProc propagates - it up the parent chain until it finds a window that processes it." - - On OS X there is no such propagation! => we need a redirection (the same wxGrid implements) - */ - - //new wxWidgets 3.0 screw-up for GTK2: wxScrollHelperEvtHandler::ProcessEvent() ignores wxEVT_MOUSEWHEEL events - //thereby breaking the scenario of redirection to parent we need here (but also breaking their very own wxGrid sample) - //=> call wxScrolledWindow mouse wheel handler directly - parent_.HandleOnMouseWheel(event); - - //if (!sendEventNow(event)) - // event.Skip(); - } - - void onPaintEvent(wxPaintEvent& event) - { -#ifndef NDEBUG -#ifdef ZEN_WIN - if (runningPaintEvent_ == true) //looks like showing the assert window here would quit the debug session - __debugbreak(); -#else - assert(runningPaintEvent_ == false); //catch unexpected recursion, e.g.: getIconByIndex() seems to run a message loop in rare cases! -#endif - runningPaintEvent_ = true; - ZEN_ON_SCOPE_EXIT(runningPaintEvent_ = false); -#endif - //wxAutoBufferedPaintDC dc(this); -> this one happily fucks up for RTL layout by not drawing the first column (x = 0)! - BufferedPaintDC dc(*this, doubleBuffer_); - - assert(GetSize() == GetClientSize()); - - const wxRegion& updateReg = GetUpdateRegion(); - for (wxRegionIterator it = updateReg; it; ++it) - render(dc, it.GetRect()); - } - - void onSizeEvent(wxSizeEvent& event) - { - Refresh(); - event.Skip(); - } - - void onEraseBackGround(wxEraseEvent& event) {} - - Grid& parent_; - Opt<wxBitmap> doubleBuffer_; -#ifndef NDEBUG - bool runningPaintEvent_ = false; -#endif -}; - -//---------------------------------------------------------------------------------------------------------------- -//---------------------------------------------------------------------------------------------------------------- - -class Grid::CornerWin : public SubWindow -{ -public: - CornerWin(Grid& parent) : SubWindow(parent) {} - -private: - bool AcceptsFocus() const override { return false; } - - void render(wxDC& dc, const wxRect& rect) override - { - const wxRect& clientRect = GetClientRect(); - - dc.GradientFillLinear(clientRect, getColorLabelGradientFrom(), getColorLabelGradientTo(), wxSOUTH); - - dc.SetPen(wxSystemSettings::GetColour(wxSYS_COLOUR_3DSHADOW)); - - { - wxDCPenChanger dummy(dc, getColorLabelGradientFrom()); - dc.DrawLine(clientRect.GetTopLeft(), clientRect.GetTopRight()); - } - - dc.GradientFillLinear(wxRect(clientRect.GetBottomLeft (), clientRect.GetTopLeft ()), getColorLabelGradientFrom(), dc.GetPen().GetColour(), wxSOUTH); - dc.GradientFillLinear(wxRect(clientRect.GetBottomRight(), clientRect.GetTopRight()), getColorLabelGradientFrom(), dc.GetPen().GetColour(), wxSOUTH); - - dc.DrawLine(clientRect.GetBottomLeft(), clientRect.GetBottomRight()); - - wxRect rectShrinked = clientRect; - rectShrinked.Deflate(1); - dc.SetPen(*wxWHITE_PEN); - - //dc.DrawLine(clientRect.GetTopLeft(), clientRect.GetTopRight() + wxPoint(1, 0)); - dc.DrawLine(rectShrinked.GetTopLeft(), rectShrinked.GetBottomLeft() + wxPoint(0, 1)); - } -}; - -//---------------------------------------------------------------------------------------------------------------- -//---------------------------------------------------------------------------------------------------------------- - -class Grid::RowLabelWin : public SubWindow -{ -public: - RowLabelWin(Grid& parent) : - SubWindow(parent), - rowHeight_(parent.GetCharHeight() + 2 + 1) {} //default height; don't call any functions on "parent" other than those from wxWindow during construction! - //2 for some more space, 1 for bottom border (gives 15 + 2 + 1 on Windows, 17 + 2 + 1 on Ubuntu) - - int getBestWidth(ptrdiff_t rowFrom, ptrdiff_t rowTo) - { - wxClientDC dc(this); - - wxFont labelFont = GetFont(); - //labelFont.SetWeight(wxFONTWEIGHT_BOLD); - dc.SetFont(labelFont); //harmonize with RowLabelWin::render()! - - int bestWidth = 0; - for (ptrdiff_t i = rowFrom; i <= rowTo; ++i) - bestWidth = std::max(bestWidth, dc.GetTextExtent(formatRow(i)).GetWidth() + 2 * ROW_LABEL_BORDER); - return bestWidth; - } - - size_t getLogicalHeight() const { return refParent().getRowCount() * rowHeight_; } - - ptrdiff_t getRowAtPos(ptrdiff_t posY) const //returns < 0 on invalid input, else row number within: [0, rowCount]; rowCount if out of range - { - if (posY >= 0 && rowHeight_ > 0) - { - const size_t row = posY / rowHeight_; - return std::min(row, refParent().getRowCount()); - } - return -1; - } - - int getRowHeight() const { return rowHeight_; } //guarantees to return size >= 1 ! - void setRowHeight(int height) { assert(height > 0); rowHeight_ = std::max(1, height); } - - wxRect getRowLabelArea(size_t row) const //returns empty rect if row not found - { - assert(GetClientAreaOrigin() == wxPoint()); - if (row < refParent().getRowCount()) - return wxRect(wxPoint(0, rowHeight_ * row), - wxSize(GetClientSize().GetWidth(), rowHeight_)); - return wxRect(); - } - - std::pair<ptrdiff_t, ptrdiff_t> getRowsOnClient(const wxRect& clientRect) const //returns range [begin, end) - { - const int yFrom = refParent().CalcUnscrolledPosition(clientRect.GetTopLeft ()).y; - const int yTo = refParent().CalcUnscrolledPosition(clientRect.GetBottomRight()).y; - - return std::make_pair(std::max(yFrom / rowHeight_, 0), - std::min<ptrdiff_t>((yTo / rowHeight_) + 1, refParent().getRowCount())); - } - -private: - static std::wstring formatRow(size_t row) { return toGuiString(row + 1); } //convert number to std::wstring including thousands separator - - bool AcceptsFocus() const override { return false; } - - void render(wxDC& dc, const wxRect& rect) override - { - /* - IsEnabled() vs IsThisEnabled() since wxWidgets 2.9.5: - - void wxWindowBase::NotifyWindowOnEnableChange(), called from bool wxWindowBase::Enable(), fails to refresh - child elements when disabling a IsTopLevel() dialog, e.g. when showing a modal dialog. - The unfortunate effect on XP for using IsEnabled() when rendering the grid is that the user can move the modal dialog - and *draw* with it on the background while the grid refreshes as disabled incrementally! - - => Don't use IsEnabled() since it considers the top level window. The brittle wxWidgets implementation is right in their intention, - but wrong when not refreshing child-windows: the control designer decides how his control should be rendered! - - => IsThisEnabled() OTOH is too shallow and does not consider parent windows which are not top level. - - The perfect solution would be a bool ShouldBeDrawnActive() { return "IsEnabled() but ignore effects of showing a modal dialog"; } - - However "IsThisEnabled()" is good enough (same like the old IsEnabled() on wxWidgets 2.8.12) and it avoids this pathetic behavior on XP. - (Similar problem on Win 7: e.g. directly click sync button without comparing first) - */ - if (IsThisEnabled()) - clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); - else - clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE)); - - wxFont labelFont = GetFont(); - //labelFont.SetWeight(wxFONTWEIGHT_BOLD); - dc.SetFont(labelFont); //harmonize with RowLabelWin::getBestWidth()! - - auto rowRange = getRowsOnClient(rect); //returns range [begin, end) - for (auto row = rowRange.first; row < rowRange.second; ++row) - { - wxRect singleLabelArea = getRowLabelArea(row); //returns empty rect if row not found - if (singleLabelArea.height > 0) - { - singleLabelArea.y = refParent().CalcScrolledPosition(singleLabelArea.GetTopLeft()).y; - drawRowLabel(dc, singleLabelArea, row); - } - } - } - - void drawRowLabel(wxDC& dc, const wxRect& rect, size_t row) - { - //clearArea(dc, rect, getColorRowLabel()); - dc.GradientFillLinear(rect, getColorLabelGradientFrom(), getColorLabelGradientTo(), wxEAST); //clear overlapping cells - wxDCTextColourChanger dummy3(dc, getColorLabelText()); //accessibility: always set both foreground AND background colors! - - //label text - wxRect textRect = rect; - textRect.Deflate(1); - - GridData::drawCellText(dc, textRect, formatRow(row), wxALIGN_CENTRE); - - //border lines - { - wxDCPenChanger dummy(dc, *wxWHITE_PEN); - dc.DrawLine(rect.GetTopLeft(), rect.GetTopRight()); - } - { - wxDCPenChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_3DSHADOW)); - dc.DrawLine(rect.GetTopLeft(), rect.GetBottomLeft()); - dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight()); - dc.DrawLine(rect.GetBottomRight(), rect.GetTopRight() + wxPoint(0, -1)); - } - } - - void onMouseLeftDown(wxMouseEvent& event) override { refParent().redirectRowLabelEvent(event); } - void onMouseMovement(wxMouseEvent& event) override { refParent().redirectRowLabelEvent(event); } - void onMouseLeftUp (wxMouseEvent& event) override { refParent().redirectRowLabelEvent(event); } - - int rowHeight_; -}; - - -namespace -{ -class ColumnResizing -{ -public: - ColumnResizing(wxWindow& wnd, size_t col, int startWidth, int clientPosX) : - wnd_(wnd), col_(col), startWidth_(startWidth), clientPosX_(clientPosX) - { - wnd_.CaptureMouse(); - } - ~ColumnResizing() - { - if (wnd_.HasCapture()) - wnd_.ReleaseMouse(); - } - - size_t getColumn () const { return col_; } - int getStartWidth () const { return startWidth_; } - int getStartPosX () const { return clientPosX_; } - -private: - wxWindow& wnd_; - const size_t col_; - const int startWidth_; - const int clientPosX_; -}; - - -class ColumnMove -{ -public: - ColumnMove(wxWindow& wnd, size_t colFrom, int clientPosX) : - wnd_(wnd), - colFrom_(colFrom), - colTo_(colFrom), - clientPosX_(clientPosX) { wnd_.CaptureMouse(); } - ~ColumnMove() { if (wnd_.HasCapture()) wnd_.ReleaseMouse(); } - - size_t getColumnFrom() const { return colFrom_; } - size_t& refColumnTo() { return colTo_; } - int getStartPosX () const { return clientPosX_; } - - bool isRealMove() const { return !singleClick_; } - void setRealMove() { singleClick_ = false; } - -private: - wxWindow& wnd_; - const size_t colFrom_; - size_t colTo_; - const int clientPosX_; - bool singleClick_ = true; -}; -} - -//---------------------------------------------------------------------------------------------------------------- - -class Grid::ColLabelWin : public SubWindow -{ -public: - ColLabelWin(Grid& parent) : SubWindow(parent) {} - -private: - bool AcceptsFocus() const override { return false; } - - void render(wxDC& dc, const wxRect& rect) override - { - if (IsThisEnabled()) - clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); - else - clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE)); - - //coordinate with "colLabelHeight" in Grid constructor: - wxFont labelFont = GetFont(); - labelFont.SetWeight(wxFONTWEIGHT_BOLD); - dc.SetFont(labelFont); - - wxDCTextColourChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels - - const int colLabelHeight = refParent().colLabelHeight_; - - wxPoint labelAreaTL(refParent().CalcScrolledPosition(wxPoint(0, 0)).x, 0); //client coordinates - - std::vector<ColumnWidth> absWidths = refParent().getColWidths(); //resolve stretched widths - for (auto it = absWidths.begin(); it != absWidths.end(); ++it) - { - const size_t col = it - absWidths.begin(); - const int width = it->width_; //don't use unsigned for calculations! - - if (labelAreaTL.x > rect.GetRight()) - return; //done, rect is fully covered - if (labelAreaTL.x + width > rect.x) - drawColumnLabel(dc, wxRect(labelAreaTL, wxSize(width, colLabelHeight)), col, it->type_); - labelAreaTL.x += width; - } - if (labelAreaTL.x > rect.GetRight()) - return; //done, rect is fully covered - - //fill gap after columns and cover full width - if (fillGapAfterColumns) - { - int totalWidth = 0; - for (const ColumnWidth& cw : absWidths) - totalWidth += cw.width_; - const int clientWidth = GetClientSize().GetWidth(); //need reliable, stable width in contrast to rect.width - - if (totalWidth < clientWidth) - drawColumnLabel(dc, wxRect(labelAreaTL, wxSize(clientWidth - totalWidth, colLabelHeight)), absWidths.size(), ColumnType::NONE); - } - } - - void drawColumnLabel(wxDC& dc, const wxRect& rect, size_t col, ColumnType colType) - { - if (auto dataView = refParent().getDataProvider()) - { - const bool isHighlighted = activeResizing_ ? col == activeResizing_ ->getColumn () : //highlight_ column on mouse-over - activeClickOrMove_ ? col == activeClickOrMove_->getColumnFrom() : - highlightCol_ ? col == *highlightCol_ : - false; - - RecursiveDcClipper clip(dc, rect); - dataView->renderColumnLabel(refParent(), dc, rect, colType, isHighlighted); - - //draw move target location - if (refParent().allowColumnMove_) - if (activeClickOrMove_ && activeClickOrMove_->isRealMove()) - { - if (col + 1 == activeClickOrMove_->refColumnTo()) //handle pos 1, 2, .. up to "at end" position - dc.GradientFillLinear(wxRect(rect.GetTopRight(), rect.GetBottomRight() + wxPoint(-2, 0)), getColorLabelGradientFrom(), *wxBLUE, wxSOUTH); - else if (col == activeClickOrMove_->refColumnTo() && col == 0) //pos 0 - dc.GradientFillLinear(wxRect(rect.GetTopLeft(), rect.GetBottomLeft() + wxPoint(2, 0)), getColorLabelGradientFrom(), *wxBLUE, wxSOUTH); - } - } - } - - void onMouseLeftDown(wxMouseEvent& event) override - { - if (FindFocus() != &refParent().getMainWin()) - refParent().getMainWin().SetFocus(); - - activeResizing_.reset(); - activeClickOrMove_.reset(); - - if (Opt<ColAction> action = refParent().clientPosToColumnAction(event.GetPosition())) - { - if (action->wantResize) - { - if (!event.LeftDClick()) //double-clicks never seem to arrive here; why is this checked at all??? - if (Opt<int> colWidth = refParent().getColWidth(action->col)) - activeResizing_ = std::make_unique<ColumnResizing>(*this, action->col, *colWidth, event.GetPosition().x); - } - else //a move or single click - activeClickOrMove_ = std::make_unique<ColumnMove>(*this, action->col, event.GetPosition().x); - } - event.Skip(); - } - - void onMouseLeftUp(wxMouseEvent& event) override - { - activeResizing_.reset(); //nothing else to do, actual work done by onMouseMovement() - - if (activeClickOrMove_) - { - if (activeClickOrMove_->isRealMove()) - { - if (refParent().allowColumnMove_) - { - const size_t colFrom = activeClickOrMove_->getColumnFrom(); - size_t colTo = activeClickOrMove_->refColumnTo(); - - if (colTo > colFrom) //simulate "colFrom" deletion - --colTo; - - refParent().moveColumn(colFrom, colTo); - } - } - else //notify single label click - { - if (const Opt<ColumnType> colType = refParent().colToType(activeClickOrMove_->getColumnFrom())) - sendEventNow(GridLabelClickEvent(EVENT_GRID_COL_LABEL_MOUSE_LEFT, event, *colType)); - } - activeClickOrMove_.reset(); - } - - refParent().updateWindowSizes(); //looks strange if done during onMouseMovement() - refParent().Refresh(); - event.Skip(); - } - - void onMouseCaptureLost(wxMouseCaptureLostEvent& event) override - { - activeResizing_.reset(); - activeClickOrMove_.reset(); - Refresh(); - //event.Skip(); -> we DID handle it! - } - - void onMouseLeftDouble(wxMouseEvent& event) override - { - if (Opt<ColAction> action = refParent().clientPosToColumnAction(event.GetPosition())) - if (action->wantResize) - { - //auto-size visible range on double-click - const int bestWidth = refParent().getBestColumnSize(action->col); //return -1 on error - if (bestWidth >= 0) - { - refParent().setColumnWidth(bestWidth, action->col, ALLOW_GRID_EVENT); - refParent().Refresh(); //refresh main grid as well! - } - } - event.Skip(); - } - - void onMouseMovement(wxMouseEvent& event) override - { - if (activeResizing_) - { - const auto col = activeResizing_->getColumn(); - const int newWidth = activeResizing_->getStartWidth() + event.GetPosition().x - activeResizing_->getStartPosX(); - - //set width tentatively - refParent().setColumnWidth(newWidth, col, ALLOW_GRID_EVENT); - - //check if there's a small gap after last column, if yes, fill it - const int gapWidth = GetClientSize().GetWidth() - refParent().getColWidthsSum(GetClientSize().GetWidth()); - if (std::abs(gapWidth) < COLUMN_FILL_GAP_TOLERANCE) - refParent().setColumnWidth(newWidth + gapWidth, col, ALLOW_GRID_EVENT); - - refParent().Refresh(); //refresh columns on main grid as well! - } - else if (activeClickOrMove_) - { - const int clientPosX = event.GetPosition().x; - if (std::abs(clientPosX - activeClickOrMove_->getStartPosX()) > COLUMN_MOVE_DELAY) //real move (not a single click) - { - activeClickOrMove_->setRealMove(); - - const ptrdiff_t col = refParent().clientPosToMoveTargetColumn(event.GetPosition()); - if (col >= 0) - activeClickOrMove_->refColumnTo() = col; - } - } - else - { - if (const Opt<ColAction> action = refParent().clientPosToColumnAction(event.GetPosition())) - { - highlightCol_ = action->col; - - if (action->wantResize) - SetCursor(wxCURSOR_SIZEWE); //set window-local only! :) - else - SetCursor(*wxSTANDARD_CURSOR); - } - else - { - highlightCol_ = NoValue(); - SetCursor(*wxSTANDARD_CURSOR); - } - } - - const std::wstring toolTip = [&] - { - const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition()); - const ColumnType colType = refParent().getColumnAtPos(absPos.x).colType; //returns ColumnType::NONE if no column at x position! - if (colType != ColumnType::NONE) - if (auto prov = refParent().getDataProvider()) - return prov->getToolTip(colType); - return std::wstring(); - }(); - setToolTip(toolTip); - - Refresh(); - event.Skip(); - } - - void onLeaveWindow(wxMouseEvent& event) override - { - highlightCol_ = NoValue(); //wxEVT_LEAVE_WINDOW does not respect mouse capture! -> however highlight_ is drawn unconditionally during move/resize! - Refresh(); - event.Skip(); - } - - void onMouseRightDown(wxMouseEvent& event) override - { - if (const Opt<ColAction> action = refParent().clientPosToColumnAction(event.GetPosition())) - { - if (const Opt<ColumnType> colType = refParent().colToType(action->col)) - sendEventNow(GridLabelClickEvent(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, event, *colType)); //notify right click - else assert(false); - } - else - //notify right click (on free space after last column) - if (fillGapAfterColumns) - sendEventNow(GridLabelClickEvent(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, event, ColumnType::NONE)); - - event.Skip(); - } - - std::unique_ptr<ColumnResizing> activeResizing_; - std::unique_ptr<ColumnMove> activeClickOrMove_; - Opt<size_t> highlightCol_; //column during mouse-over -}; - -//---------------------------------------------------------------------------------------------------------------- -namespace -{ -const wxEventType EVENT_GRID_HAS_SCROLLED = wxNewEventType(); //internal to Grid::MainWin::ScrollWindow() -} -//---------------------------------------------------------------------------------------------------------------- - -class Grid::MainWin : public SubWindow -{ -public: - MainWin(Grid& parent, - RowLabelWin& rowLabelWin, - ColLabelWin& colLabelWin) : SubWindow(parent), - rowLabelWin_(rowLabelWin), - colLabelWin_(colLabelWin) - { - Connect(EVENT_GRID_HAS_SCROLLED, wxEventHandler(MainWin::onRequestWindowUpdate), nullptr, this); - } - - ~MainWin() { assert(!gridUpdatePending_); } - - size_t getCursor() const { return cursorRow_; } - size_t getAnchor() const { return selectionAnchor_; } - - void setCursor(size_t newCursorRow, size_t newAnchorRow) - { - cursorRow_ = newCursorRow; - selectionAnchor_ = newAnchorRow; - activeSelection_.reset(); //e.g. user might search with F3 while holding down left mouse button - } - -private: - void render(wxDC& dc, const wxRect& rect) override - { - if (IsThisEnabled()) - clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); - else - clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE)); - - dc.SetFont(GetFont()); //harmonize with Grid::getBestColumnSize() - - wxDCTextColourChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels - - std::vector<ColumnWidth> absWidths = refParent().getColWidths(); //resolve stretched widths - { - int totalRowWidth = 0; - for (const ColumnWidth& cw : absWidths) - totalRowWidth += cw.width_; - - //fill gap after columns and cover full width - if (fillGapAfterColumns) - totalRowWidth = std::max(totalRowWidth, GetClientSize().GetWidth()); - - if (auto prov = refParent().getDataProvider()) - { - RecursiveDcClipper dummy2(dc, rect); //do NOT draw background on cells outside of invalidated rect invalidating foreground text! - - wxPoint cellAreaTL(refParent().CalcScrolledPosition(wxPoint(0, 0))); //client coordinates - const int rowHeight = rowLabelWin_.getRowHeight(); - const auto rowRange = rowLabelWin_.getRowsOnClient(rect); //returns range [begin, end) - - //draw background lines - for (auto row = rowRange.first; row < rowRange.second; ++row) - { - const wxRect rowRect(cellAreaTL + wxPoint(0, row * rowHeight), wxSize(totalRowWidth, rowHeight)); - RecursiveDcClipper dummy3(dc, rowRect); - prov->renderRowBackgound(dc, rowRect, row, refParent().IsThisEnabled(), drawAsSelected(row)); - } - - //draw single cells, column by column - for (const ColumnWidth& cw : absWidths) - { - if (cellAreaTL.x > rect.GetRight()) - return; //done - - if (cellAreaTL.x + cw.width_ > rect.x) - for (auto row = rowRange.first; row < rowRange.second; ++row) - { - const wxRect cellRect(cellAreaTL.x, cellAreaTL.y + row * rowHeight, cw.width_, rowHeight); - RecursiveDcClipper dummy3(dc, cellRect); - prov->renderCell(dc, cellRect, row, cw.type_, refParent().IsThisEnabled(), drawAsSelected(row), getRowHoverToDraw(row)); - } - cellAreaTL.x += cw.width_; - } - } - } - } - - HoverArea getRowHoverToDraw(ptrdiff_t row) const - { - if (activeSelection_) - { - if (activeSelection_->getFirstClick().row_ == row) - return activeSelection_->getFirstClick().hoverArea_; - } - else if (highlight_.row == row) - return highlight_.rowHover; - return HoverArea::NONE; - } - - bool drawAsSelected(size_t row) const - { - if (activeSelection_) //check if user is currently selecting with mouse - { - const size_t rowFrom = std::min(activeSelection_->getStartRow(), activeSelection_->getCurrentRow()); - const size_t rowTo = std::max(activeSelection_->getStartRow(), activeSelection_->getCurrentRow()); - - if (rowFrom <= row && row <= rowTo) - return activeSelection_->isPositiveSelect(); //overwrite default - } - return refParent().isSelected(row); - } - - void onMouseLeftDown (wxMouseEvent& event) override { onMouseDown(event); } - void onMouseLeftUp (wxMouseEvent& event) override { onMouseUp (event); } - void onMouseRightDown(wxMouseEvent& event) override { onMouseDown(event); } - void onMouseRightUp (wxMouseEvent& event) override { onMouseUp (event); } - - void onMouseLeftDouble(wxMouseEvent& event) override - { - if (auto prov = refParent().getDataProvider()) - { - const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition()); - const ptrdiff_t row = rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range - const ColumnPosInfo cpi = refParent().getColumnAtPos(absPos.x); //returns ColumnType::NONE if no column at x position! - const HoverArea rowHover = prov->getRowMouseHover(row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth); - //client is interested in all double-clicks, even those outside of the grid! - sendEventNow(GridClickEvent(EVENT_GRID_MOUSE_LEFT_DOUBLE, event, row, rowHover)); - } - event.Skip(); - } - - void onMouseDown(wxMouseEvent& event) //handle left and right mouse button clicks (almost) the same - { - if (wxWindow::FindFocus() != this) //doesn't seem to happen automatically for right mouse button - SetFocus(); - - if (auto prov = refParent().getDataProvider()) - { - const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition()); - const ptrdiff_t row = rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range - const ColumnPosInfo cpi = refParent().getColumnAtPos(absPos.x); //returns ColumnType::NONE if no column at x position! - const HoverArea rowHover = prov->getRowMouseHover(row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth); - //row < 0 possible!!! Pressing "Menu key" simulates Mouse Right Down + Up at position 0xffff/0xffff! - - GridClickEvent mouseEvent(event.RightDown() ? EVENT_GRID_MOUSE_RIGHT_DOWN : EVENT_GRID_MOUSE_LEFT_DOWN, event, row, rowHover); - - if (row >= 0) - if (!event.RightDown() || !refParent().isSelected(row)) //do NOT start a new selection if user right-clicks on a selected area! - { - if (event.ControlDown()) - activeSelection_ = std::make_unique<MouseSelection>(*this, row, !refParent().isSelected(row), mouseEvent); - else if (event.ShiftDown()) - { - activeSelection_ = std::make_unique<MouseSelection>(*this, selectionAnchor_, true, mouseEvent); - refParent().clearSelection(ALLOW_GRID_EVENT); - } - else - { - activeSelection_ = std::make_unique<MouseSelection>(*this, row, true, mouseEvent); - refParent().clearSelection(ALLOW_GRID_EVENT); - } - } - //notify event *after* potential "clearSelection(true)" above: a client should first receive a GridRangeSelectEvent for clearing the grid, if necessary, - //then GridClickEvent and the associated GridRangeSelectEvent one after the other - sendEventNow(mouseEvent); - - Refresh(); - } - event.Skip(); //allow changing focus - } - - void onMouseUp(wxMouseEvent& event) - { - if (activeSelection_) - { - const size_t rowCount = refParent().getRowCount(); - if (rowCount > 0) - { - if (activeSelection_->getCurrentRow() < rowCount) - { - cursorRow_ = activeSelection_->getCurrentRow(); - selectionAnchor_ = activeSelection_->getStartRow(); //allowed to be "out of range" - } - else if (activeSelection_->getStartRow() < rowCount) //don't change cursor if "to" and "from" are out of range - { - cursorRow_ = rowCount - 1; - selectionAnchor_ = activeSelection_->getStartRow(); //allowed to be "out of range" - } - else //total selection "out of range" - selectionAnchor_ = cursorRow_; - } - //slight deviation from Explorer: change cursor while dragging mouse! -> unify behavior with shift + direction keys - - refParent().selectRangeAndNotify(activeSelection_->getStartRow (), //from - activeSelection_->getCurrentRow(), //to - activeSelection_->isPositiveSelect(), - &activeSelection_->getFirstClick()); - activeSelection_.reset(); - } - - if (auto prov = refParent().getDataProvider()) - { - //this one may point to row which is not in visible area! - const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition()); - const ptrdiff_t row = rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range - const ColumnPosInfo cpi = refParent().getColumnAtPos(absPos.x); //returns ColumnType::NONE if no column at x position! - const HoverArea rowHover = prov->getRowMouseHover(row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth); - //notify click event after the range selection! e.g. this makes sure the selection is applied before showing a context menu - sendEventNow(GridClickEvent(event.RightUp() ? EVENT_GRID_MOUSE_RIGHT_UP : EVENT_GRID_MOUSE_LEFT_UP, event, row, rowHover)); - } - - //update highlight_ and tooltip: on OS X no mouse movement event is generated after a mouse button click (unlike on Windows) - event.SetPosition(ScreenToClient(wxGetMousePosition())); //mouse position may have changed within above callbacks (e.g. context menu was shown)! - onMouseMovement(event); - - Refresh(); - event.Skip(); //allow changing focus - } - - void onMouseCaptureLost(wxMouseCaptureLostEvent& event) override - { - activeSelection_.reset(); - highlight_.row = -1; - Refresh(); - //event.Skip(); -> we DID handle it! - } - - void onMouseMovement(wxMouseEvent& event) override - { - if (auto prov = refParent().getDataProvider()) - { - const ptrdiff_t rowCount = refParent().getRowCount(); - const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition()); - const ptrdiff_t row = rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range - const ColumnPosInfo cpi = refParent().getColumnAtPos(absPos.x); //returns ColumnType::NONE if no column at x position! - const HoverArea rowHover = prov->getRowMouseHover(row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth); - - const std::wstring toolTip = [&] - { - if (cpi.colType != ColumnType::NONE && 0 <= row && row < rowCount) - return prov->getToolTip(row, cpi.colType); - return std::wstring(); - }(); - setToolTip(toolTip); //show even during mouse selection! - - if (activeSelection_) - activeSelection_->evalMousePos(); //call on both mouse movement + timer event! - else - { - refreshHighlight(highlight_); - highlight_.row = row; - highlight_.rowHover = rowHover; - refreshHighlight(highlight_); //multiple Refresh() calls are condensed into single one! - } - } - event.Skip(); - } - - void onLeaveWindow(wxMouseEvent& event) override //wxEVT_LEAVE_WINDOW does not respect mouse capture! - { - if (!activeSelection_) - { - refreshHighlight(highlight_); - highlight_.row = -1; - } - - event.Skip(); - } - - - void onFocus(wxFocusEvent& event) override { Refresh(); event.Skip(); } - - class MouseSelection : private wxEvtHandler - { - public: - MouseSelection(MainWin& wnd, size_t rowStart, bool positiveSelect, const GridClickEvent& firstClick) : - wnd_(wnd), rowStart_(rowStart), rowCurrent_(rowStart), positiveSelect_(positiveSelect), firstClick_(firstClick) - { - wnd_.CaptureMouse(); - timer_.Connect(wxEVT_TIMER, wxEventHandler(MouseSelection::onTimer), nullptr, this); - timer_.Start(100); //timer interval in ms - evalMousePos(); - } - ~MouseSelection() { if (wnd_.HasCapture()) wnd_.ReleaseMouse(); } - - size_t getStartRow () const { return rowStart_; } - size_t getCurrentRow () const { return rowCurrent_; } - bool isPositiveSelect() const { return positiveSelect_; } //are we selecting or unselecting? - const GridClickEvent& getFirstClick() const { return firstClick_; } - - void evalMousePos() - { - const auto now = std::chrono::steady_clock::now(); - const double deltaSecs = std::chrono::duration<double>(now - lastEvalTime_).count(); //unit: [sec] - lastEvalTime_ = now; - - const wxPoint clientPos = wnd_.ScreenToClient(wxGetMousePosition()); - const wxSize clientSize = wnd_.GetClientSize(); - assert(wnd_.GetClientAreaOrigin() == wxPoint()); - - //scroll while dragging mouse - const int overlapPixY = clientPos.y < 0 ? clientPos.y : - clientPos.y >= clientSize.GetHeight() ? clientPos.y - (clientSize.GetHeight() - 1) : 0; - const int overlapPixX = clientPos.x < 0 ? clientPos.x : - clientPos.x >= clientSize.GetWidth() ? clientPos.x - (clientSize.GetWidth() - 1) : 0; - - int pixelsPerUnitY = 0; - wnd_.refParent().GetScrollPixelsPerUnit(nullptr, &pixelsPerUnitY); - if (pixelsPerUnitY <= 0) return; - - const double mouseDragSpeedIncScrollU = pixelsPerUnitY > 0 ? MOUSE_DRAG_ACCELERATION * wnd_.rowLabelWin_.getRowHeight() / pixelsPerUnitY : 0; //unit: [scroll units / (pixel * sec)] - - auto autoScroll = [&](int overlapPix, double& toScroll) - { - if (overlapPix != 0) - { - const double scrollSpeed = overlapPix * mouseDragSpeedIncScrollU; //unit: [scroll units / sec] - toScroll += scrollSpeed * deltaSecs; - } - else - toScroll = 0; - }; - - autoScroll(overlapPixX, toScrollX_); - autoScroll(overlapPixY, toScrollY_); - - if (static_cast<int>(toScrollX_) != 0 || static_cast<int>(toScrollY_) != 0) - { - wnd_.refParent().scrollDelta(static_cast<int>(toScrollX_), static_cast<int>(toScrollY_)); // - toScrollX_ -= static_cast<int>(toScrollX_); //rounds down for positive numbers, up for negative, - toScrollY_ -= static_cast<int>(toScrollY_); //exactly what we want - } - - //select current row *after* scrolling - wxPoint clientPosTrimmed = clientPos; - numeric::clamp(clientPosTrimmed.y, 0, clientSize.GetHeight() - 1); //do not select row outside client window! - - const wxPoint absPos = wnd_.refParent().CalcUnscrolledPosition(clientPosTrimmed); - const ptrdiff_t newRow = wnd_.rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range - if (newRow >= 0) - if (rowCurrent_ != newRow) - { - rowCurrent_ = newRow; - wnd_.Refresh(); - } - } - - private: - void onTimer(wxEvent& event) { evalMousePos(); } - - MainWin& wnd_; - const size_t rowStart_; - ptrdiff_t rowCurrent_; - const bool positiveSelect_; - const GridClickEvent firstClick_; - wxTimer timer_; - double toScrollX_ = 0; //count outstanding scroll unit fractions while dragging mouse - double toScrollY_ = 0; // - std::chrono::steady_clock::time_point lastEvalTime_ = std::chrono::steady_clock::now(); - }; - - struct MouseHighlight - { - ptrdiff_t row = -1; - HoverArea rowHover = HoverArea::NONE; - }; - - void ScrollWindow(int dx, int dy, const wxRect* rect) override - { - wxWindow::ScrollWindow(dx, dy, rect); - rowLabelWin_.ScrollWindow(0, dy, rect); - colLabelWin_.ScrollWindow(dx, 0, rect); - - //attention, wxGTK call sequence: wxScrolledWindow::Scroll() -> wxScrolledHelperNative::Scroll() -> wxScrolledHelperNative::DoScroll() - //which *first* calls us, MainWin::ScrollWindow(), and *then* internally updates m_yScrollPosition - //=> we cannot use CalcUnscrolledPosition() here which gives the wrong/outdated value!!! - //=> we need to update asynchronously: - //=> don't send async event repeatedly => severe performance issues on wxGTK! - //=> can't use idle event neither: too few idle events on Windows, e.g. NO idle events while mouse drag-scrolling! - //=> solution: send single async event at most! - if (!gridUpdatePending_) //without guarding, the number of outstanding async events can become very high during scrolling!! test case: Ubuntu: 170; Windows: 20 - { - gridUpdatePending_ = true; - wxCommandEvent scrollEvent(EVENT_GRID_HAS_SCROLLED); - AddPendingEvent(scrollEvent); //asynchronously call updateAfterScroll() - } - } - - void onRequestWindowUpdate(wxEvent& event) - { - assert(gridUpdatePending_); - ZEN_ON_SCOPE_EXIT(gridUpdatePending_ = false); - - refParent().updateWindowSizes(false); //row label width has changed -> do *not* update scrollbars: recursion on wxGTK! -> still a problem, now that we're called async?? - rowLabelWin_.Update(); //update while dragging scroll thumb - } - - void refreshRow(size_t row) - { - const wxRect& rowArea = rowLabelWin_.getRowLabelArea(row); //returns empty rect if row not found - const wxPoint topLeft = refParent().CalcScrolledPosition(wxPoint(0, rowArea.y)); //absolute -> client coordinates - wxRect cellArea(topLeft, wxSize(refParent().getColWidthsSum(GetClientSize().GetWidth()), rowArea.height)); - RefreshRect(cellArea, false); - } - - void refreshHighlight(const MouseHighlight& hl) - { - const ptrdiff_t rowCount = refParent().getRowCount(); - if (0 <= hl.row && hl.row < rowCount && hl.rowHover != HoverArea::NONE) //no highlight_? => NOP! - refreshRow(hl.row); - } - - RowLabelWin& rowLabelWin_; - ColLabelWin& colLabelWin_; - - std::unique_ptr<MouseSelection> activeSelection_; //bound while user is selecting with mouse - MouseHighlight highlight_; //current mouse highlight_ (superseeded by activeSelection_ if available) - - ptrdiff_t cursorRow_ = 0; - size_t selectionAnchor_ = 0; - bool gridUpdatePending_ = false; -}; - -//---------------------------------------------------------------------------------------------------------------- -//---------------------------------------------------------------------------------------------------------------- - -Grid::Grid(wxWindow* parent, - wxWindowID id, - const wxPoint& pos, - const wxSize& size, - long style, - const wxString& name) : wxScrolledWindow(parent, id, pos, size, style | wxWANTS_CHARS, name) -{ - cornerWin_ = new CornerWin (*this); // - rowLabelWin_ = new RowLabelWin(*this); //owership handled by "this" - colLabelWin_ = new ColLabelWin(*this); // - mainWin_ = new MainWin (*this, *rowLabelWin_, *colLabelWin_); // - - colLabelHeight_ = 2 * DEFAULT_COL_LABEL_BORDER + [&]() -> int - { - //coordinate with ColLabelWin::render(): - wxFont labelFont = colLabelWin_->GetFont(); - labelFont.SetWeight(wxFONTWEIGHT_BOLD); - return labelFont.GetPixelSize().GetHeight(); - }(); - - SetTargetWindow(mainWin_); - - SetInitialSize(size); //"Most controls will use this to set their initial size" -> why not - - assert(GetClientSize() == GetSize()); //borders are NOT allowed for Grid - //reason: updateWindowSizes() wants to use "GetSize()" as a "GetClientSize()" including scrollbars - - Connect(wxEVT_PAINT, wxPaintEventHandler(Grid::onPaintEvent ), nullptr, this); - Connect(wxEVT_ERASE_BACKGROUND, wxEraseEventHandler(Grid::onEraseBackGround), nullptr, this); - Connect(wxEVT_SIZE, wxSizeEventHandler (Grid::onSizeEvent ), nullptr, this); - - Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(Grid::onKeyDown), nullptr, this); -} - - -void Grid::updateWindowSizes(bool updateScrollbar) -{ - /* We have to deal with TWO nasty circular dependencies: - 1. - rowLabelWidth - /|\ - mainWin::client width - /|\ - SetScrollbars -> show/hide horizontal scrollbar depending on client width - /|\ - mainWin::client height -> possibly trimmed by horizontal scrollbars - /|\ - rowLabelWidth - - 2. - mainWin_->GetClientSize() - /|\ - SetScrollbars -> show/hide scrollbars depending on whether client size is big enough - /|\ - GetClientSize(); -> possibly trimmed by scrollbars - /|\ - mainWin_->GetClientSize() -> also trimmed, since it's a sub-window! - */ - - //break this vicious circle: - - //harmonize with Grid::GetSizeAvailableForScrollTarget()! - - //1. calculate row label width independent from scrollbars - const int mainWinHeightGross = std::max(GetSize().GetHeight() - colLabelHeight_, 0); //independent from client sizes and scrollbars! - const ptrdiff_t logicalHeight = rowLabelWin_->getLogicalHeight(); // - - int rowLabelWidth = 0; - if (drawRowLabel_ && logicalHeight > 0) - { - ptrdiff_t yFrom = CalcUnscrolledPosition(wxPoint(0, 0)).y; - ptrdiff_t yTo = CalcUnscrolledPosition(wxPoint(0, mainWinHeightGross - 1)).y ; - numeric::clamp<ptrdiff_t>(yFrom, 0, logicalHeight - 1); - numeric::clamp<ptrdiff_t>(yTo, 0, logicalHeight - 1); - - const ptrdiff_t rowFrom = rowLabelWin_->getRowAtPos(yFrom); - const ptrdiff_t rowTo = rowLabelWin_->getRowAtPos(yTo); - if (rowFrom >= 0 && rowTo >= 0) - rowLabelWidth = rowLabelWin_->getBestWidth(rowFrom, rowTo); - } - - auto getMainWinSize = [&](const wxSize& clientSize) { return wxSize(std::max(0, clientSize.GetWidth() - rowLabelWidth), std::max(0, clientSize.GetHeight() - colLabelHeight_)); }; - - auto setScrollbars2 = [&](int logWidth, int logHeight) //replace SetScrollbars, which loses precision of pixelsPerUnitX for some brain-dead reason - { - mainWin_->SetVirtualSize(logWidth, logHeight); //set before calling SetScrollRate(): - //else SetScrollRate() would fail to preserve scroll position when "new virtual pixel-pos > old virtual height" - - int ppsuX = 0; //pixel per scroll unit - int ppsuY = 0; - GetScrollPixelsPerUnit(&ppsuX, &ppsuY); - - const int ppsuNew = rowLabelWin_->getRowHeight(); - if (ppsuX != ppsuNew || ppsuY != ppsuNew) //support polling! - SetScrollRate(ppsuNew, ppsuNew); //internally calls AdjustScrollbars() and GetVirtualSize()! - - AdjustScrollbars(); //lousy wxWidgets design decision: internally calls mainWin_->GetClientSize() without considering impact of scrollbars! - //Attention: setting scrollbars triggers *synchronous* resize event if scrollbars are shown or hidden! => updateWindowSizes() recursion! (Windows) - }; - - //2. update managed windows' sizes: just assume scrollbars are already set correctly, even if they may not be (yet)! - //this ensures mainWin_->SetVirtualSize() and AdjustScrollbars() are working with the correct main window size, unless sb change later, which triggers a recalculation anyway! - const wxSize mainWinSize = getMainWinSize(GetClientSize()); - - cornerWin_ ->SetSize(0, 0, rowLabelWidth, colLabelHeight_); - rowLabelWin_->SetSize(0, colLabelHeight_, rowLabelWidth, mainWinSize.GetHeight()); - colLabelWin_->SetSize(rowLabelWidth, 0, mainWinSize.GetWidth(), colLabelHeight_); - mainWin_ ->SetSize(rowLabelWidth, colLabelHeight_, mainWinSize.GetWidth(), mainWinSize.GetHeight()); - - //avoid flicker in wxWindowMSW::HandleSize() when calling ::EndDeferWindowPos() where the sub-windows are moved only although they need to be redrawn! - colLabelWin_->Refresh(); - mainWin_ ->Refresh(); - - //3. update scrollbars: "guide wxScrolledHelper to not screw up too much" - if (updateScrollbar) - { - const int mainWinWidthGross = getMainWinSize(GetSize()).GetWidth(); - - if (logicalHeight <= mainWinHeightGross && - getColWidthsSum(mainWinWidthGross) <= mainWinWidthGross && - //this special case needs to be considered *only* when both scrollbars are flexible: - showScrollbarX_ == SB_SHOW_AUTOMATIC && - showScrollbarY_ == SB_SHOW_AUTOMATIC) - setScrollbars2(0, 0); //no scrollbars required at all! -> wxScrolledWindow requires active help to detect this special case! - else - { - const int logicalWidthTmp = getColWidthsSum(mainWinSize.GetWidth()); //assuming vertical scrollbar stays as it is... - setScrollbars2(logicalWidthTmp, logicalHeight); //if scrollbars are shown or hidden a new resize event recurses into updateWindowSizes() - /* - is there a risk of endless recursion? No, 2-level recursion at most, consider the following 6 cases: - - <----------gw----------> - <----------nw------> - ------------------------ /|\ /|\ - | | | | | - | main window | | nh | - | | | | gh - ------------------------ \|/ | - | | | | - ------------------------ \|/ - gw := gross width - nw := net width := gross width - sb size - gh := gross height - nh := net height := gross height - sb size - - There are 6 cases that can occur: - --------------------------------- - lw := logical width - lh := logical height - - 1. lw <= gw && lh <= gh => no scrollbars needed - - 2. lw > gw && lh > gh => need both scrollbars - - 3. lh > gh - 4.1 lw <= nw => need vertical scrollbar only - 4.2 nw < lw <= gw => need both scrollbars - - 4. lw > gw - 3.1 lh <= nh => need horizontal scrollbar only - 3.2 nh < lh <= gh => need both scrollbars - */ - } - } -} - - -wxSize Grid::GetSizeAvailableForScrollTarget(const wxSize& size) -{ - //harmonize with Grid::updateWindowSizes()! - - //1. calculate row label width independent from scrollbars - const int mainWinHeightGross = std::max(size.GetHeight() - colLabelHeight_, 0); //independent from client sizes and scrollbars! - const ptrdiff_t logicalHeight = rowLabelWin_->getLogicalHeight(); // - - int rowLabelWidth = 0; - if (drawRowLabel_ && logicalHeight > 0) - { - ptrdiff_t yFrom = CalcUnscrolledPosition(wxPoint(0, 0)).y; - ptrdiff_t yTo = CalcUnscrolledPosition(wxPoint(0, mainWinHeightGross - 1)).y ; - numeric::clamp<ptrdiff_t>(yFrom, 0, logicalHeight - 1); - numeric::clamp<ptrdiff_t>(yTo, 0, logicalHeight - 1); - - const ptrdiff_t rowFrom = rowLabelWin_->getRowAtPos(yFrom); - const ptrdiff_t rowTo = rowLabelWin_->getRowAtPos(yTo); - if (rowFrom >= 0 && rowTo >= 0) - rowLabelWidth = rowLabelWin_->getBestWidth(rowFrom, rowTo); - } - - return size - wxSize(rowLabelWidth, colLabelHeight_); -} - - -void Grid::onPaintEvent(wxPaintEvent& event) { wxPaintDC dc(this); } - - -void Grid::onKeyDown(wxKeyEvent& event) -{ - int keyCode = event.GetKeyCode(); - if (GetLayoutDirection() == wxLayout_RightToLeft) - { - if (keyCode == WXK_LEFT || keyCode == WXK_NUMPAD_LEFT) - keyCode = WXK_RIGHT; - else if (keyCode == WXK_RIGHT || keyCode == WXK_NUMPAD_RIGHT) - keyCode = WXK_LEFT; - } - - const ptrdiff_t rowCount = getRowCount(); - const ptrdiff_t cursorRow = mainWin_->getCursor(); - - auto moveCursorTo = [&](ptrdiff_t row) - { - if (rowCount > 0) - { - numeric::clamp<ptrdiff_t>(row, 0, rowCount - 1); - setGridCursor(row); - } - }; - - auto selectWithCursorTo = [&](ptrdiff_t row) - { - if (rowCount > 0) - { - numeric::clamp<ptrdiff_t>(row, 0, rowCount - 1); - selectWithCursor(row); - } - }; - - switch (keyCode) - { - //case WXK_TAB: - // if (Navigate(event.ShiftDown() ? wxNavigationKeyEvent::IsBackward : wxNavigationKeyEvent::IsForward)) - // return; - // break; - - case WXK_UP: - case WXK_NUMPAD_UP: - if (event.ShiftDown()) - selectWithCursorTo(cursorRow - 1); - else if (event.ControlDown()) - scrollDelta(0, -1); - else - moveCursorTo(cursorRow - 1); - return; //swallow event: wxScrolledWindow, wxWidgets 2.9.3 on Kubuntu x64 processes arrow keys: prevent this! - - case WXK_DOWN: - case WXK_NUMPAD_DOWN: - if (event.ShiftDown()) - selectWithCursorTo(cursorRow + 1); - else if (event.ControlDown()) - scrollDelta(0, 1); - else - moveCursorTo(cursorRow + 1); - return; //swallow event - - case WXK_LEFT: - case WXK_NUMPAD_LEFT: - if (event.ControlDown()) - scrollDelta(-1, 0); - else if (event.ShiftDown()) - ; - else - moveCursorTo(cursorRow); - return; - - case WXK_RIGHT: - case WXK_NUMPAD_RIGHT: - if (event.ControlDown()) - scrollDelta(1, 0); - else if (event.ShiftDown()) - ; - else - moveCursorTo(cursorRow); - return; - - case WXK_HOME: - case WXK_NUMPAD_HOME: - if (event.ShiftDown()) - selectWithCursorTo(0); - //else if (event.ControlDown()) - // ; - else - moveCursorTo(0); - return; - - case WXK_END: - case WXK_NUMPAD_END: - if (event.ShiftDown()) - selectWithCursorTo(rowCount - 1); - //else if (event.ControlDown()) - // ; - else - moveCursorTo(rowCount - 1); - return; - - case WXK_PAGEUP: - case WXK_NUMPAD_PAGEUP: - if (event.ShiftDown()) - selectWithCursorTo(cursorRow - GetClientSize().GetHeight() / rowLabelWin_->getRowHeight()); - //else if (event.ControlDown()) - // ; - else - moveCursorTo(cursorRow - GetClientSize().GetHeight() / rowLabelWin_->getRowHeight()); - return; - - case WXK_PAGEDOWN: - case WXK_NUMPAD_PAGEDOWN: - if (event.ShiftDown()) - selectWithCursorTo(cursorRow + GetClientSize().GetHeight() / rowLabelWin_->getRowHeight()); - //else if (event.ControlDown()) - // ; - else - moveCursorTo(cursorRow + GetClientSize().GetHeight() / rowLabelWin_->getRowHeight()); - return; - - case 'A': //Ctrl + A - select all - if (event.ControlDown()) - selectRangeAndNotify(0, rowCount, true /*positive*/, nullptr /*mouseInitiated*/); - break; - - case WXK_NUMPAD_ADD: //CTRL + '+' - auto-size all - if (event.ControlDown()) - autoSizeColumns(ALLOW_GRID_EVENT); - return; - } - - event.Skip(); -} - - -void Grid::setColumnLabelHeight(int height) -{ - colLabelHeight_ = std::max(0, height); - updateWindowSizes(); -} - - -void Grid::showRowLabel(bool show) -{ - drawRowLabel_ = show; - updateWindowSizes(); -} - - -void Grid::selectAllRows(GridEventPolicy rangeEventPolicy) -{ - selection_.selectAll(); - mainWin_->Refresh(); - - if (rangeEventPolicy == ALLOW_GRID_EVENT) //notify event, even if we're not triggered by user interaction - { - GridRangeSelectEvent selEvent(0, getRowCount(), true, nullptr); - if (wxEvtHandler* evtHandler = GetEventHandler()) - evtHandler->ProcessEvent(selEvent); - } -} - - -void Grid::clearSelection(GridEventPolicy rangeEventPolicy) -{ - selection_.clear(); - mainWin_->Refresh(); - - if (rangeEventPolicy == ALLOW_GRID_EVENT) //notify event, even if we're not triggered by user interaction - { - GridRangeSelectEvent unselectionEvent(0, getRowCount(), false, nullptr); - if (wxEvtHandler* evtHandler = GetEventHandler()) - evtHandler->ProcessEvent(unselectionEvent); - } -} - - -void Grid::scrollDelta(int deltaX, int deltaY) -{ - int scrollPosX = 0; - int scrollPosY = 0; - GetViewStart(&scrollPosX, &scrollPosY); - - scrollPosX += deltaX; - scrollPosY += deltaY; - - scrollPosX = std::max(0, scrollPosX); //wxScrollHelper::Scroll() will exit prematurely if input happens to be "-1"! - scrollPosY = std::max(0, scrollPosY); // - - Scroll(scrollPosX, scrollPosY); //internally calls wxWindows::Update()! - updateWindowSizes(); //may show horizontal scroll bar -} - - -void Grid::redirectRowLabelEvent(wxMouseEvent& event) -{ - event.m_x = 0; - if (wxEvtHandler* evtHandler = mainWin_->GetEventHandler()) - evtHandler->ProcessEvent(event); - - if (event.ButtonDown() && wxWindow::FindFocus() != mainWin_) - mainWin_->SetFocus(); -} - - -size_t Grid::getRowCount() const -{ - return dataView_ ? dataView_->getRowCount() : 0; -} - - -void Grid::Refresh(bool eraseBackground, const wxRect* rect) -{ - const size_t rowCountNew = getRowCount(); - if (rowCountOld_ != rowCountNew) - { - rowCountOld_ = rowCountNew; - updateWindowSizes(); - } - - if (selection_.maxSize() != rowCountNew) //clear selection only when needed (consider setSelectedRows()) - selection_.init(rowCountNew); - - wxScrolledWindow::Refresh(eraseBackground, rect); -} - - -void Grid::setRowHeight(int height) -{ - rowLabelWin_->setRowHeight(height); - updateWindowSizes(); - Refresh(); -} - - -void Grid::setColumnConfig(const std::vector<Grid::ColumnAttribute>& attr) -{ - //hold ownership of non-visible columns - oldColAttributes_ = attr; - - std::vector<VisibleColumn> visCols; - for (const ColumnAttribute& ca : attr) - { - assert(ca.type_ != ColumnType::NONE); - if (ca.visible_) - visCols.emplace_back(ca.type_, ca.offset_, ca.stretch_); - } - - //"ownership" of visible columns is now within Grid - visibleCols_ = visCols; - - updateWindowSizes(); - Refresh(); -} - - -std::vector<Grid::ColumnAttribute> Grid::getColumnConfig() const -{ - //get non-visible columns (+ outdated visible ones) - std::vector<ColumnAttribute> output = oldColAttributes_; - - auto iterVcols = visibleCols_.begin(); - auto iterVcolsend = visibleCols_.end(); - - //update visible columns but keep order of non-visible ones! - for (ColumnAttribute& ca : output) - if (ca.visible_) - { - if (iterVcols != iterVcolsend) - { - ca.type_ = iterVcols->type_; - ca.stretch_ = iterVcols->stretch_; - ca.offset_ = iterVcols->offset_; - ++iterVcols; - } - else - assert(false); - } - assert(iterVcols == iterVcolsend); - - return output; -} - - -void Grid::showScrollBars(Grid::ScrollBarStatus horizontal, Grid::ScrollBarStatus vertical) -{ - if (showScrollbarX_ == horizontal && - showScrollbarY_ == vertical) return; //support polling! - - showScrollbarX_ = horizontal; - showScrollbarY_ = vertical; - -#if defined ZEN_WIN || defined ZEN_MAC - //handled by Grid::SetScrollbar -#elif defined ZEN_LINUX //get rid of scrollbars, but preserve scrolling behavior! - //the following wxGTK approach is pretty much identical to wxWidgets 2.9 ShowScrollbars() code! - - auto mapStatus = [](ScrollBarStatus sbStatus) -> GtkPolicyType - { - switch (sbStatus) - { - case SB_SHOW_AUTOMATIC: - return GTK_POLICY_AUTOMATIC; - case SB_SHOW_ALWAYS: - return GTK_POLICY_ALWAYS; - case SB_SHOW_NEVER: - return GTK_POLICY_NEVER; - } - assert(false); - return GTK_POLICY_AUTOMATIC; - }; - - GtkWidget* gridWidget = wxWindow::m_widget; - GtkScrolledWindow* scrolledWindow = GTK_SCROLLED_WINDOW(gridWidget); - ::gtk_scrolled_window_set_policy(scrolledWindow, - mapStatus(horizontal), - mapStatus(vertical)); -#endif - - updateWindowSizes(); -} - -#if defined ZEN_WIN || defined ZEN_MAC -void Grid::SetScrollbar(int orientation, int position, int thumbSize, int range, bool refresh) -{ - /* - wxWidgets >= 2.9 ShowScrollbars() is next to useless since it doesn't - honor wxSHOW_SB_ALWAYS on OS X, so let's ditch it and avoid more non-portability surprises - */ - - ScrollBarStatus sbStatus = SB_SHOW_AUTOMATIC; - if (orientation == wxHORIZONTAL) - sbStatus = showScrollbarX_; - else if (orientation == wxVERTICAL) - sbStatus = showScrollbarY_; - else - assert(false); - - switch (sbStatus) - { - case SB_SHOW_AUTOMATIC: - wxScrolledWindow::SetScrollbar(orientation, position, thumbSize, range, refresh); - break; - - case SB_SHOW_ALWAYS: - if (range <= 1) //scrollbars would be hidden for range == 0 or 1! - wxScrolledWindow::SetScrollbar(orientation, 0, 199999, 200000, refresh); - else - wxScrolledWindow::SetScrollbar(orientation, position, thumbSize, range, refresh); - break; - - case SB_SHOW_NEVER: - wxScrolledWindow::SetScrollbar(orientation, 0, 0, 0, refresh); - break; - } -} -#endif - - -wxWindow& Grid::getCornerWin () { return *cornerWin_; } -wxWindow& Grid::getRowLabelWin() { return *rowLabelWin_; } -wxWindow& Grid::getColLabelWin() { return *colLabelWin_; } -wxWindow& Grid::getMainWin () { return *mainWin_; } -const wxWindow& Grid::getMainWin() const { return *mainWin_; } - - -Opt<Grid::ColAction> Grid::clientPosToColumnAction(const wxPoint& pos) const -{ - const int absPosX = CalcUnscrolledPosition(pos).x; - if (absPosX >= 0) - { - const int resizeTolerance = allowColumnResize_ ? COLUMN_RESIZE_TOLERANCE : 0; - std::vector<ColumnWidth> absWidths = getColWidths(); //resolve stretched widths - - int accuWidth = 0; - for (size_t col = 0; col < absWidths.size(); ++col) - { - accuWidth += absWidths[col].width_; - if (std::abs(absPosX - accuWidth) < resizeTolerance) - { - ColAction out; - out.wantResize = true; - out.col = col; - return out; - } - else if (absPosX < accuWidth) - { - ColAction out; - out.wantResize = false; - out.col = col; - return out; - } - } - } - return NoValue(); -} - - -void Grid::moveColumn(size_t colFrom, size_t colTo) -{ - if (colFrom < visibleCols_.size() && - colTo < visibleCols_.size() && - colTo != colFrom) - { - const VisibleColumn colAtt = visibleCols_[colFrom]; - visibleCols_.erase (visibleCols_.begin() + colFrom); - visibleCols_.insert(visibleCols_.begin() + colTo, colAtt); - } -} - - -ptrdiff_t Grid::clientPosToMoveTargetColumn(const wxPoint& pos) const -{ - - const int absPosX = CalcUnscrolledPosition(pos).x; - - int accWidth = 0; - std::vector<ColumnWidth> absWidths = getColWidths(); //resolve negative/stretched widths - for (auto itCol = absWidths.begin(); itCol != absWidths.end(); ++itCol) - { - const int width = itCol->width_; //beware dreaded unsigned conversions! - accWidth += width; - - if (absPosX < accWidth - width / 2) - return itCol - absWidths.begin(); - } - return absWidths.size(); -} - - -ColumnType Grid::colToType(size_t col) const -{ - if (col < visibleCols_.size()) - return visibleCols_[col].type_; - return ColumnType::NONE; -} - - -ptrdiff_t Grid::getRowAtPos(int posY) const { return rowLabelWin_->getRowAtPos(posY); } - - -Grid::ColumnPosInfo Grid::getColumnAtPos(int posX) const -{ - if (posX >= 0) - { - int accWidth = 0; - for (const ColumnWidth& cw : getColWidths()) - { - accWidth += cw.width_; - if (posX < accWidth) - return { cw.type_, posX + cw.width_ - accWidth, cw.width_ }; - } - } - return { ColumnType::NONE, 0, 0 }; -} - - -wxRect Grid::getColumnLabelArea(ColumnType colType) const -{ - std::vector<ColumnWidth> absWidths = getColWidths(); //resolve negative/stretched widths - - //colType is not unique in general, but *this* function expects it! - assert(std::count_if(absWidths.begin(), absWidths.end(), [&](const ColumnWidth& cw) { return cw.type_ == colType; }) <= 1); - - auto itCol = std::find_if(absWidths.begin(), absWidths.end(), [&](const ColumnWidth& cw) { return cw.type_ == colType; }); - if (itCol != absWidths.end()) - { - ptrdiff_t posX = 0; - for (auto it = absWidths.begin(); it != itCol; ++it) - posX += it->width_; - - return wxRect(wxPoint(posX, 0), wxSize(itCol->width_, colLabelHeight_)); - } - return wxRect(); -} - - -void Grid::refreshCell(size_t row, ColumnType colType) -{ - const wxRect& colArea = getColumnLabelArea(colType); //returns empty rect if column not found - const wxRect& rowArea = rowLabelWin_->getRowLabelArea(row); //returns empty rect if row not found - if (colArea.height > 0 && rowArea.height > 0) - { - const wxPoint topLeft = CalcScrolledPosition(wxPoint(colArea.x, rowArea.y)); //absolute -> client coordinates - const wxRect cellArea(topLeft, wxSize(colArea.width, rowArea.height)); - - getMainWin().RefreshRect(cellArea, false); - } -} - - -void Grid::setGridCursor(size_t row) -{ - mainWin_->setCursor(row, row); - makeRowVisible(row); - - selection_.clear(); //clear selection, do NOT fire event - selectRangeAndNotify(row, row, true /*positive*/, nullptr /*mouseInitiated*/); //set new selection + fire event - - mainWin_->Refresh(); - rowLabelWin_->Refresh(); //row labels! (Kubuntu) -} - - -void Grid::selectWithCursor(ptrdiff_t row) -{ - const size_t anchorRow = mainWin_->getAnchor(); - - mainWin_->setCursor(row, anchorRow); - makeRowVisible(row); - - selection_.clear(); //clear selection, do NOT fire event - selectRangeAndNotify(anchorRow, row, true /*positive*/, nullptr /*mouseInitiated*/); //set new selection + fire event - - mainWin_->Refresh(); - rowLabelWin_->Refresh(); -} - - -void Grid::makeRowVisible(size_t row) -{ - const wxRect labelRect = rowLabelWin_->getRowLabelArea(row); //returns empty rect if row not found - if (labelRect.height > 0) - { - int scrollPosX = 0; - GetViewStart(&scrollPosX, nullptr); - - int pixelsPerUnitY = 0; - GetScrollPixelsPerUnit(nullptr, &pixelsPerUnitY); - if (pixelsPerUnitY <= 0) return; - - const int clientPosY = CalcScrolledPosition(labelRect.GetTopLeft()).y; - if (clientPosY < 0) - { - const int scrollPosY = labelRect.y / pixelsPerUnitY; - Scroll(scrollPosX, scrollPosY); //internally calls wxWindows::Update()! - updateWindowSizes(); //may show horizontal scroll bar - } - else if (clientPosY + labelRect.height > rowLabelWin_->GetClientSize().GetHeight()) - { - auto execScroll = [&](int clientHeight) - { - const int scrollPosY = std::ceil((labelRect.y - clientHeight + - labelRect.height) / static_cast<double>(pixelsPerUnitY)); - Scroll(scrollPosX, scrollPosY); - updateWindowSizes(); //may show horizontal scroll bar - }; - - const int clientHeightBefore = rowLabelWin_->GetClientSize().GetHeight(); - execScroll(clientHeightBefore); - - //client height may decrease after scroll due to a new horizontal scrollbar, resulting in a partially visible last row - const int clientHeightAfter = rowLabelWin_->GetClientSize().GetHeight(); - if (clientHeightAfter < clientHeightBefore) - execScroll(clientHeightAfter); - } - } -} - - -void Grid::selectRangeAndNotify(ptrdiff_t rowFrom, ptrdiff_t rowTo, bool positive, const GridClickEvent* mouseInitiated) -{ - //sort + convert to half-open range - auto rowFirst = std::min(rowFrom, rowTo); - auto rowLast = std::max(rowFrom, rowTo) + 1; - - const size_t rowCount = getRowCount(); - numeric::clamp<ptrdiff_t>(rowFirst, 0, rowCount); - numeric::clamp<ptrdiff_t>(rowLast, 0, rowCount); - - selection_.selectRange(rowFirst, rowLast, positive); - - //notify event - GridRangeSelectEvent selectionEvent(rowFirst, rowLast, positive, mouseInitiated); - if (wxEvtHandler* evtHandler = GetEventHandler()) - evtHandler->ProcessEvent(selectionEvent); - - mainWin_->Refresh(); -} - - -void Grid::scrollTo(size_t row) -{ - const wxRect labelRect = rowLabelWin_->getRowLabelArea(row); //returns empty rect if row not found - if (labelRect.height > 0) - { - int pixelsPerUnitY = 0; - GetScrollPixelsPerUnit(nullptr, &pixelsPerUnitY); - if (pixelsPerUnitY > 0) - { - const int scrollPosYNew = labelRect.y / pixelsPerUnitY; - int scrollPosXOld = 0; - int scrollPosYOld = 0; - GetViewStart(&scrollPosXOld, &scrollPosYOld); - - if (scrollPosYOld != scrollPosYNew) //support polling - { - Scroll(scrollPosXOld, scrollPosYNew); //internally calls wxWindows::Update()! - updateWindowSizes(); //may show horizontal scroll bar - Refresh(); - } - } - } -} - - -bool Grid::Enable(bool enable) -{ - Refresh(); - return wxScrolledWindow::Enable(enable); -} - - -size_t Grid::getGridCursor() const -{ - return mainWin_->getCursor(); -} - - -int Grid::getBestColumnSize(size_t col) const -{ - if (dataView_ && col < visibleCols_.size()) - { - const ColumnType type = visibleCols_[col].type_; - - wxClientDC dc(mainWin_); - dc.SetFont(mainWin_->GetFont()); //harmonize with MainWin::render() - - int maxSize = 0; - - auto rowRange = rowLabelWin_->getRowsOnClient(mainWin_->GetClientRect()); //returns range [begin, end) - for (auto row = rowRange.first; row < rowRange.second; ++row) - maxSize = std::max(maxSize, dataView_->getBestSize(dc, row, type)); - - return maxSize; - } - return -1; -} - - -void Grid::setColumnWidth(int width, size_t col, GridEventPolicy columnResizeEventPolicy, bool notifyAsync) -{ - if (col < visibleCols_.size()) - { - VisibleColumn& vcRs = visibleCols_[col]; - - const std::vector<int> stretchedWidths = getColStretchedWidths(mainWin_->GetClientSize().GetWidth()); - if (stretchedWidths.size() != visibleCols_.size()) - { - assert(false); - return; - } - //CAVEATS: - //I. fixed-size columns: normalize offset so that resulting width is at least COLUMN_MIN_WIDTH: this is NOT enforced by getColWidths()! - //II. stretched columns: do not allow user to set offsets so small that they result in negative (non-normalized) widths: this gives an - //unusual delay when enlarging the column again later - width = std::max(width, COLUMN_MIN_WIDTH); - - vcRs.offset_ = width - stretchedWidths[col]; //width := stretchedWidth + offset - - //III. resizing any column should normalize *all* other stretched columns' offsets considering current mainWinWidth! - // test case: - //1. have columns, both fixed-size and stretched, fit whole window width - //2. shrink main window width so that horizontal scrollbars are shown despite the streched column - //3. shrink a fixed-size column so that the scrollbars vanish and columns cover full width again - //4. now verify that the stretched column is resizing immediately if main window is enlarged again - for (size_t col2 = 0; col2 < visibleCols_.size(); ++col2) - if (visibleCols_[col2].stretch_ > 0) //normalize stretched columns only - visibleCols_[col2].offset_ = std::max(visibleCols_[col2].offset_, COLUMN_MIN_WIDTH - stretchedWidths[col2]); - - if (columnResizeEventPolicy == ALLOW_GRID_EVENT) - { - GridColumnResizeEvent sizeEvent(vcRs.offset_, vcRs.type_); - if (wxEvtHandler* evtHandler = GetEventHandler()) - { - if (notifyAsync) - evtHandler->AddPendingEvent(sizeEvent); - else - evtHandler->ProcessEvent(sizeEvent); - } - } - } - else - assert(false); -} - - -void Grid::autoSizeColumns(GridEventPolicy columnResizeEventPolicy) -{ - if (allowColumnResize_) - { - for (size_t col = 0; col < visibleCols_.size(); ++col) - { - const int bestWidth = getBestColumnSize(col); //return -1 on error - if (bestWidth >= 0) - setColumnWidth(bestWidth, col, columnResizeEventPolicy, true); - } - updateWindowSizes(); - Refresh(); - } -} - - -std::vector<int> Grid::getColStretchedWidths(int clientWidth) const //final width = (normalized) (stretchedWidth + offset) -{ - assert(clientWidth >= 0); - clientWidth = std::max(clientWidth, 0); - int stretchTotal = 0; - for (const VisibleColumn& vc : visibleCols_) - { - assert(vc.stretch_ >= 0); - stretchTotal += vc.stretch_; - } - - int remainingWidth = clientWidth; - - std::vector<int> output; - - if (stretchTotal <= 0) - output.resize(visibleCols_.size()); //fill with zeros - else - { - for (const VisibleColumn& vc : visibleCols_) - { - const int width = clientWidth * vc.stretch_ / stretchTotal; //rounds down! - output.push_back(width); - remainingWidth -= width; - } - - //distribute *all* of clientWidth: should suffice to enlarge the first few stretched columns; no need to minimize total absolute error of distribution - if (remainingWidth > 0) - for (size_t col2 = 0; col2 < visibleCols_.size(); ++col2) - if (visibleCols_[col2].stretch_ > 0) - { - ++output[col2]; - if (--remainingWidth == 0) - break; - } - assert(remainingWidth == 0); - } - return output; -} - - -std::vector<Grid::ColumnWidth> Grid::getColWidths() const -{ - return getColWidths(mainWin_->GetClientSize().GetWidth()); -} - - -std::vector<Grid::ColumnWidth> Grid::getColWidths(int mainWinWidth) const //evaluate stretched columns -{ - const std::vector<int> stretchedWidths = getColStretchedWidths(mainWinWidth); - assert(stretchedWidths.size() == visibleCols_.size()); - - std::vector<ColumnWidth> output; - for (size_t col2 = 0; col2 < visibleCols_.size(); ++col2) - { - const auto& vc = visibleCols_[col2]; - int width = stretchedWidths[col2] + vc.offset_; - - if (vc.stretch_ > 0) - width = std::max(width, COLUMN_MIN_WIDTH); //normalization really needed here: e.g. smaller main window would result in negative width - else - width = std::max(width, 0); //support smaller width than COLUMN_MIN_WIDTH if set via configuration - - output.emplace_back(vc.type_, width); - } - return output; -} - - -int Grid::getColWidthsSum(int mainWinWidth) const -{ - int sum = 0; - for (const ColumnWidth& cw : getColWidths(mainWinWidth)) - sum += cw.width_; - return sum; -}; +// *****************************************************************************
+// * This file is part of the FreeFileSync project. It is distributed under *
+// * GNU General Public License: http://www.gnu.org/licenses/gpl-3.0 *
+// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
+// *****************************************************************************
+
+#include "grid.h"
+#include <cassert>
+#include <set>
+#include <chrono>
+#include <wx/settings.h>
+#include <wx/listbox.h>
+#include <wx/tooltip.h>
+#include <wx/timer.h>
+#include <wx/utils.h>
+//#include <zen/tick_count.h>
+#include <zen/string_tools.h>
+#include <zen/scope_guard.h>
+#include <zen/utf.h>
+#include <zen/format_unit.h>
+#include "dc.h"
+
+ #include <gtk/gtk.h>
+
+using namespace zen;
+
+
+wxColor Grid::getColorSelectionGradientFrom() { return { 137, 172, 255 }; } //blue: HSL: 158, 255, 196 HSV: 222, 0.46, 1
+wxColor Grid::getColorSelectionGradientTo () { return { 225, 234, 255 }; } // HSL: 158, 255, 240 HSV: 222, 0.12, 1
+
+const int GridData::COLUMN_GAP_LEFT = 4;
+
+
+void zen::clearArea(wxDC& dc, const wxRect& rect, const wxColor& col)
+{
+ wxDCPenChanger dummy (dc, col);
+ wxDCBrushChanger dummy2(dc, col);
+ dc.DrawRectangle(rect);
+}
+
+
+namespace
+{
+//let's NOT create wxWidgets objects statically:
+//------------------------------ Grid Parameters --------------------------------
+inline wxColor getColorLabelText() { return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); }
+inline wxColor getColorGridLine() { return { 192, 192, 192 }; } //light grey
+
+inline wxColor getColorLabelGradientFrom() { return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); }
+inline wxColor getColorLabelGradientTo () { return { 200, 200, 200 }; } //light grey
+
+inline wxColor getColorLabelGradientFocusFrom() { return getColorLabelGradientFrom(); }
+inline wxColor getColorLabelGradientFocusTo () { return Grid::getColorSelectionGradientFrom(); }
+
+const double MOUSE_DRAG_ACCELERATION = 1.5; //unit: [rows / (pixel * sec)] -> same value like Explorer!
+const int DEFAULT_COL_LABEL_BORDER = 6; //top + bottom border in addition to label height
+const int COLUMN_MOVE_DELAY = 5; //unit: [pixel] (from Explorer)
+const int COLUMN_MIN_WIDTH = 40; //only honored when resizing manually!
+const int ROW_LABEL_BORDER = 3;
+const int COLUMN_RESIZE_TOLERANCE = 6; //unit [pixel]
+const int COLUMN_FILL_GAP_TOLERANCE = 10; //enlarge column to fill full width when resizing
+
+const bool fillGapAfterColumns = true; //draw rows/column label to fill full window width; may become an instance variable some time?
+}
+
+//----------------------------------------------------------------------------------------------------------------
+const wxEventType zen::EVENT_GRID_MOUSE_LEFT_DOUBLE = wxNewEventType();
+const wxEventType zen::EVENT_GRID_MOUSE_LEFT_DOWN = wxNewEventType();
+const wxEventType zen::EVENT_GRID_MOUSE_LEFT_UP = wxNewEventType();
+const wxEventType zen::EVENT_GRID_MOUSE_RIGHT_DOWN = wxNewEventType();
+const wxEventType zen::EVENT_GRID_MOUSE_RIGHT_UP = wxNewEventType();
+const wxEventType zen::EVENT_GRID_SELECT_RANGE = wxNewEventType();
+const wxEventType zen::EVENT_GRID_COL_LABEL_MOUSE_LEFT = wxNewEventType();
+const wxEventType zen::EVENT_GRID_COL_LABEL_MOUSE_RIGHT = wxNewEventType();
+const wxEventType zen::EVENT_GRID_COL_RESIZE = wxNewEventType();
+//----------------------------------------------------------------------------------------------------------------
+
+void GridData::renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row, bool enabled, bool selected)
+{
+ drawCellBackground(dc, rect, enabled, selected, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
+}
+
+
+void GridData::renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover)
+{
+ wxRect rectTmp = drawCellBorder(dc, rect);
+
+ rectTmp.x += COLUMN_GAP_LEFT;
+ rectTmp.width -= COLUMN_GAP_LEFT;
+ drawCellText(dc, rectTmp, getValue(row, colType));
+}
+
+
+int GridData::getBestSize(wxDC& dc, size_t row, ColumnType colType)
+{
+ return dc.GetTextExtent(getValue(row, colType)).GetWidth() + 2 * COLUMN_GAP_LEFT + 1; //gap on left and right side + border
+}
+
+
+wxRect GridData::drawCellBorder(wxDC& dc, const wxRect& rect) //returns remaining rectangle
+{
+ wxDCPenChanger dummy2(dc, getColorGridLine());
+ dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight());
+ dc.DrawLine(rect.GetBottomRight(), rect.GetTopRight() + wxPoint(0, -1));
+
+ return wxRect(rect.GetTopLeft(), wxSize(rect.width - 1, rect.height - 1));
+}
+
+
+void GridData::drawCellBackground(wxDC& dc, const wxRect& rect, bool enabled, bool selected, const wxColor& backgroundColor)
+{
+ if (enabled)
+ {
+ if (selected)
+ dc.GradientFillLinear(rect, Grid::getColorSelectionGradientFrom(), Grid::getColorSelectionGradientTo(), wxEAST);
+ else
+ clearArea(dc, rect, backgroundColor);
+ }
+ else
+ clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE));
+}
+
+
+wxSize GridData::drawCellText(wxDC& dc, const wxRect& rect, const std::wstring& text, int alignment)
+{
+ /*
+ performance notes (Windows):
+ - wxDC::GetTextExtent() is by far the most expensive call (20x more expensive than wxDC::DrawText())
+ - wxDC::DrawLabel() is inefficiently implemented; internally calls: wxDC::GetMultiLineTextExtent(), wxDC::GetTextExtent(), wxDC::DrawText()
+ - wxDC::GetMultiLineTextExtent() calls wxDC::GetTextExtent()
+ - wxDC::DrawText also calls wxDC::GetTextExtent()!!
+ => wxDC::DrawLabel() boils down to 3(!) calls to wxDC::GetTextExtent()!!!
+ - wxDC::DrawLabel results in GetTextExtent() call even for empty strings!!!
+ => skip the wxDC::DrawLabel() cruft and directly call wxDC::DrawText!
+ */
+
+ //truncate large texts and add ellipsis
+ assert(!contains(text, L"\n"));
+ const wchar_t ELLIPSIS = L'\u2026'; //"..."
+
+ std::wstring textTrunc = text;
+ wxSize extentTrunc = dc.GetTextExtent(textTrunc);
+ if (extentTrunc.GetWidth() > rect.width)
+ {
+ //unlike Windows 7 Explorer, we truncate UTF-16 correctly: e.g. CJK-Ideogramm encodes to TWO wchar_t: utfCvrtTo<std::wstring>("\xf0\xa4\xbd\x9c");
+ size_t low = 0; //number of unicode chars!
+ size_t high = unicodeLength(text); //
+ if (high > 1)
+ for (;;)
+ {
+ const size_t middle = (low + high) / 2; //=> never 0 when "high - low > 1"
+ if (high - low <= 1)
+ {
+ if (low == 0)
+ {
+ textTrunc = ELLIPSIS;
+ extentTrunc = dc.GetTextExtent(ELLIPSIS);
+ }
+ break;
+ }
+
+ const std::wstring& candidate = std::wstring(strBegin(text), findUnicodePos(text, middle)) + ELLIPSIS;
+ const wxSize extentCand = dc.GetTextExtent(candidate); //perf: most expensive call of this routine!
+
+ if (extentCand.GetWidth() <= rect.width)
+ {
+ low = middle;
+ textTrunc = candidate;
+ extentTrunc = extentCand;
+ }
+ else
+ high = middle;
+ }
+ }
+
+ wxPoint pt = rect.GetTopLeft();
+ if (alignment & wxALIGN_RIGHT) //note: wxALIGN_LEFT == 0!
+ pt.x += rect.width - extentTrunc.GetWidth();
+ else if (alignment & wxALIGN_CENTER_HORIZONTAL)
+ pt.x += (rect.width - extentTrunc.GetWidth()) / 2;
+
+ if (alignment & wxALIGN_BOTTOM) //note: wxALIGN_TOP == 0!
+ pt.y += rect.height - extentTrunc.GetHeight();
+ else if (alignment & wxALIGN_CENTER_VERTICAL)
+ pt.y += (rect.height - extentTrunc.GetHeight()) / 2;
+
+ RecursiveDcClipper clip(dc, rect);
+ dc.DrawText(textTrunc, pt);
+ return extentTrunc;
+}
+
+
+void GridData::renderColumnLabel(Grid& grid, wxDC& dc, const wxRect& rect, ColumnType colType, bool highlighted)
+{
+ wxRect rectTmp = drawColumnLabelBorder(dc, rect);
+ drawColumnLabelBackground(dc, rectTmp, highlighted);
+
+ rectTmp.x += COLUMN_GAP_LEFT;
+ rectTmp.width -= COLUMN_GAP_LEFT;
+ drawColumnLabelText(dc, rectTmp, getColumnLabel(colType));
+}
+
+
+wxRect GridData::drawColumnLabelBorder(wxDC& dc, const wxRect& rect) //returns remaining rectangle
+{
+ //draw white line
+ {
+ wxDCPenChanger dummy(dc, *wxWHITE_PEN);
+ dc.DrawLine(rect.GetTopLeft(), rect.GetBottomLeft());
+ }
+
+ //draw border (with gradient)
+ {
+ wxDCPenChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_3DSHADOW));
+ dc.GradientFillLinear(wxRect(rect.GetTopRight(), rect.GetBottomRight()), getColorLabelGradientFrom(), dc.GetPen().GetColour(), wxSOUTH);
+ dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight() + wxPoint(1, 0));
+ }
+
+ return wxRect(rect.x + 1, rect.y, rect.width - 2, rect.height - 1); //we really don't like wxRect::Deflate, do we?
+}
+
+
+void GridData::drawColumnLabelBackground(wxDC& dc, const wxRect& rect, bool highlighted)
+{
+ if (highlighted)
+ dc.GradientFillLinear(rect, getColorLabelGradientFocusFrom(), getColorLabelGradientFocusTo(), wxSOUTH);
+ else //regular background gradient
+ dc.GradientFillLinear(rect, getColorLabelGradientFrom(), getColorLabelGradientTo(), wxSOUTH); //clear overlapping cells
+}
+
+
+void GridData::drawColumnLabelText(wxDC& dc, const wxRect& rect, const std::wstring& text)
+{
+ wxDCTextColourChanger dummy(dc, getColorLabelText()); //accessibility: always set both foreground AND background colors!
+ drawCellText(dc, rect, text, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
+}
+
+//----------------------------------------------------------------------------------------------------------------
+/*
+ SubWindow
+ /|\
+ |
+ -----------------------------------
+ | | | |
+CornerWin RowLabelWin ColLabelWin MainWin
+
+*/
+class Grid::SubWindow : public wxWindow
+{
+public:
+ SubWindow(Grid& parent) :
+ wxWindow(&parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxWANTS_CHARS | wxBORDER_NONE, wxPanelNameStr),
+ parent_(parent)
+ {
+ Connect(wxEVT_PAINT, wxPaintEventHandler(SubWindow::onPaintEvent), nullptr, this);
+ Connect(wxEVT_SIZE, wxSizeEventHandler (SubWindow::onSizeEvent), nullptr, this);
+ //http://wiki.wxwidgets.org/Flicker-Free_Drawing
+ Connect(wxEVT_ERASE_BACKGROUND, wxEraseEventHandler(SubWindow::onEraseBackGround), nullptr, this);
+
+ //SetDoubleBuffered(true); slow as hell!
+
+ SetBackgroundStyle(wxBG_STYLE_PAINT);
+
+ Connect(wxEVT_SET_FOCUS, wxFocusEventHandler(SubWindow::onFocus), nullptr, this);
+ Connect(wxEVT_KILL_FOCUS, wxFocusEventHandler(SubWindow::onFocus), nullptr, this);
+ Connect(wxEVT_CHILD_FOCUS, wxEventHandler(SubWindow::onChildFocus), nullptr, this);
+
+ Connect(wxEVT_LEFT_DOWN, wxMouseEventHandler(SubWindow::onMouseLeftDown ), nullptr, this);
+ Connect(wxEVT_LEFT_UP, wxMouseEventHandler(SubWindow::onMouseLeftUp ), nullptr, this);
+ Connect(wxEVT_LEFT_DCLICK, wxMouseEventHandler(SubWindow::onMouseLeftDouble), nullptr, this);
+ Connect(wxEVT_RIGHT_DOWN, wxMouseEventHandler(SubWindow::onMouseRightDown ), nullptr, this);
+ Connect(wxEVT_RIGHT_UP, wxMouseEventHandler(SubWindow::onMouseRightUp ), nullptr, this);
+ Connect(wxEVT_MOTION, wxMouseEventHandler(SubWindow::onMouseMovement ), nullptr, this);
+ Connect(wxEVT_LEAVE_WINDOW, wxMouseEventHandler(SubWindow::onLeaveWindow ), nullptr, this);
+ Connect(wxEVT_MOUSEWHEEL, wxMouseEventHandler(SubWindow::onMouseWheel ), nullptr, this);
+ Connect(wxEVT_MOUSE_CAPTURE_LOST, wxMouseCaptureLostEventHandler(SubWindow::onMouseCaptureLost), nullptr, this);
+
+ Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(SubWindow::onKeyDown), nullptr, this);
+
+ assert(GetClientAreaOrigin() == wxPoint()); //generally assumed when dealing with coordinates below
+ }
+ Grid& refParent() { return parent_; }
+ const Grid& refParent() const { return parent_; }
+
+ template <class T>
+ bool sendEventNow(T&& event) //take both "rvalue + lvalues", return "true" if a suitable event handler function was found and executed, and the function did not call wxEvent::Skip.
+ {
+ if (wxEvtHandler* evtHandler = parent_.GetEventHandler())
+ return evtHandler->ProcessEvent(event);
+ return false;
+ }
+
+protected:
+ void setToolTip(const std::wstring& text) //proper fix for wxWindow
+ {
+ wxToolTip* tt = GetToolTip();
+
+ const wxString oldText = tt ? tt->GetTip() : wxString();
+ if (text != oldText)
+ {
+ if (text.empty())
+ SetToolTip(nullptr); //wxGTK doesn't allow wxToolTip with empty text!
+ else
+ {
+ //wxWidgets bug: tooltip multiline property is defined by first tooltip text containing newlines or not (same is true for maximum width)
+ if (!tt)
+ SetToolTip(new wxToolTip(L"a b\n\
+ a b")); //ugly, but working (on Windows)
+ tt = GetToolTip(); //should be bound by now
+ assert(tt);
+ if (tt)
+ tt->SetTip(text);
+ }
+ }
+ }
+
+private:
+ virtual void render(wxDC& dc, const wxRect& rect) = 0;
+
+ virtual void onFocus(wxFocusEvent& event) { event.Skip(); }
+ virtual void onChildFocus(wxEvent& event) {} //wxGTK::wxScrolledWindow automatically scrolls to child window when child gets focus -> prevent!
+
+ virtual void onMouseLeftDown (wxMouseEvent& event) { event.Skip(); }
+ virtual void onMouseLeftUp (wxMouseEvent& event) { event.Skip(); }
+ virtual void onMouseLeftDouble(wxMouseEvent& event) { event.Skip(); }
+ virtual void onMouseRightDown (wxMouseEvent& event) { event.Skip(); }
+ virtual void onMouseRightUp (wxMouseEvent& event) { event.Skip(); }
+ virtual void onMouseMovement (wxMouseEvent& event) { event.Skip(); }
+ virtual void onLeaveWindow (wxMouseEvent& event) { event.Skip(); }
+ virtual void onMouseCaptureLost(wxMouseCaptureLostEvent& event) { event.Skip(); }
+
+ void onKeyDown(wxKeyEvent& event)
+ {
+ if (!sendEventNow(event)) //let parent collect all key events
+ event.Skip();
+ }
+
+ void onMouseWheel(wxMouseEvent& event)
+ {
+ /*
+ MSDN, WM_MOUSEWHEEL: "Sent to the focus window when the mouse wheel is rotated.
+ The DefWindowProc function propagates the message to the window's parent.
+ There should be no internal forwarding of the message, since DefWindowProc propagates
+ it up the parent chain until it finds a window that processes it."
+
+ On OS X there is no such propagation! => we need a redirection (the same wxGrid implements)
+ */
+
+ //new wxWidgets 3.0 screw-up for GTK2: wxScrollHelperEvtHandler::ProcessEvent() ignores wxEVT_MOUSEWHEEL events
+ //thereby breaking the scenario of redirection to parent we need here (but also breaking their very own wxGrid sample)
+ //=> call wxScrolledWindow mouse wheel handler directly
+ parent_.HandleOnMouseWheel(event);
+
+ //if (!sendEventNow(event))
+ // event.Skip();
+ }
+
+ void onPaintEvent(wxPaintEvent& event)
+ {
+ //wxAutoBufferedPaintDC dc(this); -> this one happily fucks up for RTL layout by not drawing the first column (x = 0)!
+ BufferedPaintDC dc(*this, doubleBuffer_);
+
+ assert(GetSize() == GetClientSize());
+
+ const wxRegion& updateReg = GetUpdateRegion();
+ for (wxRegionIterator it = updateReg; it; ++it)
+ render(dc, it.GetRect());
+ }
+
+ void onSizeEvent(wxSizeEvent& event)
+ {
+ Refresh();
+ event.Skip();
+ }
+
+ void onEraseBackGround(wxEraseEvent& event) {}
+
+ Grid& parent_;
+ Opt<wxBitmap> doubleBuffer_;
+};
+
+//----------------------------------------------------------------------------------------------------------------
+//----------------------------------------------------------------------------------------------------------------
+
+class Grid::CornerWin : public SubWindow
+{
+public:
+ CornerWin(Grid& parent) : SubWindow(parent) {}
+
+private:
+ bool AcceptsFocus() const override { return false; }
+
+ void render(wxDC& dc, const wxRect& rect) override
+ {
+ const wxRect& clientRect = GetClientRect();
+
+ dc.GradientFillLinear(clientRect, getColorLabelGradientFrom(), getColorLabelGradientTo(), wxSOUTH);
+
+ dc.SetPen(wxSystemSettings::GetColour(wxSYS_COLOUR_3DSHADOW));
+
+ {
+ wxDCPenChanger dummy(dc, getColorLabelGradientFrom());
+ dc.DrawLine(clientRect.GetTopLeft(), clientRect.GetTopRight());
+ }
+
+ dc.GradientFillLinear(wxRect(clientRect.GetBottomLeft (), clientRect.GetTopLeft ()), getColorLabelGradientFrom(), dc.GetPen().GetColour(), wxSOUTH);
+ dc.GradientFillLinear(wxRect(clientRect.GetBottomRight(), clientRect.GetTopRight()), getColorLabelGradientFrom(), dc.GetPen().GetColour(), wxSOUTH);
+
+ dc.DrawLine(clientRect.GetBottomLeft(), clientRect.GetBottomRight());
+
+ wxRect rectShrinked = clientRect;
+ rectShrinked.Deflate(1);
+ dc.SetPen(*wxWHITE_PEN);
+
+ //dc.DrawLine(clientRect.GetTopLeft(), clientRect.GetTopRight() + wxPoint(1, 0));
+ dc.DrawLine(rectShrinked.GetTopLeft(), rectShrinked.GetBottomLeft() + wxPoint(0, 1));
+ }
+};
+
+//----------------------------------------------------------------------------------------------------------------
+//----------------------------------------------------------------------------------------------------------------
+
+class Grid::RowLabelWin : public SubWindow
+{
+public:
+ RowLabelWin(Grid& parent) :
+ SubWindow(parent),
+ rowHeight_(parent.GetCharHeight() + 2 + 1) {} //default height; don't call any functions on "parent" other than those from wxWindow during construction!
+ //2 for some more space, 1 for bottom border (gives 15 + 2 + 1 on Windows, 17 + 2 + 1 on Ubuntu)
+
+ int getBestWidth(ptrdiff_t rowFrom, ptrdiff_t rowTo)
+ {
+ wxClientDC dc(this);
+
+ wxFont labelFont = GetFont();
+ //labelFont.SetWeight(wxFONTWEIGHT_BOLD);
+ dc.SetFont(labelFont); //harmonize with RowLabelWin::render()!
+
+ int bestWidth = 0;
+ for (ptrdiff_t i = rowFrom; i <= rowTo; ++i)
+ bestWidth = std::max(bestWidth, dc.GetTextExtent(formatRow(i)).GetWidth() + 2 * ROW_LABEL_BORDER);
+ return bestWidth;
+ }
+
+ size_t getLogicalHeight() const { return refParent().getRowCount() * rowHeight_; }
+
+ ptrdiff_t getRowAtPos(ptrdiff_t posY) const //returns < 0 on invalid input, else row number within: [0, rowCount]; rowCount if out of range
+ {
+ if (posY >= 0 && rowHeight_ > 0)
+ {
+ const size_t row = posY / rowHeight_;
+ return std::min(row, refParent().getRowCount());
+ }
+ return -1;
+ }
+
+ int getRowHeight() const { return rowHeight_; } //guarantees to return size >= 1 !
+ void setRowHeight(int height) { assert(height > 0); rowHeight_ = std::max(1, height); }
+
+ wxRect getRowLabelArea(size_t row) const //returns empty rect if row not found
+ {
+ assert(GetClientAreaOrigin() == wxPoint());
+ if (row < refParent().getRowCount())
+ return wxRect(wxPoint(0, rowHeight_ * row),
+ wxSize(GetClientSize().GetWidth(), rowHeight_));
+ return wxRect();
+ }
+
+ std::pair<ptrdiff_t, ptrdiff_t> getRowsOnClient(const wxRect& clientRect) const //returns range [begin, end)
+ {
+ const int yFrom = refParent().CalcUnscrolledPosition(clientRect.GetTopLeft ()).y;
+ const int yTo = refParent().CalcUnscrolledPosition(clientRect.GetBottomRight()).y;
+
+ return std::make_pair(std::max(yFrom / rowHeight_, 0),
+ std::min<ptrdiff_t>((yTo / rowHeight_) + 1, refParent().getRowCount()));
+ }
+
+private:
+ static std::wstring formatRow(size_t row) { return toGuiString(row + 1); } //convert number to std::wstring including thousands separator
+
+ bool AcceptsFocus() const override { return false; }
+
+ void render(wxDC& dc, const wxRect& rect) override
+ {
+ /*
+ IsEnabled() vs IsThisEnabled() since wxWidgets 2.9.5:
+
+ void wxWindowBase::NotifyWindowOnEnableChange(), called from bool wxWindowBase::Enable(), fails to refresh
+ child elements when disabling a IsTopLevel() dialog, e.g. when showing a modal dialog.
+ The unfortunate effect on XP for using IsEnabled() when rendering the grid is that the user can move the modal dialog
+ and *draw* with it on the background while the grid refreshes as disabled incrementally!
+
+ => Don't use IsEnabled() since it considers the top level window. The brittle wxWidgets implementation is right in their intention,
+ but wrong when not refreshing child-windows: the control designer decides how his control should be rendered!
+
+ => IsThisEnabled() OTOH is too shallow and does not consider parent windows which are not top level.
+
+ The perfect solution would be a bool ShouldBeDrawnActive() { return "IsEnabled() but ignore effects of showing a modal dialog"; }
+
+ However "IsThisEnabled()" is good enough (same like the old IsEnabled() on wxWidgets 2.8.12) and it avoids this pathetic behavior on XP.
+ (Similar problem on Win 7: e.g. directly click sync button without comparing first)
+ */
+ if (IsThisEnabled())
+ clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
+ else
+ clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE));
+
+ wxFont labelFont = GetFont();
+ //labelFont.SetWeight(wxFONTWEIGHT_BOLD);
+ dc.SetFont(labelFont); //harmonize with RowLabelWin::getBestWidth()!
+
+ auto rowRange = getRowsOnClient(rect); //returns range [begin, end)
+ for (auto row = rowRange.first; row < rowRange.second; ++row)
+ {
+ wxRect singleLabelArea = getRowLabelArea(row); //returns empty rect if row not found
+ if (singleLabelArea.height > 0)
+ {
+ singleLabelArea.y = refParent().CalcScrolledPosition(singleLabelArea.GetTopLeft()).y;
+ drawRowLabel(dc, singleLabelArea, row);
+ }
+ }
+ }
+
+ void drawRowLabel(wxDC& dc, const wxRect& rect, size_t row)
+ {
+ //clearArea(dc, rect, getColorRowLabel());
+ dc.GradientFillLinear(rect, getColorLabelGradientFrom(), getColorLabelGradientTo(), wxEAST); //clear overlapping cells
+ wxDCTextColourChanger dummy3(dc, getColorLabelText()); //accessibility: always set both foreground AND background colors!
+
+ //label text
+ wxRect textRect = rect;
+ textRect.Deflate(1);
+
+ GridData::drawCellText(dc, textRect, formatRow(row), wxALIGN_CENTRE);
+
+ //border lines
+ {
+ wxDCPenChanger dummy(dc, *wxWHITE_PEN);
+ dc.DrawLine(rect.GetTopLeft(), rect.GetTopRight());
+ }
+ {
+ wxDCPenChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_3DSHADOW));
+ dc.DrawLine(rect.GetTopLeft(), rect.GetBottomLeft());
+ dc.DrawLine(rect.GetBottomLeft(), rect.GetBottomRight());
+ dc.DrawLine(rect.GetBottomRight(), rect.GetTopRight() + wxPoint(0, -1));
+ }
+ }
+
+ void onMouseLeftDown(wxMouseEvent& event) override { refParent().redirectRowLabelEvent(event); }
+ void onMouseMovement(wxMouseEvent& event) override { refParent().redirectRowLabelEvent(event); }
+ void onMouseLeftUp (wxMouseEvent& event) override { refParent().redirectRowLabelEvent(event); }
+
+ int rowHeight_;
+};
+
+
+namespace
+{
+class ColumnResizing
+{
+public:
+ ColumnResizing(wxWindow& wnd, size_t col, int startWidth, int clientPosX) :
+ wnd_(wnd), col_(col), startWidth_(startWidth), clientPosX_(clientPosX)
+ {
+ wnd_.CaptureMouse();
+ }
+ ~ColumnResizing()
+ {
+ if (wnd_.HasCapture())
+ wnd_.ReleaseMouse();
+ }
+
+ size_t getColumn () const { return col_; }
+ int getStartWidth () const { return startWidth_; }
+ int getStartPosX () const { return clientPosX_; }
+
+private:
+ wxWindow& wnd_;
+ const size_t col_;
+ const int startWidth_;
+ const int clientPosX_;
+};
+
+
+class ColumnMove
+{
+public:
+ ColumnMove(wxWindow& wnd, size_t colFrom, int clientPosX) :
+ wnd_(wnd),
+ colFrom_(colFrom),
+ colTo_(colFrom),
+ clientPosX_(clientPosX) { wnd_.CaptureMouse(); }
+ ~ColumnMove() { if (wnd_.HasCapture()) wnd_.ReleaseMouse(); }
+
+ size_t getColumnFrom() const { return colFrom_; }
+ size_t& refColumnTo() { return colTo_; }
+ int getStartPosX () const { return clientPosX_; }
+
+ bool isRealMove() const { return !singleClick_; }
+ void setRealMove() { singleClick_ = false; }
+
+private:
+ wxWindow& wnd_;
+ const size_t colFrom_;
+ size_t colTo_;
+ const int clientPosX_;
+ bool singleClick_ = true;
+};
+}
+
+//----------------------------------------------------------------------------------------------------------------
+
+class Grid::ColLabelWin : public SubWindow
+{
+public:
+ ColLabelWin(Grid& parent) : SubWindow(parent) {}
+
+private:
+ bool AcceptsFocus() const override { return false; }
+
+ void render(wxDC& dc, const wxRect& rect) override
+ {
+ if (IsThisEnabled())
+ clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
+ else
+ clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE));
+
+ //coordinate with "colLabelHeight" in Grid constructor:
+ wxFont labelFont = GetFont();
+ labelFont.SetWeight(wxFONTWEIGHT_BOLD);
+ dc.SetFont(labelFont);
+
+ wxDCTextColourChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels
+
+ const int colLabelHeight = refParent().colLabelHeight_;
+
+ wxPoint labelAreaTL(refParent().CalcScrolledPosition(wxPoint(0, 0)).x, 0); //client coordinates
+
+ std::vector<ColumnWidth> absWidths = refParent().getColWidths(); //resolve stretched widths
+ for (auto it = absWidths.begin(); it != absWidths.end(); ++it)
+ {
+ const size_t col = it - absWidths.begin();
+ const int width = it->width_; //don't use unsigned for calculations!
+
+ if (labelAreaTL.x > rect.GetRight())
+ return; //done, rect is fully covered
+ if (labelAreaTL.x + width > rect.x)
+ drawColumnLabel(dc, wxRect(labelAreaTL, wxSize(width, colLabelHeight)), col, it->type_);
+ labelAreaTL.x += width;
+ }
+ if (labelAreaTL.x > rect.GetRight())
+ return; //done, rect is fully covered
+
+ //fill gap after columns and cover full width
+ if (fillGapAfterColumns)
+ {
+ int totalWidth = 0;
+ for (const ColumnWidth& cw : absWidths)
+ totalWidth += cw.width_;
+ const int clientWidth = GetClientSize().GetWidth(); //need reliable, stable width in contrast to rect.width
+
+ if (totalWidth < clientWidth)
+ drawColumnLabel(dc, wxRect(labelAreaTL, wxSize(clientWidth - totalWidth, colLabelHeight)), absWidths.size(), ColumnType::NONE);
+ }
+ }
+
+ void drawColumnLabel(wxDC& dc, const wxRect& rect, size_t col, ColumnType colType)
+ {
+ if (auto dataView = refParent().getDataProvider())
+ {
+ const bool isHighlighted = activeResizing_ ? col == activeResizing_ ->getColumn () : //highlight_ column on mouse-over
+ activeClickOrMove_ ? col == activeClickOrMove_->getColumnFrom() :
+ highlightCol_ ? col == *highlightCol_ :
+ false;
+
+ RecursiveDcClipper clip(dc, rect);
+ dataView->renderColumnLabel(refParent(), dc, rect, colType, isHighlighted);
+
+ //draw move target location
+ if (refParent().allowColumnMove_)
+ if (activeClickOrMove_ && activeClickOrMove_->isRealMove())
+ {
+ if (col + 1 == activeClickOrMove_->refColumnTo()) //handle pos 1, 2, .. up to "at end" position
+ dc.GradientFillLinear(wxRect(rect.GetTopRight(), rect.GetBottomRight() + wxPoint(-2, 0)), getColorLabelGradientFrom(), *wxBLUE, wxSOUTH);
+ else if (col == activeClickOrMove_->refColumnTo() && col == 0) //pos 0
+ dc.GradientFillLinear(wxRect(rect.GetTopLeft(), rect.GetBottomLeft() + wxPoint(2, 0)), getColorLabelGradientFrom(), *wxBLUE, wxSOUTH);
+ }
+ }
+ }
+
+ void onMouseLeftDown(wxMouseEvent& event) override
+ {
+ if (FindFocus() != &refParent().getMainWin())
+ refParent().getMainWin().SetFocus();
+
+ activeResizing_.reset();
+ activeClickOrMove_.reset();
+
+ if (Opt<ColAction> action = refParent().clientPosToColumnAction(event.GetPosition()))
+ {
+ if (action->wantResize)
+ {
+ if (!event.LeftDClick()) //double-clicks never seem to arrive here; why is this checked at all???
+ if (Opt<int> colWidth = refParent().getColWidth(action->col))
+ activeResizing_ = std::make_unique<ColumnResizing>(*this, action->col, *colWidth, event.GetPosition().x);
+ }
+ else //a move or single click
+ activeClickOrMove_ = std::make_unique<ColumnMove>(*this, action->col, event.GetPosition().x);
+ }
+ event.Skip();
+ }
+
+ void onMouseLeftUp(wxMouseEvent& event) override
+ {
+ activeResizing_.reset(); //nothing else to do, actual work done by onMouseMovement()
+
+ if (activeClickOrMove_)
+ {
+ if (activeClickOrMove_->isRealMove())
+ {
+ if (refParent().allowColumnMove_)
+ {
+ const size_t colFrom = activeClickOrMove_->getColumnFrom();
+ size_t colTo = activeClickOrMove_->refColumnTo();
+
+ if (colTo > colFrom) //simulate "colFrom" deletion
+ --colTo;
+
+ refParent().moveColumn(colFrom, colTo);
+ }
+ }
+ else //notify single label click
+ {
+ if (const Opt<ColumnType> colType = refParent().colToType(activeClickOrMove_->getColumnFrom()))
+ sendEventNow(GridLabelClickEvent(EVENT_GRID_COL_LABEL_MOUSE_LEFT, event, *colType));
+ }
+ activeClickOrMove_.reset();
+ }
+
+ refParent().updateWindowSizes(); //looks strange if done during onMouseMovement()
+ refParent().Refresh();
+ event.Skip();
+ }
+
+ void onMouseCaptureLost(wxMouseCaptureLostEvent& event) override
+ {
+ activeResizing_.reset();
+ activeClickOrMove_.reset();
+ Refresh();
+ //event.Skip(); -> we DID handle it!
+ }
+
+ void onMouseLeftDouble(wxMouseEvent& event) override
+ {
+ if (Opt<ColAction> action = refParent().clientPosToColumnAction(event.GetPosition()))
+ if (action->wantResize)
+ {
+ //auto-size visible range on double-click
+ const int bestWidth = refParent().getBestColumnSize(action->col); //return -1 on error
+ if (bestWidth >= 0)
+ {
+ refParent().setColumnWidth(bestWidth, action->col, ALLOW_GRID_EVENT);
+ refParent().Refresh(); //refresh main grid as well!
+ }
+ }
+ event.Skip();
+ }
+
+ void onMouseMovement(wxMouseEvent& event) override
+ {
+ if (activeResizing_)
+ {
+ const auto col = activeResizing_->getColumn();
+ const int newWidth = activeResizing_->getStartWidth() + event.GetPosition().x - activeResizing_->getStartPosX();
+
+ //set width tentatively
+ refParent().setColumnWidth(newWidth, col, ALLOW_GRID_EVENT);
+
+ //check if there's a small gap after last column, if yes, fill it
+ const int gapWidth = GetClientSize().GetWidth() - refParent().getColWidthsSum(GetClientSize().GetWidth());
+ if (std::abs(gapWidth) < COLUMN_FILL_GAP_TOLERANCE)
+ refParent().setColumnWidth(newWidth + gapWidth, col, ALLOW_GRID_EVENT);
+
+ refParent().Refresh(); //refresh columns on main grid as well!
+ }
+ else if (activeClickOrMove_)
+ {
+ const int clientPosX = event.GetPosition().x;
+ if (std::abs(clientPosX - activeClickOrMove_->getStartPosX()) > COLUMN_MOVE_DELAY) //real move (not a single click)
+ {
+ activeClickOrMove_->setRealMove();
+
+ const ptrdiff_t col = refParent().clientPosToMoveTargetColumn(event.GetPosition());
+ if (col >= 0)
+ activeClickOrMove_->refColumnTo() = col;
+ }
+ }
+ else
+ {
+ if (const Opt<ColAction> action = refParent().clientPosToColumnAction(event.GetPosition()))
+ {
+ highlightCol_ = action->col;
+
+ if (action->wantResize)
+ SetCursor(wxCURSOR_SIZEWE); //set window-local only! :)
+ else
+ SetCursor(*wxSTANDARD_CURSOR);
+ }
+ else
+ {
+ highlightCol_ = NoValue();
+ SetCursor(*wxSTANDARD_CURSOR);
+ }
+ }
+
+ const std::wstring toolTip = [&]
+ {
+ const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition());
+ const ColumnType colType = refParent().getColumnAtPos(absPos.x).colType; //returns ColumnType::NONE if no column at x position!
+ if (colType != ColumnType::NONE)
+ if (auto prov = refParent().getDataProvider())
+ return prov->getToolTip(colType);
+ return std::wstring();
+ }();
+ setToolTip(toolTip);
+
+ Refresh();
+ event.Skip();
+ }
+
+ void onLeaveWindow(wxMouseEvent& event) override
+ {
+ highlightCol_ = NoValue(); //wxEVT_LEAVE_WINDOW does not respect mouse capture! -> however highlight_ is drawn unconditionally during move/resize!
+ Refresh();
+ event.Skip();
+ }
+
+ void onMouseRightDown(wxMouseEvent& event) override
+ {
+ if (const Opt<ColAction> action = refParent().clientPosToColumnAction(event.GetPosition()))
+ {
+ if (const Opt<ColumnType> colType = refParent().colToType(action->col))
+ sendEventNow(GridLabelClickEvent(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, event, *colType)); //notify right click
+ else assert(false);
+ }
+ else
+ //notify right click (on free space after last column)
+ if (fillGapAfterColumns)
+ sendEventNow(GridLabelClickEvent(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, event, ColumnType::NONE));
+
+ event.Skip();
+ }
+
+ std::unique_ptr<ColumnResizing> activeResizing_;
+ std::unique_ptr<ColumnMove> activeClickOrMove_;
+ Opt<size_t> highlightCol_; //column during mouse-over
+};
+
+//----------------------------------------------------------------------------------------------------------------
+namespace
+{
+const wxEventType EVENT_GRID_HAS_SCROLLED = wxNewEventType(); //internal to Grid::MainWin::ScrollWindow()
+}
+//----------------------------------------------------------------------------------------------------------------
+
+class Grid::MainWin : public SubWindow
+{
+public:
+ MainWin(Grid& parent,
+ RowLabelWin& rowLabelWin,
+ ColLabelWin& colLabelWin) : SubWindow(parent),
+ rowLabelWin_(rowLabelWin),
+ colLabelWin_(colLabelWin)
+ {
+ Connect(EVENT_GRID_HAS_SCROLLED, wxEventHandler(MainWin::onRequestWindowUpdate), nullptr, this);
+ }
+
+ ~MainWin() { assert(!gridUpdatePending_); }
+
+ size_t getCursor() const { return cursorRow_; }
+ size_t getAnchor() const { return selectionAnchor_; }
+
+ void setCursor(size_t newCursorRow, size_t newAnchorRow)
+ {
+ cursorRow_ = newCursorRow;
+ selectionAnchor_ = newAnchorRow;
+ activeSelection_.reset(); //e.g. user might search with F3 while holding down left mouse button
+ }
+
+private:
+ void render(wxDC& dc, const wxRect& rect) override
+ {
+ if (IsThisEnabled())
+ clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
+ else
+ clearArea(dc, rect, wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE));
+
+ dc.SetFont(GetFont()); //harmonize with Grid::getBestColumnSize()
+
+ wxDCTextColourChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels
+
+ std::vector<ColumnWidth> absWidths = refParent().getColWidths(); //resolve stretched widths
+ {
+ int totalRowWidth = 0;
+ for (const ColumnWidth& cw : absWidths)
+ totalRowWidth += cw.width_;
+
+ //fill gap after columns and cover full width
+ if (fillGapAfterColumns)
+ totalRowWidth = std::max(totalRowWidth, GetClientSize().GetWidth());
+
+ if (auto prov = refParent().getDataProvider())
+ {
+ RecursiveDcClipper dummy2(dc, rect); //do NOT draw background on cells outside of invalidated rect invalidating foreground text!
+
+ wxPoint cellAreaTL(refParent().CalcScrolledPosition(wxPoint(0, 0))); //client coordinates
+ const int rowHeight = rowLabelWin_.getRowHeight();
+ const auto rowRange = rowLabelWin_.getRowsOnClient(rect); //returns range [begin, end)
+
+ //draw background lines
+ for (auto row = rowRange.first; row < rowRange.second; ++row)
+ {
+ const wxRect rowRect(cellAreaTL + wxPoint(0, row * rowHeight), wxSize(totalRowWidth, rowHeight));
+ RecursiveDcClipper dummy3(dc, rowRect);
+ prov->renderRowBackgound(dc, rowRect, row, refParent().IsThisEnabled(), drawAsSelected(row));
+ }
+
+ //draw single cells, column by column
+ for (const ColumnWidth& cw : absWidths)
+ {
+ if (cellAreaTL.x > rect.GetRight())
+ return; //done
+
+ if (cellAreaTL.x + cw.width_ > rect.x)
+ for (auto row = rowRange.first; row < rowRange.second; ++row)
+ {
+ const wxRect cellRect(cellAreaTL.x, cellAreaTL.y + row * rowHeight, cw.width_, rowHeight);
+ RecursiveDcClipper dummy3(dc, cellRect);
+ prov->renderCell(dc, cellRect, row, cw.type_, refParent().IsThisEnabled(), drawAsSelected(row), getRowHoverToDraw(row));
+ }
+ cellAreaTL.x += cw.width_;
+ }
+ }
+ }
+ }
+
+ HoverArea getRowHoverToDraw(ptrdiff_t row) const
+ {
+ if (activeSelection_)
+ {
+ if (activeSelection_->getFirstClick().row_ == row)
+ return activeSelection_->getFirstClick().hoverArea_;
+ }
+ else if (highlight_.row == row)
+ return highlight_.rowHover;
+ return HoverArea::NONE;
+ }
+
+ bool drawAsSelected(size_t row) const
+ {
+ if (activeSelection_) //check if user is currently selecting with mouse
+ {
+ const size_t rowFrom = std::min(activeSelection_->getStartRow(), activeSelection_->getCurrentRow());
+ const size_t rowTo = std::max(activeSelection_->getStartRow(), activeSelection_->getCurrentRow());
+
+ if (rowFrom <= row && row <= rowTo)
+ return activeSelection_->isPositiveSelect(); //overwrite default
+ }
+ return refParent().isSelected(row);
+ }
+
+ void onMouseLeftDown (wxMouseEvent& event) override { onMouseDown(event); }
+ void onMouseLeftUp (wxMouseEvent& event) override { onMouseUp (event); }
+ void onMouseRightDown(wxMouseEvent& event) override { onMouseDown(event); }
+ void onMouseRightUp (wxMouseEvent& event) override { onMouseUp (event); }
+
+ void onMouseLeftDouble(wxMouseEvent& event) override
+ {
+ if (auto prov = refParent().getDataProvider())
+ {
+ const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition());
+ const ptrdiff_t row = rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range
+ const ColumnPosInfo cpi = refParent().getColumnAtPos(absPos.x); //returns ColumnType::NONE if no column at x position!
+ const HoverArea rowHover = prov->getRowMouseHover(row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth);
+ //client is interested in all double-clicks, even those outside of the grid!
+ sendEventNow(GridClickEvent(EVENT_GRID_MOUSE_LEFT_DOUBLE, event, row, rowHover));
+ }
+ event.Skip();
+ }
+
+ void onMouseDown(wxMouseEvent& event) //handle left and right mouse button clicks (almost) the same
+ {
+ if (wxWindow::FindFocus() != this) //doesn't seem to happen automatically for right mouse button
+ SetFocus();
+
+ if (auto prov = refParent().getDataProvider())
+ {
+ const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition());
+ const ptrdiff_t row = rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range
+ const ColumnPosInfo cpi = refParent().getColumnAtPos(absPos.x); //returns ColumnType::NONE if no column at x position!
+ const HoverArea rowHover = prov->getRowMouseHover(row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth);
+ //row < 0 possible!!! Pressing "Menu key" simulates Mouse Right Down + Up at position 0xffff/0xffff!
+
+ GridClickEvent mouseEvent(event.RightDown() ? EVENT_GRID_MOUSE_RIGHT_DOWN : EVENT_GRID_MOUSE_LEFT_DOWN, event, row, rowHover);
+
+ if (row >= 0)
+ if (!event.RightDown() || !refParent().isSelected(row)) //do NOT start a new selection if user right-clicks on a selected area!
+ {
+ if (event.ControlDown())
+ activeSelection_ = std::make_unique<MouseSelection>(*this, row, !refParent().isSelected(row), mouseEvent);
+ else if (event.ShiftDown())
+ {
+ activeSelection_ = std::make_unique<MouseSelection>(*this, selectionAnchor_, true, mouseEvent);
+ refParent().clearSelection(ALLOW_GRID_EVENT);
+ }
+ else
+ {
+ activeSelection_ = std::make_unique<MouseSelection>(*this, row, true, mouseEvent);
+ refParent().clearSelection(ALLOW_GRID_EVENT);
+ }
+ }
+ //notify event *after* potential "clearSelection(true)" above: a client should first receive a GridRangeSelectEvent for clearing the grid, if necessary,
+ //then GridClickEvent and the associated GridRangeSelectEvent one after the other
+ sendEventNow(mouseEvent);
+
+ Refresh();
+ }
+ event.Skip(); //allow changing focus
+ }
+
+ void onMouseUp(wxMouseEvent& event)
+ {
+ if (activeSelection_)
+ {
+ const size_t rowCount = refParent().getRowCount();
+ if (rowCount > 0)
+ {
+ if (activeSelection_->getCurrentRow() < rowCount)
+ {
+ cursorRow_ = activeSelection_->getCurrentRow();
+ selectionAnchor_ = activeSelection_->getStartRow(); //allowed to be "out of range"
+ }
+ else if (activeSelection_->getStartRow() < rowCount) //don't change cursor if "to" and "from" are out of range
+ {
+ cursorRow_ = rowCount - 1;
+ selectionAnchor_ = activeSelection_->getStartRow(); //allowed to be "out of range"
+ }
+ else //total selection "out of range"
+ selectionAnchor_ = cursorRow_;
+ }
+ //slight deviation from Explorer: change cursor while dragging mouse! -> unify behavior with shift + direction keys
+
+ refParent().selectRangeAndNotify(activeSelection_->getStartRow (), //from
+ activeSelection_->getCurrentRow(), //to
+ activeSelection_->isPositiveSelect(),
+ &activeSelection_->getFirstClick());
+ activeSelection_.reset();
+ }
+
+ if (auto prov = refParent().getDataProvider())
+ {
+ //this one may point to row which is not in visible area!
+ const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition());
+ const ptrdiff_t row = rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range
+ const ColumnPosInfo cpi = refParent().getColumnAtPos(absPos.x); //returns ColumnType::NONE if no column at x position!
+ const HoverArea rowHover = prov->getRowMouseHover(row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth);
+ //notify click event after the range selection! e.g. this makes sure the selection is applied before showing a context menu
+ sendEventNow(GridClickEvent(event.RightUp() ? EVENT_GRID_MOUSE_RIGHT_UP : EVENT_GRID_MOUSE_LEFT_UP, event, row, rowHover));
+ }
+
+ //update highlight_ and tooltip: on OS X no mouse movement event is generated after a mouse button click (unlike on Windows)
+ event.SetPosition(ScreenToClient(wxGetMousePosition())); //mouse position may have changed within above callbacks (e.g. context menu was shown)!
+ onMouseMovement(event);
+
+ Refresh();
+ event.Skip(); //allow changing focus
+ }
+
+ void onMouseCaptureLost(wxMouseCaptureLostEvent& event) override
+ {
+ activeSelection_.reset();
+ highlight_.row = -1;
+ Refresh();
+ //event.Skip(); -> we DID handle it!
+ }
+
+ void onMouseMovement(wxMouseEvent& event) override
+ {
+ if (auto prov = refParent().getDataProvider())
+ {
+ const ptrdiff_t rowCount = refParent().getRowCount();
+ const wxPoint absPos = refParent().CalcUnscrolledPosition(event.GetPosition());
+ const ptrdiff_t row = rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range
+ const ColumnPosInfo cpi = refParent().getColumnAtPos(absPos.x); //returns ColumnType::NONE if no column at x position!
+ const HoverArea rowHover = prov->getRowMouseHover(row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth);
+
+ const std::wstring toolTip = [&]
+ {
+ if (cpi.colType != ColumnType::NONE && 0 <= row && row < rowCount)
+ return prov->getToolTip(row, cpi.colType);
+ return std::wstring();
+ }();
+ setToolTip(toolTip); //show even during mouse selection!
+
+ if (activeSelection_)
+ activeSelection_->evalMousePos(); //call on both mouse movement + timer event!
+ else
+ {
+ refreshHighlight(highlight_);
+ highlight_.row = row;
+ highlight_.rowHover = rowHover;
+ refreshHighlight(highlight_); //multiple Refresh() calls are condensed into single one!
+ }
+ }
+ event.Skip();
+ }
+
+ void onLeaveWindow(wxMouseEvent& event) override //wxEVT_LEAVE_WINDOW does not respect mouse capture!
+ {
+ if (!activeSelection_)
+ {
+ refreshHighlight(highlight_);
+ highlight_.row = -1;
+ }
+
+ event.Skip();
+ }
+
+
+ void onFocus(wxFocusEvent& event) override { Refresh(); event.Skip(); }
+
+ class MouseSelection : private wxEvtHandler
+ {
+ public:
+ MouseSelection(MainWin& wnd, size_t rowStart, bool positiveSelect, const GridClickEvent& firstClick) :
+ wnd_(wnd), rowStart_(rowStart), rowCurrent_(rowStart), positiveSelect_(positiveSelect), firstClick_(firstClick)
+ {
+ wnd_.CaptureMouse();
+ timer_.Connect(wxEVT_TIMER, wxEventHandler(MouseSelection::onTimer), nullptr, this);
+ timer_.Start(100); //timer interval in ms
+ evalMousePos();
+ }
+ ~MouseSelection() { if (wnd_.HasCapture()) wnd_.ReleaseMouse(); }
+
+ size_t getStartRow () const { return rowStart_; }
+ size_t getCurrentRow () const { return rowCurrent_; }
+ bool isPositiveSelect() const { return positiveSelect_; } //are we selecting or unselecting?
+ const GridClickEvent& getFirstClick() const { return firstClick_; }
+
+ void evalMousePos()
+ {
+ const auto now = std::chrono::steady_clock::now();
+ const double deltaSecs = std::chrono::duration<double>(now - lastEvalTime_).count(); //unit: [sec]
+ lastEvalTime_ = now;
+
+ const wxPoint clientPos = wnd_.ScreenToClient(wxGetMousePosition());
+ const wxSize clientSize = wnd_.GetClientSize();
+ assert(wnd_.GetClientAreaOrigin() == wxPoint());
+
+ //scroll while dragging mouse
+ const int overlapPixY = clientPos.y < 0 ? clientPos.y :
+ clientPos.y >= clientSize.GetHeight() ? clientPos.y - (clientSize.GetHeight() - 1) : 0;
+ const int overlapPixX = clientPos.x < 0 ? clientPos.x :
+ clientPos.x >= clientSize.GetWidth() ? clientPos.x - (clientSize.GetWidth() - 1) : 0;
+
+ int pixelsPerUnitY = 0;
+ wnd_.refParent().GetScrollPixelsPerUnit(nullptr, &pixelsPerUnitY);
+ if (pixelsPerUnitY <= 0) return;
+
+ const double mouseDragSpeedIncScrollU = pixelsPerUnitY > 0 ? MOUSE_DRAG_ACCELERATION * wnd_.rowLabelWin_.getRowHeight() / pixelsPerUnitY : 0; //unit: [scroll units / (pixel * sec)]
+
+ auto autoScroll = [&](int overlapPix, double& toScroll)
+ {
+ if (overlapPix != 0)
+ {
+ const double scrollSpeed = overlapPix * mouseDragSpeedIncScrollU; //unit: [scroll units / sec]
+ toScroll += scrollSpeed * deltaSecs;
+ }
+ else
+ toScroll = 0;
+ };
+
+ autoScroll(overlapPixX, toScrollX_);
+ autoScroll(overlapPixY, toScrollY_);
+
+ if (static_cast<int>(toScrollX_) != 0 || static_cast<int>(toScrollY_) != 0)
+ {
+ wnd_.refParent().scrollDelta(static_cast<int>(toScrollX_), static_cast<int>(toScrollY_)); //
+ toScrollX_ -= static_cast<int>(toScrollX_); //rounds down for positive numbers, up for negative,
+ toScrollY_ -= static_cast<int>(toScrollY_); //exactly what we want
+ }
+
+ //select current row *after* scrolling
+ wxPoint clientPosTrimmed = clientPos;
+ numeric::clamp(clientPosTrimmed.y, 0, clientSize.GetHeight() - 1); //do not select row outside client window!
+
+ const wxPoint absPos = wnd_.refParent().CalcUnscrolledPosition(clientPosTrimmed);
+ const ptrdiff_t newRow = wnd_.rowLabelWin_.getRowAtPos(absPos.y); //return -1 for invalid position; >= rowCount if out of range
+ if (newRow >= 0)
+ if (rowCurrent_ != newRow)
+ {
+ rowCurrent_ = newRow;
+ wnd_.Refresh();
+ }
+ }
+
+ private:
+ void onTimer(wxEvent& event) { evalMousePos(); }
+
+ MainWin& wnd_;
+ const size_t rowStart_;
+ ptrdiff_t rowCurrent_;
+ const bool positiveSelect_;
+ const GridClickEvent firstClick_;
+ wxTimer timer_;
+ double toScrollX_ = 0; //count outstanding scroll unit fractions while dragging mouse
+ double toScrollY_ = 0; //
+ std::chrono::steady_clock::time_point lastEvalTime_ = std::chrono::steady_clock::now();
+ };
+
+ struct MouseHighlight
+ {
+ ptrdiff_t row = -1;
+ HoverArea rowHover = HoverArea::NONE;
+ };
+
+ void ScrollWindow(int dx, int dy, const wxRect* rect) override
+ {
+ wxWindow::ScrollWindow(dx, dy, rect);
+ rowLabelWin_.ScrollWindow(0, dy, rect);
+ colLabelWin_.ScrollWindow(dx, 0, rect);
+
+ //attention, wxGTK call sequence: wxScrolledWindow::Scroll() -> wxScrolledHelperNative::Scroll() -> wxScrolledHelperNative::DoScroll()
+ //which *first* calls us, MainWin::ScrollWindow(), and *then* internally updates m_yScrollPosition
+ //=> we cannot use CalcUnscrolledPosition() here which gives the wrong/outdated value!!!
+ //=> we need to update asynchronously:
+ //=> don't send async event repeatedly => severe performance issues on wxGTK!
+ //=> can't use idle event neither: too few idle events on Windows, e.g. NO idle events while mouse drag-scrolling!
+ //=> solution: send single async event at most!
+ if (!gridUpdatePending_) //without guarding, the number of outstanding async events can become very high during scrolling!! test case: Ubuntu: 170; Windows: 20
+ {
+ gridUpdatePending_ = true;
+ wxCommandEvent scrollEvent(EVENT_GRID_HAS_SCROLLED);
+ AddPendingEvent(scrollEvent); //asynchronously call updateAfterScroll()
+ }
+ }
+
+ void onRequestWindowUpdate(wxEvent& event)
+ {
+ assert(gridUpdatePending_);
+ ZEN_ON_SCOPE_EXIT(gridUpdatePending_ = false);
+
+ refParent().updateWindowSizes(false); //row label width has changed -> do *not* update scrollbars: recursion on wxGTK! -> still a problem, now that we're called async??
+ rowLabelWin_.Update(); //update while dragging scroll thumb
+ }
+
+ void refreshRow(size_t row)
+ {
+ const wxRect& rowArea = rowLabelWin_.getRowLabelArea(row); //returns empty rect if row not found
+ const wxPoint topLeft = refParent().CalcScrolledPosition(wxPoint(0, rowArea.y)); //absolute -> client coordinates
+ wxRect cellArea(topLeft, wxSize(refParent().getColWidthsSum(GetClientSize().GetWidth()), rowArea.height));
+ RefreshRect(cellArea, false);
+ }
+
+ void refreshHighlight(const MouseHighlight& hl)
+ {
+ const ptrdiff_t rowCount = refParent().getRowCount();
+ if (0 <= hl.row && hl.row < rowCount && hl.rowHover != HoverArea::NONE) //no highlight_? => NOP!
+ refreshRow(hl.row);
+ }
+
+ RowLabelWin& rowLabelWin_;
+ ColLabelWin& colLabelWin_;
+
+ std::unique_ptr<MouseSelection> activeSelection_; //bound while user is selecting with mouse
+ MouseHighlight highlight_; //current mouse highlight_ (superseeded by activeSelection_ if available)
+
+ ptrdiff_t cursorRow_ = 0;
+ size_t selectionAnchor_ = 0;
+ bool gridUpdatePending_ = false;
+};
+
+//----------------------------------------------------------------------------------------------------------------
+//----------------------------------------------------------------------------------------------------------------
+
+Grid::Grid(wxWindow* parent,
+ wxWindowID id,
+ const wxPoint& pos,
+ const wxSize& size,
+ long style,
+ const wxString& name) : wxScrolledWindow(parent, id, pos, size, style | wxWANTS_CHARS, name)
+{
+ cornerWin_ = new CornerWin (*this); //
+ rowLabelWin_ = new RowLabelWin(*this); //owership handled by "this"
+ colLabelWin_ = new ColLabelWin(*this); //
+ mainWin_ = new MainWin (*this, *rowLabelWin_, *colLabelWin_); //
+
+ colLabelHeight_ = 2 * DEFAULT_COL_LABEL_BORDER + [&]() -> int
+ {
+ //coordinate with ColLabelWin::render():
+ wxFont labelFont = colLabelWin_->GetFont();
+ labelFont.SetWeight(wxFONTWEIGHT_BOLD);
+ return labelFont.GetPixelSize().GetHeight();
+ }();
+
+ SetTargetWindow(mainWin_);
+
+ SetInitialSize(size); //"Most controls will use this to set their initial size" -> why not
+
+ assert(GetClientSize() == GetSize()); //borders are NOT allowed for Grid
+ //reason: updateWindowSizes() wants to use "GetSize()" as a "GetClientSize()" including scrollbars
+
+ Connect(wxEVT_PAINT, wxPaintEventHandler(Grid::onPaintEvent ), nullptr, this);
+ Connect(wxEVT_ERASE_BACKGROUND, wxEraseEventHandler(Grid::onEraseBackGround), nullptr, this);
+ Connect(wxEVT_SIZE, wxSizeEventHandler (Grid::onSizeEvent ), nullptr, this);
+
+ Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(Grid::onKeyDown), nullptr, this);
+}
+
+
+void Grid::updateWindowSizes(bool updateScrollbar)
+{
+ /* We have to deal with TWO nasty circular dependencies:
+ 1.
+ rowLabelWidth
+ /|\
+ mainWin::client width
+ /|\
+ SetScrollbars -> show/hide horizontal scrollbar depending on client width
+ /|\
+ mainWin::client height -> possibly trimmed by horizontal scrollbars
+ /|\
+ rowLabelWidth
+
+ 2.
+ mainWin_->GetClientSize()
+ /|\
+ SetScrollbars -> show/hide scrollbars depending on whether client size is big enough
+ /|\
+ GetClientSize(); -> possibly trimmed by scrollbars
+ /|\
+ mainWin_->GetClientSize() -> also trimmed, since it's a sub-window!
+ */
+
+ //break this vicious circle:
+
+ //harmonize with Grid::GetSizeAvailableForScrollTarget()!
+
+ //1. calculate row label width independent from scrollbars
+ const int mainWinHeightGross = std::max(GetSize().GetHeight() - colLabelHeight_, 0); //independent from client sizes and scrollbars!
+ const ptrdiff_t logicalHeight = rowLabelWin_->getLogicalHeight(); //
+
+ int rowLabelWidth = 0;
+ if (drawRowLabel_ && logicalHeight > 0)
+ {
+ ptrdiff_t yFrom = CalcUnscrolledPosition(wxPoint(0, 0)).y;
+ ptrdiff_t yTo = CalcUnscrolledPosition(wxPoint(0, mainWinHeightGross - 1)).y ;
+ numeric::clamp<ptrdiff_t>(yFrom, 0, logicalHeight - 1);
+ numeric::clamp<ptrdiff_t>(yTo, 0, logicalHeight - 1);
+
+ const ptrdiff_t rowFrom = rowLabelWin_->getRowAtPos(yFrom);
+ const ptrdiff_t rowTo = rowLabelWin_->getRowAtPos(yTo);
+ if (rowFrom >= 0 && rowTo >= 0)
+ rowLabelWidth = rowLabelWin_->getBestWidth(rowFrom, rowTo);
+ }
+
+ auto getMainWinSize = [&](const wxSize& clientSize) { return wxSize(std::max(0, clientSize.GetWidth() - rowLabelWidth), std::max(0, clientSize.GetHeight() - colLabelHeight_)); };
+
+ auto setScrollbars2 = [&](int logWidth, int logHeight) //replace SetScrollbars, which loses precision of pixelsPerUnitX for some brain-dead reason
+ {
+ mainWin_->SetVirtualSize(logWidth, logHeight); //set before calling SetScrollRate():
+ //else SetScrollRate() would fail to preserve scroll position when "new virtual pixel-pos > old virtual height"
+
+ int ppsuX = 0; //pixel per scroll unit
+ int ppsuY = 0;
+ GetScrollPixelsPerUnit(&ppsuX, &ppsuY);
+
+ const int ppsuNew = rowLabelWin_->getRowHeight();
+ if (ppsuX != ppsuNew || ppsuY != ppsuNew) //support polling!
+ SetScrollRate(ppsuNew, ppsuNew); //internally calls AdjustScrollbars() and GetVirtualSize()!
+
+ AdjustScrollbars(); //lousy wxWidgets design decision: internally calls mainWin_->GetClientSize() without considering impact of scrollbars!
+ //Attention: setting scrollbars triggers *synchronous* resize event if scrollbars are shown or hidden! => updateWindowSizes() recursion! (Windows)
+ };
+
+ //2. update managed windows' sizes: just assume scrollbars are already set correctly, even if they may not be (yet)!
+ //this ensures mainWin_->SetVirtualSize() and AdjustScrollbars() are working with the correct main window size, unless sb change later, which triggers a recalculation anyway!
+ const wxSize mainWinSize = getMainWinSize(GetClientSize());
+
+ cornerWin_ ->SetSize(0, 0, rowLabelWidth, colLabelHeight_);
+ rowLabelWin_->SetSize(0, colLabelHeight_, rowLabelWidth, mainWinSize.GetHeight());
+ colLabelWin_->SetSize(rowLabelWidth, 0, mainWinSize.GetWidth(), colLabelHeight_);
+ mainWin_ ->SetSize(rowLabelWidth, colLabelHeight_, mainWinSize.GetWidth(), mainWinSize.GetHeight());
+
+ //avoid flicker in wxWindowMSW::HandleSize() when calling ::EndDeferWindowPos() where the sub-windows are moved only although they need to be redrawn!
+ colLabelWin_->Refresh();
+ mainWin_ ->Refresh();
+
+ //3. update scrollbars: "guide wxScrolledHelper to not screw up too much"
+ if (updateScrollbar)
+ {
+ const int mainWinWidthGross = getMainWinSize(GetSize()).GetWidth();
+
+ if (logicalHeight <= mainWinHeightGross &&
+ getColWidthsSum(mainWinWidthGross) <= mainWinWidthGross &&
+ //this special case needs to be considered *only* when both scrollbars are flexible:
+ showScrollbarX_ == SB_SHOW_AUTOMATIC &&
+ showScrollbarY_ == SB_SHOW_AUTOMATIC)
+ setScrollbars2(0, 0); //no scrollbars required at all! -> wxScrolledWindow requires active help to detect this special case!
+ else
+ {
+ const int logicalWidthTmp = getColWidthsSum(mainWinSize.GetWidth()); //assuming vertical scrollbar stays as it is...
+ setScrollbars2(logicalWidthTmp, logicalHeight); //if scrollbars are shown or hidden a new resize event recurses into updateWindowSizes()
+ /*
+ is there a risk of endless recursion? No, 2-level recursion at most, consider the following 6 cases:
+
+ <----------gw---------->
+ <----------nw------>
+ ------------------------ /|\ /|\
+ | | | | |
+ | main window | | nh |
+ | | | | gh
+ ------------------------ \|/ |
+ | | | |
+ ------------------------ \|/
+ gw := gross width
+ nw := net width := gross width - sb size
+ gh := gross height
+ nh := net height := gross height - sb size
+
+ There are 6 cases that can occur:
+ ---------------------------------
+ lw := logical width
+ lh := logical height
+
+ 1. lw <= gw && lh <= gh => no scrollbars needed
+
+ 2. lw > gw && lh > gh => need both scrollbars
+
+ 3. lh > gh
+ 4.1 lw <= nw => need vertical scrollbar only
+ 4.2 nw < lw <= gw => need both scrollbars
+
+ 4. lw > gw
+ 3.1 lh <= nh => need horizontal scrollbar only
+ 3.2 nh < lh <= gh => need both scrollbars
+ */
+ }
+ }
+}
+
+
+wxSize Grid::GetSizeAvailableForScrollTarget(const wxSize& size)
+{
+ //harmonize with Grid::updateWindowSizes()!
+
+ //1. calculate row label width independent from scrollbars
+ const int mainWinHeightGross = std::max(size.GetHeight() - colLabelHeight_, 0); //independent from client sizes and scrollbars!
+ const ptrdiff_t logicalHeight = rowLabelWin_->getLogicalHeight(); //
+
+ int rowLabelWidth = 0;
+ if (drawRowLabel_ && logicalHeight > 0)
+ {
+ ptrdiff_t yFrom = CalcUnscrolledPosition(wxPoint(0, 0)).y;
+ ptrdiff_t yTo = CalcUnscrolledPosition(wxPoint(0, mainWinHeightGross - 1)).y ;
+ numeric::clamp<ptrdiff_t>(yFrom, 0, logicalHeight - 1);
+ numeric::clamp<ptrdiff_t>(yTo, 0, logicalHeight - 1);
+
+ const ptrdiff_t rowFrom = rowLabelWin_->getRowAtPos(yFrom);
+ const ptrdiff_t rowTo = rowLabelWin_->getRowAtPos(yTo);
+ if (rowFrom >= 0 && rowTo >= 0)
+ rowLabelWidth = rowLabelWin_->getBestWidth(rowFrom, rowTo);
+ }
+
+ return size - wxSize(rowLabelWidth, colLabelHeight_);
+}
+
+
+void Grid::onPaintEvent(wxPaintEvent& event) { wxPaintDC dc(this); }
+
+
+void Grid::onKeyDown(wxKeyEvent& event)
+{
+ int keyCode = event.GetKeyCode();
+ if (GetLayoutDirection() == wxLayout_RightToLeft)
+ {
+ if (keyCode == WXK_LEFT || keyCode == WXK_NUMPAD_LEFT)
+ keyCode = WXK_RIGHT;
+ else if (keyCode == WXK_RIGHT || keyCode == WXK_NUMPAD_RIGHT)
+ keyCode = WXK_LEFT;
+ }
+
+ const ptrdiff_t rowCount = getRowCount();
+ const ptrdiff_t cursorRow = mainWin_->getCursor();
+
+ auto moveCursorTo = [&](ptrdiff_t row)
+ {
+ if (rowCount > 0)
+ {
+ numeric::clamp<ptrdiff_t>(row, 0, rowCount - 1);
+ setGridCursor(row);
+ }
+ };
+
+ auto selectWithCursorTo = [&](ptrdiff_t row)
+ {
+ if (rowCount > 0)
+ {
+ numeric::clamp<ptrdiff_t>(row, 0, rowCount - 1);
+ selectWithCursor(row);
+ }
+ };
+
+ switch (keyCode)
+ {
+ //case WXK_TAB:
+ // if (Navigate(event.ShiftDown() ? wxNavigationKeyEvent::IsBackward : wxNavigationKeyEvent::IsForward))
+ // return;
+ // break;
+
+ case WXK_UP:
+ case WXK_NUMPAD_UP:
+ if (event.ShiftDown())
+ selectWithCursorTo(cursorRow - 1);
+ else if (event.ControlDown())
+ scrollDelta(0, -1);
+ else
+ moveCursorTo(cursorRow - 1);
+ return; //swallow event: wxScrolledWindow, wxWidgets 2.9.3 on Kubuntu x64 processes arrow keys: prevent this!
+
+ case WXK_DOWN:
+ case WXK_NUMPAD_DOWN:
+ if (event.ShiftDown())
+ selectWithCursorTo(cursorRow + 1);
+ else if (event.ControlDown())
+ scrollDelta(0, 1);
+ else
+ moveCursorTo(cursorRow + 1);
+ return; //swallow event
+
+ case WXK_LEFT:
+ case WXK_NUMPAD_LEFT:
+ if (event.ControlDown())
+ scrollDelta(-1, 0);
+ else if (event.ShiftDown())
+ ;
+ else
+ moveCursorTo(cursorRow);
+ return;
+
+ case WXK_RIGHT:
+ case WXK_NUMPAD_RIGHT:
+ if (event.ControlDown())
+ scrollDelta(1, 0);
+ else if (event.ShiftDown())
+ ;
+ else
+ moveCursorTo(cursorRow);
+ return;
+
+ case WXK_HOME:
+ case WXK_NUMPAD_HOME:
+ if (event.ShiftDown())
+ selectWithCursorTo(0);
+ //else if (event.ControlDown())
+ // ;
+ else
+ moveCursorTo(0);
+ return;
+
+ case WXK_END:
+ case WXK_NUMPAD_END:
+ if (event.ShiftDown())
+ selectWithCursorTo(rowCount - 1);
+ //else if (event.ControlDown())
+ // ;
+ else
+ moveCursorTo(rowCount - 1);
+ return;
+
+ case WXK_PAGEUP:
+ case WXK_NUMPAD_PAGEUP:
+ if (event.ShiftDown())
+ selectWithCursorTo(cursorRow - GetClientSize().GetHeight() / rowLabelWin_->getRowHeight());
+ //else if (event.ControlDown())
+ // ;
+ else
+ moveCursorTo(cursorRow - GetClientSize().GetHeight() / rowLabelWin_->getRowHeight());
+ return;
+
+ case WXK_PAGEDOWN:
+ case WXK_NUMPAD_PAGEDOWN:
+ if (event.ShiftDown())
+ selectWithCursorTo(cursorRow + GetClientSize().GetHeight() / rowLabelWin_->getRowHeight());
+ //else if (event.ControlDown())
+ // ;
+ else
+ moveCursorTo(cursorRow + GetClientSize().GetHeight() / rowLabelWin_->getRowHeight());
+ return;
+
+ case 'A': //Ctrl + A - select all
+ if (event.ControlDown())
+ selectRangeAndNotify(0, rowCount, true /*positive*/, nullptr /*mouseInitiated*/);
+ break;
+
+ case WXK_NUMPAD_ADD: //CTRL + '+' - auto-size all
+ if (event.ControlDown())
+ autoSizeColumns(ALLOW_GRID_EVENT);
+ return;
+ }
+
+ event.Skip();
+}
+
+
+void Grid::setColumnLabelHeight(int height)
+{
+ colLabelHeight_ = std::max(0, height);
+ updateWindowSizes();
+}
+
+
+void Grid::showRowLabel(bool show)
+{
+ drawRowLabel_ = show;
+ updateWindowSizes();
+}
+
+
+void Grid::selectAllRows(GridEventPolicy rangeEventPolicy)
+{
+ selection_.selectAll();
+ mainWin_->Refresh();
+
+ if (rangeEventPolicy == ALLOW_GRID_EVENT) //notify event, even if we're not triggered by user interaction
+ {
+ GridRangeSelectEvent selEvent(0, getRowCount(), true, nullptr);
+ if (wxEvtHandler* evtHandler = GetEventHandler())
+ evtHandler->ProcessEvent(selEvent);
+ }
+}
+
+
+void Grid::clearSelection(GridEventPolicy rangeEventPolicy)
+{
+ selection_.clear();
+ mainWin_->Refresh();
+
+ if (rangeEventPolicy == ALLOW_GRID_EVENT) //notify event, even if we're not triggered by user interaction
+ {
+ GridRangeSelectEvent unselectionEvent(0, getRowCount(), false, nullptr);
+ if (wxEvtHandler* evtHandler = GetEventHandler())
+ evtHandler->ProcessEvent(unselectionEvent);
+ }
+}
+
+
+void Grid::scrollDelta(int deltaX, int deltaY)
+{
+ int scrollPosX = 0;
+ int scrollPosY = 0;
+ GetViewStart(&scrollPosX, &scrollPosY);
+
+ scrollPosX += deltaX;
+ scrollPosY += deltaY;
+
+ scrollPosX = std::max(0, scrollPosX); //wxScrollHelper::Scroll() will exit prematurely if input happens to be "-1"!
+ scrollPosY = std::max(0, scrollPosY); //
+
+ Scroll(scrollPosX, scrollPosY); //internally calls wxWindows::Update()!
+ updateWindowSizes(); //may show horizontal scroll bar
+}
+
+
+void Grid::redirectRowLabelEvent(wxMouseEvent& event)
+{
+ event.m_x = 0;
+ if (wxEvtHandler* evtHandler = mainWin_->GetEventHandler())
+ evtHandler->ProcessEvent(event);
+
+ if (event.ButtonDown() && wxWindow::FindFocus() != mainWin_)
+ mainWin_->SetFocus();
+}
+
+
+size_t Grid::getRowCount() const
+{
+ return dataView_ ? dataView_->getRowCount() : 0;
+}
+
+
+void Grid::Refresh(bool eraseBackground, const wxRect* rect)
+{
+ const size_t rowCountNew = getRowCount();
+ if (rowCountOld_ != rowCountNew)
+ {
+ rowCountOld_ = rowCountNew;
+ updateWindowSizes();
+ }
+
+ if (selection_.maxSize() != rowCountNew) //clear selection only when needed (consider setSelectedRows())
+ selection_.init(rowCountNew);
+
+ wxScrolledWindow::Refresh(eraseBackground, rect);
+}
+
+
+void Grid::setRowHeight(int height)
+{
+ rowLabelWin_->setRowHeight(height);
+ updateWindowSizes();
+ Refresh();
+}
+
+
+void Grid::setColumnConfig(const std::vector<Grid::ColumnAttribute>& attr)
+{
+ //hold ownership of non-visible columns
+ oldColAttributes_ = attr;
+
+ std::vector<VisibleColumn> visCols;
+ for (const ColumnAttribute& ca : attr)
+ {
+ assert(ca.type_ != ColumnType::NONE);
+ if (ca.visible_)
+ visCols.emplace_back(ca.type_, ca.offset_, ca.stretch_);
+ }
+
+ //"ownership" of visible columns is now within Grid
+ visibleCols_ = visCols;
+
+ updateWindowSizes();
+ Refresh();
+}
+
+
+std::vector<Grid::ColumnAttribute> Grid::getColumnConfig() const
+{
+ //get non-visible columns (+ outdated visible ones)
+ std::vector<ColumnAttribute> output = oldColAttributes_;
+
+ auto iterVcols = visibleCols_.begin();
+ auto iterVcolsend = visibleCols_.end();
+
+ //update visible columns but keep order of non-visible ones!
+ for (ColumnAttribute& ca : output)
+ if (ca.visible_)
+ {
+ if (iterVcols != iterVcolsend)
+ {
+ ca.type_ = iterVcols->type_;
+ ca.stretch_ = iterVcols->stretch_;
+ ca.offset_ = iterVcols->offset_;
+ ++iterVcols;
+ }
+ else
+ assert(false);
+ }
+ assert(iterVcols == iterVcolsend);
+
+ return output;
+}
+
+
+void Grid::showScrollBars(Grid::ScrollBarStatus horizontal, Grid::ScrollBarStatus vertical)
+{
+ if (showScrollbarX_ == horizontal &&
+ showScrollbarY_ == vertical) return; //support polling!
+
+ showScrollbarX_ = horizontal;
+ showScrollbarY_ = vertical;
+
+ //the following wxGTK approach is pretty much identical to wxWidgets 2.9 ShowScrollbars() code!
+
+ auto mapStatus = [](ScrollBarStatus sbStatus) -> GtkPolicyType
+ {
+ switch (sbStatus)
+ {
+ case SB_SHOW_AUTOMATIC:
+ return GTK_POLICY_AUTOMATIC;
+ case SB_SHOW_ALWAYS:
+ return GTK_POLICY_ALWAYS;
+ case SB_SHOW_NEVER:
+ return GTK_POLICY_NEVER;
+ }
+ assert(false);
+ return GTK_POLICY_AUTOMATIC;
+ };
+
+ GtkWidget* gridWidget = wxWindow::m_widget;
+ GtkScrolledWindow* scrolledWindow = GTK_SCROLLED_WINDOW(gridWidget);
+ ::gtk_scrolled_window_set_policy(scrolledWindow,
+ mapStatus(horizontal),
+ mapStatus(vertical));
+
+ updateWindowSizes();
+}
+
+
+
+wxWindow& Grid::getCornerWin () { return *cornerWin_; }
+wxWindow& Grid::getRowLabelWin() { return *rowLabelWin_; }
+wxWindow& Grid::getColLabelWin() { return *colLabelWin_; }
+wxWindow& Grid::getMainWin () { return *mainWin_; }
+const wxWindow& Grid::getMainWin() const { return *mainWin_; }
+
+
+Opt<Grid::ColAction> Grid::clientPosToColumnAction(const wxPoint& pos) const
+{
+ const int absPosX = CalcUnscrolledPosition(pos).x;
+ if (absPosX >= 0)
+ {
+ const int resizeTolerance = allowColumnResize_ ? COLUMN_RESIZE_TOLERANCE : 0;
+ std::vector<ColumnWidth> absWidths = getColWidths(); //resolve stretched widths
+
+ int accuWidth = 0;
+ for (size_t col = 0; col < absWidths.size(); ++col)
+ {
+ accuWidth += absWidths[col].width_;
+ if (std::abs(absPosX - accuWidth) < resizeTolerance)
+ {
+ ColAction out;
+ out.wantResize = true;
+ out.col = col;
+ return out;
+ }
+ else if (absPosX < accuWidth)
+ {
+ ColAction out;
+ out.wantResize = false;
+ out.col = col;
+ return out;
+ }
+ }
+ }
+ return NoValue();
+}
+
+
+void Grid::moveColumn(size_t colFrom, size_t colTo)
+{
+ if (colFrom < visibleCols_.size() &&
+ colTo < visibleCols_.size() &&
+ colTo != colFrom)
+ {
+ const VisibleColumn colAtt = visibleCols_[colFrom];
+ visibleCols_.erase (visibleCols_.begin() + colFrom);
+ visibleCols_.insert(visibleCols_.begin() + colTo, colAtt);
+ }
+}
+
+
+ptrdiff_t Grid::clientPosToMoveTargetColumn(const wxPoint& pos) const
+{
+
+ const int absPosX = CalcUnscrolledPosition(pos).x;
+
+ int accWidth = 0;
+ std::vector<ColumnWidth> absWidths = getColWidths(); //resolve negative/stretched widths
+ for (auto itCol = absWidths.begin(); itCol != absWidths.end(); ++itCol)
+ {
+ const int width = itCol->width_; //beware dreaded unsigned conversions!
+ accWidth += width;
+
+ if (absPosX < accWidth - width / 2)
+ return itCol - absWidths.begin();
+ }
+ return absWidths.size();
+}
+
+
+ColumnType Grid::colToType(size_t col) const
+{
+ if (col < visibleCols_.size())
+ return visibleCols_[col].type_;
+ return ColumnType::NONE;
+}
+
+
+ptrdiff_t Grid::getRowAtPos(int posY) const { return rowLabelWin_->getRowAtPos(posY); }
+
+
+Grid::ColumnPosInfo Grid::getColumnAtPos(int posX) const
+{
+ if (posX >= 0)
+ {
+ int accWidth = 0;
+ for (const ColumnWidth& cw : getColWidths())
+ {
+ accWidth += cw.width_;
+ if (posX < accWidth)
+ return { cw.type_, posX + cw.width_ - accWidth, cw.width_ };
+ }
+ }
+ return { ColumnType::NONE, 0, 0 };
+}
+
+
+wxRect Grid::getColumnLabelArea(ColumnType colType) const
+{
+ std::vector<ColumnWidth> absWidths = getColWidths(); //resolve negative/stretched widths
+
+ //colType is not unique in general, but *this* function expects it!
+ assert(std::count_if(absWidths.begin(), absWidths.end(), [&](const ColumnWidth& cw) { return cw.type_ == colType; }) <= 1);
+
+ auto itCol = std::find_if(absWidths.begin(), absWidths.end(), [&](const ColumnWidth& cw) { return cw.type_ == colType; });
+ if (itCol != absWidths.end())
+ {
+ ptrdiff_t posX = 0;
+ for (auto it = absWidths.begin(); it != itCol; ++it)
+ posX += it->width_;
+
+ return wxRect(wxPoint(posX, 0), wxSize(itCol->width_, colLabelHeight_));
+ }
+ return wxRect();
+}
+
+
+void Grid::refreshCell(size_t row, ColumnType colType)
+{
+ const wxRect& colArea = getColumnLabelArea(colType); //returns empty rect if column not found
+ const wxRect& rowArea = rowLabelWin_->getRowLabelArea(row); //returns empty rect if row not found
+ if (colArea.height > 0 && rowArea.height > 0)
+ {
+ const wxPoint topLeft = CalcScrolledPosition(wxPoint(colArea.x, rowArea.y)); //absolute -> client coordinates
+ const wxRect cellArea(topLeft, wxSize(colArea.width, rowArea.height));
+
+ getMainWin().RefreshRect(cellArea, false);
+ }
+}
+
+
+void Grid::setGridCursor(size_t row)
+{
+ mainWin_->setCursor(row, row);
+ makeRowVisible(row);
+
+ selection_.clear(); //clear selection, do NOT fire event
+ selectRangeAndNotify(row, row, true /*positive*/, nullptr /*mouseInitiated*/); //set new selection + fire event
+
+ mainWin_->Refresh();
+ rowLabelWin_->Refresh(); //row labels! (Kubuntu)
+}
+
+
+void Grid::selectWithCursor(ptrdiff_t row)
+{
+ const size_t anchorRow = mainWin_->getAnchor();
+
+ mainWin_->setCursor(row, anchorRow);
+ makeRowVisible(row);
+
+ selection_.clear(); //clear selection, do NOT fire event
+ selectRangeAndNotify(anchorRow, row, true /*positive*/, nullptr /*mouseInitiated*/); //set new selection + fire event
+
+ mainWin_->Refresh();
+ rowLabelWin_->Refresh();
+}
+
+
+void Grid::makeRowVisible(size_t row)
+{
+ const wxRect labelRect = rowLabelWin_->getRowLabelArea(row); //returns empty rect if row not found
+ if (labelRect.height > 0)
+ {
+ int scrollPosX = 0;
+ GetViewStart(&scrollPosX, nullptr);
+
+ int pixelsPerUnitY = 0;
+ GetScrollPixelsPerUnit(nullptr, &pixelsPerUnitY);
+ if (pixelsPerUnitY <= 0) return;
+
+ const int clientPosY = CalcScrolledPosition(labelRect.GetTopLeft()).y;
+ if (clientPosY < 0)
+ {
+ const int scrollPosY = labelRect.y / pixelsPerUnitY;
+ Scroll(scrollPosX, scrollPosY); //internally calls wxWindows::Update()!
+ updateWindowSizes(); //may show horizontal scroll bar
+ }
+ else if (clientPosY + labelRect.height > rowLabelWin_->GetClientSize().GetHeight())
+ {
+ auto execScroll = [&](int clientHeight)
+ {
+ const int scrollPosY = std::ceil((labelRect.y - clientHeight +
+ labelRect.height) / static_cast<double>(pixelsPerUnitY));
+ Scroll(scrollPosX, scrollPosY);
+ updateWindowSizes(); //may show horizontal scroll bar
+ };
+
+ const int clientHeightBefore = rowLabelWin_->GetClientSize().GetHeight();
+ execScroll(clientHeightBefore);
+
+ //client height may decrease after scroll due to a new horizontal scrollbar, resulting in a partially visible last row
+ const int clientHeightAfter = rowLabelWin_->GetClientSize().GetHeight();
+ if (clientHeightAfter < clientHeightBefore)
+ execScroll(clientHeightAfter);
+ }
+ }
+}
+
+
+void Grid::selectRangeAndNotify(ptrdiff_t rowFrom, ptrdiff_t rowTo, bool positive, const GridClickEvent* mouseInitiated)
+{
+ //sort + convert to half-open range
+ auto rowFirst = std::min(rowFrom, rowTo);
+ auto rowLast = std::max(rowFrom, rowTo) + 1;
+
+ const size_t rowCount = getRowCount();
+ numeric::clamp<ptrdiff_t>(rowFirst, 0, rowCount);
+ numeric::clamp<ptrdiff_t>(rowLast, 0, rowCount);
+
+ selection_.selectRange(rowFirst, rowLast, positive);
+
+ //notify event
+ GridRangeSelectEvent selectionEvent(rowFirst, rowLast, positive, mouseInitiated);
+ if (wxEvtHandler* evtHandler = GetEventHandler())
+ evtHandler->ProcessEvent(selectionEvent);
+
+ mainWin_->Refresh();
+}
+
+
+void Grid::scrollTo(size_t row)
+{
+ const wxRect labelRect = rowLabelWin_->getRowLabelArea(row); //returns empty rect if row not found
+ if (labelRect.height > 0)
+ {
+ int pixelsPerUnitY = 0;
+ GetScrollPixelsPerUnit(nullptr, &pixelsPerUnitY);
+ if (pixelsPerUnitY > 0)
+ {
+ const int scrollPosYNew = labelRect.y / pixelsPerUnitY;
+ int scrollPosXOld = 0;
+ int scrollPosYOld = 0;
+ GetViewStart(&scrollPosXOld, &scrollPosYOld);
+
+ if (scrollPosYOld != scrollPosYNew) //support polling
+ {
+ Scroll(scrollPosXOld, scrollPosYNew); //internally calls wxWindows::Update()!
+ updateWindowSizes(); //may show horizontal scroll bar
+ Refresh();
+ }
+ }
+ }
+}
+
+
+bool Grid::Enable(bool enable)
+{
+ Refresh();
+ return wxScrolledWindow::Enable(enable);
+}
+
+
+size_t Grid::getGridCursor() const
+{
+ return mainWin_->getCursor();
+}
+
+
+int Grid::getBestColumnSize(size_t col) const
+{
+ if (dataView_ && col < visibleCols_.size())
+ {
+ const ColumnType type = visibleCols_[col].type_;
+
+ wxClientDC dc(mainWin_);
+ dc.SetFont(mainWin_->GetFont()); //harmonize with MainWin::render()
+
+ int maxSize = 0;
+
+ auto rowRange = rowLabelWin_->getRowsOnClient(mainWin_->GetClientRect()); //returns range [begin, end)
+ for (auto row = rowRange.first; row < rowRange.second; ++row)
+ maxSize = std::max(maxSize, dataView_->getBestSize(dc, row, type));
+
+ return maxSize;
+ }
+ return -1;
+}
+
+
+void Grid::setColumnWidth(int width, size_t col, GridEventPolicy columnResizeEventPolicy, bool notifyAsync)
+{
+ if (col < visibleCols_.size())
+ {
+ VisibleColumn& vcRs = visibleCols_[col];
+
+ const std::vector<int> stretchedWidths = getColStretchedWidths(mainWin_->GetClientSize().GetWidth());
+ if (stretchedWidths.size() != visibleCols_.size())
+ {
+ assert(false);
+ return;
+ }
+ //CAVEATS:
+ //I. fixed-size columns: normalize offset so that resulting width is at least COLUMN_MIN_WIDTH: this is NOT enforced by getColWidths()!
+ //II. stretched columns: do not allow user to set offsets so small that they result in negative (non-normalized) widths: this gives an
+ //unusual delay when enlarging the column again later
+ width = std::max(width, COLUMN_MIN_WIDTH);
+
+ vcRs.offset_ = width - stretchedWidths[col]; //width := stretchedWidth + offset
+
+ //III. resizing any column should normalize *all* other stretched columns' offsets considering current mainWinWidth!
+ // test case:
+ //1. have columns, both fixed-size and stretched, fit whole window width
+ //2. shrink main window width so that horizontal scrollbars are shown despite the streched column
+ //3. shrink a fixed-size column so that the scrollbars vanish and columns cover full width again
+ //4. now verify that the stretched column is resizing immediately if main window is enlarged again
+ for (size_t col2 = 0; col2 < visibleCols_.size(); ++col2)
+ if (visibleCols_[col2].stretch_ > 0) //normalize stretched columns only
+ visibleCols_[col2].offset_ = std::max(visibleCols_[col2].offset_, COLUMN_MIN_WIDTH - stretchedWidths[col2]);
+
+ if (columnResizeEventPolicy == ALLOW_GRID_EVENT)
+ {
+ GridColumnResizeEvent sizeEvent(vcRs.offset_, vcRs.type_);
+ if (wxEvtHandler* evtHandler = GetEventHandler())
+ {
+ if (notifyAsync)
+ evtHandler->AddPendingEvent(sizeEvent);
+ else
+ evtHandler->ProcessEvent(sizeEvent);
+ }
+ }
+ }
+ else
+ assert(false);
+}
+
+
+void Grid::autoSizeColumns(GridEventPolicy columnResizeEventPolicy)
+{
+ if (allowColumnResize_)
+ {
+ for (size_t col = 0; col < visibleCols_.size(); ++col)
+ {
+ const int bestWidth = getBestColumnSize(col); //return -1 on error
+ if (bestWidth >= 0)
+ setColumnWidth(bestWidth, col, columnResizeEventPolicy, true);
+ }
+ updateWindowSizes();
+ Refresh();
+ }
+}
+
+
+std::vector<int> Grid::getColStretchedWidths(int clientWidth) const //final width = (normalized) (stretchedWidth + offset)
+{
+ assert(clientWidth >= 0);
+ clientWidth = std::max(clientWidth, 0);
+ int stretchTotal = 0;
+ for (const VisibleColumn& vc : visibleCols_)
+ {
+ assert(vc.stretch_ >= 0);
+ stretchTotal += vc.stretch_;
+ }
+
+ int remainingWidth = clientWidth;
+
+ std::vector<int> output;
+
+ if (stretchTotal <= 0)
+ output.resize(visibleCols_.size()); //fill with zeros
+ else
+ {
+ for (const VisibleColumn& vc : visibleCols_)
+ {
+ const int width = clientWidth * vc.stretch_ / stretchTotal; //rounds down!
+ output.push_back(width);
+ remainingWidth -= width;
+ }
+
+ //distribute *all* of clientWidth: should suffice to enlarge the first few stretched columns; no need to minimize total absolute error of distribution
+ if (remainingWidth > 0)
+ for (size_t col2 = 0; col2 < visibleCols_.size(); ++col2)
+ if (visibleCols_[col2].stretch_ > 0)
+ {
+ ++output[col2];
+ if (--remainingWidth == 0)
+ break;
+ }
+ assert(remainingWidth == 0);
+ }
+ return output;
+}
+
+
+std::vector<Grid::ColumnWidth> Grid::getColWidths() const
+{
+ return getColWidths(mainWin_->GetClientSize().GetWidth());
+}
+
+
+std::vector<Grid::ColumnWidth> Grid::getColWidths(int mainWinWidth) const //evaluate stretched columns
+{
+ const std::vector<int> stretchedWidths = getColStretchedWidths(mainWinWidth);
+ assert(stretchedWidths.size() == visibleCols_.size());
+
+ std::vector<ColumnWidth> output;
+ for (size_t col2 = 0; col2 < visibleCols_.size(); ++col2)
+ {
+ const auto& vc = visibleCols_[col2];
+ int width = stretchedWidths[col2] + vc.offset_;
+
+ if (vc.stretch_ > 0)
+ width = std::max(width, COLUMN_MIN_WIDTH); //normalization really needed here: e.g. smaller main window would result in negative width
+ else
+ width = std::max(width, 0); //support smaller width than COLUMN_MIN_WIDTH if set via configuration
+
+ output.emplace_back(vc.type_, width);
+ }
+ return output;
+}
+
+
+int Grid::getColWidthsSum(int mainWinWidth) const
+{
+ int sum = 0;
+ for (const ColumnWidth& cw : getColWidths(mainWinWidth))
+ sum += cw.width_;
+ return sum;
+}
diff --git a/wx+/grid.h b/wx+/grid.h index f08d6e41..ff1d4b87 100644..100755 --- a/wx+/grid.h +++ b/wx+/grid.h @@ -1,366 +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 - -#if defined ZEN_WIN || defined ZEN_MAC - void SetScrollbar(int orientation, int position, int thumbSize, int range, bool refresh) override; //get rid of scrollbars, but preserve scrolling behavior! -#endif - - 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 3428546e..851b3ed0 100644..100755 --- a/wx+/http.cpp +++ b/wx+/http.cpp @@ -1,433 +1,268 @@ -// ***************************************************************************** -// * 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" -#ifdef ZEN_WIN - #include <zen/win.h> //tame WinINet.h include - #include <WinINet.h> -#endif - -#if defined ZEN_LINUX || defined ZEN_MAC - #include <wx/app.h> - #include <zen/thread.h> //std::thread::id - #include <wx/protocol/http.h> -#endif - -using namespace zen; - - -namespace -{ -#ifdef ZEN_WIN - #if defined NDEBUG && defined __WXWINDOWS__ - #error don not use wxWidgets for this component! - #endif -#else - #ifndef NDEBUG - const std::thread::id mainThreadId = std::this_thread::get_id(); - #endif -#endif - -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, //throw SysError, UrlRedirectError - const std::string* postParams) //issue POST if bound, GET otherwise - { - ZEN_ON_SCOPE_FAIL( cleanup(); /*destructor call would lead to member double clean-up!!!*/ ); - - assert(!startsWith(makeUpperCopy(url), L"HTTPS:")); //not supported by wxHTTP! - const std::wstring urlFmt = startsWith(makeUpperCopy(url), L"HTTP://") || - startsWith(makeUpperCopy(url), L"HTTPS://") ? 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); - -#ifdef ZEN_WIN - //WinInet: 1. uses IE proxy settings! :) 2. follows HTTP redirects by default 3. swallows HTTPS if needed - hInternet_ = ::InternetOpen(userAgent.c_str(), //_In_ LPCTSTR lpszAgent, - INTERNET_OPEN_TYPE_PRECONFIG, //_In_ DWORD dwAccessType, - nullptr, //_In_ LPCTSTR lpszProxyName, - nullptr, //_In_ LPCTSTR lpszProxyBypass, - 0); //_In_ DWORD dwFlags - if (!hInternet_) - THROW_LAST_SYS_ERROR(L"InternetOpen"); - - hSession_ = ::InternetConnect(hInternet_, //_In_ HINTERNET hInternet, - server.c_str(), //_In_ LPCTSTR lpszServerName, - INTERNET_DEFAULT_HTTP_PORT, //_In_ INTERNET_PORT nServerPort, - nullptr, //_In_ LPCTSTR lpszUsername, - nullptr, //_In_ LPCTSTR lpszPassword, - INTERNET_SERVICE_HTTP, //_In_ DWORD dwService, - 0, //_In_ DWORD dwFlags, - 0); //_In_ DWORD_PTR dwContext - if (!hSession_) - THROW_LAST_SYS_ERROR(L"InternetConnect"); - - const wchar_t* acceptTypes[] = { L"*/*", nullptr }; - DWORD requestFlags = - //INTERNET_FLAG_KEEP_CONNECTION | - // the combination 1. INTERNET_FLAG_KEEP_CONNECTION (= adds "Connection: Keep-Alive" but NOT "Keep-Alive: timeout" to the header) - // 2. *no* "Keep-Alive: timeout" header entry 3. call from within VM and 4. *no* Fiddler running 5. HTTP POST - // leads to Godaddy blocking the IP: http://www.freefilesync.org/forum/viewtopic.php?t=3855 - // => it seems a broken keep alive header is the trigger: But why is it then working outside the VM or when Fiddler is running??? Why not a problem for HTTP GET? - // note: HTTP/1.1 has keep-alive semantics by default, so this flag is probably useless anyway - INTERNET_FLAG_NO_UI; - - if (postParams) - { - requestFlags |= INTERNET_FLAG_NO_AUTO_REDIRECT; //POST would be re-issued as GET during auto-redirect => handle ourselves! - } - else //HTTP GET - { - requestFlags |= INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTPS | INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTP; - requestFlags |= INTERNET_FLAG_RELOAD; //not relevant for POST (= never cached) - } - - hRequest_ = ::HttpOpenRequest(hSession_, //_In_ HINTERNET hConnect, - postParams ? L"POST" : L"GET", //_In_ LPCTSTR lpszVerb, - page.c_str(), //_In_ LPCTSTR lpszObjectName, - nullptr, //_In_ LPCTSTR lpszVersion, - nullptr, //_In_ LPCTSTR lpszReferer, - acceptTypes, //_In_ LPCTSTR *lplpszAcceptTypes, - requestFlags, //_In_ DWORD dwFlags, - 0); //_In_ DWORD_PTR dwContext - if (!hRequest_) - THROW_LAST_SYS_ERROR(L"HttpOpenRequest"); - - const std::wstring headers = postParams ? L"Content-type: application/x-www-form-urlencoded" : L""; - - assert(std::all_of(headers.begin(), headers.end(), [](wchar_t c) { return makeUnsigned(c) < 128; })); - //HttpSendRequest has finicky behavior for non-ASCII headers: https://msdn.microsoft.com/en-us/library/windows/desktop/aa384247 - - std::string postParamsBuf = postParams ? *postParams : ""; - - if (!::HttpSendRequest(hRequest_, //_In_ HINTERNET hRequest, - headers.c_str(), //_In_ LPCTSTR lpszHeaders, - static_cast<DWORD>(headers.size()), //_In_ DWORD dwHeadersLength, - postParamsBuf.empty() ? nullptr : &postParamsBuf[0], //_In_ LPVOID lpOptional, - static_cast<DWORD>(postParamsBuf.size()))) //_In_ DWORD dwOptionalLength - THROW_LAST_SYS_ERROR(L"HttpSendRequest"); - - DWORD sc = 0; - { - DWORD bufLen = sizeof(sc); - if (!::HttpQueryInfo(hRequest_, //_In_ HINTERNET hRequest, - HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, //_In_ DWORD dwInfoLevel, - &sc, //_Inout_ LPVOID lpvBuffer, - &bufLen, //_Inout_ LPDWORD lpdwBufferLength, - nullptr)) //_Inout_ LPDWORD lpdwIndex - THROW_LAST_SYS_ERROR(L"HttpQueryInfo: HTTP_QUERY_STATUS_CODE"); - } - - //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! - { - DWORD bufLen = 10000; - std::wstring location(bufLen, L'\0'); - if (!::HttpQueryInfo(hRequest_, HTTP_QUERY_LOCATION, &*location.begin(), &bufLen, nullptr)) - THROW_LAST_SYS_ERROR(L"HttpQueryInfo: HTTP_QUERY_LOCATION"); - if (bufLen >= location.size()) //HttpQueryInfo expected to write terminating zero - throw SysError(L"HttpQueryInfo: HTTP_QUERY_LOCATION, buffer overflow"); - location.resize(bufLen); - - if (location.empty()) - throw SysError(L"Unresolvable redirect. Empty target Location."); - - throw UrlRedirectError(location); - } - - if (sc != HTTP_STATUS_OK) //200 - throw SysError(replaceCpy<std::wstring>(L"HTTP status code %x.", L"%x", numberTo<std::wstring>(sc))); - //e.g. 404 - HTTP_STATUS_NOT_FOUND - -#else - 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", utfCvrtTo<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")"); -#endif - } - - ~Impl() { cleanup(); } - - 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__)); - -#ifdef ZEN_WIN - //"HttpQueryInfo() + HTTP_QUERY_CONTENT_LENGTH" not supported by all http servers... - DWORD bytesRead = 0; - if (!::InternetReadFile(hRequest_, //_In_ HINTERNET hFile, - buffer, //_Out_ LPVOID lpBuffer, - static_cast<DWORD>(bytesToRead), //_In_ DWORD dwNumberOfBytesToRead, - &bytesRead)) //_Out_ LPDWORD lpdwNumberOfBytesRead - THROW_LAST_SYS_ERROR(L"InternetReadFile"); -#else - 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); -#endif - if (bytesRead > bytesToRead) //better safe than sorry - throw SysError(L"InternetReadFile: buffer overflow."); - - return bytesRead; //"zero indicates end of file" - } - -private: - Impl (const Impl&) = delete; - Impl& operator=(const Impl&) = delete; - - void cleanup() - { -#ifdef ZEN_WIN - if (hRequest_ ) ::InternetCloseHandle(hRequest_); - if (hSession_ ) ::InternetCloseHandle(hSession_); - if (hInternet_) ::InternetCloseHandle(hInternet_); -#endif - } - -#ifdef ZEN_WIN - HINTERNET hInternet_ = nullptr; - HINTERNET hSession_ = nullptr; - HINTERNET hRequest_ = nullptr; -#else - wxHTTP webAccess_; - std::unique_ptr<wxInputStream> httpStream_; //must be deleted BEFORE webAccess is closed -#endif -}; - - -HttpInputStream::HttpInputStream(std::unique_ptr<Impl>&& pimpl) : pimpl_(std::move(pimpl)) {} - -HttpInputStream::~HttpInputStream() {} - -size_t HttpInputStream::tryRead(void* buffer, size_t bytesToRead) { return pimpl_->tryRead(buffer, bytesToRead); } //throw SysError - - -std::string HttpInputStream::readAll() //throw SysError -{ - std::string buffer; - const size_t blockSize = getBlockSize(); - - for (;;) - { - buffer.resize(buffer.size() + blockSize); - - const size_t bytesRead = pimpl_->tryRead(&*(buffer.end() - blockSize), blockSize); //throw SysError - - if (bytesRead < blockSize) - buffer.resize(buffer.size() - (blockSize - bytesRead)); //caveat: unsigned arithmetics - - if (bytesRead == 0) - return buffer; - } -} - - -namespace -{ -std::unique_ptr<HttpInputStream::Impl> sendHttpRequestImpl(const std::wstring& url, const std::wstring& userAgent, //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, 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, '&')) - if (!nvPair.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 std::vector<std::pair<std::string, std::string>>& postParams) //throw SysError -{ - const std::string encodedParams = xWwwFormUrlEncode(postParams); - return sendHttpRequestImpl(url, userAgent, &encodedParams); //throw SysError -} - - -HttpInputStream zen::sendHttpGet(const std::wstring& url, const std::wstring& userAgent) //throw SysError -{ - return sendHttpRequestImpl(url, userAgent, nullptr); //throw SysError -} - - -bool zen::internetIsAlive() //noexcept -{ -#ifdef ZEN_WIN - //::InternetAttemptConnect(0) -> not working as expected: succeeds even when there is no internet connection! - - HINTERNET hInternet = ::InternetOpen(L"FreeFileSync", //_In_ LPCTSTR lpszAgent, - INTERNET_OPEN_TYPE_PRECONFIG, //_In_ DWORD dwAccessType, - nullptr, //_In_ LPCTSTR lpszProxyName, - nullptr, //_In_ LPCTSTR lpszProxyBypass, - 0); //_In_ DWORD dwFlags - if (!hInternet) - return false; - ZEN_ON_SCOPE_EXIT(::InternetCloseHandle(hInternet)); - - //InternetOpenUrl is shortcut for HTTP:GET with InternetConnect + HttpOpenRequest + HttpSendRequest: - HINTERNET hRequest = ::InternetOpenUrl(hInternet, //_In_ HINTERNET hInternet, - L"http://www.google.com/", //_In_ LPCTSTR lpszUrl, - nullptr, //_In_ LPCTSTR lpszHeaders, - 0, //_In_ DWORD dwHeadersLength, - INTERNET_FLAG_KEEP_CONNECTION | - INTERNET_FLAG_NO_UI | - INTERNET_FLAG_RELOAD | - INTERNET_FLAG_NO_AUTO_REDIRECT, //_In_ DWORD dwFlags, - 0); //_In_ DWORD_PTR dwContext - //fails with ERROR_INTERNET_NAME_NOT_RESOLVED if server not found => the server-relative part is checked by HTTP_QUERY_STATUS_CODE!!! - if (!hRequest) - return false; - ZEN_ON_SCOPE_EXIT(::InternetCloseHandle(hRequest)); - - DWORD sc = 0; - { - DWORD bufLen = sizeof(sc); - if (!::HttpQueryInfo(hRequest, //_In_ HINTERNET hRequest, - HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, //_In_ DWORD dwInfoLevel, - &sc, //_Inout_ LPVOID lpvBuffer, - &bufLen, //_Inout_ LPDWORD lpdwBufferLength, - nullptr)) //_Inout_ LPDWORD lpdwIndex - return false; - } - -#else - 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(); -#endif - //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(makeUpperCopy(url), L"HTTPS:")); //not supported by wxHTTP!
+ const std::wstring urlFmt = startsWith(makeUpperCopy(url), L"HTTP://") ||
+ startsWith(makeUpperCopy(url), L"HTTPS://") ? 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", utfCvrtTo<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 = 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, '&'))
+ if (!nvPair.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!
+}
diff --git a/wx+/http.h b/wx+/http.h index cf385d5e..97341e18 100644..100755 --- a/wx+/http.h +++ b/wx+/http.h @@ -1,48 +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> - -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: - std::string readAll(); //throw SysError - - //support zen/serialize.h Unbuffered Input Stream Concept - size_t tryRead(void* buffer, size_t bytesToRead); //throw SysError; may return short, only 0 means EOF! => CONTRACT: bytesToRead > 0! - size_t getBlockSize() const { return 64 * 1024; } - - 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); //throw SysError -HttpInputStream sendHttpPost(const std::wstring& url, const std::wstring& userAgent, - 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 3eb91ee4..5cd9df0b 100644..100755 --- a/wx+/image_resources.cpp +++ b/wx+/image_resources.cpp @@ -1,171 +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 -{ -#ifndef NDEBUG - const std::thread::id mainThreadId = std::this_thread::get_id(); -#endif - -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(utfCvrtTo<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(utfCvrtTo<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 c29adb9f..37b94fce 100644..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 90945a44..4b0f324f 100644..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 cd0e28f0..6287b752 100644..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 ed3396e1..6794b8bd 100644..100755 --- a/wx+/no_flicker.h +++ b/wx+/no_flicker.h @@ -1,43 +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) -{ -#ifdef ZEN_WIN - //wxStaticText handles ampersands incorrectly: https://sourceforge.net/p/freefilesync/bugs/279/ - replace(newText, L'&', L"&&"); -#endif - - 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 f96884d9..dca93658 100644..100755 --- a/wx+/popup_dlg.cpp +++ b/wx+/popup_dlg.cpp @@ -1,312 +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" - -#ifdef ZEN_WIN - #include <wx+/mouse_move_dlg.h> -#endif - -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; - } - -#if defined ZEN_WIN || defined ZEN_LINUX - const int rowGap = 0; -#elif defined ZEN_MAC - const int rowGap = 1; -#endif - 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) - { -#ifdef ZEN_WIN - new zen::MouseMoveWindow(*this); //allow moving main dialog by clicking (nearly) anywhere...; ownership passed to "this" -#endif - 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 67f73a11..2f0adeff 100644..100755 --- a/wx+/popup_dlg.h +++ b/wx+/popup_dlg.h @@ -1,97 +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 - -#ifdef ZEN_WIN - #include <zen/win.h> //include before <wx/msw/wrapwin.h> -#endif -#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 a79ffa52..3b47573e 100644..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 bef871c7..64e68c08 100644..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__
diff --git a/wx+/rtl.h b/wx+/rtl.h index 206e8e6f..f516ca29 100644..100755 --- a/wx+/rtl.h +++ b/wx+/rtl.h @@ -1,95 +1,95 @@ -// ***************************************************************************** -// * This file is part of the FreeFileSync project. It is distributed under * -// * GNU General Public License: http://www.gnu.org/licenses/gpl-3.0 * -// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * -// ***************************************************************************** - -#ifndef RTL_H_0183487180058718273432148 -#define RTL_H_0183487180058718273432148 - -#include <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 3e335882..ae9e9e1c 100644..100755 --- a/wx+/std_button_layout.h +++ b/wx+/std_button_layout.h @@ -1,146 +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); - -#if defined ZEN_WIN - //Windows User Experience Interaction Guidelines: https://msdn.microsoft.com/en-us/library/windows/desktop/aa511453#sizing - const int spaceH = 6; //OK - const int spaceRimH = 10; //OK - const int spaceRimV = 8; //compromise; consider additional top row from static line; exact values: top 8, bottom 9 -#elif defined ZEN_LINUX - //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 -#elif defined ZEN_MAC - //OS X Human Interface Guidelines: http://developer.apple.com/library/mac/#documentation/UserExperience/Conceptual/AppleHIGuidelines/Windows/Windows.html - const int spaceH = 14; //OK - const int spaceRimH = 24; //OK - const int spaceRimV = 14; //OK -#endif - - 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! -#if defined ZEN_WIN || defined ZEN_LINUX - 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! -#endif - - 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); -#if defined ZEN_WIN - attach(buttonsTmp.btnYes); - attach(buttonsTmp.btnNo); - attach(buttonsTmp.btnCancel); - -#elif defined ZEN_LINUX - attach(buttonsTmp.btnNo); - attach(buttonsTmp.btnCancel); - attach(buttonsTmp.btnYes); - -#elif defined ZEN_MAC - if (buttonsTmp.btnNo) - { - attach(buttonsTmp.btnNo); - sizer.Add(83 - spaceH, 0); //OS X Human Interface Guidelines: "position it at least 24 pixels away from the “safe” buttons" -> however 83 is used in practice! - } - attach(buttonsTmp.btnCancel); - attach(buttonsTmp.btnYes); -#endif - 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+/string_conv.h b/wx+/string_conv.h index 7ec622a3..5db49ff2 100644..100755 --- a/wx+/string_conv.h +++ b/wx+/string_conv.h @@ -1,28 +1,28 @@ -// ***************************************************************************** -// * 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 STRING_CONV_H_893217450815743 -#define STRING_CONV_H_893217450815743 - -#include <zen/utf.h> -#include <wx/string.h> -#include <zen/zstring.h> - -namespace zen -{ -//conversion between Zstring and wxString -inline wxString toWx(const Zstring& str) { return utfCvrtTo<wxString>(str); } -inline Zstring toZ(const wxString& str) { return utfCvrtTo<Zstring>(str); } - -inline std::vector<Zstring> toZ(const std::vector<wxString>& strList) -{ - std::vector<Zstring> tmp; - std::transform(strList.begin(), strList.end(), std::back_inserter(tmp), [](const wxString& str) { return toZ(str); }); - return tmp; -} -} - -#endif //STRING_CONV_H_893217450815743 +// *****************************************************************************
+// * 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 STRING_CONV_H_893217450815743
+#define STRING_CONV_H_893217450815743
+
+#include <zen/utf.h>
+#include <wx/string.h>
+#include <zen/zstring.h>
+
+namespace zen
+{
+//conversion between Zstring and wxString
+inline wxString toWx(const Zstring& str) { return utfCvrtTo<wxString>(str); }
+inline Zstring toZ(const wxString& str) { return utfCvrtTo<Zstring>(str); }
+
+inline std::vector<Zstring> toZ(const std::vector<wxString>& strList)
+{
+ std::vector<Zstring> tmp;
+ std::transform(strList.begin(), strList.end(), std::back_inserter(tmp), [](const wxString& str) { return toZ(str); });
+ return tmp;
+}
+}
+
+#endif //STRING_CONV_H_893217450815743
diff --git a/wx+/timespan.h b/wx+/timespan.h index 5b9c2f0d..b165351f 100644..100755 --- a/wx+/timespan.h +++ b/wx+/timespan.h @@ -1,166 +1,166 @@ -// ***************************************************************************** -// * 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 TIMESPAN_H_254783756533456 -#define TIMESPAN_H_254783756533456 - -#include <wx/textctrl.h> -#include <wx/datetime.h> -#include <wx/spinbutt.h> -#include <wx/sizer.h> - -//user friendly time span control -//- constructor is compatible with a wxTextControl -//- emits change event: wxEVT_TIMESPAN_CHANGE - -namespace zen -{ -inline -wxEventType getEventType() -{ - static wxEventType evt = wxNewEventType(); //external linkage! - return evt; -} -const wxEventType wxEVT_TIMESPAN_CHANGE = getEventType(); - - -class TimeSpanCtrl : public wxPanel -{ -public: - TimeSpanCtrl(wxWindow* parent, wxWindowID id, - const wxString& value = {}, - const wxPoint& pos = wxDefaultPosition, - const wxSize& size = wxDefaultSize, - long style = 0, - const wxValidator& validator = wxDefaultValidator, - const wxString& name = wxTextCtrlNameStr) : - wxPanel(parent, id, pos, size, style, name), - FORMAT_TIMESPAN(wxT("%H:%M:%S")) - { - wxBoxSizer* bSizer27 = new wxBoxSizer( wxHORIZONTAL ); - - m_textCtrl = new wxTextCtrl(this, wxID_ANY, wxString(), wxDefaultPosition, wxDefaultSize, wxTE_CENTRE ); - bSizer27->Add(m_textCtrl, 1, wxALIGN_CENTER_VERTICAL | wxEXPAND, 5 ); - - m_spinBtn = new wxSpinButton(this, wxID_ANY, wxDefaultPosition, wxSize( 20, -1 ), wxSP_ARROW_KEYS ); - bSizer27->Add(m_spinBtn, 0, wxALIGN_CENTER_VERTICAL | wxEXPAND, 5 ); - - SetSizer(bSizer27); - Layout(); - - //connect events - m_spinBtn ->Connect(wxEVT_SCROLL_LINEUP, wxEventHandler (TimeSpanCtrl::OnSpinUp), nullptr, this); - m_spinBtn ->Connect(wxEVT_SCROLL_LINEDOWN, wxEventHandler (TimeSpanCtrl::OnSpinDown), nullptr, this); - m_textCtrl->Connect(wxEVT_KEY_DOWN, wxKeyEventHandler (TimeSpanCtrl::OnKeyPress), nullptr, this); - m_textCtrl->Connect(wxEVT_MOUSEWHEEL, wxMouseEventHandler(TimeSpanCtrl::OnMouseAction), nullptr, this); - - setValue(0); - } - - void setValue(int span) //unit: [s] - { - wxString newValue; - if (span < 0) - { - newValue += wxT("- "); - span = -span; - } - newValue += wxTimeSpan::Seconds(span).Format(FORMAT_TIMESPAN); - - long pos = m_textCtrl->GetInsertionPoint(); - pos += newValue.size() - m_textCtrl->GetValue().size(); - - m_textCtrl->ChangeValue(newValue); - m_textCtrl->SetInsertionPoint(pos); - - wxCommandEvent chgEvent(wxEVT_TIMESPAN_CHANGE); - wxPostEvent(this, chgEvent); - } - - int getValue() const - { - wxString textVal = m_textCtrl->GetValue(); - textVal.Trim(false); - - bool isNegative = false; - if (textVal.StartsWith(wxT("-"))) - { - isNegative = true; - textVal = textVal.substr(1); - } - textVal.Trim(false); - - wxDateTime tmp(time_t(0)); - if (tmp.ParseFormat(textVal, FORMAT_TIMESPAN, wxDateTime(tmp)) == nullptr) - return 0; - - return (isNegative ? -1 : 1) * - (tmp.GetHour () * 3600 + - tmp.GetMinute() * 60 + - tmp.GetSecond()); - } - -private: - void OnSpinUp (wxEvent& event) { spinValue(true); } - void OnSpinDown(wxEvent& event) { spinValue(false); } - - void OnKeyPress(wxKeyEvent& event) - { - const int keyCode = event.GetKeyCode(); - switch (keyCode) - { - case WXK_UP: - case WXK_NUMPAD_UP: - return spinValue(true); - case WXK_DOWN: - case WXK_NUMPAD_DOWN: - return spinValue(false); - default: - event.Skip(); - } - } - - void OnMouseAction(wxMouseEvent& event) - { - int delta = event.GetWheelRotation(); - if (delta > 0) - spinValue(true); - else if (delta < 0) - spinValue(false); - else - event.Skip(); - } - - void spinValue(bool up) - { - wxString textval = m_textCtrl->GetValue(); - long pos = m_textCtrl->GetInsertionPoint(); - - int stepSize = 1; - if (pos <= static_cast<long>(textval.size())) - { - int delimCount = std::count(textval.begin() + pos, textval.end(), wxT(':')); - if (delimCount == 1) - stepSize = 60; //minute - else if (delimCount == 2) - stepSize = 3600; //hour - } - - if (!up) - stepSize *= -1; - - setValue(getValue() + stepSize); - } - - wxTextCtrl* m_textCtrl; - wxSpinButton* m_spinBtn; - - const wxString FORMAT_TIMESPAN; -}; -} - - -#endif //TIMESPAN_H_254783756533456 +// *****************************************************************************
+// * 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 TIMESPAN_H_254783756533456
+#define TIMESPAN_H_254783756533456
+
+#include <wx/textctrl.h>
+#include <wx/datetime.h>
+#include <wx/spinbutt.h>
+#include <wx/sizer.h>
+
+//user friendly time span control
+//- constructor is compatible with a wxTextControl
+//- emits change event: wxEVT_TIMESPAN_CHANGE
+
+namespace zen
+{
+inline
+wxEventType getEventType()
+{
+ static wxEventType evt = wxNewEventType(); //external linkage!
+ return evt;
+}
+const wxEventType wxEVT_TIMESPAN_CHANGE = getEventType();
+
+
+class TimeSpanCtrl : public wxPanel
+{
+public:
+ TimeSpanCtrl(wxWindow* parent, wxWindowID id,
+ const wxString& value = {},
+ const wxPoint& pos = wxDefaultPosition,
+ const wxSize& size = wxDefaultSize,
+ long style = 0,
+ const wxValidator& validator = wxDefaultValidator,
+ const wxString& name = wxTextCtrlNameStr) :
+ wxPanel(parent, id, pos, size, style, name),
+ FORMAT_TIMESPAN(wxT("%H:%M:%S"))
+ {
+ wxBoxSizer* bSizer27 = new wxBoxSizer( wxHORIZONTAL );
+
+ m_textCtrl = new wxTextCtrl(this, wxID_ANY, wxString(), wxDefaultPosition, wxDefaultSize, wxTE_CENTRE );
+ bSizer27->Add(m_textCtrl, 1, wxALIGN_CENTER_VERTICAL | wxEXPAND, 5 );
+
+ m_spinBtn = new wxSpinButton(this, wxID_ANY, wxDefaultPosition, wxSize( 20, -1 ), wxSP_ARROW_KEYS );
+ bSizer27->Add(m_spinBtn, 0, wxALIGN_CENTER_VERTICAL | wxEXPAND, 5 );
+
+ SetSizer(bSizer27);
+ Layout();
+
+ //connect events
+ m_spinBtn ->Connect(wxEVT_SCROLL_LINEUP, wxEventHandler (TimeSpanCtrl::OnSpinUp), nullptr, this);
+ m_spinBtn ->Connect(wxEVT_SCROLL_LINEDOWN, wxEventHandler (TimeSpanCtrl::OnSpinDown), nullptr, this);
+ m_textCtrl->Connect(wxEVT_KEY_DOWN, wxKeyEventHandler (TimeSpanCtrl::OnKeyPress), nullptr, this);
+ m_textCtrl->Connect(wxEVT_MOUSEWHEEL, wxMouseEventHandler(TimeSpanCtrl::OnMouseAction), nullptr, this);
+
+ setValue(0);
+ }
+
+ void setValue(int span) //unit: [s]
+ {
+ wxString newValue;
+ if (span < 0)
+ {
+ newValue += wxT("- ");
+ span = -span;
+ }
+ newValue += wxTimeSpan::Seconds(span).Format(FORMAT_TIMESPAN);
+
+ long pos = m_textCtrl->GetInsertionPoint();
+ pos += newValue.size() - m_textCtrl->GetValue().size();
+
+ m_textCtrl->ChangeValue(newValue);
+ m_textCtrl->SetInsertionPoint(pos);
+
+ wxCommandEvent chgEvent(wxEVT_TIMESPAN_CHANGE);
+ wxPostEvent(this, chgEvent);
+ }
+
+ int getValue() const
+ {
+ wxString textVal = m_textCtrl->GetValue();
+ textVal.Trim(false);
+
+ bool isNegative = false;
+ if (textVal.StartsWith(wxT("-")))
+ {
+ isNegative = true;
+ textVal = textVal.substr(1);
+ }
+ textVal.Trim(false);
+
+ wxDateTime tmp(time_t(0));
+ if (tmp.ParseFormat(textVal, FORMAT_TIMESPAN, wxDateTime(tmp)) == nullptr)
+ return 0;
+
+ return (isNegative ? -1 : 1) *
+ (tmp.GetHour () * 3600 +
+ tmp.GetMinute() * 60 +
+ tmp.GetSecond());
+ }
+
+private:
+ void OnSpinUp (wxEvent& event) { spinValue(true); }
+ void OnSpinDown(wxEvent& event) { spinValue(false); }
+
+ void OnKeyPress(wxKeyEvent& event)
+ {
+ const int keyCode = event.GetKeyCode();
+ switch (keyCode)
+ {
+ case WXK_UP:
+ case WXK_NUMPAD_UP:
+ return spinValue(true);
+ case WXK_DOWN:
+ case WXK_NUMPAD_DOWN:
+ return spinValue(false);
+ default:
+ event.Skip();
+ }
+ }
+
+ void OnMouseAction(wxMouseEvent& event)
+ {
+ int delta = event.GetWheelRotation();
+ if (delta > 0)
+ spinValue(true);
+ else if (delta < 0)
+ spinValue(false);
+ else
+ event.Skip();
+ }
+
+ void spinValue(bool up)
+ {
+ wxString textval = m_textCtrl->GetValue();
+ long pos = m_textCtrl->GetInsertionPoint();
+
+ int stepSize = 1;
+ if (pos <= static_cast<long>(textval.size()))
+ {
+ int delimCount = std::count(textval.begin() + pos, textval.end(), wxT(':'));
+ if (delimCount == 1)
+ stepSize = 60; //minute
+ else if (delimCount == 2)
+ stepSize = 3600; //hour
+ }
+
+ if (!up)
+ stepSize *= -1;
+
+ setValue(getValue() + stepSize);
+ }
+
+ wxTextCtrl* m_textCtrl;
+ wxSpinButton* m_spinBtn;
+
+ const wxString FORMAT_TIMESPAN;
+};
+}
+
+
+#endif //TIMESPAN_H_254783756533456
diff --git a/wx+/toggle_button.h b/wx+/toggle_button.h index 9c3cb3f1..5457d986 100644..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 8dc79d73..b2946187 100644..100755 --- a/wx+/tooltip.cpp +++ b/wx+/tooltip.cpp @@ -1,104 +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); - -#ifdef ZEN_WIN //prevent window from stealing focus! - Disable(); //= dark/grey text and image on Linux; no visible difference on OS X -#endif - } - - 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_) - { -#ifdef ZEN_LINUX - //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; // -#else - tipWindow_->Hide(); -#endif - } -} +// *****************************************************************************
+// * 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 f2a7043e..1892ba23 100644..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 9858a3e9..8b2e2976 100644..100755 --- a/wx+/zlib_wrap.cpp +++ b/wx+/zlib_wrap.cpp @@ -1,54 +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" -#ifdef ZEN_WIN - #include <wx/../../src/zlib/zlib.h> //not really a "nice" place to look for a stable solution -#elif defined ZEN_LINUX || defined ZEN_MAC - #include <zlib.h> //let's pray this is the same version wxWidgets is linking against! -#endif - -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 0e596f85..11d8227b 100644..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 std::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 - std::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
|