First work on dialogic, resized guild, and started implementing portraits.

This commit is contained in:
2025-08-14 10:26:24 -04:00
parent 95a7db036b
commit 3aeb3d44e6
959 changed files with 47688 additions and 46 deletions

View File

@@ -0,0 +1,324 @@
@tool
extends Node
enum Modes {TEXT_EVENT_ONLY, FULL_HIGHLIGHTING}
var syntax_highlighter: SyntaxHighlighter = load("res://addons/dialogic/Editor/TimelineEditor/TextEditor/syntax_highlighter.gd").new()
var text_syntax_highlighter: SyntaxHighlighter = load("res://addons/dialogic/Editor/TimelineEditor/TextEditor/syntax_highlighter.gd").new()
# These RegEx's are used to deduce information from the current line for auto-completion
# To find the currently typed word and the symbol before
var completion_word_regex := RegEx.new()
# To find the shortcode of the current shortcode event (basically the type)
var completion_shortcode_getter_regex := RegEx.new()
# To find the parameter name of the current if typing a value
var completion_shortcode_param_getter_regex := RegEx.new()
# To find the value of a paramater that is being typed
var completion_shortcode_value_regex := RegEx.new()
# Stores references to all shortcode events for parameter and value suggestions
var shortcode_events := {}
var custom_syntax_events := []
var text_event: DialogicTextEvent = null
func _ready() -> void:
# Compile RegEx's
completion_word_regex.compile("(?<s>(\\W)|^)(?<word>\\w*)\\x{FFFF}")
completion_shortcode_getter_regex.compile("\\[(?<code>\\w*)")
completion_shortcode_param_getter_regex.compile("(?<param>\\w*)\\W*=\\s*\"?(\\w|\\s)*"+String.chr(0xFFFF))
completion_shortcode_value_regex.compile(r'(\[|\s)[^\[\s=]*="(?<value>[^"$]*)'+String.chr(0xFFFF))
text_syntax_highlighter.mode = text_syntax_highlighter.Modes.TEXT_EVENT_ONLY
#region AUTO COMPLETION
################################################################################
# Helper that gets the current line with a special character where the caret is
func get_code_completion_line(text:CodeEdit) -> String:
return text.get_line(text.get_caret_line()).insert(text.get_caret_column(), String.chr(0xFFFF)).strip_edges()
# Helper that gets the currently typed word
func get_code_completion_word(text:CodeEdit) -> String:
var result := completion_word_regex.search(get_code_completion_line(text))
return result.get_string('word') if result else ""
# Helper that gets the currently typed parameter
func get_code_completion_parameter_value(text:CodeEdit) -> String:
var result := completion_shortcode_value_regex.search(get_code_completion_line(text))
return result.get_string('value') if result else ""
# Helper that gets the symbol before the current word
func get_code_completion_prev_symbol(text:CodeEdit) -> String:
var result := completion_word_regex.search(get_code_completion_line(text))
return result.get_string('s') if result else ""
func get_line_untill_caret(line:String) -> String:
return line.substr(0, line.find(String.chr(0xFFFF)))
# Called if something was typed
# Adds all kinds of options depending on the
# content of the current line, the last word and the symbol that came before
# Triggers opening of the popup
func request_code_completion(force:bool, text:CodeEdit, mode:=Modes.FULL_HIGHLIGHTING) -> void:
## TODO remove this once https://github.com/godotengine/godot/issues/38560 is fixed
if mode != Modes.FULL_HIGHLIGHTING:
return
# make sure shortcode event references are loaded
if mode == Modes.FULL_HIGHLIGHTING:
var hidden_events: Array = DialogicUtil.get_editor_setting('hidden_event_buttons', [])
if shortcode_events.is_empty():
for event in DialogicResourceUtil.get_event_cache():
if event.get_shortcode() != 'default_shortcode':
shortcode_events[event.get_shortcode()] = event
else:
custom_syntax_events.append(event)
if event.event_name in hidden_events:
event.set_meta('hidden', true)
if event is DialogicTextEvent:
text_event = event
# this is done to force-load the text effects regex which is used below
event.load_text_effects()
# fill helpers
var line := get_code_completion_line(text)
var word := get_code_completion_word(text)
var symbol := get_code_completion_prev_symbol(text)
var line_part := get_line_untill_caret(line)
## Note on use of KIND types for options.
# These types are mostly useless for us.
# However I decidede to assign some special cases for them:
# - KIND_PLAIN_TEXT is only shown if the beginnging of the option is already typed
# !word.is_empty() and option.begins_with(word)
# - KIND_CLASS is only shown if anything from the options is already typed
# !word.is_empty() and word in option
# - KIND_CONSTANT is shown and checked against the beginning
# option.begins_with(word)
# - KIND_MEMBER is shown and searched completely
# word in option
## Note on VALUE key
# The value key is used to store a potential closing string for the completion.
# The completion will check if the string is already present and add it otherwise.
# Shortcode event suggestions
if mode == Modes.FULL_HIGHLIGHTING and syntax_highlighter.line_is_shortcode_event(text.get_caret_line()):
if symbol == '[':
# suggest shortcodes if a shortcode event has just begun
var shortcodes := shortcode_events.keys()
shortcodes.sort()
for shortcode in shortcodes:
if shortcode_events[shortcode].get_meta('hidden', false):
continue
if shortcode_events[shortcode].get_shortcode_parameters().is_empty():
text.add_code_completion_option(CodeEdit.KIND_MEMBER, shortcode, shortcode, shortcode_events[shortcode].event_color.lerp(syntax_highlighter.normal_color, 0.3), shortcode_events[shortcode]._get_icon())
else:
text.add_code_completion_option(CodeEdit.KIND_MEMBER, shortcode, shortcode+" ", shortcode_events[shortcode].event_color.lerp(syntax_highlighter.normal_color, 0.3), shortcode_events[shortcode]._get_icon())
else:
var full_event_text: String = syntax_highlighter.get_full_event(text.get_caret_line())
var current_shortcode := completion_shortcode_getter_regex.search(full_event_text)
if !current_shortcode:
text.update_code_completion_options(false)
return
var code := current_shortcode.get_string('code')
if !code in shortcode_events.keys():
text.update_code_completion_options(false)
return
# suggest parameters
if symbol == ' ' and line.count('"')%2 == 0:
var parameters: Array = shortcode_events[code].get_shortcode_parameters().keys()
for param in parameters:
if !param+'=' in full_event_text:
text.add_code_completion_option(CodeEdit.KIND_MEMBER, param, param+'="' , shortcode_events[code].event_color.lerp(syntax_highlighter.normal_color, 0.3), text.get_theme_icon("MemberProperty", "EditorIcons"))
# suggest values
elif symbol == '=' or symbol == '"':
var current_parameter_gex := completion_shortcode_param_getter_regex.search(line)
if !current_parameter_gex:
text.update_code_completion_options(false)
return
var current_parameter := current_parameter_gex.get_string('param')
if !shortcode_events[code].get_shortcode_parameters().has(current_parameter):
text.update_code_completion_options(false)
return
if !shortcode_events[code].get_shortcode_parameters()[current_parameter].has('suggestions'):
if typeof(shortcode_events[code].get_shortcode_parameters()[current_parameter].default) == TYPE_BOOL:
suggest_bool(text, shortcode_events[code].event_color.lerp(syntax_highlighter.normal_color, 0.3))
elif len(word) > 0:
text.add_code_completion_option(CodeEdit.KIND_VARIABLE, word, word, shortcode_events[code].event_color.lerp(syntax_highlighter.normal_color, 0.3), text.get_theme_icon("GuiScrollArrowRight", "EditorIcons"), '" ')
text.update_code_completion_options(true)
return
var suggestions: Dictionary = shortcode_events[code].get_shortcode_parameters()[current_parameter]['suggestions'].call()
suggest_custom_suggestions(suggestions, text, shortcode_events[code].event_color.lerp(syntax_highlighter.normal_color, 0.3))
# Force update and showing of the popup
text.update_code_completion_options(true)
return
for event in custom_syntax_events:
if mode == Modes.TEXT_EVENT_ONLY and !event is DialogicTextEvent:
continue
if ! ' ' in line_part:
event._get_start_code_completion(self, text)
if event.is_valid_event(line):
event._get_code_completion(self, text, line, word, symbol)
break
# Force update and showing of the popup
text.update_code_completion_options(true)
# Helper that adds all characters as options
func suggest_characters(text:CodeEdit, type := CodeEdit.KIND_MEMBER, text_event_start:=false) -> void:
for character in DialogicResourceUtil.get_character_directory():
var result: String = character
if " " in character:
result = '"'+character+'"'
if text_event_start and load(DialogicResourceUtil.get_character_directory()[character]).portraits.is_empty():
result += ':'
text.add_code_completion_option(type, character, result, syntax_highlighter.character_name_color, load("res://addons/dialogic/Editor/Images/Resources/character.svg"))
# Helper that adds all timelines as options
func suggest_timelines(text:CodeEdit, type := CodeEdit.KIND_MEMBER, color:=Color()) -> void:
for timeline in DialogicResourceUtil.get_timeline_directory():
text.add_code_completion_option(type, timeline, timeline+'/', color, text.get_theme_icon("TripleBar", "EditorIcons"))
func suggest_labels(text:CodeEdit, timeline:String='', end:='', color:=Color()) -> void:
if timeline in DialogicResourceUtil.get_label_cache():
for i in DialogicResourceUtil.get_label_cache()[timeline]:
text.add_code_completion_option(CodeEdit.KIND_MEMBER, i, i+end, color, load("res://addons/dialogic/Modules/Jump/icon_label.png"))
# Helper that adds all portraits of a given character as options
func suggest_portraits(text:CodeEdit, character_name:String, end_check:=')') -> void:
if !character_name in DialogicResourceUtil.get_character_directory():
return
var character_resource: DialogicCharacter = load(DialogicResourceUtil.get_character_directory()[character_name])
for portrait in character_resource.portraits:
text.add_code_completion_option(CodeEdit.KIND_MEMBER, portrait, portrait, syntax_highlighter.character_portrait_color, load("res://addons/dialogic/Editor/Images/Resources/character.svg"), end_check)
if character_resource.portraits.is_empty():
text.add_code_completion_option(CodeEdit.KIND_MEMBER, 'Has no portraits!', '', syntax_highlighter.character_portrait_color, load("res://addons/dialogic/Editor/Images/Pieces/warning.svg"))
# Helper that adds all variable paths as options
func suggest_variables(text:CodeEdit):
for variable in DialogicUtil.list_variables(ProjectSettings.get_setting('dialogic/variables')):
text.add_code_completion_option(CodeEdit.KIND_MEMBER, variable, variable, syntax_highlighter.variable_color, text.get_theme_icon("MemberProperty", "EditorIcons"), '}')
# Helper that adds true and false as options
func suggest_bool(text:CodeEdit, color:Color):
text.add_code_completion_option(CodeEdit.KIND_VARIABLE, 'true', 'true', color, text.get_theme_icon("GuiChecked", "EditorIcons"), '" ')
text.add_code_completion_option(CodeEdit.KIND_VARIABLE, 'false', 'false', color, text.get_theme_icon("GuiUnchecked", "EditorIcons"), '" ')
func suggest_custom_suggestions(suggestions:Dictionary, text:CodeEdit, color:Color) -> void:
for key in suggestions.keys():
if suggestions[key].has('text_alt'):
text.add_code_completion_option(CodeEdit.KIND_VARIABLE, key, suggestions[key].text_alt[0], color, suggestions[key].get('icon', null), '" ')
else:
text.add_code_completion_option(CodeEdit.KIND_VARIABLE, key, str(suggestions[key].value), color, suggestions[key].get('icon', null), '" ')
# Filters the list of all possible options, depending on what was typed
# Purpose of the different Kinds is explained in [_request_code_completion]
func filter_code_completion_candidates(candidates:Array, text:CodeEdit) -> Array:
var valid_candidates := []
var current_word := get_code_completion_word(text)
for candidate in candidates:
if candidate.kind == text.KIND_PLAIN_TEXT:
if !current_word.is_empty() and candidate.insert_text.begins_with(current_word):
valid_candidates.append(candidate)
elif candidate.kind == text.KIND_MEMBER:
if current_word.is_empty() or current_word.to_lower() in candidate.insert_text.to_lower():
valid_candidates.append(candidate)
elif candidate.kind == text.KIND_VARIABLE:
var current_param_value := get_code_completion_parameter_value(text)
if current_param_value.is_empty() or current_param_value.to_lower() in candidate.insert_text.to_lower():
valid_candidates.append(candidate)
elif candidate.kind == text.KIND_CONSTANT:
if current_word.is_empty() or candidate.insert_text.begins_with(current_word):
valid_candidates.append(candidate)
elif candidate.kind == text.KIND_CLASS:
if !current_word.is_empty() and current_word.to_lower() in candidate.insert_text.to_lower():
valid_candidates.append(candidate)
return valid_candidates
# Called when code completion was activated
# Inserts the selected item
func confirm_code_completion(replace:bool, text:CodeEdit) -> void:
# Note: I decided to ALWAYS use replace mode, as dialogic is supposed to be beginner friendly
var code_completion := text.get_code_completion_option(text.get_code_completion_selected_index())
var word := get_code_completion_word(text)
if code_completion.kind == CodeEdit.KIND_VARIABLE:
word = get_code_completion_parameter_value(text)
text.remove_text(text.get_caret_line(), text.get_caret_column()-len(word), text.get_caret_line(), text.get_caret_column())
# Something has changed between 4.2 and 4.3
# Probably about how carets are reset when text is removed or idk.
# To keep compatibility with 4.2 for at least a while this should do the trick:
# TODO: Remove once compatibility for 4.2 is dropped.
if Engine.get_version_info().hex >= 0x040300:
text.set_caret_column(text.get_caret_column())
else:
text.set_caret_column(text.get_caret_column()-len(word))
text.insert_text_at_caret(code_completion.insert_text)
if code_completion.has('default_value') and typeof(code_completion['default_value']) == TYPE_STRING:
var next_letter := text.get_line(text.get_caret_line()).substr(text.get_caret_column(), len(code_completion['default_value']))
if next_letter == code_completion['default_value'] or next_letter[0] == code_completion['default_value'][0]:
text.set_caret_column(text.get_caret_column()+1)
else:
text.insert_text_at_caret(code_completion['default_value'])
#endregion
#region SYMBOL CLICKING
################################################################################
# Performs an action (like opening a link) when a valid symbol was clicked
func symbol_lookup(symbol:String, line:int, column:int) -> void:
if symbol in shortcode_events.keys():
if !shortcode_events[symbol].help_page_path.is_empty():
OS.shell_open(shortcode_events[symbol].help_page_path)
if symbol in DialogicResourceUtil.get_character_directory():
EditorInterface.edit_resource(DialogicResourceUtil.get_resource_from_identifier(symbol, 'dch'))
if symbol in DialogicResourceUtil.get_timeline_directory():
EditorInterface.edit_resource(DialogicResourceUtil.get_resource_from_identifier(symbol, 'dtl'))
# Called to test if a symbol can be clicked
func symbol_validate(symbol:String, text:CodeEdit) -> void:
if symbol in shortcode_events.keys():
if !shortcode_events[symbol].help_page_path.is_empty():
text.set_symbol_lookup_word_as_valid(true)
if symbol in DialogicResourceUtil.get_character_directory():
text.set_symbol_lookup_word_as_valid(true)
if symbol in DialogicResourceUtil.get_timeline_directory():
text.set_symbol_lookup_word_as_valid(true)
#endregion

