| | |
| | | |
| | | namespace QWK { |
| | | |
| | | using _DWMWINDOWATTRIBUTE = enum _DWMWINDOWATTRIBUTE |
| | | { |
| | | _DWMWA_USE_HOSTBACKDROPBRUSH = 17, // [set] BOOL, Allows the use of host backdrop brushes for the window. |
| | | _DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 = 19, // Undocumented, the same with DWMWA_USE_IMMERSIVE_DARK_MODE, but available on systems before Win10 20H1. |
| | | _DWMWA_USE_IMMERSIVE_DARK_MODE = 20, // [set] BOOL, Allows a window to either use the accent color, or dark, according to the user Color Mode preferences. |
| | | _DWMWA_WINDOW_CORNER_PREFERENCE = 33, // [set] WINDOW_CORNER_PREFERENCE, Controls the policy that rounds top-level window corners |
| | | _DWMWA_VISIBLE_FRAME_BORDER_THICKNESS = 37, // [get] UINT, width of the visible border around a thick frame window |
| | | _DWMWA_SYSTEMBACKDROP_TYPE = 38, // [get, set] SYSTEMBACKDROP_TYPE, Controls the system-drawn backdrop material of a window, including behind the non-client area. |
| | | _DWMWA_MICA_EFFECT = 1029 // Undocumented, use this value to enable Mica material on Win11 21H2. You should use DWMWA_SYSTEMBACKDROP_TYPE instead on Win11 22H2 and newer. |
| | | }; |
| | | |
| | | // The thickness of an auto-hide taskbar in pixels. |
| | | static constexpr const auto kAutoHideTaskBarThickness = quint8{2}; |
| | | |
| | | static inline constexpr const auto kFrameBorderActiveColorLight = QColor{110, 110, 110}; // #6E6E6E |
| | | static inline constexpr const auto kFrameBorderActiveColorLight = |
| | | QColor{110, 110, 110}; // #6E6E6E |
| | | static inline constexpr const auto kFrameBorderActiveColorDark = QColor{51, 51, 51}; // #333333 |
| | | static inline constexpr const auto kFrameBorderInactiveColorLight = QColor{167, 167, 167}; // #A7A7A7 |
| | | static inline constexpr const auto kFrameBorderInactiveColorDark = QColor{61, 61, 62}; // #3D3D3E |
| | | static inline constexpr const auto kFrameBorderInactiveColorLight = |
| | | QColor{167, 167, 167}; // #A7A7A7 |
| | | static inline constexpr const auto kFrameBorderInactiveColorDark = |
| | | QColor{61, 61, 62}; // #3D3D3E |
| | | |
| | | // hWnd -> context |
| | | using WndProcHash = QHash<HWND, Win32WindowContext *>; |
| | |
| | | } g_hook{}; |
| | | |
| | | struct DynamicApis { |
| | | static const DynamicApis &instance() { |
| | | static const DynamicApis inst{}; |
| | | return inst; |
| | | } |
| | | |
| | | // template <typename T> |
| | | // struct DefaultFunc; |
| | | // |
| | |
| | | // |
| | | // #define DYNAMIC_API_DECLARE(NAME) decltype(&::NAME) p##NAME = |
| | | // DefaultFunc<decltype(&::NAME)>::func |
| | | |
| | | #define DYNAMIC_API_DECLARE(NAME) decltype(&::NAME) p##NAME = nullptr |
| | | |
| | | DYNAMIC_API_DECLARE(DwmFlush); |
| | | DYNAMIC_API_DECLARE(DwmIsCompositionEnabled); |
| | | DYNAMIC_API_DECLARE(DwmGetCompositionTimingInfo); |
| | | DYNAMIC_API_DECLARE(DwmGetWindowAttribute); |
| | | DYNAMIC_API_DECLARE(GetDpiForWindow); |
| | | DYNAMIC_API_DECLARE(GetSystemMetricsForDpi); |
| | | DYNAMIC_API_DECLARE(GetDpiForMonitor); |
| | |
| | | |
| | | #undef DYNAMIC_API_DECLARE |
| | | |
| | | private: |
| | | DynamicApis() { |
| | | #define DYNAMIC_API_RESOLVE(DLL, NAME) \ |
| | | p##NAME = reinterpret_cast<decltype(p##NAME)>(DLL.resolve(#NAME)) |
| | |
| | | DYNAMIC_API_RESOLVE(dwmapi, DwmFlush); |
| | | DYNAMIC_API_RESOLVE(dwmapi, DwmIsCompositionEnabled); |
| | | DYNAMIC_API_RESOLVE(dwmapi, DwmGetCompositionTimingInfo); |
| | | DYNAMIC_API_RESOLVE(dwmapi, DwmGetWindowAttribute); |
| | | |
| | | QSystemLibrary winmm(QStringLiteral("winmm")); |
| | | DYNAMIC_API_RESOLVE(winmm, timeGetDevCaps); |
| | |
| | | |
| | | ~DynamicApis() = default; |
| | | |
| | | static const DynamicApis &instance() { |
| | | static const DynamicApis inst{}; |
| | | return inst; |
| | | } |
| | | |
| | | private: |
| | | Q_DISABLE_COPY_MOVE(DynamicApis) |
| | | }; |
| | | |
| | |
| | | if (!registry.isValid()) { |
| | | return false; |
| | | } |
| | | const QVariant value = registry.value(L"ColorPrevalence"); |
| | | if (!value.isValid()) { |
| | | const auto value = registry.dwordValue(L"ColorPrevalence"); |
| | | if (!value.second) { |
| | | return false; |
| | | } |
| | | return qvariant_cast<DWORD>(value); |
| | | return value.first; |
| | | } |
| | | |
| | | static inline bool isDarkThemeActive() { |
| | | #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) |
| | | return QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark; |
| | | #else |
| | | const QWinRegistryKey registry(HKEY_CURRENT_USER, LR"(Software\Microsoft\Windows\CurrentVersion\Themes\Personalize)"); |
| | | const QWinRegistryKey registry( |
| | | HKEY_CURRENT_USER, LR"(Software\Microsoft\Windows\CurrentVersion\Themes\Personalize)"); |
| | | if (!registry.isValid()) { |
| | | return false; |
| | | } |
| | | const QVariant value = registry.value(L"AppsUseLightTheme"); |
| | | if (!value.isValid()) { |
| | | const auto value = registry.dwordValue(L"AppsUseLightTheme"); |
| | | if (!value.second) { |
| | | return false; |
| | | } |
| | | return !qvariant_cast<DWORD>(value); |
| | | return !value.first; |
| | | #endif |
| | | } |
| | | |
| | | static inline bool isDarkWindowFrameEnabled(HWND hwnd) { |
| | | BOOL enabled = FALSE; |
| | | const DynamicApis &apis = DynamicApis::instance(); |
| | | if (SUCCEEDED(apis.pDwmGetWindowAttribute(hwnd, _DWMWA_USE_IMMERSIVE_DARK_MODE, &enabled, sizeof(enabled)))) { |
| | | return enabled; |
| | | } else if (SUCCEEDED(apis.pDwmGetWindowAttribute(hwnd, _DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1, &enabled, sizeof(enabled)))) { |
| | | return enabled; |
| | | } else { |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | static inline QColor getAccentColor() { |
| | |
| | | if (!registry.isValid()) { |
| | | return {}; |
| | | } |
| | | const QVariant value = registry.value(L"AccentColor"); |
| | | if (!value.isValid()) { |
| | | const auto value = registry.dwordValue(L"AccentColor"); |
| | | if (!value.second) { |
| | | return {}; |
| | | } |
| | | // The retrieved value is in the #AABBGGRR format, we need to |
| | | // convert it to the #AARRGGBB format which Qt expects. |
| | | const QColor abgr = QColor::fromRgba(qvariant_cast<DWORD>(value)); |
| | | const QColor abgr = QColor::fromRgba(value.first); |
| | | if (!abgr.isValid()) { |
| | | return {}; |
| | | } |
| | |
| | | } else { // Win2K |
| | | HDC hdc = ::GetDC(nullptr); |
| | | const int dpiX = ::GetDeviceCaps(hdc, LOGPIXELSX); |
| | | const int dpiY = ::GetDeviceCaps(hdc, LOGPIXELSY); |
| | | //const int dpiY = ::GetDeviceCaps(hdc, LOGPIXELSY); |
| | | ::ReleaseDC(nullptr, hdc); |
| | | return quint32(dpiX); |
| | | } |
| | | } |
| | | |
| | | static inline quint32 getWindowFrameBorderThickness(HWND hwnd) { |
| | | UINT result{ 0 }; |
| | | const DynamicApis &apis = DynamicApis::instance(); |
| | | if (SUCCEEDED(apis.pDwmGetWindowAttribute(hwnd, _DWMWA_VISIBLE_FRAME_BORDER_THICKNESS, &result, sizeof(result)))) { |
| | | return result; |
| | | } else { |
| | | const quint32 dpi = getDpiForWindow(hwnd); |
| | | result = quint32(std::round(qreal(1) * qreal(dpi) / qreal(USER_DEFAULT_SCREEN_DPI))); |
| | | return result; |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | static inline void updateInternalWindowFrameMargins(HWND hwnd, QWindow *window) { |
| | | const auto margins = [hwnd]() -> QMargins { |
| | | const int titleBarHeight = getTitleBarHeight(hwnd); |
| | | const auto titleBarHeight = int(getTitleBarHeight(hwnd)); |
| | | if (isWin10OrGreater()) { |
| | | return {0, -titleBarHeight, 0, 0}; |
| | | } else { |
| | | const int frameSize = getResizeBorderThickness(hwnd); |
| | | const auto frameSize = int(getResizeBorderThickness(hwnd)); |
| | | return {-frameSize, -titleBarHeight, -frameSize, -frameSize}; |
| | | } |
| | | }(); |
| | |
| | | case HTBORDER: |
| | | return Win32WindowContext::FixedBorder; |
| | | default: |
| | | break; // unreachable |
| | | // unreachable |
| | | break; |
| | | } |
| | | return Win32WindowContext::Outside; |
| | | } |
| | |
| | | } |
| | | |
| | | QString Win32WindowContext::key() const { |
| | | return "win32"; |
| | | return QStringLiteral("win32"); |
| | | } |
| | | |
| | | void Win32WindowContext::virtual_hook(int id, void *data) { |
| | | switch (id) { |
| | | case CentralizeHook: { |
| | | const auto hwnd = reinterpret_cast<HWND>(m_windowHandle->winId()); |
| | | moveToDesktopCenter(hwnd); |
| | | return; |
| | | } |
| | | case ShowSystemMenuHook: { |
| | | const auto &pos = *reinterpret_cast<const QPoint *>(data); |
| | | auto winId = m_windowHandle->winId(); |
| | | auto hWnd = reinterpret_cast<HWND>(winId); |
| | | const auto &pos = *static_cast<const QPoint *>(data); |
| | | auto hWnd = reinterpret_cast<HWND>(m_windowHandle->winId()); |
| | | showSystemMenu2(hWnd, qpoint2point(pos), false, |
| | | m_delegate->isHostSizeFixed(m_host)); |
| | | return; |
| | | } |
| | | case NeedsDrawBordersHook: { |
| | | auto &result = *reinterpret_cast<bool *>(data); |
| | | auto &result = *static_cast<bool *>(data); |
| | | result = isWin10OrGreater() && !isWin11OrGreater(); |
| | | return; |
| | | } |
| | | case DrawBordersHook: { |
| | | auto args = reinterpret_cast<void **>(data); |
| | | auto &painter = *reinterpret_cast<QPainter *>(args[0]); |
| | | auto &rect = *reinterpret_cast<const QRect *>(args[1]); |
| | | auto ®ion = *reinterpret_cast<const QRegion *>(args[2]); |
| | | // ### TODO |
| | | auto args = static_cast<void **>(data); |
| | | auto &painter = *static_cast<QPainter *>(args[0]); |
| | | const auto &rect = *static_cast<const QRect *>(args[1]); |
| | | const auto ®ion = *static_cast<const QRegion *>(args[2]); |
| | | const auto hwnd = reinterpret_cast<HWND>(m_windowHandle->winId()); |
| | | QPen pen{}; |
| | | const auto borderThickness = int(QHighDpi::fromNativePixels(getWindowFrameBorderThickness(hwnd), m_windowHandle)); |
| | | pen.setWidth(borderThickness * 2); |
| | | const bool active = m_host->isWidgetType() ? m_host->property("isActiveWindow").toBool() : m_host->property("active").toBool(); |
| | | const bool dark = isDarkThemeActive() && isDarkWindowFrameEnabled(hwnd); |
| | | if (active) { |
| | | if (isWindowFrameBorderColorized()) { |
| | | pen.setColor(getAccentColor()); |
| | | } else { |
| | | if (dark) { |
| | | pen.setColor(kFrameBorderActiveColorDark); |
| | | } else { |
| | | pen.setColor(kFrameBorderActiveColorLight); |
| | | } |
| | | } |
| | | } else { |
| | | if (dark) { |
| | | pen.setColor(kFrameBorderInactiveColorDark); |
| | | } else { |
| | | pen.setColor(kFrameBorderInactiveColorLight); |
| | | } |
| | | } |
| | | painter.save(); |
| | | painter.setRenderHint(QPainter::Antialiasing); // ### TODO: do we need to enable or disable it? |
| | | painter.setPen(pen); |
| | | painter.drawLine(QLine{ QPoint{ 0, 0 }, QPoint{ rect.width(), 0 } }); |
| | | painter.restore(); |
| | | return; |
| | | } |
| | | default: |
| | | case QueryBorderThicknessHook: { |
| | | auto args = static_cast<void **>(data); |
| | | const bool requireNative = *static_cast<const bool *>(args[0]); |
| | | quint32 &thickness = *static_cast<quint32 *>(args[1]); |
| | | const auto hwnd = reinterpret_cast<HWND>(m_windowHandle->winId()); |
| | | const auto nativeThickness = getWindowFrameBorderThickness(hwnd); |
| | | if (requireNative) { |
| | | thickness = nativeThickness; |
| | | } else { |
| | | thickness = QHighDpi::fromNativePixels(nativeThickness, m_windowHandle); |
| | | } |
| | | return; |
| | | } |
| | | default: { |
| | | // unreachable |
| | | break; |
| | | } |
| | | } |
| | | AbstractWindowContext::virtual_hook(id, data); |
| | | } |
| | |
| | | return false; // Not handled |
| | | } |
| | | |
| | | static constexpr const auto kMessageTag = WPARAM(0xF1C9ADD4); |
| | | |
| | | static inline constexpr bool isTaggedMessage(WPARAM wParam) { |
| | | return (wParam == kMessageTag); |
| | | } |
| | | static constexpr const struct { |
| | | const WPARAM wParam = 0xF1C9ADD4; |
| | | const LPARAM lParam = 0xAFB6F4C6; |
| | | } kMessageTag; |
| | | |
| | | static inline quint64 getKeyState() { |
| | | quint64 result = 0; |
| | |
| | | // wParam is always ignored in mouse leave messages, but here we |
| | | // give them a special tag to be able to distinguish which messages |
| | | // are sent by ourselves. |
| | | return kMessageTag; |
| | | return kMessageTag.wParam; |
| | | } |
| | | const quint64 keyState = getKeyState(); |
| | | if ((myMsg >= WM_NCXBUTTONDOWN) && (myMsg <= WM_NCXBUTTONDBLCLK)) { |
| | |
| | | SEND_MESSAGE(hWnd, WM_MOUSELEAVE, wParamNew, lParamNew); |
| | | break; |
| | | default: |
| | | // unreachable |
| | | break; |
| | | } |
| | | |
| | |
| | | LPARAM lParam, LRESULT *result) { |
| | | switch (message) { |
| | | case WM_MOUSELEAVE: { |
| | | if (!isTaggedMessage(wParam)) { |
| | | if (wParam == kMessageTag.wParam) { |
| | | // Qt will call TrackMouseEvent() to get the WM_MOUSELEAVE message when it |
| | | // receives WM_MOUSEMOVE messages, and since we are converting every |
| | | // WM_NCMOUSEMOVE message to WM_MOUSEMOVE message and send it back to the window |
| | |
| | | *result = HTCLOSE; |
| | | break; |
| | | default: |
| | | break; // unreachable |
| | | // unreachable |
| | | break; |
| | | } |
| | | } |
| | | if (*result == HTNOWHERE) { |