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
+53 -4
View File
@@ -27,6 +27,9 @@ internal static class Program
{ {
public static IHost? AppHost { get; private set; } public static IHost? AppHost { get; private set; }
/// <summary>Pfad der (gitignorierten) Server-Settings-Datei Quelle u.a. für die Modul-Aktivierung.</summary>
private const string ServerSettingsPath = "server_settings.xml";
[STAThread] [STAThread]
private static void Main(string[] args) private static void Main(string[] args)
{ {
@@ -62,13 +65,21 @@ internal static class Program
ApplicationConfiguration.Initialize(); ApplicationConfiguration.Initialize();
var modules = new System.Collections.Generic.List<IPolyTraderModule> // Server-Settings früh laden (VOR der DI-Registrierung), damit deaktivierte Module gar nicht
// erst geladen werden. Dieselbe Instanz wird als Singleton weitergereicht (eine Quelle).
var serverSettings = PolyTraderSharp.Models.ServerSettings.Load(ServerSettingsPath);
// Alle bekannten Module; „modules" enthält nur die aktiven (nicht in DisabledModules).
var allModules = new System.Collections.Generic.List<IPolyTraderModule>
{ {
new CopyTradingModule(), new CopyTradingModule(),
new ResolutionFarmingModule(), new ResolutionFarmingModule(),
new SupervisorModule(), new SupervisorModule(),
new AccountingModule() new AccountingModule()
}; };
var disabledModules = new System.Collections.Generic.HashSet<string>(
serverSettings.DisabledModules, StringComparer.OrdinalIgnoreCase);
var modules = allModules.Where(m => !disabledModules.Contains(m.Name)).ToList();
AppHost = Host.CreateDefaultBuilder() AppHost = Host.CreateDefaultBuilder()
// Config immer neben der EXE suchen (nicht im Arbeitsverzeichnis), damit die App // Config immer neben der EXE suchen (nicht im Arbeitsverzeichnis), damit die App
@@ -87,7 +98,7 @@ internal static class Program
}; };
services.Configure<DatabaseOptions>(context.Configuration.GetSection(DatabaseOptions.SectionName)); services.Configure<DatabaseOptions>(context.Configuration.GetSection(DatabaseOptions.SectionName));
services.AddCorePersistence(databaseOptions); services.AddCorePersistence(databaseOptions);
services.AddSingleton((IServiceProvider sp) => ServerSettings.Load("server_settings.xml")); services.AddSingleton(serverSettings);
services.AddSingleton<TradingState>(); services.AddSingleton<TradingState>();
services.AddSingleton(delegate(IServiceProvider sp) services.AddSingleton(delegate(IServiceProvider sp)
{ {
@@ -148,11 +159,15 @@ internal static class Program
try try
{ {
// Trade-Nummerierung fortsetzen: höchste bestehende TradeId serverseitig lesen // Trade-Nummerierung fortsetzen: höchste bestehende TradeId serverseitig lesen
// (M3: kein Full-Table-Load mehr über Find(_=>true).Max()). // (M3: kein Full-Table-Load mehr über Find(_=>true).Max()). Nur wenn CopyTrading aktiv ist
// sonst sind dessen Services (CopyTradingState/…) gar nicht registriert.
if (modules.Any(m => m is CopyTradingModule))
{
var copyState = AppHost.Services.GetRequiredService<CopyTradingState>(); var copyState = AppHost.Services.GetRequiredService<CopyTradingState>();
var tradeLog = AppHost.Services.GetRequiredService<ICopyTradeLogRepository>(); var tradeLog = AppHost.Services.GetRequiredService<ICopyTradeLogRepository>();
copyState.TotalCopyTrades = tradeLog.GetMaxTradeId(); copyState.TotalCopyTrades = tradeLog.GetMaxTradeId();
} }
}
catch (Exception ex) catch (Exception ex)
{ {
// M3: NICHT still schlucken. Bliebe der Zähler bei 0, kollidierten neue TradeIds mit // M3: NICHT still schlucken. Bliebe der Zähler bei 0, kollidierten neue TradeIds mit
@@ -223,9 +238,17 @@ internal static class Program
CreateForm = () => CreateForm = () =>
{ {
var view = new PolyTraderSharp.Ui.Views.DashboardView(); var view = new PolyTraderSharp.Ui.Views.DashboardView();
var config = viewServices.GetRequiredService<Microsoft.Extensions.Configuration.IConfiguration>();
// Alle Module (auch deaktivierte) fürs Dashboard: läuft-Status = ist in dieser Session geladen.
var moduleInfos = allModules
.Select(m => new PolyTraderSharp.Ui.Views.ModuleActivationInfo(
m.Name, modules.Contains(m), m.GetActivationBlocker(config)))
.ToList();
view.Initialize( view.Initialize(
viewServices.GetRequiredService<PolyTrader.Core.Persistence.ITradeLogRepository>(), viewServices.GetRequiredService<PolyTrader.Core.Persistence.ITradeLogRepository>(),
viewServices.GetRequiredService<TradingState>()); viewServices.GetRequiredService<TradingState>(),
moduleInfos,
ServerSettingsPath);
return view; return view;
} }
}); });
@@ -235,11 +258,37 @@ internal static class Program
module.RegisterUi(uiHost, viewServices); module.RegisterUi(uiHost, viewServices);
} }
// Menü-Icons zentral zuweisen: Die App kennt alle Fenster-Ressourcen; Core und Module
// referenzieren sie nicht. So erscheint jedes Fenster mit Icon in der obersten Menüleiste.
AssignMenuIcons(uiHost);
var launcher = AppHost.Services.GetRequiredService<PolyTraderSharp.Ui.LauncherForm>(); var launcher = AppHost.Services.GetRequiredService<PolyTraderSharp.Ui.LauncherForm>();
Application.Run(launcher); Application.Run(launcher);
AppHost.StopAsync().GetAwaiter().GetResult(); AppHost.StopAsync().GetAwaiter().GetResult();
} }
/// <summary>
/// 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.
/// </summary>
private static void AssignMenuIcons(PolyTraderSharp.Ui.ShellUiHost uiHost)
{
var map = new System.Collections.Generic.Dictionary<string, System.Drawing.Image>
{
["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;
}
private static void RunConfigMigrationFromJson(string folder) private static void RunConfigMigrationFromJson(string folder)
{ {
var config = new Microsoft.Extensions.Configuration.ConfigurationBuilder() var config = new Microsoft.Extensions.Configuration.ConfigurationBuilder()
+1 -9
View File
@@ -18,7 +18,6 @@ namespace PolyTraderSharp.Ui
private void InitializeComponent() private void InitializeComponent()
{ {
menuStrip = new MenuStrip(); menuStrip = new MenuStrip();
miFenster = new ToolStripMenuItem();
toolstrip_windows = new ToolStrip(); toolstrip_windows = new ToolStrip();
btn_dashboard = new ToolStripButton(); btn_dashboard = new ToolStripButton();
btn_settings = new ToolStripButton(); btn_settings = new ToolStripButton();
@@ -56,19 +55,13 @@ namespace PolyTraderSharp.Ui
// menuStrip // menuStrip
// //
menuStrip.ImageScalingSize = new Size(24, 24); menuStrip.ImageScalingSize = new Size(24, 24);
menuStrip.Items.AddRange(new ToolStripItem[] { miFenster }); // Einträge werden zur Laufzeit über WindowMenu.Wire gefüllt (alle Fenster nebeneinander mit Icon).
menuStrip.Location = new Point(0, 0); menuStrip.Location = new Point(0, 0);
menuStrip.Name = "menuStrip"; menuStrip.Name = "menuStrip";
menuStrip.Padding = new Padding(9, 3, 0, 3); menuStrip.Padding = new Padding(9, 3, 0, 3);
menuStrip.Size = new Size(2599, 35); menuStrip.Size = new Size(2599, 35);
menuStrip.TabIndex = 0; menuStrip.TabIndex = 0;
// //
// miFenster
//
miFenster.Name = "miFenster";
miFenster.Size = new Size(85, 29);
miFenster.Text = "Fenster";
//
// toolstrip_windows // toolstrip_windows
// //
toolstrip_windows.AutoSize = false; toolstrip_windows.AutoSize = false;
@@ -345,7 +338,6 @@ namespace PolyTraderSharp.Ui
#endregion #endregion
private System.Windows.Forms.MenuStrip menuStrip; private System.Windows.Forms.MenuStrip menuStrip;
private System.Windows.Forms.ToolStripMenuItem miFenster;
private ToolStrip toolstrip_windows; private ToolStrip toolstrip_windows;
private ToolStripButton btn_settings; private ToolStripButton btn_settings;
private ToolStripButton btn_terminal; private ToolStripButton btn_terminal;
+19 -2
View File
@@ -60,8 +60,9 @@ namespace PolyTraderSharp.Ui
btn.Enabled = false; // Modul/View nicht verfügbar btn.Enabled = false; // Modul/View nicht verfügbar
} }
// Gemeinsames „Fenster"-Menü (Launcher ist das aktuelle Fenster → currentViewId null). // Gemeinsame Fenster-Menüleiste: alle Fenster nebeneinander mit Icon (Launcher ist das
PolyTrader.Core.Modularity.WindowMenu.Wire(miFenster, _uiHost, null); // aktuelle Fenster → currentViewId null).
PolyTrader.Core.Modularity.WindowMenu.Wire(menuStrip, _uiHost, null);
btn_liveTrading.Click += (_, _) => CycleLiveTrading(); btn_liveTrading.Click += (_, _) => CycleLiveTrading();
btn_demoTrading.Click += (_, _) => CycleDemoTrading(); btn_demoTrading.Click += (_, _) => CycleDemoTrading();
@@ -86,6 +87,22 @@ namespace PolyTraderSharp.Ui
UpdateWindowButtonStates(); UpdateWindowButtonStates();
} }
/// <summary>
/// Fängt das Schließen des Launchers ab: Statt direkt zu beenden, läuft auch das Schließen-X
/// über die Sicherheitsabfrage (siehe <see cref="ShellUiHost.RequestShutdown"/>). Erst wenn dort
/// bestätigt wurde (<see cref="ShellUiHost.ShutdownConfirmed"/>), darf das Fenster schließen.
/// </summary>
protected override void OnFormClosing(FormClosingEventArgs e)
{
if (!_uiHost.ShutdownConfirmed)
{
e.Cancel = true;
BeginInvoke((Action)(() => _uiHost.RequestShutdown()));
return;
}
base.OnFormClosing(e);
}
private void UpdateWindowButtonStates() private void UpdateWindowButtonStates()
{ {
if (IsDisposed) return; if (IsDisposed) return;
+38 -6
View File
@@ -17,6 +17,13 @@ namespace PolyTraderSharp.Ui
private readonly List<ModuleView> _views = new(); private readonly List<ModuleView> _views = new();
private readonly Dictionary<string, Form> _open = new(); private readonly Dictionary<string, Form> _open = new();
private Form? _mainWindow; 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> /// <summary>Wird ausgelöst, wenn sich der Offen-Status irgendeiner View ändert.</summary>
public event Action? OpenStateChanged; public event Action? OpenStateChanged;
@@ -29,15 +36,14 @@ namespace PolyTraderSharp.Ui
public void SetMainWindow(Form main) => _mainWindow = main; public void SetMainWindow(Form main) => _mainWindow = main;
/// <summary> /// <summary>
/// Fügt einem Fenster das gemeinsame Fenster"-Menü hinzu (oberste Leiste). Als letztes /// Fügt einem Fenster die gemeinsame Fenster-Menüleiste hinzu (oberste Leiste, alle Fenster
/// hinzugefügtes Top-Control belegt der MenuStrip die oberste Zeile über etwaigen ToolStrips. /// nebeneinander mit Icon). Als letztes hinzugefügtes Top-Control belegt der MenuStrip die
/// oberste Zeile über etwaigen ToolStrips.
/// </summary> /// </summary>
private void AttachWindowMenu(Form form, string currentViewId) private void AttachWindowMenu(Form form, string currentViewId)
{ {
var menu = new MenuStrip { Dock = DockStyle.Top }; var menu = new MenuStrip { Dock = DockStyle.Top, ImageScalingSize = new System.Drawing.Size(24, 24) };
var fenster = new ToolStripMenuItem("Fenster"); PolyTrader.Core.Modularity.WindowMenu.Wire(menu, this, currentViewId);
menu.Items.Add(fenster);
PolyTrader.Core.Modularity.WindowMenu.Wire(fenster, this, currentViewId);
form.Controls.Add(menu); form.Controls.Add(menu);
form.MainMenuStrip = menu; form.MainMenuStrip = menu;
} }
@@ -54,6 +60,32 @@ namespace PolyTraderSharp.Ui
public bool IsOpen(string viewId) => public bool IsOpen(string viewId) =>
_open.TryGetValue(viewId, out var form) && !form.IsDisposed; _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) public void OpenView(string viewId)
{ {
var view = _views.FirstOrDefault(v => v.Id == viewId); var view = _views.FirstOrDefault(v => v.Id == viewId);
+120
View File
@@ -0,0 +1,120 @@
namespace PolyTraderSharp.Ui
{
partial class ShutdownConfirmDialog
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Vom Komponenten-Designer generierter Code
private void InitializeComponent()
{
components = new System.ComponentModel.Container();
lblTitle = new System.Windows.Forms.Label();
lblInfo = new System.Windows.Forms.Label();
lblCountdown = new System.Windows.Forms.Label();
btnConfirm = new System.Windows.Forms.Button();
btnCancel = new System.Windows.Forms.Button();
countdownTimer = new System.Windows.Forms.Timer(components);
SuspendLayout();
//
// lblTitle
//
lblTitle.AutoSize = true;
lblTitle.Font = new System.Drawing.Font("Segoe UI", 12F, System.Drawing.FontStyle.Bold);
lblTitle.Location = new System.Drawing.Point(18, 16);
lblTitle.Name = "lblTitle";
lblTitle.Size = new System.Drawing.Size(300, 28);
lblTitle.TabIndex = 0;
lblTitle.Text = "PolyTrader sicher beenden?";
//
// lblInfo
//
lblInfo.Location = new System.Drawing.Point(20, 52);
lblInfo.Name = "lblInfo";
lblInfo.Size = new System.Drawing.Size(464, 96);
lblInfo.TabIndex = 1;
lblInfo.Text = "PolyTrader und alle Module werden geordnet heruntergefahren. Laufende API-Aufrufe, "
+ "Order- und Trading-Vorgänge werden dabei sauber abgeschlossen.\r\n\r\n"
+ "Aus Sicherheit ist die Beenden-Schaltfläche erst nach Ablauf eines kurzen Timers "
+ "aktiv. Abbrechen ist jederzeit möglich.";
//
// lblCountdown
//
lblCountdown.AutoSize = true;
lblCountdown.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Bold);
lblCountdown.ForeColor = System.Drawing.Color.Firebrick;
lblCountdown.Location = new System.Drawing.Point(20, 158);
lblCountdown.Name = "lblCountdown";
lblCountdown.Size = new System.Drawing.Size(200, 23);
lblCountdown.TabIndex = 2;
lblCountdown.Text = "Beenden möglich in 10 s …";
//
// btnConfirm
//
btnConfirm.DialogResult = System.Windows.Forms.DialogResult.OK;
btnConfirm.Enabled = false;
btnConfirm.Location = new System.Drawing.Point(232, 196);
btnConfirm.Name = "btnConfirm";
btnConfirm.Size = new System.Drawing.Size(140, 36);
btnConfirm.TabIndex = 3;
btnConfirm.Text = "Beenden (10 s)";
btnConfirm.UseVisualStyleBackColor = true;
//
// btnCancel
//
btnCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel;
btnCancel.Location = new System.Drawing.Point(380, 196);
btnCancel.Name = "btnCancel";
btnCancel.Size = new System.Drawing.Size(104, 36);
btnCancel.TabIndex = 4;
btnCancel.Text = "Abbrechen";
btnCancel.UseVisualStyleBackColor = true;
//
// countdownTimer
//
countdownTimer.Interval = 1000;
countdownTimer.Tick += CountdownTimer_Tick;
//
// ShutdownConfirmDialog
//
AcceptButton = btnConfirm;
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
CancelButton = btnCancel;
ClientSize = new System.Drawing.Size(504, 248);
Controls.Add(btnCancel);
Controls.Add(btnConfirm);
Controls.Add(lblCountdown);
Controls.Add(lblInfo);
Controls.Add(lblTitle);
FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
Name = "ShutdownConfirmDialog";
ShowIcon = false;
ShowInTaskbar = false;
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
Text = "PolyTrader beenden";
ResumeLayout(false);
PerformLayout();
}
#endregion
private System.Windows.Forms.Label lblTitle;
private System.Windows.Forms.Label lblInfo;
private System.Windows.Forms.Label lblCountdown;
private System.Windows.Forms.Button btnConfirm;
private System.Windows.Forms.Button btnCancel;
private System.Windows.Forms.Timer countdownTimer;
}
}
+62
View File
@@ -0,0 +1,62 @@
using System;
using System.Windows.Forms;
namespace PolyTraderSharp.Ui
{
/// <summary>
/// Sicherheitsabfrage vor dem Herunterfahren. Die Beenden-Schaltfläche ist erst nach Ablauf eines
/// kurzen Timers klickbar (verhindert versehentliches Beenden, z.B. während auf eine API-Antwort
/// gewartet wird); „Abbrechen" ist jederzeit möglich. Bei Bestätigung liefert der Dialog
/// <see cref="DialogResult.OK"/> das eigentliche geordnete Herunterfahren übernimmt der Aufrufer.
/// </summary>
public partial class ShutdownConfirmDialog : Form
{
private int _remaining;
/// <param name="delaySeconds">Sekunden, bis „Beenden" freigeschaltet wird (Standard 10).</param>
public ShutdownConfirmDialog(int delaySeconds = 10)
{
InitializeComponent();
_remaining = Math.Max(0, delaySeconds);
UpdateCountdownUi();
}
protected override void OnShown(EventArgs e)
{
base.OnShown(e);
if (_remaining <= 0)
{
EnableConfirm();
return;
}
countdownTimer.Start();
}
private void CountdownTimer_Tick(object? sender, EventArgs e)
{
_remaining--;
if (_remaining <= 0)
{
countdownTimer.Stop();
EnableConfirm();
return;
}
UpdateCountdownUi();
}
private void UpdateCountdownUi()
{
lblCountdown.Text = $"Beenden möglich in {_remaining} s …";
btnConfirm.Text = $"Beenden ({_remaining} s)";
}
private void EnableConfirm()
{
btnConfirm.Enabled = true;
btnConfirm.Text = "Jetzt beenden";
lblCountdown.Text = "PolyTrader kann jetzt beendet werden.";
lblCountdown.ForeColor = System.Drawing.Color.ForestGreen;
btnConfirm.Focus();
}
}
}
+96
View File
@@ -58,8 +58,17 @@ namespace PolyTraderSharp.Ui.Views
lblErgebnis = new Label(); lblErgebnis = new Label();
tbSearch = new TextBox(); tbSearch = new TextBox();
lblSuche = new Label(); lblSuche = new Label();
tabModules = new TabPage();
dgvModules = new DataGridView();
colModName = new DataGridViewTextBoxColumn();
colModStatus = new DataGridViewTextBoxColumn();
colModHint = new DataGridViewTextBoxColumn();
colModAction = new DataGridViewButtonColumn();
lblModulesInfo = new Label();
toolStripDash.SuspendLayout(); toolStripDash.SuspendLayout();
tabControlDash.SuspendLayout(); tabControlDash.SuspendLayout();
tabModules.SuspendLayout();
((System.ComponentModel.ISupportInitialize)dgvModules).BeginInit();
tabDashboard.SuspendLayout(); tabDashboard.SuspendLayout();
((System.ComponentModel.ISupportInitialize)picEquity).BeginInit(); ((System.ComponentModel.ISupportInitialize)picEquity).BeginInit();
pnlChartsBottom.SuspendLayout(); pnlChartsBottom.SuspendLayout();
@@ -140,6 +149,7 @@ namespace PolyTraderSharp.Ui.Views
// //
tabControlDash.Controls.Add(tabDashboard); tabControlDash.Controls.Add(tabDashboard);
tabControlDash.Controls.Add(tabHistory); tabControlDash.Controls.Add(tabHistory);
tabControlDash.Controls.Add(tabModules);
tabControlDash.Dock = DockStyle.Fill; tabControlDash.Dock = DockStyle.Fill;
tabControlDash.Location = new Point(0, 34); tabControlDash.Location = new Point(0, 34);
tabControlDash.Margin = new Padding(4, 5, 4, 5); tabControlDash.Margin = new Padding(4, 5, 4, 5);
@@ -453,6 +463,83 @@ namespace PolyTraderSharp.Ui.Views
lblSuche.TabIndex = 0; lblSuche.TabIndex = 0;
lblSuche.Text = "Suche:"; lblSuche.Text = "Suche:";
// //
// tabModules
//
tabModules.Controls.Add(dgvModules);
tabModules.Controls.Add(lblModulesInfo);
tabModules.Location = new Point(4, 34);
tabModules.Margin = new Padding(4, 5, 4, 5);
tabModules.Name = "tabModules";
tabModules.Padding = new Padding(12, 12, 12, 12);
tabModules.Size = new Size(1706, 1000);
tabModules.TabIndex = 2;
tabModules.Text = "Module";
tabModules.UseVisualStyleBackColor = true;
//
// lblModulesInfo
//
lblModulesInfo.Dock = DockStyle.Top;
lblModulesInfo.Font = new Font("Segoe UI", 10F);
lblModulesInfo.Location = new Point(12, 12);
lblModulesInfo.Name = "lblModulesInfo";
lblModulesInfo.Padding = new Padding(4, 8, 4, 12);
lblModulesInfo.Size = new Size(1682, 56);
lblModulesInfo.TabIndex = 0;
lblModulesInfo.Text = "Module aktivieren/deaktivieren. Änderungen greifen nach dem nächsten Neustart von PolyTrader.";
//
// dgvModules
//
dgvModules.AllowUserToAddRows = false;
dgvModules.AllowUserToDeleteRows = false;
dgvModules.AllowUserToResizeRows = false;
dgvModules.AutoGenerateColumns = false;
dgvModules.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
dgvModules.Columns.AddRange(new DataGridViewColumn[] { colModName, colModStatus, colModHint, colModAction });
dgvModules.Dock = DockStyle.Fill;
dgvModules.Location = new Point(12, 68);
dgvModules.Margin = new Padding(4, 5, 4, 5);
dgvModules.Name = "dgvModules";
dgvModules.ReadOnly = true;
dgvModules.RowHeadersVisible = false;
dgvModules.RowHeadersWidth = 62;
dgvModules.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
dgvModules.Size = new Size(1682, 920);
dgvModules.TabIndex = 1;
//
// colModName
//
colModName.DataPropertyName = "Modul";
colModName.HeaderText = "Modul";
colModName.Name = "colModName";
colModName.ReadOnly = true;
colModName.Width = 220;
//
// colModStatus
//
colModStatus.DataPropertyName = "Status";
colModStatus.HeaderText = "Status";
colModStatus.Name = "colModStatus";
colModStatus.ReadOnly = true;
colModStatus.Width = 260;
//
// colModHint
//
colModHint.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
colModHint.DataPropertyName = "Hinweis";
colModHint.HeaderText = "Hinweis";
colModHint.Name = "colModHint";
colModHint.ReadOnly = true;
//
// colModAction
//
colModAction.DataPropertyName = "Aktion";
colModAction.HeaderText = "Aktion";
colModAction.Name = "colModAction";
colModAction.ReadOnly = true;
colModAction.Text = "Umschalten";
colModAction.UseColumnTextForButtonValue = false;
colModAction.Width = 160;
//
// DashboardView // DashboardView
// //
AutoScaleDimensions = new SizeF(10F, 25F); AutoScaleDimensions = new SizeF(10F, 25F);
@@ -477,6 +564,8 @@ namespace PolyTraderSharp.Ui.Views
((System.ComponentModel.ISupportInitialize)dgvTrades).EndInit(); ((System.ComponentModel.ISupportInitialize)dgvTrades).EndInit();
pnlHistFilters.ResumeLayout(false); pnlHistFilters.ResumeLayout(false);
pnlHistFilters.PerformLayout(); pnlHistFilters.PerformLayout();
tabModules.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)dgvModules).EndInit();
ResumeLayout(false); ResumeLayout(false);
PerformLayout(); PerformLayout();
} }
@@ -523,5 +612,12 @@ namespace PolyTraderSharp.Ui.Views
private System.Windows.Forms.DataGridViewTextBoxColumn colPnl; private System.Windows.Forms.DataGridViewTextBoxColumn colPnl;
private System.Windows.Forms.DataGridViewTextBoxColumn colPnlPct; private System.Windows.Forms.DataGridViewTextBoxColumn colPnlPct;
private System.Windows.Forms.DataGridViewTextBoxColumn colClosedAt; private System.Windows.Forms.DataGridViewTextBoxColumn colClosedAt;
private System.Windows.Forms.TabPage tabModules;
private System.Windows.Forms.Label lblModulesInfo;
private System.Windows.Forms.DataGridView dgvModules;
private System.Windows.Forms.DataGridViewTextBoxColumn colModName;
private System.Windows.Forms.DataGridViewTextBoxColumn colModStatus;
private System.Windows.Forms.DataGridViewTextBoxColumn colModHint;
private System.Windows.Forms.DataGridViewButtonColumn colModAction;
} }
} }
+142 -1
View File
@@ -22,6 +22,9 @@ namespace PolyTraderSharp.Ui.Views
private ITradeLogRepository? _tradeLog; private ITradeLogRepository? _tradeLog;
private TradingState? _state; private TradingState? _state;
private IReadOnlyList<ModuleActivationInfo>? _moduleInfos; // alle Module (auch deaktivierte)
private string? _settingsPath; // Quelle/Ziel der Modul-Aktivierung
private List<TradeRecord> _allTrades = new(); // zuletzt geladener Roh-Satz private List<TradeRecord> _allTrades = new(); // zuletzt geladener Roh-Satz
private List<DashboardTradeRow> _historyBase = new(); // Scope-gefiltert, Basis für die Historie-Filter private List<DashboardTradeRow> _historyBase = new(); // Scope-gefiltert, Basis für die Historie-Filter
private bool _loading; private bool _loading;
@@ -48,13 +51,24 @@ namespace PolyTraderSharp.Ui.Views
cbRange.SelectedIndexChanged += (_, _) => { if (!_loading) ApplyScope(); }; cbRange.SelectedIndexChanged += (_, _) => { if (!_loading) ApplyScope(); };
tbSearch.TextChanged += (_, _) => { if (!_loading) ApplyHistoryFilter(); }; tbSearch.TextChanged += (_, _) => { if (!_loading) ApplyHistoryFilter(); };
cbWinLoss.SelectedIndexChanged += (_, _) => { if (!_loading) ApplyHistoryFilter(); }; cbWinLoss.SelectedIndexChanged += (_, _) => { if (!_loading) ApplyHistoryFilter(); };
dgvModules.CellContentClick += Modules_CellContentClick;
} }
public void Initialize(ITradeLogRepository tradeLog, TradingState state) /// <param name="modules">
/// Alle bekannten Module (auch deaktivierte) für den Tab „Module". <c>null</c> = kein
/// Modul-Management (Tab wird entfernt, z.B. im Headless-Smoke-Test).
/// </param>
/// <param name="settingsPath">Pfad der Server-Settings-Datei (Ziel für die Ein/Aus-Persistenz).</param>
public void Initialize(ITradeLogRepository tradeLog, TradingState state,
IReadOnlyList<ModuleActivationInfo>? modules = null, string? settingsPath = null)
{ {
_tradeLog = tradeLog; _tradeLog = tradeLog;
_state = state; _state = state;
_moduleInfos = modules;
_settingsPath = settingsPath;
RefreshData(); RefreshData();
InitModulesTab();
} }
// ===== Daten laden / Scope ===== // ===== Daten laden / Scope =====
@@ -251,7 +265,134 @@ namespace PolyTraderSharp.Ui.Views
{ {
public override string ToString() => Label; public override string ToString() => Label;
} }
// ===== Module aktivieren/deaktivieren (neustart-basiert) =====
private void InitModulesTab()
{
if (_moduleInfos == null || string.IsNullOrEmpty(_settingsPath))
{
// Kein Modul-Management (z.B. Smoke-Test) -> Tab entfernen statt leer anzuzeigen.
if (tabControlDash.TabPages.Contains(tabModules))
tabControlDash.TabPages.Remove(tabModules);
return;
} }
PopulateModules();
}
private HashSet<string> LoadDisabledModules()
{
try
{
var s = ServerSettings.Load(_settingsPath!);
return new HashSet<string>(s.DisabledModules, StringComparer.OrdinalIgnoreCase);
}
catch
{
return new HashSet<string>(StringComparer.OrdinalIgnoreCase);
}
}
private void PopulateModules()
{
if (_moduleInfos == null) return;
var disabled = LoadDisabledModules();
var rows = new List<ModuleRow>();
foreach (var m in _moduleInfos)
{
bool blocked = !string.IsNullOrEmpty(m.Blocker);
bool desiredEnabled = !disabled.Contains(m.Name);
string status, hint, action;
if (blocked)
{
status = "⚠ Nicht aktivierbar";
hint = m.Blocker!;
action = "—";
}
else if (m.IsRunning && desiredEnabled)
{
status = "Aktiv"; hint = ""; action = "Deaktivieren";
}
else if (m.IsRunning && !desiredEnabled)
{
status = "Aktiv stoppt nach Neustart"; hint = "Änderung greift nach Neustart"; action = "Aktivieren";
}
else if (!m.IsRunning && desiredEnabled)
{
status = "Startet nach Neustart"; hint = "Änderung greift nach Neustart"; action = "Deaktivieren";
}
else
{
status = "Deaktiviert"; hint = ""; action = "Aktivieren";
}
rows.Add(new ModuleRow { Modul = m.Name, Status = status, Hinweis = hint, Aktion = action });
}
dgvModules.DataSource = new BindingList<ModuleRow>(rows);
}
private void Modules_CellContentClick(object? sender, DataGridViewCellEventArgs e)
{
if (_moduleInfos == null || string.IsNullOrEmpty(_settingsPath)) return;
if (e.RowIndex < 0 || e.ColumnIndex < 0) return;
if (dgvModules.Columns[e.ColumnIndex].Name != "colModAction") return;
if (dgvModules.Rows[e.RowIndex].DataBoundItem is not ModuleRow row) return;
var info = _moduleInfos.FirstOrDefault(m => m.Name == row.Modul);
if (info == null) return;
if (!string.IsNullOrEmpty(info.Blocker))
{
MessageBox.Show($"Modul „{info.Name}“ kann nicht aktiviert werden:\n\n{info.Blocker}",
"Aktivierung nicht möglich", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
try
{
// Frisch aus der Datei lesen, damit parallele Änderungen (z.B. Settings-Fenster) nicht überschrieben werden.
var settings = ServerSettings.Load(_settingsPath!);
var set = new HashSet<string>(settings.DisabledModules, StringComparer.OrdinalIgnoreCase);
bool nowDisabled;
if (set.Contains(info.Name)) { set.Remove(info.Name); nowDisabled = false; }
else { set.Add(info.Name); nowDisabled = true; }
settings.DisabledModules = set.OrderBy(x => x).ToList();
settings.Save(_settingsPath!);
PopulateModules();
MessageBox.Show(
$"Modul „{info.Name}“ wird beim nächsten Start {(nowDisabled ? "NICHT mehr geladen" : "geladen")}.\n\n" +
"Die Änderung greift erst nach einem Neustart von PolyTrader.",
"Gespeichert", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show($"Konnte die Modul-Einstellung nicht speichern: {ex.Message}", "Fehler",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>Anzeige-Zeile des Modul-Grids (Bindung über DataPropertyName).</summary>
private sealed class ModuleRow
{
public string Modul { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public string Hinweis { get; set; } = string.Empty;
public string Aktion { get; set; } = string.Empty;
}
}
/// <summary>
/// Aktivierungs-Info eines Moduls fürs Dashboard: Name, ob es in dieser Session läuft und ein
/// optionaler Grund, warum es nicht aktivierbar ist (z.B. fehlender API-Key).
/// </summary>
public sealed record ModuleActivationInfo(string Name, bool IsRunning, string? Blocker);
/// <summary>Anzeige-Zeile für das Historie-Grid (Account bereits zu Name aufgelöst).</summary> /// <summary>Anzeige-Zeile für das Historie-Grid (Account bereits zu Name aufgelöst).</summary>
public class DashboardTradeRow public class DashboardTradeRow
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.IO; using System.IO;
using System.Xml.Serialization; using System.Xml.Serialization;
@@ -6,6 +7,15 @@ namespace PolyTraderSharp.Models
{ {
public class ServerSettings 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")] [Category("Threema Notifications")]
[DisplayName("Threema Enabled")] [DisplayName("Threema Enabled")]
[Description("Enable or disable Threema notifications.")] [Description("Enable or disable Threema notifications.")]
@@ -36,6 +36,13 @@ namespace PolyTrader.Core.Modularity
/// </summary> /// </summary>
void RegisterUi(IModuleUiHost host, IServiceProvider services); 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> /// <summary>Wird nach der Core-Hydration gestartet (kein Anlaufen gegen leeren State).</summary>
Task StartAsync(CancellationToken cancellationToken); Task StartAsync(CancellationToken cancellationToken);
+13 -2
View File
@@ -24,8 +24,12 @@ namespace PolyTrader.Core.Modularity
/// <summary>Optionale Sortierreihenfolge.</summary> /// <summary>Optionale Sortierreihenfolge.</summary>
public int Order { get; init; } = 0; public int Order { get; init; } = 0;
/// <summary>Optionales Icon (16x16) für Menü/Buttons. Wird von der Shell/den Menüs genutzt.</summary> /// <summary>
public System.Drawing.Image? Icon { get; init; } /// 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> /// <summary>Erzeugt das anzuzeigende Fenster (frische Instanz je Öffnung).</summary>
public Func<Form> CreateForm { get; init; } = () => new Form(); 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> /// <summary>Holt das Hauptfenster (Launcher) in den Vordergrund.</summary>
void ActivateMain(); 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> /// <summary>Feuert, wenn sich der Offen-Status irgendeiner View ändert.</summary>
event Action? OpenStateChanged; event Action? OpenStateChanged;
} }
+57 -19
View File
@@ -6,48 +6,86 @@ using System.Windows.Forms;
namespace PolyTrader.Core.Modularity namespace PolyTrader.Core.Modularity
{ {
/// <summary> /// <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 /// 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). /// <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 Fenster werden NICHT mehr in einem Untermenü „Fenster" versteckt, sondern als eigenständige
/// die dynamische Liste (Offen-Status ändert sich) wird hier beim Aufklappen frisch gefüllt: /// Einträge (mit Icon) direkt nebeneinander in der obersten Menüleiste aufgelistet. Das aktuelle
/// offene Fenster sind angehakt, das aktuelle Fenster ist fett + angehakt markiert. /// 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> /// </summary>
public static class WindowMenu public static class WindowMenu
{ {
/// <summary>Verdrahtet ein Designer-„Fenster"-Menüelement mit der Fensterliste (Neuaufbau beim Aufklappen).</summary> /// <summary>
public static void Wire(ToolStripMenuItem fensterMenu, IModuleUiHost host, string? currentViewId) /// 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); void Refresh()
Populate(fensterMenu, host, currentViewId); {
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);
} }
public static void Populate(ToolStripMenuItem fensterMenu, IModuleUiHost host, string? currentViewId) Populate(menu, host, currentViewId);
host.OpenStateChanged += Refresh;
menu.Disposed += (_, _) => host.OpenStateChanged -= Refresh;
}
/// <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 }; var launcher = new ToolStripMenuItem("Launcher") { Checked = currentViewId == null };
if (currentViewId == null) launcher.Font = new Font(launcher.Font, FontStyle.Bold); if (currentViewId == null) launcher.Font = new Font(launcher.Font, FontStyle.Bold);
launcher.Click += (_, _) => host.ActivateMain(); launcher.Click += (_, _) => host.ActivateMain();
fensterMenu.DropDownItems.Add(launcher); menu.Items.Add(launcher);
fensterMenu.DropDownItems.Add(new ToolStripSeparator());
foreach (var view in host.Views.OrderBy(v => v.Order).ThenBy(v => v.Title)) foreach (var view in host.Views.OrderBy(v => v.Order).ThenBy(v => v.Title))
{ {
bool isCurrent = view.Id == currentViewId; bool isCurrent = view.Id == currentViewId;
var item = new ToolStripMenuItem(view.Title) { Image = view.Icon }; var item = new ToolStripMenuItem(view.Title)
item.Checked = isCurrent || host.IsOpen(view.Id); {
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); if (isCurrent) item.Font = new Font(item.Font, FontStyle.Bold);
string id = view.Id; string id = view.Id;
item.Click += (_, _) => host.OpenView(id); item.Click += (_, _) => host.OpenView(id);
fensterMenu.DropDownItems.Add(item); menu.Items.Add(item);
} }
fensterMenu.DropDownItems.Add(new ToolStripSeparator()); // Kontextabhängige rechte Aktion: nur der Launcher darf die App beenden (Sicherheitsabfrage);
var exit = new ToolStripMenuItem("Beenden"); // jedes andere Fenster bietet nur „Fenster schließen" (kein App-Shutdown, Module laufen weiter).
exit.Click += (_, _) => Application.Exit(); if (currentViewId == null)
fensterMenu.DropDownItems.Add(exit); {
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);
}
} }
} }
} }
+61 -12
View File
@@ -17,10 +17,13 @@ namespace PolyTrader.Tests
public List<string> Opened { get; } = new(); public List<string> Opened { get; } = new();
public bool MainActivated { get; private set; } public bool MainActivated { get; private set; }
public bool ShutdownRequested { get; private set; }
public IReadOnlyList<ModuleView> Views => ViewList; public IReadOnlyList<ModuleView> Views => ViewList;
public bool IsOpen(string viewId) => OpenIds.Contains(viewId); public bool IsOpen(string viewId) => OpenIds.Contains(viewId);
public void OpenView(string viewId) => Opened.Add(viewId); public void OpenView(string viewId) => Opened.Add(viewId);
public void ActivateMain() => MainActivated = true; public void ActivateMain() => MainActivated = true;
public void RequestShutdown() => ShutdownRequested = true;
public void RegisterView(ModuleView view) => ViewList.Add(view); public void RegisterView(ModuleView view) => ViewList.Add(view);
public event Action? OpenStateChanged { add { } remove { } } public event Action? OpenStateChanged { add { } remove { } }
} }
@@ -35,30 +38,64 @@ namespace PolyTrader.Tests
} }
[Fact] [Fact]
public void Populate_lists_launcher_views_and_exit() public void Populate_lists_launcher_views_and_exit_side_by_side()
{ {
var host = Host(); var host = Host();
var fenster = new ToolStripMenuItem("Fenster"); var menu = new MenuStrip();
WindowMenu.Populate(fenster, host, currentViewId: "core.dashboard"); // currentViewId == null => Launcher (Hauptprozess): rechte Aktion ist „Beenden".
WindowMenu.Populate(menu, host, currentViewId: null);
var texts = fenster.DropDownItems.OfType<ToolStripMenuItem>().Select(i => i.Text).ToList(); var texts = menu.Items.OfType<ToolStripMenuItem>().Select(i => i.Text).ToList();
Assert.Equal("Launcher", texts[0]); Assert.Equal("Launcher", texts[0]); // Launcher als erster Top-Level-Eintrag
Assert.Contains("Dashboard", texts); Assert.Contains("Dashboard", texts);
Assert.Contains("Terminal / Logs", texts); Assert.Contains("Terminal / Logs", texts);
Assert.Contains("Beenden", 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<ToolStripMenuItem>().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<ToolStripMenuItem>().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<ToolStripMenuItem>().First(i => i.Text == "Dashboard");
Assert.Same(icon, dashboard.Image);
Assert.Equal(ToolStripItemDisplayStyle.ImageAndText, dashboard.DisplayStyle);
}
[Fact] [Fact]
public void Current_view_is_bold_and_checked_open_view_is_checked() public void Current_view_is_bold_and_checked_open_view_is_checked()
{ {
var host = Host(); var host = Host();
var fenster = new ToolStripMenuItem("Fenster"); var menu = new MenuStrip();
WindowMenu.Populate(fenster, host, currentViewId: "core.dashboard"); WindowMenu.Populate(menu, host, currentViewId: "core.dashboard");
var dashboard = fenster.DropDownItems.OfType<ToolStripMenuItem>().First(i => i.Text == "Dashboard"); var dashboard = menu.Items.OfType<ToolStripMenuItem>().First(i => i.Text == "Dashboard");
var terminal = fenster.DropDownItems.OfType<ToolStripMenuItem>().First(i => i.Text == "Terminal / Logs"); var terminal = menu.Items.OfType<ToolStripMenuItem>().First(i => i.Text == "Terminal / Logs");
Assert.True(dashboard.Checked); // aktuelles Fenster Assert.True(dashboard.Checked); // aktuelles Fenster
Assert.True(dashboard.Font.Bold); Assert.True(dashboard.Font.Bold);
@@ -70,15 +107,27 @@ namespace PolyTrader.Tests
public void Clicking_items_navigates() public void Clicking_items_navigates()
{ {
var host = Host(); var host = Host();
var fenster = new ToolStripMenuItem("Fenster"); var menu = new MenuStrip();
WindowMenu.Populate(fenster, host, currentViewId: null); WindowMenu.Populate(menu, host, currentViewId: null);
var items = fenster.DropDownItems.OfType<ToolStripMenuItem>().ToList(); var items = menu.Items.OfType<ToolStripMenuItem>().ToList();
items.First(i => i.Text == "Launcher").PerformClick(); items.First(i => i.Text == "Launcher").PerformClick();
items.First(i => i.Text == "Terminal / Logs").PerformClick(); items.First(i => i.Text == "Terminal / Logs").PerformClick();
Assert.True(host.MainActivated); Assert.True(host.MainActivated);
Assert.Contains("core.terminal", host.Opened); 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<ToolStripMenuItem>().First(i => i.Text == "Beenden").PerformClick();
Assert.True(host.ShutdownRequested);
}
} }
} }