diff --git a/Program.cs b/Program.cs index 9939bc1..71a43f6 100644 --- a/Program.cs +++ b/Program.cs @@ -215,7 +215,7 @@ internal static class Program Title = "Terminal", Group = "Core", Order = 10, - CreateForm = () => + CreateView = () => { var view = new PolyTraderSharp.Ui.Views.TerminalView(); view.Initialize(viewServices.GetRequiredService()); @@ -228,7 +228,7 @@ internal static class Program Title = "Server Settings", Group = "Core", Order = 20, - CreateForm = () => + CreateView = () => { var view = new PolyTraderSharp.Ui.Views.SettingsView(); view.Initialize( @@ -246,7 +246,7 @@ internal static class Program Title = "Server Jobs", Group = "Core", Order = 30, - CreateForm = () => + CreateView = () => { var view = new PolyTraderSharp.Ui.Views.JobsView(); view.Initialize(viewServices.GetRequiredService()); @@ -259,7 +259,7 @@ internal static class Program Title = "Dashboard", Group = "Core", Order = 5, - CreateForm = () => + CreateView = () => { var view = new PolyTraderSharp.Ui.Views.DashboardView(); var config = viewServices.GetRequiredService(); @@ -292,26 +292,12 @@ internal static class Program } /// - /// Weist den registrierten Views (Core + Module) ihr Menü-Icon aus den App-Ressourcen zu – - /// gemappt über die stabile View-ID. Bereits gesetzte Icons bleiben erhalten. + /// Weist den registrierten Views (Core + Module) ihren Standard-Symbolschlüssel zu. + /// Die Auflösung Schlüssel → Bildressource liegt in ; + /// Core und Module kennen nur noch den Schlüssel (toolkit-neutral, siehe ModuleView.IconKey). /// private static void AssignMenuIcons(PolyTraderSharp.Ui.ShellUiHost uiHost) - { - var map = new System.Collections.Generic.Dictionary - { - ["core.dashboard"] = Properties.Resources.dashboard, - ["core.settings"] = Properties.Resources.setting_tools, - ["core.terminal"] = Properties.Resources.error_log, - ["core.jobs"] = Properties.Resources.system_time, - ["accounting.main"] = Properties.Resources.coins_in_hand, - ["copytrading.main"] = Properties.Resources.cross_reference, - ["resolutionfarming.main"] = Properties.Resources.file_start_workflow, - ["supervisor.main"] = Properties.Resources.emotion_batman, - }; - foreach (var view in uiHost.Views) - if (view.Icon == null && map.TryGetValue(view.Id, out var img)) - view.Icon = img; - } + => PolyTraderSharp.Ui.ViewIcons.AssignDefaults(uiHost); private static void RunConfigMigrationFromJson(string folder) { @@ -510,7 +496,7 @@ internal static class Program { try { - using var form = view.CreateForm(); + using var form = (System.Windows.Forms.Form)view.CreateView(); Console.WriteLine($"[OK] {view.Id} ({view.Title})"); } catch (Exception ex) diff --git a/Ui/LauncherForm.cs b/Ui/LauncherForm.cs index 6b56e78..96e1d61 100644 --- a/Ui/LauncherForm.cs +++ b/Ui/LauncherForm.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -62,7 +62,7 @@ namespace PolyTraderSharp.Ui // Gemeinsame Fenster-Menüleiste: alle Fenster nebeneinander mit Icon (Launcher ist das // aktuelle Fenster → currentViewId null). - PolyTrader.Core.Modularity.WindowMenu.Wire(menuStrip, _uiHost, null); + WindowMenu.Wire(menuStrip, _uiHost, null); btn_liveTrading.Click += (_, _) => CycleLiveTrading(); btn_demoTrading.Click += (_, _) => CycleDemoTrading(); diff --git a/Ui/ShellUiHost.cs b/Ui/ShellUiHost.cs index a473857..64cd5fc 100644 --- a/Ui/ShellUiHost.cs +++ b/Ui/ShellUiHost.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Windows.Forms; @@ -43,7 +43,7 @@ namespace PolyTraderSharp.Ui private void AttachWindowMenu(Form form, string currentViewId) { var menu = new MenuStrip { Dock = DockStyle.Top, ImageScalingSize = new System.Drawing.Size(24, 24) }; - PolyTrader.Core.Modularity.WindowMenu.Wire(menu, this, currentViewId); + WindowMenu.Wire(menu, this, currentViewId); form.Controls.Add(menu); form.MainMenuStrip = menu; } @@ -103,7 +103,10 @@ namespace PolyTraderSharp.Ui return; } - var form = view.CreateForm(); + // CreateView liefert bewusst object (toolkit-neutraler Core-Contract). Die WinForms-Shell + // kennt ihr Toolkit und castet hier; ein falscher Typ ist ein Programmierfehler und soll + // laut knallen, nicht still ein leeres Fenster erzeugen. + var form = (Form)view.CreateView(); if (string.IsNullOrEmpty(form.Text) || form.Text == form.Name) form.Text = view.Title; diff --git a/Ui/ViewIcons.cs b/Ui/ViewIcons.cs new file mode 100644 index 0000000..16b8f80 --- /dev/null +++ b/Ui/ViewIcons.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Drawing; +using PolyTrader.Core.Modularity; + +namespace PolyTraderSharp.Ui +{ + /// + /// Löst die toolkit-neutralen -Schlüssel gegen die WinForms- + /// Bildressourcen der App auf. Diese Zuordnung ist die einzige Stelle, an der Views und + /// Bilddaten zusammenkommen – Core und Module kennen nur noch den Schlüssel als Zeichenkette. + /// + /// Die Avalonia-Shell bekommt später ein eigenes Gegenstück mit denselben Schlüsseln; die + /// Registrierungen in Core und Modulen bleiben dabei unverändert. + /// + internal static class ViewIcons + { + /// Symbol-Schlüssel → Bildressource. Unbekannte Schlüssel liefern null (Text-only-Menüeintrag). + private static readonly Dictionary Map = new() + { + ["dashboard"] = Properties.Resources.dashboard, + ["settings"] = Properties.Resources.setting_tools, + ["terminal"] = Properties.Resources.error_log, + ["jobs"] = Properties.Resources.system_time, + ["accounting"] = Properties.Resources.coins_in_hand, + ["copytrading"] = Properties.Resources.cross_reference, + ["resolutionfarming"] = Properties.Resources.file_start_workflow, + ["supervisor"] = Properties.Resources.emotion_batman, + }; + + /// + /// Standard-Symbolschlüssel je View-ID. Greift für Views, die selbst keinen + /// setzen – u.a. die Modul-Fenster, die die App-Ressourcen + /// nicht kennen. Ersetzt die frühere Icon-Zuweisung in Program.AssignMenuIcons. + /// + private static readonly Dictionary DefaultKeyByViewId = new() + { + ["core.dashboard"] = "dashboard", + ["core.settings"] = "settings", + ["core.terminal"] = "terminal", + ["core.jobs"] = "jobs", + ["accounting.main"] = "accounting", + ["copytrading.main"] = "copytrading", + ["resolutionfarming.main"] = "resolutionfarming", + ["supervisor.main"] = "supervisor", + }; + + /// Bildressource zum Schlüssel, oder null (kein Symbol / unbekannter Schlüssel). + public static Image? Resolve(string? iconKey) => + iconKey != null && Map.TryGetValue(iconKey, out var img) ? img : null; + + /// + /// Setzt bei allen registrierten Views den Standard-Symbolschlüssel, sofern noch keiner gesetzt ist. + /// + public static void AssignDefaults(IModuleUiHost uiHost) + { + foreach (var view in uiHost.Views) + if (view.IconKey == null && DefaultKeyByViewId.TryGetValue(view.Id, out var key)) + view.IconKey = key; + } + } +} diff --git a/src/PolyTrader.Core/Modularity/WindowMenu.cs b/Ui/WindowMenu.cs similarity index 89% rename from src/PolyTrader.Core/Modularity/WindowMenu.cs rename to Ui/WindowMenu.cs index d0a584d..74641a9 100644 --- a/src/PolyTrader.Core/Modularity/WindowMenu.cs +++ b/Ui/WindowMenu.cs @@ -1,11 +1,16 @@ -using System; +using System; using System.Drawing; using System.Linq; using System.Windows.Forms; +using PolyTrader.Core.Modularity; -namespace PolyTrader.Core.Modularity +namespace PolyTraderSharp.Ui { /// + /// WinForms-Umsetzung des gemeinsamen Fenster-Menüs. Liegt bewusst in der App und nicht mehr + /// im Core: der Core ist seit der Linux-Portierung toolkit-neutral (net8.0) und darf keine + /// WinForms-Typen mehr kennen. Die Avalonia-Shell bekommt später ihr eigenes Gegenstück. + /// /// Baut das gemeinsame Fenster-Menü, das auf JEDEM PolyTrader-Fenster erscheint und das Wechseln /// zwischen allen Fenstern (Launcher + Core + Module) erlaubt. Da es nur den Core-Contract /// nutzt, funktioniert es auch aus Modul-Fenstern (die die App nicht kennen). @@ -57,11 +62,12 @@ namespace PolyTrader.Core.Modularity foreach (var view in host.Views.OrderBy(v => v.Order).ThenBy(v => v.Title)) { bool isCurrent = view.Id == currentViewId; + var image = ViewIcons.Resolve(view.IconKey); var item = new ToolStripMenuItem(view.Title) { - Image = view.Icon, + Image = image, ImageScaling = ToolStripItemImageScaling.SizeToFit, - DisplayStyle = view.Icon != null + DisplayStyle = image != null ? ToolStripItemDisplayStyle.ImageAndText : ToolStripItemDisplayStyle.Text, Checked = isCurrent || host.IsOpen(view.Id) diff --git a/src/PolyTrader.Core/Modularity/ModuleUi.cs b/src/PolyTrader.Core/Modularity/ModuleUi.cs index 8a45470..9baec14 100644 --- a/src/PolyTrader.Core/Modularity/ModuleUi.cs +++ b/src/PolyTrader.Core/Modularity/ModuleUi.cs @@ -1,21 +1,24 @@ using System; using System.Collections.Generic; -using System.Windows.Forms; namespace PolyTrader.Core.Modularity { /// - /// Eine vom Core oder einem Modul beigesteuerte Fenster-Ansicht. Die eigentliche UI ist - /// ein designbares , das über erzeugt wird - /// (mit DI-Abhängigkeiten). Die Shell zeigt je View höchstens eine Instanz und holt ein - /// bereits offenes Fenster wieder in den Vordergrund. + /// Eine vom Core oder einem Modul beigesteuerte Fenster-Ansicht. Die Shell zeigt je View + /// höchstens eine Instanz und holt ein bereits offenes Fenster wieder in den Vordergrund. + /// + /// Bewusst toolkit-neutral: liefert ein , + /// keinen konkreten Fenstertyp. Dadurch bleibt der Core plattformunabhängig (net8.0) und + /// trägt keine WinForms-Abhängigkeit mehr. Die jeweilige Shell kennt ihr Toolkit und castet: + /// die WinForms-Shell auf System.Windows.Forms.Form, die künftige Avalonia-Shell auf + /// Avalonia.Controls.Window. Module liefern das Fenster, die Shell hängt es auf. /// public sealed class ModuleView { /// Stabile ID für Einzelinstanz-Handling (nur ein Fenster je View). public string Id { get; init; } = Guid.NewGuid().ToString(); - /// Titel (Fallback-Fenstertitel, falls das Form keinen eigenen setzt). + /// Titel (Fallback-Fenstertitel, falls das Fenster keinen eigenen setzt). public string Title { get; init; } = "Fenster"; /// Optionale Gruppierung (z.B. "Core", "CopyTrading"). @@ -25,21 +28,26 @@ namespace PolyTrader.Core.Modularity public int Order { get; init; } = 0; /// - /// Optionales Icon für Menü/Buttons. Wird von der Shell/den Menüs genutzt. Settable, damit die - /// App den von Modulen registrierten Views zentral ein Icon aus ihren Ressourcen zuweisen kann + /// Logischer Schlüssel des Symbols für Menüs/Buttons (z.B. "dashboard", "money"). Die Shell + /// löst ihn gegen ihre eigenen Ressourcen auf – der Core kennt keine Bilddaten mehr (früher + /// System.Drawing.Image, das seit .NET 7 Windows-only ist und auf Linux wirft). + /// Settable, damit die App den von Modulen registrierten Views zentral ein Symbol zuweisen kann /// (Module referenzieren die App-Ressourcen nicht). /// - public System.Drawing.Image? Icon { get; set; } + public string? IconKey { get; set; } - /// Erzeugt das anzuzeigende Fenster (frische Instanz je Öffnung). - public Func
CreateForm { get; init; } = () => new Form(); + /// + /// Erzeugt das anzuzeigende Fenster (frische Instanz je Öffnung). Rückgabetyp ist + /// – siehe Klassen-Doku zur Toolkit-Neutralität. + /// + public Func CreateView { get; init; } = () => new object(); } /// /// Wird der Shell beim Start übergeben; Core und Module registrieren hier ihre Ansichten. /// Die Shell (Launcher) öffnet die Fenster auf Anforderung und verwaltet den Offen-Status. /// Über die Navigations-Mitglieder kann JEDES Fenster (auch Modul-Fenster, die nur den Core - /// kennen) das gemeinsame „Fenster"-Menü bauen (siehe ). + /// kennen) das gemeinsame „Fenster"-Menü bauen. /// public interface IModuleUiHost { diff --git a/src/PolyTrader.Core/PolyTrader.Core.csproj b/src/PolyTrader.Core/PolyTrader.Core.csproj index 0bb23f2..13d41e7 100644 --- a/src/PolyTrader.Core/PolyTrader.Core.csproj +++ b/src/PolyTrader.Core/PolyTrader.Core.csproj @@ -1,12 +1,13 @@ - + - net8.0-windows + + net8.0 enable enable - - true diff --git a/src/PolyTrader.Modules.Accounting/AccountingModule.cs b/src/PolyTrader.Modules.Accounting/AccountingModule.cs index c5360bb..107ac02 100644 --- a/src/PolyTrader.Modules.Accounting/AccountingModule.cs +++ b/src/PolyTrader.Modules.Accounting/AccountingModule.cs @@ -1,4 +1,4 @@ -using System.Threading; +using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; @@ -57,7 +57,7 @@ namespace PolyTrader.Modules.Accounting Title = "Accounting", Group = "Accounting", Order = 400, - CreateForm = () => + CreateView = () => { var form = new Ui.AccountingMainForm(); form.Initialize(services); diff --git a/src/PolyTrader.Modules.CopyTrading/CopyTradingModule.cs b/src/PolyTrader.Modules.CopyTrading/CopyTradingModule.cs index 00283c4..907e77e 100644 --- a/src/PolyTrader.Modules.CopyTrading/CopyTradingModule.cs +++ b/src/PolyTrader.Modules.CopyTrading/CopyTradingModule.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; using System.Threading.Channels; @@ -92,7 +92,7 @@ namespace PolyTrader.Modules.CopyTrading Title = "Copytrading", Group = "CopyTrading", Order = 100, - CreateForm = () => + CreateView = () => { var form = new CopyTradingMainForm(); form.Initialize(services); diff --git a/src/PolyTrader.Modules.ResolutionFarming/ResolutionFarmingModule.cs b/src/PolyTrader.Modules.ResolutionFarming/ResolutionFarmingModule.cs index cfca717..9c69e3b 100644 --- a/src/PolyTrader.Modules.ResolutionFarming/ResolutionFarmingModule.cs +++ b/src/PolyTrader.Modules.ResolutionFarming/ResolutionFarmingModule.cs @@ -1,4 +1,4 @@ -using System.Threading; +using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; @@ -63,7 +63,7 @@ namespace PolyTrader.Modules.ResolutionFarming Title = "ResolutionFarming", Group = "ResolutionFarming", Order = 200, - CreateForm = () => + CreateView = () => { var form = new Ui.ResolutionFarmingMainForm(); form.Initialize(services); diff --git a/src/PolyTrader.Modules.Supervisor/SupervisorModule.cs b/src/PolyTrader.Modules.Supervisor/SupervisorModule.cs index 95d4d09..d86408e 100644 --- a/src/PolyTrader.Modules.Supervisor/SupervisorModule.cs +++ b/src/PolyTrader.Modules.Supervisor/SupervisorModule.cs @@ -62,7 +62,7 @@ namespace PolyTrader.Modules.Supervisor Title = "Supervisor", Group = "Supervisor", Order = 300, - CreateForm = () => + CreateView = () => { var form = new Ui.SupervisorMainForm(); form.Initialize(services); diff --git a/tests/PolyTrader.Tests/WindowMenuTests.cs b/tests/PolyTrader.Tests/WindowMenuTests.cs deleted file mode 100644 index 47f0397..0000000 --- a/tests/PolyTrader.Tests/WindowMenuTests.cs +++ /dev/null @@ -1,133 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Windows.Forms; -using PolyTrader.Core.Modularity; -using Xunit; - -namespace PolyTrader.Tests -{ - /// Sicherheitsnetz für das gemeinsame „Fenster"-Menü (Slice 1): Inhalt + Verhalten. - public class WindowMenuTests - { - private sealed class FakeUiHost : IModuleUiHost - { - public List ViewList { get; } = new(); - public HashSet OpenIds { get; } = new(); - public List Opened { get; } = new(); - public bool MainActivated { get; private set; } - - public bool ShutdownRequested { get; private set; } - - public IReadOnlyList Views => ViewList; - public bool IsOpen(string viewId) => OpenIds.Contains(viewId); - public void OpenView(string viewId) => Opened.Add(viewId); - public void ActivateMain() => MainActivated = true; - public void RequestShutdown() => ShutdownRequested = true; - public void RegisterView(ModuleView view) => ViewList.Add(view); - public event Action? OpenStateChanged { add { } remove { } } - } - - private static FakeUiHost Host() - { - var h = new FakeUiHost(); - h.RegisterView(new ModuleView { Id = "core.dashboard", Title = "Dashboard", Order = 1 }); - h.RegisterView(new ModuleView { Id = "core.terminal", Title = "Terminal / Logs", Order = 2 }); - h.OpenIds.Add("core.terminal"); // Terminal ist offen - return h; - } - - [Fact] - public void Populate_lists_launcher_views_and_exit_side_by_side() - { - var host = Host(); - var menu = new MenuStrip(); - - // currentViewId == null => Launcher (Hauptprozess): rechte Aktion ist „Beenden". - WindowMenu.Populate(menu, host, currentViewId: null); - - var texts = menu.Items.OfType().Select(i => i.Text).ToList(); - Assert.Equal("Launcher", texts[0]); // Launcher als erster Top-Level-Eintrag - Assert.Contains("Dashboard", texts); - Assert.Contains("Terminal / Logs", texts); - Assert.Contains("Beenden", texts); - } - - [Fact] - public void Non_launcher_window_offers_close_window_not_exit() - { - var host = Host(); - var menu = new MenuStrip(); - - // currentViewId != null => Modul-/Core-Fenster: KEIN „Beenden", nur „Fenster schließen". - WindowMenu.Populate(menu, host, currentViewId: "core.dashboard"); - - var texts = menu.Items.OfType().Select(i => i.Text).ToList(); - Assert.Contains("Fenster schließen", texts); - Assert.DoesNotContain("Beenden", texts); - - // Klick auf „Fenster schließen" fordert NICHT das App-Shutdown an. - menu.Items.OfType().First(i => i.Text == "Fenster schließen").PerformClick(); - Assert.False(host.ShutdownRequested); - } - - [Fact] - public void Views_carry_their_icon_in_the_menu() - { - var host = new FakeUiHost(); - using var icon = new System.Drawing.Bitmap(16, 16); - host.RegisterView(new ModuleView { Id = "core.dashboard", Title = "Dashboard", Order = 1, Icon = icon }); - var menu = new MenuStrip(); - - WindowMenu.Populate(menu, host, currentViewId: null); - - var dashboard = menu.Items.OfType().First(i => i.Text == "Dashboard"); - Assert.Same(icon, dashboard.Image); - Assert.Equal(ToolStripItemDisplayStyle.ImageAndText, dashboard.DisplayStyle); - } - - [Fact] - public void Current_view_is_bold_and_checked_open_view_is_checked() - { - var host = Host(); - var menu = new MenuStrip(); - - WindowMenu.Populate(menu, host, currentViewId: "core.dashboard"); - - var dashboard = menu.Items.OfType().First(i => i.Text == "Dashboard"); - var terminal = menu.Items.OfType().First(i => i.Text == "Terminal / Logs"); - - Assert.True(dashboard.Checked); // aktuelles Fenster - Assert.True(dashboard.Font.Bold); - Assert.True(terminal.Checked); // offenes Fenster - Assert.False(terminal.Font.Bold); - } - - [Fact] - public void Clicking_items_navigates() - { - var host = Host(); - var menu = new MenuStrip(); - WindowMenu.Populate(menu, host, currentViewId: null); - - var items = menu.Items.OfType().ToList(); - items.First(i => i.Text == "Launcher").PerformClick(); - items.First(i => i.Text == "Terminal / Logs").PerformClick(); - - Assert.True(host.MainActivated); - Assert.Contains("core.terminal", host.Opened); - } - - [Fact] - public void Clicking_exit_requests_shutdown_not_immediate_exit() - { - var host = Host(); - var menu = new MenuStrip(); - WindowMenu.Populate(menu, host, currentViewId: null); - - menu.Items.OfType().First(i => i.Text == "Beenden").PerformClick(); - - Assert.True(host.ShutdownRequested); - } - } -}