@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