feat(M4): Windows WinForms client, TOFU identity pinning, VAD threshold + always-on mode
Core ABI extensions (voicecat.h): - vc_list_channels / vc_list_users / vc_list_user_streams — pull-based snapshot getters for the channel-tree and user-list UI; session_model_mu_ guards cross-thread reads - VC_EVENT_JOIN_RESULT / vc_join_channel — channel join with optional password - VC_EVENT_SERVER_IDENTITY + vc_confirm_server_identity — TOFU gate that blocks io_thread_ until the UI approves or rejects; pins TLS leaf-cert SHA-256 (not declared Ed25519) - vc_get_server_identity_display — Ed25519 fingerprint for human-readable display only - VC_INPUT_ALWAYS_ON = 2 in vc_input_mode — transmit unconditionally, no VAD gate - vc_set_vad_threshold — live RMS threshold update (0.0–1.0); EnergyVadProcessor stores it atomically so the audio RT path reads without a lock C++ implementation: - SessionModel::apply_snapshot / apply_channel_event fixed to populate parent_id, password_protected, and max_users (were permanently zeroed) - TlsContext::peer_cert_fingerprint — SHA-256 of peer leaf cert DER via mbedTLS - TofuStore split into peek (read-only) + pin (write) so first-connect only persists after user approval; tofu_store_path in vc_config for per-user pin file location - TcpAcceptor uses dual-stack IPv6+IPv4 fallback (fixes localhost → ::1 on Windows) - windows-client CMake preset: Release shared DLL, static MinGW runtime, no tools/tests - New C++ tests: test_channel_user_list_abi, test_tofu_flow (14/14 green) Windows client (clients/windows/ — .NET 10 WinForms): - VoiceCat.Interop: LibraryImport P/Invoke surface, UnmanagedCallersOnly callbacks, Channel<VoiceCatEvent> event delivery drained by 30ms WinForms Timer - VoiceCat.App: ConnectDialog (saved servers, DPAPI password storage), ServerIdentity- Dialog (TOFU first-connect / mismatch warning), MainForm (channel TreeView, user ListBox, RichTextBox chat, voice controls, device pickers, VAD/PTT/always-on mode, per-user gain/mute/NR tuning, VAD sensitivity TrackBar, level meter ProgressBar) - PttKeyCaptureDialog — focus-scoped PTT key capture (documented limitation) - PerUserTuningDialog — real-time gain/mute/NR applied to all of a user's streams - Accessibility: explicit AccessibleName/Description on every control, & mnemonics, Activity log ListBox as durable screen-reader record, AutomationNotification for curated live announcements Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
364
clients/windows/VoiceCat.App/Forms/MainForm.Designer.cs
generated
Normal file
364
clients/windows/VoiceCat.App/Forms/MainForm.Designer.cs
generated
Normal file
@@ -0,0 +1,364 @@
|
||||
namespace VoiceCat.App.Forms;
|
||||
|
||||
partial class MainForm
|
||||
{
|
||||
private System.ComponentModel.IContainer components = null!;
|
||||
|
||||
// Status bar
|
||||
private Label lblStatus = null!;
|
||||
|
||||
// Main left/right split
|
||||
private SplitContainer splitMain = null!;
|
||||
|
||||
// Left panel: channel tree on top, user list on bottom
|
||||
private SplitContainer splitLeft = null!;
|
||||
private Label lblChannels = null!;
|
||||
private TreeView tvChannels = null!;
|
||||
private Label lblUsers = null!;
|
||||
private ListBox lstUsers = null!;
|
||||
|
||||
// Right panel: chat transcript, compose row, activity log
|
||||
private TableLayoutPanel tblRight = null!;
|
||||
private Label lblChat = null!;
|
||||
private RichTextBox rtbChat = null!;
|
||||
private TableLayoutPanel tblCompose = null!;
|
||||
private ComboBox cboScope = null!;
|
||||
private TextBox txtCompose = null!;
|
||||
private Button btnSend = null!;
|
||||
private Label lblActivity = null!;
|
||||
private ListBox lstActivity = null!;
|
||||
|
||||
// Voice control panel (docked Bottom)
|
||||
private Panel pnlVoice = null!;
|
||||
private FlowLayoutPanel flpVoiceTop = null!;
|
||||
private FlowLayoutPanel flpVoiceBottom = null!;
|
||||
private Button btnMicToggle = null!;
|
||||
private CheckBox chkMute = null!;
|
||||
private CheckBox chkDeafen = null!;
|
||||
private RadioButton radioVad = null!;
|
||||
private RadioButton radioPtt = null!;
|
||||
private RadioButton radioAlwaysOn = null!;
|
||||
private Label lblPttKey = null!;
|
||||
private Button btnChangePtt = null!;
|
||||
private Label lblInputDevice = null!;
|
||||
private ComboBox cboInputDevice = null!;
|
||||
private Button btnRefreshDevices = null!;
|
||||
private Label lblLevel = null!;
|
||||
private ProgressBar pbLevel = null!;
|
||||
private Label lblVadThreshold = null!;
|
||||
private TrackBar trkVadThreshold = null!;
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing) components?.Dispose();
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
components = new System.ComponentModel.Container();
|
||||
|
||||
lblStatus = new Label();
|
||||
splitMain = new SplitContainer();
|
||||
splitLeft = new SplitContainer();
|
||||
lblChannels = new Label();
|
||||
tvChannels = new TreeView();
|
||||
lblUsers = new Label();
|
||||
lstUsers = new ListBox();
|
||||
tblRight = new TableLayoutPanel();
|
||||
lblChat = new Label();
|
||||
rtbChat = new RichTextBox();
|
||||
tblCompose = new TableLayoutPanel();
|
||||
cboScope = new ComboBox();
|
||||
txtCompose = new TextBox();
|
||||
btnSend = new Button();
|
||||
lblActivity = new Label();
|
||||
lstActivity = new ListBox();
|
||||
pnlVoice = new Panel();
|
||||
flpVoiceTop = new FlowLayoutPanel();
|
||||
flpVoiceBottom = new FlowLayoutPanel();
|
||||
btnMicToggle = new Button();
|
||||
chkMute = new CheckBox();
|
||||
chkDeafen = new CheckBox();
|
||||
radioVad = new RadioButton();
|
||||
radioPtt = new RadioButton();
|
||||
radioAlwaysOn = new RadioButton();
|
||||
lblPttKey = new Label();
|
||||
btnChangePtt = new Button();
|
||||
lblInputDevice = new Label();
|
||||
cboInputDevice = new ComboBox();
|
||||
btnRefreshDevices = new Button();
|
||||
lblLevel = new Label();
|
||||
pbLevel = new ProgressBar();
|
||||
lblVadThreshold = new Label();
|
||||
trkVadThreshold = new TrackBar();
|
||||
|
||||
// ── Status label ──────────────────────────────────────────────────────
|
||||
lblStatus.AccessibleName = "Connection status";
|
||||
lblStatus.Dock = DockStyle.Top;
|
||||
lblStatus.AutoSize = true;
|
||||
lblStatus.Padding = new Padding(6, 4, 6, 4);
|
||||
lblStatus.TabIndex = 0;
|
||||
lblStatus.Text = "Connecting...";
|
||||
|
||||
// ── Channel tree ──────────────────────────────────────────────────────
|
||||
lblChannels.Text = "Channels:";
|
||||
lblChannels.Dock = DockStyle.Top;
|
||||
lblChannels.AutoSize = true;
|
||||
lblChannels.Padding = new Padding(4, 4, 4, 2);
|
||||
|
||||
tvChannels.AccessibleName = "Channel list";
|
||||
tvChannels.AccessibleDescription =
|
||||
"Double-click or press Enter to join a channel. " +
|
||||
"Channels marked [password] require a password.";
|
||||
tvChannels.Dock = DockStyle.Fill;
|
||||
tvChannels.HideSelection = false;
|
||||
tvChannels.TabIndex = 0;
|
||||
|
||||
// ── User list ─────────────────────────────────────────────────────────
|
||||
lblUsers.Text = "Users in channel:";
|
||||
lblUsers.Dock = DockStyle.Top;
|
||||
lblUsers.AutoSize = true;
|
||||
lblUsers.Padding = new Padding(4, 4, 4, 2);
|
||||
|
||||
lstUsers.AccessibleName = "Users in current channel";
|
||||
lstUsers.AccessibleDescription =
|
||||
"People in the same channel. Double-click or press Enter for per-user volume settings. " +
|
||||
"Talking users are marked (talking).";
|
||||
lstUsers.Dock = DockStyle.Fill;
|
||||
lstUsers.TabIndex = 1;
|
||||
|
||||
// ── Left split (channels top, users bottom) ───────────────────────────
|
||||
splitLeft.Orientation = Orientation.Horizontal;
|
||||
splitLeft.Dock = DockStyle.Fill;
|
||||
splitLeft.Panel1MinSize = 100;
|
||||
splitLeft.Panel2MinSize = 80;
|
||||
splitLeft.TabIndex = 0;
|
||||
splitLeft.Panel1.Controls.Add(tvChannels);
|
||||
splitLeft.Panel1.Controls.Add(lblChannels);
|
||||
splitLeft.Panel2.Controls.Add(lstUsers);
|
||||
splitLeft.Panel2.Controls.Add(lblUsers);
|
||||
|
||||
// ── Chat transcript ───────────────────────────────────────────────────
|
||||
lblChat.Text = "Chat:";
|
||||
lblChat.AutoSize = true;
|
||||
lblChat.Padding = new Padding(2, 2, 2, 1);
|
||||
|
||||
rtbChat.AccessibleName = "Chat transcript";
|
||||
rtbChat.AccessibleDescription = "History of channel and private messages.";
|
||||
rtbChat.Dock = DockStyle.Fill;
|
||||
rtbChat.ReadOnly = true;
|
||||
rtbChat.ScrollBars = RichTextBoxScrollBars.Vertical;
|
||||
rtbChat.BackColor = SystemColors.Window;
|
||||
rtbChat.TabIndex = 0;
|
||||
|
||||
// ── Compose row ───────────────────────────────────────────────────────
|
||||
cboScope.AccessibleName = "Send to";
|
||||
cboScope.AccessibleDescription =
|
||||
"Choose Channel to send to everyone, or a specific user for a private message.";
|
||||
cboScope.DropDownStyle = ComboBoxStyle.DropDownList;
|
||||
cboScope.Dock = DockStyle.Fill;
|
||||
cboScope.TabIndex = 1;
|
||||
|
||||
txtCompose.AccessibleName = "Message text";
|
||||
txtCompose.AccessibleDescription = "Type your message. Press Enter or click Send to send.";
|
||||
txtCompose.Dock = DockStyle.Fill;
|
||||
txtCompose.TabIndex = 2;
|
||||
|
||||
btnSend.Text = "&Send";
|
||||
btnSend.Dock = DockStyle.Fill;
|
||||
btnSend.TabIndex = 3;
|
||||
|
||||
tblCompose.ColumnCount = 3;
|
||||
tblCompose.RowCount = 1;
|
||||
tblCompose.Dock = DockStyle.Fill;
|
||||
tblCompose.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 165F));
|
||||
tblCompose.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||||
tblCompose.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 68F));
|
||||
tblCompose.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
|
||||
tblCompose.Padding = new Padding(0, 3, 0, 0);
|
||||
tblCompose.Controls.Add(cboScope, 0, 0);
|
||||
tblCompose.Controls.Add(txtCompose, 1, 0);
|
||||
tblCompose.Controls.Add(btnSend, 2, 0);
|
||||
|
||||
// ── Activity log ──────────────────────────────────────────────────────
|
||||
lblActivity.Text = "Activity:";
|
||||
lblActivity.AutoSize = true;
|
||||
lblActivity.Padding = new Padding(2, 4, 2, 1);
|
||||
|
||||
lstActivity.AccessibleName = "Activity log";
|
||||
lstActivity.AccessibleDescription =
|
||||
"Record of joins, leaves, talk-state changes, and server messages.";
|
||||
lstActivity.Dock = DockStyle.Fill;
|
||||
lstActivity.HorizontalScrollbar = true;
|
||||
lstActivity.TabIndex = 4;
|
||||
|
||||
// ── Right table layout ────────────────────────────────────────────────
|
||||
tblRight.ColumnCount = 1;
|
||||
tblRight.RowCount = 5;
|
||||
tblRight.Dock = DockStyle.Fill;
|
||||
tblRight.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||||
tblRight.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
tblRight.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
|
||||
tblRight.RowStyles.Add(new RowStyle(SizeType.Absolute, 34F));
|
||||
tblRight.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
tblRight.RowStyles.Add(new RowStyle(SizeType.Absolute, 120F));
|
||||
tblRight.Controls.Add(lblChat, 0, 0);
|
||||
tblRight.Controls.Add(rtbChat, 0, 1);
|
||||
tblRight.Controls.Add(tblCompose, 0, 2);
|
||||
tblRight.Controls.Add(lblActivity, 0, 3);
|
||||
tblRight.Controls.Add(lstActivity, 0, 4);
|
||||
|
||||
// ── Main split ────────────────────────────────────────────────────────
|
||||
// SplitterDistance set in OnLoad — see MainForm.cs.
|
||||
splitMain.Dock = DockStyle.Fill;
|
||||
splitMain.Panel1MinSize = 150;
|
||||
splitMain.TabIndex = 1;
|
||||
splitMain.Panel1.Controls.Add(splitLeft);
|
||||
splitMain.Panel2.Controls.Add(tblRight);
|
||||
|
||||
// ── Voice control panel ───────────────────────────────────────────────
|
||||
// Top row: mic start/stop, mute/deafen, VAD/PTT
|
||||
btnMicToggle.Text = "&Join Voice";
|
||||
btnMicToggle.AutoSize = true;
|
||||
btnMicToggle.Margin = new Padding(0, 2, 6, 0);
|
||||
btnMicToggle.TabIndex = 0;
|
||||
|
||||
chkMute.Text = "&Mute mic";
|
||||
chkMute.AutoSize = true;
|
||||
chkMute.Enabled = false;
|
||||
chkMute.Margin = new Padding(0, 4, 6, 0);
|
||||
chkMute.TabIndex = 1;
|
||||
|
||||
chkDeafen.Text = "&Deafen";
|
||||
chkDeafen.AutoSize = true;
|
||||
chkDeafen.Enabled = false;
|
||||
chkDeafen.Margin = new Padding(0, 4, 12, 0);
|
||||
chkDeafen.TabIndex = 2;
|
||||
|
||||
var lblMode = new Label { Text = "Mode:", AutoSize = true, Margin = new Padding(0, 5, 4, 0) };
|
||||
|
||||
radioVad.Text = "&Voice activation";
|
||||
radioVad.AutoSize = true;
|
||||
radioVad.Checked = true;
|
||||
radioVad.Enabled = false;
|
||||
radioVad.Margin = new Padding(0, 4, 6, 0);
|
||||
radioVad.TabIndex = 3;
|
||||
|
||||
radioPtt.Text = "&Push to talk";
|
||||
radioPtt.AutoSize = true;
|
||||
radioPtt.Enabled = false;
|
||||
radioPtt.Margin = new Padding(0, 4, 4, 0);
|
||||
radioPtt.TabIndex = 4;
|
||||
|
||||
radioAlwaysOn.Text = "A&lways on";
|
||||
radioAlwaysOn.AutoSize = true;
|
||||
radioAlwaysOn.Enabled = false;
|
||||
radioAlwaysOn.Margin = new Padding(0, 4, 12, 0);
|
||||
radioAlwaysOn.TabIndex = 5;
|
||||
|
||||
lblPttKey.Text = "(F8)";
|
||||
lblPttKey.AutoSize = true;
|
||||
lblPttKey.Margin = new Padding(2, 5, 4, 0);
|
||||
lblPttKey.Visible = false;
|
||||
|
||||
btnChangePtt.Text = "Change key...";
|
||||
btnChangePtt.AutoSize = true;
|
||||
btnChangePtt.Margin = new Padding(0, 2, 0, 0);
|
||||
btnChangePtt.Visible = false;
|
||||
btnChangePtt.TabIndex = 6;
|
||||
|
||||
flpVoiceTop.Dock = DockStyle.Top;
|
||||
flpVoiceTop.Height = 34;
|
||||
flpVoiceTop.AutoSize = false;
|
||||
flpVoiceTop.Padding = new Padding(4, 2, 4, 0);
|
||||
flpVoiceTop.Controls.Add(btnMicToggle);
|
||||
flpVoiceTop.Controls.Add(chkMute);
|
||||
flpVoiceTop.Controls.Add(chkDeafen);
|
||||
flpVoiceTop.Controls.Add(lblMode);
|
||||
flpVoiceTop.Controls.Add(radioVad);
|
||||
flpVoiceTop.Controls.Add(radioPtt);
|
||||
flpVoiceTop.Controls.Add(radioAlwaysOn);
|
||||
flpVoiceTop.Controls.Add(lblPttKey);
|
||||
flpVoiceTop.Controls.Add(btnChangePtt);
|
||||
|
||||
// Bottom row: device picker + level meter
|
||||
lblInputDevice.Text = "Input:";
|
||||
lblInputDevice.AutoSize = true;
|
||||
lblInputDevice.Margin = new Padding(0, 5, 4, 0);
|
||||
|
||||
cboInputDevice.AccessibleName = "Input device";
|
||||
cboInputDevice.AccessibleDescription = "Select which microphone or audio device to use.";
|
||||
cboInputDevice.DropDownStyle = ComboBoxStyle.DropDownList;
|
||||
cboInputDevice.Width = 200;
|
||||
cboInputDevice.Margin = new Padding(0, 2, 4, 0);
|
||||
cboInputDevice.TabIndex = 7;
|
||||
|
||||
btnRefreshDevices.Text = "Re&fresh";
|
||||
btnRefreshDevices.AutoSize = true;
|
||||
btnRefreshDevices.Margin = new Padding(0, 2, 12, 0);
|
||||
btnRefreshDevices.TabIndex = 8;
|
||||
|
||||
lblLevel.Text = "Level:";
|
||||
lblLevel.AutoSize = true;
|
||||
lblLevel.Margin = new Padding(0, 5, 4, 0);
|
||||
|
||||
pbLevel.AccessibleName = "Microphone level";
|
||||
pbLevel.AccessibleDescription = "Current input level from the microphone.";
|
||||
pbLevel.Width = 120;
|
||||
pbLevel.Height = 16;
|
||||
pbLevel.Maximum = 100;
|
||||
pbLevel.Margin = new Padding(0, 6, 0, 0);
|
||||
pbLevel.Style = ProgressBarStyle.Continuous;
|
||||
pbLevel.TabStop = false; // informational, not actionable
|
||||
|
||||
lblVadThreshold.Text = "Sensitivity:";
|
||||
lblVadThreshold.AutoSize = true;
|
||||
lblVadThreshold.Margin = new Padding(12, 5, 4, 0);
|
||||
lblVadThreshold.Visible = true; // shown when VAD mode active
|
||||
|
||||
trkVadThreshold.AccessibleName = "VAD sensitivity";
|
||||
trkVadThreshold.AccessibleDescription =
|
||||
"Voice detection sensitivity. Higher = more sensitive (triggers on quieter sounds). " +
|
||||
"Range 1–100; default 25.";
|
||||
trkVadThreshold.Minimum = 1;
|
||||
trkVadThreshold.Maximum = 100;
|
||||
trkVadThreshold.Value = 76; // maps to ~0.024 (≈ default 0.025 threshold)
|
||||
trkVadThreshold.TickFrequency = 10;
|
||||
trkVadThreshold.SmallChange = 1;
|
||||
trkVadThreshold.LargeChange = 10;
|
||||
trkVadThreshold.Width = 120;
|
||||
trkVadThreshold.Margin = new Padding(0, 2, 0, 0);
|
||||
trkVadThreshold.TabIndex = 9;
|
||||
trkVadThreshold.Visible = true;
|
||||
|
||||
flpVoiceBottom.Dock = DockStyle.Fill;
|
||||
flpVoiceBottom.Padding = new Padding(4, 0, 4, 2);
|
||||
flpVoiceBottom.Controls.Add(lblInputDevice);
|
||||
flpVoiceBottom.Controls.Add(cboInputDevice);
|
||||
flpVoiceBottom.Controls.Add(btnRefreshDevices);
|
||||
flpVoiceBottom.Controls.Add(lblLevel);
|
||||
flpVoiceBottom.Controls.Add(pbLevel);
|
||||
flpVoiceBottom.Controls.Add(lblVadThreshold);
|
||||
flpVoiceBottom.Controls.Add(trkVadThreshold);
|
||||
|
||||
pnlVoice.Dock = DockStyle.Bottom;
|
||||
pnlVoice.Height = 68;
|
||||
pnlVoice.BorderStyle = BorderStyle.FixedSingle;
|
||||
pnlVoice.Padding = new Padding(0);
|
||||
pnlVoice.Controls.Add(flpVoiceBottom); // Fill — added first
|
||||
pnlVoice.Controls.Add(flpVoiceTop); // Top — added last
|
||||
|
||||
// ── Form ──────────────────────────────────────────────────────────────
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
ClientSize = new Size(960, 680);
|
||||
MinimumSize = new Size(700, 540);
|
||||
KeyPreview = true; // form sees KeyDown/KeyUp before focused control (needed for PTT)
|
||||
// Controls added in reverse docking priority: Fill first, then Bottom, then Top.
|
||||
Controls.Add(splitMain); // DockStyle.Fill
|
||||
Controls.Add(pnlVoice); // DockStyle.Bottom
|
||||
Controls.Add(lblStatus); // DockStyle.Top
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
Text = "VoiceCat";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user