Skip to content

Commit ff98491

Browse files
committed
✨ feat: feat(a11y, resilience): add ORCA support via Super+Alt+S and robust compositor startup
Accessibility (ORCA / AT-SPI2): - Add accessibility.py module with AT-SPI2 announce() and start_orca() helpers - Use standard GNOME shortcut Super+Alt+S to start ORCA interactively in the wizard — no GRUB kernel parameters needed - Add accessible labels/descriptions to all wizard steps, FlowBox containers, settings cards (JamesDSP, contrast), and language search grid - Announce step navigation, filter results count, and switch toggle state to screen readers via AT-SPI2 - Add keyboard activation (Enter/Space) for toggle cards in theme view - Expose step position (e.g. "2/4") in accessible descriptions Compositor startup resilience (startbiglive): - Add multi-stage fallback for kwin_wayland: hardware → primary GPU only (with Mesa EGL override for NVIDIA hybrid) → software rendering (llvmpipe) - Add multi-stage fallback for mutter: hardware → simple KMS + copy mode (with Mesa EGL for NVIDIA) → software rendering (llvmpipe) - Add GPU readiness wait (_wait_for_gpu) before compositor launch - Add anti-loop protection: limit SDDM restart attempts with cooldown timer - Add multi-GPU detection (boot_vga, switcherooctl) and NVIDIA proprietary driver detection for correct EGL vendor selection - Apply default config automatically when wizard cannot start - Add structured logging via logger for all startup stages Session stability: - Ensure XDG_RUNTIME_DIR and DBUS_SESSION_BUS_ADDRESS are set before waiting for user session - Add rescue attempt to start default.target if session times out - Guard plasma-kglobalaccel start with error suppression Other fixes: - Fix application ID to reverse-DNS format (br.com.biglinux.livecd) - Fix TEXTDOMAIN from calamares-biglinux to biglinux-livecd - Fix geoip style from "none" to "xml" in calamares welcome config - Fix typo: pkgAvaliableToRemove → pkgAvailableToRemove
1 parent c332ed0 commit ff98491

8 files changed

