diff --git a/src/PolyTrader.App.Avalonia/App.axaml.cs b/src/PolyTrader.App.Avalonia/App.axaml.cs index a813be6..aca7f6d 100644 --- a/src/PolyTrader.App.Avalonia/App.axaml.cs +++ b/src/PolyTrader.App.Avalonia/App.axaml.cs @@ -36,6 +36,10 @@ namespace PolyTrader.App.Avalonia Shell.ThemeManager.Initialize(settings, "server_settings.xml", Services.GetRequiredService()); + // #410: Unter Linux setzt Avalonia 11 kein Fenstersymbol - Umweg, bevor das erste Fenster erscheint. + var terminal = Services.GetRequiredService(); + LinuxWindowIcon.Register(m => terminal.Warning(m)); + var uiHost = Services.GetRequiredService(); CoreViews.Register(uiHost, Services); diff --git a/src/PolyTrader.App.Avalonia/Shell/LinuxWindowIcon.cs b/src/PolyTrader.App.Avalonia/Shell/LinuxWindowIcon.cs new file mode 100644 index 0000000..fd21f2e --- /dev/null +++ b/src/PolyTrader.App.Avalonia/Shell/LinuxWindowIcon.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Avalonia.Controls; +using Avalonia.Platform; +using SkiaSharp; + +namespace PolyTrader.App.Avalonia.Shell +{ + /// + /// Setzt unter Linux das Fenstersymbol (_NET_WM_ICON) selbst - Umweg für #410. + /// + /// Avalonia 11.3.19 lädt das Symbol aus Icon="..." zwar korrekt, schreibt die Eigenschaft + /// aber nie auf das X11-Fenster (am 01.09.2026 mit einem Minimalprogramm nachgewiesen). Fensterliste und + /// Alt-Tab zeigen dann kein Symbol. Richards Entscheidung vom 15.09.2026: nicht auf Avalonia 12 warten. + /// + /// Nach dem Öffnen jedes Fensters wird die Eigenschaft über eine eigene Verbindung zum X-Server gesetzt; + /// sie gehört dem Fenster und bleibt nach dem Schließen der Verbindung bestehen. Unter Windows trägt + /// ApplicationIcon, dort passiert hier nichts. + /// + /// Entfernen, sobald Avalonia die Eigenschaft selbst setzt (nachprüfen mit + /// xprop -id <fenster> _NET_WM_ICON nach dem Wechsel auf eine neuere Avalonia-Fassung). + /// + internal static class LinuxWindowIcon + { + private const string IconUri = "avares://PolyTrader.App.Avalonia/Assets/icon.png"; + private static readonly int[] Sizes = { 64, 32 }; + + private static IntPtr[]? _data; + private static bool _failed; + private static Action? _warn; + + /// Einmal beim Start, bevor das erste Fenster erscheint. + public static void Register(Action warn) + { + if (!OperatingSystem.IsLinux()) return; + _warn = warn; + Window.WindowOpenedEvent.AddClassHandler((window, _) => Apply(window)); + } + + private static void Apply(Window window) + { + if (_failed) return; + try + { + var handle = window.TryGetPlatformHandle(); + if (handle == null || handle.HandleDescriptor != "XID") return; // z.B. kein X11 + + var data = _data ??= BuildData(); + + IntPtr display = XOpenDisplay(IntPtr.Zero); + if (display == IntPtr.Zero) return; + try + { + IntPtr atom = XInternAtom(display, "_NET_WM_ICON", false); + XChangeProperty(display, handle.Handle, atom, XaCardinal, 32, PropModeReplace, data, data.Length); + XFlush(display); + } + finally + { + XCloseDisplay(display); + } + } + catch (Exception ex) + { + // Das Symbol ist Kosmetik und darf nie den Start oder ein Fenster stören. Einmal melden, dann Ruhe. + _failed = true; + _warn?.Invoke($"Fenstersymbol unter Linux nicht gesetzt ({ex.GetType().Name}: {ex.Message})."); + } + } + + /// + /// _NET_WM_ICON-Format: je Größe Breite, Höhe, dann Breite×Höhe Bildpunkte als ARGB (nicht vormultipliziert). + /// Bei Format 32 erwartet Xlib je Wert ein long - auf 64-Bit also 8 Byte, daher IntPtr. + /// + private static IntPtr[] BuildData() + { + using var stream = AssetLoader.Open(new Uri(IconUri)); + using var source = SKBitmap.Decode(stream) + ?? throw new InvalidOperationException("icon.png nicht lesbar"); + + var values = new List(); + foreach (int size in Sizes) + { + using var scaled = source.Resize(new SKImageInfo(size, size, SKColorType.Bgra8888, SKAlphaType.Unpremul), + SKFilterQuality.High) + ?? throw new InvalidOperationException($"icon.png nicht auf {size} px skalierbar"); + + values.Add((IntPtr)size); + values.Add((IntPtr)size); + for (int y = 0; y < size; y++) + for (int x = 0; x < size; x++) + { + SKColor c = scaled.GetPixel(x, y); + long argb = ((long)c.Alpha << 24) | ((long)c.Red << 16) | ((long)c.Green << 8) | c.Blue; + values.Add((IntPtr)argb); + } + } + return values.ToArray(); + } + + private static readonly IntPtr XaCardinal = (IntPtr)6; + private const int PropModeReplace = 0; + + [DllImport("libX11.so.6")] + private static extern IntPtr XOpenDisplay(IntPtr displayName); + + [DllImport("libX11.so.6")] + private static extern IntPtr XInternAtom(IntPtr display, string atomName, bool onlyIfExists); + + [DllImport("libX11.so.6")] + private static extern int XChangeProperty(IntPtr display, IntPtr window, IntPtr property, IntPtr type, + int format, int mode, IntPtr[] data, int nelements); + + [DllImport("libX11.so.6")] + private static extern int XFlush(IntPtr display); + + [DllImport("libX11.so.6")] + private static extern int XCloseDisplay(IntPtr display); + } +}