Core, alle drei Module und das Testprojekt tragen keinen UI-Code mehr und bauen fuer linux-x64. Nur noch IBKRTrader.App ist Windows-gebunden. UI-Contract toolkit-neutral (Vorbild: PolytraderSharp): - ModuleView.CreateForm (Func<Form>) -> CreateView (Func<object>) - ModuleView.Icon (System.Drawing.Image) -> IconKey (string). System.Drawing.Common ist seit .NET 7 Windows-only und wirft auf Linux. - WindowMenu.cs war reine WinForms-Umsetzung -> in die Shell verschoben. LoggingService haelt keine RichTextBox mehr, sondern meldet Eintraege ueber event EntryWritten. Einfaerbung und UI-Thread-Wechsel liegen jetzt im LogPanelController der Shell. Nebenbei: ToUpper() -> ToUpperInvariant() (tr-TR haette aus "info" ein "İNFO" gemacht) und \r\n -> Environment.NewLine. Die drei Modul-Fenster liegen jetzt unter UI/Views/Modules/; RegisterUi der Module ist bewusst leer, die Shell registriert sie zentral ueber UI/ModuleViews.cs (nur fuer tatsaechlich geladene Module). ViewIcons loest IconKey gegen die PNG-Ressourcen auf - dieselben Schluessel bekommt spaeter die Avalonia-Shell. UiConstructionTests entfernt: die Konstruktionspruefung deckt --smoke-ui ab, das Testprojekt braucht dafuer keine UI-Referenz mehr. Der Test RegisterUi_RegistersMainView haelt jetzt das Gegenteil fest - das Modul darf nichts registrieren, sonst waere es wieder toolkit-gebunden. Verifiziert: Build 0 Fehler/0 Warnungen, 163 Tests gruen, --smoke-ui konstruiert alle 7 Fenster, und Core + 3 Module + Tests bauen fuer linux-x64. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
110 lines
3.9 KiB
C#
110 lines
3.9 KiB
C#
using IBKRTrader.Core.Modularity;
|
||
|
||
namespace IBKRTrader.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).
|
||
/// Vorbild: PolytraderSharp <c>ShellUiHost</c>.
|
||
/// </summary>
|
||
public sealed class ShellUiHost : IModuleUiHost
|
||
{
|
||
private readonly List<ModuleView> _views = new();
|
||
private readonly Dictionary<string, Form> _open = new(StringComparer.OrdinalIgnoreCase);
|
||
private Form? _mainWindow;
|
||
|
||
/// <summary>True, sobald das Beenden bestätigt wurde (der Launcher wertet das in FormClosing aus).</summary>
|
||
public bool ShutdownConfirmed { get; private set; }
|
||
|
||
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;
|
||
|
||
public bool IsOpen(string viewId) =>
|
||
_open.TryGetValue(viewId, out var form) && !form.IsDisposed;
|
||
|
||
public void ActivateMain()
|
||
{
|
||
if (_mainWindow is null || _mainWindow.IsDisposed) return;
|
||
if (_mainWindow.WindowState == FormWindowState.Minimized)
|
||
_mainWindow.WindowState = FormWindowState.Normal;
|
||
_mainWindow.BringToFront();
|
||
_mainWindow.Activate();
|
||
}
|
||
|
||
/// <summary>Sicherheitsabfrage; bei Bestätigung wird die Message-Loop beendet.</summary>
|
||
public void RequestShutdown()
|
||
{
|
||
if (ShutdownConfirmed) return;
|
||
var owner = _mainWindow is { IsDisposed: false } ? _mainWindow : null;
|
||
var result = MessageBox.Show(owner,
|
||
"IBKRTrader wirklich beenden? Laufende Worker/Services werden gestoppt.",
|
||
"Beenden", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning);
|
||
if (result != DialogResult.OK) return;
|
||
|
||
ShutdownConfirmed = true;
|
||
Application.Exit();
|
||
}
|
||
|
||
public void OpenView(string viewId)
|
||
{
|
||
var view = _views.FirstOrDefault(v => v.Id == viewId);
|
||
if (view is not null) OpenView(view);
|
||
}
|
||
|
||
private 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;
|
||
}
|
||
|
||
// Der Core-Contract ist toolkit-neutral (CreateView liefert object). Ein anderer Typ als
|
||
// Form ist ein Programmierfehler und soll laut scheitern, nicht still ein leeres Fenster geben.
|
||
var form = (Form)view.CreateView();
|
||
if (string.IsNullOrEmpty(form.Text) || form.Text == form.Name)
|
||
form.Text = view.Title;
|
||
|
||
// Gemeinsames „Fenster"-Menü in jedes Fenster injizieren (identische Shell-Chrome).
|
||
if (form.MainMenuStrip is null)
|
||
AttachWindowMenu(form, view.Id);
|
||
|
||
_open[view.Id] = form;
|
||
form.FormClosed += (_, _) =>
|
||
{
|
||
_open.Remove(view.Id);
|
||
OpenStateChanged?.Invoke();
|
||
};
|
||
|
||
form.Show();
|
||
OpenStateChanged?.Invoke();
|
||
}
|
||
|
||
private void AttachWindowMenu(Form form, string currentViewId)
|
||
{
|
||
var menu = new MenuStrip { Dock = DockStyle.Top, ImageScalingSize = new Size(24, 24) };
|
||
WindowMenu.Wire(menu, this, currentViewId);
|
||
form.Controls.Add(menu);
|
||
form.MainMenuStrip = menu;
|
||
}
|
||
|
||
/// <summary>Schließt alle offenen View-Fenster (beim Herunterfahren).</summary>
|
||
public void CloseAllViews()
|
||
{
|
||
foreach (var form in _open.Values.ToList())
|
||
if (!form.IsDisposed)
|
||
form.Close();
|
||
_open.Clear();
|
||
}
|
||
}
|