From 748687ce1cc1a55ed7f75769f19218452406dccd Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Fri, 28 Aug 2026 14:33:51 +0200 Subject: [PATCH] Eingehender Ruf: Firefox-Tab aktivieren und Fenster nativ nach vorn holen Protokollversion 9: neue Plugin-Aktion activate und der Eintrag IncomingCallTabTitle in websocket.json. Beginnt eine Leitung zu klingeln, holt SwyxTray den ersten Firefox-Tab mit passendem Titel ueber das Plugin nach vorn, ohne ihn neu zu laden. Das Plugin allein scheitert dabei am Foreground-Lock von Windows: es kann den Tab aktivieren, das Fenster bleibt aber hinter der gerade fokussierten Anwendung. FirefoxWindow uebernimmt deshalb den nativen Teil - EnumWindows auf MozillaWindowClass, minimierte Fenster wiederherstellen, dann SetForegroundWindow mit AttachThreadInput, notfalls ALT-Tastendruck. Co-Authored-By: Claude Fable 5 --- README.md | 74 +++++++-- src/SwyxTray/FirefoxWindow.cs | 200 +++++++++++++++++++++++ src/SwyxTray/TrayApplicationContext.cs | 10 ++ src/SwyxTray/Web/CommandExecutor.cs | 31 +++- src/SwyxTray/Web/LocalWebSocketServer.cs | 61 +++++++ src/SwyxTray/Web/PluginTabChannel.cs | 9 + src/SwyxTray/Web/Protocol.cs | 16 +- src/SwyxTray/Web/WebSocketConfig.cs | 19 ++- 8 files changed, 391 insertions(+), 29 deletions(-) create mode 100644 src/SwyxTray/FirefoxWindow.cs diff --git a/README.md b/README.md index d6cf441..dce194a 100644 --- a/README.md +++ b/README.md @@ -129,10 +129,20 @@ Beim ersten Start entsteht `%LOCALAPPDATA%\SwyxTray\websocket.json`: "Token": "E59BF4…", "AllowedOrigins": [], "MaxConnections": 8, - "AllowRemoteAccess": false + "AllowRemoteAccess": false, + "IncomingCallTabTitle": "SwyxWeb" } ``` +`IncomingCallTabTitle` (seit Protokollversion 9, fehlt der Eintrag, gilt +`"SwyxWeb"`): Beginnt eine Leitung zu klingeln, holt SwyxTray den ersten +Firefox-Tab, dessen Titel diesen Text enthält (Groß-/Kleinschreibung ohne +Belang, trifft also z. B. „SwyxWeb · STADTBOTE"), über das Plugin nach vorn — +ohne ihn neu zu laden, damit der Web-Client seine WebSocket-Verbindung behält. +Ist kein Plugin verbunden oder kein passender Tab offen, passiert nichts; es +wird bewusst kein neuer Tab geöffnet. Ein leerer Eintrag (`""`) schaltet das +Verhalten ab. + Solange die Prüfungen abgeschaltet sind (siehe unten), sind `Token` und `AllowedOrigins` ohne Wirkung — für einen ersten Test ist an dieser Datei nichts zu tun. Der Rest dieses Abschnitts gilt für den eingeschalteten Zustand. @@ -170,7 +180,7 @@ ab; in einer bestehenden Datei ohne diesen Eintrag gilt automatisch 17655). Auf diesem Port läuft **ausschließlich die Tab-Verwaltung** — keine Snapshots, keine Anruf-Ereignisse, keine Telefonie-Kommandos (sie werden mit `ok: false` abgewiesen). Nach dem `hello` ist die Richtung umgekehrt zum -Hauptport: **SwyxTray fragt, das Plugin antwortet.** Es gibt drei Aufträge, +Hauptport: **SwyxTray fragt, das Plugin antwortet.** Es gibt vier Aufträge, jeder wird mit einem `tabresult` samt unveränderter `id` beantwortet: **Offene Tabs auslesen:** @@ -196,18 +206,44 @@ jeder wird mit einem `tabresult` samt unveränderter `id` beantwortet: ← vom Plugin: {"cmd":"tabresult","id":3,"ok":true} ``` +**Tab aktivieren** (seit Protokollversion 9 — nach vorn holen, ohne ihn neu +zu laden; das Plugin setzt das mit `browser.tabs.update(tabId, +{active: true})` samt `browser.windows.update(windowId, {focused: true})` +um): + +```json +→ an das Plugin: {"type":"tab","id":4,"action":"activate","tabId":42} +← vom Plugin: {"cmd":"tabresult","id":4,"ok":true} +``` + Antwortet das Plugin binnen fünf Sekunden nicht (oder ist keines verbunden), gilt der Auftrag als gescheitert; die darauf aufbauenden Kommandos des -Hauptports (`focus`, `tabs`, `opentab`, `closetab`) enden dann mit -`ok: false`. Vom Plugin aus sind nur `tabresult` und `ping` zulässig. Ein -„Tab aktivieren“ kennt der Kanal bewusst nicht — beim `focus`-Kommando holt -SwyxTray einen vorhandenen Tab als Komposition nach vorn: `list`, passenden -Tab per `close` schließen und die URL per `open` neu öffnen. +Hauptports (`focus`, `tabs`, `opentab`, `closetab`, `activatetab`) enden dann +mit `ok: false`. Vom Plugin aus sind nur `tabresult` und `ping` zulässig. +Ein älteres Plugin, das `activate` noch nicht kennt, antwortet darauf mit +`ok: false` — `focus` fällt dann auf den alten Weg zurück (Tab per `close` +schließen und die URL per `open` neu öffnen), das Aktivieren beim +eingehenden Ruf (`IncomingCallTabTitle`, siehe oben) unterbleibt und das +Protokoll vermerkt es. Das Plugin selbst (SwyxFFPlugin) wird als **eigenes Projekt** gepflegt und ist -nicht Teil dieses Repositories. Sein `background.js` setzt genau diese drei +nicht Teil dieses Repositories. Sein `background.js` setzt genau diese Aufträge um und verbindet sich selbsttätig neu (1 s Abstand, je Fehlversuch verdoppelt bis 30 s), falls SwyxTray noch nicht oder nicht mehr läuft. +Für `activate` (seit Protokollversion 9) braucht das Plugin einen weiteren +Zweig in seinem `handleMessage`: + +```js +} else if (m.action === "activate") { + if (m.tabId == null) { + reply({ ok: false }); + } else { + const tab = await browser.tabs.update(m.tabId, { active: true }); + await browser.windows.update(tab.windowId, { focused: true }); + reply({ ok: true }); + } +} +``` Installation zum Erproben: `about:debugging#/runtime/this-firefox` → *Temporäres Add-on laden…* → die `manifest.json` des Plugin-Projekts wählen. @@ -229,6 +265,7 @@ wobei die UUID je Installation zufällig vergeben wird — sie steht in | `tabs` | — | liest die offenen Firefox-Tabs über das Plugin aus | | `opentab` | `url` | öffnet über das Plugin einen neuen Firefox-Tab | | `closetab` | `tabId` | schließt über das Plugin den Tab mit dieser Id | +| `activatetab` | `tabId` | holt über das Plugin den Tab mit dieser Id nach vorn, ohne ihn neu zu laden | | `addresses` | — | liefert die zwischengespeicherten Adressdaten (Telefonbücher), Antwort im Feld `addresses` | | `contacts` | `query` | durchsucht die Adressdaten des Swyx-Clients (Telefonbücher und Kontakt-Plugins), Antwort im Feld `contacts` | | `resolve` | `number` | löst eine Rufnummer über die Adressdaten in einen Namen auf, Antwort im Feld `name` (leer = unbekannt) | @@ -256,21 +293,24 @@ Zustandsmeldungen. `focus` (seit Protokollversion 3, ab Version 5 ausschließlich über das **Firefox-Plugin**, siehe unten): war ein passender Tab schon offen — sein Titel enthält den `title` oder seine Adresse ist die `url` —, wird er -geschlossen und die `url` neu geöffnet (`"focused": true`), sonst nur neu -geöffnet (`"focused": false`). Ist kein Plugin verbunden oder antwortet es -nicht, endet das Kommando mit `ok: false` — einen anderen Weg, eine Seite -anzuzeigen, gibt es nicht. Die `url` ist Pflicht, `title` dient nur dem -Tab-Abgleich; erlaubt sind nur absolute `http(s)`-Adressen. +aktiviert (`"focused": true`; seit Version 9 ohne Neuladen, bei einem +älteren Plugin ohne `activate` ersatzweise wie früher geschlossen und die +`url` neu geöffnet), sonst nur neu geöffnet (`"focused": false`). Ist kein +Plugin verbunden oder antwortet es nicht, endet das Kommando mit +`ok: false` — einen anderen Weg, eine Seite anzuzeigen, gibt es nicht. Die +`url` ist Pflicht, `title` dient nur dem Tab-Abgleich; erlaubt sind nur +absolute `http(s)`-Adressen. ```json → {"id":9,"cmd":"focus","title":"Kundenakte Muster GmbH","url":"https://crm.example.local/kunden/4711"} ← {"type":"result","id":9,"ok":true,"focused":false} ``` -`tabs`, `opentab`, `closetab` (seit Protokollversion 6): die drei Aufträge des -Tab-Verwaltungskanals stehen damit auch Webseiten offen — SwyxTray reicht sie -unverändert an das Firefox-Plugin durch. Ist kein Plugin verbunden oder -antwortet es nicht binnen fünf Sekunden, endet das Kommando mit `ok: false`. +`tabs`, `opentab`, `closetab` (seit Protokollversion 6), `activatetab` (seit +Version 9): die Aufträge des Tab-Verwaltungskanals stehen damit auch +Webseiten offen — SwyxTray reicht sie unverändert an das Firefox-Plugin +durch. Ist kein Plugin verbunden oder antwortet es nicht binnen fünf +Sekunden, endet das Kommando mit `ok: false`. Für `opentab` gilt dieselbe Regel wie bei `focus`: nur absolute `http(s)`-Adressen. Die `tabId` für `closetab` stammt aus einer vorherigen `tabs`-Antwort; nach Öffnen oder Schließen verschickt die App **keine** neue diff --git a/src/SwyxTray/FirefoxWindow.cs b/src/SwyxTray/FirefoxWindow.cs new file mode 100644 index 0000000..7fa4b5d --- /dev/null +++ b/src/SwyxTray/FirefoxWindow.cs @@ -0,0 +1,200 @@ +using System.Runtime.InteropServices; +using System.Text; + +namespace SwyxTray; + +/// +/// Holt das Firefox-Fenster auf Betriebssystem-Ebene in den Vordergrund. +/// +/// Das Firefox-Plugin aktiviert den Tab und setzt focused:true — gegen den +/// Foreground-Lock von Windows kommt es aus dem Browser heraus aber nicht an: +/// liegt der Eingabefokus gerade in einer anderen Anwendung, blinkt nur das +/// Taskleisten-Symbol. Eine native Anwendung darf mehr: mit AttachThreadInput +/// an den Thread des aktuellen Vordergrundfensters gilt SetForegroundWindow +/// als Aufruf "aus dem Vordergrund" und zieht das Fenster tatsaechlich hoch. +/// +/// Bewusst DllImport statt LibraryImport — wie in +/// begruendet. +/// +internal static class FirefoxWindow +{ + /// Klassenname aller Firefox-Hauptfenster. + private const string MozillaWindowClass = "MozillaWindowClass"; + + private const int SwRestore = 9; + private const byte VkMenu = 0x12; + private const uint KeyeventfKeyup = 0x2; + + /// + /// Firefox braucht nach dem Aktivieren des Tabs einen Moment, bis der + /// Fenstertitel den Tab-Titel traegt — darum mehrere kurze Versuche. + /// + private const int FindAttempts = 3; + private static readonly TimeSpan FindRetryDelay = TimeSpan.FromMilliseconds(150); + + /// + /// Sucht das Firefox-Fenster, dessen Titel + /// enthaelt (der aktive Tab bestimmt den Fenstertitel), und holt es in den + /// Vordergrund. Ohne Titel-Treffer nimmt der letzte Versuch das erste + /// sichtbare Firefox-Fenster. false, wenn kein Fenster gefunden + /// wurde oder Windows den Wechsel endgueltig verweigert hat. + /// + public static bool TryBringToFront(string tabTitle) + { + var hwnd = IntPtr.Zero; + + for (var attempt = 0; attempt < FindAttempts && hwnd == IntPtr.Zero; attempt++) + { + if (attempt > 0) + { + Thread.Sleep(FindRetryDelay); + } + + hwnd = FindWindowByTitle(tabTitle); + } + + if (hwnd == IntPtr.Zero) + { + hwnd = FindWindowByTitle(null); + } + + return hwnd != IntPtr.Zero && ForceForeground(hwnd); + } + + /// + /// Das erste sichtbare Firefox-Hauptfenster, dessen Titel den Suchtext + /// enthaelt; ohne Suchtext das erste sichtbare Firefox-Hauptfenster. + /// + private static IntPtr FindWindowByTitle(string? containsTitle) + { + var found = IntPtr.Zero; + var className = new StringBuilder(64); + var windowTitle = new StringBuilder(512); + + EnumWindows((hwnd, _) => + { + if (!IsWindowVisible(hwnd)) + { + return true; + } + + className.Clear(); + if (GetClassName(hwnd, className, className.Capacity) == 0 + || !string.Equals(className.ToString(), MozillaWindowClass, StringComparison.Ordinal)) + { + return true; + } + + if (containsTitle is not null) + { + windowTitle.Clear(); + GetWindowText(hwnd, windowTitle, windowTitle.Capacity); + if (!windowTitle.ToString().Contains(containsTitle, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + found = hwnd; + return false; // Treffer — Aufzaehlung beenden. + }, IntPtr.Zero); + + return found; + } + + private static bool ForceForeground(IntPtr hwnd) + { + if (IsIconic(hwnd)) + { + ShowWindow(hwnd, SwRestore); + } + + if (GetForegroundWindow() == hwnd) + { + return true; + } + + // An den Eingabe-Thread des aktuellen Vordergrundfensters anheften: + // solange die Threads verbunden sind, behandelt Windows unseren + // SetForegroundWindow-Aufruf wie einen aus dem Vordergrundprozess. + var foregroundThread = GetWindowThreadProcessId(GetForegroundWindow(), out _); + var ownThread = GetCurrentThreadId(); + var attached = foregroundThread != 0 + && foregroundThread != ownThread + && AttachThreadInput(ownThread, foregroundThread, true); + + try + { + SetForegroundWindow(hwnd); + BringWindowToTop(hwnd); + } + finally + { + if (attached) + { + AttachThreadInput(ownThread, foregroundThread, false); + } + } + + if (GetForegroundWindow() == hwnd) + { + return true; + } + + // Zweite Stufe: ein folgenloser ALT-Tastendruck setzt das interne + // "letzte Eingabe"-Kriterium — danach laesst Windows den Wechsel zu. + keybd_event(VkMenu, 0, 0, UIntPtr.Zero); + keybd_event(VkMenu, 0, KeyeventfKeyup, UIntPtr.Zero); + SetForegroundWindow(hwnd); + + return GetForegroundWindow() == hwnd; + } + + private delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool IsWindowVisible(IntPtr hwnd); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + private static extern int GetClassName(IntPtr hwnd, StringBuilder lpClassName, int nMaxCount); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + private static extern int GetWindowText(IntPtr hwnd, StringBuilder lpString, int nMaxCount); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool IsIconic(IntPtr hwnd); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool ShowWindow(IntPtr hwnd, int nCmdShow); + + [DllImport("user32.dll")] + private static extern IntPtr GetForegroundWindow(); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetForegroundWindow(IntPtr hwnd); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool BringWindowToTop(IntPtr hwnd); + + [DllImport("user32.dll")] + private static extern uint GetWindowThreadProcessId(IntPtr hwnd, out uint processId); + + [DllImport("kernel32.dll")] + private static extern uint GetCurrentThreadId(); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, [MarshalAs(UnmanagedType.Bool)] bool fAttach); + + [DllImport("user32.dll")] + private static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo); +} diff --git a/src/SwyxTray/TrayApplicationContext.cs b/src/SwyxTray/TrayApplicationContext.cs index 9f88d1c..c8fbe35 100644 --- a/src/SwyxTray/TrayApplicationContext.cs +++ b/src/SwyxTray/TrayApplicationContext.cs @@ -270,6 +270,8 @@ internal sealed class TrayApplicationContext : ApplicationContext .Select(l => l.Index) .ToHashSet(); + var anyNewRinging = false; + foreach (var line in current.Lines.Where(l => l.State.IsRinging())) { if (previouslyRinging.Contains(line.Index)) @@ -277,6 +279,7 @@ internal sealed class TrayApplicationContext : ApplicationContext continue; } + anyNewRinging = true; Log.Info($"Eingehender Ruf auf Leitung {line.Index + 1}: {line.PeerDisplayLong}"); _notifyIcon.ShowBalloonTip( 10_000, @@ -284,6 +287,13 @@ internal sealed class TrayApplicationContext : ApplicationContext $"{line.PeerDisplayLong}\nLeitung {line.Index + 1}", ToolTipIcon.Info); } + + if (anyNewRinging) + { + // Einmal je Ereignis, nicht je Leitung — mehr als nach vorn holen + // laesst sich der Tab ohnehin nicht. + _server?.ActivateIncomingCallTab(); + } } private void ShowStatusBalloon() diff --git a/src/SwyxTray/Web/CommandExecutor.cs b/src/SwyxTray/Web/CommandExecutor.cs index b56354f..0dd8bda 100644 --- a/src/SwyxTray/Web/CommandExecutor.cs +++ b/src/SwyxTray/Web/CommandExecutor.cs @@ -95,6 +95,10 @@ internal sealed class CommandExecutor await CloseTabAsync(command, cancellationToken).ConfigureAwait(false); return new ResultMessage(command.Id, true); + case "activatetab": + await ActivateTabAsync(command, cancellationToken).ConfigureAwait(false); + return new ResultMessage(command.Id, true); + case "reconnect": await RunAsync(() => { _client.Reconnect(); return null; }, cancellationToken) .ConfigureAwait(false); @@ -133,10 +137,6 @@ internal sealed class CommandExecutor var url = ValidateUrl(command.Url); var title = command.Title?.Trim(); - // Ein "Aktivieren" kennt der Tab-Kanal nicht — nach vorn kommt ein - // vorhandener Tab als Komposition der drei Operationen: Liste holen, - // den passenden Tab schliessen und die URL neu oeffnen (ein neuer Tab - // ist in Firefox von selbst vorn). if (await _tabs.TryListTabsAsync(cancellationToken).ConfigureAwait(false) is not { } openTabs) { throw new InvalidOperationException( @@ -150,6 +150,14 @@ internal sealed class CommandExecutor if (match?.Id is { } tabId) { + if (await _tabs.TryActivateTabAsync(tabId, cancellationToken).ConfigureAwait(false)) + { + return true; // Tab nach vorn geholt, ohne ihn neu zu laden. + } + + // Aeltere Plugins kennen 'activate' nicht — dann wie frueher als + // Komposition: den Tab schliessen und die URL neu oeffnen (ein + // neuer Tab ist in Firefox von selbst vorn). await _tabs.TryCloseTabAsync(tabId, cancellationToken).ConfigureAwait(false); } @@ -187,6 +195,21 @@ internal sealed class CommandExecutor } } + private async Task ActivateTabAsync(ClientCommand command, CancellationToken cancellationToken) + { + if (command.TabId is not { } tabId) + { + throw new ArgumentException("Feld 'tabId' fehlt."); + } + + if (!await _tabs.TryActivateTabAsync(tabId, cancellationToken).ConfigureAwait(false)) + { + throw new InvalidOperationException( + "Kein Firefox-Plugin verbunden oder es hat den Tab nicht aktiviert " + + "(kennt es die Aktion 'activate' schon?)."); + } + } + /// /// Solange die Zugangspruefungen abgeschaltet sind, darf jeder im Netz /// Tabs oeffnen lassen — deshalb nur absolute http(s)-Adressen. diff --git a/src/SwyxTray/Web/LocalWebSocketServer.cs b/src/SwyxTray/Web/LocalWebSocketServer.cs index f259fab..7d38f88 100644 --- a/src/SwyxTray/Web/LocalWebSocketServer.cs +++ b/src/SwyxTray/Web/LocalWebSocketServer.cs @@ -315,6 +315,67 @@ internal sealed class LocalWebSocketServer : IDisposable /// Verteilt nur den Zustand, ohne Anruf-Ereignisse abzuleiten. public void Broadcast(SwyxSnapshot snapshot) => Broadcast(snapshot, snapshot); + /// + /// Holt bei einem eingehenden Ruf den konfigurierten Firefox-Tab nach + /// vorn (): 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 + /// fehlendes Plugin ist kein Fehler des Anrufs. + /// + public void ActivateIncomingCallTab() + { + if (_config.IncomingCallTabTitle?.Trim() is not { Length: > 0 } title) + { + return; + } + + _ = Task.Run(() => ActivateTabByTitleAsync(title)); + } + + private async Task ActivateTabByTitleAsync(string title) + { + try + { + 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."); + return; + } + + var match = tabs.FirstOrDefault(t => + t.Title?.Contains(title, StringComparison.OrdinalIgnoreCase) ?? false); + if (match?.Id is not { } tabId) + { + Log.Debug($"Eingehender Ruf: kein offener Firefox-Tab mit \"{title}\" im Titel."); + return; + } + + if (await _tabChannel.TryActivateTabAsync(tabId, _shutdown.Token).ConfigureAwait(false)) + { + // Das Plugin kann nur den Tab aktivieren — gegen den + // Foreground-Lock von Windows hilft erst der native Griff. + var inFront = FirefoxWindow.TryBringToFront(match.Title ?? title); + Log.Info($"Eingehender Ruf: Firefox-Tab \"{match.Title}\" nach vorn geholt" + + (inFront ? " (Fenster im Vordergrund)." : + " — das Fenster liess sich nicht in den Vordergrund holen.")); + } + else + { + Log.Info($"Eingehender Ruf: das Firefox-Plugin hat das Aktivieren von Tab {tabId} " + + "abgelehnt — kennt es die Aktion 'activate' noch nicht?"); + } + } + catch (OperationCanceledException) + { + // Beenden — nichts zu melden. + } + catch (Exception ex) + { + Log.Error("Aktivieren des Firefox-Tabs beim eingehenden Ruf fehlgeschlagen.", ex); + } + } + /// /// Verteilt die neu eingelesenen Adressdaten an alle Verbundenen — nur an /// den Hauptport, wie die Zustandsmeldungen. Neue Verbindungen bekommen diff --git a/src/SwyxTray/Web/PluginTabChannel.cs b/src/SwyxTray/Web/PluginTabChannel.cs index beec65d..b3e09ca 100644 --- a/src/SwyxTray/Web/PluginTabChannel.cs +++ b/src/SwyxTray/Web/PluginTabChannel.cs @@ -53,6 +53,15 @@ internal sealed class PluginTabChannel => (await RequestAsync(id => new TabRequestMessage(id, "close", TabId: tabId), cancellationToken) .ConfigureAwait(false))?.Ok == true; + /// + /// Holt den Tab nach vorn, ohne ihn neu zu laden. false auch bei + /// einem aelteren Plugin, das die Aktion activate noch nicht kennt + /// — es antwortet dann mit ok: false. + /// + public async Task TryActivateTabAsync(int tabId, CancellationToken cancellationToken) + => (await RequestAsync(id => new TabRequestMessage(id, "activate", TabId: tabId), cancellationToken) + .ConfigureAwait(false))?.Ok == true; + private async Task RequestAsync( Func request, CancellationToken cancellationToken) { diff --git a/src/SwyxTray/Web/Protocol.cs b/src/SwyxTray/Web/Protocol.cs index a251021..51441c8 100644 --- a/src/SwyxTray/Web/Protocol.cs +++ b/src/SwyxTray/Web/Protocol.cs @@ -50,8 +50,9 @@ internal sealed class ClientCommand public bool? Ok { get; set; } /// - /// Bei closetab (Hauptport): der zu schliessende Tab. Bei - /// tabresult auf open (Plugin-Port): Id des neuen Tabs. + /// Bei closetab und activatetab (Hauptport): der gemeinte + /// Tab. Bei tabresult auf open (Plugin-Port): Id des neuen + /// Tabs. /// public int? TabId { get; set; } @@ -115,12 +116,13 @@ internal sealed record ResultMessage( } /// -/// Auftrag an das Firefox-Plugin, nur auf dem Plugin-Port. Drei Aktionen: +/// Auftrag an das Firefox-Plugin, nur auf dem Plugin-Port. Vier Aktionen: /// list (offene Tabs melden), open (neuen Tab mit Url -/// oeffnen), close (Tab TabId schliessen). Das Plugin antwortet -/// jeweils mit {"cmd":"tabresult","id":…,"ok":…} — bei list -/// zusaetzlich mit tabs, bei open mit tabId. Die Id -/// stammt hier vom Server, nicht von einer Webseite. +/// oeffnen), close (Tab TabId schliessen), activate +/// (Tab TabId nach vorn holen, ohne ihn neu zu laden). Das Plugin +/// antwortet jeweils mit {"cmd":"tabresult","id":…,"ok":…} — bei +/// list zusaetzlich mit tabs, bei open mit tabId. +/// Die Id stammt hier vom Server, nicht von einer Webseite. /// internal sealed record TabRequestMessage(int Id, string Action, string? Url = null, int? TabId = null) { diff --git a/src/SwyxTray/Web/WebSocketConfig.cs b/src/SwyxTray/Web/WebSocketConfig.cs index b628944..16a16c4 100644 --- a/src/SwyxTray/Web/WebSocketConfig.cs +++ b/src/SwyxTray/Web/WebSocketConfig.cs @@ -36,8 +36,15 @@ internal sealed class WebSocketConfig /// jedem Neueinlesen (Start, Neuverbindung, alle 60 Minuten). Dazu das /// Kommando addresses, mit dem eine Seite den Cache jederzeit /// selbst abrufen kann. + /// Version 9: Tab-Aktion activate auf dem Plugin-Port (Tab nach + /// vorn holen, ohne ihn neu zu laden), als activatetab auch auf dem + /// Hauptport. focus aktiviert einen gefundenen Tab jetzt, statt ihn + /// zu schliessen und neu zu oeffnen (Ersatzweg fuer aeltere Plugins + /// bleibt). Dazu : bei einem eingehenden + /// Ruf holt SwyxTray den Firefox-Tab mit diesem Titel selbsttaetig nach + /// vorn. /// - public const int ProtocolVersion = 8; + public const int ProtocolVersion = 9; /// /// ZUR ZEIT ABGESCHALTET: Token-, Origin- und Host-Pruefung entfallen — @@ -99,6 +106,16 @@ internal sealed class WebSocketConfig public int MaxConnections { get; set; } = 8; + /// + /// Beginnt eine Leitung zu klingeln, holt SwyxTray den ersten Firefox-Tab, + /// dessen Titel diesen Text enthaelt (ohne Beachtung der Gross-/ + /// Kleinschreibung), ueber das Plugin nach vorn — ohne ihn neu zu laden, + /// damit der Web-Client seine WebSocket-Verbindung behaelt. Leer = aus. + /// Ohne Plugin oder ohne passenden Tab passiert nichts; es wird bewusst + /// kein neuer Tab geoeffnet. + /// + public string IncomingCallTabTitle { get; set; } = "SwyxWeb"; + /// /// 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