From eca566321a2179a82627a3bfcfdde2b82581ce80 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Sun, 23 Aug 2026 02:12:27 +0200 Subject: [PATCH] fix(tells): a tell no longer takes the keyboard mid-sentence Reported by Carla: a tell arriving while you are typing pulls the focus away. The interruption is the visible half. The sharp half is that the input buffer belongs to the WINDOW while the send target is read off whatever tab is active at Enter -- so a line typed at one person could leave addressed to whoever just wrote, and in this game losing the keyboard means the next sentence walks the character around. Nothing is revealed now while any chat surface is mid-sentence, in any mode. The tab still appears and still carries its unread mark. The check lives in its own file because the answer has to be identical everywhere: it started inside the reveal plan, and a second pop-out path walked straight past it -- AutoTellTabsService opened windows off its own flag, at tab creation, a tick before the router was ever asked. Those two paths are one now. AutoTellTabsOpenAsPopout and TellAutoOpenMode were two settings for one decision, and the older one won every race, which is why the other looked inert. Config schema 28 carries the old flag forward so nobody's behaviour changes. "Off" went with it: it never stopped the tab from being created -- that is the auto-tell switch -- it only stopped the jump to it, which is what the switch below it does. Also in here, all from the same corner of the code: - Closing a tab was lost in the v2.0.0 rebuild. The trash entry lived in the retired ChatLogWindow menu, and the rebuilt one restored rename, sound, pop-out and pinning but not this. For tell tabs that left no way out at all: IsEditable keeps them out of the settings editor on purpose and points at the context menu, which could not close them either. Pinned tell tabs stay disabled with a tooltip rather than absent. - Re-anchoring the active tab used an unconditional Tabs[0] in three places, and Tabs[0] can be popped out -- so it ran OnTabActivated over a tab live in its own window and stripped its tell binding. With every tab popped, the seed and the re-anchor also fought each other every frame. - PinTab_LimitReached still pointed at "Promote to permanent", removed in May. Spanish said "Desija", which is not a word; Greek left "tell tabs" untranslated; pt-PT broke its own unpin verb. - Pop Out was a hardcoded English literal despite the key existing in all 25 languages since v1.5.6, and the tell-open modes were the last English display names in the plugin. - Segmented setting rows measured 200px flat, which cut German labels in half. They size to their longest label now. - Metrics.Scale still called GlobalScaleSafe. It is an alias for GlobalScale in current Dalamud, and dropping it clears the last compiler warning in the project. --- HellionChat/AutoTellTabsService.cs | 28 ++----- HellionChat/Configuration.cs | 30 +++---- HellionChat/Plugin.cs | 28 ++++++- .../Resources/HellionStrings.Designer.cs | 8 +- HellionChat/Resources/HellionStrings.ca.resx | 26 ++++-- HellionChat/Resources/HellionStrings.cs.resx | 26 ++++-- HellionChat/Resources/HellionStrings.da.resx | 26 ++++-- HellionChat/Resources/HellionStrings.de.resx | 26 ++++-- HellionChat/Resources/HellionStrings.el.resx | 26 ++++-- HellionChat/Resources/HellionStrings.es.resx | 26 ++++-- HellionChat/Resources/HellionStrings.fi.resx | 26 ++++-- HellionChat/Resources/HellionStrings.fr.resx | 26 ++++-- HellionChat/Resources/HellionStrings.hu.resx | 26 ++++-- HellionChat/Resources/HellionStrings.it.resx | 26 ++++-- HellionChat/Resources/HellionStrings.ja.resx | 26 ++++-- HellionChat/Resources/HellionStrings.ko.resx | 26 ++++-- HellionChat/Resources/HellionStrings.nb.resx | 26 ++++-- HellionChat/Resources/HellionStrings.nl.resx | 26 ++++-- HellionChat/Resources/HellionStrings.pl.resx | 26 ++++-- .../Resources/HellionStrings.pt-BR.resx | 26 ++++-- .../Resources/HellionStrings.pt-PT.resx | 26 ++++-- HellionChat/Resources/HellionStrings.resx | 26 ++++-- HellionChat/Resources/HellionStrings.ro.resx | 26 ++++-- HellionChat/Resources/HellionStrings.ru.resx | 26 ++++-- HellionChat/Resources/HellionStrings.sv.resx | 26 ++++-- HellionChat/Resources/HellionStrings.tr.resx | 26 ++++-- HellionChat/Resources/HellionStrings.uk.resx | 26 ++++-- .../Resources/HellionStrings.zh-Hans.resx | 26 ++++-- .../Resources/HellionStrings.zh-Hant.resx | 26 ++++-- .../SelfTests/ConfigMigrationV27Step.cs | 11 ++- HellionChat/Services/TellRouterService.cs | 32 ++++--- .../Ui/Components/Settings/SettingsWidgets.cs | 30 ++++++- .../Components/Settings/Tabs/ChannelsTab.cs | 68 ++++++++++++--- HellionChat/Ui/Components/Sidebar.cs | 15 +++- HellionChat/Ui/Components/TabContextMenu.cs | 83 ++++++++++++++++-- HellionChat/Ui/Components/TopTabBar.cs | 2 +- HellionChat/Ui/StyleEngine/Metrics.cs | 9 +- HellionChat/Ui/Windows/ChannelPopoutWindow.cs | 3 + .../Ui/Windows/IFocusableChatWindow.cs | 7 ++ HellionChat/Ui/Windows/MainWindow.cs | 53 +++++++++--- HellionChat/Util/ChatInputBusy.cs | 25 ++++++ HellionChat/Util/TabLifecycleHelpers.cs | 84 ++++++++++++++++--- 42 files changed, 882 insertions(+), 284 deletions(-) create mode 100644 HellionChat/Util/ChatInputBusy.cs diff --git a/HellionChat/AutoTellTabsService.cs b/HellionChat/AutoTellTabsService.cs index ac01d39..758c984 100644 --- a/HellionChat/AutoTellTabsService.cs +++ b/HellionChat/AutoTellTabsService.cs @@ -353,13 +353,6 @@ internal sealed class AutoTellTabsService : IDisposable tab.AddMessage(currentMessage, unread: true); - // Flag the tab as a pop-out if configured; the marshalled TryOpen below reads - // that flag to open the real window. - if (Plugin.Config.AutoTellTabsOpenAsPopout) - { - tab.PopOut = true; - } - return tab; } @@ -373,20 +366,13 @@ internal sealed class AutoTellTabsService : IDisposable Plugin.Config.Tabs.Add(tab); - // Actually open the pop-out window for the flagged tab — without this the - // flag was dead (a PopOut tab with no window). CommitTempTab runs on the - // PendingMessage worker thread under Plugin.TabsListLock; TryOpen does - // OnTabActivated + Bind (window state Draw reads), so marshal onto the - // framework thread. If the pool is full, drop the flag so it never claims a - // window it didn't get (flag/window parity). - if (tab.PopOut) - { - Plugin.Framework.RunOnFrameworkThread(() => - { - if (!_plugin.ChannelPopoutPool.TryOpen(tab)) - tab.PopOut = false; - }); - } + // No pop-out is opened here any more. This service owns tab CREATION and + // lifecycle; where a tell becomes VISIBLE is TellRouterService's single + // decision (v2.0.5). It used to be both, and this one always won the race + // -- it fired at creation, the router only a tick later -- which is why + // TellAutoOpenMode looked like it did nothing for anyone with the old + // AutoTellTabsOpenAsPopout flag on, and why the router's "is the user + // mid-sentence" guard could be walked straight past. } private static Tab BuildTempTab(string playerName, uint worldRowId) diff --git a/HellionChat/Configuration.cs b/HellionChat/Configuration.cs index 8504f6f..9deb59c 100755 --- a/HellionChat/Configuration.cs +++ b/HellionChat/Configuration.cs @@ -35,7 +35,7 @@ public class ConfigKeyBind [Serializable] public class Configuration : IPluginConfiguration { - internal const int LatestVersion = 27; + internal const int LatestVersion = 28; public int Version { get; set; } = LatestVersion; @@ -167,6 +167,11 @@ public class Configuration : IPluginConfiguration // On by default: the wizard's closing step tells the user to try /tell and // watch a conversation open on its own, so the behaviour it describes has to // be the behaviour they get. + // Retired in v2.0.5: this and TellAutoOpenMode were two settings for one + // decision, and this one always won because it fired first -- which made the + // other one look broken. The FIELD stays so the v28 migration can still read + // what the user actually had; nothing else reads it, and it is gone from the + // settings window. public bool AutoTellTabsOpenAsPopout = true; // How sender names are rendered in the chat log. @@ -296,7 +301,11 @@ public class Configuration : IPluginConfiguration public bool MainWindowOpen = true; public bool SettingsWindowOpen; public int MaxParallelPopouts = 8; - public TellAutoOpenMode TellAutoOpenMode = TellAutoOpenMode.Sidebar; + + // Popout, not Sidebar: with AutoTellTabsOpenAsPopout defaulting to true, a + // fresh install has ALWAYS opened tells in their own window. Naming that as + // the default is what keeps a new install behaving the way it always did. + public TellAutoOpenMode TellAutoOpenMode = TellAutoOpenMode.Popout; // When true (default) the tell-auto-open router switches the active tab to the // incoming tell on every message; when false the tab is still created/revealed @@ -318,23 +327,6 @@ public enum TellAutoOpenMode Popout, } -public static class TellAutoOpenModeExt -{ - // The only display name set still in English. It sat inline in ChannelsTab - // as a literal array, which is why it was missed when the rest moved into - // resources; here it is at least in the same place as its peers for the - // localisation pass to pick up. - public static string Name(this TellAutoOpenMode mode) => - mode switch - { - TellAutoOpenMode.Off => "Off", - TellAutoOpenMode.Sidebar => "Sidebar", - TellAutoOpenMode.TopTab => "Top tab", - TellAutoOpenMode.Popout => "Popout", - _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null), - }; -} - [Serializable] public enum MainWindowLayoutMode { diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index 7926afd..596fbaf 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -354,6 +354,32 @@ public sealed class Plugin : IAsyncDalamudPlugin ); } + // v28 migration: two settings could open a tell in its own window, + // and AutoTellTabsOpenAsPopout won every race because it fired at tab + // creation while TellAutoOpenMode only got asked a tick later -- by + // which point the window was already open and the mode read back as + // "nothing to do". Anyone with the old flag on was, in practice, on + // Popout, so that is what carries forward. The flag itself is left + // alone: it is the only record of what was chosen, and clearing it + // would make a re-run of this step silently change a later decision. + if (Config.Version < 28) + { + if (Config.AutoTellTabsOpenAsPopout) + { + Config.TellAutoOpenMode = TellAutoOpenMode.Popout; + } + else if (Config.TellAutoOpenMode == TellAutoOpenMode.Off) + { + // "Off" never stopped the tab from being created -- that is the + // auto-tell switch further up -- it only stopped the jump to it, + // which is exactly what TellAutoOpenSwitchAlways is for. Two + // controls for one decision, and the one that read like "no tell + // tabs at all" was the misleading one. Same behaviour, said once. + Config.TellAutoOpenMode = TellAutoOpenMode.Sidebar; + Config.TellAutoOpenSwitchAlways = false; + } + } + // v25 carried no migration step; the bump was documentation. // // v26 does. NameCameFromPartner is what screenshot mode reads to decide @@ -392,7 +418,7 @@ public sealed class Plugin : IAsyncDalamudPlugin } } - Config.Version = 27; + Config.Version = Configuration.LatestVersion; // Unpinned TempTabs are session-only and dropped on every load. Pinned // TempTabs survive reload -- tester feedback in v1.4.7. diff --git a/HellionChat/Resources/HellionStrings.Designer.cs b/HellionChat/Resources/HellionStrings.Designer.cs index 2d89050..864abd8 100644 --- a/HellionChat/Resources/HellionStrings.Designer.cs +++ b/HellionChat/Resources/HellionStrings.Designer.cs @@ -219,8 +219,6 @@ internal class HellionStrings internal static string ChatLog_AutoTellTabs_Compact_Description => Get(nameof(ChatLog_AutoTellTabs_Compact_Description)); internal static string ChatLog_AutoTellTabs_GreetedToggle_Name => Get(nameof(ChatLog_AutoTellTabs_GreetedToggle_Name)); internal static string ChatLog_AutoTellTabs_GreetedToggle_Description => Get(nameof(ChatLog_AutoTellTabs_GreetedToggle_Description)); - internal static string ChatLog_AutoTellTabs_OpenAsPopout_Name => Get(nameof(ChatLog_AutoTellTabs_OpenAsPopout_Name)); - internal static string ChatLog_AutoTellTabs_OpenAsPopout_Description => Get(nameof(ChatLog_AutoTellTabs_OpenAsPopout_Description)); internal static string ChatLog_AutoTellTabs_PreloadHint => Get(nameof(ChatLog_AutoTellTabs_PreloadHint)); internal static string ChatLog_AutoTellTabs_ConflictHint => Get(nameof(ChatLog_AutoTellTabs_ConflictHint)); @@ -419,6 +417,9 @@ internal class HellionStrings internal static string Settings_Chat_CommandHelpSide_Description => Get(nameof(Settings_Chat_CommandHelpSide_Description)); internal static string Settings_Channels_TellAutoOpenMode_Name => Get(nameof(Settings_Channels_TellAutoOpenMode_Name)); internal static string Settings_Channels_TellAutoOpenMode_Description => Get(nameof(Settings_Channels_TellAutoOpenMode_Description)); + internal static string Settings_Channels_TellAutoOpen_MainWindow => Get(nameof(Settings_Channels_TellAutoOpen_MainWindow)); + internal static string Settings_Channels_TellAutoOpen_Popout => Get(nameof(Settings_Channels_TellAutoOpen_Popout)); + internal static string Settings_Channels_TellAutoOpen_BusyHint => Get(nameof(Settings_Channels_TellAutoOpen_BusyHint)); internal static string Settings_Channels_TellSwitchAlways_Name => Get(nameof(Settings_Channels_TellSwitchAlways_Name)); internal static string Settings_Channels_TellSwitchAlways_Description => Get(nameof(Settings_Channels_TellSwitchAlways_Description)); internal static string Settings_Window_LayoutSidebar => Get(nameof(Settings_Window_LayoutSidebar)); @@ -504,6 +505,9 @@ internal class HellionStrings internal static string Tabs_NotificationSound_Option => Get(nameof(Tabs_NotificationSound_Option)); internal static string Tabs_NotificationSound_Preview => Get(nameof(Tabs_NotificationSound_Preview)); internal static string Tabs_NotificationSound_CustomOption => Get(nameof(Tabs_NotificationSound_CustomOption)); + internal static string Tabs_Close_MenuItem => Get(nameof(Tabs_Close_MenuItem)); + internal static string Tabs_Close_UnpinFirst => Get(nameof(Tabs_Close_UnpinFirst)); + internal static string Tabs_Close_LastTab => Get(nameof(Tabs_Close_LastTab)); // Scroll-to-bottom and item/flag linking internal static string ChatLog_ScrollToBottom_Tooltip => Get(nameof(ChatLog_ScrollToBottom_Tooltip)); diff --git a/HellionChat/Resources/HellionStrings.ca.resx b/HellionChat/Resources/HellionStrings.ca.resx index e45caac..3415598 100644 --- a/HellionChat/Resources/HellionStrings.ca.resx +++ b/HellionChat/Resources/HellionStrings.ca.resx @@ -480,7 +480,7 @@ Amplada de la barra lateral de pestanyes en píxels. El valor per defecte (44 px) és només icones; amplia-la per encabir les capçaleres de secció com "Tells actius (3)" sense que es tallin. - Has assolit el màxim de {0} pestanyes de tell fixades. Desfixa'n una primer, o utilitza Converteix en permanent. + Has assolit el màxim de {0} pestanyes de tell fixades. Desfixa'n una primer. Fixada: sobreviu al reconnectar. @@ -514,12 +514,6 @@ Afegeix un botó al costat de cada pestanya auto-tell per marcar un interlocutor com a ja saludat: el nom de la pestanya s'atenua. Útil per als greeters de clubs que gestionen moltes converses en paral·lel. Desactivat per defecte. - - Obre les pestanyes de /tell noves directament com a finestres emergents - - - Quan està actiu, cada nova pestanya de /tell s'obre immediatament com a finestra pròpia. Tancar la finestra retorna la pestanya a la barra lateral. - El nombre de tells precarregats es pot configurar a la pestanya Privadesa. @@ -1032,6 +1026,15 @@ So de Hellion + + Tanca la pestanya + + + Desfixa aquesta pestanya abans de tancar-la. + + + Ha de quedar almenys una pestanya. Crea'n una altra primer. + No s'ha pogut lliurar el tell. @@ -1187,6 +1190,15 @@ On s'obre un tell quan arriba. + + Finestra principal + + + Finestra emergent + + + Mentre escrius, no s'obre cap finestra. La pestanya espera a la finestra principal perquè el teclat no torni al joc. + Canvia a la pestanya a cada tell diff --git a/HellionChat/Resources/HellionStrings.cs.resx b/HellionChat/Resources/HellionStrings.cs.resx index 8580025..a699769 100644 --- a/HellionChat/Resources/HellionStrings.cs.resx +++ b/HellionChat/Resources/HellionStrings.cs.resx @@ -480,7 +480,7 @@ Šířka postranního panelu záložek v pixelech. Výchozí (44 px) zobrazuje pouze ikony. Rozšíř ho, aby se záhlaví sekcí jako „Aktivní telly (3)" zobrazovala celá bez oříznutí. - Dosažen maximální počet {0} připnutých tell záložek. Nejdřív jednu odepni nebo použij Povýšit na trvalou. + Dosažen maximální počet {0} připnutých tell záložek. Nejdřív jednu odepni. Připnuto: přežívá relog. @@ -514,12 +514,6 @@ Přidá klikací tlačítko vedle každé auto-tell záložky pro označení konverzačního partnera jako již pozdraveného: název záložky se pak ztlumí. Užitečné pro greetery v klubech, kteří paralelně vedou mnoho konverzací. Ve výchozím stavu vypnuto. - - Otevírat nové /tell záložky přímo jako pop-outy - - - Když je aktivní, každá nově vytvořená /tell záložka se okamžitě otevře jako vlastní okno. Zavřením okna se záložka vrátí do postranního panelu. - Počet předem načtených tellů lze nastavit v záložce Soukromí. @@ -1031,6 +1025,15 @@ Hellion zvuk + + Zavřít záložku + + + Nejdřív záložku odepni, pak ji můžeš zavřít. + + + Musí zůstat alespoň jedna záložka. Vytvoř nejdřív další. + Tell nebylo možné doručit. @@ -1186,6 +1189,15 @@ Kde se tell otevře, když dorazí. + + Hlavní okno + + + Vyskakovací okno + + + Když píšeš, žádné okno se neotevře. Záložka počká v hlavním okně, aby se klávesnice nevrátila do hry. + Přepnout na kartu při každém tell diff --git a/HellionChat/Resources/HellionStrings.da.resx b/HellionChat/Resources/HellionStrings.da.resx index 5349e5b..1645548 100644 --- a/HellionChat/Resources/HellionStrings.da.resx +++ b/HellionChat/Resources/HellionStrings.da.resx @@ -480,7 +480,7 @@ Bredden af tab-sidebjælken i pixels. Standard (44 px) er kun ikon. Udvid den for at vise sektionsoverskrifter som "Aktive tells (3)" uden afskæring. - Maksimum på {0} fastgjorte tell-tabs nået. Frigør én først, eller brug Konvertér til permanent. + Maksimum på {0} fastgjorte tell-tabs nået. Frigør én først. Fastgjort: overlever genlog. @@ -514,12 +514,6 @@ Tilføjer en klikknap ved siden af hver auto-tell-tab til at markere en samtalepartner som allerede hilset på: tab-navnet dæmpes derefter. Nyttigt for club-greeter der håndterer mange samtaler parallelt. Fra som standard. - - Åbn nye /tell-tabs direkte som pop-outs - - - Når aktiv åbnes hver nyoprettet /tell-tab straks som sit eget vindue. Lukker man vinduet vender tab'en tilbage til sidebjælken. - Antallet af forudindlæste tells kan konfigureres under fanen Privatliv. @@ -1031,6 +1025,15 @@ Hellion-lyd + + Luk tab + + + Frigør denne tab, før du lukker den. + + + Der skal blive mindst én tab tilbage. Opret en anden først. + Et tell kunne ikke leveres. @@ -1186,6 +1189,15 @@ Hvor en tell åbnes, når den ankommer. + + Hovedvindue + + + Popud-vindue + + + Mens du skriver, åbnes der ikke noget vindue. Fanen venter i hovedvinduet, så tastaturet ikke hopper tilbage til spillet. + Skift til tab ved hver tell diff --git a/HellionChat/Resources/HellionStrings.de.resx b/HellionChat/Resources/HellionStrings.de.resx index 4d95b1b..ded6a57 100644 --- a/HellionChat/Resources/HellionStrings.de.resx +++ b/HellionChat/Resources/HellionStrings.de.resx @@ -468,7 +468,7 @@ Tab lösen - Maximal {0} angepinnte Flüster-Tabs erreicht. Erst einen lösen oder dauerhaft behalten. + Maximal {0} angepinnte Flüster-Tabs erreicht. Erst einen lösen. Angepinnt: überlebt Relog. @@ -514,12 +514,6 @@ Fügt neben jedem Auto-Flüster-Tab einen Klick-Button hinzu, um einen Gesprächspartner als bereits begrüßt zu markieren: der Tab-Name wird dann gedimmt. Nützlich für Club-Greeter, die parallel viele Konversationen führen. Standardmäßig aus. - - Neue /tell-Tabs direkt als Pop-Out öffnen - - - Wenn aktiv, wird jeder neu angelegte /tell-Tab sofort als eigenes Fenster geöffnet. Beim Schließen des Fensters kehrt der Tab in die Seitenleiste zurück. - Die Anzahl der vorgeladenen Flüsternachrichten lässt sich im Datenschutz-Tab einstellen. @@ -1026,6 +1020,15 @@ Hellion-Sound + + Tab schließen + + + Löse den Tab, bevor du ihn schließt. + + + Mindestens ein Tab muss bleiben. Lege zuerst einen weiteren an. + Ein Flüstern konnte nicht zugestellt werden. @@ -1181,6 +1184,15 @@ Wo ein Flüstern geöffnet wird, wenn er ankommt. + + Hauptfenster + + + Pop-Out-Fenster + + + Während du schreibst, öffnet sich kein Fenster. Der Tab wartet im Hauptfenster, damit die Tastatur nicht ins Spiel zurückspringt. + Bei jedem Flüstern zum Tab wechseln diff --git a/HellionChat/Resources/HellionStrings.el.resx b/HellionChat/Resources/HellionStrings.el.resx index 605acd7..50ff501 100644 --- a/HellionChat/Resources/HellionStrings.el.resx +++ b/HellionChat/Resources/HellionStrings.el.resx @@ -480,7 +480,7 @@ Πλάτος του sidebar καρτελών σε pixels. Η προεπιλογή (44 px) είναι μόνο εικονίδιο. Διεύρυνε για να χωράνε οι επικεφαλίδες ενοτήτων όπως "Active Tells (3)" χωρίς αποκοπή. - Έχει συμπληρωθεί το μέγιστο των {0} καρφιτσωμένων tell tabs. Ξεκαρφίτσωσε πρώτα μία ή χρησιμοποίησε Προαγωγή σε μόνιμη. + Έχει συμπληρωθεί το μέγιστο των {0} καρφιτσωμένων καρτελών tell. Ξεκαρφίτσωσε πρώτα μία. Καρφιτσωμένο: επιβιώνει relog. @@ -514,12 +514,6 @@ Προσθέτει ένα κουμπί δίπλα σε κάθε auto-tell tab για να σημειώσεις έναν συνομιλητή ως ήδη χαιρετισμένο. Το όνομα της καρτέλας αμβλύνεται τότε. Χρήσιμο για greeters club που διαχειρίζονται πολλές συνομιλίες παράλληλα. Απενεργοποιημένο εξ ορισμού. - - Άνοιγμα νέων /tell tabs απευθείας ως pop-outs - - - Όταν είναι ενεργό, κάθε νεοδημιουργημένη /tell καρτέλα ανοίγει αμέσως ως δικό της παράθυρο. Κλείνοντας το παράθυρο επιστρέφει η καρτέλα στο sidebar. - Ο αριθμός των προφορτωμένων tells μπορεί να ρυθμιστεί στην καρτέλα Απόρρητο. @@ -1031,6 +1025,15 @@ Ήχος Hellion + + Κλείσιμο καρτέλας + + + Ξεκαρφίτσωσε πρώτα την καρτέλα για να την κλείσεις. + + + Πρέπει να μείνει τουλάχιστον μία καρτέλα. Δημιούργησε πρώτα μια άλλη. + Ένα tell δεν μπόρεσε να παραδοθεί. @@ -1186,6 +1189,15 @@ Πού ανοίγει ένα tell όταν φτάνει. + + Κύριο παράθυρο + + + Αναδυόμενο παράθυρο + + + Όσο πληκτρολογείς, δεν ανοίγει παράθυρο. Η καρτέλα περιμένει στο κύριο παράθυρο, ώστε το πληκτρολόγιο να μην επιστρέψει στο παιχνίδι. + Μετάβαση στην καρτέλα σε κάθε tell diff --git a/HellionChat/Resources/HellionStrings.es.resx b/HellionChat/Resources/HellionStrings.es.resx index 9797a3b..bdddec2 100644 --- a/HellionChat/Resources/HellionStrings.es.resx +++ b/HellionChat/Resources/HellionStrings.es.resx @@ -480,7 +480,7 @@ Ancho de la barra lateral de pestañas en píxeles. El valor predeterminado (44 px) es solo icono; amplíalo para que encabezados de sección como "Tells activos (3)" no se corten. - Se alcanzó el máximo de {0} pestañas de tell fijadas. Desija una primero o usa Convertir en permanente. + Se alcanzó el máximo de {0} pestañas de tell fijadas. Desfija una primero. Fijada: sobrevive al relog. @@ -514,12 +514,6 @@ Añade un botón junto a cada pestaña de tell automática para marcar a un interlocutor como ya saludado: el nombre de la pestaña se atenúa. Útil para los saludadores de clubs que gestionan muchas conversaciones en paralelo. Desactivado por defecto. - - Abrir nuevas pestañas de /tell directamente como ventanas emergentes - - - Cuando está activo, cada nueva pestaña de /tell se abre inmediatamente como su propia ventana. Cerrar la ventana devuelve la pestaña a la barra lateral. - El número de tells precargados se puede configurar en la pestaña Privacidad. @@ -1032,6 +1026,15 @@ Sonido Hellion + + Cerrar pestaña + + + Desfija esta pestaña antes de cerrarla. + + + Debe quedar al menos una pestaña. Crea otra primero. + No se pudo entregar el tell. @@ -1187,6 +1190,15 @@ Dónde se abre un tell cuando llega. + + Ventana principal + + + Ventana emergente + + + Mientras escribes, no se abre ninguna ventana. La pestaña espera en la ventana principal para que el teclado no vuelva al juego. + Cambiar a la pestaña con cada tell diff --git a/HellionChat/Resources/HellionStrings.fi.resx b/HellionChat/Resources/HellionStrings.fi.resx index 1c69678..29aa6f4 100644 --- a/HellionChat/Resources/HellionStrings.fi.resx +++ b/HellionChat/Resources/HellionStrings.fi.resx @@ -480,7 +480,7 @@ Välilehtisivupalkin leveys pikseleinä. Oletus (44 px) on vain kuvake; levennä niin, että osion otsikot kuten "Active Tells (3)" mahtuvat ilman katkaisua. - Kiinnitettyjen tell-välilehtien enimmäismäärä {0} saavutettu. Irrota ensin yksi tai muuta pysyväksi välilehdeksi. + Kiinnitettyjen tell-välilehtien enimmäismäärä {0} saavutettu. Irrota ensin yksi. Kiinnitetty: selviää relogista. @@ -514,12 +514,6 @@ Lisää napsautuspainikkeen jokaisen auto-tell-välilehden viereen, jolla voit merkitä keskustelukumppanin jo tervehdytyksi: välilehden nimi himmenee tällöin. Hyödyllinen klubi-tervehtijöille, jotka hallitsevat useita samanaikaisia keskusteluja. Oletuksena pois päältä. - - Avaa uudet /tell-välilehdet suoraan irrotettuna ikkunana - - - Kun aktiivinen, jokainen juuri luotu /tell-välilehti avataan välittömästi omana ikkunanaan. Ikkunan sulkeminen palauttaa välilehden sivupalkkiin. - Esivalmistellujen tellien määrä on määritettävissä Tietosuoja-välilehdellä. @@ -1031,6 +1025,15 @@ Hellion-ääni + + Sulje välilehti + + + Irrota välilehti ennen kuin suljet sen. + + + Vähintään yhden välilehden on jäätävä. Luo ensin toinen. + Telliä ei voitu toimittaa. @@ -1186,6 +1189,15 @@ Missä tell avautuu saapuessaan. + + Pääikkuna + + + Ponnahdusikkuna + + + Kirjoittaessasi ikkunaa ei avata. Välilehti odottaa pääikkunassa, jottei näppäimistö palaa peliin. + Vaihda välilehteen jokaisella tellillä diff --git a/HellionChat/Resources/HellionStrings.fr.resx b/HellionChat/Resources/HellionStrings.fr.resx index cd8523b..68df02f 100644 --- a/HellionChat/Resources/HellionStrings.fr.resx +++ b/HellionChat/Resources/HellionStrings.fr.resx @@ -480,7 +480,7 @@ Largeur de la barre latérale d'onglets en pixels. La valeur par défaut (44 px) n'affiche que les icônes ; élargissez-la pour permettre l'affichage complet d'en-têtes de section comme « Messages privés actifs (3) » sans troncature. - Maximum de {0} onglets de message privé épinglés atteint. Désépinglez-en un d'abord, ou utilisez Promouvoir en permanent. + Maximum de {0} onglets de message privé épinglés atteint. Désépinglez-en un d'abord. Épinglé : survit à la reconnexion. @@ -514,12 +514,6 @@ Ajoute un bouton à côté de chaque onglet de message privé automatique pour marquer un partenaire de conversation comme déjà salué. Le nom de l'onglet est alors atténué. Utile pour les hôtes de club qui gèrent de nombreuses conversations en parallèle. Désactivé par défaut. - - Ouvrir les nouveaux onglets /tell directement comme fenêtres détachées - - - Quand cette option est active, chaque onglet /tell nouvellement créé est immédiatement ouvert dans sa propre fenêtre. Fermer la fenêtre renvoie l'onglet dans la barre latérale. - Le nombre de messages privés préchargés peut être configuré dans l'onglet Confidentialité. @@ -1032,6 +1026,15 @@ Son Hellion + + Fermer l'onglet + + + Désépinglez cet onglet avant de le fermer. + + + Au moins un onglet doit rester. Créez-en un autre d'abord. + Un Message privé n'a pas pu être remis. @@ -1187,6 +1190,15 @@ Où s'ouvre un message privé à son arrivée. + + Fenêtre principale + + + Fenêtre détachée + + + Pendant que vous écrivez, aucune fenêtre ne s'ouvre. L'onglet attend dans la fenêtre principale, pour que le clavier ne retourne pas au jeu. + Basculer vers l'onglet à chaque message privé diff --git a/HellionChat/Resources/HellionStrings.hu.resx b/HellionChat/Resources/HellionStrings.hu.resx index ca46b60..7dd5236 100644 --- a/HellionChat/Resources/HellionStrings.hu.resx +++ b/HellionChat/Resources/HellionStrings.hu.resx @@ -480,7 +480,7 @@ A fül-oldalsáv szélessége képpontban. Az alapértelmezés (44 px) csak ikont mutat; szélesítsd meg, hogy a szakaszfejlécek (pl. „Aktív tellek (3)") ne legyenek levágva. - Elérted a {0} rögzített tell-fül maximumát. Először oldd fel az egyiket, vagy használd az Állandó füllé előléptetés funkciót. + Elérted a {0} rögzített tell-fül maximumát. Először oldd fel az egyiket. Rögzített: túléli az újrabejelentkezést. @@ -514,12 +514,6 @@ Minden auto-tell-fül mellé egy kattintható gombot ad, amellyel a beszélgető partnert már üdvözöltként lehet megjelölni: a fül neve ezután halvány lesz. Hasznos klubi köszöntőknek, akik párhuzamosan sok beszélgetést kezelnek. Alapértelmezés szerint ki van kapcsolva. - - Új /tell-fülek megnyitása közvetlenül pop-outként - - - Ha aktív, minden újonnan létrehozott /tell-fül azonnal saját ablakként nyílik meg. Az ablak bezárásakor a fül visszakerül az oldalsávba. - Az előtöltött tellek száma az Adatvédelem fülön állítható be. @@ -1031,6 +1025,15 @@ Hellion hang + + Fül bezárása + + + Előbb oldd fel a fül rögzítését, utána zárhatod be. + + + Legalább egy fülnek maradnia kell. Előbb hozz létre egy másikat. + Egy tell nem kézbesíthető. @@ -1186,6 +1189,15 @@ Hol nyílik meg egy tell, amikor megérkezik. + + Főablak + + + Kiugró ablak + + + Amíg gépelsz, nem nyílik ablak. A fül a főablakban vár, hogy a billentyűzet ne ugorjon vissza a játékba. + Váltás a fülre minden tellnél diff --git a/HellionChat/Resources/HellionStrings.it.resx b/HellionChat/Resources/HellionStrings.it.resx index 26b3ca2..0f5f94c 100644 --- a/HellionChat/Resources/HellionStrings.it.resx +++ b/HellionChat/Resources/HellionStrings.it.resx @@ -480,7 +480,7 @@ Larghezza della barra laterale dei tab in pixel. Il predefinito (44 px) mostra solo le icone; allargala per visualizzare le intestazioni di sezione come "Tell attivi (3)" senza troncamento. - Raggiunto il massimo di {0} tab tell fissi. Sblocca prima uno o usa Converti in permanente. + Raggiunto il massimo di {0} tab tell fissi. Sblocca prima uno. Fisso: sopravvive al relog. @@ -514,12 +514,6 @@ Aggiunge un pulsante cliccabile accanto a ogni tab tell automatico per contrassegnare un partner di conversazione come già salutato: il nome del tab viene poi attenuato. Utile per i greeter dei club che gestiscono molte conversazioni in parallelo. Disattivato per impostazione predefinita. - - Apri i nuovi tab /tell direttamente come pop-out - - - Se attivo, ogni nuovo tab /tell creato viene aperto immediatamente come finestra separata. Chiudendo la finestra il tab torna alla barra laterale. - Il numero di tell precaricati può essere configurato nel tab Privacy. @@ -1032,6 +1026,15 @@ Suono Hellion + + Chiudi tab + + + Sblocca questo tab prima di chiuderlo. + + + Deve rimanere almeno un tab. Creane prima un altro. + Un tell non può essere consegnato. @@ -1187,6 +1190,15 @@ Dove si apre un tell quando arriva. + + Finestra principale + + + Finestra pop-out + + + Mentre scrivi non si apre nessuna finestra. Il tab attende nella finestra principale, così la tastiera non torna al gioco. + Passa alla scheda a ogni tell diff --git a/HellionChat/Resources/HellionStrings.ja.resx b/HellionChat/Resources/HellionStrings.ja.resx index 44e5e5f..a6157c2 100644 --- a/HellionChat/Resources/HellionStrings.ja.resx +++ b/HellionChat/Resources/HellionStrings.ja.resx @@ -480,7 +480,7 @@ タブサイドバーの幅(ピクセル単位)。デフォルト(44px)はアイコンのみ表示です。「アクティブなテル (3)」などのセクションヘッダーが切れないよう広げてください。 - ピン留めテルタブの上限 {0} 件に達しました。先に1件解除するか、「永続タブに昇格」をご利用ください。 + ピン留めテルタブの上限 {0} 件に達しました。先に1件解除してください。 ピン留め済み: 再ログイン後も残ります。 @@ -514,12 +514,6 @@ 各自動テルタブの横にクリックボタンを追加し、会話相手を挨拶済みとしてマークできます。マークするとタブ名が薄く表示されます。複数の会話を並行して処理するクラブグリーター向けの機能です。デフォルトはオフです。 - - 新しい /tell タブを直接ポップアウトとして開く - - - 有効にすると、新しく作成された /tell タブが即座に独立したウィンドウとして開きます。ウィンドウを閉じるとタブはサイドバーに戻ります。 - プリロードするテルの件数はプライバシータブで設定できます。 @@ -1032,6 +1026,15 @@ Hellionサウンド + + タブを閉じる + + + 閉じる前にタブのピン留めを解除してください。 + + + タブは最低1つ必要です。先に別のタブを作成してください。 + テルを届けられませんでした。 @@ -1187,6 +1190,15 @@ テルが届いたときにどこで開くか。 + + メインウィンドウ + + + ポップアウトウィンドウ + + + 入力中はウィンドウを開きません。タブはメインウィンドウで待機するため、キーボードがゲームに戻ることはありません。 + テルのたびにタブへ切り替える diff --git a/HellionChat/Resources/HellionStrings.ko.resx b/HellionChat/Resources/HellionStrings.ko.resx index f254ed5..3bfaaa8 100644 --- a/HellionChat/Resources/HellionStrings.ko.resx +++ b/HellionChat/Resources/HellionStrings.ko.resx @@ -480,7 +480,7 @@ 탭 사이드바의 너비 (픽셀). 기본값 (44px)은 아이콘만 표시됩니다. "활성 귓속말 (3)"과 같은 섹션 헤더가 잘리지 않으려면 너비를 넓히세요. - 고정 귓속말 탭의 최대 개수 {0}에 도달했습니다. 먼저 하나를 해제하거나 영구 탭으로 변환하세요. + 고정 귓속말 탭의 최대 개수 {0}에 도달했습니다. 먼저 하나를 해제하세요. 고정됨: 재접속 후에도 유지됩니다. @@ -514,12 +514,6 @@ 각 자동 귓속말 탭 옆에 클릭 버튼을 추가하여 대화 상대를 이미 인사한 것으로 표시합니다. 탭 이름이 흐리게 표시됩니다. 동시에 여러 대화를 관리하는 클럽 접대 담당자에게 유용합니다. 기본값은 꺼짐입니다. - - 새 /tell 탭을 팝아웃으로 바로 열기 - - - 활성화하면 새로 생성된 /tell 탭이 즉시 별도 창으로 열립니다. 창을 닫으면 탭이 사이드바로 돌아옵니다. - 미리 불러올 귓속말 수는 개인정보 탭에서 설정할 수 있습니다. @@ -1032,6 +1026,15 @@ Hellion 소리 + + 탭 닫기 + + + 닫기 전에 탭 고정을 해제하세요. + + + 탭은 최소 하나가 남아 있어야 합니다. 먼저 다른 탭을 만드세요. + 귓속말을 전달하지 못했습니다. @@ -1187,6 +1190,15 @@ 귓속말이 도착했을 때 어디에서 열릴지. + + 기본 창 + + + 팝아웃 창 + + + 입력하는 동안에는 창이 열리지 않습니다. 탭이 기본 창에서 대기하므로 키보드가 게임으로 돌아가지 않습니다. + 귓속말마다 탭으로 전환 diff --git a/HellionChat/Resources/HellionStrings.nb.resx b/HellionChat/Resources/HellionStrings.nb.resx index c24b57c..acb96bf 100644 --- a/HellionChat/Resources/HellionStrings.nb.resx +++ b/HellionChat/Resources/HellionStrings.nb.resx @@ -480,7 +480,7 @@ Bredde på fane-sidefeltet i piksler. Standard (44 px) er kun ikon. Gjør det bredere for å vise seksjonsoverskrifter som "Aktive tells (3)" uten avkutting. - Maksimalt antall på {0} festede tell-faner er nådd. Løsgjør én først, eller bruk Gjør permanent. + Maksimalt antall på {0} festede tell-faner er nådd. Løsgjør én først. Festet: overlever relog. @@ -514,12 +514,6 @@ Legger til en klikknapp ved siden av hver auto-tell-fane for å merke en samtalepartner som allerede hilset: fanenavnet dempes da. Nyttig for klubbvakter som håndterer mange samtaler parallelt. Av som standard. - - Åpne nye /tell-faner direkte som pop-out-vinduer - - - Når aktivt åpnes hver nyopprettet /tell-fane umiddelbart som sitt eget vindu. Å lukke vinduet returnerer fanen til sidefeltet. - Antallet forhåndslastede tells kan konfigureres i Personvern-fanen. @@ -1031,6 +1025,15 @@ Hellion-lyd + + Lukk fane + + + Løsgjør denne fanen før du lukker den. + + + Minst én fane må bli igjen. Opprett en annen først. + Et tell kunne ikke leveres. @@ -1186,6 +1189,15 @@ Hvor en tell åpnes når den kommer. + + Hovedvindu + + + Utskilt vindu + + + Mens du skriver, åpnes ingen vinduer. Fanen venter i hovedvinduet, slik at tastaturet ikke hopper tilbake til spillet. + Bytt til fanen ved hver tell diff --git a/HellionChat/Resources/HellionStrings.nl.resx b/HellionChat/Resources/HellionStrings.nl.resx index 956cc79..330694e 100644 --- a/HellionChat/Resources/HellionStrings.nl.resx +++ b/HellionChat/Resources/HellionStrings.nl.resx @@ -480,7 +480,7 @@ Breedte van de tabbladzijbalk in pixels. De standaard (44 px) toont alleen pictogrammen; vergroot dit zodat sectiekoppen zoals "Actieve tells (3)" niet worden afgekapt. - Maximum van {0} vastgepinde tell-tabbladen bereikt. Maak er eerst één los, of gebruik Omzetten naar permanent tabblad. + Maximum van {0} vastgepinde tell-tabbladen bereikt. Maak er eerst één los. Vastgepind: overleeft relog. @@ -514,12 +514,6 @@ Voegt een klikknop toe naast elk auto-tell-tabblad om een gesprekspartner als al begroet te markeren: de tabbladnaam wordt dan gedimd. Handig voor club-greeters die tegelijkertijd veel gesprekken beheren. Standaard uitgeschakeld. - - Nieuwe /tell-tabbladen direct als pop-out openen - - - Als dit actief is, wordt elk nieuw aangemaakt /tell-tabblad meteen geopend als een eigen venster. Het sluiten van het venster brengt het tabblad terug naar de zijbalk. - Het aantal vooraf geladen tells kan worden ingesteld in het tabblad Privacy. @@ -1032,6 +1026,15 @@ Hellion-geluid + + Tabblad sluiten + + + Maak dit tabblad los voordat je het sluit. + + + Er moet minstens één tabblad overblijven. Maak eerst een ander aan. + Een tell kon niet worden bezorgd. @@ -1187,6 +1190,15 @@ Waar een tell opent wanneer die binnenkomt. + + Hoofdvenster + + + Pop-outvenster + + + Terwijl je typt, gaat er geen venster open. Het tabblad wacht in het hoofdvenster, zodat het toetsenbord niet terugspringt naar het spel. + Bij elke tell naar het tabblad schakelen diff --git a/HellionChat/Resources/HellionStrings.pl.resx b/HellionChat/Resources/HellionStrings.pl.resx index 090f796..eb05fb7 100644 --- a/HellionChat/Resources/HellionStrings.pl.resx +++ b/HellionChat/Resources/HellionStrings.pl.resx @@ -480,7 +480,7 @@ Szerokość paska bocznego zakładek w pikselach. Domyślna (44 px) pokazuje tylko ikony; poszerz, żeby nagłówki sekcji jak „Aktywne tells (3)" nie były obcinane. - Osiągnięto maksymalną liczbę {0} przypiętych zakładek tell. Najpierw odepnij jedną lub użyj opcji Przekształć w stałą zakładkę. + Osiągnięto maksymalną liczbę {0} przypiętych zakładek tell. Najpierw odepnij jedną. Przypięta: przeżywa relog. @@ -514,12 +514,6 @@ Dodaje przycisk przy każdej auto-zakładce tell, pozwalający oznaczyć partnera rozmowy jako już powitanego: nazwa zakładki jest wtedy przyciemniona. Przydatne dla greeters w klubach prowadzących wiele rozmów równocześnie. Domyślnie wyłączone. - - Otwieraj nowe zakładki /tell bezpośrednio jako pop-outy - - - Gdy aktywne, każda nowo utworzona zakładka /tell jest natychmiast otwierana jako własne okno. Zamknięcie okna powoduje powrót zakładki do paska bocznego. - Liczbę wstępnie wczytywanych tells można skonfigurować w zakładce Prywatność. @@ -1031,6 +1025,15 @@ Dźwięk Hellion + + Zamknij zakładkę + + + Najpierw odepnij zakładkę, potem możesz ją zamknąć. + + + Musi zostać co najmniej jedna zakładka. Utwórz najpierw kolejną. + Tell nie mógł zostać dostarczony. @@ -1186,6 +1189,15 @@ Gdzie otwiera się tell, gdy nadejdzie. + + Okno główne + + + Okno wyskakujące + + + Gdy piszesz, żadne okno się nie otwiera. Zakładka czeka w oknie głównym, żeby klawiatura nie wróciła do gry. + Przełączaj na zakładkę przy każdym tell diff --git a/HellionChat/Resources/HellionStrings.pt-BR.resx b/HellionChat/Resources/HellionStrings.pt-BR.resx index 8b3ca2c..7acf917 100644 --- a/HellionChat/Resources/HellionStrings.pt-BR.resx +++ b/HellionChat/Resources/HellionStrings.pt-BR.resx @@ -480,7 +480,7 @@ Largura da barra lateral de abas em pixels. O padrão (44 px) é somente ícone; aumente para que cabeçalhos como "Tells Ativos (3)" não sejam cortados. - Limite máximo de {0} abas de tell fixadas atingido. Desafixe uma primeiro ou use Promover para permanente. + Limite máximo de {0} abas de tell fixadas atingido. Desafixe uma primeiro. Fixado: sobrevive ao relog. @@ -514,12 +514,6 @@ Adiciona um botão clicável ao lado de cada aba de tell automática para marcar um parceiro de conversa como já cumprimentado: o nome da aba fica esmaecido. Útil para recepcionistas de clube que gerenciam muitas conversas em paralelo. Desativado por padrão. - - Abrir novas abas de /tell diretamente como pop-outs - - - Quando ativo, cada nova aba de /tell criada é aberta imediatamente como sua própria janela. Fechar a janela retorna a aba para a barra lateral. - O número de tells pré-carregados pode ser configurado na aba Privacidade. @@ -1032,6 +1026,15 @@ Som Hellion + + Fechar aba + + + Desafixe esta aba antes de fechá-la. + + + Pelo menos uma aba precisa permanecer. Crie outra primeiro. + Um tell não pôde ser entregue. @@ -1187,6 +1190,15 @@ Onde um tell abre quando chega. + + Janela principal + + + Janela pop-out + + + Enquanto você digita, nenhuma janela é aberta. A aba espera na janela principal, para que o teclado não volte ao jogo. + Mudar para a aba a cada tell diff --git a/HellionChat/Resources/HellionStrings.pt-PT.resx b/HellionChat/Resources/HellionStrings.pt-PT.resx index 646ab52..b90cd20 100644 --- a/HellionChat/Resources/HellionStrings.pt-PT.resx +++ b/HellionChat/Resources/HellionStrings.pt-PT.resx @@ -480,7 +480,7 @@ Largura da barra lateral de separadores em píxeis. A predefinição (44 px) é apenas ícone; aumenta para que cabeçalhos de secção como "Tells ativos (3)" não sejam cortados. - Máximo de {0} separadores de tell fixos atingido. Desfixa um primeiro, ou usa Promover a permanente. + Máximo de {0} separadores de tell fixos atingido. Desafixa um primeiro. Fixado: sobrevive ao relog. @@ -514,12 +514,6 @@ Adiciona um botão de clique junto a cada separador de tell automático para marcar um parceiro de conversa como já cumprimentado: o nome do separador fica então esbatido. Útil para greeters de clubes que gerem muitas conversas em paralelo. Desativado por predefinição. - - Abrir novos separadores de /tell diretamente como janelas flutuantes - - - Quando ativo, cada novo separador de /tell criado é imediatamente aberto como janela própria. Fechar a janela devolve o separador à barra lateral. - O número de tells pré-carregados pode ser configurado no separador Privacidade. @@ -1031,6 +1025,15 @@ Som Hellion + + Fechar separador + + + Desafixa este separador antes de o fechares. + + + Tem de ficar pelo menos um separador. Cria primeiro outro. + Um tell não pôde ser entregue. @@ -1186,6 +1189,15 @@ Onde um tell abre quando chega. + + Janela principal + + + Janela destacável + + + Enquanto escreves, não abre nenhuma janela. O separador espera na janela principal, para que o teclado não volte ao jogo. + Mudar para o separador a cada tell diff --git a/HellionChat/Resources/HellionStrings.resx b/HellionChat/Resources/HellionStrings.resx index ea1b660..e9afa08 100644 --- a/HellionChat/Resources/HellionStrings.resx +++ b/HellionChat/Resources/HellionStrings.resx @@ -480,7 +480,7 @@ Width of the tab sidebar in pixels. The default (44 px) is icon-only; widen it to fit the section headers like "Active Tells (3)" without truncation. - Maximum of {0} pinned tell tabs reached. Unpin one first, or use Promote to permanent. + Maximum of {0} pinned tell tabs reached. Unpin one first. Pinned: survives relog. @@ -514,12 +514,6 @@ Adds a click button next to each auto-tell tab to mark a conversation partner as already greeted. The tab name is then dimmed. Useful for club greeters managing many conversations in parallel. Off by default. - - Open new /tell tabs directly as pop-outs - - - When active, each newly created /tell tab is immediately opened as its own window. Closing the window returns the tab to the sidebar. - The number of preloaded tells can be configured in the Privacy tab. @@ -889,6 +883,15 @@ Hellion sound + + Close Tab + + + Unpin this tab before closing it. + + + At least one tab has to remain. Create another one first. + @@ -1198,6 +1201,15 @@ Where a tell opens when it arrives. + + Main window + + + Pop-out window + + + While you are typing, no window opens. The tab waits in the main window instead, so the keyboard does not jump back into the game. + Switch to the tab on every tell diff --git a/HellionChat/Resources/HellionStrings.ro.resx b/HellionChat/Resources/HellionStrings.ro.resx index 6c584f4..7f559e1 100644 --- a/HellionChat/Resources/HellionStrings.ro.resx +++ b/HellionChat/Resources/HellionStrings.ro.resx @@ -480,7 +480,7 @@ Lățimea barei laterale cu tab-uri în pixeli. Implicit (44 px) afișează doar icoane; mărește-o pentru a încăpea antetele de secțiuni precum „Tell-uri active (3)" fără trunchiere. - Numărul maxim de {0} tab-uri tell fixate a fost atins. Dezfixează unul mai întâi sau folosește Promovează la permanent. + Numărul maxim de {0} tab-uri tell fixate a fost atins. Dezfixează unul mai întâi. Fixat: supraviețuiește relog-ului. @@ -514,12 +514,6 @@ Adaugă un buton de clic lângă fiecare tab auto-tell pentru a marca un partener de conversație ca deja salutat: numele tab-ului devine mai estompat. Util pentru greeter-ii de club care gestionează mai multe conversații în paralel. Dezactivat implicit. - - Deschide tab-urile /tell noi direct ca ferestre pop-out - - - Când este activ, fiecare tab /tell nou creat este imediat deschis ca propria fereastră. Închiderea ferestrei returnează tab-ul în bara laterală. - Numărul de tell-uri preîncărcate poate fi configurat în tab-ul Confidențialitate. @@ -1032,6 +1026,15 @@ Sunet Hellion + + Închide tab-ul + + + Dezfixează acest tab înainte de a-l închide. + + + Trebuie să rămână cel puțin un tab. Creează mai întâi altul. + Un tell nu a putut fi livrat. @@ -1187,6 +1190,15 @@ Unde se deschide un tell când sosește. + + Fereastra principală + + + Fereastră detașată + + + Cât timp scrii, nu se deschide nicio fereastră. Tabul așteaptă în fereastra principală, ca tastatura să nu revină în joc. + Comută pe tab la fiecare tell diff --git a/HellionChat/Resources/HellionStrings.ru.resx b/HellionChat/Resources/HellionStrings.ru.resx index aaf82af..66176c7 100644 --- a/HellionChat/Resources/HellionStrings.ru.resx +++ b/HellionChat/Resources/HellionStrings.ru.resx @@ -480,7 +480,7 @@ Ширина боковой панели вкладок в пикселях. По умолчанию (44 пкс) — только иконки; увеличьте, чтобы заголовки разделов вида «Активные ЛС (3)» не обрезались. - Достигнут максимум закреплённых вкладок ЛС: {0}. Сначала открепите одну или используйте «Преобразовать в постоянную». + Достигнут максимум закреплённых вкладок ЛС: {0}. Сначала открепите одну. Закреплено — переживает релог. @@ -514,12 +514,6 @@ Добавляет кнопку рядом с каждой авто-вкладкой ЛС для отметки собеседника как уже поприветствованного — имя вкладки будет затемнено. Полезно для приветствующих в клубе, ведущих много разговоров одновременно. По умолчанию выключено. - - Открывать новые вкладки /tell сразу как всплывающие окна - - - Когда активно, каждая вновь созданная вкладка /tell немедленно открывается в собственном окне. При закрытии окна вкладка возвращается на боковую панель. - Количество предзагружаемых ЛС можно настроить на вкладке Конфиденциальность. @@ -1032,6 +1026,15 @@ Звук Hellion + + Закрыть вкладку + + + Сначала открепите вкладку, потом её можно закрыть. + + + Должна остаться хотя бы одна вкладка. Сначала создайте другую. + Сообщение не было доставлено. @@ -1187,6 +1190,15 @@ Где открывается tell при получении. + + Главное окно + + + Всплывающее окно + + + Пока вы печатаете, окно не открывается. Вкладка ждёт в главном окне, чтобы клавиатура не вернулась в игру. + Переключаться на вкладку при каждом tell diff --git a/HellionChat/Resources/HellionStrings.sv.resx b/HellionChat/Resources/HellionStrings.sv.resx index e347905..2eeb224 100644 --- a/HellionChat/Resources/HellionStrings.sv.resx +++ b/HellionChat/Resources/HellionStrings.sv.resx @@ -480,7 +480,7 @@ Bredden på fliksidofältet i pixlar. Standard (44 px) är bara ikoner. Gör det bredare för att sektionsrubriker som "Aktiva tells (3)" inte ska kapas. - Maximalt {0} fästa tell-flikar nått. Lossa en först, eller använd Uppgradera till permanent. + Maximalt {0} fästa tell-flikar nått. Lossa en först. Fäst: överlever omloggning. @@ -514,12 +514,6 @@ Lägger till en klickknapp bredvid varje auto-tell-flik för att markera en konversationspartner som redan hälsad: fliknamnet tonas då ned. Användbart för klubbhälsare som hanterar många konversationer parallellt. Av som standard. - - Öppna nya /tell-flikar direkt som pop-out-fönster - - - När aktivt öppnas varje nyskapat /tell-flik omedelbart som ett eget fönster. Att stänga fönstret återför fliken till sidofältet. - Antalet förladda tells kan konfigureras i fliken Sekretess. @@ -1032,6 +1026,15 @@ Hellion-ljud + + Stäng flik + + + Lossa den här fliken innan du stänger den. + + + Minst en flik måste finnas kvar. Skapa en annan först. + Ett tell kunde inte levereras. @@ -1187,6 +1190,15 @@ Var en tell öppnas när den kommer. + + Huvudfönster + + + Utbrytningsfönster + + + Medan du skriver öppnas inget fönster. Fliken väntar i huvudfönstret, så att tangentbordet inte hoppar tillbaka till spelet. + Byt till fliken vid varje tell diff --git a/HellionChat/Resources/HellionStrings.tr.resx b/HellionChat/Resources/HellionStrings.tr.resx index f82c809..c9691da 100644 --- a/HellionChat/Resources/HellionStrings.tr.resx +++ b/HellionChat/Resources/HellionStrings.tr.resx @@ -480,7 +480,7 @@ Sekme kenar çubuğunun piksel cinsinden genişliği. Varsayılan (44 px) yalnızca ikon modundadır; "Aktif Tell'ler (3)" gibi bölüm başlıklarının kesilmeden görünmesi için genişlet. - En fazla {0} sabitlenmiş tell sekmesi sınırına ulaşıldı. Önce birini serbest bırak ya da kalıcıya yükselt. + En fazla {0} sabitlenmiş tell sekmesi sınırına ulaşıldı. Önce birini serbest bırak. Sabitlenmiş: yeniden girişten sonra hayatta kalır. @@ -514,12 +514,6 @@ Her otomatik tell sekmesinin yanına, konuşma ortağını zaten selamlananlar olarak işaretlemek için bir tıklama düğmesi ekler; sekme adı soluklaşır. Paralel olarak çok sayıda konuşmayı yöneten kulüp karşılayıcıları için kullanışlıdır. Varsayılan olarak kapalıdır. - - Yeni /tell sekmelerini doğrudan pop-out olarak aç - - - Etkinleştirildiğinde her yeni oluşturulan /tell sekmesi hemen kendi penceresi olarak açılır. Pencereyi kapatmak sekmeyi kenar çubuğuna geri döndürür. - Önceden yüklenen tell sayısı Gizlilik sekmesinden yapılandırılabilir. @@ -1031,6 +1025,15 @@ Hellion sesi + + Sekmeyi kapat + + + Kapatmadan önce sekmeyi serbest bırak. + + + En az bir sekme kalmalı. Önce başka bir sekme oluştur. + Bir tell teslim edilemedi. @@ -1186,6 +1189,15 @@ Bir tell geldiğinde nerede açılacağı. + + Ana pencere + + + Ayrılan pencere + + + Yazarken pencere açılmaz. Sekme ana pencerede bekler, böylece klavye oyuna geri dönmez. + Her tell'de sekmeye geç diff --git a/HellionChat/Resources/HellionStrings.uk.resx b/HellionChat/Resources/HellionStrings.uk.resx index 6dd5cc3..c82e582 100644 --- a/HellionChat/Resources/HellionStrings.uk.resx +++ b/HellionChat/Resources/HellionStrings.uk.resx @@ -480,7 +480,7 @@ Ширина бокової панелі вкладок у пікселях. Стандартне значення (44 пкс) — лише іконки; розширте, щоб заголовки секцій на кшталт «Активні tells (3)» не обрізались. - Досягнуто максимум {0} закріплених tell-вкладок. Спочатку відкріпіть одну або скористайтесь «Перетворити на постійну». + Досягнуто максимум {0} закріплених tell-вкладок. Спочатку відкріпіть одну. Закріплено — виживає після релогу. @@ -514,12 +514,6 @@ Додає кнопку поруч із кожною авто-tell-вкладкою для позначення партнера розмови як вже привітаного — назва вкладки стає приглушеною. Зручно для грітерів клубу, які паралельно ведуть багато розмов. За замовчуванням вимкнено. - - Відкривати нові /tell-вкладки відразу як спливаючі вікна - - - Коли активно, кожна нова /tell-вкладка негайно відкривається як власне вікно. Закриття вікна повертає вкладку на бокову панель. - Кількість попередньо завантажених tells можна налаштувати на вкладці «Конфіденційність». @@ -1031,6 +1025,15 @@ Звук Hellion + + Закрити вкладку + + + Спочатку відкріпіть вкладку, потім її можна закрити. + + + Має залишитися хоча б одна вкладка. Спочатку створіть іншу. + Повідомлення не було доставлено. @@ -1186,6 +1189,15 @@ Де відкривається tell, коли надходить. + + Головне вікно + + + Окреме вікно + + + Поки ви друкуєте, вікно не відкривається. Вкладка чекає в головному вікні, щоб клавіатура не повернулася до гри. + Перемикатися на вкладку за кожного tell diff --git a/HellionChat/Resources/HellionStrings.zh-Hans.resx b/HellionChat/Resources/HellionStrings.zh-Hans.resx index 4afddc2..f560f87 100644 --- a/HellionChat/Resources/HellionStrings.zh-Hans.resx +++ b/HellionChat/Resources/HellionStrings.zh-Hans.resx @@ -480,7 +480,7 @@ 标签页侧边栏的宽度(像素)。默认值(44 px)仅显示图标;加宽后可完整显示"活跃密语 (3)"等分区标题而不被截断。 - 已达到最多 {0} 个固定密语标签页的上限。请先取消固定一个,或使用"提升为永久标签页"。 + 已达到最多 {0} 个固定密语标签页的上限。请先取消固定一个。 已固定,重新登录后仍保留。 @@ -514,12 +514,6 @@ 在每个自动密语标签页旁添加一个点击按钮,用于将对话伙伴标记为已打招呼,标记后标签页名称将变暗。适合同时处理多个对话的俱乐部接待员使用。默认关闭。 - - 将新 /tell 标签页直接作为弹出窗口打开 - - - 启用后,每个新建的 /tell 标签页将立即作为独立窗口打开。关闭窗口后标签页将返回侧边栏。 - 预加载密语数量可在隐私标签页中设置。 @@ -1032,6 +1026,15 @@ Hellion 音效 + + 关闭标签页 + + + 关闭前请先取消固定此标签页。 + + + 至少需要保留一个标签页。请先创建另一个。 + 悄悄话未能送达。 @@ -1187,6 +1190,15 @@ 密语到达时在何处打开。 + + 主窗口 + + + 弹出窗口 + + + 输入时不会弹出窗口。标签页会在主窗口等待,以免键盘跳回游戏。 + 每条密语都切换到标签 diff --git a/HellionChat/Resources/HellionStrings.zh-Hant.resx b/HellionChat/Resources/HellionStrings.zh-Hant.resx index 9996321..b43ac49 100644 --- a/HellionChat/Resources/HellionStrings.zh-Hant.resx +++ b/HellionChat/Resources/HellionStrings.zh-Hant.resx @@ -480,7 +480,7 @@ 標籤頁側邊欄的寬度(像素)。預設值(44 px)為僅顯示圖示;加寬可讓「進行中的悄悄話 (3)」等區段標題不被截斷。 - 已達到最多 {0} 個釘選悄悄話標籤頁的上限。請先取消釘選一個,或使用升格為永久標籤頁。 + 已達到最多 {0} 個釘選悄悄話標籤頁的上限。請先取消釘選一個。 已釘選,重新登入後仍會保留。 @@ -514,12 +514,6 @@ 在每個 Auto-悄悄話 標籤頁旁新增一個點擊按鈕,用於將對話對象標記為已問候,標籤頁名稱將隨之變暗。適合同時管理多個對話的接待人員使用。預設為關閉。 - - 直接以彈出視窗開啟新的 /tell 標籤頁 - - - 啟用後,每個新建立的 /tell 標籤頁會立即以獨立視窗開啟。關閉視窗後,標籤頁會回到側邊欄。 - 預載悄悄話的數量可在隱私標籤頁中設定。 @@ -1032,6 +1026,15 @@ Hellion 音效 + + 關閉標籤頁 + + + 關閉前請先取消釘選此標籤頁。 + + + 至少需要保留一個標籤頁。請先建立另一個。 + 悄悄話未能送達。 @@ -1187,6 +1190,15 @@ 悄悄話到達時在何處開啟。 + + 主視窗 + + + 彈出視窗 + + + 輸入時不會彈出視窗。分頁會在主視窗等待,以免鍵盤跳回遊戲。 + 每則悄悄話都切換到分頁 diff --git a/HellionChat/SelfTests/ConfigMigrationV27Step.cs b/HellionChat/SelfTests/ConfigMigrationV27Step.cs index 70de61d..d032140 100644 --- a/HellionChat/SelfTests/ConfigMigrationV27Step.cs +++ b/HellionChat/SelfTests/ConfigMigrationV27Step.cs @@ -14,13 +14,18 @@ internal sealed class ConfigMigrationV27Step : ISelfTestStep _ = plugin; } - public string Name => "Hellion Chat - Config v27 migration"; + public string Name => "Hellion Chat - Config migration"; public SelfTestStepResult RunStep() { - if (Plugin.Config.Version != 27) + // Against LatestVersion, not a literal: this read 27 and went red the + // moment the schema moved to 28, reporting a migration failure where the + // only thing that had happened was a new migration step being added. + if (Plugin.Config.Version != Configuration.LatestVersion) { - ImGui.Text($"Config.Version is {Plugin.Config.Version}, expected 27"); + ImGui.Text( + $"Config.Version is {Plugin.Config.Version}, expected {Configuration.LatestVersion}" + ); return SelfTestStepResult.Fail; } diff --git a/HellionChat/Services/TellRouterService.cs b/HellionChat/Services/TellRouterService.cs index 264b81b..0479165 100644 --- a/HellionChat/Services/TellRouterService.cs +++ b/HellionChat/Services/TellRouterService.cs @@ -4,11 +4,14 @@ using Microsoft.Extensions.Logging; namespace HellionChat.Services; -// Routes an incoming tell to the configured TellAutoOpenMode (Off/Sidebar/ -// TopTab/Popout). Deliberately decoupled from AutoTellTabsService: -// that service owns tab CREATION + lifecycle; this only REVEALS/pops the tab it -// finds. Popout guards on pool.IsOpen so it never double-pops a tab the -// AutoTellTabsOpenAsPopout path already opened. Subscribes to the resolved +// Routes an incoming tell to the configured TellAutoOpenMode (Off/main window/ +// Popout). Deliberately decoupled from AutoTellTabsService: that service owns +// tab CREATION + lifecycle; this is the SINGLE place that decides where a tell +// becomes visible. Until v2.0.5 that service opened pop-outs too, off its own +// AutoTellTabsOpenAsPopout flag, and won every race by firing at tab creation -- +// so this mode looked inert and the mid-sentence guard here could be walked +// past. Popout still guards on pool.IsOpen for the already-open case. +// Subscribes to the resolved // MessageManager.MessageProcessed stream (a resolved Message), not the raw // IChatGui event, and defers the reveal one tick so the tab exists regardless of // subscriber order. Wired by TellRouterServiceInitHostedService. @@ -84,24 +87,17 @@ internal sealed class TellRouterService : IDisposable var reveal = TabLifecycleHelpers.PlanTellReveal( mode, Plugin.Config.TellAutoOpenSwitchAlways, - Plugin.Instance.ChannelPopoutPool.IsOpen(tab.Identifier) + Plugin.Instance.ChannelPopoutPool.IsOpen(tab.Identifier), + Util.ChatInputBusy.Any() ); switch (reveal) { case TabLifecycleHelpers.TellReveal.MainWindow: - // The mode also picks the layout, so Sidebar vs TopTab are - // actually distinct outcomes, not the same ActivateTab. - var wantLayout = - mode == TellAutoOpenMode.TopTab - ? MainWindowLayoutMode.TopTabs - : MainWindowLayoutMode.Sidebar; - if (Plugin.Config.MainWindowLayoutMode != wantLayout) - { - Plugin.Config.MainWindowLayoutMode = wantLayout; - Plugin.Instance.SaveConfig(); - } - + // Deliberately does NOT touch MainWindowLayoutMode. It used to + // force the layout to match this mode and SAVE it, so a single + // incoming tell permanently undid the tab placement the user + // had chosen under Window -> Layout (Flo, 23.08.2026). Plugin.Instance.MainWindow?.ActivateTab(tab); break; diff --git a/HellionChat/Ui/Components/Settings/SettingsWidgets.cs b/HellionChat/Ui/Components/Settings/SettingsWidgets.cs index bb15801..66d8d90 100644 --- a/HellionChat/Ui/Components/Settings/SettingsWidgets.cs +++ b/HellionChat/Ui/Components/Settings/SettingsWidgets.cs @@ -203,6 +203,10 @@ internal sealed class SettingsWidgets // One setting, n choices. The control fills the whole control column rather // than right-aligning, because segments need the room to stay readable. + // Breathing room left and right of a segment's label. Chosen against the + // control's own Inset/Chamfer so the chamfered corner never bites into text. + private const float SegmentLabelPadX = 12f; + internal void SegmentRow( uint id, string label, @@ -224,6 +228,29 @@ internal sealed class SettingsWidgets var picked = selected; var colors = _segmented; + // Measured, not the 200px default: the control splits its width evenly + // across the segments, so the longest label in the CURRENT language sets + // what fits. Three short English words fit the default; two long German + // ones did not, and came out as "op-Out-Fenste". Which language is the + // longest is not knowable up front -- 25 of them ship. + // + // Only a WISH: SettingRowSplit hands the label column its MinLabelWidth + // first, so an extreme translation cannot push the label off the row. + var widest = 0f; + foreach (var text in labels) + widest = MathF.Max(widest, ImGui.CalcTextSize(text).X); + + // CalcTextSize is already scaled, PreferredControlWidth is scaled again + // downstream -- back to design pixels before handing it over. + var scale = StyleEngine.Metrics.Scale; + var style = new SettingRowStyle + { + PreferredControlWidth = MathF.Max( + 200f, + (widest / scale + SegmentLabelPadX * 2f) * labels.Length + ), + }; + SettingRow.Draw( id, label, @@ -233,7 +260,8 @@ internal sealed class SettingsWidgets { ImGui.SetCursorScreenPos(new Vector2(ctx.ControlOrigin.X, ctx.ControlOrigin.Y)); picked = SegmentedControl.Draw(id, ctx.ControlWidth, labels, selected, colors); - } + }, + styleOverride: style ); if (picked == selected) diff --git a/HellionChat/Ui/Components/Settings/Tabs/ChannelsTab.cs b/HellionChat/Ui/Components/Settings/Tabs/ChannelsTab.cs index 8b1965f..9465da4 100644 --- a/HellionChat/Ui/Components/Settings/Tabs/ChannelsTab.cs +++ b/HellionChat/Ui/Components/Settings/Tabs/ChannelsTab.cs @@ -1,6 +1,8 @@ using Dalamud.Bindings.ImGui; +using Dalamud.Interface.Utility.Raii; using HellionChat.Resources; using HellionChat.Ui.StyleEngine; +using HellionChat.Ui.StyleEngine.Widgets; namespace HellionChat.Ui.Components.Settings.Tabs; @@ -8,11 +10,31 @@ internal sealed class ChannelsTab { private readonly SettingsWidgets _w; private readonly TabEditor _editor; + private readonly WidgetPalette _palette; + + // Three offered values for a four-value enum. TopTab stays in the enum so no + // saved config needs migrating, but it no longer differs from Sidebar: the + // difference used to be that the router rewrote the window layout as a side + // effect, which is exactly what got removed. Both read back as Main window. + private static readonly TellAutoOpenMode[] TellOpenValues = + [ + TellAutoOpenMode.Sidebar, + TellAutoOpenMode.Popout, + ]; + + // Built per call, not cached: a runtime language switch has to reach these + // (same reason as WindowTab.LayoutLabels). + private static string[] TellOpenLabels => + [ + HellionStrings.Settings_Channels_TellAutoOpen_MainWindow, + HellionStrings.Settings_Channels_TellAutoOpen_Popout, + ]; public ChannelsTab(Plugin plugin, TokenResolver resolver) { _w = new SettingsWidgets(plugin, new SettingsPalette(resolver)); _editor = new TabEditor(plugin); + _palette = new WidgetPalette(resolver); } public void Draw() @@ -68,13 +90,6 @@ internal sealed class ChannelsTab () => Plugin.Config.AutoTellTabsShowGreetedToggle, v => Plugin.Config.AutoTellTabsShowGreetedToggle = v ); - _w.ToggleRow( - ImGui.GetID("channels.autotell.popout"u8), - HellionStrings.ChatLog_AutoTellTabs_OpenAsPopout_Name, - HellionStrings.ChatLog_AutoTellTabs_OpenAsPopout_Description, - () => Plugin.Config.AutoTellTabsOpenAsPopout, - v => Plugin.Config.AutoTellTabsOpenAsPopout = v - ); // Written for this screen and never shown until this cycle. It names // the one setting in a third-party plugin that silently stops @@ -91,14 +106,45 @@ internal sealed class ChannelsTab ) ) { - _w.EnumComboRow( + _w.SegmentRow( ImGui.GetID("channels.autoopen.mode"u8), HellionStrings.Settings_Channels_TellAutoOpenMode_Name, HellionStrings.Settings_Channels_TellAutoOpenMode_Description, - () => Plugin.Config.TellAutoOpenMode, - v => Plugin.Config.TellAutoOpenMode = v, - v => v.Name() + TellOpenValues, + TellOpenLabels, + // TopTab and Off are not in the offered set, and SegmentRow falls + // back to index 0 on an unknown value. The v28 migration converts + // both, so this only catches a config that skipped it. + () => + Plugin.Config.TellAutoOpenMode == TellAutoOpenMode.Popout + ? TellAutoOpenMode.Popout + : TellAutoOpenMode.Sidebar, + v => Plugin.Config.TellAutoOpenMode = v ); + + // Only under Pop-out, because it is the only setting the guard can + // visibly contradict: you picked "own window" and sometimes get a tab + // instead. Said in the theme's danger colour rather than a raw red so + // it stays legible on every palette. + if (Plugin.Config.TellAutoOpenMode == TellAutoOpenMode.Popout) + { + ImGui.Spacing(); + // Scoped block, not `using var`: that pops at the END OF THE + // METHOD, and every section drawn below this one would come out + // in the danger colour. + using ( + ImRaii.PushColor( + ImGuiCol.Text, + _palette.Abgr( + Token.StatusDanger, + Plugin.Instance.ThemeRegistry.Active.Colors + ) + ) + ) + { + ImGui.TextWrapped(HellionStrings.Settings_Channels_TellAutoOpen_BusyHint); + } + } _w.ToggleRow( ImGui.GetID("channels.autoopen.switch"u8), HellionStrings.Settings_Channels_TellSwitchAlways_Name, diff --git a/HellionChat/Ui/Components/Sidebar.cs b/HellionChat/Ui/Components/Sidebar.cs index bd5be26..d22b2c6 100644 --- a/HellionChat/Ui/Components/Sidebar.cs +++ b/HellionChat/Ui/Components/Sidebar.cs @@ -190,7 +190,17 @@ internal sealed class Sidebar unpinnedHeaderRendered = true; } - DrawRow(tab, expanded, accentRgba, textAbgr, mutedAbgr, dimAbgr, dl, ref activeTab); + DrawRow( + tab, + tabs, + expanded, + accentRgba, + textAbgr, + mutedAbgr, + dimAbgr, + dl, + ref activeTab + ); } } @@ -215,6 +225,7 @@ internal sealed class Sidebar private void DrawRow( Tab tab, + IReadOnlyList tabs, bool expanded, uint accentRgba, uint textAbgr, @@ -454,7 +465,7 @@ internal sealed class Sidebar LastRenderedUnreadDotCount++; } - TabContextMenu.Draw(tab, "ctx", _pool); + TabContextMenu.Draw(tab, "ctx", _pool, tabs); if (hasPopOut) { diff --git a/HellionChat/Ui/Components/TabContextMenu.cs b/HellionChat/Ui/Components/TabContextMenu.cs index 5cece2c..d601d45 100644 --- a/HellionChat/Ui/Components/TabContextMenu.cs +++ b/HellionChat/Ui/Components/TabContextMenu.cs @@ -24,7 +24,12 @@ internal static class TabContextMenu // popup; the open trigger is a right-click on the LAST submitted item // (g.LastItemData via IsItemHovered) — any interactive item in between // would steal the trigger. Only DrawList ops may sit between. - public static void Draw(Tab tab, string popupId, Windows.ChannelPopoutPool pool) + public static void Draw( + Tab tab, + string popupId, + Windows.ChannelPopoutPool pool, + IReadOnlyList tabs + ) { if (!ImGui.BeginPopupContextItem(popupId)) { @@ -54,13 +59,13 @@ internal static class TabContextMenu ) ) { - DrawBody(tab, pool); + DrawBody(tab, pool, tabs); } ImGui.EndPopup(); } - private static void DrawBody(Tab tab, Windows.ChannelPopoutPool pool) + private static void DrawBody(Tab tab, Windows.ChannelPopoutPool pool, IReadOnlyList tabs) { // Rename: focus the field the first frame the popup appears. if (ImGui.IsWindowAppearing()) @@ -95,10 +100,14 @@ internal static class TabContextMenu if (tab.EnableNotificationSound) DrawSoundPicker(tab); - if (ImGui.MenuItem("Pop Out")) + if (ImGui.MenuItem(Language.ChatLog_Tabs_PopOut)) pool.TryOpen(tab); + // One separator for the whole lifecycle block below, so a normal tab + // (no pin controls) still gets the rule above its close entry. + ImGui.Separator(); DrawPinControls(tab); + DrawCloseControl(tab, tabs, pool); } // Pinning has been complete since v1.4.7 -- pools, cap, persistence, logout @@ -123,8 +132,6 @@ internal static class TabContextMenu if (service is null) return; - ImGui.Separator(); - if (tab.IsPinned) { if (ImGui.MenuItem(HellionStrings.PinTab_MenuUnpin)) @@ -156,6 +163,70 @@ internal static class TabContextMenu ); } + // Closing a tab was lost in the v2.0.0 window rebuild: the trash entry lived + // in ChatLogWindow's menu, which cf4705e retired, and the rebuilt menu only + // restored rename, sound, pop-out and pinning. + // + // For tell tabs that left no way out at all. TabLifecycleHelpers.IsEditable + // keeps temp tabs out of the settings editor on purpose and says the context + // menu is where their gestures live -- so the editor was pointing at a menu + // that could not close them either. + // + // Blocked states stay visible and disabled rather than absent: both are + // states the user can undo, and the tooltip is what says how. + private static void DrawCloseControl( + Tab tab, + IReadOnlyList tabs, + Windows.ChannelPopoutPool pool + ) + { + var closeability = TabLifecycleHelpers.GetCloseability(tab, tabs); + var allowed = closeability == TabLifecycleHelpers.TabCloseability.Allowed; + + // A tell tab is closed, a layout tab is deleted. Same gesture, different + // promise: the conversation goes on without its tab, the layout entry does not. + var label = tab.IsTempTab + ? HellionStrings.Tabs_Close_MenuItem + : Language.ChatLog_Tabs_Delete; + + if (ImGui.MenuItem(label, enabled: allowed) && allowed) + { + CloseTab(tab, pool); + ImGui.CloseCurrentPopup(); + return; + } + + if (allowed || !ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + return; + + ImGuiUtil.Tooltip( + closeability == TabLifecycleHelpers.TabCloseability.BlockedByPin + ? HellionStrings.Tabs_Close_UnpinFirst + : HellionStrings.Tabs_Close_LastTab + ); + } + + // Removal order mirrors TabEditor.Delete, which already does this from a draw + // frame: drop the tab, release the pool slot bound to its identifier, then + // re-anchor the main window if this was the active tab. Safe here because the + // strip iterates a frame snapshot, not the live list, and TryClose only + // releases a fixed slot instead of mutating a window collection. + private static void CloseTab(Tab tab, Windows.ChannelPopoutPool pool) + { + // Draw will never run for this tab again, so a pending rename can no + // longer flush -- and leaving the guard armed would make the NEXT tab's + // Draw see a stale owner. Drop it before the tab goes. + if (_renamingTab == tab.Identifier) + ClearPendingRename(); + + lock (Plugin.Instance.TabsListLock) + Plugin.Config.Tabs.RemoveAll(t => t.Identifier == tab.Identifier); + + pool.TryClose(tab.Identifier); + Plugin.Instance.MainWindow?.ResetActiveTabIfRemoved(tab); + Plugin.Instance.SaveConfig(); + } + // The flush depends on Draw running once more for this tab. If it never does — // LRU eviction, logout, window closed or collapsed, plugin unload, game exit — // the name only lives in memory until some other SaveConfig happens to run. diff --git a/HellionChat/Ui/Components/TopTabBar.cs b/HellionChat/Ui/Components/TopTabBar.cs index 8b8f637..adc54b6 100644 --- a/HellionChat/Ui/Components/TopTabBar.cs +++ b/HellionChat/Ui/Components/TopTabBar.cs @@ -138,7 +138,7 @@ internal sealed class TopTabBar TabBadge ); - TabContextMenu.Draw(tab, $"toptab_ctx_{tab.Identifier}", _pool); + TabContextMenu.Draw(tab, $"toptab_ctx_{tab.Identifier}", _pool, tabs); } LineDivider.Draw(null, borderAbgr, mutedAbgr); diff --git a/HellionChat/Ui/StyleEngine/Metrics.cs b/HellionChat/Ui/StyleEngine/Metrics.cs index e9afd83..88c596d 100644 --- a/HellionChat/Ui/StyleEngine/Metrics.cs +++ b/HellionChat/Ui/StyleEngine/Metrics.cs @@ -64,9 +64,12 @@ internal static class Metrics if (frame == _cachedFrame) return _cachedScale; - // Safe variant: GlobalScale throws while the interface manager is - // still coming up, and Metrics reaches more call sites than it did. - _cachedScale = ImGuiHelpers.GlobalScaleSafe; + // GlobalScale is what the retired GlobalScaleSafe alias forwarded to; + // it falls back to the Dalamud config scale when ImGui is not up yet. + // That does not make this property pre-init safe on its own -- the + // GetFrameCount above would fault first -- but every Metrics caller + // sits under Ui/ and only runs while drawing. + _cachedScale = ImGuiHelpers.GlobalScale; _cachedFrame = frame; return _cachedScale; } diff --git a/HellionChat/Ui/Windows/ChannelPopoutWindow.cs b/HellionChat/Ui/Windows/ChannelPopoutWindow.cs index a930d9a..df943a5 100644 --- a/HellionChat/Ui/Windows/ChannelPopoutWindow.cs +++ b/HellionChat/Ui/Windows/ChannelPopoutWindow.cs @@ -106,6 +106,9 @@ internal sealed class ChannelPopoutWindow : Window, IFocusableChatWindow // the keybind tail checks when deciding whether to route at this surface. public bool HasFocusedInput => _input.IsFocused; + // See IFocusableChatWindow.IsInputBusy. + public bool IsInputBusy => _input.IsFocused || _input.PendingLength > 0; + // Arm-and-hold the one-frame Activate flag; the pop-out's Draw applies the // ImGui focus next frame. Framework-thread safe (field write only). public void RequestInputFocus() diff --git a/HellionChat/Ui/Windows/IFocusableChatWindow.cs b/HellionChat/Ui/Windows/IFocusableChatWindow.cs index 04bf634..b9af82b 100644 --- a/HellionChat/Ui/Windows/IFocusableChatWindow.cs +++ b/HellionChat/Ui/Windows/IFocusableChatWindow.cs @@ -10,5 +10,12 @@ internal interface IFocusableChatWindow { bool HasFocusedInput { get; } + // "The user is mid-sentence here." Focus alone is not enough: a typed line + // survives a click elsewhere, and stealing the tab out from under it is how + // half a message ends up addressed to whoever just said hello -- the input + // buffer belongs to the WINDOW, but the send target comes from whichever tab + // is active at Enter. + bool IsInputBusy { get; } + void RequestInputFocus(); } diff --git a/HellionChat/Ui/Windows/MainWindow.cs b/HellionChat/Ui/Windows/MainWindow.cs index 1afff71..8710d2d 100644 --- a/HellionChat/Ui/Windows/MainWindow.cs +++ b/HellionChat/Ui/Windows/MainWindow.cs @@ -149,9 +149,19 @@ internal sealed class MainWindow : Window, IFocusableChatWindow // Framework thread, not the draw frame: needs the current truth, so it takes // its own lock instead of using the frame snapshot. + // + // Through PickMainActiveTab rather than Tabs[0]: Tabs[0] may be popped out, + // and anchoring on it ran OnTabActivated over a tab that is live in its own + // window -- stripping the tell binding of a conversation the user is in the + // middle of. The next frame's PickMainActiveTab then re-anchored anyway, so + // the strip bought nothing. Passing null asks for the first VISIBLE tab. Tab? next; lock (Plugin.Instance.TabsListLock) - next = Plugin.Config.Tabs.Count > 0 ? Plugin.Config.Tabs[0] : null; + next = TabLifecycleHelpers.PickMainActiveTab( + null, + Plugin.Config.Tabs, + t => _pool.IsOpen(t.Identifier) + ); _activeTab = next; if (next is not null) TabLifecycleHelpers.OnTabActivated(next, removed); @@ -243,6 +253,9 @@ internal sealed class MainWindow : Window, IFocusableChatWindow // input focus before routing a channel-set/REPLY/prefill at it. public bool HasFocusedInput => _input.IsFocused; + // See IFocusableChatWindow.IsInputBusy. + public bool IsInputBusy => _input.IsFocused || _input.PendingLength > 0; + // Arm-and-hold: field writes only, safe from the framework thread; the draw // path applies the actual ImGui focus next frame (same path as ActivateChat). public void RequestInputFocus() @@ -290,21 +303,41 @@ internal sealed class MainWindow : Window, IFocusableChatWindow lock (Plugin.Instance.TabsListLock) tabs = Plugin.Config.Tabs.ToList(); - // First-frame seed: the active tab defaults to the first persisted - // tab so the message list isn't empty on a clean session. + // First-frame seed: the active tab defaults to the first VISIBLE tab so + // the message list isn't empty on a clean session. + // + // Through PickMainActiveTab, not tabs[0]. With every tab popped out this + // seeded tabs[0] and the re-anchor below immediately set it back to null, + // every single frame -- 60 pointless OnTabActivated runs a second, each + // one stripping tell state off a tab that is live in its own window. Null + // here means "nothing to show", which is the state the re-anchor settles + // on anyway, so the oscillation just stops. if (_activeTab is null && tabs.Count > 0) { - var seeded = tabs[0]; - _activeTab = seeded; - // The seeded Tabs[0] is the likeliest legacy stale-tell carrier - // (pre-coupling the detour wrote here); strip it like any activation. - TabLifecycleHelpers.OnTabActivated(seeded, null); + var seeded = TabLifecycleHelpers.PickMainActiveTab( + null, + tabs, + t => _pool.IsOpen(t.Identifier) + ); + if (seeded is not null) + { + _activeTab = seeded; + // The seeded tab is the likeliest legacy stale-tell carrier + // (pre-coupling the detour wrote here); strip it like any activation. + TabLifecycleHelpers.OnTabActivated(seeded, null); + } } else if (_activeTab is { } active && !tabs.Contains(active)) { // Active tab is no longer in the list (e.g. a wholesale config import - // the service repair paths never see). Re-seed on the Draw thread. - var reseed = tabs.Count > 0 ? tabs[0] : null; + // the service repair paths never see). Re-seed on the Draw thread -- + // same visible-tab rule as the seed above, so this cannot hand the + // window a popped-out tab and strip its tell state on the way. + var reseed = TabLifecycleHelpers.PickMainActiveTab( + null, + tabs, + t => _pool.IsOpen(t.Identifier) + ); _activeTab = reseed; if (reseed is not null) TabLifecycleHelpers.OnTabActivated(reseed, active); diff --git a/HellionChat/Util/ChatInputBusy.cs b/HellionChat/Util/ChatInputBusy.cs new file mode 100644 index 0000000..d62d722 --- /dev/null +++ b/HellionChat/Util/ChatInputBusy.cs @@ -0,0 +1,25 @@ +namespace HellionChat.Util; + +// "The user is mid-sentence somewhere." Asked before a tell is revealed, in any +// mode. Its own file because the answer has to be identical everywhere it is +// asked: this started as a guard inside the reveal plan, and a SECOND pop-out +// path walked straight past it -- the window opened anyway, took the keyboard +// with it, and in this game the next typed sentence walks the character around. +// The paths are one now, but a shared answer is what keeps it that way. +internal static class ChatInputBusy +{ + // Reads two fields the draw path wrote; no ImGui call, so it is safe from the + // framework thread. Call it THERE, not from the message worker: the worker + // runs ahead of the frame and would sample a stale answer. + internal static bool Any() + { + if (Plugin.Instance.MainWindow is { IsInputBusy: true }) + return true; + + foreach (var window in Plugin.Instance.ChannelPopoutPool.Instances) + if (window is { IsOpen: true, IsInputBusy: true }) + return true; + + return false; + } +} diff --git a/HellionChat/Util/TabLifecycleHelpers.cs b/HellionChat/Util/TabLifecycleHelpers.cs index a5a8b26..6173c4c 100644 --- a/HellionChat/Util/TabLifecycleHelpers.cs +++ b/HellionChat/Util/TabLifecycleHelpers.cs @@ -68,6 +68,51 @@ internal static class TabLifecycleHelpers return editable > 1; } + // Why a tab may or may not be closed from the tab strip's context menu. + // The menu needs the REASON, not just a bool: a blocked close stays visible + // and disabled with the matching tooltip, because both blocks are states the + // user can undo (unpin the tab / create a second one). + internal enum TabCloseability + { + Allowed, + + // Temp tab is pinned. Pinning is the "keep this" gesture, so closing + // takes the deliberate two-step (Flo, 2026-08-22) rather than undoing + // it silently on one misclick. + BlockedByPin, + + // Last editable tab. Same reason CanDelete guards the editor: the window + // would be left with nothing to draw and the message list has no empty + // state. + BlockedLastTab, + } + + // Closing a tab from the context menu, for both tab kinds. + // + // Temp tabs are deliberately NOT routed through CanDelete: IsEditable + // excludes them, so CanDelete would answer "no" for every tell tab and the + // conversation would have no way out at all. That was the actual v2.0.0 + // regression -- the editor sends temp tabs to the context menu, and the + // context menu had no close. + // + // Pure + Dalamud-free. + // TEST-MIRROR: ../../../Hellion Build test/_Helpers/TabCloseabilityTests.cs + internal static TabCloseability GetCloseability(Tab tab, IReadOnlyList tabs) + { + if (tab.IsTempTab) + return tab.IsPinned ? TabCloseability.BlockedByPin : TabCloseability.Allowed; + + // Counted over the whole list rather than via an index: the caller draws + // from a frame snapshot in render order, so a positional index would not + // survive the sectioning. + var editable = 0; + foreach (var t in tabs) + if (IsEditable(t)) + editable++; + + return editable > 1 ? TabCloseability.Allowed : TabCloseability.BlockedLastTab; + } + public static bool ShouldStripOnLoad(Tab t) => IsInUnpinnedPool(t); public static bool ShouldStripOnSave(Tab t) => IsInUnpinnedPool(t); @@ -217,9 +262,10 @@ internal static class TabLifecycleHelpers // Returns the tab the main window should display — the current tab if it // is not popped out, else the FIRST non-popped tab in list order, else null when - // every tab is popped. NOTE: deliberately NOT ResetActiveTabIfRemoved's - // unconditional Tabs[0] — Tabs[0] may itself be popped and would re-trigger the - // re-anchor every frame. Pure + Dalamud-free. + // every tab is popped. Skipping popped tabs is the whole point: an unconditional + // Tabs[0] would re-trigger the re-anchor every frame when Tabs[0] is itself + // popped. ResetActiveTabIfRemoved passes null to reach the same rule from its + // own entry point. Pure + Dalamud-free. // TEST-MIRROR: ../../../Hellion Build test/_Helpers/PickMainActiveTabTests.cs internal static Tab? PickMainActiveTab( Tab? current, @@ -243,7 +289,17 @@ internal static class TabLifecycleHelpers Popout, } - // The alreadyPopped case is the whole reason this is a function. + // Two reasons this is a function, and inputBusy is the second one. + // + // A tell arriving while the user is mid-sentence used to yank the active tab + // away (tester report, Carla, 23.08.2026). The interruption is the visible + // half; the sharp half is that InputBar's buffer belongs to the WINDOW while + // the send target is read off whatever tab is active at Enter -- so a line + // typed at one person could leave addressed to the one who just wrote. When + // an input is busy, nothing is revealed at all: the tab still appears and + // still carries its unread mark, the user just walks over on their own. + // + // The alreadyPopped case is the older reason. // // Revealing a popped-out tab in the main window looks like it does nothing, // and then does something worse: PickMainActiveTab re-anchors on the next @@ -259,14 +315,20 @@ internal static class TabLifecycleHelpers internal static TellReveal PlanTellReveal( TellAutoOpenMode mode, bool switchAlways, - bool alreadyPopped + bool alreadyPopped, + bool inputBusy ) => - mode switch - { - TellAutoOpenMode.Off => TellReveal.None, - TellAutoOpenMode.Popout => alreadyPopped ? TellReveal.None : TellReveal.Popout, - _ => switchAlways && !alreadyPopped ? TellReveal.MainWindow : TellReveal.None, - }; + inputBusy + ? TellReveal.None + : mode switch + { + TellAutoOpenMode.Off => TellReveal.None, + TellAutoOpenMode.Popout => alreadyPopped ? TellReveal.None : TellReveal.Popout, + // Sidebar and TopTab both land here: they used to pick the window + // LAYOUT as a side effect, which overwrote a setting the user made + // somewhere else entirely. Reveal is reveal; layout is the user's. + _ => switchAlways && !alreadyPopped ? TellReveal.MainWindow : TellReveal.None, + }; // Popout-aware sibling of WrapTabIndex for the ChatTabForward/Backward // keybind. Steps from current by delta's sign (±1), wrapping, and returns the