diff --git a/CMakeLists.txt b/CMakeLists.txt index 284978de..e3f34581 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,7 +54,7 @@ option(FRAMELESSHELPER_ENABLE_CFGUARD "Enable Control Flow Guard (CFG)." OFF) option(FRAMELESSHELPER_EXAMPLES_STANDALONE "Build the demo projects as standalone CMake projects." OFF) cmake_dependent_option(FRAMELESSHELPER_ENABLE_UNIVERSAL_BUILD "macOS only: build universal library/example for Mac." ON APPLE OFF) option(FRAMELESSHELPER_FORCE_LTO "Force enable LTO/LTCG even when building static libraries." OFF) -option(FRAMELESSHELPER_REPRODUCIBLE_OUTPUT "Don't update the build commit and date dynamically." OFF) +option(FRAMELESSHELPER_REPRODUCIBLE_OUTPUT "Don't update the build commit and date dynamically." ON) find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Gui) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Gui) diff --git a/examples/dialog/CMakeLists.txt b/examples/dialog/CMakeLists.txt index 3a1191bd..269ab754 100644 --- a/examples/dialog/CMakeLists.txt +++ b/examples/dialog/CMakeLists.txt @@ -57,29 +57,33 @@ target_sources(${DEMO_NAME} PRIVATE if(WIN32) set(__rc_path "${PROJECT_BINARY_DIR}/${DEMO_NAME}.rc") - generate_win32_rc_file( - PATH "${__rc_path}" - VERSION "${PROJECT_VERSION}" - COMPANY "wangwenx190" - DESCRIPTION "FramelessHelper Demo Application: Dialog" - COPYRIGHT "MIT License" - PRODUCT "FramelessHelper Demo" - ICONS "../shared/example.ico" - ) + if(NOT EXISTS "${__rc_path}") + generate_win32_rc_file( + PATH "${__rc_path}" + VERSION "${PROJECT_VERSION}" + COMPANY "wangwenx190" + DESCRIPTION "FramelessHelper Demo Application: Dialog" + COPYRIGHT "MIT License" + PRODUCT "FramelessHelper Demo" + ICONS "../shared/example.ico" + ) + endif() set(__manifest_path "${PROJECT_BINARY_DIR}/${DEMO_NAME}.manifest") - generate_win32_manifest_file( - PATH "${__manifest_path}" - ID "org.wangwenx190.demo.Dialog" - VERSION "${PROJECT_VERSION}" - VISTA_COMPAT - WIN7_COMPAT - WIN8_COMPAT - WIN8_1_COMPAT - WIN10_COMPAT - WIN11_COMPAT - XAML_ISLANDS_COMPAT - UTF8_CODEPAGE - ) + if(NOT EXISTS "${__manifest_path}") + generate_win32_manifest_file( + PATH "${__manifest_path}" + ID "org.wangwenx190.demo.Dialog" + VERSION "${PROJECT_VERSION}" + VISTA_COMPAT + WIN7_COMPAT + WIN8_COMPAT + WIN8_1_COMPAT + WIN10_COMPAT + WIN11_COMPAT + XAML_ISLANDS_COMPAT + UTF8_CODEPAGE + ) + endif() target_sources(${DEMO_NAME} PRIVATE "${__rc_path}" "${__manifest_path}" diff --git a/examples/mainwindow/CMakeLists.txt b/examples/mainwindow/CMakeLists.txt index 1969cbab..ff8d68ee 100644 --- a/examples/mainwindow/CMakeLists.txt +++ b/examples/mainwindow/CMakeLists.txt @@ -62,29 +62,33 @@ target_sources(${DEMO_NAME} PRIVATE if(WIN32) set(__rc_path "${PROJECT_BINARY_DIR}/${DEMO_NAME}.rc") - generate_win32_rc_file( - PATH "${__rc_path}" - VERSION "${PROJECT_VERSION}" - COMPANY "wangwenx190" - DESCRIPTION "FramelessHelper Demo Application: MainWindow" - COPYRIGHT "MIT License" - PRODUCT "FramelessHelper Demo" - ICONS "../shared/example.ico" - ) + if(NOT EXISTS "${__rc_path}") + generate_win32_rc_file( + PATH "${__rc_path}" + VERSION "${PROJECT_VERSION}" + COMPANY "wangwenx190" + DESCRIPTION "FramelessHelper Demo Application: MainWindow" + COPYRIGHT "MIT License" + PRODUCT "FramelessHelper Demo" + ICONS "../shared/example.ico" + ) + endif() set(__manifest_path "${PROJECT_BINARY_DIR}/${DEMO_NAME}.manifest") - generate_win32_manifest_file( - PATH "${__manifest_path}" - ID "org.wangwenx190.demo.MainWindow" - VERSION "${PROJECT_VERSION}" - VISTA_COMPAT - WIN7_COMPAT - WIN8_COMPAT - WIN8_1_COMPAT - WIN10_COMPAT - WIN11_COMPAT - XAML_ISLANDS_COMPAT - UTF8_CODEPAGE - ) + if(NOT EXISTS "${__manifest_path}") + generate_win32_manifest_file( + PATH "${__manifest_path}" + ID "org.wangwenx190.demo.MainWindow" + VERSION "${PROJECT_VERSION}" + VISTA_COMPAT + WIN7_COMPAT + WIN8_COMPAT + WIN8_1_COMPAT + WIN10_COMPAT + WIN11_COMPAT + XAML_ISLANDS_COMPAT + UTF8_CODEPAGE + ) + endif() target_sources(${DEMO_NAME} PRIVATE "${__rc_path}" "${__manifest_path}" diff --git a/examples/openglwidget/CMakeLists.txt b/examples/openglwidget/CMakeLists.txt index fcaf4cfe..59f72cc3 100644 --- a/examples/openglwidget/CMakeLists.txt +++ b/examples/openglwidget/CMakeLists.txt @@ -65,29 +65,33 @@ target_sources(${DEMO_NAME} PRIVATE if(WIN32) set(__rc_path "${PROJECT_BINARY_DIR}/${DEMO_NAME}.rc") - generate_win32_rc_file( - PATH "${__rc_path}" - VERSION "${PROJECT_VERSION}" - COMPANY "wangwenx190" - DESCRIPTION "FramelessHelper Demo Application: OpenGLWidget" - COPYRIGHT "MIT License" - PRODUCT "FramelessHelper Demo" - ICONS "../shared/example.ico" - ) + if(NOT EXISTS "${__rc_path}") + generate_win32_rc_file( + PATH "${__rc_path}" + VERSION "${PROJECT_VERSION}" + COMPANY "wangwenx190" + DESCRIPTION "FramelessHelper Demo Application: OpenGLWidget" + COPYRIGHT "MIT License" + PRODUCT "FramelessHelper Demo" + ICONS "../shared/example.ico" + ) + endif() set(__manifest_path "${PROJECT_BINARY_DIR}/${DEMO_NAME}.manifest") - generate_win32_manifest_file( - PATH "${__manifest_path}" - ID "org.wangwenx190.demo.OpenGLWidget" - VERSION "${PROJECT_VERSION}" - VISTA_COMPAT - WIN7_COMPAT - WIN8_COMPAT - WIN8_1_COMPAT - WIN10_COMPAT - WIN11_COMPAT - XAML_ISLANDS_COMPAT - UTF8_CODEPAGE - ) + if(NOT EXISTS "${__manifest_path}") + generate_win32_manifest_file( + PATH "${__manifest_path}" + ID "org.wangwenx190.demo.OpenGLWidget" + VERSION "${PROJECT_VERSION}" + VISTA_COMPAT + WIN7_COMPAT + WIN8_COMPAT + WIN8_1_COMPAT + WIN10_COMPAT + WIN11_COMPAT + XAML_ISLANDS_COMPAT + UTF8_CODEPAGE + ) + endif() target_sources(${DEMO_NAME} PRIVATE "${__rc_path}" "${__manifest_path}" diff --git a/examples/quick/CMakeLists.txt b/examples/quick/CMakeLists.txt index 4221c170..f523e254 100644 --- a/examples/quick/CMakeLists.txt +++ b/examples/quick/CMakeLists.txt @@ -57,29 +57,33 @@ target_sources(${DEMO_NAME} PRIVATE if(WIN32) set(__rc_path "${PROJECT_BINARY_DIR}/${DEMO_NAME}.rc") - generate_win32_rc_file( - PATH "${__rc_path}" - VERSION "${PROJECT_VERSION}" - COMPANY "wangwenx190" - DESCRIPTION "FramelessHelper Demo Application: Quick" - COPYRIGHT "MIT License" - PRODUCT "FramelessHelper Demo" - ICONS "../shared/example.ico" - ) + if(NOT EXISTS "${__rc_path}") + generate_win32_rc_file( + PATH "${__rc_path}" + VERSION "${PROJECT_VERSION}" + COMPANY "wangwenx190" + DESCRIPTION "FramelessHelper Demo Application: Quick" + COPYRIGHT "MIT License" + PRODUCT "FramelessHelper Demo" + ICONS "../shared/example.ico" + ) + endif() set(__manifest_path "${PROJECT_BINARY_DIR}/${DEMO_NAME}.manifest") - generate_win32_manifest_file( - PATH "${__manifest_path}" - ID "org.wangwenx190.demo.Quick" - VERSION "${PROJECT_VERSION}" - VISTA_COMPAT - WIN7_COMPAT - WIN8_COMPAT - WIN8_1_COMPAT - WIN10_COMPAT - WIN11_COMPAT - XAML_ISLANDS_COMPAT - UTF8_CODEPAGE - ) + if(NOT EXISTS "${__manifest_path}") + generate_win32_manifest_file( + PATH "${__manifest_path}" + ID "org.wangwenx190.demo.Quick" + VERSION "${PROJECT_VERSION}" + VISTA_COMPAT + WIN7_COMPAT + WIN8_COMPAT + WIN8_1_COMPAT + WIN10_COMPAT + WIN11_COMPAT + XAML_ISLANDS_COMPAT + UTF8_CODEPAGE + ) + endif() target_sources(${DEMO_NAME} PRIVATE "${__rc_path}" "${__manifest_path}" diff --git a/examples/widget/CMakeLists.txt b/examples/widget/CMakeLists.txt index 674c0f68..17265c05 100644 --- a/examples/widget/CMakeLists.txt +++ b/examples/widget/CMakeLists.txt @@ -57,29 +57,33 @@ target_sources(${DEMO_NAME} PRIVATE if(WIN32) set(__rc_path "${PROJECT_BINARY_DIR}/${DEMO_NAME}.rc") - generate_win32_rc_file( - PATH "${__rc_path}" - VERSION "${PROJECT_VERSION}" - COMPANY "wangwenx190" - DESCRIPTION "FramelessHelper Demo Application: Widget" - COPYRIGHT "MIT License" - PRODUCT "FramelessHelper Demo" - ICONS "../shared/example.ico" - ) + if(NOT EXISTS "${__rc_path}") + generate_win32_rc_file( + PATH "${__rc_path}" + VERSION "${PROJECT_VERSION}" + COMPANY "wangwenx190" + DESCRIPTION "FramelessHelper Demo Application: Widget" + COPYRIGHT "MIT License" + PRODUCT "FramelessHelper Demo" + ICONS "../shared/example.ico" + ) + endif() set(__manifest_path "${PROJECT_BINARY_DIR}/${DEMO_NAME}.manifest") - generate_win32_manifest_file( - PATH "${__manifest_path}" - ID "org.wangwenx190.demo.Widget" - VERSION "${PROJECT_VERSION}" - VISTA_COMPAT - WIN7_COMPAT - WIN8_COMPAT - WIN8_1_COMPAT - WIN10_COMPAT - WIN11_COMPAT - XAML_ISLANDS_COMPAT - UTF8_CODEPAGE - ) + if(NOT EXISTS "${__manifest_path}") + generate_win32_manifest_file( + PATH "${__manifest_path}" + ID "org.wangwenx190.demo.Widget" + VERSION "${PROJECT_VERSION}" + VISTA_COMPAT + WIN7_COMPAT + WIN8_COMPAT + WIN8_1_COMPAT + WIN10_COMPAT + WIN11_COMPAT + XAML_ISLANDS_COMPAT + UTF8_CODEPAGE + ) + endif() target_sources(${DEMO_NAME} PRIVATE "${__rc_path}" "${__manifest_path}" diff --git a/include/FramelessHelper/Core/micamaterial.h b/include/FramelessHelper/Core/micamaterial.h index 2c065f7a..8d916ee2 100644 --- a/include/FramelessHelper/Core/micamaterial.h +++ b/include/FramelessHelper/Core/micamaterial.h @@ -25,6 +25,7 @@ #pragma once #include +#include FRAMELESSHELPER_BEGIN_NAMESPACE @@ -62,7 +63,13 @@ class FRAMELESSHELPER_CORE_API MicaMaterial : public QObject void setFallbackEnabled(const bool value); public Q_SLOTS: - void paint(QPainter *painter, const QSize &size, const QPoint &pos, const bool active = true); + void paint(QPainter *painter, const QRect &rect, const bool active = true); + + [[deprecated("Use another overload instead.")]] + void paint(QPainter *painter, const QSize &size, const QPoint &pos, const bool active = true) + { + paint(painter, QRect{ pos, size }, active); + } Q_SIGNALS: void tintColorChanged(); diff --git a/include/FramelessHelper/Core/private/framelesshelpercore_global_p.h b/include/FramelessHelper/Core/private/framelesshelpercore_global_p.h index 599799e7..18b4c9b0 100644 --- a/include/FramelessHelper/Core/private/framelesshelpercore_global_p.h +++ b/include/FramelessHelper/Core/private/framelesshelpercore_global_p.h @@ -103,3 +103,21 @@ FRAMELESSHELPER_CORE_API void registerInitializeHook(const InitializeHookCallbac FRAMELESSHELPER_CORE_API void registerUninitializeHook(const UninitializeHookCallback &cb); FRAMELESSHELPER_END_NAMESPACE + +#define DECLARE_SIZE_COMPARE_OPERATORS(Type1, Type2) \ + [[maybe_unused]] [[nodiscard]] static inline constexpr bool operator>(const Type1 &lhs, const Type2 &rhs) noexcept \ + { \ + return ((lhs.width() * lhs.height()) > (rhs.width() * rhs.height())); \ + } \ + [[maybe_unused]] [[nodiscard]] static inline constexpr bool operator>=(const Type1 &lhs, const Type2 &rhs) noexcept \ + { \ + return (operator>(lhs, rhs) || operator==(lhs, rhs)); \ + } \ + [[maybe_unused]] [[nodiscard]] static inline constexpr bool operator<(const Type1 &lhs, const Type2 &rhs) noexcept \ + { \ + return (operator!=(lhs, rhs) && !operator>(lhs, rhs)); \ + } \ + [[maybe_unused]] [[nodiscard]] static inline constexpr bool operator<=(const Type1 &lhs, const Type2 &rhs) noexcept \ + { \ + return (operator<(lhs, rhs) || operator==(lhs, rhs)); \ + } diff --git a/include/FramelessHelper/Core/private/micamaterial_p.h b/include/FramelessHelper/Core/private/micamaterial_p.h index 68b82f08..fa8f6bed 100644 --- a/include/FramelessHelper/Core/private/micamaterial_p.h +++ b/include/FramelessHelper/Core/private/micamaterial_p.h @@ -52,10 +52,18 @@ class FRAMELESSHELPER_CORE_API MicaMaterialPrivate : public QObject Q_NODISCARD static QColor systemFallbackColor(); + Q_NODISCARD static QSize monitorSize(); + Q_NODISCARD static QSize wallpaperSize(); + + Q_NODISCARD QPoint mapToWallpaper(const QPoint &pos) const; + Q_NODISCARD QSize mapToWallpaper(const QSize &size) const; + Q_NODISCARD QRect mapToWallpaper(const QRect &rect) const; + public Q_SLOTS: void maybeGenerateBlurredWallpaper(const bool force = false); void updateMaterialBrush(); - void paint(QPainter *painter, const QSize &size, const QPoint &pos, const bool active = true); + void paint(QPainter *painter, const QRect &rect, const bool active = true); + void forceRebuildWallpaper(); private: void initialize(); diff --git a/include/FramelessHelper/Core/utils.h b/include/FramelessHelper/Core/utils.h index 70e3b959..2dc64fca 100644 --- a/include/FramelessHelper/Core/utils.h +++ b/include/FramelessHelper/Core/utils.h @@ -81,7 +81,9 @@ FRAMELESSHELPER_CORE_API void registerThemeChangeNotification(); [[nodiscard]] FRAMELESSHELPER_CORE_API QPoint fromNativeGlobalPosition(const QWindow *window, const QPoint &point); [[nodiscard]] FRAMELESSHELPER_CORE_API int horizontalAdvance(const QFontMetrics &fm, const QString &str); [[nodiscard]] FRAMELESSHELPER_CORE_API qreal getRelativeScaleFactor(const quint32 oldDpi, const quint32 newDpi); +[[nodiscard]] FRAMELESSHELPER_CORE_API QSizeF rescaleSize(const QSizeF &oldSize, const quint32 oldDpi, const quint32 newDpi); [[nodiscard]] FRAMELESSHELPER_CORE_API QSize rescaleSize(const QSize &oldSize, const quint32 oldDpi, const quint32 newDpi); +[[nodiscard]] FRAMELESSHELPER_CORE_API bool isValidGeometry(const QRectF &rect); [[nodiscard]] FRAMELESSHELPER_CORE_API bool isValidGeometry(const QRect &rect); [[nodiscard]] FRAMELESSHELPER_CORE_API QColor getAccentColor(); [[nodiscard]] FRAMELESSHELPER_CORE_API quint32 defaultScreenDpi(); diff --git a/include/FramelessHelper/Quick/private/quickimageitem_p.h b/include/FramelessHelper/Quick/private/quickimageitem_p.h index ec20cdf7..801de76d 100644 --- a/include/FramelessHelper/Quick/private/quickimageitem_p.h +++ b/include/FramelessHelper/Quick/private/quickimageitem_p.h @@ -56,7 +56,7 @@ class FRAMELESSHELPER_QUICK_API QuickImageItemPrivate : public QObject void fromImage(const QImage &value, QPainter *painter) const; void fromPixmap(const QPixmap &value, QPainter *painter) const; void fromIcon(const QIcon &value, QPainter *painter) const; - Q_NODISCARD QRect paintArea() const; + Q_NODISCARD QRectF paintArea() const; private: QuickImageItem *q_ptr = nullptr; diff --git a/include/FramelessHelper/Quick/private/quickmicamaterial_p.h b/include/FramelessHelper/Quick/private/quickmicamaterial_p.h index 0380ac87..78e419c8 100644 --- a/include/FramelessHelper/Quick/private/quickmicamaterial_p.h +++ b/include/FramelessHelper/Quick/private/quickmicamaterial_p.h @@ -53,6 +53,7 @@ public Q_SLOTS: void rebindWindow(); void forceRegenerateWallpaperImageCache(); void appendNode(WallpaperImageNode *node); + void removeNode(WallpaperImageNode *node); void updateFallbackColor(); private: diff --git a/src/core/micamaterial.cpp b/src/core/micamaterial.cpp index b0374ce6..47f366d0 100644 --- a/src/core/micamaterial.cpp +++ b/src/core/micamaterial.cpp @@ -27,6 +27,7 @@ #include "framelessmanager.h" #include "utils.h" #include "framelessconfig_p.h" +#include "framelesshelpercore_global_p.h" #include #include #include @@ -57,6 +58,11 @@ static Q_LOGGING_CATEGORY(lcMicaMaterial, "wangwenx190.framelesshelper.core.mica # define CRITICAL qCCritical(lcMicaMaterial) #endif +DECLARE_SIZE_COMPARE_OPERATORS(QSize, QSize) +DECLARE_SIZE_COMPARE_OPERATORS(QSizeF, QSizeF) +DECLARE_SIZE_COMPARE_OPERATORS(QSize, QSizeF) +DECLARE_SIZE_COMPARE_OPERATORS(QSizeF, QSize) + using namespace Global; [[maybe_unused]] static constexpr const QSize kMaximumPictureSize = { 1920, 1080 }; @@ -75,34 +81,22 @@ using namespace Global; FRAMELESSHELPER_STRING_CONSTANT2(NoiseImageFilePath, ":/org.wangwenx190.FramelessHelper/resources/images/noise.png") #endif // FRAMELESSHELPER_CORE_NO_BUNDLE_RESOURCE -struct MicaMaterialData +struct ImageData { QPixmap blurredWallpaper = {}; bool graphicsResourcesReady = false; QMutex mutex{}; }; -Q_GLOBAL_STATIC(MicaMaterialData, g_micaMaterialData) - -[[maybe_unused]] [[nodiscard]] static inline constexpr bool operator>(const QSize &lhs, const QSize &rhs) noexcept -{ - return ((lhs.width() * lhs.height()) > (rhs.width() * rhs.height())); -} - -[[maybe_unused]] [[nodiscard]] static inline constexpr bool operator>=(const QSize &lhs, const QSize &rhs) noexcept +struct MetricsData { - return (operator>(lhs, rhs) || operator==(lhs, rhs)); -} - -[[maybe_unused]] [[nodiscard]] static inline constexpr bool operator<(const QSize &lhs, const QSize &rhs) noexcept -{ - return (operator!=(lhs, rhs) && !operator>(lhs, rhs)); -} + std::optional monitorSize = std::nullopt; + std::optional wallpaperSize = std::nullopt; + QMutex mutex{}; +}; -[[maybe_unused]] [[nodiscard]] static inline constexpr bool operator<=(const QSize &lhs, const QSize &rhs) noexcept -{ - return (operator<(lhs, rhs) || operator==(lhs, rhs)); -} +Q_GLOBAL_STATIC(ImageData, g_imageData) +Q_GLOBAL_STATIC(MetricsData, g_metricsData) #ifndef FRAMELESSHELPER_CORE_NO_PRIVATE template @@ -516,13 +510,10 @@ class WallpaperThread : public QThread void run() override { Transform transform = {}; - // ### FIXME: Ideally, we should not use virtual desktop size here. - QSize monitorSize = QGuiApplication::primaryScreen()->virtualSize(); - if (monitorSize.isEmpty()) { - WARNING << "Failed to retrieve the monitor size. Using default size (1920x1080) instead ..."; - monitorSize = kMaximumPictureSize; - } - const QSize imageSize = (monitorSize > kMaximumPictureSize ? kMaximumPictureSize : monitorSize); + const QSize monitorSize = MicaMaterialPrivate::monitorSize(); + const QSize imageSize = MicaMaterialPrivate::wallpaperSize(); + // If we scaled the image size, record the scale factor and we need it to map our clip rect + // to the real (unscaled) rect. if (imageSize != monitorSize) { transform.Horizontal = (qreal(imageSize.width()) / qreal(monitorSize.width())); transform.Vertical = (qreal(imageSize.height()) / qreal(monitorSize.height())); @@ -532,6 +523,8 @@ class WallpaperThread : public QThread WARNING << "Failed to retrieve the wallpaper file path."; return; } + // QImageReader allows us read the image size before we actually loading it, this behavior + // can help us avoid consume too much memory if the image resolution is very large, eg, 4K. QImageReader reader(wallpaperFilePath); if (!reader.canRead()) { WARNING << "Qt can't read the wallpaper file:" << reader.errorString(); @@ -590,18 +583,19 @@ class WallpaperThread : public QThread const QRect rect = alignedRect(Qt::LeftToRight, Qt::AlignCenter, image.size(), desktopRect); bufferPainter.drawImage(rect.topLeft(), image); } - g_micaMaterialData()->mutex.lock(); - g_micaMaterialData()->blurredWallpaper = QPixmap(imageSize); - g_micaMaterialData()->blurredWallpaper.fill(kDefaultTransparentColor); - QPainter painter(&g_micaMaterialData()->blurredWallpaper); - painter.setRenderHints(QPainter::Antialiasing | - QPainter::TextAntialiasing | QPainter::SmoothPixmapTransform); + { + const QMutexLocker locker(&g_imageData()->mutex); + g_imageData()->blurredWallpaper = QPixmap(imageSize); + g_imageData()->blurredWallpaper.fill(kDefaultTransparentColor); + QPainter painter(&g_imageData()->blurredWallpaper); + painter.setRenderHints(QPainter::Antialiasing | + QPainter::TextAntialiasing | QPainter::SmoothPixmapTransform); #ifdef FRAMELESSHELPER_CORE_NO_PRIVATE - painter.drawImage(desktopOriginPoint, buffer); + painter.drawImage(desktopOriginPoint, buffer); #else // !FRAMELESSHELPER_CORE_NO_PRIVATE - qt_blurImage(&painter, buffer, kDefaultBlurRadius, true, false); + qt_blurImage(&painter, buffer, kDefaultBlurRadius, true, false); #endif // FRAMELESSHELPER_CORE_NO_PRIVATE - g_micaMaterialData()->mutex.unlock(); + } Q_EMIT imageUpdated(transform); } }; @@ -656,12 +650,12 @@ const MicaMaterialPrivate *MicaMaterialPrivate::get(const MicaMaterial *q) void MicaMaterialPrivate::maybeGenerateBlurredWallpaper(const bool force) { - g_micaMaterialData()->mutex.lock(); - if (!g_micaMaterialData()->blurredWallpaper.isNull() && !force) { - g_micaMaterialData()->mutex.unlock(); + g_imageData()->mutex.lock(); + if (!g_imageData()->blurredWallpaper.isNull() && !force) { + g_imageData()->mutex.unlock(); return; } - g_micaMaterialData()->mutex.unlock(); + g_imageData()->mutex.unlock(); const QMutexLocker locker(&g_threadData()->mutex); if (g_threadData()->thread->isRunning()) { g_threadData()->thread->requestInterruption(); @@ -698,37 +692,25 @@ void MicaMaterialPrivate::updateMaterialBrush() } } -void MicaMaterialPrivate::paint(QPainter *painter, const QSize &size, const QPoint &pos, const bool active) +void MicaMaterialPrivate::paint(QPainter *painter, const QRect &rect, const bool active) { Q_ASSERT(painter); - Q_ASSERT(!size.isEmpty()); - if (!painter || size.isEmpty()) { + if (!painter) { return; } prepareGraphicsResources(); - static constexpr const QPointF originPoint = {0, 0}; - QPointF correctedPos = pos; - QSizeF correctedSize = size; - if (!qFuzzyIsNull(transform.Horizontal) && (transform.Horizontal > qreal(0)) - && !qFuzzyCompare(transform.Horizontal, qreal(1))) { - correctedPos.setX(correctedPos.x() * transform.Horizontal); - correctedSize.setWidth(correctedSize.width() * transform.Horizontal); - } - if (!qFuzzyIsNull(transform.Vertical) && (transform.Vertical > qreal(0)) - && !qFuzzyCompare(transform.Vertical, qreal(1))) { - correctedPos.setY(correctedPos.y() * transform.Vertical); - correctedSize.setHeight(correctedSize.height() * transform.Vertical); - } + static constexpr const QPoint originPoint = {0, 0}; + const QRect mappedRect = mapToWallpaper(rect); painter->save(); painter->setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing | QPainter::SmoothPixmapTransform); if (active) { - const QMutexLocker locker(&g_micaMaterialData()->mutex); - painter->drawPixmap(originPoint, g_micaMaterialData()->blurredWallpaper, QRectF{correctedPos, correctedSize}); + const QMutexLocker locker(&g_imageData()->mutex); + painter->drawPixmap(originPoint, g_imageData()->blurredWallpaper, mappedRect); } painter->setCompositionMode(QPainter::CompositionMode_SourceOver); painter->setOpacity(qreal(1)); - painter->fillRect(QRectF{originPoint, correctedSize}, [this, active]() -> QBrush { + painter->fillRect(QRect{originPoint, mappedRect.size()}, [this, active]() -> QBrush { if (!fallbackEnabled || active) { return micaBrush; } @@ -740,6 +722,15 @@ void MicaMaterialPrivate::paint(QPainter *painter, const QSize &size, const QPoi painter->restore(); } +void MicaMaterialPrivate::forceRebuildWallpaper() +{ + g_metricsData()->mutex.lock(); + g_metricsData()->monitorSize = std::nullopt; + g_metricsData()->wallpaperSize = std::nullopt; + g_metricsData()->mutex.unlock(); + maybeGenerateBlurredWallpaper(true); +} + void MicaMaterialPrivate::initialize() { g_threadData()->mutex.lock(); @@ -767,9 +758,9 @@ void MicaMaterialPrivate::initialize() connect(FramelessManager::instance(), &FramelessManager::systemThemeChanged, this, &MicaMaterialPrivate::updateMaterialBrush); connect(FramelessManager::instance(), &FramelessManager::wallpaperChanged, - this, [this](){ - maybeGenerateBlurredWallpaper(true); - }); + this, &MicaMaterialPrivate::forceRebuildWallpaper); + connect(qGuiApp, &QGuiApplication::primaryScreenChanged, + this, &MicaMaterialPrivate::forceRebuildWallpaper); if (FramelessConfig::instance()->isSet(Option::DisableLazyInitializationForMicaMaterial)) { prepareGraphicsResources(); @@ -780,13 +771,13 @@ void MicaMaterialPrivate::initialize() void MicaMaterialPrivate::prepareGraphicsResources() { - g_micaMaterialData()->mutex.lock(); - if (g_micaMaterialData()->graphicsResourcesReady) { - g_micaMaterialData()->mutex.unlock(); + g_imageData()->mutex.lock(); + if (g_imageData()->graphicsResourcesReady) { + g_imageData()->mutex.unlock(); return; } - g_micaMaterialData()->graphicsResourcesReady = true; - g_micaMaterialData()->mutex.unlock(); + g_imageData()->graphicsResourcesReady = true; + g_imageData()->mutex.unlock(); maybeGenerateBlurredWallpaper(); } @@ -795,6 +786,123 @@ QColor MicaMaterialPrivate::systemFallbackColor() return ((FramelessManager::instance()->systemTheme() == SystemTheme::Dark) ? kDefaultFallbackColorDark : kDefaultFallbackColorLight); } +QSize MicaMaterialPrivate::monitorSize() +{ + g_metricsData()->mutex.lock(); + if (!g_metricsData()->monitorSize.has_value()) { + g_metricsData()->mutex.unlock(); + const QScreen * const monitor = QGuiApplication::primaryScreen(); + Q_ASSERT(monitor); + // We do not use the virtual desktop size here, instead, we only calculate the primary + // monitor size to simplify the logic, otherwise we will need a lot more code to take + // every case into account. + QSize size = (monitor ? monitor->size() : kMaximumPictureSize); + if (Q_UNLIKELY(size.isEmpty())) { + WARNING << "Failed to retrieve the monitor size. Using default size (1920x1080) instead ..."; + size = kMaximumPictureSize; + } + g_metricsData()->mutex.lock(); + g_metricsData()->monitorSize = size; + // Don't unlock the mutex here, we'll unlock it from outside. + DEBUG << "Primary monitor size:" << size * (monitor ? monitor->devicePixelRatio() : qreal(1)); + } + const QSize result = g_metricsData()->monitorSize.value(); + g_metricsData()->mutex.unlock(); + return result; +} + +QSize MicaMaterialPrivate::wallpaperSize() +{ + g_metricsData()->mutex.lock(); + if (!g_metricsData()->wallpaperSize.has_value()) { + g_metricsData()->mutex.unlock(); + const QSize desktopSize = monitorSize(); + // It's observed that QImage consumes too much memory if the image resolution is very large. + const QSize size = (desktopSize > kMaximumPictureSize ? kMaximumPictureSize : desktopSize); + g_metricsData()->mutex.lock(); + g_metricsData()->wallpaperSize = size; + // Don't unlock the mutex here, we'll unlock it from outside. + DEBUG << "Wallpaper size:" << size; + } + const QSize result = g_metricsData()->wallpaperSize.value(); + g_metricsData()->mutex.unlock(); + return result; +} + +QPoint MicaMaterialPrivate::mapToWallpaper(const QPoint &pos) const +{ + if (pos.isNull()) { + return {}; + } + QPointF result = pos; + if (!qFuzzyIsNull(transform.Horizontal) && (transform.Horizontal > qreal(0)) + && !qFuzzyCompare(transform.Horizontal, qreal(1))) { + result.setX(result.x() * transform.Horizontal); + } + if (!qFuzzyIsNull(transform.Vertical) && (transform.Vertical > qreal(0)) + && !qFuzzyCompare(transform.Vertical, qreal(1))) { + result.setY(result.y() * transform.Vertical); + } + const QSizeF imageSize = wallpaperSize(); + // Make sure the position is always inside the wallpaper rectangle. + while (result.x() < qreal(0)) { + result.setX(result.x() + imageSize.width()); + } + while (result.x() > imageSize.width()) { + result.setX(result.x() - imageSize.width()); + } + while (result.y() < qreal(0)) { + result.setY(result.y() + imageSize.height()); + } + while (result.y() > imageSize.height()) { + result.setY(result.y() - imageSize.height()); + } + return result.toPoint(); +} + +QSize MicaMaterialPrivate::mapToWallpaper(const QSize &size) const +{ + if (size.isEmpty()) { + return {}; + } + QSizeF result = size; + if (!qFuzzyIsNull(transform.Horizontal) && (transform.Horizontal > qreal(0)) + && !qFuzzyCompare(transform.Horizontal, qreal(1))) { + result.setWidth(result.width() * transform.Horizontal); + } + if (!qFuzzyIsNull(transform.Vertical) && (transform.Vertical > qreal(0)) + && !qFuzzyCompare(transform.Vertical, qreal(1))) { + result.setHeight(result.height() * transform.Vertical); + } + const QSizeF imageSize = wallpaperSize(); + // Make sure we don't get a size larger than the wallpaper's size. + if (result.width() > imageSize.width()) { + result.setWidth(imageSize.width()); + } + if (result.height() > imageSize.height()) { + result.setHeight(imageSize.height()); + } + return result.toSize(); +} + +QRect MicaMaterialPrivate::mapToWallpaper(const QRect &rect) const +{ + const auto wallpaperRect = QRectF{ QPointF{ 0, 0 }, wallpaperSize() }; + const auto mappedRect = QRectF{ mapToWallpaper(rect.topLeft()), mapToWallpaper(rect.size()) }; + if (!Utils::isValidGeometry(mappedRect)) { + WARNING << "The calculated mapped rectangle is not valid."; + return wallpaperRect.toRect(); + } + // Make sure we don't get something outside of the wallpaper area. + const QRectF intersectedRect = wallpaperRect.intersected(mappedRect); + // OK, the two rectangles are not intersected, just draw the whole wallpaper. + if (!Utils::isValidGeometry(intersectedRect)) { + WARNING << "The mapped rectangle and the wallpaper rectangle are not intersected."; + return wallpaperRect.toRect(); + } + return intersectedRect.toRect(); +} + MicaMaterial::MicaMaterial(QObject *parent) : QObject(parent), d_ptr(new MicaMaterialPrivate(this)) { @@ -900,10 +1008,10 @@ void MicaMaterial::setFallbackEnabled(const bool value) Q_EMIT fallbackEnabledChanged(); } -void MicaMaterial::paint(QPainter *painter, const QSize &size, const QPoint &pos, const bool active) +void MicaMaterial::paint(QPainter *painter, const QRect &rect, const bool active) { Q_D(MicaMaterial); - d->paint(painter, size, pos, active); + d->paint(painter, rect, active); } FRAMELESSHELPER_END_NAMESPACE diff --git a/src/core/utils.cpp b/src/core/utils.cpp index 8cce42ac..f3b77223 100644 --- a/src/core/utils.cpp +++ b/src/core/utils.cpp @@ -530,7 +530,7 @@ qreal Utils::getRelativeScaleFactor(const quint32 oldDpi, const quint32 newDpi) return qreal(newDpr / oldDpr); } -QSize Utils::rescaleSize(const QSize &oldSize, const quint32 oldDpi, const quint32 newDpi) +QSizeF Utils::rescaleSize(const QSizeF &oldSize, const quint32 oldDpi, const quint32 newDpi) { if (oldSize.isEmpty()) { return {}; @@ -545,16 +545,25 @@ QSize Utils::rescaleSize(const QSize &oldSize, const quint32 oldDpi, const quint if (qFuzzyCompare(scaleFactor, qreal(1))) { return oldSize; } - const QSizeF newSize = QSizeF(oldSize) * scaleFactor; - return newSize.toSize(); // The numbers will be rounded to the nearest integer. + return QSizeF(oldSize * scaleFactor); } -bool Utils::isValidGeometry(const QRect &rect) +QSize Utils::rescaleSize(const QSize &oldSize, const quint32 oldDpi, const quint32 newDpi) +{ + return rescaleSize(QSizeF(oldSize), oldDpi, newDpi).toSize(); +} + +bool Utils::isValidGeometry(const QRectF &rect) { // The position of the rectangle is not relevant. return ((rect.right() > rect.left()) && (rect.bottom() > rect.top())); } +bool Utils::isValidGeometry(const QRect &rect) +{ + return isValidGeometry(QRectF(rect)); +} + quint32 Utils::defaultScreenDpi() { #ifdef Q_OS_MACOS diff --git a/src/quick/quickimageitem.cpp b/src/quick/quickimageitem.cpp index cfb94400..8a40983c 100644 --- a/src/quick/quickimageitem.cpp +++ b/src/quick/quickimageitem.cpp @@ -89,7 +89,7 @@ void QuickImageItemPrivate::paint(QPainter *painter) const if (!painter) { return; } - if (!m_source.isValid()) { + if (!m_source.isValid() || m_source.isNull()) { return; } painter->save(); @@ -126,7 +126,8 @@ QVariant QuickImageItemPrivate::source() const void QuickImageItemPrivate::setSource(const QVariant &value) { Q_ASSERT(value.isValid()); - if (!value.isValid()) { + Q_ASSERT(!value.isNull()); + if (!value.isValid() || value.isNull()) { return; } if (m_source == value) { @@ -196,7 +197,9 @@ void QuickImageItemPrivate::fromPixmap(const QPixmap &value, QPainter *painter) if (value.isNull() || !painter) { return; } - painter->drawPixmap(paintArea(), value); + const QRectF paintRect = paintArea(); + const QSize paintSize = paintRect.size().toSize(); + painter->drawPixmap(paintRect.topLeft(), (value.size() == paintSize ? value : value.scaled(paintSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation))); } void QuickImageItemPrivate::fromIcon(const QIcon &value, QPainter *painter) const @@ -206,18 +209,18 @@ void QuickImageItemPrivate::fromIcon(const QIcon &value, QPainter *painter) cons if (value.isNull() || !painter) { return; } - value.paint(painter, paintArea()); + fromPixmap(value.pixmap(paintArea().size().toSize()), painter); } -QRect QuickImageItemPrivate::paintArea() const +QRectF QuickImageItemPrivate::paintArea() const { Q_Q(const QuickImageItem); #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) - const QSize size = q->size().toSize(); + const QSizeF size = q->size(); #else - const QSize size = {int(std::round(q->width())), int(std::round(q->height()))}; + const QSizeF size = {q->width(), q->height()}; #endif - return {QPoint(0, 0), size}; + return {QPointF(0, 0), size}; } QuickImageItem::QuickImageItem(QQuickItem *parent) diff --git a/src/quick/quickmicamaterial.cpp b/src/quick/quickmicamaterial.cpp index 1e705090..0323ce7f 100644 --- a/src/quick/quickmicamaterial.cpp +++ b/src/quick/quickmicamaterial.cpp @@ -28,9 +28,7 @@ #include #include #include -#include #include -#include #include #include #ifndef FRAMELESSHELPER_QUICK_NO_PRIVATE @@ -74,10 +72,12 @@ public Q_SLOTS: void initialize(); private: - QSGTexture *m_texture = nullptr; QPointer m_item = nullptr; QSGSimpleTextureNode *m_node = nullptr; - QPixmap m_pixmapCache = {}; + std::unique_ptr m_texture = nullptr; + QPointer m_mica{ nullptr }; + QPointer m_micaPriv{ nullptr }; + QPointer m_window{ nullptr }; }; WallpaperImageNode::WallpaperImageNode(QuickMicaMaterial *item) @@ -90,12 +90,18 @@ WallpaperImageNode::WallpaperImageNode(QuickMicaMaterial *item) initialize(); } -WallpaperImageNode::~WallpaperImageNode() = default; +WallpaperImageNode::~WallpaperImageNode() +{ + QuickMicaMaterialPrivate::get(m_item)->removeNode(this); +} void WallpaperImageNode::initialize() { - QQuickWindow * const window = m_item->window(); + m_window = m_item->window(); + m_mica = QuickMicaMaterialPrivate::get(m_item)->m_micaMaterial; + m_micaPriv = MicaMaterialPrivate::get(m_mica); + // QtQuick's render engine will free it when appropriate. m_node = new QSGSimpleTextureNode; m_node->setFiltering(QSGTexture::Linear); @@ -104,7 +110,7 @@ void WallpaperImageNode::initialize() appendChildNode(m_node); - connect(window, &QQuickWindow::beforeRendering, this, + connect(m_window, &QQuickWindow::beforeRendering, this, &WallpaperImageNode::maybeUpdateWallpaperImageClipRect, Qt::DirectConnection); QuickMicaMaterialPrivate::get(m_item)->appendNode(this); @@ -112,24 +118,18 @@ void WallpaperImageNode::initialize() void WallpaperImageNode::maybeGenerateWallpaperImageCache(const bool force) { - if (!m_pixmapCache.isNull() && !force) { + if (m_texture && !force) { return; } - const QSize desktopSize = QGuiApplication::primaryScreen()->virtualSize(); - static constexpr const QPoint originPoint = {0, 0}; - m_pixmapCache = QPixmap(desktopSize); - m_pixmapCache.fill(kDefaultTransparentColor); - QPainter painter(&m_pixmapCache); - MicaMaterial * const mica = QuickMicaMaterialPrivate::get(m_item)->m_micaMaterial; - Q_ASSERT(mica); + static constexpr const auto originPoint = QPoint{ 0, 0 }; + const QSize imageSize = MicaMaterialPrivate::wallpaperSize(); + auto pixmap = QPixmap(imageSize); + pixmap.fill(kDefaultTransparentColor); + QPainter painter(&pixmap); // We need the real wallpaper image here, so always use "active" state. - mica->paint(&painter, desktopSize, originPoint, true); - if (m_texture) { - delete m_texture; - m_texture = nullptr; - } - m_texture = m_item->window()->createTextureFromImage(m_pixmapCache.toImage()); - m_node->setTexture(m_texture); + m_mica->paint(&painter, QRect{ originPoint, imageSize }, true); + m_texture.reset(m_window->createTextureFromImage(pixmap.toImage())); + m_node->setTexture(m_texture.get()); } void WallpaperImageNode::maybeUpdateWallpaperImageClipRect() @@ -140,7 +140,8 @@ void WallpaperImageNode::maybeUpdateWallpaperImageClipRect() const QSizeF itemSize = {m_item->width(), m_item->height()}; #endif m_node->setRect(QRectF(QPointF(0.0, 0.0), itemSize)); - m_node->setSourceRect(QRectF(m_item->mapToGlobal(QPointF(0.0, 0.0)), itemSize)); + const auto rect = QRectF(m_item->mapToGlobal(QPointF(0.0, 0.0)), itemSize); + m_node->setSourceRect(m_micaPriv->mapToWallpaper(rect.toRect())); } QuickMicaMaterialPrivate::QuickMicaMaterialPrivate(QuickMicaMaterial *q) : QObject(q) @@ -265,6 +266,18 @@ void QuickMicaMaterialPrivate::appendNode(WallpaperImageNode *node) m_nodes.append(node); } +void QuickMicaMaterialPrivate::removeNode(WallpaperImageNode *node) +{ + Q_ASSERT(node); + if (!node) { + return; + } + if (!m_nodes.contains(node)) { + return; + } + m_nodes.removeAll(node); +} + void QuickMicaMaterialPrivate::updateFallbackColor() { #ifndef FRAMELESSHELPER_QUICK_NO_PRIVATE diff --git a/src/widgets/widgetssharedhelper.cpp b/src/widgets/widgetssharedhelper.cpp index 313185b5..4d0aaa46 100644 --- a/src/widgets/widgetssharedhelper.cpp +++ b/src/widgets/widgetssharedhelper.cpp @@ -187,8 +187,8 @@ void WidgetsSharedHelper::repaintMica() return; } QPainter painter(m_targetWidget); - m_micaMaterial->paint(&painter, m_targetWidget->size(), - m_targetWidget->mapToGlobal(QPoint(0, 0)), m_targetWidget->isActiveWindow()); + const QRect rect = { m_targetWidget->mapToGlobal(QPoint(0, 0)), m_targetWidget->size() }; + m_micaMaterial->paint(&painter, rect, m_targetWidget->isActiveWindow()); } void WidgetsSharedHelper::repaintBorder()