Created
December 3, 2022 14:05
-
-
Save RichardEllicott/8f4a505808e2b6c57a0adc5a956e54ba to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
FINISHED 03/12/2022 | |
HUGE drag drop modue, supports dragging cards about and throws signals | |
if set to check depth as 1, stacks of cards will become one object, no bugs! | |
check depth 2 allows dragging cards out of packs, is buggy still | |
mouse_filter = Control.MOUSE_FILTER_STOP ## best option for our draggables | |
mouse_filter = Control.MOUSE_FILTER_PASS | |
mouse_filter = Control.MOUSE_FILTER_IGNORE ## this script will ignore controls with ignore | |
nodes require meta tags as true: | |
'draggable' = true ## allows a control to be dragged | |
'dockable' = true ## can dock dragging objects here (this feature is not used in my game) | |
signals: | |
drag_drop | |
start_drag | |
end_drag | |
dock | |
start_hover | |
end_hover | |
start_drag_hover | |
end_drag_hover | |
""" | |
tool ## TRYING IT AS A TOOL NOW!!! | |
extends Node | |
## our signals | |
signal drag_drop(from, to) ## when we drag one child to another | |
signal start_drag(child) ## a drag action starts | |
signal end_drag(child) ## a drag action finishes | |
signal dock(from,to) # when a control is docked to a "dockable" | |
signal start_hover(child) ## when we start hovering a node | |
signal end_hover(child) ## when we end hovering a node | |
signal start_drag_hover(from, to) ## when we are dragging and we start hovering a potential target | |
signal end_drag_hover(from, to) ## ending the previous signal | |
## this target must be set so we know which controls to work on (child of this target) | |
export (NodePath) var _target_node : NodePath | |
var target_node : Node | |
func get_target_node(): | |
if not target_node: | |
target_node = get_node(_target_node) | |
return target_node | |
## assign Camera2D (this MUST be assigned so we can work out mouse to screen positions) | |
export (NodePath) var _camera2D : NodePath | |
var camera2D : Camera2D | |
func get_camera_2D() -> Camera2D: | |
if not camera2D: | |
camera2D = get_node(_camera2D) | |
return camera2D | |
## (optional) if we have a debug label, it will be detected on launch, if not, no debug text is sent | |
export (NodePath) var _debug_label : NodePath | |
onready var debug_label : Label = get_node_or_null(_debug_label) | |
## these functions will only work if the nodes are present on launch | |
export var debug_set_all_childs_draggable = false | |
export var debug_set_all_childs_dockable = false | |
func _ready(): | |
for child in get_target_node().get_children(): | |
if child is Control: | |
var child2: Control = child | |
if debug_set_all_childs_draggable: | |
child2.set_meta('draggable',true) | |
if child2.name.begins_with('DOCKABLE'): | |
child2.set_meta('dockable',true) | |
# child2.remove_meta('draggable') | |
if debug_set_all_childs_dockable: | |
child2.set_meta('dockable',true) | |
func _process(delta): | |
# gather mouse pos each frame | |
# mouse_position = get_viewport().get_mouse_position() ## try in event instead | |
_process_animate_move(delta) | |
export var animate_drag_return = true | |
var animate_move_nodes = {} ## nodes to | |
export var animate_move_speed = 1024.0 * 8 | |
func move_control_to_position(node : Control, position : Vector2): | |
if node.rect_position != position: | |
animate_move_nodes[node] = position | |
func _process_animate_move(delta): | |
if not animate_move_nodes: | |
animate_move_nodes = {} | |
for inst in animate_move_nodes: | |
var inst2 : Control = inst | |
var move_to : Vector2 = animate_move_nodes[inst2] | |
inst2.rect_position = inst2.rect_position.move_toward(move_to,delta * animate_move_speed) | |
if inst2.rect_position == move_to: | |
animate_move_nodes.erase(inst) | |
func _process_drag(): | |
## currently only running on mouse move event | |
if is_dragging: | |
mouse_position = get_viewport().get_mouse_position() | |
var camera = get_camera_2D() | |
## we scale our drag offset by the camera | |
var drag_offset = (mouse_position - drag_mouse_start_position) * camera.zoom | |
## then we add it to the orginal controls start pos | |
control_being_dragged.rect_position = (drag_offset + drag_control_start_position) | |
var is_dragging : bool = false ## we are currently dragging something | |
var control_being_dragged : Control ## the control we are dragging | |
var drag_mouse_start_position : Vector2 = Vector2() ## record where the mouse starts | |
var drag_control_start_position : Vector2 = Vector2() ## also the dragged control starts | |
func _on_start_drag(): | |
# if hovered_controls.size() > 0: | |
# control_being_dragged = hovered_controls[0] | |
if hovered_control: | |
control_being_dragged = hovered_control | |
is_dragging = true | |
drag_mouse_start_position = get_viewport().get_mouse_position() | |
drag_control_start_position = control_being_dragged.rect_position | |
var drag_parent = control_being_dragged.get_parent() | |
drag_parent.move_child (control_being_dragged,drag_parent.get_child_count()) ## move to top | |
get_start_pos_marker() | |
if draw_drag_drop_start_pos: | |
start_pos_marker.rect_position = control_being_dragged.rect_position | |
start_pos_marker.rect_size = control_being_dragged.rect_size | |
start_pos_marker.rect_scale = control_being_dragged.rect_scale | |
start_pos_marker.visible = true | |
emit_signal('start_drag',control_being_dragged) | |
export var draw_drag_drop_start_pos = true | |
var start_pos_marker : ColorRect | |
func get_start_pos_marker() -> ColorRect: | |
if not start_pos_marker: | |
start_pos_marker = ColorRect.new() | |
start_pos_marker.modulate = Color(1,0,0,0.125) | |
start_pos_marker.mouse_filter = Control.MOUSE_FILTER_IGNORE | |
start_pos_marker.visible = false | |
add_child(start_pos_marker) | |
return start_pos_marker | |
## called when a Control is dragged to another Control | |
func _on_drag_to(from : Control, to : Control) -> void: | |
# print('_on_drag_to "%s" -> "%s"' % [from.name,to.name]) | |
emit_signal("drag_drop", from, to) | |
## drag mode | |
## 0 = none, lets the control stay in new place | |
## 1 = return | |
export var enable_dock = true ## EXPERIMENTAL | |
export var dock_offset = Vector2(8,-8) | |
## it has been found only this method to first remove the child works to move child parent | |
## warning it does not happen instantly! (child count will not change until further frames) | |
static func _reparent_child(child, parent): | |
var child_parent : Node = child.get_parent() | |
if child_parent: | |
child_parent.remove_child(child) ## does not work without without this hack! | |
parent.add_child(child) | |
static func _tidy_stack(control : Control, dock_offset : Vector2): | |
## tidy up a dockable so that the cards stacker in order | |
if control.has_meta('docked_childs'): | |
var childs : Array = control.get_meta('docked_childs') | |
for i in childs.size(): | |
var child : Control = childs[i] | |
child.rect_position = dock_offset * (i + 1) | |
static func dock_to_stack_function(from : Control, to : Control, dock_offset : Vector2): | |
var merge_stacks = true # makes cards behave so you can drop a stack and stack and they merge | |
print('dock_to_stack_function: "%s" to "%s"...' % [from,to]) | |
var do_not_repar = false | |
## if we are already docked here, we avoid any actions and just tidy the stack back up | |
if to.has_meta('docked_childs'): | |
var childs = to.get_meta('docked_childs') | |
if from in childs: | |
print("ALREADY DOCKED to this child!!! ") | |
_tidy_stack(to,dock_offset) ## retidy the stack back up | |
return ## this avoids that next function firing | |
## if what we are trying to dock to is already docked, instead we will try and dock to it's parent | |
## this ensures we have no deeper childs and stops seperate stacks forming as childs | |
var dock_to_par = to.get_parent() | |
if dock_to_par.has_meta('docked_childs'): | |
var dock_to_par_childs : Array = dock_to_par.get_meta('docked_childs') | |
if to in dock_to_par_childs: | |
print("target is already docked itself! go upchain to the %s" % dock_to_par.name) | |
## the dock to child is already docked, so instead go uptree, recurse this function | |
dock_to_stack_function(from,dock_to_par,dock_offset) | |
return | |
pass | |
if not do_not_repar: | |
## remove ref from old parent | |
var child_parent = from.get_parent() | |
if child_parent.has_meta('docked_childs'): | |
var docked_childs : Array = child_parent.get_meta('docked_childs') | |
docked_childs.erase(from) | |
_tidy_stack(child_parent,dock_offset) | |
# _tidy_stack(child_parent,dock_offset) | |
_reparent_child(from,to) ## note we will not see the reparenting this frame | |
## get the containers "docked_childs" Array | |
var items : Array = [] | |
if not to.has_meta('docked_childs'): | |
to.set_meta('docked_childs',items) | |
else: | |
items = to.get_meta('docked_childs') | |
items.append(from) | |
from.rect_position = dock_offset * items.size() ## set the stacking position | |
## if we have childs on a stack of cards we dragged, they need to be reparented to the target | |
if merge_stacks and from.has_meta('docked_childs'): | |
for child in from.get_meta('docked_childs'): | |
## this recurse was lossing data, likely due to acessing lists in a funny order | |
# dock_to_stack_function(child,dock_to,dock_offset) | |
_reparent_child(child,to) ## avoid recurse doing this manually | |
items.append(child) | |
from.remove_meta('docked_childs') | |
# from.set_meta('docked_childs', []) ## ALTERNATIVE | |
## rearrange the nodes | |
_tidy_stack(to,dock_offset) | |
## moving it to the bottom of it's respective tree makes it render on top (last) | |
_node_to_bottom_of_tree(to) | |
## moves a node to the top of it's current tree | |
static func _node_to_bottom_of_tree(child : Node): | |
var child_par : Node = child.get_parent() | |
child_par.move_child(child,child_par.get_child_count()-1) | |
func play_audio_child(sound_name : String, pitch_scale = null): | |
if sound_name in get_sound_dict(): | |
var player : AudioStreamPlayer = get_sound_dict()[sound_name] | |
if pitch_scale: | |
player.pitch_scale = pitch_scale | |
player.playing = true | |
var audio_childs | |
func get_sound_dict(): | |
if not audio_childs: | |
audio_childs = {} | |
for child in get_children(): | |
if child is AudioStreamPlayer: | |
audio_childs[child.name] = child | |
return audio_childs | |
export var always_reset_drag_position = true ## return to old position instead of staying in new pos | |
func _on_end_drag(): | |
## called | |
if is_dragging: | |
is_dragging = false | |
var has_docked_to_stack = false | |
if drop_to_target: | |
_on_drag_to(control_being_dragged,drop_to_target) | |
## EXPERIMENTAL DOCK | |
if enable_dock and drop_to_target.has_meta('dockable'): | |
dock_to_stack_function(control_being_dragged,drop_to_target,dock_offset) | |
has_docked_to_stack = true | |
emit_signal('dock', control_being_dragged,drop_to_target) | |
play_audio_child('dock') | |
if always_reset_drag_position and not has_docked_to_stack: | |
play_audio_child('fail') | |
if animate_drag_return: ## animate the card back | |
move_control_to_position(control_being_dragged, drag_control_start_position) | |
else: ## instantly move the card back | |
control_being_dragged.rect_position = drag_control_start_position | |
start_pos_marker.visible = false | |
emit_signal('end_drag',control_being_dragged) | |
control_being_dragged = null | |
static func _check_if_mouse_hovers_control(control : Control, camera2D : Camera2D, viewport : Viewport) -> bool: | |
## works even when dragging, must check all childs with this function | |
## took a while to work these offsets out so saved as a function | |
## example: | |
## _check_if_mouse_hovers_control(child,get_camera_2D(),get_viewport()): | |
## we still have drag bug | |
var mouse_position : Vector2 = viewport.get_mouse_position() | |
var control_global_rect : Rect2 = control.get_global_rect() | |
## compensates if the child is scaled | |
control_global_rect.size *= control.rect_scale ## if the drag child is scaled the rect needs scaling | |
## compensates for camera pos | |
control_global_rect.position -= camera2D.position ## correct for camera pos | |
if camera2D.anchor_mode == Camera2D.ANCHOR_MODE_DRAG_CENTER: ## if we have a center screen we must offset the pos | |
mouse_position -= viewport.get_visible_rect().size/2.0 | |
## we where looking at refactoring this so it works for touch position | |
return control_global_rect.has_point(mouse_position * camera2D.zoom) | |
var hovered_controls : Array = [] | |
var total_child_count : int = 0 | |
var checked_child_count : int = 0 | |
export var check_depth : int = 1 ## the depth of childs to check for mouse hover, typically 1 will get all siblings but no deeper | |
func _get_all_hovering() -> Array: | |
## get all hovered controls, the last one will be the top | |
## this should only be called once a frame, save the results | |
# var parent_node = self ## we could point this elsewhere | |
var parent_node = get_target_node() ## we could point this elsewhere | |
var childs = get_all_children(parent_node,check_depth) | |
total_child_count = childs.size() | |
checked_child_count = 0 | |
var ret : Array = [] | |
for child in childs: | |
if _drag_filter(child): | |
checked_child_count += 1 | |
if _check_if_mouse_hovers_control(child,get_camera_2D(),get_viewport()): | |
ret.append(child) | |
return ret | |
## predicate checks if child is eligible to drag | |
func _drag_filter(child) -> bool: | |
if not child is Control: return false ## only Control nodes | |
if child is Button: return false # filter out Buttons | |
## change to use MOUSE_FILTER_STOP and MOUSE_FILTER_PASS ********** | |
# if not child.mouse_filter == Control.MOUSE_FILTER_STOP: return false ## must have MOUSE_FILTER_STOP | |
if child.mouse_filter == Control.MOUSE_FILTER_IGNORE: return false ## must have MOUSE_FILTER_STOP | |
if child in animate_move_nodes: return false ## must not be moving | |
if debug_require_draggable_meta and not child.has_meta('draggable') : return false | |
return true | |
var debug_require_draggable_meta = true | |
func _update_debug_label(): | |
if debug_label: | |
var s = "" | |
var debug_pars = [ | |
"hovered_controls", | |
"is_dragging", | |
"control_being_dragged", | |
"drop_to_target", | |
"total_child_count", | |
"check_depth", | |
"checked_child_count", | |
"hovered_control", | |
"hovered_control_stack_size", | |
"hovered_control_stack_names", | |
] | |
for par in debug_pars: | |
s += "%s: %s\n" % [par, get(par)] | |
debug_label.text = s | |
export var debug_color : bool = true | |
var drop_to_target : Control | |
func _input_check_mouse_hover() -> void: | |
## subroutine called by _input to update the "hovered_controls" | |
# print("_input_check_mouse_hover running...") ## seems to not run in editor | |
# if not Engine.is_editor_hint(): # unsure | |
# set last hover to white | |
if debug_color and hovered_control: hovered_control.modulate = Color.white | |
hovered_controls = _get_all_hovering() | |
## this block fires the "start_hover" "end_hover" signals | |
var last_hovered_control = hovered_control | |
if hovered_controls.size() > 0: | |
hovered_control = hovered_controls.back() | |
if hovered_control != last_hovered_control: ## we are hovering a new control | |
if last_hovered_control: # if a last last_hovered_control exists | |
emit_signal("end_hover", last_hovered_control) | |
emit_signal("start_hover", hovered_control) | |
else: | |
if hovered_control: ## if we last had a hovered control | |
emit_signal("end_hover", hovered_control) | |
hovered_control = null | |
# highlight top one | |
if debug_color and hovered_control: hovered_control.modulate = Color.magenta | |
## this block handles what we are dragging hover... | |
## WARNING NEW AND MIGHT NOT WORK!! | |
## THIS BLOCK SEEMS A LITTLE ODD WE DO IT OUTSIDE OUR NEXT NEST | |
var last_drop_to_target = drop_to_target | |
if drop_to_target: | |
if debug_color: drop_to_target.modulate = Color.white | |
drop_to_target = null | |
if is_dragging: | |
if hovered_controls.size() > 1: | |
drop_to_target = hovered_controls[-2] | |
if debug_color: drop_to_target.modulate = Color.palegreen | |
## we started a hover, is it a new hover? | |
else: | |
drop_to_target = null ## i think this is duplicated logic but cannot hurt anyway | |
pass | |
if drop_to_target != last_drop_to_target: | |
if last_drop_to_target: | |
emit_signal("end_drag_hover", control_being_dragged, last_drop_to_target) | |
if drop_to_target: ## we need to check we have a drop to target at all | |
emit_signal("start_drag_hover", control_being_dragged, drop_to_target) | |
if hovered_control: | |
if hovered_control.has_meta('docked_childs'): | |
hovered_control_stack = hovered_control.get_meta('docked_childs') | |
hovered_control_stack_size = hovered_control_stack.size() | |
hovered_control_stack_names = [hovered_control.name] | |
for v in hovered_control_stack: | |
hovered_control_stack_names.append(v.name) | |
else: | |
hovered_control_stack_size = 0 | |
hovered_control_stack_names = [] | |
var hovered_control : Control | |
var hovered_control_stack : Array | |
var hovered_control_stack_names : Array | |
var hovered_control_stack_size : int | |
static func get_all_children(node : Node, max_depth = 100, array : Array = [], depth = 0) -> Array: | |
## get all childs, recursive with depth | |
## get_all_children(get_tree().get_root()) # get all of scene | |
if depth <= max_depth: | |
array.push_back(node) | |
for child in node.get_children(): | |
array = get_all_children(child,max_depth,array,depth+1) | |
return array | |
var debug_test_double_click = true | |
func _input(event): | |
_input_check_mouse_hover() | |
_process_drag() | |
## double click test | |
if debug_test_double_click and event is InputEventMouseButton and event.doubleclick: | |
print('DOUBLE CLICK DETECTED! ', event) | |
if hovered_controls.size() > 0: | |
print("double clicked: ", hovered_controls[-1]) | |
## if mouse motion update our hover cache | |
elif event is InputEventMouseMotion: | |
mouse_position = event.position | |
## start drag with left click | |
elif event is InputEventMouseButton: | |
if event.button_index == BUTTON_LEFT: | |
if event.is_pressed(): | |
_on_start_drag() | |
else: | |
_on_end_drag() | |
elif event.button_index == BUTTON_RIGHT: | |
pass | |
_update_debug_label() | |
var mouse_position : Vector2 = Vector2() | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment