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:
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <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()
|
||||
{
|
||||
Log.Info("Neuverbindung durch Benutzer angefordert.");
|
||||
|
||||
@@ -317,7 +317,9 @@ internal sealed class LocalWebSocketServer : IDisposable
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// 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.");
|
||||
|
||||
@@ -116,6 +116,14 @@ internal sealed class WebSocketConfig
|
||||
/// </summary>
|
||||
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>
|
||||
/// 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
|
||||
@@ -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()
|
||||
{
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user