Skip to content

Commit 472086f

Browse files
committed
Bump core: Linux tray icons report their clicks and menu events again; add the Linux tray events GUI test
core_tray_icon_events_test_linux.sh drives the tray icon over D-Bus the way a StatusNotifierItem host does: the exported menu, WindowId's type, dbusmenu opened / closed / clicked coming back as menu events, and Activate / ContextMenu as clicks, double clicks and right clicks.
1 parent 61cbd42 commit 472086f

3 files changed

Lines changed: 136 additions & 1 deletion

File tree

‎tools/gui/README.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ runner) lives in [`.agents/skills/`](../../.agents/skills); read
3030
| `core_drag_drop_test.py` / `.ps1` | core `drag_drop_example` (C++) | macOS / Windows | `DragSource` → `DropTarget` across two windows: enter / move / drop events, dropped file path and text, drop position in content coordinates, source reports `copy`; a drag released where nothing accepts it reports exit and `none` |
3131
| `flutter_drag_drop_test.py` / `.ps1` | `drag_drop_example` | macOS / Windows | `DragOutArea` → `DropRegion` in one window: a file and a text drop arrive, the source sees `copy`, the highlight clears, a second drag works after the first |
3232
| `flutter_tray_icon_test.py` / `.ps1` / `_linux.py` | `tray_icon_example` | macOS / Windows (WinUI 3 and Native menu backends) / Linux | animated tray icons: frames really are rendered and pushed (counter, 30 and 60 fps, no dropped frames, Pause / Step / Resume, 3x resolution), a live widget captured into frames, the Download scene driving the title, three icons animating at once; title / tooltip / visibility / trigger / bounds read back from the native getters; the menu opened from code shows its items and states, an item click arrives, `closeContextMenu` closes it; "Window to icon" puts the window below the icon on macOS and above it on Windows, centred on it; the example's own checklist has no unexpected failures. Windows tray icons have no title, so the title checks become one no-op check there. On Linux the icon is a StatusNotifierItem: no bounds and no menu opened from code, so the test checks the example does not offer them; the frame pipeline is asserted at 10 fps (what 30 fps gives on the host is printed), and the icons are counted where the shell counts them: `RegisteredStatusNotifierItems` lists one, then three, then one again after two are removed. Not covered: clicks on the tray icon itself — the input driver refuses the menu-bar layer, those stay manual items of the example's Checklist tab |
33+
| `core_tray_icon_events_test_linux.sh` | core `tray_icon_example` (C++) | Linux | the tray icon's menu export and events, driven over D-Bus the way the shell drives a StatusNotifierItem (the input driver may not press the top bar): with the `RightClicked` trigger the menu is exported (`Menu`, its items in `GetLayout`) without `ItemIsMenu`; `WindowId` is an int32, as gnome-shell reads it; the host's dbusmenu events come back as menu events (`opened` / `closed` on the root once each however often they come, `clicked` on an item); from a host other than gnome-shell `Activate` is a click, a second one within the double-click time a double click (a third starts a new pair, two slow ones are two clicks), `ContextMenu` a right click; `SecondaryActivate` and `Scroll` report nothing yet. Not covered: gnome-shell's own `Activate` (a double click) and a real host's menu, which only real clicks in the top bar show — checked by hand on GNOME 46 and in KDE Plasma 6 |
3334
| `flutter_launch_at_startup_test.py` | `launch_at_startup`'s own example, in a leanflutter checkout (`$LEANFLUTTER_DIR`, default `~/Projects/leanflutter`; see `common.py`) | macOS | the 0.5.x compatible API on nativeapi: Enable registers an `SMAppService` login item, `isEnabled` reads it back, Disable removes it, and `setup(args:)` neither throws nor leaves an entry behind. Always leaves the login item off, including on a failure. Written but not yet run — the display was asleep; the same sequence was verified headlessly instead |
3435
| `flutter_detachable_window_and_browser_tabs_demo.py` / `.ps1` | both Flutter examples | macOS / Windows | the demo video scenarios; also the only coverage of `browser_tabs_example` (reorder, tear off, merge, move by the strip; its log ends with the preserved page state, no PASS/FAIL checks) |
3536
| `flutter_floating_toolbar_demo.py` / `.ps1` | `floating_toolbar_example` | macOS / Windows | the floating toolbar demo video: the pill drives the main window (colours, Stamp), follows it when it is dragged by the title bar and resized from its corner, stays behind when detached and snaps back when attached, is hidden and shown again; ends on the counter and the log. `DEMO_DRY_RUN=1` on the Windows host plays it without recording; without `--record` the macOS script does the same |
@@ -93,6 +94,8 @@ $R linux run tools/gui/build_core_example_linux.sh window_drag_session_example
9394
$R linux desktop tools/gui/core_window_drag_session_test_linux.py 180
9495
$R linux desktop tools/gui/flutter_floating_toolbar_test_linux.py 120 # no input; Wayland client
9596
$R linux desktop tools/gui/flutter_tray_icon_test_linux.py 300 # example built in the host's checkout
97+
$R linux run tools/gui/build_core_example_linux.sh tray_icon_example
98+
$R linux desktop tools/gui/core_tray_icon_events_test_linux.sh 60
9699
$R linux desktop tools/gui/flutter_window_shape_demo_linux.py 240 # dry run: no recorder
97100
.agents/skills/record-demo/scripts/record_remote_linux.sh linux \
98101
tools/gui/flutter_window_shape_demo_linux.py tools/gui/output # the take
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
#!/usr/bin/env bash
2+
# core_tray_icon_events_test_linux.sh — the Linux tray icon's menu export and click events,
3+
# driven the way the shell drives them: over D-Bus. A StatusNotifierItem is clicked by the
4+
# host calling its methods (gnome-shell's AppIndicator extension: Activate on a double
5+
# click, the menu on a single one; KDE: Activate on a click, ContextMenu on a right click),
6+
# so calling those methods is the click, minus the pointer the input driver may not send
7+
# to the top bar. Checks that the menu is exported for the host to show, that the host's
8+
# dbusmenu events come back as menu events (opened, closed, an item's click), that each
9+
# call reaches the app as the event it stands for, and that the calls with no event yet
10+
# (SecondaryActivate, Scroll) report nothing.
11+
#
12+
# $R linux run tools/gui/build_core_example_linux.sh tray_icon_example
13+
# $R linux desktop tools/gui/core_tray_icon_events_test_linux.sh 60
14+
set -uo pipefail
15+
source "$(dirname "$0")/env.sh"
16+
exe="$REMOTE_SCRATCH/core-build/examples/tray_icon_example/tray_icon_example"
17+
log="$REMOTE_SCRATCH/tray_icon_events.log"
18+
failures=0
19+
20+
"$exe" > "$log" 2>&1 &
21+
pid=$!
22+
trap 'kill $pid 2>/dev/null' EXIT
23+
24+
item=""
25+
for _ in $(seq 20); do
26+
item=$(gdbus call --session --dest org.kde.StatusNotifierWatcher \
27+
--object-path /StatusNotifierWatcher \
28+
--method org.freedesktop.DBus.Properties.Get \
29+
org.kde.StatusNotifierWatcher RegisteredStatusNotifierItems 2>/dev/null \
30+
| grep -o "org\.kde\.StatusNotifierItem-$pid-[0-9]*" | head -1)
31+
[ -n "$item" ] && break
32+
sleep 0.5
33+
done
34+
if [ -z "$item" ]; then
35+
echo "FAIL the shell lists the icon"
36+
exit 1
37+
fi
38+
echo "PASS the shell lists the icon ($item)"
39+
40+
call() {
41+
gdbus call --session --dest "$item" --object-path /StatusNotifierItem \
42+
--method "org.kde.StatusNotifierItem.$1" "${@:2}" > /dev/null
43+
}
44+
45+
# The events the app printed since the last step, as a space-separated list in $got.
46+
seen=0
47+
events() {
48+
sleep 0.8 # well past the double-click time, so steps do not pair up
49+
local all
50+
all=$(grep -o 'TRAY ICON [A-Z]* CLICKED' "$log" | awk '{print $3}')
51+
got=$(echo "$all" | tail -n +$((seen + 1)) | paste -sd' ' -)
52+
seen=$(echo "$all" | grep -c .)
53+
}
54+
55+
check() { # check <what> <expected> <got>
56+
if [ "$2" = "$3" ]; then
57+
echo "PASS $1: ${3:-nothing}"
58+
else
59+
echo "FAIL $1: expected '${2:-nothing}', got '${3:-nothing}'"
60+
failures=$((failures + 1))
61+
fi
62+
}
63+
64+
# The example sets a menu with the RightClicked trigger: exported for the host to show on
65+
# a secondary click (GNOME shows an empty one without it), but not as ItemIsMenu.
66+
prop() {
67+
gdbus call --session --dest "$item" --object-path /StatusNotifierItem \
68+
--method org.freedesktop.DBus.Properties.Get org.kde.StatusNotifierItem "$1"
69+
}
70+
check "the menu is exported" "(<objectpath '/StatusNotifierItem/Menu'>,)" "$(prop Menu)"
71+
check "ItemIsMenu is off" "(<false>,)" "$(prop ItemIsMenu)"
72+
layout=$(gdbus call --session --dest "$item" --object-path /StatusNotifierItem/Menu \
73+
--method com.canonical.dbusmenu.GetLayout -- 0 -1 '[]')
74+
for label in 'Settings...' 'About' 'Exit'; do
75+
case "$layout" in
76+
*"'label': <'$label'>"*) check "the menu has \"$label\"" "yes" "yes" ;;
77+
*) check "the menu has \"$label\"" "yes" "no" ;;
78+
esac
79+
done
80+
# gnome-shell's extension reads WindowId as an int32 and warns about anything else.
81+
check "WindowId is an int32" "(<0>,)" "$(prop WindowId)"
82+
83+
# The host shows the exported menu itself and tells the app through dbusmenu events.
84+
menu_event() { # menu_event <id> <opened|closed|clicked>
85+
gdbus call --session --dest "$item" --object-path /StatusNotifierItem/Menu \
86+
--method com.canonical.dbusmenu.Event -- "$1" "$2" '<0>' 0 > /dev/null
87+
}
88+
menu_lines() { # the number of lines the app printed so far that match $1
89+
sleep 0.5
90+
grep -c "$1" "$log"
91+
}
92+
menu_event 0 opened; menu_event 0 opened
93+
check "\"opened\" on the root is one MenuOpened, however often it comes" "1" "$(menu_lines 'Tray menu opened')"
94+
settings=$(echo "$layout" | grep -o "([0-9]*, {[^}]*'label': <'Settings...'>" | grep -o '^([0-9]*' | tr -d '(')
95+
menu_event "${settings:-0}" clicked
96+
check "\"clicked\" on an item is its click" "1" "$(menu_lines 'Settings clicked from context menu')"
97+
menu_event 0 closed; menu_event 0 closed
98+
check "\"closed\" on the root is one MenuClosed" "1" "$(menu_lines 'Tray menu closed')"
99+
100+
# gdbus is not gnome-shell: these are the calls of any other host (KDE). gnome-shell's
101+
# Activate is a double click, which only a real click in the top bar can show.
102+
call Activate 10 10
103+
events
104+
check "Activate is a click" "LEFT" "$got"
105+
106+
call Activate 10 10; call Activate 10 10
107+
events
108+
check "two quick Activates are a click, then a double click" "LEFT DOUBLE" "$got"
109+
110+
call Activate 10 10; call Activate 10 10; call Activate 10 10
111+
events
112+
check "a third quick Activate starts a new pair" "LEFT DOUBLE LEFT" "$got"
113+
114+
call Activate 10 10; sleep 1; call Activate 10 10
115+
events
116+
check "two slow Activates are two clicks" "LEFT LEFT" "$got"
117+
118+
call ContextMenu 10 10
119+
events
120+
check "ContextMenu is a right click" "RIGHT" "$got"
121+
122+
call SecondaryActivate 10 10; call Scroll 1 vertical
123+
events
124+
check "SecondaryActivate and Scroll report nothing" "" "$got"
125+
126+
if ! kill -0 $pid 2>/dev/null; then
127+
echo "FAIL the app is still running"
128+
failures=$((failures + 1))
129+
fi
130+
131+
echo "$failures failure(s)"
132+
exit $((failures > 0))

0 commit comments

Comments
 (0)