Files
PolyTraderSharp/Ui/ShellUiHost.cs
T
RichardandClaude Opus 5 ad1593bfc1 P1a: PolyTrader.Core auf net8.0 - UI-Contract toolkit-neutral
Der Core traegt keine WinForms-/System.Drawing-Abhaengigkeit mehr und baut als
plattformneutrales net8.0 (verifiziert: publish -r linux-x64 erfolgreich).

- ModuleView.CreateForm (Func<Form>) -> CreateView (Func<object>): die Shell kennt
  ihr Toolkit und castet, der Core nicht. Avalonia kann denselben Contract nutzen.
- ModuleView.Icon (System.Drawing.Image, seit .NET 7 Windows-only) -> IconKey (string).
  Aufloesung Schluessel->Bildressource neu in Ui/ViewIcons.cs, ersetzt Program.AssignMenuIcons.
- WindowMenu.cs (reine WinForms-Logik) aus dem Core nach Ui/ verschoben.
- WindowMenuTests entfernt: testet die eingefrorene WinForms-Menuelogik, die das
  Testprojekt nach dem Core-Schnitt nicht mehr erreicht. Im Tag winforms-final erhalten;
  das Avalonia-Gegenstueck bekommt eigene Tests (P6/P9).

Module und App bleiben vorerst net8.0-windows - ihre UI zieht erst mit der
Avalonia-Portierung um. Windows-App unveraendert lauffaehig (--smoke-ui gruen).

442 Tests gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:10:41 +02:00

135 lines
5.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using PolyTrader.Core.Modularity;
namespace PolyTraderSharp.Ui
{
/// <summary>
/// Verwaltet die registrierten Fenster-Views: öffnet sie als eigenständige Forms,
/// hält je View höchstens eine Instanz offen und holt ein bereits offenes Fenster
/// wieder in den Vordergrund. Meldet Änderungen am Offen-Status (für die
/// Button-Markierung im Launcher).
/// </summary>
public class ShellUiHost : IModuleUiHost
{
private readonly List<ModuleView> _views = new();
private readonly Dictionary<string, Form> _open = new();
private Form? _mainWindow;
private bool _shutdownDialogOpen;
/// <summary>
/// True, sobald das Herunterfahren über die Sicherheitsabfrage bestätigt wurde. Der Launcher
/// wertet das in FormClosing aus, um beim Schließen-X denselben Dialog zu erzwingen.
/// </summary>
public bool ShutdownConfirmed { get; private set; }
/// <summary>Wird ausgelöst, wenn sich der Offen-Status irgendeiner View ändert.</summary>
public event Action? OpenStateChanged;
public IReadOnlyList<ModuleView> Views => _views;
public void RegisterView(ModuleView view) => _views.Add(view);
/// <summary>Setzt das Hauptfenster (Launcher) Ziel für <see cref="ActivateMain"/>.</summary>
public void SetMainWindow(Form main) => _mainWindow = main;
/// <summary>
/// Fügt einem Fenster die gemeinsame Fenster-Menüleiste hinzu (oberste Leiste, alle Fenster
/// nebeneinander mit Icon). Als letztes hinzugefügtes Top-Control belegt der MenuStrip die
/// oberste Zeile über etwaigen ToolStrips.
/// </summary>
private void AttachWindowMenu(Form form, string currentViewId)
{
var menu = new MenuStrip { Dock = DockStyle.Top, ImageScalingSize = new System.Drawing.Size(24, 24) };
WindowMenu.Wire(menu, this, currentViewId);
form.Controls.Add(menu);
form.MainMenuStrip = menu;
}
public void ActivateMain()
{
if (_mainWindow == null || _mainWindow.IsDisposed) return;
if (_mainWindow.WindowState == FormWindowState.Minimized)
_mainWindow.WindowState = FormWindowState.Maximized;
_mainWindow.BringToFront();
_mainWindow.Activate();
}
public bool IsOpen(string viewId) =>
_open.TryGetValue(viewId, out var form) && !form.IsDisposed;
/// <summary>
/// Zeigt die Sicherheitsabfrage (Timer-gesperrter Beenden-Button, Abbrechen jederzeit). Bei
/// Bestätigung wird die Message-Loop beendet das geordnete Herunterfahren der Module läuft
/// anschließend in Program.Main über <c>AppHost.StopAsync()</c> (nach <c>Application.Run</c>).
/// Aus jedem Fenster aufrufbar; der Dialog erscheint zentriert über dem Hauptfenster.
/// </summary>
public void RequestShutdown()
{
if (ShutdownConfirmed || _shutdownDialogOpen) return;
_shutdownDialogOpen = true;
try
{
using var dlg = new ShutdownConfirmDialog();
var owner = _mainWindow != null && !_mainWindow.IsDisposed ? _mainWindow : null;
var result = owner != null ? dlg.ShowDialog(owner) : dlg.ShowDialog();
if (result != DialogResult.OK) return;
ShutdownConfirmed = true;
Application.Exit(); // beendet die Message-Loop; StopAsync() fährt Module danach sauber herunter
}
finally
{
_shutdownDialogOpen = false;
}
}
public void OpenView(string viewId)
{
var view = _views.FirstOrDefault(v => v.Id == viewId);
if (view != null) OpenView(view);
}
public void OpenView(ModuleView view)
{
if (_open.TryGetValue(view.Id, out var existing) && !existing.IsDisposed)
{
if (existing.WindowState == FormWindowState.Minimized)
existing.WindowState = FormWindowState.Normal;
existing.BringToFront();
existing.Activate();
return;
}
// 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;
// Gemeinsames „Fenster"-Menü zentral in jedes Fenster injizieren (identische Shell-Chrome
// auf allen Fenstern; die inhaltlichen Controls bleiben designerbasiert). Nur, wenn das
// Fenster nicht bereits ein eigenes Menü mitbringt (z. B. der Launcher).
if (form.MainMenuStrip == null)
AttachWindowMenu(form, view.Id);
// Alle Fenster maximiert (Vorgabe): Full-HD-Ziel, skaliert auf größere Auflösungen.
form.StartPosition = FormStartPosition.CenterScreen;
form.WindowState = FormWindowState.Maximized;
_open[view.Id] = form;
form.FormClosed += (_, _) =>
{
_open.Remove(view.Id);
OpenStateChanged?.Invoke();
};
form.Show();
OpenStateChanged?.Invoke();
}
}
}