Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 0 additions & 62 deletions libs/cua-driver/rust/crates/platform-linux/src/atspi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,68 +125,6 @@ pub fn perform_action_at_screen_point(
native::perform_action_at_screen_point(pid, xid, screen_x, screen_y)
}

/// Try to type text into any editable field in the window via AT-SPI EditableText.
/// This works for unfocused windows if the toolkit exposes EditableText (Qt6, some GTK).
/// For Qt5, which doesn't expose widgets when unfocused, this will return Err.
/// Returns Ok if an editable was found and text was set, Err otherwise.
pub fn type_into_editable(pid: u32, text: &str) -> Result<()> {
let safe_text = text.replace('\\', "\\\\").replace('\'', "\\'");
let script = format!(r#"
import pyatspi, sys

def find_editable(acc, depth=0):
# Try to find any EditableText interface, regardless of role
try:
et = acc.queryEditableText()
# If we can query it, return this node
return acc
except:
pass

# Recursively search children
try:
for child in acc:
result = find_editable(child, depth + 1)
if result is not None:
return result
except:
pass

return None

desktop = pyatspi.Registry.getDesktop(0)
editable = None
for app in desktop:
try:
if app.get_process_id() == {pid}:
for win in app:
editable = find_editable(win)
if editable:
break
break
except:
pass

if editable is None:
print("ERROR: No editable found", file=sys.stderr)
sys.exit(1)

try:
et = editable.queryEditableText()
et.setTextContents('{safe_text}')
print("ok:atspi")
except Exception as e:
print(f"ERROR: {{e}}", file=sys.stderr)
sys.exit(1)
"#, pid = pid, safe_text = safe_text);

let out = std::process::Command::new("python3").arg("-c").arg(&script).output()?;
if !out.status.success() {
anyhow::bail!("{}", String::from_utf8_lossy(&out.stderr).trim().to_owned());
}
Ok(())
}

/// Set the text value of element `idx` within pid's app tree via AT-SPI.
/// Tries `EditableText.set_text_contents(value)` first, then
/// `Value.set_current_value(float)`.
Expand Down
14 changes: 7 additions & 7 deletions libs/cua-driver/rust/crates/platform-linux/src/tools/impl_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1929,21 +1929,21 @@ impl Tool for TypeTextTool {
};
}

// Try AT-SPI EditableText first (focus-free, works for Qt6/GTK4).
// Try native AT-SPI EditableText first (focus-free, works for Qt6/GTK4).
let text_clone = text.clone();
let atspi_result = tokio::task::spawn_blocking(move || {
crate::atspi::type_into_editable(pid, &text_clone)
crate::atspi::insert_text(pid, &text_clone)
}).await;

match atspi_result {
Ok(Ok(())) => {
Ok(Ok(true)) => {
// AT-SPI succeeded — focus-free typing worked (Qt6, GTK4, etc.)!
// Electron/Chromium can echo this write without the renderer
// observing it, so the confirm is suppressed there (mirrors macOS).
return type_text_ax_confirm_result(pid, text_len, "via AT-SPI");
}
_ => {
// AT-SPI failed (no editable exposed). Qt5 doesn't expose widgets
// AT-SPI failed or found no editable. Qt5 doesn't expose widgets
// when unfocused, so try the synthetic-focus workaround.
}
}
Expand All @@ -1957,8 +1957,8 @@ impl Tool for TypeTextTool {
crate::input::send_focus_in(xid)?;
std::thread::sleep(std::time::Duration::from_millis(100));

// Try AT-SPI again now that widgets should be exposed
let result = crate::atspi::type_into_editable(pid, &text_clone2);
// Try native AT-SPI again now that widgets should be exposed
let result = crate::atspi::insert_text(pid, &text_clone2);

// Restore state with FocusOut
crate::input::send_focus_out(xid)?;
Expand All @@ -1967,7 +1967,7 @@ impl Tool for TypeTextTool {
}).await;

match qt5_result {
Ok(Ok(())) => {
Ok(Ok(true)) => {
return type_text_ax_confirm_result(
pid, text_len, "via AT-SPI with focus workaround",
);
Expand Down
Loading