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)