Lines changed: 198 additions & 2 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""Accessibility utilities for ORCA screen reader support (AT-SPI2)."""
2+
3+
import subprocess
4+
5+
import gi
6+
7+
gi.require_version("Gtk", "4.0")
8+
from gi.repository import Gtk
9+
from logging_config import get_logger
10+
11+
logger = get_logger()
12+
13+
_HAS_ANNOUNCE = hasattr(Gtk.Accessible, "announce")
14+
15+
16+
def announce(widget: Gtk.Accessible, message: str, assertive: bool = False) -> None:
17+
"""
18+
Announce a message to screen readers (ORCA) via AT-SPI2.
19+
Uses Gtk.Accessible.announce() on GTK 4.14+.
20+
"""
21+
if not message or not widget:
22+
return
23+
if _HAS_ANNOUNCE:
24+
try:
25+
priority = (
26+
Gtk.AccessibleAnnouncementPriority.HIGH
27+
if assertive
28+
else Gtk.AccessibleAnnouncementPriority.MEDIUM
29+
)
30+
widget.announce(message, priority)
31+
except Exception as e:
32+
logger.debug(f"announce() failed: {e}")
33+
else:
34+
logger.debug(f"a11y: {message}")
35+
36+
37+
def start_orca() -> bool:
38+
"""Start ORCA screen reader if not already running."""
39+
try:
40+
result = subprocess.run(
41+
["pgrep", "-x", "orca"],
42+
capture_output=True,
43+
timeout=5,
44+
)
45+
if result.returncode == 0:
46+
logger.info("ORCA is already running")
47+
return True
48+
subprocess.Popen(
49+
["orca"],
50+
stdout=subprocess.DEVNULL,
51+
stderr=subprocess.DEVNULL,
52+
)
53+
logger.info("Started ORCA screen reader")
54+
return True
55+
except FileNotFoundError:
56+
logger.warning("ORCA not found on this system")
57+
return False
58+
except subprocess.TimeoutExpired:
59+
logger.warning("Timeout checking for ORCA process")
60+
return False

biglinux-livecd/usr/share/biglinux/livecd/main.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import sys
22
import argparse
33
import logging
4+
45
from logging_config import setup_logging
56
from application import Application
67
from services import SystemService

biglinux-livecd/usr/share/biglinux/livecd/ui/app_window.py

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from ui.keyboard_view import KeyboardView
1212
from ui.desktop_view import DesktopView
1313
from ui.theme_view import ThemeView
14+
from accessibility import announce, start_orca
1415
from logging_config import get_logger
1516
import os
1617

@@ -71,6 +72,14 @@ def __init__(self, system_service: SystemService, **kwargs):
7172
self.completed_steps = set() # Track completed steps
7273
self.is_simplified_env = system_service.is_simplified_environment()
7374
self.set_title(_("BigLinux Setup"))
75+
self.update_property(
76+
[Gtk.AccessibleProperty.DESCRIPTION],
77+
[
78+
_("BigLinux Setup")
79+
+ " — "
80+
+ _("press Super+Alt+S to start the screen reader")
81+
],
82+
)
7483

7584
# --- Fullscreen for Xorg without compositor ---
7685
# This approach makes the window undecorated and sized to the monitor.
@@ -206,6 +215,14 @@ def _build_ui(self):
206215
def _retranslate_ui(self):
207216
"""Updates all visible text in the application to the new language."""
208217
self.set_title(_("BigLinux Setup"))
218+
self.update_property(
219+
[Gtk.AccessibleProperty.DESCRIPTION],
220+
[
221+
_("BigLinux Setup")
222+
+ " \u2014 "
223+
+ _("press Super+Alt+S to start the screen reader")
224+
],
225+
)
209226

210227
# Update step button accessible labels
211228
for step in self.steps:
@@ -273,11 +290,17 @@ def _add_step_button(self, box, step_info):
273290
button.connect("clicked", self._on_step_button_clicked, step_info["name"])
274291
button.add_css_class("flat")
275292

276-
# Accessible label for screen readers
293+
# Accessible label and description for screen readers
277294
label_fn = self._STEP_LABELS.get(step_info["name"])
278295
if label_fn:
296+
step_index = next(
297+
(i for i, s in enumerate(self.steps) if s["name"] == step_info["name"]),
298+
0,
299+
)
300+
total = len(self.steps)
279301
button.update_property(
280-
[Gtk.AccessibleProperty.LABEL], [label_fn()]
302+
[Gtk.AccessibleProperty.LABEL, Gtk.AccessibleProperty.DESCRIPTION],
303+
[label_fn(), f"{step_index + 1}/{total}"],
281304
)
282305

283306
try:
@@ -293,6 +316,24 @@ def _add_step_button(self, box, step_info):
293316

294317
def _on_view_changed(self, stack, param):
295318
GLib.idle_add(self._update_header_state)
319+
# Announce the new step to screen readers (ORCA)
320+
view_name = stack.get_visible_child_name()
321+
GLib.idle_add(lambda n=view_name: self._announce_step(n))
322+
323+
def _announce_step(self, view_name: str) -> None:
324+
"""Announce step label and position to screen readers."""
325+
search_name = "theme" if view_name == "simple_theme" else view_name
326+
try:
327+
index = next(
328+
i for i, s in enumerate(self.steps) if s["name"] == search_name
329+
)
330+
except StopIteration:
331+
return
332+
total = len(self.steps)
333+
label_fn = self._STEP_LABELS.get(search_name)
334+
step_label = label_fn() if label_fn else search_name
335+
msg = f"{step_label}{index + 1}/{total}"
336+
announce(self, msg, assertive=True)
296337

297338
def _on_step_button_clicked(self, button, view_name):
298339
# Only allow navigation to completed steps
@@ -329,14 +370,17 @@ def _update_header_state(self):
329370
# Completed step - clickable and visually active
330371
button.add_css_class("step-completed")
331372
button.set_sensitive(True)
373+
button.update_state([Gtk.AccessibleState.DISABLED], [False])
332374
elif i == current_index:
333375
# Current step - most prominent and enabled for bright appearance
334376
button.add_css_class("step-current")
335377
button.set_sensitive(True) # Keep enabled for bright appearance
378+
button.update_state([Gtk.AccessibleState.DISABLED], [False])
336379
else:
337380
# Pending step - inactive
338381
button.add_css_class("step-pending")
339382
button.set_sensitive(False)
383+
button.update_state([Gtk.AccessibleState.DISABLED], [True])
340384

341385
def _add_language_view(self):
342386
view = LanguageView()
@@ -513,6 +557,14 @@ def _on_simple_theme_selected(self, view, theme):
513557
logger.error(f"ERROR in _on_simple_theme_selected: {e}", exc_info=True)
514558

515559
def _on_key_press_event(self, controller, keyval, keycode, state):
560+
# Super+Alt+S: start ORCA screen reader (standard GNOME shortcut)
561+
if (
562+
keyval == Gdk.KEY_s
563+
and state & Gdk.ModifierType.SUPER_MASK
564+
and state & Gdk.ModifierType.ALT_MASK
565+
):
566+
start_orca()
567+
return True
516568
current_view = self.stack.get_visible_child()
517569
if isinstance(current_view, LanguageView):
518570
return current_view.handle_global_key_press(keyval)

biglinux-livecd/usr/share/biglinux/livecd/ui/base_view.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from gi.repository import Adw, Gdk, GLib, GObject, Gtk
1010
from services import SystemService
1111
from translations import _
12+
from accessibility import announce
1213
from logging_config import get_logger
1314

1415
logger = get_logger()
@@ -80,6 +81,9 @@ def _build_ui(self):
8081
)
8182
self.flow_box.set_can_focus(True)
8283
self.flow_box.set_halign(Gtk.Align.CENTER)
84+
self.flow_box.update_property(
85+
[Gtk.AccessibleProperty.LABEL], [self.get_title()]
86+
)
8387

8488
self.flow_box.connect("child-activated", self._on_child_activated)
8589
self.flow_box.connect("activate-cursor-child", self._on_activate_cursor_child)
@@ -100,6 +104,10 @@ def _retranslate_ui(self):
100104
"""Updates the view's title to the current language."""
101105
if hasattr(self, "title_label"):
102106
self.title_label.set_label(self.get_title())
107+
if hasattr(self, "flow_box"):
108+
self.flow_box.update_property(
109+
[Gtk.AccessibleProperty.LABEL], [self.get_title()]
110+
)
103111

