mirror of
https://github.com/sebastiandine/Card-Collection-Manager-3.git
synced 2026-08-28 17:01:02 +00:00
693 lines
27 KiB
C++
693 lines
27 KiB
C++
#include "ccm/ui/Theme.hpp"
|
|
|
|
#include <wx/button.h>
|
|
#include <wx/bmpbuttn.h>
|
|
#include <wx/choice.h>
|
|
#include <wx/dcbuffer.h>
|
|
#include <wx/frame.h>
|
|
#include <wx/dialog.h>
|
|
#include <wx/listbox.h>
|
|
#include <wx/listctrl.h>
|
|
#include <wx/statusbr.h>
|
|
#include <wx/spinctrl.h>
|
|
#include <wx/statbmp.h>
|
|
#include <wx/stattext.h>
|
|
#include <wx/settings.h>
|
|
#include <wx/msgdlg.h>
|
|
#include <wx/sizer.h>
|
|
#include <wx/textctrl.h>
|
|
#include <wx/toplevel.h>
|
|
#include <wx/window.h>
|
|
|
|
#include <unordered_set>
|
|
|
|
#ifdef __WXMSW__
|
|
#include <windows.h>
|
|
#include <commctrl.h>
|
|
#endif
|
|
|
|
namespace ccm::ui {
|
|
|
|
namespace {
|
|
std::unordered_set<wxWindow*> gButtonHoverBound;
|
|
std::unordered_set<wxWindow*> gDialogGripLayoutBound;
|
|
struct ButtonVisualState {
|
|
wxColour normalBg;
|
|
wxColour hoverBg;
|
|
wxColour pressedBg;
|
|
wxColour text;
|
|
bool darkLike{false};
|
|
bool hovered{false};
|
|
bool pressed{false};
|
|
bool focused{false};
|
|
};
|
|
std::unordered_map<wxWindow*, ButtonVisualState> gButtonVisualStates;
|
|
struct GripVisualState {
|
|
wxColour bg;
|
|
wxColour line;
|
|
};
|
|
std::unordered_map<wxWindow*, GripVisualState> gGripVisualStates;
|
|
|
|
wxColour lightenTowardWhite(const wxColour& c, int amount) {
|
|
auto lift = [amount](unsigned char channel) -> unsigned char {
|
|
const int raised = static_cast<int>(channel) + amount;
|
|
return static_cast<unsigned char>(raised > 255 ? 255 : raised);
|
|
};
|
|
return wxColour(lift(c.Red()), lift(c.Green()), lift(c.Blue()));
|
|
}
|
|
|
|
bool isDarkLikeTheme(Theme theme) {
|
|
return theme == Theme::Dark;
|
|
}
|
|
|
|
void ensureDarkDialogResizeGrip(wxWindow* window, const ThemePalette& palette, Theme theme) {
|
|
auto* dialog = dynamic_cast<wxDialog*>(window);
|
|
if (dialog == nullptr) return;
|
|
if ((dialog->GetWindowStyleFlag() & wxRESIZE_BORDER) == 0) return;
|
|
|
|
constexpr int kGripSize = 16;
|
|
const wxString kGripName = "ccm_dark_resize_grip_overlay";
|
|
wxWindow* grip = wxWindow::FindWindowByName(kGripName, dialog);
|
|
|
|
if (!isDarkLikeTheme(theme)) {
|
|
if (grip != nullptr) {
|
|
gGripVisualStates.erase(grip);
|
|
grip->Destroy();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (grip == nullptr) {
|
|
grip = new wxWindow(dialog, wxID_ANY, wxDefaultPosition, wxSize(kGripSize, kGripSize),
|
|
wxBORDER_NONE);
|
|
grip->SetName(kGripName);
|
|
grip->SetCursor(wxCursor(wxCURSOR_SIZENWSE));
|
|
grip->SetBackgroundStyle(wxBG_STYLE_PAINT);
|
|
|
|
grip->Bind(wxEVT_ERASE_BACKGROUND, [](wxEraseEvent&) {});
|
|
grip->Bind(wxEVT_PAINT, [grip](wxPaintEvent&) {
|
|
wxAutoBufferedPaintDC dc(grip);
|
|
const auto it = gGripVisualStates.find(grip);
|
|
const wxColour bg = (it != gGripVisualStates.end()) ? it->second.bg : wxColour(45, 45, 45);
|
|
const wxColour line = (it != gGripVisualStates.end()) ? it->second.line : wxColour(110, 110, 110);
|
|
|
|
const wxRect rect = grip->GetClientRect();
|
|
dc.SetPen(*wxTRANSPARENT_PEN);
|
|
dc.SetBrush(wxBrush(bg));
|
|
dc.DrawRectangle(rect);
|
|
|
|
dc.SetPen(wxPen(line, 1));
|
|
const int r = rect.GetRight();
|
|
const int b = rect.GetBottom();
|
|
dc.DrawLine(r - 11, b, r, b - 11);
|
|
dc.DrawLine(r - 7, b, r, b - 7);
|
|
dc.DrawLine(r - 3, b, r, b - 3);
|
|
});
|
|
#ifdef __WXMSW__
|
|
grip->Bind(wxEVT_LEFT_DOWN, [dialog](wxMouseEvent&) {
|
|
const HWND hwnd = reinterpret_cast<HWND>(dialog->GetHandle());
|
|
if (hwnd == nullptr) return;
|
|
::ReleaseCapture();
|
|
::SendMessageW(hwnd, WM_NCLBUTTONDOWN, HTBOTTOMRIGHT, 0);
|
|
});
|
|
#endif
|
|
grip->Bind(wxEVT_DESTROY, [grip](wxWindowDestroyEvent& ev) {
|
|
gGripVisualStates.erase(grip);
|
|
ev.Skip();
|
|
});
|
|
}
|
|
|
|
gGripVisualStates[grip] = GripVisualState{
|
|
palette.panelBg,
|
|
lightenTowardWhite(palette.panelBg, 48),
|
|
};
|
|
|
|
auto placeGrip = [dialog, grip]() {
|
|
const wxSize cs = dialog->GetClientSize();
|
|
const int w = kGripSize;
|
|
const int h = kGripSize;
|
|
grip->SetSize(std::max(0, cs.GetWidth() - w), std::max(0, cs.GetHeight() - h), w, h);
|
|
grip->Raise();
|
|
};
|
|
placeGrip();
|
|
grip->Show();
|
|
grip->Refresh();
|
|
|
|
if (!gDialogGripLayoutBound.count(dialog)) {
|
|
gDialogGripLayoutBound.insert(dialog);
|
|
dialog->Bind(wxEVT_SIZE, [dialog](wxSizeEvent& ev) {
|
|
if (wxWindow* w = wxWindow::FindWindowByName("ccm_dark_resize_grip_overlay", dialog)) {
|
|
constexpr int kSize = 16;
|
|
const wxSize cs = dialog->GetClientSize();
|
|
w->SetSize(std::max(0, cs.GetWidth() - kSize), std::max(0, cs.GetHeight() - kSize), kSize, kSize);
|
|
w->Raise();
|
|
}
|
|
ev.Skip();
|
|
});
|
|
dialog->Bind(wxEVT_DESTROY, [dialog](wxWindowDestroyEvent& ev) {
|
|
gDialogGripLayoutBound.erase(dialog);
|
|
ev.Skip();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef __WXMSW__
|
|
namespace {
|
|
|
|
using SetWindowThemeFn = HRESULT(WINAPI*)(HWND, LPCWSTR, LPCWSTR);
|
|
using DwmSetWindowAttributeFn = HRESULT(WINAPI*)(HWND, DWORD, LPCVOID, DWORD);
|
|
using AllowDarkModeForWindowFn = BOOL(WINAPI*)(HWND, BOOL);
|
|
enum class PreferredAppMode : int {
|
|
Default = 0,
|
|
AllowDark = 1,
|
|
ForceDark = 2,
|
|
ForceLight = 3,
|
|
Max = 4
|
|
};
|
|
using SetPreferredAppModeFn = PreferredAppMode(WINAPI*)(PreferredAppMode);
|
|
using FlushMenuThemesFn = VOID(WINAPI*)();
|
|
|
|
#ifndef HDM_SETBKCOLOR
|
|
#define HDM_SETBKCOLOR (HDM_FIRST + 19)
|
|
#endif
|
|
#ifndef HDM_SETTEXTCOLOR
|
|
#define HDM_SETTEXTCOLOR (HDM_FIRST + 20)
|
|
#endif
|
|
|
|
SetWindowThemeFn resolveSetWindowTheme() {
|
|
static HMODULE uxthemeModule = ::LoadLibraryW(L"uxtheme.dll");
|
|
static auto setWindowTheme = reinterpret_cast<SetWindowThemeFn>(
|
|
uxthemeModule ? ::GetProcAddress(uxthemeModule, "SetWindowTheme") : nullptr);
|
|
return setWindowTheme;
|
|
}
|
|
|
|
AllowDarkModeForWindowFn resolveAllowDarkModeForWindow() {
|
|
static HMODULE uxthemeModule = ::LoadLibraryW(L"uxtheme.dll");
|
|
static auto fn = reinterpret_cast<AllowDarkModeForWindowFn>(
|
|
uxthemeModule ? ::GetProcAddress(uxthemeModule, MAKEINTRESOURCEA(133)) : nullptr);
|
|
return fn;
|
|
}
|
|
|
|
SetPreferredAppModeFn resolveSetPreferredAppMode() {
|
|
static HMODULE uxthemeModule = ::LoadLibraryW(L"uxtheme.dll");
|
|
static auto fn = reinterpret_cast<SetPreferredAppModeFn>(
|
|
uxthemeModule ? ::GetProcAddress(uxthemeModule, MAKEINTRESOURCEA(135)) : nullptr);
|
|
return fn;
|
|
}
|
|
|
|
FlushMenuThemesFn resolveFlushMenuThemes() {
|
|
static HMODULE uxthemeModule = ::LoadLibraryW(L"uxtheme.dll");
|
|
static auto fn = reinterpret_cast<FlushMenuThemesFn>(
|
|
uxthemeModule ? ::GetProcAddress(uxthemeModule, MAKEINTRESOURCEA(136)) : nullptr);
|
|
return fn;
|
|
}
|
|
|
|
void applyNativeClassTheme(wxWindow* window, Theme theme, const wchar_t* darkClass, const wchar_t* lightClass) {
|
|
if (window == nullptr) return;
|
|
const HWND hwnd = reinterpret_cast<HWND>(window->GetHandle());
|
|
if (hwnd == nullptr) return;
|
|
|
|
const auto setWindowTheme = resolveSetWindowTheme();
|
|
if (setWindowTheme == nullptr) return;
|
|
|
|
const bool dark = (theme == Theme::Dark);
|
|
setWindowTheme(hwnd, dark ? darkClass : lightClass, nullptr);
|
|
}
|
|
|
|
COLORREF toColorRef(const wxColour& c) {
|
|
return RGB(c.Red(), c.Green(), c.Blue());
|
|
}
|
|
|
|
void applyListHeaderTheme(wxWindow* window, Theme theme, const ThemePalette& palette) {
|
|
auto* list = dynamic_cast<wxListCtrl*>(window);
|
|
if (list == nullptr) return;
|
|
|
|
const HWND listHwnd = reinterpret_cast<HWND>(list->GetHandle());
|
|
if (listHwnd == nullptr) return;
|
|
|
|
const auto setWindowTheme = resolveSetWindowTheme();
|
|
if (setWindowTheme == nullptr) return;
|
|
|
|
const HWND header = ListView_GetHeader(listHwnd);
|
|
if (header == nullptr) return;
|
|
|
|
const bool dark = (theme == Theme::Dark);
|
|
if (auto allowDarkModeForWindow = resolveAllowDarkModeForWindow()) {
|
|
allowDarkModeForWindow(header, dark ? TRUE : FALSE);
|
|
}
|
|
if (dark) {
|
|
// Different Windows builds react to different class tokens.
|
|
setWindowTheme(header, L"DarkMode_ItemsView", nullptr);
|
|
setWindowTheme(header, L"DarkMode_Explorer", nullptr);
|
|
setWindowTheme(header, L"ItemsView", nullptr);
|
|
} else {
|
|
setWindowTheme(header, L"Header", nullptr);
|
|
setWindowTheme(header, L"ItemsView", nullptr);
|
|
}
|
|
|
|
// Force the native header colors to match the selected app theme.
|
|
::SendMessageW(header, HDM_SETBKCOLOR, 0, static_cast<LPARAM>(toColorRef(palette.inputBg)));
|
|
::SendMessageW(header, HDM_SETTEXTCOLOR, 0, static_cast<LPARAM>(toColorRef(palette.inputText)));
|
|
InvalidateRect(header, nullptr, TRUE);
|
|
}
|
|
|
|
void applyFrameTitlebarTheme(wxWindow* window, Theme theme) {
|
|
if (dynamic_cast<wxTopLevelWindow*>(window) == nullptr) return;
|
|
|
|
const HWND hwnd = reinterpret_cast<HWND>(window->GetHandle());
|
|
if (hwnd == nullptr) return;
|
|
|
|
static HMODULE dwmModule = ::LoadLibraryW(L"dwmapi.dll");
|
|
static auto dwmSetWindowAttribute = reinterpret_cast<DwmSetWindowAttributeFn>(
|
|
dwmModule ? ::GetProcAddress(dwmModule, "DwmSetWindowAttribute") : nullptr);
|
|
if (dwmSetWindowAttribute == nullptr) return;
|
|
|
|
const bool darkLike = (theme == Theme::Dark);
|
|
const BOOL useDark = darkLike ? TRUE : FALSE;
|
|
constexpr DWORD kDwmUseImmersiveDarkModeOld = 19;
|
|
constexpr DWORD kDwmUseImmersiveDarkModeNew = 20;
|
|
dwmSetWindowAttribute(hwnd, kDwmUseImmersiveDarkModeOld, &useDark, sizeof(useDark));
|
|
dwmSetWindowAttribute(hwnd, kDwmUseImmersiveDarkModeNew, &useDark, sizeof(useDark));
|
|
|
|
// Ask uxtheme to use dark menu rendering for the top menu strip.
|
|
if (auto setPreferredAppMode = resolveSetPreferredAppMode()) {
|
|
setPreferredAppMode(darkLike ? PreferredAppMode::ForceDark : PreferredAppMode::Default);
|
|
}
|
|
if (auto allowDarkModeForWindow = resolveAllowDarkModeForWindow()) {
|
|
allowDarkModeForWindow(hwnd, useDark);
|
|
}
|
|
if (auto setWindowTheme = resolveSetWindowTheme()) {
|
|
// Ensure top-level non-client rendering (including resize grip/corner)
|
|
// uses a dark-capable class theme when the app is in dark mode.
|
|
setWindowTheme(hwnd, darkLike ? L"DarkMode_Explorer" : L"Explorer", nullptr);
|
|
}
|
|
if (auto flushMenuThemes = resolveFlushMenuThemes()) {
|
|
flushMenuThemes();
|
|
}
|
|
DrawMenuBar(hwnd);
|
|
}
|
|
|
|
void applyTopLevelSizeGripTheme(wxWindow* window, Theme theme) {
|
|
if (dynamic_cast<wxTopLevelWindow*>(window) == nullptr) return;
|
|
|
|
const HWND top = reinterpret_cast<HWND>(window->GetHandle());
|
|
if (top == nullptr) return;
|
|
|
|
const auto setWindowTheme = resolveSetWindowTheme();
|
|
if (setWindowTheme == nullptr) return;
|
|
|
|
const bool dark = (theme == Theme::Dark);
|
|
const BOOL useDark = dark ? TRUE : FALSE;
|
|
|
|
std::pair<Theme, SetWindowThemeFn> enumCtx{theme, setWindowTheme};
|
|
|
|
::EnumChildWindows(
|
|
top,
|
|
[](HWND child, LPARAM lParam) -> BOOL {
|
|
auto* ctx = reinterpret_cast<std::pair<Theme, SetWindowThemeFn>*>(lParam);
|
|
if (ctx == nullptr || ctx->second == nullptr) return TRUE;
|
|
|
|
wchar_t className[64] = {};
|
|
if (::GetClassNameW(child, className, static_cast<int>(sizeof(className) / sizeof(className[0]))) <= 0) {
|
|
return TRUE;
|
|
}
|
|
const LONG_PTR style = ::GetWindowLongPtrW(child, GWL_STYLE);
|
|
const bool isScrollbarClass = (::wcscmp(className, L"SCROLLBAR") == 0);
|
|
const bool isStatusbarClass = (::wcscmp(className, STATUSCLASSNAMEW) == 0);
|
|
const bool isSizeGrip =
|
|
(style & SBS_SIZEGRIP) != 0 ||
|
|
(style & SBS_SIZEBOX) != 0 ||
|
|
(style & SBS_SIZEBOXBOTTOMRIGHTALIGN) != 0 ||
|
|
(style & SBS_SIZEBOXTOPLEFTALIGN) != 0 ||
|
|
(style & SBARS_SIZEGRIP) != 0;
|
|
if (!isSizeGrip) return TRUE;
|
|
if (!isScrollbarClass && !isStatusbarClass) return TRUE;
|
|
|
|
|
|
const bool darkLocal = (ctx->first == Theme::Dark);
|
|
if (auto allowDarkModeForWindow = resolveAllowDarkModeForWindow()) {
|
|
allowDarkModeForWindow(child, darkLocal ? TRUE : FALSE);
|
|
}
|
|
const wchar_t* darkClass = isStatusbarClass ? L"DarkMode_StatusBar" : L"DarkMode_Explorer";
|
|
const wchar_t* lightClass = isStatusbarClass ? L"Status" : L"Explorer";
|
|
ctx->second(child, darkLocal ? darkClass : lightClass, nullptr);
|
|
::InvalidateRect(child, nullptr, TRUE);
|
|
return TRUE;
|
|
},
|
|
reinterpret_cast<LPARAM>(&enumCtx));
|
|
|
|
if (auto allowDarkModeForWindow = resolveAllowDarkModeForWindow()) {
|
|
allowDarkModeForWindow(top, useDark);
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
#endif
|
|
|
|
ThemePalette paletteForTheme(Theme theme) {
|
|
switch (theme) {
|
|
case Theme::Dark:
|
|
return ThemePalette{
|
|
wxColour(30, 30, 30),
|
|
wxColour(45, 45, 45),
|
|
wxColour(230, 230, 230),
|
|
wxColour(60, 60, 60),
|
|
wxColour(230, 230, 230),
|
|
wxColour(75, 75, 75),
|
|
wxColour(240, 240, 240),
|
|
};
|
|
case Theme::Light:
|
|
default:
|
|
return ThemePalette{
|
|
wxColour(248, 248, 248),
|
|
wxColour(255, 255, 255),
|
|
wxColour(20, 20, 20),
|
|
wxColour(255, 255, 255),
|
|
wxColour(20, 20, 20),
|
|
wxColour(245, 245, 245),
|
|
wxColour(20, 20, 20),
|
|
};
|
|
}
|
|
}
|
|
|
|
Theme inferThemeFromWindow(const wxWindow* window) {
|
|
if (window == nullptr) return Theme::Light;
|
|
|
|
const wxWindow* probe = window;
|
|
wxColour bg;
|
|
while (probe != nullptr) {
|
|
bg = probe->GetBackgroundColour();
|
|
if (bg.IsOk()) break;
|
|
probe = probe->GetParent();
|
|
}
|
|
if (!bg.IsOk()) {
|
|
bg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW);
|
|
}
|
|
|
|
const int luminance =
|
|
(299 * bg.Red() + 587 * bg.Green() + 114 * bg.Blue()) / 1000;
|
|
return luminance < 128 ? Theme::Dark : Theme::Light;
|
|
}
|
|
|
|
void applyThemeToWindowTree(wxWindow* root, const ThemePalette& palette, Theme theme) {
|
|
if (root == nullptr) return;
|
|
|
|
root->SetForegroundColour(palette.text);
|
|
root->SetBackgroundColour(palette.panelBg);
|
|
root->SetOwnForegroundColour(palette.text);
|
|
root->SetOwnBackgroundColour(palette.panelBg);
|
|
|
|
#ifdef __WXMSW__
|
|
applyFrameTitlebarTheme(root, theme);
|
|
applyTopLevelSizeGripTheme(root, theme);
|
|
#endif
|
|
ensureDarkDialogResizeGrip(root, palette, theme);
|
|
|
|
if (dynamic_cast<wxTextCtrl*>(root) != nullptr ||
|
|
dynamic_cast<wxListCtrl*>(root) != nullptr ||
|
|
dynamic_cast<wxListBox*>(root) != nullptr ||
|
|
dynamic_cast<wxChoice*>(root) != nullptr ||
|
|
dynamic_cast<wxSpinCtrl*>(root) != nullptr) {
|
|
if (auto* text = dynamic_cast<wxTextCtrl*>(root)) {
|
|
// On Windows, themed EDIT controls can ignore wx foreground color
|
|
// while typing in dark mode; disable native theming there so the
|
|
// control consistently uses palette-driven text/background colors.
|
|
text->SetThemeEnabled(!isDarkLikeTheme(theme));
|
|
}
|
|
root->SetBackgroundColour(palette.inputBg);
|
|
root->SetForegroundColour(palette.inputText);
|
|
root->SetOwnBackgroundColour(palette.inputBg);
|
|
root->SetOwnForegroundColour(palette.inputText);
|
|
#ifdef __WXMSW__
|
|
if (dynamic_cast<wxListCtrl*>(root) != nullptr) {
|
|
// Keep both native list scrollbars and the SysHeader32 control themed.
|
|
applyNativeClassTheme(root, theme, L"DarkMode_Explorer", L"Explorer");
|
|
applyListHeaderTheme(root, theme, palette);
|
|
} else if (dynamic_cast<wxTextCtrl*>(root) != nullptr) {
|
|
// Do not apply Explorer class theming to edit controls: on some
|
|
// Windows builds it forces black typed text in dark mode.
|
|
// Keep text fields palette-driven via wx colors instead.
|
|
} else {
|
|
applyNativeClassTheme(root, theme, L"DarkMode_Explorer", L"Explorer");
|
|
}
|
|
#endif
|
|
}
|
|
|
|
if (dynamic_cast<wxStatusBar*>(root) != nullptr) {
|
|
root->SetBackgroundColour(palette.panelBg);
|
|
root->SetForegroundColour(palette.text);
|
|
root->SetOwnBackgroundColour(palette.panelBg);
|
|
root->SetOwnForegroundColour(palette.text);
|
|
#ifdef __WXMSW__
|
|
applyNativeClassTheme(root, theme, L"DarkMode_StatusBar", L"Status");
|
|
#endif
|
|
}
|
|
|
|
if (dynamic_cast<wxButton*>(root) != nullptr ||
|
|
dynamic_cast<wxBitmapButton*>(root) != nullptr) {
|
|
const bool darkLike = isDarkLikeTheme(theme);
|
|
root->SetThemeEnabled(!darkLike);
|
|
root->SetBackgroundColour(palette.buttonBg);
|
|
root->SetForegroundColour(palette.buttonText);
|
|
root->SetOwnBackgroundColour(palette.buttonBg);
|
|
root->SetOwnForegroundColour(palette.buttonText);
|
|
|
|
const wxColour normalBg = palette.buttonBg;
|
|
const int hoverLift = 18;
|
|
const int pressedLift = 30;
|
|
const wxColour hoverBg = darkLike ? lightenTowardWhite(normalBg, hoverLift) : normalBg;
|
|
const wxColour pressedBg = darkLike ? lightenTowardWhite(normalBg, pressedLift) : normalBg;
|
|
const wxColour btnFg = palette.buttonText;
|
|
gButtonVisualStates[root] = ButtonVisualState{
|
|
normalBg, hoverBg, pressedBg, btnFg, darkLike, false, false, false
|
|
};
|
|
|
|
if (!gButtonHoverBound.count(root)) {
|
|
gButtonHoverBound.insert(root);
|
|
|
|
root->Bind(wxEVT_ENTER_WINDOW, [root](wxMouseEvent& event) {
|
|
auto it = gButtonVisualStates.find(root);
|
|
if (it == gButtonVisualStates.end() || !it->second.darkLike) {
|
|
event.Skip();
|
|
return;
|
|
}
|
|
it->second.hovered = true;
|
|
const wxColour bg = it->second.pressed ? it->second.pressedBg : it->second.hoverBg;
|
|
root->SetBackgroundColour(bg);
|
|
root->SetForegroundColour(it->second.text);
|
|
root->Refresh();
|
|
});
|
|
root->Bind(wxEVT_LEAVE_WINDOW, [root](wxMouseEvent& event) {
|
|
auto it = gButtonVisualStates.find(root);
|
|
if (it == gButtonVisualStates.end() || !it->second.darkLike) {
|
|
event.Skip();
|
|
return;
|
|
}
|
|
it->second.hovered = false;
|
|
const wxColour bg = it->second.focused ? it->second.hoverBg : it->second.normalBg;
|
|
root->SetBackgroundColour(bg);
|
|
root->SetForegroundColour(it->second.text);
|
|
root->Refresh();
|
|
});
|
|
root->Bind(wxEVT_LEFT_DOWN, [root](wxMouseEvent& event) {
|
|
auto it = gButtonVisualStates.find(root);
|
|
if (it == gButtonVisualStates.end() || !it->second.darkLike) {
|
|
event.Skip();
|
|
return;
|
|
}
|
|
it->second.pressed = true;
|
|
root->SetBackgroundColour(it->second.pressedBg);
|
|
root->SetForegroundColour(it->second.text);
|
|
root->Refresh();
|
|
event.Skip();
|
|
});
|
|
root->Bind(wxEVT_LEFT_UP, [root](wxMouseEvent& event) {
|
|
auto it = gButtonVisualStates.find(root);
|
|
if (it == gButtonVisualStates.end() || !it->second.darkLike) {
|
|
event.Skip();
|
|
return;
|
|
}
|
|
it->second.pressed = false;
|
|
const wxPoint mousePos = wxGetMousePosition();
|
|
const wxPoint localPos = root->ScreenToClient(mousePos);
|
|
const bool inside = root->GetClientRect().Contains(localPos);
|
|
it->second.hovered = inside;
|
|
const wxColour bg = (inside || it->second.focused) ? it->second.hoverBg : it->second.normalBg;
|
|
root->SetBackgroundColour(bg);
|
|
root->SetForegroundColour(it->second.text);
|
|
root->Refresh();
|
|
event.Skip();
|
|
});
|
|
root->Bind(wxEVT_SET_FOCUS, [root](wxFocusEvent& event) {
|
|
auto it = gButtonVisualStates.find(root);
|
|
if (it == gButtonVisualStates.end() || !it->second.darkLike) {
|
|
event.Skip();
|
|
return;
|
|
}
|
|
it->second.focused = true;
|
|
root->SetBackgroundColour(it->second.hoverBg);
|
|
root->SetForegroundColour(it->second.text);
|
|
root->Refresh();
|
|
event.Skip();
|
|
});
|
|
root->Bind(wxEVT_KILL_FOCUS, [root](wxFocusEvent& event) {
|
|
auto it = gButtonVisualStates.find(root);
|
|
if (it == gButtonVisualStates.end() || !it->second.darkLike) {
|
|
event.Skip();
|
|
return;
|
|
}
|
|
it->second.focused = false;
|
|
it->second.pressed = false;
|
|
const wxColour bg = it->second.hovered ? it->second.hoverBg : it->second.normalBg;
|
|
root->SetBackgroundColour(bg);
|
|
root->SetForegroundColour(it->second.text);
|
|
root->Refresh();
|
|
event.Skip();
|
|
});
|
|
root->SetBackgroundStyle(wxBG_STYLE_PAINT);
|
|
root->Bind(wxEVT_ERASE_BACKGROUND, [](wxEraseEvent&) {});
|
|
root->Bind(wxEVT_PAINT, [root](wxPaintEvent& event) {
|
|
const auto it = gButtonVisualStates.find(root);
|
|
if (it == gButtonVisualStates.end() || !it->second.darkLike) {
|
|
event.Skip();
|
|
return;
|
|
}
|
|
wxAutoBufferedPaintDC dc(root);
|
|
const wxRect rect = root->GetClientRect();
|
|
wxColour bg = it->second.normalBg;
|
|
if (it->second.pressed) {
|
|
bg = it->second.pressedBg;
|
|
} else if (it->second.hovered || it->second.focused) {
|
|
bg = it->second.hoverBg;
|
|
}
|
|
const wxColour fg = it->second.text;
|
|
|
|
dc.SetBrush(wxBrush(bg));
|
|
dc.SetPen(wxPen(lightenTowardWhite(bg, 28)));
|
|
dc.DrawRectangle(rect);
|
|
|
|
if (auto* bmpBtn = dynamic_cast<wxBitmapButton*>(root)) {
|
|
const wxBitmap bmp = bmpBtn->GetBitmap();
|
|
if (bmp.IsOk()) {
|
|
const int x = (rect.GetWidth() - bmp.GetWidth()) / 2;
|
|
const int y = (rect.GetHeight() - bmp.GetHeight()) / 2;
|
|
dc.DrawBitmap(bmp, x, y, true);
|
|
}
|
|
} else {
|
|
dc.SetTextForeground(fg);
|
|
const wxString label = root->GetLabel();
|
|
dc.DrawLabel(label, rect, wxALIGN_CENTER);
|
|
}
|
|
});
|
|
root->Bind(wxEVT_DESTROY, [root](wxWindowDestroyEvent& event) {
|
|
gButtonHoverBound.erase(root);
|
|
gButtonVisualStates.erase(root);
|
|
event.Skip();
|
|
});
|
|
}
|
|
#ifdef __WXMSW__
|
|
if (darkLike) {
|
|
// Disable native visual-style painting in dark mode only,
|
|
// otherwise light theme buttons should stay fully native.
|
|
applyNativeClassTheme(root, theme, L"", L"");
|
|
}
|
|
#endif
|
|
}
|
|
|
|
if (dynamic_cast<wxStaticText*>(root) != nullptr ||
|
|
dynamic_cast<wxStaticBitmap*>(root) != nullptr) {
|
|
root->SetForegroundColour(palette.text);
|
|
root->SetBackgroundColour(palette.panelBg);
|
|
root->SetOwnForegroundColour(palette.text);
|
|
root->SetOwnBackgroundColour(palette.panelBg);
|
|
}
|
|
|
|
const wxWindowList& children = root->GetChildren();
|
|
for (wxWindowList::compatibility_iterator it = children.GetFirst(); it; it = it->GetNext()) {
|
|
applyThemeToWindowTree(it->GetData(), palette, theme);
|
|
}
|
|
}
|
|
|
|
void themeModalDialog(wxDialog* dlg, Theme theme) {
|
|
if (dlg == nullptr) return;
|
|
const ThemePalette palette = paletteForTheme(theme);
|
|
applyThemeToWindowTree(dlg, palette, theme);
|
|
dlg->SetBackgroundColour(palette.panelBg);
|
|
dlg->SetForegroundColour(palette.text);
|
|
}
|
|
|
|
int showThemedMessageDialog(wxWindow* parent, const wxString& message, const wxString& caption, long style) {
|
|
wxDialog dlg(parent, wxID_ANY, caption, wxDefaultPosition, wxDefaultSize,
|
|
wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER);
|
|
auto* root = new wxBoxSizer(wxVERTICAL);
|
|
auto* label = new wxStaticText(&dlg, wxID_ANY, message);
|
|
root->Add(label, 0, wxALL | wxEXPAND, 12);
|
|
|
|
const bool yesNo = (style & wxYES_NO) != 0;
|
|
if (yesNo) {
|
|
auto* buttons = new wxStdDialogButtonSizer();
|
|
auto* yesBtn = new wxButton(&dlg, wxID_YES);
|
|
auto* noBtn = new wxButton(&dlg, wxID_NO);
|
|
yesBtn->SetLabelText("Yes");
|
|
noBtn->SetLabelText("No");
|
|
yesBtn->Bind(wxEVT_BUTTON, [&dlg](wxCommandEvent&) { dlg.EndModal(wxID_YES); });
|
|
noBtn->Bind(wxEVT_BUTTON, [&dlg](wxCommandEvent&) { dlg.EndModal(wxID_NO); });
|
|
yesBtn->SetDefault();
|
|
buttons->AddButton(yesBtn);
|
|
buttons->AddButton(noBtn);
|
|
buttons->Realize();
|
|
root->Add(buttons, 0, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, 12);
|
|
} else {
|
|
if (auto* buttons = dlg.CreateButtonSizer(wxOK)) {
|
|
root->Add(buttons, 0, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, 12);
|
|
}
|
|
}
|
|
|
|
dlg.SetSizerAndFit(root);
|
|
const wxSize fitSize = dlg.GetSize();
|
|
dlg.SetSize(fitSize.GetWidth(), static_cast<int>(fitSize.GetHeight() * 1.10));
|
|
const Theme theme = inferThemeFromWindow(parent);
|
|
const ThemePalette palette = paletteForTheme(theme);
|
|
applyThemeToWindowTree(&dlg, palette, theme);
|
|
dlg.SetBackgroundColour(palette.panelBg);
|
|
dlg.SetForegroundColour(palette.text);
|
|
dlg.CentreOnParent();
|
|
return dlg.ShowModal();
|
|
}
|
|
|
|
int showThemedConfirmDialog(wxWindow* parent, const wxString& message, const wxString& caption) {
|
|
wxDialog dlg(parent, wxID_ANY, caption, wxDefaultPosition, wxDefaultSize,
|
|
wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER);
|
|
auto* root = new wxBoxSizer(wxVERTICAL);
|
|
auto* label = new wxStaticText(&dlg, wxID_ANY, message);
|
|
root->Add(label, 0, wxALL | wxEXPAND, 12);
|
|
|
|
auto* buttons = new wxStdDialogButtonSizer();
|
|
auto* yesBtn = new wxButton(&dlg, wxID_YES);
|
|
auto* noBtn = new wxButton(&dlg, wxID_NO);
|
|
yesBtn->SetLabelText("Yes");
|
|
noBtn->SetLabelText("No");
|
|
yesBtn->Bind(wxEVT_BUTTON, [&dlg](wxCommandEvent&) { dlg.EndModal(wxID_YES); });
|
|
noBtn->Bind(wxEVT_BUTTON, [&dlg](wxCommandEvent&) { dlg.EndModal(wxID_NO); });
|
|
yesBtn->SetDefault();
|
|
buttons->AddButton(yesBtn);
|
|
buttons->AddButton(noBtn);
|
|
buttons->Realize();
|
|
root->Add(buttons, 0, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, 12);
|
|
|
|
dlg.SetSizerAndFit(root);
|
|
const wxSize fitSize = dlg.GetSize();
|
|
dlg.SetSize(fitSize.GetWidth(), static_cast<int>(fitSize.GetHeight() * 1.10));
|
|
const Theme theme = inferThemeFromWindow(parent);
|
|
const ThemePalette palette = paletteForTheme(theme);
|
|
applyThemeToWindowTree(&dlg, palette, theme);
|
|
dlg.SetBackgroundColour(palette.panelBg);
|
|
dlg.SetForegroundColour(palette.text);
|
|
dlg.CentreOnParent();
|
|
|
|
return dlg.ShowModal();
|
|
}
|
|
|
|
} // namespace ccm::ui
|