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

@@ -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;
}