Bump to v3.1.0: new audio cues (save, profile-switch), per-profile custom cue paths, redesigned tray menu
Two new cues join the existing connect / disconnect / record-start /
record-stop set:
* profile-save cue (sounds\save.wav by default) fires in
SaveProfileTo after a successful Save or Save As. Honours the
Profile.EnableSaveCue per-profile flag.
* profile-switch cue (sounds\profile.wav by default) fires in the
MainForm Shown handler after a profile finishes loading - covers
both startup-with-profile and mid-session profile switches.
Honours the Profile.EnableProfileSwitchCue flag. Because cue
loading runs AFTER settings.ApplyProfile in the ctor (line 542),
the profile being entered determines which sound plays - not the
one being left, exactly as Ed asked.
The audio cue UI in PreferencesDialog stays as a CheckedListBox (up /
down navigates, Space toggles) with TWO action buttons below that
operate on whichever cue is selected:
* Play [cue name] (Alt+P) - previews via SoundPlayer.Play on the
resolved path (custom override if set, default in sounds\
otherwise). Independent of the tick state.
* Browse for [cue name]... (Alt+B) - opens a WAV picker. If the
user picks a file inside RemSound's own sounds\ folder, it's
treated as "use default" and the override is cleared - avoids
pinning the user to a specific shipped default that a future
release might replace. Right-click "Use default sound" reverts.
Custom cue paths AND enable flags are per-profile. Lives on
Profile.CustomCuePaths (Dictionary<string,string>) and the per-cue
EnableXxxCue bool? properties. Cache mirror in
RemSoundSettingsStore.Settings.
All default WAVs moved from the install-root flat layout into a
sounds\ subfolder (csproj Content rules updated). save.wav and
profile.wav are bundled defaults.
Tray menu rewritten (MainFormTrayController) per Ed's spec:
* Show RemSound (W) - now uses Win32 SetForegroundWindow after the
standard Activate() because WinForms Activate is blocked by the
foreground-lock when invoked from a tray-menu click, which left
screen-reader users having to Alt+Tab to reach the restored
window.
* Enable sending (S) / Enable receiving (R) - tickable, reflect
current state, TOGGLE rather than always-on.
* Profiles (P) - submenu populated from AppConfig.RecentProfiles
with the same &1..&5 mnemonics the File menu uses. Pre-populated
once at construction so WinForms recognises it as a submenu and
fires DropDownOpening - originally I relied entirely on the
open event, which the framework skipped for items with no
DropDownItems, producing the "Profiles does nothing" bug.
* Exit (X).
Tray tooltip now built dynamically from snapshot tick (1 Hz):
"RemSound - [recording for MM:SS,] N peer(s), sending (lane),
receiving (lane)". Recording timer only included while
RecordingController.IsRecording is true (added
RecordingStartedUtc accessor for the elapsed calculation). Lane is
derived from which device-list ticks are active, not just the audio-
mode setting, so a BothIndependent user with only WASAPI inputs ticked
honestly reads as "sending (WASAPI)".
Fixes:
* Initial tooltip "RemSound" produced a "RemSound RemSound" read on
NVDA because the process name and tooltip matched. Set to
"RemSound - starting up" so the duplicate disappears.
* Recent profile menu items no longer carry a "Recent profile N:"
AccessibleName prefix in either the tray submenu or the File
menu's Recent profiles - now just the profile name. Number-key
mnemonics (&1..&5) untouched.
Manual (readme.html) updated: new section 17 "Audio cue sounds"
documents all six cues, the Play/Browse buttons, the right-click
"Use default sound", and the per-profile semantics. Sections 17-21
renumbered to 18-22. New "System tray icon and its menu" subsection
inside section 4 documents the redesigned right-click menu and the
hover tooltip. MANUAL.md regenerated via sync-manual.py. About box
gets a v3.1 block at the top. RELEASE_NOTES.md fully rewritten for
v3.1.
No wire format change - v3.1 talks to other v3.0.x machines exactly
as before.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
0189b12668
commit
70c2669d70
@@ -408,6 +408,60 @@ public sealed class RemSoundSettingsStore
|
||||
Save(s);
|
||||
}
|
||||
|
||||
public bool LoadEnableSaveCue() =>
|
||||
Try(() => Load()?.EnableSaveCue) ?? true;
|
||||
|
||||
public void SaveEnableSaveCue(bool value)
|
||||
{
|
||||
var s = Load() ?? new Settings();
|
||||
s.EnableSaveCue = value;
|
||||
Save(s);
|
||||
}
|
||||
|
||||
public bool LoadEnableProfileSwitchCue() =>
|
||||
Try(() => Load()?.EnableProfileSwitchCue) ?? true;
|
||||
|
||||
public void SaveEnableProfileSwitchCue(bool value)
|
||||
{
|
||||
var s = Load() ?? new Settings();
|
||||
s.EnableProfileSwitchCue = value;
|
||||
Save(s);
|
||||
}
|
||||
|
||||
/// <summary>The user's custom WAV path for a given cue, or null when they're using the
|
||||
/// default. Per-profile (lives on <see cref="Profile.CustomCuePaths"/>) so different
|
||||
/// profiles can carry different cue palettes.</summary>
|
||||
public string? LoadCustomCuePath(string cueId)
|
||||
{
|
||||
return Try(() =>
|
||||
{
|
||||
var s = Load();
|
||||
if (s?.CustomCuePaths is { } dict && dict.TryGetValue(cueId, out var path)
|
||||
&& !string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
return (string?)null;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Set a custom WAV path for a given cue. Pass null or empty to clear the
|
||||
/// override (the cue reverts to the bundled default in <c>sounds\</c>).</summary>
|
||||
public void SaveCustomCuePath(string cueId, string? path)
|
||||
{
|
||||
var s = Load() ?? new Settings();
|
||||
s.CustomCuePaths ??= new Dictionary<string, string>();
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
s.CustomCuePaths.Remove(cueId);
|
||||
}
|
||||
else
|
||||
{
|
||||
s.CustomCuePaths[cueId] = path;
|
||||
}
|
||||
Save(s);
|
||||
}
|
||||
|
||||
/// <summary>The whole recording-settings bag for the current profile. Loaded as a
|
||||
/// CLONE so callers can mutate the returned object without inadvertently writing
|
||||
/// back to the cache. Save flushes the object atomically.</summary>
|
||||
@@ -531,6 +585,12 @@ public sealed class RemSoundSettingsStore
|
||||
EnableDisconnectCue = profile.EnableDisconnectCue,
|
||||
EnableRecordStartCue = profile.EnableRecordStartCue,
|
||||
EnableRecordStopCue = profile.EnableRecordStopCue,
|
||||
EnableSaveCue = profile.EnableSaveCue,
|
||||
EnableProfileSwitchCue = profile.EnableProfileSwitchCue,
|
||||
// Defensive copy so cache mutations don't leak into the in-memory Profile graph
|
||||
// (and vice-versa). Profile is loaded once at startup; the cache evolves through
|
||||
// the session and is written back via CopyTo on save.
|
||||
CustomCuePaths = profile.CustomCuePaths is null ? new() : new Dictionary<string, string>(profile.CustomCuePaths),
|
||||
RecordingSettings = profile.RecordingSettings?.Clone() ?? new RecordingSettings(),
|
||||
};
|
||||
}
|
||||
@@ -581,6 +641,11 @@ public sealed class RemSoundSettingsStore
|
||||
profile.EnableDisconnectCue = s.EnableDisconnectCue;
|
||||
profile.EnableRecordStartCue = s.EnableRecordStartCue;
|
||||
profile.EnableRecordStopCue = s.EnableRecordStopCue;
|
||||
profile.EnableSaveCue = s.EnableSaveCue;
|
||||
profile.EnableProfileSwitchCue = s.EnableProfileSwitchCue;
|
||||
profile.CustomCuePaths = s.CustomCuePaths is null
|
||||
? new Dictionary<string, string>()
|
||||
: new Dictionary<string, string>(s.CustomCuePaths);
|
||||
if (s.RecordingSettings is RecordingSettings rs) profile.RecordingSettings = rs.Clone();
|
||||
}
|
||||
|
||||
@@ -646,6 +711,9 @@ public sealed class RemSoundSettingsStore
|
||||
public bool? EnableDisconnectCue { get; set; }
|
||||
public bool? EnableRecordStartCue { get; set; }
|
||||
public bool? EnableRecordStopCue { get; set; }
|
||||
public bool? EnableSaveCue { get; set; }
|
||||
public bool? EnableProfileSwitchCue { get; set; }
|
||||
public Dictionary<string, string>? CustomCuePaths { get; set; }
|
||||
public RecordingSettings? RecordingSettings { get; set; }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user