feat(windows): UI overhaul -- toolbar, unified log, PM windows, channel counts, output volume

- Voice actions (Join Voice, Share Screen Audio) moved to a ToolStrip toolbar and
  a new Voice menu in the menu bar; removed from the bottom voice panel
- Activity log and chat log collapsed into a single RichTextBox (rtbLog); activity
  events appear in gray, chat messages in default color
- Private messaging reworked: each conversation opens in its own modeless
  PrivateMessageForm instead of sharing the main chat log via a scope dropdown;
  cboScope removed; main compose bar always sends to the current channel
- New "Messages -> New Private Message..." menu item (Ctrl+P) opens a UserPickerDialog
  listing all connected server users (not just the current channel) so you can PM
  anyone on the server
- Channel tree now shows live user counts, e.g. "General (3)" -- counts sourced from
  the existing _users dictionary which already tracks all server users with channel IDs
- Global output volume slider (TrackBar, 0-100, default 80) added to the right panel;
  wired to new vc_set_output_volume C ABI function that applies a master gain multiplier
  in the audio engine playback callback after mixing all streams
- vc_set_output_volume added end-to-end: voicecat.h, audio_engine.h/.cpp,
  client.h/.cpp, voicecat.cpp, NativeMethods.cs, VoiceCatClient.cs
- Documented Windows PowerShell ctest requirement in AGENTS.md and CLAUDE.md:
  MinGW binaries exit 0xc0000139 in Git Bash; always run ctest/.exe via PowerShell

22/22 ctest green (PowerShell); dotnet build 0 warnings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 14:24:54 +02:00
parent fdcd8d1427
commit 97fa659422
14 changed files with 504 additions and 236 deletions

View File

@@ -0,0 +1,99 @@
using VoiceCat.Interop;
namespace VoiceCat.App.Forms;
/// <summary>
/// Simple modal dialog for picking one user from a list of all server users.
/// Used by "New Private Message" to let the user send a PM to anyone on the server.
/// </summary>
public sealed class UserPickerDialog : Form
{
private readonly ListBox _lstUsers;
private readonly Button _btnOk;
private readonly Button _btnCancel;
private readonly List<UserInfo> _users;
public uint SelectedUserId { get; private set; }
public UserPickerDialog(List<UserInfo> users)
{
_users = users;
Text = "New Private Message";
ClientSize = new Size(280, 320);
MinimumSize = new Size(220, 240);
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
StartPosition = FormStartPosition.CenterParent;
var lbl = new Label
{
Text = "Select a user:",
AutoSize = true,
Padding = new Padding(6, 6, 6, 2),
Dock = DockStyle.Top,
};
_lstUsers = new ListBox
{
Dock = DockStyle.Fill,
AccessibleName = "User list",
};
foreach (var u in users)
_lstUsers.Items.Add(new UserItem(u.Id, u.Nickname));
var btnPanel = new FlowLayoutPanel
{
Dock = DockStyle.Bottom,
FlowDirection = FlowDirection.RightToLeft,
Height = 40,
Padding = new Padding(4),
AutoSize = false,
};
_btnCancel = new Button
{
Text = "Cancel",
DialogResult = DialogResult.Cancel,
AutoSize = true,
};
_btnOk = new Button
{
Text = "OK",
DialogResult = DialogResult.OK,
AutoSize = true,
Enabled = false,
};
AcceptButton = _btnOk;
CancelButton = _btnCancel;
btnPanel.Controls.Add(_btnCancel);
btnPanel.Controls.Add(_btnOk);
Controls.Add(_lstUsers);
Controls.Add(lbl);
Controls.Add(btnPanel);
_lstUsers.SelectedIndexChanged += (_, _) =>
_btnOk.Enabled = _lstUsers.SelectedItem is UserItem;
_lstUsers.DoubleClick += (_, _) =>
{
if (_lstUsers.SelectedItem is UserItem) DialogResult = DialogResult.OK;
};
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
if (DialogResult == DialogResult.OK && _lstUsers.SelectedItem is UserItem item)
SelectedUserId = item.UserId;
base.OnFormClosing(e);
}
private sealed class UserItem(uint userId, string nickname)
{
public uint UserId { get; } = userId;
public override string ToString() => nickname;
}
}