Skip to content

Commit 9f522a0

Browse files
fix(tray): advertise IconName only when the theme resolves it; 0.1.2
StatusNotifier hosts prefer IconName over IconPixmap, so a run without the hicolor icons installed (a source build, the AppImage) rendered GNOME's "..." missing-icon placeholder in the top bar even though fallback pixmaps were shipped. The item now advertises the reverse-DNS name only when the theme lookup succeeds, and IconPixmap carries the brand mark pre-rasterised at the two published sizes and compiled into dish_core, replacing both the hand-drawn stand-in glyph and the runtime QIcon rasterisation (whose device-pixel-ratio scaling shipped 44px frames for the 22px slot). The tooltip carries the same icon fields. Ships as 0.1.2 (CMakeLists, changelog, metainfo entry). Follow-up: tag 0.1.2 on the merge commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent af813c8 commit 9f522a0

12 files changed

Lines changed: 209 additions & 83 deletions

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,19 @@ the repos share a version number.
1818

1919
## [Unreleased]
2020

21+
## [0.1.2] - 2026-08-24
22+
23+
### Fixed
24+
25+
- The tray item no longer advertises the `com.tinkernorth.Dish` icon name when
26+
the icon theme cannot resolve it. StatusNotifier hosts prefer `IconName` over
27+
`IconPixmap`, so a run without the hicolor icons installed (a source build,
28+
the AppImage) rendered GNOME's "..." missing-icon placeholder in the top bar
29+
even though pixmaps were shipped. The name is now advertised only when the
30+
theme lookup succeeds, `IconPixmap` carries the brand mark pre-rasterised at
31+
22 and 48 px and compiled into the client (replacing the hand-drawn
32+
stand-in glyph), and the tooltip carries the same icon fields.
33+
2134
## [0.1.1] - 2026-08-23
2235

2336
### Fixed

CMakeLists.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
cmake_minimum_required(VERSION 3.21)
22
project(Dish
3-
VERSION 0.1.1
3+
VERSION 0.1.2
44
DESCRIPTION "Dish Linux client for the Satellite gamepad-over-LAN protocol"
55
LANGUAGES CXX)
66

