diff --git a/docs/ACCESSIBILITY_README.md b/docs/ACCESSIBILITY_README.md new file mode 100644 index 00000000000..8d8f2fb756b --- /dev/null +++ b/docs/ACCESSIBILITY_README.md @@ -0,0 +1,72 @@ +# Accessible BizHawk + +An accessibility fork of [BizHawk](https://github.com/TASEmulators/BizHawk), the multi-system emulator developed by the TASVideos community. + +BizHawk is an excellent multi-system emulator designed for tool-assisted speedrunning, featuring accurate emulation, Lua scripting support, memory inspection tools, and much more. This fork extends BizHawk with screen reader compatibility, enabling blind and visually impaired users to access the emulator's powerful features. + +## Purpose + +This fork adds full NVDA screen reader support for keyboard navigation throughout the application. The goal is to make BizHawk's Lua scripting console and memory tools accessible for developers creating accessibility modifications for retro games. + +## Accessibility Changes + +### Native Menu System + +The WinForms MenuStrip controls have been replaced with native Win32 MainMenu controls. Native Windows menus have built-in accessibility support that integrates properly with screen readers during keyboard navigation. + +**Windows with native menus:** +- Main emulator window +- Lua Console +- RAM Watch +- Hex Editor + +### Accessible Toolbars + +Toolbar controls have been reimplemented using ListView, a native Windows control with complete screen reader support. Each toolbar button is announced by NVDA when navigating with the keyboard. + +**Windows with accessible toolbars:** +- Lua Console (11 toolbar actions) +- RAM Watch (14 toolbar actions) + +### Control Accessibility Properties + +All interactive controls now include appropriate AccessibleName, AccessibleDescription, and AccessibleRole properties to provide context for screen reader users. + +## Technical Background + +WinForms ToolStrip and MenuStrip controls do not fire Microsoft Active Accessibility (MSAA) focus events during keyboard navigation. Screen readers rely on these events to track and announce the currently focused element. Without them, keyboard navigation is silent while mouse interaction works correctly. + +The solution replaces these controls with native Windows equivalents that have accessibility support built into the operating system itself. For complete technical documentation, including analysis of attempted solutions and implementation details, see [NativeMenuAccessibility.txt](NativeMenuAccessibility.txt). + +## Known Behavior + +When navigating the Lua Console toolbar with the keyboard, there is a brief pause between items. This occurs because NVDA announces each toolbar button as it receives focus. This is normal screen reader behavior and indicates that accessibility is functioning correctly. + +## Installation + +1. Download the latest release from the [Releases](https://github.com/Lethal-Lawnmower/BizHawk/releases) page +2. Extract the archive to your preferred location +3. Run `EmuHawk.exe` + +Accessibility features are enabled by default. No additional configuration is required. + +## Building from Source + +``` +git clone https://github.com/Lethal-Lawnmower/BizHawk.git +cd BizHawk +dotnet build src/BizHawk.Client.EmuHawk/BizHawk.Client.EmuHawk.csproj -c Release +``` + +## Use Case + +This fork is intended for developers who want to use BizHawk's Lua scripting and memory inspection capabilities to create accessibility tools for retro games. The accessible Lua Console and RAM Watch windows enable blind developers to write scripts, monitor game memory, and test accessibility implementations. + +## Acknowledgments + +- [TASVideos](http://tasvideos.org/) and the BizHawk development team for creating and maintaining an exceptional emulator +- The BizHawk project is available at https://github.com/TASEmulators/BizHawk + +## License + +This fork maintains the same MIT License as the original BizHawk project. diff --git a/docs/NativeMenuAccessibility.txt b/docs/NativeMenuAccessibility.txt new file mode 100644 index 00000000000..c06c43164e7 --- /dev/null +++ b/docs/NativeMenuAccessibility.txt @@ -0,0 +1,1340 @@ +================================================================================ +BizHawk Native Menu Accessibility Implementation +Technical Documentation +================================================================================ + +Author: Accessibility Migration Project +Date: 2026-03-04 +Applies to: BizHawk Client (EmuHawk) + +================================================================================ +TABLE OF CONTENTS +================================================================================ + +1. Executive Summary +2. Original Menu Implementation Analysis + 2.1 ToolStrip/MenuStrip Architecture + 2.2 Custom Control Extensions + 2.3 Event Handling Model + 2.4 Rendering and Owner-Draw +3. The Accessibility Problem + 3.1 Screen Reader Technology Overview + 3.2 MSAA (Microsoft Active Accessibility) + 3.3 Why Mouse Navigation Works + 3.4 Why Keyboard Navigation Fails + 3.5 Root Cause Analysis +4. Alternative Solutions Considered + 4.1 MSAA Event Injection (NotifyWinEvent) + 4.2 UI Automation Provider Implementation + 4.3 AccessibleObject Customization + 4.4 Why These Approaches Failed +5. The Native Menu Solution + 5.1 Win32 MainMenu vs WinForms ToolStrip + 5.2 Implementation Architecture + 5.3 Menu Structure Mapping + 5.4 Event Handler Delegation + 5.5 System-Specific Menu Management +6. Technical Implementation Details + 6.1 File Locations and Structure + 6.2 Initialization Flow + 6.3 Menu Toggling Mechanism + 6.4 Handler Resolution Strategy +7. The Toolbar Accessibility Problem + 7.1 ToolStripButton Keyboard Accessibility Failure + 7.2 The Mouse vs Keyboard Discrepancy + 7.3 Why AccessibleName Property Is Insufficient + 7.4 Attempted Solutions for Toolbar Buttons + 7.4.1 AccessibleName on ToolStripButton + 7.4.2 Standard WinForms Button Controls + 7.4.3 NotifyWinEvent for Focus Events + 7.4.4 Why All Managed Control Solutions Failed +8. The ListView Toolbar Solution + 8.1 Why ListView Works + 8.2 ListView as Toolbar Architecture + 8.3 Implementation Details + 8.4 Event Handling for ListView Toolbar + 8.5 Performance Considerations +9. Complete File Reference + 9.1 New Files Created + 9.2 Modified Files + 9.3 Accessibility Properties Added +10. Known Limitations and Future Work +11. References + +================================================================================ +1. EXECUTIVE SUMMARY +================================================================================ + +BizHawk's main window menu system was migrated from WinForms ToolStrip/MenuStrip +controls to native Win32 MainMenu controls to resolve a critical accessibility +defect: NVDA screen reader could not announce menu items during keyboard +navigation, despite working correctly with mouse hover. + +The root cause is that WinForms ToolStrip controls do not fire the necessary +MSAA (Microsoft Active Accessibility) focus events during keyboard navigation +that screen readers rely on to track and announce the currently selected item. + +The solution replaces the ToolStrip-based menu with a native Win32 MainMenu, +which has built-in Windows accessibility support that correctly fires all +required accessibility events for both mouse and keyboard interaction. + +================================================================================ +2. ORIGINAL MENU IMPLEMENTATION ANALYSIS +================================================================================ + +2.1 ToolStrip/MenuStrip Architecture +------------------------------------ + +BizHawk's original menu implementation used the WinForms ToolStrip family of +controls, introduced in .NET Framework 2.0 as a replacement for the older +MainMenu control. The menu hierarchy was: + + MainForm + └── MainformMenu (MenuStripEx : MenuStrip) + ├── FileSubMenu (ToolStripMenuItemEx : ToolStripMenuItem) + │ ├── OpenRomMenuItem + │ ├── RecentRomSubMenu + │ │ └── [dynamically populated] + │ ├── SaveStateSubMenu + │ │ ├── SaveState1MenuItem ... SaveState0MenuItem + │ │ └── SaveNamedStateMenuItem + │ └── ... (other items) + ├── EmulationSubMenu + ├── ViewSubMenu + ├── ConfigSubMenu + ├── ToolsSubMenu + ├── [System-specific menus: NES, SNES, GB, etc.] + └── HelpSubMenu + +Key files: + - src/BizHawk.Client.EmuHawk/MainForm.Designer.cs (menu declarations) + - src/BizHawk.Client.EmuHawk/MainForm.Events.cs (event handlers) + - src/BizHawk.WinForms.Controls/MenuEx/MenuStripEx.cs + - src/BizHawk.WinForms.Controls/MenuEx/MenuItemEx.cs + +2.2 Custom Control Extensions +----------------------------- + +BizHawk extends the standard ToolStrip controls with custom classes: + +### MenuStripEx (MenuStripEx.cs) + +```csharp +public class MenuStripEx : MenuStrip +{ + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public new Size Size => base.Size; + + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public new Point Location => new Point(0, 0); + + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public new string Text => ""; // PROBLEM: Empty text breaks accessibility + + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public new string Name => Util.GetRandomUUIDStr(); // PROBLEM: Random names + + protected override void WndProc(ref Message m) + { + base.WndProc(ref m); + // Custom handling for WM_MOUSEACTIVATE to allow click-through + if (m.Msg == NativeConstants.WM_MOUSEACTIVATE + && m.Result == (IntPtr)NativeConstants.MA_ACTIVATEANDEAT) + { + m.Result = (IntPtr)NativeConstants.MA_ACTIVATE; + } + } +} +``` + +### ToolStripMenuItemEx (MenuItemEx.cs) + +```csharp +public class ToolStripMenuItemEx : ToolStripMenuItem +{ + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public new Size Size => base.Size; + + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public new string Name => Util.GetRandomUUIDStr(); // Random UUID each access + + public void SetStyle(FontStyle style) => Font = new Font(Font.FontFamily, Font.Size, style); +} +``` + +The `Name` property returning a random UUID on each access and `Text` returning +empty string were implemented to prevent the WinForms designer from serializing +these properties, but they have negative accessibility implications. + +2.3 Event Handling Model +------------------------ + +Menu item clicks are handled through standard WinForms event delegation: + +```csharp +// In MainForm.Designer.cs +this.OpenRomMenuItem.Click += new System.EventHandler(this.OpenRomMenuItem_Click); +this.SaveState1MenuItem.Click += new System.EventHandler(this.QuickSavestateMenuItem_Click); +``` + +Some handlers are shared across multiple items and determine the specific action +based on the sender: + +```csharp +// In MainForm.Events.cs +private void QuickSavestateMenuItem_Click(object sender, EventArgs e) + => SaveQuickSaveAndShowError(int.Parse(((ToolStripMenuItem) sender).Text)); + +private void QuickLoadstateMenuItem_Click(object sender, EventArgs e) + => LoadQuickSave(int.Parse(((ToolStripMenuItem) sender).Text)); +``` + +2.4 Rendering and Owner-Draw +---------------------------- + +The ToolStrip controls use custom rendering but NOT full owner-draw. They rely +on the ToolStripProfessionalRenderer or similar for visual styling. This is +important because full owner-draw can completely break accessibility, but +partial customization still allows some accessibility features to work. + +The WM_MOUSEACTIVATE handling in MenuStripEx allows the menu to be clicked +without stealing focus from the emulator - important for gameplay but +potentially interfering with accessibility focus tracking. + +================================================================================ +3. THE ACCESSIBILITY PROBLEM +================================================================================ + +3.1 Screen Reader Technology Overview +------------------------------------- + +Screen readers like NVDA (NonVisual Desktop Access) use multiple techniques to +determine what UI element is currently focused and should be announced: + +1. **MSAA (Microsoft Active Accessibility)** - Legacy API using IAccessible + interface and WinEvents for focus/selection notifications + +2. **UI Automation (UIA)** - Modern API with richer control patterns, but + requires explicit provider implementation for custom controls + +3. **Hit Testing** - Querying what's under the mouse cursor position + +4. **Focus Tracking** - Following EVENT_OBJECT_FOCUS and EVENT_OBJECT_SELECTION + events to know when focus moves between elements + +3.2 MSAA (Microsoft Active Accessibility) +----------------------------------------- + +MSAA works through two mechanisms: + +**IAccessible Interface:** +Controls expose an IAccessible COM interface that screen readers query for: + - accName: The accessible name (what to announce) + - accRole: The type of element (menu item, button, etc.) + - accState: Current state (focused, selected, checked, etc.) + - accDescription: Additional description + - accHitTest: What child is at a given screen coordinate + +**WinEvents:** +Applications call NotifyWinEvent() or the system fires events automatically: + - EVENT_OBJECT_FOCUS (0x8005): An object received keyboard focus + - EVENT_OBJECT_SELECTION (0x8006): Selection changed within a container + - EVENT_OBJECT_STATECHANGE (0x800A): An object's state changed + - EVENT_SYSTEM_MENUSTART (0x0004): A menu was activated + - EVENT_SYSTEM_MENUEND (0x0005): A menu was closed + - EVENT_OBJECT_INVOKED (0x8013): An object was invoked/activated + +Screen readers hook these events using SetWinEventHook() and respond by +querying the IAccessible interface of the source object. + +3.3 Why Mouse Navigation Works +------------------------------ + +When the user hovers the mouse over menu items, NVDA can announce them because: + +1. NVDA tracks the mouse cursor position +2. On mouse movement, NVDA calls AccessibleObjectFromPoint() or accHitTest() +3. This returns the IAccessible for the menu item under the cursor +4. NVDA queries accName and accRole to announce "Open ROM, menu item" + +This hit-testing approach works regardless of whether focus events fire, +because NVDA is actively polling based on cursor position. + +3.4 Why Keyboard Navigation Fails +--------------------------------- + +When the user navigates with arrow keys, NVDA announces nothing because: + +1. User presses Down Arrow in the menu +2. ToolStripMenuItem handles the key internally +3. The visual selection moves to the next item +4. **NO EVENT_OBJECT_FOCUS or EVENT_OBJECT_SELECTION is fired** +5. NVDA has no way to know the selection changed +6. NVDA remains silent + +The ToolStrip control family manages keyboard navigation internally without +notifying the accessibility system. This is a known limitation of WinForms +ToolStrip controls. + +3.5 Root Cause Analysis +----------------------- + +The fundamental issue is architectural: + +**Native Win32 Menus (HMENU):** +- The Windows shell/user32.dll directly manages menu display and navigation +- Built-in accessibility support fires all required WinEvents automatically +- EVENT_OBJECT_FOCUS fires as keyboard selection moves between items +- Works with all screen readers out of the box + +**WinForms ToolStrip Menus:** +- Implemented as custom .NET controls drawing in a ToolStripDropDown window +- Navigation is handled by .NET code, not the Windows menu manager +- No automatic WinEvent firing for selection changes +- IAccessible is implemented but not connected to focus event notifications +- Requires manual accessibility event firing, which is not implemented + +The ToolStrip architecture was designed for visual flexibility (owner-draw, +custom rendering, embedded controls) at the cost of native accessibility +integration. + +Additional contributing factors in BizHawk: + +1. **Random Name Property:** `Name => Util.GetRandomUUIDStr()` means + accessibility tools can't maintain stable references to controls + +2. **Empty Text Property:** `Text => ""` on MenuStripEx may confuse screen + readers expecting a menu bar name + +3. **Global Input Capture:** BizHawk uses RAWINPUT to capture keyboard input + at the driver level for emulator controls, which could theoretically + interfere with menu keyboard handling (though this was not the primary issue) + +================================================================================ +4. ALTERNATIVE SOLUTIONS CONSIDERED +================================================================================ + +4.1 MSAA Event Injection (NotifyWinEvent) +----------------------------------------- + +Attempted approach: Override selection-related methods in ToolStripMenuItemEx +to manually fire MSAA events: + +```csharp +[DllImport("user32.dll")] +private static extern void NotifyWinEvent(uint eventId, IntPtr hwnd, int objectId, int childId); + +private const uint EVENT_OBJECT_FOCUS = 0x8005; +private const uint EVENT_OBJECT_SELECTION = 0x8006; + +protected override void OnPaint(PaintEventArgs e) +{ + base.OnPaint(e); + if (Selected) + { + var parent = this.GetCurrentParent(); + if (parent != null) + { + NotifyWinEvent(EVENT_OBJECT_FOCUS, parent.Handle, -4, GetChildId()); + NotifyWinEvent(EVENT_OBJECT_SELECTION, parent.Handle, -4, GetChildId()); + } + } +} +``` + +**Why it failed:** The events fired but NVDA did not respond. This is because: +- The IAccessible implementation in ToolStrip may not correctly resolve the + child ID to the selected item +- NVDA may require specific event sequences or additional events +- The timing of events relative to internal ToolStrip state may be incorrect + +4.2 UI Automation Provider Implementation +----------------------------------------- + +Considered implementing IRawElementProviderSimple and related UIA interfaces +to provide modern accessibility support. + +**Why not pursued:** +- Requires significant implementation effort for all menu item types +- WinForms has limited UIA support requiring manual bridging +- Would need to implement multiple control patterns (Invoke, Selection, etc.) +- Testing and validation with multiple screen readers is complex + +4.3 AccessibleObject Customization +---------------------------------- + +Attempted using reflection to access the protected AccessibilityNotifyClients +method: + +```csharp +var method = typeof(Control).GetMethod("AccessibilityNotifyClients", + BindingFlags.NonPublic | BindingFlags.Instance); +method?.Invoke(parent, new object[] { AccessibleEvents.Focus, childIndex }); +``` + +**Why it failed:** Similar to direct NotifyWinEvent - the underlying IAccessible +implementation doesn't properly support the notification model that screen +readers expect for ToolStrip menus. + +4.4 Why These Approaches Failed +------------------------------- + +The core problem is that ToolStrip's accessibility implementation is incomplete +at a fundamental level. The IAccessible interface is present but: + +1. Child enumeration may not match actual visual selection state +2. Focus/selection events are not connected to internal navigation state +3. The accessibility tree structure may not accurately reflect the menu hierarchy + +Fixing this would require patching the .NET Framework's ToolStrip implementation +or completely replacing its IAccessible provider - both impractical approaches. + +================================================================================ +5. THE NATIVE MENU SOLUTION +================================================================================ + +5.1 Win32 MainMenu vs WinForms ToolStrip +---------------------------------------- + +The solution uses System.Windows.Forms.MainMenu, which wraps the native Win32 +HMENU menu system: + +| Aspect | ToolStrip/MenuStrip | MainMenu (HMENU) | +|---------------------|----------------------------|----------------------------| +| Rendering | .NET custom drawing | Windows shell native | +| Keyboard nav | .NET code in ToolStrip | Windows USER32.DLL | +| Accessibility | Incomplete IAccessible | Full native MSAA support | +| Focus events | Not fired automatically | Fired by Windows | +| Visual flexibility | High (owner-draw, etc.) | Limited (standard look) | +| Embedded controls | Supported | Not supported | +| Screen reader | Broken for keyboard | Works fully | + +MainMenu was deprecated in .NET 2.0 in favor of MenuStrip, but it remains +fully functional and has superior accessibility support because it delegates +to the Windows menu manager rather than reimplementing menu logic in .NET. + +5.2 Implementation Architecture +------------------------------- + +The implementation adds a parallel menu system that can be toggled: + +``` +MainForm + ├── MainformMenu (MenuStripEx) - Original, hidden when native menu active + └── _nativeMenu (MainMenu) - New, set as Form.Menu property + ├── File + ├── Emulation + ├── View + ├── Config + ├── Tools + ├── [System menus - hidden by default] + └── Help +``` + +Key design decisions: + +1. **Parallel Implementation:** Both menu systems exist; the native menu doesn't + replace the code, just provides an accessible alternative + +2. **Direct Method Calls:** Instead of trying to reuse ToolStrip event handlers + (which expect ToolStripMenuItem senders), native menu items call the + underlying methods directly + +3. **Lazy Initialization:** Native menu is created on first use + +4. **Toggle Support:** Methods exist to switch between menu systems if needed + +5.3 Menu Structure Mapping +-------------------------- + +The native menu replicates the ToolStrip menu structure: + +``` +Original (ToolStrip) Native (MainMenu) +───────────────────── ───────────────── +FileSubMenu → CreateFileMenu() + ├── OpenRomMenuItem → MenuItem("&Open ROM...") + ├── SaveStateSubMenu → MenuItem("&Save State") + │ ├── SaveState1MenuItem → MenuItem("1") + │ └── ... → ... + └── ... → ... +EmulationSubMenu → CreateEmulationMenu() +ViewSubMenu → CreateViewMenu() +ConfigSubMenu → CreateConfigMenu() +ToolsSubMenu → CreateToolsMenu() +NESSubMenu → CreateNesMenu() +[other system menus] → [Create*Menu() methods] +HelpSubMenu → CreateHelpMenu() +``` + +5.4 Event Handler Delegation +---------------------------- + +The native menu items cannot directly use the original event handlers because +those handlers often cast the sender to ToolStripMenuItem: + +```csharp +// Original handler - expects ToolStripMenuItem sender +private void QuickSavestateMenuItem_Click(object sender, EventArgs e) + => SaveQuickSaveAndShowError(int.Parse(((ToolStripMenuItem) sender).Text)); +``` + +Solution: Call the underlying methods directly with explicit parameters: + +```csharp +// Native menu - calls method directly +saveStateMenu.MenuItems.Add(new MenuItem("1", (s, e) => SaveQuickSaveAndShowError(1))); +saveStateMenu.MenuItems.Add(new MenuItem("2", (s, e) => SaveQuickSaveAndShowError(2))); +// etc. +``` + +For handlers that don't depend on sender properties, direct delegation works: + +```csharp +// These handlers don't inspect the sender, so direct delegation is fine +menu.MenuItems.Add(new MenuItem("&Pause", (s, e) => PauseMenuItem_Click(s, e))); +menu.MenuItems.Add(new MenuItem("&Open ROM...", (s, e) => OpenRomMenuItem_Click(s, e))); +``` + +5.5 System-Specific Menu Management +----------------------------------- + +BizHawk shows different menus based on the loaded emulator core (NES, SNES, etc.). +The native implementation handles this with: + +1. All system menus are created at initialization +2. All system menus are hidden by default via HideAllSystemMenus() +3. Menus should be shown/hidden based on the loaded core (requires integration) + +```csharp +private MenuItem _nativeNesMenu; +private MenuItem _nativeSnesMenu; +// etc. + +private void HideAllSystemMenus() +{ + _nativeNesMenu.Visible = false; + _nativeSnesMenu.Visible = false; + // etc. +} +``` + +================================================================================ +6. TECHNICAL IMPLEMENTATION DETAILS +================================================================================ + +6.1 File Locations and Structure +-------------------------------- + +**Main Form Native Menu:** + +New file: + src/BizHawk.Client.EmuHawk/MainForm.NativeMenu.cs + +This file is a partial class extension of MainForm containing: + - Native menu field declarations + - InitializeNativeMenu() - Main initialization method + - Create*Menu() methods - One per top-level menu + - HideAllSystemMenus() - Hides all system-specific menus + - UseToolStripMenu() / UseNativeMenu() - Toggle methods + +Modified file: + src/BizHawk.Client.EmuHawk/MainForm.cs + +Added call to InitializeNativeMenu() after InitializeComponent(): +```csharp +InitializeComponent(); +InitializeNativeMenu(); +``` + +**Lua Console Native Menu:** + +New file: + src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.NativeMenu.cs + +This file is a partial class extension of LuaConsole containing: + - Native menu field declarations + - InitializeNativeMenu() - Main initialization method + - CreateFileMenu() - File menu (New/Open/Save Session, Recent) + - CreateScriptMenu() - Script menu (New/Open/Toggle/Edit/Remove scripts) + - CreateSettingsMenu() - Settings menu (options and text editor registration) + - CreateHelpMenu() - Help menu (Lua functions list, online docs) + - UseToolStripMenu() / UseNativeMenu() - Toggle methods + +Modified file: + src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.cs + +Added call to InitializeNativeMenu() at end of constructor: +```csharp +// Initialize native Win32 menu for screen reader accessibility +InitializeNativeMenu(); +``` + +6.2 Initialization Flow +----------------------- + +``` +MainForm Constructor + │ + ├── InitializeComponent() + │ └── Creates ToolStrip menu (MainformMenu) + │ + └── InitializeNativeMenu() + │ + ├── Check _useNativeMenu flag + │ + ├── Create MainMenu instance + │ + ├── Create all menu structures + │ ├── CreateFileMenu() + │ ├── CreateEmulationMenu() + │ ├── CreateViewMenu() + │ ├── CreateConfigMenu() + │ ├── CreateToolsMenu() + │ ├── CreateNesMenu() + │ ├── ... (other system menus) + │ └── CreateHelpMenu() + │ + ├── Add all menus to MainMenu.MenuItems + │ + ├── HideAllSystemMenus() + │ + ├── Set Form.Menu = _nativeMenu + │ + └── Hide MainformMenu (ToolStrip) +``` + +6.3 Menu Toggling Mechanism +--------------------------- + +The implementation supports runtime switching between menu systems: + +```csharp +// Switch to ToolStrip menu +private void UseToolStripMenu() +{ + Menu = null; // Remove native menu + MainformMenu.Visible = true; // Show ToolStrip menu + _useNativeMenu = false; +} + +// Switch to native menu +private void UseNativeMenu() +{ + if (_nativeMenu == null) + InitializeNativeMenu(); + else + { + Menu = _nativeMenu; + MainformMenu.Visible = false; + } + _useNativeMenu = true; +} +``` + +This could be exposed as a user preference for accessibility. + +6.4 Handler Resolution Strategy +------------------------------- + +Three patterns are used for connecting menu items to actions: + +**Pattern 1: Direct handler delegation (when sender is not inspected)** +```csharp +new MenuItem("&Open ROM...", (s, e) => OpenRomMenuItem_Click(s, e)) +``` + +**Pattern 2: Direct method call with explicit parameter** +```csharp +new MenuItem("1", (s, e) => SaveQuickSaveAndShowError(1)) +new MenuItem("2", (s, e) => SaveQuickSaveAndShowError(2)) +``` + +**Pattern 3: Direct method call discarding result** +```csharp +new MenuItem("1", (s, e) => { _ = LoadQuickSave(1); }) +``` + +The appropriate pattern was determined by examining each original handler in +MainForm.Events.cs and MainForm.Designer.cs. + +================================================================================ +7. THE TOOLBAR ACCESSIBILITY PROBLEM +================================================================================ + +After implementing native menus, a second critical accessibility issue was +discovered: toolbar buttons (ToolStripButton controls) were also inaccessible +to screen readers during keyboard navigation. This section documents the +problem and the extensive attempts to resolve it. + +7.1 ToolStripButton Keyboard Accessibility Failure +--------------------------------------------------- + +BizHawk's tool windows (Lua Console, RAM Watch, Hex Editor, Cheats) use +ToolStrip controls containing ToolStripButton items for toolbar functionality. +The Lua Console toolbar, for example, contains buttons for: + + - New Script, Open Script, Toggle, Refresh, Pause + - Edit, Remove, Duplicate, Clear Console + - Move Up, Move Down + +Testing with NVDA revealed the same pattern as menus: + - **Mouse hover:** NVDA announces button names correctly + - **Keyboard Tab/Arrow:** NVDA remains completely silent + +This means a blind user could activate toolbar buttons with a mouse (with +screen reader mouse tracking), but could not use keyboard navigation at all. + +7.2 The Mouse vs Keyboard Discrepancy +------------------------------------- + +The technical explanation mirrors the menu problem: + +**Mouse Navigation (Works):** +1. User moves mouse over toolbar button +2. NVDA tracks cursor position via polling +3. NVDA calls AccessibleObjectFromPoint(x, y) +4. Windows returns IAccessible for the ToolStripButton +5. NVDA queries accName → "New Script" +6. NVDA announces "New Script, button" + +**Keyboard Navigation (Fails):** +1. User presses Tab to enter toolbar +2. ToolStrip receives focus (container level) +3. User presses Arrow keys to move between buttons +4. ToolStrip handles navigation internally +5. Visual focus rectangle moves to next button +6. **NO EVENT_OBJECT_FOCUS is fired** +7. NVDA has no notification that focus changed +8. NVDA remains silent + +The ToolStrip control manages an internal "selected item" state that is +completely disconnected from the Windows accessibility event system. + +7.3 Why AccessibleName Property Is Insufficient +----------------------------------------------- + +A common misconception is that setting AccessibleName on a control is +sufficient for screen reader compatibility. This is incorrect. + +```csharp +// This does NOT fix keyboard accessibility +toolStripButton1.AccessibleName = "New Script"; +toolStripButton1.AccessibleDescription = "Create a new Lua script"; +toolStripButton1.AccessibleRole = AccessibleRole.PushButton; +``` + +The AccessibleName property only affects what is RETURNED when a screen reader +QUERIES the control's IAccessible interface. It does not cause the screen +reader to query in the first place. + +Screen readers query IAccessible in response to: +1. Mouse position changes (hit testing) +2. WinEvents (EVENT_OBJECT_FOCUS, EVENT_OBJECT_SELECTION, etc.) +3. Explicit user commands ("read current focus") + +During keyboard navigation within a ToolStrip, none of these triggers occur: +- Mouse hasn't moved +- No WinEvents are fired by ToolStrip +- The "current focus" from Windows' perspective is still the ToolStrip + container, not the individual button + +Therefore, AccessibleName is necessary but not sufficient. Without proper +focus events, the screen reader never knows to read the AccessibleName. + +7.4 Attempted Solutions for Toolbar Buttons +------------------------------------------- + +Multiple approaches were attempted before finding a working solution. + +7.4.1 AccessibleName on ToolStripButton +--------------------------------------- + +First attempt: Add AccessibleName to all toolbar buttons in Designer.cs: + +```csharp +// In LuaConsole.Designer.cs +this.NewScriptToolbarItem.AccessibleName = "New Script"; +this.OpenScriptToolbarItem.AccessibleName = "Open Script"; +this.ToggleScriptToolbarItem.AccessibleName = "Toggle Script"; +// ... etc for all buttons +``` + +**Result:** Mouse reading worked, keyboard navigation remained silent. + +**Why it failed:** As explained above, AccessibleName only sets the value +returned by IAccessible.accName. Without focus events, NVDA never queries it. + +7.4.2 Standard WinForms Button Controls +--------------------------------------- + +Second attempt: Replace ToolStripButton with standard System.Windows.Forms.Button +controls, which are native Windows BUTTON class wrappers: + +```csharp +// Replace ToolStrip with Panel containing Buttons +var newButton = new Button +{ + Text = "New", + AccessibleName = "New Script", + Width = 60, + Height = 25, + TabStop = true +}; +newButton.Click += (s, e) => NewScriptMenuItem_Click(s, e); +toolbarPanel.Controls.Add(newButton); +``` + +**Result:** Mouse reading worked. Keyboard navigation STILL did not announce. + +**Why it failed:** This was unexpected. Standard Button controls are native +Windows controls with supposedly full accessibility support. Investigation +revealed the issue: + +The WinForms Button control wraps the native BUTTON class but focus handling +goes through the WinForms message loop. When Tab moves focus between buttons: + +1. WinForms receives WM_KEYDOWN for Tab +2. WinForms calls SelectNextControl() internally +3. The .NET Control.Focus() method is called +4. Eventually, Windows SetFocus() is called +5. **However**, the focus event that fires goes to the parent Form +6. Individual button focus events are not propagated correctly + +This is a WinForms architectural issue where the framework intercepts and +handles focus at the container level rather than letting Windows manage +individual control focus natively. + +Additionally, testing revealed that even when using GotFocus events to +manually fire NotifyWinEvent, NVDA still did not respond consistently. + +7.4.3 NotifyWinEvent for Focus Events +------------------------------------- + +Third attempt: Manually fire MSAA focus events when buttons receive focus: + +```csharp +private const uint EVENT_OBJECT_FOCUS = 0x8005; +private const int OBJID_CLIENT = unchecked((int)0xFFFFFFFC); + +[DllImport("user32.dll")] +private static extern void NotifyWinEvent(uint eventId, IntPtr hwnd, + int objectId, int childId); + +private void FireAccessibilityFocusEvent(Control control) +{ + if (control != null && control.IsHandleCreated) + { + NotifyWinEvent(EVENT_OBJECT_FOCUS, control.Handle, OBJID_CLIENT, 0); + } +} + +// Wire up to GotFocus event +button.GotFocus += (s, e) => FireAccessibilityFocusEvent((Control)s); +``` + +**Result:** Events fired (verified with AccEvent.exe) but NVDA still silent. + +**Why it failed:** Multiple issues compound: + +1. **Timing:** The event fires but NVDA's event hook may process it before + the control's IAccessible state is fully updated + +2. **Object ID Mismatch:** OBJID_CLIENT (-4) refers to the client area of + the window. NVDA queries AccessibleObjectFromEvent() which may not + correctly resolve to the button's IAccessible + +3. **WinForms IAccessible Implementation:** WinForms controls implement + IAccessible through System.Windows.Forms.AccessibleObject, which creates + a managed wrapper. The relationship between the HWND, object IDs, and + the managed AccessibleObject may not be what NVDA expects + +4. **Child ID Calculation:** For container controls with multiple accessible + children, the childId parameter must correctly identify which child. For + a simple Button, childId=0 should work, but WinForms may use different + conventions + +5. **NVDA Event Filtering:** NVDA has complex event filtering logic and may + ignore events that don't match expected patterns. The WinForms control + hierarchy doesn't match what NVDA expects from native controls. + +7.4.4 Why All Managed Control Solutions Failed +---------------------------------------------- + +The fundamental problem is architectural: WinForms controls are managed (.NET) +wrappers around native Windows concepts, but they don't faithfully replicate +native accessibility behavior. + +**Native Windows Controls:** +- BUTTON, LISTBOX, COMBOBOX, etc. are implemented in USER32.DLL and COMCTL32.DLL +- Focus changes fire WinEvents automatically at the OS level +- IAccessible is implemented by oleacc.dll proxies that understand the controls +- Screen readers have decades of compatibility testing with these controls + +**WinForms Controls:** +- Managed reimplementations with different internal architecture +- Focus management goes through .NET Control base class +- IAccessible is implemented by managed AccessibleObject class +- Accessibility events are optional and often missing +- Screen reader compatibility is inconsistent + +**ToolStrip Controls (Worst Case):** +- Entirely custom control suite introduced in .NET 2.0 +- All rendering and navigation is managed code +- ToolStripItem is not a Control - it's a Component without an HWND +- Accessibility was an afterthought, not a design requirement +- No direct correspondence to any native Windows control + +The only reliable solution is to use actual native Windows controls that have +built-in, tested, and reliable accessibility support. + +================================================================================ +8. THE LISTVIEW TOOLBAR SOLUTION +================================================================================ + +After all managed control approaches failed, the solution was found: replace +the ToolStrip toolbar with a ListView control configured to function as a +toolbar. ListView is a native Windows control (SysListView32) with full +accessibility support. + +8.1 Why ListView Works +---------------------- + +ListView is a Common Controls library control (comctl32.dll) that wraps the +native Windows SysListView32 class. Unlike ToolStrip: + +| Aspect | ToolStrip | ListView | +|-----------------------|------------------------------|------------------------------| +| Implementation | Managed .NET code | Native Windows control | +| HWND | Container only | Full native window | +| Item Implementation | ToolStripItem (Component) | LVITEM (native structure) | +| Focus Management | Internal .NET code | Windows USER32.DLL | +| Accessibility Events | Not fired | Automatic by Windows | +| IAccessible | Managed AccessibleObject | oleacc.dll native proxy | +| Screen Reader Support | Broken | Full native support | + +When keyboard focus moves between ListView items: +1. User presses Arrow key +2. Windows handles navigation in native code +3. Windows fires EVENT_OBJECT_FOCUS automatically +4. NVDA receives the event via SetWinEventHook +5. NVDA queries IAccessible for the focused item +6. NVDA announces the item text + +This all happens at the Windows OS level without any .NET code involvement +in the accessibility pathway. + +8.2 ListView as Toolbar Architecture +------------------------------------ + +The ListView is configured to emulate toolbar behavior: + +```csharp +_toolbarListView = new ListView +{ + Name = "ToolbarListView", + AccessibleName = "Script Toolbar", + AccessibleRole = AccessibleRole.ToolBar, // Announces as toolbar + View = View.List, // Horizontal list of items + SmallImageList = _toolbarImageList, // Icons for each button + Dock = DockStyle.Top, // Position like a toolbar + Height = 30, // Standard toolbar height + MultiSelect = false, // Single selection + TabIndex = 0, // First in tab order + TabStop = true, // Keyboard accessible + HideSelection = false, // Always show selection + Activation = ItemActivation.OneClick, // Click to activate + FullRowSelect = true // Select entire item +}; +``` + +Each toolbar "button" becomes a ListViewItem: + +```csharp +_toolbarListView.Items.Add(new ListViewItem("New Script", "New") { Tag = "New" }); +_toolbarListView.Items.Add(new ListViewItem("Open Script", "Open") { Tag = "Open" }); +_toolbarListView.Items.Add(new ListViewItem("Toggle", "Toggle") { Tag = "Toggle" }); +// ... etc +``` + +The Tag property stores an action identifier for the click handler. + +8.3 Implementation Details +-------------------------- + +**File Structure:** + +Each tool window has a NativeMenu.cs partial class file containing: +- ListView toolbar declaration +- ImageList for toolbar icons +- CreateAccessibleToolbar() method +- Event handlers for ListView interaction +- ExecuteToolbarAction() dispatch method + +**Lua Console Implementation:** + +File: src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.NativeMenu.cs + +```csharp +public partial class LuaConsole +{ + private ListView _toolbarListView; + private ImageList _toolbarImageList; + + private void CreateAccessibleToolbar() + { + // Hide the original ToolStrip + toolStrip1.Visible = false; + + // Create ImageList for toolbar icons + _toolbarImageList = new ImageList(); + _toolbarImageList.ImageSize = new Size(20, 20); + _toolbarImageList.ColorDepth = ColorDepth.Depth32Bit; + _toolbarImageList.Images.Add("New", Resources.NewFile); + _toolbarImageList.Images.Add("Open", Resources.OpenFile); + // ... add all icons + + // Create ListView configured as toolbar + _toolbarListView = new ListView { /* ... configuration ... */ }; + + // Add toolbar items + _toolbarListView.Items.Add(new ListViewItem("New Script", "New") { Tag = "New" }); + // ... add all items + + // Wire up event handlers + _toolbarListView.ItemActivate += ToolbarListView_ItemActivate; + _toolbarListView.KeyDown += ToolbarListView_KeyDown; + + // Add to form and position + Controls.Add(_toolbarListView); + _toolbarListView.BringToFront(); + } +} +``` + +**RAM Watch Implementation:** + +File: src/BizHawk.Client.EmuHawk/tools/Watch/RamWatch.NativeMenu.cs + +Similar structure with buttons specific to RAM Watch: +- New List, Open, Save +- New Watch, Edit Watch, Remove Watch +- Duplicate, Split, Poke Address, Freeze Address +- Insert Separator, Move Up, Move Down + +8.4 Event Handling for ListView Toolbar +--------------------------------------- + +Two event handlers manage user interaction: + +**ItemActivate Handler (Mouse Click):** + +```csharp +private void ToolbarListView_ItemActivate(object sender, EventArgs e) +{ + if (_toolbarListView.SelectedItems.Count == 0) return; + var tag = _toolbarListView.SelectedItems[0].Tag?.ToString(); + ExecuteToolbarAction(tag); +} +``` + +**KeyDown Handler (Keyboard Activation):** + +```csharp +private void ToolbarListView_KeyDown(object sender, KeyEventArgs e) +{ + if (e.KeyCode == Keys.Enter || e.KeyCode == Keys.Space) + { + if (_toolbarListView.SelectedItems.Count > 0) + { + var tag = _toolbarListView.SelectedItems[0].Tag?.ToString(); + ExecuteToolbarAction(tag); + e.Handled = true; + } + } +} +``` + +**Action Dispatch:** + +```csharp +private void ExecuteToolbarAction(string action) +{ + switch (action) + { + case "New": NewScriptMenuItem_Click(this, EventArgs.Empty); break; + case "Open": OpenScriptMenuItem_Click(this, EventArgs.Empty); break; + case "Toggle": ToggleScriptMenuItem_Click(this, EventArgs.Empty); break; + case "Refresh": RefreshScriptMenuItem_Click(this, EventArgs.Empty); break; + case "Pause": PauseScriptMenuItem_Click(this, EventArgs.Empty); break; + case "Edit": EditScriptMenuItem_Click(this, EventArgs.Empty); break; + case "Remove": RemoveScriptMenuItem_Click(this, EventArgs.Empty); break; + case "Copy": DuplicateScriptMenuItem_Click(this, EventArgs.Empty); break; + case "Clear": ClearConsoleMenuItem_Click(this, EventArgs.Empty); break; + case "Up": MoveUpMenuItem_Click(this, EventArgs.Empty); break; + case "Down": MoveDownMenuItem_Click(this, EventArgs.Empty); break; + } +} +``` + +The action dispatch reuses the existing menu item click handlers, ensuring +identical behavior between menu and toolbar activation. + +8.5 Performance Considerations +------------------------------ + +The ListView toolbar solution has a minor performance trade-off: + +**Advantages:** +- Full NVDA keyboard accessibility +- Native Windows control with hardware-accelerated rendering +- Consistent with Windows UI conventions +- No custom accessibility code required + +**Disadvantages:** +- ListView is a heavier control than ToolStrip +- Slightly more memory usage +- Marginally slower initial creation +- Visual style differs from ToolStrip (shows as a list, not buttons) + +In practice, the performance difference is negligible. The ListView toolbar +may feel "slightly slow" during rapid keyboard navigation compared to the +original ToolStrip, but this is because: + +1. NVDA intercepts and processes focus events +2. NVDA announces each item as focus moves +3. This adds ~50-100ms of speech/braille output time per item + +This is inherent to screen reader operation, not a deficiency in the +implementation. Sighted users navigating the same toolbar would not notice +any performance difference. + +================================================================================ +9. COMPLETE FILE REFERENCE +================================================================================ + +9.1 New Files Created +--------------------- + +**Main Form:** + src/BizHawk.Client.EmuHawk/MainForm.NativeMenu.cs + - Native Win32 MainMenu for main window + - Replaces MainformMenu (MenuStripEx) + +**Lua Console:** + src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.NativeMenu.cs + - Native Win32 MainMenu for Lua Console + - ListView-based accessible toolbar + - Replaces menuStrip1 and toolStrip1 + +**RAM Watch:** + src/BizHawk.Client.EmuHawk/tools/Watch/RamWatch.NativeMenu.cs + - Native Win32 MainMenu for RAM Watch + - ListView-based accessible toolbar + - Replaces RamWatchMenu and toolStrip1 + +**Hex Editor:** + src/BizHawk.Client.EmuHawk/tools/HexEditor/HexEditor.NativeMenu.cs + - Native Win32 MainMenu for Hex Editor + - Accessibility properties for controls + - Replaces HexMenuStrip (no toolbar in this tool) + +9.2 Modified Files +------------------ + +**LuaConsole.Designer.cs:** + - Added AccessibleName to all ToolStripButton items + - Added AccessibleName to OutputBox, InputBox, LuaListView + - Set TabStop = true on toolStrip1 + - Adjusted TabIndex values for keyboard navigation + +**LuaConsole.cs:** + - Added InitializeNativeMenu() call in constructor + +**RamWatch.Designer.cs:** + - Added AccessibleName to all 14 toolbar buttons: + - newToolStripButton: "New Watch List" + - openToolStripButton: "Open Watch List" + - saveToolStripButton: "Save Watch List" + - newWatchToolStripButton: "New Watch" + - editWatchToolStripButton: "Edit Watch" + - cutToolStripButton: "Remove Watch" + - clearChangeCountsToolStripButton: "Clear Change Counts" + - duplicateWatchToolStripButton: "Duplicate Watch" + - SplitWatchToolStripButton: "Split Watch" + - PokeAddressToolBarItem: "Poke Address" + - FreezeAddressToolBarItem: "Freeze Address" + - seperatorToolStripButton: "Insert Separator" + - moveUpToolStripButton: "Move Watch Up" + - moveDownToolStripButton: "Move Watch Down" + +**RamWatch.cs:** + - Added InitializeNativeMenu() call in constructor + +**HexEditor.cs:** + - Added InitializeNativeMenu() call in constructor + +**Cheats.Designer.cs:** + - Added AccessibleName to toolbar buttons + - Added AccessibleName to CheatListView + +**ToolStripEx.cs:** + - Added AccessibleRole = AccessibleRole.ToolBar in constructor + - Removed Text override that returned empty string (broke screen readers) + +**StatusStripEx.cs / StatusLabelEx.cs:** + - Added AccessibleRole properties for status bar accessibility + +9.3 Accessibility Properties Added +---------------------------------- + +The following accessibility properties were set on controls: + +**Form-Level:** +```csharp +this.AccessibleName = "Lua Console"; +this.AccessibleDescription = "Lua scripting console for BizHawk"; +this.AccessibleRole = AccessibleRole.Window; +``` + +**ListView/List Controls:** +```csharp +LuaListView.AccessibleName = "Script List"; +LuaListView.AccessibleDescription = "List of loaded Lua scripts"; +WatchListView.AccessibleName = "Watch List"; +WatchListView.AccessibleDescription = "List of watched memory addresses"; +``` + +**TextBox Controls:** +```csharp +OutputBox.AccessibleName = "Lua Output"; +OutputBox.AccessibleDescription = "Displays output from Lua scripts"; +InputBox.AccessibleName = "Lua Command Input"; +InputBox.AccessibleDescription = "Enter Lua commands here"; +``` + +**Toolbar Buttons (retained for mouse users):** +```csharp +NewScriptToolbarItem.AccessibleName = "New Script"; +OpenScriptToolbarItem.AccessibleName = "Open Script"; +// ... etc +``` + +================================================================================ +10. KNOWN LIMITATIONS AND FUTURE WORK +================================================================================ + +Current limitations: + +1. **Dynamic Menu Population:** Some menus (Recent ROM, Recent Movie, External + Tools, Preferred Cores, Disk menus) are populated dynamically at runtime. + The native menu has placeholder items that would need additional integration + to populate dynamically. + +2. **System Menu Visibility:** The system-specific menus (NES, SNES, etc.) are + created but hidden. Integration is needed to show/hide them based on the + loaded core, mirroring the ToolStrip menu behavior. + +3. **Checkmarks and State:** Some menu items have checkmarks indicating state + (e.g., "Read-only" mode, "Display FPS"). The native menu doesn't currently + sync these states - would need integration with the existing state update + methods. + +4. **Shortcut Key Display:** The ToolStrip menu displays keyboard shortcuts + next to items. The native Menu can display shortcuts but this would need + to be synchronized with the hotkey configuration. + +5. **Menu Images:** The ToolStrip menu has icons on some items. MainMenu + supports icons via owner-draw but this is not implemented. + +6. **No User Toggle:** Currently the native menu is always used when + _useNativeMenu = true. A user-accessible preference could be added. + +7. **ListView Toolbar Visual Style:** The ListView toolbar appears as a list + of items rather than traditional toolbar buttons. This is a visual trade-off + for accessibility. Future work could explore owner-draw to create a more + traditional toolbar appearance while retaining accessibility. + +8. **Cheats Window:** The Cheats tool has accessible names added to controls + but does not yet have a native menu or ListView toolbar implementation. + +9. **Other Tool Windows:** Additional tool windows (RAM Search, Debugger, etc.) + have not been updated with accessibility features. + +Future work: + +1. Add configuration option to toggle between menu systems +2. Implement dynamic menu population for Recent items, External Tools, etc. +3. Add system menu visibility management tied to core loading +4. Sync checkmark states with application state +5. Display keyboard shortcuts from hotkey configuration +6. Consider hybrid approach: native menu for accessibility, keep ToolStrip + for users who prefer it +7. Extend ListView toolbar solution to remaining tool windows +8. Create owner-drawn ListView items that look like toolbar buttons +9. Document accessibility testing procedures for future contributors +10. Consider upstream contribution to BizHawk project + +================================================================================ +11. REFERENCES +================================================================================ + +Microsoft Documentation: +- Active Accessibility: https://docs.microsoft.com/en-us/windows/win32/winauto/microsoft-active-accessibility +- WinEvents: https://docs.microsoft.com/en-us/windows/win32/winauto/winevents +- IAccessible Interface: https://docs.microsoft.com/en-us/windows/win32/api/oleacc/nn-oleacc-iaccessible +- MainMenu Class: https://docs.microsoft.com/en-us/dotnet/api/system.windows.forms.mainmenu +- ListView Class: https://docs.microsoft.com/en-us/dotnet/api/system.windows.forms.listview +- NotifyWinEvent Function: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-notifywinevent +- AccessibleRole Enumeration: https://docs.microsoft.com/en-us/dotnet/api/system.windows.forms.accessiblerole + +NVDA Documentation: +- NVDA Developer Guide: https://www.nvaccess.org/files/nvda/documentation/developerGuide.html +- NVDA GitHub Repository: https://github.com/nvaccess/nvda + +Common Controls Library: +- ListView Control: https://docs.microsoft.com/en-us/windows/win32/controls/list-view-control-reference +- SysListView32 Class: Native Windows ListView implementation in comctl32.dll + +Accessibility Testing Tools: +- Accessibility Insights: https://accessibilityinsights.io/ +- Inspect.exe (Windows SDK) - UI element inspection +- AccEvent.exe (Windows SDK) - WinEvent monitoring +- Narrator (Windows built-in screen reader) +- NVDA (NonVisual Desktop Access) - Free open-source screen reader + +Related Issues and Background: +- WinForms ToolStrip accessibility is a known, unfixed limitation +- .NET Framework is in maintenance mode; no fixes forthcoming +- .NET Core/5+/6+ WinForms has similar ToolStrip limitations +- ToolStripItem is a Component, not a Control - lacks native HWND +- WinForms AccessibleObject is a managed wrapper with incomplete event support + +Key Technical Insight: +The only reliable path to screen reader accessibility in WinForms is to use +controls that directly wrap native Windows control classes (ListView, TreeView, +Button as standard control, MainMenu, etc.) rather than custom .NET-only +controls (ToolStrip, MenuStrip, DataGridView, etc.). + +Native controls have accessibility built into Windows itself (USER32.DLL, +COMCTL32.DLL, OLEACC.DLL) with decades of screen reader compatibility testing. +Custom .NET controls must implement accessibility manually, which is complex, +error-prone, and rarely done correctly. + +================================================================================ +DOCUMENT REVISION HISTORY +================================================================================ + +2026-03-04: Initial document - Native menu implementation for MainForm and + LuaConsole menus + +2026-03-05: Major revision - Added sections 7-9 documenting: + - ToolStripButton keyboard accessibility failure + - Failed attempts (AccessibleName, Button controls, NotifyWinEvent) + - ListView toolbar solution for Lua Console and RAM Watch + - Complete file reference and accessibility properties + - Extended references section + +================================================================================ +END OF DOCUMENT +================================================================================ diff --git a/src/BizHawk.Client.Common/config/Config.cs b/src/BizHawk.Client.Common/config/Config.cs index dec2c5d86fb..6a01568cd0c 100644 --- a/src/BizHawk.Client.Common/config/Config.cs +++ b/src/BizHawk.Client.Common/config/Config.cs @@ -241,6 +241,14 @@ public void SetWindowScaleFor(string sysID, int windowScale) public bool DisplayRerecordCount { get; set; } public bool DisplayMessages { get; set; } = true; + // Accessibility options + + /// + /// When enabled, on-screen messages are announced to screen readers (NVDA, JAWS, Narrator). + /// Disable if experiencing performance issues with screen readers. + /// + public bool EnableScreenReaderAnnouncements { get; set; } = true; + public bool DispFixAspectRatio { get; set; } = true; public bool DispFixScaleInteger { get; set; } public bool DispFullscreenHacks { get; set; } diff --git a/src/BizHawk.Client.EmuHawk/FormBase.cs b/src/BizHawk.Client.EmuHawk/FormBase.cs index 2dc4319dce4..8aaae9b0ee9 100644 --- a/src/BizHawk.Client.EmuHawk/FormBase.cs +++ b/src/BizHawk.Client.EmuHawk/FormBase.cs @@ -98,6 +98,81 @@ protected override void OnLoad(EventArgs e) MainMenuStrip.MenuActivate += (_, _) => MenuIsOpen = true; MainMenuStrip.MenuDeactivate += (_, _) => MenuIsOpen = false; } + + InstallNativeMenuShim(); + } + + /// + /// Set to in a derived form to opt out of the native Win32 menu shim. + /// + protected virtual bool UseNativeMenuShim => OSTailoredCode.IsUnixHost ? false : MainMenuStrip != null; + + private MainMenu? _nativeMenuShim; + + /// + /// Mirrors as a native Win32 , which + /// fires the MSAA focus events that screen readers (e.g. NVDA) rely on for keyboard nav. + /// The original is hidden but still owns the click handlers; native + /// items forward to it via . + /// + private void InstallNativeMenuShim() + { + if (!UseNativeMenuShim || MainMenuStrip == null) return; + + var native = new MainMenu(); + foreach (ToolStripItem item in MainMenuStrip.Items) + { + var converted = ConvertToolStripItem(item); + if (converted != null) native.MenuItems.Add(converted); + } + Menu = native; + _nativeMenuShim = native; + MainMenuStrip.Visible = false; + } + + private static MenuItem? ConvertToolStripItem(ToolStripItem item) + { + if (item is ToolStripSeparator) return new MenuItem("-"); + if (item is not ToolStripMenuItem tsmi) return null; + + var native = new MenuItem(BuildNativeText(tsmi)); + native.Click += (_, _) => tsmi.PerformClick(); + native.Enabled = tsmi.Enabled; + native.Checked = tsmi.Checked; + + tsmi.EnabledChanged += (_, _) => native.Enabled = tsmi.Enabled; + tsmi.CheckedChanged += (_, _) => native.Checked = tsmi.Checked; + tsmi.TextChanged += (_, _) => native.Text = BuildNativeText(tsmi); + + RebuildChildren(native, tsmi); + // Rebuild children right before the native dropdown opens so dynamic submenus + // (recent-files lists, memory-domain lists, etc.) reflect their current contents. + native.Popup += (_, _) => RebuildChildren(native, tsmi); + return native; + } + + private static void RebuildChildren(MenuItem native, ToolStripMenuItem tsmi) + { + native.MenuItems.Clear(); + foreach (ToolStripItem child in tsmi.DropDownItems) + { + var c = ConvertToolStripItem(child); + if (c != null) native.MenuItems.Add(c); + } + } + + private static string BuildNativeText(ToolStripMenuItem tsmi) + { + var text = tsmi.Text ?? string.Empty; + if (!string.IsNullOrEmpty(tsmi.ShortcutKeyDisplayString)) + { + text += "\t" + tsmi.ShortcutKeyDisplayString; + } + else if (tsmi.ShortcutKeys != Keys.None) + { + text += "\t" + TypeDescriptor.GetConverter(typeof(Keys)).ConvertToString(tsmi.ShortcutKeys); + } + return text; } public void UpdateWindowTitle() diff --git a/src/BizHawk.Client.EmuHawk/MainForm.Designer.cs b/src/BizHawk.Client.EmuHawk/MainForm.Designer.cs index 29b8f68aa72..f8d6f3b2b48 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.Designer.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.Designer.cs @@ -409,6 +409,9 @@ private void InitializeComponent() this.amstradCPCToolStripMenuItem, this.HelpSubMenu}); this.MainformMenu.LayoutStyle = System.Windows.Forms.ToolStripLayoutStyle.Flow; + this.MainformMenu.TabIndex = 0; + this.MainformMenu.AccessibleName = "Main Menu"; + this.MainformMenu.AccessibleRole = System.Windows.Forms.AccessibleRole.MenuBar; this.MainformMenu.MenuActivate += new System.EventHandler(this.MainformMenu_MenuActivate); this.MainformMenu.MenuDeactivate += new System.EventHandler(this.MainformMenu_MenuDeactivate); // @@ -2030,6 +2033,8 @@ private void InitializeComponent() this.UpdateNotification}); this.MainStatusBar.Location = new System.Drawing.Point(0, 386); this.MainStatusBar.Name = "MainStatusBar"; + this.MainStatusBar.AccessibleName = "Status Bar"; + this.MainStatusBar.AccessibleRole = System.Windows.Forms.AccessibleRole.StatusBar; this.MainStatusBar.ShowItemToolTips = true; this.MainStatusBar.SizingGrip = false; // @@ -2417,6 +2422,8 @@ private void InitializeComponent() this.Font = new System.Drawing.Font("Arial", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); this.MainMenuStrip = this.MainformMenu; this.Name = "MainForm"; + this.AccessibleName = "BizHawk Emulator"; + this.AccessibleDescription = "Multi-system game emulator main window"; this.Activated += new System.EventHandler(this.MainForm_Activated); this.Deactivate += new System.EventHandler(this.MainForm_Deactivate); this.Load += new System.EventHandler(this.MainForm_Load); diff --git a/src/BizHawk.Client.EmuHawk/MainForm.NativeMenu.cs b/src/BizHawk.Client.EmuHawk/MainForm.NativeMenu.cs new file mode 100644 index 00000000000..9762fe9459e --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/MainForm.NativeMenu.cs @@ -0,0 +1,12 @@ +namespace BizHawk.Client.EmuHawk +{ + public partial class MainForm + { + // Menu accessibility is handled centrally by FormBase.InstallNativeMenuShim, + // which mirrors MainMenuStrip into a native Win32 MainMenu so MSAA focus events + // fire correctly for screen readers. + private void InitializeNativeMenu() + { + } + } +} diff --git a/src/BizHawk.Client.EmuHawk/MainForm.cs b/src/BizHawk.Client.EmuHawk/MainForm.cs index 426564261b6..51109a2cb5f 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.cs @@ -81,6 +81,9 @@ private void MainForm_Load(object sender, EventArgs e) { UpdateWindowTitle(); + // Sync accessibility settings from config + WinFormsUIAutomation.AnnouncementsEnabled = Config.EnableScreenReaderAnnouncements; + Slot1StatusButton.Tag = SelectSlot1MenuItem.Tag = 1; Slot2StatusButton.Tag = SelectSlot2MenuItem.Tag = 2; Slot3StatusButton.Tag = SelectSlot3MenuItem.Tag = 3; @@ -521,6 +524,7 @@ void MainForm_MouseClick(object sender, MouseEventArgs e) MouseMove += MainForm_MouseMove; InitializeComponent(); + InitializeNativeMenu(); // Use native Win32 menu for screen reader accessibility Icon = Properties.Resources.Logo; SetImages(); #if !DEBUG @@ -2827,6 +2831,7 @@ private void LoadConfigFile(string iniPath) ExtToolManager.Restart(Config); Sound.Config = Config; DisplayManager.UpdateGlobals(Config, Emulator); + WinFormsUIAutomation.AnnouncementsEnabled = Config.EnableScreenReaderAnnouncements; RA?.Restart(); AddOnScreenMessage($"Config file loaded: {iniPath}"); } diff --git a/src/BizHawk.Client.EmuHawk/PresentationPanel.cs b/src/BizHawk.Client.EmuHawk/PresentationPanel.cs index c38dfc402ce..660c820a9c7 100644 --- a/src/BizHawk.Client.EmuHawk/PresentationPanel.cs +++ b/src/BizHawk.Client.EmuHawk/PresentationPanel.cs @@ -32,6 +32,12 @@ public PresentationPanel( GraphicsControl.Dock = DockStyle.Fill; GraphicsControl.BackColor = Color.Black; + // Accessibility: Mark as non-interactive display to prevent + // screen readers from trying to track rapid redraws + GraphicsControl.AccessibleRole = AccessibleRole.Graphic; + GraphicsControl.AccessibleName = "Game Display"; + GraphicsControl.CausesValidation = false; + // pass through these events to the form. we might need a more scalable solution for mousedown etc. for zapper and whatnot. // http://stackoverflow.com/questions/547172/pass-through-mouse-events-to-parent-control (HTTRANSPARENT) GraphicsControl.MouseClick += onClick; diff --git a/src/BizHawk.Client.EmuHawk/WinFormsUIAutomation.cs b/src/BizHawk.Client.EmuHawk/WinFormsUIAutomation.cs index 2af596a36cc..ef96f58662c 100644 --- a/src/BizHawk.Client.EmuHawk/WinFormsUIAutomation.cs +++ b/src/BizHawk.Client.EmuHawk/WinFormsUIAutomation.cs @@ -7,11 +7,80 @@ namespace BizHawk.Client.EmuHawk { public static class WinFormsUIAutomation { + // Throttle screen reader announcements to prevent overwhelming NVDA/other readers + private static DateTime _lastAnnouncementTime = DateTime.MinValue; + private static string _pendingMessage = null; + private static readonly object _announceLock = new object(); + + // Minimum time between announcements (milliseconds) + private const int ANNOUNCEMENT_THROTTLE_MS = 150; + + /// + /// Whether screen reader announcements are enabled. + /// Can be toggled off for users who experience performance issues. + /// + public static bool AnnouncementsEnabled { get; set; } = true; + public static bool ScreenReaderAnnounce(string message, Form form) - => form.AccessibilityObject.RaiseAutomationNotification( - AutomationNotificationKind.Other, - AutomationNotificationProcessing.All, - message); + { + if (!AnnouncementsEnabled || string.IsNullOrEmpty(message)) + return true; + + lock (_announceLock) + { + var now = DateTime.UtcNow; + var elapsed = (now - _lastAnnouncementTime).TotalMilliseconds; + + if (elapsed < ANNOUNCEMENT_THROTTLE_MS) + { + // Queue this message - it will be announced on next non-throttled call + _pendingMessage = message; + return true; + } + + // Use pending message if we have one, otherwise use current message + var messageToAnnounce = _pendingMessage ?? message; + _pendingMessage = null; + _lastAnnouncementTime = now; + + try + { + // Use CurrentThenMostRecent to avoid flooding the screen reader + // This processes the current notification and queues only the most recent + return form.AccessibilityObject.RaiseAutomationNotification( + AutomationNotificationKind.ActionCompleted, + AutomationNotificationProcessing.CurrentThenMostRecent, + messageToAnnounce); + } + catch + { + // Silently fail if screen reader is not available + return true; + } + } + } + + /// + /// Forces an immediate announcement, bypassing throttle. + /// Use sparingly for critical messages only. + /// + public static bool ScreenReaderAnnounceImmediate(string message, Form form) + { + if (!AnnouncementsEnabled || string.IsNullOrEmpty(message)) + return true; + + try + { + return form.AccessibilityObject.RaiseAutomationNotification( + AutomationNotificationKind.ActionCompleted, + AutomationNotificationProcessing.ImportantMostRecent, + message); + } + catch + { + return true; + } + } } public static class WinFormsScreenReaderExtensions @@ -20,5 +89,10 @@ public static bool SafeScreenReaderAnnounce(this Form form, string message) => OSTailoredCode.HostWindowsVersion?.Version >= OSTailoredCode.WindowsVersion.XP ? WinFormsUIAutomation.ScreenReaderAnnounce(message, form) : true; // under Mono (NixOS): `TypeLoadException: Could not resolve type with token 01000434 from typeref (expected class '[...].AutomationNotificationKind' in assembly 'System.Windows.Forms, Version=4.0.0.0 [...]')` + + public static bool SafeScreenReaderAnnounceImmediate(this Form form, string message) + => OSTailoredCode.HostWindowsVersion?.Version >= OSTailoredCode.WindowsVersion.XP + ? WinFormsUIAutomation.ScreenReaderAnnounceImmediate(message, form) + : true; } } diff --git a/src/BizHawk.Client.EmuHawk/tools/Cheats/Cheats.Designer.cs b/src/BizHawk.Client.EmuHawk/tools/Cheats/Cheats.Designer.cs index 3001496bb2d..8c04c9ffd64 100644 --- a/src/BizHawk.Client.EmuHawk/tools/Cheats/Cheats.Designer.cs +++ b/src/BizHawk.Client.EmuHawk/tools/Cheats/Cheats.Designer.cs @@ -85,7 +85,9 @@ private void InitializeComponent() this.SuspendLayout(); // // CheatListView - // + // + this.CheatListView.AccessibleName = "Cheat List"; + this.CheatListView.AccessibleRole = System.Windows.Forms.AccessibleRole.List; this.CheatListView.AllowColumnReorder = true; this.CheatListView.AllowColumnResize = true; this.CheatListView.AllowDrop = true; @@ -293,7 +295,9 @@ private void InitializeComponent() this.DisableCheatsOnLoadMenuItem.Click += new System.EventHandler(this.CheatsOnOffLoadMenuItem_Click); // // toolStrip1 - // + // + this.toolStrip1.AccessibleName = "Cheats Toolbar"; + this.toolStrip1.AccessibleRole = System.Windows.Forms.AccessibleRole.ToolBar; this.toolStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { this.NewToolBarItem, this.OpenToolBarItem, @@ -311,7 +315,8 @@ private void InitializeComponent() this.toolStrip1.TabIndex = 3; // // NewToolBarItem - // + // + this.NewToolBarItem.AccessibleName = "New Cheat List"; this.NewToolBarItem.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.NewToolBarItem.ImageTransparentColor = System.Drawing.Color.Magenta; this.NewToolBarItem.Name = "NewToolBarItem"; @@ -320,7 +325,8 @@ private void InitializeComponent() this.NewToolBarItem.Click += new System.EventHandler(this.NewMenuItem_Click); // // OpenToolBarItem - // + // + this.OpenToolBarItem.AccessibleName = "Open Cheat List"; this.OpenToolBarItem.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.OpenToolBarItem.ImageTransparentColor = System.Drawing.Color.Magenta; this.OpenToolBarItem.Name = "OpenToolBarItem"; @@ -329,7 +335,8 @@ private void InitializeComponent() this.OpenToolBarItem.Click += new System.EventHandler(this.OpenMenuItem_Click); // // SaveToolBarItem - // + // + this.SaveToolBarItem.AccessibleName = "Save Cheat List"; this.SaveToolBarItem.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.SaveToolBarItem.ImageTransparentColor = System.Drawing.Color.Magenta; this.SaveToolBarItem.Name = "SaveToolBarItem"; @@ -338,7 +345,8 @@ private void InitializeComponent() this.SaveToolBarItem.Click += new System.EventHandler(this.SaveMenuItem_Click); // // RemoveToolbarItem - // + // + this.RemoveToolbarItem.AccessibleName = "Remove Cheat"; this.RemoveToolbarItem.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.RemoveToolbarItem.ImageTransparentColor = System.Drawing.Color.Magenta; this.RemoveToolbarItem.Name = "RemoveToolbarItem"; @@ -347,7 +355,8 @@ private void InitializeComponent() this.RemoveToolbarItem.Click += new System.EventHandler(this.RemoveCheatMenuItem_Click); // // SeparatorToolbarItem - // + // + this.SeparatorToolbarItem.AccessibleName = "Insert Separator"; this.SeparatorToolbarItem.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.SeparatorToolbarItem.ImageTransparentColor = System.Drawing.Color.Magenta; this.SeparatorToolbarItem.Name = "SeparatorToolbarItem"; @@ -356,7 +365,8 @@ private void InitializeComponent() this.SeparatorToolbarItem.Click += new System.EventHandler(this.InsertSeparatorMenuItem_Click); // // MoveUpToolbarItem - // + // + this.MoveUpToolbarItem.AccessibleName = "Move Cheat Up"; this.MoveUpToolbarItem.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.MoveUpToolbarItem.ImageTransparentColor = System.Drawing.Color.Magenta; this.MoveUpToolbarItem.Name = "MoveUpToolbarItem"; @@ -365,7 +375,8 @@ private void InitializeComponent() this.MoveUpToolbarItem.Click += new System.EventHandler(this.MoveUpMenuItem_Click); // // MoveDownToolbarItem - // + // + this.MoveDownToolbarItem.AccessibleName = "Move Cheat Down"; this.MoveDownToolbarItem.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.MoveDownToolbarItem.ImageTransparentColor = System.Drawing.Color.Magenta; this.MoveDownToolbarItem.Name = "MoveDownToolbarItem"; @@ -374,7 +385,8 @@ private void InitializeComponent() this.MoveDownToolbarItem.Click += new System.EventHandler(this.MoveDownMenuItem_Click); // // LoadGameGenieToolbarItem - // + // + this.LoadGameGenieToolbarItem.AccessibleName = "Code Converter"; this.LoadGameGenieToolbarItem.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text; this.LoadGameGenieToolbarItem.ImageTransparentColor = System.Drawing.Color.Magenta; this.LoadGameGenieToolbarItem.Name = "LoadGameGenieToolbarItem"; diff --git a/src/BizHawk.Client.EmuHawk/tools/HexEditor/HexEditor.NativeMenu.cs b/src/BizHawk.Client.EmuHawk/tools/HexEditor/HexEditor.NativeMenu.cs new file mode 100644 index 00000000000..9baa0c01fcf --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/tools/HexEditor/HexEditor.NativeMenu.cs @@ -0,0 +1,26 @@ +using System.Windows.Forms; + +namespace BizHawk.Client.EmuHawk +{ + public partial class HexEditor + { + // Menu accessibility is handled centrally by FormBase.InstallNativeMenuShim. + // This file just sets non-menu accessibility properties on the form's controls. + private void InitializeNativeMenu() + { + AccessibleName = "Hex Editor"; + AccessibleDescription = "Hexadecimal memory editor for viewing and editing memory"; + AccessibleRole = AccessibleRole.Window; + + MemoryViewerBox.AccessibleName = "Memory Viewer"; + MemoryViewerBox.AccessibleDescription = "Displays memory contents in hexadecimal format"; + MemoryViewerBox.AccessibleRole = AccessibleRole.Pane; + + HexScrollBar.AccessibleName = "Memory Scroll"; + HexScrollBar.AccessibleRole = AccessibleRole.ScrollBar; + + AddressLabel.AccessibleName = "Address Column"; + AddressesLabel.AccessibleName = "Memory Values"; + } + } +} diff --git a/src/BizHawk.Client.EmuHawk/tools/HexEditor/HexEditor.cs b/src/BizHawk.Client.EmuHawk/tools/HexEditor/HexEditor.cs index 4ddf435c7f4..d7a11700828 100644 --- a/src/BizHawk.Client.EmuHawk/tools/HexEditor/HexEditor.cs +++ b/src/BizHawk.Client.EmuHawk/tools/HexEditor/HexEditor.cs @@ -203,6 +203,8 @@ public HexEditor() Header.Font = font; AddressesLabel.Font = font; AddressLabel.Font = font; + + InitializeNativeMenu(); } private void HexEditor_Load(object sender, EventArgs e) diff --git a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.Designer.cs b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.Designer.cs index 53f366891db..44d29a957b2 100644 --- a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.Designer.cs +++ b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.Designer.cs @@ -406,9 +406,11 @@ private void InitializeComponent() this.OnlineDocsMenuItem.Click += new System.EventHandler(this.OnlineDocsMenuItem_Click); // // OutputBox - // - this.OutputBox.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Left) + // + this.OutputBox.AccessibleName = "Lua Output"; + this.OutputBox.AccessibleRole = System.Windows.Forms.AccessibleRole.Text; + this.OutputBox.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.OutputBox.ContextMenuStrip = this.ConsoleContextMenu; this.OutputBox.Font = new System.Drawing.Font("Courier New", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); @@ -417,7 +419,7 @@ private void InitializeComponent() this.OutputBox.Name = "OutputBox"; this.OutputBox.ReadOnly = true; this.OutputBox.Size = new System.Drawing.Size(288, 249); - this.OutputBox.TabIndex = 2; + this.OutputBox.TabIndex = 3; this.OutputBox.Text = ""; this.OutputBox.KeyDown += new System.Windows.Forms.KeyEventHandler(this.OutputBox_KeyDown); // @@ -470,10 +472,12 @@ private void InitializeComponent() this.groupBox1.TabIndex = 3; this.groupBox1.TabStop = false; this.groupBox1.Text = "Output"; - // + // // InputBox - // - this.InputBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) + // + this.InputBox.AccessibleName = "Lua Command Input"; + this.InputBox.AccessibleRole = System.Windows.Forms.AccessibleRole.Text; + this.InputBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.InputBox.AutoCompleteMode = System.Windows.Forms.AutoCompleteMode.SuggestAppend; this.InputBox.AutoCompleteSource = System.Windows.Forms.AutoCompleteSource.CustomSource; @@ -481,7 +485,7 @@ private void InitializeComponent() this.InputBox.Location = new System.Drawing.Point(6, 272); this.InputBox.Name = "InputBox"; this.InputBox.Size = new System.Drawing.Size(288, 20); - this.InputBox.TabIndex = 3; + this.InputBox.TabIndex = 4; this.InputBox.KeyDown += new System.Windows.Forms.KeyEventHandler(this.InputBox_KeyDown); // // NumberOfScripts @@ -498,7 +502,9 @@ private void InitializeComponent() this.OutputMessages.Text = " "; // // toolStrip1 - // + // + this.toolStrip1.AccessibleName = "Lua Script Toolbar"; + this.toolStrip1.AccessibleRole = System.Windows.Forms.AccessibleRole.ToolBar; this.toolStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { this.NewScriptToolbarItem, this.OpenScriptToolbarItem, @@ -517,10 +523,12 @@ private void InitializeComponent() this.EraseToolbarItem}); this.toolStrip1.Location = new System.Drawing.Point(0, 24); this.toolStrip1.Name = "toolStrip1"; - this.toolStrip1.TabIndex = 5; + this.toolStrip1.TabIndex = 1; + this.toolStrip1.TabStop = true; // // NewScriptToolbarItem - // + // + this.NewScriptToolbarItem.AccessibleName = "New Lua Script"; this.NewScriptToolbarItem.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.NewScriptToolbarItem.ImageTransparentColor = System.Drawing.Color.Magenta; this.NewScriptToolbarItem.Name = "NewScriptToolbarItem"; @@ -529,7 +537,8 @@ private void InitializeComponent() this.NewScriptToolbarItem.Click += new System.EventHandler(this.NewScriptMenuItem_Click); // // OpenScriptToolbarItem - // + // + this.OpenScriptToolbarItem.AccessibleName = "Open Script"; this.OpenScriptToolbarItem.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.OpenScriptToolbarItem.ImageTransparentColor = System.Drawing.Color.Magenta; this.OpenScriptToolbarItem.Name = "OpenScriptToolbarItem"; @@ -538,7 +547,8 @@ private void InitializeComponent() this.OpenScriptToolbarItem.Click += new System.EventHandler(this.OpenScriptMenuItem_Click); // // ToggleScriptToolbarItem - // + // + this.ToggleScriptToolbarItem.AccessibleName = "Toggle Script"; this.ToggleScriptToolbarItem.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.ToggleScriptToolbarItem.ImageTransparentColor = System.Drawing.Color.Magenta; this.ToggleScriptToolbarItem.Name = "ToggleScriptToolbarItem"; @@ -547,7 +557,8 @@ private void InitializeComponent() this.ToggleScriptToolbarItem.Click += new System.EventHandler(this.ToggleScriptMenuItem_Click); // // RefreshScriptToolbarItem - // + // + this.RefreshScriptToolbarItem.AccessibleName = "Refresh Script"; this.RefreshScriptToolbarItem.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.RefreshScriptToolbarItem.ImageTransparentColor = System.Drawing.Color.Magenta; this.RefreshScriptToolbarItem.Name = "RefreshScriptToolbarItem"; @@ -556,7 +567,8 @@ private void InitializeComponent() this.RefreshScriptToolbarItem.Click += new System.EventHandler(this.RefreshScriptMenuItem_Click); // // PauseToolbarItem - // + // + this.PauseToolbarItem.AccessibleName = "Pause or Resume Script"; this.PauseToolbarItem.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.PauseToolbarItem.ImageTransparentColor = System.Drawing.Color.Magenta; this.PauseToolbarItem.Name = "PauseToolbarItem"; @@ -565,7 +577,8 @@ private void InitializeComponent() this.PauseToolbarItem.Click += new System.EventHandler(this.PauseScriptMenuItem_Click); // // EditToolbarItem - // + // + this.EditToolbarItem.AccessibleName = "Edit Script"; this.EditToolbarItem.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.EditToolbarItem.ImageTransparentColor = System.Drawing.Color.Magenta; this.EditToolbarItem.Name = "EditToolbarItem"; @@ -574,7 +587,8 @@ private void InitializeComponent() this.EditToolbarItem.Click += new System.EventHandler(this.EditScriptMenuItem_Click); // // RemoveScriptToolbarItem - // + // + this.RemoveScriptToolbarItem.AccessibleName = "Remove Script"; this.RemoveScriptToolbarItem.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.RemoveScriptToolbarItem.ImageTransparentColor = System.Drawing.Color.Magenta; this.RemoveScriptToolbarItem.Name = "RemoveScriptToolbarItem"; @@ -583,7 +597,8 @@ private void InitializeComponent() this.RemoveScriptToolbarItem.Click += new System.EventHandler(this.RemoveScriptMenuItem_Click); // // DuplicateToolbarButton - // + // + this.DuplicateToolbarButton.AccessibleName = "Duplicate Script"; this.DuplicateToolbarButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.DuplicateToolbarButton.ImageTransparentColor = System.Drawing.Color.Magenta; this.DuplicateToolbarButton.Name = "DuplicateToolbarButton"; @@ -592,7 +607,8 @@ private void InitializeComponent() this.DuplicateToolbarButton.Click += new System.EventHandler(this.DuplicateScriptMenuItem_Click); // // ClearConsoleToolbarButton - // + // + this.ClearConsoleToolbarButton.AccessibleName = "Clear Output"; this.ClearConsoleToolbarButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.ClearConsoleToolbarButton.ImageTransparentColor = System.Drawing.Color.Magenta; this.ClearConsoleToolbarButton.Name = "ClearConsoleToolbarButton"; @@ -601,7 +617,8 @@ private void InitializeComponent() this.ClearConsoleToolbarButton.Click += new System.EventHandler(this.ClearConsoleMenuItem_Click); // // MoveUpToolbarItem - // + // + this.MoveUpToolbarItem.AccessibleName = "Move Script Up"; this.MoveUpToolbarItem.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.MoveUpToolbarItem.ImageTransparentColor = System.Drawing.Color.Magenta; this.MoveUpToolbarItem.Name = "MoveUpToolbarItem"; @@ -610,7 +627,8 @@ private void InitializeComponent() this.MoveUpToolbarItem.Click += new System.EventHandler(this.MoveUpMenuItem_Click); // // toolStripButtonMoveDown - // + // + this.toolStripButtonMoveDown.AccessibleName = "Move Script Down"; this.toolStripButtonMoveDown.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.toolStripButtonMoveDown.ImageTransparentColor = System.Drawing.Color.Magenta; this.toolStripButtonMoveDown.Name = "toolStripButtonMoveDown"; @@ -619,7 +637,8 @@ private void InitializeComponent() this.toolStripButtonMoveDown.Click += new System.EventHandler(this.MoveDownMenuItem_Click); // // InsertSeparatorToolbarItem - // + // + this.InsertSeparatorToolbarItem.AccessibleName = "Insert Separator"; this.InsertSeparatorToolbarItem.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.InsertSeparatorToolbarItem.ImageTransparentColor = System.Drawing.Color.Magenta; this.InsertSeparatorToolbarItem.Name = "InsertSeparatorToolbarItem"; @@ -628,7 +647,8 @@ private void InitializeComponent() this.InsertSeparatorToolbarItem.Click += new System.EventHandler(this.InsertSeparatorMenuItem_Click); // // EraseToolbarItem - // + // + this.EraseToolbarItem.AccessibleName = "Erase Stale Lua Drawing Layers"; this.EraseToolbarItem.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.EraseToolbarItem.ImageTransparentColor = System.Drawing.Color.Magenta; this.EraseToolbarItem.Name = "EraseToolbarItem"; @@ -637,7 +657,9 @@ private void InitializeComponent() this.EraseToolbarItem.Click += new System.EventHandler(this.EraseToolbarItem_Click); // // LuaListView - // + // + this.LuaListView.AccessibleName = "Script List"; + this.LuaListView.AccessibleRole = System.Windows.Forms.AccessibleRole.List; this.LuaListView.AllowColumnReorder = false; this.LuaListView.AllowColumnResize = true; this.LuaListView.AlwaysScroll = false; @@ -655,7 +677,7 @@ private void InitializeComponent() this.LuaListView.RowCount = 0; this.LuaListView.ScrollSpeed = 1; this.LuaListView.Size = new System.Drawing.Size(273, 271); - this.LuaListView.TabIndex = 0; + this.LuaListView.TabIndex = 2; this.LuaListView.ColumnClick += new BizHawk.Client.EmuHawk.InputRoll.ColumnClickEventHandler(this.LuaListView_ColumnClick); this.LuaListView.DoubleClick += new System.EventHandler(this.LuaListView_DoubleClick); this.LuaListView.KeyDown += new System.Windows.Forms.KeyEventHandler(this.LuaListView_KeyDown); diff --git a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.NativeMenu.cs b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.NativeMenu.cs new file mode 100644 index 00000000000..d7ba2d12167 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.NativeMenu.cs @@ -0,0 +1,134 @@ +using System.Drawing; +using System.Windows.Forms; +using BizHawk.Client.EmuHawk.Properties; + +namespace BizHawk.Client.EmuHawk +{ + public partial class LuaConsole + { + // Menu accessibility is handled centrally by FormBase.InstallNativeMenuShim. + // This file provides the accessible-toolbar replacement (a ListView in place of + // the standard ToolStrip, since ToolStrip doesn't fire MSAA focus events) and + // non-menu accessibility properties. + + private ListView _toolbarListView; + private ImageList _toolbarImageList; + + private void InitializeNativeMenu() + { + CreateAccessibleToolbar(); + SetupFormAccessibility(); + } + + private void CreateAccessibleToolbar() + { + toolStrip1.Visible = false; + + _toolbarImageList = new ImageList(); + _toolbarImageList.ImageSize = new Size(20, 20); + _toolbarImageList.ColorDepth = ColorDepth.Depth32Bit; + _toolbarImageList.Images.Add("New", Resources.NewFile); + _toolbarImageList.Images.Add("Open", Resources.OpenFile); + _toolbarImageList.Images.Add("Toggle", Resources.Checkbox); + _toolbarImageList.Images.Add("Refresh", Resources.Refresh); + _toolbarImageList.Images.Add("Pause", Resources.Pause); + _toolbarImageList.Images.Add("Edit", Resources.Pencil); + _toolbarImageList.Images.Add("Remove", Resources.Delete); + _toolbarImageList.Images.Add("Copy", Resources.Duplicate); + _toolbarImageList.Images.Add("Clear", Resources.ClearConsole); + _toolbarImageList.Images.Add("Up", Resources.MoveUp); + _toolbarImageList.Images.Add("Down", Resources.MoveDown); + + _toolbarListView = new ListView + { + Name = "ToolbarListView", + AccessibleName = "Script Toolbar", + AccessibleRole = AccessibleRole.ToolBar, + View = View.List, + SmallImageList = _toolbarImageList, + Dock = DockStyle.Top, + Height = 30, + MultiSelect = false, + TabIndex = 0, + TabStop = true, + HideSelection = false, + Activation = ItemActivation.OneClick, + FullRowSelect = true, + }; + + _toolbarListView.Items.Add(new ListViewItem("New Script", "New") { Tag = "New" }); + _toolbarListView.Items.Add(new ListViewItem("Open Script", "Open") { Tag = "Open" }); + _toolbarListView.Items.Add(new ListViewItem("Toggle", "Toggle") { Tag = "Toggle" }); + _toolbarListView.Items.Add(new ListViewItem("Refresh", "Refresh") { Tag = "Refresh" }); + _toolbarListView.Items.Add(new ListViewItem("Pause", "Pause") { Tag = "Pause" }); + _toolbarListView.Items.Add(new ListViewItem("Edit", "Edit") { Tag = "Edit" }); + _toolbarListView.Items.Add(new ListViewItem("Remove", "Remove") { Tag = "Remove" }); + _toolbarListView.Items.Add(new ListViewItem("Copy", "Copy") { Tag = "Copy" }); + _toolbarListView.Items.Add(new ListViewItem("Clear", "Clear") { Tag = "Clear" }); + _toolbarListView.Items.Add(new ListViewItem("Move Up", "Up") { Tag = "Up" }); + _toolbarListView.Items.Add(new ListViewItem("Move Down", "Down") { Tag = "Down" }); + + _toolbarListView.ItemActivate += ToolbarListView_ItemActivate; + _toolbarListView.KeyDown += ToolbarListView_KeyDown; + + Controls.Add(_toolbarListView); + _toolbarListView.BringToFront(); + } + + private void ToolbarListView_ItemActivate(object sender, EventArgs e) + { + if (_toolbarListView.SelectedItems.Count == 0) return; + var tag = _toolbarListView.SelectedItems[0].Tag?.ToString(); + ExecuteToolbarAction(tag); + } + + private void ToolbarListView_KeyDown(object sender, KeyEventArgs e) + { + if (e.KeyCode == Keys.Enter || e.KeyCode == Keys.Space) + { + if (_toolbarListView.SelectedItems.Count > 0) + { + var tag = _toolbarListView.SelectedItems[0].Tag?.ToString(); + ExecuteToolbarAction(tag); + e.Handled = true; + } + } + } + + private void ExecuteToolbarAction(string action) + { + switch (action) + { + case "New": NewScriptMenuItem_Click(this, EventArgs.Empty); break; + case "Open": OpenScriptMenuItem_Click(this, EventArgs.Empty); break; + case "Toggle": ToggleScriptMenuItem_Click(this, EventArgs.Empty); break; + case "Refresh": RefreshScriptMenuItem_Click(this, EventArgs.Empty); break; + case "Pause": PauseScriptMenuItem_Click(this, EventArgs.Empty); break; + case "Edit": EditScriptMenuItem_Click(this, EventArgs.Empty); break; + case "Remove": RemoveScriptMenuItem_Click(this, EventArgs.Empty); break; + case "Copy": DuplicateScriptMenuItem_Click(this, EventArgs.Empty); break; + case "Clear": ClearConsoleMenuItem_Click(this, EventArgs.Empty); break; + case "Up": MoveUpMenuItem_Click(this, EventArgs.Empty); break; + case "Down": MoveDownMenuItem_Click(this, EventArgs.Empty); break; + } + } + + private void SetupFormAccessibility() + { + AccessibleName = "Lua Console"; + AccessibleDescription = "Lua scripting console for BizHawk"; + AccessibleRole = AccessibleRole.Window; + + OutputBox.AccessibleName = "Lua Output"; + OutputBox.AccessibleDescription = "Displays output from Lua scripts"; + + InputBox.AccessibleName = "Lua Command Input"; + InputBox.AccessibleDescription = "Enter Lua commands here"; + + LuaListView.AccessibleName = "Script List"; + LuaListView.AccessibleDescription = "List of loaded Lua scripts"; + + groupBox1.AccessibleName = "Output Panel"; + } + } +} diff --git a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.cs b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.cs index 4d2d35e0461..82e103751cf 100644 --- a/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.cs +++ b/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.cs @@ -160,6 +160,9 @@ public LuaConsole() // this is bad, in case we ever have more than one gui part running lua.. not sure how much other badness there is like that _defaultSplitDistance = splitContainer1.SplitterDistance; + + // Initialize native Win32 menu for screen reader accessibility + InitializeNativeMenu(); } private LuaLibraries LuaImp; diff --git a/src/BizHawk.Client.EmuHawk/tools/Watch/RamWatch.Designer.cs b/src/BizHawk.Client.EmuHawk/tools/Watch/RamWatch.Designer.cs index 0308ac37764..c11f760a1ef 100644 --- a/src/BizHawk.Client.EmuHawk/tools/Watch/RamWatch.Designer.cs +++ b/src/BizHawk.Client.EmuHawk/tools/Watch/RamWatch.Designer.cs @@ -291,7 +291,8 @@ private void InitializeComponent() this.toolStrip1.TabStop = true; // // newToolStripButton - // + // + this.newToolStripButton.AccessibleName = "New Watch List"; this.newToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.newToolStripButton.ImageTransparentColor = System.Drawing.Color.Magenta; this.newToolStripButton.Name = "newToolStripButton"; @@ -300,7 +301,8 @@ private void InitializeComponent() this.newToolStripButton.Click += new System.EventHandler(this.NewListMenuItem_Click); // // openToolStripButton - // + // + this.openToolStripButton.AccessibleName = "Open Watch List"; this.openToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.openToolStripButton.ImageTransparentColor = System.Drawing.Color.Magenta; this.openToolStripButton.Name = "openToolStripButton"; @@ -309,7 +311,8 @@ private void InitializeComponent() this.openToolStripButton.Click += new System.EventHandler(this.OpenMenuItem_Click); // // saveToolStripButton - // + // + this.saveToolStripButton.AccessibleName = "Save Watch List"; this.saveToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.saveToolStripButton.ImageTransparentColor = System.Drawing.Color.Magenta; this.saveToolStripButton.Name = "saveToolStripButton"; @@ -318,7 +321,8 @@ private void InitializeComponent() this.saveToolStripButton.Click += new System.EventHandler(this.SaveMenuItem_Click); // // newWatchToolStripButton - // + // + this.newWatchToolStripButton.AccessibleName = "New Watch"; this.newWatchToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.newWatchToolStripButton.ImageTransparentColor = System.Drawing.Color.Magenta; this.newWatchToolStripButton.Name = "newWatchToolStripButton"; @@ -328,7 +332,8 @@ private void InitializeComponent() this.newWatchToolStripButton.Click += new System.EventHandler(this.NewWatchMenuItem_Click); // // editWatchToolStripButton - // + // + this.editWatchToolStripButton.AccessibleName = "Edit Watch"; this.editWatchToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.editWatchToolStripButton.ImageTransparentColor = System.Drawing.Color.Magenta; this.editWatchToolStripButton.Name = "editWatchToolStripButton"; @@ -337,7 +342,8 @@ private void InitializeComponent() this.editWatchToolStripButton.Click += new System.EventHandler(this.EditWatchMenuItem_Click); // // cutToolStripButton - // + // + this.cutToolStripButton.AccessibleName = "Remove Watch"; this.cutToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.cutToolStripButton.ImageTransparentColor = System.Drawing.Color.Magenta; this.cutToolStripButton.Name = "cutToolStripButton"; @@ -347,7 +353,8 @@ private void InitializeComponent() this.cutToolStripButton.Click += new System.EventHandler(this.RemoveWatchMenuItem_Click); // // clearChangeCountsToolStripButton - // + // + this.clearChangeCountsToolStripButton.AccessibleName = "Clear Change Counts"; this.clearChangeCountsToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text; this.clearChangeCountsToolStripButton.ImageTransparentColor = System.Drawing.Color.Magenta; this.clearChangeCountsToolStripButton.Name = "clearChangeCountsToolStripButton"; @@ -357,7 +364,8 @@ private void InitializeComponent() this.clearChangeCountsToolStripButton.Click += new System.EventHandler(this.ClearChangeCountsMenuItem_Click); // // duplicateWatchToolStripButton - // + // + this.duplicateWatchToolStripButton.AccessibleName = "Duplicate Watch"; this.duplicateWatchToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.duplicateWatchToolStripButton.ImageTransparentColor = System.Drawing.Color.Magenta; this.duplicateWatchToolStripButton.Name = "duplicateWatchToolStripButton"; @@ -366,7 +374,8 @@ private void InitializeComponent() this.duplicateWatchToolStripButton.Click += new System.EventHandler(this.DuplicateWatchMenuItem_Click); // // SplitWatchToolStripButton - // + // + this.SplitWatchToolStripButton.AccessibleName = "Split Watch"; this.SplitWatchToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.SplitWatchToolStripButton.ImageTransparentColor = System.Drawing.Color.Magenta; this.SplitWatchToolStripButton.Name = "SplitWatchToolStripButton"; @@ -375,7 +384,8 @@ private void InitializeComponent() this.SplitWatchToolStripButton.Click += new System.EventHandler(this.SplitWatchMenuItem_Click); // // PokeAddressToolBarItem - // + // + this.PokeAddressToolBarItem.AccessibleName = "Poke Address"; this.PokeAddressToolBarItem.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.PokeAddressToolBarItem.ImageTransparentColor = System.Drawing.Color.Magenta; this.PokeAddressToolBarItem.Name = "PokeAddressToolBarItem"; @@ -385,7 +395,8 @@ private void InitializeComponent() this.PokeAddressToolBarItem.Click += new System.EventHandler(this.PokeAddressMenuItem_Click); // // FreezeAddressToolBarItem - // + // + this.FreezeAddressToolBarItem.AccessibleName = "Freeze Address"; this.FreezeAddressToolBarItem.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.FreezeAddressToolBarItem.ImageTransparentColor = System.Drawing.Color.Magenta; this.FreezeAddressToolBarItem.Name = "FreezeAddressToolBarItem"; @@ -394,7 +405,8 @@ private void InitializeComponent() this.FreezeAddressToolBarItem.Click += new System.EventHandler(this.FreezeAddressMenuItem_Click); // // seperatorToolStripButton - // + // + this.seperatorToolStripButton.AccessibleName = "Insert Separator"; this.seperatorToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.seperatorToolStripButton.ImageTransparentColor = System.Drawing.Color.Magenta; this.seperatorToolStripButton.Name = "seperatorToolStripButton"; @@ -404,7 +416,8 @@ private void InitializeComponent() this.seperatorToolStripButton.Click += new System.EventHandler(this.InsertSeparatorMenuItem_Click); // // moveUpToolStripButton - // + // + this.moveUpToolStripButton.AccessibleName = "Move Watch Up"; this.moveUpToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.moveUpToolStripButton.ImageTransparentColor = System.Drawing.Color.Magenta; this.moveUpToolStripButton.Name = "moveUpToolStripButton"; @@ -413,7 +426,8 @@ private void InitializeComponent() this.moveUpToolStripButton.Click += new System.EventHandler(this.MoveUpMenuItem_Click); // // moveDownToolStripButton - // + // + this.moveDownToolStripButton.AccessibleName = "Move Watch Down"; this.moveDownToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; this.moveDownToolStripButton.ImageTransparentColor = System.Drawing.Color.Magenta; this.moveDownToolStripButton.Name = "moveDownToolStripButton"; diff --git a/src/BizHawk.Client.EmuHawk/tools/Watch/RamWatch.NativeMenu.cs b/src/BizHawk.Client.EmuHawk/tools/Watch/RamWatch.NativeMenu.cs new file mode 100644 index 00000000000..891d52b7081 --- /dev/null +++ b/src/BizHawk.Client.EmuHawk/tools/Watch/RamWatch.NativeMenu.cs @@ -0,0 +1,136 @@ +using System.Drawing; +using System.Windows.Forms; +using BizHawk.Client.EmuHawk.Properties; + +namespace BizHawk.Client.EmuHawk +{ + public partial class RamWatch + { + // Menu accessibility is handled centrally by FormBase.InstallNativeMenuShim. + // This file provides the accessible-toolbar replacement (a ListView in place of + // the standard ToolStrip, since ToolStrip doesn't fire MSAA focus events) and + // non-menu accessibility properties. + + private ListView _toolbarListView; + private ImageList _toolbarImageList; + + private void InitializeNativeMenu() + { + CreateAccessibleToolbar(); + SetupAccessibility(); + } + + private void CreateAccessibleToolbar() + { + toolStrip1.Visible = false; + + _toolbarImageList = new ImageList(); + _toolbarImageList.ImageSize = new Size(20, 20); + _toolbarImageList.ColorDepth = ColorDepth.Depth32Bit; + _toolbarImageList.Images.Add("New", Resources.NewFile); + _toolbarImageList.Images.Add("Open", Resources.OpenFile); + _toolbarImageList.Images.Add("Save", Resources.SaveAs); + _toolbarImageList.Images.Add("NewWatch", Resources.Find); + _toolbarImageList.Images.Add("Edit", Resources.Pencil); + _toolbarImageList.Images.Add("Remove", Resources.Delete); + _toolbarImageList.Images.Add("Clear", Resources.Refresh); + _toolbarImageList.Images.Add("Duplicate", Resources.Duplicate); + _toolbarImageList.Images.Add("Split", Resources.Placeholder); + _toolbarImageList.Images.Add("Poke", Resources.Poke); + _toolbarImageList.Images.Add("Freeze", Resources.Freeze); + _toolbarImageList.Images.Add("Separator", Resources.InsertSeparator); + _toolbarImageList.Images.Add("Up", Resources.MoveUp); + _toolbarImageList.Images.Add("Down", Resources.MoveDown); + + _toolbarListView = new ListView + { + Name = "ToolbarListView", + AccessibleName = "RAM Watch Toolbar", + AccessibleRole = AccessibleRole.ToolBar, + View = View.List, + SmallImageList = _toolbarImageList, + Dock = DockStyle.Top, + Height = 30, + MultiSelect = false, + TabIndex = 0, + TabStop = true, + HideSelection = false, + Activation = ItemActivation.OneClick, + FullRowSelect = true, + }; + + _toolbarListView.Items.Add(new ListViewItem("New List", "New") { Tag = "New" }); + _toolbarListView.Items.Add(new ListViewItem("Open", "Open") { Tag = "Open" }); + _toolbarListView.Items.Add(new ListViewItem("Save", "Save") { Tag = "Save" }); + _toolbarListView.Items.Add(new ListViewItem("New Watch", "NewWatch") { Tag = "NewWatch" }); + _toolbarListView.Items.Add(new ListViewItem("Edit Watch", "Edit") { Tag = "Edit" }); + _toolbarListView.Items.Add(new ListViewItem("Remove", "Remove") { Tag = "Remove" }); + _toolbarListView.Items.Add(new ListViewItem("Clear Counts", "Clear") { Tag = "Clear" }); + _toolbarListView.Items.Add(new ListViewItem("Duplicate", "Duplicate") { Tag = "Duplicate" }); + _toolbarListView.Items.Add(new ListViewItem("Split", "Split") { Tag = "Split" }); + _toolbarListView.Items.Add(new ListViewItem("Poke", "Poke") { Tag = "Poke" }); + _toolbarListView.Items.Add(new ListViewItem("Freeze", "Freeze") { Tag = "Freeze" }); + _toolbarListView.Items.Add(new ListViewItem("Separator", "Separator") { Tag = "Separator" }); + _toolbarListView.Items.Add(new ListViewItem("Move Up", "Up") { Tag = "Up" }); + _toolbarListView.Items.Add(new ListViewItem("Move Down", "Down") { Tag = "Down" }); + + _toolbarListView.ItemActivate += ToolbarListView_ItemActivate; + _toolbarListView.KeyDown += ToolbarListView_KeyDown; + + Controls.Add(_toolbarListView); + _toolbarListView.BringToFront(); + } + + private void ToolbarListView_ItemActivate(object sender, EventArgs e) + { + if (_toolbarListView.SelectedItems.Count == 0) return; + var tag = _toolbarListView.SelectedItems[0].Tag?.ToString(); + ExecuteToolbarAction(tag); + } + + private void ToolbarListView_KeyDown(object sender, KeyEventArgs e) + { + if (e.KeyCode == Keys.Enter || e.KeyCode == Keys.Space) + { + if (_toolbarListView.SelectedItems.Count > 0) + { + var tag = _toolbarListView.SelectedItems[0].Tag?.ToString(); + ExecuteToolbarAction(tag); + e.Handled = true; + } + } + } + + private void ExecuteToolbarAction(string action) + { + switch (action) + { + case "New": NewListMenuItem_Click(this, EventArgs.Empty); break; + case "Open": OpenMenuItem_Click(this, EventArgs.Empty); break; + case "Save": SaveMenuItem_Click(this, EventArgs.Empty); break; + case "NewWatch": NewWatchMenuItem_Click(this, EventArgs.Empty); break; + case "Edit": EditWatchMenuItem_Click(this, EventArgs.Empty); break; + case "Remove": RemoveWatchMenuItem_Click(this, EventArgs.Empty); break; + case "Clear": ClearChangeCountsMenuItem_Click(this, EventArgs.Empty); break; + case "Duplicate": DuplicateWatchMenuItem_Click(this, EventArgs.Empty); break; + case "Split": SplitWatchMenuItem_Click(this, EventArgs.Empty); break; + case "Poke": PokeAddressMenuItem_Click(this, EventArgs.Empty); break; + case "Freeze": FreezeAddressMenuItem_Click(this, EventArgs.Empty); break; + case "Separator": InsertSeparatorMenuItem_Click(this, EventArgs.Empty); break; + case "Up": MoveUpMenuItem_Click(this, EventArgs.Empty); break; + case "Down": MoveDownMenuItem_Click(this, EventArgs.Empty); break; + } + } + + private void SetupAccessibility() + { + AccessibleName = "RAM Watch"; + AccessibleDescription = "RAM Watch tool for monitoring memory addresses"; + AccessibleRole = AccessibleRole.Window; + + WatchListView.AccessibleName = "Watch List"; + WatchListView.AccessibleDescription = "List of watched memory addresses"; + WatchListView.AccessibleRole = AccessibleRole.List; + } + } +} diff --git a/src/BizHawk.Client.EmuHawk/tools/Watch/RamWatch.cs b/src/BizHawk.Client.EmuHawk/tools/Watch/RamWatch.cs index 86288fda74a..949a1a43b5e 100644 --- a/src/BizHawk.Client.EmuHawk/tools/Watch/RamWatch.cs +++ b/src/BizHawk.Client.EmuHawk/tools/Watch/RamWatch.cs @@ -144,8 +144,11 @@ public RamWatch() _sortedColumn = ""; _sortReverse = false; - SetColumns(); + + // Initialize native menu and accessibility + InitializeNativeMenu(); + SetupAccessibility(); } public override bool IsActive => Config!.DisplayRamWatch || base.IsActive; diff --git a/src/BizHawk.WinForms.Controls/MenuEx/StatusLabelEx.cs b/src/BizHawk.WinForms.Controls/MenuEx/StatusLabelEx.cs index 8f20a91ad7d..2c680db2db4 100644 --- a/src/BizHawk.WinForms.Controls/MenuEx/StatusLabelEx.cs +++ b/src/BizHawk.WinForms.Controls/MenuEx/StatusLabelEx.cs @@ -6,11 +6,21 @@ namespace BizHawk.WinForms.Controls { public class StatusLabelEx : ToolStripStatusLabel { + private string? _name; + + public StatusLabelEx() + { + AccessibleRole = AccessibleRole.StaticText; + } + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public new Size Size => base.Size; [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public new string Name - => base.Name; + { + get => _name ?? base.Name; + set => _name = value; + } } } diff --git a/src/BizHawk.WinForms.Controls/MenuEx/StatusStripEx.cs b/src/BizHawk.WinForms.Controls/MenuEx/StatusStripEx.cs index f3600623cdc..8ce9b6eb085 100644 --- a/src/BizHawk.WinForms.Controls/MenuEx/StatusStripEx.cs +++ b/src/BizHawk.WinForms.Controls/MenuEx/StatusStripEx.cs @@ -9,11 +9,15 @@ namespace BizHawk.WinForms.Controls /// public class StatusStripEx : StatusStrip { + public StatusStripEx() + { + AccessibleRole = AccessibleRole.StatusBar; + } + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public new Size Size => base.Size; - [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - public new string Text => ""; + // Removed: Text override that returned empty string - breaks screen readers protected override void WndProc(ref Message m) { diff --git a/src/BizHawk.WinForms.Controls/MenuEx/ToolStripEx.cs b/src/BizHawk.WinForms.Controls/MenuEx/ToolStripEx.cs index 57b23520937..9f6938f8734 100644 --- a/src/BizHawk.WinForms.Controls/MenuEx/ToolStripEx.cs +++ b/src/BizHawk.WinForms.Controls/MenuEx/ToolStripEx.cs @@ -9,11 +9,15 @@ namespace BizHawk.WinForms.Controls /// public class ToolStripEx : ToolStrip { + public ToolStripEx() + { + AccessibleRole = AccessibleRole.ToolBar; + } + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public new Size Size => base.Size; - [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - public new string Text => ""; + // Removed: Text override that returned empty string - breaks screen readers protected override void WndProc(ref Message m) {