Add QGuiApplication API to set a number-badge in the Dock/task bar
The API is supported on macOS, iOS, and Windows. On Android no official API exists for badging the application icon, and we don't want to take on dependencies like ShortcutBadger [1]. The macOS and iOS implementations are trivial. The same goes for the WinRT based implementation on Windows, but this API is only available for applications that have a so called "package identity", and does not seem to be stable for Windows 10. To cover the cases where this API is not available we fall back to drawing the badge manually, and set it as an overlay icon on the task bar using ITaskbarList3. The look of this badge has been tweaked to match the Windows 11/10 styles, and will pick up the user's choice of dark/light mode and accent color if available. [1] https://github.com/leolin310148/ShortcutBadger/ [ChangeLog][QtGui] Added QGuiApplication::setBadgeNumber() to inform the user about e.g. the number of unread e-mail or queued tasks. 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. Task-number: QTBUG-94009 Change-Id: I6447d55177f9987b0dfcd93caf63c6167f7224c7 Reviewed-by: Axel Spoerl <axel.spoerl@qt.io> Reviewed-by: Oliver Wolff <oliver.wolff@qt.io> Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
This commit is contained in:
parent
846cda7eb0
commit
7e4c7d50a7
@ -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
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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)
|
||||
|
||||
/*!
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -58,6 +58,8 @@ public:
|
||||
|
||||
void beep() const override;
|
||||
|
||||
void setApplicationBadge(qint64 number) override;
|
||||
|
||||
static QIOSIntegration *instance();
|
||||
|
||||
// -- QPlatformNativeInterface --
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -48,6 +48,11 @@
|
||||
#include <QtCore/qdebug.h>
|
||||
#include <QtCore/qvariant.h>
|
||||
|
||||
#include <QtCore/qoperatingsystemversion.h>
|
||||
#include <QtCore/private/qfunctions_win_p.h>
|
||||
|
||||
#include <wrl.h>
|
||||
|
||||
#include <limits.h>
|
||||
|
||||
#if !defined(QT_NO_OPENGL)
|
||||
@ -56,6 +61,13 @@
|
||||
|
||||
#include "qwindowsopengltester.h"
|
||||
|
||||
#if QT_CONFIG(cpp_winrt)
|
||||
# include <winrt/Windows.UI.Notifications.h>
|
||||
# include <winrt/Windows.Data.Xml.Dom.h>
|
||||
# include <winrt/Windows.Foundation.h>
|
||||
# include <winrt/Windows.UI.ViewManagement.h>
|
||||
#endif
|
||||
|
||||
#include <memory>
|
||||
|
||||
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<ITaskbarList3> 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<HWND>(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
|
||||
{
|
||||
|
@ -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<QWindowsIntegrationPrivate> d;
|
||||
|
||||
static QWindowsIntegration *m_instance;
|
||||
|
||||
qint64 m_applicationBadgeNumber = 0;
|
||||
};
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
Loading…
Reference in New Issue
Block a user