Merge pull request #115119 from Nintorch/joypad-unfocused

Add project setting to ignore joypad events if the app is unfocused
This commit is contained in:
Thaddeus Crews 2026-03-04 11:20:39 -06:00
commit bb1e018314
No known key found for this signature in database
GPG key ID: 8C6E5FEB5FC03CCC
9 changed files with 167 additions and 6 deletions

View file

@ -159,6 +159,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);
@ -207,6 +209,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);
@ -376,6 +379,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));
}
@ -1011,6 +1018,7 @@ void Input::_parse_input_event_impl(const Ref<InputEvent> &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) {
@ -1057,6 +1065,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;
@ -1072,6 +1086,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();
@ -1090,6 +1109,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();
@ -1104,6 +1128,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();
@ -1262,6 +1291,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;
}
@ -1287,6 +1321,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_
@ -1553,17 +1598,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<StringName, Input::ActionState> &E : action_states) {
if (E.value.cache.pressed) {
action_release(E.key);
for (KeyValue<StringName, Input::ActionState> &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<StringName, Input::ActionState> &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<int, ActionState::DeviceState> &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;
}
}
}
}
}
}
@ -1574,6 +1651,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);
@ -1602,6 +1684,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];
@ -1671,6 +1757,11 @@ void Input::joy_axis(int p_device, JoyAxis p_axis, float p_value) {
void Input::joy_hat(int p_device, BitField<HatMask> p_val) {
_THREAD_SAFE_METHOD_;
if (_should_ignore_joypad_events()) {
return;
}
const Joypad &joy = joy_names[p_device];
JoyEvent map[(size_t)HatDir::MAX];
@ -1713,6 +1804,11 @@ void Input::joy_hat(int p_device, BitField<HatMask> 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) {
@ -2275,6 +2371,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() {

View file

@ -126,6 +126,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;
@ -140,6 +143,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;
@ -307,6 +311,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)();
@ -321,6 +327,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();
@ -424,6 +432,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);

View file

@ -751,6 +751,9 @@
<member name="emulate_touch_from_mouse" type="bool" setter="set_emulate_touch_from_mouse" getter="is_emulating_touch_from_mouse">
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].
</member>
<member name="ignore_joypad_on_unfocused_application" type="bool" setter="set_ignore_joypad_on_unfocused_application" getter="is_ignoring_joypad_on_unfocused_application">
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.
</member>
<member name="mouse_mode" type="int" setter="set_mouse_mode" getter="get_mouse_mode" enum="Input.MouseMode">
Controls the mouse mode.
</member>

View file

@ -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.
</member>
<member name="input_devices/joypads/ignore_joypad_on_unfocused_application" type="bool" setter="" getter="" default="false">
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.
</member>
<member name="input_devices/pen_tablet/driver" type="String" setter="" getter="">
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].

View file

@ -248,6 +248,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<EditorDebuggerSession> &I : sessions) {
if (I->is_active()) {
I->send_message("scene:report_window_focused", message);
}
}
}
void GameViewDebugger::reset_camera_2d_position() {
for (Ref<EditorDebuggerSession> &I : sessions) {
if (I->is_active()) {
@ -987,6 +998,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();

View file

@ -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();

View file

@ -33,6 +33,7 @@
#include "core/config/engine.h"
#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"
@ -506,6 +507,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<String, SceneDebugger::ParseMessageFunc> SceneDebugger::message_handlers;
@ -582,6 +594,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) {

View file

@ -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);

View file

@ -921,9 +921,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;