"""Private native controls for declarative Tentacle preference pages.""" import copy from native_x11 import wait_for def probe_pages(): return [{"Session": "private-probe", "Revision": 1, "id": "probe", "title": "Extension probe", "command": "apply", "load": True, "elements": [{"id": "text", "label": "Extension text", "kind": 2, "required": True, "max_length": 32}, {"id": "choice", "label": "Extension choice", "kind": 1, "items": [{"id": "first", "label": "First choice"}, {"id": "second", "label": "Second choice"}]}, {"id": "enabled", "label": "Extension enabled", "kind": 3}], "values": {"text": {"text": "Initial"}, "choice": {"selected": "first"}, "enabled": {"checked": False}}}] def check_page(pyatspi, process, window, nodes, walk, focus, key, request, send, initial): def find(name, role=None): return next(node for node in nodes if node.name == name and (role is None or node.getRole() == role)) def focused(node): return any(child.getState().contains(pyatspi.STATE_FOCUSED) for child in walk(node)) tabs = next(node for node in nodes if node.getRole() == pyatspi.ROLE_PAGE_TAB_LIST) index = next(index for index, node in enumerate(tabs) if node.name == "Extension probe") assert tabs.querySelection().selectChild(index) text = find("Extension text", pyatspi.ROLE_TEXT) choice = find("Extension choice", pyatspi.ROLE_COMBO_BOX) enabled = find("Extension enabled", pyatspi.ROLE_CHECK_BOX) cancelButton = find("Cancel", pyatspi.ROLE_PUSH_BUTTON) assert text.queryText().getText(0, -1) == "Initial" labels = [node for node in nodes if node.name == "Extension text" and node.getRole() == pyatspi.ROLE_LABEL] assert labels and any(relation.getRelationType() == pyatspi.RELATION_LABEL_FOR for relation in labels[0].getRelationSet()), "missing label buddy" status = find(initial["Message"]) focus(text) key("Tab") key("Down") # GTK focuses an internal toggle button that need not expose FOCUSED in # AT-SPI. A keyboard selection change proves that Tab reached the combo. wait_for(lambda: choice.querySelection().getSelectedChild(0).name == "Second choice", "Tab did not reach the combo or Down did not change its value") key("Tab") wait_for(lambda: focused(enabled), "extension combo trapped Tab") key("space") key("shift+Tab") key("Up") wait_for(lambda: choice.querySelection().getSelectedChild(0).name == "First choice", "Shift+Tab did not return to the combo") key("space") wait_for(lambda: any(node.getRole() == pyatspi.ROLE_MENU and node.getState().contains(pyatspi.STATE_SHOWING) for node in walk(choice)), "extension combo popup did not open from the keyboard") key("End", "Return") wait_for(lambda: choice.querySelection().getSelectedChild(0).name == "Second choice", "popup selection was not accepted") key("shift+Tab") wait_for(lambda: focused(text), "extension combo trapped Shift+Tab") key("ctrl+a") key("e", "d", "i", "t") wait_for(lambda: text.queryText().getText(0, -1) == "edit", "entry edit missing") # A status refresh with unchanged revisions must preserve the active draft. send({"ExtensionsOnly": True, "ExtensionPages": initial["ExtensionPages"]}) wait_for(lambda: focused(text), "refresh changed focus") assert text.queryText().getText(0, -1) == "edit" focus(find("Apply", pyatspi.ROLE_PUSH_BUTTON)); key("space") draft = request() page = draft["ExtensionPages"][0] assert page["Session"] == "private-probe" and page["Revision"] == 1 assert page["Values"] == {"text": {"text": "edit"}, "choice": {"selected": "second"}, "enabled": {"checked": True}}, page send({"Accepted": False, "CanSave": True, "Message": "Probe rejected"}) wait_for(lambda: status.name == "Probe rejected", "rejection missing") assert text.queryText().getText(0, -1) == "edit", "rejection discarded draft" focus(find("Apply", pyatspi.ROLE_PUSH_BUTTON)); key("space") draft = request() pages = copy.deepcopy(initial["ExtensionPages"]) pages[0]["Revision"] = 2 pages[0]["values"] = draft["ExtensionPages"][0]["Values"] send({"Accepted": True, "CanSave": True, "Values": draft["Values"], "ExtensionPages": pages, "Message": "Probe applied"}) wait_for(lambda: status.name == "Probe applied", "apply response missing") assert tabs.querySelection().getSelectedChild(0).name == "Extension probe", "Apply changed the selected tab" nodes = list(walk(window)) text = find("Extension text", pyatspi.ROLE_TEXT) assert text.queryText().getText(0, -1) == "edit", "applied values not restored" # Removing a focused page must leave the core controls usable. focus(text) send({"ExtensionsOnly": True, "ExtensionPages": []}) wait_for(lambda: not any(node.name == "Extension probe" and node.getRole() == pyatspi.ROLE_PAGE_TAB for node in tabs), "revoked extension tab survived") assert tabs.querySelection().getSelectedChild(0).name == "General", "removal did not return to a core page" focus(cancelButton); key("space") assert process.wait(timeout=5) == 0 print("PASS: extension labels, entries, combos, checkboxes, Tab/Shift+Tab, draft retention, Apply rejection/acceptance and focused-page removal", flush=True)