@@ -2,6 +2,7 @@ import QtQuick
22import QtQuick.Layouts
33import Quickshell
44import Quickshell.Wayland
5+ import Quickshell.Services.SystemTray
56
67import "../common" as Common
78import "../../services" as Services
@@ -151,6 +152,119 @@ PanelWindow {
151152 visible: root .isRightmost
152153 spacing: 2
153154
155+ // System tray icons
156+ Repeater {
157+ model: SystemTray .items
158+
159+ delegate: MouseArea {
160+ id: trayItemArea
161+ required property var modelData
162+
163+ Layout .preferredHeight : 28
164+ Layout .preferredWidth : 28
165+ hoverEnabled: true
166+ cursorShape: Qt .PointingHandCursor
167+
168+ // Check if icon has unsupported custom path (e.g. "icon_name?path=/some/path")
169+ property bool hasCustomPath: modelData .icon && modelData .icon .includes (" ?path=" )
170+
171+ // Resolve icon source - handle paths, icon names, skip unsupported custom paths
172+ property string iconSource: {
173+ const icon = modelData .icon
174+ if (! icon || icon === " " ) return " "
175+ // Skip icons with custom paths - Quickshell doesn't support them
176+ if (icon .includes (" ?path=" )) return " "
177+ // Already a full path or URL
178+ if (icon .startsWith (" /" )) return " file://" + icon
179+ if (icon .startsWith (" file://" ) || icon .startsWith (" image://" )) return icon
180+ // Icon name - try Qt icon provider
181+ return " image://icon/" + icon
182+ }
183+
184+ // Datacube fallback lookup using app title
185+ property string datacubeIcon: Services .IconResolver .getIcon (modelData .title )
186+
187+ Rectangle {
188+ anchors .fill : parent
189+ radius: Common .Appearance .rounding .small
190+ color: trayItemArea .containsMouse
191+ ? Common .Appearance .m3colors .surfaceVariant
192+ : " transparent"
193+
194+ Behavior on color {
195+ ColorAnimation { duration: 150 }
196+ }
197+ }
198+
199+ // Track if primary icon failed (Error, Null status, or has unsupported custom path)
200+ property bool primaryFailed: trayItemArea .hasCustomPath || primaryTrayIcon .status === Image .Error || primaryTrayIcon .status === Image .Null || trayItemArea .iconSource === " "
201+
202+ // Primary icon from tray item
203+ Image {
204+ id: primaryTrayIcon
205+ anchors .centerIn : parent
206+ width: Common .Appearance .sizes .iconMedium
207+ height: Common .Appearance .sizes .iconMedium
208+ sourceSize: Qt .size (Common .Appearance .sizes .iconMedium , Common .Appearance .sizes .iconMedium )
209+ source: trayItemArea .iconSource
210+ smooth: true
211+ visible: status === Image .Ready
212+ }
213+
214+ // Datacube fallback icon
215+ Image {
216+ id: fallbackTrayIcon
217+ anchors .centerIn : parent
218+ width: Common .Appearance .sizes .iconMedium
219+ height: Common .Appearance .sizes .iconMedium
220+ sourceSize: Qt .size (Common .Appearance .sizes .iconMedium , Common .Appearance .sizes .iconMedium )
221+ source: trayItemArea .primaryFailed ? trayItemArea .datacubeIcon : " "
222+ smooth: true
223+ visible: trayItemArea .primaryFailed && status === Image .Ready
224+ }
225+
226+ // Last resort: letter icon
227+ Rectangle {
228+ anchors .centerIn : parent
229+ width: Common .Appearance .sizes .iconMedium
230+ height: Common .Appearance .sizes .iconMedium
231+ radius: Common .Appearance .rounding .small
232+ color: Common .Appearance .m3colors .primaryContainer
233+ visible: trayItemArea .primaryFailed && fallbackTrayIcon .status !== Image .Ready
234+
235+ Text {
236+ anchors .centerIn : parent
237+ text: trayItemArea .modelData .title ? trayItemArea .modelData .title .charAt (0 ).toUpperCase () : " ?"
238+ font .pixelSize : 10
239+ font .bold : true
240+ color: Common .Appearance .m3colors .onPrimaryContainer
241+ }
242+ }
243+
244+ acceptedButtons: Qt .LeftButton | Qt .RightButton | Qt .MiddleButton
245+
246+ onClicked : (mouse ) => {
247+ if (mouse .button === Qt .RightButton || (trayItemArea .modelData .onlyMenu && trayItemArea .modelData .hasMenu )) {
248+ // Right click or menu-only item: show menu
249+ if (trayItemArea .modelData .hasMenu ) {
250+ // Map coordinates to window
251+ const pos = trayItemArea .mapToItem (null , 0 , trayItemArea .height )
252+ trayItemArea .modelData .display (root, pos .x , pos .y )
253+ }
254+ } else if (mouse .button === Qt .MiddleButton ) {
255+ trayItemArea .modelData .secondaryActivate ()
256+ } else {
257+ // Left click: activate
258+ trayItemArea .modelData .activate ()
259+ }
260+ }
261+
262+ onWheel : (wheel ) => {
263+ trayItemArea .modelData .scroll (wheel .angleDelta .y , false )
264+ }
265+ }
266+ }
267+
154268 // Weather (if enabled)
155269 BarButton {
156270 visible: Common .Config .showWeather && Services .Weather .ready
@@ -167,9 +281,9 @@ PanelWindow {
167281 tooltip: " Camera in use"
168282 }
169283
170- // Network - only show if wifi available (for wifi) or ethernet connected
171- BarIndicator {
172- visible: Common .Config .showNetwork && ( Services . Network . wifiAvailable || ( Services . Network . connected && Services . Network . type === " ethernet " ))
284+ // Network
285+ BarButton {
286+ visible: Common .Config .showNetwork
173287 icon: {
174288 if (! Services .Network .connected ) {
175289 return Services .Network .wifiAvailable ? Common .Icons .icons .wifiOff : Common .Icons .icons .ethernetOff
@@ -182,6 +296,7 @@ PanelWindow {
182296 tooltip: Services .Network .connected
183297 ? Services .Network .name
184298 : " Disconnected"
299+ onClicked: Root .GlobalStates .toggleSidebarRight (root .targetScreen , " network" )
185300 }
186301
187302 // Audio output
0 commit comments