View File

@@ -0,0 +1 @@
uid://c1vcrg85ibi42

View File

@@ -0,0 +1,201 @@
@tool
extends SyntaxHighlighter
## Syntax highlighter for the dialogic text timeline editor and text events in the visual editor.
enum Modes {TEXT_EVENT_ONLY, FULL_HIGHLIGHTING}
var mode := Modes.FULL_HIGHLIGHTING
## RegEx's
var word_regex := RegEx.new()
var region_regex := RegEx.new()
var number_regex := RegEx.create_from_string(r"(\d|\.)+")
var shortcode_regex := RegEx.create_from_string(r"\W*\[(?<id>\w*)(?<args>[^\]]*)?")
var shortcode_param_regex := RegEx.create_from_string(r'((?<parameter>[^\s=]*)\s*=\s*"(?<value>([^=]|\\=)*)(?<!\\)")')
## Colors
var normal_color: Color
var translation_id_color: Color
var code_flow_color: Color
var boolean_operator_color: Color
var variable_color: Color
var string_color: Color
var character_name_color: Color
var character_portrait_color: Color
var shortcode_events := {}
var custom_syntax_events := []
var text_event: DialogicTextEvent = null
func _init() -> void:
update_colors()
DialogicUtil.get_dialogic_plugin().get_editor_interface().get_base_control().theme_changed.connect(update_colors)
func update_colors() -> void:
if not DialogicUtil.get_dialogic_plugin():
return
var editor_settings: EditorSettings = DialogicUtil.get_dialogic_plugin().get_editor_interface().get_editor_settings()
normal_color = editor_settings.get('text_editor/theme/highlighting/text_color')
translation_id_color = editor_settings.get('text_editor/theme/highlighting/comment_color')
code_flow_color = editor_settings.get("text_editor/theme/highlighting/control_flow_keyword_color")
boolean_operator_color = code_flow_color.lightened(0.5)
variable_color = editor_settings.get('text_editor/theme/highlighting/engine_type_color')
string_color = editor_settings.get('text_editor/theme/highlighting/string_color')
character_name_color = editor_settings.get('text_editor/theme/highlighting/symbol_color').lerp(normal_color, 0.3)
character_portrait_color = character_name_color.lerp(normal_color, 0.5)
func _get_line_syntax_highlighting(line:int) -> Dictionary:
var str_line := get_text_edit().get_line(line)
if shortcode_events.is_empty():
for event in DialogicResourceUtil.get_event_cache():
if event.get_shortcode() != 'default_shortcode':
shortcode_events[event.get_shortcode()] = event
else:
custom_syntax_events.append(event)
if event is DialogicTextEvent:
text_event = event
text_event.load_text_effects()
var dict := {}
dict[0] = {'color':normal_color}
dict = color_translation_id(dict, str_line)
if mode == Modes.FULL_HIGHLIGHTING:
if line_is_shortcode_event(line):
var full_event := get_full_event(line)
var result := shortcode_regex.search(full_event)
if result:
if result.get_string('id') in shortcode_events:
if full_event.begins_with(str_line):
dict[result.get_start('id')] = {"color":shortcode_events[result.get_string('id')].event_color.lerp(normal_color, 0.4)}
dict[result.get_end('id')] = {"color":normal_color}
if result.get_string('args'):
color_shortcode_content(dict, str_line, result.get_start('args'), result.get_end('args'), shortcode_events[result.get_string('id')].event_color)
else:
color_shortcode_content(dict, str_line, 0, 0, shortcode_events[result.get_string('id')].event_color)
return fix_dict(dict)
else:
for event in custom_syntax_events:
if event.is_valid_event(str_line.strip_edges()):
dict = event._get_syntax_highlighting(self, dict, str_line)
return fix_dict(dict)
else:
dict = text_event._get_syntax_highlighting(self, dict, str_line)
return fix_dict(dict)
func line_is_shortcode_event(line_idx:int) -> bool:
var str_line := get_text_edit().get_line(line_idx)
if text_event.text_effects_regex.search(str_line.get_slice(' ', 0)):
return false
if str_line.strip_edges().begins_with("["):
return true
if line_idx > 0 and get_text_edit().get_line(line_idx-1).ends_with('\\'):
return line_is_shortcode_event(line_idx-1)
return false
func get_full_event(line_idx:int) -> String:
var str_line := get_text_edit().get_line(line_idx)
var offset := 1
# Add previous lines
while get_text_edit().get_line(line_idx-offset).ends_with('\\'):
str_line = get_text_edit().get_line(line_idx-offset).trim_suffix('\\')+"\n"+str_line
offset += 1
# This is commented out, as it is not needed right now.
# However without it, this isn't actually the full event.
# Might need to be included some day.
#offset = 0
## Add following lines
#while get_text_edit().get_line(line_idx+offset).ends_with('\\'):
#str_line = str_line.trim_suffix('\\')+"\n"+get_text_edit().get_line(line_idx+offset)
#offset += 1
return str_line
func fix_dict(dict:Dictionary) -> Dictionary:
var d := {}
var k := dict.keys()
k.sort()
for i in k:
d[i] = dict[i]
return d
func color_condition(dict:Dictionary, line:String, from:int = 0, to:int = 0) -> Dictionary:
dict = color_word(dict, code_flow_color, line, 'or', from, to)
dict = color_word(dict, code_flow_color, line, 'and', from, to)
dict = color_word(dict, code_flow_color, line, '==', from, to)
dict = color_word(dict, code_flow_color, line, '!=', from, to)
if !">=" in line:
dict = color_word(dict, code_flow_color, line, '>', from, to)
else:
dict = color_word(dict, code_flow_color, line, '>=', from, to)
if !"<=" in line:
dict = color_word(dict, code_flow_color, line, '<', from, to)
else:
dict = color_word(dict, code_flow_color, line, '<=', from, to)
dict = color_region(dict, variable_color, line, '{', '}', from, to)
dict = color_region(dict, string_color, line, '"', '"', from, to)
return dict
func color_translation_id(dict:Dictionary, line:String) -> Dictionary:
dict = color_region(dict, translation_id_color, line, '#id:', '')
return dict
func color_word(dict:Dictionary, color:Color, line:String, word:String, from:int= 0, to:int = 0) -> Dictionary:
word_regex.compile("\\W(?<word>"+word+")\\W")
if to <= from:
to = len(line)-1
for i in word_regex.search_all(line.substr(from, to-from+2)):
dict[i.get_start('word')+from] = {'color':color}
dict[i.get_end('word')+from] = {'color':normal_color}
return dict
func color_region(dict:Dictionary, color:Color, line:String, start:String, end:String, from:int = 0, to:int = 0, base_color:Color=normal_color) -> Dictionary:
if start in "()[].":
start = "\\"+start
if end in "()[].":
end = "\\"+end
if end.is_empty():
region_regex.compile("(?<!\\\\)"+start+".*")
else:
region_regex.compile("(?<!\\\\)"+start+"((?!"+end+").)*"+end)
if to <= from:
to = len(line)-1
for region in region_regex.search_all(line.substr(from, to-from+2)):
dict[region.get_start()+from] = {'color':color}
dict[region.get_end()+from] = {'color':base_color}
return dict
func color_shortcode_content(dict:Dictionary, line:String, from:int = 0, to:int = 0, base_color:=normal_color) -> Dictionary:
if to <= from:
to = len(line)-1
var args_result := shortcode_param_regex.search_all(line.substr(from, to-from+2))
for x in args_result:
dict[x.get_start()+from] = {"color":base_color.lerp(normal_color, 0.5)}
dict[x.get_start('value')+from-1] = {"color":base_color.lerp(normal_color, 0.7)}
dict[x.get_end()+from] = {"color":normal_color}
return dict