104112
def load_items(self):
105113
items = self.get_items()

biglinux-livecd/usr/share/biglinux/livecd/ui/desktop_view.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ def create_item_gobject(self, name: str) -> GObject.Object:
4141
def create_item_widget(self, item: DesktopListItem):
4242
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
4343
box.set_can_focus(True)
44+
# Accessible description for screen readers
45+
box.update_property(
46+
[Gtk.AccessibleProperty.LABEL],
47+
[_("Desktop Layout") + ": " + item.name],
48+
)
4449
try:
4550
cursor = Gdk.Cursor.new_from_name("pointer", None)
4651
box.set_cursor(cursor)

biglinux-livecd/usr/share/biglinux/livecd/ui/keyboard_view.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
gi.require_version("Adw", "1")
77
from gi.repository import Gtk, Adw, GObject, GLib, Gdk
88
from translations import _
9+
from accessibility import announce
910
import os
1011

1112
ASSETS_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "assets"))
@@ -63,6 +64,10 @@ def _build_ui(self):
6364
)
6465
self.flow_box.set_can_focus(True)
6566
self.flow_box.set_halign(Gtk.Align.CENTER)
67+
self.flow_box.update_property(
68+
[Gtk.AccessibleProperty.LABEL],
69+
[_("Choose Your Keyboard Layout")],
70+
)
6671

6772
# Connect to FlowBox-level signals
6873
self.flow_box.connect("child-activated", self._on_child_activated)

biglinux-livecd/usr/share/biglinux/livecd/ui/language_view.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from urllib.parse import parse_qs, urlparse
99
from translations import _
1010
from config import LanguageSelection
11+
from accessibility import announce
1112
from logging_config import get_logger
1213
import os
1314

@@ -95,6 +96,10 @@ def _build_ui(self):
9596
max_columns=3,
9697
min_columns=3,
9798
)
99+
self.grid_view.update_property(
100+
[Gtk.AccessibleProperty.LABEL],
101+
[_("Search for a language...")],
102+
)
98103
self.grid_view.connect("activate", self._on_grid_view_activate)
99104
grid_clamp.set_child(self.grid_view)
100105

