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:
Richard
2026-07-23 18:46:09 +02:00
co-authored by Claude Opus 4.8
parent 886de3a85b
commit 039bc240f8
13 changed files with 685 additions and 61 deletions
+57 -19
View File
@@ -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);
}
}
}
}