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 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 12:30:48 +02:00
parent 8bb2ba933c
commit 65736df464
4 changed files with 43 additions and 12 deletions

View File

@@ -19,6 +19,7 @@ public partial class ConnectDialog : Form
public VoiceCatClient? ConnectedClient { get; private set; } public VoiceCatClient? ConnectedClient { get; private set; }
public uint SelfUserId { get; private set; } public uint SelfUserId { get; private set; }
public string Nickname { get; private set; } = ""; public string Nickname { get; private set; } = "";
public string ServerName { get; private set; } = "";
public ConnectDialog() public ConnectDialog()
{ {
@@ -115,6 +116,10 @@ public partial class ConnectDialog : Form
SetBusy(true); SetBusy(true);
lblStatus.Text = "Connecting..."; lblStatus.Text = "Connecting...";
ServerName = string.IsNullOrWhiteSpace(server.DisplayName)
? $"{server.Host}:{server.Port}"
: server.DisplayName;
string tofuDir = Path.GetDirectoryName(ServerListStore.TofuStorePath)!; string tofuDir = Path.GetDirectoryName(ServerListStore.TofuStorePath)!;
Console.WriteLine($"[ConnectDialog] tofu store dir: {tofuDir}"); Console.WriteLine($"[ConnectDialog] tofu store dir: {tofuDir}");
Directory.CreateDirectory(tofuDir); Directory.CreateDirectory(tofuDir);

View File

@@ -42,14 +42,16 @@ public partial class MainForm : Form
private ToolStripMenuItem _miJoinVoice = null!; private ToolStripMenuItem _miJoinVoice = null!;
private ToolStripMenuItem _miScreenShare = null!; private ToolStripMenuItem _miScreenShare = null!;
public MainForm(VoiceCatClient client, uint selfUserId, string nickname) public MainForm(VoiceCatClient client, uint selfUserId, string nickname, string serverName)
{ {
InitializeComponent(); InitializeComponent();
_client = client; _client = client;
_selfUserId = selfUserId; _selfUserId = selfUserId;
_nickname = nickname; _nickname = nickname;
Text = $"VoiceCat — {nickname}"; Text = string.IsNullOrWhiteSpace(serverName)
? $"VoiceCat — {nickname}"
: $"VoiceCat — {nickname} @ {serverName}";
_client.EventReceived += OnEvent; _client.EventReceived += OnEvent;
_client.LevelChanged += OnLevelChanged; _client.LevelChanged += OnLevelChanged;
@@ -69,7 +71,7 @@ public partial class MainForm : Form
tvChannels.DoubleClick += TvChannels_DoubleClick; tvChannels.DoubleClick += TvChannels_DoubleClick;
tvChannels.KeyDown += TvChannels_KeyDown; tvChannels.KeyDown += TvChannels_KeyDown;
lstUsers.DoubleClick += (_, _) => OpenUserTuning(); 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 // Compose
txtCompose.KeyDown += TxtCompose_KeyDown; txtCompose.KeyDown += TxtCompose_KeyDown;
@@ -599,6 +601,11 @@ public partial class MainForm : Form
private void RefreshUserList() 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.BeginUpdate();
lstUsers.Items.Clear(); lstUsers.Items.Clear();
foreach (var user in _users.Values foreach (var user in _users.Values
@@ -612,6 +619,15 @@ public partial class MainForm : Form
if (user.SelfDeafened || user.ServerDeafened) label += " (deafened)"; if (user.SelfDeafened || user.ServerDeafened) label += " (deafened)";
lstUsers.Items.Add(new UserListItem(user.Id, label)); 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(); lstUsers.EndUpdate();
UpdateStatusLabel(); UpdateStatusLabel();
} }
@@ -854,7 +870,7 @@ public partial class MainForm : Form
_pttEngaged = true; _pttEngaged = true;
_feedback.PlaySound(SoundEvent.Ptt); _feedback.PlaySound(SoundEvent.Ptt);
} }
e.Handled = true; e.Handled = e.SuppressKeyPress = true;
} }
private void MainForm_HotkeyDown(object? sender, KeyEventArgs e) private void MainForm_HotkeyDown(object? sender, KeyEventArgs e)
@@ -865,23 +881,24 @@ public partial class MainForm : Form
{ {
case Keys.V: case Keys.V:
BtnMicToggle_Click(null, EventArgs.Empty); BtnMicToggle_Click(null, EventArgs.Empty);
e.Handled = true;
break; break;
case Keys.S: case Keys.S:
BtnScreenShareToggle_Click(null, EventArgs.Empty); BtnScreenShareToggle_Click(null, EventArgs.Empty);
e.Handled = true;
break; break;
case Keys.M: case Keys.M:
chkMute.Checked = !chkMute.Checked; chkMute.Checked = !chkMute.Checked;
ApplySelfMute(); ApplySelfMute();
e.Handled = true;
break; break;
case Keys.D: case Keys.D:
chkDeafen.Checked = !chkDeafen.Checked; chkDeafen.Checked = !chkDeafen.Checked;
ApplySelfMute(); ApplySelfMute();
e.Handled = true;
break; 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) private void MainForm_KeyUp(object? sender, KeyEventArgs e)
@@ -890,7 +907,7 @@ public partial class MainForm : Form
if (e.KeyCode != _pttKey || _micStreamId == 0) return; if (e.KeyCode != _pttKey || _micStreamId == 0) return;
_client.SetPushToTalk(false); _client.SetPushToTalk(false);
_pttEngaged = false; _pttEngaged = false;
e.Handled = true; e.Handled = e.SuppressKeyPress = true;
} }
// ── Channel navigation ──────────────────────────────────────────────────── // ── Channel navigation ────────────────────────────────────────────────────
@@ -940,13 +957,17 @@ public partial class MainForm : Form
win = new PrivateMessageForm(_client, userId, nick, _selfUserId); win = new PrivateMessageForm(_client, userId, nick, _selfUserId);
win.FormClosed += (_, _) => _pmWindows.Remove(userId); win.FormClosed += (_, _) => _pmWindows.Remove(userId);
_pmWindows[userId] = win; _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 else
{ {
if (win.WindowState == FormWindowState.Minimized) if (win.WindowState == FormWindowState.Minimized)
win.WindowState = FormWindowState.Normal; win.WindowState = FormWindowState.Normal;
win.BringToFront(); // Raise it for this explicit user-initiated open without the owner-style focus trap.
win.Activate();
} }
return win; return win;
} }

View File

@@ -26,6 +26,11 @@ public sealed class PrivateMessageForm : Form
ClientSize = new Size(480, 360); ClientSize = new Size(480, 360);
MinimumSize = new Size(320, 240); MinimumSize = new Size(320, 240);
StartPosition = FormStartPosition.Manual; StartPosition = FormStartPosition.Manual;
KeyPreview = true;
KeyDown += (_, e) =>
{
if (e.KeyCode == Keys.Escape) { e.Handled = e.SuppressKeyPress = true; Close(); }
};
_rtbHistory = new RichTextBox _rtbHistory = new RichTextBox
{ {

View File

@@ -30,7 +30,7 @@ internal static class Program
Console.WriteLine("Launching MainForm..."); Console.WriteLine("Launching MainForm...");
Application.Run(new MainForm(connectDialog.ConnectedClient, connectDialog.SelfUserId, Application.Run(new MainForm(connectDialog.ConnectedClient, connectDialog.SelfUserId,
connectDialog.Nickname)); connectDialog.Nickname, connectDialog.ServerName));
Console.WriteLine("MainForm closed. Exiting."); Console.WriteLine("MainForm closed. Exiting.");
} }
} }