Skip to content

Commit

Permalink
Add method to simulate input events.
Browse files Browse the repository at this point in the history
  • Loading branch information
bruvzg committed May 27, 2024
1 parent be56cab commit f827252
Show file tree
Hide file tree
Showing 11 changed files with 460 additions and 0 deletions.
38 changes: 38 additions & 0 deletions doc/classes/DisplayServer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1174,6 +1174,41 @@
[b]Note:[/b] This method is implemented on Android, iOS, macOS, Windows, and Linux (X11/Wayland).
</description>
</method>
<method name="simulate_keypress">
<return type="bool" />
<param index="0" name="keycode" type="int" enum="Key" />
<param index="1" name="modifiers" type="int" enum="KeyModifierMask" is_bitfield="true" />
<param index="2" name="pressed" type="bool" />
<description>
Simulates a key press event which can be handled by the operating system, not just Godot (as opposed to [method Input.parse_input_event]). The key press event is typically sent to the currently focused window, but some apps may have global shortcuts active.
[b]Note:[/b] The key press does not change based on keyboard layout, so if you are using this to input text, use [method DisplayServer.keyboard_get_keycode_from_physical] to convert the key to the user's keyboard layout.
[b]Note:[/b] Simulated input events are ignored by login screen, and other security related OS UI elements.
[b]Note:[/b] On macOS, this method requires "Accessibility" permission.
[b]Note:[/b] This method is implemented on macOS, Windows, and Linux (X11).
</description>
</method>
<method name="simulate_mouse_click">
<return type="bool" />
<param index="0" name="button" type="int" enum="MouseButton" />
<param index="1" name="modifiers" type="int" enum="KeyModifierMask" is_bitfield="true" />
<param index="2" name="pressed" type="bool" />
<description>
Simulates a mouse button or wheel event which can be handled by the operating system, not just Godot (as opposed to [method Input.parse_input_event]).
[b]Note:[/b] Simulated input events are ignored by login screen, and other security related OS UI elements.
[b]Note:[/b] On macOS, this method requires "Accessibility" permission.
[b]Note:[/b] This method is implemented on macOS, Windows, and Linux (X11).
</description>
</method>
<method name="simulate_unicode_input">
<return type="bool" />
<param index="0" name="text" type="String" />
<description>
Simulates an Unicode string input which can be handled by the operating system, not just Godot (as opposed to [method Input.parse_input_event]). The key press event is typically sent to the currently focused window.
[b]Note:[/b] Simulated input events are ignored by login screen, and other security related OS UI elements.
[b]Note:[/b] On macOS, this method requires "Accessibility" permission.
[b]Note:[/b] This method is implemented on macOS and Windows.
</description>
</method>
<method name="status_indicator_get_rect" qualifiers="const">
<return type="Rect2" />
<param index="0" name="id" type="int" />
Expand Down Expand Up @@ -1860,6 +1895,9 @@
<constant name="FEATURE_NATIVE_DIALOG_FILE" value="25" enum="Feature">
Display server supports spawning dialogs for selecting files or directories using the operating system's native look-and-feel. See [method file_dialog_show] and [method file_dialog_with_options_show]. [b]Windows, macOS, Linux (X11/Wayland)[/b]
</constant>
<constant name="FEATURE_INPUT_SIMULATION" value="26" enum="Feature">
Display server supports input simulation, see [method simulate_mouse_click], [method simulate_keypress], and [method simulate_unicode_input]. [b]Windows, macOS, Linux (X11)[/b]
</constant>
<constant name="MOUSE_MODE_VISIBLE" value="0" enum="MouseMode">
Makes the mouse cursor visible if it is hidden.
</constant>
Expand Down
73 changes: 73 additions & 0 deletions platform/linuxbsd/x11/display_server_x11.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ bool DisplayServerX11::has_feature(Feature p_feature) const {
case FEATURE_CLIPBOARD_PRIMARY:
case FEATURE_TEXT_TO_SPEECH:
case FEATURE_SCREEN_CAPTURE:
case FEATURE_INPUT_SIMULATION:
return true;
default: {
}
Expand Down Expand Up @@ -3285,6 +3286,78 @@ Key DisplayServerX11::keyboard_get_label_from_physical(Key p_keycode) const {
}
return (Key)(key | modifiers);
}

