diff options
Diffstat (limited to 'src/gui/platform')
64 files changed, 11518 insertions, 175 deletions
diff --git a/src/gui/platform/android/qandroidnativeinterface.cpp b/src/gui/platform/android/qandroidnativeinterface.cpp new file mode 100644 index 0000000000..c1c4b7149f --- /dev/null +++ b/src/gui/platform/android/qandroidnativeinterface.cpp @@ -0,0 +1,54 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include <qpa/qplatformoffscreensurface.h> +#include <qpa/qplatformintegration.h> + +#include <QtGui/qoffscreensurface_platform.h> +#include <QtGui/private/qguiapplication_p.h> + +#include <QtGui/qpa/qplatformscreen_p.h> + +QT_BEGIN_NAMESPACE + +using namespace QNativeInterface::Private; + +/*! + \class QNativeInterface::QAndroidOffscreenSurface + \since 6.0 + \brief Native interface to a offscreen surface on Android. + + Accessed through QOffscreenSurface::nativeInterface(). + + \inmodule QtGui + \inheaderfile QOffscreenSurface + \ingroup native-interfaces + \ingroup native-interfaces-qoffscreensurface +*/ + +QT_DEFINE_NATIVE_INTERFACE(QAndroidOffscreenSurface); +QT_DEFINE_PRIVATE_NATIVE_INTERFACE(QAndroidOffScreenIntegration); + +QOffscreenSurface *QNativeInterface::QAndroidOffscreenSurface::fromNative(ANativeWindow *nativeSurface) +{ + return QGuiApplicationPrivate::platformIntegration()->call< + &QAndroidOffScreenIntegration::createOffscreenSurface>(nativeSurface); +} + +/*! + \class QNativeInterface::QAndroidScreen + \since 6.7 + \brief Native interface to a screen. + + Accessed through QScreen::nativeInterface(). + \inmodule QtGui + \ingroup native-interfaces + \ingroup native-interfaces-qscreen +*/ +/*! + \fn int QNativeInterface::QAndroidScreen::displayId() const; + \return the id of the underlying Android display. +*/ +QT_DEFINE_NATIVE_INTERFACE(QAndroidScreen); + +QT_END_NAMESPACE diff --git a/src/gui/platform/darwin/qappleiconengine.mm b/src/gui/platform/darwin/qappleiconengine.mm new file mode 100644 index 0000000000..7e0ed184dc --- /dev/null +++ b/src/gui/platform/darwin/qappleiconengine.mm @@ -0,0 +1,464 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qappleiconengine_p.h" + +#if defined(Q_OS_MACOS) +# include <AppKit/AppKit.h> +#elif defined(QT_PLATFORM_UIKIT) +# include <UIKit/UIKit.h> +#endif + +#include <QtGui/qguiapplication.h> +#include <QtGui/qpainter.h> +#include <QtGui/qpalette.h> +#include <QtGui/qstylehints.h> + +#include <QtGui/private/qcoregraphics_p.h> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +namespace { +auto *loadImage(const QString &iconName) +{ + static constexpr std::pair<QLatin1StringView, NSString *> iconMap[] = { + {"address-book-new"_L1, @"book.closed"}, + {"application-exit"_L1, @"xmark.circle"}, + {"appointment-new"_L1, @"calendar.badge.plus"}, + {"call-start"_L1, @"phone.arrow.up.right"}, + {"call-stop"_L1, @"phone.down"}, + {"contact-new"_L1, @"person.crop.circle.badge.plus"}, + {"document-new"_L1, @"doc.badge.plus"}, + {"document-open"_L1, @"folder"}, + {"document-open-recent"_L1, @"doc.badge.clock"}, + {"document-page-setup"_L1, @"doc.badge.gearshape"}, + {"document-print"_L1, @"printer"}, + //{"document-print-preview"_L1, @""}, + {"document-properties"_L1, @"doc.badge.ellipsis"}, + //{"document-revert"_L1, @""}, + {"document-save"_L1, @"square.and.arrow.down"}, + //{"document-save-as"_L1, @""}, + {"document-send"_L1, @"paperplane"}, + {"edit-clear"_L1, @"xmark.circle"}, + {"edit-copy"_L1, @"doc.on.doc"}, + {"edit-cut"_L1, @"scissors"}, + {"edit-delete"_L1, @"delete.left"}, + {"edit-find"_L1, @"magnifyingglass"}, + //{"edit-find-replace"_L1, @"arrow.up.left.and.down.right.magnifyingglass"}, + {"edit-paste"_L1, @"clipboard"}, + {"edit-redo"_L1, @"arrowshape.turn.up.right"}, + //{"edit-select-all"_L1, @""}, + {"edit-undo"_L1, @"arrowshape.turn.up.left"}, + {"folder-new"_L1, @"folder.badge.plus"}, + {"format-indent-less"_L1, @"decrease.indent"}, + {"format-indent-more"_L1, @"increase.indent"}, + {"format-justify-center"_L1, @"text.aligncenter"}, + {"format-justify-fill"_L1, @"text.justify"}, + {"format-justify-left"_L1, @"text.justify.left"}, + {"format-justify-right"_L1, @"text.justify.right"}, + {"format-text-direction-ltr"_L1, @"text.justify.leading"}, + {"format-text-direction-rtl"_L1, @"text.justify.trailing"}, + {"format-text-bold"_L1, @"bold"}, + {"format-text-italic"_L1, @"italic"}, + {"format-text-underline"_L1, @"underline"}, + {"format-text-strikethrough"_L1, @"strikethrough"}, + //{"go-bottom"_L1, @""}, + {"go-down"_L1, @"arrowshape.down"}, + {"go-first"_L1, @"increase.indent"}, + {"go-home"_L1, @"house"}, + //{"go-jump"_L1, @""}, + //{"go-last"_L1, @""}, + {"go-next"_L1, @"arrowshape.right"}, + {"go-previous"_L1, @"arrowshape.left"}, + //{"go-top"_L1, @""}, + {"go-up"_L1, @"arrowshape.up"}, + {"help-about"_L1, @"info.circle"}, + //{"help-contents"_L1, @""}, + {"help-faq"_L1, @"questionmark.app"}, + {"insert-image"_L1, @"photo.badge.plus"}, + {"insert-link"_L1, @"link.badge.plus"}, + //{"insert-object"_L1, @""}, + {"insert-text"_L1, @"textformat"}, + {"list-add"_L1, @"plus.circle"}, + {"list-remove"_L1, @"minus.circle"}, + {"mail-forward"_L1, @"arrowshape.turn.up.right"}, + {"mail-mark-important"_L1, @"star"}, + {"mail-mark-junk"_L1, @"xmark.bin"}, + {"mail-mark-notjunk"_L1, @"trash.slash"}, + {"mail-mark-read"_L1, @"envelope.open"}, + {"mail-mark-unread"_L1, @"envelope.fill"}, + {"mail-message-new"_L1, @"square.and.pencil"}, + {"mail-reply-all"_L1, @"arrowshape.turn.up.left.2"}, + {"mail-reply-sender"_L1, @"arrowshape.turn.up.left"}, + {"mail-send"_L1, @"paperplane"}, + {"mail-send-receive"_L1, @"envelope.arrow.triangle.branch"}, + {"media-eject"_L1, @"eject"}, + {"media-playback-pause"_L1, @"pause"}, + {"media-playback-start"_L1, @"play"}, + {"media-playback-stop"_L1, @"stop"}, + {"media-record"_L1, @"record.circle"}, + {"media-seek-backward"_L1, @"backward"}, + {"media-seek-forward"_L1, @"forward"}, + {"media-skip-backward"_L1, @"backward.end.alt"}, + {"media-skip-forward"_L1, @"forward.end.alt"}, + {"object-flip-horizontal"_L1, @"rectangle.landscape.rotate"}, + {"object-flip-vertical"_L1, @"rectangle.portrait.rotate"}, + {"object-rotate-left"_L1, @"rotate.left"}, + {"object-rotate-right"_L1, @"rotate.right"}, + {"process-stop"_L1, @"stop.circle"}, + {"system-lock-screen"_L1, @"lock.display"}, + {"system-log-out"_L1, @"door.left.hand.open"}, + //{"system-run"_L1, @""}, + {"system-search"_L1, @"magnifyingglass"}, + //{"system-reboot"_L1, @""}, + {"system-shutdown"_L1, @"power"}, + //{"tools-check-spelling"_L1, @""}, + {"view-fullscreen"_L1, @"arrow.up.left.and.arrow.down.right"}, + {"view-refresh"_L1, @"arrow.clockwise"}, + {"view-restore"_L1, @"arrow.down.right.and.arrow.up.left"}, + //{"view-sort-ascending"_L1, @""}, + //{"view-sort-descending"_L1, @""}, + {"window-close"_L1, @"xmark.circle"}, + {"window-new"_L1, @"macwindow.badge.plus"}, + {"zoom-fit-best"_L1, @"square.arrowtriangle.4.outward"}, + {"zoom-in"_L1, @"plus.magnifyingglass"}, + //{"zoom-original"_L1, @""}, + {"zoom-out"_L1, @"minus.magnifyingglass"}, + {"process-working"_L1, @"circle.dotted"}, + //{"accessories-calculator"_L1, @""}, + //{"accessories-character-map"_L1, @""}, + {"accessories-dictionary"_L1, @"character.book.closed"}, + {"accessories-text-editor"_L1, @"textformat"}, + {"help-browser"_L1, @"folder.badge.questionmark"}, + {"multimedia-volume-control"_L1, @"speaker.wave.3"}, + {"preferences-desktop-accessibility"_L1, @"accessibility"}, + //{"preferences-desktop-font"_L1, @""}, + {"preferences-desktop-keyboard"_L1, @"keyboard.badge.ellipsis"}, + //{"preferences-desktop-locale"_L1, @""}, + //{"preferences-desktop-multimedia"_L1, @""}, + //{"preferences-desktop-screensaver"_L1, @""}, + //{"preferences-desktop-theme"_L1, @""}, + //{"preferences-desktop-wallpaper"_L1, @""}, + {"system-file-manager"_L1, @"folder.badge.gearshape"}, + //{"system-software-install"_L1, @""}, + //{"system-software-update"_L1, @""}, d + //{"utilities-system-monitor"_L1, @""}, + {"utilities-terminal"_L1, @"apple.terminal"}, + //{"applications-accessories"_L1, @""}, + //{"applications-development"_L1, @""}, + //{"applications-engineering"_L1, @""}, + {"applications-games"_L1, @"gamecontroller"}, + //{"applications-graphics"_L1, @""}, + {"applications-internet"_L1, @"network"}, + {"applications-multimedia"_L1, @"tv.and.mediabox"}, + //{"applications-office"_L1, @""}, + //{"applications-other"_L1, @""}, + {"applications-science"_L1, @"atom"}, + //{"applications-system"_L1, @""}, + //{"applications-utilities"_L1, @""}, + {"preferences-desktop"_L1, @"menubar.dock.rectangle"}, + //{"preferences-desktop-peripherals"_L1, @""}, + //{"preferences-desktop-personal"_L1, @""}, + //{"preferences-other"_L1, @""}, + //{"preferences-system"_L1, @""}, + {"preferences-system-network"_L1, @"network"}, + {"system-help"_L1, @"questionmark.diamond"}, + {"audio-card"_L1, @"waveform.circle"}, + {"audio-input-microphone"_L1, @"mic"}, + {"battery"_L1, @"battery.100percent"}, + {"camera-photo"_L1, @"camera"}, + {"camera-video"_L1, @"video"}, + {"camera-web"_L1, @"web.camera"}, + {"computer"_L1, @"desktopcomputer"}, + {"drive-harddisk"_L1, @"internaldrive"}, + {"drive-optical"_L1, @"opticaldiscdrive"}, + {"drive-removable-media"_L1, @"externaldrive"}, + {"input-gaming"_L1, @"gamecontroller"}, // "games" also using this one + {"input-keyboard"_L1, @"keyboard"}, + {"input-mouse"_L1, @"computermouse"}, + {"input-tablet"_L1, @"ipad"}, + {"media-flash"_L1, @"mediastick"}, + //{"media-floppy"_L1, @""}, + //{"media-optical"_L1, @""}, + {"media-tape"_L1, @"recordingtape"}, + //{"modem"_L1, @""}, + {"multimedia-player"_L1, @"play.rectangle"}, + {"network-wired"_L1, @"app.connected.to.app.below.fill"}, + {"network-wireless"_L1, @"wifi"}, + //{"pda"_L1, @""}, + {"phone"_L1, @"iphone"}, + {"printer"_L1, @"printer"}, + {"scanner"_L1, @"scanner"}, + {"video-display"_L1, @"play.display"}, + //{"emblem-default"_L1, @""}, + {"emblem-documents"_L1, @"doc.circle"}, + {"emblem-downloads"_L1, @"arrow.down.circle"}, + {"emblem-favorite"_L1, @"star"}, + {"emblem-important"_L1, @"exclamationmark.bubble.circle"}, + {"emblem-mail"_L1, @"envelope"}, + {"emblem-photos"_L1, @"photo.stack"}, + //{"emblem-readonly"_L1, @""}, + {"emblem-shared"_L1, @"folder.badge.person.crop"}, + {"emblem-symbolic-link"_L1, @"link.circle"}, + {"emblem-synchronized"_L1, @"arrow.triangle.2.circlepath.circle"}, + {"emblem-system"_L1, @"gear"}, + //{"emblem-unreadable"_L1, @""}, + {"folder"_L1, @"folder"}, + //{"folder-remote"_L1, @""}, + {"network-server"_L1, @"server.rack"}, + //{"network-workgroup"_L1, @""}, + //{"start-here"_L1, @""}, + {"user-bookmarks"_L1, @"bookmark.circle"}, + {"user-desktop"_L1, @"desktopcomputer"}, //"computer" also using this one + {"user-home"_L1, @"house"}, //"go-home" also using this one + {"user-trash"_L1, @"trash"}, + {"appointment-missed"_L1, @"calendar.badge.exclamationmark"}, + {"appointment-soon"_L1, @"calendar.badge.clock"}, + {"audio-volume-high"_L1, @"speaker.wave.3"}, + {"audio-volume-low"_L1, @"speaker.wave.1"}, + {"audio-volume-medium"_L1, @"speaker.wave.2"}, + {"audio-volume-muted"_L1, @"speaker.slash"}, + {"battery-caution"_L1, @"minus.plus.batteryblock.exclamationmark"}, + {"battery-low"_L1, @"battery.25percent"}, // there are different levels that can be low battery + {"dialog-error"_L1, @"exclamationmark.bubble"}, + {"dialog-information"_L1, @"info.circle"}, + {"dialog-password"_L1, @"lock"}, + {"dialog-question"_L1, @"questionmark.circle"}, + {"dialog-warning"_L1, @"exclamationmark.octagon"}, + {"folder-drag-accept"_L1, @"plus.rectangle.on.folder"}, + //{"folder-open"_L1, @""}, + {"folder-visiting"_L1, @"folder.circle"}, + {"image-loading"_L1, @"photo.circle"}, + {"image-missing"_L1, @"photo"}, + {"mail-attachment"_L1, @"paperclip"}, + {"mail-unread"_L1, @"envelope.badge"}, + {"mail-read"_L1, @"envelope.open"}, + {"mail-replied"_L1, @"arrowshape.turn.up.left"}, + //{"mail-signed"_L1, @""}, + //{"mail-signed-verified"_L1, @""}, + {"media-playlist-repeat"_L1, @"repet"}, + {"media-playlist-shuffle"_L1, @"shuffle"}, + //{"network-error"_L1, @""}, + //{"network-idle"_L1, @""}, + {"network-offline"_L1, @"network.slash"}, + //{"network-receive"_L1, @""}, + //{"network-transmit"_L1, @""}, + //{"network-transmit-receive"_L1, @""}, + //{"printer-error"_L1, @""}, + {"printer-printing"_L1, @"printer.dotmatrix.filled.and.paper"}, // not sure + {"security-high"_L1, @"lock.shield"}, + //{"security-medium"_L1, @""}, + {"security-low"_L1, @"lock.trianglebadge.exclamationmark"}, + {"software-update-available"_L1, @"arrowshape.up.circle"}, + {"software-update-urgent"_L1, @"exclamationmark.transmission"}, + {"sync-error"_L1, @"exclamationmark.arrow.triangle.2.circlepath"}, + {"sync-synchronizing"_L1, @"arrow.triangle.2.circlepath"}, + {"task-due"_L1, @"clock.badge.exclamationmark"}, + {"task-past-due"_L1, @"clock.badge.xmark"}, + {"user-available"_L1, @"person.crop.circle.badge.checkmark"}, + {"user-away"_L1, @"person.crop.circle.badge.clock"}, + //{"user-idle"_L1, @""}, + {"user-offline"_L1, @"person.crop.circle.badge.xmark"}, + //{"user-trash-full"_L1, @""}, + {"weather-clear"_L1, @"sun.max"}, + {"weather-clear-night"_L1, @"moon"}, + {"weather-few-clouds"_L1, @"cloud.sun"}, + {"weather-few-clouds-night"_L1, @"cloud.moon"}, + {"weather-fog"_L1, @"cloud.fog"}, + {"weather-overcast"_L1, @"cloud"}, + //{"weather-severe-alert"_L1, @""}, + {"weather-showers"_L1, @"cloud.rain"}, + //{"weather-showers-scattered"_L1, @""}, + {"weather-snow"_L1, @"cloud.snow"}, + {"weather-storm"_L1, @"tropicalstorm"}, + }; + const auto it = std::find_if(std::begin(iconMap), std::end(iconMap), [iconName](const auto &c){ + return c.first == iconName; + }); + NSString *systemIconName = it != std::end(iconMap) ? it->second : iconName.toNSString(); +#if defined(Q_OS_MACOS) + return [NSImage imageWithSystemSymbolName:systemIconName accessibilityDescription:nil]; +#elif defined(QT_PLATFORM_UIKIT) + return [UIImage systemImageNamed:systemIconName]; +#endif +} +} + +QAppleIconEngine::QAppleIconEngine(const QString &iconName) + : m_iconName(iconName), m_image(loadImage(iconName)) +{ + if (m_image) + [m_image retain]; +} + +QAppleIconEngine::~QAppleIconEngine() +{ + if (m_image) + [m_image release]; +} + +QIconEngine *QAppleIconEngine::clone() const +{ + return new QAppleIconEngine(m_iconName); +} + +QString QAppleIconEngine::key() const +{ + return u"QAppleIconEngine"_s; +} + +QString QAppleIconEngine::iconName() +{ + return m_iconName; +} + +bool QAppleIconEngine::isNull() +{ + return m_image == nullptr; +} + +QList<QSize> QAppleIconEngine::availableIconSizes(double aspectRatio) +{ + const qreal devicePixelRatio = qGuiApp->devicePixelRatio(); + const QList<QSize> sizes = { + {qRound(16 * devicePixelRatio), qRound(16. * devicePixelRatio / aspectRatio)}, + {qRound(32 * devicePixelRatio), qRound(32. * devicePixelRatio / aspectRatio)}, + {qRound(64 * devicePixelRatio), qRound(64. * devicePixelRatio / aspectRatio)}, + {qRound(128 * devicePixelRatio), qRound(128. * devicePixelRatio / aspectRatio)}, + {qRound(256 * devicePixelRatio), qRound(256. * devicePixelRatio / aspectRatio)}, + }; + return sizes; +} + +QList<QSize> QAppleIconEngine::availableSizes(QIcon::Mode, QIcon::State) +{ + const double aspectRatio = isNull() ? 1.0 : m_image.size.width / m_image.size.height; + return availableIconSizes(aspectRatio); +} + +QSize QAppleIconEngine::actualSize(const QSize &size, QIcon::Mode /*mode*/, QIcon::State /*state*/) +{ + const double inputAspectRatio = isNull() ? 1.0 : m_image.size.width / m_image.size.height; + const double outputAspectRatio = size.width() / size.height(); + QSize result = size; + if (outputAspectRatio > inputAspectRatio) + result.rwidth() = result.height() * inputAspectRatio; + else + result.rheight() = result.width() / inputAspectRatio; + return result; +} + +QPixmap QAppleIconEngine::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) +{ + return scaledPixmap(size, mode, state, 1.0); +} + +namespace { +#if defined(Q_OS_MACOS) +auto *configuredImage(const NSImage *image, const QColor &color) +{ + auto *config = [NSImageSymbolConfiguration configurationWithPointSize:48 + weight:NSFontWeightRegular + scale:NSImageSymbolScaleLarge]; + if (@available(macOS 12, *)) { + auto *primaryColor = [NSColor colorWithSRGBRed:color.redF() + green:color.greenF() + blue:color.blueF() + alpha:color.alphaF()]; + + auto *colorConfig = [NSImageSymbolConfiguration configurationWithHierarchicalColor:primaryColor]; + config = [config configurationByApplyingConfiguration:colorConfig]; + } + + return [image imageWithSymbolConfiguration:config]; +} +#elif defined(QT_PLATFORM_UIKIT) +auto *configuredImage(const UIImage *image, const QColor &color) +{ + auto *config = [UIImageSymbolConfiguration configurationWithPointSize:48 + weight:UIImageSymbolWeightRegular + scale:UIImageSymbolScaleLarge]; + + if (@available(iOS 15, *)) { + auto *primaryColor = [UIColor colorWithRed:color.redF() + green:color.greenF() + blue:color.blueF() + alpha:color.alphaF()]; + + auto *colorConfig = [UIImageSymbolConfiguration configurationWithHierarchicalColor:primaryColor]; + config = [config configurationByApplyingConfiguration:colorConfig]; + } + return [image imageByApplyingSymbolConfiguration:config]; +} +#endif +} + +QPixmap QAppleIconEngine::scaledPixmap(const QSize &size, QIcon::Mode mode, QIcon::State state, qreal scale) +{ + const quint64 cacheKey = calculateCacheKey(mode, state); + if (cacheKey != m_cacheKey || m_pixmap.size() != size || m_pixmap.devicePixelRatio() != scale) { + const QSize paintSize = actualSize(size, mode, state); + const QSize paintOffset = paintSize != size + ? (QSizeF(size - paintSize) * 0.5).toSize() + : QSize(); + + m_pixmap = QPixmap(size * scale); + m_pixmap.setDevicePixelRatio(scale); + m_pixmap.fill(Qt::transparent); + + QPainter painter(&m_pixmap); + paint(&painter, QRect(paintOffset.width(), paintOffset.height(), + paintSize.width(), paintSize.height()), mode, state); + + m_cacheKey = cacheKey; + } + return m_pixmap; +} + +void QAppleIconEngine::paint(QPainter *painter, const QRect &rect, QIcon::Mode mode, QIcon::State state) +{ + Q_UNUSED(state); + + QColor color; + const QPalette palette; + switch (mode) { + case QIcon::Normal: + color = palette.color(QPalette::Inactive, QPalette::Text); + break; + case QIcon::Disabled: + color = palette.color(QPalette::Disabled, QPalette::Text); + break; + case QIcon::Active: + color = palette.color(QPalette::Active, QPalette::Text); + break; + case QIcon::Selected: + color = palette.color(QPalette::Active, QPalette::HighlightedText); + break; + } + const auto *image = configuredImage(m_image, color); + + QMacCGContext ctx(painter); + +#if defined(Q_OS_MACOS) + NSGraphicsContext *gc = [NSGraphicsContext graphicsContextWithCGContext:ctx flipped:YES]; + [NSGraphicsContext saveGraphicsState]; + [NSGraphicsContext setCurrentContext:gc]; + + const NSSize pixmapSize = NSMakeSize(rect.width(), rect.height()); + [image setSize:pixmapSize]; + const NSRect sourceRect = NSMakeRect(0, 0, pixmapSize.width, pixmapSize.height); + const NSRect iconRect = NSMakeRect(rect.x(), rect.y(), pixmapSize.width, pixmapSize.height); + + [image drawInRect:iconRect fromRect:sourceRect operation:NSCompositingOperationSourceOver fraction:1.0 respectFlipped:YES hints:nil]; + [NSGraphicsContext restoreGraphicsState]; +#elif defined(QT_PLATFORM_UIKIT) + UIGraphicsPushContext(ctx); + const CGRect cgrect = CGRectMake(rect.x(), rect.y(), rect.width(), rect.height()); + [image drawInRect:cgrect]; + UIGraphicsPopContext(); +#endif +} + +QT_END_NAMESPACE diff --git a/src/gui/platform/darwin/qappleiconengine_p.h b/src/gui/platform/darwin/qappleiconengine_p.h new file mode 100644 index 0000000000..2a4ff7fc64 --- /dev/null +++ b/src/gui/platform/darwin/qappleiconengine_p.h @@ -0,0 +1,64 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QAPPLEICONENGINE_P_H +#define QAPPLEICONENGINE_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtGui/qiconengine.h> + +#include <QtCore/private/qcore_mac_p.h> + +Q_FORWARD_DECLARE_OBJC_CLASS(UIImage); +Q_FORWARD_DECLARE_OBJC_CLASS(NSImage); + +QT_BEGIN_NAMESPACE + +class Q_GUI_EXPORT QAppleIconEngine : public QIconEngine +{ +public: + QAppleIconEngine(const QString &iconName); + ~QAppleIconEngine(); + QIconEngine *clone() const override; + QString key() const override; + QString iconName() override; + bool isNull() override; + + QList<QSize> availableSizes(QIcon::Mode, QIcon::State) override; + QSize actualSize(const QSize &size, QIcon::Mode mode, QIcon::State state) override; + QPixmap pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) override; + QPixmap scaledPixmap(const QSize &size, QIcon::Mode mode, QIcon::State state, qreal scale) override; + void paint(QPainter *painter, const QRect &rect, QIcon::Mode mode, QIcon::State state) override; + + static QList<QSize> availableIconSizes(double aspectRatio = 1.0); + +private: + static constexpr quint64 calculateCacheKey(QIcon::Mode mode, QIcon::State state) + { + return (quint64(mode) << 32) | state; + } + + const QString m_iconName; +#if defined(Q_OS_MACOS) + const NSImage *m_image; +#elif defined(QT_PLATFORM_UIKIT) + const UIImage *m_image; +#endif + mutable QPixmap m_pixmap; + mutable quint64 m_cacheKey = {}; +}; + + +QT_END_NAMESPACE + +#endif // QAPPLEICONENGINE_P_H diff --git a/src/gui/platform/darwin/qapplekeymapper.mm b/src/gui/platform/darwin/qapplekeymapper.mm new file mode 100644 index 0000000000..b8ff5c9d6d --- /dev/null +++ b/src/gui/platform/darwin/qapplekeymapper.mm @@ -0,0 +1,717 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include <qglobal.h> + +#ifdef Q_OS_MACOS +#include <AppKit/AppKit.h> +#endif + +#if defined(QT_PLATFORM_UIKIT) +#include <UIKit/UIKit.h> +#endif + +#include "qapplekeymapper_p.h" + +#include <QtCore/qloggingcategory.h> +#include <QtGui/QGuiApplication> + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcQpaKeyMapperKeys, "qt.qpa.keymapper.keys"); + +static Qt::KeyboardModifiers swapModifiersIfNeeded(const Qt::KeyboardModifiers modifiers) +{ + if (QCoreApplication::testAttribute(Qt::AA_MacDontSwapCtrlAndMeta)) + return modifiers; + + Qt::KeyboardModifiers swappedModifiers = modifiers; + swappedModifiers &= ~(Qt::MetaModifier | Qt::ControlModifier); + + if (modifiers & Qt::ControlModifier) + swappedModifiers |= Qt::MetaModifier; + if (modifiers & Qt::MetaModifier) + swappedModifiers |= Qt::ControlModifier; + + return swappedModifiers; +} + +#ifdef Q_OS_MACOS +static constexpr std::tuple<NSEventModifierFlags, Qt::KeyboardModifier> cocoaModifierMap[] = { + { NSEventModifierFlagShift, Qt::ShiftModifier }, + { NSEventModifierFlagControl, Qt::ControlModifier }, + { NSEventModifierFlagCommand, Qt::MetaModifier }, + { NSEventModifierFlagOption, Qt::AltModifier }, + { NSEventModifierFlagNumericPad, Qt::KeypadModifier } +}; + +Qt::KeyboardModifiers QAppleKeyMapper::fromCocoaModifiers(NSEventModifierFlags cocoaModifiers) +{ + Qt::KeyboardModifiers qtModifiers = Qt::NoModifier; + for (const auto &[cocoaModifier, qtModifier] : cocoaModifierMap) { + if (cocoaModifiers & cocoaModifier) + qtModifiers |= qtModifier; + } + + return swapModifiersIfNeeded(qtModifiers); +} + +NSEventModifierFlags QAppleKeyMapper::toCocoaModifiers(Qt::KeyboardModifiers qtModifiers) +{ + qtModifiers = swapModifiersIfNeeded(qtModifiers); + + NSEventModifierFlags cocoaModifiers = 0; + for (const auto &[cocoaModifier, qtModifier] : cocoaModifierMap) { + if (qtModifiers & qtModifier) + cocoaModifiers |= cocoaModifier; + } + + return cocoaModifiers; +} + +using CarbonModifiers = UInt32; // As opposed to EventModifiers which is UInt16 + +static CarbonModifiers toCarbonModifiers(Qt::KeyboardModifiers qtModifiers) +{ + qtModifiers = swapModifiersIfNeeded(qtModifiers); + + static constexpr std::tuple<int, Qt::KeyboardModifier> carbonModifierMap[] = { + { shiftKey, Qt::ShiftModifier }, + { controlKey, Qt::ControlModifier }, + { cmdKey, Qt::MetaModifier }, + { optionKey, Qt::AltModifier }, + { kEventKeyModifierNumLockMask, Qt::KeypadModifier } + }; + + CarbonModifiers carbonModifiers = 0; + for (const auto &[carbonModifier, qtModifier] : carbonModifierMap) { + if (qtModifiers & qtModifier) + carbonModifiers |= carbonModifier; + } + + return carbonModifiers; +} + +// Keyboard keys (non-modifiers) +static QHash<char16_t, Qt::Key> standardKeys = { + { kHomeCharCode, Qt::Key_Home }, + { kEnterCharCode, Qt::Key_Enter }, + { kEndCharCode, Qt::Key_End }, + { kBackspaceCharCode, Qt::Key_Backspace }, + { kTabCharCode, Qt::Key_Tab }, + { kPageUpCharCode, Qt::Key_PageUp }, + { kPageDownCharCode, Qt::Key_PageDown }, + { kReturnCharCode, Qt::Key_Return }, + { kEscapeCharCode, Qt::Key_Escape }, + { kLeftArrowCharCode, Qt::Key_Left }, + { kRightArrowCharCode, Qt::Key_Right }, + { kUpArrowCharCode, Qt::Key_Up }, + { kDownArrowCharCode, Qt::Key_Down }, + { kHelpCharCode, Qt::Key_Help }, + { kDeleteCharCode, Qt::Key_Delete }, + // ASCII maps, for debugging + { ':', Qt::Key_Colon }, + { ';', Qt::Key_Semicolon }, + { '<', Qt::Key_Less }, + { '=', Qt::Key_Equal }, + { '>', Qt::Key_Greater }, + { '?', Qt::Key_Question }, + { '@', Qt::Key_At }, + { ' ', Qt::Key_Space }, + { '!', Qt::Key_Exclam }, + { '"', Qt::Key_QuoteDbl }, + { '#', Qt::Key_NumberSign }, + { '$', Qt::Key_Dollar }, + { '%', Qt::Key_Percent }, + { '&', Qt::Key_Ampersand }, + { '\'', Qt::Key_Apostrophe }, + { '(', Qt::Key_ParenLeft }, + { ')', Qt::Key_ParenRight }, + { '*', Qt::Key_Asterisk }, + { '+', Qt::Key_Plus }, + { ',', Qt::Key_Comma }, + { '-', Qt::Key_Minus }, + { '.', Qt::Key_Period }, + { '/', Qt::Key_Slash }, + { '[', Qt::Key_BracketLeft }, + { ']', Qt::Key_BracketRight }, + { '\\', Qt::Key_Backslash }, + { '_', Qt::Key_Underscore }, + { '`', Qt::Key_QuoteLeft }, + { '{', Qt::Key_BraceLeft }, + { '}', Qt::Key_BraceRight }, + { '|', Qt::Key_Bar }, + { '~', Qt::Key_AsciiTilde }, + { '^', Qt::Key_AsciiCircum } +}; + +static QHash<char16_t, Qt::Key> virtualKeys = { + { kVK_F1, Qt::Key_F1 }, + { kVK_F2, Qt::Key_F2 }, + { kVK_F3, Qt::Key_F3 }, + { kVK_F4, Qt::Key_F4 }, + { kVK_F5, Qt::Key_F5 }, + { kVK_F6, Qt::Key_F6 }, + { kVK_F7, Qt::Key_F7 }, + { kVK_F8, Qt::Key_F8 }, + { kVK_F9, Qt::Key_F9 }, + { kVK_F10, Qt::Key_F10 }, + { kVK_F11, Qt::Key_F11 }, + { kVK_F12, Qt::Key_F12 }, + { kVK_F13, Qt::Key_F13 }, + { kVK_F14, Qt::Key_F14 }, + { kVK_F15, Qt::Key_F15 }, + { kVK_F16, Qt::Key_F16 }, + { kVK_Return, Qt::Key_Return }, + { kVK_Tab, Qt::Key_Tab }, + { kVK_Escape, Qt::Key_Escape }, + { kVK_Help, Qt::Key_Help }, + { kVK_UpArrow, Qt::Key_Up }, + { kVK_DownArrow, Qt::Key_Down }, + { kVK_LeftArrow, Qt::Key_Left }, + { kVK_RightArrow, Qt::Key_Right }, + { kVK_PageUp, Qt::Key_PageUp }, + { kVK_PageDown, Qt::Key_PageDown } +}; + +static QHash<char16_t, Qt::Key> functionKeys = { + { NSUpArrowFunctionKey, Qt::Key_Up }, + { NSDownArrowFunctionKey, Qt::Key_Down }, + { NSLeftArrowFunctionKey, Qt::Key_Left }, + { NSRightArrowFunctionKey, Qt::Key_Right }, + // F1-35 function keys handled manually below + { NSInsertFunctionKey, Qt::Key_Insert }, + { NSDeleteFunctionKey, Qt::Key_Delete }, + { NSHomeFunctionKey, Qt::Key_Home }, + { NSEndFunctionKey, Qt::Key_End }, + { NSPageUpFunctionKey, Qt::Key_PageUp }, + { NSPageDownFunctionKey, Qt::Key_PageDown }, + { NSPrintScreenFunctionKey, Qt::Key_Print }, + { NSScrollLockFunctionKey, Qt::Key_ScrollLock }, + { NSPauseFunctionKey, Qt::Key_Pause }, + { NSSysReqFunctionKey, Qt::Key_SysReq }, + { NSMenuFunctionKey, Qt::Key_Menu }, + { NSPrintFunctionKey, Qt::Key_Printer }, + { NSClearDisplayFunctionKey, Qt::Key_Clear }, + { NSInsertCharFunctionKey, Qt::Key_Insert }, + { NSDeleteCharFunctionKey, Qt::Key_Delete }, + { NSSelectFunctionKey, Qt::Key_Select }, + { NSExecuteFunctionKey, Qt::Key_Execute }, + { NSUndoFunctionKey, Qt::Key_Undo }, + { NSRedoFunctionKey, Qt::Key_Redo }, + { NSFindFunctionKey, Qt::Key_Find }, + { NSHelpFunctionKey, Qt::Key_Help }, + { NSModeSwitchFunctionKey, Qt::Key_Mode_switch } +}; + +static int toKeyCode(const QChar &key, int virtualKey, int modifiers) +{ + qCDebug(lcQpaKeyMapperKeys, "Mapping key: %d (0x%04x) / vk %d (0x%04x)", + key.unicode(), key.unicode(), virtualKey, virtualKey); + + if (key == QChar(kClearCharCode) && virtualKey == 0x47) + return Qt::Key_Clear; + + if (key.isDigit()) { + qCDebug(lcQpaKeyMapperKeys, "Got digit key: %d", key.digitValue()); + return key.digitValue() + Qt::Key_0; + } + + if (key.isLetter()) { + qCDebug(lcQpaKeyMapperKeys, "Got letter key: %d", (key.toUpper().unicode() - 'A')); + return (key.toUpper().unicode() - 'A') + Qt::Key_A; + } + if (key.isSymbol()) { + qCDebug(lcQpaKeyMapperKeys, "Got symbol key: %d", (key.unicode())); + return key.unicode(); + } + + if (auto qtKey = standardKeys.value(key.unicode())) { + // To work like Qt for X11 we issue Backtab when Shift + Tab are pressed + if (qtKey == Qt::Key_Tab && (modifiers & Qt::ShiftModifier)) { + qCDebug(lcQpaKeyMapperKeys, "Got key: Qt::Key_Backtab"); + return Qt::Key_Backtab; + } + + qCDebug(lcQpaKeyMapperKeys) << "Got" << qtKey; + return qtKey; + } + + // Last ditch try to match the scan code + if (auto qtKey = virtualKeys.value(virtualKey)) { + qCDebug(lcQpaKeyMapperKeys) << "Got scancode" << qtKey; + return qtKey; + } + + // Check if they belong to key codes in private unicode range + if (key >= QChar(NSUpArrowFunctionKey) && key <= QChar(NSModeSwitchFunctionKey)) { + if (auto qtKey = functionKeys.value(key.unicode())) { + qCDebug(lcQpaKeyMapperKeys) << "Got" << qtKey; + return qtKey; + } else if (key >= QChar(NSF1FunctionKey) && key <= QChar(NSF35FunctionKey)) { + auto functionKey = Qt::Key_F1 + (key.unicode() - NSF1FunctionKey) ; + qCDebug(lcQpaKeyMapperKeys) << "Got" << functionKey; + return functionKey; + } + } + + qCDebug(lcQpaKeyMapperKeys, "Unknown case.. %d[%d] %d", key.unicode(), key.toLatin1(), virtualKey); + return Qt::Key_unknown; +} + +// --------- Cocoa key mapping moved from Qt Core --------- + +static const int NSEscapeCharacter = 27; // not defined by Cocoa headers + +static const QHash<char16_t, Qt::Key> cocoaKeys = { + { NSEnterCharacter, Qt::Key_Enter }, + { NSBackspaceCharacter, Qt::Key_Backspace }, + { NSTabCharacter, Qt::Key_Tab }, + { NSNewlineCharacter, Qt::Key_Return }, + { NSCarriageReturnCharacter, Qt::Key_Return }, + { NSBackTabCharacter, Qt::Key_Backtab }, + { NSEscapeCharacter, Qt::Key_Escape }, + { NSDeleteCharacter, Qt::Key_Backspace }, + { NSUpArrowFunctionKey, Qt::Key_Up }, + { NSDownArrowFunctionKey, Qt::Key_Down }, + { NSLeftArrowFunctionKey, Qt::Key_Left }, + { NSRightArrowFunctionKey, Qt::Key_Right }, + { NSF1FunctionKey, Qt::Key_F1 }, + { NSF2FunctionKey, Qt::Key_F2 }, + { NSF3FunctionKey, Qt::Key_F3 }, + { NSF4FunctionKey, Qt::Key_F4 }, + { NSF5FunctionKey, Qt::Key_F5 }, + { NSF6FunctionKey, Qt::Key_F6 }, + { NSF7FunctionKey, Qt::Key_F7 }, + { NSF8FunctionKey, Qt::Key_F8 }, + { NSF9FunctionKey, Qt::Key_F9 }, + { NSF10FunctionKey, Qt::Key_F10 }, + { NSF11FunctionKey, Qt::Key_F11 }, + { NSF12FunctionKey, Qt::Key_F12 }, + { NSF13FunctionKey, Qt::Key_F13 }, + { NSF14FunctionKey, Qt::Key_F14 }, + { NSF15FunctionKey, Qt::Key_F15 }, + { NSF16FunctionKey, Qt::Key_F16 }, + { NSF17FunctionKey, Qt::Key_F17 }, + { NSF18FunctionKey, Qt::Key_F18 }, + { NSF19FunctionKey, Qt::Key_F19 }, + { NSF20FunctionKey, Qt::Key_F20 }, + { NSF21FunctionKey, Qt::Key_F21 }, + { NSF22FunctionKey, Qt::Key_F22 }, + { NSF23FunctionKey, Qt::Key_F23 }, + { NSF24FunctionKey, Qt::Key_F24 }, + { NSF25FunctionKey, Qt::Key_F25 }, + { NSF26FunctionKey, Qt::Key_F26 }, + { NSF27FunctionKey, Qt::Key_F27 }, + { NSF28FunctionKey, Qt::Key_F28 }, + { NSF29FunctionKey, Qt::Key_F29 }, + { NSF30FunctionKey, Qt::Key_F30 }, + { NSF31FunctionKey, Qt::Key_F31 }, + { NSF32FunctionKey, Qt::Key_F32 }, + { NSF33FunctionKey, Qt::Key_F33 }, + { NSF34FunctionKey, Qt::Key_F34 }, + { NSF35FunctionKey, Qt::Key_F35 }, + { NSInsertFunctionKey, Qt::Key_Insert }, + { NSDeleteFunctionKey, Qt::Key_Delete }, + { NSHomeFunctionKey, Qt::Key_Home }, + { NSEndFunctionKey, Qt::Key_End }, + { NSPageUpFunctionKey, Qt::Key_PageUp }, + { NSPageDownFunctionKey, Qt::Key_PageDown }, + { NSPrintScreenFunctionKey, Qt::Key_Print }, + { NSScrollLockFunctionKey, Qt::Key_ScrollLock }, + { NSPauseFunctionKey, Qt::Key_Pause }, + { NSSysReqFunctionKey, Qt::Key_SysReq }, + { NSMenuFunctionKey, Qt::Key_Menu }, + { NSHelpFunctionKey, Qt::Key_Help }, +}; + +QChar QAppleKeyMapper::toCocoaKey(Qt::Key key) +{ + // Prioritize overloaded keys + if (key == Qt::Key_Return) + return QChar(NSCarriageReturnCharacter); + if (key == Qt::Key_Backspace) + return QChar(NSBackspaceCharacter); + + Q_CONSTINIT static QHash<Qt::Key, char16_t> reverseCocoaKeys; + if (reverseCocoaKeys.isEmpty()) { + reverseCocoaKeys.reserve(cocoaKeys.size()); + for (auto it = cocoaKeys.begin(); it != cocoaKeys.end(); ++it) + reverseCocoaKeys.insert(it.value(), it.key()); + } + + return reverseCocoaKeys.value(key); +} + +Qt::Key QAppleKeyMapper::fromCocoaKey(QChar keyCode) +{ + if (auto key = cocoaKeys.value(keyCode.unicode())) + return key; + + return Qt::Key(keyCode.toUpper().unicode()); +} + +// ------------------------------------------------ + +Qt::KeyboardModifiers QAppleKeyMapper::queryKeyboardModifiers() const +{ + return fromCocoaModifiers(NSEvent.modifierFlags); +} + +bool QAppleKeyMapper::updateKeyboard() +{ + QCFType<TISInputSourceRef> source = TISCopyInputMethodKeyboardLayoutOverride(); + if (!source) + source = TISCopyCurrentKeyboardInputSource(); + + if (m_keyboardMode != NullMode && source == m_currentInputSource) + return false; + + Q_ASSERT(source); + m_currentInputSource = source; + m_keyboardKind = LMGetKbdType(); + + m_keyMap.clear(); + + if (auto data = CFDataRef(TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData))) { + const UCKeyboardLayout *uchrData = reinterpret_cast<const UCKeyboardLayout *>(CFDataGetBytePtr(data)); + Q_ASSERT(uchrData); + m_keyboardLayoutFormat = uchrData; + m_keyboardMode = UnicodeMode; + } else { + m_keyboardLayoutFormat = nullptr; + m_keyboardMode = NullMode; + } + + qCDebug(lcQpaKeyMapper) << "Updated keyboard to" + << QString::fromCFString(CFStringRef(TISGetInputSourceProperty( + m_currentInputSource, kTISPropertyLocalizedName))); + + return true; +} + +static constexpr Qt::KeyboardModifiers modifierCombinations[] = { + Qt::NoModifier, // 0 + Qt::ShiftModifier, // 1 + Qt::ControlModifier, // 2 + Qt::ControlModifier | Qt::ShiftModifier, // 3 + Qt::AltModifier, // 4 + Qt::AltModifier | Qt::ShiftModifier, // 5 + Qt::AltModifier | Qt::ControlModifier, // 6 + Qt::AltModifier | Qt::ShiftModifier | Qt::ControlModifier, // 7 + Qt::MetaModifier, // 8 + Qt::MetaModifier | Qt::ShiftModifier, // 9 + Qt::MetaModifier | Qt::ControlModifier, // 10 + Qt::MetaModifier | Qt::ControlModifier | Qt::ShiftModifier, // 11 + Qt::MetaModifier | Qt::AltModifier, // 12 + Qt::MetaModifier | Qt::AltModifier | Qt::ShiftModifier, // 13 + Qt::MetaModifier | Qt::AltModifier | Qt::ControlModifier, // 14 + Qt::MetaModifier | Qt::AltModifier | Qt::ShiftModifier | Qt::ControlModifier, // 15 +}; + +/* + Returns a key map for the given \virtualKey based on all + possible modifier combinations. +*/ +const QAppleKeyMapper::KeyMap &QAppleKeyMapper::keyMapForKey(VirtualKeyCode virtualKey) const +{ + static_assert(sizeof(modifierCombinations) / sizeof(Qt::KeyboardModifiers) == kNumModifierCombinations); + + const_cast<QAppleKeyMapper *>(this)->updateKeyboard(); + + auto &keyMap = m_keyMap[virtualKey]; + if (keyMap[Qt::NoModifier] != Qt::Key_unknown) + return keyMap; // Already filled + + qCDebug(lcQpaKeyMapper, "Updating key map for virtual key 0x%02x", (uint)virtualKey); + + // Key mapping via [NSEvent charactersByApplyingModifiers:] only works for key down + // events, but we might (wrongly) get into this code path for other key events such + // as NSEventTypeFlagsChanged. + const bool canMapCocoaEvent = NSApp.currentEvent.type == NSEventTypeKeyDown; + + if (!canMapCocoaEvent) + qCWarning(lcQpaKeyMapper) << "Could not map key to character for event" << NSApp.currentEvent; + + for (int i = 0; i < kNumModifierCombinations; ++i) { + Q_ASSERT(!i || keyMap[i] == 0); + + auto qtModifiers = modifierCombinations[i]; + auto carbonModifiers = toCarbonModifiers(qtModifiers); + const UInt32 modifierKeyState = (carbonModifiers >> 8) & 0xFF; + + UInt32 deadKeyState = 0; + static const UniCharCount maxStringLength = 10; + static UniChar unicodeString[maxStringLength]; + UniCharCount actualStringLength = 0; + OSStatus err = UCKeyTranslate(m_keyboardLayoutFormat, virtualKey, + kUCKeyActionDown, modifierKeyState, m_keyboardKind, + kUCKeyTranslateNoDeadKeysMask, &deadKeyState, + maxStringLength, &actualStringLength, + unicodeString); + + // Use translated Unicode key if valid + QChar carbonUnicodeKey; + if (err == noErr && actualStringLength) + carbonUnicodeKey = QChar(unicodeString[0]); + + if (@available(macOS 10.15, *)) { + if (canMapCocoaEvent) { + // Until we've verified that the Cocoa API works as expected + // we first run the event through the Carbon APIs and then + // compare the results to Cocoa. + auto cocoaModifiers = toCocoaModifiers(qtModifiers); + auto *charactersWithModifiers = [NSApp.currentEvent charactersByApplyingModifiers:cocoaModifiers]; + + QChar cocoaUnicodeKey; + if (charactersWithModifiers.length > 0) + cocoaUnicodeKey = QChar([charactersWithModifiers characterAtIndex:0]); + + if (cocoaUnicodeKey != carbonUnicodeKey) { + qCWarning(lcQpaKeyMapper) << "Mismatch between Cocoa" << cocoaUnicodeKey + << "and Carbon" << carbonUnicodeKey << "for virtual key" << virtualKey + << "with" << qtModifiers; + } + } + } + + int qtKey = toKeyCode(carbonUnicodeKey, virtualKey, qtModifiers); + if (qtKey == Qt::Key_unknown) + qtKey = carbonUnicodeKey.unicode(); + + keyMap[i] = qtKey; + + qCDebug(lcQpaKeyMapper).verbosity(0) << "\t" << qtModifiers + << "+" << qUtf8Printable(QString::asprintf("0x%02x", virtualKey)) + << "=" << qUtf8Printable(QString::asprintf("%d / 0x%02x /", qtKey, qtKey)) + << QKeySequence(qtKey).toString(); + } + + return keyMap; +} + +/* + Compute the possible key combinations that can map to the event's + virtual key and modifiers, in the current keyboard layout. + + For example, given a normal US keyboard layout, the virtual key + 23 combined with the Alt (⌥) and Shift (⇧) modifiers, can map + to the following key combinations: + + - Alt+Shift+5 + - Alt+% + - Shift+∞ + - fi + + The function builds on a key map produced by keyMapForKey(), + where each modifier-key combination has been mapped to the + key it will produce. +*/ +QList<QKeyCombination> QAppleKeyMapper::possibleKeyCombinations(const QKeyEvent *event) const +{ + QList<QKeyCombination> ret; + + const auto nativeVirtualKey = event->nativeVirtualKey(); + if (!nativeVirtualKey) + return ret; + + auto keyMap = keyMapForKey(nativeVirtualKey); + + auto unmodifiedKey = keyMap[Qt::NoModifier]; + Q_ASSERT(unmodifiedKey != Qt::Key_unknown); + + auto eventModifiers = event->modifiers(); + + int startingModifierLayer = 0; + if (toCocoaModifiers(eventModifiers) & NSEventModifierFlagCommand) { + // When the Command key is pressed AppKit seems to do key equivalent + // matching using a Latin/Roman interpretation of the current keyboard + // layout. For example, for a Greek layout, pressing Option+Command+C + // produces a key event with chars="ç" and unmodchars="ψ", but AppKit + // still treats this as a match for a key equivalent of Option+Command+C. + // We can't do the same by just applying the modifiers to our key map, + // as that too contains "ψ" for the Option+Command combination. What we + // can do instead is take advantage of the fact that the Command + // modifier layer in all/most keyboard layouts contains a Latin + // layer. We then combine that with the modifiers of the event + // to produce the resulting "Latin" key combination. + static constexpr int kCommandLayer = 2; + ret << QKeyCombination::fromCombined( + int(eventModifiers) + int(keyMap[kCommandLayer])); + + // If the unmodified key is outside of Latin1, we also treat + // that as a valid key combination, even if AppKit natively + // does not. For example, for a Greek layout, we still want + // to support Option+Command+ψ as a key combination, as it's + // unlikely to clash with the Latin key combination we added + // above. + + // However, if the unmodified key is within Latin1, we skip + // it, to avoid these types of conflicts. For example, in + // the same Greek layout, pressing the key next to Tab will + // produce a Latin ';' symbol, but we've already treated that + // as 'q' above, thanks to the Command modifier, so we skip + // the potential Command+; key combination. This is also in + // line with what AppKit natively does. + + // Skipping Latin1 unmodified keys also handles the case of + // a Latin layout, where the unmodified and modified keys + // are the same. + + if (unmodifiedKey <= 0xff) + startingModifierLayer = 1; + } + + // FIXME: We only compute the first 8 combinations. Why? + for (int i = startingModifierLayer; i < 15; ++i) { + auto keyAfterApplyingModifiers = keyMap[i]; + if (!keyAfterApplyingModifiers) + continue; + + // Include key if the event modifiers match exactly, + // or are a superset of the current candidate modifiers. + auto candidateModifiers = modifierCombinations[i]; + if ((eventModifiers & candidateModifiers) == candidateModifiers) { + // If the event includes more modifiers than the candidate they + // will need to be included in the resulting key combination. + auto additionalModifiers = eventModifiers & ~candidateModifiers; + + auto keyCombination = QKeyCombination::fromCombined( + int(additionalModifiers) + int(keyAfterApplyingModifiers)); + + // If there's an existing key combination with the same key, + // but a different set of modifiers, we want to choose only + // one of them, by priority (see below). + const auto existingCombination = std::find_if( + ret.begin(), ret.end(), [&](auto existingCombination) { + return existingCombination.key() == keyAfterApplyingModifiers; + }); + + if (existingCombination != ret.end()) { + // We prioritize the combination with the more specific + // modifiers. In the case where the number of modifiers + // are the same, we want to prioritize Command over Option + // over Control over Shift. Unfortunately the order (and + // hence value) of the modifiers in Qt::KeyboardModifier + // does not match our preferred order when Control and + // Meta is switched, but we can work around that by + // explicitly swapping the modifiers and using that + // for the comparison. This also works when the + // Qt::AA_MacDontSwapCtrlAndMeta application attribute + // is set, as the incoming modifiers are then left + // as is, and we can still trust the order. + auto existingModifiers = swapModifiersIfNeeded(existingCombination->keyboardModifiers()); + auto replacementModifiers = swapModifiersIfNeeded(additionalModifiers); + if (replacementModifiers > existingModifiers) + *existingCombination = keyCombination; + } else { + // All is good, no existing combination has this key + ret << keyCombination; + } + } + } + + return ret; +} + + + +#else // iOS + +Qt::Key QAppleKeyMapper::fromNSString(Qt::KeyboardModifiers qtModifiers, NSString *characters, + NSString *charactersIgnoringModifiers, QString &text) +{ + if ([characters isEqualToString:@"\t"]) { + if (qtModifiers & Qt::ShiftModifier) + return Qt::Key_Backtab; + return Qt::Key_Tab; + } else if ([characters isEqualToString:@"\r"]) { + if (qtModifiers & Qt::KeypadModifier) + return Qt::Key_Enter; + return Qt::Key_Return; + } + if ([characters length] != 0 || [charactersIgnoringModifiers length] != 0) { + QChar ch; + if (((qtModifiers & Qt::MetaModifier) || (qtModifiers & Qt::AltModifier)) && + ([charactersIgnoringModifiers length] != 0)) { + ch = QChar([charactersIgnoringModifiers characterAtIndex:0]); + } else if ([characters length] != 0) { + ch = QChar([characters characterAtIndex:0]); + } + if (!(qtModifiers & (Qt::ControlModifier | Qt::MetaModifier)) && + (ch.unicode() < 0xf700 || ch.unicode() > 0xf8ff)) { + text = QString::fromNSString(characters); + } + if (!ch.isNull()) + return Qt::Key(ch.toUpper().unicode()); + } + return Qt::Key_unknown; +} + +// Keyboard keys (non-modifiers) +API_AVAILABLE(ios(13.4)) Qt::Key QAppleKeyMapper::fromUIKitKey(NSString *keyCode) +{ + static QHash<NSString *, Qt::Key> uiKitKeys = { + { UIKeyInputF1, Qt::Key_F1 }, + { UIKeyInputF2, Qt::Key_F2 }, + { UIKeyInputF3, Qt::Key_F3 }, + { UIKeyInputF4, Qt::Key_F4 }, + { UIKeyInputF5, Qt::Key_F5 }, + { UIKeyInputF6, Qt::Key_F6 }, + { UIKeyInputF7, Qt::Key_F7 }, + { UIKeyInputF8, Qt::Key_F8 }, + { UIKeyInputF9, Qt::Key_F9 }, + { UIKeyInputF10, Qt::Key_F10 }, + { UIKeyInputF11, Qt::Key_F11 }, + { UIKeyInputF12, Qt::Key_F12 }, + { UIKeyInputHome, Qt::Key_Home }, + { UIKeyInputEnd, Qt::Key_End }, + { UIKeyInputPageUp, Qt::Key_PageUp }, + { UIKeyInputPageDown, Qt::Key_PageDown }, + { UIKeyInputEscape, Qt::Key_Escape }, + { UIKeyInputUpArrow, Qt::Key_Up }, + { UIKeyInputDownArrow, Qt::Key_Down }, + { UIKeyInputLeftArrow, Qt::Key_Left }, + { UIKeyInputRightArrow, Qt::Key_Right } + }; + + if (auto key = uiKitKeys.value(keyCode)) + return key; + + return Qt::Key_unknown; +} + +static constexpr std::tuple<ulong, Qt::KeyboardModifier> uiKitModifierMap[] = { + { UIKeyModifierShift, Qt::ShiftModifier }, + { UIKeyModifierControl, Qt::ControlModifier }, + { UIKeyModifierCommand, Qt::MetaModifier }, + { UIKeyModifierAlternate, Qt::AltModifier }, + { UIKeyModifierNumericPad, Qt::KeypadModifier } +}; + +ulong QAppleKeyMapper::toUIKitModifiers(Qt::KeyboardModifiers qtModifiers) +{ + qtModifiers = swapModifiersIfNeeded(qtModifiers); + + ulong nativeModifiers = 0; + for (const auto &[nativeModifier, qtModifier] : uiKitModifierMap) { + if (qtModifiers & qtModifier) + nativeModifiers |= nativeModifier; + } + + return nativeModifiers; +} + +Qt::KeyboardModifiers QAppleKeyMapper::fromUIKitModifiers(ulong nativeModifiers) +{ + Qt::KeyboardModifiers qtModifiers = Qt::NoModifier; + for (const auto &[nativeModifier, qtModifier] : uiKitModifierMap) { + if (nativeModifiers & nativeModifier) + qtModifiers |= qtModifier; + } + + return swapModifiersIfNeeded(qtModifiers); +} +#endif + +QT_END_NAMESPACE diff --git a/src/gui/platform/darwin/qapplekeymapper_p.h b/src/gui/platform/darwin/qapplekeymapper_p.h new file mode 100644 index 0000000000..1f3494d16f --- /dev/null +++ b/src/gui/platform/darwin/qapplekeymapper_p.h @@ -0,0 +1,82 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QAPPLEKEYMAPPER_H +#define QAPPLEKEYMAPPER_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#ifdef Q_OS_MACOS +#include <Carbon/Carbon.h> +#endif + +#include <qpa/qplatformkeymapper.h> + +#include <QtCore/QList> +#include <QtCore/QHash> +#include <QtGui/QKeyEvent> + +#include <QtCore/private/qcore_mac_p.h> + +QT_BEGIN_NAMESPACE + +class Q_GUI_EXPORT QAppleKeyMapper : public QPlatformKeyMapper +{ +public: + Qt::KeyboardModifiers queryKeyboardModifiers() const override; + QList<QKeyCombination> possibleKeyCombinations(const QKeyEvent *event) const override; + +#ifdef Q_OS_MACOS + static Qt::KeyboardModifiers fromCocoaModifiers(NSEventModifierFlags cocoaModifiers); + static NSEventModifierFlags toCocoaModifiers(Qt::KeyboardModifiers); + + static QChar toCocoaKey(Qt::Key key); + static Qt::Key fromCocoaKey(QChar keyCode); +#else + static Qt::Key fromNSString(Qt::KeyboardModifiers qtMods, NSString *characters, + NSString *charactersIgnoringModifiers, QString &text); + + static Qt::Key fromUIKitKey(NSString *keyCode); + static Qt::KeyboardModifiers fromUIKitModifiers(ulong uikitModifiers); + static ulong toUIKitModifiers(Qt::KeyboardModifiers); +#endif +private: +#ifdef Q_OS_MACOS + static constexpr int kNumModifierCombinations = 16; + struct KeyMap : std::array<char32_t, kNumModifierCombinations> + { + // Initialize first element to a sentinel that allows us + // to distinguish an uninitialized map from an initialized. + // Using 0 would not allow us to map U+0000 (NUL), however + // unlikely that is. + KeyMap() : std::array<char32_t, 16>{Qt::Key_unknown} {} + }; + + bool updateKeyboard(); + + using VirtualKeyCode = unsigned short; + const KeyMap &keyMapForKey(VirtualKeyCode virtualKey) const; + + QCFType<TISInputSourceRef> m_currentInputSource = nullptr; + + enum { NullMode, UnicodeMode, OtherMode } m_keyboardMode = NullMode; + const UCKeyboardLayout *m_keyboardLayoutFormat = nullptr; + KeyboardLayoutKind m_keyboardKind = kKLKCHRuchrKind; + + mutable QHash<VirtualKeyCode, KeyMap> m_keyMap; +#endif +}; + +QT_END_NAMESPACE + +#endif + diff --git a/src/gui/platform/darwin/qmacmimeregistry.mm b/src/gui/platform/darwin/qmacmimeregistry.mm new file mode 100644 index 0000000000..6710a0656f --- /dev/null +++ b/src/gui/platform/darwin/qmacmimeregistry.mm @@ -0,0 +1,118 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include <QtCore/qmimedata.h> + +#include "qutimimeconverter.h" +#include "qmacmimeregistry_p.h" +#include "qguiapplication.h" +#include "private/qcore_mac_p.h" + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +namespace QMacMimeRegistry { + +typedef QList<QUtiMimeConverter*> MimeList; +Q_GLOBAL_STATIC(MimeList, globalMimeList) +Q_GLOBAL_STATIC(QStringList, globalDraggedTypesList) + +// implemented in qutimimeconverter.mm +void registerBuiltInTypes(); + +void registerDraggedTypes(const QStringList &types) +{ + (*globalDraggedTypesList()) += types; +} + +const QStringList& enabledDraggedTypes() +{ + return (*globalDraggedTypesList()); +} + +/***************************************************************************** + QDnD debug facilities + *****************************************************************************/ +//#define DEBUG_MIME_MAPS + +/*! + \class QMacMimeRegistry + \internal + \ingroup draganddrop +*/ + +/*! + \internal + + This is an internal function. +*/ +void initializeMimeTypes() +{ + if (globalMimeList()->isEmpty()) + registerBuiltInTypes(); +} + +/*! + \internal +*/ +void destroyMimeTypes() +{ + MimeList *mimes = globalMimeList(); + while (!mimes->isEmpty()) + delete mimes->takeFirst(); +} + +/* + Returns a MIME type of for scope \a scope for \a uti, or \nullptr if none exists. +*/ +QString flavorToMime(QUtiMimeConverter::HandlerScope scope, const QString &uti) +{ + const MimeList &mimes = *globalMimeList(); + for (const auto &mime : mimes) { + const bool relevantScope = mime->scope() & scope; +#ifdef DEBUG_MIME_MAPS + qDebug("QMacMimeRegistry::flavorToMime: attempting (%d) for uti %s [%s]", + relevantScope, qPrintable(uti), qPrintable((*it)->mimeForUti(uti))); +#endif + if (relevantScope) { + const QString mimeType = mime->mimeForUti(uti); + if (!mimeType.isNull()) + return mimeType; + } + } + return QString(); +} + +void registerMimeConverter(QUtiMimeConverter *macMime) +{ + // globalMimeList is in decreasing priority order. Recently added + // converters take prioity over previously added converters: prepend + // to the list. + globalMimeList()->prepend(macMime); +} + +void unregisterMimeConverter(QUtiMimeConverter *macMime) +{ + if (!QGuiApplication::closingDown()) + globalMimeList()->removeAll(macMime); +} + + +/* + Returns a list of all currently defined QUtiMimeConverter objects for scope \a scope. +*/ +QList<QUtiMimeConverter *> all(QUtiMimeConverter::HandlerScope scope) +{ + MimeList ret; + const MimeList &mimes = *globalMimeList(); + for (const auto &mime : mimes) { + if (mime->scope() & scope) + ret.append(mime); + } + return ret; +} + +} // namespace QMacMimeRegistry + +QT_END_NAMESPACE diff --git a/src/gui/platform/darwin/qmacmimeregistry_p.h b/src/gui/platform/darwin/qmacmimeregistry_p.h new file mode 100644 index 0000000000..5928b81959 --- /dev/null +++ b/src/gui/platform/darwin/qmacmimeregistry_p.h @@ -0,0 +1,42 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QMACMIMEREGISTRY_H +#define QMACMIMEREGISTRY_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + + +#include <QtGui/private/qtguiglobal_p.h> +#include <QtGui/qutimimeconverter.h> + +#include <CoreFoundation/CoreFoundation.h> + +QT_BEGIN_NAMESPACE + +namespace QMacMimeRegistry { + Q_GUI_EXPORT void initializeMimeTypes(); + Q_GUI_EXPORT void destroyMimeTypes(); + + Q_GUI_EXPORT void registerMimeConverter(QUtiMimeConverter *); + Q_GUI_EXPORT void unregisterMimeConverter(QUtiMimeConverter *); + + Q_GUI_EXPORT QList<QUtiMimeConverter *> all(QUtiMimeConverter::HandlerScope scope); + Q_GUI_EXPORT QString flavorToMime(QUtiMimeConverter::HandlerScope scope, const QString &flav); + + Q_GUI_EXPORT void registerDraggedTypes(const QStringList &types); + Q_GUI_EXPORT const QStringList& enabledDraggedTypes(); +}; + +QT_END_NAMESPACE + +#endif // QMACMIMEREGISTRY_H diff --git a/src/gui/platform/darwin/qmetallayer.mm b/src/gui/platform/darwin/qmetallayer.mm new file mode 100644 index 0000000000..e8a27a7b06 --- /dev/null +++ b/src/gui/platform/darwin/qmetallayer.mm @@ -0,0 +1,73 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qmetallayer_p.h" + +#include <QtCore/qreadwritelock.h> +#include <QtCore/qrect.h> + +using namespace std::chrono_literals; + +QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcMetalLayer, "qt.gui.metal") +QT_END_NAMESPACE + +QT_USE_NAMESPACE + +@implementation QMetalLayer +{ + std::unique_ptr<QReadWriteLock> m_displayLock; +} + +- (instancetype)init +{ + if ((self = [super init])) { + m_displayLock.reset(new QReadWriteLock(QReadWriteLock::Recursive)); + self.mainThreadPresentation = nil; + } + + return self; +} + +- (QReadWriteLock &)displayLock +{ + return *m_displayLock.get(); +} + +- (void)setNeedsDisplay +{ + [self setNeedsDisplayInRect:CGRectInfinite]; +} + +- (void)setNeedsDisplayInRect:(CGRect)rect +{ + if (!self.needsDisplay) { + // We lock for writing here, blocking in case a secondary thread is in + // the middle of presenting to the layer, as we want the main thread's + // display to happen after the secondary thread finishes presenting. + qCDebug(lcMetalLayer) << "Locking" << self << "for writing" + << "due to needing display in rect" << QRectF::fromCGRect(rect); + + // For added safety, we use a 5 second timeout, and try to fail + // gracefully by not marking the layer as needing display, as + // doing so would lead us to unlock and unheld lock in displayLayer. + if (!self.displayLock.tryLockForWrite(5s)) { + qCWarning(lcMetalLayer) << "Timed out waiting for display lock"; + return; + } + } + + [super setNeedsDisplayInRect:rect]; +} + +- (id<CAMetalDrawable>)nextDrawable +{ + // Drop the presentation block early, so that if the main thread for + // some reason doesn't handle the presentation, the block won't hold on + // to a drawable unnecessarily. + self.mainThreadPresentation = nil; + return [super nextDrawable]; +} + + +@end diff --git a/src/gui/platform/darwin/qmetallayer_p.h b/src/gui/platform/darwin/qmetallayer_p.h new file mode 100644 index 0000000000..1c19f21866 --- /dev/null +++ b/src/gui/platform/darwin/qmetallayer_p.h @@ -0,0 +1,41 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QMETALLAYER_P_H +#define QMETALLAYER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtGui/qtguiglobal.h> + +#include <QtCore/qreadwritelock.h> +#include <QtCore/qloggingcategory.h> +#include <QtCore/private/qcore_mac_p.h> + +#include <QuartzCore/CAMetalLayer.h> + +QT_BEGIN_NAMESPACE +class QReadWriteLock; + +Q_DECLARE_EXPORTED_LOGGING_CATEGORY(lcMetalLayer, Q_GUI_EXPORT) + +QT_END_NAMESPACE + +#if defined(__OBJC__) +Q_GUI_EXPORT +#endif +QT_DECLARE_NAMESPACED_OBJC_INTERFACE(QMetalLayer, CAMetalLayer +@property (nonatomic, readonly) QT_PREPEND_NAMESPACE(QReadWriteLock) &displayLock; +@property (atomic, copy) void (^mainThreadPresentation)(); +) + +#endif // QMETALLAYER_P_H diff --git a/src/gui/platform/darwin/qutimimeconverter.h b/src/gui/platform/darwin/qutimimeconverter.h new file mode 100644 index 0000000000..e9297b5fa0 --- /dev/null +++ b/src/gui/platform/darwin/qutimimeconverter.h @@ -0,0 +1,62 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QUTIMIMECONVERTER_H +#define QUTIMIMECONVERTER_H + +#include <QtGui/qtguiglobal.h> + +#include <QtCore/qlist.h> + +QT_BEGIN_NAMESPACE + +class QByteArray; +class QString; +class QVariant; +class QMimeData; + +class Q_GUI_EXPORT QUtiMimeConverter +{ + Q_DISABLE_COPY(QUtiMimeConverter) +public: + enum class HandlerScopeFlag : uint8_t + { + DnD = 0x01, + Clipboard = 0x02, + Qt_compatible = 0x04, + Qt3_compatible = 0x08, + All = DnD|Clipboard, + AllCompatible = All|Qt_compatible + }; + Q_DECLARE_FLAGS(HandlerScope, HandlerScopeFlag) + + QUtiMimeConverter(); + virtual ~QUtiMimeConverter(); + + HandlerScope scope() const { return m_scope; } + bool canConvert(const QString &mime, const QString &uti) const { return mimeForUti(uti) == mime; } + + // for converting from Qt + virtual QList<QByteArray> convertFromMime(const QString &mime, const QVariant &data, const QString &uti) const = 0; + virtual QString utiForMime(const QString &mime) const = 0; + + // for converting to Qt + virtual QString mimeForUti(const QString &uti) const = 0; + virtual QVariant convertToMime(const QString &mime, const QList<QByteArray> &data, const QString &uti) const = 0; + virtual int count(const QMimeData *mimeData) const; + +private: + friend class QMacMimeTypeName; + friend class QMacMimeAny; + + explicit QUtiMimeConverter(HandlerScope scope); + + const HandlerScope m_scope; +}; +Q_DECLARE_OPERATORS_FOR_FLAGS(QUtiMimeConverter::HandlerScope) + + +QT_END_NAMESPACE + +#endif + diff --git a/src/gui/platform/darwin/qutimimeconverter.mm b/src/gui/platform/darwin/qutimimeconverter.mm new file mode 100644 index 0000000000..ee643fd0c6 --- /dev/null +++ b/src/gui/platform/darwin/qutimimeconverter.mm @@ -0,0 +1,823 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include <ImageIO/ImageIO.h> +#include <CoreFoundation/CoreFoundation.h> +#include <UniformTypeIdentifiers/UTCoreTypes.h> + +#include <QtCore/qsystemdetection.h> +#include <QtCore/qurl.h> +#include <QtGui/qimage.h> +#include <QtCore/qmimedata.h> +#include <QtCore/qstringconverter.h> + +#if defined(Q_OS_MACOS) +#import <AppKit/AppKit.h> +#else +#include <MobileCoreServices/MobileCoreServices.h> +#endif + +#if defined(QT_PLATFORM_UIKIT) +#import <UIKit/UIKit.h> +#endif + +#include "qutimimeconverter.h" +#include "qmacmimeregistry_p.h" +#include "qguiapplication.h" +#include "private/qcore_mac_p.h" + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +/***************************************************************************** + QDnD debug facilities + *****************************************************************************/ +//#define DEBUG_MIME_MAPS + +/*! + \class QUtiMimeConverter + \brief The QUtiMimeConverter class converts between a MIME type and a + \l{https://developer.apple.com/documentation/uniformtypeidentifiers} + {Uniform Type Identifier (UTI)} format. + \since 6.5 + + \ingroup draganddrop + \inmodule QtGui + + Qt's drag and drop and clipboard facilities use the MIME + standard. On X11, this maps trivially to the Xdnd protocol. On + Mac, although some applications use MIME to describe clipboard + contents, it is more common to use Apple's UTI format. + + QUtiMimeConverter's role is to bridge the gap between MIME and UTI; + By subclasses this class, one can extend Qt's drag and drop + and clipboard handling to convert to and from unsupported, or proprietary, UTI formats. + + Construct an instance of your converter implementation after instantiating + QGuiApplication: + + \code + int main(int argc, char **argv) + { + QGuiApplication app(argc, argv); + JsonMimeConverter jsonConverter; + } + \endcode + + Destroying the instance will unregister the converter and remove support + for the conversion. It is also valid to heap-allocate the converter + instance; Qt takes ownership and will delete the converter object during + QGuiApplication shut-down. + + Qt has predefined support for the following UTIs: + \list + \li public.utf8-plain-text - converts to "text/plain" + \li public.utf16-plain-text - converts to "text/plain" + \li public.text - converts to "text/plain" + \li public.html - converts to "text/html" + \li public.url - converts to "text/uri-list" + \li public.file-url - converts to "text/uri-list" + \li public.tiff - converts to "application/x-qt-image" + \li public.vcard - converts to "text/plain" + \li com.apple.traditional-mac-plain-text - converts to "text/plain" + \li com.apple.pict - converts to "application/x-qt-image" + \endlist + + When working with MIME data, Qt will iterate through all instances of QUtiMimeConverter to find + find an instance that can convert to, or from, a specific MIME type. It will do this by calling + mimeForUti() or utiForMime() on each instance, starting with (and choosing) the last created + instance first. The actual conversions will be done by using convertToMime() and convertFromMime(). +*/ + +/*! + \enum QUtiMimeConverter::HandlerScope + \internal +*/ + +/*! + \internal + Constructs a new conversion object of type \a scope, adding it to the + globally accessed list of available converters. +*/ +QUtiMimeConverter::QUtiMimeConverter(HandlerScope scope) + : m_scope(scope) +{ + QMacMimeRegistry::registerMimeConverter(this); +} + +/*! + Constructs a new conversion object and adds it to the + globally accessed list of available converters. + + Call this constructor after QGuiApplication has been created. +*/ +QUtiMimeConverter::QUtiMimeConverter() + : QUtiMimeConverter(HandlerScopeFlag::All) +{ +} + +/*! + Destroys a conversion object, removing it from the global + list of available converters. +*/ +QUtiMimeConverter::~QUtiMimeConverter() +{ + QMacMimeRegistry::unregisterMimeConverter(this); +} + +/*! + Returns the item count for the given \a mimeData +*/ +int QUtiMimeConverter::count(const QMimeData *mimeData) const +{ + Q_UNUSED(mimeData); + return 1; +} + +/*! + \fn bool QUtiMimeConverter::canConvert(const QString &mime, const QString &uti) const + + Returns \c true if the converter can convert (both ways) between + \a mime and \a uti; otherwise returns \c false. +*/ + +/*! + \fn QString QUtiMimeConverter::mimeForUti(const QString &uti) const + + Returns the MIME type used for Mac UTI \a uti, or an empty string if + this converter does not support converting from \a uti. + + All subclasses must reimplement this pure virtual function. +*/ + +/*! + \fn QString QUtiMimeConverter::utiForMime(const QString &mime) const + + Returns the Mac UTI used for MIME type \a mime, or an empty string if + this converter does not support converting from \a mime. + + All subclasses must reimplement this pure virtual function. +*/ + +/*! + \fn QVariant QUtiMimeConverter::convertToMime(const QString &mime, + const QList<QByteArray> &data, const QString &uti) const + + Returns \a data converted from Mac UTI \a uti to MIME type \a mime. + + Note that Mac UTIs must all be self-terminating. The input \a data + may contain trailing data. + + All subclasses must reimplement this pure virtual function. +*/ + +/*! + \fn QList<QByteArray> QUtiMimeConverter::convertFromMime(const QString &mime, + const QVariant &data, const QString & uti) const + + Returns \a data converted from MIME type \a mime to Mac UTI \a uti. + + Note that Mac UTIs must all be self-terminating. The return + value may contain trailing data. + + All subclasses must reimplement this pure virtual function. +*/ + + +class QMacMimeAny : public QUtiMimeConverter { +public: + QMacMimeAny() : QUtiMimeConverter(HandlerScopeFlag::AllCompatible) {} + + QString utiForMime(const QString &mime) const override; + QString mimeForUti(const QString &uti) const override; + QVariant convertToMime(const QString &mime, const QList<QByteArray> &data, + const QString &uti) const override; + QList<QByteArray> convertFromMime(const QString &mime, const QVariant &data, + const QString &uti) const override; +}; + +QString QMacMimeAny::utiForMime(const QString &mime) const +{ + // do not handle the mime type name in the drag pasteboard + if (mime == "application/x-qt-mime-type-name"_L1) + return QString(); + QString ret = "com.trolltech.anymime."_L1 + mime; + return ret.replace(u'/', "--"_L1); +} + +QString QMacMimeAny::mimeForUti(const QString &uti) const +{ + const QString any_prefix = "com.trolltech.anymime."_L1; + if (uti.size() > any_prefix.length() && uti.startsWith(any_prefix)) + return uti.mid(any_prefix.length()).replace("--"_L1, "/"_L1); + return QString(); +} + +QVariant QMacMimeAny::convertToMime(const QString &mime, const QList<QByteArray> &data, + const QString &) const +{ + if (data.count() > 1) + qWarning("QMacMimeAny: Cannot handle multiple member data"); + QVariant ret; + if (mime == "text/plain"_L1) + ret = QString::fromUtf8(data.first()); + else + ret = data.first(); + return ret; +} + +QList<QByteArray> QMacMimeAny::convertFromMime(const QString &mime, const QVariant &data, + const QString &) const +{ + QList<QByteArray> ret; + if (mime == "text/plain"_L1) + ret.append(data.toString().toUtf8()); + else + ret.append(data.toByteArray()); + return ret; +} + +class QMacMimeTypeName : public QUtiMimeConverter { +private: + +public: + QMacMimeTypeName(): QUtiMimeConverter(HandlerScopeFlag::AllCompatible) {} + + QString utiForMime(const QString &mime) const override; + QString mimeForUti(const QString &uti) const override; + QVariant convertToMime(const QString &mime, const QList<QByteArray> &data, const QString &uti) const override; + QList<QByteArray> convertFromMime(const QString &mime, const QVariant &data, const QString &uti) const override; +}; + +QString QMacMimeTypeName::utiForMime(const QString &mime) const +{ + if (mime == "application/x-qt-mime-type-name"_L1) + return u"com.trolltech.qt.MimeTypeName"_s; + return QString(); +} + +QString QMacMimeTypeName::mimeForUti(const QString &) const +{ + return QString(); +} + +QVariant QMacMimeTypeName::convertToMime(const QString &, const QList<QByteArray> &, const QString &) const +{ + QVariant ret; + return ret; +} + +QList<QByteArray> QMacMimeTypeName::convertFromMime(const QString &, const QVariant &, const QString &) const +{ + QList<QByteArray> ret; + ret.append(QString("x-qt-mime-type-name"_L1).toUtf8()); + return ret; +} + +class QMacMimePlainTextFallback : public QUtiMimeConverter +{ +public: + QString utiForMime(const QString &mime) const override; + QString mimeForUti(const QString &uti) const override; + QVariant convertToMime(const QString &mime, const QList<QByteArray> &data, + const QString &uti) const override; + QList<QByteArray> convertFromMime(const QString &mime, const QVariant &data, + const QString &uti) const override; +}; + +QString QMacMimePlainTextFallback::utiForMime(const QString &mime) const +{ + if (mime == "text/plain"_L1) + return "public.text"_L1; + return QString(); +} + +QString QMacMimePlainTextFallback::mimeForUti(const QString &uti) const +{ + if (uti == "public.text"_L1) + return "text/plain"_L1; + return QString(); +} + +QVariant +QMacMimePlainTextFallback::convertToMime(const QString &mimetype, + const QList<QByteArray> &data, const QString &uti) const +{ + if (data.count() > 1) + qWarning("QMacMimePlainTextFallback: Cannot handle multiple member data"); + + if (uti == "public.text"_L1) { + // Note that public.text is documented by Apple to have an undefined encoding. From + // testing it seems that utf8 is normally used, at least by Safari on iOS. + const QByteArray &firstData = data.first(); + return QString(QCFString(CFStringCreateWithBytes(kCFAllocatorDefault, + reinterpret_cast<const UInt8 *>(firstData.constData()), + firstData.size(), kCFStringEncodingUTF8, false))); + } else { + qWarning("QMime::convertToMime: unhandled mimetype: %s", qPrintable(mimetype)); + } + return QVariant(); +} + +QList<QByteArray> +QMacMimePlainTextFallback::convertFromMime(const QString &, const QVariant &data, + const QString &uti) const +{ + QList<QByteArray> ret; + QString string = data.toString(); + if (uti == "public.text"_L1) + ret.append(string.toUtf8()); + return ret; +} + +class QMacMimeUnicodeText : public QUtiMimeConverter +{ +public: + QString utiForMime(const QString &mime) const override; + QString mimeForUti(const QString &uti) const override; + QVariant convertToMime(const QString &mime, const QList<QByteArray> &data, + const QString &uti) const override; + QList<QByteArray> convertFromMime(const QString &mime, const QVariant &data, + const QString &uti) const override; +}; + +QString QMacMimeUnicodeText::utiForMime(const QString &mime) const +{ + if (mime == "text/plain"_L1) + return "public.utf16-plain-text"_L1; + if (qsizetype i = mime.indexOf("charset="_L1); i >= 0) { + QString cs(mime.mid(i + 8).toLower()); + i = cs.indexOf(u';'); + if (i >= 0) + cs = cs.left(i); + if (cs == "system"_L1) + return "public.utf8-plain-text"_L1; + else if (cs == "iso-10646-ucs-2"_L1 || cs == "utf16"_L1) + return "public.utf16-plain-text"_L1; + } + return QString(); +} + +QString QMacMimeUnicodeText::mimeForUti(const QString &uti) const +{ + if (uti == "public.utf16-plain-text"_L1 || uti == "public.utf8-plain-text"_L1) + return "text/plain"_L1; + return QString(); +} + +QVariant +QMacMimeUnicodeText::convertToMime(const QString &mimetype, + const QList<QByteArray> &data, const QString &uti) const +{ + if (data.count() > 1) + qWarning("QMacMimeUnicodeText: Cannot handle multiple member data"); + const QByteArray &firstData = data.first(); + // I can only handle two types (system and unicode) so deal with them that way + QVariant ret; + if (uti == "public.utf8-plain-text"_L1) { + ret = QString::fromUtf8(firstData); + } else if (uti == "public.utf16-plain-text"_L1) { + QString str = QStringDecoder(QStringDecoder::Utf16)(firstData); + ret = str; + } else { + qWarning("QMime::convertToMime: unhandled mimetype: %s", qPrintable(mimetype)); + } + return ret; +} + +QList<QByteArray> +QMacMimeUnicodeText::convertFromMime(const QString &, const QVariant &data, + const QString &uti) const +{ + QList<QByteArray> ret; + QString string = data.toString(); + if (uti == "public.utf8-plain-text"_L1) + ret.append(string.toUtf8()); + else if (uti == "public.utf16-plain-text"_L1) { + QStringEncoder::Flags f; +#if defined(Q_OS_MACOS) + // Some applications such as Microsoft Excel, don't deal well with + // a BOM present, so we follow the traditional approach of Qt on + // macOS to not generate public.utf16-plain-text with a BOM. + f = QStringEncoder::Flag::Default; +#else + // Whereas iOS applications will fail to paste if we do _not_ + // include a BOM in the public.utf16-plain-text content, most + // likely due to converting the data using NSUTF16StringEncoding + // which assumes big-endian byte order if there is no BOM. + f = QStringEncoder::Flag::WriteBom; +#endif + QStringEncoder encoder(QStringEncoder::Utf16, f); + ret.append(encoder(string)); + } + return ret; +} + +class QMacMimeHTMLText : public QUtiMimeConverter +{ +public: + QString utiForMime(const QString &mime) const override; + QString mimeForUti(const QString &uti) const override; + QVariant convertToMime(const QString &mime, const QList<QByteArray> &data, + const QString &uti) const override; + QList<QByteArray> convertFromMime(const QString &mime, const QVariant &data, + const QString &uti) const override; +}; + +QString QMacMimeHTMLText::utiForMime(const QString &mime) const +{ + if (mime == "text/html"_L1) + return "public.html"_L1; + return QString(); +} + +QString QMacMimeHTMLText::mimeForUti(const QString &uti) const +{ + if (uti == "public.html"_L1) + return "text/html"_L1; + return QString(); +} + +QVariant +QMacMimeHTMLText::convertToMime(const QString &mimeType, + const QList<QByteArray> &data, const QString &uti) const +{ + if (!canConvert(mimeType, uti)) + return QVariant(); + if (data.count() > 1) + qWarning("QMacMimeHTMLText: Cannot handle multiple member data"); + return data.first(); +} + +QList<QByteArray> +QMacMimeHTMLText::convertFromMime(const QString &mime, + const QVariant &data, const QString &uti) const +{ + QList<QByteArray> ret; + if (!canConvert(mime, uti)) + return ret; + ret.append(data.toByteArray()); + return ret; +} + +class QMacMimeRtfText : public QUtiMimeConverter +{ +public: + QString utiForMime(const QString &mime) const override; + QString mimeForUti(const QString &uti) const override; + QVariant convertToMime(const QString &mime, const QList<QByteArray> &data, + const QString &uti) const override; + QList<QByteArray> convertFromMime(const QString &mime, const QVariant &data, + const QString &uti) const override; +}; + +QString QMacMimeRtfText::utiForMime(const QString &mime) const +{ + if (mime == "text/html"_L1) + return "public.rtf"_L1; + return QString(); +} + +QString QMacMimeRtfText::mimeForUti(const QString &uti) const +{ + if (uti == "public.rtf"_L1) + return "text/html"_L1; + return QString(); +} + +QVariant +QMacMimeRtfText::convertToMime(const QString &mimeType, + const QList<QByteArray> &data, const QString &uti) const +{ + if (!canConvert(mimeType, uti)) + return QVariant(); + if (data.count() > 1) + qWarning("QMacMimeHTMLText: Cannot handle multiple member data"); + + // Read RTF into to NSAttributedString, then convert the string to HTML + NSAttributedString *string = [[NSAttributedString alloc] initWithData:data.at(0).toNSData() + options:@{NSDocumentTypeDocumentAttribute: NSRTFTextDocumentType} + documentAttributes:nil + error:nil]; + + NSError *error; + NSRange range = NSMakeRange(0, [string length]); + NSDictionary *dict = @{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType}; + NSData *htmlData = [string dataFromRange:range documentAttributes:dict error:&error]; + return QByteArray::fromNSData(htmlData); +} + +QList<QByteArray> +QMacMimeRtfText::convertFromMime(const QString &mime, + const QVariant &data, const QString &uti) const +{ + QList<QByteArray> ret; + if (!canConvert(mime, uti)) + return ret; + + NSAttributedString *string = [[NSAttributedString alloc] initWithData:data.toByteArray().toNSData() + options:@{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType} + documentAttributes:nil + error:nil]; + + NSError *error; + NSRange range = NSMakeRange(0, [string length]); + NSDictionary *dict = @{NSDocumentTypeDocumentAttribute: NSRTFTextDocumentType}; + NSData *rtfData = [string dataFromRange:range documentAttributes:dict error:&error]; + ret << QByteArray::fromNSData(rtfData); + return ret; +} + +class QMacMimeFileUri : public QUtiMimeConverter +{ +public: + QString utiForMime(const QString &mime) const override; + QString mimeForUti(const QString &uti) const override; + QVariant convertToMime(const QString &mime, const QList<QByteArray> &data, + const QString &uti) const override; + QList<QByteArray> convertFromMime(const QString &mime, const QVariant &data, + const QString &uti) const override; + int count(const QMimeData *mimeData) const override; +}; + +QString QMacMimeFileUri::utiForMime(const QString &mime) const +{ + if (mime == "text/uri-list"_L1) + return "public.file-url"_L1; + return QString(); +} + +QString QMacMimeFileUri::mimeForUti(const QString &uti) const +{ + if (uti == "public.file-url"_L1) + return "text/uri-list"_L1; + return QString(); +} + +QVariant +QMacMimeFileUri::convertToMime(const QString &mime, + const QList<QByteArray> &data, const QString &uti) const +{ + if (!canConvert(mime, uti)) + return QVariant(); + QList<QVariant> ret; + for (int i = 0; i < data.size(); ++i) { + const QByteArray &a = data.at(i); + NSString *urlString = [[[NSString alloc] initWithBytesNoCopy:(void *)a.data() length:a.size() + encoding:NSUTF8StringEncoding freeWhenDone:NO] autorelease]; + NSURL *nsurl = [NSURL URLWithString:urlString]; + QUrl url; + // OS X 10.10 sends file references instead of file paths + if ([nsurl isFileReferenceURL]) { + url = QUrl::fromNSURL([nsurl filePathURL]); + } else { + url = QUrl::fromNSURL(nsurl); + } + + if (url.host().toLower() == "localhost"_L1) + url.setHost(QString()); + + url.setPath(url.path().normalized(QString::NormalizationForm_C)); + ret.append(url); + } + return QVariant(ret); +} + +QList<QByteArray> +QMacMimeFileUri::convertFromMime(const QString &mime, + const QVariant &data, const QString &uti) const +{ + QList<QByteArray> ret; + if (!canConvert(mime, uti)) + return ret; + QList<QVariant> urls = data.toList(); + for (int i = 0; i < urls.size(); ++i) { + QUrl url = urls.at(i).toUrl(); + if (url.scheme().isEmpty()) + url.setScheme("file"_L1); + if (url.scheme() == "file"_L1) { + if (url.host().isEmpty()) + url.setHost("localhost"_L1); + url.setPath(url.path().normalized(QString::NormalizationForm_D)); + } + if (url.isLocalFile()) + ret.append(url.toEncoded()); + } + return ret; +} + +int QMacMimeFileUri::count(const QMimeData *mimeData) const +{ + return mimeData->urls().count(); +} + +class QMacMimeUrl : public QUtiMimeConverter +{ +public: + QString utiForMime(const QString &mime) const override; + QString mimeForUti(const QString &uti) const override; + QVariant convertToMime(const QString &mime, const QList<QByteArray> &data, + const QString &uti) const override; + QList<QByteArray> convertFromMime(const QString &mime, const QVariant &data, + const QString &uti) const override; +}; + +QString QMacMimeUrl::utiForMime(const QString &mime) const +{ + if (mime.startsWith("text/uri-list"_L1)) + return "public.url"_L1; + return QString(); +} + +QString QMacMimeUrl::mimeForUti(const QString &uti) const +{ + if (uti == "public.url"_L1) + return "text/uri-list"_L1; + return QString(); +} + +QVariant QMacMimeUrl::convertToMime(const QString &mime, + const QList<QByteArray> &data, const QString &uti) const +{ + if (!canConvert(mime, uti)) + return QVariant(); + + QList<QVariant> ret; + for (int i=0; i<data.size(); ++i) { + QUrl url = QUrl::fromEncoded(data.at(i)); + if (url.host().toLower() == "localhost"_L1) + url.setHost(QString()); + url.setPath(url.path().normalized(QString::NormalizationForm_C)); + ret.append(url); + } + return QVariant(ret); +} + +QList<QByteArray> QMacMimeUrl::convertFromMime(const QString &mime, + const QVariant &data, const QString &uti) const +{ + QList<QByteArray> ret; + if (!canConvert(mime, uti)) + return ret; + + QList<QVariant> urls = data.toList(); + for (int i=0; i<urls.size(); ++i) { + QUrl url = urls.at(i).toUrl(); + if (url.scheme().isEmpty()) + url.setScheme("file"_L1); + if (url.scheme() == "file"_L1) { + if (url.host().isEmpty()) + url.setHost("localhost"_L1); + url.setPath(url.path().normalized(QString::NormalizationForm_D)); + } + ret.append(url.toEncoded()); + } + return ret; +} + +class QMacMimeVCard : public QUtiMimeConverter +{ +public: + QString utiForMime(const QString &mime) const override; + QString mimeForUti(const QString &uti) const override; + QVariant convertToMime(const QString &mime, const QList<QByteArray> &data, + const QString &uti) const override; + QList<QByteArray> convertFromMime(const QString &mime, const QVariant &data, + const QString &uti) const override; +}; + +QString QMacMimeVCard::utiForMime(const QString &mime) const +{ + if (mime.startsWith("text/vcard"_L1)) + return "public.vcard"_L1; + return QString(); +} + +QString QMacMimeVCard::mimeForUti(const QString &uti) const +{ + if (uti == "public.vcard"_L1) + return "text/vcard"_L1; + return QString(); +} + +QVariant QMacMimeVCard::convertToMime(const QString &mime, + const QList<QByteArray> &data, const QString &uti) const +{ + if (!canConvert(mime, uti)) + return QVariant(); + QByteArray cards; + if (uti == "public.vcard"_L1) { + for (int i=0; i<data.size(); ++i) + cards += data[i]; + } + return QVariant(cards); +} + +QList<QByteArray> QMacMimeVCard::convertFromMime(const QString &mime, + const QVariant &data, const QString &uti) const +{ + QList<QByteArray> ret; + if (!canConvert(mime, uti)) + return ret; + + if (mime == "text/vcard"_L1) + ret.append(data.toString().toUtf8()); + return ret; +} + +extern QImage qt_mac_toQImage(CGImageRef image); +extern CGImageRef qt_mac_toCGImage(const QImage &qImage); + +class QMacMimeTiff : public QUtiMimeConverter +{ +public: + QString utiForMime(const QString &mime) const override; + QString mimeForUti(const QString &uti) const override; + QVariant convertToMime(const QString &mime, const QList<QByteArray> &data, + const QString &uti) const override; + QList<QByteArray> convertFromMime(const QString &mime, const QVariant &data, + const QString &uti) const override; +}; + +QString QMacMimeTiff::utiForMime(const QString &mime) const +{ + if (mime.startsWith("application/x-qt-image"_L1)) + return "public.tiff"_L1; + return QString(); +} + +QString QMacMimeTiff::mimeForUti(const QString &uti) const +{ + if (uti == "public.tiff"_L1) + return "application/x-qt-image"_L1; + return QString(); +} + +QVariant QMacMimeTiff::convertToMime(const QString &mime, + const QList<QByteArray> &data, const QString &uti) const +{ + if (data.count() > 1) + qWarning("QMacMimeTiff: Cannot handle multiple member data"); + + if (!canConvert(mime, uti)) + return QVariant(); + + QCFType<CFDataRef> tiffData = data.first().toRawCFData(); + QCFType<CGImageSourceRef> imageSource = CGImageSourceCreateWithData(tiffData, 0); + + if (QCFType<CGImageRef> image = CGImageSourceCreateImageAtIndex(imageSource, 0, 0)) + return QVariant(qt_mac_toQImage(image)); + + return QVariant(); +} + +QList<QByteArray> QMacMimeTiff::convertFromMime(const QString &mime, + const QVariant &variant, const QString &uti) const +{ + if (!canConvert(mime, uti)) + return QList<QByteArray>(); + + QCFType<CFMutableDataRef> data = CFDataCreateMutable(0, 0); + QCFType<CGImageDestinationRef> imageDestination = CGImageDestinationCreateWithData(data, + (CFStringRef)UTTypeTIFF.identifier, 1, 0); + + if (!imageDestination) + return QList<QByteArray>(); + + QImage img = qvariant_cast<QImage>(variant); + NSDictionary *props = @{ + static_cast<NSString *>(kCGImagePropertyPixelWidth): @(img.width()), + static_cast<NSString *>(kCGImagePropertyPixelHeight): @(img.height()) + }; + + CGImageDestinationAddImage(imageDestination, qt_mac_toCGImage(img), + static_cast<CFDictionaryRef>(props)); + CGImageDestinationFinalize(imageDestination); + + return QList<QByteArray>() << QByteArray::fromCFData(data); +} + +namespace QMacMimeRegistry { + +void registerBuiltInTypes() +{ + // Create QMacMimeAny first to put it at the end of globalMimeList + // with lowest priority. (the constructor prepends to the list) + new QMacMimeAny; + + //standard types that we wrap + new QMacMimeTiff; + new QMacMimePlainTextFallback; + new QMacMimeUnicodeText; + new QMacMimeRtfText; + new QMacMimeHTMLText; + new QMacMimeFileUri; + new QMacMimeUrl; + new QMacMimeTypeName; + new QMacMimeVCard; +} + +} + +QT_END_NAMESPACE diff --git a/src/gui/platform/ios/PrivacyInfo.xcprivacy b/src/gui/platform/ios/PrivacyInfo.xcprivacy new file mode 100644 index 0000000000..bde2b167c7 --- /dev/null +++ b/src/gui/platform/ios/PrivacyInfo.xcprivacy @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>NSPrivacyTracking</key> + <false/> + <key>NSPrivacyCollectedDataTypes</key> + <array/> + <key>NSPrivacyTrackingDomains</key> + <array/> + <key>NSPrivacyAccessedAPITypes</key> + <array> + <dict> + <key>NSPrivacyAccessedAPIType</key> + <string>NSPrivacyAccessedAPICategorySystemBootTime</string> + <key>NSPrivacyAccessedAPITypeReasons</key> + <array> + <string>35F9.1</string> <!-- QUIView event handling --> + </array> + </dict> + </array> +</dict> +</plist> diff --git a/src/gui/platform/ios/qiosnativeinterface.cpp b/src/gui/platform/ios/qiosnativeinterface.cpp new file mode 100644 index 0000000000..c942709e33 --- /dev/null +++ b/src/gui/platform/ios/qiosnativeinterface.cpp @@ -0,0 +1,26 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include <QtGui/private/qguiapplication_p.h> + +QT_BEGIN_NAMESPACE + +using namespace QNativeInterface::Private; + +#if defined(Q_OS_VISIONOS) + +/*! + \class QNativeInterface::QVisionOSApplication + \since 6.8 + \internal + \preliminary + \brief Native interface to QGuiApplication, to be retrieved from QPlatformIntegration. + \inmodule QtGui + \ingroup native-interfaces +*/ + +QT_DEFINE_NATIVE_INTERFACE(QVisionOSApplication); + +#endif // Q_OS_VISIONOS + +QT_END_NAMESPACE diff --git a/src/gui/platform/macos/qcocoanativeinterface.mm b/src/gui/platform/macos/qcocoanativeinterface.mm new file mode 100644 index 0000000000..cb6acb4496 --- /dev/null +++ b/src/gui/platform/macos/qcocoanativeinterface.mm @@ -0,0 +1,86 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include <QtGui/qtgui-config.h> +#ifndef QT_NO_OPENGL +# include <QtGui/private/qopenglcontext_p.h> +#endif + +#include <QtGui/private/qguiapplication_p.h> +#include <qpa/qplatformopenglcontext.h> +#include <qpa/qplatformintegration.h> +#include <qpa/qplatformwindow_p.h> +#include <qpa/qplatformmenu_p.h> + +#include <AppKit/AppKit.h> + +QT_BEGIN_NAMESPACE + +using namespace QNativeInterface::Private; + +/*! + \class QNativeInterface::Private::QCocoaWindow + \since 6.0 + \internal + \brief Native interface for QPlatformWindow on \macos. + \inmodule QtGui + \ingroup native-interfaces +*/ + +QT_DEFINE_PRIVATE_NATIVE_INTERFACE(QCocoaWindow); + + +/*! + \class QNativeInterface::Private::QCocoaMenu + \since 6.0 + \internal + \brief Native interface for QPlatformMenu on \macos. + \inmodule QtGui + \ingroup native-interfaces +*/ + +QT_DEFINE_PRIVATE_NATIVE_INTERFACE(QCocoaMenu); +QT_DEFINE_PRIVATE_NATIVE_INTERFACE(QCocoaMenuBar); + +#ifndef QT_NO_OPENGL + +/*! + \class QNativeInterface::QCocoaGLContext + \since 6.0 + \brief Native interface to an NSOpenGLContext on \macos. + + Accessed through QOpenGLContext::nativeInterface(). + + \inmodule QtGui + \inheaderfile QOpenGLContext + \ingroup native-interfaces + \ingroup native-interfaces-qopenglcontext +*/ + +/*! + \fn QOpenGLContext *QNativeInterface::QCocoaGLContext::fromNative(NSOpenGLContext *context, QOpenGLContext *shareContext = nullptr) + + \brief Adopts an NSOpenGLContext. + + The adopted NSOpenGLContext \a context is retained. Ownership of the + created QOpenGLContext \a shareContext is transferred to the caller. +*/ + +/*! + \fn NSOpenGLContext *QNativeInterface::QCocoaGLContext::nativeContext() const + + \return the underlying NSOpenGLContext. +*/ + +QT_DEFINE_NATIVE_INTERFACE(QCocoaGLContext); +QT_DEFINE_PRIVATE_NATIVE_INTERFACE(QCocoaGLIntegration); + +QOpenGLContext *QNativeInterface::QCocoaGLContext::fromNative(NSOpenGLContext *nativeContext, QOpenGLContext *shareContext) +{ + return QGuiApplicationPrivate::platformIntegration()->call< + &QCocoaGLIntegration::createOpenGLContext>(nativeContext, shareContext); +} + +#endif // QT_NO_OPENGL + +QT_END_NAMESPACE diff --git a/src/gui/platform/platform.pri b/src/gui/platform/platform.pri deleted file mode 100644 index 1fe2db81b0..0000000000 --- a/src/gui/platform/platform.pri +++ /dev/null @@ -1 +0,0 @@ -wasm:include(wasm/wasm.pri) diff --git a/src/gui/platform/unix/dbusmenu/qdbusmenuadaptor.cpp b/src/gui/platform/unix/dbusmenu/qdbusmenuadaptor.cpp new file mode 100644 index 0000000000..a3b7b6db00 --- /dev/null +++ b/src/gui/platform/unix/dbusmenu/qdbusmenuadaptor.cpp @@ -0,0 +1,133 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +/* + This file was originally created by qdbusxml2cpp version 0.8 + Command line was: + qdbusxml2cpp -a dbusmenu ../../3rdparty/dbus-ifaces/dbus-menu.xml + + However it is maintained manually. +*/ + +#include <QMetaObject> +#include <QByteArray> +#include <QList> +#include <QMap> +#include <QString> +#include <QStringList> +#include <QVariant> +#include <QLocale> + +#include <private/qdbusmenuadaptor_p.h> +#include <private/qdbusplatformmenu_p.h> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +QDBusMenuAdaptor::QDBusMenuAdaptor(QDBusPlatformMenu *topLevelMenu) + : QDBusAbstractAdaptor(topLevelMenu) + , m_topLevelMenu(topLevelMenu) +{ + setAutoRelaySignals(true); +} + +QDBusMenuAdaptor::~QDBusMenuAdaptor() +{ +} + +QString QDBusMenuAdaptor::status() const +{ + qCDebug(qLcMenu); + return "normal"_L1; +} + +QString QDBusMenuAdaptor::textDirection() const +{ + return QLocale().textDirection() == Qt::RightToLeft ? "rtl"_L1 : "ltr"_L1; +} + +uint QDBusMenuAdaptor::version() const +{ + return 4; +} + +bool QDBusMenuAdaptor::AboutToShow(int id) +{ + qCDebug(qLcMenu) << id; + if (id == 0) { + emit m_topLevelMenu->aboutToShow(); + } else { + QDBusPlatformMenuItem *item = QDBusPlatformMenuItem::byId(id); + if (item) { + const QDBusPlatformMenu *menu = static_cast<const QDBusPlatformMenu *>(item->menu()); + if (menu) + emit const_cast<QDBusPlatformMenu *>(menu)->aboutToShow(); + } + } + return false; // updateNeeded (we don't know that, so false) +} + +QList<int> QDBusMenuAdaptor::AboutToShowGroup(const QList<int> &ids, QList<int> &idErrors) +{ + qCDebug(qLcMenu) << ids; + Q_UNUSED(idErrors); + idErrors.clear(); + for (int id : ids) + AboutToShow(id); + return QList<int>(); // updatesNeeded +} + +void QDBusMenuAdaptor::Event(int id, const QString &eventId, const QDBusVariant &data, uint timestamp) +{ + Q_UNUSED(data); + Q_UNUSED(timestamp); + QDBusPlatformMenuItem *item = QDBusPlatformMenuItem::byId(id); + qCDebug(qLcMenu) << id << (item ? item->text() : ""_L1) << eventId; + if (item && eventId == "clicked"_L1) + item->trigger(); + if (item && eventId == "hovered"_L1) + emit item->hovered(); + if (eventId == "closed"_L1) { + // There is no explicit AboutToHide method, so map closed event to aboutToHide method + const QDBusPlatformMenu *menu = nullptr; + if (item) + menu = static_cast<const QDBusPlatformMenu *>(item->menu()); + else if (id == 0) + menu = m_topLevelMenu; + if (menu) + emit const_cast<QDBusPlatformMenu *>(menu)->aboutToHide(); + } +} + +QList<int> QDBusMenuAdaptor::EventGroup(const QDBusMenuEventList &events) +{ + for (const QDBusMenuEvent &ev : events) + Event(ev.m_id, ev.m_eventId, ev.m_data, ev.m_timestamp); + return QList<int>(); // idErrors +} + +QDBusMenuItemList QDBusMenuAdaptor::GetGroupProperties(const QList<int> &ids, const QStringList &propertyNames) +{ + qCDebug(qLcMenu) << ids << propertyNames << "=>" << QDBusMenuItem::items(ids, propertyNames); + return QDBusMenuItem::items(ids, propertyNames); +} + +uint QDBusMenuAdaptor::GetLayout(int parentId, int recursionDepth, const QStringList &propertyNames, QDBusMenuLayoutItem &layout) +{ + uint ret = layout.populate(parentId, recursionDepth, propertyNames, m_topLevelMenu); + qCDebug(qLcMenu) << parentId << "depth" << recursionDepth << propertyNames << layout.m_id << layout.m_properties << "revision" << ret << layout; + return ret; +} + +QDBusVariant QDBusMenuAdaptor::GetProperty(int id, const QString &name) +{ + qCDebug(qLcMenu) << id << name; + // handle method call com.canonical.dbusmenu.GetProperty + QDBusVariant value; + return value; +} + +QT_END_NAMESPACE + +#include "moc_qdbusmenuadaptor_p.cpp" diff --git a/src/gui/platform/unix/dbusmenu/qdbusmenuadaptor_p.h b/src/gui/platform/unix/dbusmenu/qdbusmenuadaptor_p.h new file mode 100644 index 0000000000..b6f538f6b3 --- /dev/null +++ b/src/gui/platform/unix/dbusmenu/qdbusmenuadaptor_p.h @@ -0,0 +1,147 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +/* + This file was originally created by qdbusxml2cpp version 0.8 + Command line was: + qdbusxml2cpp -a dbusmenu ../../3rdparty/dbus-ifaces/dbus-menu.xml + + However it is maintained manually. + + It is also not part of the public API. This header file may change from + version to version without notice, or even be removed. +*/ + +#ifndef DBUSMENUADAPTOR_H +#define DBUSMENUADAPTOR_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QObject> +#include <QDBusAbstractAdaptor> + +#include <private/qdbusmenutypes_p.h> + +QT_BEGIN_NAMESPACE + +/* + * Adaptor class for interface com.canonical.dbusmenu + */ +class QDBusMenuAdaptor: public QDBusAbstractAdaptor +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "com.canonical.dbusmenu") + Q_CLASSINFO("D-Bus Introspection", "" +" <interface name=\"com.canonical.dbusmenu\">\n" +" <property access=\"read\" type=\"u\" name=\"Version\">\n" +" </property>\n" +" <property access=\"read\" type=\"s\" name=\"TextDirection\">\n" +" </property>\n" +" <property access=\"read\" type=\"s\" name=\"Status\">\n" +" </property>\n" +" <property access=\"read\" type=\"as\" name=\"IconThemePath\">\n" +" </property>\n" +" <method name=\"GetLayout\">\n" +" <annotation value=\"QDBusMenuLayoutItem\" name=\"org.qtproject.QtDBus.QtTypeName.Out1\"/>\n" +" <arg direction=\"in\" type=\"i\" name=\"parentId\"/>\n" +" <arg direction=\"in\" type=\"i\" name=\"recursionDepth\"/>\n" +" <arg direction=\"in\" type=\"as\" name=\"propertyNames\"/>\n" +" <arg direction=\"out\" type=\"u\" name=\"revision\"/>\n" +" <arg direction=\"out\" type=\"(ia{sv}av)\" name=\"layout\"/>\n" +" </method>\n" +" <method name=\"GetGroupProperties\">\n" +" <annotation value=\"QList<int>\" name=\"org.qtproject.QtDBus.QtTypeName.In0\"/>\n" +" <annotation value=\"QDBusMenuItemList\" name=\"org.qtproject.QtDBus.QtTypeName.Out0\"/>\n" +" <arg direction=\"in\" type=\"ai\" name=\"ids\"/>\n" +" <arg direction=\"in\" type=\"as\" name=\"propertyNames\"/>\n" +" <arg direction=\"out\" type=\"a(ia{sv})\" name=\"properties\"/>\n" +" </method>\n" +" <method name=\"GetProperty\">\n" +" <arg direction=\"in\" type=\"i\" name=\"id\"/>\n" +" <arg direction=\"in\" type=\"s\" name=\"name\"/>\n" +" <arg direction=\"out\" type=\"v\" name=\"value\"/>\n" +" </method>\n" +" <method name=\"Event\">\n" +" <arg direction=\"in\" type=\"i\" name=\"id\"/>\n" +" <arg direction=\"in\" type=\"s\" name=\"eventId\"/>\n" +" <arg direction=\"in\" type=\"v\" name=\"data\"/>\n" +" <arg direction=\"in\" type=\"u\" name=\"timestamp\"/>\n" +" </method>\n" +" <method name=\"EventGroup\">\n" +" <annotation value=\"QList<QDBusMenuEvent>\" name=\"org.qtproject.QtDBus.QtTypeName.In0\"/>\n" +" <annotation value=\"QList<int>\" name=\"org.qtproject.QtDBus.QtTypeName.Out0\"/>\n" +" <arg direction=\"in\" type=\"a(isvu)\" name=\"events\"/>\n" +" <arg direction=\"out\" type=\"ai\" name=\"idErrors\"/>\n" +" </method>\n" +" <method name=\"AboutToShow\">\n" +" <arg direction=\"in\" type=\"i\" name=\"id\"/>\n" +" <arg direction=\"out\" type=\"b\" name=\"needUpdate\"/>\n" +" </method>\n" +" <method name=\"AboutToShowGroup\">\n" +" <annotation value=\"QList<int>\" name=\"org.qtproject.QtDBus.QtTypeName.In0\"/>\n" +" <annotation value=\"QList<int>\" name=\"org.qtproject.QtDBus.QtTypeName.Out0\"/>\n" +" <annotation value=\"QList<int>\" name=\"org.qtproject.QtDBus.QtTypeName.Out1\"/>\n" +" <arg direction=\"in\" type=\"ai\" name=\"ids\"/>\n" +" <arg direction=\"out\" type=\"ai\" name=\"updatesNeeded\"/>\n" +" <arg direction=\"out\" type=\"ai\" name=\"idErrors\"/>\n" +" </method>\n" +" <signal name=\"ItemsPropertiesUpdated\">\n" +" <annotation value=\"QDBusMenuItemList\" name=\"org.qtproject.QtDBus.QtTypeName.In0\"/>\n" +" <annotation value=\"QDBusMenuItemKeysList\" name=\"org.qtproject.QtDBus.QtTypeName.In1\"/>\n" +" <arg direction=\"out\" type=\"a(ia{sv})\" name=\"updatedProps\"/>\n" +" <arg direction=\"out\" type=\"a(ias)\" name=\"removedProps\"/>\n" +" </signal>\n" +" <signal name=\"LayoutUpdated\">\n" +" <arg direction=\"out\" type=\"u\" name=\"revision\"/>\n" +" <arg direction=\"out\" type=\"i\" name=\"parent\"/>\n" +" </signal>\n" +" <signal name=\"ItemActivationRequested\">\n" +" <arg direction=\"out\" type=\"i\" name=\"id\"/>\n" +" <arg direction=\"out\" type=\"u\" name=\"timestamp\"/>\n" +" </signal>\n" +" </interface>\n" + "") +public: + QDBusMenuAdaptor(QDBusPlatformMenu *topLevelMenu); + virtual ~QDBusMenuAdaptor(); + +public: // PROPERTIES + Q_PROPERTY(QString Status READ status) + QString status() const; + + Q_PROPERTY(QString TextDirection READ textDirection) + QString textDirection() const; + + Q_PROPERTY(uint Version READ version) + uint version() const; + +public Q_SLOTS: // METHODS + bool AboutToShow(int id); + QList<int> AboutToShowGroup(const QList<int> &ids, QList<int> &idErrors); + void Event(int id, const QString &eventId, const QDBusVariant &data, uint timestamp); + QList<int> EventGroup(const QDBusMenuEventList &events); + QDBusMenuItemList GetGroupProperties(const QList<int> &ids, const QStringList &propertyNames); + uint GetLayout(int parentId, int recursionDepth, const QStringList &propertyNames, QDBusMenuLayoutItem &layout); + QDBusVariant GetProperty(int id, const QString &name); + +Q_SIGNALS: // SIGNALS + void ItemActivationRequested(int id, uint timestamp); + void ItemsPropertiesUpdated(const QDBusMenuItemList &updatedProps, const QDBusMenuItemKeysList &removedProps); + void LayoutUpdated(uint revision, int parent); + +private: + QDBusPlatformMenu *m_topLevelMenu; +}; + +QT_END_NAMESPACE + +#endif // DBUSMENUADAPTOR_H diff --git a/src/gui/platform/unix/dbusmenu/qdbusmenubar.cpp b/src/gui/platform/unix/dbusmenu/qdbusmenubar.cpp new file mode 100644 index 0000000000..2c006366cb --- /dev/null +++ b/src/gui/platform/unix/dbusmenu/qdbusmenubar.cpp @@ -0,0 +1,147 @@ +// Copyright (C) 2016 Dmitry Shachnev <mitya57@gmail.com> +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qdbusmenubar_p.h" +#include "qdbusmenuregistrarproxy_p.h" + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +/* note: do not change these to QStringLiteral; + we are unloaded before QtDBus is done using the strings. + */ +#define REGISTRAR_SERVICE "com.canonical.AppMenu.Registrar"_L1 +#define REGISTRAR_PATH "/com/canonical/AppMenu/Registrar"_L1 + +QDBusMenuBar::QDBusMenuBar() + : QPlatformMenuBar() + , m_menu(new QDBusPlatformMenu()) + , m_menuAdaptor(new QDBusMenuAdaptor(m_menu)) + , m_windowId(0) +{ + QDBusMenuItem::registerDBusTypes(); + connect(m_menu, &QDBusPlatformMenu::propertiesUpdated, + m_menuAdaptor, &QDBusMenuAdaptor::ItemsPropertiesUpdated); + connect(m_menu, &QDBusPlatformMenu::updated, + m_menuAdaptor, &QDBusMenuAdaptor::LayoutUpdated); + connect(m_menu, &QDBusPlatformMenu::popupRequested, + m_menuAdaptor, &QDBusMenuAdaptor::ItemActivationRequested); +} + +QDBusMenuBar::~QDBusMenuBar() +{ + unregisterMenuBar(); + delete m_menuAdaptor; + delete m_menu; + qDeleteAll(m_menuItems); +} + +QDBusPlatformMenuItem *QDBusMenuBar::menuItemForMenu(QPlatformMenu *menu) +{ + if (!menu) + return nullptr; + quintptr tag = menu->tag(); + const auto it = m_menuItems.constFind(tag); + if (it != m_menuItems.cend()) { + return *it; + } else { + QDBusPlatformMenuItem *item = new QDBusPlatformMenuItem; + updateMenuItem(item, menu); + m_menuItems.insert(tag, item); + return item; + } +} + +void QDBusMenuBar::updateMenuItem(QDBusPlatformMenuItem *item, QPlatformMenu *menu) +{ + const QDBusPlatformMenu *ourMenu = qobject_cast<const QDBusPlatformMenu *>(menu); + item->setText(ourMenu->text()); + item->setIcon(ourMenu->icon()); + item->setEnabled(ourMenu->isEnabled()); + item->setVisible(ourMenu->isVisible()); + item->setMenu(menu); +} + +void QDBusMenuBar::insertMenu(QPlatformMenu *menu, QPlatformMenu *before) +{ + QDBusPlatformMenuItem *menuItem = menuItemForMenu(menu); + QDBusPlatformMenuItem *beforeItem = menuItemForMenu(before); + m_menu->insertMenuItem(menuItem, beforeItem); + m_menu->emitUpdated(); +} + +void QDBusMenuBar::removeMenu(QPlatformMenu *menu) +{ + QDBusPlatformMenuItem *menuItem = menuItemForMenu(menu); + m_menu->removeMenuItem(menuItem); + m_menu->emitUpdated(); +} + +void QDBusMenuBar::syncMenu(QPlatformMenu *menu) +{ + QDBusPlatformMenuItem *menuItem = menuItemForMenu(menu); + updateMenuItem(menuItem, menu); +} + +void QDBusMenuBar::handleReparent(QWindow *newParentWindow) +{ + if (newParentWindow) { + unregisterMenuBar(); + m_windowId = newParentWindow->winId(); + registerMenuBar(); + } +} + +QPlatformMenu *QDBusMenuBar::menuForTag(quintptr tag) const +{ + QDBusPlatformMenuItem *menuItem = m_menuItems.value(tag); + if (menuItem) + return const_cast<QPlatformMenu *>(menuItem->menu()); + return nullptr; +} + +QPlatformMenu *QDBusMenuBar::createMenu() const +{ + return new QDBusPlatformMenu; +} + +void QDBusMenuBar::registerMenuBar() +{ + static uint menuBarId = 0; + + QDBusConnection connection = QDBusConnection::sessionBus(); + m_objectPath = QStringLiteral("/MenuBar/%1").arg(++menuBarId); + if (!connection.registerObject(m_objectPath, m_menu)) + return; + + QDBusMenuRegistrarInterface registrar(REGISTRAR_SERVICE, REGISTRAR_PATH, connection, this); + QDBusPendingReply<> r = registrar.RegisterWindow(m_windowId, QDBusObjectPath(m_objectPath)); + r.waitForFinished(); + if (r.isError()) { + qWarning("Failed to register window menu, reason: %s (\"%s\")", + qUtf8Printable(r.error().name()), qUtf8Printable(r.error().message())); + connection.unregisterObject(m_objectPath); + } +} + +void QDBusMenuBar::unregisterMenuBar() +{ + QDBusConnection connection = QDBusConnection::sessionBus(); + + if (m_windowId) { + QDBusMenuRegistrarInterface registrar(REGISTRAR_SERVICE, REGISTRAR_PATH, connection, this); + QDBusPendingReply<> r = registrar.UnregisterWindow(m_windowId); + r.waitForFinished(); + if (r.isError()) + qWarning("Failed to unregister window menu, reason: %s (\"%s\")", + qUtf8Printable(r.error().name()), qUtf8Printable(r.error().message())); + } + + if (!m_objectPath.isEmpty()) + connection.unregisterObject(m_objectPath); +} + +QT_END_NAMESPACE + +#include "moc_qdbusmenubar_p.cpp" diff --git a/src/gui/platform/unix/dbusmenu/qdbusmenubar_p.h b/src/gui/platform/unix/dbusmenu/qdbusmenubar_p.h new file mode 100644 index 0000000000..3028df7215 --- /dev/null +++ b/src/gui/platform/unix/dbusmenu/qdbusmenubar_p.h @@ -0,0 +1,56 @@ +// Copyright (C) 2016 Dmitry Shachnev <mitya57@gmail.com> +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QDBUSMENUBAR_P_H +#define QDBUSMENUBAR_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <private/qdbusplatformmenu_p.h> +#include <private/qdbusmenuadaptor_p.h> +#include <QtCore/QHash> +#include <QtCore/QString> +#include <QtGui/QWindow> + +QT_BEGIN_NAMESPACE + +class QDBusMenuBar : public QPlatformMenuBar +{ + Q_OBJECT + +public: + QDBusMenuBar(); + virtual ~QDBusMenuBar(); + + void insertMenu(QPlatformMenu *menu, QPlatformMenu *before) override; + void removeMenu(QPlatformMenu *menu) override; + void syncMenu(QPlatformMenu *menu) override; + void handleReparent(QWindow *newParentWindow) override; + QPlatformMenu *menuForTag(quintptr tag) const override; + QPlatformMenu *createMenu() const override; + +private: + QDBusPlatformMenu *m_menu; + QDBusMenuAdaptor *m_menuAdaptor; + QHash<quintptr, QDBusPlatformMenuItem *> m_menuItems; + uint m_windowId; + QString m_objectPath; + + QDBusPlatformMenuItem *menuItemForMenu(QPlatformMenu *menu); + static void updateMenuItem(QDBusPlatformMenuItem *item, QPlatformMenu *menu); + void registerMenuBar(); + void unregisterMenuBar(); +}; + +QT_END_NAMESPACE + +#endif // QDBUSMENUBAR_P_H diff --git a/src/gui/platform/unix/dbusmenu/qdbusmenuconnection.cpp b/src/gui/platform/unix/dbusmenu/qdbusmenuconnection.cpp new file mode 100644 index 0000000000..1023b16662 --- /dev/null +++ b/src/gui/platform/unix/dbusmenu/qdbusmenuconnection.cpp @@ -0,0 +1,114 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include <QtGui/qtgui-config.h> + +#include <QDBusMessage> +#include <QDBusInterface> +#include <QDBusServiceWatcher> +#include <QDBusConnectionInterface> +#include <QDebug> +#include <QCoreApplication> + +#ifndef QT_NO_SYSTEMTRAYICON +#include <private/qdbustrayicon_p.h> +#endif +#include <private/qdbusmenuconnection_p.h> +#include <private/qdbusmenuadaptor_p.h> +#include <private/qdbusplatformmenu_p.h> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +Q_DECLARE_LOGGING_CATEGORY(qLcMenu) + +const QString StatusNotifierWatcherService = "org.kde.StatusNotifierWatcher"_L1; +const QString StatusNotifierWatcherPath = "/StatusNotifierWatcher"_L1; +const QString StatusNotifierItemPath = "/StatusNotifierItem"_L1; +const QString MenuBarPath = "/MenuBar"_L1; + +/*! + \class QDBusMenuConnection + \internal + A D-Bus connection which is used for both menu and tray icon services. + Connects to the session bus and registers with the respective watcher services. +*/ +QDBusMenuConnection::QDBusMenuConnection(QObject *parent, const QString &serviceName) + : QObject(parent) + , m_serviceName(serviceName) + , m_connection(serviceName.isNull() ? QDBusConnection::sessionBus() + : QDBusConnection::connectToBus(QDBusConnection::SessionBus, serviceName)) + , m_dbusWatcher(new QDBusServiceWatcher(StatusNotifierWatcherService, m_connection, QDBusServiceWatcher::WatchForRegistration, this)) + , m_watcherRegistered(false) +{ +#ifndef QT_NO_SYSTEMTRAYICON + // Start monitoring if any known tray-related services are registered. + if (m_connection.interface()->isServiceRegistered(StatusNotifierWatcherService)) + m_watcherRegistered = true; + else + qCDebug(qLcMenu) << "failed to find service" << StatusNotifierWatcherService; +#endif +} + +QDBusMenuConnection::~QDBusMenuConnection() +{ + if (!m_serviceName.isEmpty() && m_connection.isConnected()) + QDBusConnection::disconnectFromBus(m_serviceName); +} + +void QDBusMenuConnection::dbusError(const QDBusError &error) +{ + qWarning() << "QDBusTrayIcon encountered a D-Bus error:" << error; +} + +#ifndef QT_NO_SYSTEMTRAYICON +bool QDBusMenuConnection::registerTrayIconMenu(QDBusTrayIcon *item) +{ + bool success = connection().registerObject(MenuBarPath, item->menu()); + if (!success) // success == false is normal, because the object may be already registered + qCDebug(qLcMenu) << "failed to register" << item->instanceId() << MenuBarPath; + return success; +} + +void QDBusMenuConnection::unregisterTrayIconMenu(QDBusTrayIcon *item) +{ + if (item->menu()) + connection().unregisterObject(MenuBarPath); +} + +bool QDBusMenuConnection::registerTrayIcon(QDBusTrayIcon *item) +{ + bool success = connection().registerObject(StatusNotifierItemPath, item); + if (!success) { + unregisterTrayIcon(item); + qWarning() << "failed to register" << item->instanceId() << StatusNotifierItemPath; + return false; + } + + if (item->menu()) + registerTrayIconMenu(item); + + return registerTrayIconWithWatcher(item); +} + +bool QDBusMenuConnection::registerTrayIconWithWatcher(QDBusTrayIcon *item) +{ + Q_UNUSED(item); + QDBusMessage registerMethod = QDBusMessage::createMethodCall( + StatusNotifierWatcherService, StatusNotifierWatcherPath, StatusNotifierWatcherService, + "RegisterStatusNotifierItem"_L1); + registerMethod.setArguments(QVariantList() << m_connection.baseService()); + return m_connection.callWithCallback(registerMethod, this, SIGNAL(trayIconRegistered()), SLOT(dbusError(QDBusError))); +} + +void QDBusMenuConnection::unregisterTrayIcon(QDBusTrayIcon *item) +{ + unregisterTrayIconMenu(item); + connection().unregisterObject(StatusNotifierItemPath); +} +#endif // QT_NO_SYSTEMTRAYICON + +QT_END_NAMESPACE + +#include "moc_qdbusmenuconnection_p.cpp" diff --git a/src/gui/platform/unix/dbusmenu/qdbusmenuconnection_p.h b/src/gui/platform/unix/dbusmenu/qdbusmenuconnection_p.h new file mode 100644 index 0000000000..37033e2fa3 --- /dev/null +++ b/src/gui/platform/unix/dbusmenu/qdbusmenuconnection_p.h @@ -0,0 +1,68 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QDBUSMENUCONNECTION_H +#define QDBUSMENUCONNECTION_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/QString> +#include <QtDBus/QDBusConnection> +#include <QtDBus/QDBusVariant> + +#include <QtGui/qtgui-config.h> +#include <QtCore/private/qglobal_p.h> +Q_MOC_INCLUDE(<QtDBus/QDBusError>) + +QT_BEGIN_NAMESPACE + +class QDBusServiceWatcher; +#ifndef QT_NO_SYSTEMTRAYICON +class QDBusTrayIcon; +#endif // QT_NO_SYSTEMTRAYICON + +class QDBusMenuConnection : public QObject +{ + Q_OBJECT + +public: + QDBusMenuConnection(QObject *parent = nullptr, const QString &serviceName = QString()); + ~QDBusMenuConnection(); + QDBusConnection connection() const { return m_connection; } + QDBusServiceWatcher *dbusWatcher() const { return m_dbusWatcher; } + bool isWatcherRegistered() const { return m_watcherRegistered; } +#ifndef QT_NO_SYSTEMTRAYICON + bool registerTrayIconMenu(QDBusTrayIcon *item); + void unregisterTrayIconMenu(QDBusTrayIcon *item); + bool registerTrayIcon(QDBusTrayIcon *item); + bool registerTrayIconWithWatcher(QDBusTrayIcon *item); + void unregisterTrayIcon(QDBusTrayIcon *item); +#endif // QT_NO_SYSTEMTRAYICON + +Q_SIGNALS: +#ifndef QT_NO_SYSTEMTRAYICON + void trayIconRegistered(); +#endif // QT_NO_SYSTEMTRAYICON + +private Q_SLOTS: + void dbusError(const QDBusError &error); + +private: + QString m_serviceName; + QDBusConnection m_connection; + QDBusServiceWatcher *m_dbusWatcher; + bool m_watcherRegistered; +}; + +QT_END_NAMESPACE + +#endif // QDBUSMENUCONNECTION_H diff --git a/src/gui/platform/unix/dbusmenu/qdbusmenuregistrarproxy.cpp b/src/gui/platform/unix/dbusmenu/qdbusmenuregistrarproxy.cpp new file mode 100644 index 0000000000..089bf5de47 --- /dev/null +++ b/src/gui/platform/unix/dbusmenu/qdbusmenuregistrarproxy.cpp @@ -0,0 +1,30 @@ +// Copyright (C) 2016 Dmitry Shachnev <mitya57@gmail.com> +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +/* + * This file was originally created by qdbusxml2cpp version 0.8 + * Command line was: qdbusxml2cpp -p qdbusmenuregistrarproxy ../../3rdparty/dbus-ifaces/com.canonical.AppMenu.Registrar.xml + * + * However it is maintained manually. + */ + +#include "qdbusmenuregistrarproxy_p.h" + +QT_BEGIN_NAMESPACE + +/* + * Implementation of interface class QDBusMenuRegistrarInterface + */ + +QDBusMenuRegistrarInterface::QDBusMenuRegistrarInterface(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent) + : QDBusAbstractInterface(service, path, staticInterfaceName(), connection, parent) +{ +} + +QDBusMenuRegistrarInterface::~QDBusMenuRegistrarInterface() +{ +} + +QT_END_NAMESPACE + +#include "moc_qdbusmenuregistrarproxy_p.cpp" diff --git a/src/gui/platform/unix/dbusmenu/qdbusmenuregistrarproxy_p.h b/src/gui/platform/unix/dbusmenu/qdbusmenuregistrarproxy_p.h new file mode 100644 index 0000000000..8041f3af0a --- /dev/null +++ b/src/gui/platform/unix/dbusmenu/qdbusmenuregistrarproxy_p.h @@ -0,0 +1,84 @@ +// Copyright (C) 2016 Dmitry Shachnev <mitya57@gmail.com> +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +/* + * This file was originally created by qdbusxml2cpp version 0.8 + * Command line was: qdbusxml2cpp -p qdbusmenuregistrarproxy ../../3rdparty/dbus-ifaces/com.canonical.AppMenu.Registrar.xml + * + * However it is maintained manually. + */ + +#ifndef QDBUSMENUREGISTRARPROXY_P_H +#define QDBUSMENUREGISTRARPROXY_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/QObject> +#include <QtCore/QByteArray> +#include <QtCore/QList> +#include <QtCore/QString> +#include <QtCore/QVariant> +#include <QtDBus/QDBusAbstractInterface> +#include <QtDBus/QDBusConnection> +#include <QtDBus/QDBusReply> +#include <QtCore/private/qglobal_p.h> + +QT_BEGIN_NAMESPACE + +/* + * Proxy class for interface com.canonical.AppMenu.Registrar + */ +class QDBusMenuRegistrarInterface : public QDBusAbstractInterface +{ + Q_OBJECT +public: + static inline const char *staticInterfaceName() + { + return "com.canonical.AppMenu.Registrar"; + } + +public: + explicit QDBusMenuRegistrarInterface(const QString &service, + const QString &path, + const QDBusConnection &connection, + QObject *parent = nullptr); + + ~QDBusMenuRegistrarInterface(); + +public Q_SLOTS: // METHODS + QDBusPendingReply<QString, QDBusObjectPath> GetMenuForWindow(uint windowId) + { + return asyncCall(QStringLiteral("GetMenuForWindow"), windowId); + } + QDBusReply<QString> GetMenuForWindow(uint windowId, QDBusObjectPath &menuObjectPath) + { + QDBusMessage reply = call(QDBus::Block, QStringLiteral("GetMenuForWindow"), windowId); + QList<QVariant> arguments = reply.arguments(); + if (reply.type() == QDBusMessage::ReplyMessage && arguments.size() == 2) + menuObjectPath = qdbus_cast<QDBusObjectPath>(arguments.at(1)); + return reply; + } + + QDBusPendingReply<> RegisterWindow(uint windowId, const QDBusObjectPath &menuObjectPath) + { + return asyncCall(QStringLiteral("RegisterWindow"), windowId, menuObjectPath); + } + + QDBusPendingReply<> UnregisterWindow(uint windowId) + { + return asyncCall(QStringLiteral("UnregisterWindow"), windowId); + } +}; + +QT_END_NAMESPACE + +#endif // QDBUSMENUREGISTRARPROXY_P_H diff --git a/src/gui/platform/unix/dbusmenu/qdbusmenutypes.cpp b/src/gui/platform/unix/dbusmenu/qdbusmenutypes.cpp new file mode 100644 index 0000000000..b7fd035883 --- /dev/null +++ b/src/gui/platform/unix/dbusmenu/qdbusmenutypes.cpp @@ -0,0 +1,279 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qdbusmenutypes_p.h" + +#include <QDBusConnection> +#include <QDBusMetaType> +#include <QImage> +#include <QIcon> +#include <QImage> +#include <QPixmap> +#include <QDebug> +#include <QtEndian> +#include <QBuffer> +#if QT_CONFIG(shortcut) +# include <private/qkeysequence_p.h> +#endif +#include <qpa/qplatformmenu.h> +#include "qdbusplatformmenu_p.h" + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +QT_IMPL_METATYPE_EXTERN(QDBusMenuItem) +QT_IMPL_METATYPE_EXTERN(QDBusMenuItemList) +QT_IMPL_METATYPE_EXTERN(QDBusMenuItemKeys) +QT_IMPL_METATYPE_EXTERN(QDBusMenuItemKeysList) +QT_IMPL_METATYPE_EXTERN(QDBusMenuLayoutItem) +QT_IMPL_METATYPE_EXTERN(QDBusMenuLayoutItemList) +QT_IMPL_METATYPE_EXTERN(QDBusMenuEvent) +QT_IMPL_METATYPE_EXTERN(QDBusMenuEventList) +QT_IMPL_METATYPE_EXTERN(QDBusMenuShortcut) + +const QDBusArgument &operator<<(QDBusArgument &arg, const QDBusMenuItem &item) +{ + arg.beginStructure(); + arg << item.m_id << item.m_properties; + arg.endStructure(); + return arg; +} + +const QDBusArgument &operator>>(const QDBusArgument &arg, QDBusMenuItem &item) +{ + arg.beginStructure(); + arg >> item.m_id >> item.m_properties; + arg.endStructure(); + return arg; +} + +const QDBusArgument &operator<<(QDBusArgument &arg, const QDBusMenuItemKeys &keys) +{ + arg.beginStructure(); + arg << keys.id << keys.properties; + arg.endStructure(); + return arg; +} + +const QDBusArgument &operator>>(const QDBusArgument &arg, QDBusMenuItemKeys &keys) +{ + arg.beginStructure(); + arg >> keys.id >> keys.properties; + arg.endStructure(); + return arg; +} + +uint QDBusMenuLayoutItem::populate(int id, int depth, const QStringList &propertyNames, const QDBusPlatformMenu *topLevelMenu) +{ + qCDebug(qLcMenu) << id << "depth" << depth << propertyNames; + m_id = id; + if (id == 0) { + m_properties.insert("children-display"_L1, "submenu"_L1); + if (topLevelMenu) + populate(topLevelMenu, depth, propertyNames); + return 1; // revision + } + + QDBusPlatformMenuItem *item = QDBusPlatformMenuItem::byId(id); + if (item) { + const QDBusPlatformMenu *menu = static_cast<const QDBusPlatformMenu *>(item->menu()); + + if (menu) { + if (depth != 0) + populate(menu, depth, propertyNames); + return menu->revision(); + } + } + + return 1; // revision +} + +void QDBusMenuLayoutItem::populate(const QDBusPlatformMenu *menu, int depth, const QStringList &propertyNames) +{ + const auto items = menu->items(); + for (QDBusPlatformMenuItem *item : items) { + QDBusMenuLayoutItem child; + child.populate(item, depth - 1, propertyNames); + m_children << child; + } +} + +void QDBusMenuLayoutItem::populate(const QDBusPlatformMenuItem *item, int depth, const QStringList &propertyNames) +{ + m_id = item->dbusID(); + QDBusMenuItem proxy(item); + m_properties = proxy.m_properties; + + const QDBusPlatformMenu *menu = static_cast<const QDBusPlatformMenu *>(item->menu()); + if (depth != 0 && menu) + populate(menu, depth, propertyNames); +} + +const QDBusArgument &operator<<(QDBusArgument &arg, const QDBusMenuLayoutItem &item) +{ + arg.beginStructure(); + arg << item.m_id << item.m_properties; + arg.beginArray(qMetaTypeId<QDBusVariant>()); + for (const QDBusMenuLayoutItem &child : item.m_children) + arg << QDBusVariant(QVariant::fromValue<QDBusMenuLayoutItem>(child)); + arg.endArray(); + arg.endStructure(); + return arg; +} + +const QDBusArgument &operator>>(const QDBusArgument &arg, QDBusMenuLayoutItem &item) +{ + arg.beginStructure(); + arg >> item.m_id >> item.m_properties; + arg.beginArray(); + while (!arg.atEnd()) { + QDBusVariant dbusVariant; + arg >> dbusVariant; + QDBusArgument childArgument = qvariant_cast<QDBusArgument>(dbusVariant.variant()); + + QDBusMenuLayoutItem child; + childArgument >> child; + item.m_children.append(child); + } + arg.endArray(); + arg.endStructure(); + return arg; +} + +void QDBusMenuItem::registerDBusTypes() +{ + qDBusRegisterMetaType<QDBusMenuItem>(); + qDBusRegisterMetaType<QDBusMenuItemList>(); + qDBusRegisterMetaType<QDBusMenuItemKeys>(); + qDBusRegisterMetaType<QDBusMenuItemKeysList>(); + qDBusRegisterMetaType<QDBusMenuLayoutItem>(); + qDBusRegisterMetaType<QDBusMenuLayoutItemList>(); + qDBusRegisterMetaType<QDBusMenuEvent>(); + qDBusRegisterMetaType<QDBusMenuEventList>(); + qDBusRegisterMetaType<QDBusMenuShortcut>(); +} + +QDBusMenuItem::QDBusMenuItem(const QDBusPlatformMenuItem *item) + : m_id(item->dbusID()) +{ + if (item->isSeparator()) { + m_properties.insert("type"_L1, "separator"_L1); + } else { + m_properties.insert("label"_L1, convertMnemonic(item->text())); + if (item->menu()) + m_properties.insert("children-display"_L1, "submenu"_L1); + m_properties.insert("enabled"_L1, item->isEnabled()); + if (item->isCheckable()) { + QString toggleType = item->hasExclusiveGroup() ? "radio"_L1 : "checkmark"_L1; + m_properties.insert("toggle-type"_L1, toggleType); + m_properties.insert("toggle-state"_L1, item->isChecked() ? 1 : 0); + } +#ifndef QT_NO_SHORTCUT + const QKeySequence &scut = item->shortcut(); + if (!scut.isEmpty()) { + QDBusMenuShortcut shortcut = convertKeySequence(scut); + m_properties.insert("shortcut"_L1, QVariant::fromValue(shortcut)); + } +#endif + const QIcon &icon = item->icon(); + if (!icon.name().isEmpty()) { + m_properties.insert("icon-name"_L1, icon.name()); + } else if (!icon.isNull()) { + QBuffer buf; + icon.pixmap(16).save(&buf, "PNG"); + m_properties.insert("icon-data"_L1, buf.data()); + } + } + m_properties.insert("visible"_L1, item->isVisible()); +} + +QDBusMenuItemList QDBusMenuItem::items(const QList<int> &ids, const QStringList &propertyNames) +{ + Q_UNUSED(propertyNames); + QDBusMenuItemList ret; + const QList<const QDBusPlatformMenuItem *> items = QDBusPlatformMenuItem::byIds(ids); + ret.reserve(items.size()); + for (const QDBusPlatformMenuItem *item : items) + ret << QDBusMenuItem(item); + return ret; +} + +QString QDBusMenuItem::convertMnemonic(const QString &label) +{ + // convert only the first occurrence of ampersand which is not at the end + // dbusmenu uses underscore instead of ampersand + int idx = label.indexOf(u'&'); + if (idx < 0 || idx == label.size() - 1) + return label; + QString ret(label); + ret[idx] = u'_'; + return ret; +} + +#ifndef QT_NO_SHORTCUT +QDBusMenuShortcut QDBusMenuItem::convertKeySequence(const QKeySequence &sequence) +{ + QDBusMenuShortcut shortcut; + for (int i = 0; i < sequence.count(); ++i) { + QStringList tokens; + auto modifiers = sequence[i].keyboardModifiers(); + if (modifiers & Qt::MetaModifier) + tokens << QStringLiteral("Super"); + if (modifiers & Qt::ControlModifier) + tokens << QStringLiteral("Control"); + if (modifiers & Qt::AltModifier) + tokens << QStringLiteral("Alt"); + if (modifiers & Qt::ShiftModifier) + tokens << QStringLiteral("Shift"); + if (modifiers & Qt::KeypadModifier) + tokens << QStringLiteral("Num"); + + QString keyName = QKeySequencePrivate::keyName(sequence[i].key(), QKeySequence::PortableText); + if (keyName == "+"_L1) + tokens << QStringLiteral("plus"); + else if (keyName == "-"_L1) + tokens << QStringLiteral("minus"); + else + tokens << keyName; + shortcut << tokens; + } + return shortcut; +} +#endif + +const QDBusArgument &operator<<(QDBusArgument &arg, const QDBusMenuEvent &ev) +{ + arg.beginStructure(); + arg << ev.m_id << ev.m_eventId << ev.m_data << ev.m_timestamp; + arg.endStructure(); + return arg; +} + +const QDBusArgument &operator>>(const QDBusArgument &arg, QDBusMenuEvent &ev) +{ + arg.beginStructure(); + arg >> ev.m_id >> ev.m_eventId >> ev.m_data >> ev.m_timestamp; + arg.endStructure(); + return arg; +} + +#ifndef QT_NO_DEBUG_STREAM +QDebug operator<<(QDebug d, const QDBusMenuItem &item) +{ + QDebugStateSaver saver(d); + d.nospace(); + d << "QDBusMenuItem(id=" << item.m_id << ", properties=" << item.m_properties << ')'; + return d; +} + +QDebug operator<<(QDebug d, const QDBusMenuLayoutItem &item) +{ + QDebugStateSaver saver(d); + d.nospace(); + d << "QDBusMenuLayoutItem(id=" << item.m_id << ", properties=" << item.m_properties << ", " << item.m_children.size() << " children)"; + return d; +} +#endif + +QT_END_NAMESPACE diff --git a/src/gui/platform/unix/dbusmenu/qdbusmenutypes_p.h b/src/gui/platform/unix/dbusmenu/qdbusmenutypes_p.h new file mode 100644 index 0000000000..4775c0094d --- /dev/null +++ b/src/gui/platform/unix/dbusmenu/qdbusmenutypes_p.h @@ -0,0 +1,120 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QDBUSMENUTYPES_H +#define QDBUSMENUTYPES_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of other Qt classes. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QObject> +#include <QString> +#include <QDBusArgument> +#include <QDBusConnection> +#include <QDBusObjectPath> +#include <QPixmap> +#include <private/qglobal_p.h> + +QT_BEGIN_NAMESPACE + +class QDBusPlatformMenu; +class QDBusPlatformMenuItem; +class QDBusMenuItem; +typedef QList<QDBusMenuItem> QDBusMenuItemList; +typedef QList<QStringList> QDBusMenuShortcut; + +class QDBusMenuItem +{ +public: + QDBusMenuItem() { } + QDBusMenuItem(const QDBusPlatformMenuItem *item); + + static QDBusMenuItemList items(const QList<int> &ids, const QStringList &propertyNames); + static QString convertMnemonic(const QString &label); +#ifndef QT_NO_SHORTCUT + static QDBusMenuShortcut convertKeySequence(const QKeySequence &sequence); +#endif + static void registerDBusTypes(); + + int m_id; + QVariantMap m_properties; +}; +Q_DECLARE_TYPEINFO(QDBusMenuItem, Q_RELOCATABLE_TYPE); + +const QDBusArgument &operator<<(QDBusArgument &arg, const QDBusMenuItem &item); +const QDBusArgument &operator>>(const QDBusArgument &arg, QDBusMenuItem &item); + +class QDBusMenuItemKeys +{ +public: + + int id; + QStringList properties; +}; +Q_DECLARE_TYPEINFO(QDBusMenuItemKeys, Q_RELOCATABLE_TYPE); + +const QDBusArgument &operator<<(QDBusArgument &arg, const QDBusMenuItemKeys &keys); +const QDBusArgument &operator>>(const QDBusArgument &arg, QDBusMenuItemKeys &keys); + +typedef QList<QDBusMenuItemKeys> QDBusMenuItemKeysList; + +class QDBusMenuLayoutItem +{ +public: + uint populate(int id, int depth, const QStringList &propertyNames, const QDBusPlatformMenu *topLevelMenu); + void populate(const QDBusPlatformMenu *menu, int depth, const QStringList &propertyNames); + void populate(const QDBusPlatformMenuItem *item, int depth, const QStringList &propertyNames); + + int m_id; + QVariantMap m_properties; + QList<QDBusMenuLayoutItem> m_children; +}; +Q_DECLARE_TYPEINFO(QDBusMenuLayoutItem, Q_RELOCATABLE_TYPE); + +const QDBusArgument &operator<<(QDBusArgument &arg, const QDBusMenuLayoutItem &); +const QDBusArgument &operator>>(const QDBusArgument &arg, QDBusMenuLayoutItem &item); + +typedef QList<QDBusMenuLayoutItem> QDBusMenuLayoutItemList; + +class QDBusMenuEvent +{ +public: + int m_id; + QString m_eventId; + QDBusVariant m_data; + uint m_timestamp; +}; +Q_DECLARE_TYPEINFO(QDBusMenuEvent, Q_RELOCATABLE_TYPE); // QDBusVariant is movable, even though it cannot + // be marked as such until Qt 6. + +const QDBusArgument &operator<<(QDBusArgument &arg, const QDBusMenuEvent &ev); +const QDBusArgument &operator>>(const QDBusArgument &arg, QDBusMenuEvent &ev); + +typedef QList<QDBusMenuEvent> QDBusMenuEventList; + +#ifndef QT_NO_DEBUG_STREAM +QDebug operator<<(QDebug d, const QDBusMenuItem &item); +QDebug operator<<(QDebug d, const QDBusMenuLayoutItem &item); +#endif + +QT_END_NAMESPACE + +QT_DECL_METATYPE_EXTERN(QDBusMenuItem, Q_GUI_EXPORT) +QT_DECL_METATYPE_EXTERN(QDBusMenuItemList, Q_GUI_EXPORT) +QT_DECL_METATYPE_EXTERN(QDBusMenuItemKeys, Q_GUI_EXPORT) +QT_DECL_METATYPE_EXTERN(QDBusMenuItemKeysList, Q_GUI_EXPORT) +QT_DECL_METATYPE_EXTERN(QDBusMenuLayoutItem, Q_GUI_EXPORT) +QT_DECL_METATYPE_EXTERN(QDBusMenuLayoutItemList, Q_GUI_EXPORT) +QT_DECL_METATYPE_EXTERN(QDBusMenuEvent, Q_GUI_EXPORT) +QT_DECL_METATYPE_EXTERN(QDBusMenuEventList, Q_GUI_EXPORT) +QT_DECL_METATYPE_EXTERN(QDBusMenuShortcut, Q_GUI_EXPORT) + +#endif diff --git a/src/gui/platform/unix/dbusmenu/qdbusplatformmenu.cpp b/src/gui/platform/unix/dbusmenu/qdbusplatformmenu.cpp new file mode 100644 index 0000000000..f25fb0831b --- /dev/null +++ b/src/gui/platform/unix/dbusmenu/qdbusplatformmenu.cpp @@ -0,0 +1,274 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qdbusplatformmenu_p.h" + +#include <QDateTime> +#include <QDebug> +#include <QWindow> + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(qLcMenu, "qt.qpa.menu") + +static int nextDBusID = 1; +QHash<int, QDBusPlatformMenuItem *> menuItemsByID; + +QDBusPlatformMenuItem::QDBusPlatformMenuItem() + : m_subMenu(nullptr) + , m_role(NoRole) + , m_isEnabled(true) + , m_isVisible(true) + , m_isSeparator(false) + , m_isCheckable(false) + , m_isChecked(false) + , m_hasExclusiveGroup(false) + , m_dbusID(nextDBusID++) +{ + menuItemsByID.insert(m_dbusID, this); +} + +QDBusPlatformMenuItem::~QDBusPlatformMenuItem() +{ + menuItemsByID.remove(m_dbusID); + if (m_subMenu) + static_cast<QDBusPlatformMenu *>(m_subMenu)->setContainingMenuItem(nullptr); +} + +void QDBusPlatformMenuItem::setText(const QString &text) +{ + qCDebug(qLcMenu) << m_dbusID << text; + m_text = text; +} + +void QDBusPlatformMenuItem::setIcon(const QIcon &icon) +{ + m_icon = icon; +} + +/*! + Set a submenu under this menu item. +*/ +void QDBusPlatformMenuItem::setMenu(QPlatformMenu *menu) +{ + if (m_subMenu) + static_cast<QDBusPlatformMenu *>(m_subMenu)->setContainingMenuItem(nullptr); + m_subMenu = menu; + if (menu) + static_cast<QDBusPlatformMenu *>(menu)->setContainingMenuItem(this); +} + +void QDBusPlatformMenuItem::setEnabled(bool enabled) +{ + m_isEnabled = enabled; +} + +void QDBusPlatformMenuItem::setVisible(bool isVisible) +{ + m_isVisible = isVisible; +} + +void QDBusPlatformMenuItem::setIsSeparator(bool isSeparator) +{ + m_isSeparator = isSeparator; +} + +void QDBusPlatformMenuItem::setRole(QPlatformMenuItem::MenuRole role) +{ + m_role = role; +} + +void QDBusPlatformMenuItem::setCheckable(bool checkable) +{ + m_isCheckable = checkable; +} + +void QDBusPlatformMenuItem::setChecked(bool isChecked) +{ + m_isChecked = isChecked; +} + +void QDBusPlatformMenuItem::setHasExclusiveGroup(bool hasExclusiveGroup) +{ + m_hasExclusiveGroup = hasExclusiveGroup; +} + +#ifndef QT_NO_SHORTCUT +void QDBusPlatformMenuItem::setShortcut(const QKeySequence &shortcut) +{ + m_shortcut = shortcut; +} +#endif + +void QDBusPlatformMenuItem::trigger() +{ + emit activated(); +} + +QDBusPlatformMenuItem *QDBusPlatformMenuItem::byId(int id) +{ + // We need to check contains because otherwise QHash would insert + // a default-constructed nullptr value into menuItemsByID + if (menuItemsByID.contains(id)) + return menuItemsByID[id]; + return nullptr; +} + +QList<const QDBusPlatformMenuItem *> QDBusPlatformMenuItem::byIds(const QList<int> &ids) +{ + QList<const QDBusPlatformMenuItem *> ret; + for (int id : ids) { + if (menuItemsByID.contains(id)) + ret << menuItemsByID[id]; + } + return ret; +} + + +QDBusPlatformMenu::QDBusPlatformMenu() + : m_isEnabled(true) + , m_isVisible(true) + , m_revision(1) + , m_containingMenuItem(nullptr) +{ +} + +QDBusPlatformMenu::~QDBusPlatformMenu() +{ + if (m_containingMenuItem) + m_containingMenuItem->setMenu(nullptr); +} + +void QDBusPlatformMenu::insertMenuItem(QPlatformMenuItem *menuItem, QPlatformMenuItem *before) +{ + QDBusPlatformMenuItem *item = static_cast<QDBusPlatformMenuItem *>(menuItem); + QDBusPlatformMenuItem *beforeItem = static_cast<QDBusPlatformMenuItem *>(before); + int idx = m_items.indexOf(beforeItem); + qCDebug(qLcMenu) << item->dbusID() << item->text(); + if (idx < 0) + m_items.append(item); + else + m_items.insert(idx, item); + m_itemsByTag.insert(item->tag(), item); + if (item->menu()) + syncSubMenu(static_cast<const QDBusPlatformMenu *>(item->menu())); + emitUpdated(); +} + +void QDBusPlatformMenu::removeMenuItem(QPlatformMenuItem *menuItem) +{ + QDBusPlatformMenuItem *item = static_cast<QDBusPlatformMenuItem *>(menuItem); + m_items.removeAll(item); + m_itemsByTag.remove(menuItem->tag()); + if (item->menu()) { + // disconnect from the signals we connected to in syncSubMenu() + const QDBusPlatformMenu *menu = static_cast<const QDBusPlatformMenu *>(item->menu()); + disconnect(menu, &QDBusPlatformMenu::propertiesUpdated, + this, &QDBusPlatformMenu::propertiesUpdated); + disconnect(menu, &QDBusPlatformMenu::updated, + this, &QDBusPlatformMenu::updated); + disconnect(menu, &QDBusPlatformMenu::popupRequested, + this, &QDBusPlatformMenu::popupRequested); + } + emitUpdated(); +} + +void QDBusPlatformMenu::syncSubMenu(const QDBusPlatformMenu *menu) +{ + // The adaptor is only connected to the propertiesUpdated signal of the top-level + // menu, so the submenus should transfer their signals to their parents. + connect(menu, &QDBusPlatformMenu::propertiesUpdated, + this, &QDBusPlatformMenu::propertiesUpdated, Qt::UniqueConnection); + connect(menu, &QDBusPlatformMenu::updated, + this, &QDBusPlatformMenu::updated, Qt::UniqueConnection); + connect(menu, &QDBusPlatformMenu::popupRequested, + this, &QDBusPlatformMenu::popupRequested, Qt::UniqueConnection); +} + +void QDBusPlatformMenu::syncMenuItem(QPlatformMenuItem *menuItem) +{ + QDBusPlatformMenuItem *item = static_cast<QDBusPlatformMenuItem *>(menuItem); + // if a submenu was added to this item, we need to connect to its signals + if (item->menu()) + syncSubMenu(static_cast<const QDBusPlatformMenu *>(item->menu())); + // TODO keep around copies of the QDBusMenuLayoutItems so they can be updated? + // or eliminate them by putting dbus streaming operators in this class instead? + // or somehow tell the dbusmenu client that something has changed, so it will ask for properties again + QDBusMenuItemList updated; + QDBusMenuItemKeysList removed; + updated << QDBusMenuItem(item); + qCDebug(qLcMenu) << updated; + emit propertiesUpdated(updated, removed); +} + +void QDBusPlatformMenu::emitUpdated() +{ + if (m_containingMenuItem) + emit updated(++m_revision, m_containingMenuItem->dbusID()); + else + emit updated(++m_revision, 0); +} + +void QDBusPlatformMenu::setText(const QString &text) +{ + m_text = text; +} + +void QDBusPlatformMenu::setIcon(const QIcon &icon) +{ + m_icon = icon; +} + +void QDBusPlatformMenu::setEnabled(bool enabled) +{ + m_isEnabled = enabled; +} + +void QDBusPlatformMenu::setVisible(bool isVisible) +{ + m_isVisible = isVisible; +} + +void QDBusPlatformMenu::setContainingMenuItem(QDBusPlatformMenuItem *item) +{ + m_containingMenuItem = item; +} + +void QDBusPlatformMenu::showPopup(const QWindow *parentWindow, const QRect &targetRect, const QPlatformMenuItem *item) +{ + Q_UNUSED(parentWindow); + Q_UNUSED(targetRect); + Q_UNUSED(item); + setVisible(true); + emit popupRequested(m_containingMenuItem->dbusID(), QDateTime::currentMSecsSinceEpoch()); +} + +QPlatformMenuItem *QDBusPlatformMenu::menuItemAt(int position) const +{ + return m_items.value(position); +} + +QPlatformMenuItem *QDBusPlatformMenu::menuItemForTag(quintptr tag) const +{ + return m_itemsByTag[tag]; +} + +const QList<QDBusPlatformMenuItem *> QDBusPlatformMenu::items() const +{ + return m_items; +} + +QPlatformMenuItem *QDBusPlatformMenu::createMenuItem() const +{ + QDBusPlatformMenuItem *ret = new QDBusPlatformMenuItem(); + return ret; +} + +QPlatformMenu *QDBusPlatformMenu::createSubMenu() const +{ + return new QDBusPlatformMenu; +} + +QT_END_NAMESPACE + +#include "moc_qdbusplatformmenu_p.cpp" diff --git a/src/gui/platform/unix/dbusmenu/qdbusplatformmenu_p.h b/src/gui/platform/unix/dbusmenu/qdbusplatformmenu_p.h new file mode 100644 index 0000000000..cb672fee48 --- /dev/null +++ b/src/gui/platform/unix/dbusmenu/qdbusplatformmenu_p.h @@ -0,0 +1,155 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QDBUSPLATFORMMENU_H +#define QDBUSPLATFORMMENU_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// +// +// W A R N I N G +// ------------- +// +// This file is part of the DBus menu support and is not meant to be used +// in applications. Usage of this API may make your code +// source and binary incompatible with future versions of Qt. +// + +#include <qpa/qplatformmenu.h> +#include <QLoggingCategory> +#include "qdbusmenutypes_p.h" + +QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(qLcMenu) + +class QDBusPlatformMenu; + +class QDBusPlatformMenuItem : public QPlatformMenuItem +{ + Q_OBJECT + +public: + QDBusPlatformMenuItem(); + ~QDBusPlatformMenuItem(); + + const QString text() const { return m_text; } + void setText(const QString &text) override; + QIcon icon() const { return m_icon; } + void setIcon(const QIcon &icon) override; + const QPlatformMenu *menu() const { return m_subMenu; } + void setMenu(QPlatformMenu *menu) override; + bool isEnabled() const { return m_isEnabled; } + void setEnabled(bool enabled) override; + bool isVisible() const { return m_isVisible; } + void setVisible(bool isVisible) override; + bool isSeparator() const { return m_isSeparator; } + void setIsSeparator(bool isSeparator) override; + void setFont(const QFont &font) override { Q_UNUSED(font); } + void setRole(MenuRole role) override; + bool isCheckable() const { return m_isCheckable; } + void setCheckable(bool checkable) override; + bool isChecked() const { return m_isChecked; } + void setChecked(bool isChecked) override; + bool hasExclusiveGroup() const { return m_hasExclusiveGroup; } + void setHasExclusiveGroup(bool hasExclusiveGroup) override; +#if QT_CONFIG(shortcut) + QKeySequence shortcut() const { return m_shortcut; } + void setShortcut(const QKeySequence& shortcut) override; +#endif + void setIconSize(int size) override { Q_UNUSED(size); } + void setNativeContents(WId item) override { Q_UNUSED(item); } + + int dbusID() const { return m_dbusID; } + + void trigger(); + + static QDBusPlatformMenuItem *byId(int id); + static QList<const QDBusPlatformMenuItem *> byIds(const QList<int> &ids); + +private: + QString m_text; + QIcon m_icon; + QPlatformMenu *m_subMenu; + MenuRole m_role : 4; + bool m_isEnabled : 1; + bool m_isVisible : 1; + bool m_isSeparator : 1; + bool m_isCheckable : 1; + bool m_isChecked : 1; + bool m_hasExclusiveGroup : 1; + short /*unused*/ : 6; + short m_dbusID : 16; +#if QT_CONFIG(shortcut) + QKeySequence m_shortcut; +#endif +}; + +class QDBusPlatformMenu : public QPlatformMenu +{ + Q_OBJECT + +public: + QDBusPlatformMenu(); + ~QDBusPlatformMenu(); + void insertMenuItem(QPlatformMenuItem *menuItem, QPlatformMenuItem *before) override; + void removeMenuItem(QPlatformMenuItem *menuItem) override; + void syncSubMenu(const QDBusPlatformMenu *menu); + void syncMenuItem(QPlatformMenuItem *menuItem) override; + void syncSeparatorsCollapsible(bool enable) override { Q_UNUSED(enable); } + + const QString text() const { return m_text; } + void setText(const QString &text) override; + QIcon icon() const { return m_icon; } + void setIcon(const QIcon &icon) override; + bool isEnabled() const override { return m_isEnabled; } + void setEnabled(bool enabled) override; + bool isVisible() const { return m_isVisible; } + void setVisible(bool visible) override; + void setMinimumWidth(int width) override { Q_UNUSED(width); } + void setFont(const QFont &font) override { Q_UNUSED(font); } + void setMenuType(MenuType type) override { Q_UNUSED(type); } + void setContainingMenuItem(QDBusPlatformMenuItem *item); + + void showPopup(const QWindow *parentWindow, const QRect &targetRect, const QPlatformMenuItem *item) override; + + void dismiss() override { } // Closes this and all its related menu popups + + QPlatformMenuItem *menuItemAt(int position) const override; + QPlatformMenuItem *menuItemForTag(quintptr tag) const override; + const QList<QDBusPlatformMenuItem *> items() const; + + QPlatformMenuItem *createMenuItem() const override; + QPlatformMenu *createSubMenu() const override; + + uint revision() const { return m_revision; } + + void emitUpdated(); + +signals: + void updated(uint revision, int dbusId); + void propertiesUpdated(QDBusMenuItemList updatedProps, QDBusMenuItemKeysList removedProps); + void popupRequested(int id, uint timestamp); + +private: + QString m_text; + QIcon m_icon; + bool m_isEnabled; + bool m_isVisible; + uint m_revision; + QHash<quintptr, QDBusPlatformMenuItem *> m_itemsByTag; + QList<QDBusPlatformMenuItem *> m_items; + QDBusPlatformMenuItem *m_containingMenuItem; +}; + +QT_END_NAMESPACE + +#endif + diff --git a/src/gui/platform/unix/dbustray/qdbustrayicon.cpp b/src/gui/platform/unix/dbustray/qdbustrayicon.cpp new file mode 100644 index 0000000000..0dff9b598e --- /dev/null +++ b/src/gui/platform/unix/dbustray/qdbustrayicon.cpp @@ -0,0 +1,347 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qdbustrayicon_p.h" + +#ifndef QT_NO_SYSTEMTRAYICON + +#include <QString> +#include <QDebug> +#include <QRect> +#include <QLoggingCategory> +#include <QStandardPaths> +#include <QFileInfo> +#include <QDir> +#include <QMetaObject> +#include <QMetaEnum> +#include <QDBusConnectionInterface> +#include <QDBusArgument> +#include <QDBusMetaType> +#include <QDBusServiceWatcher> + +#include <qpa/qplatformmenu.h> +#include <qpa/qplatformintegration.h> +#include <qpa/qplatformservices.h> + +#include <private/qdbusmenuconnection_p.h> +#include <private/qstatusnotifieritemadaptor_p.h> +#include <private/qdbusmenuadaptor_p.h> +#include <private/qdbusplatformmenu_p.h> +#include <private/qxdgnotificationproxy_p.h> +#include <private/qlockfile_p.h> +#include <private/qguiapplication_p.h> + +// Defined in Windows headers which get included by qlockfile_p.h +#undef interface + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +Q_LOGGING_CATEGORY(qLcTray, "qt.qpa.tray") + +static QString iconTempPath() +{ + QString tempPath = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation); + if (!tempPath.isEmpty()) { + QString flatpakId = qEnvironmentVariable("FLATPAK_ID"); + if (!flatpakId.isEmpty() && QFileInfo::exists("/.flatpak-info"_L1)) + tempPath += "/app/"_L1 + flatpakId; + return tempPath; + } + + tempPath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation); + + if (!tempPath.isEmpty()) { + QDir tempDir(tempPath); + if (tempDir.exists()) + return tempPath; + + if (tempDir.mkpath(QStringLiteral("."))) { + const QFile::Permissions permissions = QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner; + if (QFile(tempPath).setPermissions(permissions)) + return tempPath; + } + } + + return QDir::tempPath(); +} + +static const QString KDEItemFormat = QStringLiteral("org.kde.StatusNotifierItem-%1-%2"); +static const QString KDEWatcherService = QStringLiteral("org.kde.StatusNotifierWatcher"); +static const QString XdgNotificationService = QStringLiteral("org.freedesktop.Notifications"); +static const QString XdgNotificationPath = QStringLiteral("/org/freedesktop/Notifications"); +static const QString DefaultAction = QStringLiteral("default"); +static int instanceCount = 0; + +static inline QString tempFileTemplate() +{ + static const QString TempFileTemplate = iconTempPath() + "/qt-trayicon-XXXXXX.png"_L1; + return TempFileTemplate; +} + +/*! + \class QDBusTrayIcon + \internal +*/ + +QDBusTrayIcon::QDBusTrayIcon() + : m_dbusConnection(nullptr) + , m_adaptor(new QStatusNotifierItemAdaptor(this)) + , m_menuAdaptor(nullptr) + , m_menu(nullptr) + , m_notifier(nullptr) + , m_instanceId(KDEItemFormat.arg(QCoreApplication::applicationPid()).arg(++instanceCount)) + , m_category(QStringLiteral("ApplicationStatus")) + , m_defaultStatus(QStringLiteral("Active")) // be visible all the time. QSystemTrayIcon has no API to control this. + , m_status(m_defaultStatus) + , m_tempIcon(nullptr) + , m_tempAttentionIcon(nullptr) + , m_registered(false) +{ + qCDebug(qLcTray); + if (instanceCount == 1) { + QDBusMenuItem::registerDBusTypes(); + qDBusRegisterMetaType<QXdgDBusImageStruct>(); + qDBusRegisterMetaType<QXdgDBusImageVector>(); + qDBusRegisterMetaType<QXdgDBusToolTipStruct>(); + } + connect(this, SIGNAL(statusChanged(QString)), m_adaptor, SIGNAL(NewStatus(QString))); + connect(this, SIGNAL(tooltipChanged()), m_adaptor, SIGNAL(NewToolTip())); + connect(this, SIGNAL(iconChanged()), m_adaptor, SIGNAL(NewIcon())); + connect(this, SIGNAL(attention()), m_adaptor, SIGNAL(NewAttentionIcon())); + connect(this, SIGNAL(menuChanged()), m_adaptor, SIGNAL(NewMenu())); + connect(this, SIGNAL(attention()), m_adaptor, SIGNAL(NewTitle())); + connect(&m_attentionTimer, SIGNAL(timeout()), this, SLOT(attentionTimerExpired())); + m_attentionTimer.setSingleShot(true); +} + +QDBusTrayIcon::~QDBusTrayIcon() +{ +} + +void QDBusTrayIcon::init() +{ + qCDebug(qLcTray) << "registering" << m_instanceId; + m_registered = dBusConnection()->registerTrayIcon(this); + QObject::connect(dBusConnection()->dbusWatcher(), &QDBusServiceWatcher::serviceRegistered, + this, &QDBusTrayIcon::watcherServiceRegistered); +} + +void QDBusTrayIcon::cleanup() +{ + qCDebug(qLcTray) << "unregistering" << m_instanceId; + if (m_registered) + dBusConnection()->unregisterTrayIcon(this); + delete m_dbusConnection; + m_dbusConnection = nullptr; + delete m_notifier; + m_notifier = nullptr; + m_registered = false; +} + +void QDBusTrayIcon::watcherServiceRegistered(const QString &serviceName) +{ + Q_UNUSED(serviceName); + // We have the icon registered, but the watcher has restarted or + // changed, so we need to tell it about our icon again + if (m_registered) + dBusConnection()->registerTrayIconWithWatcher(this); +} + +void QDBusTrayIcon::attentionTimerExpired() +{ + m_messageTitle = QString(); + m_message = QString(); + m_attentionIcon = QIcon(); + emit attention(); + emit tooltipChanged(); + setStatus(m_defaultStatus); +} + +void QDBusTrayIcon::setStatus(const QString &status) +{ + qCDebug(qLcTray) << status; + if (m_status == status) + return; + m_status = status; + emit statusChanged(m_status); +} + +QTemporaryFile *QDBusTrayIcon::tempIcon(const QIcon &icon) +{ + // Hack for indicator-application, which doesn't handle icons sent across D-Bus: + // save the icon to a temp file and set the icon name to that filename. + static bool necessity_checked = false; + static bool necessary = false; + if (!necessity_checked) { + QDBusConnection session = QDBusConnection::sessionBus(); + uint pid = session.interface()->servicePid(KDEWatcherService).value(); + QString processName = QLockFilePrivate::processNameByPid(pid); + necessary = processName.endsWith("indicator-application-service"_L1); + if (!necessary) { + necessary = session.interface()->isServiceRegistered( + QStringLiteral("com.canonical.indicator.application")); + } + if (!necessary) { + necessary = session.interface()->isServiceRegistered( + QStringLiteral("org.ayatana.indicator.application")); + } + if (!necessary && QGuiApplication::desktopSettingsAware()) { + // Accessing to process name might be not allowed if the application + // is confined, thus we can just rely on the current desktop in use + const QPlatformServices *services = QGuiApplicationPrivate::platformIntegration()->services(); + necessary = services->desktopEnvironment().split(':').contains("UNITY"); + } + necessity_checked = true; + } + if (!necessary) + return nullptr; + QTemporaryFile *ret = new QTemporaryFile(tempFileTemplate(), this); + if (!ret->open()) { + delete ret; + return nullptr; + } + icon.pixmap(QSize(22, 22)).save(ret); + ret->close(); + return ret; +} + +QDBusMenuConnection * QDBusTrayIcon::dBusConnection() +{ + if (!m_dbusConnection) { + m_dbusConnection = new QDBusMenuConnection(this, m_instanceId); + m_notifier = new QXdgNotificationInterface(XdgNotificationService, + XdgNotificationPath, m_dbusConnection->connection(), this); + connect(m_notifier, SIGNAL(NotificationClosed(uint,uint)), this, SLOT(notificationClosed(uint,uint))); + connect(m_notifier, SIGNAL(ActionInvoked(uint,QString)), this, SLOT(actionInvoked(uint,QString))); + } + return m_dbusConnection; +} + +void QDBusTrayIcon::updateIcon(const QIcon &icon) +{ + m_iconName = icon.name(); + m_icon = icon; + if (m_iconName.isEmpty()) { + if (m_tempIcon) + delete m_tempIcon; + m_tempIcon = tempIcon(icon); + if (m_tempIcon) + m_iconName = m_tempIcon->fileName(); + } + qCDebug(qLcTray) << m_iconName << icon.availableSizes(); + emit iconChanged(); +} + +void QDBusTrayIcon::updateToolTip(const QString &tooltip) +{ + qCDebug(qLcTray) << tooltip; + m_tooltip = tooltip; + emit tooltipChanged(); +} + +QPlatformMenu *QDBusTrayIcon::createMenu() const +{ + return new QDBusPlatformMenu(); +} + +void QDBusTrayIcon::updateMenu(QPlatformMenu * menu) +{ + qCDebug(qLcTray) << menu; + QDBusPlatformMenu *newMenu = qobject_cast<QDBusPlatformMenu *>(menu); + if (m_menu != newMenu) { + if (m_menu) { + dBusConnection()->unregisterTrayIconMenu(this); + delete m_menuAdaptor; + } + m_menu = newMenu; + m_menuAdaptor = new QDBusMenuAdaptor(m_menu); + // TODO connect(m_menu, , m_menuAdaptor, SIGNAL(ItemActivationRequested(int,uint))); + connect(m_menu, SIGNAL(propertiesUpdated(QDBusMenuItemList,QDBusMenuItemKeysList)), + m_menuAdaptor, SIGNAL(ItemsPropertiesUpdated(QDBusMenuItemList,QDBusMenuItemKeysList))); + connect(m_menu, SIGNAL(updated(uint,int)), + m_menuAdaptor, SIGNAL(LayoutUpdated(uint,int))); + dBusConnection()->registerTrayIconMenu(this); + emit menuChanged(); + } +} + +void QDBusTrayIcon::showMessage(const QString &title, const QString &msg, const QIcon &icon, + QPlatformSystemTrayIcon::MessageIcon iconType, int msecs) +{ + m_messageTitle = title; + m_message = msg; + m_attentionIcon = icon; + QStringList notificationActions; + switch (iconType) { + case Information: + m_attentionIconName = QStringLiteral("dialog-information"); + break; + case Warning: + m_attentionIconName = QStringLiteral("dialog-warning"); + break; + case Critical: + m_attentionIconName = QStringLiteral("dialog-error"); + // If there are actions, the desktop notification may appear as a message dialog + // with button(s), which will interrupt the user and require a response. + // That is an optional feature in implementations of org.freedesktop.Notifications + notificationActions << DefaultAction << tr("OK"); + break; + default: + m_attentionIconName.clear(); + break; + } + if (m_attentionIconName.isEmpty()) { + if (m_tempAttentionIcon) + delete m_tempAttentionIcon; + m_tempAttentionIcon = tempIcon(icon); + if (m_tempAttentionIcon) + m_attentionIconName = m_tempAttentionIcon->fileName(); + } + qCDebug(qLcTray) << title << msg << + QPlatformSystemTrayIcon::metaObject()->enumerator( + QPlatformSystemTrayIcon::staticMetaObject.indexOfEnumerator("MessageIcon")).valueToKey(iconType) + << m_attentionIconName << msecs; + setStatus(QStringLiteral("NeedsAttention")); + m_attentionTimer.start(msecs); + emit tooltipChanged(); + emit attention(); + + // Desktop notification + QVariantMap hints; + // urgency levels according to https://developer.gnome.org/notification-spec/#urgency-levels + // 0 low, 1 normal, 2 critical + int urgency = static_cast<int>(iconType) - 1; + if (urgency < 0) // no icon + urgency = 0; + hints.insert("urgency"_L1, QVariant(urgency)); + m_notifier->notify(QCoreApplication::applicationName(), 0, + m_attentionIconName, title, msg, notificationActions, hints, msecs); +} + +void QDBusTrayIcon::actionInvoked(uint id, const QString &action) +{ + qCDebug(qLcTray) << id << action; + emit messageClicked(); +} + +void QDBusTrayIcon::notificationClosed(uint id, uint reason) +{ + qCDebug(qLcTray) << id << reason; +} + +bool QDBusTrayIcon::isSystemTrayAvailable() const +{ + QDBusMenuConnection * conn = const_cast<QDBusTrayIcon *>(this)->dBusConnection(); + + // If the KDE watcher service is registered, we must be on a desktop + // where a StatusNotifier-conforming system tray exists. + qCDebug(qLcTray) << conn->isWatcherRegistered(); + return conn->isWatcherRegistered(); +} + +QT_END_NAMESPACE + +#include "moc_qdbustrayicon_p.cpp" +#endif //QT_NO_SYSTEMTRAYICON diff --git a/src/gui/platform/unix/dbustray/qdbustrayicon_p.h b/src/gui/platform/unix/dbustray/qdbustrayicon_p.h new file mode 100644 index 0000000000..3041e132fd --- /dev/null +++ b/src/gui/platform/unix/dbustray/qdbustrayicon_p.h @@ -0,0 +1,132 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + + +#ifndef QDBUSTRAYICON_H +#define QDBUSTRAYICON_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtGui/private/qtguiglobal_p.h> + +QT_REQUIRE_CONFIG(systemtrayicon); + +#include <QIcon> +#include <QTemporaryFile> +#include <QTimer> +#include "QtGui/qpa/qplatformsystemtrayicon.h" +#include "private/qdbusmenuconnection_p.h" + +QT_BEGIN_NAMESPACE + +class QStatusNotifierItemAdaptor; +class QDBusMenuAdaptor; +class QDBusPlatformMenu; +class QXdgNotificationInterface; + +class QDBusTrayIcon: public QPlatformSystemTrayIcon +{ + Q_OBJECT + Q_PROPERTY(QString category READ category NOTIFY categoryChanged) + Q_PROPERTY(QString status READ status NOTIFY statusChanged) + Q_PROPERTY(QString tooltip READ tooltip NOTIFY tooltipChanged) + Q_PROPERTY(QString iconName READ iconName NOTIFY iconChanged) + Q_PROPERTY(QIcon icon READ icon NOTIFY iconChanged) + Q_PROPERTY(bool isRequestingAttention READ isRequestingAttention NOTIFY attention) + Q_PROPERTY(QString attentionTitle READ attentionTitle NOTIFY attention) + Q_PROPERTY(QString attentionMessage READ attentionMessage NOTIFY attention) + Q_PROPERTY(QString attentionIconName READ attentionIconName NOTIFY attention) + Q_PROPERTY(QIcon attentionIcon READ attentionIcon NOTIFY attention) + Q_PROPERTY(QDBusPlatformMenu *menu READ menu NOTIFY menuChanged) + Q_MOC_INCLUDE(<private/qdbusplatformmenu_p.h>) + +public: + QDBusTrayIcon(); + + virtual ~QDBusTrayIcon(); + + QDBusMenuConnection * dBusConnection(); + + void init() override; + void cleanup() override; + void updateIcon(const QIcon &icon) override; + void updateToolTip(const QString &tooltip) override; + void updateMenu(QPlatformMenu *menu) override; + QPlatformMenu *createMenu() const override; + void showMessage(const QString &title, const QString &msg, + const QIcon &icon, MessageIcon iconType, int msecs) override; + + bool isSystemTrayAvailable() const override; + bool supportsMessages() const override { return true; } + QRect geometry() const override { return QRect(); } + + QString category() const { return m_category; } + QString status() const { return m_status; } + QString tooltip() const { return m_tooltip; } + + QString iconName() const { return m_iconName; } + const QIcon & icon() const { return m_icon; } + + bool isRequestingAttention() const { return m_attentionTimer.isActive(); } + QString attentionTitle() const { return m_messageTitle; } + QString attentionMessage() const { return m_message; } + QString attentionIconName() const { return m_attentionIconName; } + const QIcon & attentionIcon() const { return m_attentionIcon; } + + QString instanceId() const { return m_instanceId; } + + QDBusPlatformMenu *menu() { return m_menu; } + +signals: + void categoryChanged(); + void statusChanged(QString arg); + void tooltipChanged(); + void iconChanged(); + void attention(); + void menuChanged(); + +private Q_SLOTS: + void attentionTimerExpired(); + void actionInvoked(uint id, const QString &action); + void notificationClosed(uint id, uint reason); + void watcherServiceRegistered(const QString &serviceName); + +private: + void setStatus(const QString &status); + QTemporaryFile *tempIcon(const QIcon &icon); + +private: + QDBusMenuConnection* m_dbusConnection; + QStatusNotifierItemAdaptor *m_adaptor; + QDBusMenuAdaptor *m_menuAdaptor; + QDBusPlatformMenu *m_menu; + QXdgNotificationInterface *m_notifier; + QString m_instanceId; + QString m_category; + QString m_defaultStatus; + QString m_status; + QString m_tooltip; + QString m_messageTitle; + QString m_message; + QIcon m_icon; + QTemporaryFile *m_tempIcon; + QString m_iconName; + QIcon m_attentionIcon; + QTemporaryFile *m_tempAttentionIcon; + QString m_attentionIconName; + QTimer m_attentionTimer; + bool m_registered; +}; + +QT_END_NAMESPACE + +#endif // QDBUSTRAYICON_H diff --git a/src/gui/platform/unix/dbustray/qdbustraytypes.cpp b/src/gui/platform/unix/dbustray/qdbustraytypes.cpp new file mode 100644 index 0000000000..accbd87e7e --- /dev/null +++ b/src/gui/platform/unix/dbustray/qdbustraytypes.cpp @@ -0,0 +1,181 @@ +// Copyright (C) 2009 Marco Martin <notmart@gmail.com> +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QT_NO_SYSTEMTRAYICON + +#include "qdbustraytypes_p.h" + +#include <QDBusConnection> +#include <QDBusMetaType> +#include <QImage> +#include <QIcon> +#include <QIconEngine> +#include <QImage> +#include <QPixmap> +#include <QDebug> +#include <QtEndian> +#include <QPainter> +#include <QGuiApplication> +#include <qpa/qplatformmenu.h> +#include <private/qdbusplatformmenu_p.h> +#include <private/qicon_p.h> + +QT_BEGIN_NAMESPACE + +QT_IMPL_METATYPE_EXTERN(QXdgDBusImageStruct) +QT_IMPL_METATYPE_EXTERN(QXdgDBusImageVector) +QT_IMPL_METATYPE_EXTERN(QXdgDBusToolTipStruct) + +static const int IconSizeLimit = 64; +static const int IconNormalSmallSize = 22; +static const int IconNormalMediumSize = 64; + +QXdgDBusImageVector iconToQXdgDBusImageVector(const QIcon &icon) +{ + QXdgDBusImageVector ret; + if (icon.isNull()) + return ret; + QIconEngine *engine = const_cast<QIcon &>(icon).data_ptr()->engine; + QList<QSize> sizes = engine->availableSizes(QIcon::Normal, QIcon::Off); + + // Omit any size larger than 64 px, to save D-Bus bandwidth; + // ensure that 22px or smaller exists, because it's a common size; + // and ensure that something between 22px and 64px exists, for better scaling to other sizes. + bool hasSmallIcon = false; + bool hasMediumIcon = false; + QList<QSize> toRemove; + for (const QSize &size : std::as_const(sizes)) { + int maxSize = qMax(size.width(), size.height()); + if (maxSize <= IconNormalSmallSize) + hasSmallIcon = true; + else if (maxSize <= IconNormalMediumSize) + hasMediumIcon = true; + else if (maxSize > IconSizeLimit) + toRemove << size; + } + for (const QSize &size : std::as_const(toRemove)) + sizes.removeOne(size); + if (!hasSmallIcon) + sizes.append(QSize(IconNormalSmallSize, IconNormalSmallSize)); + if (!hasMediumIcon) + sizes.append(QSize(IconNormalMediumSize, IconNormalMediumSize)); + + ret.reserve(sizes.size()); + for (const QSize &size : std::as_const(sizes)) { + // Protocol specifies ARGB32 format in network byte order + QImage im = engine->pixmap(size, QIcon::Normal, QIcon::Off).toImage().convertToFormat(QImage::Format_ARGB32); + // letterbox if necessary to make it square + if (im.height() != im.width()) { + int maxSize = qMax(im.width(), im.height()); + QImage padded(maxSize, maxSize, QImage::Format_ARGB32); + padded.fill(Qt::transparent); + QPainter painter(&padded); + painter.drawImage((maxSize - im.width()) / 2, (maxSize - im.height()) / 2, im); + im = padded; + } + // copy and endian-convert + QXdgDBusImageStruct kim(im.width(), im.height()); + qToBigEndian<quint32>(im.constBits(), im.width() * im.height(), kim.data.data()); + + ret << kim; + } + return ret; +} + +// Marshall the ImageStruct data into a D-Bus argument +const QDBusArgument &operator<<(QDBusArgument &argument, const QXdgDBusImageStruct &icon) +{ + argument.beginStructure(); + argument << icon.width; + argument << icon.height; + argument << icon.data; + argument.endStructure(); + return argument; +} + +// Retrieve the ImageStruct data from the D-Bus argument +const QDBusArgument &operator>>(const QDBusArgument &argument, QXdgDBusImageStruct &icon) +{ + qint32 width; + qint32 height; + QByteArray data; + + argument.beginStructure(); + argument >> width; + argument >> height; + argument >> data; + argument.endStructure(); + + icon.width = width; + icon.height = height; + icon.data = data; + + return argument; +} + +// Marshall the ImageVector data into a D-Bus argument +const QDBusArgument &operator<<(QDBusArgument &argument, const QXdgDBusImageVector &iconVector) +{ + argument.beginArray(qMetaTypeId<QXdgDBusImageStruct>()); + for (int i = 0; i < iconVector.size(); ++i) { + argument << iconVector[i]; + } + argument.endArray(); + return argument; +} + +// Retrieve the ImageVector data from the D-Bus argument +const QDBusArgument &operator>>(const QDBusArgument &argument, QXdgDBusImageVector &iconVector) +{ + argument.beginArray(); + iconVector.clear(); + + while (!argument.atEnd()) { + QXdgDBusImageStruct element; + argument >> element; + iconVector.append(element); + } + + argument.endArray(); + + return argument; +} + +// Marshall the ToolTipStruct data into a D-Bus argument +const QDBusArgument &operator<<(QDBusArgument &argument, const QXdgDBusToolTipStruct &toolTip) +{ + argument.beginStructure(); + argument << toolTip.icon; + argument << toolTip.image; + argument << toolTip.title; + argument << toolTip.subTitle; + argument.endStructure(); + return argument; +} + +// Retrieve the ToolTipStruct data from the D-Bus argument +const QDBusArgument &operator>>(const QDBusArgument &argument, QXdgDBusToolTipStruct &toolTip) +{ + QString icon; + QXdgDBusImageVector image; + QString title; + QString subTitle; + + argument.beginStructure(); + argument >> icon; + argument >> image; + argument >> title; + argument >> subTitle; + argument.endStructure(); + + toolTip.icon = icon; + toolTip.image = image; + toolTip.title = title; + toolTip.subTitle = subTitle; + + return argument; +} + +QT_END_NAMESPACE +#endif // QT_NO_SYSTEMTRAYICON diff --git a/src/gui/platform/unix/dbustray/qdbustraytypes_p.h b/src/gui/platform/unix/dbustray/qdbustraytypes_p.h new file mode 100644 index 0000000000..08294d486c --- /dev/null +++ b/src/gui/platform/unix/dbustray/qdbustraytypes_p.h @@ -0,0 +1,73 @@ +// Copyright (C) 2009 Marco Martin <notmart@gmail.com> +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QDBUSTRAYTYPES_P_H +#define QDBUSTRAYTYPES_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of other Qt classes. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtGui/private/qtguiglobal_p.h> + +QT_REQUIRE_CONFIG(systemtrayicon); + +#include <QObject> +#include <QString> +#include <QDBusArgument> +#include <QDBusConnection> +#include <QDBusObjectPath> +#include <QPixmap> + +QT_BEGIN_NAMESPACE + +// Custom message type to send icons across D-Bus +struct QXdgDBusImageStruct +{ + QXdgDBusImageStruct() { } + QXdgDBusImageStruct(int w, int h) + : width(w), height(h), data(width * height * 4, 0) { } + int width; + int height; + QByteArray data; +}; +Q_DECLARE_TYPEINFO(QXdgDBusImageStruct, Q_RELOCATABLE_TYPE); + +using QXdgDBusImageVector = QList<QXdgDBusImageStruct>; + +QXdgDBusImageVector iconToQXdgDBusImageVector(const QIcon &icon); + +// Custom message type to send tooltips across D-Bus +struct QXdgDBusToolTipStruct +{ + QString icon; + QXdgDBusImageVector image; + QString title; + QString subTitle; +}; +Q_DECLARE_TYPEINFO(QXdgDBusToolTipStruct, Q_RELOCATABLE_TYPE); + +const QDBusArgument &operator<<(QDBusArgument &argument, const QXdgDBusImageStruct &icon); +const QDBusArgument &operator>>(const QDBusArgument &argument, QXdgDBusImageStruct &icon); + +const QDBusArgument &operator<<(QDBusArgument &argument, const QXdgDBusImageVector &iconVector); +const QDBusArgument &operator>>(const QDBusArgument &argument, QXdgDBusImageVector &iconVector); + +const QDBusArgument &operator<<(QDBusArgument &argument, const QXdgDBusToolTipStruct &toolTip); +const QDBusArgument &operator>>(const QDBusArgument &argument, QXdgDBusToolTipStruct &toolTip); + +QT_END_NAMESPACE + +QT_DECL_METATYPE_EXTERN(QXdgDBusImageStruct, Q_GUI_EXPORT) +QT_DECL_METATYPE_EXTERN(QXdgDBusImageVector, Q_GUI_EXPORT) +QT_DECL_METATYPE_EXTERN(QXdgDBusToolTipStruct, Q_GUI_EXPORT) + +#endif // QDBUSTRAYTYPES_P_H diff --git a/src/gui/platform/unix/dbustray/qstatusnotifieritemadaptor.cpp b/src/gui/platform/unix/dbustray/qstatusnotifieritemadaptor.cpp new file mode 100644 index 0000000000..2f6c13b6cf --- /dev/null +++ b/src/gui/platform/unix/dbustray/qstatusnotifieritemadaptor.cpp @@ -0,0 +1,161 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +/* + This file was originally created by qdbusxml2cpp version 0.8 + Command line was: + qdbusxml2cpp -a statusnotifieritem ../../3rdparty/dbus-ifaces/org.kde.StatusNotifierItem.xml + + However it is maintained manually, because this adapter needs to do + significant interface adaptation, and can do it more efficiently using the + QDBusTrayIcon API directly rather than via QObject::property() and + QMetaObject::invokeMethod(). +*/ + +#include "qstatusnotifieritemadaptor_p.h" + +#ifndef QT_NO_SYSTEMTRAYICON + +#include <QtCore/QLoggingCategory> +#include <QtCore/QCoreApplication> + +#include "qdbustrayicon_p.h" + +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(qLcMenu) +Q_DECLARE_LOGGING_CATEGORY(qLcTray) + +QStatusNotifierItemAdaptor::QStatusNotifierItemAdaptor(QDBusTrayIcon *parent) + : QDBusAbstractAdaptor(parent), m_trayIcon(parent) +{ + setAutoRelaySignals(true); +} + +QStatusNotifierItemAdaptor::~QStatusNotifierItemAdaptor() +{ +} + +QString QStatusNotifierItemAdaptor::attentionIconName() const +{ + return m_trayIcon->attentionIconName(); +} + +QXdgDBusImageVector QStatusNotifierItemAdaptor::attentionIconPixmap() const +{ + return iconToQXdgDBusImageVector(m_trayIcon->attentionIcon()); +} + +QString QStatusNotifierItemAdaptor::attentionMovieName() const +{ + return QString(); +} + +QString QStatusNotifierItemAdaptor::category() const +{ + return m_trayIcon->category(); +} + +QString QStatusNotifierItemAdaptor::iconName() const +{ + return m_trayIcon->iconName(); +} + +QXdgDBusImageVector QStatusNotifierItemAdaptor::iconPixmap() const +{ + return iconToQXdgDBusImageVector(m_trayIcon->icon()); +} + +QString QStatusNotifierItemAdaptor::id() const +{ + // from the API docs: "a name that should be unique for this application and + // consistent between sessions, such as the application name itself" + return QCoreApplication::applicationName(); +} + +bool QStatusNotifierItemAdaptor::itemIsMenu() const +{ + // From KDE docs: if this is true, the item only supports the context menu, + // so the visualization should prefer sending ContextMenu() instead of Activate(). + // But QSystemTrayIcon doesn't have such a setting: it will emit activated() + // and the application is free to use it or ignore it; we don't know whether it will. + return false; +} + +QDBusObjectPath QStatusNotifierItemAdaptor::menu() const +{ + return QDBusObjectPath(m_trayIcon->menu() ? "/MenuBar" : "/NO_DBUSMENU"); +} + +QString QStatusNotifierItemAdaptor::overlayIconName() const +{ + return QString(); +} + +QXdgDBusImageVector QStatusNotifierItemAdaptor::overlayIconPixmap() const +{ + QXdgDBusImageVector ret; // empty vector + return ret; +} + +QString QStatusNotifierItemAdaptor::status() const +{ + return m_trayIcon->status(); +} + +QString QStatusNotifierItemAdaptor::title() const +{ + // Shown e.g. when the icon is hidden, in the popup showing all hidden items. + // Since QSystemTrayIcon doesn't have this property, the application name + // is the best information we have available. + return QCoreApplication::applicationName(); +} + +QXdgDBusToolTipStruct QStatusNotifierItemAdaptor::toolTip() const +{ + QXdgDBusToolTipStruct ret; + if (m_trayIcon->isRequestingAttention()) { + ret.title = m_trayIcon->attentionTitle(); + ret.subTitle = m_trayIcon->attentionMessage(); + ret.icon = m_trayIcon->attentionIconName(); + } else { + ret.title = m_trayIcon->tooltip(); + } + return ret; +} + +void QStatusNotifierItemAdaptor::Activate(int x, int y) +{ + qCDebug(qLcTray) << x << y; + emit m_trayIcon->activated(QPlatformSystemTrayIcon::Trigger); +} + +void QStatusNotifierItemAdaptor::ContextMenu(int x, int y) +{ + qCDebug(qLcTray) << x << y; + emit m_trayIcon->activated(QPlatformSystemTrayIcon::Context); +} + +void QStatusNotifierItemAdaptor::ProvideXdgActivationToken(const QString &token) +{ + qCDebug(qLcTray) << token; + qputenv("XDG_ACTIVATION_TOKEN", token.toUtf8()); +} + +void QStatusNotifierItemAdaptor::Scroll(int w, const QString &s) +{ + qCDebug(qLcTray) << w << s; + // unsupported +} + +void QStatusNotifierItemAdaptor::SecondaryActivate(int x, int y) +{ + qCDebug(qLcTray) << x << y; + emit m_trayIcon->activated(QPlatformSystemTrayIcon::MiddleClick); +} + +QT_END_NAMESPACE + +#include "moc_qstatusnotifieritemadaptor_p.cpp" + +#endif // QT_NO_SYSTEMTRAYICON diff --git a/src/gui/platform/unix/dbustray/qstatusnotifieritemadaptor_p.h b/src/gui/platform/unix/dbustray/qstatusnotifieritemadaptor_p.h new file mode 100644 index 0000000000..103fc974dd --- /dev/null +++ b/src/gui/platform/unix/dbustray/qstatusnotifieritemadaptor_p.h @@ -0,0 +1,174 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +/* + This file was originally created by qdbusxml2cpp version 0.8 + Command line was: + qdbusxml2cpp -a statusnotifieritem ../../3rdparty/dbus-ifaces/org.kde.StatusNotifierItem.xml + + However it is maintained manually. + + It is also not part of the public API. This header file may change from + version to version without notice, or even be removed. +*/ + +#ifndef QSTATUSNOTIFIERITEMADAPTER_P_H +#define QSTATUSNOTIFIERITEMADAPTER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <private/qtguiglobal_p.h> + +QT_REQUIRE_CONFIG(systemtrayicon); + +#include <QObject> +#include <QDBusAbstractAdaptor> + +#include <private/qdbustraytypes_p.h> + +QT_BEGIN_NAMESPACE +class QDBusTrayIcon; + +/* + Adaptor class for interface org.kde.StatusNotifierItem + see http://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/ + (also http://www.notmart.org/misc/statusnotifieritem/) +*/ +class QStatusNotifierItemAdaptor: public QDBusAbstractAdaptor +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.StatusNotifierItem") + Q_CLASSINFO("D-Bus Introspection", "" +" <interface name=\"org.kde.StatusNotifierItem\">\n" +" <property access=\"read\" type=\"s\" name=\"Category\"/>\n" +" <property access=\"read\" type=\"s\" name=\"Id\"/>\n" +" <property access=\"read\" type=\"s\" name=\"Title\"/>\n" +" <property access=\"read\" type=\"s\" name=\"Status\"/>\n" +" <property access=\"read\" type=\"i\" name=\"WindowId\"/>\n" +" <property access=\"read\" type=\"s\" name=\"IconThemePath\"/>\n" +" <property access=\"read\" type=\"o\" name=\"Menu\"/>\n" +" <property access=\"read\" type=\"b\" name=\"ItemIsMenu\"/>\n" +" <property access=\"read\" type=\"s\" name=\"IconName\"/>\n" +" <property access=\"read\" type=\"a(iiay)\" name=\"IconPixmap\">\n" +" <annotation value=\"QXdgDBusImageVector\" name=\"org.qtproject.QtDBus.QtTypeName\"/>\n" +" </property>\n" +" <property access=\"read\" type=\"s\" name=\"OverlayIconName\"/>\n" +" <property access=\"read\" type=\"a(iiay)\" name=\"OverlayIconPixmap\">\n" +" <annotation value=\"QXdgDBusImageVector\" name=\"org.qtproject.QtDBus.QtTypeName\"/>\n" +" </property>\n" +" <property access=\"read\" type=\"s\" name=\"AttentionIconName\"/>\n" +" <property access=\"read\" type=\"a(iiay)\" name=\"AttentionIconPixmap\">\n" +" <annotation value=\"QXdgDBusImageVector\" name=\"org.qtproject.QtDBus.QtTypeName\"/>\n" +" </property>\n" +" <property access=\"read\" type=\"s\" name=\"AttentionMovieName\"/>\n" +" <property access=\"read\" type=\"(sa(iiay)ss)\" name=\"ToolTip\">\n" +" <annotation value=\"QXdgDBusToolTipStruct\" name=\"org.qtproject.QtDBus.QtTypeName\"/>\n" +" </property>\n" +" <method name=\"ProvideXdgActivationToken\">\n" +" <arg name=\"token\" type=\"s\" direction=\"in\"/>\n" +" </method>\n" +" <method name=\"ContextMenu\">\n" +" <arg direction=\"in\" type=\"i\" name=\"x\"/>\n" +" <arg direction=\"in\" type=\"i\" name=\"y\"/>\n" +" </method>\n" +" <method name=\"Activate\">\n" +" <arg direction=\"in\" type=\"i\" name=\"x\"/>\n" +" <arg direction=\"in\" type=\"i\" name=\"y\"/>\n" +" </method>\n" +" <method name=\"SecondaryActivate\">\n" +" <arg direction=\"in\" type=\"i\" name=\"x\"/>\n" +" <arg direction=\"in\" type=\"i\" name=\"y\"/>\n" +" </method>\n" +" <method name=\"Scroll\">\n" +" <arg direction=\"in\" type=\"i\" name=\"delta\"/>\n" +" <arg direction=\"in\" type=\"s\" name=\"orientation\"/>\n" +" </method>\n" +" <signal name=\"NewTitle\"/>\n" +" <signal name=\"NewIcon\"/>\n" +" <signal name=\"NewAttentionIcon\"/>\n" +" <signal name=\"NewOverlayIcon\"/>\n" +" <signal name=\"NewMenu\"/>\n" +" <signal name=\"NewToolTip\"/>\n" +" <signal name=\"NewStatus\">\n" +" <arg type=\"s\" name=\"status\"/>\n" +" </signal>\n" +" </interface>\n" + "") +public: + QStatusNotifierItemAdaptor(QDBusTrayIcon *parent); + virtual ~QStatusNotifierItemAdaptor(); + +public: // PROPERTIES + Q_PROPERTY(QString AttentionIconName READ attentionIconName) + QString attentionIconName() const; + + Q_PROPERTY(QXdgDBusImageVector AttentionIconPixmap READ attentionIconPixmap) + QXdgDBusImageVector attentionIconPixmap() const; + + Q_PROPERTY(QString AttentionMovieName READ attentionMovieName) + QString attentionMovieName() const; + + Q_PROPERTY(QString Category READ category) + QString category() const; + + Q_PROPERTY(QString IconName READ iconName) + QString iconName() const; + + Q_PROPERTY(QXdgDBusImageVector IconPixmap READ iconPixmap) + QXdgDBusImageVector iconPixmap() const; + + Q_PROPERTY(QString Id READ id) + QString id() const; + + Q_PROPERTY(bool ItemIsMenu READ itemIsMenu) + bool itemIsMenu() const; + + Q_PROPERTY(QDBusObjectPath Menu READ menu) + QDBusObjectPath menu() const; + + Q_PROPERTY(QString OverlayIconName READ overlayIconName) + QString overlayIconName() const; + + Q_PROPERTY(QXdgDBusImageVector OverlayIconPixmap READ overlayIconPixmap) + QXdgDBusImageVector overlayIconPixmap() const; + + Q_PROPERTY(QString Status READ status) + QString status() const; + + Q_PROPERTY(QString Title READ title) + QString title() const; + + Q_PROPERTY(QXdgDBusToolTipStruct ToolTip READ toolTip) + QXdgDBusToolTipStruct toolTip() const; + +public Q_SLOTS: // METHODS + void Activate(int x, int y); + void ContextMenu(int x, int y); + void ProvideXdgActivationToken(const QString &token); + void Scroll(int delta, const QString &orientation); + void SecondaryActivate(int x, int y); +Q_SIGNALS: // SIGNALS + void NewAttentionIcon(); + void NewIcon(); + void NewOverlayIcon(); + void NewMenu(); + void NewStatus(const QString &status); + void NewTitle(); + void NewToolTip(); + +private: + QDBusTrayIcon *m_trayIcon; +}; + +QT_END_NAMESPACE + +#endif // QSTATUSNOTIFIERITEMADAPTER_P_H diff --git a/src/gui/platform/unix/dbustray/qxdgnotificationproxy.cpp b/src/gui/platform/unix/dbustray/qxdgnotificationproxy.cpp new file mode 100644 index 0000000000..97b6697e6d --- /dev/null +++ b/src/gui/platform/unix/dbustray/qxdgnotificationproxy.cpp @@ -0,0 +1,19 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qxdgnotificationproxy_p.h" + +QT_BEGIN_NAMESPACE + +QXdgNotificationInterface::QXdgNotificationInterface(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent) + : QDBusAbstractInterface(service, path, staticInterfaceName(), connection, parent) +{ +} + +QXdgNotificationInterface::~QXdgNotificationInterface() +{ +} + +QT_END_NAMESPACE + +#include "moc_qxdgnotificationproxy_p.cpp" diff --git a/src/gui/platform/unix/dbustray/qxdgnotificationproxy_p.h b/src/gui/platform/unix/dbustray/qxdgnotificationproxy_p.h new file mode 100644 index 0000000000..dfbc64f33b --- /dev/null +++ b/src/gui/platform/unix/dbustray/qxdgnotificationproxy_p.h @@ -0,0 +1,110 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +/* + This file was originally created by qdbusxml2cpp version 0.8 + Command line was: + qdbusxml2cpp -p qxdgnotificationproxy ../../3rdparty/dbus-ifaces/org.freedesktop.Notifications.xml + + However it is maintained manually. + + It is also not part of the public API. This header file may change from + version to version without notice, or even be removed. +*/ + +#ifndef QXDGNOTIFICATIONPROXY_P_H +#define QXDGNOTIFICATIONPROXY_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of other Qt classes. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QObject> +#include <QByteArray> +#include <QList> +#include <QLoggingCategory> +#include <QMap> +#include <QString> +#include <QStringList> +#include <QVariant> +#include <QDBusAbstractInterface> +#include <QDBusPendingReply> +#include <QDBusReply> +#include <private/qglobal_p.h> + +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(qLcTray) + +/* + * Proxy class for interface org.freedesktop.Notifications + */ +class QXdgNotificationInterface: public QDBusAbstractInterface +{ + Q_OBJECT +public: + static inline const char *staticInterfaceName() + { return "org.freedesktop.Notifications"; } + +public: + QXdgNotificationInterface(const QString &service, const QString &path, + const QDBusConnection &connection, QObject *parent = nullptr); + + ~QXdgNotificationInterface(); + +public Q_SLOTS: // METHODS + inline QDBusPendingReply<> closeNotification(uint id) + { + return asyncCall(QStringLiteral("CloseNotification"), id); + } + + inline QDBusPendingReply<QStringList> getCapabilities() + { + return asyncCall(QStringLiteral("GetCapabilities")); + } + + inline QDBusPendingReply<QString, QString, QString, QString> getServerInformation() + { + return asyncCall(QStringLiteral("GetServerInformation")); + } + inline QDBusReply<QString> getServerInformation(QString &vendor, QString &version, QString &specVersion) + { + QDBusMessage reply = call(QDBus::Block, QStringLiteral("GetServerInformation")); + if (reply.type() == QDBusMessage::ReplyMessage && reply.arguments().size() == 4) { + vendor = qdbus_cast<QString>(reply.arguments().at(1)); + version = qdbus_cast<QString>(reply.arguments().at(2)); + specVersion = qdbus_cast<QString>(reply.arguments().at(3)); + } + return reply; + } + + // see https://developer.gnome.org/notification-spec/#basic-design + inline QDBusPendingReply<uint> notify(const QString &appName, uint replacesId, const QString &appIcon, + const QString &summary, const QString &body, const QStringList &actions, + const QVariantMap &hints, int timeout) + { + qCDebug(qLcTray) << appName << replacesId << appIcon << summary << body << actions << hints << timeout; + return asyncCall(QStringLiteral("Notify"), appName, replacesId, appIcon, summary, body, actions, hints, timeout); + } + +Q_SIGNALS: + void ActionInvoked(uint id, const QString &action_key); + void NotificationClosed(uint id, uint reason); +}; + +QT_END_NAMESPACE + +namespace org { + namespace freedesktop { + using Notifications = QT_PREPEND_NAMESPACE(QXdgNotificationInterface); + } +} + +#endif diff --git a/src/gui/platform/unix/qeventdispatcher_glib.cpp b/src/gui/platform/unix/qeventdispatcher_glib.cpp new file mode 100644 index 0000000000..368006f302 --- /dev/null +++ b/src/gui/platform/unix/qeventdispatcher_glib.cpp @@ -0,0 +1,92 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qeventdispatcher_glib_p.h" + +#include "qguiapplication.h" + +#include "qplatformdefs.h" + +#include <glib.h> +#include "private/qguiapplication_p.h" + +QT_BEGIN_NAMESPACE + +struct GUserEventSource +{ + GSource source; + QPAEventDispatcherGlib *q; + QPAEventDispatcherGlibPrivate *d; +}; + +static gboolean userEventSourcePrepare(GSource *source, gint *timeout) +{ + Q_UNUSED(timeout); + GUserEventSource *userEventSource = reinterpret_cast<GUserEventSource *>(source); + return userEventSource->d->wakeUpCalled; +} + +static gboolean userEventSourceCheck(GSource *source) +{ + return userEventSourcePrepare(source, nullptr); +} + +static gboolean userEventSourceDispatch(GSource *source, GSourceFunc, gpointer) +{ + GUserEventSource *userEventSource = reinterpret_cast<GUserEventSource *>(source); + QPAEventDispatcherGlib *dispatcher = userEventSource->q; + QWindowSystemInterface::sendWindowSystemEvents(dispatcher->m_flags); + return true; +} + +static GSourceFuncs userEventSourceFuncs = { + userEventSourcePrepare, + userEventSourceCheck, + userEventSourceDispatch, + NULL, + NULL, + NULL +}; + +QPAEventDispatcherGlibPrivate::QPAEventDispatcherGlibPrivate(GMainContext *context) + : QEventDispatcherGlibPrivate(context) +{ + Q_Q(QPAEventDispatcherGlib); + + GSource *source = g_source_new(&userEventSourceFuncs, sizeof(GUserEventSource)); + g_source_set_name(source, "[Qt] GUserEventSource"); + userEventSource = reinterpret_cast<GUserEventSource *>(source); + + userEventSource->q = q; + userEventSource->d = this; + g_source_set_can_recurse(&userEventSource->source, true); + g_source_attach(&userEventSource->source, mainContext); +} + + +QPAEventDispatcherGlib::QPAEventDispatcherGlib(QObject *parent) + : QEventDispatcherGlib(*new QPAEventDispatcherGlibPrivate, parent) + , m_flags(QEventLoop::AllEvents) +{ + Q_D(QPAEventDispatcherGlib); + d->userEventSource->q = this; +} + +QPAEventDispatcherGlib::~QPAEventDispatcherGlib() +{ + Q_D(QPAEventDispatcherGlib); + + g_source_destroy(&d->userEventSource->source); + g_source_unref(&d->userEventSource->source); + d->userEventSource = nullptr; +} + +bool QPAEventDispatcherGlib::processEvents(QEventLoop::ProcessEventsFlags flags) +{ + m_flags = flags; + return QEventDispatcherGlib::processEvents(m_flags); +} + +QT_END_NAMESPACE + +#include "moc_qeventdispatcher_glib_p.cpp" diff --git a/src/gui/platform/unix/qeventdispatcher_glib_p.h b/src/gui/platform/unix/qeventdispatcher_glib_p.h new file mode 100644 index 0000000000..95fac16237 --- /dev/null +++ b/src/gui/platform/unix/qeventdispatcher_glib_p.h @@ -0,0 +1,52 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QEVENTDISPATCHER_GLIB_QPA_P_H +#define QEVENTDISPATCHER_GLIB_QPA_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/private/qeventdispatcher_glib_p.h> +#include <QtGui/qtguiglobal.h> + +typedef struct _GMainContext GMainContext; + +QT_BEGIN_NAMESPACE +class QPAEventDispatcherGlibPrivate; + +class Q_GUI_EXPORT QPAEventDispatcherGlib : public QEventDispatcherGlib +{ + Q_OBJECT + Q_DECLARE_PRIVATE(QPAEventDispatcherGlib) + +public: + explicit QPAEventDispatcherGlib(QObject *parent = nullptr); + ~QPAEventDispatcherGlib(); + + bool processEvents(QEventLoop::ProcessEventsFlags flags) override; + QEventLoop::ProcessEventsFlags m_flags; +}; + +struct GUserEventSource; + +class QPAEventDispatcherGlibPrivate : public QEventDispatcherGlibPrivate +{ + Q_DECLARE_PUBLIC(QPAEventDispatcherGlib) +public: + QPAEventDispatcherGlibPrivate(GMainContext *context = nullptr); + GUserEventSource *userEventSource; +}; + + +QT_END_NAMESPACE + +#endif // QEVENTDISPATCHER_GLIB_QPA_P_H diff --git a/src/gui/platform/unix/qgenericunixeventdispatcher.cpp b/src/gui/platform/unix/qgenericunixeventdispatcher.cpp new file mode 100644 index 0000000000..b551aefe99 --- /dev/null +++ b/src/gui/platform/unix/qgenericunixeventdispatcher.cpp @@ -0,0 +1,21 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qgenericunixeventdispatcher_p.h" +#include "qunixeventdispatcher_qpa_p.h" +#if QT_CONFIG(glib) +# include "qeventdispatcher_glib_p.h" +#endif +QT_BEGIN_NAMESPACE + +class QAbstractEventDispatcher *QtGenericUnixDispatcher::createUnixEventDispatcher() +{ +#if !defined(QT_NO_GLIB) && !defined(Q_OS_WIN) + if (qEnvironmentVariableIsEmpty("QT_NO_GLIB") && QEventDispatcherGlib::versionSupported()) + return new QPAEventDispatcherGlib(); + else +#endif + return new QUnixEventDispatcherQPA(); +} + +QT_END_NAMESPACE diff --git a/src/gui/platform/unix/qgenericunixeventdispatcher_p.h b/src/gui/platform/unix/qgenericunixeventdispatcher_p.h new file mode 100644 index 0000000000..82dc35da0b --- /dev/null +++ b/src/gui/platform/unix/qgenericunixeventdispatcher_p.h @@ -0,0 +1,31 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of other Qt classes. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#ifndef QGENERICUNIXEVENTDISPATCHER_P_H +#define QGENERICUNIXEVENTDISPATCHER_P_H + +#include <QtGui/qtguiglobal.h> +#include <QtCore/private/qglobal_p.h> + +QT_BEGIN_NAMESPACE + +class QAbstractEventDispatcher; +namespace QtGenericUnixDispatcher { +Q_GUI_EXPORT QAbstractEventDispatcher *createUnixEventDispatcher(); +} +using QtGenericUnixDispatcher::createUnixEventDispatcher; + +QT_END_NAMESPACE + +#endif // QGENERICUNIXEVENTDISPATCHER_P_H diff --git a/src/gui/platform/unix/qgenericunixservices.cpp b/src/gui/platform/unix/qgenericunixservices.cpp new file mode 100644 index 0000000000..bfd2556b1e --- /dev/null +++ b/src/gui/platform/unix/qgenericunixservices.cpp @@ -0,0 +1,613 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qgenericunixservices_p.h" +#include <QtGui/private/qtguiglobal_p.h> +#include "qguiapplication.h" +#include "qwindow.h" +#include <QtGui/qpa/qplatformwindow_p.h> +#include <QtGui/qpa/qplatformwindow.h> +#include <QtGui/qpa/qplatformnativeinterface.h> + +#include <QtCore/QDebug> +#include <QtCore/QFile> +#if QT_CONFIG(process) +# include <QtCore/QProcess> +#endif +#if QT_CONFIG(settings) +#include <QtCore/QSettings> +#endif +#include <QtCore/QStandardPaths> +#include <QtCore/QUrl> + +#if QT_CONFIG(dbus) +// These QtCore includes are needed for xdg-desktop-portal support +#include <QtCore/private/qcore_unix_p.h> + +#include <QtCore/QFileInfo> +#include <QtCore/QUrlQuery> + +#include <QtDBus/QDBusConnection> +#include <QtDBus/QDBusMessage> +#include <QtDBus/QDBusPendingCall> +#include <QtDBus/QDBusPendingCallWatcher> +#include <QtDBus/QDBusPendingReply> +#include <QtDBus/QDBusUnixFileDescriptor> + +#include <fcntl.h> + +#endif // QT_CONFIG(dbus) + +#include <stdlib.h> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +#if QT_CONFIG(multiprocess) + +enum { debug = 0 }; + +static inline QByteArray detectDesktopEnvironment() +{ + const QByteArray xdgCurrentDesktop = qgetenv("XDG_CURRENT_DESKTOP"); + if (!xdgCurrentDesktop.isEmpty()) + return xdgCurrentDesktop.toUpper(); // KDE, GNOME, UNITY, LXDE, MATE, XFCE... + + // Classic fallbacks + if (!qEnvironmentVariableIsEmpty("KDE_FULL_SESSION")) + return QByteArrayLiteral("KDE"); + if (!qEnvironmentVariableIsEmpty("GNOME_DESKTOP_SESSION_ID")) + return QByteArrayLiteral("GNOME"); + + // Fallback to checking $DESKTOP_SESSION (unreliable) + QByteArray desktopSession = qgetenv("DESKTOP_SESSION"); + + // This can be a path in /usr/share/xsessions + int slash = desktopSession.lastIndexOf('/'); + if (slash != -1) { +#if QT_CONFIG(settings) + QSettings desktopFile(QFile::decodeName(desktopSession + ".desktop"), QSettings::IniFormat); + desktopFile.beginGroup(QStringLiteral("Desktop Entry")); + QByteArray desktopName = desktopFile.value(QStringLiteral("DesktopNames")).toByteArray(); + if (!desktopName.isEmpty()) + return desktopName; +#endif + + // try decoding just the basename + desktopSession = desktopSession.mid(slash + 1); + } + + if (desktopSession == "gnome") + return QByteArrayLiteral("GNOME"); + else if (desktopSession == "xfce") + return QByteArrayLiteral("XFCE"); + else if (desktopSession == "kde") + return QByteArrayLiteral("KDE"); + + return QByteArrayLiteral("UNKNOWN"); +} + +static inline bool checkExecutable(const QString &candidate, QString *result) +{ + *result = QStandardPaths::findExecutable(candidate); + return !result->isEmpty(); +} + +static inline bool detectWebBrowser(const QByteArray &desktop, + bool checkBrowserVariable, + QString *browser) +{ + const char *browsers[] = {"google-chrome", "firefox", "mozilla", "opera"}; + + browser->clear(); + if (checkExecutable(QStringLiteral("xdg-open"), browser)) + return true; + + if (checkBrowserVariable) { + QByteArray browserVariable = qgetenv("DEFAULT_BROWSER"); + if (browserVariable.isEmpty()) + browserVariable = qgetenv("BROWSER"); + if (!browserVariable.isEmpty() && checkExecutable(QString::fromLocal8Bit(browserVariable), browser)) + return true; + } + + if (desktop == QByteArray("KDE")) { + if (checkExecutable(QStringLiteral("kde-open5"), browser)) + return true; + // Konqueror launcher + if (checkExecutable(QStringLiteral("kfmclient"), browser)) { + browser->append(" exec"_L1); + return true; + } + } else if (desktop == QByteArray("GNOME")) { + if (checkExecutable(QStringLiteral("gnome-open"), browser)) + return true; + } + + for (size_t i = 0; i < sizeof(browsers)/sizeof(char *); ++i) + if (checkExecutable(QLatin1StringView(browsers[i]), browser)) + return true; + return false; +} + +static inline bool launch(const QString &launcher, const QUrl &url, + const QString &xdgActivationToken) +{ + if (!xdgActivationToken.isEmpty()) { + qputenv("XDG_ACTIVATION_TOKEN", xdgActivationToken.toUtf8()); + } + + const QString command = launcher + u' ' + QLatin1StringView(url.toEncoded()); + if (debug) + qDebug("Launching %s", qPrintable(command)); +#if !QT_CONFIG(process) + const bool ok = ::system(qPrintable(command + " &"_L1)); +#else + QStringList args = QProcess::splitCommand(command); + bool ok = false; + if (!args.isEmpty()) { + QString program = args.takeFirst(); + ok = QProcess::startDetached(program, args); + } +#endif + if (!ok) + qWarning("Launch failed (%s)", qPrintable(command)); + + qunsetenv("XDG_ACTIVATION_TOKEN"); + + return ok; +} + +#if QT_CONFIG(dbus) +static inline bool checkNeedPortalSupport() +{ + return QFileInfo::exists("/.flatpak-info"_L1) || qEnvironmentVariableIsSet("SNAP"); +} + +static inline QDBusMessage xdgDesktopPortalOpenFile(const QUrl &url, const QString &parentWindow, + const QString &xdgActivationToken) +{ + // DBus signature: + // OpenFile (IN s parent_window, + // IN h fd, + // IN a{sv} options, + // OUT o handle) + // Options: + // handle_token (s) - A string that will be used as the last element of the @handle. + // writable (b) - Whether to allow the chosen application to write to the file. + + const int fd = qt_safe_open(QFile::encodeName(url.toLocalFile()), O_RDONLY); + if (fd != -1) { + QDBusMessage message = QDBusMessage::createMethodCall("org.freedesktop.portal.Desktop"_L1, + "/org/freedesktop/portal/desktop"_L1, + "org.freedesktop.portal.OpenURI"_L1, + "OpenFile"_L1); + + QDBusUnixFileDescriptor descriptor; + descriptor.giveFileDescriptor(fd); + + QVariantMap options = {}; + + if (!xdgActivationToken.isEmpty()) { + options.insert("activation_token"_L1, xdgActivationToken); + } + + message << parentWindow << QVariant::fromValue(descriptor) << options; + + return QDBusConnection::sessionBus().call(message); + } + + return QDBusMessage::createError(QDBusError::InternalError, qt_error_string()); +} + +static inline QDBusMessage xdgDesktopPortalOpenUrl(const QUrl &url, const QString &parentWindow, + const QString &xdgActivationToken) +{ + // DBus signature: + // OpenURI (IN s parent_window, + // IN s uri, + // IN a{sv} options, + // OUT o handle) + // Options: + // handle_token (s) - A string that will be used as the last element of the @handle. + // writable (b) - Whether to allow the chosen application to write to the file. + // This key only takes effect the uri points to a local file that is exported in the document portal, + // and the chosen application is sandboxed itself. + + QDBusMessage message = QDBusMessage::createMethodCall("org.freedesktop.portal.Desktop"_L1, + "/org/freedesktop/portal/desktop"_L1, + "org.freedesktop.portal.OpenURI"_L1, + "OpenURI"_L1); + // FIXME parent_window_id and handle writable option + QVariantMap options; + + if (!xdgActivationToken.isEmpty()) { + options.insert("activation_token"_L1, xdgActivationToken); + } + + message << parentWindow << url.toString() << options; + + return QDBusConnection::sessionBus().call(message); +} + +static inline QDBusMessage xdgDesktopPortalSendEmail(const QUrl &url, const QString &parentWindow, + const QString &xdgActivationToken) +{ + // DBus signature: + // ComposeEmail (IN s parent_window, + // IN a{sv} options, + // OUT o handle) + // Options: + // address (s) - The email address to send to. + // subject (s) - The subject for the email. + // body (s) - The body for the email. + // attachment_fds (ah) - File descriptors for files to attach. + + QUrlQuery urlQuery(url); + QVariantMap options; + options.insert("address"_L1, url.path()); + options.insert("subject"_L1, urlQuery.queryItemValue("subject"_L1)); + options.insert("body"_L1, urlQuery.queryItemValue("body"_L1)); + + // O_PATH seems to be present since Linux 2.6.39, which is not case of RHEL 6 +#ifdef O_PATH + QList<QDBusUnixFileDescriptor> attachments; + const QStringList attachmentUris = urlQuery.allQueryItemValues("attachment"_L1); + + for (const QString &attachmentUri : attachmentUris) { + const int fd = qt_safe_open(QFile::encodeName(attachmentUri), O_PATH); + if (fd != -1) { + QDBusUnixFileDescriptor descriptor(fd); + attachments << descriptor; + qt_safe_close(fd); + } + } + + options.insert("attachment_fds"_L1, QVariant::fromValue(attachments)); +#endif + + if (!xdgActivationToken.isEmpty()) { + options.insert("activation_token"_L1, xdgActivationToken); + } + + QDBusMessage message = QDBusMessage::createMethodCall("org.freedesktop.portal.Desktop"_L1, + "/org/freedesktop/portal/desktop"_L1, + "org.freedesktop.portal.Email"_L1, + "ComposeEmail"_L1); + + message << parentWindow << options; + + return QDBusConnection::sessionBus().call(message); +} + +namespace { +struct XDGDesktopColor +{ + double r = 0; + double g = 0; + double b = 0; + + QColor toQColor() const + { + constexpr auto rgbMax = 255; + return { static_cast<int>(r * rgbMax), static_cast<int>(g * rgbMax), + static_cast<int>(b * rgbMax) }; + } +}; + +const QDBusArgument &operator>>(const QDBusArgument &argument, XDGDesktopColor &myStruct) +{ + argument.beginStructure(); + argument >> myStruct.r >> myStruct.g >> myStruct.b; + argument.endStructure(); + return argument; +} + +class XdgDesktopPortalColorPicker : public QPlatformServiceColorPicker +{ + Q_OBJECT +public: + XdgDesktopPortalColorPicker(const QString &parentWindowId, QWindow *parent) + : QPlatformServiceColorPicker(parent), m_parentWindowId(parentWindowId) + { + } + + void pickColor() override + { + // DBus signature: + // PickColor (IN s parent_window, + // IN a{sv} options + // OUT o handle) + // Options: + // handle_token (s) - A string that will be used as the last element of the @handle. + + QDBusMessage message = QDBusMessage::createMethodCall( + "org.freedesktop.portal.Desktop"_L1, "/org/freedesktop/portal/desktop"_L1, + "org.freedesktop.portal.Screenshot"_L1, "PickColor"_L1); + message << m_parentWindowId << QVariantMap(); + + QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message); + auto watcher = new QDBusPendingCallWatcher(pendingCall, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, + [this](QDBusPendingCallWatcher *watcher) { + watcher->deleteLater(); + QDBusPendingReply<QDBusObjectPath> reply = *watcher; + if (reply.isError()) { + qWarning("DBus call to pick color failed: %s", + qPrintable(reply.error().message())); + Q_EMIT colorPicked({}); + } else { + QDBusConnection::sessionBus().connect( + "org.freedesktop.portal.Desktop"_L1, reply.value().path(), + "org.freedesktop.portal.Request"_L1, "Response"_L1, this, + // clang-format off + SLOT(gotColorResponse(uint,QVariantMap)) + // clang-format on + ); + } + }); + } + +private Q_SLOTS: + void gotColorResponse(uint result, const QVariantMap &map) + { + if (result != 0) + return; + if (map.contains(u"color"_s)) { + XDGDesktopColor color{}; + map.value(u"color"_s).value<QDBusArgument>() >> color; + Q_EMIT colorPicked(color.toQColor()); + } else { + Q_EMIT colorPicked({}); + } + deleteLater(); + } + +private: + const QString m_parentWindowId; +}; +} // namespace + +#endif // QT_CONFIG(dbus) + +QGenericUnixServices::QGenericUnixServices() +{ +#if QT_CONFIG(dbus) + if (qEnvironmentVariableIntValue("QT_NO_XDG_DESKTOP_PORTAL") > 0) { + return; + } + QDBusMessage message = QDBusMessage::createMethodCall( + "org.freedesktop.portal.Desktop"_L1, "/org/freedesktop/portal/desktop"_L1, + "org.freedesktop.DBus.Properties"_L1, "Get"_L1); + message << "org.freedesktop.portal.Screenshot"_L1 + << "version"_L1; + + QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message); + auto watcher = new QDBusPendingCallWatcher(pendingCall); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, watcher, + [this](QDBusPendingCallWatcher *watcher) { + watcher->deleteLater(); + QDBusPendingReply<QVariant> reply = *watcher; + if (!reply.isError() && reply.value().toUInt() >= 2) + m_hasScreenshotPortalWithColorPicking = true; + }); + +#endif +} + +QPlatformServiceColorPicker *QGenericUnixServices::colorPicker(QWindow *parent) +{ +#if QT_CONFIG(dbus) + // Make double sure that we are in a wayland environment. In particular check + // WAYLAND_DISPLAY so also XWayland apps benefit from portal-based color picking. + // Outside wayland we'll rather rely on other means than the XDG desktop portal. + if (!qEnvironmentVariableIsEmpty("WAYLAND_DISPLAY") + || QGuiApplication::platformName().startsWith("wayland"_L1)) { + return new XdgDesktopPortalColorPicker(portalWindowIdentifier(parent), parent); + } + return nullptr; +#else + Q_UNUSED(parent); + return nullptr; +#endif +} + +QByteArray QGenericUnixServices::desktopEnvironment() const +{ + static const QByteArray result = detectDesktopEnvironment(); + return result; +} + +template<typename F> +void runWithXdgActivationToken(F &&functionToCall) +{ +#if QT_CONFIG(wayland) + QWindow *window = qGuiApp->focusWindow(); + + if (!window) { + functionToCall({}); + return; + } + + auto waylandApp = dynamic_cast<QNativeInterface::QWaylandApplication *>( + qGuiApp->platformNativeInterface()); + auto waylandWindow = + dynamic_cast<QNativeInterface::Private::QWaylandWindow *>(window->handle()); + + if (!waylandWindow || !waylandApp) { + functionToCall({}); + return; + } + + QObject::connect(waylandWindow, + &QNativeInterface::Private::QWaylandWindow::xdgActivationTokenCreated, + waylandWindow, functionToCall, Qt::SingleShotConnection); + waylandWindow->requestXdgActivationToken(waylandApp->lastInputSerial()); +#else + functionToCall({}); +#endif +} + +bool QGenericUnixServices::openUrl(const QUrl &url) +{ + auto openUrlInternal = [this](const QUrl &url, const QString &xdgActivationToken) { + if (url.scheme() == "mailto"_L1) { +# if QT_CONFIG(dbus) + if (checkNeedPortalSupport()) { + const QString parentWindow = QGuiApplication::focusWindow() + ? portalWindowIdentifier(QGuiApplication::focusWindow()) + : QString(); + QDBusError error = xdgDesktopPortalSendEmail(url, parentWindow, xdgActivationToken); + if (!error.isValid()) + return true; + + // service not running, fall back + } +# endif + return openDocument(url); + } + +# if QT_CONFIG(dbus) + if (checkNeedPortalSupport()) { + const QString parentWindow = QGuiApplication::focusWindow() + ? portalWindowIdentifier(QGuiApplication::focusWindow()) + : QString(); + QDBusError error = xdgDesktopPortalOpenUrl(url, parentWindow, xdgActivationToken); + if (!error.isValid()) + return true; + } +# endif + + if (m_webBrowser.isEmpty() + && !detectWebBrowser(desktopEnvironment(), true, &m_webBrowser)) { + qWarning("Unable to detect a web browser to launch '%s'", qPrintable(url.toString())); + return false; + } + return launch(m_webBrowser, url, xdgActivationToken); + }; + + if (QGuiApplication::platformName().startsWith("wayland"_L1)) { + runWithXdgActivationToken( + [openUrlInternal, url](const QString &token) { openUrlInternal(url, token); }); + + return true; + + } else { + return openUrlInternal(url, QString()); + } +} + +bool QGenericUnixServices::openDocument(const QUrl &url) +{ + auto openDocumentInternal = [this](const QUrl &url, const QString &xdgActivationToken) { + +# if QT_CONFIG(dbus) + if (checkNeedPortalSupport()) { + const QString parentWindow = QGuiApplication::focusWindow() + ? portalWindowIdentifier(QGuiApplication::focusWindow()) + : QString(); + QDBusError error = xdgDesktopPortalOpenFile(url, parentWindow, xdgActivationToken); + if (!error.isValid()) + return true; + } +# endif + + if (m_documentLauncher.isEmpty() + && !detectWebBrowser(desktopEnvironment(), false, &m_documentLauncher)) { + qWarning("Unable to detect a launcher for '%s'", qPrintable(url.toString())); + return false; + } + return launch(m_documentLauncher, url, xdgActivationToken); + }; + + if (QGuiApplication::platformName().startsWith("wayland"_L1)) { + runWithXdgActivationToken([openDocumentInternal, url](const QString &token) { + openDocumentInternal(url, token); + }); + + return true; + } else { + return openDocumentInternal(url, QString()); + } +} + +#else +QGenericUnixServices::QGenericUnixServices() = default; + +QByteArray QGenericUnixServices::desktopEnvironment() const +{ + return QByteArrayLiteral("UNKNOWN"); +} + +bool QGenericUnixServices::openUrl(const QUrl &url) +{ + Q_UNUSED(url); + qWarning("openUrl() not supported on this platform"); + return false; +} + +bool QGenericUnixServices::openDocument(const QUrl &url) +{ + Q_UNUSED(url); + qWarning("openDocument() not supported on this platform"); + return false; +} + +QPlatformServiceColorPicker *QGenericUnixServices::colorPicker(QWindow *parent) +{ + Q_UNUSED(parent); + return nullptr; +} + +#endif // QT_NO_MULTIPROCESS + +QString QGenericUnixServices::portalWindowIdentifier(QWindow *window) +{ + Q_UNUSED(window); + return QString(); +} + +bool QGenericUnixServices::hasCapability(Capability capability) const +{ + switch (capability) { + case Capability::ColorPicking: + return m_hasScreenshotPortalWithColorPicking; + } + return false; +} + +void QGenericUnixServices::setApplicationBadge(qint64 number) +{ +#if QT_CONFIG(dbus) + if (qGuiApp->desktopFileName().isEmpty()) { + qWarning("QGuiApplication::desktopFileName() is empty"); + return; + } + + + const QString launcherUrl = QStringLiteral("application://") + qGuiApp->desktopFileName() + QStringLiteral(".desktop"); + const qint64 count = qBound(0, number, 9999); + QVariantMap dbusUnityProperties; + + if (count > 0) { + dbusUnityProperties[QStringLiteral("count")] = count; + dbusUnityProperties[QStringLiteral("count-visible")] = true; + } else { + dbusUnityProperties[QStringLiteral("count-visible")] = false; + } + + auto signal = QDBusMessage::createSignal(QStringLiteral("/com/canonical/unity/launcherentry/") + + qGuiApp->applicationName(), QStringLiteral("com.canonical.Unity.LauncherEntry"), QStringLiteral("Update")); + + signal.setArguments({launcherUrl, dbusUnityProperties}); + + QDBusConnection::sessionBus().send(signal); +#else + Q_UNUSED(number) +#endif +} + +QT_END_NAMESPACE + +#include "qgenericunixservices.moc" diff --git a/src/gui/platform/unix/qgenericunixservices_p.h b/src/gui/platform/unix/qgenericunixservices_p.h new file mode 100644 index 0000000000..56e15103f7 --- /dev/null +++ b/src/gui/platform/unix/qgenericunixservices_p.h @@ -0,0 +1,49 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QGENERICUNIXDESKTOPSERVICES_H +#define QGENERICUNIXDESKTOPSERVICES_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <qpa/qplatformservices.h> +#include <QtCore/QString> +#include <QtCore/private/qglobal_p.h> + +QT_BEGIN_NAMESPACE + +class QWindow; + +class Q_GUI_EXPORT QGenericUnixServices : public QPlatformServices +{ +public: + QGenericUnixServices(); + + QByteArray desktopEnvironment() const override; + + bool hasCapability(Capability capability) const override; + bool openUrl(const QUrl &url) override; + bool openDocument(const QUrl &url) override; + QPlatformServiceColorPicker *colorPicker(QWindow *parent = nullptr) override; + + void setApplicationBadge(qint64 number); + virtual QString portalWindowIdentifier(QWindow *window); + +private: + QString m_webBrowser; + QString m_documentLauncher; + bool m_hasScreenshotPortalWithColorPicking = false; +}; + +QT_END_NAMESPACE + +#endif // QGENERICUNIXDESKTOPSERVICES_H diff --git a/src/gui/platform/unix/qgenericunixthemes.cpp b/src/gui/platform/unix/qgenericunixthemes.cpp new file mode 100644 index 0000000000..8a7f7cd6f7 --- /dev/null +++ b/src/gui/platform/unix/qgenericunixthemes.cpp @@ -0,0 +1,1524 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qgenericunixthemes_p.h" + +#include <QPalette> +#include <QFont> +#include <QGuiApplication> +#include <QDir> +#include <QFileInfo> +#include <QFile> +#include <QDebug> +#include <QHash> +#include <QLoggingCategory> +#include <QVariant> +#include <QStandardPaths> +#include <QStringList> +#if QT_CONFIG(mimetype) +#include <QMimeDatabase> +#endif +#if QT_CONFIG(settings) +#include <QSettings> +#endif + +#include <qpa/qplatformfontdatabase.h> // lcQpaFonts +#include <qpa/qplatformintegration.h> +#include <qpa/qplatformservices.h> +#include <qpa/qplatformdialoghelper.h> +#include <qpa/qplatformtheme_p.h> + +#include <private/qguiapplication_p.h> +#ifndef QT_NO_DBUS +#include <QDBusConnectionInterface> +#include <private/qdbusplatformmenu_p.h> +#include <private/qdbusmenubar_p.h> +#include <private/qflatmap_p.h> +#include <QJsonDocument> +#include <QJsonArray> +#include <QJsonObject> +#include <QJsonValue> +#include <QJsonParseError> +#endif +#if !defined(QT_NO_DBUS) && !defined(QT_NO_SYSTEMTRAYICON) +#include <private/qdbustrayicon_p.h> +#endif + +#include <algorithm> + +QT_BEGIN_NAMESPACE +#ifndef QT_NO_DBUS +Q_LOGGING_CATEGORY(lcQpaThemeDBus, "qt.qpa.theme.dbus") +#endif + +using namespace Qt::StringLiterals; + +Q_DECLARE_LOGGING_CATEGORY(qLcTray) + +ResourceHelper::ResourceHelper() +{ + std::fill(palettes, palettes + QPlatformTheme::NPalettes, static_cast<QPalette *>(nullptr)); + std::fill(fonts, fonts + QPlatformTheme::NFonts, static_cast<QFont *>(nullptr)); +} + +void ResourceHelper::clear() +{ + qDeleteAll(palettes, palettes + QPlatformTheme::NPalettes); + qDeleteAll(fonts, fonts + QPlatformTheme::NFonts); + std::fill(palettes, palettes + QPlatformTheme::NPalettes, static_cast<QPalette *>(nullptr)); + std::fill(fonts, fonts + QPlatformTheme::NFonts, static_cast<QFont *>(nullptr)); +} + +const char *QGenericUnixTheme::name = "generic"; + +// Default system font, corresponding to the value returned by 4.8 for +// XRender/FontConfig which we can now assume as default. +static const char defaultSystemFontNameC[] = "Sans Serif"; +static const char defaultFixedFontNameC[] = "monospace"; +enum { defaultSystemFontSize = 9 }; + +#if !defined(QT_NO_DBUS) && !defined(QT_NO_SYSTEMTRAYICON) +static bool shouldUseDBusTray() { + // There's no other tray implementation to fallback to on non-X11 + // and QDBusTrayIcon can register the icon on the fly after creation + if (QGuiApplication::platformName() != "xcb"_L1) + return true; + const bool result = QDBusMenuConnection().isWatcherRegistered(); + qCDebug(qLcTray) << "D-Bus tray available:" << result; + return result; +} +#endif + +static QString mouseCursorTheme() +{ + static QString themeName = qEnvironmentVariable("XCURSOR_THEME"); + return themeName; +} + +static QSize mouseCursorSize() +{ + constexpr int defaultCursorSize = 24; + static const int xCursorSize = qEnvironmentVariableIntValue("XCURSOR_SIZE"); + static const int s = xCursorSize > 0 ? xCursorSize : defaultCursorSize; + return QSize(s, s); +} + +#ifndef QT_NO_DBUS +static bool checkDBusGlobalMenuAvailable() +{ + const QDBusConnection connection = QDBusConnection::sessionBus(); + static const QString registrarService = QStringLiteral("com.canonical.AppMenu.Registrar"); + if (const auto iface = connection.interface()) + return iface->isServiceRegistered(registrarService); + return false; +} + +static bool isDBusGlobalMenuAvailable() +{ + static bool dbusGlobalMenuAvailable = checkDBusGlobalMenuAvailable(); + return dbusGlobalMenuAvailable; +} + +/*! + * \internal + * The QGenericUnixThemeDBusListener class listens to the SettingChanged DBus signal + * and translates it into combinations of the enums \c Provider and \c Setting. + * Upon construction, it logs success/failure of the DBus connection. + * + * The signal settingChanged delivers the normalized setting type and the new value as a string. + * It is emitted on known setting types only. + */ + +class QGenericUnixThemeDBusListener : public QObject +{ + Q_OBJECT + +public: + + enum class Provider { + Kde, + Gtk, + Gnome, + }; + Q_ENUM(Provider) + + enum class Setting { + Theme, + ApplicationStyle, + ColorScheme, + }; + Q_ENUM(Setting) + + QGenericUnixThemeDBusListener(); + QGenericUnixThemeDBusListener(const QString &service, const QString &path, + const QString &interface, const QString &signal); + +private Q_SLOTS: + void onSettingChanged(const QString &location, const QString &key, const QDBusVariant &value); + +Q_SIGNALS: + void settingChanged(QGenericUnixThemeDBusListener::Provider provider, + QGenericUnixThemeDBusListener::Setting setting, + const QString &value); + +private: + struct DBusKey + { + QString location; + QString key; + DBusKey(const QString &loc, const QString &k) : location(loc), key(k) {}; + bool operator<(const DBusKey &other) const + { + return location + key < other.location + other.key; + } + }; + + struct ChangeSignal + { + Provider provider; + Setting setting; + ChangeSignal(Provider p, Setting s) : provider(p), setting(s) {} + ChangeSignal() {} + }; + + // Json keys + static constexpr QLatin1StringView s_dbusLocation = QLatin1StringView("DBusLocation"); + static constexpr QLatin1StringView s_dbusKey = QLatin1StringView("DBusKey"); + static constexpr QLatin1StringView s_provider = QLatin1StringView("Provider"); + static constexpr QLatin1StringView s_setting = QLatin1StringView("Setting"); + static constexpr QLatin1StringView s_signals = QLatin1StringView("DbusSignals"); + static constexpr QLatin1StringView s_root = QLatin1StringView("Qt.qpa.DBusSignals"); + + QFlatMap <DBusKey, ChangeSignal> m_signalMap; + + void init(const QString &service, const QString &path, + const QString &interface, const QString &signal); + + std::optional<ChangeSignal> findSignal(const QString &location, const QString &key) const; + void populateSignalMap(); + void loadJson(const QString &fileName); + void saveJson(const QString &fileName) const; +}; + +QGenericUnixThemeDBusListener::QGenericUnixThemeDBusListener(const QString &service, + const QString &path, const QString &interface, const QString &signal) +{ + init (service, path, interface, signal); +} + +QGenericUnixThemeDBusListener::QGenericUnixThemeDBusListener() +{ + static constexpr QLatin1StringView service(""); + static constexpr QLatin1StringView path("/org/freedesktop/portal/desktop"); + static constexpr QLatin1StringView interface("org.freedesktop.portal.Settings"); + static constexpr QLatin1StringView signal("SettingChanged"); + + init (service, path, interface, signal); +} + +void QGenericUnixThemeDBusListener::init(const QString &service, const QString &path, + const QString &interface, const QString &signal) +{ + QDBusConnection dbus = QDBusConnection::sessionBus(); + const bool dBusRunning = dbus.isConnected(); + bool dBusSignalConnected = false; +#define LOG service << path << interface << signal; + + if (dBusRunning) { + populateSignalMap(); + qRegisterMetaType<QDBusVariant>(); + dBusSignalConnected = dbus.connect(service, path, interface, signal, this, + SLOT(onSettingChanged(QString,QString,QDBusVariant))); + } + + if (dBusSignalConnected) { + // Connection successful + qCDebug(lcQpaThemeDBus) << LOG; + } else { + if (dBusRunning) { + // DBus running, but connection failed + qCWarning(lcQpaThemeDBus) << "DBus connection failed:" << LOG; + } else { + // DBus not running + qCWarning(lcQpaThemeDBus) << "Session DBus not running."; + } + qCWarning(lcQpaThemeDBus) << "Application will not react to setting changes.\n" + << "Check your DBus installation."; + } +#undef LOG +} + +void QGenericUnixThemeDBusListener::loadJson(const QString &fileName) +{ + Q_ASSERT(!fileName.isEmpty()); +#define CHECK(cond, warning)\ + if (!cond) {\ + qCWarning(lcQpaThemeDBus) << fileName << warning << "Falling back to default.";\ + return;\ + } + +#define PARSE(var, enumeration, string)\ + enumeration var;\ + {\ + bool success;\ + const int val = QMetaEnum::fromType<enumeration>().keyToValue(string.toLatin1(), &success);\ + CHECK(success, "Parse Error: Invalid value" << string << "for" << #var);\ + var = static_cast<enumeration>(val);\ + } + + QFile file(fileName); + CHECK(file.exists(), fileName << "doesn't exist."); + CHECK(file.open(QIODevice::ReadOnly), "could not be opened for reading."); + + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error); + CHECK((error.error == QJsonParseError::NoError), error.errorString()); + CHECK(doc.isObject(), "Parse Error: Expected root object" << s_root); + + const QJsonObject &root = doc.object(); + CHECK(root.contains(s_root), "Parse Error: Expected root object" << s_root); + CHECK(root[s_root][s_signals].isArray(), "Parse Error: Expected array" << s_signals); + + const QJsonArray &sigs = root[s_root][s_signals].toArray(); + CHECK((sigs.count() > 0), "Parse Error: Found empty array" << s_signals); + + for (auto sig = sigs.constBegin(); sig != sigs.constEnd(); ++sig) { + CHECK(sig->isObject(), "Parse Error: Expected object array" << s_signals); + const QJsonObject &obj = sig->toObject(); + CHECK(obj.contains(s_dbusLocation), "Parse Error: Expected key" << s_dbusLocation); + CHECK(obj.contains(s_dbusKey), "Parse Error: Expected key" << s_dbusKey); + CHECK(obj.contains(s_provider), "Parse Error: Expected key" << s_provider); + CHECK(obj.contains(s_setting), "Parse Error: Expected key" << s_setting); + const QString &location = obj[s_dbusLocation].toString(); + const QString &key = obj[s_dbusKey].toString(); + const QString &providerString = obj[s_provider].toString(); + const QString &settingString = obj[s_setting].toString(); + PARSE(provider, Provider, providerString); + PARSE(setting, Setting, settingString); + const DBusKey dkey(location, key); + CHECK (!m_signalMap.contains(dkey), "Duplicate key" << location << key); + m_signalMap.insert(dkey, ChangeSignal(provider, setting)); + } +#undef PARSE +#undef CHECK + + if (m_signalMap.count() > 0) + qCInfo(lcQpaThemeDBus) << "Successfully imported" << fileName; + else + qCWarning(lcQpaThemeDBus) << "No data imported from" << fileName << "falling back to default."; + +#ifdef QT_DEBUG + const int count = m_signalMap.count(); + if (count == 0) + return; + + qCDebug(lcQpaThemeDBus) << "Listening to" << count << "signals:"; + for (auto it = m_signalMap.constBegin(); it != m_signalMap.constEnd(); ++it) { + qDebug() << it.key().key << it.key().location << "mapped to" + << it.value().provider << it.value().setting; + } + +#endif +} + +void QGenericUnixThemeDBusListener::saveJson(const QString &fileName) const +{ + Q_ASSERT(!m_signalMap.isEmpty()); + Q_ASSERT(!fileName.isEmpty()); + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly)) { + qCWarning(lcQpaThemeDBus) << fileName << "could not be opened for writing."; + return; + } + + QJsonArray sigs; + for (auto sig = m_signalMap.constBegin(); sig != m_signalMap.constEnd(); ++sig) { + const DBusKey &dkey = sig.key(); + const ChangeSignal &csig = sig.value(); + QJsonObject obj; + obj[s_dbusLocation] = dkey.location; + obj[s_dbusKey] = dkey.key; + obj[s_provider] = QLatin1StringView(QMetaEnum::fromType<Provider>() + .valueToKey(static_cast<int>(csig.provider))); + obj[s_setting] = QLatin1StringView(QMetaEnum::fromType<Setting>() + .valueToKey(static_cast<int>(csig.setting))); + sigs.append(obj); + } + QJsonObject obj; + obj[s_signals] = sigs; + QJsonObject root; + root[s_root] = obj; + QJsonDocument doc(root); + file.write(doc.toJson()); + file.close(); +} + +void QGenericUnixThemeDBusListener::populateSignalMap() +{ + m_signalMap.clear(); + const QString &loadJsonFile = qEnvironmentVariable("QT_QPA_DBUS_SIGNALS"); + if (!loadJsonFile.isEmpty()) + loadJson(loadJsonFile); + if (!m_signalMap.isEmpty()) + return; + + m_signalMap.insert(DBusKey("org.kde.kdeglobals.KDE"_L1, "widgetStyle"_L1), + ChangeSignal(Provider::Kde, Setting::ApplicationStyle)); + + m_signalMap.insert(DBusKey("org.kde.kdeglobals.General"_L1, "ColorScheme"_L1), + ChangeSignal(Provider::Kde, Setting::Theme)); + + m_signalMap.insert(DBusKey("org.gnome.desktop.interface"_L1, "gtk-theme"_L1), + ChangeSignal(Provider::Gtk, Setting::Theme)); + + m_signalMap.insert(DBusKey("org.freedesktop.appearance"_L1, "color-scheme"_L1), + ChangeSignal(Provider::Gnome, Setting::ColorScheme)); + + const QString &saveJsonFile = qEnvironmentVariable("QT_QPA_DBUS_SIGNALS_SAVE"); + if (!saveJsonFile.isEmpty()) + saveJson(saveJsonFile); +} + +std::optional<QGenericUnixThemeDBusListener::ChangeSignal> + QGenericUnixThemeDBusListener::findSignal(const QString &location, const QString &key) const +{ + const DBusKey dkey(location, key); + std::optional<QGenericUnixThemeDBusListener::ChangeSignal> ret; + if (m_signalMap.contains(dkey)) + ret.emplace(m_signalMap.value(dkey)); + + return ret; +} + +void QGenericUnixThemeDBusListener::onSettingChanged(const QString &location, const QString &key, const QDBusVariant &value) +{ + auto sig = findSignal(location, key); + if (!sig.has_value()) + return; + + emit settingChanged(sig.value().provider, sig.value().setting, value.variant().toString()); +} + +#endif //QT_NO_DBUS + +class QGenericUnixThemePrivate : public QPlatformThemePrivate +{ +public: + QGenericUnixThemePrivate() + : QPlatformThemePrivate() + , systemFont(QLatin1StringView(defaultSystemFontNameC), defaultSystemFontSize) + , fixedFont(QLatin1StringView(defaultFixedFontNameC), systemFont.pointSize()) + { + fixedFont.setStyleHint(QFont::TypeWriter); + qCDebug(lcQpaFonts) << "default fonts: system" << systemFont << "fixed" << fixedFont; + } + + const QFont systemFont; + QFont fixedFont; +}; + +QGenericUnixTheme::QGenericUnixTheme() + : QPlatformTheme(new QGenericUnixThemePrivate()) +{ +} + +const QFont *QGenericUnixTheme::font(Font type) const +{ + Q_D(const QGenericUnixTheme); + switch (type) { + case QPlatformTheme::SystemFont: + return &d->systemFont; + case QPlatformTheme::FixedFont: + return &d->fixedFont; + default: + return nullptr; + } +} + +// Helper to return the icon theme paths from XDG. +QStringList QGenericUnixTheme::xdgIconThemePaths() +{ + QStringList paths; + // Add home directory first in search path + const QFileInfo homeIconDir(QDir::homePath() + "/.icons"_L1); + if (homeIconDir.isDir()) + paths.prepend(homeIconDir.absoluteFilePath()); + + paths.append(QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, + QStringLiteral("icons"), + QStandardPaths::LocateDirectory)); + + return paths; +} + +QStringList QGenericUnixTheme::iconFallbackPaths() +{ + QStringList paths; + const QFileInfo pixmapsIconsDir(QStringLiteral("/usr/share/pixmaps")); + if (pixmapsIconsDir.isDir()) + paths.append(pixmapsIconsDir.absoluteFilePath()); + + return paths; +} + +#ifndef QT_NO_DBUS +QPlatformMenuBar *QGenericUnixTheme::createPlatformMenuBar() const +{ + if (isDBusGlobalMenuAvailable()) + return new QDBusMenuBar(); + return nullptr; +} +#endif + +#if !defined(QT_NO_DBUS) && !defined(QT_NO_SYSTEMTRAYICON) +QPlatformSystemTrayIcon *QGenericUnixTheme::createPlatformSystemTrayIcon() const +{ + if (shouldUseDBusTray()) + return new QDBusTrayIcon(); + return nullptr; +} +#endif + +QVariant QGenericUnixTheme::themeHint(ThemeHint hint) const +{ + switch (hint) { + case QPlatformTheme::SystemIconFallbackThemeName: + return QVariant(QString(QStringLiteral("hicolor"))); + case QPlatformTheme::IconThemeSearchPaths: + return xdgIconThemePaths(); + case QPlatformTheme::IconFallbackSearchPaths: + return iconFallbackPaths(); + case QPlatformTheme::DialogButtonBoxButtonsHaveIcons: + return QVariant(true); + case QPlatformTheme::StyleNames: { + QStringList styleNames; + styleNames << QStringLiteral("Fusion") << QStringLiteral("Windows"); + return QVariant(styleNames); + } + case QPlatformTheme::KeyboardScheme: + return QVariant(int(X11KeyboardScheme)); + case QPlatformTheme::UiEffects: + return QVariant(int(HoverEffect)); + case QPlatformTheme::MouseCursorTheme: + return QVariant(mouseCursorTheme()); + case QPlatformTheme::MouseCursorSize: + return QVariant(mouseCursorSize()); + default: + break; + } + return QPlatformTheme::themeHint(hint); +} + +// Helper functions for implementing QPlatformTheme::fileIcon() for XDG icon themes. +static QList<QSize> availableXdgFileIconSizes() +{ + return QIcon::fromTheme(QStringLiteral("inode-directory")).availableSizes(); +} + +#if QT_CONFIG(mimetype) +static QIcon xdgFileIcon(const QFileInfo &fileInfo) +{ + QMimeDatabase mimeDatabase; + QMimeType mimeType = mimeDatabase.mimeTypeForFile(fileInfo); + if (!mimeType.isValid()) + return QIcon(); + const QString &iconName = mimeType.iconName(); + if (!iconName.isEmpty()) { + const QIcon icon = QIcon::fromTheme(iconName); + if (!icon.isNull()) + return icon; + } + const QString &genericIconName = mimeType.genericIconName(); + return genericIconName.isEmpty() ? QIcon() : QIcon::fromTheme(genericIconName); +} +#endif + +#if QT_CONFIG(settings) +class QKdeThemePrivate : public QPlatformThemePrivate +{ + +public: + enum class KdeSettingType { + Root, + KDE, + Icons, + ToolBarIcons, + ToolBarStyle, + Fonts, + Colors, + }; + + enum class KdeSetting { + WidgetStyle, + ColorScheme, + SingleClick, + ShowIconsOnPushButtons, + IconTheme, + ToolBarIconSize, + ToolButtonStyle, + WheelScrollLines, + DoubleClickInterval, + StartDragDistance, + StartDragTime, + CursorBlinkRate, + Font, + Fixed, + MenuFont, + ToolBarFont, + ButtonBackground, + WindowBackground, + ViewForeground, + WindowForeground, + ViewBackground, + SelectionBackground, + SelectionForeground, + ViewBackgroundAlternate, + ButtonForeground, + ViewForegroundLink, + ViewForegroundVisited, + TooltipBackground, + TooltipForeground, + }; + + QKdeThemePrivate(const QStringList &kdeDirs, int kdeVersion); + + static QString kdeGlobals(const QString &kdeDir, int kdeVersion) + { + if (kdeVersion > 4) + return kdeDir + "/kdeglobals"_L1; + return kdeDir + "/share/config/kdeglobals"_L1; + } + + void refresh(); + static QVariant readKdeSetting(KdeSetting s, const QStringList &kdeDirs, int kdeVersion, QHash<QString, QSettings*> &settings); + QVariant readKdeSetting(KdeSetting s) const; + void clearKdeSettings() const; + static void readKdeSystemPalette(const QStringList &kdeDirs, int kdeVersion, QHash<QString, QSettings*> &kdeSettings, QPalette *pal); + static QFont *kdeFont(const QVariant &fontValue); + static QStringList kdeIconThemeSearchPaths(const QStringList &kdeDirs); + + const QStringList kdeDirs; + const int kdeVersion; + + ResourceHelper resources; + QString iconThemeName; + QString iconFallbackThemeName; + QStringList styleNames; + int toolButtonStyle = Qt::ToolButtonTextBesideIcon; + int toolBarIconSize = 0; + bool singleClick = true; + bool showIconsOnPushButtons = true; + int wheelScrollLines = 3; + int doubleClickInterval = 400; + int startDragDist = 10; + int startDragTime = 500; + int cursorBlinkRate = 1000; + Qt::ColorScheme m_colorScheme = Qt::ColorScheme::Unknown; + void updateColorScheme(const QString &themeName); + +private: + mutable QHash<QString, QSettings *> kdeSettings; +#ifndef QT_NO_DBUS + std::unique_ptr<QGenericUnixThemeDBusListener> dbus; + bool initDbus(); + void settingChangedHandler(QGenericUnixThemeDBusListener::Provider provider, + QGenericUnixThemeDBusListener::Setting setting, + const QString &value); +#endif // QT_NO_DBUS +}; + +#ifndef QT_NO_DBUS +void QKdeThemePrivate::settingChangedHandler(QGenericUnixThemeDBusListener::Provider provider, + QGenericUnixThemeDBusListener::Setting setting, + const QString &value) +{ + if (provider != QGenericUnixThemeDBusListener::Provider::Kde) + return; + + switch (setting) { + case QGenericUnixThemeDBusListener::Setting::ColorScheme: + qCDebug(lcQpaThemeDBus) << "KDE color theme changed to:" << value; + break; + case QGenericUnixThemeDBusListener::Setting::Theme: + qCDebug(lcQpaThemeDBus) << "KDE global theme changed to:" << value; + break; + case QGenericUnixThemeDBusListener::Setting::ApplicationStyle: + qCDebug(lcQpaThemeDBus) << "KDE application style changed to:" << value; + break; + } + + refresh(); +} + +bool QKdeThemePrivate::initDbus() +{ + dbus.reset(new QGenericUnixThemeDBusListener()); + Q_ASSERT(dbus); + + // Wrap slot in a lambda to avoid inheriting QKdeThemePrivate from QObject + auto wrapper = [this](QGenericUnixThemeDBusListener::Provider provider, + QGenericUnixThemeDBusListener::Setting setting, + const QString &value) { + settingChangedHandler(provider, setting, value); + }; + + return QObject::connect(dbus.get(), &QGenericUnixThemeDBusListener::settingChanged, dbus.get(), wrapper); +} +#endif // QT_NO_DBUS + +QKdeThemePrivate::QKdeThemePrivate(const QStringList &kdeDirs, int kdeVersion) + : kdeDirs(kdeDirs), kdeVersion(kdeVersion) +{ +#ifndef QT_NO_DBUS + initDbus(); +#endif // QT_NO_DBUS +} + +static constexpr QLatin1StringView settingsPrefix(QKdeThemePrivate::KdeSettingType type) +{ + switch (type) { + case QKdeThemePrivate::KdeSettingType::Root: + return QLatin1StringView(); + case QKdeThemePrivate::KdeSettingType::KDE: + return QLatin1StringView("KDE/"); + case QKdeThemePrivate::KdeSettingType::Fonts: + return QLatin1StringView(); + case QKdeThemePrivate::KdeSettingType::Colors: + return QLatin1StringView("Colors:"); + case QKdeThemePrivate::KdeSettingType::Icons: + return QLatin1StringView("Icons/"); + case QKdeThemePrivate::KdeSettingType::ToolBarIcons: + return QLatin1StringView("ToolbarIcons/"); + case QKdeThemePrivate::KdeSettingType::ToolBarStyle: + return QLatin1StringView("Toolbar style/"); + } + Q_UNREACHABLE_RETURN(QLatin1StringView()); +} + +static constexpr QKdeThemePrivate::KdeSettingType settingsType(QKdeThemePrivate::KdeSetting setting) +{ +#define CASE(s, type) case QKdeThemePrivate::KdeSetting::s:\ + return QKdeThemePrivate::KdeSettingType::type + + switch (setting) { + CASE(WidgetStyle, Root); + CASE(ColorScheme, Root); + CASE(SingleClick, KDE); + CASE(ShowIconsOnPushButtons, KDE); + CASE(IconTheme, Icons); + CASE(ToolBarIconSize, ToolBarIcons); + CASE(ToolButtonStyle, ToolBarStyle); + CASE(WheelScrollLines, KDE); + CASE(DoubleClickInterval, KDE); + CASE(StartDragDistance, KDE); + CASE(StartDragTime, KDE); + CASE(CursorBlinkRate, KDE); + CASE(Font, Root); + CASE(Fixed, Root); + CASE(MenuFont, Root); + CASE(ToolBarFont, Root); + CASE(ButtonBackground, Colors); + CASE(WindowBackground, Colors); + CASE(ViewForeground, Colors); + CASE(WindowForeground, Colors); + CASE(ViewBackground, Colors); + CASE(SelectionBackground, Colors); + CASE(SelectionForeground, Colors); + CASE(ViewBackgroundAlternate, Colors); + CASE(ButtonForeground, Colors); + CASE(ViewForegroundLink, Colors); + CASE(ViewForegroundVisited, Colors); + CASE(TooltipBackground, Colors); + CASE(TooltipForeground, Colors); + }; + Q_UNREACHABLE_RETURN(QKdeThemePrivate::KdeSettingType::Root); +} +#undef CASE + +static constexpr QLatin1StringView settingsKey(QKdeThemePrivate::KdeSetting setting) +{ + switch (setting) { + case QKdeThemePrivate::KdeSetting::WidgetStyle: + return QLatin1StringView("widgetStyle"); + case QKdeThemePrivate::KdeSetting::ColorScheme: + return QLatin1StringView("ColorScheme"); + case QKdeThemePrivate::KdeSetting::SingleClick: + return QLatin1StringView("SingleClick"); + case QKdeThemePrivate::KdeSetting::ShowIconsOnPushButtons: + return QLatin1StringView("ShowIconsOnPushButtons"); + case QKdeThemePrivate::KdeSetting::IconTheme: + return QLatin1StringView("Theme"); + case QKdeThemePrivate::KdeSetting::ToolBarIconSize: + return QLatin1StringView("Size"); + case QKdeThemePrivate::KdeSetting::ToolButtonStyle: + return QLatin1StringView("ToolButtonStyle"); + case QKdeThemePrivate::KdeSetting::WheelScrollLines: + return QLatin1StringView("WheelScrollLines"); + case QKdeThemePrivate::KdeSetting::DoubleClickInterval: + return QLatin1StringView("DoubleClickInterval"); + case QKdeThemePrivate::KdeSetting::StartDragDistance: + return QLatin1StringView("StartDragDist"); + case QKdeThemePrivate::KdeSetting::StartDragTime: + return QLatin1StringView("StartDragTime"); + case QKdeThemePrivate::KdeSetting::CursorBlinkRate: + return QLatin1StringView("CursorBlinkRate"); + case QKdeThemePrivate::KdeSetting::Font: + return QLatin1StringView("font"); + case QKdeThemePrivate::KdeSetting::Fixed: + return QLatin1StringView("fixed"); + case QKdeThemePrivate::KdeSetting::MenuFont: + return QLatin1StringView("menuFont"); + case QKdeThemePrivate::KdeSetting::ToolBarFont: + return QLatin1StringView("toolBarFont"); + case QKdeThemePrivate::KdeSetting::ButtonBackground: + return QLatin1StringView("Button/BackgroundNormal"); + case QKdeThemePrivate::KdeSetting::WindowBackground: + return QLatin1StringView("Window/BackgroundNormal"); + case QKdeThemePrivate::KdeSetting::ViewForeground: + return QLatin1StringView("View/ForegroundNormal"); + case QKdeThemePrivate::KdeSetting::WindowForeground: + return QLatin1StringView("Window/ForegroundNormal"); + case QKdeThemePrivate::KdeSetting::ViewBackground: + return QLatin1StringView("View/BackgroundNormal"); + case QKdeThemePrivate::KdeSetting::SelectionBackground: + return QLatin1StringView("Selection/BackgroundNormal"); + case QKdeThemePrivate::KdeSetting::SelectionForeground: + return QLatin1StringView("Selection/ForegroundNormal"); + case QKdeThemePrivate::KdeSetting::ViewBackgroundAlternate: + return QLatin1StringView("View/BackgroundAlternate"); + case QKdeThemePrivate::KdeSetting::ButtonForeground: + return QLatin1StringView("Button/ForegroundNormal"); + case QKdeThemePrivate::KdeSetting::ViewForegroundLink: + return QLatin1StringView("View/ForegroundLink"); + case QKdeThemePrivate::KdeSetting::ViewForegroundVisited: + return QLatin1StringView("View/ForegroundVisited"); + case QKdeThemePrivate::KdeSetting::TooltipBackground: + return QLatin1StringView("Tooltip/BackgroundNormal"); + case QKdeThemePrivate::KdeSetting::TooltipForeground: + return QLatin1StringView("Tooltip/ForegroundNormal"); + }; + Q_UNREACHABLE_RETURN(QLatin1StringView()); +} + +void QKdeThemePrivate::refresh() +{ + resources.clear(); + clearKdeSettings(); + + toolButtonStyle = Qt::ToolButtonTextBesideIcon; + toolBarIconSize = 0; + styleNames.clear(); + if (kdeVersion >= 5) + styleNames << QStringLiteral("breeze"); + styleNames << QStringLiteral("Oxygen") << QStringLiteral("Fusion") << QStringLiteral("windows"); + if (kdeVersion >= 5) + iconFallbackThemeName = iconThemeName = QStringLiteral("breeze"); + else + iconFallbackThemeName = iconThemeName = QStringLiteral("oxygen"); + + QPalette systemPalette = QPalette(); + readKdeSystemPalette(kdeDirs, kdeVersion, kdeSettings, &systemPalette); + resources.palettes[QPlatformTheme::SystemPalette] = new QPalette(systemPalette); + //## TODO tooltip color + + const QVariant styleValue = readKdeSetting(KdeSetting::WidgetStyle); + if (styleValue.isValid()) { + const QString style = styleValue.toString(); + if (style != styleNames.front()) + styleNames.push_front(style); + } + + const QVariant colorScheme = readKdeSetting(KdeSetting::ColorScheme); + + updateColorScheme(colorScheme.toString()); + + const QVariant singleClickValue = readKdeSetting(KdeSetting::SingleClick); + if (singleClickValue.isValid()) + singleClick = singleClickValue.toBool(); + + const QVariant showIconsOnPushButtonsValue = readKdeSetting(KdeSetting::ShowIconsOnPushButtons); + if (showIconsOnPushButtonsValue.isValid()) + showIconsOnPushButtons = showIconsOnPushButtonsValue.toBool(); + + const QVariant themeValue = readKdeSetting(KdeSetting::IconTheme); + if (themeValue.isValid()) + iconThemeName = themeValue.toString(); + + const QVariant toolBarIconSizeValue = readKdeSetting(KdeSetting::ToolBarIconSize); + if (toolBarIconSizeValue.isValid()) + toolBarIconSize = toolBarIconSizeValue.toInt(); + + const QVariant toolbarStyleValue = readKdeSetting(KdeSetting::ToolButtonStyle); + if (toolbarStyleValue.isValid()) { + const QString toolBarStyle = toolbarStyleValue.toString(); + if (toolBarStyle == "TextBesideIcon"_L1) + toolButtonStyle = Qt::ToolButtonTextBesideIcon; + else if (toolBarStyle == "TextOnly"_L1) + toolButtonStyle = Qt::ToolButtonTextOnly; + else if (toolBarStyle == "TextUnderIcon"_L1) + toolButtonStyle = Qt::ToolButtonTextUnderIcon; + } + + const QVariant wheelScrollLinesValue = readKdeSetting(KdeSetting::WheelScrollLines); + if (wheelScrollLinesValue.isValid()) + wheelScrollLines = wheelScrollLinesValue.toInt(); + + const QVariant doubleClickIntervalValue = readKdeSetting(KdeSetting::DoubleClickInterval); + if (doubleClickIntervalValue.isValid()) + doubleClickInterval = doubleClickIntervalValue.toInt(); + + const QVariant startDragDistValue = readKdeSetting(KdeSetting::StartDragDistance); + if (startDragDistValue.isValid()) + startDragDist = startDragDistValue.toInt(); + + const QVariant startDragTimeValue = readKdeSetting(KdeSetting::StartDragTime); + if (startDragTimeValue.isValid()) + startDragTime = startDragTimeValue.toInt(); + + const QVariant cursorBlinkRateValue = readKdeSetting(KdeSetting::CursorBlinkRate); + if (cursorBlinkRateValue.isValid()) { + cursorBlinkRate = cursorBlinkRateValue.toInt(); + cursorBlinkRate = cursorBlinkRate > 0 ? qBound(200, cursorBlinkRate, 2000) : 0; + } + + // Read system font, ignore 'smallestReadableFont' + if (QFont *systemFont = kdeFont(readKdeSetting(KdeSetting::Font))) + resources.fonts[QPlatformTheme::SystemFont] = systemFont; + else + resources.fonts[QPlatformTheme::SystemFont] = new QFont(QLatin1StringView(defaultSystemFontNameC), defaultSystemFontSize); + + if (QFont *fixedFont = kdeFont(readKdeSetting(KdeSetting::Fixed))) { + resources.fonts[QPlatformTheme::FixedFont] = fixedFont; + } else { + fixedFont = new QFont(QLatin1StringView(defaultFixedFontNameC), defaultSystemFontSize); + fixedFont->setStyleHint(QFont::TypeWriter); + resources.fonts[QPlatformTheme::FixedFont] = fixedFont; + } + + if (QFont *menuFont = kdeFont(readKdeSetting(KdeSetting::MenuFont))) { + resources.fonts[QPlatformTheme::MenuFont] = menuFont; + resources.fonts[QPlatformTheme::MenuBarFont] = new QFont(*menuFont); + } + + if (QFont *toolBarFont = kdeFont(readKdeSetting(KdeSetting::ToolBarFont))) + resources.fonts[QPlatformTheme::ToolButtonFont] = toolBarFont; + + QWindowSystemInterface::handleThemeChange(); + + qCDebug(lcQpaFonts) << "default fonts: system" << resources.fonts[QPlatformTheme::SystemFont] + << "fixed" << resources.fonts[QPlatformTheme::FixedFont]; + qDeleteAll(kdeSettings); +} + +QVariant QKdeThemePrivate::readKdeSetting(KdeSetting s, const QStringList &kdeDirs, int kdeVersion, QHash<QString, QSettings*> &kdeSettings) +{ + for (const QString &kdeDir : kdeDirs) { + QSettings *settings = kdeSettings.value(kdeDir); + if (!settings) { + const QString kdeGlobalsPath = kdeGlobals(kdeDir, kdeVersion); + if (QFileInfo(kdeGlobalsPath).isReadable()) { + settings = new QSettings(kdeGlobalsPath, QSettings::IniFormat); + kdeSettings.insert(kdeDir, settings); + } + } + if (settings) { + const QString key = settingsPrefix(settingsType(s)) + settingsKey(s); + const QVariant value = settings->value(key); + if (value.isValid()) + return value; + } + } + return QVariant(); +} + +QVariant QKdeThemePrivate::readKdeSetting(KdeSetting s) const +{ + return readKdeSetting(s, kdeDirs, kdeVersion, kdeSettings); +} + +void QKdeThemePrivate::clearKdeSettings() const +{ + kdeSettings.clear(); +} + +// Reads the color from the KDE configuration, and store it in the +// palette with the given color role if found. +static inline bool kdeColor(QPalette *pal, QPalette::ColorRole role, const QVariant &value) +{ + if (!value.isValid()) + return false; + const QStringList values = value.toStringList(); + if (values.size() != 3) + return false; + pal->setBrush(role, QColor(values.at(0).toInt(), values.at(1).toInt(), values.at(2).toInt())); + return true; +} + +void QKdeThemePrivate::readKdeSystemPalette(const QStringList &kdeDirs, int kdeVersion, QHash<QString, QSettings*> &kdeSettings, QPalette *pal) +{ + if (!kdeColor(pal, QPalette::Button, readKdeSetting(KdeSetting::ButtonBackground, kdeDirs, kdeVersion, kdeSettings))) { + // kcolorscheme.cpp: SetDefaultColors + const QColor defaultWindowBackground(214, 210, 208); + const QColor defaultButtonBackground(223, 220, 217); + *pal = QPalette(defaultButtonBackground, defaultWindowBackground); + return; + } + + kdeColor(pal, QPalette::Window, readKdeSetting(KdeSetting::WindowBackground, kdeDirs, kdeVersion, kdeSettings)); + kdeColor(pal, QPalette::Text, readKdeSetting(KdeSetting::ViewForeground, kdeDirs, kdeVersion, kdeSettings)); + kdeColor(pal, QPalette::WindowText, readKdeSetting(KdeSetting::WindowForeground, kdeDirs, kdeVersion, kdeSettings)); + kdeColor(pal, QPalette::Base, readKdeSetting(KdeSetting::ViewBackground, kdeDirs, kdeVersion, kdeSettings)); + kdeColor(pal, QPalette::Highlight, readKdeSetting(KdeSetting::SelectionBackground, kdeDirs, kdeVersion, kdeSettings)); + kdeColor(pal, QPalette::HighlightedText, readKdeSetting(KdeSetting::SelectionForeground, kdeDirs, kdeVersion, kdeSettings)); + kdeColor(pal, QPalette::AlternateBase, readKdeSetting(KdeSetting::ViewBackgroundAlternate, kdeDirs, kdeVersion, kdeSettings)); + kdeColor(pal, QPalette::ButtonText, readKdeSetting(KdeSetting::ButtonForeground, kdeDirs, kdeVersion, kdeSettings)); + kdeColor(pal, QPalette::Link, readKdeSetting(KdeSetting::ViewForegroundLink, kdeDirs, kdeVersion, kdeSettings)); + kdeColor(pal, QPalette::LinkVisited, readKdeSetting(KdeSetting::ViewForegroundVisited, kdeDirs, kdeVersion, kdeSettings)); + kdeColor(pal, QPalette::ToolTipBase, readKdeSetting(KdeSetting::TooltipBackground, kdeDirs, kdeVersion, kdeSettings)); + kdeColor(pal, QPalette::ToolTipText, readKdeSetting(KdeSetting::TooltipForeground, kdeDirs, kdeVersion, kdeSettings)); + + // The above code sets _all_ color roles to "normal" colors. In KDE, the disabled + // color roles are calculated by applying various effects described in kdeglobals. + // We use a bit simpler approach here, similar logic than in qt_palette_from_color(). + const QColor button = pal->color(QPalette::Button); + int h, s, v; + button.getHsv(&h, &s, &v); + + const QBrush whiteBrush = QBrush(Qt::white); + const QBrush buttonBrush = QBrush(button); + const QBrush buttonBrushDark = QBrush(button.darker(v > 128 ? 200 : 50)); + const QBrush buttonBrushDark150 = QBrush(button.darker(v > 128 ? 150 : 75)); + const QBrush buttonBrushLight150 = QBrush(button.lighter(v > 128 ? 150 : 75)); + const QBrush buttonBrushLight = QBrush(button.lighter(v > 128 ? 200 : 50)); + + pal->setBrush(QPalette::Disabled, QPalette::WindowText, buttonBrushDark); + pal->setBrush(QPalette::Disabled, QPalette::ButtonText, buttonBrushDark); + pal->setBrush(QPalette::Disabled, QPalette::Button, buttonBrush); + pal->setBrush(QPalette::Disabled, QPalette::Text, buttonBrushDark); + pal->setBrush(QPalette::Disabled, QPalette::BrightText, whiteBrush); + pal->setBrush(QPalette::Disabled, QPalette::Base, buttonBrush); + pal->setBrush(QPalette::Disabled, QPalette::Window, buttonBrush); + pal->setBrush(QPalette::Disabled, QPalette::Highlight, buttonBrushDark150); + pal->setBrush(QPalette::Disabled, QPalette::HighlightedText, buttonBrushLight150); + + // set calculated colors for all groups + pal->setBrush(QPalette::Light, buttonBrushLight); + pal->setBrush(QPalette::Midlight, buttonBrushLight150); + pal->setBrush(QPalette::Mid, buttonBrushDark150); + pal->setBrush(QPalette::Dark, buttonBrushDark); +} + +/*! + \class QKdeTheme + \brief QKdeTheme is a theme implementation for the KDE desktop (version 4 or higher). + \since 5.0 + \internal + \ingroup qpa +*/ + +const char *QKdeTheme::name = "kde"; + +QKdeTheme::QKdeTheme(const QStringList& kdeDirs, int kdeVersion) + : QPlatformTheme(new QKdeThemePrivate(kdeDirs,kdeVersion)) +{ + d_func()->refresh(); +} + +QFont *QKdeThemePrivate::kdeFont(const QVariant &fontValue) +{ + if (fontValue.isValid()) { + // Read font value: Might be a QStringList as KDE stores fonts without quotes. + // Also retrieve the family for the constructor since we cannot use the + // default constructor of QFont, which accesses QGuiApplication::systemFont() + // causing recursion. + QString fontDescription; + QString fontFamily; + if (fontValue.userType() == QMetaType::QStringList) { + const QStringList list = fontValue.toStringList(); + if (!list.isEmpty()) { + fontFamily = list.first(); + fontDescription = list.join(u','); + } + } else { + fontDescription = fontFamily = fontValue.toString(); + } + if (!fontDescription.isEmpty()) { + QFont font(fontFamily); + if (font.fromString(fontDescription)) + return new QFont(font); + } + } + return nullptr; +} + + +QStringList QKdeThemePrivate::kdeIconThemeSearchPaths(const QStringList &kdeDirs) +{ + QStringList paths = QGenericUnixTheme::xdgIconThemePaths(); + const QString iconPath = QStringLiteral("/share/icons"); + for (const QString &candidate : kdeDirs) { + const QFileInfo fi(candidate + iconPath); + if (fi.isDir()) + paths.append(fi.absoluteFilePath()); + } + return paths; +} + +QVariant QKdeTheme::themeHint(QPlatformTheme::ThemeHint hint) const +{ + Q_D(const QKdeTheme); + switch (hint) { + case QPlatformTheme::UseFullScreenForPopupMenu: + return QVariant(true); + case QPlatformTheme::DialogButtonBoxButtonsHaveIcons: + return QVariant(d->showIconsOnPushButtons); + case QPlatformTheme::DialogButtonBoxLayout: + return QVariant(QPlatformDialogHelper::KdeLayout); + case QPlatformTheme::ToolButtonStyle: + return QVariant(d->toolButtonStyle); + case QPlatformTheme::ToolBarIconSize: + return QVariant(d->toolBarIconSize); + case QPlatformTheme::SystemIconThemeName: + return QVariant(d->iconThemeName); + case QPlatformTheme::SystemIconFallbackThemeName: + return QVariant(d->iconFallbackThemeName); + case QPlatformTheme::IconThemeSearchPaths: + return QVariant(d->kdeIconThemeSearchPaths(d->kdeDirs)); + case QPlatformTheme::IconPixmapSizes: + return QVariant::fromValue(availableXdgFileIconSizes()); + case QPlatformTheme::StyleNames: + return QVariant(d->styleNames); + case QPlatformTheme::KeyboardScheme: + return QVariant(int(KdeKeyboardScheme)); + case QPlatformTheme::ItemViewActivateItemOnSingleClick: + return QVariant(d->singleClick); + case QPlatformTheme::WheelScrollLines: + return QVariant(d->wheelScrollLines); + case QPlatformTheme::MouseDoubleClickInterval: + return QVariant(d->doubleClickInterval); + case QPlatformTheme::StartDragTime: + return QVariant(d->startDragTime); + case QPlatformTheme::StartDragDistance: + return QVariant(d->startDragDist); + case QPlatformTheme::CursorFlashTime: + return QVariant(d->cursorBlinkRate); + case QPlatformTheme::UiEffects: + return QVariant(int(HoverEffect)); + case QPlatformTheme::MouseCursorTheme: + return QVariant(mouseCursorTheme()); + case QPlatformTheme::MouseCursorSize: + return QVariant(mouseCursorSize()); + default: + break; + } + return QPlatformTheme::themeHint(hint); +} + +QIcon QKdeTheme::fileIcon(const QFileInfo &fileInfo, QPlatformTheme::IconOptions) const +{ +#if QT_CONFIG(mimetype) + return xdgFileIcon(fileInfo); +#else + Q_UNUSED(fileInfo); + return QIcon(); +#endif +} + +Qt::ColorScheme QKdeTheme::colorScheme() const +{ + return d_func()->m_colorScheme; +} + +/*! + \internal + \brief QKdeTheme::updateColorScheme - guess and set a color scheme for unix themes. + KDE themes do not have a color scheme property. + The key words "dark" or "light" are usually part of the theme name. + This is, however, not a mandatory convention. + + If \param themeName contains a valid key word, the respective color scheme is set. + If it doesn't, the color scheme is heuristically determined by comparing text and base color + of the system palette. + */ +void QKdeThemePrivate::updateColorScheme(const QString &themeName) +{ + if (themeName.contains(QLatin1StringView("light"), Qt::CaseInsensitive)) { + m_colorScheme = Qt::ColorScheme::Light; + return; + } + if (themeName.contains(QLatin1StringView("dark"), Qt::CaseInsensitive)) { + m_colorScheme = Qt::ColorScheme::Dark; + return; + } + + if (systemPalette) { + if (systemPalette->text().color().lightness() < systemPalette->base().color().lightness()) { + m_colorScheme = Qt::ColorScheme::Light; + return; + } + if (systemPalette->text().color().lightness() > systemPalette->base().color().lightness()) { + m_colorScheme = Qt::ColorScheme::Dark; + return; + } + } + + m_colorScheme = Qt::ColorScheme::Unknown; +} + +const QPalette *QKdeTheme::palette(Palette type) const +{ + Q_D(const QKdeTheme); + return d->resources.palettes[type]; +} + +const QFont *QKdeTheme::font(Font type) const +{ + Q_D(const QKdeTheme); + return d->resources.fonts[type]; +} + +QPlatformTheme *QKdeTheme::createKdeTheme() +{ + const QByteArray kdeVersionBA = qgetenv("KDE_SESSION_VERSION"); + const int kdeVersion = kdeVersionBA.toInt(); + if (kdeVersion < 4) + return nullptr; + + if (kdeVersion > 4) + // Plasma 5 follows XDG spec + // but uses the same config file format: + return new QKdeTheme(QStandardPaths::standardLocations(QStandardPaths::GenericConfigLocation), kdeVersion); + + // Determine KDE prefixes in the following priority order: + // - KDEHOME and KDEDIRS environment variables + // - ~/.kde(<version>) + // - read prefixes from /etc/kde<version>rc + // - fallback to /etc/kde<version> + + QStringList kdeDirs; + const QString kdeHomePathVar = QFile::decodeName(qgetenv("KDEHOME")); + if (!kdeHomePathVar.isEmpty()) + kdeDirs += kdeHomePathVar; + + const QString kdeDirsVar = QFile::decodeName(qgetenv("KDEDIRS")); + if (!kdeDirsVar.isEmpty()) + kdeDirs += kdeDirsVar.split(u':', Qt::SkipEmptyParts); + + const QString kdeVersionHomePath = QDir::homePath() + "/.kde"_L1 + QLatin1StringView(kdeVersionBA); + if (QFileInfo(kdeVersionHomePath).isDir()) + kdeDirs += kdeVersionHomePath; + + const QString kdeHomePath = QDir::homePath() + "/.kde"_L1; + if (QFileInfo(kdeHomePath).isDir()) + kdeDirs += kdeHomePath; + + const QString kdeRcPath = "/etc/kde"_L1 + QLatin1StringView(kdeVersionBA) + "rc"_L1; + if (QFileInfo(kdeRcPath).isReadable()) { + QSettings kdeSettings(kdeRcPath, QSettings::IniFormat); + kdeSettings.beginGroup(QStringLiteral("Directories-default")); + kdeDirs += kdeSettings.value(QStringLiteral("prefixes")).toStringList(); + } + + const QString kdeVersionPrefix = "/etc/kde"_L1 + QLatin1StringView(kdeVersionBA); + if (QFileInfo(kdeVersionPrefix).isDir()) + kdeDirs += kdeVersionPrefix; + + kdeDirs.removeDuplicates(); + if (kdeDirs.isEmpty()) { + qWarning("Unable to determine KDE dirs"); + return nullptr; + } + + return new QKdeTheme(kdeDirs, kdeVersion); +} + +#ifndef QT_NO_DBUS +QPlatformMenuBar *QKdeTheme::createPlatformMenuBar() const +{ + if (isDBusGlobalMenuAvailable()) + return new QDBusMenuBar(); + return nullptr; +} +#endif + +#if !defined(QT_NO_DBUS) && !defined(QT_NO_SYSTEMTRAYICON) +QPlatformSystemTrayIcon *QKdeTheme::createPlatformSystemTrayIcon() const +{ + if (shouldUseDBusTray()) + return new QDBusTrayIcon(); + return nullptr; +} +#endif + +#endif // settings + +/*! + \class QGnomeTheme + \brief QGnomeTheme is a theme implementation for the Gnome desktop. + \since 5.0 + \internal + \ingroup qpa +*/ + +const char *QGnomeTheme::name = "gnome"; + +class QGnomeThemePrivate : public QPlatformThemePrivate +{ +public: + QGnomeThemePrivate(); + ~QGnomeThemePrivate(); + + void configureFonts(const QString >kFontName) const + { + Q_ASSERT(!systemFont); + const int split = gtkFontName.lastIndexOf(QChar::Space); + float size = QStringView{gtkFontName}.mid(split + 1).toFloat(); + QString fontName = gtkFontName.left(split); + + systemFont = new QFont(fontName, size); + fixedFont = new QFont(QLatin1StringView(defaultFixedFontNameC), systemFont->pointSize()); + fixedFont->setStyleHint(QFont::TypeWriter); + qCDebug(lcQpaFonts) << "default fonts: system" << systemFont << "fixed" << fixedFont; + } + + mutable QFont *systemFont = nullptr; + mutable QFont *fixedFont = nullptr; + +#ifndef QT_NO_DBUS + Qt::ColorScheme m_colorScheme = Qt::ColorScheme::Unknown; +private: + std::unique_ptr<QGenericUnixThemeDBusListener> dbus; + bool initDbus(); + void updateColorScheme(const QString &themeName); +#endif // QT_NO_DBUS +}; + +QGnomeThemePrivate::QGnomeThemePrivate() +{ +#ifndef QT_NO_DBUS + initDbus(); +#endif // QT_NO_DBUS +} +QGnomeThemePrivate::~QGnomeThemePrivate() +{ + if (systemFont) + delete systemFont; + if (fixedFont) + delete fixedFont; +} + +#ifndef QT_NO_DBUS +bool QGnomeThemePrivate::initDbus() +{ + dbus.reset(new QGenericUnixThemeDBusListener()); + Q_ASSERT(dbus); + + // Wrap slot in a lambda to avoid inheriting QGnomeThemePrivate from QObject + auto wrapper = [this](QGenericUnixThemeDBusListener::Provider provider, + QGenericUnixThemeDBusListener::Setting setting, + const QString &value) { + if (provider != QGenericUnixThemeDBusListener::Provider::Gnome + && provider != QGenericUnixThemeDBusListener::Provider::Gtk) { + return; + } + + if (setting == QGenericUnixThemeDBusListener::Setting::Theme) + updateColorScheme(value); + }; + + return QObject::connect(dbus.get(), &QGenericUnixThemeDBusListener::settingChanged, dbus.get(), wrapper); +} + +void QGnomeThemePrivate::updateColorScheme(const QString &themeName) +{ + const auto oldColorScheme = m_colorScheme; + if (themeName.contains(QLatin1StringView("light"), Qt::CaseInsensitive)) { + m_colorScheme = Qt::ColorScheme::Light; + } else if (themeName.contains(QLatin1StringView("dark"), Qt::CaseInsensitive)) { + m_colorScheme = Qt::ColorScheme::Dark; + } else { + m_colorScheme = Qt::ColorScheme::Unknown; + } + + if (oldColorScheme != m_colorScheme) + QWindowSystemInterface::handleThemeChange(); +} +#endif // QT_NO_DBUS + +QGnomeTheme::QGnomeTheme() + : QPlatformTheme(new QGnomeThemePrivate()) +{ +} + +QVariant QGnomeTheme::themeHint(QPlatformTheme::ThemeHint hint) const +{ + switch (hint) { + case QPlatformTheme::DialogButtonBoxButtonsHaveIcons: + return QVariant(true); + case QPlatformTheme::DialogButtonBoxLayout: + return QVariant(QPlatformDialogHelper::GnomeLayout); + case QPlatformTheme::SystemIconThemeName: + return QVariant(QStringLiteral("Adwaita")); + case QPlatformTheme::SystemIconFallbackThemeName: + return QVariant(QStringLiteral("gnome")); + case QPlatformTheme::IconThemeSearchPaths: + return QVariant(QGenericUnixTheme::xdgIconThemePaths()); + case QPlatformTheme::IconPixmapSizes: + return QVariant::fromValue(availableXdgFileIconSizes()); + case QPlatformTheme::StyleNames: { + QStringList styleNames; + styleNames << QStringLiteral("Fusion") << QStringLiteral("windows"); + return QVariant(styleNames); + } + case QPlatformTheme::KeyboardScheme: + return QVariant(int(GnomeKeyboardScheme)); + case QPlatformTheme::PasswordMaskCharacter: + return QVariant(QChar(0x2022)); + case QPlatformTheme::UiEffects: + return QVariant(int(HoverEffect)); + case QPlatformTheme::ButtonPressKeys: + return QVariant::fromValue( + QList<Qt::Key>({ Qt::Key_Space, Qt::Key_Return, Qt::Key_Enter, Qt::Key_Select })); + case QPlatformTheme::PreselectFirstFileInDirectory: + return true; + case QPlatformTheme::MouseCursorTheme: + return QVariant(mouseCursorTheme()); + case QPlatformTheme::MouseCursorSize: + return QVariant(mouseCursorSize()); + default: + break; + } + return QPlatformTheme::themeHint(hint); +} + +QIcon QGnomeTheme::fileIcon(const QFileInfo &fileInfo, QPlatformTheme::IconOptions) const +{ +#if QT_CONFIG(mimetype) + return xdgFileIcon(fileInfo); +#else + Q_UNUSED(fileInfo); + return QIcon(); +#endif +} + +const QFont *QGnomeTheme::font(Font type) const +{ + Q_D(const QGnomeTheme); + if (!d->systemFont) + d->configureFonts(gtkFontName()); + switch (type) { + case QPlatformTheme::SystemFont: + return d->systemFont; + case QPlatformTheme::FixedFont: + return d->fixedFont; + default: + return nullptr; + } +} + +QString QGnomeTheme::gtkFontName() const +{ + return QStringLiteral("%1 %2").arg(QLatin1StringView(defaultSystemFontNameC)).arg(defaultSystemFontSize); +} + +#ifndef QT_NO_DBUS +QPlatformMenuBar *QGnomeTheme::createPlatformMenuBar() const +{ + if (isDBusGlobalMenuAvailable()) + return new QDBusMenuBar(); + return nullptr; +} + +Qt::ColorScheme QGnomeTheme::colorScheme() const +{ + return d_func()->m_colorScheme; +} + +#endif + +#if !defined(QT_NO_DBUS) && !defined(QT_NO_SYSTEMTRAYICON) +QPlatformSystemTrayIcon *QGnomeTheme::createPlatformSystemTrayIcon() const +{ + if (shouldUseDBusTray()) + return new QDBusTrayIcon(); + return nullptr; +} +#endif + +QString QGnomeTheme::standardButtonText(int button) const +{ + switch (button) { + case QPlatformDialogHelper::Ok: + return QCoreApplication::translate("QGnomeTheme", "&OK"); + case QPlatformDialogHelper::Save: + return QCoreApplication::translate("QGnomeTheme", "&Save"); + case QPlatformDialogHelper::Cancel: + return QCoreApplication::translate("QGnomeTheme", "&Cancel"); + case QPlatformDialogHelper::Close: + return QCoreApplication::translate("QGnomeTheme", "&Close"); + case QPlatformDialogHelper::Discard: + return QCoreApplication::translate("QGnomeTheme", "Close without Saving"); + default: + break; + } + return QPlatformTheme::standardButtonText(button); +} + +/*! + \brief Creates a UNIX theme according to the detected desktop environment. +*/ + +QPlatformTheme *QGenericUnixTheme::createUnixTheme(const QString &name) +{ + if (name == QLatin1StringView(QGenericUnixTheme::name)) + return new QGenericUnixTheme; +#if QT_CONFIG(settings) + if (name == QLatin1StringView(QKdeTheme::name)) + if (QPlatformTheme *kdeTheme = QKdeTheme::createKdeTheme()) + return kdeTheme; +#endif + if (name == QLatin1StringView(QGnomeTheme::name)) + return new QGnomeTheme; + return nullptr; +} + +QStringList QGenericUnixTheme::themeNames() +{ + QStringList result; + if (QGuiApplication::desktopSettingsAware()) { + const QByteArray desktopEnvironment = QGuiApplicationPrivate::platformIntegration()->services()->desktopEnvironment(); + QList<QByteArray> gtkBasedEnvironments; + gtkBasedEnvironments << "GNOME" + << "X-CINNAMON" + << "UNITY" + << "MATE" + << "XFCE" + << "LXDE"; + const QList<QByteArray> desktopNames = desktopEnvironment.split(':'); + for (const QByteArray &desktopName : desktopNames) { + if (desktopEnvironment == "KDE") { +#if QT_CONFIG(settings) + result.push_back(QLatin1StringView(QKdeTheme::name)); +#endif + } else if (gtkBasedEnvironments.contains(desktopName)) { + // prefer the GTK3 theme implementation with native dialogs etc. + result.push_back(QStringLiteral("gtk3")); + // fallback to the generic Gnome theme if loading the GTK3 theme fails + result.push_back(QLatin1StringView(QGnomeTheme::name)); + } else { + // unknown, but lowercase the name (our standard practice) and + // remove any "x-" prefix + QString s = QString::fromLatin1(desktopName.toLower()); + result.push_back(s.startsWith("x-"_L1) ? s.mid(2) : s); + } + } + } // desktopSettingsAware + result.append(QLatin1StringView(QGenericUnixTheme::name)); + return result; +} + +QT_END_NAMESPACE + +#ifndef QT_NO_DBUS +#include "qgenericunixthemes.moc" +#endif // QT_NO_DBUS diff --git a/src/gui/platform/unix/qgenericunixthemes_p.h b/src/gui/platform/unix/qgenericunixthemes_p.h new file mode 100644 index 0000000000..63b20651e6 --- /dev/null +++ b/src/gui/platform/unix/qgenericunixthemes_p.h @@ -0,0 +1,123 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QGENERICUNIXTHEMES_H +#define QGENERICUNIXTHEMES_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <qpa/qplatformtheme.h> +#include <QtCore/QString> +#include <QtCore/QStringList> +#include <QtGui/QFont> +#include <QtCore/private/qglobal_p.h> + +QT_BEGIN_NAMESPACE + +class ResourceHelper +{ +public: + ResourceHelper(); + ~ResourceHelper() { clear(); } + + void clear(); + + QPalette *palettes[QPlatformTheme::NPalettes]; + QFont *fonts[QPlatformTheme::NFonts]; +}; + +class QGenericUnixThemePrivate; + +class Q_GUI_EXPORT QGenericUnixTheme : public QPlatformTheme +{ + Q_DECLARE_PRIVATE(QGenericUnixTheme) +public: + QGenericUnixTheme(); + + static QPlatformTheme *createUnixTheme(const QString &name); + static QStringList themeNames(); + + const QFont *font(Font type) const override; + QVariant themeHint(ThemeHint hint) const override; + + static QStringList xdgIconThemePaths(); + static QStringList iconFallbackPaths(); +#ifndef QT_NO_DBUS + QPlatformMenuBar *createPlatformMenuBar() const override; +#endif +#if !defined(QT_NO_DBUS) && !defined(QT_NO_SYSTEMTRAYICON) + QPlatformSystemTrayIcon *createPlatformSystemTrayIcon() const override; +#endif + + static const char *name; +}; + +#if QT_CONFIG(settings) +class QKdeThemePrivate; + +class QKdeTheme : public QPlatformTheme +{ + Q_DECLARE_PRIVATE(QKdeTheme) +public: + QKdeTheme(const QStringList& kdeDirs, int kdeVersion); + + static QPlatformTheme *createKdeTheme(); + QVariant themeHint(ThemeHint hint) const override; + + QIcon fileIcon(const QFileInfo &fileInfo, + QPlatformTheme::IconOptions iconOptions = { }) const override; + + const QPalette *palette(Palette type = SystemPalette) const override; + Qt::ColorScheme colorScheme() const override; + + const QFont *font(Font type) const override; +#ifndef QT_NO_DBUS + QPlatformMenuBar *createPlatformMenuBar() const override; +#endif +#if !defined(QT_NO_DBUS) && !defined(QT_NO_SYSTEMTRAYICON) + QPlatformSystemTrayIcon *createPlatformSystemTrayIcon() const override; +#endif + + static const char *name; +}; +#endif // settings + +class QGnomeThemePrivate; + +class Q_GUI_EXPORT QGnomeTheme : public QPlatformTheme +{ + Q_DECLARE_PRIVATE(QGnomeTheme) +public: + QGnomeTheme(); + QVariant themeHint(ThemeHint hint) const override; + QIcon fileIcon(const QFileInfo &fileInfo, + QPlatformTheme::IconOptions = { }) const override; + const QFont *font(Font type) const override; + QString standardButtonText(int button) const override; + + virtual QString gtkFontName() const; +#ifndef QT_NO_DBUS + QPlatformMenuBar *createPlatformMenuBar() const override; + Qt::ColorScheme colorScheme() const override; +#endif +#if !defined(QT_NO_DBUS) && !defined(QT_NO_SYSTEMTRAYICON) + QPlatformSystemTrayIcon *createPlatformSystemTrayIcon() const override; +#endif + + static const char *name; +}; + +QPlatformTheme *qt_createUnixTheme(); + +QT_END_NAMESPACE + +#endif // QGENERICUNIXTHEMES_H diff --git a/src/gui/platform/unix/qtx11extras.cpp b/src/gui/platform/unix/qtx11extras.cpp new file mode 100644 index 0000000000..ef37518d95 --- /dev/null +++ b/src/gui/platform/unix/qtx11extras.cpp @@ -0,0 +1,514 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// Copyright (C) 2016 Richard Moore <rich@kde.org> +// Copyright (C) 2016 David Faure <david.faure@kdab.com> +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qtx11extras_p.h" + +#include <qpa/qplatformnativeinterface.h> +#include <qpa/qplatformwindow.h> +#include <qpa/qplatformscreen_p.h> +#include <qpa/qplatformscreen.h> +#include <qscreen.h> +#include <qwindow.h> +#include <qguiapplication.h> +#include <xcb/xcb.h> +#include <QtCore/qdebug.h> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +static QScreen *findScreenForVirtualDesktop(int virtualDesktopNumber) +{ + const auto screens = QGuiApplication::screens(); + for (QScreen *screen : screens) { + auto *qxcbScreen = dynamic_cast<QNativeInterface::Private::QXcbScreen *>(screen->handle()); + if (qxcbScreen && qxcbScreen->virtualDesktopNumber() == virtualDesktopNumber) + return screen; + } + return nullptr; +} + +/*! + \class QX11Info + \inmodule QtGui + \since 6.2 + \internal + + \brief Provides information about the X display configuration. + + The class provides two APIs: a set of non-static functions that + provide information about a specific widget or pixmap, and a set + of static functions that provide the default information for the + application. + + \warning This class is only available on X11. For querying + per-screen information in a portable way, use QScreen. +*/ + +/*! + Constructs an empty QX11Info object. +*/ +QX11Info::QX11Info() +{ +} + +/*! + Returns true if the application is currently running on X11. + + \since 6.2 + */ +bool QX11Info::isPlatformX11() +{ + return QGuiApplication::platformName() == "xcb"_L1; +} + +/*! + Returns the horizontal resolution of the given \a screen in terms of the + number of dots per inch. + + The \a screen argument is an X screen number. Be aware that if + the user's system uses Xinerama (as opposed to traditional X11 + multiscreen), there is only one X screen. Use QScreen to + query for information about Xinerama screens. + + \sa appDpiY() +*/ +int QX11Info::appDpiX(int screen) +{ + if (screen == -1) { + const QScreen *scr = QGuiApplication::primaryScreen(); + if (!scr) + return 75; + return qRound(scr->logicalDotsPerInchX()); + } + + QScreen *scr = findScreenForVirtualDesktop(screen); + if (!scr) + return 0; + + return scr->logicalDotsPerInchX(); +} + +/*! + Returns the vertical resolution of the given \a screen in terms of the + number of dots per inch. + + The \a screen argument is an X screen number. Be aware that if + the user's system uses Xinerama (as opposed to traditional X11 + multiscreen), there is only one X screen. Use QScreen to + query for information about Xinerama screens. + + \sa appDpiX() +*/ +int QX11Info::appDpiY(int screen) +{ + if (screen == -1) { + const QScreen *scr = QGuiApplication::primaryScreen(); + if (!scr) + return 75; + return qRound(scr->logicalDotsPerInchY()); + } + + QScreen *scr = findScreenForVirtualDesktop(screen); + if (!scr) + return 0; + + return scr->logicalDotsPerInchY(); +} + +/*! + Returns a handle for the applications root window on the given \a screen. + + The \a screen argument is an X screen number. Be aware that if + the user's system uses Xinerama (as opposed to traditional X11 + multiscreen), there is only one X screen. Use QScreen to + query for information about Xinerama screens. +*/ +quint32 QX11Info::appRootWindow(int screen) +{ + if (!qApp) + return 0; + QPlatformNativeInterface *native = qApp->platformNativeInterface(); + if (!native) + return 0; + QScreen *scr = screen == -1 ? QGuiApplication::primaryScreen() : findScreenForVirtualDesktop(screen); + if (!scr) + return 0; + return static_cast<xcb_window_t>(reinterpret_cast<quintptr>(native->nativeResourceForScreen(QByteArrayLiteral("rootwindow"), scr))); +} + +/*! + Returns the number of the screen where the application is being + displayed. + + This method refers to screens in the original X11 meaning with a + different DISPLAY environment variable per screen. + This information is only useful if your application needs to know + on which X screen it is running. + + In a typical multi-head configuration, multiple physical monitors + are combined in one X11 screen. This means this method returns the + same number for each of the physical monitors. In such a setup you + are interested in the monitor information as provided by the X11 + RandR extension. This is available through QScreen. + + \sa display() +*/ +int QX11Info::appScreen() +{ + if (!qApp) + return 0; + QPlatformNativeInterface *native = qApp->platformNativeInterface(); + if (!native) + return 0; + return reinterpret_cast<qintptr>(native->nativeResourceForIntegration(QByteArrayLiteral("x11screen"))); +} + +/*! + Returns the X11 time. + + \sa setAppTime(), appUserTime() +*/ +quint32 QX11Info::appTime() +{ + if (!qApp) + return 0; + QPlatformNativeInterface *native = qApp->platformNativeInterface(); + if (!native) + return 0; + QScreen* screen = QGuiApplication::primaryScreen(); + return static_cast<xcb_timestamp_t>(reinterpret_cast<quintptr>(native->nativeResourceForScreen("apptime", screen))); +} + +/*! + Returns the X11 user time. + + \sa setAppUserTime(), appTime() +*/ +quint32 QX11Info::appUserTime() +{ + if (!qApp) + return 0; + QPlatformNativeInterface *native = qApp->platformNativeInterface(); + if (!native) + return 0; + QScreen* screen = QGuiApplication::primaryScreen(); + return static_cast<xcb_timestamp_t>(reinterpret_cast<quintptr>(native->nativeResourceForScreen("appusertime", screen))); +} + +/*! + Sets the X11 time to the value specified by \a time. + + \sa appTime(), setAppUserTime() +*/ +void QX11Info::setAppTime(quint32 time) +{ + if (!qApp) + return; + QPlatformNativeInterface *native = qApp->platformNativeInterface(); + if (!native) + return; + typedef void (*SetAppTimeFunc)(QScreen *, xcb_timestamp_t); + QScreen* screen = QGuiApplication::primaryScreen(); + SetAppTimeFunc func = reinterpret_cast<SetAppTimeFunc>(reinterpret_cast<void *>(native->nativeResourceFunctionForScreen("setapptime"))); + if (func) + func(screen, time); + else + qWarning("Internal error: QPA plugin doesn't implement setAppTime"); +} + +/*! + Sets the X11 user time as specified by \a time. + + \sa appUserTime(), setAppTime() +*/ +void QX11Info::setAppUserTime(quint32 time) +{ + if (!qApp) + return; + QPlatformNativeInterface *native = qApp->platformNativeInterface(); + if (!native) + return; + typedef void (*SetAppUserTimeFunc)(QScreen *, xcb_timestamp_t); + QScreen* screen = QGuiApplication::primaryScreen(); + SetAppUserTimeFunc func = reinterpret_cast<SetAppUserTimeFunc>(reinterpret_cast<void *>(native->nativeResourceFunctionForScreen("setappusertime"))); + if (func) + func(screen, time); + else + qWarning("Internal error: QPA plugin doesn't implement setAppUserTime"); +} + +/*! + Fetches the current X11 time stamp from the X Server. + + This method creates a property notify event and blocks till it is + received back from the X Server. +*/ +quint32 QX11Info::getTimestamp() +{ + if (!qApp) + return 0; + QPlatformNativeInterface *native = qApp->platformNativeInterface(); + if (!native) + return 0; + QScreen* screen = QGuiApplication::primaryScreen(); + return static_cast<xcb_timestamp_t>(reinterpret_cast<quintptr>(native->nativeResourceForScreen("gettimestamp", screen))); +} + +/*! + Returns the startup ID that will be used for the next window to be shown by this process. + + After the next window is shown, the next startup ID will be empty. + + http://standards.freedesktop.org/startup-notification-spec/startup-notification-latest.txt + + \sa setNextStartupId() +*/ +QByteArray QX11Info::nextStartupId() +{ + if (!qApp) + return QByteArray(); + QPlatformNativeInterface *native = qApp->platformNativeInterface(); + if (!native) + return QByteArray(); + return static_cast<char *>(native->nativeResourceForIntegration("startupid")); +} + +/*! + Sets the next startup ID to \a id. + + This is the startup ID that will be used for the next window to be shown by this process. + + The startup ID of the first window comes from the environment variable DESKTOP_STARTUP_ID. + This method is useful for subsequent windows, when the request comes from another process + (e.g. via DBus). + + \sa nextStartupId() +*/ +void QX11Info::setNextStartupId(const QByteArray &id) +{ + if (!qApp) + return; + QPlatformNativeInterface *native = qApp->platformNativeInterface(); + if (!native) + return; + typedef void (*SetStartupIdFunc)(const char*); + SetStartupIdFunc func = reinterpret_cast<SetStartupIdFunc>(reinterpret_cast<void *>(native->nativeResourceFunctionForIntegration("setstartupid"))); + if (func) + func(id.constData()); + else + qWarning("Internal error: QPA plugin doesn't implement setStartupId"); +} + +/*! + Returns the default display for the application. + + \sa appScreen() +*/ +Display *QX11Info::display() +{ + if (!qApp) + return nullptr; + QPlatformNativeInterface *native = qApp->platformNativeInterface(); + if (!native) + return nullptr; + + void *display = native->nativeResourceForIntegration(QByteArray("display")); + return reinterpret_cast<Display *>(display); +} + +/*! + Returns the default XCB connection for the application. + + \sa display() +*/ +xcb_connection_t *QX11Info::connection() +{ + if (!qApp) + return nullptr; + QPlatformNativeInterface *native = qApp->platformNativeInterface(); + if (!native) + return nullptr; + + void *connection = native->nativeResourceForIntegration(QByteArray("connection")); + return reinterpret_cast<xcb_connection_t *>(connection); +} + +/*! + Returns true if there is a compositing manager running for the connection + attached to \a screen. + + If \a screen equals -1, the application's primary screen is used. +*/ +bool QX11Info::isCompositingManagerRunning(int screen) +{ + if (!qApp) + return false; + QPlatformNativeInterface *native = qApp->platformNativeInterface(); + if (!native) + return false; + + QScreen *scr = screen == -1 ? QGuiApplication::primaryScreen() : findScreenForVirtualDesktop(screen); + if (!scr) { + qWarning() << "isCompositingManagerRunning: Could not find screen number" << screen; + return false; + } + + return native->nativeResourceForScreen(QByteArray("compositingEnabled"), scr); +} + +/*! + Returns a new peeker id or -1 if some internal error has occurred. + Each peeker id is associated with an index in the buffered native + event queue. + + For more details see QX11Info::PeekOption and peekEventQueue(). + + \sa peekEventQueue(), removePeekerId() +*/ +qint32 QX11Info::generatePeekerId() +{ + if (!qApp) + return -1; + QPlatformNativeInterface *native = qApp->platformNativeInterface(); + if (!native) + return -1; + + typedef qint32 (*GeneratePeekerIdFunc)(void); + GeneratePeekerIdFunc generatepeekerid = reinterpret_cast<GeneratePeekerIdFunc>( + reinterpret_cast<void *>(native->nativeResourceFunctionForIntegration("generatepeekerid"))); + if (!generatepeekerid) { + qWarning("Internal error: QPA plugin doesn't implement generatePeekerId"); + return -1; + } + + return generatepeekerid(); +} + +/*! + Removes \a peekerId, which was earlier obtained via generatePeekerId(). + + Returns \c true on success or \c false if unknown peeker id was + provided or some internal error has occurred. + + \sa generatePeekerId() +*/ +bool QX11Info::removePeekerId(qint32 peekerId) +{ + if (!qApp) + return false; + QPlatformNativeInterface *native = qApp->platformNativeInterface(); + if (!native) + return false; + + typedef bool (*RemovePeekerIdFunc)(qint32); + RemovePeekerIdFunc removePeekerId = reinterpret_cast<RemovePeekerIdFunc>( + reinterpret_cast<void *>(native->nativeResourceFunctionForIntegration("removepeekerid"))); + if (!removePeekerId) { + qWarning("Internal error: QPA plugin doesn't implement removePeekerId"); + return false; + } + + return removePeekerId(peekerId); +} + +/*! + \enum QX11Info::PeekOption + \brief An enum to tune the behavior of QX11Info::peekEventQueue(). + + \value PeekDefault + Peek from the beginning of the buffered native event queue. A peeker + id is optional with PeekDefault. If a peeker id is provided to + peekEventQueue() when using PeekDefault, then peeking starts from + the beginning of the queue, not from the cached index; thus, this + can be used to manually reset a cached index to peek from the start + of the queue. When this operation completes, the associated index + will be updated to the new position in the queue. + + \value PeekFromCachedIndex + QX11Info::peekEventQueue() can optimize the peeking algorithm by + skipping events that it already has seen in earlier calls to + peekEventQueue(). When control returns to the main event loop, + which causes the buffered native event queue to be flushed to Qt's + event queue, the cached indices are marked invalid and will be + reset on the next access. The same is true if the program + explicitly flushes the buffered native event queue by + QCoreApplication::processEvents(). +*/ + +/*! + \typedef QX11Info::PeekerCallback + Typedef for a pointer to a function with the following signature: + + \code + bool (*PeekerCallback)(xcb_generic_event_t *event, void *peekerData); + \endcode + + The \a event is a native XCB event. + The \a peekerData is a pointer to data, passed in via peekEventQueue(). + + Return \c true from this function to stop examining the buffered + native event queue or \c false to continue. + + \note A non-capturing lambda can serve as a PeekerCallback. +*/ + +/*! + \brief Peek into the buffered XCB event queue. + + You can call peekEventQueue() periodically, when your program is busy + performing a long-running operation, to peek into the buffered native + event queue. The more time the long-running operation blocks the + program from returning control to the main event loop, the more + events will accumulate in the buffered XCB event queue. Once control + returns to the main event loop these events will be flushed to Qt's + event queue, which is a separate event queue from the queue this + function is peeking into. + + \note It is usually better to run CPU-intensive operations in a + non-GUI thread, instead of blocking the main event loop. + + The buffered XCB event queue is populated from a non-GUI thread and + therefore might be ahead of the current GUI state. To handle native + events as they are processed by the GUI thread, see + QAbstractNativeEventFilter::nativeEventFilter(). + + The \a peeker is a callback function as documented in PeekerCallback. + The \a peekerData can be used to pass in arbitrary data to the \a + peeker callback. + The \a option is an enum that tunes the behavior of peekEventQueue(). + The \a peekerId is used to track an index in the queue, for more + details see QX11Info::PeekOption. There can be several indices, + each tracked individually by a peeker id obtained via generatePeekerId(). + + This function returns \c true when the peeker has stopped the event + proccesing by returning \c true from the callback. If there were no + events in the buffered native event queue to peek at or all the + events have been processed by the peeker, this function returns \c + false. + + \sa generatePeekerId(), QAbstractNativeEventFilter::nativeEventFilter() +*/ +bool QX11Info::peekEventQueue(PeekerCallback peeker, void *peekerData, PeekOptions option, + qint32 peekerId) +{ + if (!peeker || !qApp) + return false; + QPlatformNativeInterface *native = qApp->platformNativeInterface(); + if (!native) + return false; + + typedef bool (*PeekEventQueueFunc)(PeekerCallback, void *, PeekOptions, qint32); + PeekEventQueueFunc peekeventqueue = reinterpret_cast<PeekEventQueueFunc>( + reinterpret_cast<void *>(native->nativeResourceFunctionForIntegration("peekeventqueue"))); + if (!peekeventqueue) { + qWarning("Internal error: QPA plugin doesn't implement peekEventQueue"); + return false; + } + + return peekeventqueue(peeker, peekerData, option, peekerId); +} + +QT_END_NAMESPACE diff --git a/src/gui/platform/unix/qtx11extras_p.h b/src/gui/platform/unix/qtx11extras_p.h new file mode 100644 index 0000000000..253162f83a --- /dev/null +++ b/src/gui/platform/unix/qtx11extras_p.h @@ -0,0 +1,75 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QTX11EXTRAS_P_H +#define QTX11EXTRAS_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtGui/qtguiglobal.h> +#include <QtCore/private/qglobal_p.h> + +#include <xcb/xcb.h> + +typedef struct _XDisplay Display; + +QT_BEGIN_NAMESPACE + +class Q_GUI_EXPORT QX11Info +{ +public: + enum PeekOption { + PeekDefault = 0, + PeekFromCachedIndex = 1 + }; + Q_DECLARE_FLAGS(PeekOptions, PeekOption) + + static bool isPlatformX11(); + + static int appDpiX(int screen=-1); + static int appDpiY(int screen=-1); + + static quint32 appRootWindow(int screen=-1); + static int appScreen(); + + static quint32 appTime(); + static quint32 appUserTime(); + + static void setAppTime(quint32 time); + static void setAppUserTime(quint32 time); + + static quint32 getTimestamp(); + + static QByteArray nextStartupId(); + static void setNextStartupId(const QByteArray &id); + + static Display *display(); + static xcb_connection_t *connection(); + + static bool isCompositingManagerRunning(int screen = -1); + + static qint32 generatePeekerId(); + static bool removePeekerId(qint32 peekerId); + typedef bool (*PeekerCallback)(xcb_generic_event_t *event, void *peekerData); + static bool peekEventQueue(PeekerCallback peeker, void *peekerData = nullptr, + PeekOptions option = PeekDefault, qint32 peekerId = -1); + +private: + QX11Info(); +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(QX11Info::PeekOptions) + +QT_END_NAMESPACE + +#endif // QTX11EXTRAS_P_H + diff --git a/src/gui/platform/unix/qunixeventdispatcher.cpp b/src/gui/platform/unix/qunixeventdispatcher.cpp new file mode 100644 index 0000000000..0178b7544e --- /dev/null +++ b/src/gui/platform/unix/qunixeventdispatcher.cpp @@ -0,0 +1,31 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qplatformdefs.h" +#include "qcoreapplication.h" +#include "qunixeventdispatcher_qpa_p.h" +#include "private/qguiapplication_p.h" + +#include <qpa/qwindowsysteminterface.h> + +#include <QtCore/QDebug> + +QT_BEGIN_NAMESPACE + +QT_USE_NAMESPACE + + +QUnixEventDispatcherQPA::QUnixEventDispatcherQPA(QObject *parent) + : QEventDispatcherUNIX(parent) +{ } + +QUnixEventDispatcherQPA::~QUnixEventDispatcherQPA() +{ } + +bool QUnixEventDispatcherQPA::processEvents(QEventLoop::ProcessEventsFlags flags) +{ + const bool didSendEvents = QEventDispatcherUNIX::processEvents(flags); + return QWindowSystemInterface::sendWindowSystemEvents(flags) || didSendEvents; +} + +QT_END_NAMESPACE diff --git a/src/gui/platform/unix/qunixeventdispatcher_qpa_p.h b/src/gui/platform/unix/qunixeventdispatcher_qpa_p.h new file mode 100644 index 0000000000..bcae9b5a1c --- /dev/null +++ b/src/gui/platform/unix/qunixeventdispatcher_qpa_p.h @@ -0,0 +1,36 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QUNIXEVENTDISPATCHER_QPA_H +#define QUNIXEVENTDISPATCHER_QPA_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtGui/qtguiglobal.h> +#include <QtCore/private/qeventdispatcher_unix_p.h> + +QT_BEGIN_NAMESPACE + +class Q_GUI_EXPORT QUnixEventDispatcherQPA : public QEventDispatcherUNIX +{ + Q_OBJECT + +public: + explicit QUnixEventDispatcherQPA(QObject *parent = nullptr); + ~QUnixEventDispatcherQPA(); + + bool processEvents(QEventLoop::ProcessEventsFlags flags) override; +}; + +QT_END_NAMESPACE + +#endif // QUNIXEVENTDISPATCHER_QPA_H diff --git a/src/gui/platform/unix/qunixnativeinterface.cpp b/src/gui/platform/unix/qunixnativeinterface.cpp new file mode 100644 index 0000000000..09561d9ada --- /dev/null +++ b/src/gui/platform/unix/qunixnativeinterface.cpp @@ -0,0 +1,311 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include <QtGui/private/qtguiglobal_p.h> + +#if QT_CONFIG(opengl) +# include <QtGui/private/qopenglcontext_p.h> +#endif +#include <QtGui/private/qguiapplication_p.h> + +#include <qpa/qplatformopenglcontext.h> +#include <qpa/qplatformintegration.h> +#include <qpa/qplatformscreen_p.h> +#include <qpa/qplatformwindow_p.h> + +#include <QtGui/private/qkeymapper_p.h> + +QT_BEGIN_NAMESPACE + +using namespace QNativeInterface::Private; + +#ifndef QT_NO_OPENGL + +#if QT_CONFIG(xcb_glx_plugin) + +/*! + \class QNativeInterface::QGLXContext + \since 6.0 + \brief Native interface to a GLX context. + + Accessed through QOpenGLContext::nativeInterface(). + + \inmodule QtGui + \inheaderfile QOpenGLContext + \ingroup native-interfaces + \ingroup native-interfaces-qopenglcontext +*/ + +/*! + \fn QOpenGLContext *QNativeInterface::QGLXContext::fromNative(GLXContext configBasedContext, QOpenGLContext *shareContext = nullptr) + + \brief Adopts a GLXContext \a configBasedContext created from an FBConfig. + + The context must be created from a framebuffer configuration, using the \c glXCreateNewContext function. + + Ownership of the created QOpenGLContext \a shareContext is transferred to the caller. +*/ + +/*! + \fn QOpenGLContext *QNativeInterface::QGLXContext::fromNative(GLXContext visualBasedContext, void *visualInfo, QOpenGLContext *shareContext = nullptr) + + \brief Adopts a GLXContext created from an X visual. + + The context must be created from a visual, using the \c glXCreateContext function. + The same visual must be passed as a pointer to an \c XVisualInfo struct, in the \a visualInfo argument. + + Ownership of the created QOpenGLContext is transferred to the caller. +*/ + +/*! + \fn GLXContext QNativeInterface::QGLXContext::nativeContext() const + + \return the underlying GLXContext. +*/ + +QT_DEFINE_NATIVE_INTERFACE(QGLXContext); +QT_DEFINE_PRIVATE_NATIVE_INTERFACE(QGLXIntegration); + +QOpenGLContext *QNativeInterface::QGLXContext::fromNative(GLXContext configBasedContext, QOpenGLContext *shareContext) +{ + return QGuiApplicationPrivate::platformIntegration()->call< + &QGLXIntegration::createOpenGLContext>(configBasedContext, nullptr, shareContext); +} + +QOpenGLContext *QNativeInterface::QGLXContext::fromNative(GLXContext visualBasedContext, void *visualInfo, QOpenGLContext *shareContext) +{ + return QGuiApplicationPrivate::platformIntegration()->call< + &QGLXIntegration::createOpenGLContext>(visualBasedContext, visualInfo, shareContext); +} +#endif // QT_CONFIG(xcb_glx_plugin) + +#if QT_CONFIG(egl) + +/*! + \class QNativeInterface::QEGLContext + \since 6.0 + \brief Native interface to an EGL context. + + Accessed through QOpenGLContext::nativeInterface(). + + \inmodule QtGui + \inheaderfile QOpenGLContext + \ingroup native-interfaces + \ingroup native-interfaces-qopenglcontext +*/ + +/*! + \fn QOpenGLContext *QNativeInterface::QEGLContext::fromNative(EGLContext context, EGLDisplay display, QOpenGLContext *shareContext = nullptr) + + \brief Adopts an EGLContext \a context. + + The same \c EGLDisplay passed to \c eglCreateContext must be passed as the \a display argument. + + Ownership of the created QOpenGLContext \a shareContext is transferred + to the caller. +*/ + +/*! + \fn EGLContext QNativeInterface::QEGLContext::nativeContext() const + + \return the underlying EGLContext. +*/ + +/*! + \fn EGLConfig QNativeInterface::QEGLContext::config() const + \since 6.3 + \return the EGLConfig associated with the underlying EGLContext. +*/ + +/*! + \fn EGLDisplay QNativeInterface::QEGLContext::display() const + \since 6.3 + \return the EGLDisplay associated with the underlying EGLContext. +*/ + + +/*! + \fn void QNativeInterface::QEGLContext::invalidateContext() + \since 6.5 + \brief Marks the context as invalid + + If this context is used by the Qt Quick scenegraph, this will trigger the + SceneGraph to destroy this context and create a new one. + + Similarly to QPlatformWindow::invalidateSurface(), + this function can only be expected to have an effect on certain platforms, + such as eglfs. + + \sa QOpenGLContext::isValid(), QPlatformWindow::invalidateSurface() +*/ + +QT_DEFINE_NATIVE_INTERFACE(QEGLContext); +QT_DEFINE_PRIVATE_NATIVE_INTERFACE(QEGLIntegration); + +QOpenGLContext *QNativeInterface::QEGLContext::fromNative(EGLContext context, EGLDisplay display, QOpenGLContext *shareContext) +{ + return QGuiApplicationPrivate::platformIntegration()->call< + &QEGLIntegration::createOpenGLContext>(context, display, shareContext); +} +#endif // QT_CONFIG(egl) + +#endif // QT_NO_OPENGL + +#if QT_CONFIG(xcb) + +/*! + \class QNativeInterface::Private::QXcbScreen + \since 6.0 + \internal + \brief Native interface to QPlatformScreen. + \inmodule QtGui + \ingroup native-interfaces +*/ + +QT_DEFINE_PRIVATE_NATIVE_INTERFACE(QXcbScreen); + +/*! + \class QNativeInterface::Private::QXcbWindow + \since 6.0 + \internal + \brief Native interface to QPlatformWindow. + \inmodule QtGui + \ingroup native-interfaces +*/ + +QT_DEFINE_PRIVATE_NATIVE_INTERFACE(QXcbWindow); + +/*! + \class QNativeInterface::QX11Application + \since 6.2 + \brief Native interface to an X11 application. + + Accessed through QGuiApplication::nativeInterface(). + + \inmodule QtGui + \inheaderfile QGuiApplication + \ingroup native-interfaces + \ingroup native-interfaces-qguiapplication +*/ + +/*! + \fn Display *QNativeInterface::QX11Application::display() const + + \return the X display of the application, for use with Xlib. + + \sa connection() +*/ + +/*! + \fn xcb_connection_t *QNativeInterface::QX11Application::connection() const + + \return the X connection of the application, for use with XCB. + + \sa display() +*/ + +QT_DEFINE_NATIVE_INTERFACE(QX11Application); + +#endif // QT_CONFIG(xcb) + +#if QT_CONFIG(vsp2) +QT_DEFINE_PRIVATE_NATIVE_INTERFACE(QVsp2Screen); +#endif + +#ifdef Q_OS_WEBOS +QT_DEFINE_PRIVATE_NATIVE_INTERFACE(QWebOSScreen); +#endif + +#if QT_CONFIG(evdev) + +/*! + \class QNativeInterface::Private::QEvdevKeyMapper + \since 6.0 + \internal + \brief Native interface to QKeyMapper. + \inmodule QtGui + \ingroup native-interfaces +*/ + +QT_DEFINE_PRIVATE_NATIVE_INTERFACE(QEvdevKeyMapper); + +#endif // QT_CONFIG(evdev) + +#if QT_CONFIG(wayland) + +/*! + \class QNativeInterface::QWaylandApplication + \inheaderfile QGuiApplication + \since 6.5 + \brief Native interface to a Wayland application. + + Accessed through QGuiApplication::nativeInterface(). + \inmodule QtGui + \ingroup native-interfaces + \ingroup native-interfaces-qguiapplication +*/ +/*! + \fn wl_display *QNativeInterface::QWaylandApplication::display() const + \return the wl_display that the application is using. +*/ +/*! + \fn wl_compositor *QNativeInterface::QWaylandApplication::compositor() const + \return the wl_compositor that the application is using. +*/ +/*! + \fn wl_keyboard *QNativeInterface::QWaylandApplication::keyboard() const + \return the wl_keyboard belonging to seat() if available. +*/ +/*! + \fn wl_pointer *QNativeInterface::QWaylandApplication::pointer() const + \return the wl_pointer belonging to seat() if available. +*/ +/*! + \fn wl_touch *QNativeInterface::QWaylandApplication::touch() const + \return the wl_touch belonging to seat() if available. +*/ +/*! + \fn uint *QNativeInterface::QWaylandApplication::lastInputSerial() const + \return the serial of the last input event on any seat. +*/ +/*! + \fn wl_seat *QNativeInterface::QWaylandApplication::lastInputSeat() const + \return the seat on which the last input event happened. +*/ +/*! + \fn wl_seat *QNativeInterface::QWaylandApplication::seat() const + \return the seat associated with the default input device. +*/ + +QT_DEFINE_NATIVE_INTERFACE(QWaylandApplication); + +/*! + \class QNativeInterface::QWaylandScreen + \since 6.7 + \brief Native interface to a screen on Wayland. + + Accessed through QScreen::nativeInterface(). + \inmodule QtGui + \ingroup native-interfaces + \ingroup native-interfaces-qscreen +*/ +/*! + \fn wl_output *QNativeInterface::QWaylandScreen::output() const + \return the underlying wl_output of this QScreen. +*/ +QT_DEFINE_NATIVE_INTERFACE(QWaylandScreen); + +/*! + \class QNativeInterface::QWaylandWindow + \since 6.5 + \internal + \brief Native interface to a Wayland window. + \inmodule QtGui + \ingroup native-interfaces +*/ + +QT_DEFINE_PRIVATE_NATIVE_INTERFACE(QWaylandWindow); + +#endif // QT_CONFIG(wayland) + +QT_END_NAMESPACE diff --git a/src/gui/platform/unix/qxkbcommon.cpp b/src/gui/platform/unix/qxkbcommon.cpp new file mode 100644 index 0000000000..ed29db3005 --- /dev/null +++ b/src/gui/platform/unix/qxkbcommon.cpp @@ -0,0 +1,831 @@ +// Copyright (C) 2019 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qxkbcommon_p.h" + +#include <private/qmakearray_p.h> + +#include <QtCore/private/qstringiterator_p.h> +#include <QtCore/qvarlengtharray.h> +#include <QtCore/QMetaMethod> + +#include <QtGui/QKeyEvent> +#include <QtGui/private/qguiapplication_p.h> + +#include <qpa/qplatforminputcontext.h> +#include <qpa/qplatformintegration.h> + +QT_BEGIN_NAMESPACE + +static int keysymToQtKey_internal(xkb_keysym_t keysym, Qt::KeyboardModifiers modifiers, + xkb_state *state, xkb_keycode_t code, + bool superAsMeta, bool hyperAsMeta); + +typedef struct xkb2qt +{ + unsigned int xkb; + unsigned int qt; + + constexpr bool operator <=(const xkb2qt &that) const noexcept + { + return xkb <= that.xkb; + } + + constexpr bool operator <(const xkb2qt &that) const noexcept + { + return xkb < that.xkb; + } +} xkb2qt_t; + +template<std::size_t Xkb, std::size_t Qt> +struct Xkb2Qt +{ + using Type = xkb2qt_t; + static constexpr Type data() noexcept { return Type{Xkb, Qt}; } +}; + +static constexpr const auto KeyTbl = qMakeArray( + QSortedData< + // misc keys + + Xkb2Qt<XKB_KEY_Escape, Qt::Key_Escape>, + Xkb2Qt<XKB_KEY_Tab, Qt::Key_Tab>, + Xkb2Qt<XKB_KEY_ISO_Left_Tab, Qt::Key_Backtab>, + Xkb2Qt<XKB_KEY_BackSpace, Qt::Key_Backspace>, + Xkb2Qt<XKB_KEY_Return, Qt::Key_Return>, + Xkb2Qt<XKB_KEY_Insert, Qt::Key_Insert>, + Xkb2Qt<XKB_KEY_Delete, Qt::Key_Delete>, + Xkb2Qt<XKB_KEY_Clear, Qt::Key_Delete>, + Xkb2Qt<XKB_KEY_Pause, Qt::Key_Pause>, + Xkb2Qt<XKB_KEY_Print, Qt::Key_Print>, + Xkb2Qt<XKB_KEY_Sys_Req, Qt::Key_SysReq>, + Xkb2Qt<0x1005FF60, Qt::Key_SysReq>, // hardcoded Sun SysReq + Xkb2Qt<0x1007ff00, Qt::Key_SysReq>, // hardcoded X386 SysReq + + // cursor movement + + Xkb2Qt<XKB_KEY_Home, Qt::Key_Home>, + Xkb2Qt<XKB_KEY_End, Qt::Key_End>, + Xkb2Qt<XKB_KEY_Left, Qt::Key_Left>, + Xkb2Qt<XKB_KEY_Up, Qt::Key_Up>, + Xkb2Qt<XKB_KEY_Right, Qt::Key_Right>, + Xkb2Qt<XKB_KEY_Down, Qt::Key_Down>, + Xkb2Qt<XKB_KEY_Prior, Qt::Key_PageUp>, + Xkb2Qt<XKB_KEY_Next, Qt::Key_PageDown>, + + // modifiers + + Xkb2Qt<XKB_KEY_Shift_L, Qt::Key_Shift>, + Xkb2Qt<XKB_KEY_Shift_R, Qt::Key_Shift>, + Xkb2Qt<XKB_KEY_Shift_Lock, Qt::Key_Shift>, + Xkb2Qt<XKB_KEY_Control_L, Qt::Key_Control>, + Xkb2Qt<XKB_KEY_Control_R, Qt::Key_Control>, + Xkb2Qt<XKB_KEY_Meta_L, Qt::Key_Meta>, + Xkb2Qt<XKB_KEY_Meta_R, Qt::Key_Meta>, + Xkb2Qt<XKB_KEY_Alt_L, Qt::Key_Alt>, + Xkb2Qt<XKB_KEY_Alt_R, Qt::Key_Alt>, + Xkb2Qt<XKB_KEY_Caps_Lock, Qt::Key_CapsLock>, + Xkb2Qt<XKB_KEY_Num_Lock, Qt::Key_NumLock>, + Xkb2Qt<XKB_KEY_Scroll_Lock, Qt::Key_ScrollLock>, + Xkb2Qt<XKB_KEY_Super_L, Qt::Key_Super_L>, + Xkb2Qt<XKB_KEY_Super_R, Qt::Key_Super_R>, + Xkb2Qt<XKB_KEY_Menu, Qt::Key_Menu>, + Xkb2Qt<XKB_KEY_Hyper_L, Qt::Key_Hyper_L>, + Xkb2Qt<XKB_KEY_Hyper_R, Qt::Key_Hyper_R>, + Xkb2Qt<XKB_KEY_Help, Qt::Key_Help>, + Xkb2Qt<0x1000FF74, Qt::Key_Backtab>, // hardcoded HP backtab + Xkb2Qt<0x1005FF10, Qt::Key_F11>, // hardcoded Sun F36 (labeled F11) + Xkb2Qt<0x1005FF11, Qt::Key_F12>, // hardcoded Sun F37 (labeled F12) + + // numeric and function keypad keys + + Xkb2Qt<XKB_KEY_KP_Space, Qt::Key_Space>, + Xkb2Qt<XKB_KEY_KP_Tab, Qt::Key_Tab>, + Xkb2Qt<XKB_KEY_KP_Enter, Qt::Key_Enter>, + Xkb2Qt<XKB_KEY_KP_Home, Qt::Key_Home>, + Xkb2Qt<XKB_KEY_KP_Left, Qt::Key_Left>, + Xkb2Qt<XKB_KEY_KP_Up, Qt::Key_Up>, + Xkb2Qt<XKB_KEY_KP_Right, Qt::Key_Right>, + Xkb2Qt<XKB_KEY_KP_Down, Qt::Key_Down>, + Xkb2Qt<XKB_KEY_KP_Prior, Qt::Key_PageUp>, + Xkb2Qt<XKB_KEY_KP_Next, Qt::Key_PageDown>, + Xkb2Qt<XKB_KEY_KP_End, Qt::Key_End>, + Xkb2Qt<XKB_KEY_KP_Begin, Qt::Key_Clear>, + Xkb2Qt<XKB_KEY_KP_Insert, Qt::Key_Insert>, + Xkb2Qt<XKB_KEY_KP_Delete, Qt::Key_Delete>, + Xkb2Qt<XKB_KEY_KP_Equal, Qt::Key_Equal>, + Xkb2Qt<XKB_KEY_KP_Multiply, Qt::Key_Asterisk>, + Xkb2Qt<XKB_KEY_KP_Add, Qt::Key_Plus>, + Xkb2Qt<XKB_KEY_KP_Separator, Qt::Key_Comma>, + Xkb2Qt<XKB_KEY_KP_Subtract, Qt::Key_Minus>, + Xkb2Qt<XKB_KEY_KP_Decimal, Qt::Key_Period>, + Xkb2Qt<XKB_KEY_KP_Divide, Qt::Key_Slash>, + + // special non-XF86 function keys + + Xkb2Qt<XKB_KEY_Undo, Qt::Key_Undo>, + Xkb2Qt<XKB_KEY_Redo, Qt::Key_Redo>, + Xkb2Qt<XKB_KEY_Find, Qt::Key_Find>, + Xkb2Qt<XKB_KEY_Cancel, Qt::Key_Cancel>, + + // International input method support keys + + // International & multi-key character composition + Xkb2Qt<XKB_KEY_ISO_Level3_Shift, Qt::Key_AltGr>, + Xkb2Qt<XKB_KEY_Multi_key, Qt::Key_Multi_key>, + Xkb2Qt<XKB_KEY_Codeinput, Qt::Key_Codeinput>, + Xkb2Qt<XKB_KEY_SingleCandidate, Qt::Key_SingleCandidate>, + Xkb2Qt<XKB_KEY_MultipleCandidate, Qt::Key_MultipleCandidate>, + Xkb2Qt<XKB_KEY_PreviousCandidate, Qt::Key_PreviousCandidate>, + + // Misc Functions + Xkb2Qt<XKB_KEY_Mode_switch, Qt::Key_Mode_switch>, + Xkb2Qt<XKB_KEY_script_switch, Qt::Key_Mode_switch>, + + // Japanese keyboard support + Xkb2Qt<XKB_KEY_Kanji, Qt::Key_Kanji>, + Xkb2Qt<XKB_KEY_Muhenkan, Qt::Key_Muhenkan>, + //Xkb2Qt<XKB_KEY_Henkan_Mode, Qt::Key_Henkan_Mode>, + Xkb2Qt<XKB_KEY_Henkan_Mode, Qt::Key_Henkan>, + Xkb2Qt<XKB_KEY_Henkan, Qt::Key_Henkan>, + Xkb2Qt<XKB_KEY_Romaji, Qt::Key_Romaji>, + Xkb2Qt<XKB_KEY_Hiragana, Qt::Key_Hiragana>, + Xkb2Qt<XKB_KEY_Katakana, Qt::Key_Katakana>, + Xkb2Qt<XKB_KEY_Hiragana_Katakana, Qt::Key_Hiragana_Katakana>, + Xkb2Qt<XKB_KEY_Zenkaku, Qt::Key_Zenkaku>, + Xkb2Qt<XKB_KEY_Hankaku, Qt::Key_Hankaku>, + Xkb2Qt<XKB_KEY_Zenkaku_Hankaku, Qt::Key_Zenkaku_Hankaku>, + Xkb2Qt<XKB_KEY_Touroku, Qt::Key_Touroku>, + Xkb2Qt<XKB_KEY_Massyo, Qt::Key_Massyo>, + Xkb2Qt<XKB_KEY_Kana_Lock, Qt::Key_Kana_Lock>, + Xkb2Qt<XKB_KEY_Kana_Shift, Qt::Key_Kana_Shift>, + Xkb2Qt<XKB_KEY_Eisu_Shift, Qt::Key_Eisu_Shift>, + Xkb2Qt<XKB_KEY_Eisu_toggle, Qt::Key_Eisu_toggle>, + //Xkb2Qt<XKB_KEY_Kanji_Bangou, Qt::Key_Kanji_Bangou>, + //Xkb2Qt<XKB_KEY_Zen_Koho, Qt::Key_Zen_Koho>, + //Xkb2Qt<XKB_KEY_Mae_Koho, Qt::Key_Mae_Koho>, + Xkb2Qt<XKB_KEY_Kanji_Bangou, Qt::Key_Codeinput>, + Xkb2Qt<XKB_KEY_Zen_Koho, Qt::Key_MultipleCandidate>, + Xkb2Qt<XKB_KEY_Mae_Koho, Qt::Key_PreviousCandidate>, + + // Korean keyboard support + Xkb2Qt<XKB_KEY_Hangul, Qt::Key_Hangul>, + Xkb2Qt<XKB_KEY_Hangul_Start, Qt::Key_Hangul_Start>, + Xkb2Qt<XKB_KEY_Hangul_End, Qt::Key_Hangul_End>, + Xkb2Qt<XKB_KEY_Hangul_Hanja, Qt::Key_Hangul_Hanja>, + Xkb2Qt<XKB_KEY_Hangul_Jamo, Qt::Key_Hangul_Jamo>, + Xkb2Qt<XKB_KEY_Hangul_Romaja, Qt::Key_Hangul_Romaja>, + //Xkb2Qt<XKB_KEY_Hangul_Codeinput, Qt::Key_Hangul_Codeinput>, + Xkb2Qt<XKB_KEY_Hangul_Codeinput, Qt::Key_Codeinput>, + Xkb2Qt<XKB_KEY_Hangul_Jeonja, Qt::Key_Hangul_Jeonja>, + Xkb2Qt<XKB_KEY_Hangul_Banja, Qt::Key_Hangul_Banja>, + Xkb2Qt<XKB_KEY_Hangul_PreHanja, Qt::Key_Hangul_PreHanja>, + Xkb2Qt<XKB_KEY_Hangul_PostHanja, Qt::Key_Hangul_PostHanja>, + //Xkb2Qt<XKB_KEY_Hangul_SingleCandidate,Qt::Key_Hangul_SingleCandidate>, + //Xkb2Qt<XKB_KEY_Hangul_MultipleCandidate,Qt::Key_Hangul_MultipleCandidate>, + //Xkb2Qt<XKB_KEY_Hangul_PreviousCandidate,Qt::Key_Hangul_PreviousCandidate>, + Xkb2Qt<XKB_KEY_Hangul_SingleCandidate, Qt::Key_SingleCandidate>, + Xkb2Qt<XKB_KEY_Hangul_MultipleCandidate,Qt::Key_MultipleCandidate>, + Xkb2Qt<XKB_KEY_Hangul_PreviousCandidate,Qt::Key_PreviousCandidate>, + Xkb2Qt<XKB_KEY_Hangul_Special, Qt::Key_Hangul_Special>, + //Xkb2Qt<XKB_KEY_Hangul_switch, Qt::Key_Hangul_switch>, + Xkb2Qt<XKB_KEY_Hangul_switch, Qt::Key_Mode_switch>, + + // dead keys + Xkb2Qt<XKB_KEY_dead_grave, Qt::Key_Dead_Grave>, + Xkb2Qt<XKB_KEY_dead_acute, Qt::Key_Dead_Acute>, + Xkb2Qt<XKB_KEY_dead_circumflex, Qt::Key_Dead_Circumflex>, + Xkb2Qt<XKB_KEY_dead_tilde, Qt::Key_Dead_Tilde>, + Xkb2Qt<XKB_KEY_dead_macron, Qt::Key_Dead_Macron>, + Xkb2Qt<XKB_KEY_dead_breve, Qt::Key_Dead_Breve>, + Xkb2Qt<XKB_KEY_dead_abovedot, Qt::Key_Dead_Abovedot>, + Xkb2Qt<XKB_KEY_dead_diaeresis, Qt::Key_Dead_Diaeresis>, + Xkb2Qt<XKB_KEY_dead_abovering, Qt::Key_Dead_Abovering>, + Xkb2Qt<XKB_KEY_dead_doubleacute, Qt::Key_Dead_Doubleacute>, + Xkb2Qt<XKB_KEY_dead_caron, Qt::Key_Dead_Caron>, + Xkb2Qt<XKB_KEY_dead_cedilla, Qt::Key_Dead_Cedilla>, + Xkb2Qt<XKB_KEY_dead_ogonek, Qt::Key_Dead_Ogonek>, + Xkb2Qt<XKB_KEY_dead_iota, Qt::Key_Dead_Iota>, + Xkb2Qt<XKB_KEY_dead_voiced_sound, Qt::Key_Dead_Voiced_Sound>, + Xkb2Qt<XKB_KEY_dead_semivoiced_sound, Qt::Key_Dead_Semivoiced_Sound>, + Xkb2Qt<XKB_KEY_dead_belowdot, Qt::Key_Dead_Belowdot>, + Xkb2Qt<XKB_KEY_dead_hook, Qt::Key_Dead_Hook>, + Xkb2Qt<XKB_KEY_dead_horn, Qt::Key_Dead_Horn>, + Xkb2Qt<XKB_KEY_dead_stroke, Qt::Key_Dead_Stroke>, + Xkb2Qt<XKB_KEY_dead_abovecomma, Qt::Key_Dead_Abovecomma>, + Xkb2Qt<XKB_KEY_dead_abovereversedcomma, Qt::Key_Dead_Abovereversedcomma>, + Xkb2Qt<XKB_KEY_dead_doublegrave, Qt::Key_Dead_Doublegrave>, + Xkb2Qt<XKB_KEY_dead_belowring, Qt::Key_Dead_Belowring>, + Xkb2Qt<XKB_KEY_dead_belowmacron, Qt::Key_Dead_Belowmacron>, + Xkb2Qt<XKB_KEY_dead_belowcircumflex, Qt::Key_Dead_Belowcircumflex>, + Xkb2Qt<XKB_KEY_dead_belowtilde, Qt::Key_Dead_Belowtilde>, + Xkb2Qt<XKB_KEY_dead_belowbreve, Qt::Key_Dead_Belowbreve>, + Xkb2Qt<XKB_KEY_dead_belowdiaeresis, Qt::Key_Dead_Belowdiaeresis>, + Xkb2Qt<XKB_KEY_dead_invertedbreve, Qt::Key_Dead_Invertedbreve>, + Xkb2Qt<XKB_KEY_dead_belowcomma, Qt::Key_Dead_Belowcomma>, + Xkb2Qt<XKB_KEY_dead_currency, Qt::Key_Dead_Currency>, + Xkb2Qt<XKB_KEY_dead_a, Qt::Key_Dead_a>, + Xkb2Qt<XKB_KEY_dead_A, Qt::Key_Dead_A>, + Xkb2Qt<XKB_KEY_dead_e, Qt::Key_Dead_e>, + Xkb2Qt<XKB_KEY_dead_E, Qt::Key_Dead_E>, + Xkb2Qt<XKB_KEY_dead_i, Qt::Key_Dead_i>, + Xkb2Qt<XKB_KEY_dead_I, Qt::Key_Dead_I>, + Xkb2Qt<XKB_KEY_dead_o, Qt::Key_Dead_o>, + Xkb2Qt<XKB_KEY_dead_O, Qt::Key_Dead_O>, + Xkb2Qt<XKB_KEY_dead_u, Qt::Key_Dead_u>, + Xkb2Qt<XKB_KEY_dead_U, Qt::Key_Dead_U>, + Xkb2Qt<XKB_KEY_dead_small_schwa, Qt::Key_Dead_Small_Schwa>, + Xkb2Qt<XKB_KEY_dead_capital_schwa, Qt::Key_Dead_Capital_Schwa>, + Xkb2Qt<XKB_KEY_dead_greek, Qt::Key_Dead_Greek>, +/* The following four XKB_KEY_dead keys got removed in libxkbcommon 1.6.0 + The define check is kind of version check here. */ +#ifdef XKB_KEY_dead_lowline + Xkb2Qt<XKB_KEY_dead_lowline, Qt::Key_Dead_Lowline>, + Xkb2Qt<XKB_KEY_dead_aboveverticalline, Qt::Key_Dead_Aboveverticalline>, + Xkb2Qt<XKB_KEY_dead_belowverticalline, Qt::Key_Dead_Belowverticalline>, + Xkb2Qt<XKB_KEY_dead_longsolidusoverlay, Qt::Key_Dead_Longsolidusoverlay>, +#endif + + // Special keys from X.org - This include multimedia keys, + // wireless/bluetooth/uwb keys, special launcher keys, etc. + Xkb2Qt<XKB_KEY_XF86Back, Qt::Key_Back>, + Xkb2Qt<XKB_KEY_XF86Forward, Qt::Key_Forward>, + Xkb2Qt<XKB_KEY_XF86Stop, Qt::Key_Stop>, + Xkb2Qt<XKB_KEY_XF86Refresh, Qt::Key_Refresh>, + Xkb2Qt<XKB_KEY_XF86Favorites, Qt::Key_Favorites>, + Xkb2Qt<XKB_KEY_XF86AudioMedia, Qt::Key_LaunchMedia>, + Xkb2Qt<XKB_KEY_XF86OpenURL, Qt::Key_OpenUrl>, + Xkb2Qt<XKB_KEY_XF86HomePage, Qt::Key_HomePage>, + Xkb2Qt<XKB_KEY_XF86Search, Qt::Key_Search>, + Xkb2Qt<XKB_KEY_XF86AudioLowerVolume, Qt::Key_VolumeDown>, + Xkb2Qt<XKB_KEY_XF86AudioMute, Qt::Key_VolumeMute>, + Xkb2Qt<XKB_KEY_XF86AudioRaiseVolume, Qt::Key_VolumeUp>, + Xkb2Qt<XKB_KEY_XF86AudioPlay, Qt::Key_MediaPlay>, + Xkb2Qt<XKB_KEY_XF86AudioStop, Qt::Key_MediaStop>, + Xkb2Qt<XKB_KEY_XF86AudioPrev, Qt::Key_MediaPrevious>, + Xkb2Qt<XKB_KEY_XF86AudioNext, Qt::Key_MediaNext>, + Xkb2Qt<XKB_KEY_XF86AudioRecord, Qt::Key_MediaRecord>, + Xkb2Qt<XKB_KEY_XF86AudioPause, Qt::Key_MediaPause>, + Xkb2Qt<XKB_KEY_XF86Mail, Qt::Key_LaunchMail>, + Xkb2Qt<XKB_KEY_XF86MyComputer, Qt::Key_LaunchMedia>, + Xkb2Qt<XKB_KEY_XF86Memo, Qt::Key_Memo>, + Xkb2Qt<XKB_KEY_XF86ToDoList, Qt::Key_ToDoList>, + Xkb2Qt<XKB_KEY_XF86Calendar, Qt::Key_Calendar>, + Xkb2Qt<XKB_KEY_XF86PowerDown, Qt::Key_PowerDown>, + Xkb2Qt<XKB_KEY_XF86ContrastAdjust, Qt::Key_ContrastAdjust>, + Xkb2Qt<XKB_KEY_XF86Standby, Qt::Key_Standby>, + Xkb2Qt<XKB_KEY_XF86MonBrightnessUp, Qt::Key_MonBrightnessUp>, + Xkb2Qt<XKB_KEY_XF86MonBrightnessDown, Qt::Key_MonBrightnessDown>, + Xkb2Qt<XKB_KEY_XF86KbdLightOnOff, Qt::Key_KeyboardLightOnOff>, + Xkb2Qt<XKB_KEY_XF86KbdBrightnessUp, Qt::Key_KeyboardBrightnessUp>, + Xkb2Qt<XKB_KEY_XF86KbdBrightnessDown, Qt::Key_KeyboardBrightnessDown>, + Xkb2Qt<XKB_KEY_XF86PowerOff, Qt::Key_PowerOff>, + Xkb2Qt<XKB_KEY_XF86WakeUp, Qt::Key_WakeUp>, + Xkb2Qt<XKB_KEY_XF86Eject, Qt::Key_Eject>, + Xkb2Qt<XKB_KEY_XF86ScreenSaver, Qt::Key_ScreenSaver>, + Xkb2Qt<XKB_KEY_XF86WWW, Qt::Key_WWW>, + Xkb2Qt<XKB_KEY_XF86Sleep, Qt::Key_Sleep>, + Xkb2Qt<XKB_KEY_XF86LightBulb, Qt::Key_LightBulb>, + Xkb2Qt<XKB_KEY_XF86Shop, Qt::Key_Shop>, + Xkb2Qt<XKB_KEY_XF86History, Qt::Key_History>, + Xkb2Qt<XKB_KEY_XF86AddFavorite, Qt::Key_AddFavorite>, + Xkb2Qt<XKB_KEY_XF86HotLinks, Qt::Key_HotLinks>, + Xkb2Qt<XKB_KEY_XF86BrightnessAdjust, Qt::Key_BrightnessAdjust>, + Xkb2Qt<XKB_KEY_XF86Finance, Qt::Key_Finance>, + Xkb2Qt<XKB_KEY_XF86Community, Qt::Key_Community>, + Xkb2Qt<XKB_KEY_XF86AudioRewind, Qt::Key_AudioRewind>, + Xkb2Qt<XKB_KEY_XF86BackForward, Qt::Key_BackForward>, + Xkb2Qt<XKB_KEY_XF86ApplicationLeft, Qt::Key_ApplicationLeft>, + Xkb2Qt<XKB_KEY_XF86ApplicationRight, Qt::Key_ApplicationRight>, + Xkb2Qt<XKB_KEY_XF86Book, Qt::Key_Book>, + Xkb2Qt<XKB_KEY_XF86CD, Qt::Key_CD>, + Xkb2Qt<XKB_KEY_XF86Calculater, Qt::Key_Calculator>, + Xkb2Qt<XKB_KEY_XF86Calculator, Qt::Key_Calculator>, + Xkb2Qt<XKB_KEY_XF86Clear, Qt::Key_Clear>, + Xkb2Qt<XKB_KEY_XF86ClearGrab, Qt::Key_ClearGrab>, + Xkb2Qt<XKB_KEY_XF86Close, Qt::Key_Close>, + Xkb2Qt<XKB_KEY_XF86Copy, Qt::Key_Copy>, + Xkb2Qt<XKB_KEY_XF86Cut, Qt::Key_Cut>, + Xkb2Qt<XKB_KEY_XF86Display, Qt::Key_Display>, + Xkb2Qt<XKB_KEY_XF86DOS, Qt::Key_DOS>, + Xkb2Qt<XKB_KEY_XF86Documents, Qt::Key_Documents>, + Xkb2Qt<XKB_KEY_XF86Excel, Qt::Key_Excel>, + Xkb2Qt<XKB_KEY_XF86Explorer, Qt::Key_Explorer>, + Xkb2Qt<XKB_KEY_XF86Game, Qt::Key_Game>, + Xkb2Qt<XKB_KEY_XF86Go, Qt::Key_Go>, + Xkb2Qt<XKB_KEY_XF86iTouch, Qt::Key_iTouch>, + Xkb2Qt<XKB_KEY_XF86LogOff, Qt::Key_LogOff>, + Xkb2Qt<XKB_KEY_XF86Market, Qt::Key_Market>, + Xkb2Qt<XKB_KEY_XF86Meeting, Qt::Key_Meeting>, + Xkb2Qt<XKB_KEY_XF86MenuKB, Qt::Key_MenuKB>, + Xkb2Qt<XKB_KEY_XF86MenuPB, Qt::Key_MenuPB>, + Xkb2Qt<XKB_KEY_XF86MySites, Qt::Key_MySites>, + Xkb2Qt<XKB_KEY_XF86New, Qt::Key_New>, + Xkb2Qt<XKB_KEY_XF86News, Qt::Key_News>, + Xkb2Qt<XKB_KEY_XF86OfficeHome, Qt::Key_OfficeHome>, + Xkb2Qt<XKB_KEY_XF86Open, Qt::Key_Open>, + Xkb2Qt<XKB_KEY_XF86Option, Qt::Key_Option>, + Xkb2Qt<XKB_KEY_XF86Paste, Qt::Key_Paste>, + Xkb2Qt<XKB_KEY_XF86Phone, Qt::Key_Phone>, + Xkb2Qt<XKB_KEY_XF86Reply, Qt::Key_Reply>, + Xkb2Qt<XKB_KEY_XF86Reload, Qt::Key_Reload>, + Xkb2Qt<XKB_KEY_XF86RotateWindows, Qt::Key_RotateWindows>, + Xkb2Qt<XKB_KEY_XF86RotationPB, Qt::Key_RotationPB>, + Xkb2Qt<XKB_KEY_XF86RotationKB, Qt::Key_RotationKB>, + Xkb2Qt<XKB_KEY_XF86Save, Qt::Key_Save>, + Xkb2Qt<XKB_KEY_XF86Send, Qt::Key_Send>, + Xkb2Qt<XKB_KEY_XF86Spell, Qt::Key_Spell>, + Xkb2Qt<XKB_KEY_XF86SplitScreen, Qt::Key_SplitScreen>, + Xkb2Qt<XKB_KEY_XF86Support, Qt::Key_Support>, + Xkb2Qt<XKB_KEY_XF86TaskPane, Qt::Key_TaskPane>, + Xkb2Qt<XKB_KEY_XF86Terminal, Qt::Key_Terminal>, + Xkb2Qt<XKB_KEY_XF86Tools, Qt::Key_Tools>, + Xkb2Qt<XKB_KEY_XF86Travel, Qt::Key_Travel>, + Xkb2Qt<XKB_KEY_XF86Video, Qt::Key_Video>, + Xkb2Qt<XKB_KEY_XF86Word, Qt::Key_Word>, + Xkb2Qt<XKB_KEY_XF86Xfer, Qt::Key_Xfer>, + Xkb2Qt<XKB_KEY_XF86ZoomIn, Qt::Key_ZoomIn>, + Xkb2Qt<XKB_KEY_XF86ZoomOut, Qt::Key_ZoomOut>, + Xkb2Qt<XKB_KEY_XF86Away, Qt::Key_Away>, + Xkb2Qt<XKB_KEY_XF86Messenger, Qt::Key_Messenger>, + Xkb2Qt<XKB_KEY_XF86WebCam, Qt::Key_WebCam>, + Xkb2Qt<XKB_KEY_XF86MailForward, Qt::Key_MailForward>, + Xkb2Qt<XKB_KEY_XF86Pictures, Qt::Key_Pictures>, + Xkb2Qt<XKB_KEY_XF86Music, Qt::Key_Music>, + Xkb2Qt<XKB_KEY_XF86Battery, Qt::Key_Battery>, + Xkb2Qt<XKB_KEY_XF86Bluetooth, Qt::Key_Bluetooth>, + Xkb2Qt<XKB_KEY_XF86WLAN, Qt::Key_WLAN>, + Xkb2Qt<XKB_KEY_XF86UWB, Qt::Key_UWB>, + Xkb2Qt<XKB_KEY_XF86AudioForward, Qt::Key_AudioForward>, + Xkb2Qt<XKB_KEY_XF86AudioRepeat, Qt::Key_AudioRepeat>, + Xkb2Qt<XKB_KEY_XF86AudioRandomPlay, Qt::Key_AudioRandomPlay>, + Xkb2Qt<XKB_KEY_XF86Subtitle, Qt::Key_Subtitle>, + Xkb2Qt<XKB_KEY_XF86AudioCycleTrack, Qt::Key_AudioCycleTrack>, + Xkb2Qt<XKB_KEY_XF86Time, Qt::Key_Time>, + Xkb2Qt<XKB_KEY_XF86Select, Qt::Key_Select>, + Xkb2Qt<XKB_KEY_XF86View, Qt::Key_View>, + Xkb2Qt<XKB_KEY_XF86TopMenu, Qt::Key_TopMenu>, + Xkb2Qt<XKB_KEY_XF86Red, Qt::Key_Red>, + Xkb2Qt<XKB_KEY_XF86Green, Qt::Key_Green>, + Xkb2Qt<XKB_KEY_XF86Yellow, Qt::Key_Yellow>, + Xkb2Qt<XKB_KEY_XF86Blue, Qt::Key_Blue>, + Xkb2Qt<XKB_KEY_XF86Bluetooth, Qt::Key_Bluetooth>, + Xkb2Qt<XKB_KEY_XF86Suspend, Qt::Key_Suspend>, + Xkb2Qt<XKB_KEY_XF86Hibernate, Qt::Key_Hibernate>, + Xkb2Qt<XKB_KEY_XF86TouchpadToggle, Qt::Key_TouchpadToggle>, + Xkb2Qt<XKB_KEY_XF86TouchpadOn, Qt::Key_TouchpadOn>, + Xkb2Qt<XKB_KEY_XF86TouchpadOff, Qt::Key_TouchpadOff>, + Xkb2Qt<XKB_KEY_XF86AudioMicMute, Qt::Key_MicMute>, + Xkb2Qt<XKB_KEY_XF86Launch0, Qt::Key_Launch0>, + Xkb2Qt<XKB_KEY_XF86Launch1, Qt::Key_Launch1>, + Xkb2Qt<XKB_KEY_XF86Launch2, Qt::Key_Launch2>, + Xkb2Qt<XKB_KEY_XF86Launch3, Qt::Key_Launch3>, + Xkb2Qt<XKB_KEY_XF86Launch4, Qt::Key_Launch4>, + Xkb2Qt<XKB_KEY_XF86Launch5, Qt::Key_Launch5>, + Xkb2Qt<XKB_KEY_XF86Launch6, Qt::Key_Launch6>, + Xkb2Qt<XKB_KEY_XF86Launch7, Qt::Key_Launch7>, + Xkb2Qt<XKB_KEY_XF86Launch8, Qt::Key_Launch8>, + Xkb2Qt<XKB_KEY_XF86Launch9, Qt::Key_Launch9>, + Xkb2Qt<XKB_KEY_XF86LaunchA, Qt::Key_LaunchA>, + Xkb2Qt<XKB_KEY_XF86LaunchB, Qt::Key_LaunchB>, + Xkb2Qt<XKB_KEY_XF86LaunchC, Qt::Key_LaunchC>, + Xkb2Qt<XKB_KEY_XF86LaunchD, Qt::Key_LaunchD>, + Xkb2Qt<XKB_KEY_XF86LaunchE, Qt::Key_LaunchE>, + Xkb2Qt<XKB_KEY_XF86LaunchF, Qt::Key_LaunchF> + >::Data{} +); + +xkb_keysym_t QXkbCommon::qxkbcommon_xkb_keysym_to_upper(xkb_keysym_t ks) +{ + xkb_keysym_t lower, upper; + + xkbcommon_XConvertCase(ks, &lower, &upper); + + return upper; +} + +QString QXkbCommon::lookupString(struct xkb_state *state, xkb_keycode_t code) +{ + QVarLengthArray<char, 32> chars(32); + const int size = xkb_state_key_get_utf8(state, code, chars.data(), chars.size()); + if (Q_UNLIKELY(size + 1 > chars.size())) { // +1 for NUL + chars.resize(size + 1); + xkb_state_key_get_utf8(state, code, chars.data(), chars.size()); + } + return QString::fromUtf8(chars.constData(), size); +} + +QString QXkbCommon::lookupStringNoKeysymTransformations(xkb_keysym_t keysym) +{ + QVarLengthArray<char, 32> chars(32); + const int size = xkb_keysym_to_utf8(keysym, chars.data(), chars.size()); + if (size == 0) + return QString(); // the keysym does not have a Unicode representation + + if (Q_UNLIKELY(size > chars.size())) { + chars.resize(size); + xkb_keysym_to_utf8(keysym, chars.data(), chars.size()); + } + return QString::fromUtf8(chars.constData(), size - 1); +} + +QList<xkb_keysym_t> QXkbCommon::toKeysym(QKeyEvent *event) +{ + QList<xkb_keysym_t> keysyms; + int qtKey = event->key(); + + if (qtKey >= Qt::Key_F1 && qtKey <= Qt::Key_F35) { + keysyms.append(XKB_KEY_F1 + (qtKey - Qt::Key_F1)); + } else if (event->modifiers() & Qt::KeypadModifier) { + if (qtKey >= Qt::Key_0 && qtKey <= Qt::Key_9) + keysyms.append(XKB_KEY_KP_0 + (qtKey - Qt::Key_0)); + } else if (isLatin1(qtKey) && event->text().isUpper()) { + keysyms.append(qtKey); + } + + if (!keysyms.isEmpty()) + return keysyms; + + // check if we have a direct mapping + auto it = std::find_if(KeyTbl.cbegin(), KeyTbl.cend(), [&qtKey](xkb2qt_t elem) { + return elem.qt == static_cast<uint>(qtKey); + }); + if (it != KeyTbl.end()) { + keysyms.append(it->xkb); + return keysyms; + } + + QList<uint> ucs4; + if (event->text().isEmpty()) + ucs4.append(qtKey); + else + ucs4 = event->text().toUcs4(); + + // From libxkbcommon keysym-utf.c: + // "We allow to represent any UCS character in the range U-00000000 to + // U-00FFFFFF by a keysym value in the range 0x01000000 to 0x01ffffff." + for (uint utf32 : std::as_const(ucs4)) + keysyms.append(utf32 | 0x01000000); + + return keysyms; +} + +int QXkbCommon::keysymToQtKey(xkb_keysym_t keysym, Qt::KeyboardModifiers modifiers) +{ + return keysymToQtKey(keysym, modifiers, nullptr, 0); +} + +int QXkbCommon::keysymToQtKey(xkb_keysym_t keysym, Qt::KeyboardModifiers modifiers, + xkb_state *state, xkb_keycode_t code, + bool superAsMeta, bool hyperAsMeta) +{ + // Note 1: All standard key sequences on linux (as defined in platform theme) + // that use a latin character also contain a control modifier, which is why + // checking for Qt::ControlModifier is sufficient here. It is possible to + // override QPlatformTheme::keyBindings() and provide custom sequences for + // QKeySequence::StandardKey. Custom sequences probably should respect this + // convention (alternatively, we could test against other modifiers here). + // Note 2: The possibleKeys() shorcut mechanism is not affected by this value + // adjustment and does its own thing. + if (modifiers & Qt::ControlModifier) { + // With standard shortcuts we should prefer a latin character, this is + // for checks like "some qkeyevent == QKeySequence::Copy" to work even + // when using for example 'russian' keyboard layout. + if (!QXkbCommon::isLatin1(keysym)) { + xkb_keysym_t latinKeysym = QXkbCommon::lookupLatinKeysym(state, code); + if (latinKeysym != XKB_KEY_NoSymbol) + keysym = latinKeysym; + } + } + + return keysymToQtKey_internal(keysym, modifiers, state, code, superAsMeta, hyperAsMeta); +} + +static int keysymToQtKey_internal(xkb_keysym_t keysym, Qt::KeyboardModifiers modifiers, + xkb_state *state, xkb_keycode_t code, + bool superAsMeta, bool hyperAsMeta) +{ + int qtKey = 0; + + // lookup from direct mapping + if (keysym >= XKB_KEY_F1 && keysym <= XKB_KEY_F35) { + // function keys + qtKey = Qt::Key_F1 + (keysym - XKB_KEY_F1); + } else if (keysym >= XKB_KEY_KP_0 && keysym <= XKB_KEY_KP_9) { + // numeric keypad keys + qtKey = Qt::Key_0 + (keysym - XKB_KEY_KP_0); + } else if (QXkbCommon::isLatin1(keysym)) { + // Most Qt::Key values are determined by their upper-case version, + // where this is in the Latin-1 repertoire. So start with that: + qtKey = QXkbCommon::qxkbcommon_xkb_keysym_to_upper(keysym); + // However, Key_mu and Key_ydiaeresis are U+00B5 MICRO SIGN and + // U+00FF LATIN SMALL LETTER Y WITH DIAERESIS, both lower-case, + // with upper-case forms outside Latin-1, so use them as they are + // since they're the Qt::Key values. + if (!QXkbCommon::isLatin1(qtKey)) + qtKey = keysym; + } else { + // check if we have a direct mapping + xkb2qt_t searchKey{keysym, 0}; + auto it = std::lower_bound(KeyTbl.cbegin(), KeyTbl.cend(), searchKey); + if (it != KeyTbl.end() && !(searchKey < *it)) + qtKey = it->qt; + + // translate Super/Hyper keys to Meta if we're using them as the MetaModifier + if (superAsMeta && (qtKey == Qt::Key_Super_L || qtKey == Qt::Key_Super_R)) + qtKey = Qt::Key_Meta; + if (hyperAsMeta && (qtKey == Qt::Key_Hyper_L || qtKey == Qt::Key_Hyper_R)) + qtKey = Qt::Key_Meta; + } + + if (qtKey) + return qtKey; + + // lookup from unicode + QString text; + if (!state || modifiers & Qt::ControlModifier) { + // Control modifier changes the text to ASCII control character, therefore we + // can't use this text to map keysym to a qt key. We can use the same keysym + // (it is not affectd by transformation) to obtain untransformed text. For details + // see "Appendix A. Default Symbol Transformations" in the XKB specification. + text = QXkbCommon::lookupStringNoKeysymTransformations(keysym); + } else { + text = QXkbCommon::lookupString(state, code); + } + if (!text.isEmpty()) { + if (text.unicode()->isDigit()) { + // Ensures that also non-latin digits are mapped to corresponding qt keys, + // e.g CTRL + ۲ (arabic two), is mapped to CTRL + Qt::Key_2. + qtKey = Qt::Key_0 + text.unicode()->digitValue(); + } else { + text = text.toUpper(); + QStringIterator i(text); + qtKey = i.next(0); + } + } + + return qtKey; +} + +Qt::KeyboardModifiers QXkbCommon::modifiers(struct xkb_state *state, xkb_keysym_t keysym) +{ + Qt::KeyboardModifiers modifiers = Qt::NoModifier; + + if (xkb_state_mod_name_is_active(state, XKB_MOD_NAME_CTRL, XKB_STATE_MODS_EFFECTIVE) > 0) + modifiers |= Qt::ControlModifier; + if (xkb_state_mod_name_is_active(state, XKB_MOD_NAME_ALT, XKB_STATE_MODS_EFFECTIVE) > 0) + modifiers |= Qt::AltModifier; + if (xkb_state_mod_name_is_active(state, XKB_MOD_NAME_SHIFT, XKB_STATE_MODS_EFFECTIVE) > 0) + modifiers |= Qt::ShiftModifier; + if (xkb_state_mod_name_is_active(state, XKB_MOD_NAME_LOGO, XKB_STATE_MODS_EFFECTIVE) > 0) + modifiers |= Qt::MetaModifier; + + if (isKeypad(keysym)) + modifiers |= Qt::KeypadModifier; + + return modifiers; +} + +// Possible modifier states. +static const Qt::KeyboardModifiers ModsTbl[] = { + Qt::NoModifier, // 0 + Qt::ShiftModifier, // 1 + Qt::ControlModifier, // 2 + Qt::ControlModifier | Qt::ShiftModifier, // 3 + Qt::AltModifier, // 4 + Qt::AltModifier | Qt::ShiftModifier, // 5 + Qt::AltModifier | Qt::ControlModifier, // 6 + Qt::AltModifier | Qt::ShiftModifier | Qt::ControlModifier, // 7 + Qt::NoModifier // Fall-back to raw Key_*, for non-latin1 kb layouts +}; + +/* + Compatibility until all sub modules have transitioned to new API below +*/ +QList<int> QXkbCommon::possibleKeys(xkb_state *state, const QKeyEvent *event, + bool superAsMeta, bool hyperAsMeta) +{ + QList<int> result; + auto keyCombinations = possibleKeyCombinations(state, event, superAsMeta, hyperAsMeta); + for (auto keyCombination : keyCombinations) + result << keyCombination.toCombined(); + + return result; +} + +QList<QKeyCombination> QXkbCommon::possibleKeyCombinations(xkb_state *state, const QKeyEvent *event, + bool superAsMeta, bool hyperAsMeta) +{ + QList<QKeyCombination> result; + quint32 keycode = event->nativeScanCode(); + if (!keycode) + return result; + + Qt::KeyboardModifiers modifiers = event->modifiers(); + xkb_keymap *keymap = xkb_state_get_keymap(state); + // turn off the modifier bits which doesn't participate in shortcuts + Qt::KeyboardModifiers notNeeded = Qt::KeypadModifier | Qt::GroupSwitchModifier; + modifiers &= ~notNeeded; + // create a fresh kb state and test against the relevant modifier combinations + ScopedXKBState scopedXkbQueryState(xkb_state_new(keymap)); + xkb_state *queryState = scopedXkbQueryState.get(); + if (!queryState) { + qCWarning(lcQpaKeyMapper) << Q_FUNC_INFO << "failed to compile xkb keymap"; + return result; + } + // get kb state from the master state and update the temporary state + xkb_layout_index_t lockedLayout = xkb_state_serialize_layout(state, XKB_STATE_LAYOUT_LOCKED); + xkb_mod_mask_t latchedMods = xkb_state_serialize_mods(state, XKB_STATE_MODS_LATCHED); + xkb_mod_mask_t lockedMods = xkb_state_serialize_mods(state, XKB_STATE_MODS_LOCKED); + xkb_mod_mask_t depressedMods = xkb_state_serialize_mods(state, XKB_STATE_MODS_DEPRESSED); + xkb_state_update_mask(queryState, depressedMods, latchedMods, lockedMods, 0, 0, lockedLayout); + // handle shortcuts for level three and above + xkb_layout_index_t layoutIndex = xkb_state_key_get_layout(queryState, keycode); + xkb_level_index_t levelIndex = 0; + if (layoutIndex != XKB_LAYOUT_INVALID) { + levelIndex = xkb_state_key_get_level(queryState, keycode, layoutIndex); + if (levelIndex == XKB_LEVEL_INVALID) + levelIndex = 0; + } + if (levelIndex <= 1) + xkb_state_update_mask(queryState, 0, latchedMods, lockedMods, 0, 0, lockedLayout); + + xkb_keysym_t sym = xkb_state_key_get_one_sym(queryState, keycode); + if (sym == XKB_KEY_NoSymbol) + return result; + + int baseQtKey = keysymToQtKey_internal(sym, modifiers, queryState, keycode, superAsMeta, hyperAsMeta); + if (baseQtKey) + result += QKeyCombination::fromCombined(baseQtKey + int(modifiers)); + + xkb_mod_index_t shiftMod = xkb_keymap_mod_get_index(keymap, "Shift"); + xkb_mod_index_t altMod = xkb_keymap_mod_get_index(keymap, "Alt"); + xkb_mod_index_t controlMod = xkb_keymap_mod_get_index(keymap, "Control"); + xkb_mod_index_t metaMod = xkb_keymap_mod_get_index(keymap, "Meta"); + + Q_ASSERT(shiftMod < 32); + Q_ASSERT(altMod < 32); + Q_ASSERT(controlMod < 32); + + xkb_mod_mask_t depressed; + int qtKey = 0; + // obtain a list of possible shortcuts for the given key event + for (uint i = 1; i < sizeof(ModsTbl) / sizeof(*ModsTbl) ; ++i) { + Qt::KeyboardModifiers neededMods = ModsTbl[i]; + if ((modifiers & neededMods) == neededMods) { + if (i == 8) { + if (isLatin1(baseQtKey)) + continue; + // add a latin key as a fall back key + sym = lookupLatinKeysym(state, keycode); + } else { + depressed = 0; + if (neededMods & Qt::AltModifier) + depressed |= (1 << altMod); + if (neededMods & Qt::ShiftModifier) + depressed |= (1 << shiftMod); + if (neededMods & Qt::ControlModifier) + depressed |= (1 << controlMod); + if (metaMod < 32 && neededMods & Qt::MetaModifier) + depressed |= (1 << metaMod); + xkb_state_update_mask(queryState, depressed, latchedMods, lockedMods, 0, 0, lockedLayout); + sym = xkb_state_key_get_one_sym(queryState, keycode); + } + if (sym == XKB_KEY_NoSymbol) + continue; + + Qt::KeyboardModifiers mods = modifiers & ~neededMods; + qtKey = keysymToQtKey_internal(sym, mods, queryState, keycode, superAsMeta, hyperAsMeta); + if (!qtKey || qtKey == baseQtKey) + continue; + + // catch only more specific shortcuts, i.e. Ctrl+Shift+= also generates Ctrl++ and +, + // but Ctrl++ is more specific than +, so we should skip the last one + bool ambiguous = false; + for (auto keyCombination : std::as_const(result)) { + if (keyCombination.key() == qtKey + && (keyCombination.keyboardModifiers() & mods) == mods) { + ambiguous = true; + break; + } + } + if (ambiguous) + continue; + + result += QKeyCombination::fromCombined(qtKey + int(mods)); + } + } + + return result; +} + +void QXkbCommon::verifyHasLatinLayout(xkb_keymap *keymap) +{ + const xkb_layout_index_t layoutCount = xkb_keymap_num_layouts(keymap); + const xkb_keycode_t minKeycode = xkb_keymap_min_keycode(keymap); + const xkb_keycode_t maxKeycode = xkb_keymap_max_keycode(keymap); + + const xkb_keysym_t *keysyms = nullptr; + int nrLatinKeys = 0; + for (xkb_layout_index_t layout = 0; layout < layoutCount; ++layout) { + for (xkb_keycode_t code = minKeycode; code < maxKeycode; ++code) { + xkb_keymap_key_get_syms_by_level(keymap, code, layout, 0, &keysyms); + if (keysyms && isLatin1(keysyms[0])) + nrLatinKeys++; + if (nrLatinKeys > 10) // arbitrarily chosen threshold + return; + } + } + // This means that lookupLatinKeysym() will not find anything and latin + // key shortcuts might not work. This is a bug in the affected desktop + // environment. Usually can be solved via system settings by adding e.g. 'us' + // layout to the list of selected layouts, or by using command line, "setxkbmap + // -layout rus,en". The position of latin key based layout in the list of the + // selected layouts is irrelevant. Properly functioning desktop environments + // handle this behind the scenes, even if no latin key based layout has been + // explicitly listed in the selected layouts. + qCDebug(lcQpaKeyMapper, "no keyboard layouts with latin keys present"); +} + +xkb_keysym_t QXkbCommon::lookupLatinKeysym(xkb_state *state, xkb_keycode_t keycode) +{ + xkb_layout_index_t layout; + xkb_keysym_t sym = XKB_KEY_NoSymbol; + if (!state) + return sym; + xkb_keymap *keymap = xkb_state_get_keymap(state); + const xkb_layout_index_t layoutCount = xkb_keymap_num_layouts_for_key(keymap, keycode); + const xkb_layout_index_t currentLayout = xkb_state_key_get_layout(state, keycode); + // Look at user layouts in the order in which they are defined in system + // settings to find a latin keysym. + for (layout = 0; layout < layoutCount; ++layout) { + if (layout == currentLayout) + continue; + const xkb_keysym_t *syms = nullptr; + xkb_level_index_t level = xkb_state_key_get_level(state, keycode, layout); + if (xkb_keymap_key_get_syms_by_level(keymap, keycode, layout, level, &syms) != 1) + continue; + if (isLatin1(syms[0])) { + sym = syms[0]; + break; + } + } + + if (sym == XKB_KEY_NoSymbol) + return sym; + + xkb_mod_mask_t latchedMods = xkb_state_serialize_mods(state, XKB_STATE_MODS_LATCHED); + xkb_mod_mask_t lockedMods = xkb_state_serialize_mods(state, XKB_STATE_MODS_LOCKED); + + // Check for uniqueness, consider the following setup: + // setxkbmap -layout us,ru,us -variant dvorak,, -option 'grp:ctrl_alt_toggle' (set 'ru' as active). + // In this setup, the user would expect to trigger a ctrl+q shortcut by pressing ctrl+<physical x key>, + // because "US dvorak" is higher up in the layout settings list. This check verifies that an obtained + // 'sym' can not be acquired by any other layout higher up in the user's layout list. If it can be acquired + // then the obtained key is not unique. This prevents ctrl+<physical q key> from generating a ctrl+q + // shortcut in the above described setup. We don't want ctrl+<physical x key> and ctrl+<physical q key> to + // generate the same shortcut event in this case. + const xkb_keycode_t minKeycode = xkb_keymap_min_keycode(keymap); + const xkb_keycode_t maxKeycode = xkb_keymap_max_keycode(keymap); + ScopedXKBState queryState(xkb_state_new(keymap)); + for (xkb_layout_index_t prevLayout = 0; prevLayout < layout; ++prevLayout) { + xkb_state_update_mask(queryState.get(), 0, latchedMods, lockedMods, 0, 0, prevLayout); + for (xkb_keycode_t code = minKeycode; code < maxKeycode; ++code) { + xkb_keysym_t prevSym = xkb_state_key_get_one_sym(queryState.get(), code); + if (prevSym == sym) { + sym = XKB_KEY_NoSymbol; + break; + } + } + } + + return sym; +} + +void QXkbCommon::setXkbContext(QPlatformInputContext *inputContext, struct xkb_context *context) +{ + if (!inputContext || !context) + return; + + const char *const inputContextClassName = "QComposeInputContext"; + const char *const normalizedSignature = "setXkbContext(xkb_context*)"; + + if (inputContext->objectName() != QLatin1StringView(inputContextClassName)) + return; + + static const QMetaMethod setXkbContext = [&]() { + int methodIndex = inputContext->metaObject()->indexOfMethod(normalizedSignature); + QMetaMethod method = inputContext->metaObject()->method(methodIndex); + Q_ASSERT(method.isValid()); + if (!method.isValid()) + qCWarning(lcQpaKeyMapper) << normalizedSignature << "not found on" << inputContextClassName; + return method; + }(); + + if (!setXkbContext.isValid()) + return; + + setXkbContext.invoke(inputContext, Qt::DirectConnection, Q_ARG(struct xkb_context*, context)); +} + +QT_END_NAMESPACE diff --git a/src/gui/platform/unix/qxkbcommon_3rdparty.cpp b/src/gui/platform/unix/qxkbcommon_3rdparty.cpp new file mode 100644 index 0000000000..207c103235 --- /dev/null +++ b/src/gui/platform/unix/qxkbcommon_3rdparty.cpp @@ -0,0 +1,187 @@ +// Copyright (C) 2019 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +/* Copyright 1985, 1987, 1990, 1998 The Open Group + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + Except as contained in this notice, the names of the authors or their + institutions shall not be used in advertising or otherwise to promote the + sale, use or other dealings in this Software without prior written + authorization from the authors. + + + + Copyright © 2009 Dan Nicholson + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +/* + XConvertCase was copied from src/3rdparty/xkbcommon/src/keysym.c + The following code modifications were applied: + + XConvertCase() was renamed to xkbcommon_XConvertCase(), to not confuse it + with Xlib's XConvertCase(). + + UCSConvertCase() was renamed to qt_UCSConvertCase() and function's body was + replaced to use Qt APIs for doing case conversion, which should give us better + results instead of using the less complete version from keysym.c +*/ + +#include "qxkbcommon_p.h" + +#include <QtCore/QChar> + +QT_BEGIN_NAMESPACE + +static void qt_UCSConvertCase(uint32_t code, xkb_keysym_t *lower, xkb_keysym_t *upper) +{ + *lower = QChar::toLower(code); + *upper = QChar::toUpper(code); +} + +void QXkbCommon::xkbcommon_XConvertCase(xkb_keysym_t sym, xkb_keysym_t *lower, xkb_keysym_t *upper) +{ + /* Latin 1 keysym */ + if (sym < 0x100) { + qt_UCSConvertCase(sym, lower, upper); + return; + } + + /* Unicode keysym */ + if ((sym & 0xff000000) == 0x01000000) { + qt_UCSConvertCase((sym & 0x00ffffff), lower, upper); + *upper |= 0x01000000; + *lower |= 0x01000000; + return; + } + + /* Legacy keysym */ + + *lower = sym; + *upper = sym; + + switch (sym >> 8) { + case 1: /* Latin 2 */ + /* Assume the KeySym is a legal value (ignore discontinuities) */ + if (sym == XKB_KEY_Aogonek) + *lower = XKB_KEY_aogonek; + else if (sym >= XKB_KEY_Lstroke && sym <= XKB_KEY_Sacute) + *lower += (XKB_KEY_lstroke - XKB_KEY_Lstroke); + else if (sym >= XKB_KEY_Scaron && sym <= XKB_KEY_Zacute) + *lower += (XKB_KEY_scaron - XKB_KEY_Scaron); + else if (sym >= XKB_KEY_Zcaron && sym <= XKB_KEY_Zabovedot) + *lower += (XKB_KEY_zcaron - XKB_KEY_Zcaron); + else if (sym == XKB_KEY_aogonek) + *upper = XKB_KEY_Aogonek; + else if (sym >= XKB_KEY_lstroke && sym <= XKB_KEY_sacute) + *upper -= (XKB_KEY_lstroke - XKB_KEY_Lstroke); + else if (sym >= XKB_KEY_scaron && sym <= XKB_KEY_zacute) + *upper -= (XKB_KEY_scaron - XKB_KEY_Scaron); + else if (sym >= XKB_KEY_zcaron && sym <= XKB_KEY_zabovedot) + *upper -= (XKB_KEY_zcaron - XKB_KEY_Zcaron); + else if (sym >= XKB_KEY_Racute && sym <= XKB_KEY_Tcedilla) + *lower += (XKB_KEY_racute - XKB_KEY_Racute); + else if (sym >= XKB_KEY_racute && sym <= XKB_KEY_tcedilla) + *upper -= (XKB_KEY_racute - XKB_KEY_Racute); + break; + case 2: /* Latin 3 */ + /* Assume the KeySym is a legal value (ignore discontinuities) */ + if (sym >= XKB_KEY_Hstroke && sym <= XKB_KEY_Hcircumflex) + *lower += (XKB_KEY_hstroke - XKB_KEY_Hstroke); + else if (sym >= XKB_KEY_Gbreve && sym <= XKB_KEY_Jcircumflex) + *lower += (XKB_KEY_gbreve - XKB_KEY_Gbreve); + else if (sym >= XKB_KEY_hstroke && sym <= XKB_KEY_hcircumflex) + *upper -= (XKB_KEY_hstroke - XKB_KEY_Hstroke); + else if (sym >= XKB_KEY_gbreve && sym <= XKB_KEY_jcircumflex) + *upper -= (XKB_KEY_gbreve - XKB_KEY_Gbreve); + else if (sym >= XKB_KEY_Cabovedot && sym <= XKB_KEY_Scircumflex) + *lower += (XKB_KEY_cabovedot - XKB_KEY_Cabovedot); + else if (sym >= XKB_KEY_cabovedot && sym <= XKB_KEY_scircumflex) + *upper -= (XKB_KEY_cabovedot - XKB_KEY_Cabovedot); + break; + case 3: /* Latin 4 */ + /* Assume the KeySym is a legal value (ignore discontinuities) */ + if (sym >= XKB_KEY_Rcedilla && sym <= XKB_KEY_Tslash) + *lower += (XKB_KEY_rcedilla - XKB_KEY_Rcedilla); + else if (sym >= XKB_KEY_rcedilla && sym <= XKB_KEY_tslash) + *upper -= (XKB_KEY_rcedilla - XKB_KEY_Rcedilla); + else if (sym == XKB_KEY_ENG) + *lower = XKB_KEY_eng; + else if (sym == XKB_KEY_eng) + *upper = XKB_KEY_ENG; + else if (sym >= XKB_KEY_Amacron && sym <= XKB_KEY_Umacron) + *lower += (XKB_KEY_amacron - XKB_KEY_Amacron); + else if (sym >= XKB_KEY_amacron && sym <= XKB_KEY_umacron) + *upper -= (XKB_KEY_amacron - XKB_KEY_Amacron); + break; + case 6: /* Cyrillic */ + /* Assume the KeySym is a legal value (ignore discontinuities) */ + if (sym >= XKB_KEY_Serbian_DJE && sym <= XKB_KEY_Serbian_DZE) + *lower -= (XKB_KEY_Serbian_DJE - XKB_KEY_Serbian_dje); + else if (sym >= XKB_KEY_Serbian_dje && sym <= XKB_KEY_Serbian_dze) + *upper += (XKB_KEY_Serbian_DJE - XKB_KEY_Serbian_dje); + else if (sym >= XKB_KEY_Cyrillic_YU && sym <= XKB_KEY_Cyrillic_HARDSIGN) + *lower -= (XKB_KEY_Cyrillic_YU - XKB_KEY_Cyrillic_yu); + else if (sym >= XKB_KEY_Cyrillic_yu && sym <= XKB_KEY_Cyrillic_hardsign) + *upper += (XKB_KEY_Cyrillic_YU - XKB_KEY_Cyrillic_yu); + break; + case 7: /* Greek */ + /* Assume the KeySym is a legal value (ignore discontinuities) */ + if (sym >= XKB_KEY_Greek_ALPHAaccent && sym <= XKB_KEY_Greek_OMEGAaccent) + *lower += (XKB_KEY_Greek_alphaaccent - XKB_KEY_Greek_ALPHAaccent); + else if (sym >= XKB_KEY_Greek_alphaaccent && sym <= XKB_KEY_Greek_omegaaccent && + sym != XKB_KEY_Greek_iotaaccentdieresis && + sym != XKB_KEY_Greek_upsilonaccentdieresis) + *upper -= (XKB_KEY_Greek_alphaaccent - XKB_KEY_Greek_ALPHAaccent); + else if (sym >= XKB_KEY_Greek_ALPHA && sym <= XKB_KEY_Greek_OMEGA) + *lower += (XKB_KEY_Greek_alpha - XKB_KEY_Greek_ALPHA); + else if (sym >= XKB_KEY_Greek_alpha && sym <= XKB_KEY_Greek_omega && + sym != XKB_KEY_Greek_finalsmallsigma) + *upper -= (XKB_KEY_Greek_alpha - XKB_KEY_Greek_ALPHA); + break; + case 0x13: /* Latin 9 */ + if (sym == XKB_KEY_OE) + *lower = XKB_KEY_oe; + else if (sym == XKB_KEY_oe) + *upper = XKB_KEY_OE; + else if (sym == XKB_KEY_Ydiaeresis) + *lower = XKB_KEY_ydiaeresis; + break; + } +} + +QT_END_NAMESPACE diff --git a/src/gui/platform/unix/qxkbcommon_p.h b/src/gui/platform/unix/qxkbcommon_p.h new file mode 100644 index 0000000000..a40d794451 --- /dev/null +++ b/src/gui/platform/unix/qxkbcommon_p.h @@ -0,0 +1,128 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QXKBCOMMON_P_H +#define QXKBCOMMON_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtGui/qtguiglobal.h> +#include <QtCore/qstring.h> +#include <QtCore/qloggingcategory.h> +#include <QtCore/qlist.h> +#include <QtCore/private/qglobal_p.h> + +#include <xkbcommon/xkbcommon.h> + +#include <qpa/qplatformkeymapper.h> + +#include <memory> + +QT_BEGIN_NAMESPACE + +class QEvent; +class QKeyEvent; +class QPlatformInputContext; + +class Q_GUI_EXPORT QXkbCommon +{ +public: + static QString lookupString(struct xkb_state *state, xkb_keycode_t code); + static QString lookupStringNoKeysymTransformations(xkb_keysym_t keysym); + + static QList<xkb_keysym_t> toKeysym(QKeyEvent *event); + + static int keysymToQtKey(xkb_keysym_t keysym, Qt::KeyboardModifiers modifiers); + static int keysymToQtKey(xkb_keysym_t keysym, Qt::KeyboardModifiers modifiers, + xkb_state *state, xkb_keycode_t code, + bool superAsMeta = true, bool hyperAsMeta = true); + + // xkbcommon_* API is part of libxkbcommon internals, with modifications as + // described in the header of the implementation file. + static void xkbcommon_XConvertCase(xkb_keysym_t sym, xkb_keysym_t *lower, xkb_keysym_t *upper); + static xkb_keysym_t qxkbcommon_xkb_keysym_to_upper(xkb_keysym_t ks); + + static Qt::KeyboardModifiers modifiers(struct xkb_state *state, xkb_keysym_t keysym = XKB_KEY_VoidSymbol); + + static QList<int> possibleKeys(xkb_state *state, + const QKeyEvent *event, bool superAsMeta = false, bool hyperAsMeta = false); + static QList<QKeyCombination> possibleKeyCombinations(xkb_state *state, + const QKeyEvent *event, bool superAsMeta = false, bool hyperAsMeta = false); + + static void verifyHasLatinLayout(xkb_keymap *keymap); + static xkb_keysym_t lookupLatinKeysym(xkb_state *state, xkb_keycode_t keycode); + + static bool isLatin1(xkb_keysym_t sym) { + return sym >= 0x20 && sym <= 0xff; + } + static bool isKeypad(xkb_keysym_t sym) { + switch (sym) { + case XKB_KEY_KP_Space: + case XKB_KEY_KP_Tab: + case XKB_KEY_KP_Enter: + case XKB_KEY_KP_F1: + case XKB_KEY_KP_F2: + case XKB_KEY_KP_F3: + case XKB_KEY_KP_F4: + case XKB_KEY_KP_Home: + case XKB_KEY_KP_Left: + case XKB_KEY_KP_Up: + case XKB_KEY_KP_Right: + case XKB_KEY_KP_Down: + case XKB_KEY_KP_Prior: + case XKB_KEY_KP_Next: + case XKB_KEY_KP_End: + case XKB_KEY_KP_Begin: + case XKB_KEY_KP_Insert: + case XKB_KEY_KP_Delete: + case XKB_KEY_KP_Equal: + case XKB_KEY_KP_Multiply: + case XKB_KEY_KP_Add: + case XKB_KEY_KP_Separator: + case XKB_KEY_KP_Subtract: + case XKB_KEY_KP_Decimal: + case XKB_KEY_KP_Divide: + case XKB_KEY_KP_0: + case XKB_KEY_KP_1: + case XKB_KEY_KP_2: + case XKB_KEY_KP_3: + case XKB_KEY_KP_4: + case XKB_KEY_KP_5: + case XKB_KEY_KP_6: + case XKB_KEY_KP_7: + case XKB_KEY_KP_8: + case XKB_KEY_KP_9: + return true; + default: + return false; + } + } + + static void setXkbContext(QPlatformInputContext *inputContext, struct xkb_context *context); + + struct XKBStateDeleter { + void operator()(struct xkb_state *state) const { return xkb_state_unref(state); } + }; + struct XKBKeymapDeleter { + void operator()(struct xkb_keymap *keymap) const { return xkb_keymap_unref(keymap); } + }; + struct XKBContextDeleter { + void operator()(struct xkb_context *context) const { return xkb_context_unref(context); } + }; + using ScopedXKBState = std::unique_ptr<struct xkb_state, XKBStateDeleter>; + using ScopedXKBKeymap = std::unique_ptr<struct xkb_keymap, XKBKeymapDeleter>; + using ScopedXKBContext = std::unique_ptr<struct xkb_context, XKBContextDeleter>; +}; + +QT_END_NAMESPACE + +#endif // QXKBCOMMON_P_H diff --git a/src/gui/platform/wasm/qlocalfileapi.cpp b/src/gui/platform/wasm/qlocalfileapi.cpp new file mode 100644 index 0000000000..76b99361c4 --- /dev/null +++ b/src/gui/platform/wasm/qlocalfileapi.cpp @@ -0,0 +1,211 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qlocalfileapi_p.h" +#include <private/qstdweb_p.h> +#include <QtCore/QRegularExpression> + +QT_BEGIN_NAMESPACE +namespace LocalFileApi { +namespace { +std::string qtFilterListToFileInputAccept(const QStringList &filterList) +{ + QStringList transformed; + for (const auto &filter : filterList) { + const auto type = Type::fromQt(filter); + if (type && type->accept()) { + const auto &extensions = type->accept()->mimeType().extensions(); + std::transform(extensions.begin(), extensions.end(), std::back_inserter(transformed), + [](const Type::Accept::MimeType::Extension &extension) { + return extension.value().toString(); + }); + } + } + return transformed.join(QStringLiteral(",")).toStdString(); +} + +std::optional<emscripten::val> qtFilterListToTypes(const QStringList &filterList) +{ + using namespace qstdweb; + using namespace emscripten; + auto types = emscripten::val::array(); + + for (const auto &fileFilter : filterList) { + auto type = Type::fromQt(fileFilter); + if (type) { + auto jsType = emscripten::val::object(); + jsType.set("description", type->description().toString().toStdString()); + if (type->accept()) { + jsType.set("accept", ([&mimeType = type->accept()->mimeType()]() { + val acceptDict = val::object(); + + QList<emscripten::val> extensions; + extensions.reserve(mimeType.extensions().size()); + std::transform( + mimeType.extensions().begin(), mimeType.extensions().end(), + std::back_inserter(extensions), + [](const Type::Accept::MimeType::Extension &extension) { + return val(extension.value().toString().toStdString()); + }); + acceptDict.set("application/octet-stream", + emscripten::val::array(extensions.begin(), + extensions.end())); + return acceptDict; + })()); + } + types.call<void>("push", std::move(jsType)); + } + } + + return types["length"].as<int>() == 0 ? std::optional<emscripten::val>() : types; +} +} // namespace + +Type::Type(QStringView description, std::optional<Accept> accept) + : m_description(description.trimmed()), m_accept(std::move(accept)) +{ +} + +Type::~Type() = default; + +std::optional<Type> Type::fromQt(QStringView type) +{ + using namespace emscripten; + + // Accepts either a string in format: + // GROUP3 + // or in this format: + // GROUP1 (GROUP2) + // Group 1 is treated as the description, whereas group 2 or 3 are treated as the filter list. + static QRegularExpression regex( + QString(QStringLiteral("(?:(?:([^(]*)\\(([^()]+)\\)[^)]*)|([^()]+))"))); + const auto match = regex.matchView(type); + + if (!match.hasMatch()) + return std::nullopt; + + constexpr size_t DescriptionIndex = 1; + constexpr size_t FilterListFromParensIndex = 2; + constexpr size_t PlainFilterListIndex = 3; + + const auto description = match.hasCaptured(DescriptionIndex) + ? match.capturedView(DescriptionIndex) + : QStringView(); + const auto filterList = match.capturedView(match.hasCaptured(FilterListFromParensIndex) + ? FilterListFromParensIndex + : PlainFilterListIndex); + + auto accept = Type::Accept::fromQt(filterList); + if (!accept) + return std::nullopt; + + return Type(description, std::move(*accept)); +} + +Type::Accept::Accept() = default; + +Type::Accept::~Accept() = default; + +std::optional<Type::Accept> Type::Accept::fromQt(QStringView qtRepresentation) +{ + Accept accept; + + // Used for accepting multiple extension specifications on a filter list. + // The next group of non-empty characters. + static QRegularExpression internalRegex(QString(QStringLiteral("([^\\s]+)\\s*"))); + int offset = 0; + auto internalMatch = internalRegex.matchView(qtRepresentation, offset); + MimeType mimeType; + + while (internalMatch.hasMatch()) { + auto webExtension = MimeType::Extension::fromQt(internalMatch.capturedView(1)); + + if (!webExtension) + return std::nullopt; + + mimeType.addExtension(*webExtension); + + internalMatch = internalRegex.matchView(qtRepresentation, internalMatch.capturedEnd()); + } + + accept.setMimeType(mimeType); + return accept; +} + +void Type::Accept::setMimeType(MimeType mimeType) +{ + m_mimeType = std::move(mimeType); +} + +Type::Accept::MimeType::MimeType() = default; + +Type::Accept::MimeType::~MimeType() = default; + +void Type::Accept::MimeType::addExtension(Extension extension) +{ + m_extensions.push_back(std::move(extension)); +} + +Type::Accept::MimeType::Extension::Extension(QStringView extension) : m_value(extension) { } + +Type::Accept::MimeType::Extension::~Extension() = default; + +std::optional<Type::Accept::MimeType::Extension> +Type::Accept::MimeType::Extension::fromQt(QStringView qtRepresentation) +{ + // Checks for a filter that matches everything: + // Any number of asterisks or any number of asterisks with a '.' between them. + // The web filter does not support wildcards. + static QRegularExpression qtAcceptAllRegex( + QRegularExpression::anchoredPattern(QString(QStringLiteral("[*]+|[*]+\\.[*]+")))); + if (qtAcceptAllRegex.matchView(qtRepresentation).hasMatch()) + return std::nullopt; + + // Checks for correctness. The web filter only allows filename extensions and does not filter + // the actual filenames, therefore we check whether the filter provided only filters for the + // extension. + static QRegularExpression qtFilenameMatcherRegex( + QRegularExpression::anchoredPattern(QString(QStringLiteral("(\\*?)(\\.[^*]+)")))); + + auto extensionMatch = qtFilenameMatcherRegex.matchView(qtRepresentation); + if (extensionMatch.hasMatch()) + return Extension(extensionMatch.capturedView(2)); + + // Mapping impossible. + return std::nullopt; +} + +emscripten::val makeOpenFileOptions(const QStringList &filterList, bool acceptMultiple) +{ + auto options = emscripten::val::object(); + if (auto typeList = qtFilterListToTypes(filterList); typeList) { + options.set("types", std::move(*typeList)); + options.set("excludeAcceptAllOption", true); + } + + options.set("multiple", acceptMultiple); + + return options; +} + +emscripten::val makeSaveFileOptions(const QStringList &filterList, const std::string& suggestedName) +{ + auto options = emscripten::val::object(); + + if (!suggestedName.empty()) + options.set("suggestedName", emscripten::val(suggestedName)); + + if (auto typeList = qtFilterListToTypes(filterList)) + options.set("types", emscripten::val(std::move(*typeList))); + + return options; +} + +std::string makeFileInputAccept(const QStringList &filterList) +{ + return qtFilterListToFileInputAccept(filterList); +} + +} // namespace LocalFileApi + +QT_END_NAMESPACE diff --git a/src/gui/platform/wasm/qlocalfileapi_p.h b/src/gui/platform/wasm/qlocalfileapi_p.h new file mode 100644 index 0000000000..1398d674d8 --- /dev/null +++ b/src/gui/platform/wasm/qlocalfileapi_p.h @@ -0,0 +1,94 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QLOCALFILEAPI_P_H +#define QLOCALFILEAPI_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <private/qglobal_p.h> +#include <qstringview.h> +#include <emscripten/val.h> +#include <cstdint> +#include <functional> + +QT_BEGIN_NAMESPACE + +namespace LocalFileApi { +class Q_AUTOTEST_EXPORT Type +{ +public: + class Accept { + public: + class MimeType { + public: + class Extension { + public: + static std::optional<Extension> fromQt(QStringView extension); + + ~Extension(); + + const QStringView &value() const { return m_value; } + + private: + explicit Extension(QStringView extension); + + QStringView m_value; + }; + + MimeType(); + ~MimeType(); + + void addExtension(Extension type); + + const std::vector<Extension> &extensions() const { return m_extensions; } + + private: + std::vector<Extension> m_extensions; + }; + + static std::optional<Accept> fromQt(QStringView type); + + ~Accept(); + + void setMimeType(MimeType mimeType); + + const MimeType &mimeType() const { return m_mimeType; } + + private: + Accept(); + MimeType m_mimeType; + }; + + Type(QStringView description, std::optional<Accept> accept); + ~Type(); + + static std::optional<Type> fromQt(QStringView type); + const QStringView &description() const { return m_description; } + const std::optional<Accept> &accept() const { return m_accept; } + +private: + QStringView m_description; + std::optional<Accept> m_accept; +}; + +Q_AUTOTEST_EXPORT emscripten::val makeOpenFileOptions(const QStringList &filterList, + bool acceptMultiple); +Q_AUTOTEST_EXPORT emscripten::val makeSaveFileOptions(const QStringList &filterList, + const std::string &suggestedName); + +Q_AUTOTEST_EXPORT std::string makeFileInputAccept(const QStringList &filterList); + +} // namespace LocalFileApi +QT_END_NAMESPACE + +#endif // QLOCALFILEAPI_P_H diff --git a/src/gui/platform/wasm/qwasmlocalfileaccess.cpp b/src/gui/platform/wasm/qwasmlocalfileaccess.cpp index 85c894a74a..a946cda043 100644 --- a/src/gui/platform/wasm/qwasmlocalfileaccess.cpp +++ b/src/gui/platform/wasm/qwasmlocalfileaccess.cpp @@ -1,94 +1,111 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtGui 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$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qwasmlocalfileaccess_p.h" +#include "qlocalfileapi_p.h" #include <private/qstdweb_p.h> #include <emscripten.h> #include <emscripten/bind.h> #include <emscripten/html5.h> #include <emscripten/val.h> +#include <QtCore/qregularexpression.h> + QT_BEGIN_NAMESPACE namespace QWasmLocalFileAccess { +namespace FileDialog { +namespace { +bool hasLocalFilesApi() +{ + return !qstdweb::window()["showOpenFilePicker"].isUndefined(); +} -void streamFile(const qstdweb::File &file, uint32_t offset, uint32_t length, char *buffer, const std::function<void ()> &completed) +void showOpenViaHTMLPolyfill(const QStringList &accept, FileSelectMode fileSelectMode, + qstdweb::PromiseCallbacks onFilesSelected) { - // Read file in chunks in order to avoid holding two copies in memory at the same time - const uint32_t chunkSize = 256 * 1024; - const uint32_t end = offset + length; - // assert end < file.size - auto fileReader = std::make_shared<qstdweb::FileReader>(); - - auto chunkCompleted = std::make_shared<std::function<void (uint32_t, char *buffer)>>(); - *chunkCompleted = [=](uint32_t chunkBegin, char *chunkBuffer) mutable { - - // Copy current chunk from JS memory to Wasm memory - qstdweb::ArrayBuffer result = fileReader->result(); - qstdweb::Uint8Array(result).copyTo(chunkBuffer); - - // Read next chunk if not at buffer end - uint32_t nextChunkBegin = std::min(chunkBegin + result.byteLength(), end); - uint32_t nextChunkEnd = std::min(nextChunkBegin + chunkSize, end); - if (nextChunkBegin == end) { - completed(); - chunkCompleted.reset(); - return; - } - char *nextChunkBuffer = chunkBuffer + result.byteLength(); - fileReader->onLoad([=]() { (*chunkCompleted)(nextChunkBegin, nextChunkBuffer); }); - qstdweb::Blob blob = file.slice(nextChunkBegin, nextChunkEnd); - fileReader->readAsArrayBuffer(blob); - }; + // Create file input html element which will display a native file dialog + // and call back to our onchange handler once the user has selected + // one or more files. + emscripten::val document = emscripten::val::global("document"); + emscripten::val input = document.call<emscripten::val>("createElement", std::string("input")); + input.set("type", "file"); + input.set("style", "display:none"); + input.set("accept", LocalFileApi::makeFileInputAccept(accept)); + Q_UNUSED(accept); + input.set("multiple", emscripten::val(fileSelectMode == FileSelectMode::MultipleFiles)); + + // Note: there is no event in case the user cancels the file dialog. + static std::unique_ptr<qstdweb::EventCallback> changeEvent; + auto callback = [=](emscripten::val) { onFilesSelected.thenFunc(input["files"]); }; + changeEvent = std::make_unique<qstdweb::EventCallback>(input, "change", callback); + + // Activate file input + emscripten::val body = document["body"]; + body.call<void>("appendChild", input); + input.call<void>("click"); + body.call<void>("removeChild", input); +} + +void showOpenViaLocalFileApi(const QStringList &accept, FileSelectMode fileSelectMode, + qstdweb::PromiseCallbacks callbacks) +{ + using namespace qstdweb; + + auto options = LocalFileApi::makeOpenFileOptions(accept, fileSelectMode == FileSelectMode::MultipleFiles); + + Promise::make( + window(), QStringLiteral("showOpenFilePicker"), + { + .thenFunc = [=](emscripten::val fileHandles) mutable { + std::vector<emscripten::val> filePromises; + filePromises.reserve(fileHandles["length"].as<int>()); + for (int i = 0; i < fileHandles["length"].as<int>(); ++i) + filePromises.push_back(fileHandles[i].call<emscripten::val>("getFile")); + Promise::all(std::move(filePromises), callbacks); + }, + .catchFunc = callbacks.catchFunc, + .finallyFunc = callbacks.finallyFunc, + }, std::move(options)); +} + +void showSaveViaLocalFileApi(const std::string &fileNameHint, qstdweb::PromiseCallbacks callbacks) +{ + using namespace qstdweb; + using namespace emscripten; + + auto options = LocalFileApi::makeSaveFileOptions(QStringList(), fileNameHint); + + Promise::make( + window(), QStringLiteral("showSaveFilePicker"), + std::move(callbacks), std::move(options)); +} +} // namespace - // Read first chunk. First iteration is a dummy iteration with no available data. - (*chunkCompleted)(offset, buffer); +void showOpen(const QStringList &accept, FileSelectMode fileSelectMode, + qstdweb::PromiseCallbacks callbacks) +{ + hasLocalFilesApi() ? + showOpenViaLocalFileApi(accept, fileSelectMode, std::move(callbacks)) : + showOpenViaHTMLPolyfill(accept, fileSelectMode, std::move(callbacks)); +} + +bool canShowSave() +{ + return hasLocalFilesApi(); } -void streamFile(const qstdweb::File &file, char *buffer, const std::function<void ()> &completed) +void showSave(const std::string &fileNameHint, qstdweb::PromiseCallbacks callbacks) { - streamFile(file, 0, file.size(), buffer, completed); + Q_ASSERT(canShowSave()); + showSaveViaLocalFileApi(fileNameHint, std::move(callbacks)); } +} // namespace FileDialog +namespace { void readFiles(const qstdweb::FileList &fileList, - const std::function<char *(uint64_t size, const std::string name)> &acceptFile, - const std::function<void ()> &fileDataReady) + const std::function<char *(uint64_t size, const std::string name)> &acceptFile, + const std::function<void ()> &fileDataReady) { auto readFile = std::make_shared<std::function<void(int)>>(); @@ -99,7 +116,7 @@ void readFiles(const qstdweb::FileList &fileList, return; } - const qstdweb::File file = fileList[fileIndex]; + const qstdweb::File file = qstdweb::File(fileList[fileIndex]); // Ask caller if the file should be accepted char *buffer = acceptFile(file.size(), file.name()); @@ -109,7 +126,7 @@ void readFiles(const qstdweb::FileList &fileList, } // Read file data into caller-provided buffer - streamFile(file, buffer, [=]() { + file.stream(buffer, [readFile = readFile.get(), fileIndex, fileDataReady]() { fileDataReady(); (*readFile)(fileIndex + 1); }); @@ -118,87 +135,153 @@ void readFiles(const qstdweb::FileList &fileList, (*readFile)(0); } -typedef std::function<void (const qstdweb::FileList &fileList)> OpenFileDialogCallback; -void openFileDialog(const std::string &accept, FileSelectMode fileSelectMode, - const OpenFileDialogCallback &filesSelected) +QStringList makeFilterList(const std::string &qtAcceptList) { - // Create file input html element which will display a native file dialog - // and call back to our onchange handler once the user has selected - // one or more files. - emscripten::val document = emscripten::val::global("document"); - emscripten::val input = document.call<emscripten::val>("createElement", std::string("input")); - input.set("type", "file"); - input.set("style", "display:none"); - input.set("accept", emscripten::val(accept)); - input.set("multiple", emscripten::val(fileSelectMode == MultipleFiles)); + // copy of qt_make_filter_list() from qfiledialog.cpp + auto filter = QString::fromStdString(qtAcceptList); + if (filter.isEmpty()) + return QStringList(); + QString sep(";;"); + if (!filter.contains(sep) && filter.contains(u'\n')) + sep = u'\n'; - // Note: there is no event in case the user cancels the file dialog. - static std::unique_ptr<qstdweb::EventCallback> changeEvent; - auto callback = [=]() { filesSelected(qstdweb::FileList(input["files"])); }; - changeEvent.reset(new qstdweb::EventCallback(input, "change", callback)); + return filter.split(sep); +} +} + +void downloadDataAsFile(const char *content, size_t size, const std::string &fileNameHint) +{ + // Save a file by creating programmatically clicking a download + // link to an object url to a Blob containing a copy of the file + // content. The copy is made so that the passed in content buffer + // can be released as soon as this function returns. + qstdweb::Blob contentBlob = qstdweb::Blob::copyFrom(content, size); + emscripten::val document = emscripten::val::global("document"); + emscripten::val window = qstdweb::window(); + emscripten::val contentUrl = window["URL"].call<emscripten::val>("createObjectURL", contentBlob.val()); + emscripten::val contentLink = document.call<emscripten::val>("createElement", std::string("a")); + contentLink.set("href", contentUrl); + contentLink.set("download", fileNameHint); + contentLink.set("style", "display:none"); - // Activate file input emscripten::val body = document["body"]; - body.call<void>("appendChild", input); - input.call<void>("click"); - body.call<void>("removeChild", input); + body.call<void>("appendChild", contentLink); + contentLink.call<void>("click"); + body.call<void>("removeChild", contentLink); + + window["URL"].call<emscripten::val>("revokeObjectURL", contentUrl); } void openFiles(const std::string &accept, FileSelectMode fileSelectMode, const std::function<void (int fileCount)> &fileDialogClosed, - const std::function<char *(uint64_t size, const std::string name)> &acceptFile, + const std::function<char *(uint64_t size, const std::string& name)> &acceptFile, const std::function<void()> &fileDataReady) { - openFileDialog(accept, fileSelectMode, [=](const qstdweb::FileList &files) { - fileDialogClosed(files.length()); - readFiles(files, acceptFile, fileDataReady); + FileDialog::showOpen(makeFilterList(accept), fileSelectMode, { + .thenFunc = [=](emscripten::val result) { + auto files = qstdweb::FileList(result); + fileDialogClosed(files.length()); + readFiles(files, acceptFile, fileDataReady); + }, + .catchFunc = [=](emscripten::val) { + fileDialogClosed(0); + } }); } void openFile(const std::string &accept, const std::function<void (bool fileSelected)> &fileDialogClosed, - const std::function<char *(uint64_t size, const std::string name)> &acceptFile, + const std::function<char *(uint64_t size, const std::string& name)> &acceptFile, const std::function<void()> &fileDataReady) { auto fileDialogClosedWithInt = [=](int fileCount) { fileDialogClosed(fileCount != 0); }; openFiles(accept, FileSelectMode::SingleFile, fileDialogClosedWithInt, acceptFile, fileDataReady); } -void saveFile(const char *content, size_t size, const std::string &fileNameHint) +void saveDataToFileInChunks(emscripten::val fileHandle, const QByteArray &data) { - // Save a file by creating programatically clicking a download - // link to an object url to a Blob containing the file content. - // File content is copied once, so that the passed in content - // buffer can be released as soon as this function returns - we - // don't know for how long the browser will retain the TypedArray - // view used to create the Blob. + using namespace emscripten; + using namespace qstdweb; - emscripten::val document = emscripten::val::global("document"); - emscripten::val window = emscripten::val::global("window"); + Promise::make(fileHandle, QStringLiteral("createWritable"), { + .thenFunc = [=](val writable) { + struct State { + size_t written; + std::function<void(val result)> continuation; + }; - emscripten::val fileContentView = emscripten::val(emscripten::typed_memory_view(size, content)); - emscripten::val fileContentCopy = emscripten::val::global("ArrayBuffer").new_(size); - emscripten::val fileContentCopyView = emscripten::val::global("Uint8Array").new_(fileContentCopy); - fileContentCopyView.call<void>("set", fileContentView); + static constexpr size_t desiredChunkSize = 1024u; +#if defined(__EMSCRIPTEN_SHARED_MEMORY__) + qstdweb::Uint8Array chunkArray(desiredChunkSize); +#endif - emscripten::val contentArray = emscripten::val::array(); - contentArray.call<void>("push", fileContentCopyView); - emscripten::val type = emscripten::val::object(); - type.set("type","application/octet-stream"); - emscripten::val contentBlob = emscripten::val::global("Blob").new_(contentArray, type); + auto state = std::make_shared<State>(); + state->written = 0u; + state->continuation = [=](val) mutable { + const size_t remaining = data.size() - state->written; + if (remaining == 0) { + Promise::make(writable, QStringLiteral("close"), { .thenFunc = [=](val) {} }); + state.reset(); + return; + } - emscripten::val contentUrl = window["URL"].call<emscripten::val>("createObjectURL", contentBlob); - emscripten::val contentLink = document.call<emscripten::val>("createElement", std::string("a")); - contentLink.set("href", contentUrl); - contentLink.set("download", fileNameHint); - contentLink.set("style", "display:none"); + const auto currentChunkSize = std::min(remaining, desiredChunkSize); - emscripten::val body = document["body"]; - body.call<void>("appendChild", contentLink); - contentLink.call<void>("click"); - body.call<void>("removeChild", contentLink); +#if defined(__EMSCRIPTEN_SHARED_MEMORY__) + // If shared memory is used, WebAssembly.Memory is instantiated with the 'shared' + // option on. Passing a typed_memory_view to SharedArrayBuffer to + // FileSystemWritableFileStream.write is disallowed by security policies, so we + // need to make a copy of the data to a chunk array buffer. + Promise::make( + writable, QStringLiteral("write"), + { + .thenFunc = state->continuation, + }, + chunkArray.copyFrom(data.constData() + state->written, currentChunkSize) + .val() + .call<emscripten::val>("subarray", emscripten::val(0), + emscripten::val(currentChunkSize))); +#else + Promise::make(writable, QStringLiteral("write"), + { + .thenFunc = state->continuation, + }, + val(typed_memory_view(currentChunkSize, data.constData() + state->written))); +#endif + state->written += currentChunkSize; + }; - window["URL"].call<emscripten::val>("revokeObjectURL", contentUrl); + state->continuation(val::undefined()); + }, + }); +} + +void saveFile(const QByteArray &data, const std::string &fileNameHint) +{ + if (!FileDialog::canShowSave()) { + downloadDataAsFile(data.constData(), data.size(), fileNameHint); + return; + } + + FileDialog::showSave(fileNameHint, { + .thenFunc = [=](emscripten::val result) { + saveDataToFileInChunks(result, data); + }, + }); +} + +void saveFile(const char *content, size_t size, const std::string &fileNameHint) +{ + if (!FileDialog::canShowSave()) { + downloadDataAsFile(content, size, fileNameHint); + return; + } + + FileDialog::showSave(fileNameHint, { + .thenFunc = [=](emscripten::val result) { + saveDataToFileInChunks(result, QByteArray(content, size)); + }, + }); } } // namespace QWasmLocalFileAccess diff --git a/src/gui/platform/wasm/qwasmlocalfileaccess_p.h b/src/gui/platform/wasm/qwasmlocalfileaccess_p.h index ccd88570c8..77b14577f7 100644 --- a/src/gui/platform/wasm/qwasmlocalfileaccess_p.h +++ b/src/gui/platform/wasm/qwasmlocalfileaccess_p.h @@ -1,41 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtGui 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$ -** -****************************************************************************/ +// Copyright (C) 2019 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #ifndef QWASMLOCALFILEACCESS_P_H #define QWASMLOCALFILEACCESS_P_H @@ -51,7 +15,7 @@ // We mean it. // -#include <qglobal.h> +#include <private/qglobal_p.h> #include <cstdint> #include <functional> @@ -59,19 +23,20 @@ QT_BEGIN_NAMESPACE namespace QWasmLocalFileAccess { -enum FileSelectMode { SingleFile, MultipleFiles }; +enum class FileSelectMode { SingleFile, MultipleFiles }; -void openFiles(const std::string &accept, FileSelectMode fileSelectMode, +Q_CORE_EXPORT void openFiles(const std::string &accept, FileSelectMode fileSelectMode, const std::function<void (int fileCount)> &fileDialogClosed, - const std::function<char *(uint64_t size, const std::string name)> &acceptFile, + const std::function<char *(uint64_t size, const std::string& name)> &acceptFile, const std::function<void()> &fileDataReady); -void openFile(const std::string &accept, +Q_CORE_EXPORT void openFile(const std::string &accept, const std::function<void (bool fileSelected)> &fileDialogClosed, - const std::function<char *(uint64_t size, const std::string name)> &acceptFile, + const std::function<char *(uint64_t size, const std::string& name)> &acceptFile, const std::function<void()> &fileDataReady); -void saveFile(const char *content, size_t size, const std::string &fileNameHint); +Q_CORE_EXPORT void saveFile(const QByteArray &data, const std::string &fileNameHint); +Q_CORE_EXPORT void saveFile(const char *content, size_t size, const std::string &fileNameHint); } // namespace QWasmLocalFileAccess diff --git a/src/gui/platform/wasm/qwasmnativeinterface.cpp b/src/gui/platform/wasm/qwasmnativeinterface.cpp new file mode 100644 index 0000000000..7313629a8d --- /dev/null +++ b/src/gui/platform/wasm/qwasmnativeinterface.cpp @@ -0,0 +1,17 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + + + +#include <QtGui/private/qguiapplication_p.h> +#include <qpa/qplatformintegration.h> +#include <qpa/qplatformwindow_p.h> + + +QT_BEGIN_NAMESPACE + +using namespace QNativeInterface::Private; + +QT_DEFINE_PRIVATE_NATIVE_INTERFACE(QWasmWindow); + +QT_END_NAMESPACE diff --git a/src/gui/platform/wasm/wasm.pri b/src/gui/platform/wasm/wasm.pri deleted file mode 100644 index 1b5d7eadb5..0000000000 --- a/src/gui/platform/wasm/wasm.pri +++ /dev/null @@ -1,3 +0,0 @@ -INCLUDEDIR += $$PWD -HEADERS += $$PWD/qwasmlocalfileaccess_p.h -SOURCES += $$PWD/qwasmlocalfileaccess.cpp diff --git a/src/gui/platform/windows/qwindowsguieventdispatcher.cpp b/src/gui/platform/windows/qwindowsguieventdispatcher.cpp new file mode 100644 index 0000000000..c2f0efe96e --- /dev/null +++ b/src/gui/platform/windows/qwindowsguieventdispatcher.cpp @@ -0,0 +1,201 @@ +// Copyright (C) 2013 Samuel Gaist <samuel.gaist@edeltech.ch> +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qwindowsguieventdispatcher_p.h" + +#include <qpa/qwindowsysteminterface.h> + +#include <QtCore/QCoreApplication> +#include <QtCore/QDebug> + +QT_BEGIN_NAMESPACE + +/*! + \class QWindowsGuiEventDispatcher + \brief Event dispatcher for Windows + + Maintains a global stack storing the current event dispatcher and + its processing flags for access from the Windows procedure + qWindowsWndProc. Handling the QPA gui events should be done + from within the qWindowsWndProc to ensure correct processing of messages. + + \internal +*/ + +QWindowsGuiEventDispatcher::QWindowsGuiEventDispatcher(QObject *parent) : + QEventDispatcherWin32(parent) +{ + setObjectName(QStringLiteral("QWindowsGuiEventDispatcher")); +} + +bool QWindowsGuiEventDispatcher::processEvents(QEventLoop::ProcessEventsFlags flags) +{ + const QEventLoop::ProcessEventsFlags oldFlags = m_flags; + m_flags = flags; + const bool rc = QEventDispatcherWin32::processEvents(flags); + m_flags = oldFlags; + return rc; +} + +void QWindowsGuiEventDispatcher::sendPostedEvents() +{ + QEventDispatcherWin32::sendPostedEvents(); + QWindowSystemInterface::sendWindowSystemEvents(m_flags); +} + +// Helpers for printing debug output for WM_* messages. +struct MessageDebugEntry +{ + UINT message; + const char *description; + bool interesting; +}; + +static const MessageDebugEntry +messageDebugEntries[] = { + {WM_CREATE, "WM_CREATE", true}, + {WM_PAINT, "WM_PAINT", true}, + {WM_CLOSE, "WM_CLOSE", true}, + {WM_DESTROY, "WM_DESTROY", true}, + {WM_MOVE, "WM_MOVE", true}, + {WM_SIZE, "WM_SIZE", true}, + {WM_GETICON, "WM_GETICON", false}, + {WM_KEYDOWN, "WM_KEYDOWN", true}, + {WM_SYSKEYDOWN, "WM_SYSKEYDOWN", true}, + {WM_SYSCOMMAND, "WM_SYSCOMMAND", true}, + {WM_KEYUP, "WM_KEYUP", true}, + {WM_SYSKEYUP, "WM_SYSKEYUP", true}, +#if defined(WM_APPCOMMAND) + {WM_APPCOMMAND, "WM_APPCOMMAND", true}, +#endif + {WM_IME_CHAR, "WM_IMECHAR", true}, + {WM_IME_KEYDOWN, "WM_IMECHAR", true}, + {WM_CANCELMODE, "WM_CANCELMODE", true}, + {WM_CHAR, "WM_CHAR", true}, + {WM_DEADCHAR, "WM_DEADCHAR", true}, + {WM_ACTIVATE, "WM_ACTIVATE", true}, + {WM_SETFOCUS, "WM_SETFOCUS", true}, + {WM_KILLFOCUS, "WM_KILLFOCUS", true}, + {WM_ENABLE, "WM_ENABLE", true}, + {WM_SHOWWINDOW, "WM_SHOWWINDOW", true}, + {WM_WINDOWPOSCHANGED, "WM_WINDOWPOSCHANGED", true}, + {WM_SETCURSOR, "WM_SETCURSOR", false}, + {WM_GETFONT, "WM_GETFONT", true}, + {WM_LBUTTONDOWN, "WM_LBUTTONDOWN", true}, + {WM_LBUTTONUP, "WM_LBUTTONUP", true}, + {WM_LBUTTONDBLCLK, "WM_LBUTTONDBLCLK", true}, + {WM_RBUTTONDOWN, "WM_RBUTTONDOWN", true}, + {WM_RBUTTONUP, "WM_RBUTTONUP", true}, + {WM_RBUTTONDBLCLK, "WM_RBUTTONDBLCLK", true}, + {WM_MBUTTONDOWN, "WM_MBUTTONDOWN", true}, + {WM_MBUTTONUP, "WM_MBUTTONUP", true}, + {WM_MBUTTONDBLCLK, "WM_MBUTTONDBLCLK", true}, + {WM_MOUSEWHEEL, "WM_MOUSEWHEEL", true}, + {WM_XBUTTONDOWN, "WM_XBUTTONDOWN", true}, + {WM_XBUTTONUP, "WM_XBUTTONUP", true}, + {WM_XBUTTONDBLCLK, "WM_XBUTTONDBLCLK", true}, + {WM_MOUSEHWHEEL, "WM_MOUSEHWHEEL", true}, + {WM_GETOBJECT, "WM_GETOBJECT", true}, + {WM_IME_SETCONTEXT, "WM_IME_SETCONTEXT", true}, + {WM_INPUTLANGCHANGE, "WM_INPUTLANGCHANGE", true}, + {WM_IME_NOTIFY, "WM_IME_NOTIFY", true}, +#if defined(WM_DWMNCRENDERINGCHANGED) + {WM_DWMNCRENDERINGCHANGED, "WM_DWMNCRENDERINGCHANGED", true}, +#endif + {WM_IME_SETCONTEXT, "WM_IME_SETCONTEXT", true}, + {WM_IME_NOTIFY, "WM_IME_NOTIFY", true}, + {WM_RENDERFORMAT, "WM_RENDERFORMAT", true}, + {WM_RENDERALLFORMATS, "WM_RENDERALLFORMATS", true}, + {WM_DESTROYCLIPBOARD, "WM_DESTROYCLIPBOARD", true}, + {WM_CAPTURECHANGED, "WM_CAPTURECHANGED", true}, + {WM_IME_STARTCOMPOSITION, "WM_IME_STARTCOMPOSITION", true}, + {WM_IME_COMPOSITION, "WM_IME_COMPOSITION", true}, + {WM_IME_ENDCOMPOSITION, "WM_IME_ENDCOMPOSITION", true}, + {WM_IME_NOTIFY, "WM_IME_NOTIFY", true}, + {WM_IME_REQUEST, "WM_IME_REQUEST", true}, +#if !defined(QT_NO_SESSIONMANAGER) + {WM_QUERYENDSESSION, "WM_QUERYENDSESSION", true}, + {WM_ENDSESSION, "WM_ENDSESSION", true}, +#endif + {WM_MOUSEACTIVATE,"WM_MOUSEACTIVATE", true}, + {WM_CHILDACTIVATE, "WM_CHILDACTIVATE", true}, + {WM_PARENTNOTIFY, "WM_PARENTNOTIFY", true}, + {WM_ENTERIDLE, "WM_ENTERIDLE", false}, + {WM_GETMINMAXINFO, "WM_GETMINMAXINFO", true}, + {WM_WINDOWPOSCHANGING, "WM_WINDOWPOSCHANGING", true}, + {WM_NCCREATE, "WM_NCCREATE", true}, + {WM_NCDESTROY, "WM_NCDESTROY", true}, + {WM_NCCALCSIZE, "WM_NCCALCSIZE", true}, + {WM_NCACTIVATE, "WM_NCACTIVATE", true}, + {WM_NCMOUSEMOVE, "WM_NCMOUSEMOVE", true}, + {WM_NCMOUSELEAVE, "WM_NCMOUSELEAVE", true}, + {WM_NCLBUTTONDOWN, "WM_NCLBUTTONDOWN", true}, + {WM_NCLBUTTONUP, "WM_NCLBUTTONUP", true}, + {WM_ACTIVATEAPP, "WM_ACTIVATEAPP", true}, + {WM_NCPAINT, "WM_NCPAINT", true}, + {WM_ERASEBKGND, "WM_ERASEBKGND", true}, + {WM_MOUSEMOVE, "WM_MOUSEMOVE", true}, + {WM_MOUSELEAVE, "WM_MOUSELEAVE", true}, + {WM_NCHITTEST, "WM_NCHITTEST", false}, +#ifdef WM_TOUCH + {WM_TOUCH, "WM_TOUCH", true}, +#endif + {WM_CHANGECBCHAIN, "WM_CHANGECBCHAIN", true}, + {WM_DISPLAYCHANGE, "WM_DISPLAYCHANGE", true}, + {WM_DRAWCLIPBOARD, "WM_DRAWCLIPBOARD", true}, + {WM_THEMECHANGED, "WM_THEMECHANGED", true}, + {0x90, "WM_UAHDESTROYWINDOW", true}, + {0x272, "WM_UNREGISTER_WINDOW_SERVICES", true}, +#ifdef WM_POINTERUPDATE + {WM_POINTERDEVICECHANGE, "WM_POINTERDEVICECHANGE", true}, + {WM_POINTERDEVICEINRANGE, "WM_POINTERDEVICEINRANGE", true}, + {WM_POINTERDEVICEOUTOFRANGE, "WM_POINTERDEVICEOUTOFRANGE", true}, + {WM_NCPOINTERUPDATE, "WM_NCPOINTERUPDATE", true}, + {WM_NCPOINTERDOWN, "WM_NCPOINTERDOWN", true}, + {WM_NCPOINTERUP, "WM_NCPOINTERUP", true}, + {WM_POINTERUPDATE, "WM_POINTERUPDATE", true}, + {WM_POINTERDOWN, "WM_POINTERDOWN", true}, + {WM_POINTERUP, "WM_POINTERUP", true}, + {WM_POINTERENTER, "WM_POINTERENTER", true}, + {WM_POINTERLEAVE, "WM_POINTERLEAVE", true}, + {WM_POINTERACTIVATE, "WM_POINTERACTIVATE", true}, + {WM_POINTERCAPTURECHANGED, "WM_POINTERCAPTURECHANGED", true}, + {WM_TOUCHHITTESTING, "WM_TOUCHHITTESTING", true}, + {WM_POINTERWHEEL, "WM_POINTERWHEEL", true}, + {WM_POINTERHWHEEL, "WM_POINTERHWHEEL", true}, +#endif // WM_POINTERUPDATE +#ifdef DM_POINTERHITTEST + {DM_POINTERHITTEST, "DM_POINTERHITTEST", true}, +#endif // DM_POINTERHITTEST +#ifdef WM_POINTERROUTEDTO + {WM_POINTERROUTEDTO, "WM_POINTERROUTEDTO", true}, + {WM_POINTERROUTEDAWAY, "WM_POINTERROUTEDAWAY", true}, + {WM_POINTERROUTEDRELEASED, "WM_POINTERROUTEDRELEASED", true}, +#endif // WM_POINTERROUTEDTO +#ifdef WM_GETDPISCALEDSIZE + {WM_GETDPISCALEDSIZE, "WM_GETDPISCALEDSIZE", true}, +#endif +#ifdef WM_DPICHANGED + {WM_DPICHANGED, "WM_DPICHANGED", true}, +#endif +}; + +static inline const MessageDebugEntry *messageDebugEntry(UINT msg) +{ + for (size_t i = 0; i < sizeof(messageDebugEntries)/sizeof(MessageDebugEntry); i++) + if (messageDebugEntries[i].message == msg) + return messageDebugEntries + i; + return 0; +} + +const char *QWindowsGuiEventDispatcher::windowsMessageName(UINT msg) +{ + if (const MessageDebugEntry *e = messageDebugEntry(msg)) + return e->description; + return "Unknown"; +} + +QT_END_NAMESPACE + +#include "moc_qwindowsguieventdispatcher_p.cpp" diff --git a/src/gui/platform/windows/qwindowsguieventdispatcher_p.h b/src/gui/platform/windows/qwindowsguieventdispatcher_p.h new file mode 100644 index 0000000000..7d326c0780 --- /dev/null +++ b/src/gui/platform/windows/qwindowsguieventdispatcher_p.h @@ -0,0 +1,40 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QWINDOWSGUIEVENTDISPATCHER_H +#define QWINDOWSGUIEVENTDISPATCHER_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/private/qeventdispatcher_win_p.h> +#include <QtGui/qtguiglobal.h> + +QT_BEGIN_NAMESPACE + +class Q_GUI_EXPORT QWindowsGuiEventDispatcher : public QEventDispatcherWin32 +{ + Q_OBJECT +public: + explicit QWindowsGuiEventDispatcher(QObject *parent = nullptr); + + static const char *windowsMessageName(UINT msg); + + bool QT_ENSURE_STACK_ALIGNED_FOR_SSE processEvents(QEventLoop::ProcessEventsFlags flags) override; + void sendPostedEvents() override; + +private: + QEventLoop::ProcessEventsFlags m_flags; +}; + +QT_END_NAMESPACE + +#endif // QWINDOWSGUIEVENTDISPATCHER_H diff --git a/src/gui/platform/windows/qwindowsmimeconverter.cpp b/src/gui/platform/windows/qwindowsmimeconverter.cpp new file mode 100644 index 0000000000..49d524cb99 --- /dev/null +++ b/src/gui/platform/windows/qwindowsmimeconverter.cpp @@ -0,0 +1,170 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qwindowsmimeconverter.h" + +#include <QtCore/qt_windows.h> + +#include <QtGui/private/qguiapplication_p.h> +#include <QtGui/qpa/qplatformintegration.h> + +QT_BEGIN_NAMESPACE + +/*! + \class QWindowsMimeConverter + \brief The QWindowsMimeConverter class maps open-standard MIME to Window Clipboard formats. + \inmodule QtGui + + Qt's drag-and-drop and clipboard facilities use the MIME standard. + On X11, this maps trivially to the Xdnd protocol, but on Windows + although some applications use MIME types to describe clipboard + formats, others use arbitrary non-standardized naming conventions, + or unnamed built-in formats of Windows. + + By instantiating subclasses of QWindowsMimeConverter that provide + conversions between Windows Clipboard and MIME formats, you can convert + proprietary clipboard formats to MIME formats. + + Construct an instance of your converter implementation after instantiating + QGuiApplication: + + \code + int main(int argc, char **argv) + { + QGuiApplication app(argc, argv); + JsonMimeConverter jsonConverter; + } + \endcode + + Destroying the instance will unregister the converter and remove support + for the conversion. It is also valid to heap-allocate the converter + instance; Qt takes ownership and will delete the converter object during + QGuiApplication shut-down. + + Qt has predefined support for the following Windows Clipboard formats: + + \table + \header \li Windows Format \li Equivalent MIME type + \row \li \c CF_UNICODETEXT \li \c text/plain + \row \li \c CF_TEXT \li \c text/plain + \row \li \c CF_DIB \li \c{image/xyz}, where \c xyz is + a \l{QImageWriter::supportedImageFormats()}{Qt image format} + \row \li \c CF_HDROP \li \c text/uri-list + \row \li \c CF_INETURL \li \c text/uri-list + \row \li \c CF_HTML \li \c text/html + \endtable + + An example use of this class would be to map the Windows Metafile + clipboard format (\c CF_METAFILEPICT) to and from the MIME type + \c{image/x-wmf}. This conversion might simply be adding or removing + a header, or even just passing on the data. See \l{Drag and Drop} + for more information on choosing and definition MIME types. + + You can check if a MIME type is convertible using canConvertFromMime() and + can perform conversions with convertToMime() and convertFromMime(). +*/ + + +/*! + \fn bool QWindowsMimeConverter::canConvertFromMime(const FORMATETC &formatetc, const QMimeData *mimeData) const + + Returns \c true if the converter can convert from the \a mimeData to + the format specified in \a formatetc. + + All subclasses must reimplement this pure virtual function. +*/ + +/*! + \fn bool QWindowsMimeConverter::canConvertToMime(const QString &mimeType, IDataObject *pDataObj) const + + Returns \c true if the converter can convert to the \a mimeType from + the available formats in \a pDataObj. + + All subclasses must reimplement this pure virtual function. +*/ + +/*! + \fn QString QWindowsMimeConverter::mimeForFormat(const FORMATETC &formatetc) const + + Returns the mime type that will be created form the format specified + in \a formatetc, or an empty string if this converter does not support + \a formatetc. + + All subclasses must reimplement this pure virtual function. +*/ + +/*! + \fn QList<FORMATETC> QWindowsMimeConverter::formatsForMime(const QString &mimeType, const QMimeData *mimeData) const + + Returns a QList of FORMATETC structures representing the different windows clipboard + formats that can be provided for the \a mimeType from the \a mimeData. + + All subclasses must reimplement this pure virtual function. +*/ + +/*! + \fn QVariant QWindowsMimeConverter::convertToMime(const QString &mimeType, IDataObject *pDataObj, + QMetaType preferredType) const + + Returns a QVariant containing the converted data for \a mimeType from \a pDataObj. + If possible the QVariant should be of the \a preferredType to avoid needless conversions. + + All subclasses must reimplement this pure virtual function. +*/ + +/*! + \fn bool QWindowsMimeConverter::convertFromMime(const FORMATETC &formatetc, const QMimeData *mimeData, STGMEDIUM * pmedium) const + + Convert the \a mimeData to the format specified in \a formatetc. + The converted data should then be placed in \a pmedium structure. + + Return true if the conversion was successful. + + All subclasses must reimplement this pure virtual function. +*/ + +/*! + Constructs a QWindowsMimeConverter instance. + + The instance is automatically registered, and will be called to convert data during + clipboard or drag'n'drop operations. + + Call this constructor after QGuiApplication has been created. +*/ +QWindowsMimeConverter::QWindowsMimeConverter() +{ + using QWindowsApplication = QNativeInterface::Private::QWindowsApplication; + auto nativeWindowsApp = dynamic_cast<QWindowsApplication *>(QGuiApplicationPrivate::platformIntegration()); + Q_ASSERT(nativeWindowsApp); + nativeWindowsApp->registerMime(this); +} + +/*! + Constructs a QWindowsMimeConverter instance. + + The instance is automatically unregistered. +*/ +QWindowsMimeConverter::~QWindowsMimeConverter() +{ + using QWindowsApplication = QNativeInterface::Private::QWindowsApplication; + auto nativeWindowsApp = dynamic_cast<QWindowsApplication *>(QGuiApplicationPrivate::platformIntegration()); + Q_ASSERT(nativeWindowsApp); + nativeWindowsApp->unregisterMime(this); +} + +/*! + Registers the MIME type \a mimeType, and returns an ID number + identifying the format on Windows. + + A mime type \c {application/x-qt-windows-mime;value="WindowsType"} will be + registered as the clipboard format for \c WindowsType. +*/ +int QWindowsMimeConverter::registerMimeType(const QString &mimeType) +{ + using QWindowsApplication = QNativeInterface::Private::QWindowsApplication; + auto nativeWindowsApp = dynamic_cast<QWindowsApplication *>(QGuiApplicationPrivate::platformIntegration()); + Q_ASSERT(nativeWindowsApp); + return nativeWindowsApp->registerMimeType(mimeType); +} + +QT_END_NAMESPACE diff --git a/src/gui/platform/windows/qwindowsmimeconverter.h b/src/gui/platform/windows/qwindowsmimeconverter.h new file mode 100644 index 0000000000..145355fe15 --- /dev/null +++ b/src/gui/platform/windows/qwindowsmimeconverter.h @@ -0,0 +1,43 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QWINDOWSMIMECONVERTER_P_H +#define QWINDOWSMIMECONVERTER_P_H + +#include <QtGui/qtguiglobal.h> + +struct tagFORMATETC; +using FORMATETC = tagFORMATETC; +struct tagSTGMEDIUM; +using STGMEDIUM = tagSTGMEDIUM; +struct IDataObject; + +QT_BEGIN_NAMESPACE + +class QMetaType; +class QMimeData; +class QVariant; + +class Q_GUI_EXPORT QWindowsMimeConverter +{ + Q_DISABLE_COPY(QWindowsMimeConverter) +public: + QWindowsMimeConverter(); + virtual ~QWindowsMimeConverter(); + + static int registerMimeType(const QString &mimeType); + + // for converting from Qt + virtual bool canConvertFromMime(const FORMATETC &formatetc, const QMimeData *mimeData) const = 0; + virtual bool convertFromMime(const FORMATETC &formatetc, const QMimeData *mimeData, STGMEDIUM * pmedium) const = 0; + virtual QList<FORMATETC> formatsForMime(const QString &mimeType, const QMimeData *mimeData) const = 0; + + // for converting to Qt + virtual bool canConvertToMime(const QString &mimeType, IDataObject *pDataObj) const = 0; + virtual QVariant convertToMime(const QString &mimeType, IDataObject *pDataObj, QMetaType preferredType) const = 0; + virtual QString mimeForFormat(const FORMATETC &formatetc) const = 0; +}; + +QT_END_NAMESPACE + +#endif // QWINDOWSMIMECONVERTER_H diff --git a/src/gui/platform/windows/qwindowsnativeinterface.cpp b/src/gui/platform/windows/qwindowsnativeinterface.cpp new file mode 100644 index 0000000000..44f230e1d3 --- /dev/null +++ b/src/gui/platform/windows/qwindowsnativeinterface.cpp @@ -0,0 +1,312 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include <QtGui/qopenglcontext.h> +#include <QtGui/private/qguiapplication_p.h> +#include <qpa/qplatformopenglcontext.h> +#include <qpa/qplatformintegration.h> +#include <qpa/qplatformwindow.h> +#include <qpa/qplatformwindow_p.h> +#include <qpa/qplatformscreen_p.h> + +QT_BEGIN_NAMESPACE + +using namespace QNativeInterface::Private; + +#ifndef QT_NO_OPENGL + +/*! + \class QNativeInterface::QWGLContext + \inheaderfile QOpenGLContext + \since 6.0 + \brief Native interface to a WGL context on Windows. + + Accessed through QOpenGLContext::nativeInterface(). + + \inmodule QtGui + \inheaderfile QOpenGLContext + \ingroup native-interfaces + \ingroup native-interfaces-qopenglcontext +*/ + +/*! + \fn QOpenGLContext *QNativeInterface::QWGLContext::fromNative(HGLRC context, HWND window, QOpenGLContext *shareContext = nullptr) + + \brief Adopts an WGL \a context handle. + + The \a window is needed because the its pixel format will be queried. When the + adoption is successful, QOpenGLContext::format() will return a QSurfaceFormat + describing this pixel format. + + \note The window specified by \a window must have its pixel format set to a + format compatible with the context's. If no SetPixelFormat() call was made on + any device context belonging to the window, adopting the context will fail. + + Ownership of the created QOpenGLContext \a shareContext is transferred to the + caller. +*/ + +/*! + \fn HGLRC QNativeInterface::QWGLContext::nativeContext() const + + \return the underlying context handle. +*/ + +/*! + \fn HMODULE QNativeInterface::QWGLContext::openGLModuleHandle() + + \return the handle for the OpenGL implementation that is currently in use. + + \note This function requires that the QGuiApplication instance is already created. +*/ + +QT_DEFINE_NATIVE_INTERFACE(QWGLContext); +QT_DEFINE_PRIVATE_NATIVE_INTERFACE(QWindowsGLIntegration); + +HMODULE QNativeInterface::QWGLContext::openGLModuleHandle() +{ + return QGuiApplicationPrivate::platformIntegration()->call< + &QWindowsGLIntegration::openGLModuleHandle>(); +} + +QOpenGLContext *QNativeInterface::QWGLContext::fromNative(HGLRC context, HWND window, QOpenGLContext *shareContext) +{ + return QGuiApplicationPrivate::platformIntegration()->call< + &QWindowsGLIntegration::createOpenGLContext>(context, window, shareContext); +} + +#endif // QT_NO_OPENGL + +/*! + \class QNativeInterface::Private::QWindowsApplication + \since 6.0 + \internal + \brief Native interface to QGuiApplication, to be retrieved from QPlatformIntegration. + \inmodule QtGui + \ingroup native-interfaces +*/ + +QT_DEFINE_PRIVATE_NATIVE_INTERFACE(QWindowsApplication); + +/*! + \class QNativeInterface::QWindowsScreen + \since 6.7 + \brief Native interface to a screen. + + Accessed through QScreen::nativeInterface(). + \inmodule QtGui + \ingroup native-interfaces + \ingroup native-interfaces-qscreen +*/ +/*! + * \fn HWMONITOR QNativeInterface::QWindowsScreen::handle() const; + * \return The underlying HWMONITOR of the screen. + */ +QT_DEFINE_NATIVE_INTERFACE(QWindowsScreen); +/*! + \enum QNativeInterface::Private::QWindowsApplication::TouchWindowTouchType + + This enum represents the supported TouchWindow touch flags for registerTouchWindow(). + + \value NormalTouch + \value FineTouch + \value WantPalmTouch +*/ + +/*! + \fn void QNativeInterface::Private::QWindowsApplication::setTouchWindowTouchType(QNativeInterface::Private::QWindowsApplication::TouchWindowTouchTypes type) + \internal + + Sets the touch window type for all windows to \a type. +*/ + +/*! + \fn QNativeInterface::Private::QWindowsApplication::TouchWindowTouchTypes QNativeInterface::Private::QWindowsApplication::touchWindowTouchType() const + \internal + + Returns the currently set the touch window type. +*/ + +/*! + \enum QNativeInterface::Private::QWindowsApplication::WindowActivationBehavior + + This enum specifies the behavior of QWidget::activateWindow() and + QWindow::requestActivate(). + + \value DefaultActivateWindow The window is activated according to the default + behavior of the Windows operating system. This means the window will not + be activated in some circumstances (most notably when the calling process + is not the active process); only the taskbar entry will be flashed. + \value AlwaysActivateWindow The window is always activated, even when the + calling process is not the active process. + + \sa QWidget::activateWindow(), QWindow::requestActivate() +*/ + +/*! + \fn void QNativeInterface::Private::QWindowsApplication::setWindowActivationBehavior(QNativeInterface::Private::QWindowsApplication::WindowActivationBehavior behavior) + \internal + + Sets the window activation behavior to \a behavior. + + \sa QWidget::activateWindow(), QWindow::requestActivate() +*/ + +/*! + \fn QNativeInterface::Private::QWindowsApplication::WindowActivationBehavior QNativeInterface::Private::QWindowsApplication::windowActivationBehavior() const + \internal + + Returns the currently set the window activation behavior. +*/ + +/*! + \fn bool QNativeInterface::Private::QWindowsApplication::isTabletMode() const + \internal + + Returns \c true if Windows 10 operates in \e{Tablet Mode}. + In this mode, Windows forces all application main windows to open in maximized + state. Applications should then avoid resizing windows or restoring geometries + to non-maximized states. + + \sa QWidget::showMaximized(), QWidget::saveGeometry(), QWidget::restoreGeometry() +*/ + +/*! + \enum QNativeInterface::Private::QWindowsApplication::DarkModeHandlingFlag + + This enum specifies the behavior of the application when Windows + is configured to use dark mode for applications. + + \value DarkModeWindowFrames The window frames will be switched to dark. + \value DarkModeStyle The Windows Vista style will be turned off and + a simple dark style will be used. + + \sa setDarkModeHandling() +*/ + +/*! + \fn void QNativeInterface::Private::QWindowsApplication::setDarkModeHandling(DarkModeHandling handling) = 0 + \internal + + Sets the dark mode handling to \a handling. +*/ + +/*! + \fn QNativeInterface::Private::QWindowsApplication::DarkModeHandling QNativeInterface::Private::QWindowsApplication::darkModeHandling() const + \internal + + Returns the currently set dark mode handling. +*/ + +/*! + \fn bool QNativeInterface::Private::QWindowsApplication::isWinTabEnabled() const = 0 + \internal + + Returns whether the \e{Tablet WinTab Driver} (\c Wintab32.dll) is used. +*/ + +/*! + \fn bool QNativeInterface::Private::QWindowsApplication::setWinTabEnabled(bool enabled) + \internal + + Sets whether the \e{Tablet WinTab Driver} (\c Wintab32.dll) should be used to \a enabled. + + Returns \c true on success, \c false otherwise. +*/ + +/*! + \fn bool QNativeInterface::Private::QWindowsApplication::registerMime(QWindowsMimeConverter *mime) + \internal + + Registers the converter \a mime to the system. + + \sa QWindowsMimeConverter, unregisterMime() +*/ + +/*! + \fn void QNativeInterface::Private::QWindowsApplication::unregisterMime(QWindowsMimeConverter *mime) + \internal + + Unregisters the converter \a mime from the system. + + \sa QWindowsMimeConverter, registerMime() +*/ + +/*! + \fn int QNativeInterface::Private::QWindowsApplication::registerMimeType(const QString &mime) + \internal + + Registers the MIME type \a mime, and returns an ID number + identifying the format on Windows. +*/ + +/*! + \fn HWND QNativeInterface::Private::QWindowsApplication::createMessageWindow(const QString &, const QString &, QFunctionPointer) const + \internal +*/ + +/*! + \fn bool QNativeInterface::Private::QWindowsApplication::asyncExpose() const + \internal +*/ + +/*! + \fn void QNativeInterface::Private::QWindowsApplication::setAsyncExpose(bool) + \internal +*/ + +/*! + \fn QVariant QNativeInterface::Private::QWindowsApplication::gpu() + \internal +*/ + +/*! + \fn QVariant QNativeInterface::Private::QWindowsApplication::gpuList() + \internal +*/ + +/*! + \class QNativeInterface::Private::QWindowsWindow + \since 6.0 + \internal + \brief Native interface to QPlatformWindow. + \inmodule QtGui + \ingroup native-interfaces +*/ + +QT_DEFINE_PRIVATE_NATIVE_INTERFACE(QWindowsWindow); + +/*! + \fn void QNativeInterface::Private::QWindowsWindow::setHasBorderInFullScreen(bool border) + \internal + + Sets whether the WS_BORDER flag will be set for the window in full screen mode + to \a border. + + See also \l [QtDoc] {Fullscreen OpenGL Based Windows} +*/ + +/*! + \fn bool QNativeInterface::Private::QWindowsWindow::hasBorderInFullScreen() const + \internal + + Returns whether the WS_BORDER flag will be set for the window in full screen + mode. +*/ + +/*! + \fn QMargins QNativeInterface::Private::QWindowsWindow::customMargins() const + \internal + + Returns the margin to be used when handling the \c WM_NCCALCSIZE message. +*/ + +/*! + \fn void QNativeInterface::Private::QWindowsWindow::setCustomMargins(const QMargins &margins) + \internal + + Sets the\a margins to be used when handling the \c WM_NCCALCSIZE message. It is + possible to remove a frame border by specifying a negative value. +*/ + +QT_END_NAMESPACE diff --git a/src/gui/platform/windows/qwindowsthemecache.cpp b/src/gui/platform/windows/qwindowsthemecache.cpp new file mode 100644 index 0000000000..3bb92e67ca --- /dev/null +++ b/src/gui/platform/windows/qwindowsthemecache.cpp @@ -0,0 +1,79 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qwindowsthemecache_p.h" +#include <QtCore/qdebug.h> +#include <QtCore/qhash.h> + +QT_BEGIN_NAMESPACE + +// Theme names matching the QWindowsVistaStylePrivate::Theme enumeration. +constexpr const wchar_t *themeNames[] = { + L"BUTTON", L"COMBOBOX", L"EDIT", L"HEADER", L"LISTVIEW", + L"MENU", L"PROGRESS", L"REBAR", L"SCROLLBAR", L"SPIN", + L"TAB", L"TASKDIALOG", L"TOOLBAR", L"TOOLTIP", L"TRACKBAR", + L"WINDOW", L"STATUS", L"TREEVIEW" +}; + +typedef std::array<HTHEME, std::size(themeNames)> ThemeArray; +typedef QHash<HWND, ThemeArray> ThemesCache; +Q_GLOBAL_STATIC(ThemesCache, themesCache); + +QString QWindowsThemeCache::themeName(int theme) +{ + return theme >= 0 && theme < int(std::size(themeNames)) + ? QString::fromWCharArray(themeNames[theme]) : QString(); +} + +HTHEME QWindowsThemeCache::createTheme(int theme, HWND hwnd) +{ + if (Q_UNLIKELY(theme < 0 || theme >= int(std::size(themeNames)) || !hwnd)) { + qWarning("Invalid parameters #%d, %p", theme, hwnd); + return nullptr; + } + + // Get or create themes array for this window. + ThemesCache *cache = themesCache(); + auto it = cache->find(hwnd); + if (it == cache->end()) + it = cache->insert(hwnd, ThemeArray {}); + + // Get or create theme data + ThemeArray &themes = *it; + if (!themes[theme]) { + const wchar_t *name = themeNames[theme]; + themes[theme] = OpenThemeData(hwnd, name); + if (Q_UNLIKELY(!themes[theme])) + qErrnoWarning("OpenThemeData() failed for theme %d (%s).", + theme, qPrintable(themeName(theme))); + } + return themes[theme]; +} + +static void clearThemes(ThemeArray &themes) +{ + for (auto &theme : themes) { + if (theme) { + CloseThemeData(theme); + theme = nullptr; + } + } +} + +void QWindowsThemeCache::clearThemeCache(HWND hwnd) +{ + ThemesCache *cache = themesCache(); + auto it = cache->find(hwnd); + if (it == cache->end()) + return; + clearThemes(*it); +} + +void QWindowsThemeCache::clearAllThemeCaches() +{ + ThemesCache *cache = themesCache(); + for (auto &themeArray : *cache) + clearThemes(themeArray); +} + +QT_END_NAMESPACE diff --git a/src/gui/platform/windows/qwindowsthemecache_p.h b/src/gui/platform/windows/qwindowsthemecache_p.h new file mode 100644 index 0000000000..beb724dc5c --- /dev/null +++ b/src/gui/platform/windows/qwindowsthemecache_p.h @@ -0,0 +1,35 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QWINDOWSTHEME_CACHE_P_H +#define QWINDOWSTHEME_CACHE_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "QtGui/private/qtguiglobal_p.h" + +#include <QtCore/qt_windows.h> +#include <uxtheme.h> + +QT_BEGIN_NAMESPACE + +namespace QWindowsThemeCache +{ + Q_GUI_EXPORT QString themeName(int theme); + Q_GUI_EXPORT HTHEME createTheme(int theme, HWND hwnd); + Q_GUI_EXPORT void clearThemeCache(HWND hwnd); + Q_GUI_EXPORT void clearAllThemeCaches(); +} + +QT_END_NAMESPACE + +#endif // QWINDOWSTHEME_CACHE_P_H |