From 7a0e758253f4d98cb2aed95d9b925c7f78dbc48d Mon Sep 17 00:00:00 2001 From: Bad Sector Date: Tue, 14 Jan 2025 10:21:02 +0200 Subject: [PATCH] Preserve mouse and key state when reparenting controls. --- scene/gui/control.cpp | 13 +++++- scene/main/viewport.cpp | 88 +++++++++++++++++++++++++++++++++++++++++ scene/main/viewport.h | 3 ++ 3 files changed, 102 insertions(+), 2 deletions(-) diff --git a/scene/gui/control.cpp b/scene/gui/control.cpp index 92b5f327775..6eaa334f01d 100644 --- a/scene/gui/control.cpp +++ b/scene/gui/control.cpp @@ -184,12 +184,21 @@ bool Control::_edit_use_rect() const { void Control::reparent(Node *p_parent, bool p_keep_global_transform) { ERR_MAIN_THREAD_GUARD; + Viewport *vp = get_viewport(); if (p_keep_global_transform) { Transform2D temp = get_global_transform(); - Node::reparent(p_parent); + if (vp) { + vp->_gui_reparent_control(this, p_parent); + } else { + Node::reparent(p_parent); + } set_global_position(temp.get_origin()); } else { - Node::reparent(p_parent); + if (vp) { + vp->_gui_reparent_control(this, p_parent); + } else { + Node::reparent(p_parent); + } } } diff --git a/scene/main/viewport.cpp b/scene/main/viewport.cpp index d0054baeff2..85b479c8725 100644 --- a/scene/main/viewport.cpp +++ b/scene/main/viewport.cpp @@ -2337,6 +2337,93 @@ void Viewport::_gui_remove_root_control(List::Element *RI) { gui.roots.erase(RI); } +void Viewport::_gui_send_fake_release_button_event(Control *p_control, int p_button) { + Ref mb; + Viewport *vp = p_control->get_viewport(); + bool save_handle_input_locally = vp->handle_input_locally; + bool save_local_input_handled = vp->local_input_handled; + vp->handle_input_locally = true; + vp->local_input_handled = false; + mb.instantiate(); + mb->set_position(p_control->get_local_mouse_position()); + mb->set_global_position(p_control->get_global_mouse_position()); + mb->set_button_index(MouseButton(p_button)); + mb->set_pressed(false); + mb->set_device(InputEvent::DEVICE_ID_EMULATION); + p_control->_call_gui_input(mb); + vp->handle_input_locally = save_handle_input_locally; + vp->local_input_handled = save_local_input_handled; +} + +void Viewport::_gui_reparent_control(Control *p_control, Node *p_parent) { + DEV_ASSERT(p_control); + DEV_ASSERT(p_parent); + + bool restore_mouse_focus = false; + BitField mouse_focus_mask; + bool restore_key_focus = false; + + if (p_control == gui.mouse_focus) { + restore_mouse_focus = true; + mouse_focus_mask = gui.mouse_focus_mask; + gui.mouse_focus = nullptr; // do not generate focus loss events + gui.mouse_focus_mask.clear(); + } + if (p_control == gui.key_focus) { + restore_key_focus = true; + gui.key_focus = nullptr; + } + + p_control->Node::reparent(p_parent); + + // Restore state + if (restore_mouse_focus || restore_key_focus) { + // Find subviewport ancestor, if any + Viewport *vp = nullptr; + for (Node *ancestor = p_parent; ancestor != nullptr; ancestor = ancestor->get_parent()) { + if (Object::cast_to(ancestor)) { + vp = static_cast(ancestor); + break; + } + } + if (vp == nullptr) { + vp = p_control->get_viewport(); + } + if (vp) { + // If the viewport changed, the 'release' events will go to the original viewport instead of the new one, + // which can leave a control in an intermediate state. To avoid this we generate fake events for releasing + // the mouse buttons and key focus + if (vp != this) { + if (restore_mouse_focus) { + for (int i = 0; i < (int)MouseButton::MB_XBUTTON2; i++) { + if ((int)mouse_focus_mask & (1 << i)) { + // The request to reparent a control could come from within a press event handler, so we + // use a deferred call instead of generating the release event immediately to avoid the + // release event handler being called before the press event handler exits and thus ending + // up with a control in a mixed state (e.g. the state of the control after release handler + // exits mixed with the state of the control after the call to reparent is made until the + // press handler exits) + call_deferred("_gui_send_fake_release_button_event", p_control, i + 1); + } + } + } + if (restore_key_focus) { + p_control->notification(Control::NOTIFICATION_FOCUS_EXIT, true); + p_control->queue_redraw(); + } + } else { // otherwise restore the state + if (restore_mouse_focus) { + gui.mouse_focus = p_control; + gui.mouse_focus_mask = mouse_focus_mask; + } + if (restore_key_focus) { + gui.key_focus = p_control; + } + } + } + } +} + void Viewport::_gui_unfocus_control(Control *p_control) { if (gui.key_focus == p_control) { gui.key_focus->release_focus(); @@ -4834,6 +4921,7 @@ void Viewport::_bind_methods() { ClassDB::bind_method(D_METHOD("set_disable_input", "disable"), &Viewport::set_disable_input); ClassDB::bind_method(D_METHOD("is_input_disabled"), &Viewport::is_input_disabled); + ClassDB::bind_method(D_METHOD("_gui_send_fake_release_button_event"), &Viewport::_gui_send_fake_release_button_event); ClassDB::bind_method(D_METHOD("_gui_remove_focus_for_window"), &Viewport::_gui_remove_focus_for_window); ClassDB::bind_method(D_METHOD("set_positional_shadow_atlas_size", "size"), &Viewport::set_positional_shadow_atlas_size); diff --git a/scene/main/viewport.h b/scene/main/viewport.h index 5260b513a95..de86e8321a0 100644 --- a/scene/main/viewport.h +++ b/scene/main/viewport.h @@ -435,6 +435,9 @@ private: void _gui_remove_root_control(List::Element *RI); + void _gui_send_fake_release_button_event(Control *p_control, int p_button); + void _gui_reparent_control(Control *p_control, Node *p_parent); + String _gui_get_tooltip(Control *p_control, const Vector2 &p_pos, Control **r_tooltip_owner = nullptr); void _gui_cancel_tooltip(); void _gui_show_tooltip();