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:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user