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 <noreply@anthropic.com>
This commit is contained in:
2026-08-31 11:06:22 +02:00
co-authored by Claude Fable 5
parent 748687ce1c
commit ce9e6816f8
4 changed files with 165 additions and 1 deletions
+68
View File
@@ -0,0 +1,68 @@
namespace SwyxTray;
/// <summary>
/// Kleiner modaler Dialog hinter dem Menuepunkt "Einstellungen": stellt die
/// Wartezeit ein, bis Firefox bei einem eingehenden Ruf nach vorn geholt wird
/// (<see cref="Web.WebSocketConfig.IncomingCallFocusDelayMs"/>). Der Aufrufer
/// liest nach OK den Wert aus <see cref="FocusDelayMs"/> und speichert selbst.
/// </summary>
internal sealed class SettingsDialog : Form
{
private const int MaxDelayMs = 60_000;
private readonly NumericUpDown _delayInput;
/// <summary>Die eingestellte Verzoegerung in Millisekunden.</summary>
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 });
}
}
+53
View File
@@ -41,6 +41,8 @@ internal sealed class TrayApplicationContext : ApplicationContext
private readonly ToolStripMenuItem _copyEndpointItem; private readonly ToolStripMenuItem _copyEndpointItem;
private LocalWebSocketServer? _server; private LocalWebSocketServer? _server;
private WebSocketConfig? _webSocketConfig;
private bool _settingsDialogOpen;
private bool _disposed; private bool _disposed;
public TrayApplicationContext() public TrayApplicationContext()
@@ -69,6 +71,7 @@ internal sealed class TrayApplicationContext : ApplicationContext
new ToolStripSeparator(), new ToolStripSeparator(),
new ToolStripMenuItem("Neu verbinden", null, (_, _) => Reconnect()), new ToolStripMenuItem("Neu verbinden", null, (_, _) => Reconnect()),
new ToolStripMenuItem("Protokoll oeffnen", null, (_, _) => OpenLog()), new ToolStripMenuItem("Protokoll oeffnen", null, (_, _) => OpenLog()),
new ToolStripMenuItem("Einstellungen", null, (_, _) => OpenSettings()),
new ToolStripSeparator(), new ToolStripSeparator(),
new ToolStripMenuItem("Beenden", null, (_, _) => ExitApplication()) new ToolStripMenuItem("Beenden", null, (_, _) => ExitApplication())
}); });
@@ -129,6 +132,8 @@ internal sealed class TrayApplicationContext : ApplicationContext
return; return;
} }
_webSocketConfig = config;
if (UiDispatcher.Capture() is not { } dispatcher) if (UiDispatcher.Capture() is not { } dispatcher)
{ {
Log.Error("Kein UI-Kontext vorhanden — der WebSocket-Zugang bleibt aus."); 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); _notifyIcon.ShowBalloonTip(5_000, s.StatusText, body, ToolTipIcon.Info);
} }
/// <summary>
/// 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.
/// </summary>
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() private void Reconnect()
{ {
Log.Info("Neuverbindung durch Benutzer angefordert."); Log.Info("Neuverbindung durch Benutzer angefordert.");
+11 -1
View File
@@ -317,7 +317,9 @@ internal sealed class LocalWebSocketServer : IDisposable
/// <summary> /// <summary>
/// Holt bei einem eingehenden Ruf den konfigurierten Firefox-Tab nach /// Holt bei einem eingehenden Ruf den konfigurierten Firefox-Tab nach
/// vorn (<see cref="WebSocketConfig.IncomingCallTabTitle"/>): Tabs ueber /// vorn (<see cref="WebSocketConfig.IncomingCallTabTitle"/>): die
/// konfigurierte Zeit warten (<see
/// cref="WebSocketConfig.IncomingCallFocusDelayMs"/>), dann Tabs ueber
/// das Plugin auslesen, den ersten mit passendem Titel aktivieren. Laeuft /// das Plugin auslesen, den ersten mit passendem Titel aktivieren. Laeuft
/// im Hintergrund, damit der UI-Thread nicht auf das Plugin wartet; /// im Hintergrund, damit der UI-Thread nicht auf das Plugin wartet;
/// scheitert still bis auf das Protokoll — ein fehlender Tab oder ein /// scheitert still bis auf das Protokoll — ein fehlender Tab oder ein
@@ -337,6 +339,14 @@ internal sealed class LocalWebSocketServer : IDisposable
{ {
try 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) 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."); Log.Debug($"Eingehender Ruf: kein Firefox-Plugin verbunden — Tab \"{title}\" bleibt, wo er ist.");
+33
View File
@@ -116,6 +116,14 @@ internal sealed class WebSocketConfig
/// </summary> /// </summary>
public string IncomingCallTabTitle { get; set; } = "SwyxWeb"; public string IncomingCallTabTitle { get; set; } = "SwyxWeb";
/// <summary>
/// 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.
/// </summary>
public int IncomingCallFocusDelayMs { get; set; } = 1000;
/// <summary> /// <summary>
/// Voreingestellt hoert die App nur auf 127.0.0.1 und ::1. Auf <c>true</c> /// Voreingestellt hoert die App nur auf 127.0.0.1 und ::1. Auf <c>true</c>
/// bindet sie an alle Schnittstellen und ist damit aus dem lokalen Netz /// bindet sie an alle Schnittstellen und ist damit aus dem lokalen Netz
@@ -165,6 +173,26 @@ internal sealed class WebSocketConfig
} }
} }
/// <summary>
/// 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.
/// </summary>
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() private bool Validate()
{ {
if (Port is < 1 or > 65535) if (Port is < 1 or > 65535)
@@ -185,6 +213,11 @@ internal sealed class WebSocketConfig
MaxConnections = 1; MaxConnections = 1;
} }
if (IncomingCallFocusDelayMs < 0)
{
IncomingCallFocusDelayMs = 0;
}
// Solange die Pruefung abgeschaltet ist, darf ein fehlendes oder kurzes // Solange die Pruefung abgeschaltet ist, darf ein fehlendes oder kurzes
// Token den Start nicht verhindern. // Token den Start nicht verhindern.
if (Token.Length < 16 && !SecurityDisabled) if (Token.Length < 16 && !SecurityDisabled)