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>
This commit is contained in:
Volker Hilsheimer 2023-07-12 11:05:43 +02:00
parent fdc2036640
commit 5ae6355487
10 changed files with 884 additions and 13 deletions

View File

@ -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

View File

@ -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);

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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:

View File

@ -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;

View File

@ -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

View File

@ -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
)

View File

@ -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();
}