From 65736df464fef282fb7fd2e11c1aace5bb5c5226 Mon Sep 17 00:00:00 2001 From: Talon Date: Wed, 24 Jun 2026 12:30:48 +0200 Subject: [PATCH] fix(windows): polish hotkeys, user list, titlebar, and PM window - Suppress the system ding on global hotkeys/PTT and the user-list Enter key by setting SuppressKeyPress (Handled alone leaves WM_CHAR to beep). - Preserve the user-list keyboard selection across talking/mute refreshes instead of resetting it on every Items.Clear(). - Include the connected server name in the main window titlebar. - Close the private-message window on Escape. - Show the PM window without an owner so focus is no longer trapped to it and the main window can be worked in while a PM is open. Co-Authored-By: Claude Opus 4.8 --- .../VoiceCat.App/Forms/ConnectDialog.cs | 5 +++ .../windows/VoiceCat.App/Forms/MainForm.cs | 43 ++++++++++++++----- .../VoiceCat.App/Forms/PrivateMessageForm.cs | 5 +++ clients/windows/VoiceCat.App/Program.cs | 2 +- 4 files changed, 43 insertions(+), 12 deletions(-) diff --git a/clients/windows/VoiceCat.App/Forms/ConnectDialog.cs b/clients/windows/VoiceCat.App/Forms/ConnectDialog.cs index 63a0642..395251f 100644 --- a/clients/windows/VoiceCat.App/Forms/ConnectDialog.cs +++ b/clients/windows/VoiceCat.App/Forms/ConnectDialog.cs @@ -19,6 +19,7 @@ public partial class ConnectDialog : Form public VoiceCatClient? ConnectedClient { get; private set; } public uint SelfUserId { get; private set; } public string Nickname { get; private set; } = ""; + public string ServerName { get; private set; } = ""; public ConnectDialog() { @@ -115,6 +116,10 @@ public partial class ConnectDialog : Form SetBusy(true); lblStatus.Text = "Connecting..."; + ServerName = string.IsNullOrWhiteSpace(server.DisplayName) + ? $"{server.Host}:{server.Port}" + : server.DisplayName; + string tofuDir = Path.GetDirectoryName(ServerListStore.TofuStorePath)!; Console.WriteLine($"[ConnectDialog] tofu store dir: {tofuDir}"); Directory.CreateDirectory(tofuDir); diff --git a/clients/windows/VoiceCat.App/Forms/MainForm.cs b/clients/windows/VoiceCat.App/Forms/MainForm.cs index 19d7f0c..8c69cd9 100644 --- a/clients/windows/VoiceCat.App/Forms/MainForm.cs +++ b/clients/windows/VoiceCat.App/Forms/MainForm.cs @@ -42,14 +42,16 @@ public partial class MainForm : Form private ToolStripMenuItem _miJoinVoice = null!; private ToolStripMenuItem _miScreenShare = null!; - public MainForm(VoiceCatClient client, uint selfUserId, string nickname) + public MainForm(VoiceCatClient client, uint selfUserId, string nickname, string serverName) { InitializeComponent(); _client = client; _selfUserId = selfUserId; _nickname = nickname; - Text = $"VoiceCat — {nickname}"; + Text = string.IsNullOrWhiteSpace(serverName) + ? $"VoiceCat — {nickname}" + : $"VoiceCat — {nickname} @ {serverName}"; _client.EventReceived += OnEvent; _client.LevelChanged += OnLevelChanged; @@ -69,7 +71,7 @@ public partial class MainForm : Form tvChannels.DoubleClick += TvChannels_DoubleClick; tvChannels.KeyDown += TvChannels_KeyDown; lstUsers.DoubleClick += (_, _) => OpenUserTuning(); - lstUsers.KeyDown += (_, e) => { if (e.KeyCode == Keys.Enter) OpenUserTuning(); }; + lstUsers.KeyDown += (_, e) => { if (e.KeyCode == Keys.Enter) { OpenUserTuning(); e.Handled = e.SuppressKeyPress = true; } }; // Compose txtCompose.KeyDown += TxtCompose_KeyDown; @@ -599,6 +601,11 @@ public partial class MainForm : Form private void RefreshUserList() { + // Preserve the keyboard selection across the rebuild: clearing the list resets + // SelectedIndex to -1, which would throw focus around every time a talking/mute + // indicator toggles. Capture the selected user id and reselect it afterwards. + uint? prevSel = (lstUsers.SelectedItem as UserListItem)?.UserId; + lstUsers.BeginUpdate(); lstUsers.Items.Clear(); foreach (var user in _users.Values @@ -612,6 +619,15 @@ public partial class MainForm : Form if (user.SelfDeafened || user.ServerDeafened) label += " (deafened)"; lstUsers.Items.Add(new UserListItem(user.Id, label)); } + if (prevSel is uint sel) + { + for (int i = 0; i < lstUsers.Items.Count; i++) + if (lstUsers.Items[i] is UserListItem item && item.UserId == sel) + { + lstUsers.SelectedIndex = i; + break; + } + } lstUsers.EndUpdate(); UpdateStatusLabel(); } @@ -854,7 +870,7 @@ public partial class MainForm : Form _pttEngaged = true; _feedback.PlaySound(SoundEvent.Ptt); } - e.Handled = true; + e.Handled = e.SuppressKeyPress = true; } private void MainForm_HotkeyDown(object? sender, KeyEventArgs e) @@ -865,23 +881,24 @@ public partial class MainForm : Form { case Keys.V: BtnMicToggle_Click(null, EventArgs.Empty); - e.Handled = true; break; case Keys.S: BtnScreenShareToggle_Click(null, EventArgs.Empty); - e.Handled = true; break; case Keys.M: chkMute.Checked = !chkMute.Checked; ApplySelfMute(); - e.Handled = true; break; case Keys.D: chkDeafen.Checked = !chkDeafen.Checked; ApplySelfMute(); - e.Handled = true; break; + default: + return; } + // A handled hotkey: suppress the follow-on WM_CHAR so the focused control + // (channel tree / user list) doesn't emit the system "ding". + e.Handled = e.SuppressKeyPress = true; } private void MainForm_KeyUp(object? sender, KeyEventArgs e) @@ -890,7 +907,7 @@ public partial class MainForm : Form if (e.KeyCode != _pttKey || _micStreamId == 0) return; _client.SetPushToTalk(false); _pttEngaged = false; - e.Handled = true; + e.Handled = e.SuppressKeyPress = true; } // ── Channel navigation ──────────────────────────────────────────────────── @@ -940,13 +957,17 @@ public partial class MainForm : Form win = new PrivateMessageForm(_client, userId, nick, _selfUserId); win.FormClosed += (_, _) => _pmWindows.Remove(userId); _pmWindows[userId] = win; - win.Show(this); + // Show without an owner: an owned form is forced to stay above MainForm and pulls + // focus back to itself, so the main window can't be worked in while a PM is open. + // OnFormClosed already closes any open PM windows, so this doesn't leak. + win.Show(); } else { if (win.WindowState == FormWindowState.Minimized) win.WindowState = FormWindowState.Normal; - win.BringToFront(); + // Raise it for this explicit user-initiated open without the owner-style focus trap. + win.Activate(); } return win; } diff --git a/clients/windows/VoiceCat.App/Forms/PrivateMessageForm.cs b/clients/windows/VoiceCat.App/Forms/PrivateMessageForm.cs index 18fecc2..8284594 100644 --- a/clients/windows/VoiceCat.App/Forms/PrivateMessageForm.cs +++ b/clients/windows/VoiceCat.App/Forms/PrivateMessageForm.cs @@ -26,6 +26,11 @@ public sealed class PrivateMessageForm : Form ClientSize = new Size(480, 360); MinimumSize = new Size(320, 240); StartPosition = FormStartPosition.Manual; + KeyPreview = true; + KeyDown += (_, e) => + { + if (e.KeyCode == Keys.Escape) { e.Handled = e.SuppressKeyPress = true; Close(); } + }; _rtbHistory = new RichTextBox { diff --git a/clients/windows/VoiceCat.App/Program.cs b/clients/windows/VoiceCat.App/Program.cs index f2f3b46..19ccacb 100644 --- a/clients/windows/VoiceCat.App/Program.cs +++ b/clients/windows/VoiceCat.App/Program.cs @@ -30,7 +30,7 @@ internal static class Program Console.WriteLine("Launching MainForm..."); Application.Run(new MainForm(connectDialog.ConnectedClient, connectDialog.SelfUserId, - connectDialog.Nickname)); + connectDialog.Nickname, connectDialog.ServerName)); Console.WriteLine("MainForm closed. Exiting."); } }