init: 从 gilzoide/godot-dockable-container 提取的插件文件
This commit is contained in:
121
LICENSE
Normal file
121
LICENSE
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
Creative Commons Legal Code
|
||||||
|
|
||||||
|
CC0 1.0 Universal
|
||||||
|
|
||||||
|
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
||||||
|
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
||||||
|
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
||||||
|
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
||||||
|
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
||||||
|
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
||||||
|
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
||||||
|
HEREUNDER.
|
||||||
|
|
||||||
|
Statement of Purpose
|
||||||
|
|
||||||
|
The laws of most jurisdictions throughout the world automatically confer
|
||||||
|
exclusive Copyright and Related Rights (defined below) upon the creator
|
||||||
|
and subsequent owner(s) (each and all, an "owner") of an original work of
|
||||||
|
authorship and/or a database (each, a "Work").
|
||||||
|
|
||||||
|
Certain owners wish to permanently relinquish those rights to a Work for
|
||||||
|
the purpose of contributing to a commons of creative, cultural and
|
||||||
|
scientific works ("Commons") that the public can reliably and without fear
|
||||||
|
of later claims of infringement build upon, modify, incorporate in other
|
||||||
|
works, reuse and redistribute as freely as possible in any form whatsoever
|
||||||
|
and for any purposes, including without limitation commercial purposes.
|
||||||
|
These owners may contribute to the Commons to promote the ideal of a free
|
||||||
|
culture and the further production of creative, cultural and scientific
|
||||||
|
works, or to gain reputation or greater distribution for their Work in
|
||||||
|
part through the use and efforts of others.
|
||||||
|
|
||||||
|
For these and/or other purposes and motivations, and without any
|
||||||
|
expectation of additional consideration or compensation, the person
|
||||||
|
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
||||||
|
is an owner of Copyright and Related Rights in the Work, voluntarily
|
||||||
|
elects to apply CC0 to the Work and publicly distribute the Work under its
|
||||||
|
terms, with knowledge of his or her Copyright and Related Rights in the
|
||||||
|
Work and the meaning and intended legal effect of CC0 on those rights.
|
||||||
|
|
||||||
|
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||||
|
protected by copyright and related or neighboring rights ("Copyright and
|
||||||
|
Related Rights"). Copyright and Related Rights include, but are not
|
||||||
|
limited to, the following:
|
||||||
|
|
||||||
|
i. the right to reproduce, adapt, distribute, perform, display,
|
||||||
|
communicate, and translate a Work;
|
||||||
|
ii. moral rights retained by the original author(s) and/or performer(s);
|
||||||
|
iii. publicity and privacy rights pertaining to a person's image or
|
||||||
|
likeness depicted in a Work;
|
||||||
|
iv. rights protecting against unfair competition in regards to a Work,
|
||||||
|
subject to the limitations in paragraph 4(a), below;
|
||||||
|
v. rights protecting the extraction, dissemination, use and reuse of data
|
||||||
|
in a Work;
|
||||||
|
vi. database rights (such as those arising under Directive 96/9/EC of the
|
||||||
|
European Parliament and of the Council of 11 March 1996 on the legal
|
||||||
|
protection of databases, and under any national implementation
|
||||||
|
thereof, including any amended or successor version of such
|
||||||
|
directive); and
|
||||||
|
vii. other similar, equivalent or corresponding rights throughout the
|
||||||
|
world based on applicable law or treaty, and any national
|
||||||
|
implementations thereof.
|
||||||
|
|
||||||
|
2. Waiver. To the greatest extent permitted by, but not in contravention
|
||||||
|
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
||||||
|
irrevocably and unconditionally waives, abandons, and surrenders all of
|
||||||
|
Affirmer's Copyright and Related Rights and associated claims and causes
|
||||||
|
of action, whether now known or unknown (including existing as well as
|
||||||
|
future claims and causes of action), in the Work (i) in all territories
|
||||||
|
worldwide, (ii) for the maximum duration provided by applicable law or
|
||||||
|
treaty (including future time extensions), (iii) in any current or future
|
||||||
|
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
||||||
|
including without limitation commercial, advertising or promotional
|
||||||
|
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
||||||
|
member of the public at large and to the detriment of Affirmer's heirs and
|
||||||
|
successors, fully intending that such Waiver shall not be subject to
|
||||||
|
revocation, rescission, cancellation, termination, or any other legal or
|
||||||
|
equitable action to disrupt the quiet enjoyment of the Work by the public
|
||||||
|
as contemplated by Affirmer's express Statement of Purpose.
|
||||||
|
|
||||||
|
3. Public License Fallback. Should any part of the Waiver for any reason
|
||||||
|
be judged legally invalid or ineffective under applicable law, then the
|
||||||
|
Waiver shall be preserved to the maximum extent permitted taking into
|
||||||
|
account Affirmer's express Statement of Purpose. In addition, to the
|
||||||
|
extent the Waiver is so judged Affirmer hereby grants to each affected
|
||||||
|
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
||||||
|
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
||||||
|
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
||||||
|
maximum duration provided by applicable law or treaty (including future
|
||||||
|
time extensions), (iii) in any current or future medium and for any number
|
||||||
|
of copies, and (iv) for any purpose whatsoever, including without
|
||||||
|
limitation commercial, advertising or promotional purposes (the
|
||||||
|
"License"). The License shall be deemed effective as of the date CC0 was
|
||||||
|
applied by Affirmer to the Work. Should any part of the License for any
|
||||||
|
reason be judged legally invalid or ineffective under applicable law, such
|
||||||
|
partial invalidity or ineffectiveness shall not invalidate the remainder
|
||||||
|
of the License, and in such case Affirmer hereby affirms that he or she
|
||||||
|
will not (i) exercise any of his or her remaining Copyright and Related
|
||||||
|
Rights in the Work or (ii) assert any associated claims and causes of
|
||||||
|
action with respect to the Work, in either case contrary to Affirmer's
|
||||||
|
express Statement of Purpose.
|
||||||
|
|
||||||
|
4. Limitations and Disclaimers.
|
||||||
|
|
||||||
|
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||||
|
surrendered, licensed or otherwise affected by this document.
|
||||||
|
b. Affirmer offers the Work as-is and makes no representations or
|
||||||
|
warranties of any kind concerning the Work, express, implied,
|
||||||
|
statutory or otherwise, including without limitation warranties of
|
||||||
|
title, merchantability, fitness for a particular purpose, non
|
||||||
|
infringement, or the absence of latent or other defects, accuracy, or
|
||||||
|
the present or absence of errors, whether or not discoverable, all to
|
||||||
|
the greatest extent permissible under applicable law.
|
||||||
|
c. Affirmer disclaims responsibility for clearing rights of other persons
|
||||||
|
that may apply to the Work or any use thereof, including without
|
||||||
|
limitation any person's Copyright and Related Rights in the Work.
|
||||||
|
Further, Affirmer disclaims responsibility for obtaining any necessary
|
||||||
|
consents, permissions or other rights required for any use of the
|
||||||
|
Work.
|
||||||
|
d. Affirmer understands and acknowledges that Creative Commons is not a
|
||||||
|
party to this document and has no duty or obligation with respect to
|
||||||
|
this CC0 or use of the Work.
|
||||||
455
dockable_container.gd
Normal file
455
dockable_container.gd
Normal file
@@ -0,0 +1,455 @@
|
|||||||
|
@tool
|
||||||
|
class_name DockableContainer
|
||||||
|
extends Container
|
||||||
|
|
||||||
|
const SplitHandle := preload("split_handle.gd")
|
||||||
|
const DockablePanel := preload("dockable_panel.gd")
|
||||||
|
const DragNDropPanel := preload("drag_n_drop_panel.gd")
|
||||||
|
|
||||||
|
@export var tab_alignment := TabBar.ALIGNMENT_CENTER:
|
||||||
|
get:
|
||||||
|
return _tab_align
|
||||||
|
set(value):
|
||||||
|
_tab_align = value
|
||||||
|
for i in range(1, _panel_container.get_child_count()):
|
||||||
|
var panel := _panel_container.get_child(i) as DockablePanel
|
||||||
|
panel.tab_alignment = value
|
||||||
|
@export var use_hidden_tabs_for_min_size := false:
|
||||||
|
get:
|
||||||
|
return _use_hidden_tabs_for_min_size
|
||||||
|
set(value):
|
||||||
|
_use_hidden_tabs_for_min_size = value
|
||||||
|
for i in range(1, _panel_container.get_child_count()):
|
||||||
|
var panel := _panel_container.get_child(i) as DockablePanel
|
||||||
|
panel.use_hidden_tabs_for_min_size = value
|
||||||
|
@export var tabs_visible := true:
|
||||||
|
get:
|
||||||
|
return _tabs_visible
|
||||||
|
set(value):
|
||||||
|
_tabs_visible = value
|
||||||
|
for i in range(1, _panel_container.get_child_count()):
|
||||||
|
var panel := _panel_container.get_child(i) as DockablePanel
|
||||||
|
panel.show_tabs = _tabs_visible
|
||||||
|
## If [code]true[/code] and a panel only has one tab, it keeps that tab hidden even if
|
||||||
|
## [member tabs_visible] is [code]true[/code].
|
||||||
|
## Only takes effect is [member tabs_visible] is [code]true[/code].
|
||||||
|
@export var hide_single_tab := false:
|
||||||
|
get:
|
||||||
|
return _hide_single_tab
|
||||||
|
set(value):
|
||||||
|
_hide_single_tab = value
|
||||||
|
for i in range(1, _panel_container.get_child_count()):
|
||||||
|
var panel := _panel_container.get_child(i) as DockablePanel
|
||||||
|
panel.hide_single_tab = _hide_single_tab
|
||||||
|
@export var rearrange_group := 0
|
||||||
|
@export var layout := DockableLayout.new():
|
||||||
|
get:
|
||||||
|
return _layout
|
||||||
|
set(value):
|
||||||
|
set_layout(value)
|
||||||
|
## If `clone_layout_on_ready` is true, `layout` will be cloned checked `_ready`.
|
||||||
|
## This is useful for leaving layout Resources untouched in case you want to
|
||||||
|
## restore layout to its default later.
|
||||||
|
@export var clone_layout_on_ready := true
|
||||||
|
|
||||||
|
var _layout := DockableLayout.new()
|
||||||
|
var _panel_container := Container.new()
|
||||||
|
var _split_container := Container.new()
|
||||||
|
var _drag_n_drop_panel := DragNDropPanel.new()
|
||||||
|
var _drag_panel: DockablePanel
|
||||||
|
var _tab_align := TabBar.ALIGNMENT_CENTER
|
||||||
|
var _tabs_visible := true
|
||||||
|
var _use_hidden_tabs_for_min_size := false
|
||||||
|
var _hide_single_tab := false
|
||||||
|
var _current_panel_index := 0
|
||||||
|
var _current_split_index := 0
|
||||||
|
var _children_names := {}
|
||||||
|
var _layout_dirty := false
|
||||||
|
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
child_entered_tree.connect(_child_entered_tree)
|
||||||
|
child_exiting_tree.connect(_child_exiting_tree)
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
set_process_input(false)
|
||||||
|
_panel_container.name = "_panel_container"
|
||||||
|
add_child(_panel_container)
|
||||||
|
move_child(_panel_container, 0)
|
||||||
|
_split_container.name = "_split_container"
|
||||||
|
_split_container.mouse_filter = MOUSE_FILTER_PASS
|
||||||
|
_panel_container.add_child(_split_container)
|
||||||
|
|
||||||
|
_drag_n_drop_panel.name = "_drag_n_drop_panel"
|
||||||
|
_drag_n_drop_panel.mouse_filter = MOUSE_FILTER_PASS
|
||||||
|
_drag_n_drop_panel.visible = false
|
||||||
|
add_child(_drag_n_drop_panel)
|
||||||
|
|
||||||
|
if not _layout:
|
||||||
|
set_layout(null)
|
||||||
|
elif clone_layout_on_ready and not Engine.is_editor_hint():
|
||||||
|
set_layout(_layout.clone())
|
||||||
|
|
||||||
|
|
||||||
|
func _notification(what: int) -> void:
|
||||||
|
if what == NOTIFICATION_SORT_CHILDREN:
|
||||||
|
_resort()
|
||||||
|
elif (
|
||||||
|
what == NOTIFICATION_DRAG_BEGIN
|
||||||
|
and _can_handle_drag_data(get_viewport().gui_get_drag_data())
|
||||||
|
):
|
||||||
|
_drag_n_drop_panel.set_enabled(true, not _layout.root.is_empty())
|
||||||
|
set_process_input(true)
|
||||||
|
elif what == NOTIFICATION_DRAG_END:
|
||||||
|
_drag_n_drop_panel.set_enabled(false)
|
||||||
|
set_process_input(false)
|
||||||
|
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
assert(get_viewport().gui_is_dragging(), "FIXME: should only be called when dragging")
|
||||||
|
if event is InputEventMouseMotion:
|
||||||
|
var local_position := get_local_mouse_position()
|
||||||
|
var panel: DockablePanel
|
||||||
|
for i in range(1, _panel_container.get_child_count()):
|
||||||
|
var p := _panel_container.get_child(i) as DockablePanel
|
||||||
|
if p.get_rect().has_point(local_position):
|
||||||
|
panel = p
|
||||||
|
break
|
||||||
|
_drag_panel = panel
|
||||||
|
if not panel:
|
||||||
|
return
|
||||||
|
fit_child_in_rect(_drag_n_drop_panel, panel.get_child_rect())
|
||||||
|
|
||||||
|
|
||||||
|
func _child_entered_tree(node: Node) -> void:
|
||||||
|
if node == _panel_container or node == _drag_n_drop_panel:
|
||||||
|
return
|
||||||
|
_drag_n_drop_panel.move_to_front()
|
||||||
|
_track_and_add_node(node)
|
||||||
|
|
||||||
|
|
||||||
|
func _child_exiting_tree(node: Node) -> void:
|
||||||
|
if node == _panel_container or node == _drag_n_drop_panel:
|
||||||
|
return
|
||||||
|
_untrack_node(node)
|
||||||
|
|
||||||
|
|
||||||
|
func _can_drop_data(_position: Vector2, data) -> bool:
|
||||||
|
return _can_handle_drag_data(data)
|
||||||
|
|
||||||
|
|
||||||
|
func _drop_data(_position: Vector2, data) -> void:
|
||||||
|
var from_node := get_node(data.from_path)
|
||||||
|
if from_node is TabBar:
|
||||||
|
from_node = from_node.get_parent()
|
||||||
|
if from_node == _drag_panel and _drag_panel.get_child_count() == 1:
|
||||||
|
return
|
||||||
|
var tab_index = data.tabc_element if data.has("tabc_element") else data.tab_index
|
||||||
|
var moved_tab = from_node.get_tab_control(tab_index)
|
||||||
|
if moved_tab is DockableReferenceControl:
|
||||||
|
moved_tab = moved_tab.reference_to
|
||||||
|
if not _is_managed_node(moved_tab):
|
||||||
|
moved_tab.get_parent().remove_child(moved_tab)
|
||||||
|
add_child(moved_tab)
|
||||||
|
|
||||||
|
if _drag_panel != null:
|
||||||
|
var margin := _drag_n_drop_panel.get_hover_margin()
|
||||||
|
_layout.split_leaf_with_node(_drag_panel.leaf, moved_tab, margin)
|
||||||
|
|
||||||
|
_layout_dirty = true
|
||||||
|
queue_sort()
|
||||||
|
|
||||||
|
|
||||||
|
func set_control_as_current_tab(control: Control) -> void:
|
||||||
|
assert(
|
||||||
|
control.get_parent_control() == self,
|
||||||
|
"Trying to focus a control not managed by this container"
|
||||||
|
)
|
||||||
|
if is_control_hidden(control):
|
||||||
|
push_warning("Trying to focus a hidden control")
|
||||||
|
return
|
||||||
|
var leaf := _layout.get_leaf_for_node(control)
|
||||||
|
if not leaf:
|
||||||
|
return
|
||||||
|
var position_in_leaf := leaf.find_child(control)
|
||||||
|
if position_in_leaf < 0:
|
||||||
|
return
|
||||||
|
var panel: DockablePanel
|
||||||
|
for i in range(1, _panel_container.get_child_count()):
|
||||||
|
var p := _panel_container.get_child(i) as DockablePanel
|
||||||
|
if p.leaf == leaf:
|
||||||
|
panel = p
|
||||||
|
break
|
||||||
|
if not panel:
|
||||||
|
return
|
||||||
|
panel.current_tab = clampi(position_in_leaf, 0, panel.get_tab_count() - 1)
|
||||||
|
|
||||||
|
|
||||||
|
func set_layout(value: DockableLayout) -> void:
|
||||||
|
if value == null:
|
||||||
|
value = DockableLayout.new()
|
||||||
|
if value == _layout:
|
||||||
|
return
|
||||||
|
if _layout and _layout.changed.is_connected(queue_sort):
|
||||||
|
_layout.changed.disconnect(queue_sort)
|
||||||
|
_layout = value
|
||||||
|
_layout.changed.connect(queue_sort)
|
||||||
|
_layout_dirty = true
|
||||||
|
queue_sort()
|
||||||
|
|
||||||
|
|
||||||
|
func set_use_hidden_tabs_for_min_size(value: bool) -> void:
|
||||||
|
_use_hidden_tabs_for_min_size = value
|
||||||
|
for i in range(1, _panel_container.get_child_count()):
|
||||||
|
var panel = _panel_container.get_child(i)
|
||||||
|
panel.use_hidden_tabs_for_min_size = value
|
||||||
|
|
||||||
|
|
||||||
|
func get_use_hidden_tabs_for_min_size() -> bool:
|
||||||
|
return _use_hidden_tabs_for_min_size
|
||||||
|
|
||||||
|
|
||||||
|
func set_control_hidden(child: Control, is_hidden: bool) -> void:
|
||||||
|
_layout.set_node_hidden(child, is_hidden)
|
||||||
|
|
||||||
|
|
||||||
|
func is_control_hidden(child: Control) -> bool:
|
||||||
|
return _layout.is_node_hidden(child)
|
||||||
|
|
||||||
|
|
||||||
|
func get_tabs() -> Array[Control]:
|
||||||
|
var tabs: Array[Control] = []
|
||||||
|
for i in get_child_count():
|
||||||
|
var child := get_child(i)
|
||||||
|
if _is_managed_node(child):
|
||||||
|
tabs.append(child)
|
||||||
|
return tabs
|
||||||
|
|
||||||
|
|
||||||
|
func get_tab_count() -> int:
|
||||||
|
var count := 0
|
||||||
|
for i in get_child_count():
|
||||||
|
var child := get_child(i)
|
||||||
|
if _is_managed_node(child):
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
func _can_handle_drag_data(data) -> bool:
|
||||||
|
if not data is Dictionary:
|
||||||
|
return false
|
||||||
|
var type = data.get("type", "")
|
||||||
|
var tab_type = data.get("tab_type", "")
|
||||||
|
var is_valid_type = (
|
||||||
|
type in ["tab", "tab_container_tab", "tabc_element"]
|
||||||
|
or tab_type in ["tab_container_tab", "tabc_element"]
|
||||||
|
)
|
||||||
|
if is_valid_type:
|
||||||
|
var from_path = data.get("from_path")
|
||||||
|
if from_path:
|
||||||
|
var source_node = get_node_or_null(from_path)
|
||||||
|
if source_node and source_node.has_method("get_tabs_rearrange_group"):
|
||||||
|
return source_node.get_tabs_rearrange_group() == rearrange_group
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _is_managed_node(node: Node) -> bool:
|
||||||
|
return (
|
||||||
|
node.get_parent() == self
|
||||||
|
and node != _panel_container
|
||||||
|
and node != _drag_n_drop_panel
|
||||||
|
and node is Control
|
||||||
|
and not node.top_level
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _update_layout_with_children() -> void:
|
||||||
|
var names := PackedStringArray()
|
||||||
|
_children_names.clear()
|
||||||
|
for i in range(1, get_child_count() - 1):
|
||||||
|
var c := get_child(i)
|
||||||
|
if _track_node(c):
|
||||||
|
names.append(c.name)
|
||||||
|
_layout.update_nodes(names)
|
||||||
|
_layout_dirty = false
|
||||||
|
|
||||||
|
|
||||||
|
func _track_node(node: Node) -> bool:
|
||||||
|
if not _is_managed_node(node):
|
||||||
|
return false
|
||||||
|
_children_names[node] = node.name
|
||||||
|
_children_names[node.name] = node
|
||||||
|
if not node.renamed.is_connected(_on_child_renamed):
|
||||||
|
node.renamed.connect(_on_child_renamed.bind(node))
|
||||||
|
if not node.tree_exiting.is_connected(_untrack_node):
|
||||||
|
node.tree_exiting.connect(_untrack_node.bind(node))
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _track_and_add_node(node: Node) -> void:
|
||||||
|
var tracked_name = _children_names.get(node)
|
||||||
|
if not _track_node(node):
|
||||||
|
return
|
||||||
|
if tracked_name and tracked_name != node.name:
|
||||||
|
_layout.rename_node(tracked_name, node.name)
|
||||||
|
_layout_dirty = true
|
||||||
|
|
||||||
|
|
||||||
|
func _untrack_node(node: Node) -> void:
|
||||||
|
_children_names.erase(node)
|
||||||
|
_children_names.erase(node.name)
|
||||||
|
if node.renamed.is_connected(_on_child_renamed):
|
||||||
|
node.renamed.disconnect(_on_child_renamed)
|
||||||
|
if node.tree_exiting.is_connected(_untrack_node):
|
||||||
|
node.tree_exiting.disconnect(_untrack_node)
|
||||||
|
_layout_dirty = true
|
||||||
|
|
||||||
|
|
||||||
|
func _resort() -> void:
|
||||||
|
assert(_panel_container, "FIXME: resorting without _panel_container")
|
||||||
|
if _panel_container.get_index() != 0:
|
||||||
|
move_child(_panel_container, 0)
|
||||||
|
if _drag_n_drop_panel.get_index() < get_child_count() - 1:
|
||||||
|
_drag_n_drop_panel.move_to_front()
|
||||||
|
|
||||||
|
if _layout_dirty:
|
||||||
|
_update_layout_with_children()
|
||||||
|
|
||||||
|
var rect := Rect2(Vector2.ZERO, size)
|
||||||
|
fit_child_in_rect(_panel_container, rect)
|
||||||
|
_panel_container.fit_child_in_rect(_split_container, rect)
|
||||||
|
|
||||||
|
_current_panel_index = 1
|
||||||
|
_current_split_index = 0
|
||||||
|
|
||||||
|
var children_list := []
|
||||||
|
_calculate_panel_and_split_list(children_list, _layout.root)
|
||||||
|
_fit_panel_and_split_list_to_rect(children_list, rect)
|
||||||
|
|
||||||
|
_untrack_children_after(_panel_container, _current_panel_index)
|
||||||
|
_untrack_children_after(_split_container, _current_split_index)
|
||||||
|
|
||||||
|
|
||||||
|
## Calculate DockablePanel and SplitHandle minimum sizes, skipping empty
|
||||||
|
## branches.
|
||||||
|
##
|
||||||
|
## Returns a DockablePanel checked non-empty leaves, a SplitHandle checked non-empty
|
||||||
|
## splits, `null` if the whole branch is empty and no space should be used.
|
||||||
|
##
|
||||||
|
## `result` will be filled with the non-empty nodes in this post-order tree
|
||||||
|
## traversal.
|
||||||
|
func _calculate_panel_and_split_list(result: Array, layout_node: DockableLayoutNode):
|
||||||
|
if layout_node is DockableLayoutPanel:
|
||||||
|
var nodes: Array[Control] = []
|
||||||
|
for n in layout_node.names:
|
||||||
|
var node: Control = _children_names.get(n)
|
||||||
|
if node:
|
||||||
|
assert(node is Control, "FIXME: node is not a control %s" % node)
|
||||||
|
assert(
|
||||||
|
node.get_parent_control() == self,
|
||||||
|
"FIXME: node is not child of container %s" % node
|
||||||
|
)
|
||||||
|
if is_control_hidden(node):
|
||||||
|
node.visible = false
|
||||||
|
else:
|
||||||
|
nodes.append(node)
|
||||||
|
if nodes.is_empty():
|
||||||
|
return null
|
||||||
|
else:
|
||||||
|
var panel := _get_panel(_current_panel_index)
|
||||||
|
_current_panel_index += 1
|
||||||
|
panel.track_nodes(nodes, layout_node)
|
||||||
|
result.append(panel)
|
||||||
|
return panel
|
||||||
|
elif layout_node is DockableLayoutSplit:
|
||||||
|
# by processing `second` before `first`, traversing `result` from back
|
||||||
|
# to front yields a nice pre-order tree traversal
|
||||||
|
var second_result = _calculate_panel_and_split_list(result, layout_node.second)
|
||||||
|
var first_result = _calculate_panel_and_split_list(result, layout_node.first)
|
||||||
|
if first_result and second_result:
|
||||||
|
var split := _get_split(_current_split_index)
|
||||||
|
_current_split_index += 1
|
||||||
|
split.layout_split = layout_node
|
||||||
|
split.first_minimum_size = first_result.get_layout_minimum_size()
|
||||||
|
split.second_minimum_size = second_result.get_layout_minimum_size()
|
||||||
|
result.append(split)
|
||||||
|
return split
|
||||||
|
elif first_result:
|
||||||
|
return first_result
|
||||||
|
else: # NOTE: this returns null if `second_result` is null
|
||||||
|
return second_result
|
||||||
|
else:
|
||||||
|
push_warning("FIXME: invalid Resource, should be branch or leaf, found %s" % layout_node)
|
||||||
|
|
||||||
|
|
||||||
|
## Traverse list from back to front fitting controls where they belong.
|
||||||
|
##
|
||||||
|
## Be sure to call this with the result from `_calculate_split_minimum_sizes`.
|
||||||
|
func _fit_panel_and_split_list_to_rect(panel_and_split_list: Array, rect: Rect2) -> void:
|
||||||
|
var control = panel_and_split_list.pop_back()
|
||||||
|
if control is DockablePanel:
|
||||||
|
_panel_container.fit_child_in_rect(control, rect)
|
||||||
|
elif control is SplitHandle:
|
||||||
|
var split_rects = control.get_split_rects(rect)
|
||||||
|
_split_container.fit_child_in_rect(control, split_rects["self"])
|
||||||
|
_fit_panel_and_split_list_to_rect(panel_and_split_list, split_rects["first"])
|
||||||
|
_fit_panel_and_split_list_to_rect(panel_and_split_list, split_rects["second"])
|
||||||
|
|
||||||
|
|
||||||
|
## Get the idx'th DockablePanel, reusing an instanced one if possible
|
||||||
|
func _get_panel(idx: int) -> DockablePanel:
|
||||||
|
assert(_panel_container, "FIXME: creating panel without _panel_container")
|
||||||
|
if idx < _panel_container.get_child_count():
|
||||||
|
return _panel_container.get_child(idx)
|
||||||
|
var panel := DockablePanel.new()
|
||||||
|
panel.tab_alignment = _tab_align
|
||||||
|
panel.show_tabs = _tabs_visible
|
||||||
|
panel.hide_single_tab = _hide_single_tab
|
||||||
|
panel.use_hidden_tabs_for_min_size = _use_hidden_tabs_for_min_size
|
||||||
|
panel.set_tabs_rearrange_group(maxi(0, rearrange_group))
|
||||||
|
_panel_container.add_child(panel)
|
||||||
|
panel.tab_layout_changed.connect(_on_panel_tab_layout_changed.bind(panel))
|
||||||
|
return panel
|
||||||
|
|
||||||
|
|
||||||
|
## Get the idx'th SplitHandle, reusing an instanced one if possible
|
||||||
|
func _get_split(idx: int) -> SplitHandle:
|
||||||
|
assert(_split_container, "FIXME: creating split without _split_container")
|
||||||
|
if idx < _split_container.get_child_count():
|
||||||
|
return _split_container.get_child(idx)
|
||||||
|
var split := SplitHandle.new()
|
||||||
|
_split_container.add_child(split)
|
||||||
|
return split
|
||||||
|
|
||||||
|
|
||||||
|
## Helper for removing and freeing all remaining children from node
|
||||||
|
func _untrack_children_after(node: Control, idx: int) -> void:
|
||||||
|
for i in range(idx, node.get_child_count()):
|
||||||
|
var child := node.get_child(idx)
|
||||||
|
node.remove_child(child)
|
||||||
|
child.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
## Handler for `DockablePanel.tab_layout_changed`, update its DockableLayoutPanel
|
||||||
|
func _on_panel_tab_layout_changed(tab: int, panel: DockablePanel) -> void:
|
||||||
|
_layout_dirty = true
|
||||||
|
var control := panel.get_tab_control(tab)
|
||||||
|
if control is DockableReferenceControl:
|
||||||
|
control = control.reference_to
|
||||||
|
if not _is_managed_node(control):
|
||||||
|
control.get_parent().remove_child(control)
|
||||||
|
add_child(control)
|
||||||
|
_layout.move_node_to_leaf(control, panel.leaf, tab)
|
||||||
|
queue_sort()
|
||||||
|
|
||||||
|
|
||||||
|
## Handler for `Node.renamed` signal, updates tracked name for node
|
||||||
|
func _on_child_renamed(child: Node) -> void:
|
||||||
|
var old_name: String = _children_names.get(child)
|
||||||
|
if old_name == str(child.name):
|
||||||
|
return
|
||||||
|
_children_names.erase(old_name)
|
||||||
|
_children_names[child] = child.name
|
||||||
|
_children_names[child.name] = child
|
||||||
|
_layout.rename_node(old_name, child.name)
|
||||||
1
dockable_container.gd.uid
Normal file
1
dockable_container.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://ccdi3mi0a414a
|
||||||
108
dockable_panel.gd
Normal file
108
dockable_panel.gd
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
@tool
|
||||||
|
extends TabContainer
|
||||||
|
|
||||||
|
signal tab_layout_changed(tab)
|
||||||
|
|
||||||
|
var leaf: DockableLayoutPanel:
|
||||||
|
get:
|
||||||
|
return get_leaf()
|
||||||
|
set(value):
|
||||||
|
set_leaf(value)
|
||||||
|
var show_tabs := true:
|
||||||
|
get:
|
||||||
|
return _show_tabs
|
||||||
|
set(value):
|
||||||
|
_show_tabs = value
|
||||||
|
_handle_tab_visibility()
|
||||||
|
var hide_single_tab := false:
|
||||||
|
get:
|
||||||
|
return _hide_single_tab
|
||||||
|
set(value):
|
||||||
|
_hide_single_tab = value
|
||||||
|
_handle_tab_visibility()
|
||||||
|
|
||||||
|
var _leaf: DockableLayoutPanel
|
||||||
|
var _show_tabs := true
|
||||||
|
var _hide_single_tab := false
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
drag_to_rearrange_enabled = true
|
||||||
|
|
||||||
|
|
||||||
|
func _enter_tree() -> void:
|
||||||
|
active_tab_rearranged.connect(_on_tab_changed)
|
||||||
|
tab_selected.connect(_on_tab_selected)
|
||||||
|
tab_changed.connect(_on_tab_changed)
|
||||||
|
|
||||||
|
|
||||||
|
func _exit_tree() -> void:
|
||||||
|
active_tab_rearranged.disconnect(_on_tab_changed)
|
||||||
|
tab_selected.disconnect(_on_tab_selected)
|
||||||
|
tab_changed.disconnect(_on_tab_changed)
|
||||||
|
|
||||||
|
|
||||||
|
func track_nodes(nodes: Array[Control], new_leaf: DockableLayoutPanel) -> void:
|
||||||
|
_leaf = null # avoid using previous leaf in tab_changed signals
|
||||||
|
var min_size := mini(nodes.size(), get_child_count())
|
||||||
|
# remove spare children
|
||||||
|
for i in range(min_size, get_child_count()):
|
||||||
|
var child := get_child(min_size) as DockableReferenceControl
|
||||||
|
child.reference_to = null
|
||||||
|
remove_child(child)
|
||||||
|
child.queue_free()
|
||||||
|
# add missing children
|
||||||
|
for i in range(min_size, nodes.size()):
|
||||||
|
var ref_control := DockableReferenceControl.new()
|
||||||
|
add_child(ref_control)
|
||||||
|
assert(nodes.size() == get_child_count(), "FIXME")
|
||||||
|
# setup children
|
||||||
|
for i in nodes.size():
|
||||||
|
var ref_control := get_child(i) as DockableReferenceControl
|
||||||
|
ref_control.reference_to = nodes[i]
|
||||||
|
set_tab_title(i, nodes[i].name)
|
||||||
|
set_leaf(new_leaf)
|
||||||
|
_handle_tab_visibility()
|
||||||
|
|
||||||
|
|
||||||
|
func get_child_rect() -> Rect2:
|
||||||
|
var control := get_current_tab_control()
|
||||||
|
return Rect2(position + control.position, control.size)
|
||||||
|
|
||||||
|
|
||||||
|
func set_leaf(value: DockableLayoutPanel) -> void:
|
||||||
|
if get_tab_count() > 0 and value:
|
||||||
|
current_tab = clampi(value.current_tab, 0, get_tab_count() - 1)
|
||||||
|
_leaf = value
|
||||||
|
|
||||||
|
|
||||||
|
func get_leaf() -> DockableLayoutPanel:
|
||||||
|
return _leaf
|
||||||
|
|
||||||
|
|
||||||
|
func get_layout_minimum_size() -> Vector2:
|
||||||
|
return get_combined_minimum_size()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_tab_selected(tab: int) -> void:
|
||||||
|
if _leaf:
|
||||||
|
_leaf.current_tab = tab
|
||||||
|
|
||||||
|
|
||||||
|
func _on_tab_changed(tab: int) -> void:
|
||||||
|
if not _leaf:
|
||||||
|
return
|
||||||
|
var control := get_tab_control(tab)
|
||||||
|
if not control:
|
||||||
|
return
|
||||||
|
var tab_name := control.name
|
||||||
|
var name_index_in_leaf := _leaf.find_name(tab_name)
|
||||||
|
if name_index_in_leaf != tab: # NOTE: this handles added tabs (index == -1)
|
||||||
|
tab_layout_changed.emit(tab)
|
||||||
|
|
||||||
|
|
||||||
|
func _handle_tab_visibility() -> void:
|
||||||
|
if _hide_single_tab and get_tab_count() == 1:
|
||||||
|
tabs_visible = false
|
||||||
|
else:
|
||||||
|
tabs_visible = _show_tabs
|
||||||
1
dockable_panel.gd.uid
Normal file
1
dockable_panel.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://eab55b0hqw02
|
||||||
49
dockable_panel_reference_control.gd
Normal file
49
dockable_panel_reference_control.gd
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
@tool
|
||||||
|
class_name DockableReferenceControl
|
||||||
|
extends Container
|
||||||
|
## Control that mimics its own visibility and rect into another Control.
|
||||||
|
|
||||||
|
var reference_to: Control:
|
||||||
|
get:
|
||||||
|
return _reference_to
|
||||||
|
set(control):
|
||||||
|
if _reference_to != control:
|
||||||
|
if is_instance_valid(_reference_to):
|
||||||
|
_reference_to.renamed.disconnect(_on_reference_to_renamed)
|
||||||
|
_reference_to.minimum_size_changed.disconnect(update_minimum_size)
|
||||||
|
_reference_to = control
|
||||||
|
|
||||||
|
minimum_size_changed.emit()
|
||||||
|
if not is_instance_valid(_reference_to):
|
||||||
|
return
|
||||||
|
_reference_to.renamed.connect(_on_reference_to_renamed)
|
||||||
|
_reference_to.minimum_size_changed.connect(update_minimum_size)
|
||||||
|
_reference_to.visible = visible
|
||||||
|
_reposition_reference()
|
||||||
|
|
||||||
|
var _reference_to: Control = null
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
mouse_filter = MOUSE_FILTER_IGNORE
|
||||||
|
set_notify_transform(true)
|
||||||
|
|
||||||
|
|
||||||
|
func _notification(what: int) -> void:
|
||||||
|
if what == NOTIFICATION_VISIBILITY_CHANGED and _reference_to:
|
||||||
|
_reference_to.visible = visible
|
||||||
|
elif what == NOTIFICATION_TRANSFORM_CHANGED and _reference_to:
|
||||||
|
_reposition_reference()
|
||||||
|
|
||||||
|
|
||||||
|
func _get_minimum_size() -> Vector2:
|
||||||
|
return _reference_to.get_combined_minimum_size() if _reference_to else Vector2.ZERO
|
||||||
|
|
||||||
|
|
||||||
|
func _reposition_reference() -> void:
|
||||||
|
_reference_to.global_position = global_position
|
||||||
|
_reference_to.size = size
|
||||||
|
|
||||||
|
|
||||||
|
func _on_reference_to_renamed() -> void:
|
||||||
|
name = _reference_to.name
|
||||||
1
dockable_panel_reference_control.gd.uid
Normal file
1
dockable_panel_reference_control.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dpy1ktn2frppg
|
||||||
82
drag_n_drop_panel.gd
Normal file
82
drag_n_drop_panel.gd
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
@tool
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
enum { MARGIN_LEFT, MARGIN_RIGHT, MARGIN_TOP, MARGIN_BOTTOM, MARGIN_CENTER }
|
||||||
|
|
||||||
|
const DRAW_NOTHING := -1
|
||||||
|
const DRAW_CENTERED := -2
|
||||||
|
const MARGIN_NONE := -1
|
||||||
|
|
||||||
|
var _draw_margin := DRAW_NOTHING
|
||||||
|
var _should_split := false
|
||||||
|
|
||||||
|
|
||||||
|
func _notification(what: int) -> void:
|
||||||
|
if what == NOTIFICATION_MOUSE_EXIT:
|
||||||
|
_draw_margin = DRAW_NOTHING
|
||||||
|
queue_redraw()
|
||||||
|
elif what == NOTIFICATION_MOUSE_ENTER and not _should_split:
|
||||||
|
_draw_margin = DRAW_CENTERED
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func _gui_input(event: InputEvent) -> void:
|
||||||
|
if _should_split and event is InputEventMouseMotion:
|
||||||
|
_draw_margin = _find_hover_margin(event.position)
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
var rect: Rect2
|
||||||
|
if _draw_margin == DRAW_NOTHING:
|
||||||
|
return
|
||||||
|
elif _draw_margin == DRAW_CENTERED:
|
||||||
|
rect = Rect2(Vector2.ZERO, size)
|
||||||
|
elif _draw_margin == MARGIN_LEFT:
|
||||||
|
rect = Rect2(0, 0, size.x * 0.5, size.y)
|
||||||
|
elif _draw_margin == MARGIN_TOP:
|
||||||
|
rect = Rect2(0, 0, size.x, size.y * 0.5)
|
||||||
|
elif _draw_margin == MARGIN_RIGHT:
|
||||||
|
var half_width = size.x * 0.5
|
||||||
|
rect = Rect2(half_width, 0, half_width, size.y)
|
||||||
|
elif _draw_margin == MARGIN_BOTTOM:
|
||||||
|
var half_height = size.y * 0.5
|
||||||
|
rect = Rect2(0, half_height, size.x, half_height)
|
||||||
|
var stylebox := get_theme_stylebox("panel", "TooltipPanel")
|
||||||
|
draw_style_box(stylebox, rect)
|
||||||
|
|
||||||
|
|
||||||
|
func set_enabled(enabled: bool, should_split: bool = true) -> void:
|
||||||
|
visible = enabled
|
||||||
|
_should_split = should_split
|
||||||
|
if enabled:
|
||||||
|
_draw_margin = DRAW_NOTHING
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func get_hover_margin() -> int:
|
||||||
|
return _draw_margin
|
||||||
|
|
||||||
|
|
||||||
|
func _find_hover_margin(point: Vector2) -> int:
|
||||||
|
var half_size := size * 0.5
|
||||||
|
|
||||||
|
var left := point.distance_squared_to(Vector2(0, half_size.y))
|
||||||
|
var lesser := left
|
||||||
|
var lesser_margin := MARGIN_LEFT
|
||||||
|
|
||||||
|
var top := point.distance_squared_to(Vector2(half_size.x, 0))
|
||||||
|
if lesser > top:
|
||||||
|
lesser = top
|
||||||
|
lesser_margin = MARGIN_TOP
|
||||||
|
|
||||||
|
var right := point.distance_squared_to(Vector2(size.x, half_size.y))
|
||||||
|
if lesser > right:
|
||||||
|
lesser = right
|
||||||
|
lesser_margin = MARGIN_RIGHT
|
||||||
|
|
||||||
|
var bottom := point.distance_squared_to(Vector2(half_size.x, size.y))
|
||||||
|
if lesser > bottom:
|
||||||
|
#lesser = bottom # unused result
|
||||||
|
lesser_margin = MARGIN_BOTTOM
|
||||||
|
return lesser_margin
|
||||||
1
drag_n_drop_panel.gd.uid
Normal file
1
drag_n_drop_panel.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cyjm4hrcfacxv
|
||||||
10
icon.svg
Normal file
10
icon.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g>
|
||||||
|
<rect style="fill-rule: nonzero; fill: rgb(142, 239, 152);" x="4.001" y="4.016" width="4.005" height="5.004" rx="1" ry="1"/>
|
||||||
|
<rect style="fill-rule: nonzero; fill: rgb(142, 239, 152);" x="8.999" y="7.016" width="3.006" height="5.004" rx="1" ry="1"/>
|
||||||
|
<rect style="fill-rule: nonzero; fill: rgb(142, 239, 152);" x="4.004" y="10.023" width="4.005" height="1.99" rx="1" ry="1"/>
|
||||||
|
<rect style="fill-rule: nonzero; fill: rgb(142, 239, 152);" x="9" y="3.991" width="3.006" height="2.031" rx="1" ry="1"/>
|
||||||
|
</g>
|
||||||
|
<path d="M 13 1 C 14.097 1 15 1.903 15 3 L 15 13 C 15 14.097 14.097 15 13 15 L 3 15 C 1.903 15 1 14.097 1 13 L 1 3 C 1 1.903 1.903 1 3 1 L 13 1 Z M 3 13 L 13 13 L 13 3 L 3 3 L 3 13 Z" fill-rule="nonzero" style="fill-rule: nonzero; fill: rgb(142, 239, 152);"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 979 B |
43
icon.svg.import
Normal file
43
icon.svg.import
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dy25danh2am23"
|
||||||
|
path="res://.godot/imported/icon.svg-35635e7bbda4487d4b2942da1d987df8.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://addons/dockable_container/icon.svg"
|
||||||
|
dest_files=["res://.godot/imported/icon.svg-35635e7bbda4487d4b2942da1d987df8.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
svg/scale=1.0
|
||||||
|
editor/scale_with_editor_scale=false
|
||||||
|
editor/convert_colors_with_editor_theme=false
|
||||||
22
inspector_plugin/editor_inspector_plugin.gd
Normal file
22
inspector_plugin/editor_inspector_plugin.gd
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
extends EditorInspectorPlugin
|
||||||
|
|
||||||
|
const LayoutEditorProperty := preload("layout_editor_property.gd")
|
||||||
|
|
||||||
|
|
||||||
|
func _can_handle(object: Object) -> bool:
|
||||||
|
return object is DockableContainer
|
||||||
|
|
||||||
|
|
||||||
|
func _parse_property(
|
||||||
|
_object: Object,
|
||||||
|
_type: Variant.Type,
|
||||||
|
name: String,
|
||||||
|
_hint: PropertyHint,
|
||||||
|
_hint_text: String,
|
||||||
|
_usage: int,
|
||||||
|
_wide: bool
|
||||||
|
) -> bool:
|
||||||
|
if name == "layout":
|
||||||
|
var editor_property := LayoutEditorProperty.new()
|
||||||
|
add_property_editor("layout", editor_property)
|
||||||
|
return false
|
||||||
1
inspector_plugin/editor_inspector_plugin.gd.uid
Normal file
1
inspector_plugin/editor_inspector_plugin.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://c37eakt4eetx
|
||||||
71
inspector_plugin/layout_editor_property.gd
Normal file
71
inspector_plugin/layout_editor_property.gd
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
extends EditorProperty
|
||||||
|
|
||||||
|
var _container := DockableContainer.new()
|
||||||
|
var _hidden_menu_button := MenuButton.new()
|
||||||
|
var _hidden_menu_popup: PopupMenu
|
||||||
|
var _hidden_menu_list: PackedStringArray
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
custom_minimum_size = Vector2(128, 256)
|
||||||
|
|
||||||
|
_hidden_menu_button.text = "Visible nodes"
|
||||||
|
add_child(_hidden_menu_button)
|
||||||
|
_hidden_menu_popup = _hidden_menu_button.get_popup()
|
||||||
|
_hidden_menu_popup.hide_on_checkable_item_selection = false
|
||||||
|
_hidden_menu_popup.about_to_popup.connect(_on_hidden_menu_popup_about_to_show)
|
||||||
|
_hidden_menu_popup.id_pressed.connect(_on_hidden_menu_popup_id_pressed)
|
||||||
|
|
||||||
|
_container.clone_layout_on_ready = false
|
||||||
|
_container.custom_minimum_size = custom_minimum_size
|
||||||
|
|
||||||
|
var value := _get_layout().clone() # The layout gets reset when selecting it without clone
|
||||||
|
for n in value.get_names():
|
||||||
|
var child := _create_child_control(n)
|
||||||
|
_container.add_child(child)
|
||||||
|
_container.set(get_edited_property(), value)
|
||||||
|
add_child(_container)
|
||||||
|
set_bottom_editor(_container)
|
||||||
|
|
||||||
|
|
||||||
|
func _exit_tree() -> void: # Not sure if this is needed, but just to be sure
|
||||||
|
queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func _update_property() -> void:
|
||||||
|
var value := _get_layout()
|
||||||
|
_container.set(get_edited_property(), value)
|
||||||
|
|
||||||
|
|
||||||
|
func _get_layout() -> DockableLayout:
|
||||||
|
var original_container := get_edited_object() as DockableContainer
|
||||||
|
return original_container.get(get_edited_property())
|
||||||
|
|
||||||
|
|
||||||
|
func _create_child_control(named: String) -> Label:
|
||||||
|
var new_control := Label.new()
|
||||||
|
new_control.name = named
|
||||||
|
new_control.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
new_control.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
new_control.clip_text = true
|
||||||
|
new_control.text = named
|
||||||
|
return new_control
|
||||||
|
|
||||||
|
|
||||||
|
func _on_hidden_menu_popup_about_to_show() -> void:
|
||||||
|
var layout := _get_layout().clone()
|
||||||
|
_hidden_menu_popup.clear()
|
||||||
|
_hidden_menu_list = layout.get_names()
|
||||||
|
for i in _hidden_menu_list.size():
|
||||||
|
var tab_name := _hidden_menu_list[i]
|
||||||
|
_hidden_menu_popup.add_check_item(tab_name, i)
|
||||||
|
_hidden_menu_popup.set_item_checked(i, not layout.is_tab_hidden(tab_name))
|
||||||
|
|
||||||
|
|
||||||
|
func _on_hidden_menu_popup_id_pressed(id: int) -> void:
|
||||||
|
var layout := _get_layout().clone()
|
||||||
|
var tab_name := _hidden_menu_list[id]
|
||||||
|
var new_hidden := not layout.is_tab_hidden(tab_name)
|
||||||
|
_get_layout().set_tab_hidden(tab_name, new_hidden)
|
||||||
|
_hidden_menu_popup.set_item_checked(id, not new_hidden)
|
||||||
|
emit_changed(get_edited_property(), _get_layout()) # This line may not be needed
|
||||||
1
inspector_plugin/layout_editor_property.gd.uid
Normal file
1
inspector_plugin/layout_editor_property.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dggtoctbof2yq
|
||||||
242
layout.gd
Normal file
242
layout.gd
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
@tool
|
||||||
|
class_name DockableLayout
|
||||||
|
extends Resource
|
||||||
|
## DockableLayout Resource definition, holding the root DockableLayoutNode and hidden tabs.
|
||||||
|
##
|
||||||
|
## DockableLayoutSplit are binary trees with nested DockableLayoutSplit subtrees
|
||||||
|
## and DockableLayoutPanel leaves. Both of them inherit from DockableLayoutNode to help with
|
||||||
|
## type annotation and define common functionality.
|
||||||
|
##
|
||||||
|
## Hidden tabs are marked in the `hidden_tabs` Dictionary by name.
|
||||||
|
|
||||||
|
enum { MARGIN_LEFT, MARGIN_RIGHT, MARGIN_TOP, MARGIN_BOTTOM, MARGIN_CENTER }
|
||||||
|
|
||||||
|
@export var root: DockableLayoutNode = DockableLayoutPanel.new():
|
||||||
|
get:
|
||||||
|
return _root
|
||||||
|
set(value):
|
||||||
|
set_root(value)
|
||||||
|
@export var hidden_tabs := {}:
|
||||||
|
get:
|
||||||
|
return _hidden_tabs
|
||||||
|
set(value):
|
||||||
|
if value != _hidden_tabs:
|
||||||
|
_hidden_tabs = value
|
||||||
|
changed.emit()
|
||||||
|
|
||||||
|
var _changed_signal_queued := false
|
||||||
|
var _first_leaf: DockableLayoutPanel
|
||||||
|
var _hidden_tabs: Dictionary
|
||||||
|
var _leaf_by_node_name: Dictionary
|
||||||
|
var _root: DockableLayoutNode = DockableLayoutPanel.new()
|
||||||
|
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
resource_name = "Layout"
|
||||||
|
|
||||||
|
|
||||||
|
func set_root(value: DockableLayoutNode, should_emit_changed := true) -> void:
|
||||||
|
if not value:
|
||||||
|
value = DockableLayoutPanel.new()
|
||||||
|
if _root == value:
|
||||||
|
return
|
||||||
|
if _root and _root.changed.is_connected(_on_root_changed):
|
||||||
|
_root.changed.disconnect(_on_root_changed)
|
||||||
|
_root = value
|
||||||
|
_root.parent = null
|
||||||
|
_root.changed.connect(_on_root_changed)
|
||||||
|
if should_emit_changed:
|
||||||
|
_on_root_changed()
|
||||||
|
|
||||||
|
|
||||||
|
func get_root() -> DockableLayoutNode:
|
||||||
|
return _root
|
||||||
|
|
||||||
|
|
||||||
|
func clone() -> DockableLayout:
|
||||||
|
return duplicate(true)
|
||||||
|
|
||||||
|
|
||||||
|
func get_names() -> PackedStringArray:
|
||||||
|
return _root.get_names()
|
||||||
|
|
||||||
|
|
||||||
|
## Add missing nodes on first leaf and remove nodes outside indices from leaves.
|
||||||
|
##
|
||||||
|
## _leaf_by_node_name = {
|
||||||
|
## (string keys) = respective Leaf that holds the node name,
|
||||||
|
## }
|
||||||
|
func update_nodes(names: PackedStringArray) -> void:
|
||||||
|
_leaf_by_node_name.clear()
|
||||||
|
_first_leaf = null
|
||||||
|
var empty_leaves: Array[DockableLayoutPanel] = []
|
||||||
|
_ensure_names_in_node(_root, names, empty_leaves) # Changes _leaf_by_node_name and empty_leaves
|
||||||
|
for l in empty_leaves:
|
||||||
|
_remove_leaf(l)
|
||||||
|
if not _first_leaf:
|
||||||
|
_first_leaf = DockableLayoutPanel.new()
|
||||||
|
set_root(_first_leaf)
|
||||||
|
for n in names:
|
||||||
|
if not _leaf_by_node_name.has(n):
|
||||||
|
_first_leaf.push_name(n)
|
||||||
|
_leaf_by_node_name[n] = _first_leaf
|
||||||
|
_on_root_changed()
|
||||||
|
|
||||||
|
|
||||||
|
func move_node_to_leaf(node: Node, leaf: DockableLayoutPanel, relative_position: int) -> void:
|
||||||
|
var node_name := node.name
|
||||||
|
var previous_leaf: DockableLayoutPanel = _leaf_by_node_name.get(node_name)
|
||||||
|
if previous_leaf:
|
||||||
|
previous_leaf.remove_node(node)
|
||||||
|
if previous_leaf.is_empty():
|
||||||
|
_remove_leaf(previous_leaf)
|
||||||
|
|
||||||
|
leaf.insert_node(relative_position, node)
|
||||||
|
_leaf_by_node_name[node_name] = leaf
|
||||||
|
_on_root_changed()
|
||||||
|
|
||||||
|
|
||||||
|
func get_leaf_for_node(node: Node) -> DockableLayoutPanel:
|
||||||
|
return _leaf_by_node_name.get(node.name)
|
||||||
|
|
||||||
|
|
||||||
|
func split_leaf_with_node(leaf: DockableLayoutPanel, node: Node, margin: int) -> void:
|
||||||
|
var root_branch := leaf.parent
|
||||||
|
var new_leaf := DockableLayoutPanel.new()
|
||||||
|
var new_branch := DockableLayoutSplit.new()
|
||||||
|
if margin == MARGIN_LEFT or margin == MARGIN_RIGHT:
|
||||||
|
new_branch.direction = DockableLayoutSplit.Direction.HORIZONTAL
|
||||||
|
else:
|
||||||
|
new_branch.direction = DockableLayoutSplit.Direction.VERTICAL
|
||||||
|
if margin == MARGIN_LEFT or margin == MARGIN_TOP:
|
||||||
|
new_branch.first = new_leaf
|
||||||
|
new_branch.second = leaf
|
||||||
|
else:
|
||||||
|
new_branch.first = leaf
|
||||||
|
new_branch.second = new_leaf
|
||||||
|
if _root == leaf:
|
||||||
|
set_root(new_branch, false)
|
||||||
|
elif root_branch:
|
||||||
|
if leaf == root_branch.first:
|
||||||
|
root_branch.first = new_branch
|
||||||
|
else:
|
||||||
|
root_branch.second = new_branch
|
||||||
|
|
||||||
|
move_node_to_leaf(node, new_leaf, 0)
|
||||||
|
|
||||||
|
|
||||||
|
func add_node(node: Node) -> void:
|
||||||
|
var node_name := node.name
|
||||||
|
if _leaf_by_node_name.has(node_name):
|
||||||
|
return
|
||||||
|
_first_leaf.push_name(node_name)
|
||||||
|
_leaf_by_node_name[node_name] = _first_leaf
|
||||||
|
_on_root_changed()
|
||||||
|
|
||||||
|
|
||||||
|
func remove_node(node: Node) -> void:
|
||||||
|
var node_name := node.name
|
||||||
|
var leaf: DockableLayoutPanel = _leaf_by_node_name.get(node_name)
|
||||||
|
if not leaf:
|
||||||
|
return
|
||||||
|
leaf.remove_node(node)
|
||||||
|
_leaf_by_node_name.erase(node_name)
|
||||||
|
if leaf.is_empty():
|
||||||
|
_remove_leaf(leaf)
|
||||||
|
_on_root_changed()
|
||||||
|
|
||||||
|
|
||||||
|
func rename_node(previous_name: String, new_name: String) -> void:
|
||||||
|
var leaf: DockableLayoutPanel = _leaf_by_node_name.get(previous_name)
|
||||||
|
if not leaf:
|
||||||
|
return
|
||||||
|
leaf.rename_node(previous_name, new_name)
|
||||||
|
_leaf_by_node_name.erase(previous_name)
|
||||||
|
_leaf_by_node_name[new_name] = leaf
|
||||||
|
_on_root_changed()
|
||||||
|
|
||||||
|
|
||||||
|
func set_tab_hidden(name: String, hidden: bool) -> void:
|
||||||
|
if not _leaf_by_node_name.has(name):
|
||||||
|
return
|
||||||
|
if hidden:
|
||||||
|
_hidden_tabs[name] = true
|
||||||
|
else:
|
||||||
|
_hidden_tabs.erase(name)
|
||||||
|
_on_root_changed()
|
||||||
|
|
||||||
|
|
||||||
|
func is_tab_hidden(name: String) -> bool:
|
||||||
|
return _hidden_tabs.get(name, false)
|
||||||
|
|
||||||
|
|
||||||
|
func set_node_hidden(node: Node, hidden: bool) -> void:
|
||||||
|
set_tab_hidden(node.name, hidden)
|
||||||
|
|
||||||
|
|
||||||
|
func is_node_hidden(node: Node) -> bool:
|
||||||
|
return is_tab_hidden(node.name)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_root_changed() -> void:
|
||||||
|
if _changed_signal_queued:
|
||||||
|
return
|
||||||
|
_changed_signal_queued = true
|
||||||
|
set_deferred("_changed_signal_queued", false)
|
||||||
|
emit_changed.call_deferred()
|
||||||
|
|
||||||
|
|
||||||
|
func _ensure_names_in_node(
|
||||||
|
node: DockableLayoutNode, names: PackedStringArray, empty_leaves: Array[DockableLayoutPanel]
|
||||||
|
) -> void:
|
||||||
|
if node is DockableLayoutPanel:
|
||||||
|
node.update_nodes(names, _leaf_by_node_name) # This changes _leaf_by_node_name
|
||||||
|
if node.is_empty():
|
||||||
|
empty_leaves.append(node)
|
||||||
|
if not _first_leaf:
|
||||||
|
_first_leaf = node
|
||||||
|
elif node is DockableLayoutSplit:
|
||||||
|
_ensure_names_in_node(node.first, names, empty_leaves)
|
||||||
|
_ensure_names_in_node(node.second, names, empty_leaves)
|
||||||
|
else:
|
||||||
|
assert(false, "Invalid Resource, should be branch or leaf, found %s" % node)
|
||||||
|
|
||||||
|
|
||||||
|
func _remove_leaf(leaf: DockableLayoutPanel) -> void:
|
||||||
|
assert(leaf.is_empty(), "FIXME: trying to remove_at a leaf with nodes")
|
||||||
|
if _root == leaf:
|
||||||
|
return
|
||||||
|
var collapsed_branch := leaf.parent
|
||||||
|
assert(collapsed_branch is DockableLayoutSplit, "FIXME: leaf is not a child of branch")
|
||||||
|
var kept_branch: DockableLayoutNode = (
|
||||||
|
collapsed_branch.first if leaf == collapsed_branch.second else collapsed_branch.second
|
||||||
|
)
|
||||||
|
var root_branch := collapsed_branch.parent #HERE
|
||||||
|
if collapsed_branch == _root:
|
||||||
|
set_root(kept_branch, true)
|
||||||
|
elif root_branch:
|
||||||
|
if collapsed_branch == root_branch.first:
|
||||||
|
root_branch.first = kept_branch
|
||||||
|
else:
|
||||||
|
root_branch.second = kept_branch
|
||||||
|
|
||||||
|
|
||||||
|
func _print_tree() -> void:
|
||||||
|
print("TREE")
|
||||||
|
_print_tree_step(_root, 0, 0)
|
||||||
|
print("")
|
||||||
|
|
||||||
|
|
||||||
|
func _print_tree_step(tree_or_leaf: DockableLayoutNode, level: int, idx: int) -> void:
|
||||||
|
if tree_or_leaf is DockableLayoutPanel:
|
||||||
|
print(" |".repeat(level), "- (%d) = " % idx, tree_or_leaf.names)
|
||||||
|
elif tree_or_leaf is DockableLayoutSplit:
|
||||||
|
print(
|
||||||
|
" |".repeat(level),
|
||||||
|
"-+ (%d) = " % idx,
|
||||||
|
tree_or_leaf.direction,
|
||||||
|
" ",
|
||||||
|
tree_or_leaf.percent
|
||||||
|
)
|
||||||
|
_print_tree_step(tree_or_leaf.first, level + 1, 1)
|
||||||
|
_print_tree_step(tree_or_leaf.second, level + 1, 2)
|
||||||
1
layout.gd.uid
Normal file
1
layout.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://b1bj11c3wrsh3
|
||||||
29
layout_node.gd
Normal file
29
layout_node.gd
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
@tool
|
||||||
|
class_name DockableLayoutNode
|
||||||
|
extends Resource
|
||||||
|
## Base class for DockableLayout tree nodes
|
||||||
|
|
||||||
|
var parent: DockableLayoutSplit:
|
||||||
|
get:
|
||||||
|
return _parent_ref.get_ref()
|
||||||
|
set(value):
|
||||||
|
_parent_ref = weakref(value)
|
||||||
|
|
||||||
|
var _parent_ref := WeakRef.new()
|
||||||
|
|
||||||
|
|
||||||
|
func emit_tree_changed() -> void:
|
||||||
|
var node := self
|
||||||
|
while node:
|
||||||
|
node.emit_changed()
|
||||||
|
node = node.parent
|
||||||
|
|
||||||
|
|
||||||
|
## Returns whether there are any nodes
|
||||||
|
func is_empty() -> bool:
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
## Returns all tab names in this node
|
||||||
|
func get_names() -> PackedStringArray:
|
||||||
|
return PackedStringArray()
|
||||||
1
layout_node.gd.uid
Normal file
1
layout_node.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dget6gbeqpvlm
|
||||||
89
layout_panel.gd
Normal file
89
layout_panel.gd
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
@tool
|
||||||
|
class_name DockableLayoutPanel
|
||||||
|
extends DockableLayoutNode
|
||||||
|
## DockableLayout leaf nodes, defining tabs
|
||||||
|
|
||||||
|
@export var names: PackedStringArray:
|
||||||
|
get:
|
||||||
|
return get_names()
|
||||||
|
set(value):
|
||||||
|
_names = value
|
||||||
|
emit_tree_changed()
|
||||||
|
@export var current_tab: int:
|
||||||
|
get:
|
||||||
|
return int(clamp(_current_tab, 0, _names.size() - 1))
|
||||||
|
set(value):
|
||||||
|
if value != _current_tab:
|
||||||
|
_current_tab = value
|
||||||
|
emit_tree_changed()
|
||||||
|
|
||||||
|
var _names := PackedStringArray()
|
||||||
|
var _current_tab := 0
|
||||||
|
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
resource_name = "Tabs"
|
||||||
|
|
||||||
|
|
||||||
|
## Returns all tab names in this node
|
||||||
|
func get_names() -> PackedStringArray:
|
||||||
|
return _names
|
||||||
|
|
||||||
|
|
||||||
|
func push_name(name: String) -> void:
|
||||||
|
_names.append(name)
|
||||||
|
emit_tree_changed()
|
||||||
|
|
||||||
|
|
||||||
|
func insert_node(position: int, node: Node) -> void:
|
||||||
|
_names.insert(position, node.name)
|
||||||
|
emit_tree_changed()
|
||||||
|
|
||||||
|
|
||||||
|
func find_name(node_name: String) -> int:
|
||||||
|
for i in _names.size():
|
||||||
|
if _names[i] == node_name:
|
||||||
|
return i
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
|
func find_child(node: Node) -> int:
|
||||||
|
return find_name(node.name)
|
||||||
|
|
||||||
|
|
||||||
|
func remove_node(node: Node) -> void:
|
||||||
|
var i := find_child(node)
|
||||||
|
if i >= 0:
|
||||||
|
_names.remove_at(i)
|
||||||
|
emit_tree_changed()
|
||||||
|
else:
|
||||||
|
push_warning("Remove failed, node '%s' was not found" % node)
|
||||||
|
|
||||||
|
|
||||||
|
func rename_node(previous_name: String, new_name: String) -> void:
|
||||||
|
var i := find_name(previous_name)
|
||||||
|
if i >= 0:
|
||||||
|
_names.set(i, new_name)
|
||||||
|
emit_tree_changed()
|
||||||
|
else:
|
||||||
|
push_warning("Rename failed, name '%s' was not found" % previous_name)
|
||||||
|
|
||||||
|
|
||||||
|
## Returns whether there are any nodes
|
||||||
|
func is_empty() -> bool:
|
||||||
|
return _names.is_empty()
|
||||||
|
|
||||||
|
|
||||||
|
func update_nodes(node_names: PackedStringArray, data: Dictionary) -> void:
|
||||||
|
var i := 0
|
||||||
|
var removed_any := false
|
||||||
|
while i < _names.size():
|
||||||
|
var current := _names[i]
|
||||||
|
if not current in node_names or data.has(current):
|
||||||
|
_names.remove_at(i)
|
||||||
|
removed_any = true
|
||||||
|
else:
|
||||||
|
data[current] = self
|
||||||
|
i += 1
|
||||||
|
if removed_any:
|
||||||
|
emit_tree_changed()
|
||||||
1
layout_panel.gd.uid
Normal file
1
layout_panel.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://d15h5pn8k8tjc
|
||||||
100
layout_split.gd
Normal file
100
layout_split.gd
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
@tool
|
||||||
|
class_name DockableLayoutSplit
|
||||||
|
extends DockableLayoutNode
|
||||||
|
## DockableLayout binary tree nodes, defining subtrees and leaf panels
|
||||||
|
|
||||||
|
enum Direction { HORIZONTAL, VERTICAL }
|
||||||
|
|
||||||
|
@export var direction := Direction.HORIZONTAL:
|
||||||
|
get:
|
||||||
|
return get_direction()
|
||||||
|
set(value):
|
||||||
|
set_direction(value)
|
||||||
|
@export_range(0, 1) var percent := 0.5:
|
||||||
|
get = get_percent,
|
||||||
|
set = set_percent
|
||||||
|
@export var first: DockableLayoutNode = DockableLayoutPanel.new():
|
||||||
|
get:
|
||||||
|
return get_first()
|
||||||
|
set(value):
|
||||||
|
set_first(value)
|
||||||
|
@export var second: DockableLayoutNode = DockableLayoutPanel.new():
|
||||||
|
get:
|
||||||
|
return get_second()
|
||||||
|
set(value):
|
||||||
|
set_second(value)
|
||||||
|
|
||||||
|
var _direction := Direction.HORIZONTAL
|
||||||
|
var _percent := 0.5
|
||||||
|
var _first: DockableLayoutNode
|
||||||
|
var _second: DockableLayoutNode
|
||||||
|
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
resource_name = "Split"
|
||||||
|
|
||||||
|
|
||||||
|
func set_first(value: DockableLayoutNode) -> void:
|
||||||
|
if value == null:
|
||||||
|
_first = DockableLayoutPanel.new()
|
||||||
|
else:
|
||||||
|
_first = value
|
||||||
|
_first.parent = self
|
||||||
|
emit_tree_changed()
|
||||||
|
|
||||||
|
|
||||||
|
func get_first() -> DockableLayoutNode:
|
||||||
|
return _first
|
||||||
|
|
||||||
|
|
||||||
|
func set_second(value: DockableLayoutNode) -> void:
|
||||||
|
if value == null:
|
||||||
|
_second = DockableLayoutPanel.new()
|
||||||
|
else:
|
||||||
|
_second = value
|
||||||
|
_second.parent = self
|
||||||
|
emit_tree_changed()
|
||||||
|
|
||||||
|
|
||||||
|
func get_second() -> DockableLayoutNode:
|
||||||
|
return _second
|
||||||
|
|
||||||
|
|
||||||
|
func set_direction(value: Direction) -> void:
|
||||||
|
if value != _direction:
|
||||||
|
_direction = value
|
||||||
|
emit_tree_changed()
|
||||||
|
|
||||||
|
|
||||||
|
func get_direction() -> Direction:
|
||||||
|
return _direction
|
||||||
|
|
||||||
|
|
||||||
|
func set_percent(value: float) -> void:
|
||||||
|
var clamped_value := clampf(value, 0, 1)
|
||||||
|
if not is_equal_approx(_percent, clamped_value):
|
||||||
|
_percent = clamped_value
|
||||||
|
emit_tree_changed()
|
||||||
|
|
||||||
|
|
||||||
|
func get_percent() -> float:
|
||||||
|
return _percent
|
||||||
|
|
||||||
|
|
||||||
|
func get_names() -> PackedStringArray:
|
||||||
|
var names := _first.get_names()
|
||||||
|
names.append_array(_second.get_names())
|
||||||
|
return names
|
||||||
|
|
||||||
|
|
||||||
|
## Returns whether there are any nodes
|
||||||
|
func is_empty() -> bool:
|
||||||
|
return _first.is_empty() and _second.is_empty()
|
||||||
|
|
||||||
|
|
||||||
|
func is_horizontal() -> bool:
|
||||||
|
return _direction == Direction.HORIZONTAL
|
||||||
|
|
||||||
|
|
||||||
|
func is_vertical() -> bool:
|
||||||
|
return _direction == Direction.VERTICAL
|
||||||
1
layout_split.gd.uid
Normal file
1
layout_split.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://btj8xjkmusjq5
|
||||||
13
plugin.cfg
Normal file
13
plugin.cfg
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[plugin]
|
||||||
|
|
||||||
|
name="Dockable Container"
|
||||||
|
description="Container script that manages docking/tiling UI panels.
|
||||||
|
|
||||||
|
Panels are composed of tabs that can be dragged around and dropped to split another panel or compose its tabs.
|
||||||
|
|
||||||
|
Layout information is stored in Resource objects, so they can be saved/loaded from disk easily.
|
||||||
|
|
||||||
|
This plugin also offers a replica of the Container layout to be edited directly in the inspector."
|
||||||
|
author="gilzoide"
|
||||||
|
version="1.1.2"
|
||||||
|
script="plugin.gd"
|
||||||
19
plugin.gd
Normal file
19
plugin.gd
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
@tool
|
||||||
|
extends EditorPlugin
|
||||||
|
|
||||||
|
const LayoutInspectorPlugin := preload("inspector_plugin/editor_inspector_plugin.gd")
|
||||||
|
const Icon := preload("icon.svg")
|
||||||
|
|
||||||
|
var _layout_inspector_plugin: LayoutInspectorPlugin
|
||||||
|
|
||||||
|
|
||||||
|
func _enter_tree() -> void:
|
||||||
|
_layout_inspector_plugin = LayoutInspectorPlugin.new()
|
||||||
|
add_custom_type("DockableContainer", "Container", DockableContainer, Icon)
|
||||||
|
add_inspector_plugin(_layout_inspector_plugin)
|
||||||
|
|
||||||
|
|
||||||
|
func _exit_tree() -> void:
|
||||||
|
remove_inspector_plugin(_layout_inspector_plugin)
|
||||||
|
remove_custom_type("DockableContainer")
|
||||||
|
_layout_inspector_plugin = null
|
||||||
1
plugin.gd.uid
Normal file
1
plugin.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bsdsrvwv6f10c
|
||||||
63
samples/TestScene.gd
Normal file
63
samples/TestScene.gd
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
extends VBoxContainer
|
||||||
|
|
||||||
|
const SAVED_LAYOUT_PATH := "user://layout.tres"
|
||||||
|
|
||||||
|
@onready var _container := $DockableContainers/DockableContainer as DockableContainer
|
||||||
|
@onready var _clone_control := $HBoxContainer/ControlPrefab as ColorRect
|
||||||
|
@onready var _checkbox_container := $HBoxContainer as HBoxContainer
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
if not OS.is_userfs_persistent():
|
||||||
|
$HBoxContainer/SaveLayoutButton.visible = false
|
||||||
|
$HBoxContainer/LoadLayoutButton.visible = false
|
||||||
|
|
||||||
|
var tabs := _container.get_tabs()
|
||||||
|
for i in tabs.size():
|
||||||
|
var checkbox := CheckBox.new()
|
||||||
|
checkbox.text = str(i)
|
||||||
|
checkbox.button_pressed = not _container.is_control_hidden(tabs[i])
|
||||||
|
checkbox.toggled.connect(_on_CheckButton_toggled.bind(tabs[i]))
|
||||||
|
_checkbox_container.add_child(checkbox)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_add_pressed() -> void:
|
||||||
|
var control := _clone_control.duplicate()
|
||||||
|
control.get_node("Buttons/Rename").pressed.connect(
|
||||||
|
_on_control_rename_button_pressed.bind(control)
|
||||||
|
)
|
||||||
|
control.get_node("Buttons/Remove").pressed.connect(
|
||||||
|
_on_control_remove_button_pressed.bind(control)
|
||||||
|
)
|
||||||
|
control.color = Color(randf(), randf(), randf())
|
||||||
|
control.name = "Control0"
|
||||||
|
|
||||||
|
_container.add_child(control, true)
|
||||||
|
await _container.sort_children
|
||||||
|
_container.set_control_as_current_tab(control)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_save_pressed() -> void:
|
||||||
|
if ResourceSaver.save(_container.layout, SAVED_LAYOUT_PATH) != OK:
|
||||||
|
print("ERROR")
|
||||||
|
|
||||||
|
|
||||||
|
func _on_load_pressed() -> void:
|
||||||
|
var res = load(SAVED_LAYOUT_PATH)
|
||||||
|
if res:
|
||||||
|
_container.set_layout(res.clone())
|
||||||
|
else:
|
||||||
|
print("Error")
|
||||||
|
|
||||||
|
|
||||||
|
func _on_control_rename_button_pressed(control: Control) -> void:
|
||||||
|
control.name = StringName(str(control.name) + " =D")
|
||||||
|
|
||||||
|
|
||||||
|
func _on_control_remove_button_pressed(control: Control) -> void:
|
||||||
|
control.get_parent().remove_child(control)
|
||||||
|
control.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_CheckButton_toggled(button_pressed: bool, tab: Control) -> void:
|
||||||
|
_container.set_control_hidden(tab, not button_pressed)
|
||||||
1
samples/TestScene.gd.uid
Normal file
1
samples/TestScene.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bhop40eih06by
|
||||||
174
samples/TestScene.tscn
Normal file
174
samples/TestScene.tscn
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
[gd_scene format=3 uid="uid://drlvhuchtk6if"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://ccdi3mi0a414a" path="res://addons/dockable_container/dockable_container.gd" id="1"]
|
||||||
|
[ext_resource type="Script" uid="uid://b1bj11c3wrsh3" path="res://addons/dockable_container/layout.gd" id="2"]
|
||||||
|
[ext_resource type="Script" uid="uid://bhop40eih06by" path="res://addons/dockable_container/samples/TestScene.gd" id="4"]
|
||||||
|
[ext_resource type="Script" uid="uid://btj8xjkmusjq5" path="res://addons/dockable_container/layout_split.gd" id="4_yhgfb"]
|
||||||
|
[ext_resource type="Script" uid="uid://d15h5pn8k8tjc" path="res://addons/dockable_container/layout_panel.gd" id="5"]
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Resource_8aoc2"]
|
||||||
|
resource_name = "Tabs"
|
||||||
|
script = ExtResource("5")
|
||||||
|
names = PackedStringArray("Control0")
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Resource_6kjom"]
|
||||||
|
resource_name = "Tabs"
|
||||||
|
script = ExtResource("5")
|
||||||
|
names = PackedStringArray("Control1", "Control2")
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Resource_hl8y1"]
|
||||||
|
resource_name = "Split"
|
||||||
|
script = ExtResource("4_yhgfb")
|
||||||
|
direction = 1
|
||||||
|
first = SubResource("Resource_8aoc2")
|
||||||
|
second = SubResource("Resource_6kjom")
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Resource_ybwqe"]
|
||||||
|
resource_name = "Layout"
|
||||||
|
script = ExtResource("2")
|
||||||
|
root = SubResource("Resource_hl8y1")
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Resource_ntwfj"]
|
||||||
|
resource_name = "Tabs"
|
||||||
|
script = ExtResource("5")
|
||||||
|
names = PackedStringArray("Control3")
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Resource_dmyvf"]
|
||||||
|
resource_name = "Tabs"
|
||||||
|
script = ExtResource("5")
|
||||||
|
names = PackedStringArray("Control4")
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Resource_vag66"]
|
||||||
|
resource_name = "Split"
|
||||||
|
script = ExtResource("4_yhgfb")
|
||||||
|
direction = 1
|
||||||
|
percent = 0.281
|
||||||
|
first = SubResource("Resource_ntwfj")
|
||||||
|
second = SubResource("Resource_dmyvf")
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Resource_4q660"]
|
||||||
|
resource_name = "Tabs"
|
||||||
|
script = ExtResource("5")
|
||||||
|
names = PackedStringArray("Control5")
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Resource_jhibs"]
|
||||||
|
resource_name = "Split"
|
||||||
|
script = ExtResource("4_yhgfb")
|
||||||
|
first = SubResource("Resource_vag66")
|
||||||
|
second = SubResource("Resource_4q660")
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Resource_xhxpg"]
|
||||||
|
resource_name = "Layout"
|
||||||
|
script = ExtResource("2")
|
||||||
|
root = SubResource("Resource_jhibs")
|
||||||
|
|
||||||
|
[node name="SampleScene" type="VBoxContainer" unique_id=63257421]
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
script = ExtResource("4")
|
||||||
|
|
||||||
|
[node name="HBoxContainer" type="HBoxContainer" parent="." unique_id=387084736]
|
||||||
|
layout_mode = 2
|
||||||
|
alignment = 1
|
||||||
|
|
||||||
|
[node name="AddControlButton" type="Button" parent="HBoxContainer" unique_id=2125688693]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 0
|
||||||
|
size_flags_vertical = 4
|
||||||
|
text = "(+) ADD CONTROL"
|
||||||
|
|
||||||
|
[node name="SaveLayoutButton" type="Button" parent="HBoxContainer" unique_id=1305135861]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 0
|
||||||
|
size_flags_vertical = 4
|
||||||
|
text = "Save Layout"
|
||||||
|
|
||||||
|
[node name="LoadLayoutButton" type="Button" parent="HBoxContainer" unique_id=1243058994]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 0
|
||||||
|
size_flags_vertical = 4
|
||||||
|
text = "Load Layout"
|
||||||
|
|
||||||
|
[node name="ControlPrefab" type="ColorRect" parent="HBoxContainer" unique_id=399318649]
|
||||||
|
visible = false
|
||||||
|
custom_minimum_size = Vector2(100, 100)
|
||||||
|
layout_mode = 2
|
||||||
|
color = Color(0.129412, 0.121569, 0.121569, 1)
|
||||||
|
|
||||||
|
[node name="Buttons" type="VBoxContainer" parent="HBoxContainer/ControlPrefab" unique_id=1055220509]
|
||||||
|
layout_mode = 0
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_left = -65.5
|
||||||
|
offset_top = -22.0
|
||||||
|
offset_right = 65.5
|
||||||
|
offset_bottom = 22.0
|
||||||
|
|
||||||
|
[node name="Rename" type="Button" parent="HBoxContainer/ControlPrefab/Buttons" unique_id=1635652992]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Rename"
|
||||||
|
|
||||||
|
[node name="Remove" type="Button" parent="HBoxContainer/ControlPrefab/Buttons" unique_id=325010205]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "REMOVE"
|
||||||
|
|
||||||
|
[node name="DockableContainers" type="HBoxContainer" parent="." unique_id=1263000341]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="DockableContainer" type="Container" parent="DockableContainers" unique_id=1541533577]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
script = ExtResource("1")
|
||||||
|
layout = SubResource("Resource_ybwqe")
|
||||||
|
|
||||||
|
[node name="Control0" type="ColorRect" parent="DockableContainers/DockableContainer" unique_id=1755020147]
|
||||||
|
custom_minimum_size = Vector2(100, 100)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Control1" type="ColorRect" parent="DockableContainers/DockableContainer" unique_id=1411986801]
|
||||||
|
custom_minimum_size = Vector2(100, 100)
|
||||||
|
layout_mode = 2
|
||||||
|
color = Color(0.141176, 0.0745098, 0.603922, 1)
|
||||||
|
|
||||||
|
[node name="Control2" type="ColorRect" parent="DockableContainers/DockableContainer" unique_id=199396917]
|
||||||
|
visible = false
|
||||||
|
custom_minimum_size = Vector2(100, 100)
|
||||||
|
layout_mode = 2
|
||||||
|
color = Color(0.533333, 0.380392, 0.380392, 1)
|
||||||
|
|
||||||
|
[node name="Separator" type="ColorRect" parent="DockableContainers" unique_id=1232713094]
|
||||||
|
custom_minimum_size = Vector2(50, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
color = Color(0, 0, 0, 1)
|
||||||
|
|
||||||
|
[node name="DockableContainer2" type="Container" parent="DockableContainers" unique_id=1148470831]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
script = ExtResource("1")
|
||||||
|
layout = SubResource("Resource_xhxpg")
|
||||||
|
|
||||||
|
[node name="Control3" type="ColorRect" parent="DockableContainers/DockableContainer2" unique_id=209835842]
|
||||||
|
custom_minimum_size = Vector2(100, 100)
|
||||||
|
layout_mode = 2
|
||||||
|
color = Color(0, 1, 0.905882, 1)
|
||||||
|
|
||||||
|
[node name="Control4" type="ColorRect" parent="DockableContainers/DockableContainer2" unique_id=1376035184]
|
||||||
|
custom_minimum_size = Vector2(100, 100)
|
||||||
|
layout_mode = 2
|
||||||
|
color = Color(0, 0.698039, 0.0588235, 1)
|
||||||
|
|
||||||
|
[node name="Control5" type="ColorRect" parent="DockableContainers/DockableContainer2" unique_id=32573735]
|
||||||
|
custom_minimum_size = Vector2(100, 100)
|
||||||
|
layout_mode = 2
|
||||||
|
color = Color(1, 0.937255, 0, 1)
|
||||||
|
|
||||||
|
[connection signal="pressed" from="HBoxContainer/AddControlButton" to="." method="_on_add_pressed"]
|
||||||
|
[connection signal="pressed" from="HBoxContainer/SaveLayoutButton" to="." method="_on_save_pressed"]
|
||||||
|
[connection signal="pressed" from="HBoxContainer/LoadLayoutButton" to="." method="_on_load_pressed"]
|
||||||
120
split_handle.gd
Normal file
120
split_handle.gd
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
@tool
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
const SPLIT_THEME_CLASS: PackedStringArray = [
|
||||||
|
"HSplitContainer", # SPLIT_THEME_CLASS[DockableLayoutSplit.Direction.HORIZONTAL]
|
||||||
|
"VSplitContainer", # SPLIT_THEME_CLASS[DockableLayoutSplit.Direction.VERTICAL]
|
||||||
|
]
|
||||||
|
|
||||||
|
const SPLIT_MOUSE_CURSOR_SHAPE: Array[Control.CursorShape] = [
|
||||||
|
Control.CURSOR_HSPLIT, # SPLIT_MOUSE_CURSOR_SHAPE[DockableLayoutSplit.Direction.HORIZONTAL]
|
||||||
|
Control.CURSOR_VSPLIT, # SPLIT_MOUSE_CURSOR_SHAPE[DockableLayoutSplit.Direction.VERTICAL]
|
||||||
|
]
|
||||||
|
|
||||||
|
var layout_split: DockableLayoutSplit
|
||||||
|
var first_minimum_size: Vector2
|
||||||
|
var second_minimum_size: Vector2
|
||||||
|
|
||||||
|
var _parent_rect: Rect2
|
||||||
|
var _mouse_hovering := false
|
||||||
|
var _dragging := false
|
||||||
|
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
var theme_class := SPLIT_THEME_CLASS[layout_split.direction]
|
||||||
|
var icon := get_theme_icon("grabber", theme_class)
|
||||||
|
var autohide := bool(get_theme_constant("autohide", theme_class))
|
||||||
|
if not icon or (autohide and not _mouse_hovering):
|
||||||
|
return
|
||||||
|
|
||||||
|
draw_texture(icon, (size - icon.get_size()) * 0.5)
|
||||||
|
|
||||||
|
|
||||||
|
func _gui_input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
|
||||||
|
_dragging = event.is_pressed()
|
||||||
|
if event.double_click:
|
||||||
|
layout_split.percent = 0.5
|
||||||
|
elif _dragging and event is InputEventMouseMotion:
|
||||||
|
var mouse_in_parent := get_parent_control().get_local_mouse_position()
|
||||||
|
if layout_split.is_horizontal():
|
||||||
|
layout_split.percent = (
|
||||||
|
(mouse_in_parent.x - _parent_rect.position.x) / _parent_rect.size.x
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
layout_split.percent = (
|
||||||
|
(mouse_in_parent.y - _parent_rect.position.y) / _parent_rect.size.y
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _notification(what: int) -> void:
|
||||||
|
if what == NOTIFICATION_MOUSE_ENTER:
|
||||||
|
_mouse_hovering = true
|
||||||
|
set_split_cursor(true)
|
||||||
|
if bool(get_theme_constant("autohide", SPLIT_THEME_CLASS[layout_split.direction])):
|
||||||
|
queue_redraw()
|
||||||
|
elif what == NOTIFICATION_MOUSE_EXIT:
|
||||||
|
_mouse_hovering = false
|
||||||
|
set_split_cursor(false)
|
||||||
|
if bool(get_theme_constant("autohide", SPLIT_THEME_CLASS[layout_split.direction])):
|
||||||
|
queue_redraw()
|
||||||
|
elif what == NOTIFICATION_FOCUS_EXIT:
|
||||||
|
_dragging = false
|
||||||
|
|
||||||
|
|
||||||
|
func get_layout_minimum_size() -> Vector2:
|
||||||
|
if not layout_split:
|
||||||
|
return Vector2.ZERO
|
||||||
|
var separation := get_theme_constant("separation", SPLIT_THEME_CLASS[layout_split.direction])
|
||||||
|
if layout_split.is_horizontal():
|
||||||
|
return Vector2(
|
||||||
|
first_minimum_size.x + separation + second_minimum_size.x,
|
||||||
|
maxf(first_minimum_size.y, second_minimum_size.y)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return Vector2(
|
||||||
|
maxf(first_minimum_size.x, second_minimum_size.x),
|
||||||
|
first_minimum_size.y + separation + second_minimum_size.y
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func set_split_cursor(value: bool) -> void:
|
||||||
|
if value:
|
||||||
|
mouse_default_cursor_shape = SPLIT_MOUSE_CURSOR_SHAPE[layout_split.direction]
|
||||||
|
else:
|
||||||
|
mouse_default_cursor_shape = CURSOR_ARROW
|
||||||
|
|
||||||
|
|
||||||
|
func get_split_rects(rect: Rect2) -> Dictionary:
|
||||||
|
_parent_rect = rect
|
||||||
|
var separation := get_theme_constant("separation", SPLIT_THEME_CLASS[layout_split.direction])
|
||||||
|
var origin := rect.position
|
||||||
|
var percent := layout_split.percent
|
||||||
|
if layout_split.is_horizontal():
|
||||||
|
var split_offset := clampf(
|
||||||
|
rect.size.x * percent - separation * 0.5,
|
||||||
|
first_minimum_size.x,
|
||||||
|
rect.size.x - second_minimum_size.x - separation
|
||||||
|
)
|
||||||
|
var second_width := rect.size.x - split_offset - separation
|
||||||
|
|
||||||
|
return {
|
||||||
|
"first": Rect2(origin.x, origin.y, split_offset, rect.size.y),
|
||||||
|
"self": Rect2(origin.x + split_offset, origin.y, separation, rect.size.y),
|
||||||
|
"second":
|
||||||
|
Rect2(origin.x + split_offset + separation, origin.y, second_width, rect.size.y),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
var split_offset := clampf(
|
||||||
|
rect.size.y * percent - separation * 0.5,
|
||||||
|
first_minimum_size.y,
|
||||||
|
rect.size.y - second_minimum_size.y - separation
|
||||||
|
)
|
||||||
|
var second_height := rect.size.y - split_offset - separation
|
||||||
|
|
||||||
|
return {
|
||||||
|
"first": Rect2(origin.x, origin.y, rect.size.x, split_offset),
|
||||||
|
"self": Rect2(origin.x, origin.y + split_offset, rect.size.x, separation),
|
||||||
|
"second":
|
||||||
|
Rect2(origin.x, origin.y + split_offset + separation, rect.size.x, second_height),
|
||||||
|
}
|
||||||
1
split_handle.gd.uid
Normal file
1
split_handle.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dst2j6djj1vdv
|
||||||
Reference in New Issue
Block a user