diff --git a/cockatrice/src/interface/theme_icon.cpp b/cockatrice/src/interface/theme_icon.cpp index 038a2fba0..161c436ca 100644 --- a/cockatrice/src/interface/theme_icon.cpp +++ b/cockatrice/src/interface/theme_icon.cpp @@ -85,9 +85,9 @@ bool hasSentinel(const QByteArray &svg) return !svg.isEmpty() && QString::fromUtf8(svg).contains(QStringLiteral("currentColor"), Qt::CaseInsensitive); } -QString cacheKey(const QString &path, const QByteArray &color, const QSize &size, qreal devicePixelRatio) +QString cacheKey(const QString &path, const QByteArray &variant, const QSize &size, qreal devicePixelRatio) { - return path + QLatin1Char('|') + QString::fromLatin1(color) + QLatin1Char('|') + QString::number(size.width()) + + return path + QLatin1Char('|') + QString::fromLatin1(variant) + QLatin1Char('|') + QString::number(size.width()) + QLatin1Char('x') + QString::number(size.height()) + QLatin1Char('@') + QString::number(devicePixelRatio); } @@ -97,9 +97,11 @@ QString cacheKey(const QString &path, const QByteArray &color, const QSize &size * Qt's SVG renderer has no ambient color, so the sentinel is substituted from * the current palette on every pixmap request. Asset resolution, the file bytes * and the intrinsic size are cached until the theme changes: a repaint then - * does no file I/O at all. The render cache is keyed by resolved path, palette - * color, pixel size and DPR, so a palette change produces new keys instead of - * reusing art built under the previous palette. + * does no file I/O at all. The render cache is keyed by resolved path and pixel + * size plus a variant: the palette color for sentinel SVGs, the icon mode for + * assets that carry their own colors. A palette change therefore produces new + * keys instead of reusing art built under the previous palette, and the + * non-sentinel path (raster overrides, plain SVGs) is cached like any other. */ class ThemeSvgIconEngine final : public QObject, public QIconEngine { @@ -231,24 +233,26 @@ private: QPixmap render(const QSize &pixelSize, qreal devicePixelRatio, QIcon::Mode mode) const { ensureResolved(); - if (!resolvedHasSentinel) { - // Authored art carries its own colors: use Qt's normal loader so - // raster overrides and non-SVG assets keep working. - QPixmap pixmap = fromAsset(pixelSize, mode); - if (!pixmap.isNull()) { - pixmap.setDevicePixelRatio(devicePixelRatio); - } - return pixmap; - } - const QByteArray color = sentinelColor(mode, role); - const QString key = cacheKey(resolvedPath, color, pixelSize, devicePixelRatio); + // Same cache for both paths: sentinel art varies by palette color, art + // with its own colors varies by mode (Disabled is generated). A color + // variant is never a bare digit, so the two key spaces cannot collide. + const QByteArray variant = + resolvedHasSentinel ? sentinelColor(mode, role) : QByteArray::number(static_cast(mode)); + const QString key = cacheKey(resolvedPath, variant, pixelSize, devicePixelRatio); const auto cached = cache.constFind(key); if (cached != cache.constEnd()) { return cached.value(); } - QPixmap pixmap = renderSvg(resolveSvgColorSentinel(resolvedSvg, color), pixelSize, devicePixelRatio); + QPixmap pixmap; + if (resolvedHasSentinel) { + pixmap = renderSvg(resolveSvgColorSentinel(resolvedSvg, variant), pixelSize, devicePixelRatio); + } else { + // Authored art carries its own colors: use Qt's normal loader so + // raster overrides and non-SVG assets keep working. + pixmap = fromAsset(pixelSize, mode); + } if (!pixmap.isNull()) { // Set the ratio before caching: assigning it to a cached pixmap // afterwards detaches (deep-copies) it on every paint. diff --git a/cockatrice/src/interface/widgets/server/games_model.cpp b/cockatrice/src/interface/widgets/server/games_model.cpp index 217a62abc..95ea97bd2 100644 --- a/cockatrice/src/interface/widgets/server/games_model.cpp +++ b/cockatrice/src/interface/widgets/server/games_model.cpp @@ -86,7 +86,8 @@ const QString GamesModel::getGameCreatedString(const int secs) } GamesModel::GamesModel(const QMap &_rooms, const QMap &_gameTypes, QObject *parent) - : QAbstractTableModel(parent), rooms(_rooms), gameTypes(_gameTypes) + : QAbstractTableModel(parent), rooms(_rooms), gameTypes(_gameTypes), + lockIcon(loadThemedIcon(QStringLiteral("icons/lock"))) { } @@ -189,10 +190,9 @@ QVariant GamesModel::data(const QModelIndex &index, int role) const return result.join(", "); } case Qt::DecorationRole: { - // The themed icon follows the palette, so the lock also - // switches to HighlightedText on a selected row, and the - // item view paints it at the device pixel ratio. - return gameentry.with_password() ? loadThemedIcon(QStringLiteral("icons/lock")) : QVariant(); + // One engine per model: it is palette- and theme-aware and + // invalidates itself, so a repaint must not build a new one. + return gameentry.with_password() ? lockIcon : QVariant(); case Qt::TextAlignmentRole: return Qt::AlignLeft; default: diff --git a/cockatrice/src/interface/widgets/server/games_model.h b/cockatrice/src/interface/widgets/server/games_model.h index 9bf54b83a..62862417d 100644 --- a/cockatrice/src/interface/widgets/server/games_model.h +++ b/cockatrice/src/interface/widgets/server/games_model.h @@ -5,6 +5,7 @@ #include "game_type_map.h" #include +#include #include #include #include @@ -32,6 +33,7 @@ private: QList gameList; /**< List of games currently displayed. */ QMap rooms; /**< Map of room IDs to room names. */ QMap gameTypes; /**< Map of room IDs to available game types. */ + QIcon lockIcon; /**< Themed lock for password-protected games. */ static const int NUM_COLS = 8; /**< Number of columns in the table. */ diff --git a/libcockatrice_utility/libcockatrice/utility/svg_recolor.cpp b/libcockatrice_utility/libcockatrice/utility/svg_recolor.cpp index e25a31ba5..041e289fe 100644 --- a/libcockatrice_utility/libcockatrice/utility/svg_recolor.cpp +++ b/libcockatrice_utility/libcockatrice/utility/svg_recolor.cpp @@ -11,8 +11,11 @@ constexpr QByteArrayView SENTINEL = "currentColor"; /// its char overload cannot match a word. qsizetype findSentinel(const QByteArray &bytes, qsizetype from) { - for (qsizetype i = from; i + SENTINEL.size() <= bytes.size(); ++i) { - if (bytes.sliced(i, SENTINEL.size()).compare(SENTINEL, Qt::CaseInsensitive) == 0) { + // QByteArrayView::sliced() is a view, so the scan does not allocate a copy + // at every byte position (QByteArray::sliced() would). + const QByteArrayView view(bytes); + for (qsizetype i = from; i + SENTINEL.size() <= view.size(); ++i) { + if (view.sliced(i, SENTINEL.size()).compare(SENTINEL, Qt::CaseInsensitive) == 0) { return i; } }