2021-08-21 17:15:07 +02:00

372 lines
12 KiB
GDScript

extends Control
enum SplitLocation {
TOP,
BOTTOM,
LEFT,
RIGHT
}
enum DropPosition {
TOP,
BOTTOM,
LEFT,
RIGHT,
CENTER
}
const FlexViewSplitter = preload("res://addons/de.mewin.gduibasics/scenes/scripts/flexview_splitter.gd")
const META_UNIQUE_TYPE = "__flexview_unique_type__"
const META_UNIQUE_KEY = "__flexview_unique_key__"
const META_VIEW_TITLE = "__flexview_title__"
const META_LAST_DROP_TARGET = "__flexview_last_drop_target__"
const GROUP_NAME = "__flexview__"
const INDICATOR_ANIM_DURATION = 0.1
const __HANDLERS_TO_COPY = ["view_added", "view_removed", "view_closing"]
onready var _rect_left := $drop_indicator/rect_left
onready var _rect_right := $drop_indicator/rect_right
onready var _rect_top := $drop_indicator/rect_top
onready var _rect_bottom := $drop_indicator/rect_bottom
onready var _rect_center := $drop_indicator/rect_center
onready var _drop_indicator := $drop_indicator/drop_indicator
onready var _tween := $drop_indicator/tween
var current_view := 0 setget _set_current_view, _get_current_view
export var show_tabs := true setget _set_show_tabs
var __drop_position := -1
#############
# overrides #
#############
func _ready() -> void:
set_process(false) # only used for the drop indicator
add_to_group(GROUP_NAME)
$tabs.show_tabs = show_tabs
func _process(delta : float) -> void:
var mouse_pos := get_viewport().get_mouse_position()
var drop_position : int = DropPosition.CENTER
if get_view_count() > 0: # only allow splitting if there is at least one view
var my_rect := get_global_rect()
var distances := [
(mouse_pos.y - my_rect.position.y) / my_rect.size.y,
(my_rect.position.y + my_rect.size.y - mouse_pos.y) / my_rect.size.y,
(mouse_pos.x - my_rect.position.x) / my_rect.size.x,
(my_rect.position.x + my_rect.size.x - mouse_pos.x) / my_rect.size.x
]
var idx := GDBMath.min_element(distances)
if distances[idx] < 0.25:
drop_position = idx
if __drop_position == drop_position:
return
__drop_position = drop_position
var new_rect := __get_drop_rect(drop_position)
_tween.stop(_drop_indicator, "rect_size")
_tween.stop(_drop_indicator, "rect_position")
_tween.interpolate_property(_drop_indicator, "rect_size", _drop_indicator.rect_size, new_rect.size, INDICATOR_ANIM_DURATION)
_tween.interpolate_property(_drop_indicator, "rect_position", _drop_indicator.rect_position, new_rect.position, INDICATOR_ANIM_DURATION)
_tween.start()
################
# public stuff #
################
func add_view(title : String, control : Control, make_current := true) -> int:
control.set_meta(META_VIEW_TITLE, title)
var view : int = $tabs.add_tab(get_view_title_from_control(control), control)
emit_signal("view_added", view, self)
if make_current:
_set_current_view(view)
# support for control-controlled (lel) titles
if control.has_signal("flexview_title_changed"):
control.connect("flexview_title_changed", self, "_on_flexview_title_changed")
if control.has_signal("flexview_close_me"):
control.connect("flexview_close_me", self, "_on_flexview_close_me")
return view
func remove_view(view : int) -> void:
var control := get_view_control(view)
if control:
GDBUtility.disconnect_all(control, self)
$tabs.remove_tab(view)
emit_signal("view_removed", view, self)
if get_view_count() == 0 && get_parent() is FlexViewSplitter:
self.queue_free()
func close_view(view : int) -> void:
emit_signal("view_closing", view, self)
func remove_all_views() -> void:
for view in range(get_view_count() - 1, -1, -1):
remove_view(view)
func find_view(control : Control) -> int:
return $tabs.find_tab(control)
func close_all_views() -> void:
for view in range(get_view_count() - 1, -1, -1):
close_view(view)
func set_view_title(view : int, title : String) -> void:
$tabs.set_tab_title(view, title)
func get_view_control(view : int) -> Control:
return $tabs.get_tab_control(view)
func get_view_title(view : int) -> String:
return $tabs.get_tab_title(view)
func get_views() -> Array:
return $tabs.get_tabs()
func get_view_count() -> int:
return $tabs.get_tab_count()
func split_view(placement : int, title : String, view : Control) -> Control:
var parent := get_parent()
var self_first : bool = (placement == SplitLocation.BOTTOM || placement == SplitLocation.RIGHT)
# add splitter
var splitter : SplitContainer
if placement == SplitLocation.TOP || placement == SplitLocation.BOTTOM:
splitter = preload("res://addons/de.mewin.gduibasics/scenes/flexview_vspliter.tscn").instance()
else:
splitter = preload("res://addons/de.mewin.gduibasics/scenes/flexview_hspliter.tscn").instance()
parent.add_child_below_node(self, splitter)
parent.remove_child(self)
GDBUIUtility.copy_size(splitter, self)
# add self first (if applicable)
if self_first:
splitter.add_child(self)
# create new flex container for other half
var new_container : Control = load("res://addons/de.mewin.gduibasics/scenes/flexview.tscn").instance()
new_container.add_view(title, view)
__copy_signal_handlers(new_container)
splitter.add_child(new_container)
# add self second (if applicable)
if !self_first:
splitter.add_child(self)
if placement == SplitLocation.TOP || placement == SplitLocation.BOTTOM:
splitter.split_offset = 0.5 * splitter.rect_size.y
else:
splitter.split_offset = 0.5 * splitter.rect_size.x
splitter.clamp_split_offset()
return new_container
################
# static stuff #
################
static func get_view_title_from_control(control : Control) -> String:
var prop = control.get("flexview_title")
if prop is String:
return prop
var title = control.get_meta(META_VIEW_TITLE)
if title is String:
return title
return control.name
static func get_all_flex_views() -> Array:
return GDBUtility.get_scene_tree().get_nodes_in_group(GROUP_NAME)
static func get_all_views() -> Array:
var views := []
for flex_view in get_all_flex_views():
views.append_array(flex_view.get_views())
return views
static func get_top_flex_view() -> Control:
# can't use GDBUtility.find_node_by_type as we cannot use our own type
var all_views := get_all_flex_views()
var top_view : Control = null
for flex_view in all_views:
if top_view == null || top_view.is_greater_than(flex_view):
top_view = flex_view
return top_view
static func add_view_anywhere(title : String, view : Control, make_current := true) -> Control:
var flex_view := _get_last_drop_target()
if flex_view == null:
flex_view = get_top_flex_view()
if flex_view != null:
flex_view.add_view(title, view, make_current)
return flex_view
static func add_unique_view_anywhere(title : String, view : Control, type : String, key, make_current := true) -> Control:
var flex_view := add_view_anywhere(title, view, make_current)
if flex_view == null: # failed
return null
view.set_meta(META_UNIQUE_TYPE, type)
view.set_meta(META_UNIQUE_KEY, key)
return flex_view
static func remove_all_views_anywhere() -> void:
for flex_view in get_all_flex_views():
flex_view.remove_all_views()
static func find_unique_view(type : String, key) -> Control:
for view_ctrl in get_all_views():
if view_ctrl.has_meta(META_UNIQUE_TYPE) && view_ctrl.has_meta(META_UNIQUE_KEY)\
&& view_ctrl.get_meta(META_UNIQUE_TYPE) == type && view_ctrl.get_meta(META_UNIQUE_KEY) == key:
return view_ctrl
return null
static func find_flex_view(view_control : Control) -> Control:
var my_script : Script = load("res://addons/de.mewin.gduibasics/scripts/types/controls/flexview.gd")
return GDBUtility.find_parent_with_type(view_control, my_script) as Control
static func make_view_current(view_control : Control) -> void:
var flex_view := find_flex_view(view_control)
if flex_view:
flex_view.current_view = view_control.get_position_in_parent()
static func _set_last_drop_target(flex_view : Control) -> void:
flex_view.get_tree().set_meta(META_LAST_DROP_TARGET, flex_view.get_path())
static func _get_last_drop_target() -> Control:
var tree := GDBUtility.get_scene_tree()
if tree.has_meta(META_LAST_DROP_TARGET):
var flex_view_path := tree.get_meta(META_LAST_DROP_TARGET) as NodePath
if flex_view_path:
var flex_view := tree.root.get_node_or_null(flex_view_path) as Control
return flex_view
return null
#################
# private stuff #
#################
func _start_dropping() -> void:
__drop_position = -1
_drop_indicator.visible = true
_tween.stop_all()
_tween.interpolate_property(_drop_indicator, "modulate", Color.transparent, Color.white, INDICATOR_ANIM_DURATION)
_tween.start()
set_process(true)
func _stop_dropping() -> void:
_tween.stop_all()
_tween.interpolate_property(_drop_indicator, "modulate", Color.white, Color.transparent, INDICATOR_ANIM_DURATION)
_tween.start()
set_process(false)
yield(get_tree().create_timer(INDICATOR_ANIM_DURATION), "timeout")
if !is_processing(): # mOybe we are dragging again
_drop_indicator.visible = false
func _drop(view : Control) -> void:
_stop_dropping()
var title := get_view_title_from_control(view)
if __drop_position != DropPosition.CENTER:
var new_container := split_view(__drop_position, title, view)
_set_last_drop_target(new_container)
else:
add_view(title, view)
_set_last_drop_target(self)
func __make_floating_tab(tab : int) -> void:
var window : WindowDialog = load("res://addons/de.mewin.gduibasics/scenes/flexview_window.tscn").instance()
get_tree().root.add_child(window)
# remove tab
var tab_ctrl : Control = $tabs.get_tab_control(tab)
remove_view(tab)
# add to new window
var title = get_view_title_from_control(tab_ctrl)
window.flex_view.add_view(title, tab_ctrl)
__copy_signal_handlers(window.flex_view)
# place window
var vp_size := get_viewport().size
var mouse_pos := get_viewport().get_mouse_position()
var win_size := vp_size * Vector2(0.5, 0.5)
var min_size := window.get_combined_minimum_size()
var title_height := window.get_constant("title_height")
window.rect_size = Vector2(max(win_size.x, min_size.x), max(win_size.y, min_size.y))
window.rect_position = mouse_pos - Vector2(0.5 * window.rect_size.x, -0.5 * title_height)
window.visible = true
# emulate mouse down for dragging the window
var event := InputEventMouseButton.new()
event.button_index = BUTTON_LEFT
event.pressed = true
event.button_mask = BUTTON_MASK_LEFT
event.position = mouse_pos
event.global_position = mouse_pos
Input.parse_input_event(event)
func __get_drop_rect(drop_position : int) -> Rect2:
var full_rect := Rect2(Vector2(), rect_size)
match drop_position:
DropPosition.TOP:
return full_rect.grow_individual(0.0, 0.0, 0.0, -0.5 * full_rect.size.y)
DropPosition.BOTTOM:
return full_rect.grow_individual(0.0, -0.5 * full_rect.size.y, 0.0, 0.0)
DropPosition.LEFT:
return full_rect.grow_individual(0.0, 0.0, -0.5 * full_rect.size.x, 0.0)
DropPosition.RIGHT:
return full_rect.grow_individual(-0.5 * full_rect.size.x, 0.0, 0.0, 0.0)
return full_rect
func __copy_signal_handlers(other_view : Control) -> void:
GDBUtility.copy_all_signal_handlers(other_view, self, __HANDLERS_TO_COPY)
#####################
# setters & getters #
#####################
func _set_current_view(val : int) -> void:
$tabs.current_tab = val
emit_signal("current_view_changed", self)
func _get_current_view() -> int:
return $tabs.current_tab
func _set_show_tabs(show : bool) -> void:
show_tabs = show
if $tabs:
$tabs.show_tabs = show
###################
# signal handlers #
###################
func _on_tabs_tab_dragged_out(tab : int, position : Vector2) -> void:
__make_floating_tab(tab)
func _on_tabs_tab_close(tab : int):
close_view(tab)
func _on_flexview_title_changed(control : Control) -> void:
var view := find_view(control)
var title = control.get("flexview_title")
if view >= 0 && title is String:
set_view_title(view, title)
func _on_flexview_close_me(control : Control) -> void:
var view := find_view(control)
if view >= 0:
remove_view(view)
###########
# signals #
###########
signal view_added(view, flex_view)
signal view_removed(view, flex_view)
signal view_closing(view, flex_view)
signal current_view_changed(flex_view)