@@ -157,6 +162,9 @@ def _on_search_changed(self, entry):
157162
def _trigger_filter_update(self):
158163
self.filter.changed(Gtk.FilterChange.DIFFERENT)
159164
GLib.idle_add(self._select_first_item_after_filter)
165+
# Announce results count for screen readers
166+
count = self.filter_model.get_n_items()
167+
GLib.idle_add(lambda c=count: announce(self, _("%d results") % c))
160168
self.filter_timeout_id = 0
161169
return GLib.SOURCE_REMOVE
162170

biglinux-livecd/usr/share/biglinux/livecd/ui/theme_view.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from logging_config import get_logger
1111
from services import SystemService
1212
from translations import _
13+
from accessibility import announce
1314
from ui.base_view import BaseItemView
1415

1516
logger = get_logger()
@@ -133,11 +134,24 @@ def _retranslate_ui(self):
133134
def _create_jamesdsp_card(self, parent_box):
134135
audio_card = Gtk.Box(css_classes=["settings-card"])
135136
audio_card.set_focusable(True)
137+
audio_card.update_property(
138+
[Gtk.AccessibleProperty.LABEL, Gtk.AccessibleProperty.DESCRIPTION],
139+
[
140+
_("JamesDSP Audio"),
141+
_("Enable audio improvements")
142+
+ " — "
143+
+ _("press Enter or Space to toggle"),
144+
],
145+
)
136146
try:
137147
cursor = Gdk.Cursor.new_from_name("pointer", None)
138148
audio_card.set_cursor(cursor)
139149
except Exception:
140150
pass
151+
# Keyboard activation: Enter / Space toggles switch
152+
card_key_ctl = Gtk.EventControllerKey.new()
153+
card_key_ctl.connect("key-pressed", self._on_settings_card_key, None)
154+
audio_card.add_controller(card_key_ctl)
141155
parent_box.append(audio_card)
142156

143157
content = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=15)
@@ -180,11 +194,24 @@ def _create_jamesdsp_card(self, parent_box):
180194
def _create_contrast_card(self, parent_box):
181195
contrast_card = Gtk.Box(css_classes=["settings-card"])
182196
contrast_card.set_focusable(True)
197+
contrast_card.update_property(
198+
[Gtk.AccessibleProperty.LABEL, Gtk.AccessibleProperty.DESCRIPTION],
199+
[
200+
_("Image quality"),
201+
_("Enable enhanced contrast")
202+
+ " — "
203+
+ _("press Enter or Space to toggle"),
204+
],
205+
)
183206
try:
184207
cursor = Gdk.Cursor.new_from_name("pointer", None)
185208
contrast_card.set_cursor(cursor)
186209
except Exception:
187210
pass
211+
# Keyboard activation: Enter / Space toggles switch
212+
card_key_ctl = Gtk.EventControllerKey.new()
213+
card_key_ctl.connect("key-pressed", self._on_settings_card_key, None)
214+
contrast_card.add_controller(card_key_ctl)
188215
parent_box.append(contrast_card)
189216

190217
content = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=15)
@@ -230,6 +257,36 @@ def _on_settings_card_clicked(self, gesture, n_press, x, y, switch):
230257
if n_press == 1:
231258
switch.set_active(not switch.get_active())
232259

260+
def _on_settings_card_key(self, controller, keyval, keycode, state, _data):
261+
"""Toggle the switch inside a settings card via Enter or Space."""
262+
if keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter, Gdk.KEY_space):
263+
card = controller.get_widget()
264+
# Find the Gtk.Switch inside the card
265+
child = card.get_first_child()
266+
while child:
267+
if isinstance(child, Gtk.Switch):
268+
child.set_active(not child.get_active())
269+
state_msg = _("enabled") if child.get_active() else _("disabled")
270+
announce(self, state_msg)
271+
return True
272+
# Search inside nested containers
273+
inner = (
274+
child.get_first_child()
275+
if hasattr(child, "get_first_child")
276+
else None
277+
)
278+
while inner:
279+
if isinstance(inner, Gtk.Switch):
280+
inner.set_active(not inner.get_active())
281+
state_msg = (
282+
_("enabled") if inner.get_active() else _("disabled")
283+
)
284+
announce(self, state_msg)
285+
return True
286+
inner = inner.get_next_sibling()
287+
child = child.get_next_sibling()
288+
return False
289+
233290
# --- Implementation of BaseItemView abstract methods ---
234291

235292
def get_title(self) -> str:

0 commit comments

Comments
 (0)