summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorVolker Hilsheimer <volker.hilsheimer@qt.io>2023-07-12 11:05:43 +0200
committerVolker Hilsheimer <volker.hilsheimer@qt.io>2023-11-23 00:52:12 +0100
commit5ae635548789098f8097647e23b91f2ea123b78f (patch)
tree2e2d3742ab8081ab2f84751b5afd64aac668954a
parentfdc2036640ff7501b17b13c504743786b1f8bf6e (diff)
Add initial implementation of macOS and iOS icon theme implementations
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ø <tor.arne.vestbo@qt.io>
-rw-r--r--src/gui/CMakeLists.txt1
-rw-r--r--src/gui/painting/qcoregraphics.mm4
-rw-r--r--src/gui/platform/darwin/qappleiconengine.mm230
-rw-r--r--src/gui/platform/darwin/qappleiconengine_p.h64
-rw-r--r--src/plugins/platforms/cocoa/qcocoatheme.h1
-rw-r--r--src/plugins/platforms/cocoa/qcocoatheme.mm24
-rw-r--r--src/plugins/platforms/ios/qiostheme.h1
-rw-r--r--src/plugins/platforms/ios/qiostheme.mm9
-rw-r--r--tests/manual/iconbrowser/CMakeLists.txt15
-rw-r--r--tests/manual/iconbrowser/main.cpp548
10 files changed, 884 insertions, 13 deletions
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 <AppKit/AppKit.h>
+#elif defined (Q_OS_IOS)
+# 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<QStringView, NSString *> 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<QSize> QAppleIconEngine::availableIconSizes()
+{
+ const qreal devicePixelRatio = qGuiApp->devicePixelRatio();
+ const QList<QSize> 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<QSize> 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 <typename Image>
+QPixmap imageToPixmap(const Image *image, QSizeF renderSize)
+{
+ if constexpr (std::is_same_v<Image, NSImage>)
+ 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 <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();
+
+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 <QtGui/qpainter.h>
#include <QtGui/qtextformat.h>
#include <QtGui/private/qcoretextfontdatabase_p.h>
+#include <QtGui/private/qappleiconengine_p.h>
#include <QtGui/private/qfontengine_coretext_p.h>
#include <QtGui/private/qabstractfileiconengine_p.h>
#include <qpa/qplatformdialoghelper.h>
@@ -409,19 +410,8 @@ public:
QPlatformTheme::IconOptions opts) :
QAbstractFileIconEngine(info, opts) {}
- static QList<QSize> 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<QSize> 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 <QtGui/private/qcoregraphics_p.h>
#include <QtGui/private/qcoretextfontdatabase_p.h>
+#include <QtGui/private/qappleiconengine_p.h>
#include <QtGui/private/qguiapplication_p.h>
#include <qpa/qplatformintegration.h>
@@ -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 <QtWidgets>
+
+#include <QtWidgets/private/qapplication_p.h>
+#include <QtGui/qpa/qplatformtheme.h>
+
+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<IconModel::Columns Column>
+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<QIcon>();
+ return !iconData.isNull();
+ }
+};
+
+template<IconModel::Columns Column>
+struct IconView : public QListView
+{
+ ColumnModel<Column> 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<IconModel::Icon>(&model), "QIcon::fromTheme");
+ widget.addTab(new IconView<IconModel::Style>(&model), "QStyle");
+ widget.addTab(new IconView<IconModel::Theme>(&model), "QPlatformTheme");
+
+ widget.show();
+ return app.exec();
+}