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