From 9f522a0a39331fbfae9c926cd592d8fe87dc8bea Mon Sep 17 00:00:00 2001 From: Emir Hasanbegovic Date: Mon, 24 Aug 2026 09:32:29 -0400 Subject: [PATCH] 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 --- CHANGELOG.md | 13 +++ CMakeLists.txt | 7 +- packaging/com.tinkernorth.Dish.metainfo.xml | 6 ++ resources/tray.qrc | 13 +++ resources/tray/dish-22.png | Bin 0 -> 1343 bytes resources/tray/dish-48.png | Bin 0 -> 3776 bytes src/source/tray/SniIcon.cpp | 59 +++++++++++++ src/source/tray/SniIcon.h | 19 ++++ src/source/tray/StatusNotifierTrayIcon.cpp | 91 ++------------------ src/source/tray/StatusNotifierTrayIcon.h | 1 + tests/CMakeLists.txt | 1 + tests/test_tray_icon_fallback.cpp | 82 ++++++++++++++++++ 12 files changed, 209 insertions(+), 83 deletions(-) create mode 100644 resources/tray.qrc create mode 100644 resources/tray/dish-22.png create mode 100644 resources/tray/dish-48.png create mode 100644 src/source/tray/SniIcon.cpp create mode 100644 src/source/tray/SniIcon.h create mode 100644 tests/test_tray_icon_fallback.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index ae8b6eb..daf36ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,19 @@ the repos share a version number. ## [Unreleased] +## [0.1.2] - 2026-08-24 + +### Fixed + +- The tray item no longer advertises the `com.tinkernorth.Dish` icon name when + the icon theme cannot resolve it. 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 pixmaps were shipped. The name is now advertised only when the + theme lookup succeeds, `IconPixmap` carries the brand mark pre-rasterised at + 22 and 48 px and compiled into the client (replacing the hand-drawn + stand-in glyph), and the tooltip carries the same icon fields. + ## [0.1.1] - 2026-08-23 ### Fixed diff --git a/CMakeLists.txt b/CMakeLists.txt index 94ac22d..5283bc9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.21) project(Dish - VERSION 0.1.1 + VERSION 0.1.2 DESCRIPTION "Dish Linux client for the Satellite gamepad-over-LAN protocol" LANGUAGES CXX) @@ -306,8 +306,13 @@ set(DISH_CORE_SOURCES src/source/system/SleepMonitor.cpp src/source/system/WakeInhibitor.cpp src/source/tray/TrayIcon.h + src/source/tray/SniIcon.h + src/source/tray/SniIcon.cpp src/source/tray/StatusNotifierTrayIcon.h src/source/tray/StatusNotifierTrayIcon.cpp + # The tray's IconPixmap PNGs ride dish_core so DishTests and the exe both + # carry them; SniIcon.cpp's Q_INIT_RESOURCE keeps the archive member alive. + resources/tray.qrc # Update checking. In dish_core (not the exe) because the tests link this # library, not the exe. src/update/UpdatePorts.h diff --git a/packaging/com.tinkernorth.Dish.metainfo.xml b/packaging/com.tinkernorth.Dish.metainfo.xml index 9dbb3dc..9d95179 100644 --- a/packaging/com.tinkernorth.Dish.metainfo.xml +++ b/packaging/com.tinkernorth.Dish.metainfo.xml @@ -102,6 +102,12 @@ + + https://github.com/TinkerNorth/dish-linux/releases/tag/0.1.2 + +

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.

