v4.1: two screen-reader fixes — silent start-in-tray, announce on restore
- Starting straight into the tray (StartMinimised / --minimized) no longer plays the "minimise" cue; only a genuine user minimise does (the startup path passes playCue:false) - Restoring the window from the tray now lands focus on a real named control on the active tab so NVDA announces it, instead of resting on the role-less QuietTabControl and surfacing silently - Shared the focus-a-leaf-for-announcement helper between the main window and Preferences Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a408d2b56e
commit
5abbeefe29
@@ -20,6 +20,17 @@ internal sealed class AboutDialog : Form
|
||||
/// updates" path.</summary>
|
||||
private const string ReleaseNotes =
|
||||
"""
|
||||
RemSound v4.1
|
||||
|
||||
A couple of small fixes for screen-reader users. When
|
||||
RemSound starts straight into the notification area, it no
|
||||
longer plays the "minimise" sound — starting in the tray
|
||||
isn't the same as you choosing to hide the window, so it
|
||||
shouldn't sound the cue. And when you bring the window back
|
||||
from the tray, RemSound now lands on a control on whichever
|
||||
tab you'd left open, so your screen reader announces the
|
||||
window instead of coming up silent.
|
||||
|
||||
RemSound v4.0
|
||||
|
||||
RemSound now has a sound for nearly everything you do.
|
||||
|
||||
@@ -27,6 +27,36 @@ internal static class WinEventNotifier
|
||||
NotifyWinEvent(EVENT_OBJECT_FOCUS, control.Handle, OBJID_CLIENT, CHILDID_SELF);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Land NVDA on a named control so a freshly-shown window or dialog announces itself
|
||||
/// instead of surfacing silently. Focuses the first focusable leaf inside <paramref name="page"/>
|
||||
/// (falling back to <paramref name="fallback"/>), forcing a genuine focus CHANGE (ActiveControl=null
|
||||
/// first) and re-firing the MSAA focus event. Used by the Preferences dialog on open and by the
|
||||
/// main window when it's restored from the tray. The page sits inside a <see cref="QuietTabControl"/>
|
||||
/// whose own accessible object is deliberately role-less and nameless, so focusing the tab control
|
||||
/// itself would be silent — this finds a real leaf (the first interactive control on whatever tab
|
||||
/// is showing) instead. Best run deferred (BeginInvoke), after the show/foreground settles.</summary>
|
||||
public static void AnnounceByFocusingLeaf(ContainerControl form, Control? page, Control fallback)
|
||||
{
|
||||
var leaf = FirstFocusableLeaf(page) ?? fallback;
|
||||
form.ActiveControl = null;
|
||||
leaf.Focus();
|
||||
if (leaf.IsHandleCreated) NotifyFocus(leaf);
|
||||
}
|
||||
|
||||
/// <summary>The first visible, enabled, tab-stop control inside <paramref name="container"/>,
|
||||
/// searched depth-first in child order (which matches the order controls were added). Returns a
|
||||
/// real leaf (button / list / combo / checkbox) — never a layout panel or the role-less tab strip.</summary>
|
||||
private static Control? FirstFocusableLeaf(Control? container)
|
||||
{
|
||||
if (container is null) return null;
|
||||
foreach (Control c in container.Controls)
|
||||
{
|
||||
if (c is { CanSelect: true, TabStop: true, Visible: true, Enabled: true }) return c;
|
||||
if (FirstFocusableLeaf(c) is { } nested) return nested;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -606,6 +606,16 @@ public sealed class MainForm : Form
|
||||
{
|
||||
if (InvokeRequired) { BeginInvoke(new Action(RestoreFromTray)); return; }
|
||||
trayController.Restore();
|
||||
// Land focus on a real, named control on whichever tab is showing, so NVDA announces
|
||||
// something when the window comes back from the tray. Without this, focus rests on the
|
||||
// QuietTabControl (deliberately role-less / nameless) and a screen reader has nothing to
|
||||
// read, so the window surfaces silently — the same issue the Preferences dialog had.
|
||||
// Deferred so it runs after the show/foreground settles.
|
||||
BeginInvoke(new Action(() =>
|
||||
{
|
||||
if (IsDisposed) return;
|
||||
WinEventNotifier.AnnounceByFocusingLeaf(this, mainTabControl.SelectedTab, mainTabControl);
|
||||
}));
|
||||
}
|
||||
catch { /* best-effort — surfacing the window is a convenience, not load-critical */ }
|
||||
}
|
||||
@@ -1265,7 +1275,9 @@ public sealed class MainForm : Form
|
||||
startNextInstanceMinimized = false;
|
||||
if (minimizeThisInstance)
|
||||
{
|
||||
BeginInvoke(() => trayController.Minimize());
|
||||
// playCue:false — starting up in the tray (StartMinimised / --minimized) isn't the
|
||||
// user choosing to minimise, so it must not sound the "minimise" cue.
|
||||
BeginInvoke(() => trayController.Minimize(playCue: false));
|
||||
}
|
||||
|
||||
// Kick off UPnP discovery if the user has the box ticked. Off by default; the
|
||||
|
||||
@@ -227,13 +227,17 @@ internal sealed class MainFormTrayController : IDisposable
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||
|
||||
public void Minimize()
|
||||
/// <summary>Hide the window to the tray. <paramref name="playCue"/> defaults true for a genuine
|
||||
/// user-initiated minimise; the startup-minimise path passes false because that's not the user
|
||||
/// choosing to hide the window — and the usual "was it visible?" guard can't tell the two apart,
|
||||
/// since by the time the deferred startup minimise runs the window HAS been shown (Visible=true).</summary>
|
||||
public void Minimize(bool playCue = true)
|
||||
{
|
||||
// Only sound the "hide" cue on a genuine shown -> hidden transition (not a startup-minimise
|
||||
// before the window has ever been shown, nor a repeat call once already hidden).
|
||||
// Only sound the "hide" cue on a genuine shown -> hidden transition the user asked for: not a
|
||||
// startup-minimise (playCue:false), and not a repeat call once already hidden (wasVisible).
|
||||
var wasVisible = owner.Visible;
|
||||
owner.Hide();
|
||||
if (wasVisible) (owner as MainForm)?.PlayWindowVisibilityCue(show: false);
|
||||
if (wasVisible && playCue) (owner as MainForm)?.PlayWindowVisibilityCue(show: false);
|
||||
// Refresh the tooltip BEFORE showing the icon so the shell's NIM_ADD call carries
|
||||
// the current live state (peer count, send / receive routing, recording timer),
|
||||
// not a stale "starting up" string set earlier. The shell tends to cache hover
|
||||
|
||||
@@ -731,29 +731,10 @@ internal sealed class PreferencesDialog : Form
|
||||
{
|
||||
if (IsDisposed) return;
|
||||
if (tabs.TabCount > 0) tabs.SelectedIndex = 0;
|
||||
var leaf = FirstTabStopLeaf(tabs.SelectedTab) ?? (Control)tabs;
|
||||
ActiveControl = null;
|
||||
leaf.Focus();
|
||||
if (leaf.IsHandleCreated) WinEventNotifier.NotifyFocus(leaf);
|
||||
WinEventNotifier.AnnounceByFocusingLeaf(this, tabs.SelectedTab, tabs);
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>The first visible, enabled, tab-stop control inside <paramref name="container"/>,
|
||||
/// searched depth-first in child order (which matches the order controls were added to each tab).
|
||||
/// Returns a real leaf the dialog can focus on open so NVDA has a named control to announce —
|
||||
/// never a layout panel or the role-less tab strip.</summary>
|
||||
private static Control? FirstTabStopLeaf(Control? container)
|
||||
{
|
||||
if (container is null) return null;
|
||||
foreach (Control c in container.Controls)
|
||||
{
|
||||
if (c is { CanSelect: true, TabStop: true, Visible: true, Enabled: true })
|
||||
return c;
|
||||
if (FirstTabStopLeaf(c) is { } nested) return nested;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Wire up the Startup behaviour tab (moved here from StartupBehaviourDialog): load the
|
||||
/// current state, populate the profile list, and persist each change immediately to AppConfig /
|
||||
/// the Windows auto-start registry entry, exactly as the old dialog did.</summary>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
tag_name on the latest GitHub release; bump it on every public release. The
|
||||
AssemblyVersion / FileVersion default to this value, and Assembly.GetName().Version
|
||||
is what the About dialog and the updater both read. -->
|
||||
<Version>4.0</Version>
|
||||
<Version>4.1</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user