diff options
Diffstat (limited to 'src/gui/platform')
41 files changed, 3673 insertions, 1340 deletions
diff --git a/src/gui/platform/android/qandroidnativeinterface.cpp b/src/gui/platform/android/qandroidnativeinterface.cpp index 3062b5255e..c1c4b7149f 100644 --- a/src/gui/platform/android/qandroidnativeinterface.cpp +++ b/src/gui/platform/android/qandroidnativeinterface.cpp @@ -7,6 +7,8 @@ #include <QtGui/qoffscreensurface_platform.h> #include <QtGui/private/qguiapplication_p.h> +#include <QtGui/qpa/qplatformscreen_p.h> + QT_BEGIN_NAMESPACE using namespace QNativeInterface::Private; @@ -33,4 +35,20 @@ QOffscreenSurface *QNativeInterface::QAndroidOffscreenSurface::fromNative(ANati &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 index dcc51bfca6..b8ff5c9d6d 100644 --- a/src/gui/platform/darwin/qapplekeymapper.mm +++ b/src/gui/platform/darwin/qapplekeymapper.mm @@ -18,7 +18,6 @@ QT_BEGIN_NAMESPACE -Q_LOGGING_CATEGORY(lcQpaKeyMapper, "qt.qpa.keymapper"); Q_LOGGING_CATEGORY(lcQpaKeyMapperKeys, "qt.qpa.keymapper.keys"); static Qt::KeyboardModifiers swapModifiersIfNeeded(const Qt::KeyboardModifiers modifiers) @@ -37,36 +36,6 @@ static Qt::KeyboardModifiers swapModifiersIfNeeded(const Qt::KeyboardModifiers m return swappedModifiers; } -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; -} - #ifdef Q_OS_MACOS static constexpr std::tuple<NSEventModifierFlags, Qt::KeyboardModifier> cocoaModifierMap[] = { { NSEventModifierFlagShift, Qt::ShiftModifier }, @@ -360,11 +329,11 @@ QChar QAppleKeyMapper::toCocoaKey(Qt::Key key) { // Prioritize overloaded keys if (key == Qt::Key_Return) - return QChar(NSNewlineCharacter); + return QChar(NSCarriageReturnCharacter); if (key == Qt::Key_Backspace) return QChar(NSBackspaceCharacter); - static QHash<Qt::Key, char16_t> reverseCocoaKeys; + 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) @@ -384,7 +353,7 @@ Qt::Key QAppleKeyMapper::fromCocoaKey(QChar keyCode) // ------------------------------------------------ -Qt::KeyboardModifiers QAppleKeyMapper::queryKeyboardModifiers() +Qt::KeyboardModifiers QAppleKeyMapper::queryKeyboardModifiers() const { return fromCocoaModifiers(NSEvent.modifierFlags); } @@ -538,11 +507,9 @@ const QAppleKeyMapper::KeyMap &QAppleKeyMapper::keyMapForKey(VirtualKeyCode virt where each modifier-key combination has been mapped to the key it will produce. */ -QList<int> QAppleKeyMapper::possibleKeys(const QKeyEvent *event) const +QList<QKeyCombination> QAppleKeyMapper::possibleKeyCombinations(const QKeyEvent *event) const { - QList<int> ret; - - qCDebug(lcQpaKeyMapper) << "Computing possible keys for" << event; + QList<QKeyCombination> ret; const auto nativeVirtualKey = event->nativeVirtualKey(); if (!nativeVirtualKey) @@ -555,16 +522,49 @@ QList<int> QAppleKeyMapper::possibleKeys(const QKeyEvent *event) const auto eventModifiers = event->modifiers(); - // The complete set of event modifiers, along with the - // unmodified key, is always a valid key combination, - // and the first priority. - ret << int(eventModifiers) + int(unmodifiedKey); + 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 = 1; i < 8; ++i) { + for (int i = startingModifierLayer; i < 15; ++i) { auto keyAfterApplyingModifiers = keyMap[i]; - if (keyAfterApplyingModifiers == unmodifiedKey) - continue; if (!keyAfterApplyingModifiers) continue; @@ -575,18 +575,39 @@ QList<int> QAppleKeyMapper::possibleKeys(const QKeyEvent *event) const // If the event includes more modifiers than the candidate they // will need to be included in the resulting key combination. auto additionalModifiers = eventModifiers & ~candidateModifiers; - ret << int(additionalModifiers) + int(keyAfterApplyingModifiers); - } - } - if (lcQpaKeyMapper().isDebugEnabled()) { - qCDebug(lcQpaKeyMapper) << "Possible keys:"; - for (int keyAndModifiers : ret) { - auto keyCombination = QKeyCombination::fromCombined(keyAndModifiers); - auto keySequence = QKeySequence(keyCombination); - qCDebug(lcQpaKeyMapper).verbosity(0) << "\t-" - << keyCombination << "/" << keySequence << "/" - << qUtf8Printable(keySequence.toString(QKeySequence::NativeText)); + 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; + } } } @@ -597,11 +618,40 @@ QList<int> QAppleKeyMapper::possibleKeys(const QKeyEvent *event) const #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 = { -#if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_13_4) { UIKeyInputF1, Qt::Key_F1 }, { UIKeyInputF2, Qt::Key_F2 }, { UIKeyInputF3, Qt::Key_F3 }, @@ -618,7 +668,6 @@ API_AVAILABLE(ios(13.4)) Qt::Key QAppleKeyMapper::fromUIKitKey(NSString *keyCode { UIKeyInputEnd, Qt::Key_End }, { UIKeyInputPageUp, Qt::Key_PageUp }, { UIKeyInputPageDown, Qt::Key_PageDown }, -#endif { UIKeyInputEscape, Qt::Key_Escape }, { UIKeyInputUpArrow, Qt::Key_Up }, { UIKeyInputDownArrow, Qt::Key_Down }, diff --git a/src/gui/platform/darwin/qapplekeymapper_p.h b/src/gui/platform/darwin/qapplekeymapper_p.h index 34557c8ede..1f3494d16f 100644 --- a/src/gui/platform/darwin/qapplekeymapper_p.h +++ b/src/gui/platform/darwin/qapplekeymapper_p.h @@ -19,6 +19,8 @@ #include <Carbon/Carbon.h> #endif +#include <qpa/qplatformkeymapper.h> + #include <QtCore/QList> #include <QtCore/QHash> #include <QtGui/QKeyEvent> @@ -27,13 +29,12 @@ QT_BEGIN_NAMESPACE -class Q_GUI_EXPORT QAppleKeyMapper +class Q_GUI_EXPORT QAppleKeyMapper : public QPlatformKeyMapper { public: - static Qt::KeyboardModifiers queryKeyboardModifiers(); - QList<int> possibleKeys(const QKeyEvent *event) const; - static Qt::Key fromNSString(Qt::KeyboardModifiers qtMods, NSString *characters, - NSString *charactersIgnoringModifiers, QString &text); + 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); @@ -41,6 +42,9 @@ public: 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); diff --git a/src/gui/platform/darwin/qmacmime.mm b/src/gui/platform/darwin/qmacmime.mm deleted file mode 100644 index fe323bf60d..0000000000 --- a/src/gui/platform/darwin/qmacmime.mm +++ /dev/null @@ -1,1013 +0,0 @@ -// 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 <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 "qmacmime_p.h" -#include "qguiapplication.h" -#include "private/qcore_mac_p.h" - -QT_BEGIN_NAMESPACE - -using namespace Qt::StringLiterals; - -typedef QList<QMacInternalPasteboardMime*> MimeList; -Q_GLOBAL_STATIC(MimeList, globalMimeList) -Q_GLOBAL_STATIC(QStringList, globalDraggedTypesList) - -void qt_mac_addToGlobalMimeList(QMacInternalPasteboardMime *macMime) -{ - // globalMimeList is in decreasing priority order. Recently added - // converters take prioity over previously added converters: prepend - // to the list. - globalMimeList()->prepend(macMime); -} - -void qt_mac_removeFromGlobalMimeList(QMacInternalPasteboardMime *macMime) -{ - if (!QGuiApplication::closingDown()) - globalMimeList()->removeAll(macMime); -} - -/*! - \fn void qRegisterDraggedTypes(const QStringList &types) - \relates QMacPasteboardMime - - Registers the given \a types as custom pasteboard types. - - This function should be called to enable the Drag and Drop events - for custom pasteboard types on Cocoa implementations. This is required - in addition to a QMacPasteboardMime subclass implementation. By default - drag and drop is enabled for all standard pasteboard types. - - \sa QMacPasteboardMime -*/ -void qt_mac_registerDraggedTypes(const QStringList &types) -{ - (*globalDraggedTypesList()) += types; -} - -const QStringList& qt_mac_enabledDraggedTypes() -{ - return (*globalDraggedTypesList()); -} - -/***************************************************************************** - QDnD debug facilities - *****************************************************************************/ -//#define DEBUG_MIME_MAPS - -/*! - \class QMacInternalPasteboardMime - \internal - \brief The QMacPasteboardMime class converts between a MIME type and a - \l{http://developer.apple.com/macosx/uniformtypeidentifiers.html}{Uniform - Type Identifier (UTI)} format. - \since 4.2 - - \ingroup draganddrop - \inmodule QtWidgets - - 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. - - QMacPasteboardMime'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. - - A subclass of QMacPasteboardMime will automatically be registered, and active, upon instantiation. - - 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 QMacPasteboardMime to - find an instance that can convert to, or from, a specific MIME type. It will do this by calling - canConvert() on each instance, starting with (and choosing) the last created instance first. - The actual conversions will be done by using convertToMime() and convertFromMime(). - - \note The API uses the term "flavor" in some cases. This is for backwards - compatibility reasons, and should now be understood as UTIs. -*/ - -/* - \enum QMacPasteboardMime::QMacPasteboardMimeType - \internal -*/ - -/* - Constructs a new conversion object of type \a t, adding it to the - globally accessed list of available converters. -*/ -QMacInternalPasteboardMime::QMacInternalPasteboardMime(char t) : type(t) -{ - qt_mac_addToGlobalMimeList(this); -} - -/* - Destroys a conversion object, removing it from the global - list of available converters. -*/ -QMacInternalPasteboardMime::~QMacInternalPasteboardMime() -{ - qt_mac_removeFromGlobalMimeList(this); -} - -/* - Returns the item count for the given \a mimeData -*/ -int QMacInternalPasteboardMime::count(QMimeData *mimeData) -{ - Q_UNUSED(mimeData); - return 1; -} - -class QMacPasteboardMimeAny : public QMacInternalPasteboardMime { -private: - -public: - QMacPasteboardMimeAny() : QMacInternalPasteboardMime(MIME_QT_CONVERTOR|MIME_ALL) { - } - ~QMacPasteboardMimeAny() { - } - QString convertorName(); - - QString flavorFor(const QString &mime); - QString mimeFor(QString flav); - bool canConvert(const QString &mime, QString flav); - QVariant convertToMime(const QString &mime, QList<QByteArray> data, QString flav); - QList<QByteArray> convertFromMime(const QString &mime, QVariant data, QString flav); -}; - -QString QMacPasteboardMimeAny::convertorName() -{ - return "Any-Mime"_L1; -} - -QString QMacPasteboardMimeAny::flavorFor(const QString &mime) -{ - // 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 QMacPasteboardMimeAny::mimeFor(QString flav) -{ - const QString any_prefix = "com.trolltech.anymime."_L1; - if (flav.size() > any_prefix.length() && flav.startsWith(any_prefix)) - return flav.mid(any_prefix.length()).replace("--"_L1, "/"_L1); - return QString(); -} - -bool QMacPasteboardMimeAny::canConvert(const QString &mime, QString flav) -{ - return mimeFor(flav) == mime; -} - -QVariant QMacPasteboardMimeAny::convertToMime(const QString &mime, QList<QByteArray> data, QString) -{ - if (data.count() > 1) - qWarning("QMacPasteboardMimeAny: 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> QMacPasteboardMimeAny::convertFromMime(const QString &mime, QVariant data, QString) -{ - QList<QByteArray> ret; - if (mime == "text/plain"_L1) - ret.append(data.toString().toUtf8()); - else - ret.append(data.toByteArray()); - return ret; -} - -class QMacPasteboardMimeTypeName : public QMacInternalPasteboardMime { -private: - -public: - QMacPasteboardMimeTypeName() : QMacInternalPasteboardMime(MIME_QT_CONVERTOR|MIME_ALL) { - } - ~QMacPasteboardMimeTypeName() { - } - QString convertorName(); - - QString flavorFor(const QString &mime); - QString mimeFor(QString flav); - bool canConvert(const QString &mime, QString flav); - QVariant convertToMime(const QString &mime, QList<QByteArray> data, QString flav); - QList<QByteArray> convertFromMime(const QString &mime, QVariant data, QString flav); -}; - -QString QMacPasteboardMimeTypeName::convertorName() -{ - return "Qt-Mime-Type"_L1; -} - -QString QMacPasteboardMimeTypeName::flavorFor(const QString &mime) -{ - if (mime == "application/x-qt-mime-type-name"_L1) - return "com.trolltech.qt.MimeTypeName"_L1; - return QString(); -} - -QString QMacPasteboardMimeTypeName::mimeFor(QString) -{ - return QString(); -} - -bool QMacPasteboardMimeTypeName::canConvert(const QString &, QString) -{ - return false; -} - -QVariant QMacPasteboardMimeTypeName::convertToMime(const QString &, QList<QByteArray>, QString) -{ - QVariant ret; - return ret; -} - -QList<QByteArray> QMacPasteboardMimeTypeName::convertFromMime(const QString &, QVariant, QString) -{ - QList<QByteArray> ret; - ret.append(QString("x-qt-mime-type-name"_L1).toUtf8()); - return ret; -} - -class QMacPasteboardMimePlainTextFallback : public QMacInternalPasteboardMime { -public: - QMacPasteboardMimePlainTextFallback() : QMacInternalPasteboardMime(MIME_ALL) { } - QString convertorName(); - - QString flavorFor(const QString &mime); - QString mimeFor(QString flav); - bool canConvert(const QString &mime, QString flav); - QVariant convertToMime(const QString &mime, QList<QByteArray> data, QString flav); - QList<QByteArray> convertFromMime(const QString &mime, QVariant data, QString flav); -}; - -QString QMacPasteboardMimePlainTextFallback::convertorName() -{ - return "PlainText (public.text)"_L1; -} - -QString QMacPasteboardMimePlainTextFallback::flavorFor(const QString &mime) -{ - if (mime == "text/plain"_L1) - return "public.text"_L1; - return QString(); -} - -QString QMacPasteboardMimePlainTextFallback::mimeFor(QString flav) -{ - if (flav == "public.text"_L1) - return "text/plain"_L1; - return QString(); -} - -bool QMacPasteboardMimePlainTextFallback::canConvert(const QString &mime, QString flav) -{ - return mime == mimeFor(flav); -} - -QVariant QMacPasteboardMimePlainTextFallback::convertToMime(const QString &mimetype, QList<QByteArray> data, QString flavor) -{ - if (data.count() > 1) - qWarning("QMacPasteboardMimePlainTextFallback: Cannot handle multiple member data"); - - if (flavor == "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> QMacPasteboardMimePlainTextFallback::convertFromMime(const QString &, QVariant data, QString flavor) -{ - QList<QByteArray> ret; - QString string = data.toString(); - if (flavor == "public.text"_L1) - ret.append(string.toUtf8()); - return ret; -} - -class QMacPasteboardMimeUnicodeText : public QMacInternalPasteboardMime { -public: - QMacPasteboardMimeUnicodeText() : QMacInternalPasteboardMime(MIME_ALL) { } - QString convertorName(); - - QString flavorFor(const QString &mime); - QString mimeFor(QString flav); - bool canConvert(const QString &mime, QString flav); - QVariant convertToMime(const QString &mime, QList<QByteArray> data, QString flav); - QList<QByteArray> convertFromMime(const QString &mime, QVariant data, QString flav); -}; - -QString QMacPasteboardMimeUnicodeText::convertorName() -{ - return "UnicodeText"_L1; -} - -QString QMacPasteboardMimeUnicodeText::flavorFor(const QString &mime) -{ - if (mime == "text/plain"_L1) - return "public.utf16-plain-text"_L1; - int i = mime.indexOf("charset="_L1); - if (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 QMacPasteboardMimeUnicodeText::mimeFor(QString flav) -{ - if (flav == "public.utf16-plain-text"_L1 || flav == "public.utf8-plain-text"_L1) - return "text/plain"_L1; - return QString(); -} - -bool QMacPasteboardMimeUnicodeText::canConvert(const QString &mime, QString flav) -{ - return (mime == "text/plain"_L1 - && (flav == "public.utf8-plain-text"_L1 || (flav == "public.utf16-plain-text"_L1))); -} - -QVariant QMacPasteboardMimeUnicodeText::convertToMime(const QString &mimetype, QList<QByteArray> data, QString flavor) -{ - if (data.count() > 1) - qWarning("QMacPasteboardMimeUnicodeText: 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 (flavor == "public.utf8-plain-text"_L1) { - ret = QString::fromUtf8(firstData); - } else if (flavor == "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> QMacPasteboardMimeUnicodeText::convertFromMime(const QString &, QVariant data, QString flavor) -{ - QList<QByteArray> ret; - QString string = data.toString(); - if (flavor == "public.utf8-plain-text"_L1) - ret.append(string.toUtf8()); - else if (flavor == "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 QMacPasteboardMimeHTMLText : public QMacInternalPasteboardMime { -public: - QMacPasteboardMimeHTMLText() : QMacInternalPasteboardMime(MIME_ALL) { } - QString convertorName(); - - QString flavorFor(const QString &mime); - QString mimeFor(QString flav); - bool canConvert(const QString &mime, QString flav); - QVariant convertToMime(const QString &mime, QList<QByteArray> data, QString flav); - QList<QByteArray> convertFromMime(const QString &mime, QVariant data, QString flav); -}; - -QString QMacPasteboardMimeHTMLText::convertorName() -{ - return "HTML"_L1; -} - -QString QMacPasteboardMimeHTMLText::flavorFor(const QString &mime) -{ - if (mime == "text/html"_L1) - return "public.html"_L1; - return QString(); -} - -QString QMacPasteboardMimeHTMLText::mimeFor(QString flav) -{ - if (flav == "public.html"_L1) - return "text/html"_L1; - return QString(); -} - -bool QMacPasteboardMimeHTMLText::canConvert(const QString &mime, QString flav) -{ - return flavorFor(mime) == flav; -} - -QVariant QMacPasteboardMimeHTMLText::convertToMime(const QString &mimeType, QList<QByteArray> data, QString flavor) -{ - if (!canConvert(mimeType, flavor)) - return QVariant(); - if (data.count() > 1) - qWarning("QMacPasteboardMimeHTMLText: Cannot handle multiple member data"); - return data.first(); -} - -QList<QByteArray> QMacPasteboardMimeHTMLText::convertFromMime(const QString &mime, QVariant data, QString flavor) -{ - QList<QByteArray> ret; - if (!canConvert(mime, flavor)) - return ret; - ret.append(data.toByteArray()); - return ret; -} - -class QMacPasteboardMimeRtfText : public QMacInternalPasteboardMime { -public: - QMacPasteboardMimeRtfText() : QMacInternalPasteboardMime(MIME_ALL) { } - QString convertorName(); - - QString flavorFor(const QString &mime); - QString mimeFor(QString flav); - bool canConvert(const QString &mime, QString flav); - QVariant convertToMime(const QString &mime, QList<QByteArray> data, QString flav); - QList<QByteArray> convertFromMime(const QString &mime, QVariant data, QString flav); -}; - -QString QMacPasteboardMimeRtfText::convertorName() -{ - return "Rtf"_L1; -} - -QString QMacPasteboardMimeRtfText::flavorFor(const QString &mime) -{ - if (mime == "text/html"_L1) - return "public.rtf"_L1; - return QString(); -} - -QString QMacPasteboardMimeRtfText::mimeFor(QString flav) -{ - if (flav == "public.rtf"_L1) - return "text/html"_L1; - return QString(); -} - -bool QMacPasteboardMimeRtfText::canConvert(const QString &mime, QString flav) -{ - return mime == mimeFor(flav); -} - -QVariant QMacPasteboardMimeRtfText::convertToMime(const QString &mimeType, QList<QByteArray> data, QString flavor) -{ - if (!canConvert(mimeType, flavor)) - return QVariant(); - if (data.count() > 1) - qWarning("QMacPasteboardMimeHTMLText: 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> QMacPasteboardMimeRtfText::convertFromMime(const QString &mime, QVariant data, QString flavor) -{ - QList<QByteArray> ret; - if (!canConvert(mime, flavor)) - 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 QMacPasteboardMimeFileUri : public QMacInternalPasteboardMime { -public: - QMacPasteboardMimeFileUri() : QMacInternalPasteboardMime(MIME_ALL) { } - QString convertorName(); - - QString flavorFor(const QString &mime); - QString mimeFor(QString flav); - bool canConvert(const QString &mime, QString flav); - QVariant convertToMime(const QString &mime, QList<QByteArray> data, QString flav); - QList<QByteArray> convertFromMime(const QString &mime, QVariant data, QString flav); - int count(QMimeData *mimeData); -}; - -QString QMacPasteboardMimeFileUri::convertorName() -{ - return "FileURL"_L1; -} - -QString QMacPasteboardMimeFileUri::flavorFor(const QString &mime) -{ - if (mime == "text/uri-list"_L1) - return "public.file-url"_L1; - return QString(); -} - -QString QMacPasteboardMimeFileUri::mimeFor(QString flav) -{ - if (flav == "public.file-url"_L1) - return "text/uri-list"_L1; - return QString(); -} - -bool QMacPasteboardMimeFileUri::canConvert(const QString &mime, QString flav) -{ - return mime == "text/uri-list"_L1 && flav == "public.file-url"_L1; -} - -QVariant QMacPasteboardMimeFileUri::convertToMime(const QString &mime, QList<QByteArray> data, QString flav) -{ - if (!canConvert(mime, flav)) - 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> QMacPasteboardMimeFileUri::convertFromMime(const QString &mime, QVariant data, QString flav) -{ - QList<QByteArray> ret; - if (!canConvert(mime, flav)) - 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 QMacPasteboardMimeFileUri::count(QMimeData *mimeData) -{ - return mimeData->urls().count(); -} - -class QMacPasteboardMimeUrl : public QMacInternalPasteboardMime { -public: - QMacPasteboardMimeUrl() : QMacInternalPasteboardMime(MIME_ALL) { } - QString convertorName(); - - QString flavorFor(const QString &mime); - QString mimeFor(QString flav); - bool canConvert(const QString &mime, QString flav); - QVariant convertToMime(const QString &mime, QList<QByteArray> data, QString flav); - QList<QByteArray> convertFromMime(const QString &mime, QVariant data, QString flav); -}; - -QString QMacPasteboardMimeUrl::convertorName() -{ - return "URL"_L1; -} - -QString QMacPasteboardMimeUrl::flavorFor(const QString &mime) -{ - if (mime.startsWith("text/uri-list"_L1)) - return "public.url"_L1; - return QString(); -} - -QString QMacPasteboardMimeUrl::mimeFor(QString flav) -{ - if (flav == "public.url"_L1) - return "text/uri-list"_L1; - return QString(); -} - -bool QMacPasteboardMimeUrl::canConvert(const QString &mime, QString flav) -{ - return flav == "public.url"_L1 - && mime == "text/uri-list"_L1; -} - -QVariant QMacPasteboardMimeUrl::convertToMime(const QString &mime, QList<QByteArray> data, QString flav) -{ - if (!canConvert(mime, flav)) - 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> QMacPasteboardMimeUrl::convertFromMime(const QString &mime, QVariant data, QString flav) -{ - QList<QByteArray> ret; - if (!canConvert(mime, flav)) - 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 QMacPasteboardMimeVCard : public QMacInternalPasteboardMime -{ -public: - QMacPasteboardMimeVCard() : QMacInternalPasteboardMime(MIME_ALL){ } - QString convertorName(); - - QString flavorFor(const QString &mime); - QString mimeFor(QString flav); - bool canConvert(const QString &mime, QString flav); - QVariant convertToMime(const QString &mime, QList<QByteArray> data, QString flav); - QList<QByteArray> convertFromMime(const QString &mime, QVariant data, QString flav); -}; - -QString QMacPasteboardMimeVCard::convertorName() -{ - return "VCard"_L1; -} - -bool QMacPasteboardMimeVCard::canConvert(const QString &mime, QString flav) -{ - return mimeFor(flav) == mime; -} - -QString QMacPasteboardMimeVCard::flavorFor(const QString &mime) -{ - if (mime.startsWith("text/vcard"_L1)) - return "public.vcard"_L1; - return QString(); -} - -QString QMacPasteboardMimeVCard::mimeFor(QString flav) -{ - if (flav == "public.vcard"_L1) - return "text/vcard"_L1; - return QString(); -} - -QVariant QMacPasteboardMimeVCard::convertToMime(const QString &mime, QList<QByteArray> data, QString) -{ - QByteArray cards; - if (mime == "text/vcard"_L1) { - for (int i=0; i<data.size(); ++i) - cards += data[i]; - } - return QVariant(cards); -} - -QList<QByteArray> QMacPasteboardMimeVCard::convertFromMime(const QString &mime, QVariant data, QString) -{ - QList<QByteArray> 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 QMacPasteboardMimeTiff : public QMacInternalPasteboardMime { -public: - QMacPasteboardMimeTiff() : QMacInternalPasteboardMime(MIME_ALL) { } - QString convertorName(); - - QString flavorFor(const QString &mime); - QString mimeFor(QString flav); - bool canConvert(const QString &mime, QString flav); - QVariant convertToMime(const QString &mime, QList<QByteArray> data, QString flav); - QList<QByteArray> convertFromMime(const QString &mime, QVariant data, QString flav); -}; - -QString QMacPasteboardMimeTiff::convertorName() -{ - return "Tiff"_L1; -} - -QString QMacPasteboardMimeTiff::flavorFor(const QString &mime) -{ - if (mime.startsWith("application/x-qt-image"_L1)) - return "public.tiff"_L1; - return QString(); -} - -QString QMacPasteboardMimeTiff::mimeFor(QString flav) -{ - if (flav == "public.tiff"_L1) - return "application/x-qt-image"_L1; - return QString(); -} - -bool QMacPasteboardMimeTiff::canConvert(const QString &mime, QString flav) -{ - return flav == "public.tiff"_L1 && mime == "application/x-qt-image"_L1; -} - -QVariant QMacPasteboardMimeTiff::convertToMime(const QString &mime, QList<QByteArray> data, QString flav) -{ - if (data.count() > 1) - qWarning("QMacPasteboardMimeTiff: Cannot handle multiple member data"); - - if (!canConvert(mime, flav)) - 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> QMacPasteboardMimeTiff::convertFromMime(const QString &mime, QVariant variant, QString flav) -{ - if (!canConvert(mime, flav)) - return QList<QByteArray>(); - - QCFType<CFMutableDataRef> data = CFDataCreateMutable(0, 0); - QCFType<CGImageDestinationRef> imageDestination = CGImageDestinationCreateWithData(data, kUTTypeTIFF, 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); -} - -/*! - \internal - - This is an internal function. -*/ -void QMacInternalPasteboardMime::initializeMimeTypes() -{ - if (globalMimeList()->isEmpty()) { - // Create QMacPasteboardMimeAny first to put it at the end of globalMimeList - // with lowest priority. (the constructor prepends to the list) - new QMacPasteboardMimeAny; - - //standard types that we wrap - new QMacPasteboardMimeTiff; - new QMacPasteboardMimePlainTextFallback; - new QMacPasteboardMimeUnicodeText; - new QMacPasteboardMimeRtfText; - new QMacPasteboardMimeHTMLText; - new QMacPasteboardMimeFileUri; - new QMacPasteboardMimeUrl; - new QMacPasteboardMimeTypeName; - new QMacPasteboardMimeVCard; - } -} - -/*! - \internal -*/ -void QMacInternalPasteboardMime::destroyMimeTypes() -{ - MimeList *mimes = globalMimeList(); - while (!mimes->isEmpty()) - delete mimes->takeFirst(); -} - -/* - Returns the most-recently created QMacPasteboardMime of type \a t that can convert - between the \a mime and \a flav formats. Returns 0 if no such convertor - exists. -*/ -QMacInternalPasteboardMime* -QMacInternalPasteboardMime::convertor(uchar t, const QString &mime, QString flav) -{ - MimeList *mimes = globalMimeList(); - for (MimeList::const_iterator it = mimes->constBegin(); it != mimes->constEnd(); ++it) { -#ifdef DEBUG_MIME_MAPS - qDebug("QMacPasteboardMime::convertor: seeing if %s (%d) can convert %s to %d[%c%c%c%c] [%d]", - (*it)->convertorName().toLatin1().constData(), - (*it)->type & t, mime.toLatin1().constData(), - flav, (flav >> 24) & 0xFF, (flav >> 16) & 0xFF, (flav >> 8) & 0xFF, (flav) & 0xFF, - (*it)->canConvert(mime,flav)); - for (int i = 0; i < (*it)->countFlavors(); ++i) { - int f = (*it)->flavor(i); - qDebug(" %d) %d[%c%c%c%c] [%s]", i, f, - (f >> 24) & 0xFF, (f >> 16) & 0xFF, (f >> 8) & 0xFF, (f) & 0xFF, - (*it)->convertorName().toLatin1().constData()); - } -#endif - if (((*it)->type & t) && (*it)->canConvert(mime, flav)) - return (*it); - } - return 0; -} -/* - Returns a MIME type of type \a t for \a flav, or 0 if none exists. -*/ -QString QMacInternalPasteboardMime::flavorToMime(uchar t, QString flav) -{ - MimeList *mimes = globalMimeList(); - for (MimeList::const_iterator it = mimes->constBegin(); it != mimes->constEnd(); ++it) { -#ifdef DEBUG_MIME_MAPS - qDebug("QMacMIme::flavorToMime: attempting %s (%d) for flavor %d[%c%c%c%c] [%s]", - (*it)->convertorName().toLatin1().constData(), - (*it)->type & t, flav, (flav >> 24) & 0xFF, (flav >> 16) & 0xFF, (flav >> 8) & 0xFF, (flav) & 0xFF, - (*it)->mimeFor(flav).toLatin1().constData()); - -#endif - if ((*it)->type & t) { - QString mimeType = (*it)->mimeFor(flav); - if (!mimeType.isNull()) - return mimeType; - } - } - return QString(); -} - -/* - Returns a list of all currently defined QMacPasteboardMime objects of type \a t. -*/ -QList<QMacInternalPasteboardMime*> QMacInternalPasteboardMime::all(uchar t) -{ - MimeList ret; - MimeList *mimes = globalMimeList(); - for (MimeList::const_iterator it = mimes->constBegin(); it != mimes->constEnd(); ++it) { - if ((*it)->type & t) - ret.append((*it)); - } - return ret; -} - - -/* - \fn QString QMacPasteboardMime::convertorName() - - Returns a name for the convertor. - - All subclasses must reimplement this pure virtual function. -*/ - -/* - \fn bool QMacPasteboardMime::canConvert(const QString &mime, QString flav) - - Returns \c true if the convertor can convert (both ways) between - \a mime and \a flav; otherwise returns \c false. - - All subclasses must reimplement this pure virtual function. -*/ - -/* - \fn QString QMacPasteboardMime::mimeFor(QString flav) - - Returns the MIME UTI used for Mac flavor \a flav, or 0 if this - convertor does not support \a flav. - - All subclasses must reimplement this pure virtual function. -*/ - -/* - \fn QString QMacPasteboardMime::flavorFor(const QString &mime) - - Returns the Mac UTI used for MIME type \a mime, or 0 if this - convertor does not support \a mime. - - All subclasses must reimplement this pure virtual function. -*/ - -/* - \fn QVariant QMacPasteboardMime::convertToMime(const QString &mime, QList<QByteArray> data, QString flav) - - Returns \a data converted from Mac UTI \a flav to MIME type \a - mime. - - Note that Mac flavors must all be self-terminating. The input \a - data may contain trailing data. - - All subclasses must reimplement this pure virtual function. -*/ - -/* - \fn QList<QByteArray> QMacPasteboardMime::convertFromMime(const QString &mime, QVariant data, QString flav) - - Returns \a data converted from MIME type \a mime - to Mac UTI \a flav. - - Note that Mac flavors must all be self-terminating. The return - value may contain trailing data. - - All subclasses must reimplement this pure virtual function. -*/ - -QT_END_NAMESPACE diff --git a/src/gui/platform/darwin/qmacmime_p.h b/src/gui/platform/darwin/qmacmime_p.h deleted file mode 100644 index bd926f59fe..0000000000 --- a/src/gui/platform/darwin/qmacmime_p.h +++ /dev/null @@ -1,63 +0,0 @@ -// 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 QMACMIME_H -#define QMACMIME_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 <CoreFoundation/CoreFoundation.h> - -QT_BEGIN_NAMESPACE - -// Duplicate of QMacPasteboardMime in QtMacExtras. Keep in sync! -class Q_GUI_EXPORT QMacInternalPasteboardMime { - char type; -public: - enum QMacPasteboardMimeType { MIME_DND=0x01, - MIME_CLIP=0x02, - MIME_QT_CONVERTOR=0x04, - MIME_QT3_CONVERTOR=0x08, - MIME_ALL=MIME_DND|MIME_CLIP - }; - explicit QMacInternalPasteboardMime(char); - virtual ~QMacInternalPasteboardMime(); - - static void initializeMimeTypes(); - static void destroyMimeTypes(); - - static QList<QMacInternalPasteboardMime*> all(uchar); - static QMacInternalPasteboardMime *convertor(uchar, const QString &mime, QString flav); - static QString flavorToMime(uchar, QString flav); - - virtual QString convertorName() = 0; - - virtual bool canConvert(const QString &mime, QString flav) = 0; - virtual QString mimeFor(QString flav) = 0; - virtual QString flavorFor(const QString &mime) = 0; - virtual QVariant convertToMime(const QString &mime, QList<QByteArray> data, QString flav) = 0; - virtual QList<QByteArray> convertFromMime(const QString &mime, QVariant data, QString flav) = 0; - virtual int count(QMimeData *mimeData); -}; - -Q_GUI_EXPORT void qt_mac_addToGlobalMimeList(QMacInternalPasteboardMime *macMime); -Q_GUI_EXPORT void qt_mac_removeFromGlobalMimeList(QMacInternalPasteboardMime *macMime); -Q_GUI_EXPORT void qt_mac_registerDraggedTypes(const QStringList &types); -Q_GUI_EXPORT const QStringList& qt_mac_enabledDraggedTypes(); - -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/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/macos/qcocoanativeinterface.mm b/src/gui/platform/macos/qcocoanativeinterface.mm index a41f9b16da..cb6acb4496 100644 --- a/src/gui/platform/macos/qcocoanativeinterface.mm +++ b/src/gui/platform/macos/qcocoanativeinterface.mm @@ -1,7 +1,11 @@ // 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/qopenglcontext_p.h> +#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> diff --git a/src/gui/platform/unix/dbusmenu/qdbusmenuconnection.cpp b/src/gui/platform/unix/dbusmenu/qdbusmenuconnection.cpp index 9391b77f6a..1023b16662 100644 --- a/src/gui/platform/unix/dbusmenu/qdbusmenuconnection.cpp +++ b/src/gui/platform/unix/dbusmenu/qdbusmenuconnection.cpp @@ -40,14 +40,14 @@ QDBusMenuConnection::QDBusMenuConnection(QObject *parent, const QString &service , m_connection(serviceName.isNull() ? QDBusConnection::sessionBus() : QDBusConnection::connectToBus(QDBusConnection::SessionBus, serviceName)) , m_dbusWatcher(new QDBusServiceWatcher(StatusNotifierWatcherService, m_connection, QDBusServiceWatcher::WatchForRegistration, this)) - , m_statusNotifierHostRegistered(false) + , m_watcherRegistered(false) { #ifndef QT_NO_SYSTEMTRAYICON - QDBusInterface systrayHost(StatusNotifierWatcherService, StatusNotifierWatcherPath, StatusNotifierWatcherService, m_connection); - if (systrayHost.isValid() && systrayHost.property("IsStatusNotifierHostRegistered").toBool()) - m_statusNotifierHostRegistered = true; + // Start monitoring if any known tray-related services are registered. + if (m_connection.interface()->isServiceRegistered(StatusNotifierWatcherService)) + m_watcherRegistered = true; else - qCDebug(qLcMenu) << "StatusNotifierHost is not registered"; + qCDebug(qLcMenu) << "failed to find service" << StatusNotifierWatcherService; #endif } diff --git a/src/gui/platform/unix/dbusmenu/qdbusmenuconnection_p.h b/src/gui/platform/unix/dbusmenu/qdbusmenuconnection_p.h index 69713b12bd..37033e2fa3 100644 --- a/src/gui/platform/unix/dbusmenu/qdbusmenuconnection_p.h +++ b/src/gui/platform/unix/dbusmenu/qdbusmenuconnection_p.h @@ -39,7 +39,7 @@ public: ~QDBusMenuConnection(); QDBusConnection connection() const { return m_connection; } QDBusServiceWatcher *dbusWatcher() const { return m_dbusWatcher; } - bool isStatusNotifierHostRegistered() const { return m_statusNotifierHostRegistered; } + bool isWatcherRegistered() const { return m_watcherRegistered; } #ifndef QT_NO_SYSTEMTRAYICON bool registerTrayIconMenu(QDBusTrayIcon *item); void unregisterTrayIconMenu(QDBusTrayIcon *item); @@ -60,7 +60,7 @@ private: QString m_serviceName; QDBusConnection m_connection; QDBusServiceWatcher *m_dbusWatcher; - bool m_statusNotifierHostRegistered; + bool m_watcherRegistered; }; QT_END_NAMESPACE diff --git a/src/gui/platform/unix/dbusmenu/qdbusmenuregistrarproxy_p.h b/src/gui/platform/unix/dbusmenu/qdbusmenuregistrarproxy_p.h index 4568fd2aa4..8041f3af0a 100644 --- a/src/gui/platform/unix/dbusmenu/qdbusmenuregistrarproxy_p.h +++ b/src/gui/platform/unix/dbusmenu/qdbusmenuregistrarproxy_p.h @@ -63,7 +63,7 @@ public Q_SLOTS: // METHODS { QDBusMessage reply = call(QDBus::Block, QStringLiteral("GetMenuForWindow"), windowId); QList<QVariant> arguments = reply.arguments(); - if (reply.type() == QDBusMessage::ReplyMessage && arguments.count() == 2) + if (reply.type() == QDBusMessage::ReplyMessage && arguments.size() == 2) menuObjectPath = qdbus_cast<QDBusObjectPath>(arguments.at(1)); return reply; } diff --git a/src/gui/platform/unix/dbusmenu/qdbusmenutypes.cpp b/src/gui/platform/unix/dbusmenu/qdbusmenutypes.cpp index f70ce67d4e..b7fd035883 100644 --- a/src/gui/platform/unix/dbusmenu/qdbusmenutypes.cpp +++ b/src/gui/platform/unix/dbusmenu/qdbusmenutypes.cpp @@ -204,7 +204,7 @@ 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.length() - 1) + if (idx < 0 || idx == label.size() - 1) return label; QString ret(label); ret[idx] = u'_'; @@ -217,19 +217,19 @@ QDBusMenuShortcut QDBusMenuItem::convertKeySequence(const QKeySequence &sequence QDBusMenuShortcut shortcut; for (int i = 0; i < sequence.count(); ++i) { QStringList tokens; - int key = sequence[i].toCombined(); - if (key & Qt::MetaModifier) + auto modifiers = sequence[i].keyboardModifiers(); + if (modifiers & Qt::MetaModifier) tokens << QStringLiteral("Super"); - if (key & Qt::ControlModifier) + if (modifiers & Qt::ControlModifier) tokens << QStringLiteral("Control"); - if (key & Qt::AltModifier) + if (modifiers & Qt::AltModifier) tokens << QStringLiteral("Alt"); - if (key & Qt::ShiftModifier) + if (modifiers & Qt::ShiftModifier) tokens << QStringLiteral("Shift"); - if (key & Qt::KeypadModifier) + if (modifiers & Qt::KeypadModifier) tokens << QStringLiteral("Num"); - QString keyName = QKeySequencePrivate::keyName(key, QKeySequence::PortableText); + QString keyName = QKeySequencePrivate::keyName(sequence[i].key(), QKeySequence::PortableText); if (keyName == "+"_L1) tokens << QStringLiteral("plus"); else if (keyName == "-"_L1) @@ -271,7 +271,7 @@ 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.count() << " children)"; + d << "QDBusMenuLayoutItem(id=" << item.m_id << ", properties=" << item.m_properties << ", " << item.m_children.size() << " children)"; return d; } #endif diff --git a/src/gui/platform/unix/dbustray/qdbustrayicon.cpp b/src/gui/platform/unix/dbustray/qdbustrayicon.cpp index 18334e5715..0dff9b598e 100644 --- a/src/gui/platform/unix/dbustray/qdbustrayicon.cpp +++ b/src/gui/platform/unix/dbustray/qdbustrayicon.cpp @@ -198,7 +198,10 @@ QTemporaryFile *QDBusTrayIcon::tempIcon(const QIcon &icon) if (!necessary) return nullptr; QTemporaryFile *ret = new QTemporaryFile(tempFileTemplate(), this); - ret->open(); + if (!ret->open()) { + delete ret; + return nullptr; + } icon.pixmap(QSize(22, 22)).save(ret); ret->close(); return ret; @@ -331,8 +334,11 @@ void QDBusTrayIcon::notificationClosed(uint id, uint reason) bool QDBusTrayIcon::isSystemTrayAvailable() const { QDBusMenuConnection * conn = const_cast<QDBusTrayIcon *>(this)->dBusConnection(); - qCDebug(qLcTray) << conn->isStatusNotifierHostRegistered(); - return conn->isStatusNotifierHostRegistered(); + + // 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 diff --git a/src/gui/platform/unix/dbustray/qdbustraytypes.cpp b/src/gui/platform/unix/dbustray/qdbustraytypes.cpp index b2d87d7b8c..accbd87e7e 100644 --- a/src/gui/platform/unix/dbustray/qdbustraytypes.cpp +++ b/src/gui/platform/unix/dbustray/qdbustraytypes.cpp @@ -45,7 +45,7 @@ QXdgDBusImageVector iconToQXdgDBusImageVector(const QIcon &icon) bool hasSmallIcon = false; bool hasMediumIcon = false; QList<QSize> toRemove; - for (const QSize &size : qAsConst(sizes)) { + for (const QSize &size : std::as_const(sizes)) { int maxSize = qMax(size.width(), size.height()); if (maxSize <= IconNormalSmallSize) hasSmallIcon = true; @@ -54,7 +54,7 @@ QXdgDBusImageVector iconToQXdgDBusImageVector(const QIcon &icon) else if (maxSize > IconSizeLimit) toRemove << size; } - for (const QSize &size : qAsConst(toRemove)) + for (const QSize &size : std::as_const(toRemove)) sizes.removeOne(size); if (!hasSmallIcon) sizes.append(QSize(IconNormalSmallSize, IconNormalSmallSize)); @@ -62,7 +62,7 @@ QXdgDBusImageVector iconToQXdgDBusImageVector(const QIcon &icon) sizes.append(QSize(IconNormalMediumSize, IconNormalMediumSize)); ret.reserve(sizes.size()); - for (const QSize &size : qAsConst(sizes)) { + 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 diff --git a/src/gui/platform/unix/dbustray/qstatusnotifieritemadaptor.cpp b/src/gui/platform/unix/dbustray/qstatusnotifieritemadaptor.cpp index 27a14e408a..2f6c13b6cf 100644 --- a/src/gui/platform/unix/dbustray/qstatusnotifieritemadaptor.cpp +++ b/src/gui/platform/unix/dbustray/qstatusnotifieritemadaptor.cpp @@ -136,6 +136,12 @@ void QStatusNotifierItemAdaptor::ContextMenu(int x, int 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; diff --git a/src/gui/platform/unix/dbustray/qstatusnotifieritemadaptor_p.h b/src/gui/platform/unix/dbustray/qstatusnotifieritemadaptor_p.h index 286aafa9bb..103fc974dd 100644 --- a/src/gui/platform/unix/dbustray/qstatusnotifieritemadaptor_p.h +++ b/src/gui/platform/unix/dbustray/qstatusnotifieritemadaptor_p.h @@ -73,6 +73,9 @@ class QStatusNotifierItemAdaptor: public QDBusAbstractAdaptor " <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" @@ -150,6 +153,7 @@ public: // PROPERTIES 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 diff --git a/src/gui/platform/unix/dbustray/qxdgnotificationproxy_p.h b/src/gui/platform/unix/dbustray/qxdgnotificationproxy_p.h index fe61723bd2..dfbc64f33b 100644 --- a/src/gui/platform/unix/dbustray/qxdgnotificationproxy_p.h +++ b/src/gui/platform/unix/dbustray/qxdgnotificationproxy_p.h @@ -77,7 +77,7 @@ public Q_SLOTS: // METHODS inline QDBusReply<QString> getServerInformation(QString &vendor, QString &version, QString &specVersion) { QDBusMessage reply = call(QDBus::Block, QStringLiteral("GetServerInformation")); - if (reply.type() == QDBusMessage::ReplyMessage && reply.arguments().count() == 4) { + 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)); diff --git a/src/gui/platform/unix/qgenericunixservices.cpp b/src/gui/platform/unix/qgenericunixservices.cpp index a0e5466c58..bfd2556b1e 100644 --- a/src/gui/platform/unix/qgenericunixservices.cpp +++ b/src/gui/platform/unix/qgenericunixservices.cpp @@ -3,6 +3,11 @@ #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> @@ -126,8 +131,13 @@ static inline bool detectWebBrowser(const QByteArray &desktop, return false; } -static inline bool launch(const QString &launcher, const QUrl &url) +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)); @@ -143,16 +153,20 @@ static inline bool launch(const QString &launcher, const QUrl &url) #endif if (!ok) qWarning("Launch failed (%s)", qPrintable(command)); + + qunsetenv("XDG_ACTIVATION_TOKEN"); + return ok; } #if QT_CONFIG(dbus) static inline bool checkNeedPortalSupport() { - return !QStandardPaths::locate(QStandardPaths::RuntimeLocation, "flatpak-info"_L1).isEmpty() || qEnvironmentVariableIsSet("SNAP"); + return QFileInfo::exists("/.flatpak-info"_L1) || qEnvironmentVariableIsSet("SNAP"); } -static inline QDBusMessage xdgDesktopPortalOpenFile(const QUrl &url) +static inline QDBusMessage xdgDesktopPortalOpenFile(const QUrl &url, const QString &parentWindow, + const QString &xdgActivationToken) { // DBus signature: // OpenFile (IN s parent_window, @@ -163,8 +177,7 @@ static inline QDBusMessage xdgDesktopPortalOpenFile(const QUrl &url) // 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. -#ifdef O_PATH - const int fd = qt_safe_open(QFile::encodeName(url.toLocalFile()), O_PATH); + 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, @@ -174,21 +187,22 @@ static inline QDBusMessage xdgDesktopPortalOpenFile(const QUrl &url) QDBusUnixFileDescriptor descriptor; descriptor.giveFileDescriptor(fd); - const QVariantMap options = {{"writable"_L1, true}}; + QVariantMap options = {}; + + if (!xdgActivationToken.isEmpty()) { + options.insert("activation_token"_L1, xdgActivationToken); + } - // FIXME parent_window_id - message << QString() << QVariant::fromValue(descriptor) << options; + message << parentWindow << QVariant::fromValue(descriptor) << options; return QDBusConnection::sessionBus().call(message); } -#else - Q_UNUSED(url); -#endif return QDBusMessage::createError(QDBusError::InternalError, qt_error_string()); } -static inline QDBusMessage xdgDesktopPortalOpenUrl(const QUrl &url) +static inline QDBusMessage xdgDesktopPortalOpenUrl(const QUrl &url, const QString &parentWindow, + const QString &xdgActivationToken) { // DBus signature: // OpenURI (IN s parent_window, @@ -206,12 +220,19 @@ static inline QDBusMessage xdgDesktopPortalOpenUrl(const QUrl &url) "org.freedesktop.portal.OpenURI"_L1, "OpenURI"_L1); // FIXME parent_window_id and handle writable option - message << QString() << url.toString() << QVariantMap(); + 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) +static inline QDBusMessage xdgDesktopPortalSendEmail(const QUrl &url, const QString &parentWindow, + const QString &xdgActivationToken) { // DBus signature: // ComposeEmail (IN s parent_window, @@ -246,72 +267,274 @@ static inline QDBusMessage xdgDesktopPortalSendEmail(const QUrl &url) 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); - // FIXME parent_window_id - message << QString() << options; + 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) { - if (url.scheme() == "mailto"_L1) { -#if QT_CONFIG(dbus) + 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()) { - QDBusError error = xdgDesktopPortalSendEmail(url); + const QString parentWindow = QGuiApplication::focusWindow() + ? portalWindowIdentifier(QGuiApplication::focusWindow()) + : QString(); + QDBusError error = xdgDesktopPortalOpenUrl(url, parentWindow, xdgActivationToken); if (!error.isValid()) return true; + } +# endif - // service not running, fall back + if (m_webBrowser.isEmpty() + && !detectWebBrowser(desktopEnvironment(), true, &m_webBrowser)) { + qWarning("Unable to detect a web browser to launch '%s'", qPrintable(url.toString())); + return false; } -#endif - return openDocument(url); - } + return launch(m_webBrowser, url, xdgActivationToken); + }; -#if QT_CONFIG(dbus) - if (checkNeedPortalSupport()) { - QDBusError error = xdgDesktopPortalOpenUrl(url); - if (!error.isValid()) - return true; - } -#endif + if (QGuiApplication::platformName().startsWith("wayland"_L1)) { + runWithXdgActivationToken( + [openUrlInternal, url](const QString &token) { openUrlInternal(url, token); }); + + return true; - if (m_webBrowser.isEmpty() && !detectWebBrowser(desktopEnvironment(), true, &m_webBrowser)) { - qWarning("Unable to detect a web browser to launch '%s'", qPrintable(url.toString())); - return false; + } else { + return openUrlInternal(url, QString()); } - return launch(m_webBrowser, url); } bool QGenericUnixServices::openDocument(const QUrl &url) { -#if QT_CONFIG(dbus) - if (checkNeedPortalSupport()) { - QDBusError error = xdgDesktopPortalOpenFile(url); - if (!error.isValid()) - return true; - } -#endif + auto openDocumentInternal = [this](const QUrl &url, const QString &xdgActivationToken) { - if (m_documentLauncher.isEmpty() && !detectWebBrowser(desktopEnvironment(), false, &m_documentLauncher)) { - qWarning("Unable to detect a launcher for '%s'", qPrintable(url.toString())); - return false; +# 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()); } - return launch(m_documentLauncher, url); } #else +QGenericUnixServices::QGenericUnixServices() = default; + QByteArray QGenericUnixServices::desktopEnvironment() const { return QByteArrayLiteral("UNKNOWN"); @@ -331,6 +554,60 @@ bool QGenericUnixServices::openDocument(const QUrl &url) 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 index 8d1228375f..56e15103f7 100644 --- a/src/gui/platform/unix/qgenericunixservices_p.h +++ b/src/gui/platform/unix/qgenericunixservices_p.h @@ -21,19 +21,27 @@ QT_BEGIN_NAMESPACE +class QWindow; + class Q_GUI_EXPORT QGenericUnixServices : public QPlatformServices { public: - QGenericUnixServices() {} + 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 diff --git a/src/gui/platform/unix/qgenericunixthemes.cpp b/src/gui/platform/unix/qgenericunixthemes.cpp index d90d5de098..fc4b2296d2 100644 --- a/src/gui/platform/unix/qgenericunixthemes.cpp +++ b/src/gui/platform/unix/qgenericunixthemes.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2016 The Qt Company Ltd. +// 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" @@ -33,6 +33,12 @@ #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> @@ -41,6 +47,9 @@ #include <algorithm> QT_BEGIN_NAMESPACE +#ifndef QT_NO_DBUS +Q_LOGGING_CATEGORY(lcQpaThemeDBus, "qt.qpa.theme.dbus") +#endif using namespace Qt::StringLiterals; @@ -74,7 +83,7 @@ static bool isDBusTrayAvailable() { static bool dbusTrayAvailableKnown = false; if (!dbusTrayAvailableKnown) { QDBusMenuConnection conn; - if (conn.isStatusNotifierHostRegistered()) + if (conn.isWatcherRegistered()) dbusTrayAvailable = true; dbusTrayAvailableKnown = true; qCDebug(qLcTray) << "D-Bus tray available:" << dbusTrayAvailable; @@ -83,6 +92,20 @@ static bool isDBusTrayAvailable() { } #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() { @@ -98,7 +121,288 @@ 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 { @@ -198,6 +502,10 @@ QVariant QGenericUnixTheme::themeHint(ThemeHint hint) const 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; } @@ -231,11 +539,9 @@ static QIcon xdgFileIcon(const QFileInfo &fileInfo) #if QT_CONFIG(settings) class QKdeThemePrivate : public QPlatformThemePrivate { + public: - QKdeThemePrivate(const QStringList &kdeDirs, int kdeVersion) - : kdeDirs(kdeDirs) - , kdeVersion(kdeVersion) - { } + QKdeThemePrivate(const QStringList &kdeDirs, int kdeVersion); static QString kdeGlobals(const QString &kdeDir, int kdeVersion) { @@ -266,8 +572,66 @@ public: int startDragDist = 10; int startDragTime = 500; int cursorBlinkRate = 1000; + Qt::ColorScheme m_colorScheme = Qt::ColorScheme::Unknown; + void updateColorScheme(const QString &themeName); + +#ifndef QT_NO_DBUS +private: + 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 +} + void QKdeThemePrivate::refresh() { resources.clear(); @@ -297,6 +661,14 @@ void QKdeThemePrivate::refresh() styleNames.push_front(style); } + const QVariant colorScheme = readKdeSetting(QStringLiteral("ColorScheme"), kdeDirs, + kdeVersion, kdeSettings); + + if (colorScheme.isValid()) + updateColorScheme(colorScheme.toString()); + else + m_colorScheme = Qt::ColorScheme::Unknown; + const QVariant singleClickValue = readKdeSetting(QStringLiteral("KDE/SingleClick"), kdeDirs, kdeVersion, kdeSettings); if (singleClickValue.isValid()) singleClick = singleClickValue.toBool(); @@ -368,6 +740,8 @@ void QKdeThemePrivate::refresh() if (QFont *toolBarFont = kdeFont(readKdeSetting(QStringLiteral("toolBarFont"), kdeDirs, kdeVersion, kdeSettings))) resources.fonts[QPlatformTheme::ToolButtonFont] = toolBarFont; + QWindowSystemInterface::handleThemeChange(); + qCDebug(lcQpaFonts) << "default fonts: system" << resources.fonts[QPlatformTheme::SystemFont] << "fixed" << resources.fonts[QPlatformTheme::FixedFont]; qDeleteAll(kdeSettings); @@ -556,6 +930,10 @@ QVariant QKdeTheme::themeHint(QPlatformTheme::ThemeHint hint) const 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; } @@ -572,6 +950,48 @@ QIcon QKdeTheme::fileIcon(const QFileInfo &fileInfo, QPlatformTheme::IconOptions #endif } +Qt::ColorScheme QKdeTheme::colorScheme() const +{ + return d_func()->m_colorScheme; +} + +/*! + \internal + \brief QKdeTheme::updateColorScheme - guess and set appearance for unix themes. + KDE themes do not have an appearance property. + The key words "dark" or "light" should be part of the theme name. + This is, however, not a mandatory convention. + + If \param themeName contains a key word, the respective appearance is set. + If it doesn't, the appearance 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); @@ -672,8 +1092,8 @@ const char *QGnomeTheme::name = "gnome"; class QGnomeThemePrivate : public QPlatformThemePrivate { public: - QGnomeThemePrivate() : systemFont(nullptr), fixedFont(nullptr) {} - ~QGnomeThemePrivate() { delete systemFont; delete fixedFont; } + QGnomeThemePrivate(); + ~QGnomeThemePrivate(); void configureFonts(const QString >kFontName) const { @@ -688,10 +1108,70 @@ public: qCDebug(lcQpaFonts) << "default fonts: system" << systemFont << "fixed" << fixedFont; } - mutable QFont *systemFont; - mutable QFont *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()) { @@ -726,6 +1206,12 @@ QVariant QGnomeTheme::themeHint(QPlatformTheme::ThemeHint hint) const 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; } @@ -769,6 +1255,12 @@ QPlatformMenuBar *QGnomeTheme::createPlatformMenuBar() const 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) @@ -853,3 +1345,7 @@ QStringList QGenericUnixTheme::themeNames() } 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 index f5f60865aa..63b20651e6 100644 --- a/src/gui/platform/unix/qgenericunixthemes_p.h +++ b/src/gui/platform/unix/qgenericunixthemes_p.h @@ -77,6 +77,7 @@ public: 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 @@ -106,6 +107,7 @@ public: 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; diff --git a/src/gui/platform/unix/qtx11extras.cpp b/src/gui/platform/unix/qtx11extras.cpp index b21561fca4..ef37518d95 100644 --- a/src/gui/platform/unix/qtx11extras.cpp +++ b/src/gui/platform/unix/qtx11extras.cpp @@ -360,7 +360,7 @@ bool QX11Info::isCompositingManagerRunning(int screen) } /*! - Returns a new peeker id or -1 if some interal error has occurred. + 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. @@ -391,7 +391,7 @@ qint32 QX11Info::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 interal error has occurred. + provided or some internal error has occurred. \sa generatePeekerId() */ diff --git a/src/gui/platform/unix/qunixnativeinterface.cpp b/src/gui/platform/unix/qunixnativeinterface.cpp index 4ea88a590b..09561d9ada 100644 --- a/src/gui/platform/unix/qunixnativeinterface.cpp +++ b/src/gui/platform/unix/qunixnativeinterface.cpp @@ -3,7 +3,9 @@ #include <QtGui/private/qtguiglobal_p.h> -#include <QtGui/private/qopenglcontext_p.h> +#if QT_CONFIG(opengl) +# include <QtGui/private/qopenglcontext_p.h> +#endif #include <QtGui/private/qguiapplication_p.h> #include <qpa/qplatformopenglcontext.h> @@ -121,6 +123,22 @@ QOpenGLContext *QNativeInterface::QGLXContext::fromNative(GLXContext visualBased \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); @@ -213,4 +231,81 @@ 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 index b3ee0f4948..ed29db3005 100644 --- a/src/gui/platform/unix/qxkbcommon.cpp +++ b/src/gui/platform/unix/qxkbcommon.cpp @@ -17,8 +17,6 @@ QT_BEGIN_NAMESPACE -Q_LOGGING_CATEGORY(lcXkbcommon, "qt.xkbcommon") - static int keysymToQtKey_internal(xkb_keysym_t keysym, Qt::KeyboardModifiers modifiers, xkb_state *state, xkb_keycode_t code, bool superAsMeta, bool hyperAsMeta); @@ -239,10 +237,14 @@ static constexpr const auto KeyTbl = qMakeArray( 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. @@ -298,6 +300,7 @@ static constexpr const auto KeyTbl = qMakeArray( 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>, @@ -461,7 +464,7 @@ QList<xkb_keysym_t> QXkbCommon::toKeysym(QKeyEvent *event) // 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 : qAsConst(ucs4)) + for (uint utf32 : std::as_const(ucs4)) keysyms.append(utf32 | 0x01000000); return keysyms; @@ -512,13 +515,13 @@ static int keysymToQtKey_internal(xkb_keysym_t keysym, Qt::KeyboardModifiers mod // numeric keypad keys qtKey = Qt::Key_0 + (keysym - XKB_KEY_KP_0); } else if (QXkbCommon::isLatin1(keysym)) { - // Upper-case first, since Qt::Keys are defined in terms of their - // upper-case versions. + // 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); - // Upper-casing a Latin1 character might move it out of Latin1 range, - // for example U+00B5 MICRO SIGN, which upper-case equivalent is - // U+039C GREEK CAPITAL LETTER MU. If that's the case, then map the - // original lower-case character. + // 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 { @@ -564,7 +567,7 @@ static int keysymToQtKey_internal(xkb_keysym_t keysym, Qt::KeyboardModifiers mod return qtKey; } -Qt::KeyboardModifiers QXkbCommon::modifiers(struct xkb_state *state) +Qt::KeyboardModifiers QXkbCommon::modifiers(struct xkb_state *state, xkb_keysym_t keysym) { Qt::KeyboardModifiers modifiers = Qt::NoModifier; @@ -577,6 +580,9 @@ Qt::KeyboardModifiers QXkbCommon::modifiers(struct xkb_state *state) 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; } @@ -593,10 +599,24 @@ static const Qt::KeyboardModifiers ModsTbl[] = { 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; @@ -610,7 +630,7 @@ QList<int> QXkbCommon::possibleKeys(xkb_state *state, const QKeyEvent *event, ScopedXKBState scopedXkbQueryState(xkb_state_new(keymap)); xkb_state *queryState = scopedXkbQueryState.get(); if (!queryState) { - qCWarning(lcXkbcommon) << Q_FUNC_INFO << "failed to compile xkb keymap"; + qCWarning(lcQpaKeyMapper) << Q_FUNC_INFO << "failed to compile xkb keymap"; return result; } // get kb state from the master state and update the temporary state @@ -636,7 +656,7 @@ QList<int> QXkbCommon::possibleKeys(xkb_state *state, const QKeyEvent *event, int baseQtKey = keysymToQtKey_internal(sym, modifiers, queryState, keycode, superAsMeta, hyperAsMeta); if (baseQtKey) - result += (baseQtKey + int(modifiers)); + 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"); @@ -682,8 +702,9 @@ QList<int> QXkbCommon::possibleKeys(xkb_state *state, const QKeyEvent *event, // 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 (int shortcut : qAsConst(result)) { - if (int(shortcut & ~Qt::KeyboardModifierMask) == qtKey && (shortcut & mods) == mods) { + for (auto keyCombination : std::as_const(result)) { + if (keyCombination.key() == qtKey + && (keyCombination.keyboardModifiers() & mods) == mods) { ambiguous = true; break; } @@ -691,7 +712,7 @@ QList<int> QXkbCommon::possibleKeys(xkb_state *state, const QKeyEvent *event, if (ambiguous) continue; - result += (qtKey + int(mods)); + result += QKeyCombination::fromCombined(qtKey + int(mods)); } } @@ -723,13 +744,15 @@ void QXkbCommon::verifyHasLatinLayout(xkb_keymap *keymap) // 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(lcXkbcommon, "no keyboard layouts with latin keys present"); + 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); @@ -795,7 +818,7 @@ void QXkbCommon::setXkbContext(QPlatformInputContext *inputContext, struct xkb_c QMetaMethod method = inputContext->metaObject()->method(methodIndex); Q_ASSERT(method.isValid()); if (!method.isValid()) - qCWarning(lcXkbcommon) << normalizedSignature << "not found on" << inputContextClassName; + qCWarning(lcQpaKeyMapper) << normalizedSignature << "not found on" << inputContextClassName; return method; }(); diff --git a/src/gui/platform/unix/qxkbcommon_p.h b/src/gui/platform/unix/qxkbcommon_p.h index adc96b2ad4..a40d794451 100644 --- a/src/gui/platform/unix/qxkbcommon_p.h +++ b/src/gui/platform/unix/qxkbcommon_p.h @@ -23,12 +23,12 @@ #include <xkbcommon/xkbcommon.h> +#include <qpa/qplatformkeymapper.h> + #include <memory> QT_BEGIN_NAMESPACE -Q_DECLARE_LOGGING_CATEGORY(lcXkbcommon) - class QEvent; class QKeyEvent; class QPlatformInputContext; @@ -44,26 +44,67 @@ public: 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 = false, bool hyperAsMeta = false); + 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); + 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<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 <= 0xff; + return sym >= 0x20 && sym <= 0xff; } static bool isKeypad(xkb_keysym_t sym) { - return sym >= XKB_KEY_KP_Space && sym <= XKB_KEY_KP_9; + 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); 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 172c8f6814..a946cda043 100644 --- a/src/gui/platform/wasm/qwasmlocalfileaccess.cpp +++ b/src/gui/platform/wasm/qwasmlocalfileaccess.cpp @@ -2,19 +2,110 @@ // 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 showOpenViaHTMLPolyfill(const QStringList &accept, FileSelectMode fileSelectMode, + qstdweb::PromiseCallbacks onFilesSelected) +{ + // 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 + +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 showSave(const std::string &fileNameHint, qstdweb::PromiseCallbacks callbacks) +{ + 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)>>(); @@ -25,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()); @@ -35,7 +126,7 @@ void readFiles(const qstdweb::FileList &fileList, } // Read file data into caller-provided buffer - file.stream(buffer, [=]() { + file.stream(buffer, [readFile = readFile.get(), fileIndex, fileDataReady]() { fileDataReady(); (*readFile)(fileIndex + 1); }); @@ -44,53 +135,21 @@ 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)); - - // Note: there is no event in case the user cancels the file dialog. - static std::unique_ptr<qstdweb::EventCallback> changeEvent; - auto callback = [=](emscripten::val) { filesSelected(qstdweb::FileList(input["files"])); }; - changeEvent.reset(new 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); -} + // 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'; -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<void()> &fileDataReady) -{ - openFileDialog(accept, fileSelectMode, [=](const qstdweb::FileList &files) { - fileDialogClosed(files.length()); - readFiles(files, acceptFile, fileDataReady); - }); + return filter.split(sep); } - -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<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 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 @@ -98,7 +157,7 @@ void saveFile(const char *content, size_t size, const std::string &fileNameHint) // 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 = emscripten::val::global("window"); + 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); @@ -113,6 +172,118 @@ void saveFile(const char *content, size_t size, const std::string &fileNameHint) 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<void()> &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<void()> &fileDataReady) +{ + auto fileDialogClosedWithInt = [=](int fileCount) { fileDialogClosed(fileCount != 0); }; + openFiles(accept, FileSelectMode::SingleFile, fileDialogClosedWithInt, acceptFile, fileDataReady); +} + +void saveDataToFileInChunks(emscripten::val fileHandle, const QByteArray &data) +{ + using namespace emscripten; + using namespace qstdweb; + + Promise::make(fileHandle, QStringLiteral("createWritable"), { + .thenFunc = [=](val writable) { + struct State { + size_t written; + std::function<void(val result)> continuation; + }; + + static constexpr size_t desiredChunkSize = 1024u; +#if defined(__EMSCRIPTEN_SHARED_MEMORY__) + qstdweb::Uint8Array chunkArray(desiredChunkSize); +#endif + + 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; + } + + const auto currentChunkSize = std::min(remaining, desiredChunkSize); + +#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; + }; + + 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 QT_END_NAMESPACE diff --git a/src/gui/platform/wasm/qwasmlocalfileaccess_p.h b/src/gui/platform/wasm/qwasmlocalfileaccess_p.h index 3c7a874a43..77b14577f7 100644 --- a/src/gui/platform/wasm/qwasmlocalfileaccess_p.h +++ b/src/gui/platform/wasm/qwasmlocalfileaccess_p.h @@ -23,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/windows/qwindowsguieventdispatcher.cpp b/src/gui/platform/windows/qwindowsguieventdispatcher.cpp index f70655380d..c2f0efe96e 100644 --- a/src/gui/platform/windows/qwindowsguieventdispatcher.cpp +++ b/src/gui/platform/windows/qwindowsguieventdispatcher.cpp @@ -197,3 +197,5 @@ const char *QWindowsGuiEventDispatcher::windowsMessageName(UINT msg) } QT_END_NAMESPACE + +#include "moc_qwindowsguieventdispatcher_p.cpp" 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/qwindowsmime_p.h b/src/gui/platform/windows/qwindowsmimeconverter.h index a2fbd38119..145355fe15 100644 --- a/src/gui/platform/windows/qwindowsmime_p.h +++ b/src/gui/platform/windows/qwindowsmimeconverter.h @@ -1,36 +1,31 @@ -// Copyright (C) 2020 The Qt Company Ltd. +// 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 QWINDOWSMIME_P_H -#define QWINDOWSMIME_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/qt_windows.h> -#include <QtCore/qvariant.h> +#ifndef QWINDOWSMIMECONVERTER_P_H +#define QWINDOWSMIMECONVERTER_P_H #include <QtGui/qtguiglobal.h> -#include <QtCore/private/qglobal_p.h> + +struct tagFORMATETC; +using FORMATETC = tagFORMATETC; +struct tagSTGMEDIUM; +using STGMEDIUM = tagSTGMEDIUM; +struct IDataObject; QT_BEGIN_NAMESPACE +class QMetaType; class QMimeData; +class QVariant; -namespace QNativeInterface::Private { - -class Q_GUI_EXPORT QWindowsMime +class Q_GUI_EXPORT QWindowsMimeConverter { + Q_DISABLE_COPY(QWindowsMimeConverter) public: - virtual ~QWindowsMime() = default; + QWindowsMimeConverter(); + virtual ~QWindowsMimeConverter(); + + static int registerMimeType(const QString &mimeType); // for converting from Qt virtual bool canConvertFromMime(const FORMATETC &formatetc, const QMimeData *mimeData) const = 0; @@ -43,8 +38,6 @@ public: virtual QString mimeForFormat(const FORMATETC &formatetc) const = 0; }; -} // QNativeInterface::Private - QT_END_NAMESPACE -#endif // QWINDOWSMIME_P_H +#endif // QWINDOWSMIMECONVERTER_H diff --git a/src/gui/platform/windows/qwindowsnativeinterface.cpp b/src/gui/platform/windows/qwindowsnativeinterface.cpp index e4339c9aa9..44f230e1d3 100644 --- a/src/gui/platform/windows/qwindowsnativeinterface.cpp +++ b/src/gui/platform/windows/qwindowsnativeinterface.cpp @@ -3,11 +3,11 @@ #include <QtGui/qopenglcontext.h> #include <QtGui/private/qguiapplication_p.h> -#include <QtGui/private/qwindowsmime_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 @@ -89,6 +89,21 @@ QOpenGLContext *QNativeInterface::QWGLContext::fromNative(HGLRC context, HWND wi 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(). @@ -166,15 +181,7 @@ QT_DEFINE_PRIVATE_NATIVE_INTERFACE(QWindowsApplication); \value DarkModeStyle The Windows Vista style will be turned off and a simple dark style will be used. - \sa isDarkMode(), setDarkModeHandling() -*/ - -/*! - \fn bool QNativeInterface::Private::QWindowsApplication::isDarkMode() const = 0 - \internal - - Returns \c true if Windows 10 is configured to use dark mode for - applications. + \sa setDarkModeHandling() */ /*! @@ -208,21 +215,21 @@ QT_DEFINE_PRIVATE_NATIVE_INTERFACE(QWindowsApplication); */ /*! - \fn bool QNativeInterface::Private::QWindowsApplication::registerMime(QWindowsMime *mime) + \fn bool QNativeInterface::Private::QWindowsApplication::registerMime(QWindowsMimeConverter *mime) \internal Registers the converter \a mime to the system. - \sa QNativeInterface::Private::QWindowsMime, unregisterMime() + \sa QWindowsMimeConverter, unregisterMime() */ /*! - \fn void QNativeInterface::Private::QWindowsApplication::unregisterMime(QWindowsMime *mime) + \fn void QNativeInterface::Private::QWindowsApplication::unregisterMime(QWindowsMimeConverter *mime) \internal Unregisters the converter \a mime from the system. - \sa QNativeInterface::Private::QWindowsMime, registerMime() + \sa QWindowsMimeConverter, registerMime() */ /*! 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 |