Skip to content

Commit c6136c0

Browse files
authored
Improve Avalonia log UI and tab navigation shortcuts (#179)
* Improve the displaying of Avalonia Desktop app log messages. Make the log text selectable, and add right click menu for copy. Add copy all log button. Improve automation capability with shortcuts for the different tabs at lower part of UI.
1 parent a5aa4e7 commit c6136c0

5 files changed

Lines changed: 220 additions & 39 deletions

File tree

doc/APPS_AVALONIA_AUTOMATION.md

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -112,43 +112,83 @@ A non-exhaustive list of the most useful AutomationIds, grouped by view. All of
112112
- `StatisticsView` (root)
113113
- `C64InfoView` (root, keyboard mapping reference)
114114

115-
# Keyboard shortcuts (system menu contributions)
115+
# Keyboard shortcuts
116116

117-
Some controls inside nested `UserControl`s do not traverse cleanly to the macOS AX tree (see "known gaps" below — the left-pane `C64MenuView` sections are the most visible example). To keep those operations reachable for agents and keyboard users, the active system's menu ViewModel implements `ISystemMenuContributor` ([`Core/SystemSetup/ISystemMenuContributor.cs`](../src/apps/Avalonia/Highbyte.DotNet6502.App.Avalonia.Core/SystemSetup/ISystemMenuContributor.cs)) and contributes:
118-
- A `NativeMenu` that Avalonia installs on the **macOS system menu bar** (shown under a top-level header for the active system, e.g. `C64`). On macOS, `NativeMenu` items appear in the OS-level menu bar *outside* the app window — which is the desired UX. The macOS Accessibility API also exposes these items with their `Gesture` string, making shortcuts self-describing: an AI agent can discover them at runtime via `peekaboo menu list` without needing any prior documentation.
119-
- A parallel list of `KeyBinding`s applied to the main window on **Windows / Linux**. `NativeMenu` on these platforms would render as in-window chrome, which is not desired, so `KeyBinding`s are used instead. The shortcuts fire regardless of which child control has focus, but they are invisible to accessibility tools — an automation agent needs to know them in advance (e.g. from this document).
117+
The app exposes two layers of shortcuts:
120118

121-
`MainViewModel.ActiveMenuContributor` swaps when `SelectedSystemName` changes; `MainView.axaml.cs` applies the new menu / keybindings, and clears the previous one on teardown.
119+
1. **General tab-navigation shortcuts** — always active, independent of which emulator system is running.
120+
2. **System-specific shortcuts** — active only when a particular system is selected (e.g. C64). The active system's menu ViewModel implements `ISystemMenuContributor` ([`Core/SystemSetup/ISystemMenuContributor.cs`](../src/apps/Avalonia/Highbyte.DotNet6502.App.Avalonia.Core/SystemSetup/ISystemMenuContributor.cs)).
121+
122+
On macOS both layers appear in the OS-level **system menu bar** (outside the app window) — general shortcuts under a `View` top-level menu, system-specific shortcuts under the system name (e.g. `C64`). The macOS Accessibility API exposes these with their `Gesture` string, so an AI agent can discover them at runtime without prior documentation:
123+
124+
```sh
125+
peekaboo menu list --app "DotNet 6502 Emulator"
126+
```
127+
128+
On **Windows / Linux**, `NativeMenu` would render as in-window chrome (not desired), so `KeyBinding`s registered on the main window are used instead. They fire regardless of which child control has focus, but are invisible to accessibility tools — an agent needs to know them from this document.
129+
130+
## Tab navigation shortcuts (always active)
131+
132+
These shortcuts jump directly to a named tab regardless of tab order — reordering tabs in code does **not** break automation scripts.
133+
134+
| Tab | macOS | Windows / Linux |
135+
| --------------- | -------- | ------------------ |
136+
| Information | `⌘⌥I` | `Ctrl+Alt+I` |
137+
| Config status | `⌘⌥C` | `Ctrl+Alt+C` |
138+
| Log | `⌘⌥L` | `Ctrl+Alt+L` |
139+
| Scripts | `⌘⌥S` | `Ctrl+Alt+S` |
140+
| General info | `⌘⌥G` | `Ctrl+Alt+G` |
141+
| Debug | `⌘⌥D` | `Ctrl+Alt+D` |
142+
143+
On macOS, click via the menu bar instead of counting arrow-key presses:
144+
145+
```sh
146+
peekaboo menu click --app "DotNet 6502 Emulator" --path "DotNet 6502 Emulator > View > Log"
147+
```
122148

123149
## C64 shortcuts (active when the C64 system is selected)
124150

125151
| Action | macOS | Windows / Linux |
126152
| -------------------------------- | ------------------- | --------------------- |
127153
| Toggle Disk Drive section | `⌘⌥⇧D` | `Ctrl+Alt+Shift+D` |
128-
| Toggle Load/Save section | `⌘⌥L` | `Ctrl+Alt+L` |
129-
| Toggle Configuration section | `⌘⌥C` | `Ctrl+Alt+C` |
154+
| Toggle Load/Save section | `⌘⌥L` | `Ctrl+Alt+Shift+L` |
155+
| Toggle Configuration section | `⌘⌥C` | `Ctrl+Alt+Shift+C` |
130156
| Active joystick → Port 1 | `⌘⌥1` | `Ctrl+Alt+1` |
131157
| Active joystick → Port 2 | `⌘⌥2` | `Ctrl+Alt+2` |
132158
| Toggle Joystick KB | `⌘⌥K` | `Ctrl+Alt+K` |
133159
| Keyboard joystick → Port 1 | `⌘⌥⇧1` | `Ctrl+Alt+Shift+1` |
134160
| Keyboard joystick → Port 2 | `⌘⌥⇧2` | `Ctrl+Alt+Shift+2` |
135161

136-
On macOS, the shortcuts are discoverable by walking the app's menu bar via peekaboo:
162+
On macOS, click via the menu bar:
137163

138164
```sh
139-
peekaboo menu list --app "DotNet 6502 Emulator"
140-
peekaboo menu click --app "DotNet 6502 Emulator" --path "C64 > Toggle Configuration section"
165+
peekaboo menu click --app "DotNet 6502 Emulator" --path "DotNet 6502 Emulator > C64 > Toggle Configuration section"
141166
```
142167

143-
On Windows / Linux, the same shortcuts are dispatched by the main window's key bindings; an automation harness simulates the key combo instead of clicking a menu.
144-
145168
# What is NOT surfaced (known gaps)
146169

147170
1. **Individual `TabItem` controls on macOS** — verified with `peekaboo see` after running the app. The `InformationTabControl` surfaces, but its `TabItem` children (`InformationTab`, `LogTab`, etc.) do not appear as distinct clickable elements in the AX tree, *despite* having explicit `AutomationProperties.AutomationId` + `Name`. The AX tree on macOS reports roles limited to `button`, `group`, `menu`, `other`, `slider` — no `AXTabGroup` / `AXTab`.
148171

149172
This is most likely an Avalonia `TabItemAutomationPeer` / macOS NSAccessibility bridge limitation, not a bug in this codebase. Worth filing an issue upstream in `avaloniaui/Avalonia`.
150173

151-
**Workaround**: click the tab by screen coordinates (see the peekaboo section below), or use keyboard navigation (`Ctrl+Tab` / arrow keys when the tab control is focused).
174+
**Workaround**: use keyboard navigation — this is the **reliable** approach. Find the `InformationTabControl` element via `peekaboo see`, click it to give it focus, then press the right-arrow key once per tab step:
175+
176+
```sh
177+
# Capture the AX tree and find InformationTabControl's elem_NN
178+
peekaboo see --app "DotNet 6502 Emulator" --json | jq '.. | objects | select(.identifier == "InformationTabControl") | .id'
179+
# → e.g. "elem_49"
180+
181+
# Focus the tab control
182+
peekaboo click --on elem_49 --app "DotNet 6502 Emulator" --window-index 0
183+
184+
# Navigate right to reach the target tab (count depends on which tab is currently active)
185+
# Tab order: Information → ConfigStatus → Log → Scripts → GeneralInfo → Debug
186+
peekaboo press right # repeat as needed
187+
```
188+
189+
The number of right-arrow presses depends on the **currently active tab**, not a fixed offset. If "Information" is active, pressing right twice reaches "Log". If another tab is already active, adjust accordingly.
190+
191+
**Avoid** clicking by screen coordinates for tabs: coordinates are window-size-dependent and scale across display densities. **Avoid** `peekaboo click "Log"`: text-query matching is global and can hit an element with the same label in another app or inside the tab's content area (e.g. Ghostty's "Log Out" menu).
152192

