Updated dialogic

This commit is contained in:
MaddoScientisto 2026-01-05 16:00:41 +01:00
commit cbb82512ee
483 changed files with 5743 additions and 2177 deletions

View file

@ -23,9 +23,10 @@ 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_word_regex.compile(r"(?<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))
@ -143,25 +144,9 @@ func request_code_completion(force:bool, text:CodeEdit, mode:=Modes.FULL_HIGHLIG
# 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))
suggest_shortcode_values(text, shortcode_events[code], line, word)
text.update_code_completion_options(true)
return
# Force update and showing of the popup
text.update_code_completion_options(true)
@ -172,7 +157,7 @@ func request_code_completion(force:bool, text:CodeEdit, mode:=Modes.FULL_HIGHLIG
if mode == Modes.TEXT_EVENT_ONLY and !event is DialogicTextEvent:
continue
if ! ' ' in line_part:
if not ' ' in line_part:
event._get_start_code_completion(self, text)
if event.is_valid_event(line):
@ -182,19 +167,23 @@ func request_code_completion(force:bool, text:CodeEdit, mode:=Modes.FULL_HIGHLIG
# Force update and showing of the popup
text.update_code_completion_options(true)
# USEFUL FOR DEBUGGING
#print(text.get_code_completion_options().map(func(x):return "{display_text}".format(x)))
# Helper that adds all characters as options
func suggest_characters(text:CodeEdit, type := CodeEdit.KIND_MEMBER, text_event_start:=false) -> void:
func suggest_characters(text:CodeEdit, type := CodeEdit.KIND_MEMBER, event:DialogicEvent=null) -> 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 += ':'
if event and event is DialogicTextEvent and load(DialogicResourceUtil.get_character_directory()[character]).portraits.is_empty():
result += ': '
elif event and event is DialogicCharacterEvent:
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():
@ -209,7 +198,7 @@ func suggest_labels(text:CodeEdit, timeline:String='', end:='', color:=Color())
# 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():
if not 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:
@ -238,10 +227,30 @@ func suggest_custom_suggestions(suggestions:Dictionary, text:CodeEdit, color:Col
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 suggest_shortcode_values(text:CodeEdit, event:DialogicEvent, line:String, word:String) -> void:
var current_parameter_gex := completion_shortcode_param_getter_regex.search(line)
if !current_parameter_gex:
return
var current_parameter := current_parameter_gex.get_string('param')
if !event.get_shortcode_parameters().has(current_parameter):
return
if !event.get_shortcode_parameters()[current_parameter].has('suggestions'):
if typeof(event.get_shortcode_parameters()[current_parameter].default) == TYPE_BOOL:
suggest_bool(text, event.event_color.lerp(syntax_highlighter.normal_color, 0.3))
elif len(word) > 0:
text.add_code_completion_option(CodeEdit.KIND_VARIABLE, word, word, event.event_color.lerp(syntax_highlighter.normal_color, 0.3), text.get_theme_icon("GuiScrollArrowRight", "EditorIcons"), '" ')
return
var suggestions: Dictionary = event.get_shortcode_parameters()[current_parameter]['suggestions'].call()
suggest_custom_suggestions(suggestions, text, event.event_color.lerp(syntax_highlighter.normal_color, 0.3))
## 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:
@ -289,7 +298,7 @@ func confirm_code_completion(replace:bool, text:CodeEdit) -> void:
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]:
if next_letter and (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'])

View file

@ -1 +1 @@
uid://b60na7qjgkmaa
uid://camdhr6iwaywr

View file

@ -11,7 +11,7 @@ var mode := Modes.FULL_HIGHLIGHTING
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_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
@ -179,9 +179,10 @@ func color_region(dict:Dictionary, color:Color, line:String, start:String, end:S
end = "\\"+end
if end.is_empty():
region_regex.compile("(?<!\\\\)"+start+".*")
region_regex.compile(r"(?<!\\)"+start+".*")
else:
region_regex.compile("(?<!\\\\)"+start+"((?!"+end+").)*"+end)
r"(?<!\\){([^{}]|({[^}]*}))*}"
region_regex.compile(r"(?<!\\)"+start+"([^"+start+end+"]|("+start+"[^"+end+"]*"+end+"))*"+end)
if to <= from:
to = len(line)-1
for region in region_regex.search_all(line.substr(from, to-from+2)):
@ -199,3 +200,13 @@ func color_shortcode_content(dict:Dictionary, line:String, from:int = 0, to:int
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
func dict_get_color_at_column(dict:Dictionary, column:int) -> Color:
var prev_idx := -1
for i in dict:
if i > prev_idx and i <= column:
prev_idx = i
if prev_idx != -1:
return dict[prev_idx].color
return normal_color

View file

@ -1 +1 @@
uid://ccjw4fyc586j1
uid://bf2nivn8txcw5

View file

@ -4,15 +4,19 @@ 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')
@onready var code_completion_helper: Node = find_parent('EditorsManager').get_node('CodeCompletionHelper')
var label_regex := RegEx.create_from_string('label +(?<name>[^\n]+)')
var channel_regex := RegEx.create_from_string(r'audio +(?<channel>[\w-]{2,}|[\w]+)')
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)
get_menu().add_icon_item(get_theme_icon("PlayStart", "EditorIcons"), "Play from here", 42)
get_menu().id_pressed.connect(_on_context_menu_id_pressed)
func _on_text_editor_text_changed() -> void:
timeline_editor.current_resource_state = DialogicEditor.ResourceStates.UNSAVED
@ -72,22 +76,52 @@ func text_timeline_to_array(text:String) -> Array:
## HELPFUL EDITOR FUNCTIONALITY
################################################################################
func _on_context_menu_id_pressed(id:int) -> void:
if id == 42:
play_from_here()
func play_from_here() -> void:
timeline_editor.play_timeline(timeline_editor.current_resource.get_index_from_text_line(text, get_caret_line()))
func _gui_input(event):
if not event is InputEventKey: return
if not event.is_pressed(): return
match event.as_text():
"Ctrl+K":
"Ctrl+K", "Ctrl+Slash":
toggle_comment()
# TODO clean this up when dropping 4.2 support
"Alt+Up":
move_line(-1)
if has_method("move_lines_up"):
call("move_lines_up")
"Alt+Down":
move_line(1)
"Ctrl+Shift+D":
duplicate_line()
if has_method("move_lines_down"):
call("move_lines_down")
"Ctrl+Shift+D", "Ctrl+D":
duplicate_lines()
"Ctrl+F6" when OS.get_name() != "macOS": # Play from here
play_from_here()
"Ctrl+Shift+B" when OS.get_name() == "macOS": # Play from here
play_from_here()
"Enter":
if get_code_completion_options():
return
for caret in range(get_caret_count()):
var line := get_line(get_caret_line(caret)).strip_edges()
var event_res := DialogicTimeline.event_from_string(line, DialogicResourceUtil.get_event_cache())
var indent_format: String = timeline_editor.current_resource.indent_format
if event_res.can_contain_events:
insert_text_at_caret("\n"+indent_format.repeat(get_indent_level(get_caret_line(caret))/4+1), caret)
else:
insert_text_at_caret("\n"+indent_format.repeat(get_indent_level(get_caret_line(caret))/4), caret)
_:
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())
@ -127,78 +161,27 @@ func toggle_comment() -> void:
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
## 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
## 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] != '"':
var line := get_line(get_caret_line())
if line[get_caret_column()-1] != '"':
result = '"'+result
if get_line(get_caret_line())[get_caret_column()] != '"':
if line.length() == get_caret_column() or line[get_caret_column()] != '"':
result = result+'"'
insert_text_at_caret(result)
grab_focus()
func _on_update_timer_timeout() -> void:
@ -211,6 +194,11 @@ func update_content_list() -> void:
labels.append(i.get_string('name'))
timeline_editor.editors_manager.sidebar.update_content_list(labels)
var channels: PackedStringArray = []
for i in channel_regex.search_all(text):
channels.append(i.get_string('channel'))
timeline_editor.update_audio_channel_cache(channels)
func _on_content_item_clicked(label:String) -> void:
if label == "~ Top":
@ -227,12 +215,23 @@ func _on_content_item_clicked(label:String) -> void:
return
func _search_timeline(search_text:String) -> bool:
set_search_text(search_text)
queue_redraw()
func _search_timeline(search_text:String, match_case := false, whole_words := false) -> bool:
var flags := 0
if match_case:
flags = flags | SEARCH_MATCH_CASE
if whole_words:
flags = flags | SEARCH_WHOLE_WORDS
set_meta("current_search", search_text)
set_meta("current_search_flags", flags)
return search(search_text, 0, 0, 0).y != -1
set_search_text(search_text)
set_search_flags(flags)
queue_redraw()
var result := search(search_text, flags, get_selection_from_line(), get_selection_from_column())
if result.y != -1:
select.call_deferred(result.y, result.x, result.y, result.x + search_text.length())
return result.y != -1
func _search_navigate_down() -> void:
@ -244,8 +243,18 @@ func _search_navigate_up() -> void:
func search_navigate(navigate_up := false) -> void:
if not has_meta("current_search"):
var pos := get_next_search_position(navigate_up)
if pos.x == -1:
return
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()
func get_next_search_position(navigate_up := false) -> Vector2i:
if not has_meta("current_search"):
return Vector2i(-1, -1)
var pos: Vector2i
var search_from_line := 0
var search_from_column := 0
@ -266,30 +275,75 @@ func search_navigate(navigate_up := false) -> void:
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")))
var flags: int = get_meta("current_search_flags", 0)
if navigate_up:
flags = flags | SEARCH_BACKWARDS
pos = search(get_meta("current_search"), flags, search_from_line, search_from_column)
return pos
func replace(replace_text:String) -> void:
if has_selection():
set_caret_line(get_selection_from_line())
set_caret_column(get_selection_from_column())
var pos := get_next_search_position()
if pos.x == -1:
return
if not has_meta("current_search"):
return
begin_complex_operation()
insert_text("@@", pos.y, pos.x)
if get_meta("current_search_flags") & SEARCH_MATCH_CASE:
text = text.replace("@@"+get_meta("current_search"), replace_text)
else:
text = text.replacen("@@"+get_meta("current_search"), replace_text)
end_complex_operation()
set_caret_line(pos.y)
center_viewport_to_caret()
queue_redraw()
set_caret_column(pos.x)
timeline_editor.replace_in_timeline()
func replace_all(replace_text:String) -> void:
begin_complex_operation()
var next_pos := get_next_search_position()
var counter := 0
while next_pos.y != -1:
insert_text("@@", next_pos.y, next_pos.x)
if get_meta("current_search_flags") & SEARCH_MATCH_CASE:
text = text.replace("@@"+get_meta("current_search"), replace_text)
else:
text = text.replacen("@@"+get_meta("current_search"), replace_text)
next_pos = get_next_search_position()
set_caret_line(next_pos.y)
set_caret_column(next_pos.x)
end_complex_operation()
timeline_editor.replace_in_timeline()
################################################################################
## AUTO COMPLETION
################################################################################
# Called if something was typed
## 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]
## 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
## 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)
@ -298,11 +352,11 @@ func _confirm_code_completion(replace:bool) -> void:
## SYMBOL CLICKING
################################################################################
# Performs an action (like opening a link) when a valid symbol was clicked
## 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
## 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

@ -1 +1 @@
uid://ckdcx4qnilpv1
uid://dshp0vy2xrxv

View file

@ -1,17 +1,17 @@
[gd_scene load_steps=2 format=3 uid="uid://defdeav8rli6o"]
[ext_resource type="Script" uid="uid://ckdcx4qnilpv1" path="res://addons/dialogic/Editor/TimelineEditor/TextEditor/timeline_editor_text.gd" id="1_1kbx2"]
[ext_resource type="Script" uid="uid://dshp0vy2xrxv" 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
highlight_current_line = true
draw_tabs = true
symbol_lookup_on_click = true
line_folding = true
gutters_draw_line_numbers = true
gutters_draw_fold_gutter = true