diff options
author | B. Stack <bgstack15@gmail.com> | 2025-01-20 19:25:18 -0500 |
---|---|---|
committer | B. Stack <bgstack15@gmail.com> | 2025-01-20 19:25:18 -0500 |
commit | de65d3c0295894f8eafc4c7582dfe180dc58c81e (patch) | |
tree | 3ba8ec770b81468ca4ad83d985b991c5f669de22 /wx+ | |
parent | add upstream 13.9 (diff) | |
download | FreeFileSync-de65d3c0295894f8eafc4c7582dfe180dc58c81e.tar.gz FreeFileSync-de65d3c0295894f8eafc4c7582dfe180dc58c81e.tar.bz2 FreeFileSync-de65d3c0295894f8eafc4c7582dfe180dc58c81e.zip |
Diffstat (limited to 'wx+')
-rw-r--r-- | wx+/app_main.h | 1 | ||||
-rw-r--r-- | wx+/async_task.h | 1 | ||||
-rw-r--r-- | wx+/choice_enum.h | 127 | ||||
-rw-r--r-- | wx+/color_tools.h | 234 | ||||
-rw-r--r-- | wx+/context_menu.h | 3 | ||||
-rw-r--r-- | wx+/darkmode.cpp | 110 | ||||
-rw-r--r-- | wx+/darkmode.h | 28 | ||||
-rw-r--r-- | wx+/dc.h | 87 | ||||
-rw-r--r-- | wx+/file_drop.h | 1 | ||||
-rw-r--r-- | wx+/graph.cpp | 39 | ||||
-rw-r--r-- | wx+/graph.h | 33 | ||||
-rw-r--r-- | wx+/grid.cpp | 119 | ||||
-rw-r--r-- | wx+/grid.h | 7 | ||||
-rw-r--r-- | wx+/image_resources.cpp | 3 | ||||
-rw-r--r-- | wx+/image_tools.cpp | 66 | ||||
-rw-r--r-- | wx+/image_tools.h | 74 | ||||
-rw-r--r-- | wx+/no_flicker.h | 6 | ||||
-rw-r--r-- | wx+/popup_dlg.cpp | 1 | ||||
-rw-r--r-- | wx+/std_button_layout.h | 1 | ||||
-rw-r--r-- | wx+/tooltip.cpp | 2 | ||||
-rw-r--r-- | wx+/window_layout.h | 3 | ||||
-rw-r--r-- | wx+/window_tools.h | 2 |
22 files changed, 658 insertions, 290 deletions
diff --git a/wx+/app_main.h b/wx+/app_main.h index c1fd2441..fa8b1069 100644 --- a/wx+/app_main.h +++ b/wx+/app_main.h @@ -7,7 +7,6 @@ #ifndef APP_MAIN_H_08215601837818347575856 #define APP_MAIN_H_08215601837818347575856 -//#include <wx/window.h> #include <wx/app.h> diff --git a/wx+/async_task.h b/wx+/async_task.h index 85f0d9d0..9b4b6573 100644 --- a/wx+/async_task.h +++ b/wx+/async_task.h @@ -7,7 +7,6 @@ #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> diff --git a/wx+/choice_enum.h b/wx+/choice_enum.h index a25bf104..6b28b4f9 100644 --- a/wx+/choice_enum.h +++ b/wx+/choice_enum.h @@ -7,52 +7,33 @@ #ifndef CHOICE_ENUM_H_132413545345687 #define CHOICE_ENUM_H_132413545345687 -#include <unordered_map> -#include <vector> +//#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 { +//handle mapping of enum values to wxChoice controls template <class Enum> -struct EnumDescrList +class EnumDescrList { - EnumDescrList& add(Enum value, const wxString& text, const wxString& tooltip = {}) - { - descrList.push_back({value, {text, tooltip}}); - return *this; - } +public: + using DescrItem = std::tuple<Enum, wxString /*label*/, wxString /*tooltip*/>; + + EnumDescrList(wxChoice& ctrl, std::vector<DescrItem> list); + ~EnumDescrList(); - using DescrList = std::vector<std::pair<Enum, std::pair<wxString, wxString>>>; - DescrList descrList; + void set(Enum value); + Enum get() const ; + void updateTooltip(); //after user changed selection - std::unordered_map<const wxChoice*, std::vector<wxString>> labelsSetLast; + const std::vector<DescrItem>& getConfig() const { return descrList_; } + +private: + wxChoice& ctrl_; + const std::vector<DescrItem> descrList_; + std::vector<wxString> labels_; }; -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); @@ -69,61 +50,65 @@ template <class Enum> void updateTooltipEnumVal(const EnumDescrList<Enum>& mappi //--------------- impelementation ------------------------------------------- template <class Enum> -void setEnumVal(EnumDescrList<Enum>& mapping, wxChoice& ctrl, Enum value) +EnumDescrList<Enum>::EnumDescrList(wxChoice& ctrl, std::vector<DescrItem> list) : ctrl_(ctrl), descrList_(std::move(list)) { - auto& labelsSetLast = mapping.labelsSetLast[&ctrl]; + for (const auto& [val, label, tooltip] : descrList_) + labels_.push_back(label); - std::vector<wxString> labels; - for (const auto& [val, texts] : mapping.descrList) - labels.push_back(texts.first); + ctrl_.Set(labels_); //expensive as fuck! => only call when needed! +} - if (labels != labelsSetLast) - { - ctrl.Set(labels); //expensive as fuck! => only call when absolutely needed! - labelsSetLast = std::move(labels); - } - //----------------------------------------------------------------- - const auto it = std::find_if(mapping.descrList.begin(), mapping.descrList.end(), [&](const auto& mapItem) { return mapItem.first == value; }); - if (it != mapping.descrList.end()) +template <class Enum> inline +EnumDescrList<Enum>::~EnumDescrList() +{ +} + + +template <class Enum> +void EnumDescrList<Enum>::set(Enum value) +{ + const auto it = std::find_if(descrList_.begin(), descrList_.end(), [&](const auto& mapItem) { return std::get<Enum>(mapItem) == value; }); + if (it != descrList_.end()) { - if (const wxString& tooltip = it->second.second; - !tooltip.empty()) - ctrl.SetToolTip(tooltip); + const auto& [val, label, tooltip] = *it; + if (!tooltip.empty()) + ctrl_.SetToolTip(tooltip); else - ctrl.UnsetToolTip(); + ctrl_.UnsetToolTip(); - const int selectedPos = it - mapping.descrList.begin(); - ctrl.SetSelection(selectedPos); + const int selectedPos = it - descrList_.begin(); + ctrl_.SetSelection(selectedPos); } else assert(false); } + template <class Enum> -Enum getEnumVal(const EnumDescrList<Enum>& mapping, const wxChoice& ctrl) +Enum EnumDescrList<Enum>::get() const { - const int selectedPos = ctrl.GetSelection(); + const int selectedPos = ctrl_.GetSelection(); - if (0 <= selectedPos && selectedPos < std::ssize(mapping.descrList)) - return mapping.descrList[selectedPos].first; - else - { - assert(false); - return Enum(0); - } + if (0 <= selectedPos && selectedPos < std::ssize(descrList_)) + return std::get<Enum>(descrList_[selectedPos]); + + assert(false); + return Enum(0); } -template <class Enum> void updateTooltipEnumVal(const EnumDescrList<Enum>& mapping, wxChoice& ctrl) + +template <class Enum> +void EnumDescrList<Enum>::updateTooltip() { - const int selectedPos = ctrl.GetSelection(); + const int selectedPos = ctrl_.GetSelection(); - if (0 <= selectedPos && selectedPos < std::ssize(mapping.descrList)) + if (0 <= selectedPos && selectedPos < std::ssize(descrList_)) { - if (const auto& [text, tooltip] = mapping.descrList[selectedPos].second; - !tooltip.empty()) - ctrl.SetToolTip(tooltip); + const auto& [val, label, tooltip] = descrList_[selectedPos]; + if (!tooltip.empty()) + ctrl_.SetToolTip(tooltip); else - ctrl.UnsetToolTip(); + ctrl_.UnsetToolTip(); } else assert(false); } diff --git a/wx+/color_tools.h b/wx+/color_tools.h new file mode 100644 index 00000000..19d34caf --- /dev/null +++ b/wx+/color_tools.h @@ -0,0 +1,234 @@ +// ***************************************************************************** +// * This file is part of the FreeFileSync project. It is distributed under * +// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * +// ***************************************************************************** + +#ifndef COLOR_TOOLS_H_18301239864123785613 +#define COLOR_TOOLS_H_18301239864123785613 + +#include <zen/basic_math.h> +#include <wx/colour.h> + + +namespace zen +{ +inline +double srgbDecode(unsigned char c) //https://en.wikipedia.org/wiki/SRGB +{ + const double c_ = c / 255.0; + return c_ <= 0.04045 ? c_ / 12.92 : std::pow((c_ + 0.055) / 1.055, 2.4); +} + + +inline +unsigned char srgbEncode(double c) +{ + const double c_ = c <= 0.0031308 ? c * 12.92 : std::pow(c, 1 / 2.4) * 1.055 - 0.055; + return std::clamp<int>(std::round(c_ * 255), 0, 255); +} + + +inline //https://www.w3.org/WAI/GL/wiki/Relative_luminance +double relLuminance(double r, double g, double b) //input: gamma-decoded sRGB +{ + return 0.2126 * r + 0.7152 * g + 0.0722 * b; //= the Y part of CIEXYZ +} + + +inline +double relativeLuminance(const wxColor& col) //[0, 1] +{ + assert(col.Alpha() == wxALPHA_OPAQUE); + return relLuminance(srgbDecode(col.Red()), srgbDecode(col.Green()), srgbDecode(col.Blue())); +} + + +inline +double relativeContrast(const wxColor& c1, const wxColor& c2) +{ + //https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef + //https://snook.ca/technical/colour_contrast/colour.html + double lum1 = relativeLuminance(c1); + double lum2 = relativeLuminance(c2); + if (lum1 < lum2) + std::swap(lum1, lum2); + return (lum1 + 0.05) / (lum2 + 0.05); +} + + +namespace +{ +//get first color between [col1, colMax] (assuming direct line in decoded sRGB) where minimum contrast is satisfied against col2 +wxColor enhanceContrast(wxColor col1, wxColor colMax, const wxColor& col2, double contrastRatioMin) +{ + /* Caveat: macOS uses partially-transparent colors! e.g. #RGBA + wxSYS_COLOUR_GRAYTEXT #FFFFFF3F + wxSYS_COLOUR_WINDOWTEXT #FFFFFFD8 + wxSYS_COLOUR_WINDOW #171717FF */ + auto alphaBlend = [](const wxColor& front, const wxColor& back) + { + if (front.Alpha() == wxALPHA_OPAQUE) + return front; + + if (back.Alpha() != wxALPHA_OPAQUE) + { + assert(false); + return *wxRED; //make some noise + } + + auto calcChannel = [a = front.Alpha()](unsigned char f, unsigned char b) + { + return static_cast<unsigned char>(numeric::intDivRound(f * a + b * (255 - a), 255)); + }; + + return wxColor(calcChannel(front.Red (), back.Red ()), + calcChannel(front.Green(), back.Green()), + calcChannel(front.Blue (), back.Blue ())); + }; + col1 = alphaBlend(col1, col2); + colMax = alphaBlend(colMax, col2); + + //--------------------------------------------------------------- + assert(contrastRatioMin >= 4); //lower values (especially near 1) probably aren't sensible mathematically, also: W3C recommends >= 4.5 for base AA compliance + auto contrast = [](double lum1, double lum2) //input: relative luminance + { + if (lum1 < lum2) + std::swap(lum1, lum2); + return (lum1 + 0.05) / (lum2 + 0.05); + }; + const double r_1 = srgbDecode(col1.Red()); + const double g_1 = srgbDecode(col1.Green()); + const double b_1 = srgbDecode(col1.Blue()); + const double r_m = srgbDecode(colMax.Red()); + const double g_m = srgbDecode(colMax.Green()); + const double b_m = srgbDecode(colMax.Blue()); + + const double lum_1 = relLuminance(r_1, g_1, b_1); + const double lum_m = relLuminance(r_m, g_m, b_m); + const double lum_2 = relativeLuminance(col2); + + if (contrast(lum_1, lum_2) >= contrastRatioMin) + return col1; //nothing to do! + + if (contrast(lum_m, lum_2) <= contrastRatioMin) + { + assert(false); //problem! + return colMax; + } + + if (lum_m < lum_2) + contrastRatioMin = 1 / contrastRatioMin; + + const double lum_t = contrastRatioMin * (lum_2 + 0.05) - 0.05; //target luminance + const double t = (lum_t - lum_1) / (lum_m - lum_1); + + return wxColor(srgbEncode(t * (r_m - r_1) + r_1), + srgbEncode(t * (g_m - g_1) + g_1), + srgbEncode(t * (b_m - b_1) + b_1)); +} +} + +inline //get first color between [col1, white/black] (assuming direct line in decoded sRGB) where minimum contrast is satisfied against col2 +wxColor enhanceContrast(const wxColor& col1, const wxColor& col2, double contrastRatioMin) +{ + return enhanceContrast(col1, + relativeContrast(col2, *wxWHITE) > //equivalent to: relativeLuminance(col2) < 0.1791287847 + relativeContrast(col2, *wxBLACK) ? // + *wxWHITE : *wxBLACK, + col2, contrastRatioMin); +} + + +#if 0 +//toy sample code: gamma-encoded sRGB -> CIEXYZ -> CIELAB and back: input === output RGB color (verified) +wxColor colorConversion(const wxColor& col) +{ + assert(col.GetAlpha() == wxALPHA_OPAQUE); + const double r = srgbDecode(col.Red()); + const double g = srgbDecode(col.Green()); + const double b = srgbDecode(col.Blue()); + + //https://en.wikipedia.org/wiki/SRGB#Correspondence_to_CIE_XYZ_stimulus + const double x = 0.4124 * r + 0.3576 * g + 0.1805 * b; + const double y = 0.2126 * r + 0.7152 * g + 0.0722 * b; + const double z = 0.0193 * r + 0.1192 * g + 0.9505 * b; + //----------------------------------------------- + //https://en.wikipedia.org/wiki/CIELAB_color_space#Converting_between_CIELAB_and_CIEXYZ_coordinates + using numeric::power; + auto f = [](double t) + { + constexpr double delta = 6.0 / 29; + return t > power<3>(delta) ? + std::pow(t, 1.0 / 3) : + t / (3 * power<2>(delta)) + 4.0 / 29; + }; + const double L_ = 116 * f(y) - 16; //[ 0, 100] + const double a_ = 500 * (f(x / 0.950489) - f(y)); //[-128, 127] + const double b_ = 200 * (f(y) - f(z / 1.088840)); //[-128, 127] + //----------------------------------------------- + auto f_1 = [](double t) + { + constexpr double delta = 6.0 / 29; + return t > delta ? + power<3>(t) : + 3 * power<2>(delta) * (t - 4.0 / 29); + }; + const double x2 = 0.950489 * f_1((L_ + 16) / 116 + a_ / 500); + const double y2 = f_1((L_ + 16) / 116); + const double z2 = 1.088840 * f_1((L_ + 16) / 116 - b_ / 200); + //----------------------------------------------- + const double r2 = 3.2406255 * x2 + -1.5372080 * y2 + -0.4986286 * z2; + const double g2 = -0.9689307 * x2 + 1.8757561 * y2 + 0.0415175 * z2; + const double b2 = 0.0557101 * x2 + -0.2040211 * y2 + 1.0569959 * z2; + + return wxColor(srgbEncode(r2), srgbEncode(g2), srgbEncode(b2)); +} + + +//https://en.wikipedia.org/wiki/HSL_and_HSV +wxColor hsvColor(double h, double s, double v) //h within [0, 360), s, v within [0, 1] +{ + //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; + s = std::clamp(s, 0.0, 1.0); + v = std::clamp(v, 0.0, 1.0); + //------------------------------------ + const int h_i = h / 60; + const float f = h / 60 - h_i; + + auto to8Bit = [](double val) -> unsigned char + { + return std::clamp<int>(std::round(val * 255), 0, 255); + }; + + const unsigned char p = to8Bit(v * (1 - s)); + const unsigned char q = to8Bit(v * (1 - s * f)); + const unsigned char t = to8Bit(v * (1 - s * (1 - f))); + const unsigned char vi = to8Bit(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 +} + +#endif //COLOR_TOOLS_H_18301239864123785613 diff --git a/wx+/context_menu.h b/wx+/context_menu.h index 1e2d4d69..06e84709 100644 --- a/wx+/context_menu.h +++ b/wx+/context_menu.h @@ -7,7 +7,6 @@ #ifndef CONTEXT_MENU_H_18047302153418174632141234 #define CONTEXT_MENU_H_18047302153418174632141234 -//#include <map> #include <vector> #include <functional> #include <wx/app.h> @@ -87,7 +86,7 @@ public: 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 + //eventually all events from submenu items will be received by the parent menu for (const auto& [itemId, command] : commandList_) menu_->Bind(wxEVT_COMMAND_MENU_SELECTED, [command /*clang bug*/= command](wxCommandEvent& event) { command(); }, itemId); diff --git a/wx+/darkmode.cpp b/wx+/darkmode.cpp new file mode 100644 index 00000000..c8a7ea28 --- /dev/null +++ b/wx+/darkmode.cpp @@ -0,0 +1,110 @@ +// ***************************************************************************** +// * This file is part of the FreeFileSync project. It is distributed under * +// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * +// ***************************************************************************** + +#include "darkmode.h" +#include <zen/sys_version.h> +#include <wx/settings.h> +#include "color_tools.h" + #include <gtk/gtk.h> + +using namespace zen; + + +bool zen::darkModeAvailable() +{ + +#if GTK_MAJOR_VERSION == 2 + return false; +#elif GTK_MAJOR_VERSION >= 3 + return true; +#else +#error unknown GTK version! +#endif + +} + + +namespace +{ +class SysColorsHook : public wxColorHook +{ +public: + + wxColor getColor(wxSystemColour index) const override + { + //fix contrast e.g. Ubuntu's Adwaita-Dark theme and macOS dark mode: + if (index == wxSYS_COLOUR_GRAYTEXT) + return colGreyTextEnhContrast_; +#if 0 + auto colToString = [](wxColor c) + { + const auto& [rh, rl] = hexify(c.Red ()); + const auto& [gh, gl] = hexify(c.Green()); + const auto& [bh, bl] = hexify(c.Blue ()); + const auto& [ah, al] = hexify(c.Alpha()); + return "#" + std::string({rh, rl, gh, gl, bh, bl, ah, al}); + }; + std::cerr << "wxSYS_COLOUR_GRAYTEXT " << colToString(wxSystemSettingsNative::GetColour(wxSYS_COLOUR_GRAYTEXT)) << "\n"; +#endif + return wxSystemSettingsNative::GetColour(index); //fallback + } + +private: + const wxColor colGreyTextEnhContrast_ = + enhanceContrast(wxSystemSettingsNative::GetColour(wxSYS_COLOUR_GRAYTEXT), + wxSystemSettingsNative::GetColour(wxSYS_COLOUR_WINDOWTEXT), + wxSystemSettingsNative::GetColour(wxSYS_COLOUR_WINDOW), 4.5 /*contrastRatioMin*/); //W3C recommends >= 4.5 +}; + + +std::optional<bool> globalDefaultThemeIsDark; +} + + +void zen::colorThemeInit(wxApp& app, ColorTheme colTheme) //throw FileError +{ + assert(!refGlobalColorHook()); + + globalDefaultThemeIsDark = wxSystemSettings::GetAppearance().AreAppsDark(); + ZEN_ON_SCOPE_EXIT(if (!refGlobalColorHook()) refGlobalColorHook() = std::make_unique<SysColorsHook>()); //*after* SetAppearance() and despite errors + + //caveat: on macOS there are more themes than light/dark: https://developer.apple.com/documentation/appkit/nsappearance/name-swift.struct + if (colTheme != ColorTheme::System && //"System" is already the default for macOS/Linux(GTK3) + darkModeAvailable()) + changeColorTheme(colTheme); //throw FileError +} + + +void zen::colorThemeCleanup() +{ + assert(refGlobalColorHook()); + refGlobalColorHook().reset(); +} + + +bool zen::equalAppearance(ColorTheme colTheme1, ColorTheme colTheme2) +{ + if (colTheme1 == ColorTheme::System) colTheme1 = *globalDefaultThemeIsDark ? ColorTheme::Dark : ColorTheme::Light; + if (colTheme2 == ColorTheme::System) colTheme2 = *globalDefaultThemeIsDark ? ColorTheme::Dark : ColorTheme::Light; + return colTheme1 == colTheme2; +} + + +void zen::changeColorTheme(ColorTheme colTheme) //throw FileError +{ + if (colTheme == ColorTheme::System) //SetAppearance(System) isn't working reliably! surprise!? + colTheme = *globalDefaultThemeIsDark ? ColorTheme::Dark : ColorTheme::Light; + + try + { + ZEN_ON_SCOPE_SUCCESS(refGlobalColorHook() = std::make_unique<SysColorsHook>()); //*after* SetAppearance() + if (wxApp::AppearanceResult rv = wxTheApp->SetAppearance(colTheme); + rv != wxApp::AppearanceResult::Ok) + throw SysError(formatSystemError("wxApp::SetAppearance", + rv == wxApp::AppearanceResult::CannotChange ? L"CannotChange" : L"Failure", L"" /*errorMsg*/)); + } + catch (const SysError& e) { throw FileError(_("Failed to update the color theme."), e.toString()); } +} diff --git a/wx+/darkmode.h b/wx+/darkmode.h new file mode 100644 index 00000000..91d2a789 --- /dev/null +++ b/wx+/darkmode.h @@ -0,0 +1,28 @@ +// ***************************************************************************** +// * This file is part of the FreeFileSync project. It is distributed under * +// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * +// ***************************************************************************** + +#ifndef DARKMODE_H_754298057018 +#define DARKMODE_H_754298057018 + +#include <zen/file_error.h> +#include <wx/app.h> + + +namespace zen +{ +bool darkModeAvailable(); + +//support not only "dark mode" but dark themes in general +using ColorTheme = wxApp::Appearance; //why reinvent the wheel? + +void colorThemeInit(wxApp& app, ColorTheme colTheme); //throw FileError +void colorThemeCleanup(); + +bool equalAppearance(ColorTheme colTheme1, ColorTheme colTheme2); +void changeColorTheme(ColorTheme colTheme); //throw FileError +} + +#endif //DARKMODE_H_754298057018 @@ -9,29 +9,12 @@ #include <unordered_map> #include <optional> -//#include <zen/basic_math.h> #include <wx/dcbuffer.h> //for macro: wxALWAYS_NATIVE_DOUBLE_BUFFER #include <wx/dcscreen.h> -//#include <wx/bmpbndl.h> - // #include <gtk/gtk.h> namespace zen { -/* 1. wxDCClipper does *not* stack: another fix for yet another poor wxWidgets implementation - - class RecursiveDcClipper - { - RecursiveDcClipper(wxDC& dc, const wxRect& r) - }; - - 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) - }; */ - inline void clearArea(wxDC& dc, const wxRect& rect, const wxColor& col) { @@ -176,44 +159,68 @@ wxRect getIntersection(const wxRect& rect1, const wxRect& rect2) //---------------------- implementation ------------------------ -class RecursiveDcClipper +class RecursiveDcClipper //wxDCClipper does *not* stack => fix for yet another poor wxWidgets implementation: { public: RecursiveDcClipper(wxDC& dc, const wxRect& r) : dc_(dc) { - if (auto it = clippingAreas_.find(&dc_); + if (auto it = clippingAreas_.find(&dc); it != clippingAreas_.end()) { oldRect_ = it->second; const wxRect tmp = getIntersection(r, *oldRect_); //better safe than sorry - assert(!tmp.IsEmpty()); //"setting an empty clipping region is equivalent to DestroyClippingRegion()" - dc_.SetClippingRegion(tmp); //new clipping region is intersection of given and previously set regions - it->second = tmp; + if (tmp != *oldRect_) + { + dc.SetClippingRegion(tmp); //new clipping region is intersection of given and previously set regions + it->second = tmp; + clippingDone = true; + } } else { - //caveat: actual clipping region is smaller when rect is not fully inside the DC + const wxRect dcArea(dc.GetSize()); + + //since wxWidgets 3.3.0 the DC may be pre-clipped to wxDC::GetSize() or smaller (related to double-buffering) + //=> consider "no clipping" and "clipped to wxDC::GetSize()" equivalent! + wxRect rectClip; + if (dc.GetClippingBox(rectClip)) + { + rectClip = getIntersection(rectClip, dcArea); + if (rectClip != dcArea) + oldRect_ = rectClip; + } + + //caveat: actual clipping region is smaller when rect is partially outside the DC //=> ensure consistency for validateClippingBuffer() - const wxRect tmp = getIntersection(r, wxRect(dc.GetSize())); + const wxRect tmp = getIntersection(r, oldRect_? *oldRect_ : dcArea); + assert(!tmp.IsEmpty()); - dc_.SetClippingRegion(tmp); - clippingAreas_.emplace(&dc_, tmp); + if (tmp != (oldRect_? *oldRect_ : dcArea)) + { + dc.SetClippingRegion(tmp); + clippingAreas_.emplace(&dc, tmp); + clippingDone = true; + recursionBegin_ = true; + } } } ~RecursiveDcClipper() { - dc_.DestroyClippingRegion(); - if (oldRect_) + if (clippingDone) { - dc_.SetClippingRegion(*oldRect_); - clippingAreas_[&dc_] = *oldRect_; + dc_.DestroyClippingRegion(); + if (oldRect_) + dc_.SetClippingRegion(*oldRect_); + + if (recursionBegin_) + clippingAreas_.erase(&dc_); + else + clippingAreas_[&dc_] = *oldRect_; } - else - clippingAreas_.erase(&dc_); } private: @@ -224,25 +231,21 @@ private: //associate "active" clipping area with each DC inline static std::unordered_map<wxDC*, wxRect> clippingAreas_; + bool recursionBegin_ = false; + bool clippingDone = false; std::optional<wxRect> oldRect_; wxDC& dc_; }; -#ifndef wxALWAYS_NATIVE_DOUBLE_BUFFER - #error we need this one! -#endif - -//CAVEAT: wxPaintDC on wxGTK/wxMAC does not implement SetLayoutDirection()!!! => GetLayoutDirection() == wxLayout_Default -#if wxALWAYS_NATIVE_DOUBLE_BUFFER -struct BufferedPaintDC : public wxPaintDC { BufferedPaintDC(wxWindow& wnd, std::optional<wxBitmap>& buffer) : wxPaintDC(&wnd) {} }; - -#else +//fix wxBufferedPaintDC: happily fucks up for RTL layout by not drawing the first column (x = 0)! class BufferedPaintDC : public wxMemoryDC { public: BufferedPaintDC(wxWindow& wnd, std::optional<wxBitmap>& buffer) : buffer_(buffer), paintDc_(&wnd) { + assert(!wnd.IsDoubleBuffered()); + const wxSize clientSize = wnd.GetClientSize(); if (clientSize.GetWidth() > 0 && clientSize.GetHeight() > 0) //wxBitmap asserts this!! width can be 0; test case "Grid::CornerWin": compare both sides, then change config { @@ -254,6 +257,7 @@ public: SelectObject(*buffer); //copies scale factor from wxBitmap + //note: wxPaintDC on wxGTK/wxMAC does not implement SetLayoutDirection()!!! => GetLayoutDirection() == wxLayout_Default if (paintDc_.IsOk() && paintDc_.GetLayoutDirection() == wxLayout_RightToLeft) SetLayoutDirection(wxLayout_RightToLeft); } @@ -283,7 +287,6 @@ private: std::optional<wxBitmap>& buffer_; wxPaintDC paintDc_; }; -#endif } #endif //DC_H_4987123956832143243214 diff --git a/wx+/file_drop.h b/wx+/file_drop.h index e9cb9e72..2474ae45 100644 --- a/wx+/file_drop.h +++ b/wx+/file_drop.h @@ -8,7 +8,6 @@ #define FILE_DROP_H_09457802957842560325626 #include <vector> -//#include <functional> #include <zen/zstring.h> #include <wx/window.h> #include <wx/event.h> diff --git a/wx+/graph.cpp b/wx+/graph.cpp index da5c1d98..e589976d 100644 --- a/wx+/graph.cpp +++ b/wx+/graph.cpp @@ -7,15 +7,14 @@ #include "graph.h" #include <cassert> #include <algorithm> -//#include <numeric> #include <zen/basic_math.h> #include <zen/scope_guard.h> -//#include <zen/perf.h> using namespace zen; -//todo: support zoom via mouse wheel? + +//TODO: support zoom via mouse wheel? namespace zen { @@ -139,7 +138,8 @@ int widenRange(double& valMin, double& valMax, //in/out } -void drawXLabel(wxDC& dc, double xMin, double xMax, int blockCount, const ConvertCoord& cvrtX, const wxRect& graphArea, const wxRect& labelArea, const LabelFormatter& labelFmt) +void drawXLabel(wxDC& dc, double xMin, double xMax, int blockCount, const ConvertCoord& cvrtX, const wxRect& graphArea, + const wxRect& labelArea, const LabelFormatter& labelFmt, const wxColor& colGridLine) { assert(graphArea.width == labelArea.width && graphArea.x == labelArea.x); if (blockCount <= 0) @@ -153,7 +153,7 @@ void drawXLabel(wxDC& dc, double xMin, double xMax, int blockCount, const Conver const int x = graphArea.x + cvrtX.realToScreenRound(valX); //draw grey vertical lines - clearArea(dc, {x - dipToWxsize(1) / 2, graphArea.y, dipToWxsize(1), graphArea.height}, wxColor(192, 192, 192)); //light grey => not accessible! but no big deal... + clearArea(dc, {x - dipToWxsize(1) / 2, graphArea.y, dipToWxsize(1), graphArea.height}, colGridLine); //draw x axis labels const wxString label = labelFmt.formatText(valX, valRangePerBlock); @@ -163,7 +163,8 @@ void drawXLabel(wxDC& dc, double xMin, double xMax, int blockCount, const Conver } -void drawYLabel(wxDC& dc, double yMin, double yMax, int blockCount, const ConvertCoord& cvrtY, const wxRect& graphArea, const wxRect& labelArea, const LabelFormatter& labelFmt) +void drawYLabel(wxDC& dc, double yMin, double yMax, int blockCount, const ConvertCoord& cvrtY, const wxRect& graphArea, + const wxRect& labelArea, const LabelFormatter& labelFmt, const wxColor& colGridLine) { assert(graphArea.height == labelArea.height && graphArea.y == labelArea.y); if (blockCount <= 0) @@ -177,7 +178,7 @@ void drawYLabel(wxDC& dc, double yMin, double yMax, int blockCount, const Conver 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); - clearArea(dc, {graphArea.x, y - dipToWxsize(1) / 2, graphArea.width, dipToWxsize(1)}, wxColor(192, 192, 192)); //light grey => not accessible! but no big deal... + clearArea(dc, {graphArea.x, y - dipToWxsize(1) / 2, graphArea.width, dipToWxsize(1)}, colGridLine); //draw y axis labels const wxString label = labelFmt.formatText(valY, valRangePerBlock); @@ -436,13 +437,13 @@ Graph2D::Graph2D(wxWindow* parent, long style, const wxString& name) : wxPanel(parent, winid, pos, size, style, name) { +#ifdef FFS_CUSTOM_DOUBLE_BUFFERING + MSWDisableComposited(); +#endif + //https://wiki.wxwidgets.org/Flicker-Free_Drawing + SetBackgroundStyle(wxBG_STYLE_PAINT); //get's rid of needless wxEVT_ERASE_BACKGROUND Bind(wxEVT_PAINT, [this](wxPaintEvent& event) { onPaintEvent(event); }); - Bind(wxEVT_SIZE, [this](wxSizeEvent& event) { Refresh(); event.Skip(); }); - Bind(wxEVT_ERASE_BACKGROUND, [](wxEraseEvent& event) {}); //https://wiki.wxwidgets.org/Flicker-Free_Drawing - - //SetDoubleBuffered(true); slow as hell! - - SetBackgroundStyle(wxBG_STYLE_PAINT); + Bind(wxEVT_SIZE, [this](wxSizeEvent& event) { Refresh(); event.Skip(); }); Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& event) { onMouseLeftDown(event); }); Bind(wxEVT_MOTION, [this](wxMouseEvent& event) { onMouseMovement(event); }); @@ -453,8 +454,12 @@ Graph2D::Graph2D(wxWindow* parent, void Graph2D::onPaintEvent(wxPaintEvent& event) { - //wxAutoBufferedPaintDC dc(this); -> this one happily fucks up for RTL layout by not drawing the first column (x = 0)! +#ifdef FFS_CUSTOM_DOUBLE_BUFFERING BufferedPaintDC dc(*this, doubleBuffer_); +#else + wxPaintDC dc(this); + static_assert(wxALWAYS_NATIVE_DOUBLE_BUFFER); +#endif render(dc); } @@ -569,7 +574,7 @@ void Graph2D::render(wxDC& dc) const assert(attr_.yLabelpos == YLabelPos::none || attr_.labelFmtY); //paint graph background (excluding label area) - drawFilledRectangle(dc, graphArea, attr_.colorBack, getBorderColor(), dipToWxsize(1)); + drawFilledRectangle(dc, graphArea, attr_.colorBack, attr_.colorGridLine, dipToWxsize(1)); graphArea.Deflate(dipToWxsize(1)); //set label areas respecting graph area border! @@ -759,8 +764,8 @@ void Graph2D::render(wxDC& dc) const } //3. draw labels and background grid - if (attr_.labelFmtX) drawXLabel(dc, minX, maxX, blockCountX, cvrtX, graphArea, xLabelArea, *attr_.labelFmtX); - if (attr_.labelFmtY) drawYLabel(dc, minY, maxY, blockCountY, cvrtY, graphArea, yLabelArea, *attr_.labelFmtY); + if (attr_.labelFmtX) drawXLabel(dc, minX, maxX, blockCountX, cvrtX, graphArea, xLabelArea, *attr_.labelFmtX, attr_.colorGridLine); + if (attr_.labelFmtY) drawYLabel(dc, minY, maxY, blockCountY, cvrtY, graphArea, yLabelArea, *attr_.labelFmtY, attr_.colorGridLine); //4. finally draw curves { diff --git a/wx+/graph.h b/wx+/graph.h index d7ea51b5..1bd06635 100644 --- a/wx+/graph.h +++ b/wx+/graph.h @@ -14,6 +14,7 @@ #include <wx/settings.h> #include <wx/bitmap.h> #include <zen/string_tools.h> +#include "color_tools.h" #include "dc.h" @@ -218,11 +219,16 @@ public: void addCurve(const SharedRef<CurveData>& data, const CurveAttributes& ca = CurveAttributes()); void clearCurves() { curves_.clear(); } - static wxColor getBorderColor() { return {130, 135, 144}; } //medium grey, the same Win7 uses for other frame borders => not accessible! but no big deal... - class MainAttributes { public: + MainAttributes() + { + //accessibility: consider system text and background colors; + //small drawback: color of graphs is NOT related to the background! => responsibility of client to use correct colors + setBaseColors(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT), + wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + } MainAttributes& setMinX(double newMinX) { minX = newMinX; return *this; } MainAttributes& setMaxX(double newMaxX) { maxX = newMaxX; return *this; } @@ -248,8 +254,16 @@ public: MainAttributes& setCornerText(const wxString& txt, GraphCorner pos) { cornerTexts[pos] = txt; return *this; } - //accessibility: always set both colors - MainAttributes& setBaseColors(const wxColor& text, const wxColor& back) { colorText = text; colorBack = back; return *this; } + MainAttributes& setBaseColors(const wxColor& text, const wxColor& back) //accessibility: always set both colors + { + colorText = text; + colorBack = back; + colorGridLine = enhanceContrast(colorBack, //start with back color and deviate only as little as required + colorText, colorBack, 4 /*contrastRatioMin*/); //W3C recommends >= 4.5 for text + return *this; + } + + wxColor getGridLineColor() const { return colorGridLine; } MainAttributes& setSelectionMode(GraphSelMode mode) { mouseSelMode = mode; return *this; } @@ -272,10 +286,9 @@ public: std::map<GraphCorner, wxString> cornerTexts; - //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 - wxColor colorText = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); - wxColor colorBack = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); + wxColor colorText; + wxColor colorBack; + wxColor colorGridLine; GraphSelMode mouseSelMode = GraphSelMode::rect; }; @@ -324,9 +337,9 @@ private: MainAttributes attr_; //global attributes - std::optional<wxBitmap> doubleBuffer_; - std::vector<std::pair<SharedRef<CurveData>, CurveAttributes>> curves_; + + std::optional<wxBitmap> doubleBuffer_; }; } diff --git a/wx+/grid.cpp b/wx+/grid.cpp index 3665c6cf..147feef0 100644 --- a/wx+/grid.cpp +++ b/wx+/grid.cpp @@ -6,19 +6,17 @@ #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/basic_math.h> #include <zen/string_tools.h> #include <zen/scope_guard.h> #include <zen/utf.h> #include <zen/zstring.h> #include <zen/format_unit.h> +#include "color_tools.h" #include "dc.h" #include <gtk/gtk.h> @@ -26,6 +24,16 @@ using namespace zen; +/* wxWidgets 3.3 defaults to system-powered double-buffering (WS_EX_COMPOSITED) on Windows: + => ~60% higher CPU time (test case: scrolling large file list via keyboard) :(( + => opt-out! + + "wxMSW now uses double buffering by default, meaning that updating the + windows using wxClientDC doesn't work any longer, which is consistent with + the behaviour of wxGTK with Wayland backend and of wxOSX, but not with the + traditional historic behaviour of wxMSW (or wxGTK/X11). You may call + MSWDisableComposited() to restore the previous behaviour [...]" */ + //let's NOT create wxWidgets objects statically: wxColor GridData::getColorSelectionGradientFrom() { return {137, 172, 255}; } //blue: HSL: 158, 255, 196 HSV: 222, 0.46, 1 wxColor GridData::getColorSelectionGradientTo () { return {225, 234, 255}; } // HSL: 158, 255, 240 HSV: 222, 0.12, 1 @@ -36,14 +44,28 @@ int GridData::getColumnGapLeft() { return dipToWxsize(4); } namespace { //------------------------------ Grid Parameters -------------------------------- -inline wxColor getColorLabelText(bool enabled) { return wxSystemSettings::GetColour(enabled ? wxSYS_COLOUR_BTNTEXT : wxSYS_COLOUR_GRAYTEXT); } -inline wxColor getColorGridLine() { return wxSystemSettings::GetColour(wxSYS_COLOUR_BTNSHADOW); } +wxColor getColorLabelText(bool enabled) { return wxSystemSettings::GetColour(enabled ? wxSYS_COLOUR_BTNTEXT : wxSYS_COLOUR_GRAYTEXT); } +wxColor getColorGridLine() { return wxSystemSettings::GetColour(wxSYS_COLOUR_BTNSHADOW); } + +wxColor getColorLabelGradientFrom() +{ + if (wxSystemSettings::GetAppearance().IsDark()) //upper gradient part must always be lighter than lower part! + { + const wxColor backCol = wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE); -inline wxColor getColorLabelGradientFrom() { return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); } -inline wxColor getColorLabelGradientTo () { return wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE); } + auto liftChannel = [](unsigned char c) { return static_cast<unsigned char>(std::clamp(c + 25, 0, 255)); }; -inline wxColor getColorLabelGradientFocusFrom() { return getColorLabelGradientFrom(); } -inline wxColor getColorLabelGradientFocusTo () { return GridData::getColorSelectionGradientFrom(); } + return wxColor(liftChannel(backCol.Red ()), + liftChannel(backCol.Green()), + liftChannel(backCol.Blue ())); + } + else + return wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); +} +wxColor getColorLabelGradientTo() { return wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE); } + +wxColor getColorLabelGradientFocusFrom() { return wxSystemSettings::GetAppearance().IsDark() ? wxSystemSettings::GetColour(wxSYS_COLOUR_BTNHIGHLIGHT) : getColorLabelGradientFrom(); } +wxColor getColorLabelGradientFocusTo () { return wxSystemSettings::GetAppearance().IsDark() ? getColorLabelGradientTo() : GridData::getColorSelectionGradientFrom(); } const double MOUSE_DRAG_ACCELERATION_DIP = 1.5; //unit: [rows / (DIP * sec)] -> same value like Explorer! const int DEFAULT_COL_LABEL_BORDER_DIP = 6; //top + bottom border in addition to label height @@ -127,7 +149,7 @@ void GridData::renderCell(wxDC& dc, const wxRect& rect, size_t row, ColumnType c } -int GridData::getBestSize(wxDC& dc, size_t row, ColumnType colType) +int GridData::getBestSize(const wxReadOnlyDC& dc, size_t row, ColumnType colType) { return dc.GetTextExtent(getValue(row, colType)).GetWidth() + 2 * getColumnGapLeft() + dipToWxsize(1); //gap on left and right side + border } @@ -264,10 +286,10 @@ public: wxWindow(&parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxWANTS_CHARS | wxBORDER_NONE, wxASCII_STR(wxPanelNameStr)), parent_(parent) { + //https://wiki.wxwidgets.org/Flicker-Free_Drawing + SetBackgroundStyle(wxBG_STYLE_PAINT); //get's rid of needless wxEVT_ERASE_BACKGROUND Bind(wxEVT_PAINT, [this](wxPaintEvent& event) { onPaintEvent(event); }); Bind(wxEVT_SIZE, [this](wxSizeEvent& event) { Refresh(); event.Skip(); }); - Bind(wxEVT_ERASE_BACKGROUND, [](wxEraseEvent& event) {}); //https://wiki.wxwidgets.org/Flicker-Free_Drawing - SetBackgroundStyle(wxBG_STYLE_PAINT); //SetDoubleBuffered(true); -> slow as hell! Bind(wxEVT_CHILD_FOCUS, [](wxChildFocusEvent& event) {}); //wxGTK::wxScrolledWindow automatically scrolls to child window when child gets focus -> prevent! @@ -376,9 +398,13 @@ private: 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_); +#ifdef FFS_CUSTOM_DOUBLE_BUFFERING + BufferedPaintDC dc(*this, doubleBuffer_); +#else + wxPaintDC dc(this); + static_assert(wxALWAYS_NATIVE_DOUBLE_BUFFER); +#endif assert(GetSize() == GetClientSize()); const wxRegion& updateReg = GetUpdateRegion(); @@ -387,7 +413,9 @@ private: } Grid& parent_; +#ifdef FFS_CUSTOM_DOUBLE_BUFFERING std::optional<wxBitmap> doubleBuffer_; +#endif int mouseRotateRemainder_ = 0; }; @@ -442,8 +470,7 @@ public: int getBestWidth(ptrdiff_t rowFrom, ptrdiff_t rowTo) { - wxClientDC dc(this); - + wxInfoDC dc(this); dc.SetFont(GetFont()); //harmonize with RowLabelWin::render()! int bestWidth = 0; @@ -676,9 +703,9 @@ private: const int markerWidth = dipToWxsize(COLUMN_MOVE_MARKER_WIDTH_DIP); if (col + 1 == activeClickOrMove_->refColumnTo()) //handle pos 1, 2, .. up to "at end" position - dc.GradientFillLinear(wxRect(rect.x + rect.width - markerWidth, rect.y, markerWidth, rect.height), getColorLabelGradientFrom(), *wxBLUE, wxSOUTH); + dc.GradientFillLinear(wxRect(rect.x + rect.width - markerWidth, rect.y, markerWidth, rect.height), getColorLabelGradientFrom(), colDropMarkerColor_, wxSOUTH); else if (col == activeClickOrMove_->refColumnTo() && col == 0) //pos 0 - dc.GradientFillLinear(wxRect(rect.GetTopLeft(), wxSize(markerWidth, rect.height)), getColorLabelGradientFrom(), *wxBLUE, wxSOUTH); + dc.GradientFillLinear(wxRect(rect.GetTopLeft(), wxSize(markerWidth, rect.height)), getColorLabelGradientFrom(), colDropMarkerColor_, wxSOUTH); } } } @@ -925,14 +952,13 @@ private: int colLabelHeight_ = 0; const wxFont labelFont_; + + const wxColor colDropMarkerColor_ = enhanceContrast(*wxBLUE, //primarily needed for dark mode! + wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT), + getColorLabelGradientTo(), 5 /*contrastRatioMin*/); //W3C recommends >= 4.5 }; //---------------------------------------------------------------------------------------------------------------- -namespace -{ -wxDEFINE_EVENT(EVENT_GRID_HAS_SCROLLED, wxCommandEvent); -} -//---------------------------------------------------------------------------------------------------------------- class Grid::MainWin : public SubWindow { @@ -943,8 +969,6 @@ public: rowLabelWin_(rowLabelWin), colLabelWin_(colLabelWin) { - Bind(EVENT_GRID_HAS_SCROLLED, [this](wxCommandEvent& event) { onRequestWindowUpdate(event); }); - Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { if (event.GetKeyCode() == WXK_ESCAPE && activeSelection_) //allow Escape key to cancel active selection! @@ -1082,7 +1106,7 @@ private: { if (0 <= row && row < rowCount && cpi.colType != ColumnType::none) { - wxClientDC dc(this); + wxInfoDC dc(this); dc.SetFont(GetFont()); return prov->getMouseHover(dc, row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth); } @@ -1116,7 +1140,7 @@ private: { if (0 <= row && row < rowCount && cpi.colType != ColumnType::none) { - wxClientDC dc(this); + wxInfoDC dc(this); dc.SetFont(GetFont()); return prov->getMouseHover(dc, row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth); } @@ -1213,7 +1237,7 @@ private: { if (0 <= row && row < rowCount && cpi.colType != ColumnType::none) { - wxClientDC dc(this); + wxInfoDC dc(this); dc.SetFont(GetFont()); return prov->getMouseHover(dc, row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth); } @@ -1247,7 +1271,7 @@ private: { if (0 <= row && row < rowCount && cpi.colType != ColumnType::none) { - wxClientDC dc(this); + wxInfoDC dc(this); dc.SetFont(GetFont()); return prov->getMouseHover(dc, row, cpi.colType, cpi.cellRelativePosX, cpi.colWidth); } @@ -1403,17 +1427,16 @@ private: if (!gridUpdatePending_) //without guarding, the number of outstanding async events can become very high during scrolling!! test case: Ubuntu: 170; Windows: 20 { gridUpdatePending_ = true; - GetEventHandler()->AddPendingEvent(wxCommandEvent(EVENT_GRID_HAS_SCROLLED)); //asynchronously call updateAfterScroll() - } - } - void onRequestWindowUpdate(wxEvent& event) - { - assert(gridUpdatePending_); - ZEN_ON_SCOPE_EXIT(gridUpdatePending_ = false); + GetEventHandler()->CallAfter([this] + { + refParent().updateWindowSizes(false); //row label width has changed -> do *not* update scrollbars: recursion on wxGTK! -> still a problem, now that this function is called async?? + rowLabelWin_.Update(); //update while dragging scroll thumb - refParent().updateWindowSizes(false); //row label width has changed -> do *not* update scrollbars: recursion on wxGTK! -> still a problem, now that this function is called async?? - rowLabelWin_.Update(); //update while dragging scroll thumb + assert(gridUpdatePending_); + gridUpdatePending_ = false; + }); + } } void refreshRow(size_t row) @@ -1474,6 +1497,13 @@ Grid::Grid(wxWindow* parent, colLabelWin_ = new ColLabelWin(*this); // mainWin_ = new MainWin (*this, *rowLabelWin_, *colLabelWin_); // +#ifdef FFS_CUSTOM_DOUBLE_BUFFERING + cornerWin_ ->MSWDisableComposited(); + rowLabelWin_->MSWDisableComposited(); + colLabelWin_->MSWDisableComposited(); + mainWin_ ->MSWDisableComposited(); +#endif + SetTargetWindow(mainWin_); SetInitialSize(size); //"Most controls will use this to set their initial size" -> why not @@ -1481,11 +1511,12 @@ Grid::Grid(wxWindow* parent, assert(GetClientSize() == GetSize() && GetWindowBorderSize() == wxSize()); //borders are NOT allowed for Grid //reason: updateWindowSizes() wants to use "GetSize()" as a "GetClientSize()" including scrollbars - Bind(wxEVT_PAINT, [this](wxPaintEvent& event) { wxPaintDC dc(this); }); - Bind(wxEVT_SIZE, [this](wxSizeEvent& event) { updateWindowSizes(); event.Skip(); }); - Bind(wxEVT_ERASE_BACKGROUND, [](wxEraseEvent& event) {}); //https://wiki.wxwidgets.org/Flicker-Free_Drawing - - Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onKeyDown(event); }); + //https://wiki.wxwidgets.org/Flicker-Free_Drawing + SetBackgroundStyle(wxBG_STYLE_PAINT); //get's rid of needless wxEVT_ERASE_BACKGROUND + //wxEVT_PAINT: "If you have an EVT_PAINT() handler, you must create a wxPaintDC object within it even if you don't actually use it." + //=> and if not, wxScrollHelperEvtHandler::ProcessEvent() helps out and creates wxPaintDC (without rendering anything) + Bind(wxEVT_SIZE, [this](wxSizeEvent& event) { updateWindowSizes(); event.Skip(); }); + Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onKeyDown(event); }); } @@ -2260,7 +2291,7 @@ int Grid::getBestColumnSize(size_t col) const { const ColumnType type = visibleCols_[col].type; - wxClientDC dc(mainWin_); + wxInfoDC dc(mainWin_); dc.SetFont(mainWin_->GetFont()); //harmonize with MainWin::render() const auto& [rowFirst, rowLast] = getVisibleRows(mainWin_->GetClientRect()); @@ -8,7 +8,6 @@ #define GRID_H_834702134831734869987 #include <memory> -//#include <numeric> #include <optional> #include <vector> #include <zen/stl_tools.h> @@ -108,9 +107,9 @@ public: 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, HoverArea rowHover); //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 HoverArea getMouseHover (wxDC& dc, size_t row, ColumnType colType, int cellRelativePosX, int cellWidth) { return HoverArea::none; } - virtual std::wstring getToolTip ( size_t row, ColumnType colType, HoverArea rowHover) { return std::wstring(); } + virtual int getBestSize (const wxReadOnlyDC& dc, size_t row, ColumnType colType); //must correspond to renderCell()! + virtual HoverArea getMouseHover(const wxReadOnlyDC& dc, size_t row, ColumnType colType, int cellRelativePosX, int cellWidth) { return HoverArea::none; } + virtual std::wstring getToolTip (size_t row, ColumnType colType, HoverArea rowHover) { return std::wstring(); } //label area: virtual std::wstring getColumnLabel(ColumnType colType) const = 0; diff --git a/wx+/image_resources.cpp b/wx+/image_resources.cpp index 3e162651..14d41a8e 100644 --- a/wx+/image_resources.cpp +++ b/wx+/image_resources.cpp @@ -5,7 +5,6 @@ // ***************************************************************************** #include "image_resources.h" -//#include <map> #include <zen/utf.h> #include <zen/thread.h> #include <zen/file_io.h> @@ -74,7 +73,7 @@ auto createScalerTask(const std::string& imageName, const wxImage& img, int hqSc assert(runningOnMainThread()); return [imageName, width = img.GetWidth(), // - height = img.GetHeight(), //don't call wxWidgets functions from worker thread + height = img.GetHeight(), //don't call these wxWidgets functions from worker thread rgb = img.GetData(), // alpha = img.GetAlpha(), // hqScale, &protResult] diff --git a/wx+/image_tools.cpp b/wx+/image_tools.cpp index 4dfa7b30..cefc800e 100644 --- a/wx+/image_tools.cpp +++ b/wx+/image_tools.cpp @@ -9,6 +9,7 @@ #include <zen/scope_guard.h> #include <wx/app.h> #include <wx/dcmemory.h> +#include <wx/settings.h> #include <wx+/dc.h> #include <xBRZ/src/xbrz_tools.h> @@ -188,6 +189,8 @@ wxImage zen::createImageFromText(const wxString& text, const wxFont& font, const if (maxWidth == 0 || lineHeight == 0) return wxNullImage; + const bool darkMode = wxSystemSettings::GetAppearance().IsDark(); //small but noticeable difference for "ClearType" + wxBitmap newBitmap(wxsizeToScreen(maxWidth), wxsizeToScreen(static_cast<int>(lineHeight * lineInfo.size()))); //seems we don't need to pass 24-bit depth here even for high-contrast color schemes newBitmap.SetScaleFactor(getScreenDpiScale()); @@ -198,11 +201,11 @@ wxImage zen::createImageFromText(const wxString& text, const wxFont& font, const if (wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft) dc.SetLayoutDirection(wxLayout_RightToLeft); //handle e.g. "weak" bidi characters: -> arrows in hebrew/arabic - dc.SetBackground(*wxWHITE_BRUSH); + dc.SetBackground(darkMode ? *wxBLACK_BRUSH : *wxWHITE_BRUSH); dc.Clear(); - dc.SetTextForeground(*wxBLACK); //for proper alpha-channel calculation - dc.SetTextBackground(*wxWHITE); // + dc.SetTextBackground(darkMode ? *wxBLACK : *wxWHITE); //for proper alpha-channel calculation + dc.SetTextForeground(darkMode ? *wxWHITE : *wxBLACK); // int posY = 0; for (const auto& [lineText, lineSize] : lineInfo) @@ -225,6 +228,7 @@ wxImage zen::createImageFromText(const wxString& text, const wxFont& font, const } //wxDC::DrawLabel() doesn't respect alpha channel => calculate alpha values manually: + //gamma correction? does not seem to apply here! wxImage output(newBitmap.ConvertToImage()); output.SetAlpha(); @@ -232,15 +236,29 @@ wxImage zen::createImageFromText(const wxString& text, const wxFont& font, const unsigned char* alpha = output.GetAlpha(); const int pixelCount = output.GetWidth() * output.GetHeight(); - for (int i = 0; i < pixelCount; ++i) - { - //black(0,0,0) becomes wxIMAGE_ALPHA_OPAQUE(255), while white(255,255,255) becomes wxIMAGE_ALPHA_TRANSPARENT(0) - //gamma correction? does not seem to apply here! - *alpha++ = static_cast<unsigned char>(numeric::intDivRound(3 * 255 - rgb[0] - rgb[1] - rgb[2], 3)); //mixed-mode arithmetics! - *rgb++ = col.Red (); // - *rgb++ = col.Green(); //apply actual text color - *rgb++ = col.Blue (); // - } + const unsigned char r = col.Red (); // + const unsigned char g = col.Green(); //getting RGB involves virtual function calls! + const unsigned char b = col.Blue (); // + + if (darkMode) + for (int i = 0; i < pixelCount; ++i) + { + //black(0,0,0) becomes wxIMAGE_ALPHA_TRANSPARENT(0), white(255,255,255) becomes wxIMAGE_ALPHA_OPAQUE(255) + *alpha++ = static_cast<unsigned char>(numeric::intDivRound(rgb[0] + rgb[1] + rgb[2], 3)); //mixed-mode arithmetics! + *rgb++ = r; // + *rgb++ = g; //apply actual text color + *rgb++ = b; // + } + else + for (int i = 0; i < pixelCount; ++i) + { + //black(0,0,0) becomes wxIMAGE_ALPHA_OPAQUE(255), white(255,255,255) becomes wxIMAGE_ALPHA_TRANSPARENT(0) + *alpha++ = static_cast<unsigned char>(numeric::intDivRound(3 * 255 - rgb[0] - rgb[1] - rgb[2], 3)); //mixed-mode arithmetics! + *rgb++ = r; // + *rgb++ = g; //apply actual text color + *rgb++ = b; // + } + return output; } @@ -304,7 +322,7 @@ wxImage zen::resizeCanvas(const wxImage& img, wxSize newSize, int alignment) std::memset(output.GetAlpha(), wxIMAGE_ALPHA_TRANSPARENT, newSize.x * newSize.y); copySubImage(img, wxPoint(), output, newPos, img.GetSize()); - //about 50x faster than e.g. wxImage::Resize!!! suprise :> + //about 50x faster than e.g. wxImage::Resize!!! surprise :> return output; } @@ -418,18 +436,23 @@ void zen::convertToVanillaImage(wxImage& img) } } + wxImage zen::rectangleImage(wxSize size, const wxColor& col) { assert(col.IsSolid()); wxImage img(size); + const unsigned char r = col.Red (); // + const unsigned char g = col.Green(); //getting RGB involves virtual function calls! + const unsigned char b = col.Blue (); // + unsigned char* rgb = img.GetData(); const int pixelCount = size.GetWidth() * size.GetHeight(); for (int i = 0; i < pixelCount; ++i) { - *rgb++ = col.GetRed(); - *rgb++ = col.GetGreen(); - *rgb++ = col.GetBlue(); + *rgb++ = r; + *rgb++ = g; + *rgb++ = b; } convertToVanillaImage(img); return img; @@ -439,11 +462,16 @@ wxImage zen::rectangleImage(wxSize size, const wxColor& col) wxImage zen::rectangleImage(wxSize size, const wxColor& innerCol, const wxColor& borderCol, int borderWidth) { assert(innerCol.IsSolid() && borderCol.IsSolid()); + assert(borderWidth > 0); wxImage img = rectangleImage(size, borderCol); const int heightInner = size.GetHeight() - 2 * borderWidth; const int widthInner = size.GetWidth () - 2 * borderWidth; + const unsigned char r = innerCol.Red (); // + const unsigned char g = innerCol.Green(); //getting RGB involves virtual function calls! + const unsigned char b = innerCol.Blue (); // + if (widthInner > 0 && heightInner > 0 && innerCol != borderCol) //copyImageLayover(rectangleImage({widthInner, heightInner}, innerCol), img, {borderWidth, borderWidth}); => inline: for (int y = 0; y < heightInner; ++y) @@ -452,9 +480,9 @@ wxImage zen::rectangleImage(wxSize size, const wxColor& innerCol, const wxColor& for (int x = 0; x < widthInner; ++x) { - *rgb++ = innerCol.GetRed(); - *rgb++ = innerCol.GetGreen(); - *rgb++ = innerCol.GetBlue(); + *rgb++ = r; + *rgb++ = g; + *rgb++ = b; } } diff --git a/wx+/image_tools.h b/wx+/image_tools.h index 8374a26f..9cb5b718 100644 --- a/wx+/image_tools.h +++ b/wx+/image_tools.h @@ -38,7 +38,6 @@ wxImage layOver(const wxImage& back, const wxImage& front, int alignment = wxALI wxImage greyScale(const wxImage& img); //greyscale + brightness adaption wxImage greyScaleIfDisabled(const wxImage& img, bool enabled); -//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 @@ -72,9 +71,9 @@ wxImage rectangleImage(wxSize size, const wxColor& innerCol, const wxColor& bord //################################### implementation ################################### inline -wxImage greyScale(const wxImage& img) +wxImage greyScale(const wxImage& img) //TODO support gamma-decoding and perceptual colors!? { - wxImage output = img.ConvertToGreyscale(1.0 / 3, 1.0 / 3, 1.0 / 3); //treat all channels equally! + wxImage output = img.ConvertToGreyscale(1.0 / 3, 1.0 / 3, 1.0 / 3); //treat all channels equally adjustBrightness(output, 160); return output; } @@ -91,7 +90,7 @@ wxImage greyScaleIfDisabled(const wxImage& img, bool enabled) inline -double getAvgBrightness(const wxImage& img) +double getAvgBrightness(const wxImage& img) //TODO: consider gamma-encoded sRGB!? { const int pixelCount = img.GetWidth() * img.GetHeight(); auto pixBegin = img.GetData(); @@ -128,9 +127,9 @@ void brighten(wxImage& img, int level) 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)); }); + std::for_each(pixBegin, pixEnd, [level](unsigned char& c) { c = static_cast<unsigned char>(std::min(c + level, 255)); }); else - std::for_each(pixBegin, pixEnd, [&](unsigned char& c) { c = static_cast<unsigned char>(std::max(0, c + level)); }); + std::for_each(pixBegin, pixEnd, [level](unsigned char& c) { c = static_cast<unsigned char>(std::max(c + level, 0)); }); } } @@ -140,69 +139,6 @@ void adjustBrightness(wxImage& img, int targetLevel) { brighten(img, targetLevel - getAvgBrightness(img)); } - - -/* -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] -{ - //https://en.wikipedia.org/wiki/HSL_and_HSV - - //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 = std::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 3d7c0ee0..b9f82349 100644 --- a/wx+/no_flicker.h +++ b/wx+/no_flicker.h @@ -12,7 +12,9 @@ #include <wx/textctrl.h> #include <wx/stattext.h> #include <wx/richtext/richtextctrl.h> +#include <wx/settings.h> #include <wx/wupdlock.h> +#include "color_tools.h" namespace zen @@ -81,7 +83,9 @@ void setTextWithUrls(wxRichTextCtrl& richCtrl, const wxString& newText) richCtrl.Clear(); wxRichTextAttr urlStyle; - urlStyle.SetTextColour(*wxBLUE); + urlStyle.SetTextColour(enhanceContrast(*wxBLUE, + wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT), + wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW), 5 /*contrastRatioMin*/)); //W3C recommends >= 4.5 urlStyle.SetFontUnderlined(true); for (auto& [type, text] : blocks) diff --git a/wx+/popup_dlg.cpp b/wx+/popup_dlg.cpp index d6c25fc8..291bfc38 100644 --- a/wx+/popup_dlg.cpp +++ b/wx+/popup_dlg.cpp @@ -10,7 +10,6 @@ #include <wx/app.h> #include <wx/display.h> #include <wx/sound.h> -//#include "app_main.h" #include "bitmap_button.h" #include "no_flicker.h" #include "window_layout.h" diff --git a/wx+/std_button_layout.h b/wx+/std_button_layout.h index 9a38ec1b..2a2fd7ce 100644 --- a/wx+/std_button_layout.h +++ b/wx+/std_button_layout.h @@ -7,7 +7,6 @@ #ifndef STD_BUTTON_LAYOUT_H_183470321478317214 #define STD_BUTTON_LAYOUT_H_183470321478317214 -//#include <algorithm> #include <wx/sizer.h> #include <wx/button.h> #include "dc.h" diff --git a/wx+/tooltip.cpp b/wx+/tooltip.cpp index 5b6b5f45..007abbfc 100644 --- a/wx+/tooltip.cpp +++ b/wx+/tooltip.cpp @@ -8,10 +8,8 @@ #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 "image_tools.h" #include "bitmap_button.h" #include "dc.h" #include <gtk/gtk.h> diff --git a/wx+/window_layout.h b/wx+/window_layout.h index 05b9316e..dee29fb5 100644 --- a/wx+/window_layout.h +++ b/wx+/window_layout.h @@ -7,11 +7,10 @@ #ifndef WINDOW_LAYOUT_H_23849632846734343234532 #define WINDOW_LAYOUT_H_23849632846734343234532 -//#include <zen/basic_math.h> -//#include <wx/window.h> #include <wx/spinctrl.h> #include <zen/scope_guard.h> #include <gtk/gtk.h> +#include "color_tools.h" #include "dc.h" diff --git a/wx+/window_tools.h b/wx+/window_tools.h index 45f43556..5be28fc8 100644 --- a/wx+/window_tools.h +++ b/wx+/window_tools.h @@ -93,6 +93,8 @@ struct FocusPreserver assert(oldFocusId_ != wxID_ANY); } + void dismiss() { oldFocusId_ = wxID_ANY; } + private: wxWindowID oldFocusId_ = wxID_ANY; //don't store wxWindow* which may be dangling during ~FocusPreserver()! |