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
+53
-4
@@ -27,6 +27,9 @@ internal static class Program
|
||||
{
|
||||
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]
|
||||
private static void Main(string[] args)
|
||||
{
|
||||
@@ -62,13 +65,21 @@ internal static class Program
|
||||
|
||||
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 ResolutionFarmingModule(),
|
||||
new SupervisorModule(),
|
||||
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()
|
||||
// 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.AddCorePersistence(databaseOptions);
|
||||
services.AddSingleton((IServiceProvider sp) => ServerSettings.Load("server_settings.xml"));
|
||||
services.AddSingleton(serverSettings);
|
||||
services.AddSingleton<TradingState>();
|
||||
services.AddSingleton(delegate(IServiceProvider sp)
|
||||
{
|
||||
@@ -148,11 +159,15 @@ internal static class Program
|
||||
try
|
||||
{
|
||||
// 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 tradeLog = AppHost.Services.GetRequiredService<ICopyTradeLogRepository>();
|
||||
copyState.TotalCopyTrades = tradeLog.GetMaxTradeId();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// M3: NICHT still schlucken. Bliebe der Zähler bei 0, kollidierten neue TradeIds mit
|
||||
@@ -223,9 +238,17 @@ internal static class Program
|
||||
CreateForm = () =>
|
||||
{
|
||||
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(
|
||||
viewServices.GetRequiredService<PolyTrader.Core.Persistence.ITradeLogRepository>(),
|
||||
viewServices.GetRequiredService<TradingState>());
|
||||
viewServices.GetRequiredService<TradingState>(),
|
||||
moduleInfos,
|
||||
ServerSettingsPath);
|
||||
return view;
|
||||
}
|
||||
});
|
||||
@@ -235,11 +258,37 @@ internal static class Program
|
||||
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>();
|
||||
Application.Run(launcher);
|
||||
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)
|
||||
{
|
||||
var config = new Microsoft.Extensions.Configuration.ConfigurationBuilder()
|
||||
|
||||
Generated
+1
-9
@@ -18,7 +18,6 @@ namespace PolyTraderSharp.Ui
|
||||
private void InitializeComponent()
|
||||
{
|
||||
menuStrip = new MenuStrip();
|
||||
miFenster = new ToolStripMenuItem();
|
||||
toolstrip_windows = new ToolStrip();
|
||||
btn_dashboard = new ToolStripButton();
|
||||
btn_settings = new ToolStripButton();
|
||||
@@ -56,19 +55,13 @@ namespace PolyTraderSharp.Ui
|
||||
// menuStrip
|
||||
//
|
||||
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.Name = "menuStrip";
|
||||
menuStrip.Padding = new Padding(9, 3, 0, 3);
|
||||
menuStrip.Size = new Size(2599, 35);
|
||||
menuStrip.TabIndex = 0;
|
||||
//
|
||||
// miFenster
|
||||
//
|
||||
miFenster.Name = "miFenster";
|
||||
miFenster.Size = new Size(85, 29);
|
||||
miFenster.Text = "Fenster";
|
||||
//
|
||||
// toolstrip_windows
|
||||
//
|
||||
toolstrip_windows.AutoSize = false;
|
||||
@@ -345,7 +338,6 @@ namespace PolyTraderSharp.Ui
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.MenuStrip menuStrip;
|
||||
private System.Windows.Forms.ToolStripMenuItem miFenster;
|
||||
private ToolStrip toolstrip_windows;
|
||||
private ToolStripButton btn_settings;
|
||||
private ToolStripButton btn_terminal;
|
||||
|
||||
+19
-2
@@ -60,8 +60,9 @@ namespace PolyTraderSharp.Ui
|
||||
btn.Enabled = false; // Modul/View nicht verfügbar
|
||||
}
|
||||
|
||||
// Gemeinsames „Fenster"-Menü (Launcher ist das aktuelle Fenster → currentViewId null).
|
||||
PolyTrader.Core.Modularity.WindowMenu.Wire(miFenster, _uiHost, null);
|
||||
// Gemeinsame Fenster-Menüleiste: alle Fenster nebeneinander mit Icon (Launcher ist das
|
||||
// aktuelle Fenster → currentViewId null).
|
||||
PolyTrader.Core.Modularity.WindowMenu.Wire(menuStrip, _uiHost, null);
|
||||
|
||||
btn_liveTrading.Click += (_, _) => CycleLiveTrading();
|
||||
btn_demoTrading.Click += (_, _) => CycleDemoTrading();
|
||||
@@ -86,6 +87,22 @@ namespace PolyTraderSharp.Ui
|
||||
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()
|
||||
{
|
||||
if (IsDisposed) return;
|
||||
|
||||
+38
-6
@@ -17,6 +17,13 @@ namespace PolyTraderSharp.Ui
|
||||
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;
|
||||
@@ -29,15 +36,14 @@ namespace PolyTraderSharp.Ui
|
||||
public void SetMainWindow(Form main) => _mainWindow = main;
|
||||
|
||||
/// <summary>
|
||||
/// Fügt einem Fenster das gemeinsame „Fenster"-Menü hinzu (oberste Leiste). Als letztes
|
||||
/// hinzugefügtes Top-Control belegt der MenuStrip die oberste Zeile über etwaigen ToolStrips.
|
||||
/// 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 };
|
||||
var fenster = new ToolStripMenuItem("Fenster");
|
||||
menu.Items.Add(fenster);
|
||||
PolyTrader.Core.Modularity.WindowMenu.Wire(fenster, this, currentViewId);
|
||||
var menu = new MenuStrip { Dock = DockStyle.Top, ImageScalingSize = new System.Drawing.Size(24, 24) };
|
||||
PolyTrader.Core.Modularity.WindowMenu.Wire(menu, this, currentViewId);
|
||||
form.Controls.Add(menu);
|
||||
form.MainMenuStrip = menu;
|
||||
}
|
||||
@@ -54,6 +60,32 @@ namespace PolyTraderSharp.Ui
|
||||
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);
|
||||
|
||||
Generated
+120
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+96
@@ -58,8 +58,17 @@ namespace PolyTraderSharp.Ui.Views
|
||||
lblErgebnis = new Label();
|
||||
tbSearch = new TextBox();
|
||||
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();
|
||||
tabControlDash.SuspendLayout();
|
||||
tabModules.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)dgvModules).BeginInit();
|
||||
tabDashboard.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)picEquity).BeginInit();
|
||||
pnlChartsBottom.SuspendLayout();
|
||||
@@ -140,6 +149,7 @@ namespace PolyTraderSharp.Ui.Views
|
||||
//
|
||||
tabControlDash.Controls.Add(tabDashboard);
|
||||
tabControlDash.Controls.Add(tabHistory);
|
||||
tabControlDash.Controls.Add(tabModules);
|
||||
tabControlDash.Dock = DockStyle.Fill;
|
||||
tabControlDash.Location = new Point(0, 34);
|
||||
tabControlDash.Margin = new Padding(4, 5, 4, 5);
|
||||
@@ -453,6 +463,83 @@ namespace PolyTraderSharp.Ui.Views
|
||||
lblSuche.TabIndex = 0;
|
||||
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
|
||||
//
|
||||
AutoScaleDimensions = new SizeF(10F, 25F);
|
||||
@@ -477,6 +564,8 @@ namespace PolyTraderSharp.Ui.Views
|
||||
((System.ComponentModel.ISupportInitialize)dgvTrades).EndInit();
|
||||
pnlHistFilters.ResumeLayout(false);
|
||||
pnlHistFilters.PerformLayout();
|
||||
tabModules.ResumeLayout(false);
|
||||
((System.ComponentModel.ISupportInitialize)dgvModules).EndInit();
|
||||
ResumeLayout(false);
|
||||
PerformLayout();
|
||||
}
|
||||
@@ -523,5 +612,12 @@ namespace PolyTraderSharp.Ui.Views
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn colPnl;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn colPnlPct;
|
||||
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
@@ -22,6 +22,9 @@ namespace PolyTraderSharp.Ui.Views
|
||||
private ITradeLogRepository? _tradeLog;
|
||||
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<DashboardTradeRow> _historyBase = new(); // Scope-gefiltert, Basis für die Historie-Filter
|
||||
private bool _loading;
|
||||
@@ -48,13 +51,24 @@ namespace PolyTraderSharp.Ui.Views
|
||||
cbRange.SelectedIndexChanged += (_, _) => { if (!_loading) ApplyScope(); };
|
||||
tbSearch.TextChanged += (_, _) => { 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;
|
||||
_state = state;
|
||||
_moduleInfos = modules;
|
||||
_settingsPath = settingsPath;
|
||||
RefreshData();
|
||||
InitModulesTab();
|
||||
}
|
||||
|
||||
// ===== Daten laden / Scope =====
|
||||
@@ -251,7 +265,134 @@ namespace PolyTraderSharp.Ui.Views
|
||||
{
|
||||
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>
|
||||
public class DashboardTradeRow
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
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 };
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,13 @@ namespace PolyTrader.Tests
|
||||
public List<string> Opened { get; } = new();
|
||||
public bool MainActivated { get; private set; }
|
||||
|
||||
public bool ShutdownRequested { get; private set; }
|
||||
|
||||
public IReadOnlyList<ModuleView> Views => ViewList;
|
||||
public bool IsOpen(string viewId) => OpenIds.Contains(viewId);
|
||||
public void OpenView(string viewId) => Opened.Add(viewId);
|
||||
public void ActivateMain() => MainActivated = true;
|
||||
public void RequestShutdown() => ShutdownRequested = true;
|
||||
public void RegisterView(ModuleView view) => ViewList.Add(view);
|
||||
public event Action? OpenStateChanged { add { } remove { } }
|
||||
}
|
||||
@@ -35,30 +38,64 @@ namespace PolyTrader.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Populate_lists_launcher_views_and_exit()
|
||||
public void Populate_lists_launcher_views_and_exit_side_by_side()
|
||||
{
|
||||
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();
|
||||
Assert.Equal("Launcher", texts[0]);
|
||||
var texts = menu.Items.OfType<ToolStripMenuItem>().Select(i => i.Text).ToList();
|
||||
Assert.Equal("Launcher", texts[0]); // Launcher als erster Top-Level-Eintrag
|
||||
Assert.Contains("Dashboard", texts);
|
||||
Assert.Contains("Terminal / Logs", 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]
|
||||
public void Current_view_is_bold_and_checked_open_view_is_checked()
|
||||
{
|
||||
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 terminal = fenster.DropDownItems.OfType<ToolStripMenuItem>().First(i => i.Text == "Terminal / Logs");
|
||||
var dashboard = menu.Items.OfType<ToolStripMenuItem>().First(i => i.Text == "Dashboard");
|
||||
var terminal = menu.Items.OfType<ToolStripMenuItem>().First(i => i.Text == "Terminal / Logs");
|
||||
|
||||
Assert.True(dashboard.Checked); // aktuelles Fenster
|
||||
Assert.True(dashboard.Font.Bold);
|
||||
@@ -70,15 +107,27 @@ namespace PolyTrader.Tests
|
||||
public void Clicking_items_navigates()
|
||||
{
|
||||
var host = Host();
|
||||
var fenster = new ToolStripMenuItem("Fenster");
|
||||
WindowMenu.Populate(fenster, host, currentViewId: null);
|
||||
var menu = new MenuStrip();
|
||||
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 == "Terminal / Logs").PerformClick();
|
||||
|
||||
Assert.True(host.MainActivated);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user