godot-xterm/addons/godot_xterm/editor_plugins/terminal/terminal_panel.gd
Leroy Hopson d6a4adf6aa
feat(terminal): and stylebox support
Adds support for 'normal' and 'focus' Style Boxes to Terminal node.
Changes default background color to transparent, with background to be
set by StyleBox.

If background color is not transparent, will draw a background color
rect to cover the entire control over the top of any stylebox. This is
consistent with the behavior of the TextEdit node with regards to theme
colors and styleboxes.
2024-04-28 18:09:21 +12:00

286 lines
9.2 KiB
GDScript

# Copyright (c) 2021, Leroy Hopson (MIT License).
#
# This file contains snippets of code derived from Godot's editor_node.cpp file.
# These snippets are copyright of their authors and released under the MIT license:
# - Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur (MIT License).
# - Copyright (c) 2014-2021 Godot Engine contributors (MIT License).
@tool
extends Control
const EditorTerminal := preload("./editor_terminal.tscn")
const TerminalSettings := preload("./settings/terminal_settings.gd")
const SETTINGS_FILE_PATH := "res://.gdxterm/settings.tres"
enum TerminalPopupMenuOptions {
NEW_TERMINAL = 0,
COPY = 2,
PASTE = 3,
COPY_ALL = 4,
CLEAR = 6,
KILL_TERMINAL = 7,
}
# Has access to the EditorSettings singleton so it can dynamically generate the
# terminal color scheme based on editor theme settings.
var editor_plugin: EditorPlugin
var editor_interface: EditorInterface
@onready var editor_settings: EditorSettings = editor_interface.get_editor_settings()
@onready var tabs: TabBar = $VBoxContainer/TabbarContainer/Tabs
@onready var tabbar_container: HBoxContainer = $VBoxContainer/TabbarContainer
@onready var add_button: Button = $VBoxContainer/TabbarContainer/AddButton
@onready var tab_container: TabContainer = $VBoxContainer/TabContainer
@onready var terminal_popup_menu: PopupMenu = $VBoxContainer/TerminalPopupMenu
# Size label.
# Used to show the size of the terminal (rows/cols) and panel (pixels) when resized.
@onready var size_label: Label = $SizeLabel
@onready var size_label_timer: Timer = $SizeLabel/SizeLabelTimer
@onready var is_ready := true
var _theme := Theme.new()
var _settings: TerminalSettings
var _tab_container_min_size
func _ready():
_update_settings()
func _load_or_create_settings() -> void:
# Use only default settings for now, until settings are properly defined
# and documented.
_settings = TerminalSettings.new()
func _update_settings() -> void:
_load_or_create_settings()
var editor_scale: float = 1.0
if editor_interface.has_method("get_editor_scale"):
editor_scale = editor_interface.get_editor_scale()
custom_minimum_size = Vector2(0, tabbar_container.size.y + 182) * editor_scale
size.y = 415
tabs.tab_close_display_policy = TabBar.CLOSE_BUTTON_SHOW_ALWAYS
#FIXME
# # Update shortcuts.
# if _settings.new_terminal_shortcut:
# terminal_popup_menu.set_item_shortcut(
# TerminalPopupMenuOptions.NEW_TERMINAL, _settings.new_terminal_shortcut, true
# )
# if _settings.kill_terminal_shortcut:
# terminal_popup_menu.set_item_shortcut(
# TerminalPopupMenuOptions.KILL_TERMINAL, _settings.kill_terminal_shortcut, false
# )
# if _settings.copy_shortcut:
# terminal_popup_menu.set_item_shortcut(
# TerminalPopupMenuOptions.COPY, _settings.copy_shortcut, false
# )
# if _settings.paste_shortcut:
# terminal_popup_menu.set_item_shortcut(
# TerminalPopupMenuOptions.PASTE, _settings.paste_shortcut, false
# )
_update_terminal_tabs()
func _update_terminal_tabs():
# Wait a couple of frames to allow everything to resize before updating.
await get_tree().process_frame
await get_tree().process_frame
if tabs.get_offset_buttons_visible():
# Move add button to fixed position on the tabbar.
if add_button.get_parent() == tabs:
add_button.position = Vector2.ZERO
tabs.remove_child(add_button)
tabbar_container.add_child(add_button)
tabbar_container.move_child(add_button, 0)
else:
# Move add button after last tab.
if tabs.tab_count > 0 and add_button.get_parent() == tabbar_container:
tabbar_container.remove_child(add_button)
tabs.add_child(add_button)
var last_tab := Rect2()
if tabs.get_tab_count() > 0:
last_tab = tabs.get_tab_rect(tabs.get_tab_count() - 1)
add_button.position = Vector2(
last_tab.position.x + last_tab.size.x + 3, last_tab.position.y
)
if tabs.tab_count == 0 and add_button.get_parent() == tabs:
tabs.remove_child(add_button)
tabbar_container.add_child(add_button)
# Make sure we still own the button, so it gets saved with our scene.
add_button.owner = self
func _on_AddButton_pressed():
var shell = OS.get_environment("SHELL") if OS.has_environment("SHELL") else "sh"
var terminal := EditorTerminal.instantiate()
tabs.add_tab(shell.get_file())
terminal.editor_settings = editor_settings
terminal.set_anchors_preset(PRESET_BOTTOM_WIDE)
terminal.connect("gui_input", Callable(self, "_on_TabContainer_gui_input"))
terminal.connect("exited", Callable(self, "_on_Terminal_exited").bind(terminal))
tab_container.add_child(terminal)
terminal.pty.fork(shell)
terminal.grab_focus()
tabs.current_tab = tabs.get_tab_count() - 1
tab_container.current_tab = tabs.current_tab
_update_terminal_tabs()
func _on_Tabs_tab_changed(tab_index):
tab_container.call_deferred("set_current_tab", tab_index)
await get_tree().process_frame
tab_container.get_current_tab_control().grab_focus()
func _on_Tabs_tab_close(tab_index):
tabs.remove_tab(tab_index)
tab_container.get_child(tab_index).queue_free()
# Switch focus to the next active tab.
if tabs.get_tab_count() > 0:
tab_container.get_child(tabs.current_tab).grab_focus()
_update_terminal_tabs()
func _notification(what):
if not is_ready:
return
match what:
EditorSettings.NOTIFICATION_EDITOR_SETTINGS_CHANGED:
_update_settings()
_update_terminal_tabs()
NOTIFICATION_RESIZED:
_update_terminal_tabs()
NOTIFICATION_APPLICATION_FOCUS_IN:
_update_terminal_tabs()
func _input(event: InputEvent) -> void:
if not _settings or not event.is_pressed():
return
# Global shortcut to open new terminal and make terminal panel visible.
# FIXME
# if _settings.new_terminal_shortcut and _settings.new_terminal_shortcut.shortcut:
# if event.is_match(_settings.new_terminal_shortcut.shortcut):
# get_tree().set_input_as_handled()
# editor_plugin.make_bottom_panel_item_visible(self)
# _on_AddButton_pressed()
# Non-global shortcuts, only applied if terminal is active and focused.
if (
tabs.get_tab_count() > 0 and tab_container.get_child(tabs.current_tab).has_focus()
or terminal_popup_menu.has_focus()
):
pass
# Kill terminal.
# FIXME shortcut
# if _settings.kill_terminal_shortcut and _settings.kill_terminal_shortcut.shortcut:
# if event.is_match(_settings.kill_terminal_shortcut.shortcut):
# get_tree().set_input_as_handled()
# _on_TerminalPopupMenu_id_pressed(TerminalPopupMenuOptions.KILL_TERMINAL)
# Copy.
#FIXME
# if _settings.copy_shortcut and _settings.copy_shortcut.shortcut:
# if event.is_match(_settings.copy_shortcut.shortcut):
# get_tree().set_input_as_handled()
# _on_TerminalPopupMenu_id_pressed(TerminalPopupMenuOptions.COPY)
# Paste.
#FIXME
# if _settings.paste_shortcut and _settings.paste_shortcut.shortcut:
# if event.is_match(_settings.paste_shortcut.shortcut):
# get_tree().set_input_as_handled()
# _on_TerminalPopupMenu_id_pressed(TerminalPopupMenuOptions.PASTE)
#
# # Next tab.
# if _settings.next_tab_shortcut and _settings.next_tab_shortcut.shortcut:
# if event.is_match(_settings.next_tab_shortcut.shortcut):
# get_tree().set_input_as_handled()
# tabs.current_tab = min(tabs.current_tab + 1, tabs.get_tab_count() - 1)
#
# # Previous tab.
# if _settings.previous_tab_shortcut and _settings.previous_tab_shortcut.shortcut:
# if event.is_match(_settings.previous_tab_shortcut.shortcut):
# get_tree().set_input_as_handled()
# tabs.current_tab = max(tabs.current_tab - 1, 0)
func _on_TabContainer_gui_input(event):
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT:
terminal_popup_menu.position = event.global_position
terminal_popup_menu.popup()
func _on_TerminalPopupMenu_id_pressed(id):
match id:
TerminalPopupMenuOptions.NEW_TERMINAL:
_on_AddButton_pressed()
if tabs.get_tab_count() > 0:
var terminal = tab_container.get_child(tab_container.current_tab)
match id:
TerminalPopupMenuOptions.COPY:
DisplayServer.clipboard_set(terminal.copy_selection())
TerminalPopupMenuOptions.PASTE:
for i in DisplayServer.clipboard_get().length():
var event = InputEventKey.new()
event.unicode = DisplayServer.clipboard_get().unicode_at(i)
event.button_pressed = true
terminal._gui_input(event)
TerminalPopupMenuOptions.COPY_ALL:
DisplayServer.clipboard_set(terminal.copy_all())
TerminalPopupMenuOptions.CLEAR:
terminal.clear()
TerminalPopupMenuOptions.KILL_TERMINAL:
_on_Tabs_tab_close(tabs.current_tab)
func _on_Tabs_reposition_active_tab_request(idx_to):
var active = tab_container.get_child(tab_container.current_tab)
tab_container.move_child(active, idx_to)
func _on_Panel_resized():
if not size_label:
return
var size = tab_container.size
if tabs.get_tab_count() > 0:
var terminal = tab_container.get_child(tabs.current_tab)
var cols = terminal.get_cols()
var rows = terminal.get_rows()
size_label.text = "Size: %d cols; %d rows\n(%d x %d px)" % [cols, rows, size.x, size.y]
else:
size_label.text = "Size:\n(%d x %d px)" % [size.x, size.y]
size_label.visible = true
size_label_timer.wait_time = 1
size_label_timer.start()
func _on_SizeLabelTimer_timeout():
if size_label:
size_label.visible = false
func _on_Terminal_exited(exit_code, signum, terminal):
# Leave non-zero exit code terminals open in case they have some important
# error information.
if exit_code == 0:
_on_Tabs_tab_close(terminal.get_index())