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)