fix: atspi_click.py sprachunabhaengig statt reine deutsche Werteliste
Real beim Testen entdeckt: die Mint-eigene 'Multimedia-Codecs'-Seite zeigt
trotz debian-installer/language=de englische Buttons ('Quit'/'Back'/
'Continue') - vermutlich derselbe fehlende .mo-Uebersetzungskatalog wie
beim bereits gepatchten ubi-prepare.py-None-Bug. Die deutsche
FORWARD_LABELS-Werteliste haette hier fuer immer haengen bleiben.
Neue Strategie in select_target(): bekannte rueckwaerts-/abbrechende
Beschriftungen (zweisprachig) werden ausgeschlossen, danach wird bevorzugt
wer eine bekannte vorwaerts-Beschriftung traegt, sonst der einzige
verbleibende Kandidat (Sole-Survivor) - deckt auch unbekannte/nicht
uebersetzte Labels ab. Bei echter Mehrdeutigkeit (mehrere unbekannte
Kandidaten gleichzeitig) wird bewusst NICHT geklickt statt zu raten.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
a415de1eaa
commit
6e99b3bf3a
1
SOURCE_ISO.sha256
Normal file
1
SOURCE_ISO.sha256
Normal file
@ -0,0 +1 @@
|
|||||||
|
fa2dc8d20357d0bd4443b724617498802d3ed772e2f154d3d3690e711595a74b Fedora-Cinnamon-Live-44-1.7.x86_64.iso
|
||||||
Binary file not shown.
@ -12,13 +12,27 @@ ADR-0023-Nachtrag). AT-SPI identifiziert den Button stattdessen ueber seine
|
|||||||
Rolle (PUSH_BUTTON) und seinen Namen im Accessibility-Baum der Anwendung
|
Rolle (PUSH_BUTTON) und seinen Namen im Accessibility-Baum der Anwendung
|
||||||
selbst - unabhaengig von Fenstergeometrie/-dekoration.
|
selbst - unabhaengig von Fenstergeometrie/-dekoration.
|
||||||
|
|
||||||
Sprache ist durch "debian-installer/language=de" im Kexec-Append-Preseed
|
WICHTIG (real beim Testen entdeckt, 29.08.2026): trotz fest gesetztem
|
||||||
fest auf de_DE gesetzt (siehe backend.sh) - die Button-Beschriftungen unten
|
"debian-installer/language=de" sind NICHT alle Assistentenseiten auf
|
||||||
sind daher als feste deutsche Werteliste hinterlegt, nicht lokalisierungs-
|
Deutsch beschriftet - die Mint-eigene "Multimedia-Codecs"-Seite zeigt reale
|
||||||
unabhaengig ermittelt.
|
englische Buttons ("Quit"/"Back"/"Continue"), vermutlich dieselbe Ursache
|
||||||
|
wie der bereits gepatchte ubi-prepare.py-None-Bug (fehlender/unvollstaendiger
|
||||||
|
.mo-Uebersetzungskatalog fuer dieses Plugin auf dem Live-Medium). Eine reine
|
||||||
|
Werteliste bekannter deutscher Beschriftungen (fruehere Version dieses
|
||||||
|
Skripts) haette hier fuer immer haengen bleiben. Die Logik wurde deshalb auf
|
||||||
|
eine sprachunabhaengige Ausschluss-Strategie umgestellt: bekannte
|
||||||
|
rueckwaerts-/abbrechende Beschriftungen (zweisprachig) werden aus der
|
||||||
|
Kandidatenliste entfernt, der bzw. die verbleibenden Kandidaten werden
|
||||||
|
geklickt - siehe select_target() fuer die genaue Regel samt
|
||||||
|
Sicherheitsverhalten bei Mehrdeutigkeit.
|
||||||
|
|
||||||
Exit-Codes: 0 = ein passender Button wurde gefunden und geklickt,
|
Sprache ist durch "debian-installer/language=de" im Kexec-Append-Preseed
|
||||||
1 = kein passender Button gefunden (z.B. Seite gerade im Ladezustand),
|
grundsaetzlich auf de_DE gesetzt (siehe backend.sh), s.o. aber nicht
|
||||||
|
verlaesslich fuer jede einzelne Seite.
|
||||||
|
|
||||||
|
Exit-Codes: 0 = ein Button wurde gefunden und geklickt,
|
||||||
|
1 = kein eindeutiger Button gefunden (z.B. Seite im Ladezustand, oder echte
|
||||||
|
Mehrdeutigkeit - dann bewusst NICHT geklickt, siehe select_target()),
|
||||||
2 = pyatspi/AT-SPI-Bus nicht erreichbar (Bruecke evtl. nicht aktiv).
|
2 = pyatspi/AT-SPI-Bus nicht erreichbar (Bruecke evtl. nicht aktiv).
|
||||||
"""
|
"""
|
||||||
import sys
|
import sys
|
||||||
@ -29,70 +43,78 @@ except ImportError:
|
|||||||
print("pyatspi nicht verfuegbar", file=sys.stderr)
|
print("pyatspi nicht verfuegbar", file=sys.stderr)
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
# Bewusst nur vorwaerts-/abschliessende Aktionen - "Zurueck", "Abbrechen"
|
# Zweisprachig, da nicht jede Seite verlaesslich deutsch beschriftet ist
|
||||||
# und "Beenden" (Installer-Quit) duerfen hier NIE stehen, auch nicht
|
# (siehe Modul-Docstring). Diese Namen werden aus der Kandidatenliste
|
||||||
# versehentlich, da ein falscher Klick sonst den automatisierten Ablauf
|
# AUSGESCHLOSSEN, nie geklickt - unabhaengig davon, ob sie der einzige
|
||||||
# abbricht statt ihn voranzutreiben.
|
# sichtbare Button waeren.
|
||||||
FORWARD_LABELS = {
|
EXCLUDE_LABELS = {
|
||||||
"Weiter",
|
"zurück",
|
||||||
"Jetzt installieren",
|
"zurueck",
|
||||||
"Fortfahren",
|
"back",
|
||||||
"Installieren",
|
"abbrechen",
|
||||||
"Neu starten",
|
"abbruch",
|
||||||
"Schließen",
|
"cancel",
|
||||||
|
"beenden",
|
||||||
|
"quit",
|
||||||
|
"exit",
|
||||||
|
"verwerfen",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Bevorzugt geklickt, falls mehrere Kandidaten nach dem Ausschluss uebrig
|
||||||
|
# bleiben (siehe select_target()). Kein Ausschlusskriterium fuer
|
||||||
|
# Kandidaten, die HIER nicht drin stehen - die duerfen trotzdem als
|
||||||
|
# alleiniger Kandidat geklickt werden (Sole-Survivor-Regel), damit auch
|
||||||
|
# unbekannte/nicht uebersetzte Beschriftungen wie das reale "Continue"
|
||||||
|
# oben nicht zum Stillstand fuehren.
|
||||||
|
PREFER_LABELS = {
|
||||||
|
"weiter",
|
||||||
|
"continue",
|
||||||
|
"next",
|
||||||
|
"jetzt installieren",
|
||||||
|
"install now",
|
||||||
|
"fortfahren",
|
||||||
|
"proceed",
|
||||||
|
"installieren",
|
||||||
|
"install",
|
||||||
|
"neu starten",
|
||||||
|
"restart now",
|
||||||
|
"schließen",
|
||||||
|
"schliessen",
|
||||||
|
"close",
|
||||||
|
"finish",
|
||||||
|
"fertigstellen",
|
||||||
|
"done",
|
||||||
}
|
}
|
||||||
|
|
||||||
MAX_DEPTH = 40
|
MAX_DEPTH = 40
|
||||||
|
|
||||||
|
|
||||||
def find_forward_button(node, depth=0):
|
def collect_buttons(node, depth=0, acc=None):
|
||||||
if node is None or depth > MAX_DEPTH:
|
"""Alle sichtbaren+aktivierten Push-Buttons im Baum einsammeln (Rolle,
|
||||||
return None
|
Zustand). Absichtlich ALLE Kandidaten sammeln statt beim ersten Treffer
|
||||||
try:
|
abzubrechen - select_target() braucht die vollstaendige Liste, um
|
||||||
role = node.getRole()
|
Mehrdeutigkeit erkennen zu koennen."""
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if role == pyatspi.ROLE_PUSH_BUTTON:
|
|
||||||
name = (node.name or "").strip()
|
|
||||||
if name in FORWARD_LABELS:
|
|
||||||
try:
|
|
||||||
state = node.getState()
|
|
||||||
if state.contains(pyatspi.STATE_SENSITIVE) and state.contains(
|
|
||||||
pyatspi.STATE_SHOWING
|
|
||||||
):
|
|
||||||
return node
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
count = node.childCount
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
for i in range(count):
|
|
||||||
try:
|
|
||||||
child = node.getChildAtIndex(i)
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
found = find_forward_button(child, depth + 1)
|
|
||||||
if found is not None:
|
|
||||||
return found
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def collect_button_names(node, depth=0, acc=None):
|
|
||||||
"""Nur fuer Diagnose-Logging: alle sichtbaren Buttonnamen einsammeln,
|
|
||||||
damit ein Nicht-Treffer im Log nachvollziehbar bleibt, ohne dass man
|
|
||||||
interaktiv am laufenden System nachsehen muss."""
|
|
||||||
if acc is None:
|
if acc is None:
|
||||||
acc = []
|
acc = []
|
||||||
if node is None or depth > MAX_DEPTH:
|
if node is None or depth > MAX_DEPTH:
|
||||||
return acc
|
return acc
|
||||||
try:
|
try:
|
||||||
if node.getRole() == pyatspi.ROLE_PUSH_BUTTON:
|
role = node.getRole()
|
||||||
acc.append((node.name or "").strip())
|
except Exception:
|
||||||
return acc
|
return acc
|
||||||
|
|
||||||
|
if role == pyatspi.ROLE_PUSH_BUTTON:
|
||||||
|
try:
|
||||||
|
state = node.getState()
|
||||||
|
if state.contains(pyatspi.STATE_SENSITIVE) and state.contains(
|
||||||
|
pyatspi.STATE_SHOWING
|
||||||
|
):
|
||||||
|
acc.append(node)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return acc
|
||||||
|
|
||||||
|
try:
|
||||||
count = node.childCount
|
count = node.childCount
|
||||||
except Exception:
|
except Exception:
|
||||||
return acc
|
return acc
|
||||||
@ -101,10 +123,47 @@ def collect_button_names(node, depth=0, acc=None):
|
|||||||
child = node.getChildAtIndex(i)
|
child = node.getChildAtIndex(i)
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
collect_button_names(child, depth + 1, acc)
|
collect_buttons(child, depth + 1, acc)
|
||||||
return acc
|
return acc
|
||||||
|
|
||||||
|
|
||||||
|
def select_target(buttons):
|
||||||
|
"""Waehlt aus allen sichtbaren+aktivierten Buttons genau einen aus, der
|
||||||
|
geklickt werden darf - oder None, wenn das nicht sicher moeglich ist.
|
||||||
|
|
||||||
|
Regel (bewusst sicherheitsorientiert, siehe Modul-Docstring):
|
||||||
|
1. Bekannte rueckwaerts-/abbrechende Beschriftungen (EXCLUDE_LABELS)
|
||||||
|
werden nie geklickt, unabhaengig vom Rest.
|
||||||
|
2. Von den verbleibenden Kandidaten wird bevorzugt, wer eine bekannte
|
||||||
|
vorwaerts-/abschliessende Beschriftung traegt (PREFER_LABELS).
|
||||||
|
3. Bleibt nach 1./2. KEIN bekannter Treffer, aber genau EIN Kandidat
|
||||||
|
uebrig, wird dieser geklickt (Sole-Survivor-Regel - deckt unbekannte/
|
||||||
|
nicht uebersetzte Beschriftungen wie das real beobachtete "Continue"
|
||||||
|
ab, siehe Modul-Docstring).
|
||||||
|
4. Bleiben mehrere unbekannte Kandidaten gleichzeitig uebrig, wird NICHT
|
||||||
|
geklickt (echte Mehrdeutigkeit - ein Fehlklick waere hier riskanter
|
||||||
|
als ein uebersprungener Zyklus, die Schleife in autoclicker.sh
|
||||||
|
versucht es 2 Sekunden spaeter erneut)."""
|
||||||
|
candidates = []
|
||||||
|
for btn in buttons:
|
||||||
|
name = (btn.name or "").strip().lower()
|
||||||
|
if name in EXCLUDE_LABELS:
|
||||||
|
continue
|
||||||
|
candidates.append(btn)
|
||||||
|
|
||||||
|
if not candidates:
|
||||||
|
return None
|
||||||
|
|
||||||
|
preferred = [b for b in candidates if (b.name or "").strip().lower() in PREFER_LABELS]
|
||||||
|
if preferred:
|
||||||
|
return preferred[0]
|
||||||
|
|
||||||
|
if len(candidates) == 1:
|
||||||
|
return candidates[0]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
try:
|
try:
|
||||||
desktop = pyatspi.Registry.getDesktop(0)
|
desktop = pyatspi.Registry.getDesktop(0)
|
||||||
@ -112,7 +171,7 @@ def main():
|
|||||||
print(f"AT-SPI-Desktop nicht erreichbar: {exc}", file=sys.stderr)
|
print(f"AT-SPI-Desktop nicht erreichbar: {exc}", file=sys.stderr)
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
seen_buttons = []
|
all_buttons = []
|
||||||
for i in range(desktop.childCount):
|
for i in range(desktop.childCount):
|
||||||
try:
|
try:
|
||||||
app = desktop.getChildAtIndex(i)
|
app = desktop.getChildAtIndex(i)
|
||||||
@ -120,26 +179,27 @@ def main():
|
|||||||
continue
|
continue
|
||||||
if app is None:
|
if app is None:
|
||||||
continue
|
continue
|
||||||
|
collect_buttons(app, 0, all_buttons)
|
||||||
|
|
||||||
btn = find_forward_button(app)
|
target = select_target(all_buttons)
|
||||||
if btn is not None:
|
if target is None:
|
||||||
try:
|
names = [repr((b.name or "").strip()) for b in all_buttons]
|
||||||
action = btn.queryAction()
|
print(
|
||||||
action.doAction(0)
|
"kein eindeutiger Button gefunden, sichtbare Buttons: "
|
||||||
except Exception as exc:
|
+ (", ".join(names) if names else "(keine)"),
|
||||||
print(f"Klick auf '{btn.name}' fehlgeschlagen: {exc}", file=sys.stderr)
|
file=sys.stderr,
|
||||||
return 1
|
)
|
||||||
print(f"geklickt: {btn.name}")
|
return 1
|
||||||
return 0
|
|
||||||
|
|
||||||
seen_buttons.extend(collect_button_names(app))
|
try:
|
||||||
|
action = target.queryAction()
|
||||||
|
action.doAction(0)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"Klick auf '{target.name}' fehlgeschlagen: {exc}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
print(
|
print(f"geklickt: {target.name}")
|
||||||
"kein passender Button gefunden, sichtbare Buttons: "
|
return 0
|
||||||
+ (", ".join(repr(n) for n in seen_buttons) if seen_buttons else "(keine)"),
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user