diff --git a/src/gui/kernel/qguiapplication.cpp b/src/gui/kernel/qguiapplication.cpp index b45113a665..fd7739b06f 100644 --- a/src/gui/kernel/qguiapplication.cpp +++ b/src/gui/kernel/qguiapplication.cpp @@ -739,6 +739,29 @@ QString QGuiApplication::applicationDisplayName() return QGuiApplicationPrivate::displayName ? *QGuiApplicationPrivate::displayName : applicationName(); } +/*! + Sets the application's badge to \a number. + + Useful for providing feedback to the user about the number + of unread messages or similar. + + The badge will be overlaid on the application's icon in the Dock + on \macos, the home screen icon on iOS, or the task bar on Windows. + + If the number is outside the range supported by the platform, the + number will be clamped to the supported range. If the number does + not fit within the badge, the number may be visually elided. + + Setting the number to 0 will clear the badge. + + \since 6.5 + \sa applicationName +*/ +void QGuiApplication::setBadgeNumber(qint64 number) +{ + QGuiApplicationPrivate::platformIntegration()->setApplicationBadge(number); +} + /*! \property QGuiApplication::desktopFileName \brief the base name of the desktop entry for this application diff --git a/src/gui/kernel/qguiapplication.h b/src/gui/kernel/qguiapplication.h index b77984c381..14bce88c62 100644 --- a/src/gui/kernel/qguiapplication.h +++ b/src/gui/kernel/qguiapplication.h @@ -58,6 +58,8 @@ public: static void setApplicationDisplayName(const QString &name); static QString applicationDisplayName(); + Q_SLOT void setBadgeNumber(qint64 number); + static void setDesktopFileName(const QString &name); static QString desktopFileName(); diff --git a/src/gui/kernel/qplatformintegration.cpp b/src/gui/kernel/qplatformintegration.cpp index 12f8505255..c95742b34d 100644 --- a/src/gui/kernel/qplatformintegration.cpp +++ b/src/gui/kernel/qplatformintegration.cpp @@ -569,6 +569,20 @@ void QPlatformIntegration::setApplicationIcon(const QIcon &icon) const Q_UNUSED(icon); } +/*! + \since 6.5 + + Should set the application's badge to \a number. + + If the number is 0 the badge should be cleared. + + \sa QGuiApplication::setBadge() +*/ +void QPlatformIntegration::setApplicationBadge(qint64 number) +{ + Q_UNUSED(number); +} + #if QT_CONFIG(vulkan) || defined(Q_QDOC) /*! diff --git a/src/gui/kernel/qplatformintegration.h b/src/gui/kernel/qplatformintegration.h index 7c166ee698..3d43395767 100644 --- a/src/gui/kernel/qplatformintegration.h +++ b/src/gui/kernel/qplatformintegration.h @@ -188,6 +188,7 @@ public: virtual QOpenGLContext::OpenGLModuleType openGLModuleType(); #endif virtual void setApplicationIcon(const QIcon &icon) const; + virtual void setApplicationBadge(qint64 number); virtual void beep() const; virtual void quit() const; diff --git a/src/plugins/platforms/cocoa/qcocoaintegration.h b/src/plugins/platforms/cocoa/qcocoaintegration.h index c6d73e3c06..256b7b36ad 100644 --- a/src/plugins/platforms/cocoa/qcocoaintegration.h +++ b/src/plugins/platforms/cocoa/qcocoaintegration.h @@ -90,6 +90,7 @@ public: void clearToolbars(); void setApplicationIcon(const QIcon &icon) const override; + void setApplicationBadge(qint64 number) override; void beep() const override; void quit() const override; diff --git a/src/plugins/platforms/cocoa/qcocoaintegration.mm b/src/plugins/platforms/cocoa/qcocoaintegration.mm index b17ab38b10..2ec225cbea 100644 --- a/src/plugins/platforms/cocoa/qcocoaintegration.mm +++ b/src/plugins/platforms/cocoa/qcocoaintegration.mm @@ -438,6 +438,11 @@ void QCocoaIntegration::setApplicationIcon(const QIcon &icon) const NSApp.applicationIconImage = [NSImage imageFromQIcon:icon withSize:fallbackSize]; } +void QCocoaIntegration::setApplicationBadge(qint64 number) +{ + NSApp.dockTile.badgeLabel = number ? [NSString stringWithFormat:@"%" PRId64, number] : nil; +} + void QCocoaIntegration::beep() const { NSBeep(); diff --git a/src/plugins/platforms/ios/qiosintegration.h b/src/plugins/platforms/ios/qiosintegration.h index 6830a5b455..2dae15efb8 100644 --- a/src/plugins/platforms/ios/qiosintegration.h +++ b/src/plugins/platforms/ios/qiosintegration.h @@ -58,6 +58,8 @@ public: void beep() const override; + void setApplicationBadge(qint64 number) override; + static QIOSIntegration *instance(); // -- QPlatformNativeInterface -- diff --git a/src/plugins/platforms/ios/qiosintegration.mm b/src/plugins/platforms/ios/qiosintegration.mm index cb8eba651d..4bf981bf11 100644 --- a/src/plugins/platforms/ios/qiosintegration.mm +++ b/src/plugins/platforms/ios/qiosintegration.mm @@ -270,6 +270,11 @@ void QIOSIntegration::beep() const #endif } +void QIOSIntegration::setApplicationBadge(qint64 number) +{ + UIApplication.sharedApplication.applicationIconBadgeNumber = number; +} + // --------------------------------------------------------- void *QIOSIntegration::nativeResourceForWindow(const QByteArray &resource, QWindow *window) diff --git a/src/plugins/platforms/windows/qtwindowsglobal.h b/src/plugins/platforms/windows/qtwindowsglobal.h index 75f984b4b0..d39e923644 100644 --- a/src/plugins/platforms/windows/qtwindowsglobal.h +++ b/src/plugins/platforms/windows/qtwindowsglobal.h @@ -107,6 +107,7 @@ enum WindowsEventType // Simplify event types PointerActivateWindowEvent = WindowEventFlag + 24, DpiScaledSizeEvent = WindowEventFlag + 25, DpiChangedAfterParentEvent = WindowEventFlag + 27, + TaskbarButtonCreated = WindowEventFlag + 28, MouseEvent = MouseEventFlag + 1, MouseWheelEvent = MouseEventFlag + 2, CursorEvent = MouseEventFlag + 3, @@ -162,6 +163,14 @@ enum ProcessDpiAwareness inline QtWindows::WindowsEventType windowsEventType(UINT message, WPARAM wParamIn, LPARAM lParamIn) { + static const UINT WM_TASKBAR_BUTTON_CREATED = []{ + UINT message = RegisterWindowMessage(L"TaskbarButtonCreated"); + // In case the application is run elevated, allow the + // TaskbarButtonCreated message through. + ChangeWindowMessageFilter(message, MSGFLT_ADD); + return message; + }(); + switch (message) { case WM_PAINT: case WM_ERASEBKGND: @@ -312,6 +321,8 @@ inline QtWindows::WindowsEventType windowsEventType(UINT message, WPARAM wParamI return QtWindows::NonClientPointerEvent; if (message >= WM_POINTERUPDATE && message <= WM_POINTERHWHEEL) return QtWindows::PointerEvent; + if (message == WM_TASKBAR_BUTTON_CREATED) + return QtWindows::TaskbarButtonCreated; return QtWindows::UnknownEvent; } diff --git a/src/plugins/platforms/windows/qwindowscontext.cpp b/src/plugins/platforms/windows/qwindowscontext.cpp index e11bb5e335..e1c1e99a5a 100644 --- a/src/plugins/platforms/windows/qwindowscontext.cpp +++ b/src/plugins/platforms/windows/qwindowscontext.cpp @@ -1022,6 +1022,7 @@ bool QWindowsContext::windowsProc(HWND hwnd, UINT message, const bool darkModeChanged = darkMode != QWindowsContextPrivate::m_darkMode; QWindowsContextPrivate::m_darkMode = darkMode; auto integration = QWindowsIntegration::instance(); + integration->updateApplicationBadge(); if (integration->darkModeHandling().testFlag(QWindowsApplication::DarkModeStyle)) { QWindowsTheme::instance()->refresh(); QWindowSystemInterface::handleThemeChange(); @@ -1286,6 +1287,11 @@ bool QWindowsContext::windowsProc(HWND hwnd, UINT message, return true; } #endif // !defined(QT_NO_SESSIONMANAGER) + case QtWindows::TaskbarButtonCreated: + // Apply application badge if this is the first time we have a taskbar + // button, or after Explorer restart. + QWindowsIntegration::instance()->updateApplicationBadge(); + break; default: break; } diff --git a/src/plugins/platforms/windows/qwindowsintegration.cpp b/src/plugins/platforms/windows/qwindowsintegration.cpp index dfbf5609fb..bf5e73f002 100644 --- a/src/plugins/platforms/windows/qwindowsintegration.cpp +++ b/src/plugins/platforms/windows/qwindowsintegration.cpp @@ -48,6 +48,11 @@ #include #include +#include +#include + +#include + #include #if !defined(QT_NO_OPENGL) @@ -56,6 +61,13 @@ #include "qwindowsopengltester.h" +#if QT_CONFIG(cpp_winrt) +# include +# include +# include +# include +#endif + #include static inline void initOpenGlBlacklistResources() @@ -635,6 +647,153 @@ void QWindowsIntegration::beep() const MessageBeep(MB_OK); // For QApplication } +void QWindowsIntegration::setApplicationBadge(qint64 number) +{ + // Clamp to positive numbers, as the Windows API doesn't support negative numbers + number = qMax(0, number); + + // Persist, so we can re-apply it on setting changes and Explorer restart + m_applicationBadgeNumber = number; + + static const bool isWindows11 = QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows11; + +#if QT_CONFIG(cpp_winrt) + // We prefer the native BadgeUpdater API, that allows us to set a number directly, + // but it requires that the application has a package identity, and also doesn't + // seem to work in all cases on < Windows 11. + if (isWindows11 && qt_win_hasPackageIdentity()) { + using namespace winrt::Windows::UI::Notifications; + auto badgeXml = BadgeUpdateManager::GetTemplateContent(BadgeTemplateType::BadgeNumber); + badgeXml.SelectSingleNode(L"//badge/@value").NodeValue(winrt::box_value(winrt::to_hstring(number))); + BadgeUpdateManager::CreateBadgeUpdaterForApplication().Update(BadgeNotification(badgeXml)); + return; + } +#endif + + // Fallback for non-packaged apps, Windows 10, or Qt builds without WinRT/C++ support + + if (!number) { + // Clear badge + setApplicationBadge(QImage()); + return; + } + + const bool isDarkMode = QWindowsContext::isDarkMode(); + + QColor badgeColor; + QColor textColor; + +#if QT_CONFIG(cpp_winrt) + if (isWindows11) { + // Match colors used by BadgeUpdater + static const auto fromUIColor = [](winrt::Windows::UI::Color &&color) { + return QColor(color.R, color.G, color.B, color.A); + }; + using namespace winrt::Windows::UI::ViewManagement; + const auto settings = UISettings(); + badgeColor = fromUIColor(settings.GetColorValue(isDarkMode ? + UIColorType::AccentLight2 : UIColorType::Accent)); + textColor = fromUIColor(settings.GetColorValue(UIColorType::Background)); + } +#endif + + if (!badgeColor.isValid()) { + // Fall back to basic badge colors, based on Windows 10 look + badgeColor = isDarkMode ? Qt::black : QColor(220, 220, 220); + badgeColor.setAlphaF(0.5f); + textColor = isDarkMode ? Qt::white : Qt::black; + } + + const auto devicePixelRatio = qApp->devicePixelRatio(); + + static const QSize iconBaseSize(16, 16); + QImage image(iconBaseSize * devicePixelRatio, + QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::transparent); + + QPainter painter(&image); + + QRect badgeRect = image.rect(); + QPen badgeBorderPen = Qt::NoPen; + if (!isWindows11) { + QColor badgeBorderColor = textColor; + badgeBorderColor.setAlphaF(0.5f); + badgeBorderPen = badgeBorderColor; + badgeRect.adjust(1, 1, -1, -1); + } + painter.setBrush(badgeColor); + painter.setPen(badgeBorderPen); + painter.setRenderHint(QPainter::Antialiasing); + painter.drawEllipse(badgeRect); + + auto pixelSize = qCeil(10.5 * devicePixelRatio); + // Unlike the BadgeUpdater API we're limited by a square + // badge, so adjust the font size when above two digits. + const bool textOverflow = number > 99; + if (textOverflow) + pixelSize *= 0.8; + + QFont font = painter.font(); + font.setPixelSize(pixelSize); + font.setWeight(isWindows11 ? QFont::Medium : QFont::DemiBold); + painter.setFont(font); + + painter.setRenderHint(QPainter::TextAntialiasing, devicePixelRatio > 1); + painter.setPen(textColor); + + auto text = textOverflow ? u"99+"_s : QString::number(number); + painter.translate(textOverflow ? 1 : 0, textOverflow ? 0 : -1); + painter.drawText(image.rect(), Qt::AlignCenter, text); + + painter.end(); + + setApplicationBadge(image); +} + +void QWindowsIntegration::setApplicationBadge(const QImage &image) +{ + QComHelper comHelper; + + using Microsoft::WRL::ComPtr; + + ComPtr taskbarList; + CoCreateInstance(CLSID_TaskbarList, nullptr, + CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&taskbarList)); + if (!taskbarList) { + // There may not be any windows with a task bar button yet, + // in which case we'll apply the badge once a window with + // a button has been created. + return; + } + + const auto hIcon = image.toHICON(); + + // Apply the icon to all top level windows, since the badge is + // set on an application level. If one of the windows go away + // the other windows will take over in showing the badge. + const auto topLevelWindows = QGuiApplication::topLevelWindows(); + for (auto *topLevelWindow : topLevelWindows) { + auto hwnd = reinterpret_cast(topLevelWindow->winId()); + taskbarList->SetOverlayIcon(hwnd, hIcon, L""); + } + + DestroyIcon(hIcon); + + // FIXME: Update icon when the application scale factor changes. + // Doing so in response to screen DPI changes is too soon, as the + // task bar is not yet ready for an updated icon, and will just + // result in a blurred icon even if our icon is high-DPI. +} + +void QWindowsIntegration::updateApplicationBadge() +{ + // The system color settings have changed, or we are reacting + // to a task bar button being created for the fist time or after + // Explorer had crashed and re-started. In any case, re-apply the + // badge so that everything is up to date. + setApplicationBadge(m_applicationBadgeNumber); +} + #if QT_CONFIG(vulkan) QPlatformVulkanInstance *QWindowsIntegration::createPlatformVulkanInstance(QVulkanInstance *instance) const { diff --git a/src/plugins/platforms/windows/qwindowsintegration.h b/src/plugins/platforms/windows/qwindowsintegration.h index 4391208374..1f3c71c41b 100644 --- a/src/plugins/platforms/windows/qwindowsintegration.h +++ b/src/plugins/platforms/windows/qwindowsintegration.h @@ -91,6 +91,10 @@ public: void beep() const override; + void setApplicationBadge(qint64 number) override; + void setApplicationBadge(const QImage &image); + void updateApplicationBadge(); + #if QT_CONFIG(sessionmanager) QPlatformSessionManager *createPlatformSessionManager(const QString &id, const QString &key) const override; #endif @@ -106,6 +110,8 @@ private: QScopedPointer d; static QWindowsIntegration *m_instance; + + qint64 m_applicationBadgeNumber = 0; }; QT_END_NAMESPACE