@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() @export var save_on_change := false: set(value): save_on_change = value if value: if not changed.is_connected(save): changed.connect(save) else: if changed.is_connected(save): changed.disconnect(save) 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: if resource_name.is_empty(): 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() func save(path := resource_path) -> void: if path.is_empty(): return ResourceSaver.save(self, path) ## 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)