@@ -306,8 +306,13 @@ set(DISH_CORE_SOURCES
306306
src/source/system/SleepMonitor.cpp
307307
src/source/system/WakeInhibitor.cpp
308308
src/source/tray/TrayIcon.h
309+
src/source/tray/SniIcon.h
310+
src/source/tray/SniIcon.cpp
309311
src/source/tray/StatusNotifierTrayIcon.h
310312
src/source/tray/StatusNotifierTrayIcon.cpp
313+
# The tray's IconPixmap PNGs ride dish_core so DishTests and the exe both
314+
# carry them; SniIcon.cpp's Q_INIT_RESOURCE keeps the archive member alive.
315+
resources/tray.qrc
311316
# Update checking. In dish_core (not the exe) because the tests link this
312317
# library, not the exe.
313318
src/update/UpdatePorts.h

packaging/com.tinkernorth.Dish.metainfo.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@
102102
<!-- Kept in step with project(Dish VERSION ...); version-consistency.yml
103103
fails the PR that moves one without the other. -->
104104
<releases>
105+
<release version="0.1.2" date="2026-08-24" type="stable">
106+
<url type="details">https://github.com/TinkerNorth/dish-linux/releases/tag/0.1.2</url>
107+
<description>
108+
<p>Fixes the tray icon showing as a missing-icon placeholder when Dish's launcher icons are not installed in the icon theme, as with the AppImage or a source build: the tray now ships the brand mark inside the client and advertises a theme icon name only where the theme can resolve it.</p>
109+
</description>
110+
</release>
105111
<release version="0.1.1" date="2026-08-23" type="stable">
106112
<url type="details">https://github.com/TinkerNorth/dish-linux/releases/tag/0.1.1</url>
107113
<description>

resources/tray.qrc

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!--
2+
The StatusNotifier tray icon, pre-rasterised from packaging/dish.svg at the
3+
two sizes the item publishes over IconPixmap. Compiled into dish_core (not
4+
the exe) so the tray never depends on an installed icon theme, the Qt SVG
5+
plugin, or runtime painting; regenerate with Qt's QSvgRenderer if the brand
6+
mark changes.
7+
-->
8+
<RCC>
9+
<qresource prefix="/tray">
10+
<file alias="dish-22.png">tray/dish-22.png</file>
11+
<file alias="dish-48.png">tray/dish-48.png</file>
12+
</qresource>
13+
</RCC>

resources/tray/dish-22.png

1.31 KB
Loading

resources/tray/dish-48.png

3.69 KB
Loading

src/source/tray/SniIcon.cpp

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// SPDX-License-Identifier: LGPL-3.0-or-later
2+
// Copyright (C) 2026 Dish contributors.
3+
4+
#include "source/tray/SniIcon.h"
5+
6+
#include "Util/Endian.h"
7+
8+
#include <QLatin1String>
9+
#include <QStringLiteral>
10+
11+
#include <cstdint>
12+
13+
// Outside the namespace, as Q_INIT_RESOURCE requires. dish_core is a static
14+
// library, so nothing else forces the linker to keep the resource object.
15+
static void initTrayResource() {
16+
static const bool sInitialised = [] {
17+
Q_INIT_RESOURCE(tray);
18+
return true;
19+
}();
20+
static_cast<void>(sInitialised);
21+
}
22+
23+
namespace dish::source {
24+
25+
QString sniIconName(bool themeHasIcon) {
26+
return themeHasIcon ? QLatin1String(kSniIconName) : QLatin1String("");
27+
}
28+
29+
SniIconPixmapList sniTrayPixmaps() {
30+
initTrayResource();
31+
SniIconPixmapList pixmaps;
32+
for (const int size : {22, 48}) {
33+
const QImage image(QStringLiteral(":/tray/dish-%1.png").arg(size));
34+
if (image.isNull()) { continue; }
35+
pixmaps.append(toSniPixmap(image));
36+
}
37+
return pixmaps;
38+
}
39+
40+
SniIconPixmap toSniPixmap(const QImage& source) {
41+
const QImage image = source.convertToFormat(QImage::Format_ARGB32);
42+
SniIconPixmap pixmap;
43+
pixmap.width = image.width();
44+
pixmap.height = image.height();
45+
const qsizetype byteCount =
46+
static_cast<qsizetype>(pixmap.width) * static_cast<qsizetype>(pixmap.height) * 4;
47+
pixmap.data = QByteArray(byteCount, '\0');
48+
auto* out = reinterpret_cast<std::uint8_t*>(pixmap.data.data());
49+
qsizetype offset = 0;
50+
for (int y = 0; y < pixmap.height; ++y) {
51+
for (int x = 0; x < pixmap.width; ++x) {
52+
util::putU32Be(out + offset, static_cast<std::uint32_t>(image.pixel(x, y)));
53+
offset += 4;
54+
}
55+
}
56+
return pixmap;
57+
}
58+
59+
} // namespace dish::source

src/source/tray/SniIcon.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// SPDX-License-Identifier: LGPL-3.0-or-later
2+
// Copyright (C) 2026 Dish contributors.
3+
4+
#pragma once
5+
6+
#include "source/tray/StatusNotifierTrayIcon.h"
7+
8+
#include <QImage>
9+
#include <QString>
10+
11+
namespace dish::source {
12+
13+
inline constexpr auto kSniIconName = "com.tinkernorth.Dish";
14+
15+
QString sniIconName(bool themeHasIcon);
16+
SniIconPixmapList sniTrayPixmaps();
17+
SniIconPixmap toSniPixmap(const QImage& source);
18+
19+
} // namespace dish::source

src/source/tray/StatusNotifierTrayIcon.cpp

Lines changed: 9 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33

44
#include "source/tray/StatusNotifierTrayIcon.h"
55

6-
#include "Util/Endian.h"
6+
#include "source/tray/SniIcon.h"
77

8-
#include <QColor>
98
#include <QCoreApplication>
109
#include <QDBusAbstractAdaptor>
1110
#include <QDBusConnectionInterface>
@@ -18,17 +17,9 @@
1817
#include <QDBusVariant>
1918
#include <QGuiApplication>
2019
#include <QIcon>
21-
#include <QImage>
2220
#include <QLoggingCategory>
23-
#include <QPainter>
24-
#include <QPen>
25-
#include <QPointF>
26-
#include <QRectF>
27-
#include <QSize>
2821
#include <QStringLiteral>
2922

30-
#include <cstdint>
31-
3223
namespace dish::source {
3324

3425
namespace {
@@ -40,7 +31,6 @@ constexpr auto kWatcherPath = "/StatusNotifierWatcher";
4031
constexpr auto kItemInterface = "org.kde.StatusNotifierItem";
4132
constexpr auto kItemPath = "/StatusNotifierItem";
4233
constexpr auto kMenuPath = "/MenuBar";
43-
constexpr auto kIconName = "com.tinkernorth.Dish";
4434
constexpr auto kNewIconSignal = "NewIcon";
4535
constexpr auto kNewToolTipSignal = "NewToolTip";
4636

@@ -87,73 +77,6 @@ QVariantMap filterProperties(const QVariantMap& properties, const QStringList& n
8777
return filtered;
8878
}
8979

90-
// a(iiay) carries raw 32-bit ARGB in network byte order, so the source has to
91-
// be unpremultiplied ARGB32 and every pixel goes out through putU32Be —
92-
// casting the scan line to a uint32_t* would raise alignment and trip
93-
// -Wcast-align.
94-
SniIconPixmap toSniPixmap(const QImage& source) {
95-
const QImage image = source.convertToFormat(QImage::Format_ARGB32);
96-
SniIconPixmap pixmap;
97-
pixmap.width = image.width();
98-
pixmap.height = image.height();
99-
const qsizetype byteCount =
100-
static_cast<qsizetype>(pixmap.width) * static_cast<qsizetype>(pixmap.height) * 4;
101-
pixmap.data = QByteArray(byteCount, '\0');
102-
auto* out = reinterpret_cast<std::uint8_t*>(pixmap.data.data());
103-
qsizetype offset = 0;
104-
for (int y = 0; y < pixmap.height; ++y) {
105-
for (int x = 0; x < pixmap.width; ++x) {
106-
util::putU32Be(out + offset, static_cast<std::uint32_t>(image.pixel(x, y)));
107-
offset += 4;
108-
}
109-
}
110-
return pixmap;
111-
}
112-
113-
// :/icons/dish.svg belongs to the exe's resource bundle, not to dish_core, so
114-
// the fallback is drawn rather than loaded.
115-
QImage renderFallbackIcon(int size) {
116-
QImage image(size, size, QImage::Format_ARGB32);
117-
image.fill(Qt::transparent);
118-
const auto extent = static_cast<qreal>(size);
119-
const QPointF origin(extent * 0.24, extent * 0.78);
120-
const QColor ink(0xF5, 0xF6, 0xF8);
121-
122-
QPainter painter(&image);
123-
painter.setRenderHint(QPainter::Antialiasing, true);
124-
QPen pen(ink);
125-
pen.setWidthF(qMax(1.0, extent * 0.11));
126-
pen.setCapStyle(Qt::RoundCap);
127-
painter.setPen(pen);
128-
painter.setBrush(Qt::NoBrush);
129-
for (int ring = 1; ring <= 3; ++ring) {
130-
const qreal radius = extent * 0.18 * static_cast<qreal>(ring);
131-
painter.drawArc(
132-
QRectF(origin.x() - radius, origin.y() - radius, radius * 2.0, radius * 2.0), 5 * 16,
133-
80 * 16);
134-
}
135-
painter.setPen(Qt::NoPen);
136-
painter.setBrush(ink);
137-
painter.drawEllipse(origin, extent * 0.09, extent * 0.09);
138-
painter.end();
139-
return image;
140-
}
141-
142-
// Theme lookup misses entirely under Flatpak, Snap and AppImage, so IconPixmap
143-
// always ships alongside IconName instead of only when the theme fails.
144-
SniIconPixmapList buildIconPixmaps() {
145-
const QIcon themed = QIcon::fromTheme(QLatin1String(kIconName));
146-
SniIconPixmapList pixmaps;
147-
for (const int size : {22, 48}) {
148-
QImage image;
149-
if (!themed.isNull()) { image = themed.pixmap(QSize(size, size)).toImage(); }
150-
if (image.isNull()) { image = renderFallbackIcon(size); }
151-
if (image.isNull()) { continue; }
152-
pixmaps.append(toSniPixmap(image));
153-
}
154-
return pixmaps;
155-
}
156-
15780
} // namespace
15881

15982
// Returning the const reference parameter is the signature QtDBus REQUIRES of a
@@ -258,7 +181,7 @@ class StatusNotifierItemAdaptor final : public QDBusAbstractAdaptor {
258181
: QDBusAbstractAdaptor(host), owner_(owner) {}
259182

260183
QString category() const { return QStringLiteral("ApplicationStatus"); }
261-
QString id() const { return QLatin1String(kIconName); }
184+
QString id() const { return QLatin1String(kSniIconName); }
262185
QString title() const { return QStringLiteral("Dish"); }
263186
// Pinned Active: a Passive item may be hidden by the host, and a hidden
264187
// item is a running Dish the user can neither reach nor quit.
@@ -350,7 +273,7 @@ StatusNotifierTrayIcon::~StatusNotifierTrayIcon() { teardown(); }
350273

351274
bool StatusNotifierTrayIcon::isAvailable() const { return available_; }
352275

353-
QString StatusNotifierTrayIcon::iconName() const { return QLatin1String(kIconName); }
276+
QString StatusNotifierTrayIcon::iconName() const { return sniIconName(themeHasIcon_); }
354277

355278
SniIconPixmapList StatusNotifierTrayIcon::iconPixmap() const { return iconPixmap_; }
356279

@@ -360,7 +283,8 @@ uint StatusNotifierTrayIcon::menuRevision() const { return 1; }
360283

361284
SniToolTip StatusNotifierTrayIcon::toolTip() const {
362285
SniToolTip tip;
363-
tip.iconName = QLatin1String(kIconName);
286+
tip.iconName = sniIconName(themeHasIcon_);
287+
tip.iconPixmap = iconPixmap_;
364288
tip.title = QStringLiteral("Dish");
365289
switch (presentation_.activity) {
366290
case reducer::TrayActivity::Idle:
@@ -547,8 +471,11 @@ void StatusNotifierTrayIcon::refreshAvailability() {
547471
emit availabilityChanged(available_);
548472
}
549473

474+
// Theme lookup misses entirely under Flatpak, Snap and AppImage, so IconPixmap
475+
// always ships alongside IconName instead of only when the theme fails.
550476
void StatusNotifierTrayIcon::updateIcon() {
551-
SniIconPixmapList pixmaps = buildIconPixmaps();
477+
themeHasIcon_ = QIcon::hasThemeIcon(QLatin1String(kSniIconName));
478+
SniIconPixmapList pixmaps = sniTrayPixmaps();
552479
if (pixmaps == iconPixmap_) { return; }
553480
iconPixmap_ = pixmaps;
554481
if (exported_) { emitItemSignal(kNewIconSignal); }

src/source/tray/StatusNotifierTrayIcon.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ class StatusNotifierTrayIcon final : public TrayIcon {
119119
QObject* menuHost_ = nullptr;
120120
reducer::TrayPresentation presentation_;
121121
SniIconPixmapList iconPixmap_;
122+
bool themeHasIcon_ = false;
122123
bool shown_ = false;
123124
bool exported_ = false;
124125
bool registered_ = false;

0 commit comments

Comments
 (0)