neowarsaw/addons/script-ide/tabbar/multiline_tab_container.gd
2026-01-04 08:43:50 +01:00

434 lines
12 KiB
GDScript

@tool
extends PanelContainer
const CLOSE_BTN_SPACER: String = " "
const CustomTab := preload("custom_tab.gd")
@onready var multiline_tab_bar: HFlowContainer = %MultilineTabBar
@onready var popup_btn: Button = %PopupBtn
var tab_hovered: StyleBoxFlat
var tab_focus: StyleBoxFlat
var tab_selected: StyleBoxFlat
var tab_unselected: StyleBoxFlat
var font_selected_color: Color
var font_unselected_color: Color
var font_hovered_color: Color
var show_close_button_always: bool = false : set = set_show_close_button_always
var is_singleline_tabs: bool = false : set = set_singleline_tabs
var tab_group: ButtonGroup = ButtonGroup.new()
var script_filter_txt: LineEdit
var scripts_item_list: ItemList
var scripts_tab_container: TabContainer
var popup: PopupPanel
var plugin: EditorPlugin
var suppress_theme_changed: bool
var last_drag_over_tab: CustomTab
var drag_marker: ColorRect
var current_tab: CustomTab
func _init() -> void:
tab_group.pressed.connect(on_new_tab_selected)
#region Plugin and related tab handling processing
func _ready() -> void:
popup_btn.pressed.connect(show_popup)
set_process(false)
if (plugin != null):
schedule_update()
func _notification(what: int) -> void:
if (what == NOTIFICATION_DRAG_END || what == NOTIFICATION_MOUSE_EXIT):
clear_drag_mark()
return
if (what == NOTIFICATION_THEME_CHANGED):
if (suppress_theme_changed):
return
suppress_theme_changed = true
add_theme_stylebox_override(&"panel", EditorInterface.get_editor_theme().get_stylebox(&"tabbar_background", &"TabContainer"))
suppress_theme_changed = false
tab_hovered = EditorInterface.get_editor_theme().get_stylebox(&"tab_hovered", &"TabContainer")
tab_focus = EditorInterface.get_editor_theme().get_stylebox(&"tab_focus", &"TabContainer")
tab_selected = EditorInterface.get_editor_theme().get_stylebox(&"tab_selected", &"TabContainer")
tab_unselected = EditorInterface.get_editor_theme().get_stylebox(&"tab_unselected", &"TabContainer")
if (drag_marker == null):
drag_marker = ColorRect.new()
drag_marker.set_anchors_and_offsets_preset(PRESET_LEFT_WIDE)
drag_marker.mouse_filter = Control.MOUSE_FILTER_IGNORE
drag_marker.custom_minimum_size.x = 4 * EditorInterface.get_editor_scale()
drag_marker.color = EditorInterface.get_editor_theme().get_color(&"drop_mark_color", &"TabContainer")
font_hovered_color = EditorInterface.get_editor_theme().get_color(&"font_hovered_color", &"TabContainer")
font_selected_color = EditorInterface.get_editor_theme().get_color(&"font_selected_color", &"TabContainer")
font_unselected_color = EditorInterface.get_editor_theme().get_color(&"font_unselected_color", &"TabContainer")
if (plugin == null || multiline_tab_bar == null):
return
for tab: CustomTab in get_tabs():
update_tab_style(tab)
func update_tab_style(tab: CustomTab):
tab.add_theme_stylebox_override(&"normal", tab_unselected)
tab.add_theme_stylebox_override(&"hover", tab_hovered)
tab.add_theme_stylebox_override(&"hover_pressed", tab_hovered)
tab.add_theme_stylebox_override(&"focus", tab_focus)
tab.add_theme_stylebox_override(&"pressed", tab_selected)
tab.add_theme_color_override(&"font_color", font_unselected_color)
tab.add_theme_color_override(&"font_hover_color", font_hovered_color)
tab.add_theme_color_override(&"font_pressed_color", font_selected_color)
func update_icon_color(tab: CustomTab, color: Color):
tab.add_theme_color_override(&"icon_normal_color", color)
tab.add_theme_color_override(&"icon_hover_color", color)
tab.add_theme_color_override(&"icon_hover_pressed_color", color)
tab.add_theme_color_override(&"icon_pressed_color", color)
tab.add_theme_color_override(&"icon_focus_color", color)
func _process(delta: float) -> void:
sync_tabs_with_item_list()
if (is_singleline_tabs):
shift_singleline_tabs_to(current_tab)
set_process(false)
func _shortcut_input(event: InputEvent) -> void:
if (!event.is_pressed() || event.is_echo()):
return
if (!is_visible_in_tree()):
return
if (current_tab == null):
return
if (plugin.tab_cycle_forward_shc.matches_event(event)):
get_viewport().set_input_as_handled()
var tab_count: int = get_tab_count()
if (tab_count <= 1):
return
var index: int = current_tab.get_index()
var new_tab: int = index + 1
if (new_tab == tab_count):
new_tab = 0
var tab: CustomTab = get_tab(new_tab)
tab.button_pressed = true
elif (plugin.tab_cycle_backward_shc.matches_event(event)):
get_viewport().set_input_as_handled()
var tab_count: int = get_tab_count()
if (tab_count <= 1):
return
var index: int = current_tab.get_index()
var new_tab: int = index - 1
if (new_tab == -1):
new_tab = tab_count - 1
var tab: CustomTab = get_tab(new_tab)
tab.button_pressed = true
func _can_drop_data(at_position: Vector2, data: Variant) -> bool:
if !(data is Dictionary):
return false
var can_drop: bool = data.has("index") && data["index"] != get_tab_count() - 1
if (can_drop):
on_drag_over(get_tab(get_tab_count() - 1))
return can_drop
func _drop_data(at_position: Vector2, data: Variant) -> void:
if (!_can_drop_data(at_position, data)):
return
on_drag_drop(data["index"], get_tab_count() - 1)
#endregion
func schedule_update():
set_process(true)
func on_drag_drop(source_index: int, target_index: int):
var child: Node = scripts_tab_container.get_child(source_index)
scripts_tab_container.move_child(child, target_index);
var tab: CustomTab = get_tab(target_index)
tab.grab_focus()
func on_drag_over(tab: CustomTab):
if (last_drag_over_tab == tab):
return
# The drag marker should always be orphan when here.
tab.add_child(drag_marker)
last_drag_over_tab = tab
func clear_drag_mark():
if (last_drag_over_tab == null):
return
last_drag_over_tab = null
if (drag_marker.get_parent() != null):
drag_marker.get_parent().remove_child(drag_marker)
func update_tabs():
update_script_text_filter()
for tab: CustomTab in get_tabs():
update_tab(tab)
func get_tabs() -> Array[Node]:
return multiline_tab_bar.get_children()
func update_selected_tab():
update_tab(tab_group.get_pressed_button())
func update_tab(tab: CustomTab):
if (tab == null):
return
var index: int = tab.get_index()
tab.text = scripts_item_list.get_item_text(index)
tab.icon = scripts_item_list.get_item_icon(index)
tab.tooltip_text = scripts_item_list.get_item_tooltip(index)
update_icon_color(tab, scripts_item_list.get_item_icon_modulate(index))
if (scripts_item_list.is_selected(index)):
tab.button_pressed = true
tab.text += CLOSE_BTN_SPACER
elif (show_close_button_always):
tab.text += CLOSE_BTN_SPACER
func get_tab(index: int) -> CustomTab:
if (index < 0 || index >= get_tab_count()):
return null
return multiline_tab_bar.get_child(index)
func get_tab_count() -> int:
return multiline_tab_bar.get_child_count()
func add_tab() -> CustomTab:
var tab: CustomTab = CustomTab.new()
tab.button_group = tab_group
if (show_close_button_always):
tab.show_close_button()
update_tab_style(tab)
tab.close_pressed.connect(on_tab_close_pressed.bind(tab))
tab.right_clicked.connect(on_tab_right_click.bind(tab))
tab.mouse_exited.connect(clear_drag_mark)
tab.dragged_over.connect(on_drag_over.bind(tab))
tab.dropped.connect(on_drag_drop)
multiline_tab_bar.add_child(tab)
return tab
func on_tab_right_click(tab: CustomTab):
var index: int = tab.get_index()
scripts_item_list.item_clicked.emit(index, scripts_item_list.get_local_mouse_position(), MOUSE_BUTTON_RIGHT)
func on_new_tab_selected(tab: CustomTab):
# Hide and show close button.
if (!show_close_button_always):
if (current_tab != null):
current_tab.hide_close_button()
if (tab != null):
tab.show_close_button()
update_script_text_filter()
var index: int = tab.get_index()
if (scripts_item_list != null && !scripts_item_list.is_selected(index)):
scripts_item_list.select(index)
scripts_item_list.item_selected.emit(index)
scripts_item_list.ensure_current_is_visible()
# Remove spacing from previous tab.
if (!show_close_button_always && current_tab != null):
update_tab(current_tab)
current_tab = tab
## Removes the script filter text and emits the signal so that the tabs stay
## and we do not break anything there.
func update_script_text_filter():
if (script_filter_txt.text != &""):
script_filter_txt.text = &""
script_filter_txt.text_changed.emit(&"")
func on_tab_close_pressed(tab: CustomTab) -> void:
scripts_item_list.item_clicked.emit(tab.get_index(), scripts_item_list.get_local_mouse_position(), MOUSE_BUTTON_MIDDLE)
func sync_tabs_with_item_list() -> void:
if (plugin == null):
return
if (get_tab_count() > scripts_item_list.item_count):
for index: int in range(get_tab_count() - 1, scripts_item_list.item_count - 1, -1):
var tab: CustomTab = get_tab(index)
if (tab == current_tab):
current_tab = null
multiline_tab_bar.remove_child(tab)
tab.free()
for index: int in scripts_item_list.item_count:
var tab: CustomTab = get_tab(index)
if (tab == null):
tab = add_tab()
update_tab(tab)
func tab_changed():
update_script_text_filter()
# When the tab change was not triggered by our component,
# we need to sync the selection.
update_tab(get_tab(scripts_tab_container.current_tab))
func script_order_changed() -> void:
schedule_update()
func set_popup(new_popup: PopupPanel) -> void:
popup = new_popup
func show_popup() -> void:
if (popup == null):
return
scripts_item_list.get_parent().reparent(popup)
scripts_item_list.get_parent().visible = true
popup.size = Vector2(250 * get_editor_scale(), get_parent().size.y - size.y)
popup.position = popup_btn.get_screen_position() - Vector2(popup.size.x, 0)
popup.popup()
script_filter_txt.grab_focus()
func get_editor_scale() -> float:
return EditorInterface.get_editor_scale()
func set_show_close_button_always(new_value: bool):
if (show_close_button_always == new_value):
return
show_close_button_always = new_value
if (multiline_tab_bar == null):
return
for tab: CustomTab in get_tabs():
tab.text = scripts_item_list.get_item_text(tab.get_index())
if (show_close_button_always):
tab.text += CLOSE_BTN_SPACER
if (!tab.button_pressed):
tab.show_close_button()
else:
if (!tab.button_pressed):
tab.hide_close_button()
else:
tab.text += CLOSE_BTN_SPACER
#region Singeline handling
func set_singleline_tabs(new_value: bool):
if (is_singleline_tabs == new_value):
return
is_singleline_tabs = new_value
if (is_singleline_tabs):
item_rect_changed.connect(update_singleline_tabs_width)
tab_group.pressed.connect(ensure_singleline_tab_visible.unbind(1))
if (multiline_tab_bar == null):
return
shift_singleline_tabs_to(current_tab)
else:
item_rect_changed.disconnect(update_singleline_tabs_width)
tab_group.pressed.disconnect(ensure_singleline_tab_visible)
if (multiline_tab_bar == null):
return
for tab: CustomTab in get_tabs():
tab.visible = true
func ensure_singleline_tab_visible():
if (current_tab != null && current_tab.visible):
return
shift_singleline_tabs_to(current_tab)
func update_singleline_tabs_width():
if (current_tab != null && !current_tab.visible):
shift_singleline_tabs_to(current_tab)
return
for tab: CustomTab in get_tabs():
if (tab.visible):
shift_singleline_tabs_to(tab)
break
func shift_singleline_tabs_to(start_tab: CustomTab):
var start: bool
var tab_bar_width: float = multiline_tab_bar.size.x
var tabs_width: float
var one_fit: bool = true
for tab: CustomTab in get_tabs():
if (start_tab == null || tab == start_tab):
start = true
if (start):
tabs_width += tab.size.x
tab.visible = tabs_width <= tab_bar_width
one_fit = one_fit || tab.visible
else:
tab.visible = false
if (current_tab != null && !current_tab.visible):
if (start_tab != current_tab):
shift_singleline_tabs_to(current_tab)
return
if (start_tab == null):
return
for index: int in range(start_tab.get_index() - 1, -1, -1):
var tab: CustomTab = get_tabs().get(index)
tabs_width += tab.size.x
if (tabs_width > tab_bar_width):
return
tab.visible = true
#endregion