View File

@@ -0,0 +1 @@
uid://bbiesd7cfjjvg

View File

@@ -0,0 +1,308 @@
@tool
extends CodeEdit
## Sub-Editor that allows editing timelines in a text format.
@onready var timeline_editor := get_parent().get_parent()
@onready var code_completion_helper: Node= find_parent('EditorsManager').get_node('CodeCompletionHelper')
var label_regex := RegEx.create_from_string('label +(?<name>[^\n]+)')
func _ready() -> void:
await find_parent('EditorView').ready
syntax_highlighter = code_completion_helper.syntax_highlighter
timeline_editor.editors_manager.sidebar.content_item_activated.connect(_on_content_item_clicked)
func _on_text_editor_text_changed() -> void:
timeline_editor.current_resource_state = DialogicEditor.ResourceStates.UNSAVED
request_code_completion(true)
$UpdateTimer.start()
func clear_timeline() -> void:
text = ''
update_content_list()
func load_timeline(timeline:DialogicTimeline) -> void:
clear_timeline()
text = timeline.as_text()
timeline_editor.current_resource.set_meta("timeline_not_saved", false)
clear_undo_history()
await get_tree().process_frame
update_content_list()
func save_timeline() -> void:
if !timeline_editor.current_resource:
return
var text_array: Array = text_timeline_to_array(text)
timeline_editor.current_resource.events = text_array
timeline_editor.current_resource.events_processed = false
ResourceSaver.save(timeline_editor.current_resource, timeline_editor.current_resource.resource_path)
timeline_editor.current_resource.set_meta("timeline_not_saved", false)
timeline_editor.current_resource_state = DialogicEditor.ResourceStates.SAVED
DialogicResourceUtil.update_directory('dtl')
func text_timeline_to_array(text:String) -> Array:
# Parse the lines down into an array
var events := []
var lines := text.split('\n', true)
var idx := -1
while idx < len(lines)-1:
idx += 1
var line: String = lines[idx]
var line_stripped: String = line.strip_edges(true, true)
events.append(line)
return events
################################################################################
## HELPFUL EDITOR FUNCTIONALITY
################################################################################
func _gui_input(event):
if not event is InputEventKey: return
if not event.is_pressed(): return
match event.as_text():
"Ctrl+K":
toggle_comment()
"Alt+Up":
move_line(-1)
"Alt+Down":
move_line(1)
"Ctrl+Shift+D":
duplicate_line()
_:
return
get_viewport().set_input_as_handled()
# Toggle the selected lines as comments
func toggle_comment() -> void:
var cursor: Vector2 = Vector2(get_caret_column(), get_caret_line())
var selection := Rect2i(
Vector2i(get_selection_line(), get_selection_column()),
# TODO When ditching godot 4.2, switch to this, the above methods have been deprecated in 4.3
#Vector2i(get_selection_origin_line(), get_selection_origin_column()),
Vector2i(get_caret_line(), get_caret_column()))
var from: int = cursor.y
var to: int = cursor.y
if has_selection():
from = get_selection_from_line()
to = get_selection_to_line()
var lines: PackedStringArray = text.split("\n")
var will_comment: bool = false
for i in range(from, to+1):
if not lines[i].begins_with("#"):
will_comment = true
for i in range(from, to + 1):
if will_comment:
lines[i] = "#" + lines[i]
else:
lines[i] = lines[i].trim_prefix("#")
text = "\n".join(lines)
if will_comment:
cursor.x += 1
selection.position.y += 1
selection.size.y += 1
else:
cursor.x -= 1
selection.position.y -= 1
selection.size.y -= 1
select(selection.position.x, selection.position.y, selection.size.x, selection.size.y)
text_changed.emit()
# Move the selected lines up or down
func move_line(offset: int) -> void:
offset = clamp(offset, -1, 1)
var cursor: Vector2 = Vector2(get_caret_column(), get_caret_line())
var reselect: bool = false
var from: int = cursor.y
var to: int = cursor.y
if has_selection():
reselect = true
from = get_selection_from_line()
to = get_selection_to_line()
var lines := text.split("\n")
if from + offset < 0 or to + offset >= lines.size(): return
var target_from_index: int = from - 1 if offset == -1 else to + 1
var target_to_index: int = to if offset == -1 else from
var line_to_move: String = lines[target_from_index]
lines.remove_at(target_from_index)
lines.insert(target_to_index, line_to_move)
text = "\n".join(lines)
cursor.y += offset
from += offset
to += offset
if reselect:
select(from, 0, to, get_line_width(to))
set_caret_line(cursor.y)
set_caret_column(cursor.x)
text_changed.emit()
func duplicate_line() -> void:
var cursor: Vector2 = Vector2(get_caret_column(), get_caret_line())
var from: int = cursor.y
var to: int = cursor.y+1
if has_selection():
from = get_selection_from_line()
to = get_selection_to_line()+1
var lines := text.split("\n")
var lines_to_dupl: PackedStringArray = lines.slice(from, to)
text = "\n".join(lines.slice(0, from)+lines_to_dupl+lines.slice(from))
set_caret_line(cursor.y+to-from)
set_caret_column(cursor.x)
text_changed.emit()
# Allows dragging files into the editor
func _can_drop_data(at_position:Vector2, data:Variant) -> bool:
if typeof(data) == TYPE_DICTIONARY and 'files' in data.keys() and len(data.files) == 1:
return true
return false
# Allows dragging files into the editor
func _drop_data(at_position:Vector2, data:Variant) -> void:
if typeof(data) == TYPE_DICTIONARY and 'files' in data.keys() and len(data.files) == 1:
set_caret_column(get_line_column_at_pos(at_position).x)
set_caret_line(get_line_column_at_pos(at_position).y)
var result: String = data.files[0]
if get_line(get_caret_line())[get_caret_column()-1] != '"':
result = '"'+result
if get_line(get_caret_line())[get_caret_column()] != '"':
result = result+'"'
insert_text_at_caret(result)
func _on_update_timer_timeout() -> void:
update_content_list()
func update_content_list() -> void:
var labels: PackedStringArray = []
for i in label_regex.search_all(text):
labels.append(i.get_string('name'))
timeline_editor.editors_manager.sidebar.update_content_list(labels)
func _on_content_item_clicked(label:String) -> void:
if label == "~ Top":
set_caret_line(0)
set_caret_column(0)
adjust_viewport_to_caret()
return
for i in label_regex.search_all(text):
if i.get_string('name') == label:
set_caret_column(0)
set_caret_line(text.count('\n', 0, i.get_start()+1))
center_viewport_to_caret()
return
func _search_timeline(search_text:String) -> bool:
set_search_text(search_text)
queue_redraw()
set_meta("current_search", search_text)
return search(search_text, 0, 0, 0).y != -1
func _search_navigate_down() -> void:
search_navigate(false)
func _search_navigate_up() -> void:
search_navigate(true)
func search_navigate(navigate_up := false) -> void:
if not has_meta("current_search"):
return
var pos: Vector2i
var search_from_line := 0
var search_from_column := 0
if has_selection():
if navigate_up:
search_from_line = get_selection_from_line()
search_from_column = get_selection_from_column()-1
if search_from_column == -1:
if search_from_line == 0:
search_from_line = get_line_count()
else:
search_from_line -= 1
search_from_column = max(get_line(search_from_line).length()-1,0)
else:
search_from_line = get_selection_to_line()
search_from_column = get_selection_to_column()
else:
search_from_line = get_caret_line()
search_from_column = get_caret_column()
pos = search(get_meta("current_search"), 4 if navigate_up else 0, search_from_line, search_from_column)
select(pos.y, pos.x, pos.y, pos.x+len(get_meta("current_search")))
set_caret_line(pos.y)
center_viewport_to_caret()
queue_redraw()
################################################################################
## AUTO COMPLETION
################################################################################
# Called if something was typed
func _request_code_completion(force:bool):
code_completion_helper.request_code_completion(force, self)
# Filters the list of all possible options, depending on what was typed
# Purpose of the different Kinds is explained in [_request_code_completion]
func _filter_code_completion_candidates(candidates:Array) -> Array:
return code_completion_helper.filter_code_completion_candidates(candidates, self)
# Called when code completion was activated
# Inserts the selected item
func _confirm_code_completion(replace:bool) -> void:
code_completion_helper.confirm_code_completion(replace, self)
################################################################################
## SYMBOL CLICKING
################################################################################
# Performs an action (like opening a link) when a valid symbol was clicked
func _on_symbol_lookup(symbol, line, column):
code_completion_helper.symbol_lookup(symbol, line, column)
# Called to test if a symbol can be clicked
func _on_symbol_validate(symbol:String) -> void:
code_completion_helper.symbol_validate(symbol, self)

