Review bug batch: Delete key on remembered apps (#26), phantom service cues, suppress-flag guards
- Remembered applications list now supports the Delete key (issue #26): deleting an entry forgets it machine-wide, mirroring the remembered-peers list exactly (same focus-next-item behaviour). Logic lives in RemoveRememberedApplication so the self-test drives the real path; the send-app semantics test now covers deletion. Manual documents Delete on BOTH remembered lists (neither was documented). - Service dialog's Additional options no longer offers connect/disconnect cue checkboxes: the headless service never plays cues (nothing in it touches CuePlayer, and a logged-out session couldn't render them), so the switches did nothing. Manual sentence updated to match. - Three re-entrancy suppress flags (send-mode reset x2, streaming password gate) now clear in try/finally - a throw can no longer leave list events suppressed for the rest of the session. Gate 47/47. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3706,6 +3706,21 @@ public sealed class MainForm : Form
|
||||
WireAppList(sendAppsList);
|
||||
WireAppList(rememberedAppsList);
|
||||
|
||||
// Delete on a remembered application forgets it — the same affordance the remembered-peers list
|
||||
// has had all along (issue #26; the two remembered lists should feel identical). Items in this
|
||||
// list are by definition not ticked (ticked apps show in the Active list), so deleting one only
|
||||
// edits the machine-wide remembered set; no capture change is implied.
|
||||
rememberedAppsList.KeyDown += (_, args) =>
|
||||
{
|
||||
if (args.KeyCode != Keys.Delete) return;
|
||||
args.Handled = true;
|
||||
args.SuppressKeyPress = true;
|
||||
if (rememberedAppsList.SelectedItem is not AudioAppChoice choice) return;
|
||||
var prevIndex = rememberedAppsList.SelectedIndex;
|
||||
RemoveRememberedApplication(choice.ProcessName);
|
||||
FocusListItemAfterDelete(rememberedAppsList, prevIndex);
|
||||
};
|
||||
|
||||
// Reconcile the app list on a slow timer so entries appear/disappear as apps open and close,
|
||||
// without ever piling up (each pass releases every session object — see AudioAppEnumerator).
|
||||
// Only ticks while the applications list is actually visible.
|
||||
@@ -3742,8 +3757,8 @@ public sealed class MainForm : Form
|
||||
if (!supported && sendModeList.Items.Count > 0 && sendModeList.SelectedIndex != SendModeDevicesIndex)
|
||||
{
|
||||
suppressSendAppEvents = true;
|
||||
sendModeList.SelectedIndex = SendModeDevicesIndex;
|
||||
suppressSendAppEvents = false;
|
||||
try { sendModeList.SelectedIndex = SendModeDevicesIndex; }
|
||||
finally { suppressSendAppEvents = false; } // a throw must not leave events suppressed for good
|
||||
}
|
||||
|
||||
var appsMode = supported && sendModeList.SelectedIndex == SendModeApplicationsIndex;
|
||||
@@ -3842,6 +3857,18 @@ public sealed class MainForm : Form
|
||||
UpdateCheckedListStatus(rememberedAppsList, rememberedAppsStatusLabel, "remembered application");
|
||||
}
|
||||
|
||||
/// <summary>Forget one app from the machine-wide remembered-applications list and re-render both app
|
||||
/// lists. Backs the Delete key on the remembered list (issue #26) — the peers list has the same
|
||||
/// affordance. Internal so the self-test can drive the real removal path headlessly.</summary>
|
||||
internal void RemoveRememberedApplication(string processName)
|
||||
{
|
||||
var remaining = settings.LoadRememberedApplications()
|
||||
.Where(n => !string.Equals(n, processName, StringComparison.OrdinalIgnoreCase));
|
||||
settings.SaveRememberedApplications(remaining);
|
||||
logFile.Event($"ui: remembered application '{processName}' deleted from the remembered list");
|
||||
ReconcileSendAppsList();
|
||||
}
|
||||
|
||||
/// <summary>Test seam: reconcile now and return the two send-app lists' rows — the active list's
|
||||
/// process names, which of them are ticked, and the remembered list's process names. Lets the
|
||||
/// self-test pin the list semantics (ticked apps leave Remembered; a ticked app that isn't running
|
||||
@@ -8682,8 +8709,8 @@ public sealed class MainForm : Form
|
||||
{
|
||||
// No password → can't stream. Put the box back without re-firing this gate.
|
||||
suppressStreamingPasswordGate = true;
|
||||
box.Checked = false;
|
||||
suppressStreamingPasswordGate = false;
|
||||
try { box.Checked = false; }
|
||||
finally { suppressStreamingPasswordGate = false; } // a throw must not disable the gate for good
|
||||
return false;
|
||||
}
|
||||
currentProfilePassword = entered;
|
||||
|
||||
@@ -1957,8 +1957,17 @@ internal static class SelfTest
|
||||
"a ticked app must NOT appear in the Remembered list");
|
||||
Check(rememberedRows.Contains(unticked, StringComparer.OrdinalIgnoreCase),
|
||||
"an unticked remembered app must stay in the Remembered list");
|
||||
|
||||
// Delete on the Remembered list (issue #26): removing forgets the app machine-wide and
|
||||
// the list re-renders without it. Drives the real removal path the Delete key uses.
|
||||
mf.RemoveRememberedApplication(unticked);
|
||||
var (_, _, afterDelete) = mf.SnapshotAppListsForTest();
|
||||
Check(!afterDelete.Contains(unticked, StringComparer.OrdinalIgnoreCase),
|
||||
"a deleted remembered app must leave the Remembered list");
|
||||
Check(!store.LoadRememberedApplications().Contains(unticked, StringComparer.OrdinalIgnoreCase),
|
||||
"a deleted remembered app must leave the machine-wide remembered set");
|
||||
}
|
||||
return "ticked app: in Active (not running) + out of Remembered; unticked app: stays in Remembered";
|
||||
return "ticked app: in Active + out of Remembered; unticked stays; Delete forgets machine-wide";
|
||||
}
|
||||
finally { store.SaveRememberedApplications(original); }
|
||||
}
|
||||
|
||||
@@ -300,7 +300,12 @@ internal sealed class ServiceProfileDialog : Form
|
||||
var supported = ProcessLoopbackCapture.IsSupported;
|
||||
if (sendModeLabel is not null) sendModeLabel.Visible = supported;
|
||||
SetRowVisible(sendModeList, supported);
|
||||
if (!supported && sendModeList.SelectedIndex != 0) { suppressAppEvents = true; sendModeList.SelectedIndex = 0; suppressAppEvents = false; }
|
||||
if (!supported && sendModeList.SelectedIndex != 0)
|
||||
{
|
||||
suppressAppEvents = true;
|
||||
try { sendModeList.SelectedIndex = 0; }
|
||||
finally { suppressAppEvents = false; } // a throw must not leave events suppressed for good
|
||||
}
|
||||
|
||||
var appsMode = supported && sendModeList.SelectedIndex == 1;
|
||||
if (outputsLabel is not null) outputsLabel.Visible = !appsMode;
|
||||
@@ -338,23 +343,23 @@ internal sealed class ServiceProfileDialog : Form
|
||||
MaximizeBox = false,
|
||||
ShowInTaskbar = false,
|
||||
StartPosition = FormStartPosition.CenterParent,
|
||||
ClientSize = new Size(460, 200),
|
||||
ClientSize = new Size(460, 110),
|
||||
AccessibleName = "Additional service options",
|
||||
};
|
||||
var connect = new AccessibleCheckBox { Text = "Play a sound when a peer &connects (Alt+C)", AccessibleName = "Play connect sound", AutoSize = true, Checked = working.EnableConnectCue ?? true };
|
||||
var disconnect = new AccessibleCheckBox { Text = "Play a sound when a peer &disconnects (Alt+D)", AccessibleName = "Play disconnect sound", AutoSize = true, Checked = working.EnableDisconnectCue ?? true };
|
||||
// No connect/disconnect cue checkboxes here (removed 2026-07-19, review sweep): the dialog used
|
||||
// to offer them, but the headless service NEVER plays cues — nothing in the service host touches
|
||||
// CuePlayer, and a logged-out session couldn't render them anyway. Offering a switch that does
|
||||
// nothing is worse than not offering it. The cue fields stay on Profile for the app's own use.
|
||||
var logging = new AccessibleCheckBox { Text = "Enable service &logging (Alt+L)", AccessibleName = "Enable service logging", AutoSize = true, Checked = ServiceLoggingEnabled };
|
||||
var ok = new Button { Text = "&OK", AutoSize = true, DialogResult = DialogResult.OK };
|
||||
|
||||
var layout = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 1, Padding = new Padding(12), AutoSize = true };
|
||||
foreach (var c in new Control[] { connect, disconnect, logging, ok }) { var w = new FlowLayoutPanel { AutoSize = true, Dock = DockStyle.Fill }; w.Controls.Add(c); layout.Controls.Add(w); }
|
||||
foreach (var c in new Control[] { logging, ok }) { var w = new FlowLayoutPanel { AutoSize = true, Dock = DockStyle.Fill }; w.Controls.Add(c); layout.Controls.Add(w); }
|
||||
dlg.Controls.Add(layout);
|
||||
dlg.AcceptButton = ok;
|
||||
|
||||
if (ForegroundDialog.Show(owner => dlg.ShowDialog(owner)) == DialogResult.OK)
|
||||
{
|
||||
working.EnableConnectCue = connect.Checked;
|
||||
working.EnableDisconnectCue = disconnect.Checked;
|
||||
ServiceLoggingEnabled = logging.Checked;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user