From d418d692b91952ae5f3f0fb3d58fd0300b3089d2 Mon Sep 17 00:00:00 2001 From: Nintorch <92302738+Nintorch@users.noreply.github.com> Date: Thu, 26 Feb 2026 21:08:31 +0500 Subject: [PATCH] Project setting to ignore joypads on unfocused app Co-Authored-By: Christoph Taucher <4498502+chtau@users.noreply.github.com> --- core/input/input.cpp | 107 ++++++++++++++++++++++++++++-- core/input/input.h | 11 +++ doc/classes/Input.xml | 3 + doc/classes/ProjectSettings.xml | 3 + editor/run/game_view_plugin.cpp | 19 ++++++ editor/run/game_view_plugin.h | 2 + scene/debugger/scene_debugger.cpp | 13 ++++ scene/debugger/scene_debugger.h | 1 + scene/main/scene_tree.cpp | 14 +++- 9 files changed, 167 insertions(+), 6 deletions(-) diff --git a/core/input/input.cpp b/core/input/input.cpp index 6d716cf91d..f1488135c3 100644 --- a/core/input/input.cpp +++ b/core/input/input.cpp @@ -158,6 +158,8 @@ void Input::_bind_methods() { ClassDB::bind_method(D_METHOD("start_joy_vibration", "device", "weak_magnitude", "strong_magnitude", "duration"), &Input::start_joy_vibration, DEFVAL(0)); ClassDB::bind_method(D_METHOD("stop_joy_vibration", "device"), &Input::stop_joy_vibration); ClassDB::bind_method(D_METHOD("vibrate_handheld", "duration_ms", "amplitude"), &Input::vibrate_handheld, DEFVAL(500), DEFVAL(-1.0)); + ClassDB::bind_method(D_METHOD("set_ignore_joypad_on_unfocused_application", "enable"), &Input::set_ignore_joypad_on_unfocused_application); + ClassDB::bind_method(D_METHOD("is_ignoring_joypad_on_unfocused_application"), &Input::is_ignoring_joypad_on_unfocused_application); ClassDB::bind_method(D_METHOD("get_gravity"), &Input::get_gravity); ClassDB::bind_method(D_METHOD("get_accelerometer"), &Input::get_accelerometer); ClassDB::bind_method(D_METHOD("get_magnetometer"), &Input::get_magnetometer); @@ -206,6 +208,7 @@ void Input::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_accumulated_input"), "set_use_accumulated_input", "is_using_accumulated_input"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "emulate_mouse_from_touch"), "set_emulate_mouse_from_touch", "is_emulating_mouse_from_touch"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "emulate_touch_from_mouse"), "set_emulate_touch_from_mouse", "is_emulating_touch_from_mouse"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "ignore_joypad_on_unfocused_application"), "set_ignore_joypad_on_unfocused_application", "is_ignoring_joypad_on_unfocused_application"); BIND_ENUM_CONSTANT(MOUSE_MODE_VISIBLE); BIND_ENUM_CONSTANT(MOUSE_MODE_HIDDEN); @@ -375,6 +378,10 @@ bool Input::is_mouse_button_pressed(MouseButton p_button) const { return mouse_button_mask.has_flag(mouse_button_to_mask(p_button)); } +bool Input::_should_ignore_joypad_events() const { + return ignore_joypad_on_unfocused_application && !application_focused && !embedder_focused; +} + static JoyAxis _combine_device(JoyAxis p_value, int p_device) { return JoyAxis((int)p_value | (p_device << 20)); } @@ -1010,6 +1017,7 @@ void Input::_parse_input_event_impl(const Ref &p_event, bool p_is_em device_state.pressed[event_index] = is_pressed; device_state.strength[event_index] = p_event->get_action_strength(E.key); device_state.raw_strength[event_index] = p_event->get_action_raw_strength(E.key); + device_state.event_type[event_index] = p_event->get_type(); // Update the action's global state and cache. if (!is_pressed) { @@ -1056,6 +1064,12 @@ void Input::set_joy_features(int p_device, JoypadFeatures *p_features) { } void Input::set_joy_light(int p_device, const Color &p_color) { + _THREAD_SAFE_METHOD_ + + if (_should_ignore_joypad_events()) { + return; + } + Joypad *joypad = joy_names.getptr(p_device); if (!joypad || !joypad->has_light || joypad->features == nullptr) { return; @@ -1071,6 +1085,11 @@ bool Input::has_joy_light(int p_device) const { Vector3 Input::get_joy_accelerometer(int p_device) const { _THREAD_SAFE_METHOD_ + + if (_should_ignore_joypad_events()) { + return Vector3(); + } + const MotionInfo *motion = joy_motion.getptr(p_device); if (motion == nullptr) { return Vector3(); @@ -1089,6 +1108,11 @@ Vector3 Input::get_joy_accelerometer(int p_device) const { Vector3 Input::get_joy_gravity(int p_device) const { _THREAD_SAFE_METHOD_ + + if (_should_ignore_joypad_events()) { + return Vector3(); + } + const MotionInfo *motion = joy_motion.getptr(p_device); if (motion == nullptr) { return Vector3(); @@ -1103,6 +1127,11 @@ Vector3 Input::get_joy_gravity(int p_device) const { Vector3 Input::get_joy_gyroscope(int p_device) const { _THREAD_SAFE_METHOD_ + + if (_should_ignore_joypad_events()) { + return Vector3(); + } + const MotionInfo *motion = joy_motion.getptr(p_device); if (motion == nullptr) { return Vector3(); @@ -1261,6 +1290,11 @@ void Input::set_joy_motion_sensors_rate(int p_device, float p_rate) { void Input::start_joy_vibration(int p_device, float p_weak_magnitude, float p_strong_magnitude, float p_duration) { _THREAD_SAFE_METHOD_ + + if (_should_ignore_joypad_events()) { + return; + } + if (p_weak_magnitude < 0.f || p_weak_magnitude > 1.f || p_strong_magnitude < 0.f || p_strong_magnitude > 1.f) { return; } @@ -1286,6 +1320,17 @@ void Input::vibrate_handheld(int p_duration_ms, float p_amplitude) { OS::get_singleton()->vibrate_handheld(p_duration_ms, p_amplitude); } +void Input::set_ignore_joypad_on_unfocused_application(bool p_ignore) { + ignore_joypad_on_unfocused_application = p_ignore; + if (_should_ignore_joypad_events()) { + release_pressed_events(); + } +} + +bool Input::is_ignoring_joypad_on_unfocused_application() const { + return ignore_joypad_on_unfocused_application; +} + void Input::set_gravity(const Vector3 &p_gravity) { _THREAD_SAFE_METHOD_ @@ -1552,17 +1597,49 @@ bool Input::is_using_accumulated_input() { } void Input::release_pressed_events() { + // Don't release the events if the application (or the window it's embedded in) is still focused. + if (application_focused || embedder_focused) { + return; + } + flush_buffered_events(); // this is needed to release actions strengths keys_pressed.clear(); physical_keys_pressed.clear(); key_label_pressed.clear(); - joy_buttons_pressed.clear(); - _joy_axis.clear(); + if (ignore_joypad_on_unfocused_application) { + joy_buttons_pressed.clear(); + _joy_axis.clear(); - for (KeyValue &E : action_states) { - if (E.value.cache.pressed) { - action_release(E.key); + for (KeyValue &E : action_states) { + if (E.value.cache.pressed) { + action_release(E.key); + } + } + + for (int device : get_connected_joypads()) { + stop_joy_vibration(device); + } + } else { + for (KeyValue &E : action_states) { + if (E.value.cache.pressed) { + bool is_only_joypad_pressed = true; + int max_event = InputMap::get_singleton()->action_get_events(E.key)->size() + 1; // +1 comes from InputEventAction. + + for (const KeyValue &kv : E.value.device_states) { + const ActionState::DeviceState &device_state = kv.value; + for (int i = 0; i < max_event; i++) { + if (device_state.event_type[i] != InputEventType::JOY_MOTION && device_state.event_type[i] != InputEventType::JOY_BUTTON && device_state.strength[i] > 0.0f) { + is_only_joypad_pressed = false; + action_release(E.key); + break; + } + } + if (!is_only_joypad_pressed) { + break; + } + } + } } } } @@ -1573,6 +1650,11 @@ void Input::set_event_dispatch_function(EventDispatchFunc p_function) { void Input::joy_button(int p_device, JoyButton p_button, bool p_pressed) { _THREAD_SAFE_METHOD_; + + if (_should_ignore_joypad_events()) { + return; + } + Joypad &joy = joy_names[p_device]; ERR_FAIL_INDEX((int)p_button, (int)JoyButton::MAX); @@ -1601,6 +1683,10 @@ void Input::joy_button(int p_device, JoyButton p_button, bool p_pressed) { void Input::joy_axis(int p_device, JoyAxis p_axis, float p_value) { _THREAD_SAFE_METHOD_; + if (_should_ignore_joypad_events()) { + return; + } + ERR_FAIL_INDEX((int)p_axis, (int)JoyAxis::MAX); Joypad &joy = joy_names[p_device]; @@ -1670,6 +1756,11 @@ void Input::joy_axis(int p_device, JoyAxis p_axis, float p_value) { void Input::joy_hat(int p_device, BitField p_val) { _THREAD_SAFE_METHOD_; + + if (_should_ignore_joypad_events()) { + return; + } + const Joypad &joy = joy_names[p_device]; JoyEvent map[(size_t)HatDir::MAX]; @@ -1712,6 +1803,11 @@ void Input::joy_hat(int p_device, BitField p_val) { void Input::joy_motion_sensors(int p_device, const Vector3 &p_accelerometer, const Vector3 &p_gyroscope) { _THREAD_SAFE_METHOD_ + + if (_should_ignore_joypad_events()) { + return; + } + // TODO: events MotionInfo *motion = joy_motion.getptr(p_device); if (motion == nullptr) { @@ -2274,6 +2370,7 @@ Input::Input() { gravity_enabled = GLOBAL_DEF_RST_BASIC("input_devices/sensors/enable_gravity", false); gyroscope_enabled = GLOBAL_DEF_RST_BASIC("input_devices/sensors/enable_gyroscope", false); magnetometer_enabled = GLOBAL_DEF_RST_BASIC("input_devices/sensors/enable_magnetometer", false); + ignore_joypad_on_unfocused_application = GLOBAL_DEF_RST_BASIC("input_devices/joypads/ignore_joypad_on_unfocused_application", false); } Input::~Input() { diff --git a/core/input/input.h b/core/input/input.h index 4fe7662a33..afefe91936 100644 --- a/core/input/input.h +++ b/core/input/input.h @@ -125,6 +125,9 @@ private: int64_t mouse_window = 0; bool legacy_just_pressed_behavior = false; bool disable_input = false; + bool ignore_joypad_on_unfocused_application = false; + bool application_focused = true; + bool embedder_focused = false; struct ActionState { uint64_t pressed_physics_frame = UINT64_MAX; @@ -139,6 +142,7 @@ private: bool pressed[MAX_EVENT] = { false }; float strength[MAX_EVENT] = { 0.0 }; float raw_strength[MAX_EVENT] = { 0.0 }; + InputEventType event_type[MAX_EVENT] = { InputEventType::INVALID }; }; bool api_pressed = false; float api_strength = 0.0; @@ -306,6 +310,8 @@ private: #endif friend class DisplayServer; + friend class SceneTree; + friend class SceneDebugger; static void (*set_mouse_mode_func)(MouseMode); static MouseMode (*get_mouse_mode_func)(); @@ -320,6 +326,8 @@ private: EventDispatchFunc event_dispatch_function = nullptr; + bool _should_ignore_joypad_events() const; + #ifndef DISABLE_DEPRECATED void _vibrate_handheld_bind_compat_91143(int p_duration_ms = 500); static void _bind_compatibility_methods(); @@ -423,6 +431,9 @@ public: void stop_joy_vibration(int p_device); void vibrate_handheld(int p_duration_ms = 500, float p_amplitude = -1.0); + void set_ignore_joypad_on_unfocused_application(bool p_ignore); + bool is_ignoring_joypad_on_unfocused_application() const; + void set_mouse_position(const Point2 &p_posf); void action_press(const StringName &p_action, float p_strength = 1.f); diff --git a/doc/classes/Input.xml b/doc/classes/Input.xml index f99d275556..ee7d3aa640 100644 --- a/doc/classes/Input.xml +++ b/doc/classes/Input.xml @@ -751,6 +751,9 @@ If [code]true[/code], sends touch input events when clicking or dragging the mouse. See also [member ProjectSettings.input_devices/pointing/emulate_touch_from_mouse]. + + If [code]true[/code], joypad input (including motion sensors) and LED light changes will be ignored and joypad vibration will be stopped when the application is not focused. + Controls the mouse mode. diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index 93b7b14d9b..b5ce49092b 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -1662,6 +1662,9 @@ If [code]false[/code], no input will be lost. [b]Note:[/b] You should in nearly all cases prefer the [code]false[/code] setting. The legacy behavior is to enable supporting old projects that rely on the old logic, without changes to script. + + If [code]true[/code], joypad input (including motion sensors) and LED light changes will be ignored and joypad vibration will be stopped when the application is not focused. + Specifies the tablet driver to use. If left empty, the default driver will be used. [b]Note:[/b] The driver in use can be overridden at runtime via the [code]--tablet-driver[/code] [url=$DOCS_URL/tutorials/editor/command_line_tutorial.html]command line argument[/url]. diff --git a/editor/run/game_view_plugin.cpp b/editor/run/game_view_plugin.cpp index 99df061e7a..632c1b882d 100644 --- a/editor/run/game_view_plugin.cpp +++ b/editor/run/game_view_plugin.cpp @@ -245,6 +245,17 @@ void GameViewDebugger::set_camera_manipulate_mode(EditorDebuggerNode::CameraOver } } +void GameViewDebugger::report_window_focused(bool p_focused) { + Array message; + message.append(p_focused); + + for (Ref &I : sessions) { + if (I->is_active()) { + I->send_message("scene:report_window_focused", message); + } + } +} + void GameViewDebugger::reset_camera_2d_position() { for (Ref &I : sessions) { if (I->is_active()) { @@ -984,6 +995,14 @@ void GameView::_notification(int p_what) { _update_ui(); } break; + + case NOTIFICATION_WM_WINDOW_FOCUS_IN: + case NOTIFICATION_WM_WINDOW_FOCUS_OUT: { + if (embed_on_play) { + debugger->report_window_focused(p_what == NOTIFICATION_WM_WINDOW_FOCUS_IN); + } + } break; + case NOTIFICATION_WM_POSITION_CHANGED: { if (window_wrapper->get_window_enabled()) { _update_floating_window_settings(); diff --git a/editor/run/game_view_plugin.h b/editor/run/game_view_plugin.h index 8857cda337..a42a4bf91a 100644 --- a/editor/run/game_view_plugin.h +++ b/editor/run/game_view_plugin.h @@ -101,6 +101,8 @@ public: void set_camera_override(bool p_enabled); void set_camera_manipulate_mode(EditorDebuggerNode::CameraOverride p_mode); + void report_window_focused(bool p_focused); + void reset_camera_2d_position(); void reset_camera_3d_position(); diff --git a/scene/debugger/scene_debugger.cpp b/scene/debugger/scene_debugger.cpp index f0b0b8b282..3dc3afd2bf 100644 --- a/scene/debugger/scene_debugger.cpp +++ b/scene/debugger/scene_debugger.cpp @@ -32,6 +32,7 @@ #include "core/debugger/debugger_marshalls.h" #include "core/debugger/engine_debugger.h" +#include "core/input/input.h" #include "core/input/shortcut.h" #include "core/io/dir_access.h" #include "core/io/resource_loader.h" @@ -503,6 +504,17 @@ Error SceneDebugger::_msg_rq_screenshot(const Array &p_args) { return OK; } +Error SceneDebugger::_msg_report_window_focused(const Array &p_args) { + ERR_FAIL_COND_V(p_args.is_empty(), ERR_INVALID_DATA); + + bool focused = p_args[0]; + Input::get_singleton()->embedder_focused = focused; + if (Input::get_singleton()->_should_ignore_joypad_events()) { + Input::get_singleton()->release_pressed_events(); + } + return OK; +} + // endregion HashMap SceneDebugger::message_handlers; @@ -579,6 +591,7 @@ void SceneDebugger::_init_message_handlers() { message_handlers["runtime_node_select_reset_camera_3d"] = _msg_runtime_node_select_reset_camera_3d; #endif message_handlers["rq_screenshot"] = _msg_rq_screenshot; + message_handlers["report_window_focused"] = _msg_report_window_focused; } void SceneDebugger::_save_node(ObjectID id, const String &p_path) { diff --git a/scene/debugger/scene_debugger.h b/scene/debugger/scene_debugger.h index 1960fc73ab..9acc093ab2 100644 --- a/scene/debugger/scene_debugger.h +++ b/scene/debugger/scene_debugger.h @@ -106,6 +106,7 @@ private: static Error _msg_runtime_node_select_set_avoid_locked(const Array &p_args); static Error _msg_runtime_node_select_set_prefer_group(const Array &p_args); static Error _msg_rq_screenshot(const Array &p_args); + static Error _msg_report_window_focused(const Array &p_args); static Error _msg_runtime_node_select_reset_camera_2d(const Array &p_args); static Error _msg_transform_camera_2d(const Array &p_args); diff --git a/scene/main/scene_tree.cpp b/scene/main/scene_tree.cpp index 8f5e40c244..b5fd95ba25 100644 --- a/scene/main/scene_tree.cpp +++ b/scene/main/scene_tree.cpp @@ -918,9 +918,21 @@ void SceneTree::_notification(int p_notification) { case NOTIFICATION_WM_ABOUT: case NOTIFICATION_CRASH: case NOTIFICATION_APPLICATION_RESUMED: - case NOTIFICATION_APPLICATION_PAUSED: + case NOTIFICATION_APPLICATION_PAUSED: { + // Pass these to nodes, since they are mirrored. + get_root()->propagate_notification(p_notification); + } break; + case NOTIFICATION_APPLICATION_FOCUS_IN: case NOTIFICATION_APPLICATION_FOCUS_OUT: { + if (Input::get_singleton()) { + Input::get_singleton()->application_focused = p_notification == NOTIFICATION_APPLICATION_FOCUS_IN; + + if (Input::get_singleton()->_should_ignore_joypad_events()) { + Input::get_singleton()->release_pressed_events(); + } + } + // Pass these to nodes, since they are mirrored. get_root()->propagate_notification(p_notification); } break;