Skip to content

Instantly share code, notes, and snippets.

@RichardEllicott
Created December 3, 2022 14:05
Show Gist options
  • Save RichardEllicott/8f4a505808e2b6c57a0adc5a956e54ba to your computer and use it in GitHub Desktop.
Save RichardEllicott/8f4a505808e2b6c57a0adc5a956e54ba to your computer and use it in GitHub Desktop.
"""
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