From affb6700a4a9621e5c80ac1f733f891cb31be701 Mon Sep 17 00:00:00 2001 From: Friedemann Kleint Date: Fri, 21 Apr 2017 12:26:44 +0200 Subject: Windows QPA: Move system tray icon implementation into the plugin The system tray icon implementation relied on QMenu. After the introduction of QPlatformSystemTrayIcon::contextMenuRequested(), the code can be moved into the plugin, making use of native menus when enabled or falling back to QMenu. This enables the SystemTrayIcon QML Type to work. [ChangeLog][QtGui][Windows] A native system tray icon is now available for SystemTrayIcon. Change-Id: I0fdbfb5cbb815c1ea6fb19305a9bceb9c5bcc034 Reviewed-by: Joerg Bornemann --- src/plugins/platforms/windows/qwindowscontext.cpp | 1 + src/plugins/platforms/windows/qwindowscontext.h | 1 + .../platforms/windows/qwindowssystemtrayicon.cpp | 456 ++++++++++++++++++ .../platforms/windows/qwindowssystemtrayicon.h | 103 ++++ src/plugins/platforms/windows/qwindowstheme.cpp | 10 + src/plugins/platforms/windows/qwindowstheme.h | 3 + src/plugins/platforms/windows/windows.pri | 5 + src/widgets/util/qsystemtrayicon_win.cpp | 530 --------------------- src/widgets/util/util.pri | 4 +- 9 files changed, 580 insertions(+), 533 deletions(-) create mode 100644 src/plugins/platforms/windows/qwindowssystemtrayicon.cpp create mode 100644 src/plugins/platforms/windows/qwindowssystemtrayicon.h delete mode 100644 src/widgets/util/qsystemtrayicon_win.cpp (limited to 'src') diff --git a/src/plugins/platforms/windows/qwindowscontext.cpp b/src/plugins/platforms/windows/qwindowscontext.cpp index b6b51cd60a..56275f7028 100644 --- a/src/plugins/platforms/windows/qwindowscontext.cpp +++ b/src/plugins/platforms/windows/qwindowscontext.cpp @@ -94,6 +94,7 @@ Q_LOGGING_CATEGORY(lcQpaDialogs, "qt.qpa.dialogs") Q_LOGGING_CATEGORY(lcQpaMenus, "qt.qpa.menus") Q_LOGGING_CATEGORY(lcQpaTablet, "qt.qpa.input.tablet") Q_LOGGING_CATEGORY(lcQpaAccessibility, "qt.qpa.accessibility") +Q_LOGGING_CATEGORY(lcQpaTrayIcon, "qt.qpa.trayicon") int QWindowsContext::verbose = 0; diff --git a/src/plugins/platforms/windows/qwindowscontext.h b/src/plugins/platforms/windows/qwindowscontext.h index 7f4a76af05..338c27e785 100644 --- a/src/plugins/platforms/windows/qwindowscontext.h +++ b/src/plugins/platforms/windows/qwindowscontext.h @@ -66,6 +66,7 @@ Q_DECLARE_LOGGING_CATEGORY(lcQpaDialogs) Q_DECLARE_LOGGING_CATEGORY(lcQpaMenus) Q_DECLARE_LOGGING_CATEGORY(lcQpaTablet) Q_DECLARE_LOGGING_CATEGORY(lcQpaAccessibility) +Q_DECLARE_LOGGING_CATEGORY(lcQpaTrayIcon) class QWindow; class QPlatformScreen; diff --git a/src/plugins/platforms/windows/qwindowssystemtrayicon.cpp b/src/plugins/platforms/windows/qwindowssystemtrayicon.cpp new file mode 100644 index 0000000000..049989e9e6 --- /dev/null +++ b/src/plugins/platforms/windows/qwindowssystemtrayicon.cpp @@ -0,0 +1,456 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the plugins of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#if defined(WINVER) && WINVER < 0x0601 +# undef WINVER +#endif +#if !defined(WINVER) +# define WINVER 0x0601 // required for NOTIFYICONDATA_V2_SIZE, ChangeWindowMessageFilterEx() (MinGW 5.3) +#endif + +#if defined(NTDDI_VERSION) && NTDDI_VERSION < 0x06010000 +# undef NTDDI_VERSION +#endif +#if !defined(NTDDI_VERSION) +# define NTDDI_VERSION 0x06010000 // required for Shell_NotifyIconGetRect (MinGW 5.3) +#endif + +#include "qwindowssystemtrayicon.h" +#include "qwindowscontext.h" +#include "qwindowstheme.h" +#include "qwindowsmenu.h" +#include "qwindowsscreen.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +static const UINT q_uNOTIFYICONID = 0; + +static uint MYWM_TASKBARCREATED = 0; +#define MYWM_NOTIFYICON (WM_APP+101) + +Q_GUI_EXPORT HICON qt_pixmapToWinHICON(const QPixmap &); + +// Copy QString data to a limited wchar_t array including \0. +static inline void qStringToLimitedWCharArray(QString in, wchar_t *target, int maxLength) +{ + const int length = qMin(maxLength - 1, in.size()); + if (length < in.size()) + in.truncate(length); + in.toWCharArray(target); + target[length] = wchar_t(0); +} + +static inline void initNotifyIconData(NOTIFYICONDATA &tnd) +{ + memset(&tnd, 0, sizeof(NOTIFYICONDATA)); + tnd.cbSize = sizeof(NOTIFYICONDATA); + tnd.uVersion = NOTIFYICON_VERSION_4; +} + +static void setIconContents(NOTIFYICONDATA &tnd, const QString &tip, HICON hIcon) +{ + tnd.uFlags |= NIF_MESSAGE | NIF_ICON | NIF_TIP; + tnd.uCallbackMessage = MYWM_NOTIFYICON; + tnd.hIcon = hIcon; + qStringToLimitedWCharArray(tip, tnd.szTip, sizeof(tnd.szTip) / sizeof(wchar_t)); +} + +// Match the HWND of the dummy window to the instances +struct QWindowsHwndSystemTrayIconEntry +{ + HWND hwnd; + QWindowsSystemTrayIcon *trayIcon; +}; + +typedef QVector HwndTrayIconEntries; + +Q_GLOBAL_STATIC(HwndTrayIconEntries, hwndTrayIconEntries) + +static int indexOfHwnd(HWND hwnd) +{ + const HwndTrayIconEntries *entries = hwndTrayIconEntries(); + for (int i = 0, size = entries->size(); i < size; ++i) { + if (entries->at(i).hwnd == hwnd) + return i; + } + return -1; +} + +extern "C" LRESULT QT_WIN_CALLBACK qWindowsTrayIconWndProc(HWND hwnd, UINT message, + WPARAM wParam, LPARAM lParam) +{ + if (message == MYWM_TASKBARCREATED || message == MYWM_NOTIFYICON + || message == WM_INITMENU || message == WM_INITMENUPOPUP + || message == WM_COMMAND) { + const int index = indexOfHwnd(hwnd); + if (index >= 0) { + MSG msg; + msg.hwnd = hwnd; // re-create MSG structure + msg.message = message; // time and pt fields ignored + msg.wParam = wParam; + msg.lParam = lParam; + msg.pt.x = GET_X_LPARAM(lParam); + msg.pt.y = GET_Y_LPARAM(lParam); + long result = 0; + if (hwndTrayIconEntries()->at(index).trayIcon->winEvent(msg, &result)) + return result; + } + } + return DefWindowProc(hwnd, message, wParam, lParam); +} + +// Note: Message windows (HWND_MESSAGE) are not sufficient, they +// will not receive the "TaskbarCreated" message. +static inline HWND createTrayIconMessageWindow() +{ + QWindowsContext *ctx = QWindowsContext::instance(); + if (!ctx) + return 0; + // Register window class in the platform plugin. + const QString className = + ctx->registerWindowClass(QStringLiteral("QTrayIconMessageWindowClass"), + qWindowsTrayIconWndProc); + const wchar_t windowName[] = L"QTrayIconMessageWindow"; + return CreateWindowEx(0, reinterpret_cast(className.utf16()), + windowName, WS_OVERLAPPED, + CW_USEDEFAULT, CW_USEDEFAULT, + CW_USEDEFAULT, CW_USEDEFAULT, + NULL, NULL, (HINSTANCE)GetModuleHandle(0), NULL); +} + +/*! + \class QWindowsSystemTrayIcon + \brief Windows native system tray icon + + \internal + \ingroup qt-lighthouse-win +*/ + +QWindowsSystemTrayIcon::QWindowsSystemTrayIcon() +{ + // For restoring the tray icon after explorer crashes + if (!MYWM_TASKBARCREATED) + MYWM_TASKBARCREATED = RegisterWindowMessage(L"TaskbarCreated"); + // Allow the WM_TASKBARCREATED message through the UIPI filter + ChangeWindowMessageFilterEx(m_hwnd, MYWM_TASKBARCREATED, MSGFLT_ALLOW, 0); + qCDebug(lcQpaTrayIcon) << __FUNCTION__ << this << "MYWM_TASKBARCREATED=" << MYWM_TASKBARCREATED; +} + +QWindowsSystemTrayIcon::~QWindowsSystemTrayIcon() +{ + qCDebug(lcQpaTrayIcon) << __FUNCTION__ << this; + ensureCleanup(); +} + +void QWindowsSystemTrayIcon::init() +{ + qCDebug(lcQpaTrayIcon) << __FUNCTION__ << this; + ensureInstalled(); +} + +void QWindowsSystemTrayIcon::cleanup() +{ + qCDebug(lcQpaTrayIcon) << __FUNCTION__ << this; + ensureCleanup(); +} + +void QWindowsSystemTrayIcon::updateIcon(const QIcon &icon) +{ + qCDebug(lcQpaTrayIcon) << __FUNCTION__ << '(' << icon << ')' << this; + if (icon.cacheKey() == m_icon.cacheKey()) + return; + const HICON hIconToDestroy = createIcon(icon); + if (ensureInstalled()) + sendTrayMessage(NIM_MODIFY); + if (hIconToDestroy) + DestroyIcon(hIconToDestroy); +} + +void QWindowsSystemTrayIcon::updateToolTip(const QString &tooltip) +{ + qCDebug(lcQpaTrayIcon) << __FUNCTION__ << '(' << tooltip << ')' << this; + if (m_toolTip == tooltip) + return; + m_toolTip = tooltip; + if (isInstalled()) + sendTrayMessage(NIM_MODIFY); +} + +QRect QWindowsSystemTrayIcon::geometry() const +{ + NOTIFYICONIDENTIFIER nid; + memset(&nid, 0, sizeof(nid)); + nid.cbSize = sizeof(nid); + nid.hWnd = m_hwnd; + nid.uID = q_uNOTIFYICONID; + RECT rect; + const QRect result = SUCCEEDED(Shell_NotifyIconGetRect(&nid, &rect)) + ? QRect(rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top) + : QRect(); + qCDebug(lcQpaTrayIcon) << __FUNCTION__ << this << "returns" << result; + return result; +} + +void QWindowsSystemTrayIcon::showMessage(const QString &title, const QString &messageIn, + const QIcon &icon, + QPlatformSystemTrayIcon::MessageIcon iconType, + int msecsIn) +{ + qCDebug(lcQpaTrayIcon) << __FUNCTION__ << '(' << title << messageIn << icon + << iconType << msecsIn << ')' << this; + if (!supportsMessages()) + return; + // For empty messages, ensures that they show when only title is set + QString message = messageIn; + if (message.isEmpty() && !title.isEmpty()) + message.append(QLatin1Char(' ')); + + NOTIFYICONDATA tnd; + initNotifyIconData(tnd); + qStringToLimitedWCharArray(message, tnd.szInfo, 256); + qStringToLimitedWCharArray(title, tnd.szInfoTitle, 64); + + tnd.uID = q_uNOTIFYICONID; + tnd.dwInfoFlags = NIIF_USER; + + QSize size(GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON)); + const QSize largeIcon(GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON)); + const QSize more = icon.actualSize(largeIcon); + if (more.height() > (largeIcon.height() * 3/4) || more.width() > (largeIcon.width() * 3/4)) { + tnd.dwInfoFlags |= NIIF_LARGE_ICON; + size = largeIcon; + } + QPixmap pm = icon.pixmap(size); + if (pm.isNull()) { + tnd.dwInfoFlags = NIIF_INFO; + } else { + if (pm.size() != size) { + qWarning("QSystemTrayIcon::showMessage: Wrong icon size (%dx%d), please add standard one: %dx%d", + pm.size().width(), pm.size().height(), size.width(), size.height()); + pm = pm.scaled(size, Qt::IgnoreAspectRatio); + } + tnd.hBalloonIcon = qt_pixmapToWinHICON(pm); + } + tnd.hWnd = m_hwnd; + tnd.uTimeout = msecsIn <= 0 ? UINT(10000) : UINT(msecsIn); // 10s default + tnd.uFlags = NIF_INFO | NIF_SHOWTIP; + + Shell_NotifyIcon(NIM_MODIFY, &tnd); +} + +bool QWindowsSystemTrayIcon::supportsMessages() const +{ + bool result = true; // The key does typically not exist on Windows 10, default to true. + static const wchar_t regKey[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced"; + HKEY handle; + if (RegOpenKeyEx(HKEY_CURRENT_USER, regKey, 0, KEY_READ, &handle) == ERROR_SUCCESS) { + DWORD type; + static const wchar_t subKey[] = L"EnableBalloonTips"; + if (RegQueryValueEx(handle, subKey, 0, &type, 0, 0) == ERROR_SUCCESS && type == REG_DWORD) { + DWORD value; + DWORD size = sizeof(value); + if (RegQueryValueEx(handle, subKey, 0, 0, reinterpret_cast(&value), &size) == ERROR_SUCCESS) + result = value != 0; + } + RegCloseKey(handle); + } + return result; +} + +QPlatformMenu *QWindowsSystemTrayIcon::createMenu() const +{ + if (QWindowsTheme::useNativeMenus() && m_menu.isNull()) + m_menu = new QWindowsPopupMenu; + qCDebug(lcQpaTrayIcon) << __FUNCTION__ << this << "returns" << m_menu.data(); + return m_menu.data(); +} + +// Delay-install until an Icon exists +bool QWindowsSystemTrayIcon::ensureInstalled() +{ + if (isInstalled()) + return true; + if (m_hIcon == nullptr) + return false; + m_hwnd = createTrayIconMessageWindow(); + if (Q_UNLIKELY(m_hwnd == nullptr)) + return false; + QWindowsHwndSystemTrayIconEntry entry{m_hwnd, this}; + hwndTrayIconEntries()->append(entry); + sendTrayMessage(NIM_ADD); + return true; +} + +void QWindowsSystemTrayIcon::ensureCleanup() +{ + if (isInstalled()) { + const int index = indexOfHwnd(m_hwnd); + if (index >= 0) + hwndTrayIconEntries()->removeAt(index); + sendTrayMessage(NIM_DELETE); + DestroyWindow(m_hwnd); + m_hwnd = nullptr; + } + if (m_hIcon != nullptr) + DestroyIcon(m_hIcon); + m_hIcon = nullptr; + m_menu = nullptr; // externally owned + m_toolTip.clear(); +} + +bool QWindowsSystemTrayIcon::sendTrayMessage(DWORD msg) +{ + NOTIFYICONDATA tnd; + initNotifyIconData(tnd); + tnd.uID = q_uNOTIFYICONID; + tnd.hWnd = m_hwnd; + tnd.uFlags = NIF_SHOWTIP; + if (msg == NIM_ADD || msg == NIM_MODIFY) + setIconContents(tnd, m_toolTip, m_hIcon); + if (!Shell_NotifyIcon(msg, &tnd)) + return false; + return msg != NIM_ADD || Shell_NotifyIcon(NIM_SETVERSION, &tnd); +} + +// Return the old icon to be freed after modifying the tray icon. +HICON QWindowsSystemTrayIcon::createIcon(const QIcon &icon) +{ + const HICON oldIcon = m_hIcon; + m_hIcon = nullptr; + if (icon.isNull()) + return oldIcon; + const QSize requestedSize(GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON)); + const QSize size = icon.actualSize(requestedSize); + const QPixmap pm = icon.pixmap(size); + if (!pm.isNull()) + m_hIcon = qt_pixmapToWinHICON(pm); + return oldIcon; +} + +bool QWindowsSystemTrayIcon::winEvent(const MSG &message, long *result) +{ + *result = 0; + switch (message.message) { + case MYWM_NOTIFYICON: { + Q_ASSERT(q_uNOTIFYICONID == HIWORD(message.lParam)); + const int trayMessage = LOWORD(message.lParam); + switch (trayMessage) { + case NIN_SELECT: + case NIN_KEYSELECT: + if (m_ignoreNextMouseRelease) + m_ignoreNextMouseRelease = false; + else + emit activated(Trigger); + break; + case WM_LBUTTONDBLCLK: + m_ignoreNextMouseRelease = true; // Since DBLCLICK Generates a second mouse + emit activated(DoubleClick); // release we must ignore it + break; + case WM_CONTEXTMENU: { + const QPoint globalPos = QPoint(GET_X_LPARAM(message.wParam), GET_Y_LPARAM(message.wParam)); + const QPlatformScreen *screen = QWindowsContext::instance()->screenManager().screenAtDp(globalPos); + emit contextMenuRequested(globalPos, screen); + emit activated(Context); + if (m_menu) + m_menu->trackPopupMenu(message.hwnd, globalPos.x(), globalPos.y()); + } + break; + case NIN_BALLOONUSERCLICK: + emit messageClicked(); + break; + case WM_MBUTTONUP: + emit activated(MiddleClick); + break; + default: + break; + } + } + break; + case WM_INITMENU: + case WM_INITMENUPOPUP: + QWindowsPopupMenu::notifyAboutToShow(reinterpret_cast(message.wParam)); + break; + case WM_COMMAND: + QWindowsPopupMenu::notifyTriggered(LOWORD(message.wParam)); + break; + default: + if (message.message == MYWM_TASKBARCREATED) // self-registered message id (tray crashed) + sendTrayMessage(NIM_ADD); + break; + } + return false; +} + +#ifndef QT_NO_DEBUG_STREAM + +void QWindowsSystemTrayIcon::formatDebug(QDebug &d) const +{ + d << static_cast(this) << ", \"" << m_toolTip + << "\", hwnd=" << m_hwnd << ", m_hIcon=" << m_hIcon << ", menu=" + << m_menu.data(); +} + +QDebug operator<<(QDebug d, const QWindowsSystemTrayIcon *t) +{ + QDebugStateSaver saver(d); + d.nospace(); + d.noquote(); + d << "QWindowsSystemTrayIcon("; + if (t) + t->formatDebug(d); + else + d << '0'; + d << ')'; + return d; +} +#endif // !QT_NO_DEBUG_STREAM + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/windows/qwindowssystemtrayicon.h b/src/plugins/platforms/windows/qwindowssystemtrayicon.h new file mode 100644 index 0000000000..1f696180cd --- /dev/null +++ b/src/plugins/platforms/windows/qwindowssystemtrayicon.h @@ -0,0 +1,103 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the plugins of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QWINDOWSSYSTEMTRAYICON_H +#define QWINDOWSSYSTEMTRAYICON_H + +#include +#include + +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class QDebug; + +class QWindowsPopupMenu; + +class QWindowsSystemTrayIcon : public QPlatformSystemTrayIcon +{ +public: + QWindowsSystemTrayIcon(); + ~QWindowsSystemTrayIcon(); + + void init() override; + void cleanup() override; + void updateIcon(const QIcon &icon) override; + void updateToolTip(const QString &tooltip) override; + void updateMenu(QPlatformMenu *) override {} + QRect geometry() const override; + void showMessage(const QString &title, const QString &msg, + const QIcon &icon, MessageIcon iconType, int msecs) override; + + bool isSystemTrayAvailable() const override { return true; } + bool supportsMessages() const override; + + QPlatformMenu *createMenu() const override; + + bool winEvent(const MSG &message, long *result); + +#ifndef QT_NO_DEBUG_STREAM + void formatDebug(QDebug &d) const; +#endif + +private: + bool isInstalled() const { return m_hwnd != nullptr; } + bool ensureInstalled(); + void ensureCleanup(); + bool sendTrayMessage(DWORD msg); + HICON createIcon(const QIcon &icon); + + QIcon m_icon; + QString m_toolTip; + HWND m_hwnd = nullptr; + HICON m_hIcon = nullptr; + mutable QPointer m_menu; + bool m_ignoreNextMouseRelease = false; +}; + +#ifndef QT_NO_DEBUG_STREAM +QDebug operator<<(QDebug d, const QWindowsSystemTrayIcon *); +#endif // !QT_NO_DEBUG_STREAM + +QT_END_NAMESPACE + +#endif // QWINDOWSSYSTEMTRAYICON_H diff --git a/src/plugins/platforms/windows/qwindowstheme.cpp b/src/plugins/platforms/windows/qwindowstheme.cpp index 5fe7c7c071..3165835d2d 100644 --- a/src/plugins/platforms/windows/qwindowstheme.cpp +++ b/src/plugins/platforms/windows/qwindowstheme.cpp @@ -48,6 +48,9 @@ #include "qwindowsdialoghelpers.h" #include "qwindowscontext.h" #include "qwindowsintegration.h" +#if QT_CONFIG(systemtrayicon) +# include "qwindowssystemtrayicon.h" +#endif #include "qt_windows.h" #include #include @@ -549,6 +552,13 @@ QPlatformDialogHelper *QWindowsTheme::createPlatformDialogHelper(DialogType type return QWindowsDialogs::createHelper(type); } +#if QT_CONFIG(systemtrayicon) +QPlatformSystemTrayIcon *QWindowsTheme::createPlatformSystemTrayIcon() const +{ + return new QWindowsSystemTrayIcon; +} +#endif + void QWindowsTheme::windowsThemeChanged(QWindow * window) { refresh(); diff --git a/src/plugins/platforms/windows/qwindowstheme.h b/src/plugins/platforms/windows/qwindowstheme.h index 7ce3023111..237e8158fa 100644 --- a/src/plugins/platforms/windows/qwindowstheme.h +++ b/src/plugins/platforms/windows/qwindowstheme.h @@ -59,6 +59,9 @@ public: bool usePlatformNativeDialog(DialogType type) const override; QPlatformDialogHelper *createPlatformDialogHelper(DialogType type) const override; +#if QT_CONFIG(systemtrayicon) + QPlatformSystemTrayIcon *createPlatformSystemTrayIcon() const override; +#endif QVariant themeHint(ThemeHint) const override; const QPalette *palette(Palette type = SystemPalette) const override { return m_palettes[type]; } diff --git a/src/plugins/platforms/windows/windows.pri b/src/plugins/platforms/windows/windows.pri index a7166e90e4..26f9e3aefe 100644 --- a/src/plugins/platforms/windows/windows.pri +++ b/src/plugins/platforms/windows/windows.pri @@ -70,6 +70,11 @@ qtConfig(dynamicgl) { HEADERS += $$PWD/qwindowseglcontext.h } +qtConfig(systemtrayicon) { + SOURCES += $$PWD/qwindowssystemtrayicon.cpp + HEADERS += $$PWD/qwindowssystemtrayicon.h +} + qtConfig(vulkan) { SOURCES += $$PWD/qwindowsvulkaninstance.cpp HEADERS += $$PWD/qwindowsvulkaninstance.h diff --git a/src/widgets/util/qsystemtrayicon_win.cpp b/src/widgets/util/qsystemtrayicon_win.cpp deleted file mode 100644 index 91a553e28f..0000000000 --- a/src/widgets/util/qsystemtrayicon_win.cpp +++ /dev/null @@ -1,530 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtWidgets module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#include "qsystemtrayicon_p.h" -#ifndef QT_NO_SYSTEMTRAYICON - -#if defined(WINVER) && WINVER < 0x0601 -# undef WINVER -#endif -#if !defined(WINVER) -# define WINVER 0x0601 // required for NOTIFYICONDATA_V2_SIZE, ChangeWindowMessageFilterEx() (MinGW 5.3) -#endif - -#if defined(NTDDI_VERSION) && NTDDI_VERSION < 0x06010000 -# undef NTDDI_VERSION -#endif -#if !defined(NTDDI_VERSION) -# define NTDDI_VERSION 0x06010000 // required for Shell_NotifyIconGetRect (MinGW 5.3) -#endif - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -QT_BEGIN_NAMESPACE - -static const UINT q_uNOTIFYICONID = 0; - -static uint MYWM_TASKBARCREATED = 0; -#define MYWM_NOTIFYICON (WM_APP+101) - -Q_GUI_EXPORT HICON qt_pixmapToWinHICON(const QPixmap &); - -// Copy QString data to a limited wchar_t array including \0. -static inline void qStringToLimitedWCharArray(QString in, wchar_t *target, int maxLength) -{ - const int length = qMin(maxLength - 1, in.size()); - if (length < in.size()) - in.truncate(length); - in.toWCharArray(target); - target[length] = wchar_t(0); -} - -class QSystemTrayIconSys -{ -public: - QSystemTrayIconSys(HWND hwnd, QSystemTrayIcon *object); - ~QSystemTrayIconSys(); - bool trayMessage(DWORD msg); - void setIconContents(NOTIFYICONDATA &data); - bool showMessage(const QString &title, const QString &message, const QIcon &icon, uint uSecs); - QRect findIconGeometry(UINT iconId); - HICON createIcon(); - bool winEvent(MSG *m, long *result); - -private: - const HWND m_hwnd; - HICON hIcon; - QPoint globalPos; - QSystemTrayIcon *q; - bool ignoreNextMouseRelease; -}; - -static bool allowsMessages() -{ -#ifndef QT_NO_SETTINGS - const QString key = QStringLiteral("HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced"); - const QSettings settings(key, QSettings::NativeFormat); - return settings.value(QStringLiteral("EnableBalloonTips"), true).toBool(); -#else - return false; -#endif -} - -typedef QHash HandleTrayIconHash; - -Q_GLOBAL_STATIC(HandleTrayIconHash, handleTrayIconHash) - -extern "C" LRESULT QT_WIN_CALLBACK qWindowsTrayconWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) -{ - if (message == MYWM_TASKBARCREATED || message == MYWM_NOTIFYICON) { - if (QSystemTrayIconSys *trayIcon = handleTrayIconHash()->value(hwnd)) { - MSG msg; - msg.hwnd = hwnd; // re-create MSG structure - msg.message = message; // time and pt fields ignored - msg.wParam = wParam; - msg.lParam = lParam; - msg.pt.x = GET_X_LPARAM(lParam); - msg.pt.y = GET_Y_LPARAM(lParam); - long result = 0; - if (trayIcon->winEvent(&msg, &result)) - return result; - } - } - return DefWindowProc(hwnd, message, wParam, lParam); -} - -// Invoke a service of the native Windows interface to create -// a non-visible toplevel window to receive tray messages. -// Note: Message windows (HWND_MESSAGE) are not sufficient, they -// will not receive the "TaskbarCreated" message. -static inline HWND createTrayIconMessageWindow() -{ - QPlatformNativeInterface *ni = QGuiApplication::platformNativeInterface(); - if (!ni) - return 0; - // Register window class in the platform plugin. - QString className; - void *wndProc = reinterpret_cast(qWindowsTrayconWndProc); - if (!QMetaObject::invokeMethod(ni, "registerWindowClass", Qt::DirectConnection, - Q_RETURN_ARG(QString, className), - Q_ARG(QString, QStringLiteral("QTrayIconMessageWindowClass")), - Q_ARG(void *, wndProc))) { - return 0; - } - const wchar_t windowName[] = L"QTrayIconMessageWindow"; - return CreateWindowEx(0, (wchar_t*)className.utf16(), - windowName, WS_OVERLAPPED, - CW_USEDEFAULT, CW_USEDEFAULT, - CW_USEDEFAULT, CW_USEDEFAULT, - NULL, NULL, (HINSTANCE)GetModuleHandle(0), NULL); -} - -static inline void initNotifyIconData(NOTIFYICONDATA &tnd) -{ - memset(&tnd, 0, sizeof(NOTIFYICONDATA)); - tnd.cbSize = sizeof(NOTIFYICONDATA); - tnd.uVersion = NOTIFYICON_VERSION_4; -} - -QSystemTrayIconSys::QSystemTrayIconSys(HWND hwnd, QSystemTrayIcon *object) - : m_hwnd(hwnd), hIcon(0), q(object) - , ignoreNextMouseRelease(false) - -{ - handleTrayIconHash()->insert(m_hwnd, this); - - // For restoring the tray icon after explorer crashes - if (!MYWM_TASKBARCREATED) { - MYWM_TASKBARCREATED = RegisterWindowMessage(L"TaskbarCreated"); - } - - // Allow the WM_TASKBARCREATED message through the UIPI filter - ChangeWindowMessageFilterEx(m_hwnd, MYWM_TASKBARCREATED, MSGFLT_ALLOW, 0); -} - -QSystemTrayIconSys::~QSystemTrayIconSys() -{ - handleTrayIconHash()->remove(m_hwnd); - if (hIcon) - DestroyIcon(hIcon); - DestroyWindow(m_hwnd); -} - -void QSystemTrayIconSys::setIconContents(NOTIFYICONDATA &tnd) -{ - tnd.uFlags |= NIF_MESSAGE | NIF_ICON | NIF_TIP; - tnd.uCallbackMessage = MYWM_NOTIFYICON; - tnd.hIcon = hIcon; - const QString tip = q->toolTip(); - if (!tip.isNull()) - qStringToLimitedWCharArray(tip, tnd.szTip, sizeof(tnd.szTip)/sizeof(wchar_t)); -} - -bool QSystemTrayIconSys::showMessage(const QString &title, const QString &message, const QIcon &icon, uint uSecs) -{ - NOTIFYICONDATA tnd; - initNotifyIconData(tnd); - qStringToLimitedWCharArray(message, tnd.szInfo, 256); - qStringToLimitedWCharArray(title, tnd.szInfoTitle, 64); - - tnd.uID = q_uNOTIFYICONID; - tnd.dwInfoFlags = NIIF_USER; - - QSize size(GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON)); - const QSize largeIcon(GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON)); - const QSize more = icon.actualSize(largeIcon); - if (more.height() > (largeIcon.height() * 3/4) || more.width() > (largeIcon.width() * 3/4)) { - tnd.dwInfoFlags |= NIIF_LARGE_ICON; - size = largeIcon; - } - QPixmap pm = icon.pixmap(size); - if (pm.isNull()) { - tnd.dwInfoFlags = NIIF_INFO; - } else { - if (pm.size() != size) { - qWarning("QSystemTrayIcon::showMessage: Wrong icon size (%dx%d), please add standard one: %dx%d", - pm.size().width(), pm.size().height(), size.width(), size.height()); - pm = pm.scaled(size, Qt::IgnoreAspectRatio); - } - tnd.hBalloonIcon = qt_pixmapToWinHICON(pm); - } - tnd.hWnd = m_hwnd; - tnd.uTimeout = uSecs; - tnd.uFlags = NIF_INFO | NIF_SHOWTIP; - - return Shell_NotifyIcon(NIM_MODIFY, &tnd); -} - -bool QSystemTrayIconSys::trayMessage(DWORD msg) -{ - NOTIFYICONDATA tnd; - initNotifyIconData(tnd); - - tnd.uID = q_uNOTIFYICONID; - tnd.hWnd = m_hwnd; - tnd.uFlags = NIF_SHOWTIP; - - if (msg == NIM_ADD || msg == NIM_MODIFY) { - setIconContents(tnd); - } - - bool success = Shell_NotifyIcon(msg, &tnd); - - if (msg == NIM_ADD) - return success && Shell_NotifyIcon(NIM_SETVERSION, &tnd); - else - return success; -} - -HICON QSystemTrayIconSys::createIcon() -{ - const HICON oldIcon = hIcon; - hIcon = 0; - const QIcon icon = q->icon(); - if (icon.isNull()) - return oldIcon; - const QSize requestedSize(GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON)); - const QSize size = icon.actualSize(requestedSize); - const QPixmap pm = icon.pixmap(size); - if (pm.isNull()) - return oldIcon; - hIcon = qt_pixmapToWinHICON(pm); - return oldIcon; -} - -bool QSystemTrayIconSys::winEvent( MSG *m, long *result ) -{ - *result = 0; - switch(m->message) { - case MYWM_NOTIFYICON: - { - Q_ASSERT(q_uNOTIFYICONID == HIWORD(m->lParam)); - const int message = LOWORD(m->lParam); - const QPoint gpos = QPoint(GET_X_LPARAM(m->wParam), GET_Y_LPARAM(m->wParam)); - - switch (message) { - case NIN_SELECT: - case NIN_KEYSELECT: - if (ignoreNextMouseRelease) - ignoreNextMouseRelease = false; - else - emit q->activated(QSystemTrayIcon::Trigger); - break; - - case WM_LBUTTONDBLCLK: - ignoreNextMouseRelease = true; // Since DBLCLICK Generates a second mouse - // release we must ignore it - emit q->activated(QSystemTrayIcon::DoubleClick); - break; - - case WM_CONTEXTMENU: - if (q->contextMenu()) { - q->contextMenu()->popup(gpos); - q->contextMenu()->activateWindow(); - } - emit q->activated(QSystemTrayIcon::Context); - break; - - case NIN_BALLOONUSERCLICK: - emit q->messageClicked(); - break; - - case WM_MBUTTONUP: - emit q->activated(QSystemTrayIcon::MiddleClick); - break; - - default: - break; - } - break; - } - default: - if (m->message == MYWM_TASKBARCREATED) // self-registered message id. - trayMessage(NIM_ADD); - break; - } - return false; -} - -QSystemTrayIconPrivate::QSystemTrayIconPrivate() - : sys(0), - qpa_sys(nullptr), - visible(false) -{ -} - -QSystemTrayIconPrivate::~QSystemTrayIconPrivate() -{ -} - -void QSystemTrayIconPrivate::install_sys() -{ - Q_Q(QSystemTrayIcon); - if (!sys) { - if (const HWND hwnd = createTrayIconMessageWindow()) { - sys = new QSystemTrayIconSys(hwnd, q); - sys->createIcon(); - sys->trayMessage(NIM_ADD); - } else { - qWarning("The platform plugin failed to create a message window."); - } - } -} - -/* -* This function tries to determine the icon geometry from the tray -* -* If it fails an invalid rect is returned. -*/ - -QRect QSystemTrayIconSys::findIconGeometry(UINT iconId) -{ - struct AppData - { - HWND hwnd; - UINT uID; - }; - - NOTIFYICONIDENTIFIER nid; - memset(&nid, 0, sizeof(nid)); - nid.cbSize = sizeof(nid); - nid.hWnd = m_hwnd; - nid.uID = iconId; - - RECT rect; - if (SUCCEEDED(Shell_NotifyIconGetRect(&nid, &rect))) - return QRect(rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top); - - QRect ret; - - TBBUTTON buttonData; - DWORD processID = 0; - HWND trayHandle = FindWindow(L"Shell_TrayWnd", NULL); - - //find the toolbar used in the notification area - if (trayHandle) { - trayHandle = FindWindowEx(trayHandle, NULL, L"TrayNotifyWnd", NULL); - if (trayHandle) { - HWND hwnd = FindWindowEx(trayHandle, NULL, L"SysPager", NULL); - if (hwnd) { - hwnd = FindWindowEx(hwnd, NULL, L"ToolbarWindow32", NULL); - if (hwnd) - trayHandle = hwnd; - } - } - } - - if (!trayHandle) - return ret; - - GetWindowThreadProcessId(trayHandle, &processID); - if (processID <= 0) - return ret; - - HANDLE trayProcess = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_READ, 0, processID); - if (!trayProcess) - return ret; - - int buttonCount = SendMessage(trayHandle, TB_BUTTONCOUNT, 0, 0); - LPVOID data = VirtualAllocEx(trayProcess, NULL, sizeof(TBBUTTON), MEM_COMMIT, PAGE_READWRITE); - - if ( buttonCount < 1 || !data ) { - CloseHandle(trayProcess); - return ret; - } - - //search for our icon among all toolbar buttons - for (int toolbarButton = 0; toolbarButton < buttonCount; ++toolbarButton ) { - SIZE_T numBytes = 0; - AppData appData = { 0, 0 }; - SendMessage(trayHandle, TB_GETBUTTON, toolbarButton , (LPARAM)data); - - if (!ReadProcessMemory(trayProcess, data, &buttonData, sizeof(TBBUTTON), &numBytes)) - continue; - - if (!ReadProcessMemory(trayProcess, (LPVOID) buttonData.dwData, &appData, sizeof(AppData), &numBytes)) - continue; - - bool isHidden = buttonData.fsState & TBSTATE_HIDDEN; - - if (m_hwnd == appData.hwnd && appData.uID == iconId && !isHidden) { - SendMessage(trayHandle, TB_GETITEMRECT, toolbarButton , (LPARAM)data); - RECT iconRect = {0, 0, 0, 0}; - if(ReadProcessMemory(trayProcess, data, &iconRect, sizeof(RECT), &numBytes)) { - MapWindowPoints(trayHandle, NULL, (LPPOINT)&iconRect, 2); - QRect geometry(iconRect.left + 1, iconRect.top + 1, - iconRect.right - iconRect.left - 2, - iconRect.bottom - iconRect.top - 2); - if (geometry.isValid()) - ret = geometry; - break; - } - } - } - VirtualFreeEx(trayProcess, data, 0, MEM_RELEASE); - CloseHandle(trayProcess); - return ret; -} - -void QSystemTrayIconPrivate::showMessage_sys(const QString &title, - const QString &messageIn, - const QIcon &icon, - QSystemTrayIcon::MessageIcon, - int timeOut) -{ - if (!sys || !allowsMessages()) - return; - - // 10 sec default - const uint uSecs = timeOut < 0 ? uint(10000) : uint(timeOut); - // For empty messages, ensures that they show when only title is set - QString message = messageIn; - if (message.isEmpty() && !title.isEmpty()) - message.append(QLatin1Char(' ')); - - sys->showMessage(title, message, icon, uSecs); -} - -QRect QSystemTrayIconPrivate::geometry_sys() const -{ - if (!sys) - return QRect(); - - return sys->findIconGeometry(q_uNOTIFYICONID); -} - -void QSystemTrayIconPrivate::remove_sys() -{ - if (!sys) - return; - - sys->trayMessage(NIM_DELETE); - delete sys; - sys = 0; -} - -void QSystemTrayIconPrivate::updateIcon_sys() -{ - if (!sys) - return; - - const HICON hIconToDestroy = sys->createIcon(); - sys->trayMessage(NIM_MODIFY); - - if (hIconToDestroy) - DestroyIcon(hIconToDestroy); -} - -void QSystemTrayIconPrivate::updateMenu_sys() -{ -#if QT_CONFIG(menu) -#endif -} - -void QSystemTrayIconPrivate::updateToolTip_sys() -{ - if (!sys) - return; - - sys->trayMessage(NIM_MODIFY); -} - -bool QSystemTrayIconPrivate::isSystemTrayAvailable_sys() -{ - return true; -} - -bool QSystemTrayIconPrivate::supportsMessages_sys() -{ - return allowsMessages(); -} - -QT_END_NAMESPACE - -#endif diff --git a/src/widgets/util/util.pri b/src/widgets/util/util.pri index b9b62d9bb0..6abf6c8809 100644 --- a/src/widgets/util/util.pri +++ b/src/widgets/util/util.pri @@ -33,9 +33,7 @@ qtConfig(scroller) { util/qflickgesture.cpp \ } -win32:!winrt { - SOURCES += util/qsystemtrayicon_win.cpp -} else: qtConfig(xcb) { +qtConfig(xcb) { SOURCES += util/qsystemtrayicon_x11.cpp } else { SOURCES += util/qsystemtrayicon_qpa.cpp -- cgit v1.2.3