From 5ae635548789098f8097647e23b91f2ea123b78f Mon Sep 17 00:00:00 2001 From: Volker Hilsheimer Date: Wed, 12 Jul 2023 11:05:43 +0200 Subject: [PATCH] Add initial implementation of macOS and iOS icon theme implementations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From macOS 13 on, AppKit provides an API to get a scalable system image from a symbolic icon name. We can map those icon names to the XDG-based icon names we support in Qt, and render the NSImage with palette-based coloring when needed, in an appropriate scale. On iOS, we can use the equivalent UIKit APIs. Coloring functionality is only available from iOS 15 on. Implement a QAppleIconEngine that does that in its scaledPixmap implementation. Use basic caching to store a single QPixmap version of the native vector image. We regenerate the pixmap whenever a different size, mode, or state is requested. Add a manual test for browsing all icons we can get from the various Qt APIs that: standard icons and pixmaps from QStyle, QPlatformTheme, and QIcon::fromTheme, in addition to showing all icon variations for a single QIcon. Task-number: QTBUG-102346 Change-Id: If5ab683ec18d140bd8700ac99b0edada980de9b4 Reviewed-by: Tor Arne Vestbø --- src/gui/CMakeLists.txt | 1 + src/gui/painting/qcoregraphics.mm | 4 + src/gui/platform/darwin/qappleiconengine.mm | 230 ++++++++ src/gui/platform/darwin/qappleiconengine_p.h | 64 +++ src/plugins/platforms/cocoa/qcocoatheme.h | 1 + src/plugins/platforms/cocoa/qcocoatheme.mm | 24 +- src/plugins/platforms/ios/qiostheme.h | 1 + src/plugins/platforms/ios/qiostheme.mm | 9 + tests/manual/iconbrowser/CMakeLists.txt | 15 + tests/manual/iconbrowser/main.cpp | 548 +++++++++++++++++++ 10 files changed, 884 insertions(+), 13 deletions(-) create mode 100644 src/gui/platform/darwin/qappleiconengine.mm create mode 100644 src/gui/platform/darwin/qappleiconengine_p.h create mode 100644 tests/manual/iconbrowser/CMakeLists.txt create mode 100644 tests/manual/iconbrowser/main.cpp diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 9f2d3b4882..1f1fbc65c8 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -385,6 +385,7 @@ qt_internal_extend_target(Gui CONDITION APPLE platform/darwin/qmacmimeregistry.mm platform/darwin/qmacmimeregistry_p.h platform/darwin/qutimimeconverter.mm platform/darwin/qutimimeconverter.h platform/darwin/qapplekeymapper.mm platform/darwin/qapplekeymapper_p.h + platform/darwin/qappleiconengine.mm platform/darwin/qappleiconengine_p.h text/coretext/qcoretextfontdatabase.mm text/coretext/qcoretextfontdatabase_p.h text/coretext/qfontengine_coretext.mm text/coretext/qfontengine_coretext_p.h LIBRARIES diff --git a/src/gui/painting/qcoregraphics.mm b/src/gui/painting/qcoregraphics.mm index e8904dff06..7b64106323 100644 --- a/src/gui/painting/qcoregraphics.mm +++ b/src/gui/painting/qcoregraphics.mm @@ -162,6 +162,9 @@ QT_BEGIN_NAMESPACE QPixmap qt_mac_toQPixmap(const NSImage *image, const QSizeF &size) { + // ### TODO: add parameter so that we can decide whether to maintain the aspect + // ratio of the image (positioning the image inside the pixmap of size \a size), + // or whether we want to fill the resulting pixmap by stretching the image. const NSSize pixmapSize = NSMakeSize(size.width(), size.height()); QPixmap pixmap(pixmapSize.width, pixmapSize.height); pixmap.fill(Qt::transparent); @@ -186,6 +189,7 @@ QPixmap qt_mac_toQPixmap(const NSImage *image, const QSizeF &size) QImage qt_mac_toQImage(const UIImage *image, QSizeF size) { + // ### TODO: same as above QImage ret(size.width(), size.height(), QImage::Format_ARGB32_Premultiplied); ret.fill(Qt::transparent); QMacCGContext ctx(&ret); diff --git a/src/gui/platform/darwin/qappleiconengine.mm b/src/gui/platform/darwin/qappleiconengine.mm new file mode 100644 index 0000000000..5dc9a6ed82 --- /dev/null +++ b/src/gui/platform/darwin/qappleiconengine.mm @@ -0,0 +1,230 @@ +// 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 +#elif defined (Q_OS_IOS) +# include +#endif + +#include +#include +#include +#include + +#include + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +namespace { +auto *loadImage(const QString &iconName) +{ + static constexpr std::pair iconMap[] = { + {u"address-book-new", @"folder.circle"}, + {u"application-exit", @"xmark.circle"}, + {u"appointment-new", @"hourglass.badge.plus"}, + {u"call-start", @"phone.arrow.up.right"}, + {u"call-stop", @"phone.arrow.down.left"}, + {u"edit-clear", @"clear"}, + {u"edit-copy", @"doc.on.doc"}, + {u"edit-cut", @"scissors"}, + {u"edit-delete", @"delete.left"}, + {u"edit-find", @"magnifyingglass"}, + {u"edit-find-replace", @"arrow.up.left.and.down.right.magnifyingglass"}, + {u"edit-paste", @"clipboard"}, + {u"edit-redo", @"arrowshape.turn.up.right"}, + {u"edit-select-all", @""}, + {u"edit-undo", @"arrowshape.turn.up.left"}, + }; + 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(Q_OS_IOS) + 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 QAppleIconEngine::availableIconSizes() +{ + const qreal devicePixelRatio = qGuiApp->devicePixelRatio(); + const QList sizes = { + {qRound(16 * devicePixelRatio), qRound(16 * devicePixelRatio)}, + {qRound(32 * devicePixelRatio), qRound(32 * devicePixelRatio)}, + {qRound(64 * devicePixelRatio), qRound(64 * devicePixelRatio)}, + {qRound(128 * devicePixelRatio), qRound(128 * devicePixelRatio)}, + {qRound(256 * devicePixelRatio), qRound(256 * devicePixelRatio)}, + }; + return sizes; +} + +QList QAppleIconEngine::availableSizes(QIcon::Mode, QIcon::State) +{ + return availableIconSizes(); +} + +QSize QAppleIconEngine::actualSize(const QSize &size, QIcon::Mode mode, QIcon::State state) +{ + return QIconEngine::actualSize(size, mode, state); +} + +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(Q_OS_IOS) +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 +} + +namespace { +template +QPixmap imageToPixmap(const Image *image, QSizeF renderSize) +{ + if constexpr (std::is_same_v) + return qt_mac_toQPixmap(image, renderSize.toSize()); + else + return QPixmap::fromImage(qt_mac_toQImage(image, renderSize.toSize())); +} +} + +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) { + 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); + + // the size we want is typically square, but the icon might not be. So + // ask for a pixmap with the same aspect ratio as the icon, and then + // center that within a pixmap of the requested size. + QSizeF renderSize = size * scale; + const bool aspectRatioAdjusted = image.size.width != image.size.height; + if (aspectRatioAdjusted) { + double aspectRatio = image.size.width / image.size.height; + // don't grow + if (aspectRatio < 1) + renderSize.rwidth() = renderSize.height() * aspectRatio; + else + renderSize.rheight() = renderSize.width() / aspectRatio; + } + + QPixmap iconPixmap = imageToPixmap(image, renderSize); + iconPixmap.setDevicePixelRatio(scale); + + if (aspectRatioAdjusted) { + m_pixmap = QPixmap(size * scale); + m_pixmap.fill(Qt::transparent); + m_pixmap.setDevicePixelRatio(scale); + + QPainter painter(&m_pixmap); + const QSize offset = ((m_pixmap.deviceIndependentSize() + - iconPixmap.deviceIndependentSize()) / 2).toSize(); + painter.drawPixmap(offset.width(), offset.height(), iconPixmap); + } else { + m_pixmap = iconPixmap; + } + m_cacheKey = cacheKey; + } + return m_pixmap; +} + +void QAppleIconEngine::paint(QPainter *painter, const QRect &rect, QIcon::Mode mode, QIcon::State state) +{ + const qreal scale = painter->device()->devicePixelRatio(); + // TODO: render the image directly if we don't have the pixmap yet and paint on an image + painter->drawPixmap(rect, scaledPixmap(rect.size(), mode, state, scale)); +} + +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..1735d2f501 --- /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 + +#include + +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 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 availableIconSizes(); + +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(Q_OS_IOS) + const UIImage *m_image; +#endif + mutable QPixmap m_pixmap; + mutable quint64 m_cacheKey = {}; +}; + + +QT_END_NAMESPACE + +#endif // QAPPLEICONENGINE_P_H diff --git a/src/plugins/platforms/cocoa/qcocoatheme.h b/src/plugins/platforms/cocoa/qcocoatheme.h index 90dc45264e..c49d83feae 100644 --- a/src/plugins/platforms/cocoa/qcocoatheme.h +++ b/src/plugins/platforms/cocoa/qcocoatheme.h @@ -35,6 +35,7 @@ public: const QFont *font(Font type = SystemFont) const override; QPixmap standardPixmap(StandardPixmap sp, const QSizeF &size) const override; QIcon fileIcon(const QFileInfo &fileInfo, QPlatformTheme::IconOptions options = {}) const override; + QIconEngine *createIconEngine(const QString &iconName) const override; QVariant themeHint(ThemeHint hint) const override; Qt::ColorScheme colorScheme() const override; diff --git a/src/plugins/platforms/cocoa/qcocoatheme.mm b/src/plugins/platforms/cocoa/qcocoatheme.mm index 56171b6dc2..f3a2d3886c 100644 --- a/src/plugins/platforms/cocoa/qcocoatheme.mm +++ b/src/plugins/platforms/cocoa/qcocoatheme.mm @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -409,19 +410,8 @@ public: QPlatformTheme::IconOptions opts) : QAbstractFileIconEngine(info, opts) {} - static QList availableIconSizes() - { - const qreal devicePixelRatio = qGuiApp->devicePixelRatio(); - const int sizes[] = { - qRound(16 * devicePixelRatio), qRound(32 * devicePixelRatio), - qRound(64 * devicePixelRatio), qRound(128 * devicePixelRatio), - qRound(256 * devicePixelRatio) - }; - return QAbstractFileIconEngine::toSizeList(sizes, sizes + sizeof(sizes) / sizeof(sizes[0])); - } - QList availableSizes(QIcon::Mode = QIcon::Normal, QIcon::State = QIcon::Off) override - { return QCocoaFileIconEngine::availableIconSizes(); } + { return QAppleIconEngine::availableIconSizes(); } protected: QPixmap filePixmap(const QSize &size, QIcon::Mode, QIcon::State) override @@ -440,6 +430,14 @@ QIcon QCocoaTheme::fileIcon(const QFileInfo &fileInfo, QPlatformTheme::IconOptio return QIcon(new QCocoaFileIconEngine(fileInfo, iconOptions)); } +QIconEngine *QCocoaTheme::createIconEngine(const QString &iconName) const +{ + static bool experimentalIconEngines = qEnvironmentVariableIsSet("QT_ENABLE_EXPERIMENTAL_ICON_ENGINES"); + if (experimentalIconEngines) + return new QAppleIconEngine(iconName); + return nullptr; +} + QVariant QCocoaTheme::themeHint(ThemeHint hint) const { switch (hint) { @@ -453,7 +451,7 @@ QVariant QCocoaTheme::themeHint(ThemeHint hint) const return QVariant([[NSApplication sharedApplication] isFullKeyboardAccessEnabled] ? int(Qt::TabFocusAllControls) : int(Qt::TabFocusTextControls | Qt::TabFocusListControls)); case IconPixmapSizes: - return QVariant::fromValue(QCocoaFileIconEngine::availableIconSizes()); + return QVariant::fromValue(QAppleIconEngine::availableIconSizes()); case QPlatformTheme::PasswordMaskCharacter: return QVariant(QChar(0x2022)); case QPlatformTheme::UiEffects: diff --git a/src/plugins/platforms/ios/qiostheme.h b/src/plugins/platforms/ios/qiostheme.h index 9aa2ebaf82..0f12ce099c 100644 --- a/src/plugins/platforms/ios/qiostheme.h +++ b/src/plugins/platforms/ios/qiostheme.h @@ -30,6 +30,7 @@ public: QPlatformDialogHelper *createPlatformDialogHelper(DialogType type) const override; const QFont *font(Font type = SystemFont) const override; + QIconEngine *createIconEngine(const QString &iconName) const override; static const char *name; diff --git a/src/plugins/platforms/ios/qiostheme.mm b/src/plugins/platforms/ios/qiostheme.mm index 4126ce8d1e..d8a4ef9ca4 100644 --- a/src/plugins/platforms/ios/qiostheme.mm +++ b/src/plugins/platforms/ios/qiostheme.mm @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -171,4 +172,12 @@ const QFont *QIOSTheme::font(Font type) const return coreTextFontDatabase->themeFont(type); } +QIconEngine *QIOSTheme::createIconEngine(const QString &iconName) const +{ + static bool experimentalIconEngines = qEnvironmentVariableIsSet("QT_ENABLE_EXPERIMENTAL_ICON_ENGINES"); + if (experimentalIconEngines) + return new QAppleIconEngine(iconName); + return nullptr; +} + QT_END_NAMESPACE diff --git a/tests/manual/iconbrowser/CMakeLists.txt b/tests/manual/iconbrowser/CMakeLists.txt new file mode 100644 index 0000000000..4bd22f7eff --- /dev/null +++ b/tests/manual/iconbrowser/CMakeLists.txt @@ -0,0 +1,15 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +find_package(Qt6 REQUIRED COMPONENTS Gui Widgets) + +qt_internal_add_manual_test(iconbrowser + GUI + SOURCES + main.cpp + LIBRARIES + Qt::Gui + Qt::GuiPrivate + Qt::Widgets + Qt::WidgetsPrivate +) diff --git a/tests/manual/iconbrowser/main.cpp b/tests/manual/iconbrowser/main.cpp new file mode 100644 index 0000000000..f145485329 --- /dev/null +++ b/tests/manual/iconbrowser/main.cpp @@ -0,0 +1,548 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include + +#include +#include + +using namespace Qt::StringLiterals; + +class IconModel : public QAbstractItemModel +{ + const QStringList themedIcons = { + u"address-book-new"_s, + u"application-exit"_s, + u"appointment-new"_s, + u"call-start"_s, + u"call-stop"_s, + u"contact-new"_s, + u"document-new"_s, + u"document-open"_s, + u"document-open-recent"_s, + u"document-page-setup"_s, + u"document-print"_s, + u"document-print-preview"_s, + u"document-properties"_s, + u"document-revert"_s, + u"document-save"_s, + u"document-save-as"_s, + u"document-send"_s, + u"edit-clear"_s, + u"edit-copy"_s, + u"edit-cut"_s, + u"edit-delete"_s, + u"edit-find"_s, + u"edit-find-replace"_s, + u"edit-paste"_s, + u"edit-redo"_s, + u"edit-select-all"_s, + u"edit-undo"_s, + u"folder-new"_s, + u"format-indent-less"_s, + u"format-indent-more"_s, + u"format-justify-center"_s, + u"format-justify-fill"_s, + u"format-justify-left"_s, + u"format-justify-right"_s, + u"format-text-direction-ltr"_s, + u"format-text-direction-rtl"_s, + u"format-text-bold"_s, + u"format-text-italic"_s, + u"format-text-underline"_s, + u"format-text-strikethrough"_s, + u"go-bottom"_s, + u"go-down"_s, + u"go-first"_s, + u"go-home"_s, + u"go-jump"_s, + u"go-last"_s, + u"go-next"_s, + u"go-previous"_s, + u"go-top"_s, + u"go-up"_s, + u"help-about"_s, + u"help-contents"_s, + u"help-faq"_s, + u"insert-image"_s, + u"insert-link"_s, + u"insert-object"_s, + u"insert-text"_s, + u"list-add"_s, + u"list-remove"_s, + u"mail-forward"_s, + u"mail-mark-important"_s, + u"mail-mark-junk"_s, + u"mail-mark-notjunk"_s, + u"mail-mark-read"_s, + u"mail-mark-unread"_s, + u"mail-message-new"_s, + u"mail-reply-all"_s, + u"mail-reply-sender"_s, + u"mail-send"_s, + u"mail-send-receive"_s, + u"media-eject"_s, + u"media-playback-pause"_s, + u"media-playback-start"_s, + u"media-playback-stop"_s, + u"media-record"_s, + u"media-seek-backward"_s, + u"media-seek-forward"_s, + u"media-skip-backward"_s, + u"media-skip-forward"_s, + u"object-flip-horizontal"_s, + u"object-flip-vertical"_s, + u"object-rotate-left"_s, + u"object-rotate-right"_s, + u"process-stop"_s, + u"system-lock-screen"_s, + u"system-log-out"_s, + u"system-run"_s, + u"system-search"_s, + u"system-reboot"_s, + u"system-shutdown"_s, + u"tools-check-spelling"_s, + u"view-fullscreen"_s, + u"view-refresh"_s, + u"view-restore"_s, + u"view-sort-ascending"_s, + u"view-sort-descending"_s, + u"window-close"_s, + u"window-new"_s, + u"zoom-fit-best"_s, + u"zoom-in"_s, + u"zoom-original"_s, + u"zoom-out"_s, + + + u"process-working"_s, + + + u"accessories-calculator"_s, + u"accessories-character-map"_s, + u"accessories-dictionary"_s, + u"accessories-text-editor"_s, + u"help-browser"_s, + u"multimedia-volume-control"_s, + u"preferences-desktop-accessibility"_s, + u"preferences-desktop-font"_s, + u"preferences-desktop-keyboard"_s, + u"preferences-desktop-locale"_s, + u"preferences-desktop-multimedia"_s, + u"preferences-desktop-screensaver"_s, + u"preferences-desktop-theme"_s, + u"preferences-desktop-wallpaper"_s, + u"system-file-manager"_s, + u"system-software-install"_s, + u"system-software-update"_s, + u"utilities-system-monitor"_s, + u"utilities-terminal"_s, + + + u"applications-accessories"_s, + u"applications-development"_s, + u"applications-engineering"_s, + u"applications-games"_s, + u"applications-graphics"_s, + u"applications-internet"_s, + u"applications-multimedia"_s, + u"applications-office"_s, + u"applications-other"_s, + u"applications-science"_s, + u"applications-system"_s, + u"applications-utilities"_s, + u"preferences-desktop"_s, + u"preferences-desktop-peripherals"_s, + u"preferences-desktop-personal"_s, + u"preferences-other"_s, + u"preferences-system"_s, + u"preferences-system-network"_s, + u"system-help"_s, + + + u"audio-card"_s, + u"audio-input-microphone"_s, + u"battery"_s, + u"camera-photo"_s, + u"camera-video"_s, + u"camera-web"_s, + u"computer"_s, + u"drive-harddisk"_s, + u"drive-optical"_s, + u"drive-removable-media"_s, + u"input-gaming"_s, + u"input-keyboard"_s, + u"input-mouse"_s, + u"input-tablet"_s, + u"media-flash"_s, + u"media-floppy"_s, + u"media-optical"_s, + u"media-tape"_s, + u"modem"_s, + u"multimedia-player"_s, + u"network-wired"_s, + u"network-wireless"_s, + u"pda"_s, + u"phone"_s, + u"printer"_s, + u"scanner"_s, + u"video-display"_s, + + + u"emblem-default"_s, + u"emblem-documents"_s, + u"emblem-downloads"_s, + u"emblem-favorite"_s, + u"emblem-important"_s, + u"emblem-mail"_s, + u"emblem-photos"_s, + u"emblem-readonly"_s, + u"emblem-shared"_s, + u"emblem-symbolic-link"_s, + u"emblem-synchronized"_s, + u"emblem-system"_s, + u"emblem-unreadable"_s, + + + u"face-angel"_s, + u"face-angry"_s, + u"face-cool"_s, + u"face-crying"_s, + u"face-devilish"_s, + u"face-embarrassed"_s, + u"face-kiss"_s, + u"face-laugh"_s, + u"face-monkey"_s, + u"face-plain"_s, + u"face-raspberry"_s, + u"face-sad"_s, + u"face-sick"_s, + u"face-smile"_s, + u"face-smile-big"_s, + u"face-smirk"_s, + u"face-surprise"_s, + u"face-tired"_s, + u"face-uncertain"_s, + u"face-wink"_s, + u"face-worried"_s, + + + u"flag-aa"_s, + + + u"application-x-executable"_s, + u"audio-x-generic"_s, + u"font-x-generic"_s, + u"image-x-generic"_s, + u"package-x-generic"_s, + u"text-html"_s, + u"text-x-generic"_s, + u"text-x-generic-template"_s, + u"text-x-script"_s, + u"video-x-generic"_s, + u"x-office-address-book"_s, + u"x-office-calendar"_s, + u"x-office-document"_s, + u"x-office-presentation"_s, + u"x-office-spreadsheet"_s, + + + u"folder"_s, + u"folder-remote"_s, + u"network-server"_s, + u"network-workgroup"_s, + u"start-here"_s, + u"user-bookmarks"_s, + u"user-desktop"_s, + u"user-home"_s, + u"user-trash"_s, + + + u"appointment-missed"_s, + u"appointment-soon"_s, + u"audio-volume-high"_s, + u"audio-volume-low"_s, + u"audio-volume-medium"_s, + u"audio-volume-muted"_s, + u"battery-caution"_s, + u"battery-low"_s, + u"dialog-error"_s, + u"dialog-information"_s, + u"dialog-password"_s, + u"dialog-question"_s, + u"dialog-warning"_s, + u"folder-drag-accept"_s, + u"folder-open"_s, + u"folder-visiting"_s, + u"image-loading"_s, + u"image-missing"_s, + u"mail-attachment"_s, + u"mail-unread"_s, + u"mail-read"_s, + u"mail-replied"_s, + u"mail-signed"_s, + u"mail-signed-verified"_s, + u"media-playlist-repeat"_s, + u"media-playlist-shuffle"_s, + u"network-error"_s, + u"network-idle"_s, + u"network-offline"_s, + u"network-receive"_s, + u"network-transmit"_s, + u"network-transmit-receive"_s, + u"printer-error"_s, + u"printer-printing"_s, + u"security-high"_s, + u"security-medium"_s, + u"security-low"_s, + u"software-update-available"_s, + u"software-update-urgent"_s, + u"sync-error"_s, + u"sync-synchronizing"_s, + u"task-due"_s, + u"task-past-due"_s, + u"user-available"_s, + u"user-away"_s, + u"user-idle"_s, + u"user-offline"_s, + u"user-trash-full"_s, + u"weather-clear"_s, + u"weather-clear-night"_s, + u"weather-few-clouds"_s, + u"weather-few-clouds-night"_s, + u"weather-fog"_s, + u"weather-overcast"_s, + u"weather-severe-alert"_s, + u"weather-showers"_s, + u"weather-showers-scattered"_s, + u"weather-snow"_s, + u"weather-storm"_s, + }; +public: + using QAbstractItemModel::QAbstractItemModel; + + enum Columns { + Name, + Style, + Theme, + Icon + }; + + int rowCount(const QModelIndex &parent) const override + { + if (parent.isValid()) + return 0; + return themedIcons.size() + QStyle::NStandardPixmap; + } + int columnCount(const QModelIndex &parent) const override + { + if (parent.isValid()) + return 0; + return Icon + 1; + } + QModelIndex index(int row, int column, const QModelIndex &parent) const override + { + if (parent.isValid()) + return {}; + if (column > columnCount(parent) || row > rowCount(parent)) + return {}; + return createIndex(row, column, quintptr(row)); + } + QModelIndex parent(const QModelIndex &) const override + { + return {}; + } + + QVariant data(const QModelIndex &index, int role) const override + { + int row = index.row(); + const Columns column = Columns(index.column()); + if (!index.isValid() || row >= rowCount(index.parent()) || column >= columnCount(index.parent())) + return {}; + const bool fromIcon = row < themedIcons.size(); + if (!fromIcon) + row -= themedIcons.size(); + switch (role) { + case Qt::DisplayRole: + if (fromIcon) { + return themedIcons.at(row); + } else { + const QMetaObject *styleMO = &QStyle::staticMetaObject; + const int pixmapIndex = styleMO->indexOfEnumerator("StandardPixmap"); + Q_ASSERT(pixmapIndex >= 0); + const QMetaEnum pixmapEnum = styleMO->enumerator(pixmapIndex); + const QString pixmapName = QString::fromUtf8(pixmapEnum.key(row)); + return QVariant(pixmapName); + } + break; + case Qt::DecorationRole: + switch (index.column()) { + case Name: + break; + case Style: + if (fromIcon) + break; + return QApplication::style()->standardIcon(QStyle::StandardPixmap(row)); + case Theme: + if (fromIcon) + break; + return QIcon(QApplicationPrivate::platformTheme()->standardPixmap(QPlatformTheme::StandardPixmap(row), {36, 36})); + case Icon: + if (fromIcon) + return QIcon::fromTheme(themedIcons.at(row)); + break; + } + break; + default: + break; + } + return {}; + } + QVariant headerData(int section, Qt::Orientation orientation, int role) const override + { + switch (orientation) { + case Qt::Vertical: + break; + case Qt::Horizontal: + if (role == Qt::DisplayRole) { + switch (section) { + case Name: + return "Name"; + case Style: + return "Style"; + case Theme: + return "Theme"; + case Icon: + return"Icon"; + } + } + } + return QAbstractItemModel::headerData(section, orientation, role); + } +}; + +template +struct ColumnModel : public QSortFilterProxyModel +{ + bool filterAcceptsColumn(int sourceColumn, const QModelIndex &) const override + { + return sourceColumn == Column; + } + + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override + { + const QModelIndex sourceIndex = sourceModel()->index(sourceRow, Column, sourceParent); + const QIcon iconData = sourceModel()->data(sourceIndex, Qt::DecorationRole).template value(); + return !iconData.isNull(); + } +}; + +template +struct IconView : public QListView +{ + ColumnModel proxyModel; + + IconView(QAbstractItemModel *model) + { + setViewMode(QListView::IconMode); + setUniformItemSizes(true); + proxyModel.setSourceModel(model); + setModel(&proxyModel); + } +}; + +class IconInspector : public QFrame +{ +public: + IconInspector() + { + setFrameShape(QFrame::StyledPanel); + + QLineEdit *lineEdit = new QLineEdit; + connect(lineEdit, &QLineEdit::textChanged, + this, &IconInspector::updateIcon); + + QVBoxLayout *vbox = new QVBoxLayout; + vbox->addStretch(10); + vbox->addWidget(lineEdit); + setLayout(vbox); + } + +protected: + void paintEvent(QPaintEvent *event) override + { + QPainter painter(this); + painter.fillRect(event->rect(), palette().window()); + if (!icon.isNull()) { + const QString modeLabels[] = { u"Normal"_s, u"Disabled"_s, u"Active"_s, u"Selected"_s}; + const QString stateLabels[] = { u"On"_s, u"Off"_s}; + const int labelWidth = fontMetrics().horizontalAdvance(u"Disabled"_s); + const int labelHeight = fontMetrics().height(); + int labelYs[4] = {}; + int labelXs[2] = {}; + + painter.save(); + painter.translate(labelWidth + contentsMargins().left(), labelHeight * 2); + const QBrush brush(palette().base().color(), Qt::CrossPattern); + + QPoint point; + for (const auto &mode : {QIcon::Normal, QIcon::Disabled, QIcon::Active, QIcon::Selected}) { + int height = 0; + for (const auto &state : {QIcon::On, QIcon::Off}) { + int totalWidth = 0; + const int relativeX = point.x(); + const auto sizes = icon.availableSizes(mode, state); + for (const auto &size : sizes) { + if (size.width() > 256) + continue; + const QRect iconRect(point, size); + painter.fillRect(iconRect, brush); + icon.paint(&painter, iconRect, Qt::AlignCenter, mode, state); + totalWidth += size.width(); + point.rx() += size.width(); + height = std::max(height, size.height()); + } + labelXs[state] = relativeX + totalWidth / 2; + } + point.rx() = 0; + labelYs[mode] = point.ry() + height / 2; + point.ry() += height; + } + painter.restore(); + + painter.translate(contentsMargins().left(), labelHeight); + for (const auto &mode : {QIcon::Normal, QIcon::Disabled, QIcon::Active, QIcon::Selected}) + painter.drawText(QPoint(0, labelYs[mode]), modeLabels[mode]); + painter.translate(labelWidth, 0); + for (const auto &state : {QIcon::On, QIcon::Off}) + painter.drawText(QPoint(labelXs[state], 0), stateLabels[state]); + } + QFrame::paintEvent(event); + } +private: + QIcon icon; + void updateIcon(const QString &iconName) + { + icon = QIcon::fromTheme(iconName); + update(); + } +}; + +int main(int argc, char* argv[]) +{ + qputenv("QT_ENABLE_EXPERIMENTAL_ICON_ENGINES", "1"); + + QApplication app(argc, argv); + + IconModel model; + + QTabWidget widget; + widget.setTabPosition(QTabWidget::West); + widget.addTab(new IconInspector, "Inspect"); + widget.addTab(new IconView(&model), "QIcon::fromTheme"); + widget.addTab(new IconView(&model), "QStyle"); + widget.addTab(new IconView(&model), "QPlatformTheme"); + + widget.show(); + return app.exec(); +}