bool DisplayServerX11::simulate_mouse_click(MouseButton p_button, BitField<KeyModifierMask> p_modifiers, bool p_pressed) {
XEvent event;
memset(&event, 0, sizeof(event));
event.xbutton.button = (unsigned int)p_button;
event.xbutton.same_screen = True;
event.xbutton.subwindow = DefaultRootWindow(x11_display);
while (event.xbutton.subwindow) {
event.xbutton.window = event.xbutton.subwindow;
XQueryPointer(x11_display, event.xbutton.window, &event.xbutton.root, &event.xbutton.subwindow, &event.xbutton.x_root, &event.xbutton.y_root, &event.xbutton.x, &event.xbutton.y, &event.xbutton.state);
}
event.xbutton.type = p_pressed ? ButtonPress : ButtonRelease;
event.xbutton.state = 0;
if (p_modifiers.has_flag(KeyModifierMask::SHIFT)) {
event.xkey.state |= ShiftMask;
}
if (p_modifiers.has_flag(KeyModifierMask::CTRL) || p_modifiers.has_flag(KeyModifierMask::CMD_OR_CTRL)) {
event.xkey.state |= ControlMask;
}
if (p_modifiers.has_flag(KeyModifierMask::ALT)) {
event.xkey.state |= Mod1Mask;
}
if (p_modifiers.has_flag(KeyModifierMask::META)) {
event.xkey.state |= Mod4Mask;
}

bool ok = (XSendEvent(x11_display, PointerWindow, True, ButtonPressMask | ButtonReleaseMask, &event) == 0);
XSync(x11_display, False);
return ok;
}

bool DisplayServerX11::simulate_keypress(Key p_keycode, BitField<KeyModifierMask> p_modifiers, bool p_pressed) {
unsigned int xlib_keycode = KeyMappingX11::get_xlibcode(p_keycode & KeyModifierMask::CODE_MASK);

Window focused_window;
int focus_ret_state;
XGetInputFocus(x11_display, &focused_window, &focus_ret_state);

XEvent event;
memset(&event, 0, sizeof(event));
event.xkey.type = p_pressed ? KeyPress : KeyRelease;
event.xkey.keycode = xlib_keycode;
event.xkey.root = DefaultRootWindow(x11_display);
event.xkey.window = focused_window;
event.xkey.display = x11_display;
XQueryPointer(x11_display, event.xkey.window, &event.xkey.root, &event.xkey.subwindow, &event.xkey.x_root, &event.xkey.y_root, &event.xkey.x, &event.xkey.y, &event.xkey.state);
event.xkey.same_screen = True;
event.xkey.state = 0;
if (p_modifiers.has_flag(KeyModifierMask::SHIFT)) {
event.xkey.state |= ShiftMask;
}
if (p_modifiers.has_flag(KeyModifierMask::CTRL) || p_modifiers.has_flag(KeyModifierMask::CMD_OR_CTRL)) {
event.xkey.state |= ControlMask;
}
if (p_modifiers.has_flag(KeyModifierMask::ALT)) {
event.xkey.state |= Mod1Mask;
}
if (p_modifiers.has_flag(KeyModifierMask::META)) {
event.xkey.state |= Mod4Mask;
}

bool ok = (XSendEvent(x11_display, focused_window, True, KeyPressMask | KeyReleaseMask, &event) == 0);
XSync(x11_display, False);

return ok;
}

bool DisplayServerX11::simulate_unicode_input(const String &p_text) {
// TODO
return false;
}

