Enable Drag and Drop for SubViewports and Windows

Make Drag and Drop an application-wide operation.
This allows do drop on Controls in other Viewports/Windows.

In order to achieve this, `Viewport::_update_mouse_over` is adjusted to
remember the Control, that the mouse is over (possibly within nested
viewports). This Control is used as a basis for the Drop-operation, which
replaces the previous algorithm, which was only aware of the topmost
Viewport.

Also now all nodes in the SceneTree are notified about the Drag and Drop
operation, with the exception of SubViewports that are not children of
SubViewportContainers.
This commit is contained in:
Markus Sauermann 2023-01-20 00:21:11 +01:00
parent 6681f2563b
commit 60aaa017ff
7 changed files with 213 additions and 148 deletions

View File

@ -151,7 +151,7 @@
<method name="gui_is_dragging" qualifiers="const">
<return type="bool" />
<description>
Returns [code]true[/code] if the viewport is currently performing a drag operation.
Returns [code]true[/code] if a drag operation is currently ongoing and where the drop action could happen in this viewport.
Alternative to [constant Node.NOTIFICATION_DRAG_BEGIN] and [constant Node.NOTIFICATION_DRAG_END] when you prefer polling the value.
</description>
</method>

View File

@ -1787,12 +1787,6 @@ void DisplayServerX11::delete_sub_window(WindowID p_id) {
_send_window_event(windows[p_id], WINDOW_EVENT_MOUSE_EXIT);
}
window_set_rect_changed_callback(Callable(), p_id);
window_set_window_event_callback(Callable(), p_id);
window_set_input_event_callback(Callable(), p_id);
window_set_input_text_callback(Callable(), p_id);
window_set_drop_files_callback(Callable(), p_id);
while (wd.transient_children.size()) {
window_set_transient(*wd.transient_children.begin(), INVALID_WINDOW_ID);
}
@ -1836,6 +1830,12 @@ void DisplayServerX11::delete_sub_window(WindowID p_id) {
XUnmapWindow(x11_display, wd.x11_window);
XDestroyWindow(x11_display, wd.x11_window);
window_set_rect_changed_callback(Callable(), p_id);
window_set_window_event_callback(Callable(), p_id);
window_set_input_event_callback(Callable(), p_id);
window_set_input_text_callback(Callable(), p_id);
window_set_drop_files_callback(Callable(), p_id);
windows.erase(p_id);
}

View File