+
+
https://github.com/TinkerNorth/dish-linux/releases/tag/0.1.1 diff --git a/resources/tray.qrc b/resources/tray.qrc new file mode 100644 index 0000000..c2574eb --- /dev/null +++ b/resources/tray.qrc @@ -0,0 +1,13 @@ + + + + tray/dish-22.png + tray/dish-48.png + + diff --git a/resources/tray/dish-22.png b/resources/tray/dish-22.png new file mode 100644 index 0000000000000000000000000000000000000000..38c205ca45df05874f0b56a55e6d502870751f16 GIT binary patch literal 1343 zcmV-F1;F}=P)FsQO z;eXY+;xI_e?aV}t z14$#Z!^2n1(+(iVxP!w{6dtY3rq|-)e?YJeb7wt%lYID#zOFUzJAl&hY7X>e!)ies z2D`dir*JkD*D^;W?sA@Iy{y~Ix}E1)dA84;Pd@LRa=o3`ya#qtiALq!Kb`#TmuURx z@TtqwC^rN4FwTbX{?DNP!l{;0wY~aT^IX~6)$(HL#Cum0x-TJH7np+B2X9&E-?hYA zgOziG>ABo@6PMYysBzMiTML}eM2!Qa{x_ih-p3mb>?{Zw`XPZ7HNcU9oM(fFLrEjE17ogms-*-ldF}gG z1r|14k(LhMChL=n;c;63A?; zN8fkisqSt|izAhrE{}qtwpIDn0JCYz>FGxDuUh<*>5a`l-emJ!;=1F z*sTac;Iw1|4|FyE3ZOJndB&r$fw9V_E3?@@d}eh%GnaU#&eu6J7aysMzCE2S;|mg| zvXjoM{eiCLR{$%HRPQy@_Hg-+0Z1A)lZ|v=Elo3HAD){ym--=+#?#Z$?A!h8*EjxK z&GHBlt!8?nRIWS$`nHVmY=%v{H@58Qf0YM+bh9M@kGxskyt1C{0*03Y0@HQgF!E=d@{e zN9*5UiC5TMeFC^Fu4nHda9;=OA1GJ$N^<5oD6j7!J(M~-D9K%b{t)MSAoC5N=>mgA zaPe3rpGLVEgK{YUcyC8x1n0JRo-vlU*Ox%ERBiNlzoRx76 z$YXY*Fy7qL-EHY58h3YFS{QHc0e1@6AhXv2M>hLTkh&9#cVYIIjCXGv1UOz}epHPw zg)U05G$h&nvFcAHZZ|ZHxeslAH0VOX{91fcXtNnZyE~d6GP^iE=0C`?Ff$cLD~}n; zlLA~2tno>5BhH-zkoPNG7{1|?#?-v#VZ0m)@E?XTaKeO6NW}mE002ovPDHLkV1hR< Bodo~@ literal 0 HcmV?d00001 diff --git a/resources/tray/dish-48.png b/resources/tray/dish-48.png new file mode 100644 index 0000000000000000000000000000000000000000..318d4bdcbbbf49a81c209c16df8c8ec8695743e1 GIT binary patch literal 3776 zcmV;x4nOgUP)MPmb| zZsZLW2zgZ9d-wcNl~9CAf<4S$D{Gy5?ml~e-#MrD*=OGfXGzLc4PTu#T~$Z)rY9o% z;z9rWi$&bC;n#CEi~#uRtlNN>MWj#N!a8dX@XFcnYn%-u0ID+J%$>@#v*q`!5x~&e z&ZZj1EU*V<{)y^$PAa==&z9RW+Cr)-m36WERpeHIW#M4pjnkA``GkBNz{GVg2p6CK zADOQsQR@Ow0Ox4j`6Qd46?N7OLv^(vTjvLhejp;+7q&iEAO*hZry$EkxeVp?mD^*3 zJKvXAt7m0><5iN+62Jry;j+^ViI${WW}(lsWMzHhRpr&%HGxllWDPNtSAp@0-m+rb z-XVQqgFaT^BzS!&v)@Rvy1ZJy@AFxAfK`qw{%!*gXEAmdQa^Bkg}AcN=d-LxG`2V% z_peRnSgV2IX||gel#V@j3?2QLs+*a_U$!;mj7UDIDZ6pjlkt28v>8UF8X?+;X2PDGD1Y}3SWKsPV%StOy#GwpbmJXmt>!BhO# z#;!-G0^L*K9Jry?95G(eap9o<)m3%Y>k0?M#eutfArRXRj8DIOC24r?S~zLMiSByo z)L{YeD9|s%VAO?oM2UgyP4()zDg+F;R-yQ$t}Y7|k9sH)wfMjHuaS?O`}{xzh{VEjOzMO$*7Hy+StD#{Ij8Q{rnRD ze*;w3HcnTOx6F=qznhys6VGhifHt#Es~>qOk!Jb3Yh z35hiLcY(=i1J#OsXXd^Hh(ve$!o}yudn3~+sw%6Z)*~u?YXXF8OxIKBD&LQfFX~z~L**yxO2aR9 z9vtw%B}0z`;BlW3`{coybp_J1zZ>w2@ZtPP{wr5)-#tui?_Ys_0@eA>{+XFG52>vX zskbV`#SQ1WST4bVmhl_EU*xW$K(E?L-M~Y zaB}kw0y!eGA{6v5$Y!kalU>8r_O1ZtsttV%a??7aNz?k+7&ipbmPzD1JHaZx@J(Wiiy2Su$LjVyOqv$LV9 z8&fS~Hlz-z?s2|i`}Ukj)LJ7d&kxJ@7M2bhz_|q}uK8Yr6Sk9{b!F$#Rz5q@itVzo z!%18bnpE_8rmeK?IsgN11W?!Am<=0%iwr#_3aMet5?4nfS|X~4;?05A(nLyA19&G> zzp8fcP$y}>W(w0s7ZxzQzftI&A71FNM2M|H1?gu;`G*7tH*c?Srp>SX}& z=D>QNFE$yGA*yV{Fm_27DK~x!0dW%_biYZds;sKF{Hl;j57OWp8gkte_C}A$lnF!n zF~EDuM}xm>PgQ9Fx5x^ZLY$OBPZB-w3S#($KIBt^Q;=itOCavQC1vSL@P&DZjR-D zBSf0=0knn3`;Ydee(G;P&FL%atkI~Jh{}PYFN+q5h!cs%EZ{;F0s!+$NBuQYvuCs# z-tl6}5!cXz$0m$yI<4B4FP(dL*%{+c`v5Ev(ei5b4D!VuS0oG)7Ik-NM*eVW6cTw; zR6E&VjcGazEEAC$RlU%FYddwrrGK{<*vr|MYXL-L8-TAcc5@0mfJU@i<csVS`ki)rbfE?<>p^k$VK49FN=m z6_%$N%q*`~kI~+-2}l6o(RpVAX5Bh94bf7;y?os|HTxa`YUuoQGvR4(d;95{8n3Ee zm+pH!9=H1oI$1>SQJ51C`rprfp{Z}YN?ds#Kt*Sk1xNj1Wo>M=2=}Fk*cW%dz;u^% z>ir7!1CVoSJ&@3-*(NNKn}v{a4;~hvaWHaYATfwajyym&--)taap5K^O3+7 zz)C5|VpSExn_LQ<0CK?d!qYW1-fUw5D!0WZ0zU=55NJ?Xy7J@Lr9J6CT~p(=UGw>r zZCY&cE>%@gmBnclYvdQBwqzTdl|Z__vlu{?DUo_>cKQ;%B&~9Ft2ts-wy{+W4Y_LY zYamCJyOdP5LL7cwv3+~iszsY;SQT(JfWVT4lSUk4^M{eBwWj+@I4!y5z(he_C@Ts4DO?kqsVCDAMV4u%=sqUEla14 znetzPTm^I{a&3x!u%+R{8?GEP^%0;P_&V2gCnOxXP~k<8u`2T2vY>w*bQRa$Ia!L7 z$3nrPN4l@n|E0X%0rn-}Sw`xc!53!#_KEYxjOpmKk3t$`G{-&ACJrnRf66lFUw+|evM0AU){5JhO!#2EsF`V}6jy0e6X0{I; z0}H?UPX7@(n_3RIR~V}A0;tG$L8pl_FYV_QVCsDw61n0HXSo*v`)teIf1V z_A>?FD}ihzQhn?zaE%|^BF_PXfy0g?3!nVV8Uk`}TIuu;bX|RN?~|WdLmbCk02~Ge zi)lXB&oS=<*`G0>Pw;&bIF*FX6)~zh9Os%79{nd$-?>oa{spB)hYs#5YDRbq*dWNd z?sn8M)&m<5-a5Fip?N`R(IL +#include + +#include + +// Outside the namespace, as Q_INIT_RESOURCE requires. dish_core is a static +// library, so nothing else forces the linker to keep the resource object. +static void initTrayResource() { + static const bool sInitialised = [] { + Q_INIT_RESOURCE(tray); + return true; + }(); + static_cast(sInitialised); +} + +namespace dish::source { + +QString sniIconName(bool themeHasIcon) { + return themeHasIcon ? QLatin1String(kSniIconName) : QLatin1String(""); +} + +SniIconPixmapList sniTrayPixmaps() { + initTrayResource(); + SniIconPixmapList pixmaps; + for (const int size : {22, 48}) { + const QImage image(QStringLiteral(":/tray/dish-%1.png").arg(size)); + if (image.isNull()) { continue; } + pixmaps.append(toSniPixmap(image)); + } + return pixmaps; +} + +SniIconPixmap toSniPixmap(const QImage& source) { + const QImage image = source.convertToFormat(QImage::Format_ARGB32); + SniIconPixmap pixmap; + pixmap.width = image.width(); + pixmap.height = image.height(); + const qsizetype byteCount = + static_cast(pixmap.width) * static_cast(pixmap.height) * 4; + pixmap.data = QByteArray(byteCount, '\0'); + auto* out = reinterpret_cast(pixmap.data.data()); + qsizetype offset = 0; + for (int y = 0; y < pixmap.height; ++y) { + for (int x = 0; x < pixmap.width; ++x) { + util::putU32Be(out + offset, static_cast(image.pixel(x, y))); + offset += 4; + } + } + return pixmap; +} + +} // namespace dish::source diff --git a/src/source/tray/SniIcon.h b/src/source/tray/SniIcon.h new file mode 100644 index 0000000..6e5cfba --- /dev/null +++ b/src/source/tray/SniIcon.h @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright (C) 2026 Dish contributors. + +#pragma once + +#include "source/tray/StatusNotifierTrayIcon.h" + +#include +#include + +namespace dish::source { + +inline constexpr auto kSniIconName = "com.tinkernorth.Dish"; + +QString sniIconName(bool themeHasIcon); +SniIconPixmapList sniTrayPixmaps(); +SniIconPixmap toSniPixmap(const QImage& source); + +} // namespace dish::source diff --git a/src/source/tray/StatusNotifierTrayIcon.cpp b/src/source/tray/StatusNotifierTrayIcon.cpp index 38159c7..12abb3e 100644 --- a/src/source/tray/StatusNotifierTrayIcon.cpp +++ b/src/source/tray/StatusNotifierTrayIcon.cpp @@ -3,9 +3,8 @@ #include "source/tray/StatusNotifierTrayIcon.h" -#include "Util/Endian.h" +#include "source/tray/SniIcon.h" -#include #include #include #include @@ -18,17 +17,9 @@ #include #include #include -#include #include -#include -#include -#include -#include -#include #include -#include - namespace dish::source { namespace { @@ -40,7 +31,6 @@ constexpr auto kWatcherPath = "/StatusNotifierWatcher"; constexpr auto kItemInterface = "org.kde.StatusNotifierItem"; constexpr auto kItemPath = "/StatusNotifierItem"; constexpr auto kMenuPath = "/MenuBar"; -constexpr auto kIconName = "com.tinkernorth.Dish"; constexpr auto kNewIconSignal = "NewIcon"; constexpr auto kNewToolTipSignal = "NewToolTip"; @@ -87,73 +77,6 @@ QVariantMap filterProperties(const QVariantMap& properties, const QStringList& n return filtered; } -// a(iiay) carries raw 32-bit ARGB in network byte order, so the source has to -// be unpremultiplied ARGB32 and every pixel goes out through putU32Be — -// casting the scan line to a uint32_t* would raise alignment and trip -// -Wcast-align. -SniIconPixmap toSniPixmap(const QImage& source) { - const QImage image = source.convertToFormat(QImage::Format_ARGB32); - SniIconPixmap pixmap; - pixmap.width = image.width(); - pixmap.height = image.height(); - const qsizetype byteCount = - static_cast(pixmap.width) * static_cast(pixmap.height) * 4; - pixmap.data = QByteArray(byteCount, '\0'); - auto* out = reinterpret_cast(pixmap.data.data()); - qsizetype offset = 0; - for (int y = 0; y < pixmap.height; ++y) { - for (int x = 0; x < pixmap.width; ++x) { - util::putU32Be(out + offset, static_cast(image.pixel(x, y))); - offset += 4; - } - } - return pixmap; -} - -// :/icons/dish.svg belongs to the exe's resource bundle, not to dish_core, so -// the fallback is drawn rather than loaded. -QImage renderFallbackIcon(int size) { - QImage image(size, size, QImage::Format_ARGB32); - image.fill(Qt::transparent); - const auto extent = static_cast(size); - const QPointF origin(extent * 0.24, extent * 0.78); - const QColor ink(0xF5, 0xF6, 0xF8); - - QPainter painter(&image); - painter.setRenderHint(QPainter::Antialiasing, true); - QPen pen(ink); - pen.setWidthF(qMax(1.0, extent * 0.11)); - pen.setCapStyle(Qt::RoundCap); - painter.setPen(pen); - painter.setBrush(Qt::NoBrush); - for (int ring = 1; ring <= 3; ++ring) { - const qreal radius = extent * 0.18 * static_cast(ring); - painter.drawArc( - QRectF(origin.x() - radius, origin.y() - radius, radius * 2.0, radius * 2.0), 5 * 16, - 80 * 16); - } - painter.setPen(Qt::NoPen); - painter.setBrush(ink); - painter.drawEllipse(origin, extent * 0.09, extent * 0.09); - painter.end(); - return image; -} - -// Theme lookup misses entirely under Flatpak, Snap and AppImage, so IconPixmap -// always ships alongside IconName instead of only when the theme fails. -SniIconPixmapList buildIconPixmaps() { - const QIcon themed = QIcon::fromTheme(QLatin1String(kIconName)); - SniIconPixmapList pixmaps; - for (const int size : {22, 48}) { - QImage image; - if (!themed.isNull()) { image = themed.pixmap(QSize(size, size)).toImage(); } - if (image.isNull()) { image = renderFallbackIcon(size); } - if (image.isNull()) { continue; } - pixmaps.append(toSniPixmap(image)); - } - return pixmaps; -} - } // namespace // Returning the const reference parameter is the signature QtDBus REQUIRES of a @@ -258,7 +181,7 @@ class StatusNotifierItemAdaptor final : public QDBusAbstractAdaptor { : QDBusAbstractAdaptor(host), owner_(owner) {} QString category() const { return QStringLiteral("ApplicationStatus"); } - QString id() const { return QLatin1String(kIconName); } + QString id() const { return QLatin1String(kSniIconName); } QString title() const { return QStringLiteral("Dish"); } // Pinned Active: a Passive item may be hidden by the host, and a hidden // item is a running Dish the user can neither reach nor quit. @@ -350,7 +273,7 @@ StatusNotifierTrayIcon::~StatusNotifierTrayIcon() { teardown(); } bool StatusNotifierTrayIcon::isAvailable() const { return available_; } -QString StatusNotifierTrayIcon::iconName() const { return QLatin1String(kIconName); } +QString StatusNotifierTrayIcon::iconName() const { return sniIconName(themeHasIcon_); } SniIconPixmapList StatusNotifierTrayIcon::iconPixmap() const { return iconPixmap_; } @@ -360,7 +283,8 @@ uint StatusNotifierTrayIcon::menuRevision() const { return 1; } SniToolTip StatusNotifierTrayIcon::toolTip() const { SniToolTip tip; - tip.iconName = QLatin1String(kIconName); + tip.iconName = sniIconName(themeHasIcon_); + tip.iconPixmap = iconPixmap_; tip.title = QStringLiteral("Dish"); switch (presentation_.activity) { case reducer::TrayActivity::Idle: @@ -547,8 +471,11 @@ void StatusNotifierTrayIcon::refreshAvailability() { emit availabilityChanged(available_); } +// Theme lookup misses entirely under Flatpak, Snap and AppImage, so IconPixmap +// always ships alongside IconName instead of only when the theme fails. void StatusNotifierTrayIcon::updateIcon() { - SniIconPixmapList pixmaps = buildIconPixmaps(); + themeHasIcon_ = QIcon::hasThemeIcon(QLatin1String(kSniIconName)); + SniIconPixmapList pixmaps = sniTrayPixmaps(); if (pixmaps == iconPixmap_) { return; } iconPixmap_ = pixmaps; if (exported_) { emitItemSignal(kNewIconSignal); } diff --git a/src/source/tray/StatusNotifierTrayIcon.h b/src/source/tray/StatusNotifierTrayIcon.h index d73eb54..73d1e3f 100644 --- a/src/source/tray/StatusNotifierTrayIcon.h +++ b/src/source/tray/StatusNotifierTrayIcon.h @@ -119,6 +119,7 @@ class StatusNotifierTrayIcon final : public TrayIcon { QObject* menuHost_ = nullptr; reducer::TrayPresentation presentation_; SniIconPixmapList iconPixmap_; + bool themeHasIcon_ = false; bool shown_ = false; bool exported_ = false; bool registered_ = false; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 837e564..226ac14 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -93,6 +93,7 @@ set(DISH_TEST_SOURCES test_tray_presentation.cpp test_tray_composer.cpp test_tray_controller.cpp + test_tray_icon_fallback.cpp test_sleep_cycle.cpp test_sleep_coordinator.cpp test_dish_notifications.cpp diff --git a/tests/test_tray_icon_fallback.cpp b/tests/test_tray_icon_fallback.cpp new file mode 100644 index 0000000..11004cb --- /dev/null +++ b/tests/test_tray_icon_fallback.cpp @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright (C) 2026 Dish contributors. + +#include "source/tray/SniIcon.h" +#include "source/tray/StatusNotifierTrayIcon.h" + +#include + +#include +#include + +#include + +using dish::source::kSniIconName; +using dish::source::sniIconName; +using dish::source::sniTrayPixmaps; +using dish::source::StatusNotifierTrayIcon; +using dish::source::toSniPixmap; + +TEST_CASE("SniIcon: an unresolvable theme icon advertises no IconName", "[tray]") { + REQUIRE(sniIconName(false).isEmpty()); +} + +TEST_CASE("SniIcon: a resolvable theme icon advertises the reverse-DNS name", "[tray]") { + REQUIRE(sniIconName(true) == QLatin1String(kSniIconName)); +} + +TEST_CASE("SniIcon: the bundled pixmaps decode at the exact panel sizes", "[tray]") { + const auto pixmaps = sniTrayPixmaps(); + REQUIRE(pixmaps.size() == 2); + const int sizes[] = {22, 48}; + for (int i = 0; i < 2; ++i) { + const auto& pixmap = pixmaps.at(i); + REQUIRE(pixmap.width == sizes[i]); + REQUIRE(pixmap.height == sizes[i]); + REQUIRE(pixmap.data.size() == + static_cast(pixmap.width) * static_cast(pixmap.height) * 4); + bool anyInk = false; + bool anyTransparent = false; + for (qsizetype offset = 0; offset < pixmap.data.size(); offset += 4) { + const auto alpha = static_cast(pixmap.data.at(offset)); + if (alpha > 0) { anyInk = true; } + if (alpha == 0) { anyTransparent = true; } + } + REQUIRE(anyInk); + REQUIRE(anyTransparent); + } +} + +TEST_CASE("SniIcon: pixmap bytes are ARGB in network byte order", "[tray]") { + QImage image(2, 1, QImage::Format_ARGB32); + image.setPixel(0, 0, 0x80402010U); + image.setPixel(1, 0, 0x01020304U); + + const auto pixmap = toSniPixmap(image); + REQUIRE(pixmap.width == 2); + REQUIRE(pixmap.height == 1); + REQUIRE(pixmap.data.size() == 8); + + const auto* bytes = reinterpret_cast(pixmap.data.constData()); + REQUIRE(bytes[0] == 0x80); + REQUIRE(bytes[1] == 0x40); + REQUIRE(bytes[2] == 0x20); + REQUIRE(bytes[3] == 0x10); + REQUIRE(bytes[4] == 0x01); + REQUIRE(bytes[5] == 0x02); + REQUIRE(bytes[6] == 0x03); + REQUIRE(bytes[7] == 0x04); +} + +TEST_CASE("StatusNotifierTrayIcon: no IconName before the theme is probed", "[tray]") { + StatusNotifierTrayIcon icon; + REQUIRE(icon.iconName().isEmpty()); +} + +TEST_CASE("StatusNotifierTrayIcon: the tooltip mirrors the item's icon fields", "[tray]") { + StatusNotifierTrayIcon icon; + const auto tip = icon.toolTip(); + REQUIRE(tip.iconName == icon.iconName()); + REQUIRE(tip.iconPixmap == icon.iconPixmap()); + REQUIRE(tip.title == QStringLiteral("Dish")); +}