DisplayServerX11::Property DisplayServerX11::_read_property(Display *p_display, Window p_window, Atom p_property) {
Atom actual_type = None;
int actual_format = 0;
Expand Down
6 changes: 6 additions & 0 deletions platform/linuxbsd/x11/display_server_x11.h
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,8 @@ class DisplayServerX11 : public DisplayServer {
void _poll_events();
void _check_pending_events(LocalVector<XEvent> &r_events);

unsigned int _find_free_keycode() const;

static Bool _predicate_all_events(Display *display, XEvent *event, XPointer arg);
static Bool _predicate_clipboard_selection(Display *display, XEvent *event, XPointer arg);
static Bool _predicate_clipboard_incr(Display *display, XEvent *event, XPointer arg);
Expand Down Expand Up @@ -523,6 +525,10 @@ class DisplayServerX11 : public DisplayServer {
virtual Key keyboard_get_keycode_from_physical(Key p_keycode) const override;
virtual Key keyboard_get_label_from_physical(Key p_keycode) const override;

virtual bool simulate_mouse_click(MouseButton p_button, BitField<KeyModifierMask> p_modifiers, bool p_pressed) override;
virtual bool simulate_keypress(Key p_keycode, BitField<KeyModifierMask> p_modifiers, bool p_pressed) override;
virtual bool simulate_unicode_input(const String &p_text) override;

virtual void process_events() override;

virtual void release_rendering_thread() override;
Expand Down
4 changes: 4 additions & 0 deletions platform/macos/display_server_macos.h
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,10 @@ class DisplayServerMacOS : public DisplayServer {
virtual Key keyboard_get_keycode_from_physical(Key p_keycode) const override;
virtual Key keyboard_get_label_from_physical(Key p_keycode) const override;

virtual bool simulate_mouse_click(MouseButton p_button, BitField<KeyModifierMask> p_modifiers, bool p_pressed) override;
virtual bool simulate_keypress(Key p_keycode, BitField<KeyModifierMask> p_modifiers, bool p_pressed) override;
virtual bool simulate_unicode_input(const String &p_text) override;

virtual void process_events() override;
virtual void force_process_and_drop_events() override;

Expand Down
122 changes: 122 additions & 0 deletions platform/macos/display_server_macos.mm
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,7 @@
case FEATURE_SCREEN_CAPTURE:
case FEATURE_STATUS_INDICATOR:
case FEATURE_NATIVE_HELP:
case FEATURE_INPUT_SIMULATION:
return true;
default: {
}
Expand Down Expand Up @@ -2984,6 +2985,127 @@
return (Key)(KeyMappingMacOS::remap_key(macos_keycode, 0, true) | modifiers);
}

bool DisplayServerMacOS::simulate_mouse_click(MouseButton p_button, BitField<KeyModifierMask> p_modifiers, bool p_pressed) {
Point2 position_on_screen = mouse_get_position();
position_on_screen.y *= -1;
position_on_screen += _get_screens_origin();
position_on_screen /= screen_get_max_scale();
CGPoint mouse_pos = { position_on_screen.x, CGDisplayBounds(CGMainDisplayID()).size.height - position_on_screen.y };

CGEventType mouse_type = kCGEventNull;
CGMouseButton mouse_button = (CGMouseButton)0;
Vector2i wheel_factor;
switch (p_button) {
case MouseButton::LEFT: {
mouse_type = p_pressed ? kCGEventLeftMouseDown : kCGEventLeftMouseUp;
mouse_button = (CGMouseButton)0;
} break;
case MouseButton::RIGHT: {
mouse_type = p_pressed ? kCGEventRightMouseDown : kCGEventRightMouseUp;
mouse_button = (CGMouseButton)1;
} break;
case MouseButton::MIDDLE: {
mouse_type = p_pressed ? kCGEventOtherMouseDown : kCGEventOtherMouseUp;
mouse_button = (CGMouseButton)2;
} break;
case MouseButton::MB_XBUTTON1: {
mouse_type = p_pressed ? kCGEventOtherMouseDown : kCGEventOtherMouseUp;
mouse_button = (CGMouseButton)3;
} break;
case MouseButton::MB_XBUTTON2: {
mouse_type = p_pressed ? kCGEventOtherMouseDown : kCGEventOtherMouseUp;
mouse_button = (CGMouseButton)4;
} break;
case MouseButton::WHEEL_UP: {
mouse_type = kCGEventScrollWheel;
wheel_factor = Vector2i(-1, 0);
} break;
case MouseButton::WHEEL_DOWN: {
mouse_type = kCGEventScrollWheel;
wheel_factor = Vector2i(1, 0);
} break;
case MouseButton::WHEEL_LEFT: {
mouse_type = kCGEventScrollWheel;
wheel_factor = Vector2i(0, -1);
} break;
case MouseButton::WHEEL_RIGHT: {
mouse_type = kCGEventScrollWheel;
wheel_factor = Vector2i(0, 1);
} break;
default:
break;
}
if (mouse_type == kCGEventScrollWheel) {
CGEventRef event = CGEventCreateScrollWheelEvent(nullptr, kCGScrollEventUnitLine, 2, wheel_factor.x, wheel_factor.y);
CGEventFlags flags = 0;
if (p_modifiers.has_flag(KeyModifierMask::SHIFT)) {
flags |= kCGEventFlagMaskShift;
}
if (p_modifiers.has_flag(KeyModifierMask::ALT)) {
flags |= kCGEventFlagMaskAlternate;
}
if (p_modifiers.has_flag(KeyModifierMask::META) || p_modifiers.has_flag(KeyModifierMask::CMD_OR_CTRL)) {
flags |= kCGEventFlagMaskCommand;
}
if (p_modifiers.has_flag(KeyModifierMask::CTRL)) {
flags |= kCGEventFlagMaskControl;
}
CGEventSetFlags(event, flags);
CGEventPost(kCGHIDEventTap, event);
CFRelease(event);
} else {
CGEventRef event = CGEventCreateMouseEvent(nullptr, mouse_type, mouse_pos, mouse_button);
CGEventFlags flags = 0;
if (p_modifiers.has_flag(KeyModifierMask::SHIFT)) {
flags |= kCGEventFlagMaskShift;
}
if (p_modifiers.has_flag(KeyModifierMask::ALT)) {
flags |= kCGEventFlagMaskAlternate;
}
if (p_modifiers.has_flag(KeyModifierMask::META) || p_modifiers.has_flag(KeyModifierMask::CMD_OR_CTRL)) {
flags |= kCGEventFlagMaskCommand;
}
if (p_modifiers.has_flag(KeyModifierMask::CTRL)) {
flags |= kCGEventFlagMaskControl;
}
CGEventSetFlags(event, flags);
CGEventPost(kCGHIDEventTap, event);
CFRelease(event);
}
return true;
}

bool DisplayServerMacOS::simulate_keypress(Key p_keycode, BitField<KeyModifierMask> p_modifiers, bool p_pressed) {
unsigned int macos_keycode = KeyMappingMacOS::unmap_key(p_keycode & KeyModifierMask::CODE_MASK);
CGEventRef event = CGEventCreateKeyboardEvent(nullptr, (CGKeyCode)macos_keycode, p_pressed);
CGEventFlags flags = 0;
if (p_modifiers.has_flag(KeyModifierMask::SHIFT)) {
flags |= kCGEventFlagMaskShift;
}
if (p_modifiers.has_flag(KeyModifierMask::ALT)) {
flags |= kCGEventFlagMaskAlternate;
}
if (p_modifiers.has_flag(KeyModifierMask::META) || p_modifiers.has_flag(KeyModifierMask::CMD_OR_CTRL)) {
flags |= kCGEventFlagMaskCommand;
}
if (p_modifiers.has_flag(KeyModifierMask::CTRL)) {
flags |= kCGEventFlagMaskControl;
}
CGEventSetFlags(event, flags);
CGEventPost(kCGHIDEventTap, event);
CFRelease(event);
return true;
}

bool DisplayServerMacOS::simulate_unicode_input(const String &p_text) {
CGEventRef event = CGEventCreateKeyboardEvent(nullptr, 0, true);
Char16String text16 = p_text.utf16();
CGEventKeyboardSetUnicodeString(event, text16.length(), (const UniChar *)text16.get_data());
CGEventPost(kCGHIDEventTap, event);
CFRelease(event);
return true;
}

void DisplayServerMacOS::process_events() {
ERR_FAIL_COND(!Thread::is_main_thread());

Expand Down
Loading

0 comments on commit f827252

Please sign in to comment.