View File

@@ -0,0 +1 @@
uid://bhrn8xx7nmb5o

View File

@@ -0,0 +1,32 @@
[gd_scene load_steps=2 format=3 uid="uid://defdeav8rli6o"]
[ext_resource type="Script" path="res://addons/dialogic/Editor/TimelineEditor/TextEditor/timeline_editor_text.gd" id="1_1kbx2"]
[node name="TimelineTextEditor" type="CodeEdit"]
offset_top = 592.0
offset_right = 1024.0
offset_bottom = 600.0
theme_override_constants/line_spacing = 10
wrap_mode = 1
highlight_current_line = true
draw_tabs = true
minimap_draw = true
caret_blink = true
line_folding = true
gutters_draw_line_numbers = true
gutters_draw_fold_gutter = true
code_completion_enabled = true
code_completion_prefixes = Array[String](["[", "{"])
indent_automatic = true
auto_brace_completion_enabled = true
auto_brace_completion_highlight_matching = true
script = ExtResource("1_1kbx2")
[node name="UpdateTimer" type="Timer" parent="."]
one_shot = true
[connection signal="code_completion_requested" from="." to="." method="_on_code_completion_requested"]
[connection signal="symbol_lookup" from="." to="." method="_on_symbol_lookup"]
[connection signal="symbol_validate" from="." to="." method="_on_symbol_validate"]
[connection signal="text_changed" from="." to="." method="_on_text_editor_text_changed"]
[connection signal="timeout" from="UpdateTimer" to="." method="_on_update_timer_timeout"]