-
-
Notifications
You must be signed in to change notification settings - Fork 21
/
Copy pathdockable_container.gd
517 lines (435 loc) · 16.6 KB
/
dockable_container.gd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
@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 _windows_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)
_windows_container.name = "_windows_container"
get_parent().call_deferred("add_child", _windows_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 _add_floating_options(tab_container: DockablePanel) -> void:
var options := PopupMenu.new()
options.add_item("Make Floating")
options.id_pressed.connect(_toggle_floating.bind(tab_container))
options.size.y = 0
_windows_container.add_child(options)
tab_container.set_popup(options)
## Required when converting a window back to panel.
func _refresh_tabs_visible() -> void:
if tabs_visible:
tabs_visible = false
await get_tree().process_frame
await get_tree().process_frame
tabs_visible = true
func _toggle_floating(_id: int, tab_container: DockablePanel) -> void:
var node_name := tab_container.get_tab_title(tab_container.current_tab)
var node := get_node(node_name)
if is_instance_valid(node):
var tab_position := maxi(tab_container.leaf.find_child(node), 0)
_convert_to_window(node, {"tab_position": tab_position, "tab_container": tab_container})
else:
print("Node ", node_name, " not found!")
## Converts a panel to floating window.
func _convert_to_window(content: Control, previous_data := {}) -> void:
var old_owner := content.owner
var data := {}
if content.name in layout.windows:
data = layout.windows[content.name]
var window := FloatingWindow.new(content, data)
_windows_container.add_child(window)
window.show()
_refresh_tabs_visible()
window.close_requested.connect(_convert_to_panel.bind(window, old_owner, previous_data))
window.data_changed.connect(layout.save_window_properties)
## Converts a floating window into a panel.
func _convert_to_panel(window: FloatingWindow, old_owner: Node, previous_data := {}) -> void:
var content := window.window_content
window.remove_child(content)
window.destroy()
add_child(content)
content.owner = old_owner
if previous_data.has("tab_container") and is_instance_valid(previous_data["tab_container"]):
var tab_position := previous_data.get("tab_position", 0) as int
previous_data["tab_container"].leaf.insert_node(tab_position, content)
_refresh_tabs_visible()
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)
for window in _windows_container.get_children():
if not window.name in _layout.windows and window is FloatingWindow:
window.prevent_data_erasure = true # We don't want to delete data.
window.close_requested.emit() # Removes the window.
continue
for window: String in _layout.windows.keys():
var panel := find_child(window, false)
# Only those windows get created which were not previously created.
if panel:
_convert_to_window(panel)
_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) as DockablePanel
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 data is Dictionary and data.get("type") in ["tab_container_tab", "tabc_element"]:
var tabc := get_node_or_null(data.get("from_path"))
return (
tabc
and tabc.has_method("get_tabs_rearrange_group")
and tabc.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))
_add_floating_options(panel)
_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)