Send-app UI: 'Send all applications' is now the first row of the list

Per Ed: instead of a separate 'Send all applications' checkbox that hides the
list, that toggle is now the FIRST ROW of the active-applications list. Ticked =
send the whole system and the individual apps collapse away; untick it and the
running apps appear beneath it (the remembered list shows too). Backed by the
now-hidden sendAllApplicationsCheckbox as state; a reserved sentinel row name
routes the first-row toggle to it. One scrollable control instead of a checkbox
plus a list. Coverage audit + profile round-trip still clean; gate 37/37.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-07-16 00:00:17 +01:00
co-authored by Claude Opus 4.8
parent 3103bac791
commit 18b079ba3a
+37 -14
View File
@@ -124,6 +124,11 @@ public sealed class MainForm : Form
// when the ticked apps' actual process ids change (an app opened or closed). // when the ticked apps' actual process ids change (an app opened or closed).
private RemSound.Sender.AudioSessionStartWatcher? sessionStartWatcher; private RemSound.Sender.AudioSessionStartWatcher? sessionStartWatcher;
private string? lastSendAppPidSignature; private string? lastSendAppPidSignature;
// The active-apps list's FIRST ROW is a synthetic "Send all applications" toggle (Ed's design): ticked =
// send the whole system, and the individual apps below collapse away; unticked reveals them. It's backed
// by sendAllApplicationsCheckbox (kept as hidden state). This reserved process-name marks that row so the
// handlers can tell it apart from a real app.
private const string SendAllAppsSentinel = "__send_all_apps__";
// Guards the sendModeList / sendAllApplicationsCheckbox / sendAppsList handlers while we // Guards the sendModeList / sendAllApplicationsCheckbox / sendAppsList handlers while we
// programmatically repopulate them (mode switch, profile apply, reconcile) so those handlers // programmatically repopulate them (mode switch, profile apply, reconcile) so those handlers
// don't fire MarkProfileDirty or trigger re-entrant rebuilds on our own writes. // don't fire MarkProfileDirty or trigger re-entrant rebuilds on our own writes.
@@ -3624,7 +3629,7 @@ public sealed class MainForm : Form
sendAllApplicationsCheckbox.CheckedChanged += (_, _) => sendAllApplicationsCheckbox.CheckedChanged += (_, _) =>
{ {
if (suppressSendAppEvents) return; if (suppressSendAppEvents) return;
ApplySendModeVisibility(); ApplySendModeVisibility(); // re-renders the active list (show/hide apps under the "send all" row)
MarkProfileDirty(); MarkProfileDirty();
ApplySendSources(); ApplySendSources();
}; };
@@ -3639,6 +3644,11 @@ public sealed class MainForm : Form
if (suppressSendAppEvents) return; if (suppressSendAppEvents) return;
if (list.Items[args.Index] is not AudioAppChoice choice) return; if (list.Items[args.Index] is not AudioAppChoice choice) return;
var nowChecked = args.NewValue == CheckState.Checked; var nowChecked = args.NewValue == CheckState.Checked;
// The "Send all applications" first row drives the backing checkbox (which re-applies the
// mode and rebuilds the list to show/hide the individual apps); real rows toggle the send set.
if (choice.ProcessName == SendAllAppsSentinel)
BeginInvoke(() => sendAllApplicationsCheckbox.Checked = nowChecked);
else
BeginInvoke(() => OnSendAppToggled(choice.ProcessName, nowChecked)); BeginInvoke(() => OnSendAppToggled(choice.ProcessName, nowChecked));
}; };
} }
@@ -3691,20 +3701,23 @@ public sealed class MainForm : Form
if (sendOutputDevicesLabel is not null) sendOutputDevicesLabel.Visible = !appsMode; if (sendOutputDevicesLabel is not null) sendOutputDevicesLabel.Visible = !appsMode;
SetRowControlVisible(sendOutputDevicesList, !appsMode); SetRowControlVisible(sendOutputDevicesList, !appsMode);
// Applications mode → show the master checkbox; the app list (and its label) show only when // The "Send all applications" toggle is now the first ROW of the active list, so the standalone
// "Send all applications" is unticked (with it ticked there's nothing to pick). // checkbox is hidden (kept only as backing state). The active list (with that row on top) shows
SetRowControlVisible(sendAllApplicationsCheckbox, appsMode); // whenever we're in applications mode. The remembered list only makes sense when picking specific
var showAppList = appsMode && !sendAllApplicationsCheckbox.Checked; // apps — i.e. when "send all" is off.
if (sendAppsLabel is not null) sendAppsLabel.Visible = showAppList; SetRowControlVisible(sendAllApplicationsCheckbox, false);
SetRowControlVisible(sendAppsList, showAppList); if (sendAppsLabel is not null) sendAppsLabel.Visible = appsMode;
if (rememberedAppsLabel is not null) rememberedAppsLabel.Visible = showAppList; SetRowControlVisible(sendAppsList, appsMode);
SetRowControlVisible(rememberedAppsList, showAppList); var showRemembered = appsMode && !sendAllApplicationsCheckbox.Checked;
if (rememberedAppsLabel is not null) rememberedAppsLabel.Visible = showRemembered;
SetRowControlVisible(rememberedAppsList, showRemembered);
// Only the specific-apps case needs the reconcile poll + the instant session-start watcher (in // Populate the active list whenever it's on screen (so the "Send all applications" row — and the
// "send all applications" mode we just loopback the whole default device, no per-app tracking). // apps beneath it, when send-all is off — are shown). Only the specific-apps case needs the ongoing
// reconcile poll + the instant session-start watcher; "send all" just loopbacks the whole device.
if (appsMode) ReconcileSendAppsList();
if (appsMode && !sendAllApplicationsCheckbox.Checked) if (appsMode && !sendAllApplicationsCheckbox.Checked)
{ {
if (sendAppsList.Items.Count == 0) ReconcileSendAppsList();
sendAppsReconcileTimer?.Start(); sendAppsReconcileTimer?.Start();
EnsureSessionStartWatcher(); EnsureSessionStartWatcher();
} }
@@ -3755,10 +3768,17 @@ public sealed class MainForm : Form
.OrderBy(n => n, StringComparer.CurrentCultureIgnoreCase) .OrderBy(n => n, StringComparer.CurrentCultureIgnoreCase)
.ToList(); .ToList();
var sendAll = sendAllApplicationsCheckbox.Checked;
// Active list: the "Send all applications" toggle is always the first row. Ticked, the individual
// apps below collapse away (there's nothing to pick); unticked, the running apps appear under it.
var activeChoices = new List<AudioAppChoice> { new(SendAllAppsSentinel, "Send all applications", running: true) };
if (!sendAll)
activeChoices.AddRange(running.Select(a => new AudioAppChoice(a.ProcessName, a.DisplayName, running: true)));
suppressSendAppEvents = true; suppressSendAppEvents = true;
try try
{ {
FillAppList(sendAppsList, running.Select(a => new AudioAppChoice(a.ProcessName, a.DisplayName, running: true))); FillAppList(sendAppsList, activeChoices);
FillAppList(rememberedAppsList, remembered.Select(name => FillAppList(rememberedAppsList, remembered.Select(name =>
{ {
var live = running.FirstOrDefault(a => string.Equals(a.ProcessName, name, StringComparison.OrdinalIgnoreCase)); var live = running.FirstOrDefault(a => string.Equals(a.ProcessName, name, StringComparison.OrdinalIgnoreCase));
@@ -3782,7 +3802,10 @@ public sealed class MainForm : Form
foreach (var c in choices) foreach (var c in choices)
{ {
var i = list.Items.Add(c); var i = list.Items.Add(c);
if (selectedSendApps.Contains(c.ProcessName)) list.SetItemChecked(i, true); // The "Send all applications" sentinel row is ticked from the send-all state; every real app row
// is ticked from the shared send set.
var ticked = c.ProcessName == SendAllAppsSentinel ? sendAllApplicationsCheckbox.Checked : selectedSendApps.Contains(c.ProcessName);
if (ticked) list.SetItemChecked(i, true);
} }
list.EndUpdate(); list.EndUpdate();
} }