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,115 @@
using VoiceCat.Interop;
namespace VoiceCat.App.Forms;
/// <summary>
/// Modeless window for a single private message conversation.
/// Created by MainForm and shown non-modally; MainForm routes incoming PMs here.
/// </summary>
public sealed class PrivateMessageForm : Form
{
private readonly VoiceCatClient _client;
private readonly uint _userId;
private readonly uint _selfUserId;
private readonly RichTextBox _rtbHistory;
private readonly TextBox _txtCompose;
private readonly Button _btnSend;
public PrivateMessageForm(VoiceCatClient client, uint userId, string nickname, uint selfUserId)
{
_client = client;
_userId = userId;
_selfUserId = selfUserId;
Text = $"Private Message — {nickname}";
ClientSize = new Size(480, 360);
MinimumSize = new Size(320, 240);
StartPosition = FormStartPosition.Manual;
_rtbHistory = new RichTextBox
{
ReadOnly = true,
Dock = DockStyle.Fill,
ScrollBars = RichTextBoxScrollBars.Vertical,
BackColor = SystemColors.Window,
AccessibleName = "Message history",
};
var composePanel = new TableLayoutPanel
{
Dock = DockStyle.Bottom,
Height = 34,
ColumnCount = 2,
RowCount = 1,
Padding = new Padding(0, 3, 0, 0),
};
composePanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
composePanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 68F));
composePanel.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
_txtCompose = new TextBox
{
Dock = DockStyle.Fill,
AccessibleName = "Message text",
};
_btnSend = new Button
{
Text = "&Send",
Dock = DockStyle.Fill,
};
composePanel.Controls.Add(_txtCompose, 0, 0);
composePanel.Controls.Add(_btnSend, 1, 0);
Controls.Add(_rtbHistory);
Controls.Add(composePanel);
_txtCompose.KeyDown += (_, e) =>
{
if (e.KeyCode == Keys.Enter) { Send(); e.Handled = e.SuppressKeyPress = true; }
};
_btnSend.Click += (_, _) => Send();
}
// Called by MainForm when a PM message arrives (or we send one and the server echoes it).
public void AppendMessage(string time, bool isSelf, string sender, string text)
{
if (IsDisposed) return;
string line = $"[{time}] {sender}: {text}\n";
int selStart = _rtbHistory.TextLength;
_rtbHistory.AppendText(line);
if (isSelf)
{
_rtbHistory.Select(selStart, line.Length);
_rtbHistory.SelectionColor = Color.Gray;
_rtbHistory.Select(_rtbHistory.TextLength, 0);
_rtbHistory.SelectionColor = _rtbHistory.ForeColor;
}
_rtbHistory.ScrollToCaret();
}
// Called by MainForm for status events (e.g., the other user disconnected).
public void AppendActivity(string text)
{
if (IsDisposed) return;
string line = $"[{DateTime.Now:HH:mm}] {text}\n";
int selStart = _rtbHistory.TextLength;
_rtbHistory.AppendText(line);
_rtbHistory.Select(selStart, line.Length);
_rtbHistory.SelectionColor = Color.Gray;
_rtbHistory.Select(_rtbHistory.TextLength, 0);
_rtbHistory.SelectionColor = _rtbHistory.ForeColor;
_rtbHistory.ScrollToCaret();
}
private void Send()
{
string msg = _txtCompose.Text.Trim();
if (string.IsNullOrEmpty(msg)) return;
_client.SendText(VcTextScope.Private, _userId, msg);
_txtCompose.Clear();
// No optimistic echo: the server relays our own message back, so AppendMessage is
// called from MainForm.HandleTextMessage when the echo arrives.
}
}