@ -1235,14 +1235,16 @@ Ref<World2D> Viewport::find_world_2d() const {
}
}
void Viewport::_propagate_viewport_notification(Node *p_node, int p_what) {
void Viewport::_propagate_drag_notification(Node *p_node, int p_what) {
// Send notification to p_node and all children and descendant nodes of p_node, except to SubViewports which are not children of a SubViewportContainer.
p_node->notification(p_what);
bool is_svc = Object::cast_to<SubViewportContainer>(p_node);
for (int i = 0; i < p_node->get_child_count(); i++) {
Node *c = p_node->get_child(i);
if (Object::cast_to<Viewport>(c)) {
if (!is_svc && Object::cast_to<SubViewport>(c)) {
continue;
}
_propagate_viewport_notification(c, p_what);
Viewport::_propagate_drag_notification(c, p_what);
}
}
@ -1345,7 +1347,7 @@ Ref<InputEvent> Viewport::_make_input_local(const Ref<InputEvent> &ev) {
Vector2 Viewport::get_mouse_position() const {
ERR_READ_THREAD_GUARD_V(Vector2());
if (!is_directly_attached_to_screen()) {
if (get_section_root_viewport() != SceneTree::get_singleton()->get_root()) {
// Rely on the most recent mouse coordinate from an InputEventMouse in push_input.
// In this case get_screen_transform is not applicable, because it is ambiguous.
return gui.last_mouse_pos;
@ -1701,14 +1703,15 @@ Control *Viewport::_gui_find_control_at_pos(CanvasItem *p_node, const Point2 &p_
}
bool Viewport::_gui_drop(Control *p_at_control, Point2 p_at_pos, bool p_just_check) {
// Attempt grab, try parent controls too.
// Attempt drop, try parent controls too.
CanvasItem *ci = p_at_control;
Viewport *section_root = get_section_root_viewport();
while (ci) {
Control *control = Object::cast_to<Control>(ci);
if (control) {
if (control->can_drop_data(p_at_pos, gui.drag_data)) {
if (control->can_drop_data(p_at_pos, section_root->gui.drag_data)) {
if (!p_just_check) {
control->drop_data(p_at_pos, gui.drag_data);
control->drop_data(p_at_pos, section_root->gui.drag_data);
}
return true;
@ -1806,13 +1809,13 @@ void Viewport::_gui_input_event(Ref<InputEvent> p_event) {
if (gui.dragging && mb->get_button_index() == MouseButton::LEFT) {
// Alternate drop use (when using force_drag(), as proposed by #5342).
_perform_drop(gui.mouse_focus, pos);
_perform_drop(gui.mouse_focus);
}
_gui_cancel_tooltip();
} else {
if (gui.dragging && mb->get_button_index() == MouseButton::LEFT) {
_perform_drop(gui.drag_mouse_over, gui.drag_mouse_over_pos);
_perform_drop(gui.drag_mouse_over);
}
gui.mouse_focus_mask.clear_flag(mouse_button_to_mask(mb->get_button_index())); // Remove from mask.
@ -1848,7 +1851,8 @@ void Viewport::_gui_input_event(Ref<InputEvent> p_event) {
Point2 mpos = mm->get_position();
// Drag & drop.
if (!gui.drag_attempted && gui.mouse_focus && (mm->get_button_mask().has_flag(MouseButtonMask::LEFT))) {
Viewport *section_root = get_section_root_viewport();
if (!gui.drag_attempted && gui.mouse_focus && section_root && !section_root->gui.global_dragging && (mm->get_button_mask().has_flag(MouseButtonMask::LEFT))) {
gui.drag_accum += mm->get_relative();
float len = gui.drag_accum.length();
if (len > 10) {
@ -1857,11 +1861,12 @@ void Viewport::_gui_input_event(Ref<InputEvent> p_event) {
while (ci) {
Control *control = Object::cast_to<Control>(ci);
if (control) {
gui.dragging = true;
gui.drag_data = control->get_drag_data(control->get_global_transform_with_canvas().affine_inverse().xform(mpos - gui.drag_accum));
if (gui.drag_data.get_type() != Variant::NIL) {
section_root->gui.global_dragging = true;
section_root->gui.drag_data = control->get_drag_data(control->get_global_transform_with_canvas().affine_inverse().xform(mpos - gui.drag_accum));
if (section_root->gui.drag_data.get_type() != Variant::NIL) {
gui.mouse_focus = nullptr;
gui.mouse_focus_mask.clear();
gui.dragging = true;
break;
} else {
Control *drag_preview = _gui_get_drag_preview();
@ -1870,7 +1875,7 @@ void Viewport::_gui_input_event(Ref<InputEvent> p_event) {
memdelete(drag_preview);
gui.drag_preview_id = ObjectID();
}
gui.dragging = false;
section_root->gui.global_dragging = false;
}
if (control->data.mouse_filter == Control::MOUSE_FILTER_STOP) {
@ -1888,7 +1893,7 @@ void Viewport::_gui_input_event(Ref<InputEvent> p_event) {
gui.drag_attempted = true;
if (gui.dragging) {
_propagate_viewport_notification(this, NOTIFICATION_DRAG_BEGIN);
Viewport::_propagate_drag_notification(section_root, NOTIFICATION_DRAG_BEGIN);
}
}
}
@ -1986,105 +1991,29 @@ void Viewport::_gui_input_event(Ref<InputEvent> p_event) {
}
if (gui.dragging) {
// Handle drag & drop.
// Handle drag & drop. This happens in the viewport where dragging started.
Control *drag_preview = _gui_get_drag_preview();
if (drag_preview) {
drag_preview->set_position(mpos);
}
gui.drag_mouse_over = over;
gui.drag_mouse_over_pos = Vector2();
// Find the window this is above of.
// See if there is an embedder.
Viewport *embedder = nullptr;
Vector2 viewport_pos;
if (is_embedding_subwindows()) {
embedder = this;
viewport_pos = mpos;
} else {
// Not an embedder, but may be a subwindow of an embedder.
Window *w = Object::cast_to<Window>(this);
if (w) {
if (w->is_embedded()) {
embedder = w->get_embedder();
viewport_pos = get_final_transform().xform(mpos) + w->get_position(); // To parent coords.
}
gui.drag_mouse_over = section_root->gui.target_control;
if (gui.drag_mouse_over) {
if (!_gui_drop(gui.drag_mouse_over, gui.drag_mouse_over->get_local_mouse_position(), true)) {
gui.drag_mouse_over = nullptr;
}
}
Viewport *viewport_under = nullptr;
if (embedder) {
// Use embedder logic.
for (int i = embedder->gui.sub_windows.size() - 1; i >= 0; i--) {
Window *sw = embedder->gui.sub_windows[i].window;
Rect2 swrect = Rect2i(sw->get_position(), sw->get_size());
if (!sw->get_flag(Window::FLAG_BORDERLESS)) {
int title_height = sw->theme_cache.title_height;
swrect.position.y -= title_height;
swrect.size.y += title_height;
}
if (swrect.has_point(viewport_pos)) {
viewport_under = sw;
viewport_pos -= sw->get_position();
}
}
if (!viewport_under) {
// Not in a subwindow, likely in embedder.
viewport_under = embedder;
}
} else {
// Use DisplayServer logic.
Vector2i screen_mouse_pos = DisplayServer::get_singleton()->mouse_get_position();
DisplayServer::WindowID window_id = DisplayServer::get_singleton()->get_window_at_screen_position(screen_mouse_pos);
if (window_id != DisplayServer::INVALID_WINDOW_ID) {
ObjectID object_under = DisplayServer::get_singleton()->window_get_attached_instance_id(window_id);
if (object_under != ObjectID()) { // Fetch window.
Window *w = Object::cast_to<Window>(ObjectDB::get_instance(object_under));
if (w) {
viewport_under = w;
viewport_pos = w->get_final_transform().affine_inverse().xform(screen_mouse_pos - w->get_position());
}
}
}
}
if (viewport_under) {
if (viewport_under != this) {
Transform2D ai = viewport_under->get_final_transform().affine_inverse();
viewport_pos = ai.xform(viewport_pos);
}
// Find control under at position.
gui.drag_mouse_over = viewport_under->gui_find_control(viewport_pos);
if (gui.drag_mouse_over) {
Transform2D localizer = gui.drag_mouse_over->get_global_transform_with_canvas().affine_inverse();
gui.drag_mouse_over_pos = localizer.xform(viewport_pos);
bool can_drop = _gui_drop(gui.drag_mouse_over, gui.drag_mouse_over_pos, true);
if (!can_drop) {
ds_cursor_shape = DisplayServer::CURSOR_FORBIDDEN;
} else {
ds_cursor_shape = DisplayServer::CURSOR_CAN_DROP;
}
ds_cursor_shape = DisplayServer::CURSOR_CAN_DROP;
} else {
ds_cursor_shape = DisplayServer::CURSOR_FORBIDDEN;
}
} else {
gui.drag_mouse_over = nullptr;
}
}
if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CURSOR_SHAPE) && !Object::cast_to<SubViewportContainer>(over)) {
if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CURSOR_SHAPE) && (gui.dragging || (!section_root->gui.global_dragging && !Object::cast_to<SubViewportContainer>(over)))) {
// If dragging is active, then set the cursor shape only from the Viewport where dragging started.
// If dragging is inactive, then set the cursor shape only when not over a SubViewportContainer.
DisplayServer::get_singleton()->cursor_set_shape(ds_cursor_shape);
}
}
@ -2284,10 +2213,10 @@ void Viewport::_gui_input_event(Ref<InputEvent> p_event) {
}
}
void Viewport::_perform_drop(Control *p_control, Point2 p_pos) {
void Viewport::_perform_drop(Control *p_control) {
// Without any arguments, simply cancel Drag and Drop.
if (p_control) {
gui.drag_successful = _gui_drop(p_control, p_pos, false);
gui.drag_successful = _gui_drop(p_control, p_control->get_local_mouse_position(), false);
} else {
gui.drag_successful = false;
}
@ -2298,10 +2227,12 @@ void Viewport::_perform_drop(Control *p_control, Point2 p_pos) {
gui.drag_preview_id = ObjectID();
}
gui.drag_data = Variant();
Viewport *section_root = get_section_root_viewport();
section_root->gui.drag_data = Variant();
gui.dragging = false;
section_root->gui.global_dragging = false;
gui.drag_mouse_over = nullptr;
_propagate_viewport_notification(this, NOTIFICATION_DRAG_END);
Viewport::_propagate_drag_notification(section_root, NOTIFICATION_DRAG_END);
// Display the new cursor shape instantly.
update_mouse_cursor_state();
}
@ -2331,14 +2262,16 @@ void Viewport::_gui_force_drag(Control *p_base, const Variant &p_data, Control *
ERR_FAIL_COND_MSG(p_data.get_type() == Variant::NIL, "Drag data must be a value.");
gui.dragging = true;
gui.drag_data = p_data;
Viewport *section_root = get_section_root_viewport();
section_root->gui.global_dragging = true;
section_root->gui.drag_data = p_data;
gui.mouse_focus = nullptr;
gui.mouse_focus_mask.clear();
if (p_control) {
_gui_set_drag_preview(p_base, p_control);
}
_propagate_viewport_notification(this, NOTIFICATION_DRAG_BEGIN);
Viewport::_propagate_drag_notification(section_root, NOTIFICATION_DRAG_BEGIN);
}
void Viewport::_gui_set_drag_preview(Control *p_base, Control *p_control) {
@ -3022,6 +2955,7 @@ void Viewport::_update_mouse_over() {
}
void Viewport::_update_mouse_over(Vector2 p_pos) {
gui.last_mouse_pos = p_pos; // Necessary, because mouse cursor can be over Viewports that are not reached by the InputEvent.
// Look for embedded windows at mouse position.
if (is_embedding_subwindows()) {
for (int i = gui.sub_windows.size() - 1; i >= 0; i--) {
@ -3073,6 +3007,7 @@ void Viewport::_update_mouse_over(Vector2 p_pos) {
// Look for Controls at mouse position.
Control *over = gui_find_control(p_pos);
get_section_root_viewport()->gui.target_control = over;
bool notify_embedded_viewports = false;
if (over != gui.mouse_over || (!over && !gui.mouse_over_hierarchy.is_empty())) {
// Find the common ancestor of `gui.mouse_over` and `over`.
@ -3195,6 +3130,10 @@ void Viewport::_drop_mouse_over(Control *p_until_control) {
if (gui.mouse_over && gui.mouse_over->is_inside_tree()) {
gui.mouse_over->notification(Control::NOTIFICATION_MOUSE_EXIT_SELF);
}
Viewport *section_root = get_section_root_viewport();
if (section_root && section_root->gui.target_control == gui.mouse_over) {
section_root->gui.target_control = nullptr;
}
gui.mouse_over = nullptr;
// Send Mouse Exit notifications to children first. Don't send to p_until_control or above.
@ -3403,7 +3342,7 @@ bool Viewport::is_input_disabled() const {
Variant Viewport::gui_get_drag_data() const {
ERR_READ_THREAD_GUARD_V(Variant());
return gui.drag_data;
return get_section_root_viewport()->gui.drag_data;
}
PackedStringArray Viewport::get_configuration_warnings() const {
@ -3597,7 +3536,7 @@ bool Viewport::is_snap_2d_vertices_to_pixel_enabled() const {
bool Viewport::gui_is_dragging() const {
ERR_READ_THREAD_GUARD_V(false);
return gui.dragging;
return get_section_root_viewport()->gui.global_dragging;
}
bool Viewport::gui_is_drag_successful() const {
@ -5156,9 +5095,12 @@ Transform2D SubViewport::get_popup_base_transform() const {
return c->get_screen_transform() * container_transform * get_final_transform();
}
bool SubViewport::is_directly_attached_to_screen() const {
// SubViewports, that are used as Textures are not considered to be directly attached to screen.
return Object::cast_to<SubViewportContainer>(get_parent()) && get_parent()->get_viewport() && get_parent()->get_viewport()->is_directly_attached_to_screen();
Viewport *SubViewport::get_section_root_viewport() const {
if (Object::cast_to<SubViewportContainer>(get_parent()) && get_parent()->get_viewport()) {
return get_parent()->get_viewport()->get_section_root_viewport();
}
SubViewport *vp = const_cast<SubViewport *>(this);
return vp;
}
bool SubViewport::is_attached_in_viewport() const {

View File

@ -281,7 +281,7 @@ private:
bool disable_3d = false;
void _propagate_viewport_notification(Node *p_node, int p_what);
static void _propagate_drag_notification(Node *p_node, int p_what);
void _update_global_transform();
@ -362,7 +362,6 @@ private:
Window *subwindow_over = nullptr; // mouse_over and subwindow_over are mutually exclusive. At all times at least one of them is nullptr.
Window *windowmanager_window_over = nullptr; // Only used in root Viewport.
Control *drag_mouse_over = nullptr;
Vector2 drag_mouse_over_pos;
Control *tooltip_control = nullptr;
Window *tooltip_popup = nullptr;
Label *tooltip_label = nullptr;
@ -371,7 +370,7 @@ private:
Point2 last_mouse_pos;
Point2 drag_accum;
bool drag_attempted = false;
Variant drag_data;
Variant drag_data; // Only used in root-Viewport and SubViewports, that are not children of a SubViewportContainer.
ObjectID drag_preview_id;
Ref<SceneTreeTimer> tooltip_timer;
double tooltip_delay = 0.0;
@ -379,8 +378,10 @@ private:
List<Control *> roots;
HashSet<ObjectID> canvas_parents_with_dirty_order;
int canvas_sort_index = 0; //for sorting items with canvas as root
bool dragging = false;
bool dragging = false; // Is true in the viewport in which dragging started while dragging is active.
bool global_dragging = false; // Is true while dragging is active. Only used in root-Viewport and SubViewports that are not children of a SubViewportContainer.
bool drag_successful = false;
Control *target_control = nullptr; // Control that the mouse is over in the innermost nested Viewport. Only used in root-Viewport and SubViewports, that are not children of a SubViewportContainer.
bool embed_subwindows_hint = false;
Window *subwindow_focused = nullptr;
@ -408,7 +409,7 @@ private:
Control *_gui_find_control_at_pos(CanvasItem *p_node, const Point2 &p_global, const Transform2D &p_xform);
void _gui_input_event(Ref<InputEvent> p_event);
void _perform_drop(Control *p_control = nullptr, Point2 p_pos = Point2());
void _perform_drop(Control *p_control = nullptr);
void _gui_cleanup_internal_state(Ref<InputEvent> p_event);
void _push_unhandled_input_internal(const Ref<InputEvent> &p_event);
@ -672,9 +673,9 @@ public:
Transform2D get_screen_transform() const;
virtual Transform2D get_screen_transform_internal(bool p_absolute_position = false) const;
virtual Transform2D get_popup_base_transform() const { return Transform2D(); }
virtual bool is_directly_attached_to_screen() const { return false; };
virtual bool is_attached_in_viewport() const { return false; };
virtual bool is_sub_viewport() const { return false; };
virtual Viewport *get_section_root_viewport() const { return nullptr; }
virtual bool is_attached_in_viewport() const { return false; }
virtual bool is_sub_viewport() const { return false; }
private:
// 2D audio, camera, and physics. (don't put World2D here because World2D is needed for Control nodes).
@ -836,9 +837,9 @@ public:
virtual Transform2D get_screen_transform_internal(bool p_absolute_position = false) const override;
virtual Transform2D get_popup_base_transform() const override;
virtual bool is_directly_attached_to_screen() const override;
virtual Viewport *get_section_root_viewport() const override;
virtual bool is_attached_in_viewport() const override;
virtual bool is_sub_viewport() const override { return true; };
virtual bool is_sub_viewport() const override { return true; }
void _validate_property(PropertyInfo &p_property) const;
SubViewport();

View File

@ -2755,12 +2755,16 @@ Transform2D Window::get_popup_base_transform() const {
return popup_base_transform;
}
bool Window::is_directly_attached_to_screen() const {
Viewport *Window::get_section_root_viewport() const {
if (get_embedder()) {
return get_embedder()->is_directly_attached_to_screen();
return get_embedder()->get_section_root_viewport();
}
// Distinguish between the case that this is a native Window and not inside the tree.
return is_inside_tree();
if (is_inside_tree()) {
// Native window.
return SceneTree::get_singleton()->get_root();
}
Window *vp = const_cast<Window *>(this);
return vp;
}
bool Window::is_attached_in_viewport() const {

View File

@ -469,7 +469,7 @@ public:
virtual Transform2D get_final_transform() const override;
virtual Transform2D get_screen_transform_internal(bool p_absolute_position = false) const override;
virtual Transform2D get_popup_base_transform() const override;
virtual bool is_directly_attached_to_screen() const override;
virtual Viewport *get_section_root_viewport() const override;
virtual bool is_attached_in_viewport() const override;
Rect2i get_parent_rect() const;

View File

@ -119,8 +119,23 @@ public:
class DragTarget : public NotificationControlViewport {
GDCLASS(DragTarget, NotificationControlViewport);
protected:
void _notification(int p_what) {
switch (p_what) {
case NOTIFICATION_DRAG_BEGIN: {
during_drag = true;
} break;
case NOTIFICATION_DRAG_END: {
during_drag = false;
} break;
}
}
public:
Variant drag_data;
bool valid_drop = false;
bool during_drag = false;
virtual bool can_drop_data(const Point2 &p_point, const Variant &p_data) const override {
StringName string_data = p_data;
// Verify drag data is compatible.
@ -136,6 +151,7 @@ public:
virtual void drop_data(const Point2 &p_point, const Variant &p_data) override {
drag_data = p_data;
valid_drop = true;
}
};
@ -1107,12 +1123,10 @@ TEST_CASE("[SceneTree][Viewport] Controls and InputEvent handling") {
SUBCASE("[Viewport][GuiInputEvent] Drag and Drop") {
// FIXME: Drag-Preview will likely change. Tests for this part would have to be rewritten anyway.
// See https://github.com/godotengine/godot/pull/67531#issuecomment-1385353430 for details.
// FIXME: Testing Drag and Drop with non-embedded windows would require DisplayServerMock additions
// FIXME: Drag and Drop currently doesn't work with embedded Windows and SubViewports - not testing.
// See https://github.com/godotengine/godot/issues/28522 for example.
// Note: Testing Drag and Drop with non-embedded windows would require DisplayServerMock additions.
int min_grab_movement = 11;
SUBCASE("[Viewport][GuiInputEvent] Drag from one Control to another in the same viewport.") {
SUBCASE("[Viewport][GuiInputEvent] Perform successful Drag and Drop on a different Control.") {
SUBCASE("[Viewport][GuiInputEvent][DnD] Drag from one Control to another in the same viewport.") {
SUBCASE("[Viewport][GuiInputEvent][DnD] Perform successful Drag and Drop on a different Control.") {
SEND_GUI_MOUSE_BUTTON_EVENT(on_a, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
CHECK_FALSE(root->gui_is_dragging());
@ -1131,7 +1145,7 @@ TEST_CASE("[SceneTree][Viewport] Controls and InputEvent handling") {
CHECK((StringName)node_d->drag_data == SNAME("Drag Data"));
}
SUBCASE("[Viewport][GuiInputEvent] Perform unsuccessful drop on Control.") {
SUBCASE("[Viewport][GuiInputEvent][DnD] Perform unsuccessful drop on Control.") {
SEND_GUI_MOUSE_BUTTON_EVENT(on_a, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
CHECK_FALSE(root->gui_is_dragging());
@ -1157,7 +1171,7 @@ TEST_CASE("[SceneTree][Viewport] Controls and InputEvent handling") {
CHECK_FALSE(root->gui_is_drag_successful());
}
SUBCASE("[Viewport][GuiInputEvent] Perform unsuccessful drop on No-Control.") {
SUBCASE("[Viewport][GuiInputEvent][DnD] Perform unsuccessful drop on No-Control.") {
SEND_GUI_MOUSE_BUTTON_EVENT(on_a, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
CHECK_FALSE(root->gui_is_dragging());
@ -1171,7 +1185,7 @@ TEST_CASE("[SceneTree][Viewport] Controls and InputEvent handling") {
// Move away from Controls.
SEND_GUI_MOUSE_MOTION_EVENT(on_background, MouseButtonMask::LEFT, Key::NONE);
CHECK(DS->get_cursor_shape() == DisplayServer::CURSOR_ARROW); // This could also be CURSOR_FORBIDDEN.
CHECK(DS->get_cursor_shape() == DisplayServer::CURSOR_ARROW);
CHECK(root->gui_is_dragging());
SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_background, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
@ -1179,7 +1193,7 @@ TEST_CASE("[SceneTree][Viewport] Controls and InputEvent handling") {
CHECK_FALSE(root->gui_is_drag_successful());
}
SUBCASE("[Viewport][GuiInputEvent] Perform unsuccessful drop outside of window.") {
SUBCASE("[Viewport][GuiInputEvent][DnD] Perform unsuccessful drop outside of window.") {
SEND_GUI_MOUSE_BUTTON_EVENT(on_a, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
CHECK_FALSE(root->gui_is_dragging());
@ -1192,7 +1206,6 @@ TEST_CASE("[SceneTree][Viewport] Controls and InputEvent handling") {
// Move outside of window.
SEND_GUI_MOUSE_MOTION_EVENT(on_outside, MouseButtonMask::LEFT, Key::NONE);
CHECK(DS->get_cursor_shape() == DisplayServer::CURSOR_ARROW);
CHECK(root->gui_is_dragging());
SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_outside, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
@ -1200,7 +1213,7 @@ TEST_CASE("[SceneTree][Viewport] Controls and InputEvent handling") {
CHECK_FALSE(root->gui_is_drag_successful());
}
SUBCASE("[Viewport][GuiInputEvent] Drag and Drop doesn't work with other Mouse Buttons than LMB.") {
SUBCASE("[Viewport][GuiInputEvent][DnD] Drag and Drop doesn't work with other Mouse Buttons than LMB.") {
SEND_GUI_MOUSE_BUTTON_EVENT(on_a, MouseButton::MIDDLE, MouseButtonMask::MIDDLE, Key::NONE);
CHECK_FALSE(root->gui_is_dragging());
@ -1209,7 +1222,7 @@ TEST_CASE("[SceneTree][Viewport] Controls and InputEvent handling") {
SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_a, MouseButton::MIDDLE, MouseButtonMask::NONE, Key::NONE);
}
SUBCASE("[Viewport][GuiInputEvent] Drag and Drop parent propagation.") {
SUBCASE("[Viewport][GuiInputEvent][DnD] Drag and Drop parent propagation.") {
Node2D *node_aa = memnew(Node2D);
Control *node_aaa = memnew(Control);
Node2D *node_dd = memnew(Node2D);
@ -1318,7 +1331,7 @@ TEST_CASE("[SceneTree][Viewport] Controls and InputEvent handling") {
memdelete(node_aa);
}
SUBCASE("[Viewport][GuiInputEvent] Force Drag and Drop.") {
SUBCASE("[Viewport][GuiInputEvent][DnD] Force Drag and Drop.") {
SEND_GUI_MOUSE_MOTION_EVENT(on_background, MouseButtonMask::NONE, Key::NONE);
CHECK_FALSE(root->gui_is_dragging());
node_a->force_drag(SNAME("Drag Data"), nullptr);
@ -1339,6 +1352,111 @@ TEST_CASE("[SceneTree][Viewport] Controls and InputEvent handling") {
SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_d, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
}
}
SUBCASE("[Viewport][GuiInputEvent][DnD] Drag to a different Viewport.") {
SubViewportContainer *svc = memnew(SubViewportContainer);
svc->set_size(Size2(100, 100));
svc->set_position(Point2(200, 50));
root->add_child(svc);
SubViewport *sv = memnew(SubViewport);
sv->set_embedding_subwindows(true);
sv->set_size(Size2i(100, 100));
svc->add_child(sv);
DragStart *sv_a = memnew(DragStart);
sv_a->set_position(Point2(10, 10));
sv_a->set_size(Size2(10, 10));
sv->add_child(sv_a);
Point2i on_sva = Point2i(215, 65);
DragTarget *sv_b = memnew(DragTarget);
sv_b->set_position(Point2(30, 30));
sv_b->set_size(Size2(20, 20));
sv->add_child(sv_b);
Point2i on_svb = Point2i(235, 85);
Window *ew = memnew(Window);
ew->set_position(Point2(50, 200));
ew->set_size(Size2(100, 100));
root->add_child(ew);
DragStart *ew_a = memnew(DragStart);
ew_a->set_position(Point2(10, 10));
ew_a->set_size(Size2(10, 10));
ew->add_child(ew_a);
Point2i on_ewa = Point2i(65, 215);
DragTarget *ew_b = memnew(DragTarget);
ew_b->set_position(Point2(30, 30));
ew_b->set_size(Size2(20, 20));
ew->add_child(ew_b);
Point2i on_ewb = Point2i(85, 235);
SUBCASE("[Viewport][GuiInputEvent][DnD] Drag to SubViewport") {
sv_b->valid_drop = false;
SEND_GUI_MOUSE_BUTTON_EVENT(on_a, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
SEND_GUI_MOUSE_MOTION_EVENT(on_a + Point2i(min_grab_movement, 0), MouseButtonMask::LEFT, Key::NONE);
CHECK(root->gui_is_dragging());
CHECK(sv_b->during_drag);
SEND_GUI_MOUSE_MOTION_EVENT(on_svb, MouseButtonMask::LEFT, Key::NONE);
CHECK(DS->get_cursor_shape() == DisplayServer::CURSOR_CAN_DROP);
SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_svb, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
CHECK(sv_b->valid_drop);
CHECK(!sv_b->during_drag);
}
SUBCASE("[Viewport][GuiInputEvent][DnD] Drag from SubViewport") {
node_d->valid_drop = false;
SEND_GUI_MOUSE_BUTTON_EVENT(on_sva, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
SEND_GUI_MOUSE_MOTION_EVENT(on_sva + Point2i(min_grab_movement, 0), MouseButtonMask::LEFT, Key::NONE);
CHECK(sv->gui_is_dragging());
CHECK(node_d->during_drag);
SEND_GUI_MOUSE_MOTION_EVENT(on_d, MouseButtonMask::LEFT, Key::NONE);
CHECK(DS->get_cursor_shape() == DisplayServer::CURSOR_CAN_DROP);
SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_d, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
CHECK(node_d->valid_drop);
CHECK(!node_d->during_drag);
}
SUBCASE("[Viewport][GuiInputEvent][DnD] Drag to embedded Window") {
ew_b->valid_drop = false;
SEND_GUI_MOUSE_BUTTON_EVENT(on_a, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
SEND_GUI_MOUSE_MOTION_EVENT(on_a + Point2i(min_grab_movement, 0), MouseButtonMask::LEFT, Key::NONE);
CHECK(root->gui_is_dragging());
CHECK(ew_b->during_drag);
SEND_GUI_MOUSE_MOTION_EVENT(on_ewb, MouseButtonMask::LEFT, Key::NONE);
CHECK(DS->get_cursor_shape() == DisplayServer::CURSOR_CAN_DROP);
SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_ewb, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
CHECK(ew_b->valid_drop);
CHECK(!ew_b->during_drag);
}
SUBCASE("[Viewport][GuiInputEvent][DnD] Drag from embedded Window") {
node_d->valid_drop = false;
SEND_GUI_MOUSE_BUTTON_EVENT(on_ewa, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE);
SEND_GUI_MOUSE_MOTION_EVENT(on_ewa + Point2i(min_grab_movement, 0), MouseButtonMask::LEFT, Key::NONE);
CHECK(ew->gui_is_dragging());
CHECK(node_d->during_drag);
SEND_GUI_MOUSE_MOTION_EVENT(on_d, MouseButtonMask::LEFT, Key::NONE);
CHECK(DS->get_cursor_shape() == DisplayServer::CURSOR_CAN_DROP);
SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(on_d, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE);
CHECK(node_d->valid_drop);
CHECK(!node_d->during_drag);
}
memdelete(ew_a);
memdelete(ew_b);
memdelete(ew);
memdelete(sv_a);
memdelete(sv_b);
memdelete(sv);
memdelete(svc);
}
}
memdelete(node_j);