UI Slice 5: Menueleiste (Fenster nebeneinander), sicheres Beenden, Modul-Aktivierung
- Fenster-Menue: WindowMenu fuellt die oberste MenuStrip mit Top-Level-Eintraegen nebeneinander (mit Icon) statt Untermenue "Fenster"; ModuleView.Icon zentral in der App zugewiesen (AssignMenuIcons). Kein miFenster mehr im Launcher-Designer. - Sicheres Beenden: ShutdownConfirmDialog (10s-Timer sperrt "Jetzt beenden", Abbrechen jederzeit) via IModuleUiHost.RequestShutdown(). Nur der Launcher (Hauptprozess) bietet "Beenden"; andere Fenster nur "Fenster schliessen" (kein App-Shutdown, Module laufen weiter). Launcher-Schliessen-X routet ueber dieselbe Abfrage. - Modul-Aktivierung (restart-basiert): ServerSettings.DisabledModules, in Program.Main vor der DI-Registrierung gefiltert -> deaktivierte Module werden nicht geladen. Dashboard-Tab "Module" zeigt Status + schaltet um (Hinweis: greift nach Neustart). Optionaler IPolyTraderModule.GetActivationBlocker fuer "nicht aktivierbar"-Info. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
886de3a85b
commit
039bc240f8
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Xml.Serialization;
|
||||
@@ -6,6 +7,15 @@ namespace PolyTraderSharp.Models
|
||||
{
|
||||
public class ServerSettings
|
||||
{
|
||||
[Category("Module")]
|
||||
[DisplayName("Deaktivierte Module")]
|
||||
[Description("Namen der Module (z.B. \"CopyTrading\"), die beim App-Start NICHT geladen werden. " +
|
||||
"Leer = alle Module aktiv. Änderungen greifen nach dem nächsten Neustart. " +
|
||||
"Wird i.d.R. über das Dashboard gepflegt.")]
|
||||
[XmlArray("DisabledModules")]
|
||||
[XmlArrayItem("Module")]
|
||||
public List<string> DisabledModules { get; set; } = new();
|
||||
|
||||
[Category("Threema Notifications")]
|
||||
[DisplayName("Threema Enabled")]
|
||||
[Description("Enable or disable Threema notifications.")]
|
||||
|
||||
@@ -36,6 +36,13 @@ namespace PolyTrader.Core.Modularity
|
||||
/// </summary>
|
||||
void RegisterUi(IModuleUiHost host, IServiceProvider services);
|
||||
|
||||
/// <summary>
|
||||
/// Optionale Vorab-Prüfung, ob das Modul aktiviert werden kann. Gibt <c>null</c> zurück, wenn
|
||||
/// aktivierbar, sonst einen kurzen, im Dashboard anzeigbaren Grund (z.B. „Alchemy-API-Key fehlt").
|
||||
/// Default: immer aktivierbar. Module überschreiben dies bei harten Voraussetzungen.
|
||||
/// </summary>
|
||||
string? GetActivationBlocker(IConfiguration configuration) => null;
|
||||
|
||||
/// <summary>Wird nach der Core-Hydration gestartet (kein Anlaufen gegen leeren State).</summary>
|
||||
Task StartAsync(CancellationToken cancellationToken);
|
||||
|
||||
|
||||
@@ -24,8 +24,12 @@ namespace PolyTrader.Core.Modularity
|
||||
/// <summary>Optionale Sortierreihenfolge.</summary>
|
||||
public int Order { get; init; } = 0;
|
||||
|
||||
/// <summary>Optionales Icon (16x16) für Menü/Buttons. Wird von der Shell/den Menüs genutzt.</summary>
|
||||
public System.Drawing.Image? Icon { get; init; }
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// (Module referenzieren die App-Ressourcen nicht).
|
||||
/// </summary>
|
||||
public System.Drawing.Image? Icon { get; set; }
|
||||
|
||||
/// <summary>Erzeugt das anzuzeigende Fenster (frische Instanz je Öffnung).</summary>
|
||||
public Func<Form> CreateForm { get; init; } = () => new Form();
|
||||
@@ -53,6 +57,13 @@ namespace PolyTrader.Core.Modularity
|
||||
/// <summary>Holt das Hauptfenster (Launcher) in den Vordergrund.</summary>
|
||||
void ActivateMain();
|
||||
|
||||
/// <summary>
|
||||
/// Leitet das sichere Herunterfahren ein (Bestätigungsdialog mit Timer). Wird vom „Beenden"-
|
||||
/// Eintrag JEDES Fensters aufgerufen – auch aus Modul-Fenstern, die die App nicht kennen.
|
||||
/// Die konkrete Shell zeigt den Dialog und fährt bei Bestätigung geordnet herunter.
|
||||
/// </summary>
|
||||
void RequestShutdown();
|
||||
|
||||
/// <summary>Feuert, wenn sich der Offen-Status irgendeiner View ändert.</summary>
|
||||
event Action? OpenStateChanged;
|
||||
}
|
||||
|
||||
@@ -6,48 +6,86 @@ using System.Windows.Forms;
|
||||
namespace PolyTrader.Core.Modularity
|
||||
{
|
||||
/// <summary>
|
||||
/// Baut das gemeinsame „Fenster"-Menü, das auf JEDEM PolyTrader-Fenster erscheint und das Wechseln
|
||||
/// 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
|
||||
/// <see cref="IModuleUiHost"/> nutzt, funktioniert es auch aus Modul-Fenstern (die die App nicht kennen).
|
||||
///
|
||||
/// Der Menü-Container liegt im Designer jedes Fensters (ein <see cref="ToolStripMenuItem"/> „Fenster");
|
||||
/// die dynamische Liste (Offen-Status ändert sich) wird hier beim Aufklappen frisch gefüllt:
|
||||
/// offene Fenster sind angehakt, das aktuelle Fenster ist fett + angehakt markiert.
|
||||
/// Die Fenster werden NICHT mehr in einem Untermenü „Fenster" versteckt, sondern als eigenständige
|
||||
/// Einträge (mit Icon) direkt nebeneinander in der obersten Menüleiste aufgelistet. Das aktuelle
|
||||
/// Fenster ist fett + angehakt, weitere offene Fenster sind angehakt.
|
||||
/// Rechts liegt eine kontextabhängige Aktion: NUR auf dem Launcher (Hauptprozess) „Beenden"
|
||||
/// (fährt die gesamte App über die Sicherheitsabfrage herunter); auf allen anderen Fenstern
|
||||
/// „Fenster schließen" (schließt nur dieses Fenster – ohne Einfluss auf laufende Module).
|
||||
/// Die Leiste wird bei jeder Statusänderung (Fenster geöffnet/geschlossen) neu aufgebaut.
|
||||
/// </summary>
|
||||
public static class WindowMenu
|
||||
{
|
||||
/// <summary>Verdrahtet ein Designer-„Fenster"-Menüelement mit der Fensterliste (Neuaufbau beim Aufklappen).</summary>
|
||||
public static void Wire(ToolStripMenuItem fensterMenu, IModuleUiHost host, string? currentViewId)
|
||||
/// <summary>
|
||||
/// Verdrahtet eine Designer-<see cref="MenuStrip"/> mit der Fensterliste: füllt sie sofort und
|
||||
/// baut sie bei jeder Offen-Status-Änderung neu auf. Die Registrierung wird sauber gelöst, sobald
|
||||
/// die Menüleiste (mit ihrem Fenster) entsorgt wird – kein Event-Leak über die Host-Lebensdauer.
|
||||
/// </summary>
|
||||
public static void Wire(MenuStrip menu, IModuleUiHost host, string? currentViewId)
|
||||
{
|
||||
fensterMenu.DropDownOpening += (_, _) => Populate(fensterMenu, host, currentViewId);
|
||||
Populate(fensterMenu, host, currentViewId);
|
||||
void Refresh()
|
||||
{
|
||||
if (menu.IsDisposed) return;
|
||||
if (menu.IsHandleCreated && menu.InvokeRequired)
|
||||
{
|
||||
try { menu.BeginInvoke((Action)(() => Populate(menu, host, currentViewId))); }
|
||||
catch { /* Fenster wird gerade geschlossen */ }
|
||||
return;
|
||||
}
|
||||
Populate(menu, host, currentViewId);
|
||||
}
|
||||
|
||||
Populate(menu, host, currentViewId);
|
||||
host.OpenStateChanged += Refresh;
|
||||
menu.Disposed += (_, _) => host.OpenStateChanged -= Refresh;
|
||||
}
|
||||
|
||||
public static void Populate(ToolStripMenuItem fensterMenu, IModuleUiHost host, string? currentViewId)
|
||||
/// <summary>Baut die Menüleiste komplett neu auf (Launcher, alle Views nebeneinander, Beenden rechts).</summary>
|
||||
public static void Populate(MenuStrip menu, IModuleUiHost host, string? currentViewId)
|
||||
{
|
||||
fensterMenu.DropDownItems.Clear();
|
||||
menu.Items.Clear();
|
||||
|
||||
var launcher = new ToolStripMenuItem("Launcher") { Checked = currentViewId == null };
|
||||
if (currentViewId == null) launcher.Font = new Font(launcher.Font, FontStyle.Bold);
|
||||
launcher.Click += (_, _) => host.ActivateMain();
|
||||
fensterMenu.DropDownItems.Add(launcher);
|
||||
fensterMenu.DropDownItems.Add(new ToolStripSeparator());
|
||||
menu.Items.Add(launcher);
|
||||
|
||||
foreach (var view in host.Views.OrderBy(v => v.Order).ThenBy(v => v.Title))
|
||||
{
|
||||
bool isCurrent = view.Id == currentViewId;
|
||||
var item = new ToolStripMenuItem(view.Title) { Image = view.Icon };
|
||||
item.Checked = isCurrent || host.IsOpen(view.Id);
|
||||
var item = new ToolStripMenuItem(view.Title)
|
||||
{
|
||||
Image = view.Icon,
|
||||
ImageScaling = ToolStripItemImageScaling.SizeToFit,
|
||||
DisplayStyle = view.Icon != null
|
||||
? ToolStripItemDisplayStyle.ImageAndText
|
||||
: ToolStripItemDisplayStyle.Text,
|
||||
Checked = isCurrent || host.IsOpen(view.Id)
|
||||
};
|
||||
if (isCurrent) item.Font = new Font(item.Font, FontStyle.Bold);
|
||||
string id = view.Id;
|
||||
item.Click += (_, _) => host.OpenView(id);
|
||||
fensterMenu.DropDownItems.Add(item);
|
||||
menu.Items.Add(item);
|
||||
}
|
||||
|
||||
fensterMenu.DropDownItems.Add(new ToolStripSeparator());
|
||||
var exit = new ToolStripMenuItem("Beenden");
|
||||
exit.Click += (_, _) => Application.Exit();
|
||||
fensterMenu.DropDownItems.Add(exit);
|
||||
// Kontextabhängige rechte Aktion: nur der Launcher darf die App beenden (Sicherheitsabfrage);
|
||||
// jedes andere Fenster bietet nur „Fenster schließen" (kein App-Shutdown, Module laufen weiter).
|
||||
if (currentViewId == null)
|
||||
{
|
||||
var exit = new ToolStripMenuItem("Beenden") { Alignment = ToolStripItemAlignment.Right };
|
||||
exit.Click += (_, _) => host.RequestShutdown();
|
||||
menu.Items.Add(exit);
|
||||
}
|
||||
else
|
||||
{
|
||||
var close = new ToolStripMenuItem("Fenster schließen") { Alignment = ToolStripItemAlignment.Right };
|
||||
close.Click += (_, _) => menu.FindForm()?.Close();
|
||||
menu.Items.Add(close);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user