From ce9e6816f819b4869ca6524b63de1a1617f5378d Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Mon, 31 Aug 2026 11:06:22 +0200 Subject: [PATCH] Einstellungen-Dialog: Verzoegerung fuer das Nach-vorn-Holen von Firefox Bei einem eingehenden Ruf wartet SwyxTray jetzt eine konfigurierbare Zeit (Standard 1000 ms), bevor der Firefox-Tab aktiviert und das Fenster in den Vordergrund geholt wird. Der Wert steht als IncomingCallFocusDelayMs in der websocket.json und laesst sich ueber den neuen Menuepunkt Einstellungen im Tray-Menue aendern; die Aenderung wirkt sofort und wird zurueckgeschrieben. Co-Authored-By: Claude Fable 5 --- src/SwyxTray/SettingsDialog.cs | 68 ++++++++++++++++++++++++ src/SwyxTray/TrayApplicationContext.cs | 53 ++++++++++++++++++ src/SwyxTray/Web/LocalWebSocketServer.cs | 12 ++++- src/SwyxTray/Web/WebSocketConfig.cs | 33 ++++++++++++ 4 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 src/SwyxTray/SettingsDialog.cs diff --git a/src/SwyxTray/SettingsDialog.cs b/src/SwyxTray/SettingsDialog.cs new file mode 100644 index 0000000..8f297f6 --- /dev/null +++ b/src/SwyxTray/SettingsDialog.cs @@ -0,0 +1,68 @@ +namespace SwyxTray; + +/// +/// Kleiner modaler Dialog hinter dem Menuepunkt "Einstellungen": stellt die +/// Wartezeit ein, bis Firefox bei einem eingehenden Ruf nach vorn geholt wird +/// (). Der Aufrufer +/// liest nach OK den Wert aus und speichert selbst. +/// +internal sealed class SettingsDialog : Form +{ + private const int MaxDelayMs = 60_000; + + private readonly NumericUpDown _delayInput; + + /// Die eingestellte Verzoegerung in Millisekunden. + public int FocusDelayMs => (int)_delayInput.Value; + + public SettingsDialog(int focusDelayMs) + { + Text = "SwyxTray — Einstellungen"; + FormBorderStyle = FormBorderStyle.FixedDialog; + MaximizeBox = false; + MinimizeBox = false; + ShowInTaskbar = false; + StartPosition = FormStartPosition.CenterScreen; + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(570, 174); + + var label = new Label + { + Text = "Verzoegerung, bis Firefox bei einem eingehenden Ruf\n" + + "nach vorn geholt wird (Millisekunden):", + Location = new Point(18, 18), + AutoSize = true + }; + + _delayInput = new NumericUpDown + { + Minimum = 0, + Maximum = MaxDelayMs, + Increment = 100, + ThousandsSeparator = true, + Location = new Point(18, 81), + Width = 150, + Value = Math.Clamp(focusDelayMs, 0, MaxDelayMs) + }; + + var ok = new Button + { + Text = "OK", + DialogResult = DialogResult.OK, + Location = new Point(273, 123), + Size = new Size(135, 35) + }; + + var cancel = new Button + { + Text = "Abbrechen", + DialogResult = DialogResult.Cancel, + Location = new Point(417, 123), + Size = new Size(135, 35) + }; + + AcceptButton = ok; + CancelButton = cancel; + Controls.AddRange(new Control[] { label, _delayInput, ok, cancel }); + } +} diff --git a/src/SwyxTray/TrayApplicationContext.cs b/src/SwyxTray/TrayApplicationContext.cs index c8fbe35..4373a09 100644 --- a/src/SwyxTray/TrayApplicationContext.cs +++ b/src/SwyxTray/TrayApplicationContext.cs @@ -41,6 +41,8 @@ internal sealed class TrayApplicationContext : ApplicationContext private readonly ToolStripMenuItem _copyEndpointItem; private LocalWebSocketServer? _server; + private WebSocketConfig? _webSocketConfig; + private bool _settingsDialogOpen; private bool _disposed; public TrayApplicationContext() @@ -69,6 +71,7 @@ internal sealed class TrayApplicationContext : ApplicationContext new ToolStripSeparator(), new ToolStripMenuItem("Neu verbinden", null, (_, _) => Reconnect()), new ToolStripMenuItem("Protokoll oeffnen", null, (_, _) => OpenLog()), + new ToolStripMenuItem("Einstellungen", null, (_, _) => OpenSettings()), new ToolStripSeparator(), new ToolStripMenuItem("Beenden", null, (_, _) => ExitApplication()) }); @@ -129,6 +132,8 @@ internal sealed class TrayApplicationContext : ApplicationContext return; } + _webSocketConfig = config; + if (UiDispatcher.Capture() is not { } dispatcher) { Log.Error("Kein UI-Kontext vorhanden — der WebSocket-Zugang bleibt aus."); @@ -308,6 +313,54 @@ internal sealed class TrayApplicationContext : ApplicationContext _notifyIcon.ShowBalloonTip(5_000, s.StatusText, body, ToolTipIcon.Info); } + /// + /// Zeigt den Einstellungen-Dialog. Der laufende Server benutzt dieselbe + /// Konfigurations-Instanz, deshalb wirkt eine Aenderung sofort — ohne + /// Neustart. Gespeichert wird in die websocket.json, damit der Wert den + /// naechsten Start ueberlebt. + /// + private void OpenSettings() + { + // ShowDialog laesst das Tray-Menue weiter bedienbar — ein zweiter + // Klick soll keinen zweiten Dialog oeffnen. + if (_settingsDialogOpen) + { + return; + } + + var config = _webSocketConfig ?? WebSocketConfig.Load(); + if (config is null) + { + _notifyIcon.ShowBalloonTip(5_000, "Einstellungen", + "Die Konfiguration konnte nicht geladen werden (siehe Protokoll).", + ToolTipIcon.Warning); + return; + } + + _webSocketConfig = config; + _settingsDialogOpen = true; + try + { + using var dialog = new SettingsDialog(config.IncomingCallFocusDelayMs); + if (dialog.ShowDialog() != DialogResult.OK + || dialog.FocusDelayMs == config.IncomingCallFocusDelayMs) + { + return; + } + + config.IncomingCallFocusDelayMs = dialog.FocusDelayMs; + if (config.Save()) + { + Log.Info("Einstellungen gespeichert: Verzoegerung fuer das Nach-vorn-Holen " + + $"von Firefox jetzt {config.IncomingCallFocusDelayMs} ms."); + } + } + finally + { + _settingsDialogOpen = false; + } + } + private void Reconnect() { Log.Info("Neuverbindung durch Benutzer angefordert."); diff --git a/src/SwyxTray/Web/LocalWebSocketServer.cs b/src/SwyxTray/Web/LocalWebSocketServer.cs index 7d38f88..49e4573 100644 --- a/src/SwyxTray/Web/LocalWebSocketServer.cs +++ b/src/SwyxTray/Web/LocalWebSocketServer.cs @@ -317,7 +317,9 @@ internal sealed class LocalWebSocketServer : IDisposable /// /// Holt bei einem eingehenden Ruf den konfigurierten Firefox-Tab nach - /// vorn (): Tabs ueber + /// vorn (): die + /// konfigurierte Zeit warten (), dann Tabs ueber /// das Plugin auslesen, den ersten mit passendem Titel aktivieren. Laeuft /// im Hintergrund, damit der UI-Thread nicht auf das Plugin wartet; /// scheitert still bis auf das Protokoll — ein fehlender Tab oder ein @@ -337,6 +339,14 @@ internal sealed class LocalWebSocketServer : IDisposable { try { + // Kurz warten, damit der Ruf erst sichtbar klingelt, bevor + // Firefox dem Nutzer den Fokus wegnimmt. Die Dauer kommt aus der + // Konfiguration (Einstellungen-Dialog im Tray-Menue). + if (_config.IncomingCallFocusDelayMs > 0) + { + await Task.Delay(_config.IncomingCallFocusDelayMs, _shutdown.Token).ConfigureAwait(false); + } + if (await _tabChannel.TryListTabsAsync(_shutdown.Token).ConfigureAwait(false) is not { } tabs) { Log.Debug($"Eingehender Ruf: kein Firefox-Plugin verbunden — Tab \"{title}\" bleibt, wo er ist."); diff --git a/src/SwyxTray/Web/WebSocketConfig.cs b/src/SwyxTray/Web/WebSocketConfig.cs index 16a16c4..964ecab 100644 --- a/src/SwyxTray/Web/WebSocketConfig.cs +++ b/src/SwyxTray/Web/WebSocketConfig.cs @@ -116,6 +116,14 @@ internal sealed class WebSocketConfig /// public string IncomingCallTabTitle { get; set; } = "SwyxWeb"; + /// + /// Wartezeit in Millisekunden zwischen Klingelbeginn und dem Nach-vorn- + /// Holen des Firefox-Fensters — der Ruf soll erst sichtbar klingeln, + /// bevor der Fokus wechselt. 0 = sofort. Einstellbar auch ueber den + /// Menuepunkt "Einstellungen" im Tray-Menue. + /// + public int IncomingCallFocusDelayMs { get; set; } = 1000; + /// /// Voreingestellt hoert die App nur auf 127.0.0.1 und ::1. Auf true /// bindet sie an alle Schnittstellen und ist damit aus dem lokalen Netz @@ -165,6 +173,26 @@ internal sealed class WebSocketConfig } } + /// + /// Schreibt die Konfiguration zurueck in die Datei — etwa nach einer + /// Aenderung ueber den Einstellungen-Dialog. Kommentare, die jemand von + /// Hand in die JSON-Datei geschrieben hat, gehen dabei verloren. + /// + public bool Save() + { + try + { + Directory.CreateDirectory(Log.Directory); + File.WriteAllText(FilePath, JsonSerializer.Serialize(this, WriteOptions), Encoding.UTF8); + return true; + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + Log.Error($"WebSocket-Konfiguration {FilePath} konnte nicht gespeichert werden.", ex); + return false; + } + } + private bool Validate() { if (Port is < 1 or > 65535) @@ -185,6 +213,11 @@ internal sealed class WebSocketConfig MaxConnections = 1; } + if (IncomingCallFocusDelayMs < 0) + { + IncomingCallFocusDelayMs = 0; + } + // Solange die Pruefung abgeschaltet ist, darf ein fehlendes oder kurzes // Token den Start nicht verhindern. if (Token.Length < 16 && !SecurityDisabled)