153193
2. **Collapsed/conditional content** only appears in the AX tree when its container is rendered. Examples:
154194
- `C64MenuView` section contents (`DiskSectionContent`, `LoadSaveSectionContent`, `ConfigSectionContent`) — only visible when the section header is expanded.
@@ -224,7 +264,7 @@ peekaboo click --coords "440,595" --app "DotNet 6502 Emulator" --window-index 0
224264

225265
- **Don't use `--no-auto-focus` from a terminal.** The terminal emulator (e.g. Ghostty) reclaims focus between commands, so a `--no-auto-focus` click lands on the terminal window at the same screen coordinates — `click` still reports "✅ Click successful" but against the wrong app. Let peekaboo's auto-focus bring the Avalonia window forward.
226266

227-
- **Text-query clicks can hit the wrong element.** `peekaboo click "Log"` may match a `TextBlock` labelled "Log" *inside* the tab content rather than the tab header, because the header is behind the Avalonia TabItem AX gap described above. When targeting tabs specifically, use coordinates.
267+
- **Text-query clicks are unreliable for tabs — and can hit other apps.** `peekaboo click "Log"` searches globally across all visible AX elements. It can match a label *inside the tab content*, a menu item in another app (e.g. Ghostty's "Log Out" item), or any other element named "Log" that happens to be on screen. For tab navigation, always use the keyboard approach described in "Known Gaps" item 1 above.
228268

229269
- **Screenshot coordinates vs. screen coordinates.** The annotated screenshot from `peekaboo see --annotate` is scaled to roughly 0.75× the window-point size. To convert a pixel position in the screenshot to a click coordinate, scale by ~1.33× and add the window's screen offset (`peekaboo list` shows the window Position).
230270

@@ -258,8 +298,8 @@ START=$(peekaboo see --app "DotNet 6502 Emulator" --json \
258298
peekaboo click --on "$START" --snapshot "$SNAP" \
259299
--app "DotNet 6502 Emulator" --window-index 0
260300

261-
# 2. Click Log tab by coordinates (tab row is ~y=595 at the given window size)
262-
peekaboo click --coords "440,595" --app "DotNet 6502 Emulator" --window-index 0
301+
# 2. Navigate to the Log tab via its named menu shortcut (order-independent)
302+
peekaboo menu click --app "DotNet 6502 Emulator" --path "DotNet 6502 Emulator > View > Log"
263303

264304
# Verify
265305
peekaboo see --app "DotNet 6502 Emulator" --annotate /tmp/after.png

src/apps/Avalonia/Highbyte.DotNet6502.App.Avalonia.Core/ViewModels/C64MenuViewModel.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -495,8 +495,8 @@ public IReadOnlyList<NativeMenuItemBase> GetNativeMenuItems()
495495
return new NativeMenuItemBase[]
496496
{
497497
BuildMenuItem("Toggle Disk Drive section", new KeyGesture(Key.D, macShift), ToggleDiskSectionCommand),
498-
BuildMenuItem("Toggle Load/Save section", new KeyGesture(Key.L, macBase), ToggleLoadSaveSectionCommand),
499-
BuildMenuItem("Toggle Configuration section", new KeyGesture(Key.C, macBase), ToggleConfigSectionCommand),
498+
BuildMenuItem("Toggle Load/Save section", new KeyGesture(Key.L, macShift), ToggleLoadSaveSectionCommand),
499+
BuildMenuItem("Toggle Configuration section", new KeyGesture(Key.C, macShift), ToggleConfigSectionCommand),
500500
new NativeMenuItemSeparator(),
501501
BuildMenuItem("Active joystick: Port 1", new KeyGesture(Key.D1, macBase), SetActiveJoystickCommand, 1),
502502
BuildMenuItem("Active joystick: Port 2", new KeyGesture(Key.D2, macBase), SetActiveJoystickCommand, 2),
@@ -520,8 +520,8 @@ public IReadOnlyList<KeyBinding> GetKeyBindings()
520520
return new[]
521521
{
522522
BuildKeyBinding(new KeyGesture(Key.D, nonMacShift), ToggleDiskSectionCommand),
523-
BuildKeyBinding(new KeyGesture(Key.L, nonMacBase), ToggleLoadSaveSectionCommand),
524-
BuildKeyBinding(new KeyGesture(Key.C, nonMacBase), ToggleConfigSectionCommand),
523+
BuildKeyBinding(new KeyGesture(Key.L, nonMacShift), ToggleLoadSaveSectionCommand),
524+
BuildKeyBinding(new KeyGesture(Key.C, nonMacShift), ToggleConfigSectionCommand),
525525
BuildKeyBinding(new KeyGesture(Key.D1, nonMacBase), SetActiveJoystickCommand, 1),
526526
BuildKeyBinding(new KeyGesture(Key.D2, nonMacBase), SetActiveJoystickCommand, 2),
527527
BuildKeyBinding(new KeyGesture(Key.K, nonMacBase), ToggleJoystickKeyboardCommand),

src/apps/Avalonia/Highbyte.DotNet6502.App.Avalonia.Core/ViewModels/MainViewModel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1061,7 +1061,7 @@ public class LogDisplayEntry
10611061
public LogDisplayEntry(LogEntry logEntry)
10621062
{
10631063
LogLevel = logEntry.LogLevel;
1064-
Message = logEntry.Message;
1064+
Message = logEntry.Message.TrimEnd();
10651065
Symbol = GetSymbolForLogLevel(logEntry.LogLevel);
10661066
FormattedDisplay = $"{Symbol} {Message}";
10671067

0 commit comments

Comments
 (0)