From 4ba8149e64f651f70d38e1c5cb29525d71549213 Mon Sep 17 00:00:00 2001 From: Richard Date: Thu, 6 Aug 2026 20:55:47 +0200 Subject: [PATCH] Settings nach Avalonia - PropertyGrid durch kategorisierten Editor ersetzt Der PropertyGrid-Ersatz ist EIN wiederverwendbares Steuerelement statt Handarbeit je Feld: Controls/SettingsEditor zeigt ein beliebiges Einstellungsobjekt nach Richards Vorlage - Kategorie-Ueberschrift, Beschriftung links, Feld rechts, Erklaerung klein darunter. - SettingsModelBuilder liest die Attribute, die fuers PropertyGrid ohnehin gepflegt waren: [Category] gruppiert, [DisplayName] beschriftet, [Description] wird zum Hinweistext, [Browsable(false)] blendet aus (Watchdog-Token, Lizenzschluessel bleiben unsichtbar). Ergebnis fuer ServerSettings: 6 Abschnitte, 15 Felder - ohne eine Zeile Feld-Code. - Layout-Regel gewahrt: WELCHE Felder es gibt, kommt als Daten; WIE ein Feld aussieht, steht deklarativ als DataTemplate je Feldtyp (Text/Zahl/Ja-Nein/Auswahl/Nur-Lese). - Damit sind auch die drei restlichen PropertyGrid-Stellen (Account-Einstellungen, Master-Trader, ResolutionFarming) mit je einem Aufruf erledigt. SettingsWindow: Server-Settings + Polymarket-Accounts, Master-Key erzeugen, OpenRouter-Key/Watchdog-Token setzen, Test-Heartbeat. Rueckmeldungen laufen ueber die Statuszeile statt ueber Dialoge - nur echte Entscheidungen bekommen einen Dialog (neu: Views/DialogWindow fuer Hinweis/Rueckfrage/maskierte Eingabe, Avalonia hat keine MessageBox). Nebenbei einen offenen Punkt der Linux-Analyse erledigt: Schluesseldateien (master.key, openrouter.key) werden jetzt per File.SetUnixFileMode auf 600 gesetzt. Auf Linux legt File.WriteAllText sonst mit ueblicher umask 644 an - weltweit lesbar. Lizenzdialog bewusst NICHT portiert: haengt am alten LicenseLabrador-SDK, das mit der Deploymentcenter-Anbindung (P3c) ohnehin ersetzt wird - waere Wegwerfarbeit. Smoke-UI prueft jetzt zusaetzlich die Feldzahl des Editors: ein Fenster kann fehlerfrei konstruieren und trotzdem leer sein, wenn die Attribute verlorengehen. Verifiziert: Solution baut, 442 Tests gruen, --smoke-ui gruen (5 Fenster + Editor-Pruefung), Linux-Publish laeuft. Co-Authored-By: Claude Opus 5 --- .../Controls/SettingsEditor.axaml | 124 +++++++ .../Controls/SettingsEditor.axaml.cs | 26 ++ src/PolyTrader.App.Avalonia/Program.cs | 25 ++ .../Shell/CoreViews.cs | 17 +- .../ViewModels/SettingsFields.cs | 188 ++++++++++ .../Views/DialogWindow.axaml | 26 ++ .../Views/DialogWindow.axaml.cs | 66 ++++ .../Views/SettingsWindow.axaml | 75 ++++ .../Views/SettingsWindow.axaml.cs | 327 ++++++++++++++++++ 9 files changed, 873 insertions(+), 1 deletion(-) create mode 100644 src/PolyTrader.App.Avalonia/Controls/SettingsEditor.axaml create mode 100644 src/PolyTrader.App.Avalonia/Controls/SettingsEditor.axaml.cs create mode 100644 src/PolyTrader.App.Avalonia/ViewModels/SettingsFields.cs create mode 100644 src/PolyTrader.App.Avalonia/Views/DialogWindow.axaml create mode 100644 src/PolyTrader.App.Avalonia/Views/DialogWindow.axaml.cs create mode 100644 src/PolyTrader.App.Avalonia/Views/SettingsWindow.axaml create mode 100644 src/PolyTrader.App.Avalonia/Views/SettingsWindow.axaml.cs diff --git a/src/PolyTrader.App.Avalonia/Controls/SettingsEditor.axaml b/src/PolyTrader.App.Avalonia/Controls/SettingsEditor.axaml new file mode 100644 index 0000000..6acc58a --- /dev/null +++ b/src/PolyTrader.App.Avalonia/Controls/SettingsEditor.axaml @@ -0,0 +1,124 @@ + + + + + + 210 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/PolyTrader.App.Avalonia/Controls/SettingsEditor.axaml.cs b/src/PolyTrader.App.Avalonia/Controls/SettingsEditor.axaml.cs new file mode 100644 index 0000000..5dd24d9 --- /dev/null +++ b/src/PolyTrader.App.Avalonia/Controls/SettingsEditor.axaml.cs @@ -0,0 +1,26 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using PolyTrader.App.Avalonia.ViewModels; + +namespace PolyTrader.App.Avalonia.Controls +{ + /// + /// Zeigt ein beliebiges Einstellungsobjekt als kategorisiertes Formular. Ersetzt das + /// WinForms-PropertyGrid an allen vier Stellen (Server-Settings, Account-Einstellungen, + /// Master-Trader, ResolutionFarming) – ein Aufruf je Fenster, kein Feld-Code. + /// + /// Änderungen wirken direkt auf dem übergebenen Objekt; das Speichern (Datei/DB) bleibt + /// Sache des jeweiligen Fensters – genau wie zuvor beim PropertyGrid. + /// + public partial class SettingsEditor : UserControl + { + public SettingsEditor() => AvaloniaXamlLoader.Load(this); + + /// Setzt das anzuzeigende Objekt (oder null, um zu leeren). + public void Show(object? target) + { + var list = this.FindControl("sections")!; + list.ItemsSource = target == null ? null : SettingsModelBuilder.Build(target); + } + } +} diff --git a/src/PolyTrader.App.Avalonia/Program.cs b/src/PolyTrader.App.Avalonia/Program.cs index cbf56f9..9855250 100644 --- a/src/PolyTrader.App.Avalonia/Program.cs +++ b/src/PolyTrader.App.Avalonia/Program.cs @@ -210,6 +210,31 @@ namespace PolyTrader.App.Avalonia Console.WriteLine($"[FEHLER] ShutdownConfirmWindow: {ex.GetType().Name}: {ex.Message}"); } + // Der Einstellungs-Editor wird aus den Attributen des Modells aufgebaut. Ein Fenster kann + // fehlerfrei konstruieren und trotzdem leer sein, wenn die Attribute verlorengehen - + // deshalb hier gegen die tatsaechliche Feldzahl pruefen. + try + { + var sections = ViewModels.SettingsModelBuilder.Build(new ServerSettings()); + int fieldCount = sections.Sum(x => x.Fields.Count); + if (sections.Count == 0 || fieldCount == 0) + { + failures++; + Console.WriteLine("[FEHLER] Einstellungs-Editor: keine Felder aus ServerSettings ermittelt " + + "(Category-/DisplayName-Attribute verloren?)."); + } + else + { + Console.WriteLine($"[OK] Einstellungs-Editor: {sections.Count} Abschnitte, {fieldCount} Felder " + + $"({string.Join(", ", sections.Select(x => x.Title))})"); + } + } + catch (Exception ex) + { + failures++; + Console.WriteLine($"[FEHLER] Einstellungs-Editor: {ex.GetType().Name}: {ex.Message}"); + } + Console.WriteLine(failures == 0 ? "=== Smoke-UI OK ===" : $"=== Smoke-UI: {failures} Fehler ==="); return failures == 0 ? 0 : 1; } diff --git a/src/PolyTrader.App.Avalonia/Shell/CoreViews.cs b/src/PolyTrader.App.Avalonia/Shell/CoreViews.cs index ae28174..ffd49bc 100644 --- a/src/PolyTrader.App.Avalonia/Shell/CoreViews.cs +++ b/src/PolyTrader.App.Avalonia/Shell/CoreViews.cs @@ -35,6 +35,21 @@ namespace PolyTrader.App.Avalonia.Shell ServerSettingsPath) }); + host.RegisterView(new ModuleView + { + Id = "core.settings", + Title = "Server Settings", + Group = "Core", + Order = 20, + CreateView = () => new Views.SettingsWindow( + host, + services.GetRequiredService(), + services.GetRequiredService(), + services.GetRequiredService(), + services.GetService(), + services.GetService()) + }); + host.RegisterView(new ModuleView { Id = "core.jobs", @@ -44,7 +59,7 @@ namespace PolyTrader.App.Avalonia.Shell CreateView = () => new Views.JobsWindow(host, services.GetRequiredService()) }); - // TODO Portierung: core.settings (Order 20) und core.terminal (Order 40) folgen. + // TODO Portierung: core.terminal (Order 40) folgt. // Aufbau siehe docs/UI-SPEZIFIKATION-WinForms.md, Originalcode im Git-Tag winforms-final. } diff --git a/src/PolyTrader.App.Avalonia/ViewModels/SettingsFields.cs b/src/PolyTrader.App.Avalonia/ViewModels/SettingsFields.cs new file mode 100644 index 0000000..f5729c9 --- /dev/null +++ b/src/PolyTrader.App.Avalonia/ViewModels/SettingsFields.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace PolyTrader.App.Avalonia.ViewModels +{ + /// Ein Eingabefeld der Einstellungen. Schreibt Änderungen direkt in das Zielobjekt. + public abstract class SettingsField : INotifyPropertyChanged + { + protected readonly object Target; + protected readonly PropertyInfo Property; + + protected SettingsField(object target, PropertyInfo property, string label, string? description) + { + Target = target; + Property = property; + Label = label; + Description = description ?? string.Empty; + } + + public string Label { get; } + + /// Erklärungstext aus dem – erscheint klein unter dem Feld. + public string Description { get; } + + public bool HasDescription => Description.Length > 0; + + /// Falsch bei Eigenschaften ohne Setter (reine Statusanzeigen). + public bool IsEditable => Property.CanWrite; + + protected void Raise([CallerMemberName] string? name = null) => + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + + public event PropertyChangedEventHandler? PropertyChanged; + } + + /// Freitext (string). + public sealed class TextSettingsField : SettingsField + { + public TextSettingsField(object t, PropertyInfo p, string l, string? d) : base(t, p, l, d) { } + + public string Value + { + get => Property.GetValue(Target) as string ?? string.Empty; + set { if (IsEditable) { Property.SetValue(Target, value ?? string.Empty); Raise(); } } + } + } + + /// Ja/Nein (bool) – als Kontrollkästchen. + public sealed class BoolSettingsField : SettingsField + { + public BoolSettingsField(object t, PropertyInfo p, string l, string? d) : base(t, p, l, d) { } + + public bool Value + { + get => Property.GetValue(Target) is true; + set { if (IsEditable) { Property.SetValue(Target, value); Raise(); } } + } + } + + /// + /// Ganzzahl. Bewusst als Text mit invarianter Umwandlung: eine ungültige Eingabe lässt den + /// bisherigen Wert stehen, statt still eine 0 zu schreiben. + /// + public sealed class IntSettingsField : SettingsField + { + public IntSettingsField(object t, PropertyInfo p, string l, string? d) : base(t, p, l, d) { } + + public string Value + { + get => Convert.ToInt32(Property.GetValue(Target) ?? 0).ToString(CultureInfo.InvariantCulture); + set + { + if (!IsEditable) return; + if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed)) + Property.SetValue(Target, parsed); + Raise(); + } + } + } + + /// Auswahl aus festen Werten (Enum) – als Aufklappliste. + public sealed class ChoiceSettingsField : SettingsField + { + public ChoiceSettingsField(object t, PropertyInfo p, string l, string? d) : base(t, p, l, d) + { + Options = Enum.GetNames(Nullable.GetUnderlyingType(p.PropertyType) ?? p.PropertyType); + } + + public IReadOnlyList Options { get; } + + public string? Value + { + get => Property.GetValue(Target)?.ToString(); + set + { + if (!IsEditable || value == null) return; + Property.SetValue(Target, Enum.Parse( + Nullable.GetUnderlyingType(Property.PropertyType) ?? Property.PropertyType, value)); + Raise(); + } + } + } + + /// Nur-Lese-Anzeige (Eigenschaft ohne Setter, oder nicht editierbarer Typ). + public sealed class ReadOnlySettingsField : SettingsField + { + public ReadOnlySettingsField(object t, PropertyInfo p, string l, string? d) : base(t, p, l, d) { } + + public string Value + { + get + { + object? raw = Property.GetValue(Target); + if (raw is System.Collections.IEnumerable seq and not string) + return string.Join(", ", seq.Cast().Select(o => o?.ToString())); + return raw?.ToString() ?? string.Empty; + } + } + } + + /// Eine Kategorie mit ihren Feldern – entspricht einer Überschrift im Dialog. + public sealed class SettingsSection + { + public string Title { get; init; } = string.Empty; + public IReadOnlyList Fields { get; init; } = Array.Empty(); + } + + /// + /// Baut aus einem Einstellungsobjekt die Abschnitte für den SettingsEditor. + /// + /// Nutzt die Attribute, die für das frühere PropertyGrid ohnehin schon gepflegt + /// wurden: gruppiert, liefert + /// die Beschriftung, den Hinweistext unter dem Feld, und + /// [Browsable(false)] blendet aus (z.B. Watchdog-Token und Lizenzschlüssel – die gehören + /// nicht offen in die Oberfläche). + /// + /// Damit ist der Ersatz des PropertyGrid keine Fleißarbeit je Feld: die Beschreibung der + /// Felder steht weiterhin am Modell, nur die Darstellung ist jetzt eine eigene, gestaltbare + /// Oberfläche statt einer generischen Reflection-Liste. + /// + public static class SettingsModelBuilder + { + public static IReadOnlyList Build(object target) + { + var fields = target.GetType() + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.GetIndexParameters().Length == 0) + .Where(p => p.GetCustomAttribute()?.Browsable != false) + .Select(p => new + { + Property = p, + Category = p.GetCustomAttribute()?.Category ?? "Allgemein", + Label = p.GetCustomAttribute()?.DisplayName ?? p.Name, + Description = p.GetCustomAttribute()?.Description + }) + .ToList(); + + return fields + .GroupBy(f => f.Category) + .Select(g => new SettingsSection + { + Title = g.Key, + Fields = g.Select(f => Create(target, f.Property, f.Label, f.Description)).ToList() + }) + .ToList(); + } + + private static SettingsField Create(object target, PropertyInfo p, string label, string? description) + { + Type t = Nullable.GetUnderlyingType(p.PropertyType) ?? p.PropertyType; + + if (!p.CanWrite) return new ReadOnlySettingsField(target, p, label, description); + if (t.IsEnum) return new ChoiceSettingsField(target, p, label, description); + if (t == typeof(bool)) return new BoolSettingsField(target, p, label, description); + if (t == typeof(int) || t == typeof(long)) return new IntSettingsField(target, p, label, description); + if (t == typeof(string)) return new TextSettingsField(target, p, label, description); + + // Listen u.a. werden angezeigt, aber nicht hier bearbeitet (z.B. DisabledModules – + // die pflegt der Modul-Tab des Dashboards). + return new ReadOnlySettingsField(target, p, label, description); + } + } +} diff --git a/src/PolyTrader.App.Avalonia/Views/DialogWindow.axaml b/src/PolyTrader.App.Avalonia/Views/DialogWindow.axaml new file mode 100644 index 0000000..af3c0e0 --- /dev/null +++ b/src/PolyTrader.App.Avalonia/Views/DialogWindow.axaml @@ -0,0 +1,26 @@ + + + + + + + + + + +